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