feat: 3qfx/xdfx 任务深链 1:N 工作区 + xdfx 行动id解析 + 3q详情链接

- 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) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-18 11:37:05 +08:00
1 parent 47bf0c2a1b
commit ecb79ce49c
13 files changed
+347 -95

No files matched your search

+1 -1
View File
@@ -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/<business-path>/{roundtable,agent/:agent_id/chats/:thread_id}` in `StrategyRoutes.tsx` (`<business-path>` = `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/<business-path>/{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`<business-path>` = `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-deeLine truncated
### Configuration
@@ -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,
},
};
}
+48 -21
View File
@@ -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<string> {
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<string, unknown>).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<string> {
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` };
}
@@ -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 {
@@ -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,
+38 -13
View File
@@ -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<string | null>(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
+6 -2
View File
@@ -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 });
+37 -21
View File
@@ -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 (
<ChatRuntime>
<AgentChatPage chatBasePath={`${taskBase}/agent/${agent_id}/chats`} />
</ChatRuntime>
);
}
export default function StrategyRoutes() {
return (
<RouteBaseProvider value="/page/strategy">
@@ -144,12 +131,26 @@ export default function StrategyRoutes() {
</ChatRuntime>
}
/>
{/* 三情分析:任务工作区(左对话列表 1:N,无目的树闸门;新建对话直接带开场信息)。 */}
<Route
path="plan-task-sentiment/agent/:agent_id/chats/:thread_id"
path="plan-task-sentiment/agent/:agent_id/chats"
element={
<TaskEmbeddedAgentChat taskBase="/page/strategy/task-management/plan-task-sentiment" />
<TaskFxWorkspace taskBase="/page/strategy/task-management/plan-task-sentiment" lite />
}
/>
>
<Route
index
element={
<TaskFxIndexResolver taskBase="/page/strategy/task-management/plan-task-sentiment" />
}
/>
<Route
path=":thread_id"
element={
<TaskBindChat taskBase="/page/strategy/task-management/plan-task-sentiment" />
}
/>
</Route>
<Route
path="plan-task/plan-task-fx"
element={
@@ -208,12 +209,27 @@ export default function StrategyRoutes() {
</ChatRuntime>
}
/>
{/* 行动分析:任务工作区(左对话列表 1:N 精简版;右侧带目的树闸门,与 rwfx 同一查询/显示)。
注意:xdfx 深链传入的是「行动id」,登录时已先经 resolveDeeplinkTaskId 换成任务id。 */}
<Route
path="plan-task/plan-task-action/agent/:agent_id/chats/:thread_id"
path="plan-task/plan-task-action/agent/:agent_id/chats"
element={
<TaskEmbeddedAgentChat taskBase="/page/strategy/task-management/plan-task/plan-task-action" />
<TaskFxWorkspace taskBase="/page/strategy/task-management/plan-task/plan-task-action" lite />
}
/>
>
<Route
index
element={
<TaskFxIndexResolver taskBase="/page/strategy/task-management/plan-task/plan-task-action" />
}
/>
<Route
path=":thread_id"
element={
<TaskFxChat taskBase="/page/strategy/task-management/plan-task/plan-task-action" />
}
/>
</Route>
<Route path="mnty-task-mechanism" element={<TaskTyFrom defaultPage="iframeXdty" title="XDTY任务" />} />
<Route path="mnty-task-plan" element={<TaskTyFrom defaultPage="iframeFaty" title="方案TY任务" />} />
<Route path="mnty-task-zsty" element={<TaskTyFrom defaultPage="iframeSysystem" title="XXTY任务" />} />
@@ -1,15 +1,17 @@
/**
* 任务深链(rwfx / 任务FX)的「任务工作区」:一个任务挂多个对话(1:N)。
* 任务深链的「任务工作区」:一个任务挂多个对话(1:N)。三种业务共用本布局:
* - rwfx(任务FX,完整版):左对话列表 + 任务操作(编辑/下一步/清除记录)+ 跳转三情分析按钮;
* 右侧问答带「目的树闸门」(AgentChatPage taskGate)。
* - 3qfx(三情分析)/ xdfx(行动分析)(``lite`` 精简版):只有左对话列表(按 taskId + agentId 隔离),
* 无任务操作、无闸门;新建对话直接带任务开场信息自动发出(AgentChatPage taskBind)。
*
* 左侧 = 该任务下的对话列表(按 metadata.taskId 过滤)+「新建对话」;
* 右下角 = 常驻浮动操作条(编辑 / 下一步 / 清除任务记录),absolute 定位,时刻可见、不被内容挤走;
* 右侧 = <Outlet/> 渲染选中的对话(AgentChatPage,新建对话顶部带目的树闸门面板)。
* 左侧顶部 = 当前任务卡片(任务名称 + 简介,来自登录深链暂存的 copTaskDetail)+「新建对话」;
* 右侧 = <Outlet/> 渲染选中的对话。
*
* - 进入 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 (
<div className="mb-3 rounded-md border border-[var(--card-border)] bg-background/70 p-2.5 shadow-sm dark:border-[#2b8ffe]/40 dark:bg-[#0b2546]/70">
<div className="text-muted-foreground mb-1 text-[11px] font-medium">当前任务</div>
{name && (
<div
className="text-foreground line-clamp-2 text-sm font-semibold leading-snug"
title={name}
>
{name}
</div>
)}
{brief && (
<div
className="text-muted-foreground mt-1 line-clamp-3 text-xs leading-relaxed"
title={brief}
>
{brief}
</div>
)}
</div>
);
}
/** 从路由路径里解析当前打开的会话 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 (
<div className="bg-background/85 border-border/70 flex w-64 shrink-0 flex-col border-r shadow-[12px_0_30px_-30px_rgba(15,23,42,0.45)] backdrop-blur-xl dark:border-[#1b477e]/70 dark:bg-[#061b35]/82">
<div className="border-border/70 border-b p-3 dark:border-[#1b477e]/70">
<TaskDetailHeader />
<div className="mb-2 flex items-center justify-between gap-2">
<div className="min-w-0">
<div className="text-foreground text-sm font-semibold">任务对话</div>
@@ -78,6 +144,17 @@ function TaskConversationsList({ taskId, chatsBase }: { taskId: string; chatsBas
>
<PlusSquare className="h-4 w-4" /> 新建对话
</Button>
{!lite && (
<Button
size="sm"
variant="outline"
className="mt-2 h-9 w-full border-[var(--card-border)] bg-background/80 text-[var(--color-main)] hover:bg-[var(--bg-color)] hover:text-[var(--color-main)] dark:border-[#2b8ffe]/50 dark:bg-[#0b2546]/80 dark:text-[#bde9ff] dark:hover:bg-[#10375f]"
onClick={handleJumpToSentiment}
title="带当前任务跳转到三情分析"
>
切换到三情分析 <ArrowRight className="h-4 w-4" />
</Button>
)}
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-2">
@@ -114,7 +191,7 @@ function TaskConversationsList({ taskId, chatsBase }: { taskId: string; chatsBas
))
)}
</div>
<TaskActionsPanel taskId={taskId} />
{!lite && <TaskActionsPanel taskId={taskId} />}
</div>
);
}
@@ -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 (
<div className="flex size-full min-h-0 overflow-hidden">
<TaskConversationsList taskId={taskId} chatsBase={chatsBase} />
<TaskConversationsList taskId={taskId} agentId={agentId} chatsBase={chatsBase} lite={lite} />
<div className="relative min-h-0 min-w-0 flex-1">
<Outlet />
</div>
@@ -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 }) {
</ChatRuntime>
);
}
/** 右侧对话宿主(3qfx/xdfx):AgentChatPage taskBind → 仅 1:N 绑定,新建对话直接带开场信息自动发出。 */
export function TaskBindChat({ taskBase }: { taskBase: string }) {
const { agent_id } = useParams<{ agent_id: string }>();
return (
<ChatRuntime>
<AgentChatPage chatBasePath={`${taskBase}/agent/${agent_id}/chats`} taskBind />
</ChatRuntime>
);
}
@@ -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({
<Maximize2 className="h-3.5 w-3.5" /> 弹框查看
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => {
const url = buildDetail3qUrl(taskId);
if (url) window.open(url, "_blank", "noopener,noreferrer");
else toast.error("未配置3q详情地址");
}}
>
<ExternalLink className="h-3.5 w-3.5" /> 3q详情
</Button>
<Button size="sm" onClick={handleStart} disabled={loading || working}>
{result?.hasData ? "覆盖并开始问答" : "开始问答"}
</Button>
+18 -1
View File
@@ -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: ""
@@ -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, ...}}}",
)
+18 -1
View File
@@ -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: ""