feat: 新增 agentfx 任务深链与研判报告入库流程

支持 goPath=agentfx 的 iframe 嵌入问答、内置研判助手种子与报告导入协助卡,并完善深度研究流式报告能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-27 16:32:22 +08:00
1 parent 6e9e89a6cb
commit 52b9cbf707
34 files changed
+4488 -392

No files matched your search

@@ -0,0 +1,174 @@
"use client";
/**
* agentfx「入库」用户协助卡片:任务研判报告智能体生成 report.md + task-reports.json
* 并给出总结后,由 AgentChatPage(taskBind)注入到消息流尾部渲染本卡。
*
* 「开始入库」:读取沙箱产物 task-reports.json → 按 14 类结构校验(与后端
* task-report-import 技能同规则)→ POST /api/task-reports/import/by-task/{taskId}
* 整体替换任务详情。「暂不入库」仅收起卡片(用户之后可让智能体走技能入库)。
*/
import { useCallback, useEffect, useState } from "react";
import { CheckCircle2, Database, Loader2, TriangleAlert } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { loadArtifactContent } from "@/core/artifacts/loader";
import { importTaskReport } from "@/strategy-components/api/task-reports";
import {
AGENTFX_REPORT_JSON_OUTPUT_PATH,
notifyTaskReportSaved,
validateAgentfxReportText,
type AgentfxReportValidation,
} from "@/strategy-components/lib/agentfx-report-schema";
type Phase = "loading" | "ready" | "importing" | "done" | "error";
export function TaskReportImportCard({
taskId,
threadId,
jsonPath = AGENTFX_REPORT_JSON_OUTPUT_PATH,
onImported,
onDismiss,
}: {
taskId: string;
threadId: string;
/** 沙箱产物路径(默认智能体 SOUL 约定的 task-reports.json)。 */
jsonPath?: string;
/** 入库成功回调(宿主据此刷新任务详情状态、收起卡片)。 */
onImported?: () => void;
/** 「暂不入库」回调(宿主收起卡片)。 */
onDismiss?: () => void;
}) {
const [phase, setPhase] = useState<Phase>("loading");
const [validation, setValidation] = useState<AgentfxReportValidation | null>(null);
const [errorMsg, setErrorMsg] = useState("");
useEffect(() => {
let alive = true;
setPhase("loading");
void loadArtifactContent({ filepath: jsonPath, threadId })
.then(({ content }) => {
if (!alive) return;
if (!content) {
setErrorMsg("读取报告 JSON 失败(文件为空或不存在)");
setPhase("error");
return;
}
setValidation(validateAgentfxReportText(content, { sessionId: threadId }));
setPhase("ready");
})
.catch((error) => {
if (!alive) return;
setErrorMsg(error instanceof Error ? error.message : "读取报告 JSON 失败");
setPhase("error");
});
return () => {
alive = false;
};
}, [jsonPath, threadId]);
const handleImport = useCallback(async () => {
if (!validation || !validation.ok) return;
setPhase("importing");
try {
const response = await importTaskReport(taskId, validation.records);
const count = response.data?.length ?? 0;
setPhase("done");
toast.success(`入库成功:已写入 ${count} 条任务详情`);
// iframe 嵌入场景:通知外部宿主系统任务详情已保存。
notifyTaskReportSaved(taskId);
onImported?.();
} catch (error) {
const message = error instanceof Error ? error.message : "入库失败";
setErrorMsg(message);
setPhase("error");
toast.error(`入库失败:${message}`);
}
}, [onImported, taskId, validation]);
return (
<div className="border-border/70 bg-background/85 mx-auto w-full max-w-[760px] rounded-xl border p-4 shadow-sm backdrop-blur-xl dark:border-[#1b477e]/70 dark:bg-[#061b35]/82">
<div className="flex items-start gap-3">
<div className="bg-[var(--color-main)]/10 text-[var(--color-main)] mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<Database className="h-4.5 w-4.5" />
</div>
<div className="min-w-0 flex-1">
<div className="text-foreground text-sm font-semibold">研判报告已生成,是否入库?</div>
<div className="text-muted-foreground mt-1 text-xs leading-relaxed">
入库会把结构化结果(任务详情,14 个分类)整体写入任务
<span className="text-foreground mx-1 font-medium">{taskId}</span>
,已有详情会被覆盖;报告文件(report.md / task-reports.json)保留在沙箱中。
</div>
{phase === "loading" && (
<div className="text-muted-foreground mt-3 flex items-center gap-1.5 text-xs">
<Loader2 className="h-3.5 w-3.5 animate-spin" /> 正在校验报告 JSON…
</div>
)}
{phase === "ready" && validation && (
<div className="mt-3 space-y-1.5 text-xs">
{validation.ok ? (
<div className="text-muted-foreground flex items-center gap-1.5">
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />
校验通过:{validation.presentCategories.length} / 14 个分类
{validation.missingCategories.length > 0 && (
<span>(缺少:{validation.missingCategories.join("、")})</span>
)}
</div>
) : (
<div className="text-muted-foreground flex items-start gap-1.5">
<TriangleAlert className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
<span>
{validation.records.length > 0
? `校验发现 ${validation.errors.length} 处问题,可导入 ${validation.records.length} 条合法记录:`
: "校验未通过:"}
{validation.errors.slice(0, 3).join(";")}
{validation.errors.length > 3 && ` 等 ${validation.errors.length} 处`}
</span>
</div>
)}
</div>
)}
{phase === "error" && (
<div className="text-muted-foreground mt-3 flex items-start gap-1.5 text-xs">
<TriangleAlert className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
<span>{errorMsg || "发生错误"}</span>
</div>
)}
{phase === "done" && (
<div className="text-muted-foreground mt-3 flex items-center gap-1.5 text-xs">
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" /> 已入库;可在任务管理侧查看任务详情。
</div>
)}
{(phase === "ready" || phase === "error") && (
<div className="mt-3 flex items-center gap-2">
<Button
size="sm"
className="h-8 bg-[var(--color-main)] text-white shadow-sm hover:bg-[var(--brand-950)]"
disabled={phase !== "ready" || !validation?.ok}
onClick={() => void handleImport()}
>
<Database className="h-3.5 w-3.5" /> 开始入库
</Button>
<Button size="sm" variant="ghost" className="h-8" onClick={onDismiss}>
暂不入库
</Button>
</div>
)}
{phase === "importing" && (
<div className="text-muted-foreground mt-3 flex items-center gap-1.5 text-xs">
<Loader2 className="h-3.5 w-3.5 animate-spin" /> 正在入库…
</div>
)}
</div>
</div>
</div>
);
}