feat(desktop,gateway): 报告归口运行页 —— 能找回、能复盘、能导出
承接上一个提交(报告终于落库进运行历史),把「所有执行归口运行页」做完整:
只能看不能存,不算归口。
- 运行页的输出面板:报告类运行显示「报告正文」并在右上角出 Word/Markdown
导出。报告页那两个导出按钮依赖它自己一切页面就没的本地状态,所以从历史
里找回来的报告,只能在运行页导。
- 运行历史列表显示报告主题。此前只有 report_<hex>,谁也认不出是哪份报告。
主题从 graph->>'topic' 取(报告的 graph 就是占位 DSL {"topic":"…"},普通
任务 DSL 没有顶层 topic → 空串,不会误伤)。
- isReportRun() 按 task_id 的 report_ 前缀判定 —— 这是既有契约,导出接口
/reports/:id/export 本来就按同一个 id 寻址。+4 单测(含"不能只看是否包含
report"的误判防线)。
- reportFilename 从 ReportView 提到 lib:运行页也要用,不能私藏在一个 view 里。
live 验证(真账号,桌面端实机):运行历史首条显示「Redis 缓存穿透的三种解法」
→ 点开复盘 8 节点 + 全文 → 点 Word → **原生另存为对话框弹出**(文件名预填
主题)→ 保存 → 落盘 5KB,file 认 Microsoft Word 2007+,解 zip 得
word/document.xml,正文 2446 字。
**顺带把 wails3 迁移最后一个盲区验掉了**:v3 把 runtime.SaveFileDialog 重写成
application.Get().Dialog.SaveFile().SetFilename().AddFilter()
.PromptForSingleSelection(),此前从没被点过一次,记忆里只敢写"理应工作"。
现在实测通了。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -593,6 +593,12 @@ export function reportDownloadUrl(taskId: string): string {
|
||||
}
|
||||
|
||||
// reportExportUrl: 按需导出报告地址(format=docx|md;后端现渲染)。PDF 由前端打印预览生成。
|
||||
// 安全文件名:去掉路径不安全字符,限长 + 指定后缀。主题为空则退回 task_id。
|
||||
export function reportFilename(topic: string, id: string, ext: string): string {
|
||||
const base = topic.replace(/[\\/:*?"<>|]/g, "").trim().slice(0, 40) || id;
|
||||
return `${base}.${ext}`;
|
||||
}
|
||||
|
||||
export function reportExportUrl(taskId: string, format: "docx" | "md"): string {
|
||||
return `${GATEWAY}/api/v1/reports/${taskId}/export?format=${format}`;
|
||||
}
|
||||
@@ -674,6 +680,7 @@ export interface RunSummary {
|
||||
at: string;
|
||||
eval_level: string;
|
||||
eval_overall: number;
|
||||
topic: string; // 报告类运行的主题;普通任务为空串
|
||||
}
|
||||
|
||||
export async function listRuns(limit = 30): Promise<RunSummary[]> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { ExecEvent } from "./api";
|
||||
import { deriveNodes, pendingApproval, isMultiAgent, deriveTeam, teamNow } from "./run";
|
||||
import { deriveNodes, pendingApproval, isMultiAgent, deriveTeam, teamNow, isReportRun } from "./run";
|
||||
|
||||
let seq = 0;
|
||||
function ev(node: string, phase: string, extra: Partial<ExecEvent> = {}): ExecEvent {
|
||||
@@ -218,3 +218,19 @@ describe("teamNow(并发时间轴的右端)", () => {
|
||||
expect(t.t1 - t.t0).toBe(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isReportRun(运行是不是报告生成)", () => {
|
||||
it("report_ 前缀 → true", () => {
|
||||
expect(isReportRun("report_5dae9155af5cb500")).toBe(true);
|
||||
});
|
||||
it("普通任务 → false", () => {
|
||||
expect(isReportRun("task_e984b1f3e38a35c7")).toBe(false);
|
||||
});
|
||||
it("空/未定义 → false(当前无选中运行时不该冒出导出按钮)", () => {
|
||||
expect(isReportRun(undefined)).toBe(false);
|
||||
expect(isReportRun("")).toBe(false);
|
||||
});
|
||||
it("不能只看是否包含 report(避免误判)", () => {
|
||||
expect(isReportRun("task_report_like")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,6 +146,13 @@ function splitBrief(detail?: string): { brief?: string; output?: string } {
|
||||
return { output: detail };
|
||||
}
|
||||
|
||||
// isReportRun 判定一次运行是不是「报告生成」。
|
||||
// 靠 task_id 前缀:后端 newReportID() 出 "report_<hex>"、普通任务是 "task_<hex>",
|
||||
// 而报告导出接口 /reports/:id/export 本来就按同一个 id 寻址——这个前缀是既有契约,不是我临时约的。
|
||||
export function isReportRun(taskId: string | undefined): boolean {
|
||||
return !!taskId && taskId.startsWith("report_");
|
||||
}
|
||||
|
||||
// teamNow 决定并发时间轴的右端。直播时是此刻——未收口的工位应持续生长;
|
||||
// 复盘时必须是最后一个事件的时间戳:回放一条卡在 running 的历史任务(崩溃/超时留下的),
|
||||
// 未收口工位若量到 Date.now(),时间轴会一路拉到今天,跨度直接爆表。
|
||||
|
||||
Reference in New Issue
Block a user