Files
deerflow-code/frontend-web/src/strategy-components/components/task-components/TaskReportImportCard.tsx
T
董宏凯andCursor 52b9cbf707 feat: 新增 agentfx 任务深链与研判报告入库流程
支持 goPath=agentfx 的 iframe 嵌入问答、内置研判助手种子与报告导入协助卡,并完善深度研究流式报告能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 16:32:22 +08:00

175 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}