feat(downloads): fold download plans into download panel (DL-3)

- Waiting plans (approval / disk space / automation off / queued) are
  active-segment rows on /download-tasks/board, merged by created_at,
  counted, with approve / cancel actions; ?plan= deep links and legacy
  /downloads/plans routes redirect to the panel
- Download detail shows "why this download" (source, purpose, replaced
  version, reasons); raw evaluation in tech info
- Failed / cancelled / expired plans appear in the entity download
  history; rejected / revoked / expired approvals close draft plans
- New GET /acquisition-plans/{id}/why, POST /acquisition-plans/{id}/cancel
- User-facing copy no longer says 下载计划; glossary updated

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 01:15:25 +08:00
1 parent c0522f2531
commit a9147e647d
50 files changed
+1535 -348

No files matched your search

@@ -298,7 +298,7 @@ export function OptionsCard({ runId, view, waiting }: { runId: string; view: Opt
const RISK_LABEL: Record<RiskLevel, string> = { low: "低", medium: "中", high: "高" };
const PLAN_LABEL: Record<string, string> = {
acquisition_plan: "下载计划",
acquisition_plan: "下载",
organize_plan: "整理计划",
policy_revision: "策略版本",
};
@@ -189,6 +189,8 @@ describe("聚合:进度行用能力元数据的人话计数", () => {
expect(activitySummary([call({ capabilityId: "download.inspect" }), call({ capabilityId: "site.list" })])).toBe(
"查了 1 个下载任务和站点",
);
// DL-3:不再说"生成了 1 份下载计划"
expect(activitySummary([call({ capabilityId: "download.plan" })])).toBe("准备下载 1 个资源");
});
it("一回合的全部调用聚成一行(1 次也折叠),展开前看不到能力 ID", () => {
const activity: ActivityItem[] = [{ kind: "tool", key: "c1", call: call({ capabilityId: "site.list" }) }];
@@ -43,7 +43,7 @@ export function deniedReason(call: ToolCallView): { text: string; approvable: bo
// ── 进度行的人话计数 ──
type Group = { verb: string; nouns: { noun: string; unit: string | null; n: number }[] };
type Group = { verb: string; bare: boolean; nouns: { noun: string; unit: string | null; n: number }[] };
function activityOf(id: string) {
return (
@@ -73,7 +73,7 @@ export function activitySummary(calls: Pick<ToolCallView, "capabilityId" | "stat
if (c.status === "denied" || c.status === "failed" || authDenial(c)) continue;
let g = groups.find((x) => x.verb === a.verb);
if (!g) {
g = { verb: a.verb, nouns: [] };
g = { verb: a.verb, bare: !!a.bare, nouns: [] };
groups.push(g);
}
const n = g.nouns.find((x) => x.noun === a.noun);
@@ -81,7 +81,10 @@ export function activitySummary(calls: Pick<ToolCallView, "capabilityId" | "stat
else g.nouns.push({ noun: a.noun, unit: a.unit, n: 1 });
}
const text = (g: Group) =>
cjkJoin(`${g.verb}了`, joinNouns(g.nouns.map((x) => (x.unit ? `${x.n} ${x.unit}${x.noun}` : x.noun))));
cjkJoin(
g.bare ? g.verb : `${g.verb}了`,
joinNouns(g.nouns.map((x) => (x.unit ? `${x.n} ${x.unit}${x.noun}` : x.noun))),
);
if (groups.length > 4) return `${groups.slice(0, 3).map(text).join(",")}等 ${groups.length} 类操作`;
return groups.map(text).join(",");
}
+3 -3
View File
@@ -52,7 +52,7 @@ function script(goal: string, opts: AgentSessionOptions): Step[] {
const steps: AgentPlanStep[] = [
{ id: "s1", title: "查本地库存和收录版本", status: "running", note: null },
{ id: "s2", title: "在站点搜 4K 候选", status: "pending", note: null },
{ id: "s3", title: "选资源并生成下载计划", status: "pending", note: null },
{ id: "s3", title: "选资源并准备下载", status: "pending", note: null },
{ id: "s4", title: "提交下载", status: "pending", note: null },
];
let revision = 0;
@@ -324,7 +324,7 @@ function checkupSteps(goal: string, report: string): Step[] {
]);
}
/** 例 3:方案绑定下载计划(选定即批准),选了之后直接提交 */
/** 例 3:方案绑定计划(选定即批准),选了之后直接提交 */
const DUNE_PLAN = "00000000-0000-4000-8000-00000000b003";
const DUNE_DECISION = "00000000-0000-4000-8000-00000000d003";
function optionSteps(goal: string): Step[] {
@@ -338,7 +338,7 @@ function optionSteps(goal: string): Step[] {
revision: 1,
steps: [
{ id: "s1", title: "找 1080p 资源", status: "done", note: null },
{ id: "s2", title: "生成下载计划", status: "done", note: null },
{ id: "s2", title: "准备下载", status: "done", note: null },
{ id: "s3", title: "等你选方案", status: "running", note: null },
{ id: "s4", title: "提交下载", status: "pending", note: null },
],
@@ -56,7 +56,7 @@ describe("humanizeReport(汇报兜底)", () => {
names,
);
expect(r.text).toBe(
"检测站点 被拒;查看下载任务 无法调用(返回 参数不对);生成下载计划 返回 plan=null,拒绝原因 下载盘用量未知",
"检测站点 被拒;查看下载任务 无法调用(返回 参数不对);准备下载 返回 plan=null,拒绝原因 下载盘用量未知",
);
expect(r.text).not.toMatch(/site\.health_check|invalid_input|volume_state_unknown/);
expect(humanizeReport("被规则拒绝(unauthorized),未重试", names).text).toBe("被规则拒绝(需要你批准),未重试");