feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1
@@ -18,6 +18,7 @@ import { Login } from "./views/Login";
|
|||||||
import { submitTask, generateReport, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, myTenants, switchTenant, spaceCurrent, mySpaces, switchSpace, createSpace, type Identity, type AuthUser, type TenantCtx, type MyTenant, type SpaceCtx, type MySpace } from "./lib/api";
|
import { submitTask, generateReport, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, myTenants, switchTenant, spaceCurrent, mySpaces, switchSpace, createSpace, type Identity, type AuthUser, type TenantCtx, type MyTenant, type SpaceCtx, type MySpace } from "./lib/api";
|
||||||
import type { TaskDsl } from "./lib/dsl";
|
import type { TaskDsl } from "./lib/dsl";
|
||||||
import { emptyRun, type RunState } from "./lib/run";
|
import { emptyRun, type RunState } from "./lib/run";
|
||||||
|
import { notify } from "./lib/desktop";
|
||||||
import { ToastProvider } from "./ui";
|
import { ToastProvider } from "./ui";
|
||||||
|
|
||||||
// 会话标识:本地持久化生成一次(多轮历史用,与鉴权身份分离)。
|
// 会话标识:本地持久化生成一次(多轮历史用,与鉴权身份分离)。
|
||||||
@@ -178,7 +179,8 @@ export default function App() {
|
|||||||
|
|
||||||
// attachRun:给一个 task 挂上「状态轮询 + 执行轨迹流 + token 流」。新发起与恢复待审任务共用,
|
// attachRun:给一个 task 挂上「状态轮询 + 执行轨迹流 + token 流」。新发起与恢复待审任务共用,
|
||||||
// 故页面刷新/重开后能重新挂回在途任务(HITL 审批可能等数小时,必须可恢复,否则待审任务点不到批准)。
|
// 故页面刷新/重开后能重新挂回在途任务(HITL 审批可能等数小时,必须可恢复,否则待审任务点不到批准)。
|
||||||
const attachRun = useCallback((taskId: string, t0: number) => {
|
// label 用于完成时的系统通知文案(报告用主题,编排用泛称)。
|
||||||
|
const attachRun = useCallback((taskId: string, t0: number, label: string) => {
|
||||||
let first = true;
|
let first = true;
|
||||||
// 轮询后端任务状态:可靠捕获 waiting(审批中断)—— exec 实时事件会抢跑,状态落 PG 不会丢。
|
// 轮询后端任务状态:可靠捕获 waiting(审批中断)—— exec 实时事件会抢跑,状态落 PG 不会丢。
|
||||||
const terminal = new Set(["done", "failed", "timeout", "rejected"]);
|
const terminal = new Set(["done", "failed", "timeout", "rejected"]);
|
||||||
@@ -217,7 +219,12 @@ export default function App() {
|
|||||||
first = false;
|
first = false;
|
||||||
return { ...r, output: r.output + tok, events: ev };
|
return { ...r, output: r.output + tok, events: ev };
|
||||||
}),
|
}),
|
||||||
() => setRun((r) => (r.taskId === taskId ? { ...r, phase: "done", events: [...r.events, { t: Date.now() - t0, label: "完成" }] } : r)),
|
() => {
|
||||||
|
// 系统通知:跑一次动辄几分钟,用户多半已经切走干别的了——不通知就只能自己回来刷。
|
||||||
|
// 浏览器预览下 notify 是空操作,桌面壳里才弹。
|
||||||
|
notify("运行完成", label);
|
||||||
|
setRun((r) => (r.taskId === taskId ? { ...r, phase: "done", events: [...r.events, { t: Date.now() - t0, label: "完成" }] } : r));
|
||||||
|
},
|
||||||
() => setRun((r) => (r.taskId === taskId ? { ...r, phase: "error", error: "连接中断" } : r)),
|
() => setRun((r) => (r.taskId === taskId ? { ...r, phase: "error", error: "连接中断" } : r)),
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -238,7 +245,7 @@ export default function App() {
|
|||||||
taskId,
|
taskId,
|
||||||
events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }],
|
events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }],
|
||||||
}));
|
}));
|
||||||
attachRun(taskId, t0);
|
attachRun(taskId, t0, "编排任务已跑完");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setRun((r) => ({ ...r, phase: "error", error: (e as Error).message }));
|
setRun((r) => ({ ...r, phase: "error", error: (e as Error).message }));
|
||||||
}
|
}
|
||||||
@@ -266,7 +273,7 @@ export default function App() {
|
|||||||
taskId,
|
taskId,
|
||||||
events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }],
|
events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }],
|
||||||
}));
|
}));
|
||||||
attachRun(taskId, t0);
|
attachRun(taskId, t0, `报告已生成:${topic}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setRun((r) => ({ ...r, phase: "error", error: (e as Error).message }));
|
setRun((r) => ({ ...r, phase: "error", error: (e as Error).message }));
|
||||||
}
|
}
|
||||||
@@ -291,7 +298,7 @@ export default function App() {
|
|||||||
? r // 已有 live run,不覆盖
|
? r // 已有 live run,不覆盖
|
||||||
: { phase: "streaming", taskId: waiting.task_id, output: "", events: [{ t: 0, label: "恢复待审任务" }], exec: [], lifecycle: "waiting" },
|
: { phase: "streaming", taskId: waiting.task_id, output: "", events: [{ t: 0, label: "恢复待审任务" }], exec: [], lifecycle: "waiting" },
|
||||||
);
|
);
|
||||||
attachRun(waiting.task_id, t0);
|
attachRun(waiting.task_id, t0, waiting.topic || "待审任务已跑完");
|
||||||
} catch {
|
} catch {
|
||||||
/* 列表拉取失败则跳过恢复,不影响正常使用 */
|
/* 列表拉取失败则跳过恢复,不影响正常使用 */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Activity, FileText, FileType2, FileCode, History, Users, Wrench } from "lucide-react";
|
import { Activity, FileText, FileType2, FileCode, Printer, History, Users, Wrench } from "lucide-react";
|
||||||
import { ExecTrace } from "../components/ExecTrace";
|
import { ExecTrace } from "../components/ExecTrace";
|
||||||
import { TeamView } from "../components/TeamView";
|
import { TeamView } from "../components/TeamView";
|
||||||
import { OfficeView } from "../components/OfficeView";
|
import { OfficeView } from "../components/OfficeView";
|
||||||
@@ -9,7 +9,7 @@ import { extractChartBlocks, hasChart } from "../lib/chartspec";
|
|||||||
import { deriveNodes, isMultiAgent, isReportRun, emptyRun, type RunState } from "../lib/run";
|
import { deriveNodes, isMultiAgent, isReportRun, emptyRun, type RunState } from "../lib/run";
|
||||||
import { listRuns, taskEval, runReplay, reportExportUrl, reportFilename, type RunSummary, type EvalResult } from "../lib/api";
|
import { listRuns, taskEval, runReplay, reportExportUrl, reportFilename, type RunSummary, type EvalResult } from "../lib/api";
|
||||||
import { Tabs, Panel, Dot, Badge, Button, EmptyState, useToast, cn, type TabDef } from "../ui";
|
import { Tabs, Panel, Dot, Badge, Button, EmptyState, useToast, cn, type TabDef } from "../ui";
|
||||||
import { saveReportAs } from "../lib/desktop";
|
import { saveReportAs, printReportHtml } from "../lib/desktop";
|
||||||
|
|
||||||
type DetailTab = "trace" | "team" | "tools" | "eval";
|
type DetailTab = "trace" | "team" | "tools" | "eval";
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ export function RunsView({ run, focusTaskId }: { run: RunState; focusTaskId?: st
|
|||||||
const [evalRes, setEvalRes] = useState<EvalResult | null>(null);
|
const [evalRes, setEvalRes] = useState<EvalResult | null>(null);
|
||||||
const [tab, setTab] = useState<DetailTab>("trace");
|
const [tab, setTab] = useState<DetailTab>("trace");
|
||||||
const [teamSkin, setTeamSkin] = useState<"office" | "board">("office"); // 团队视图皮肤:卡通办公室 / 卡片看板
|
const [teamSkin, setTeamSkin] = useState<"office" | "board">("office"); // 团队视图皮肤:卡通办公室 / 卡片看板
|
||||||
|
const previewRef = useRef<HTMLDivElement>(null); // 导出 PDF 要拿已渲染的正文 DOM
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -88,6 +89,17 @@ export function RunsView({ run, focusTaskId }: { run: RunState; focusTaskId?: st
|
|||||||
toast.push("error", (e as Error).message);
|
toast.push("error", (e as Error).message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// 导出 PDF:把预览到的正文(已渲染 HTML)送进打印视图(CJK 零字体依赖,故走前端打印而非后端渲染)。
|
||||||
|
const exportPdf = () => {
|
||||||
|
const html = previewRef.current?.innerHTML;
|
||||||
|
if (!html) {
|
||||||
|
toast.push("error", "暂无报告正文可导出");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!printReportHtml(curTopic || curId, html)) {
|
||||||
|
toast.push("error", "打印窗口被拦截,请允许弹出窗口后重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
const nodes = deriveNodes(cur.exec);
|
const nodes = deriveNodes(cur.exec);
|
||||||
// 工具调用面板纳入专家派发:MCP 工具(kind=tool)与多智能体协调里的子智能体派发(kind=agent)都是「调用」。
|
// 工具调用面板纳入专家派发:MCP 工具(kind=tool)与多智能体协调里的子智能体派发(kind=agent)都是「调用」。
|
||||||
const calls = nodes.filter((n) => n.kind === "tool" || n.kind === "agent");
|
const calls = nodes.filter((n) => n.kind === "tool" || n.kind === "agent");
|
||||||
@@ -201,6 +213,9 @@ export function RunsView({ run, focusTaskId }: { run: RunState; focusTaskId?: st
|
|||||||
<Button icon={FileType2} onClick={() => exportReport("docx")}>
|
<Button icon={FileType2} onClick={() => exportReport("docx")}>
|
||||||
Word
|
Word
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" icon={Printer} onClick={exportPdf}>
|
||||||
|
PDF
|
||||||
|
</Button>
|
||||||
<Button variant="ghost" icon={FileCode} onClick={() => exportReport("md")}>
|
<Button variant="ghost" icon={FileCode} onClick={() => exportReport("md")}>
|
||||||
Markdown
|
Markdown
|
||||||
</Button>
|
</Button>
|
||||||
@@ -209,7 +224,9 @@ export function RunsView({ run, focusTaskId }: { run: RunState; focusTaskId?: st
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{cur.output ? (
|
{cur.output ? (
|
||||||
|
<div ref={previewRef}>
|
||||||
<OutputView output={cur.output} />
|
<OutputView output={cur.output} />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState icon={Activity} title="尚无输出" desc="选择一次运行查看结果;或发起新运行,回复会在这里逐字呈现。" />
|
<EmptyState icon={Activity} title="尚无输出" desc="选择一次运行查看结果;或发起新运行,回复会在这里逐字呈现。" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user