- 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>
226 lines
9.9 KiB
TypeScript
226 lines
9.9 KiB
TypeScript
/**
|
||
* 外部系统跳转登录时的「任务深链」:登录 URL 带 ``taskId`` + ``goPath`` 时,
|
||
* 拉取任务详情并据 goPath 把用户直接送到对应的智能体问答 / 多智能体会商页。
|
||
*
|
||
* 流程:
|
||
* 1. 调 consumer 接口 ``GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=``,
|
||
* 请求头 ``Authorization: Bearer admin``(由 service 拦截器统一注入)。
|
||
* 2. 把返回的 data(id/overview/taskDirection/taskName/taskContent)整包存到 sessionStorage。
|
||
* 3. 按 goPath 解析出业务码 + 模式:
|
||
* - ``3qfx`` / ``rwfx`` / ``xdfx`` → 单智能体问答(用「配置管理」里该业务配置的智能体)
|
||
* - ``3qfx-A`` / ``rwfx-A`` / ``xdfx-A`` → 多智能体会商(用该业务配置的业务链条)
|
||
* 携带 taskName / taskContent 作为开场信息。
|
||
*
|
||
* 业务码映射与「配置管理」(`/api/business-mapping`) 一致:
|
||
* 3Q=三情分析(3QFX)、6BF=任务分析(任务FX)、7BF=行动分析(XDFX)。
|
||
*/
|
||
|
||
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";
|
||
|
||
/** xdfx 业务码:深链传入的是「行动id」,需先调 action 接口解析出真正的「任务id」。 */
|
||
const XDFX_BUSINESS_CODE = "7BF";
|
||
|
||
/** consumer 接口返回的任务详情(data 字段)。 */
|
||
export interface CopTaskDetail {
|
||
id: string | number | null;
|
||
overview: string;
|
||
taskDirection: string;
|
||
taskName: string;
|
||
taskContent: string;
|
||
}
|
||
|
||
/** 任务详情整包暂存的 sessionStorage key(目标页/后续可读取)。 */
|
||
export const COP_TASK_DETAIL_STORAGE_KEY = "login.copTaskDetail";
|
||
|
||
/** goPath 基础码 → 业务码(配置管理 businessCode)。 */
|
||
const GOPATH_BUSINESS_CODE: Record<string, string> = {
|
||
"3qfx": "3Q", // 三情分析
|
||
rwfx: "6BF", // 任务分析(任务FX)
|
||
xdfx: "7BF", // 行动分析(XDFX)
|
||
};
|
||
|
||
/**
|
||
* 业务码 → 「任务管理」布局下的路由前缀(与 sidebar-menu.ts 的 COH 中任务菜单路径一致)。
|
||
* 会商 / 智能体问答都挂在此前缀下,保证侧边栏停留在 任务管理 → COH中任务 → 对应业务 高亮。
|
||
*/
|
||
const BUSINESS_TASK_BASE: Record<string, string> = {
|
||
"3Q": "/page/strategy/task-management/plan-task-sentiment",
|
||
"6BF": "/page/strategy/task-management/plan-task/plan-task-fx",
|
||
"7BF": "/page/strategy/task-management/plan-task/plan-task-action",
|
||
};
|
||
|
||
export interface ParsedGoPath {
|
||
businessCode: string;
|
||
/** true = 多智能体会商(``-A`` 后缀);false = 单智能体问答。 */
|
||
roundtable: boolean;
|
||
}
|
||
|
||
/** 解析 goPath(大小写不敏感);``-A`` 后缀表示多智能体会商。无法识别返回 null。 */
|
||
export function parseGoPath(goPath: string): ParsedGoPath | null {
|
||
const raw = goPath.trim().toLowerCase();
|
||
if (!raw) return null;
|
||
const roundtable = raw.endsWith("-a");
|
||
const base = roundtable ? raw.slice(0, -2) : raw;
|
||
const businessCode = GOPATH_BUSINESS_CODE[base];
|
||
if (!businessCode) return null;
|
||
return { businessCode, roundtable };
|
||
}
|
||
|
||
function consumerBaseUrl(): string {
|
||
const value = import.meta.env.VITE_AIPI_CONSUMER_URL;
|
||
return typeof value === "string" ? value.replace(/\/+$/, "") : "";
|
||
}
|
||
|
||
/** 拉取任务详情;缺少 consumer 地址 / 返回不合法时返回 null。 */
|
||
export async function fetchCopTaskDetail(taskId: string): Promise<CopTaskDetail | null> {
|
||
const base = consumerBaseUrl();
|
||
if (!base || !taskId) return null;
|
||
// service 拦截器已统一注入 `Authorization: Bearer admin` 并返回 response.data(整包 body)。
|
||
const body = await requestCrudService.get(`${base}/taskAnalyseSearch/cop-task-detail`, { taskId });
|
||
const data = body && typeof body === "object" ? (body as { data?: unknown }).data : null;
|
||
if (!data || typeof data !== "object") return null;
|
||
const d = data as Record<string, unknown>;
|
||
return {
|
||
id: (d.id as string | number | null) ?? null,
|
||
overview: typeof d.overview === "string" ? d.overview : "",
|
||
taskDirection: typeof d.taskDirection === "string" ? d.taskDirection : "",
|
||
taskName: typeof d.taskName === "string" ? d.taskName : "",
|
||
taskContent: typeof d.taskContent === "string" ? d.taskContent : "",
|
||
};
|
||
}
|
||
|
||
/** 把任务详情整包暂存到 sessionStorage(隐私模式/quota 异常时静默失败)。 */
|
||
export function persistCopTaskDetail(detail: CopTaskDetail): void {
|
||
try {
|
||
sessionStorage.setItem(COP_TASK_DETAIL_STORAGE_KEY, JSON.stringify(detail));
|
||
} catch {
|
||
/* noop */
|
||
}
|
||
}
|
||
|
||
/** 读取暂存的任务详情。 */
|
||
export function readCopTaskDetail(): CopTaskDetail | null {
|
||
try {
|
||
const raw = sessionStorage.getItem(COP_TASK_DETAIL_STORAGE_KEY);
|
||
return raw ? (JSON.parse(raw) as CopTaskDetail) : null;
|
||
} catch {
|
||
return 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)。 */
|
||
state?: Record<string, unknown>;
|
||
}
|
||
|
||
/** 用 taskName / taskContent 拼出开场信息文本;末尾附上任务 id(task_id),供后端识别任务。 */
|
||
function buildOpeningText(detail: CopTaskDetail, taskId: string): string {
|
||
return [
|
||
detail.taskName ? `任务名称:${detail.taskName}` : "",
|
||
detail.taskContent ? `任务内容:${detail.taskContent}` : "",
|
||
taskId ? `任务id(task_id)为${taskId}` : "",
|
||
]
|
||
.filter(Boolean)
|
||
.join("\n");
|
||
}
|
||
|
||
/**
|
||
* 据 goPath + 任务详情解析跳转目标。
|
||
* - 多智能体会商:返回带 ``roundtablePrefill``(businessCode 让会商页自动选用配置的业务链条;
|
||
* taskId 让会商页按任务隔离存取草稿/聊天记录)的 state。
|
||
* - 单智能体问答:解析「配置管理」里该业务配置的智能体;把开场信息写入
|
||
* ``sessionStorage.pendingAgentMessage``(AgentChatPage 自动发出)。
|
||
* 无法识别 goPath / 未配置智能体时返回 null(调用方回退到默认首页)。
|
||
*
|
||
* @param taskId 深链 URL 的原始 ``?taskId=``(外部任务 id,用于按任务隔离草稿;与
|
||
* consumer 返回的 detail.id 区分——后者不保证等于外部任务 id)。
|
||
*/
|
||
export async function resolveTaskDeeplinkTarget(
|
||
goPath: string,
|
||
detail: CopTaskDetail,
|
||
taskId: string,
|
||
): Promise<TaskDeeplinkTarget | null> {
|
||
const parsed = parseGoPath(goPath);
|
||
if (!parsed) return null;
|
||
const taskBase = BUSINESS_TASK_BASE[parsed.businessCode];
|
||
if (!taskBase) return null;
|
||
|
||
// 清掉上一次深链可能残留的任务上下文(单智能体分支会用本次任务重新写入)。
|
||
clearTaskContext();
|
||
|
||
if (parsed.roundtable) {
|
||
// 多智能体会商:嵌进对应业务的「任务管理」布局;businessCode 让会商页自动选用配置的业务链条;
|
||
// taskId 让会商页按任务隔离存取草稿(一个 taskId 多条记录),并触发第三步自动结构化输出。
|
||
return {
|
||
path: `${taskBase}/roundtable`,
|
||
state: {
|
||
roundtablePrefill: {
|
||
taskTitle: detail.taskName,
|
||
taskGoal: detail.taskContent,
|
||
businessCode: parsed.businessCode,
|
||
taskId: taskId || undefined,
|
||
autoStart: true,
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
let agentId: string | null = null;
|
||
try {
|
||
const mappings = await listBusinessMappings();
|
||
agentId = mappings.find((m) => m.businessCode === parsed.businessCode)?.agentId ?? null;
|
||
} catch {
|
||
agentId = null;
|
||
}
|
||
if (!agentId) return null;
|
||
|
||
// 单智能体问答统一走「任务工作区」(任务↔对话 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` };
|
||
}
|