From ecb79ce49c1179e32a04832fa31e3d8fc1b1e951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Thu, 18 Jun 2026 11:37:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=203qfx/xdfx=20=E4=BB=BB=E5=8A=A1=E6=B7=B1?= =?UTF-8?q?=E9=93=BE=201:N=20=E5=B7=A5=E4=BD=9C=E5=8C=BA=20+=20xdfx=20?= =?UTF-8?q?=E8=A1=8C=E5=8A=A8id=E8=A7=A3=E6=9E=90=20+=203q=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3qfx/xdfx 单智能体改走任务工作区(TaskFxWorkspace):按 taskId+agent_id 隔离的 1:N 对话列表;3qfx 纯绑定(taskBind 自动发开场信息),xdfx 带目的树闸门(taskGate) - 左上角「当前任务」卡片:留存查询到的任务名称+简介(readCopTaskDetail) - rwfx 工作区新增「切换到三情分析」按钮,带同一 taskId 进入 3qfx - xdfx 深链传入的是行动id:resolveDeeplinkTaskId 经 action 接口解析出真正任务id (可配置 action_detail_url + action_test/mock.action_detail,离线可测) - 目的树面板「弹框查看」旁新增可配置「3q详情」链接(detail_3q_url + ?id=任务id) - AgentChatPage 拆分 taskGate/taskBind;use-task-threads 增加 agentId 过滤隔离 Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- .../src/core/auth/task-deeplink-config.ts | 18 ++- frontend-web/src/core/auth/task-deeplink.ts | 69 ++++++--- frontend-web/src/core/auth/task-purpose.ts | 6 + .../src/core/threads/use-task-threads.ts | 20 ++- frontend-web/src/pages/AgentChatPage.tsx | 51 +++++-- frontend-web/src/pages/LoginPage.tsx | 8 +- frontend-web/src/pages/StrategyRoutes.tsx | 58 +++++--- .../task-components/TaskFxWorkspace.tsx | 138 +++++++++++++++--- .../task-components/TaskPurposePanel.tsx | 14 +- offline-backend-20260512/backend/config.yaml | 19 ++- .../deerflow/config/task_deeplink_config.py | 20 ++- offline-backend-20260512/config.yaml | 19 ++- 13 files changed, 347 insertions(+), 95 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 91f9c7b3..8d90edd9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,7 +86,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA: - **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE - **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/` - **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js) -- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A (resolved via `/api/business-mapping`, opening message in `sessionStorage.pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **per-task draft isolation** — drafts save/list/restore by `task_id` (one taskId = many records, shared across users; backend `GET /api/roundtable-drafts/by-task/{id}` + `/latest`); the history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management//{roundtable,agent/:agent_id/chats/:thread_id}` in `StrategyRoutes.tsx` (`` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. The agent chat is rendered via `TaskEmbeddedAgentChat`, which passes `AgentChatPage`'s `chatBasePath` prop so the post-send URL rewrite + new-chat button stay in the task-management context (instead of the standalone `/agents/...` route). Any failure (no consumer URL / fetch error / unmapped goPath / no configured agent) silently falls back to the default home. Helpers: `core/auth/task-deeplink.ts`. +- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A inside a **任务工作区** (resolved via `/api/business-mapping`; `resolveTaskDeeplinkTarget` calls `persistTaskContext({taskId, agentId, openingText})` and navigates to the `…/chats` **index**, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT `pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. **xdfx's deep-link `?taskId=` is actually an 行动id, not a 任务id** — `resolveDeeplinkTaskId(goPath, rawId)` (called in `LoginPage` before everything) detects `xdfx`/`xdfx-A` and resolves it via `GET {action_detail_url}/{actionId}` → `data.taskId` (`fetchTaskIdFromAction`; `action_test`+`mock.action_detail` for offline testing); the resolved 任务id then drives `cop-task-detail`, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **per-task draft isolation** — drafts save/list/restore by `task_id` (one taskId = many records, shared across users; backend `GET /api/roundtable-drafts/by-task/{id}` + `/latest`); the history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management//{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. **All three single-agent businesses share `TaskFxWorkspace`** (`task-components/TaskFxWorkspace.tsx`): left = a「当前任务」card (`readCopTaskDetail()` 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (`useTaskThreads(taskId, agentId)` — filtered by **both** `metadata.taskId` AND `metadata.agent_id` so the three businesses' conversations stay isolated even though they share one taskId) + 新建对话; the `…/chats` index (`TaskFxIndexResolver`) opens the latest task thread or 新建. The right `:thread_id` host differs by business: **rwfx** → `TaskFxChat` (`AgentChatPage taskGate`) = full workspace (non-`lite`): 目的树闸门 panel + 编辑/下一步/清除记录 task actions + a「切换到三情分析」button (carries the same taskId into 3qfx via `resolveTaskDeeplinkTarget("3qfx", …)`); **xdfx** → also `TaskFxChat` (same 目的树闸门 query+display, same `purpose_detail_url`) but workspace passed `lite` (no task-actions/jump in the left panel); **3qfx** → `TaskBindChat` (`AgentChatPage taskBind`, `lite`) = 1:N list only, no gate, and 新建对话 **auto-sends** the task opening text (`taskAutoSendText`; gate businesses send it via the gate's 开始问答 instead). The shared 目的树闸门 (`TaskPurposePanel`) carries a「3q详情」link beside「弹框查看」(`buildDetail3qUrl` → `detail_3q_url`+`?id=任务id`, opens new tab; toasts 未配置 when the URL is blank — it's TBD). `chatBasePath` keeps the post-send URL rewrite + new-chat button in the task-management context. Backend config `task_deeplink` (`config.yaml`, surfaced by `GET /api/public/config/task-deeplink`, mirrored in `core/auth/task-deeplink-config.ts`) adds `action_test`/`action_detail_url`/`detail_3q_url` + `mock.action_detail`. Any failure (no consumer URL / fetch error / unmapped goPath / unresolvable action id / no configured agent) silently falls back to the default home. Helpers: `core/auth/task-deeplink.ts`, `core/auth/task-purpose.ts`, `core/auth/task-deeplink-config.ts`, `core/threads/use-task-threads.ts`. ### Configuration diff --git a/frontend-web/src/core/auth/task-deeplink-config.ts b/frontend-web/src/core/auth/task-deeplink-config.ts index f31a74a3..86d5b716 100644 --- a/frontend-web/src/core/auth/task-deeplink-config.ts +++ b/frontend-web/src/core/auth/task-deeplink-config.ts @@ -19,6 +19,8 @@ export interface TaskDeeplinkConfig { test: boolean; /** 删除接口走假数据(该接口未开发完时打开)。 */ deleteTest: boolean; + /** 行动→任务解析走假数据(xdfx 行动id→任务id)。 */ + actionTest: boolean; /** 目的树查询完整 URL(调用追加 ?taskId=)。 */ purposeDetailUrl: string; /** 删除完整 URL(调用追加 ?taskId=)。 */ @@ -27,10 +29,16 @@ export interface TaskDeeplinkConfig { treemapEditUrl: string; /** B 系统树形下一步页 URL(跳转追加 ?id=)。 */ treemapNextUrl: string; - /** 测试假数据:purposeDetail = 原始 {state,msg,data};delete = {message,success}。 */ + /** 行动详情接口基址(xdfx 行动→任务):调用追加 /{actionId},返回 data.taskId 为真正任务id。 */ + actionDetailUrl: string; + /** 「3q详情」详情页 URL(rwfx 目的树面板链接,跳转追加 ?id= 任务id);留空则前端提示未配置。 */ + detail3qUrl: string; + /** 测试假数据:purposeDetail = 原始 {state,msg,data};delete = {message,success}; + * actionDetail = 原始 {state,msg,data:{taskId,...}}。 */ mock: { purposeDetail?: unknown; delete?: { message?: string; success?: boolean }; + actionDetail?: unknown; }; } @@ -38,10 +46,13 @@ export interface TaskDeeplinkConfig { export const DEFAULT_TASK_DEEPLINK_CONFIG: TaskDeeplinkConfig = { test: false, deleteTest: false, + actionTest: false, purposeDetailUrl: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/getTaskPurposeDetail", deleteUrl: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/delete", treemapEditUrl: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-edit", treemapNextUrl: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-next", + actionDetailUrl: "https://ch1.b.uat.4.cn/consumer/plan/action", + detail3qUrl: "", mock: {}, }; @@ -55,16 +66,21 @@ function normalize(body: unknown): TaskDeeplinkConfig { return { test: o.test === true, deleteTest: o.delete_test === true, + actionTest: o.action_test === true, purposeDetailUrl: str(o.purpose_detail_url, d.purposeDetailUrl), deleteUrl: str(o.delete_url, d.deleteUrl), treemapEditUrl: str(o.treemap_edit_url, d.treemapEditUrl), treemapNextUrl: str(o.treemap_next_url, d.treemapNextUrl), + actionDetailUrl: str(o.action_detail_url, d.actionDetailUrl), + // detail_3q_url 允许为空(地址待定),不回退默认;只在配了非空时才生效。 + detail3qUrl: typeof o.detail_3q_url === "string" ? o.detail_3q_url : d.detail3qUrl, mock: { purposeDetail: mock.purpose_detail, delete: mock.delete && typeof mock.delete === "object" ? (mock.delete as { message?: string; success?: boolean }) : undefined, + actionDetail: mock.action_detail, }, }; } diff --git a/frontend-web/src/core/auth/task-deeplink.ts b/frontend-web/src/core/auth/task-deeplink.ts index 84867792..e9fe6bd5 100644 --- a/frontend-web/src/core/auth/task-deeplink.ts +++ b/frontend-web/src/core/auth/task-deeplink.ts @@ -17,10 +17,11 @@ import { requestCrudService } from "@/service/request-axios-service"; import { clearTaskContext, persistTaskContext } from "@/core/auth/task-purpose"; +import { fetchTaskDeeplinkConfig } from "@/core/auth/task-deeplink-config"; import { listBusinessMappings } from "@/strategy-components/api/business-mapping"; -/** rwfx 业务码:单智能体走「任务工作区 + 目的树闸门」(其它业务保持原问答逻辑)。 */ -const RWFX_BUSINESS_CODE = "6BF"; +/** xdfx 业务码:深链传入的是「行动id」,需先调 action 接口解析出真正的「任务id」。 */ +const XDFX_BUSINESS_CODE = "7BF"; /** consumer 接口返回的任务详情(data 字段)。 */ export interface CopTaskDetail { @@ -110,6 +111,44 @@ export function readCopTaskDetail(): CopTaskDetail | null { } } +/** + * xdfx 深链:用「行动id」调行动详情接口 ``{action_detail_url}/{actionId}``,取出 ``data.taskId`` + * 作为真正的「任务id」。``action_test=true`` 时直接用后端配置里的假数据(接口未通时用)。 + * 缺地址 / 返回不合法 → 返回 ""(调用方据此回退)。 + */ +export async function fetchTaskIdFromAction(actionId: string): Promise { + if (!actionId) return ""; + const cfg = await fetchTaskDeeplinkConfig(); + let body: unknown; + if (cfg.actionTest) { + body = cfg.mock.actionDetail; + } else if (cfg.actionDetailUrl) { + const base = cfg.actionDetailUrl.replace(/\/+$/, ""); + // service 拦截器已统一注入 Authorization: Bearer admin 并返回整包 body;行动id 走路径段。 + body = await requestCrudService.get(`${base}/${encodeURIComponent(actionId)}`); + } else { + return ""; + } + const data = body && typeof body === "object" ? (body as { data?: unknown }).data : null; + if (!data || typeof data !== "object") return ""; + const taskId = (data as Record).taskId; + return taskId != null && taskId !== "" ? String(taskId) : ""; +} + +/** + * 据 goPath 把深链 URL 里的原始 id 解析成真正的「任务id」: + * - xdfx(行动分析):URL 传入的是「行动id」,调 action 接口换成 ``data.taskId``; + * 解析失败返回 ""(让调用方回退默认首页,避免拿行动id当任务id污染后续查询/绑定)。 + * - 其它业务:URL 传入的本就是任务id,原样返回。 + */ +export async function resolveDeeplinkTaskId(goPath: string, rawId: string): Promise { + const parsed = parseGoPath(goPath); + if (parsed?.businessCode === XDFX_BUSINESS_CODE) { + return fetchTaskIdFromAction(rawId); + } + return rawId; +} + export interface TaskDeeplinkTarget { path: string; /** react-router navigate 的 state(多智能体会商用 roundtablePrefill)。 */ @@ -148,7 +187,7 @@ export async function resolveTaskDeeplinkTarget( const taskBase = BUSINESS_TASK_BASE[parsed.businessCode]; if (!taskBase) return null; - // 清掉上一次深链可能残留的任务上下文(仅 rwfx 单智能体会重新写入)。 + // 清掉上一次深链可能残留的任务上下文(单智能体分支会用本次任务重新写入)。 clearTaskContext(); if (parsed.roundtable) { @@ -177,22 +216,10 @@ export async function resolveTaskDeeplinkTarget( } if (!agentId) return null; - if (parsed.businessCode === RWFX_BUSINESS_CODE) { - // rwfx:走「任务工作区」——暂存任务上下文(taskId + 开场信息),跳到 chats 索引, - // 由工作区按 taskId 决定开最新历史对话 / 新建(新建走目的树闸门)。不设 pendingAgentMessage: - // 开场信息在闸门通过后由目的树面板发出。 - persistTaskContext({ taskId, agentId, openingText: buildOpeningText(detail, taskId) }); - return { path: `${taskBase}/agent/${agentId}/chats` }; - } - - // 其它单智能体问答:保持原逻辑(pendingAgentMessage 自动发出 + chats/new)。 - try { - sessionStorage.setItem( - "pendingAgentMessage", - JSON.stringify({ agentId, text: buildOpeningText(detail, taskId) }), - ); - } catch { - /* noop */ - } - return { path: `${taskBase}/agent/${agentId}/chats/new` }; + // 单智能体问答统一走「任务工作区」(任务↔对话 1:N,按 taskId + agentId 隔离不同业务的对话列表): + // 暂存任务上下文(taskId + agentId + 开场信息),跳到 chats 索引,由工作区按 taskId 决定 + // 开最新历史对话 / 新建。rwfx 在工作区里额外过「目的树闸门」;3qfx/xdfx 仅 1:N 列表, + // 新建对话直接带开场信息自动发出(见 task-purpose / AgentChatPage 的 taskGate / taskBind)。 + persistTaskContext({ taskId, agentId, openingText: buildOpeningText(detail, taskId) }); + return { path: `${taskBase}/agent/${agentId}/chats` }; } diff --git a/frontend-web/src/core/auth/task-purpose.ts b/frontend-web/src/core/auth/task-purpose.ts index b1a0bc3a..a8f91a85 100644 --- a/frontend-web/src/core/auth/task-purpose.ts +++ b/frontend-web/src/core/auth/task-purpose.ts @@ -190,6 +190,12 @@ export function buildTreemapNextUrl(taskId: string): string { return url && taskId ? appendQueryParam(url, "id", taskId) : ""; } +/** 「3q详情」详情页地址(目的树面板链接,新标签打开);追加 ?id=任务id。地址未配置时返回 ""。 */ +export function buildDetail3qUrl(taskId: string): string { + const url = getCachedTaskDeeplinkConfig().detail3qUrl; + return url && taskId ? appendQueryParam(url, "id", taskId) : ""; +} + /* --------------------------- 任务上下文暂存 --------------------------- */ export function persistTaskContext(ctx: TaskContext): void { diff --git a/frontend-web/src/core/threads/use-task-threads.ts b/frontend-web/src/core/threads/use-task-threads.ts index cad4b9f1..b9101dc4 100644 --- a/frontend-web/src/core/threads/use-task-threads.ts +++ b/frontend-web/src/core/threads/use-task-threads.ts @@ -4,19 +4,27 @@ * 新建对话时由 [[task-purpose]] 流程给 thread 打 ``metadata.taskId`` 标签 * (见 useThreadStream 的 ``threadMetadata``);这里复用服务端分页搜索按该标签过滤, * 后端默认按 ``updated_at desc`` 排序,所以 ``threads[0]`` 即最新一条。 + * + * 同一个 taskId 可能挂在多条业务链上(rwfx / 3qfx / xdfx 共用任务 id),每条业务用各自的 + * 智能体。传入 ``agentId`` 时额外按 ``metadata.agent_id`` 过滤(后端 metadata 过滤是 AND/子集 + * 匹配),把不同业务的对话列表隔离开——否则三种业务的对话会混在一起。 */ import { useThreadsPage } from "@/core/threads/hooks"; import type { AgentThread } from "@/core/threads/types"; -export function useTaskThreads(taskId: string | null | undefined) { +export function useTaskThreads( + taskId: string | null | undefined, + agentId?: string | null | undefined, +) { const enabled = !!taskId; // 任务工作区只在有 taskId 时渲染;无 taskId 时传一个不可能命中的过滤,返回空列表。 - const query = useThreadsPage({ - page: 1, - pageSize: 100, - metadata: enabled ? { taskId } : { taskId: "__none__" }, - }); + const metadata = enabled + ? agentId + ? { taskId, agent_id: agentId } + : { taskId } + : { taskId: "__none__" }; + const query = useThreadsPage({ page: 1, pageSize: 100, metadata }); const threads: AgentThread[] = enabled ? (query.data?.threads ?? []) : []; return { ...query, diff --git a/frontend-web/src/pages/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index 2d7372cd..0b1ef2ff 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -37,6 +37,7 @@ export default function AgentChatPage({ embeddedAgentId, chatBasePath, taskGate, + taskBind, }: { /** 当作为「通用问答右侧嵌入」渲染时由父组件传入的 agent id(替代路由参数)。 * 传入即视为嵌入态:路由仍停留在 /chats/new,仅右侧问答区换成该智能体的对话。 */ @@ -49,6 +50,9 @@ export default function AgentChatPage({ /** rwfx 任务深链闸门:新建对话时顶部展示目的树面板(覆盖确认后才开始问答), * 并给新建 thread 打 `metadata.taskId` 标签(任务↔对话 1:N)。见 task-purpose。 */ taskGate?: boolean; + /** 3qfx / xdfx 任务深链:仅做任务↔对话 1:N 绑定(打 `metadata.taskId` 标签 + 切换会话停旧 run), + * 不展示目的树闸门;新建对话直接把任务开场信息自动发出。与 `taskGate` 互斥使用。 */ + taskBind?: boolean; } = {}) { const routeBase = useRouteBase(); const { t } = useI18n(); @@ -63,15 +67,23 @@ export default function AgentChatPage({ // 对话路由前缀:调用方可覆盖(任务深链嵌入态),否则用当前 routeBase 下的独立智能体对话路径。 const agentChatsBase = chatBasePath ?? `${routeBase}/agents/${agent_id}/chats`; - // rwfx 任务闸门:读取登录深链/「新建对话」暂存的任务上下文(taskId + 开场信息)。 + // 任务深链上下文(taskId + 开场信息):闸门(rwfx)与纯绑定(3qfx/xdfx)都要读取。 + const taskCtxOn = !!(taskGate || taskBind); const taskContext = useMemo( - () => (taskGate ? readTaskContext() : null), - [taskGate], + () => (taskCtxOn ? readTaskContext() : null), + [taskCtxOn], ); const taskId = - taskGate && taskContext && taskContext.agentId === agent_id + taskCtxOn && taskContext && taskContext.agentId === agent_id ? taskContext.taskId : ""; + // 纯绑定(taskBind 且无闸门):新建对话自动发出的任务开场信息。rwfx 走闸门发出,不在此列。 + const taskAutoSendText = + taskBind && !taskGate && taskContext && taskContext.agentId === agent_id + ? (taskContext.openingText?.trim() ?? "") + : ""; + // 纯绑定每次进入新建对话都自动发一次开场信息;ref 防止同一新建会话内重复发出。 + const taskAutoSentRef = useRef(false); // If ChatPage stashed an opening message for this agent, skip the landing // screen on first render so the user doesn't see a flash of the welcome UI @@ -88,7 +100,7 @@ export default function AgentChatPage({ } }; const [hasStartedConversation, setHasStartedConversation] = useState( - hasPendingForThisAgent, + () => hasPendingForThisAgent() || !!taskAutoSendText, ); const { agent, isLoading: isAgentLoading } = useAgent(agent_id); @@ -107,7 +119,9 @@ export default function AgentChatPage({ useEffect(() => { if (isNewThread) { - setHasStartedConversation(hasPendingForThisAgent()); + setHasStartedConversation(hasPendingForThisAgent() || !!taskAutoSendText); + // 进入新建对话:允许纯绑定再次自动发出开场信息。 + taskAutoSentRef.current = false; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isNewThread, threadId, agent_id]); @@ -162,20 +176,20 @@ export default function AgentChatPage({ ); // 任务流程:切换 / 新建会话时,停掉「上一会话服务端在跑」的 run(老对话要停止)。 - // 仅 taskGate(rwfx 任务工作区)启用——普通问答页保留后台续跑行为不变。 + // 任务工作区(rwfx 闸门 + 3qfx/xdfx 纯绑定)启用——普通问答页保留后台续跑行为不变。 const prevTaskThreadRef = useRef(null); useEffect(() => { - if (!taskGate) return; + if (!taskCtxOn) return; const prev = prevTaskThreadRef.current; if (prev && prev !== threadId) void stopThreadRun(prev); prevTaskThreadRef.current = threadId ?? null; - }, [taskGate, threadId]); + }, [taskCtxOn, threadId]); useEffect(() => { // 离开任务问答页(卸载)时也停掉当前会话的服务端 run。 return () => { - if (taskGate) void stopThreadRun(prevTaskThreadRef.current); + if (taskCtxOn) void stopThreadRun(prevTaskThreadRef.current); }; - }, [taskGate]); + }, [taskCtxOn]); // 任务闸门通过(无目的数据 / 覆盖成功):发出开场信息开始问答;无开场信息则 // 仅切到对话态(隐藏面板),让用户手动输入。 @@ -210,6 +224,16 @@ export default function AgentChatPage({ handleSubmit({ text: pending.text, files: [] }); }, [agent_id, threadId, isNewThread, handleSubmit]); + // 纯绑定任务深链(3qfx/xdfx,无闸门):进入新建对话即自动发出任务开场信息。 + // 每次新建对话都带(与 rwfx 闸门「开始问答」发出等价);ref 防止同一会话内重复发出。 + useEffect(() => { + if (!taskAutoSendText) return; + if (!isNewThread) return; + if (taskAutoSentRef.current) return; + taskAutoSentRef.current = true; + handleSubmit({ text: taskAutoSendText, files: [] }); + }, [taskAutoSendText, isNewThread, handleSubmit]); + const handleClarificationSubmit = useCallback( (answer: string) => { setHasStartedConversation(true); @@ -223,8 +247,9 @@ export default function AgentChatPage({ }, [thread]); const isLandingState = isNewThread && !hasStartedConversation; - // 任务闸门面板:仅在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。 - const showTaskGate = !!taskId && isNewThread && !hasStartedConversation; + // 任务闸门面板:仅 rwfx(taskGate)在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。 + // 3qfx/xdfx(taskBind)不展示闸门,靠 taskAutoSendText 直接发出开场信息。 + const showTaskGate = !!taskGate && !!taskId && isNewThread && !hasStartedConversation; const isDemoMode = env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"; const status = thread.error diff --git a/frontend-web/src/pages/LoginPage.tsx b/frontend-web/src/pages/LoginPage.tsx index a8b91c68..0550e2f7 100644 --- a/frontend-web/src/pages/LoginPage.tsx +++ b/frontend-web/src/pages/LoginPage.tsx @@ -15,6 +15,7 @@ import { import { fetchCopTaskDetail, persistCopTaskDetail, + resolveDeeplinkTaskId, resolveTaskDeeplinkTarget, } from '@/core/auth/task-deeplink'; import { resolveUsernameByOnlineTicket } from '@/core/auth/wujie-ticket-login'; @@ -160,11 +161,14 @@ export default function LoginPage() { // 任一环节失败(无地址/拉取失败/未配置智能体/goPath 无法识别)都回退默认首页。 if (taskId && goPath) { try { - const detail = await fetchCopTaskDetail(taskId); + // xdfx 深链 ?taskId= 传入的是「行动id」,先换成真正的「任务id」;其它业务原样。 + const realTaskId = await resolveDeeplinkTaskId(goPath, taskId); + if (cancelled) return; + const detail = await fetchCopTaskDetail(realTaskId); if (cancelled) return; if (detail) { persistCopTaskDetail(detail); - const target = await resolveTaskDeeplinkTarget(goPath, detail, taskId); + const target = await resolveTaskDeeplinkTarget(goPath, detail, realTaskId); if (cancelled) return; if (target) { navigate(target.path, { replace: true, state: target.state }); diff --git a/frontend-web/src/pages/StrategyRoutes.tsx b/frontend-web/src/pages/StrategyRoutes.tsx index 27816fa0..e467773f 100644 --- a/frontend-web/src/pages/StrategyRoutes.tsx +++ b/frontend-web/src/pages/StrategyRoutes.tsx @@ -1,4 +1,4 @@ -import { Navigate, Route, Routes, useParams } from "react-router-dom"; +import { Navigate, Route, Routes } from "react-router-dom"; import { Toaster } from "sonner"; import { SidebarProvider } from "@/components/ui/sidebar"; @@ -12,6 +12,7 @@ import TaskTyFrom from "@/strategy-components/components/task-components/TaskTyF import TaskXkFrom from "@/strategy-components/components/task-components/TaskXkFrom"; import TaskAnalyzes from "@/strategy-components/components/task-components/TaskAnalyzes"; import { + TaskBindChat, TaskFxChat, TaskFxIndexResolver, TaskFxWorkspace, @@ -58,20 +59,6 @@ import AdminScheduledTasksPage from "./AdminScheduledTasksPage"; import AdminUserActivityPage from "./AdminUserActivityPage"; import { ChatRuntime } from "./WorkspaceRoutes"; -/** - * 任务深链:把智能体问答嵌进「任务管理」布局。读路由参数 agent_id,给 AgentChatPage - * 传入以 taskBase 为前缀的对话路由(开新会话 / URL 改写都停留在任务管理上下文, - * 侧边栏保持对应业务高亮)。 - */ -function TaskEmbeddedAgentChat({ taskBase }: { taskBase: string }) { - const { agent_id } = useParams<{ agent_id: string }>(); - return ( - - - - ); -} - export default function StrategyRoutes() { return ( @@ -144,12 +131,26 @@ export default function StrategyRoutes() { } /> + {/* 三情分析:任务工作区(左对话列表 1:N,无目的树闸门;新建对话直接带开场信息)。 */} + } - /> + > + + } + /> + + } + /> + } /> + {/* 行动分析:任务工作区(左对话列表 1:N 精简版;右侧带目的树闸门,与 rwfx 同一查询/显示)。 + 注意:xdfx 深链传入的是「行动id」,登录时已先经 resolveDeeplinkTaskId 换成任务id。 */} + } - /> + > + + } + /> + + } + /> + } /> } /> } /> diff --git a/frontend-web/src/strategy-components/components/task-components/TaskFxWorkspace.tsx b/frontend-web/src/strategy-components/components/task-components/TaskFxWorkspace.tsx index 8813e37c..3a1dd031 100644 --- a/frontend-web/src/strategy-components/components/task-components/TaskFxWorkspace.tsx +++ b/frontend-web/src/strategy-components/components/task-components/TaskFxWorkspace.tsx @@ -1,15 +1,17 @@ /** - * 任务深链(rwfx / 任务FX)的「任务工作区」:一个任务挂多个对话(1:N)。 + * 任务深链的「任务工作区」:一个任务挂多个对话(1:N)。三种业务共用本布局: + * - rwfx(任务FX,完整版):左对话列表 + 任务操作(编辑/下一步/清除记录)+ 跳转三情分析按钮; + * 右侧问答带「目的树闸门」(AgentChatPage taskGate)。 + * - 3qfx(三情分析)/ xdfx(行动分析)(``lite`` 精简版):只有左对话列表(按 taskId + agentId 隔离), + * 无任务操作、无闸门;新建对话直接带任务开场信息自动发出(AgentChatPage taskBind)。 * - * 左侧 = 该任务下的对话列表(按 metadata.taskId 过滤)+「新建对话」; - * 右下角 = 常驻浮动操作条(编辑 / 下一步 / 清除任务记录),absolute 定位,时刻可见、不被内容挤走; - * 右侧 = 渲染选中的对话(AgentChatPage,新建对话顶部带目的树闸门面板)。 + * 左侧顶部 = 当前任务卡片(任务名称 + 简介,来自登录深链暂存的 copTaskDetail)+「新建对话」; + * 右侧 = 渲染选中的对话。 * - * - 进入 chats 索引:有历史 → 跳最新;无历史 → 跳 new(=新建,走闸门)。 - * - 新建对话 = 直接跳入:navigate 到 chats/new;切换/新建时 AgentChatPage 会停掉老对话的服务端 run。 - * - 清除任务记录:仅调 delete 接口(聊天对话保留)。 - * - 编辑 / 下一步:window.open 配置里的 B 系统地址(?id=taskId)。 - * 见 [[task-purpose]] / [[task-deeplink-config]]。 + * - 进入 chats 索引:有历史 → 跳最新;无历史 → 跳 new(rwfx 走闸门 / 3qfx/xdfx 直接带开场信息)。 + * - 切换/新建时 AgentChatPage 会停掉老对话的服务端 run。 + * - (rwfx)清除任务记录:仅调 delete 接口(聊天对话保留);编辑/下一步:window.open B 系统地址。 + * 见 [[task-purpose]] / [[task-deeplink]] / [[task-deeplink-config]]。 */ import { useEffect, useMemo, useState } from "react"; @@ -32,6 +34,7 @@ import { deleteTaskPurpose, readTaskContext, } from "@/core/auth/task-purpose"; +import { readCopTaskDetail, resolveTaskDeeplinkTarget } from "@/core/auth/task-deeplink"; import { fetchTaskDeeplinkConfig } from "@/core/auth/task-deeplink-config"; import { useTaskThreads } from "@/core/threads/use-task-threads"; import { titleOfThread } from "@/core/threads/utils"; @@ -40,10 +43,43 @@ import { cn } from "@/lib/utils"; import AgentChatPage from "@/pages/AgentChatPage"; import { ChatRuntime } from "@/pages/WorkspaceRoutes"; -function useChatsBase(taskBase: string): { taskId: string; chatsBase: string } { +function useChatsBase(taskBase: string): { taskId: string; agentId: string; chatsBase: string } { const { agent_id } = useParams<{ agent_id: string }>(); + const agentId = agent_id ?? ""; const taskId = readTaskContext()?.taskId ?? ""; - return { taskId, chatsBase: `${taskBase}/agent/${agent_id}/chats` }; + return { taskId, agentId, chatsBase: `${taskBase}/agent/${agentId}/chats` }; +} + +/** + * 左侧顶部「当前任务」卡片:展示登录深链查询出的任务名称 + 简介(留存在 sessionStorage 的 + * copTaskDetail)。三种业务工作区都展示,放在「新建对话」上面。无任务详情时不渲染。 + */ +function TaskDetailHeader() { + const detail = useMemo(() => readCopTaskDetail(), []); + const name = desensitize((detail?.taskName ?? "").trim()); + const brief = desensitize(((detail?.overview ?? "").trim() || (detail?.taskContent ?? "").trim())); + if (!name && !brief) return null; + return ( +
+
当前任务
+ {name && ( +
+ {name} +
+ )} + {brief && ( +
+ {brief} +
+ )} +
+ ); } /** 从路由路径里解析当前打开的会话 id(布局组件读不到子路由的 :thread_id,故从 path 取)。 */ @@ -53,16 +89,46 @@ function parseThreadId(pathname: string, chatsBase: string): string | null { return seg && seg !== "new" ? seg : null; } -/** 左侧任务对话列表 +「新建对话」。 */ -function TaskConversationsList({ taskId, chatsBase }: { taskId: string; chatsBase: string }) { +/** + * 左侧任务对话列表:顶部「当前任务」卡片 +「新建对话」+(rwfx)跳转三情分析;下方对话列表(1:N)。 + * ``lite``(3qfx/xdfx)隐藏跳转按钮与底部任务操作面板。 + */ +function TaskConversationsList({ + taskId, + agentId, + chatsBase, + lite, +}: { + taskId: string; + agentId: string; + chatsBase: string; + lite?: boolean; +}) { const { pathname } = useLocation(); const navigate = useNavigate(); - const { threads, isLoading } = useTaskThreads(taskId); + const { threads, isLoading } = useTaskThreads(taskId, agentId); const currentThreadId = useMemo(() => parseThreadId(pathname, chatsBase), [pathname, chatsBase]); + // rwfx:带当前任务(同一 taskId)跳到三情分析工作区。复用深链解析(会按 3Q 业务码重绑任务上下文)。 + const handleJumpToSentiment = async () => { + const detail = readCopTaskDetail(); + if (!detail || !taskId) { + toast.error("缺少任务信息,无法跳转三情分析"); + return; + } + try { + const target = await resolveTaskDeeplinkTarget("3qfx", detail, taskId); + if (target) navigate(target.path, { state: target.state }); + else toast.error("未配置三情分析智能体"); + } catch { + toast.error("跳转失败,请稍后再试"); + } + }; + return (
+
任务对话
@@ -78,6 +144,17 @@ function TaskConversationsList({ taskId, chatsBase }: { taskId: string; chatsBas > 新建对话 + {!lite && ( + + )}
@@ -114,7 +191,7 @@ function TaskConversationsList({ taskId, chatsBase }: { taskId: string; chatsBas )) )}
- + {!lite && }
); } @@ -209,16 +286,19 @@ function TaskActionsPanel({ taskId }: { taskId: string }) { ); } -/** 任务工作区布局:左列表 + 右 Outlet。 */ -export function TaskFxWorkspace({ taskBase }: { taskBase: string }) { - const { taskId, chatsBase } = useChatsBase(taskBase); - // 预拉配置,保证「编辑/下一步」的同步 URL 构造能读到缓存。 +/** + * 任务工作区布局:左列表 + 右 Outlet。 + * ``lite``(3qfx/xdfx)= 精简版:左列表无跳转按钮、无底部任务操作面板。 + */ +export function TaskFxWorkspace({ taskBase, lite }: { taskBase: string; lite?: boolean }) { + const { taskId, agentId, chatsBase } = useChatsBase(taskBase); + // 预拉配置,保证(rwfx)「编辑/下一步」的同步 URL 构造能读到缓存。 useEffect(() => { void fetchTaskDeeplinkConfig(); }, []); return (
- +
@@ -226,11 +306,11 @@ export function TaskFxWorkspace({ taskBase }: { taskBase: string }) { ); } -/** chats 索引:有历史对话 → 跳最新;否则 → 跳 new(走闸门)。 */ +/** chats 索引:有历史对话 → 跳最新;否则 → 跳 new(rwfx 走闸门 / 3qfx/xdfx 直接带开场信息)。 */ export function TaskFxIndexResolver({ taskBase }: { taskBase: string }) { - const { taskId, chatsBase } = useChatsBase(taskBase); + const { taskId, agentId, chatsBase } = useChatsBase(taskBase); const navigate = useNavigate(); - const { isLoading, hasThreads, latestThreadId } = useTaskThreads(taskId); + const { isLoading, hasThreads, latestThreadId } = useTaskThreads(taskId, agentId); useEffect(() => { if (isLoading) return; @@ -248,7 +328,7 @@ export function TaskFxIndexResolver({ taskBase }: { taskBase: string }) { ); } -/** 右侧对话宿主:AgentChatPage(taskGate 开启 → 新建对话顶部展示目的树闸门面板 + 老对话停止)。 */ +/** 右侧对话宿主(rwfx):AgentChatPage taskGate → 新建对话顶部展示目的树闸门面板 + 老对话停止。 */ export function TaskFxChat({ taskBase }: { taskBase: string }) { const { agent_id } = useParams<{ agent_id: string }>(); return ( @@ -257,3 +337,13 @@ export function TaskFxChat({ taskBase }: { taskBase: string }) { ); } + +/** 右侧对话宿主(3qfx/xdfx):AgentChatPage taskBind → 仅 1:N 绑定,新建对话直接带开场信息自动发出。 */ +export function TaskBindChat({ taskBase }: { taskBase: string }) { + const { agent_id } = useParams<{ agent_id: string }>(); + return ( + + + + ); +} diff --git a/frontend-web/src/strategy-components/components/task-components/TaskPurposePanel.tsx b/frontend-web/src/strategy-components/components/task-components/TaskPurposePanel.tsx index 802d85d7..6ed516ea 100644 --- a/frontend-web/src/strategy-components/components/task-components/TaskPurposePanel.tsx +++ b/frontend-web/src/strategy-components/components/task-components/TaskPurposePanel.tsx @@ -11,7 +11,7 @@ */ import { useEffect, useMemo, useState } from "react"; -import { Loader2, Maximize2 } from "lucide-react"; +import { ExternalLink, Loader2, Maximize2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -24,6 +24,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { + buildDetail3qUrl, deleteTaskPurpose, fetchTaskPurposeDetail, flattenPurposeTree, @@ -158,6 +159,17 @@ export function TaskPurposePanel({ 弹框查看 )} + diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml index 953730c2..50ed0d35 100644 --- a/offline-backend-20260512/backend/config.yaml +++ b/offline-backend-20260512/backend/config.yaml @@ -420,13 +420,18 @@ log_system: label: '日志系统' task_deeplink: - # 线上接口未通时用假数据;两个开关互相独立。 + # 线上接口未通时用假数据;三个开关互相独立。 test: true # 目的树查询走 mock.purpose_detail 假数据 delete_test: true # 删除接口走 mock.delete 假数据(该接口尚未开发完) + action_test: true # 行动→任务解析走 mock.action_detail 假数据(xdfx 行动id→任务id) purpose_detail_url: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/getTaskPurposeDetail" delete_url: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/delete" treemap_edit_url: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-edit" treemap_next_url: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-next" + # xdfx 深链传入的是「行动id」,先调该接口(追加 /{actionId})拿 data.taskId 解析出真正「任务id」。 + action_detail_url: "https://ch1.b.uat.4.cn/consumer/plan/action" + # rwfx 目的树面板「3q详情」链接的目标页(地址待定,留空则前端提示未配置);跳转追加 ?id=任务id。 + detail_3q_url: "" mock: # test=true 时原样返回给前端(结构同真实 getTaskPurposeDetail 返回)。 purpose_detail: @@ -455,3 +460,15 @@ task_deeplink: delete: message: "行动已经开始策划,不可覆盖" success: true + # action_test=true 时原样返回给前端(结构同真实 /consumer/plan/action/{actionId} 返回)。 + # 前端取 data.taskId 作为真正的「任务id」(xdfx 深链 ?taskId= 传入的是 data.id 这个行动id)。 + action_detail: + state: "200" + msg: "操作成功!" + data: + id: 27354 + purposeId: 2850 + taskId: 2055 + actionName: "" + actionStatus: 25 + narration: "" diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/config/task_deeplink_config.py b/offline-backend-20260512/backend/packages/harness/deerflow/config/task_deeplink_config.py index 5ebc2f13..e5089c2a 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/config/task_deeplink_config.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/config/task_deeplink_config.py @@ -1,12 +1,14 @@ -"""任务深链(``goPath=rwfx``)的「目的树闸门 + 跳转」配置。 +"""任务深链(``goPath=rwfx`` / ``xdfx``)的「目的树闸门 + 行动→任务解析 + 跳转」配置。 前端(登录深链流程)通过公开只读接口 ``GET /api/public/config/task-deeplink`` -读取这一段:所有外部地址都是**完整 URL**(调用时仅追加 ``?taskId=`` / ``?id=``), +读取这一段:所有外部地址都是**完整 URL**(调用时仅追加 ``?taskId=`` / ``?id=`` 或 ``/{actionId}`` 路径段), 随时改 ``config.yaml`` 即可(Gateway 有 mtime 热重载,无需重新构建前端)。 测试开关(线上接口未通时用假数据,互相独立): - ``test`` → 目的树查询(``purpose_detail_url``)走 ``mock.purpose_detail`` 假数据。 - ``delete_test`` → 删除接口(``delete_url``)走 ``mock.delete`` 假数据(该接口尚未开发完时打开)。 + - ``action_test`` → 行动详情接口(``action_detail_url``)走 ``mock.action_detail`` 假数据。 + xdfx 深链传入的是「行动id」,需先调该接口拿 ``data.taskId`` 解析出真正的「任务id」。 """ from __future__ import annotations @@ -29,10 +31,22 @@ class TaskDeeplinkConfig(BaseModel): default=False, description="为真时前端删除接口走 mock.delete 假数据(删除接口未开发完时打开)", ) + action_test: bool = Field( + default=False, + description="为真时前端「行动→任务」解析走 mock.action_detail 假数据(不打真实接口)", + ) purpose_detail_url: str = Field( default="", description="目的树查询完整 URL,调用时追加 ?taskId=", ) + action_detail_url: str = Field( + default="", + description="行动详情接口基址(xdfx 行动→任务),调用时追加 /{actionId} 路径段;返回 data.taskId 为真正任务id", + ) + detail_3q_url: str = Field( + default="", + description="「3q详情」详情页 URL(rwfx 目的树面板的链接),跳转时追加 ?id=(任务id)", + ) delete_url: str = Field( default="", description="删除完整 URL,调用时追加 ?taskId=", @@ -47,5 +61,5 @@ class TaskDeeplinkConfig(BaseModel): ) mock: dict[str, Any] = Field( default_factory=dict, - description="test/delete_test 为真时返回的假数据:{purpose_detail: {...}, delete: {message, success}}", + description="test/delete_test/action_test 为真时返回的假数据:{purpose_detail: {...}, delete: {message, success}, action_detail: {state, msg, data: {taskId, ...}}}", ) diff --git a/offline-backend-20260512/config.yaml b/offline-backend-20260512/config.yaml index 94cb54a0..5c70686c 100644 --- a/offline-backend-20260512/config.yaml +++ b/offline-backend-20260512/config.yaml @@ -330,13 +330,18 @@ summarization: # GET /api/public/config/task-deeplink 读取。地址都是完整 URL(调用时仅追加 # ?taskId= / ?id=),随时改这里即可(Gateway mtime 热重载,前端无需重建)。 task_deeplink: - # 线上接口未通时用假数据;两个开关互相独立。 + # 线上接口未通时用假数据;三个开关互相独立。 test: true # 目的树查询走 mock.purpose_detail 假数据 delete_test: true # 删除接口走 mock.delete 假数据(该接口尚未开发完) + action_test: true # 行动→任务解析走 mock.action_detail 假数据(xdfx 行动id→任务id) purpose_detail_url: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/getTaskPurposeDetail" delete_url: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/delete" treemap_edit_url: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-edit" treemap_next_url: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-next" + # xdfx 深链传入的是「行动id」,先调该接口(追加 /{actionId})拿 data.taskId 解析出真正「任务id」。 + action_detail_url: "https://ch1.b.uat.4.cn/consumer/plan/action" + # rwfx 目的树面板「3q详情」链接的目标页(地址待定,留空则前端提示未配置);跳转追加 ?id=任务id。 + detail_3q_url: "" mock: # test=true 时原样返回给前端(结构同真实 getTaskPurposeDetail 返回)。 purpose_detail: @@ -365,3 +370,15 @@ task_deeplink: delete: message: "行动已经开始策划,不可覆盖" success: true + # action_test=true 时原样返回给前端(结构同真实 /consumer/plan/action/{actionId} 返回)。 + # 前端取 data.taskId 作为真正的「任务id」(xdfx 深链 ?taskId= 传入的是 data.id 这个行动id)。 + action_detail: + state: "200" + msg: "操作成功!" + data: + id: 27354 + purposeId: 2850 + taskId: 2055 + actionName: "" + actionStatus: 25 + narration: ""