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:
1 parent
47bf0c2a1b
commit
ecb79ce49c
13 files changed
+347
-95
No files matched your search
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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任务" />} />
|
||||
|
||||
+114
-24
@@ -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>
|
||||
);
|
||||
}
|
||||
+13
-1
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user