/** * 任务深链的「任务工作区」:一个任务挂多个对话(1:N)。三种业务共用本布局: * - rwfx(任务FX,完整版):左对话列表 + 任务操作(编辑/下一步/清除记录)+ 跳转三情分析按钮; * 右侧问答带「目的树闸门」(AgentChatPage taskGate)。 * - 3qfx(三情分析)/ xdfx(行动分析)(``lite`` 精简版):只有左对话列表(按 taskId + agentId 隔离), * 无任务操作、无闸门;新建对话进问答页后,右上角「开始回答」按钮手动发出任务开场信息(AgentChatPage taskBind)。 * xdfx 额外在左上「当前任务」卡片下展示「行动详情」(行动名称/叙事,来自行动详情接口)。 * * 左侧顶部 = 当前任务卡片(任务名称 + 简介,来自登录深链暂存的 copTaskDetail)+「新建对话」; * 右侧 = 渲染选中的对话。 * * - 进入 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"; import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom"; import { Loader2, MessageSquare, PlusSquare, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { deleteTaskPurpose, readTaskContext } from "@/core/auth/task-purpose"; import { fetchActionDetail, readCopTaskDetail, resolveTaskDeeplinkTarget, type ActionDetail, } from "@/core/auth/task-deeplink"; import { fetchSqReportStatus } from "@/core/auth/dashboard-input"; import { TASK_BUSINESSES, type TaskBusiness } from "@/core/auth/task-buttons"; import { useTaskThreads } from "@/core/threads/use-task-threads"; import { titleOfThread } from "@/core/threads/utils"; import { desensitize } from "@/lib/desensitize"; import { cn } from "@/lib/utils"; import AgentChatPage from "@/pages/AgentChatPage"; import { ChatRuntime } from "@/pages/WorkspaceRoutes"; import { TaskJumpBar } from "@/strategy-components/components/task-components/TaskJumpBar"; function useChatsBase(taskBase: string): { taskId: string; agentId: string; /** xdfx(partitionByAction):按「行动id」划分聊天记录用;其它业务为 ""(仍按 taskId 划分)。 */ actionId: string; chatsBase: string; } { const { agent_id } = useParams<{ agent_id: string }>(); const agentId = agent_id ?? ""; const ctx = readTaskContext(); const taskId = ctx?.taskId ?? ""; const actionId = ctx?.partitionByAction ? (ctx.actionId ?? "") : ""; return { taskId, agentId, actionId, 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 (
当前任务
{name && (
{name}
)} {brief && (
{brief}
)}
); } /** * xdfx 专用「行动详情」卡片:放在「当前任务」卡片下面。用任务上下文里的「行动id」 * (xdfx 深链原始 ?taskId=)调行动详情接口,展示行动名称 + 叙事。无数据时不渲染。 */ function ActionDetailHeader() { const actionId = useMemo(() => readTaskContext()?.actionId ?? "", []); const [detail, setDetail] = useState(null); useEffect(() => { if (!actionId) return; let alive = true; void fetchActionDetail(actionId).then((d) => { if (alive) setDetail(d); }); return () => { alive = false; }; }, [actionId]); const name = desensitize((detail?.actionName ?? "").trim()); const narration = desensitize((detail?.narration ?? "").trim()); // 行动名称为空时用「行动 #id」兜底,至少把卡片标出来(mock 行动名/叙事常为空)。 const title = name || (detail?.id ? `行动 #${detail.id}` : ""); if (!title && !narration) return null; return (
行动详情
{title && (
{title}
)} {narration && (
{narration}
)}
); } /** 从路由路径里解析当前打开的会话 id(布局组件读不到子路由的 :thread_id,故从 path 取)。 */ function parseThreadId(pathname: string, chatsBase: string): string | null { if (!pathname.startsWith(chatsBase)) return null; const seg = pathname.slice(chatsBase.length).replace(/^\/+/, "").split("/")[0]; return seg && seg !== "new" ? seg : null; } /** * 左侧任务对话列表:顶部「当前任务」卡片 +「新建对话」;下方对话列表(1:N)。 * 跳转按钮已移到工作区底部「快捷跳转」栏(见 ``TaskJumpBar``)。 */ function TaskConversationsList({ taskId, agentId, actionId, chatsBase, business, lite, }: { taskId: string; agentId: string; /** xdfx:按「行动id」划分聊天记录;其它业务为 ""。 */ actionId: string; chatsBase: string; business: TaskBusiness; lite?: boolean; }) { const { pathname } = useLocation(); const navigate = useNavigate(); const { threads, isLoading } = useTaskThreads(taskId, agentId, actionId); const currentThreadId = useMemo(() => parseThreadId(pathname, chatsBase), [pathname, chatsBase]); const businessName = desensitize(TASK_BUSINESSES.find((b) => b.code === business)?.name ?? ""); return (
{/* 左上角标明当前是哪个分析页面 */}
{businessName}
{/* xdfx:任务详情下面再展示行动详情 */} {business === "xdfx" && }
任务对话
{isLoading ? "同步中" : `${threads.length} 个会话`}
{!lite && }
{isLoading ? (
加载中…
) : threads.length === 0 ? (
暂无对话
) : ( threads.map((thread) => ( )) )}
); } /** * 「清除记录」按钮(rwfx):放在「新建对话」下面,仅调 delete 接口清空 B 系统目的策划数据 * (聊天对话保留)。带二次确认弹框。 */ function TaskClearButton({ taskId }: { taskId: string }) { const [clearOpen, setClearOpen] = useState(false); const [clearing, setClearing] = useState(false); const handleClear = async () => { setClearing(true); try { const r = await deleteTaskPurpose(taskId); if (r.success) toast.success("已清除任务记录"); else toast.error(r.message || "清除失败"); } catch { toast.error("清除失败,请稍后再试"); } finally { setClearing(false); setClearOpen(false); } }; return ( <> !clearing && setClearOpen(o)}> 清除任务记录 将清除该任务在 B 系统中的目的策划数据(聊天对话保留),是否确认? ); } /** * rwfx 专用「3q 完成预检」闸门:进入任务分析工作区时调研判情报报告接口 * (``GET {VITE_AIPI_CONSUMER_URL}/cop/selectSqReport?taskId=``)。若该任务的报告为空 * (数组为空 / 内容全为 null,即 3q 未完成)→ 弹框提示「3q 未完成,是否进行 3qfx」, * 并提供「前往 3qfx」按钮切换到**同一任务**的三情分析工作区(复用深链解析重绑任务上下文)。 * 报告正常 / 接口不可判定(缺地址、请求失败)时不打扰用户。见 [[dashboard-input]] / [[task-deeplink]]。 */ function RwfxSqReportGate({ taskId }: { taskId: string }) { const navigate = useNavigate(); const [open, setOpen] = useState(false); const [jumping, setJumping] = useState(false); useEffect(() => { if (!taskId) return; let alive = true; void fetchSqReportStatus(taskId).then((status) => { // 仅「明确为空(3q 未完成)」才弹框;complete / unknown 静默放行。 if (alive && status === "incomplete") setOpen(true); }); return () => { alive = false; }; }, [taskId]); const handleGo3qfx = async () => { const detail = readCopTaskDetail(); if (!detail || !taskId) { toast.error("缺少任务信息,无法跳转"); return; } setJumping(true); try { // 复用深链解析:会清掉当前 rwfx 任务上下文并按 3qfx 业务重新绑定(同一 taskId)。 const target = await resolveTaskDeeplinkTarget("3qfx", detail, taskId); if (target) { setOpen(false); navigate(target.path, { state: target.state }); } else { toast.error("未配置三情分析(3qfx)对应的业务智能体"); } } catch { toast.error("跳转失败,请稍后再试"); } finally { setJumping(false); } }; return ( !jumping && setOpen(o)}> 3q 未完成 未查询到该任务的研判情报报告(3q 未完成),是否前往该任务的三情分析(3qfx)? ); } /** * 任务工作区布局:左列表 + 右 Outlet。 * ``lite``(3qfx/xdfx)= 精简版:左列表无跳转按钮、无底部任务操作面板。 */ export function TaskFxWorkspace({ taskBase, business, lite, }: { taskBase: string; business: TaskBusiness; lite?: boolean; }) { const { taskId, agentId, actionId, chatsBase } = useChatsBase(taskBase); return (
{/* rwfx 专属:进入时预检 3q 是否完成,未完成则弹框引导去 3qfx。 */} {business === "rwfx" && }
); } /** chats 索引:有历史对话 → 跳最新;否则 → 跳 new(rwfx 走闸门 / 3qfx/xdfx 直接带开场信息)。 */ export function TaskFxIndexResolver({ taskBase }: { taskBase: string }) { const { taskId, agentId, actionId, chatsBase } = useChatsBase(taskBase); const navigate = useNavigate(); const { isLoading, hasThreads, latestThreadId } = useTaskThreads(taskId, agentId, actionId); useEffect(() => { if (isLoading) return; if (hasThreads && latestThreadId) { navigate(`${chatsBase}/${latestThreadId}`, { replace: true }); } else { // 首次进入、该任务尚无聊天对话 → /new 并打 taskAutoStart 标记:3qfx/xdfx 直接自动发开场词, // rwfx 目的树为空时自动开始(用户主动「新建对话」走 TaskConversationsList 的 navigate,不带此标记)。 navigate(`${chatsBase}/new`, { replace: true, state: { taskAutoStart: true } }); } }, [isLoading, hasThreads, latestThreadId, chatsBase, navigate]); return (
); } /** * 右侧对话宿主(rwfx / xdfx):AgentChatPage taskGate → 新建对话顶部展示目的树闸门面板 + 老对话停止。 * ``autoStartWhenPurposeEmpty``(rwfx):查询任务目的为空数据时自动开始第一次问答。 */ export function TaskFxChat({ taskBase, autoStartWhenPurposeEmpty, }: { taskBase: string; autoStartWhenPurposeEmpty?: boolean; }) { const { agent_id } = useParams<{ agent_id: string }>(); return ( ); } /** 右侧对话宿主(3qfx/xdfx):AgentChatPage taskBind → 仅 1:N 绑定,新建对话进问答页,右上角「开始回答」按钮手动发出开场信息。 */ export function TaskBindChat({ taskBase }: { taskBase: string }) { const { agent_id } = useParams<{ agent_id: string }>(); return ( ); }