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,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>