支持 goPath=agentfx 的 iframe 嵌入问答、内置研判助手种子与报告导入协助卡,并完善深度研究流式报告能力。 Co-authored-by: Cursor <cursoragent@cursor.com>
175 lines
7.2 KiB
TypeScript
175 lines
7.2 KiB
TypeScript
"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>
|
||
);
|
||
}
|