feat(agent): add read-only download.list capability
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0023872b9b
commit
0f20c19b7c
6 files changed
+294
-7
No files matched your search
@@ -1,5 +1,12 @@
|
||||
import { CapabilityError, type CapabilityRegistry } from "@umt/agent";
|
||||
import { capabilitySpec } from "@umt/contracts";
|
||||
import {
|
||||
CapabilityError,
|
||||
type CapabilityRegistry,
|
||||
downloadStatusName,
|
||||
humanBytes,
|
||||
humanPercent,
|
||||
humanRelative,
|
||||
} from "@umt/agent";
|
||||
import { capabilitySpec, type DownloadStatus } from "@umt/contracts";
|
||||
import { sql } from "@umt/db";
|
||||
import { normalizeTitle, transition } from "@umt/domain";
|
||||
import { coveringGrant, type PlanBinding, planBinding } from "../agent/authorize.ts";
|
||||
@@ -10,7 +17,7 @@ import { type FrozenPlan, planImport } from "./services/organize.ts";
|
||||
import { errMessage, hashJson, jsonb, toApi, toApiList } from "./util.ts";
|
||||
|
||||
// 本模块领域的能力处理器(规格一律取 contracts):
|
||||
// inventory.query / downloader.list / download.plan / download.submit / organize.plan / organize.execute / policy.preview / policy.apply
|
||||
// inventory.query / downloader.list / download.list / download.plan / download.submit / organize.plan / organize.execute / policy.preview / policy.apply
|
||||
// 授权(grant/approval)、幂等键与操作账本由注册中心统一处理(modules/agent/capability-registry.ts);
|
||||
// 这里的处理器只调本模块服务,并保证自身幂等(崩溃后同幂等键重跑用于对账)。
|
||||
|
||||
@@ -210,6 +217,90 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () =>
|
||||
};
|
||||
});
|
||||
|
||||
// AG-5:下载任务列表(先列再看):按状态 / 作品 / 名称筛选,附按状态计数;大小、进度、时间给人话,不给原始字节数与 hash
|
||||
registry.register(capabilitySpec("download.list"), async (input) => {
|
||||
const db = deps().ctx.db;
|
||||
const now = deps().ports.now();
|
||||
const esc = (v: string) => v.replace(/[\\%_]/g, (m) => `\\${m}`);
|
||||
const q = input.query;
|
||||
const filtered = db
|
||||
.selectFrom("download_task as dt")
|
||||
.leftJoin("acquisition_plan as p", "p.id", "dt.acquisition_plan_id")
|
||||
.leftJoin("acquisition_slot as sl", "sl.id", "p.slot_id")
|
||||
.leftJoin("media_entity as e", (j) =>
|
||||
j.on((eb) => eb("e.id", "=", eb.fn.coalesce("dt.media_entity_id", "sl.media_entity_id"))),
|
||||
)
|
||||
.leftJoin("downloader_instance as i", "i.id", "dt.instance_id")
|
||||
.where("dt.removed_at", "is", null)
|
||||
.$if(!input.includeExternal, (b) => b.where("dt.managed", "=", true))
|
||||
.$if(!!input.mediaEntityId, (b) => b.where("e.id", "=", input.mediaEntityId!))
|
||||
.$if(!!q, (b) =>
|
||||
b.where((eb) =>
|
||||
eb.or([
|
||||
eb("dt.name", "ilike", `%${esc(q!)}%`),
|
||||
eb("e.title", "ilike", `%${esc(q!)}%`),
|
||||
eb("e.original_title", "ilike", `%${esc(q!)}%`),
|
||||
eb(sql`replace(e.title, ' ', '')`, "ilike", `%${esc(q!.replace(/\s+/g, ""))}%`),
|
||||
]),
|
||||
),
|
||||
);
|
||||
const [groups, rows] = await Promise.all([
|
||||
filtered
|
||||
.select(["dt.status", (eb) => eb.fn.countAll<number>().as("n")])
|
||||
.groupBy("dt.status")
|
||||
.execute(),
|
||||
filtered
|
||||
.$if(!!input.status?.length, (b) => b.where("dt.status", "in", input.status!))
|
||||
.select([
|
||||
"dt.id",
|
||||
"dt.name",
|
||||
"dt.status",
|
||||
"dt.progress",
|
||||
"dt.size_bytes",
|
||||
"dt.managed",
|
||||
"dt.last_progress_at",
|
||||
"dt.no_progress_since",
|
||||
"dt.completed_at",
|
||||
"dt.last_error",
|
||||
"e.title as title",
|
||||
"i.name as downloader",
|
||||
])
|
||||
.orderBy("dt.updated_at", "desc")
|
||||
.limit(input.limit)
|
||||
.execute(),
|
||||
]);
|
||||
const byStatus = groups
|
||||
.map((g) => ({ status: g.status as DownloadStatus, count: Number(g.n) }))
|
||||
.filter((g) => !input.status?.length || input.status.includes(g.status))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.map((g) => ({ ...g, label: downloadStatusName(g.status) }));
|
||||
const rel = (d: Date | null) => (d ? humanRelative(d.toISOString(), now) : null);
|
||||
const errText = (e: unknown): string | null => {
|
||||
if (!e || typeof e !== "object") return null;
|
||||
const m = (e as { message?: unknown }).message;
|
||||
return typeof m === "string" ? m.slice(0, 200) : null;
|
||||
};
|
||||
return {
|
||||
total: byStatus.reduce((n, g) => n + g.count, 0),
|
||||
byStatus,
|
||||
items: rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
title: r.title ?? null,
|
||||
status: r.status as DownloadStatus,
|
||||
statusLabel: downloadStatusName(r.status),
|
||||
progress: humanPercent(Number(r.progress)) ?? "0%",
|
||||
size: humanBytes(r.size_bytes === null ? null : Number(r.size_bytes)),
|
||||
downloader: r.downloader ?? null,
|
||||
managed: r.managed,
|
||||
lastProgress: rel(r.last_progress_at),
|
||||
noProgressSince: rel(r.no_progress_since),
|
||||
completed: rel(r.completed_at),
|
||||
error: errText(r.last_error),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// AG-3:下载任务详情(按 id 或 hash);关联计划 / 资源 / 业务任务 / 整理记录 / 待处理事项
|
||||
registry.register(capabilitySpec("download.inspect"), async (input) => {
|
||||
const db = deps().ctx.db;
|
||||
|
||||
Reference in new issue
Block a user