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

+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任务" />} />