feat(sources,organize): site agent capabilities and organize.minSizeBytes setting (W10-pre2)

- site.list / site.health_check / site.refresh_login / site.user_data capabilities
  over the W9-A site center services; outputs whitelisted (no secret refs,
  cookies, passwords, login names); execute kinds need automation grant.
  IDs use snake_case (CapabilityId regex); registry count 17 -> 21.
- settings.organize.minSizeBytes (default 50 MiB); file-manager identify /
  organize and planImport take request > setting > port default.
- tests and runbook notes (sites, files).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 16:02:30 +08:00
1 parent c08d8ce62a
commit 81bf010e3e
18 files changed
+563 -15

No files matched your search

+6 -1
View File
@@ -144,7 +144,12 @@ export async function createFmHarness(
const out: Record<string, unknown> = {};
for (const k of Object.keys(DEFAULT_SETTINGS) as SettingsKey[]) {
const v = stored.get(k);
out[k] = GlobalSettings.shape[k].parse({ ...DEFAULT_SETTINGS[k], ...((v as object) ?? {}) });
// 整理最小体积缺省跟随端口注入的 minVideoBytes(测试用小文件)
const base =
k === "organize"
? { ...DEFAULT_SETTINGS.organize, minSizeBytes: overrides.ao?.options?.minVideoBytes ?? 10 }
: DEFAULT_SETTINGS[k];
out[k] = GlobalSettings.shape[k].parse({ ...base, ...((v as object) ?? {}) });
}
return out as GlobalSettings;
},
@@ -53,7 +53,7 @@ describe.skipIf(skip)("files 识别 / 整理 / 探测 / 刮削", () => {
const r = await h.api(
"POST",
"/files/identify",
{ items: [{ root: h.dlRoot, path: "Some.Show.S01.1080p.WEB-DL" }] },
{ items: [{ root: h.dlRoot, path: "Some.Show.S01.1080p.WEB-DL" }], minSizeBytes: 0 },
READ_KEY,
);
expect(r.status).toBe(200);
@@ -72,6 +72,7 @@ describe.skipIf(skip)("files 识别 / 整理 / 探测 / 刮削", () => {
it("identify:远端候选(catalog.lookup)", async () => {
const r = await h.api("POST", "/files/identify", {
items: [{ root: h.dlRoot, path: "Remote.Film.2019.1080p.mkv" }],
minSizeBytes: 0,
});
expect(r.status).toBe(200);
const c = r.body.data.items[0].candidates;
@@ -82,7 +83,12 @@ describe.skipIf(skip)("files 识别 / 整理 / 探测 / 刮削", () => {
await mkdir(join(h.dl, "many"), { recursive: true });
const names = Array.from({ length: 23 }, (_, i) => `Distinct Title ${String.fromCharCode(65 + i)}.2019.1080p.mkv`);
for (const n of names) await writeFile(join(h.dl, "many", n), "x");
const r = await h.api("POST", "/files/identify", { items: [{ root: h.dlRoot, path: "many" }] }, READ_KEY);
const r = await h.api(
"POST",
"/files/identify",
{ items: [{ root: h.dlRoot, path: "many" }], minSizeBytes: 0 },
READ_KEY,
);
expect(r.status).toBe(200);
expect(r.body.data.items).toHaveLength(23);
const capped = r.body.data.items.filter((i: { warnings: string[] }) => i.warnings.some((w) => w.includes("上限")));
@@ -0,0 +1,73 @@
import { writeFile } from "node:fs/promises";
import { basename, join } from "node:path";
import { DEFAULT_SETTINGS, GlobalSettings } from "@umt/contracts";
import { sql } from "@umt/db";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createFmHarness, EXEC_KEY, type FmHarness, key, pgAvailable, READ_KEY, seedEntity } from "./harness.ts";
// W10-pre2:settings.organize.minSizeBytes(缺省 50 MiB)是识别与整理(planImport)的默认最小体积;请求 / 向导给的值优先。
const skip = !(await pgAvailable());
describe("settings.organize.minSizeBytes 契约", () => {
it("缺省 50 MiB;非负整数;旧的 organize 设置补缺省后仍合法", () => {
expect(DEFAULT_SETTINGS.organize.minSizeBytes).toBe(50 * 1024 * 1024);
expect(GlobalSettings.shape.organize.safeParse({ ...DEFAULT_SETTINGS.organize, minSizeBytes: -1 }).success).toBe(
false,
);
expect(
GlobalSettings.shape.organize.parse({ ...DEFAULT_SETTINGS.organize, defaultMode: "move" }).minSizeBytes,
).toBe(50 * 1024 * 1024);
});
});
describe.skipIf(skip)("最小体积读设置(W10-pre2)", () => {
let h: FmHarness;
let film: string;
const setMin = (n: number) =>
sql`insert into settings (key, value) values ('organize', ${JSON.stringify({ minSizeBytes: n })}::jsonb)
on conflict (key) do update set value = excluded.value`.execute(h.db);
beforeAll(async () => {
h = await createFmHarness("minsize");
film = await seedEntity(h.db, { kind: "movie", title: "Size Film", year: 2022 });
await writeFile(join(h.dl, "Size.Film.2022.2160p.BluRay.x265-GRP.mkv"), "x".repeat(64));
await writeFile(join(h.dl, "Size.Film.2022.1080p.small.mkv"), "0123456789ab");
});
afterAll(async () => {
await h?.close();
});
const items = () => [
{ root: h.dlRoot, path: "Size.Film.2022.2160p.BluRay.x265-GRP.mkv", mediaEntityId: film },
{ root: h.dlRoot, path: "Size.Film.2022.1080p.small.mkv", mediaEntityId: film },
];
const organize = (body: Record<string, unknown>) =>
h.api("POST", "/files/organize", { idempotencyKey: key(), action: "copy", dryRun: true, ...body }, EXEC_KEY);
it("整理:不给 minSizeBytes 时用设置值;请求给的值优先", async () => {
await setMin(32);
const r = await organize({ items: items() });
expect(r.status).toBe(200);
const g = r.body.data.groups[0];
expect(g.minSizeBytes).toBe(32);
expect(g.files.map((f: { source: string }) => basename(f.source))).toEqual([
"Size.Film.2022.2160p.BluRay.x265-GRP.mkv",
]);
expect(g.excluded).toContainEqual(expect.objectContaining({ size: 12, reason: "below_min_size" }));
const over = await organize({ items: items(), minSizeBytes: 0 });
expect(over.body.data.groups[0].excluded).toEqual([]);
expect(over.body.data.groups[0].files).toHaveLength(2);
});
it("识别:不给 minSizeBytes 时按设置跳过小文件;请求 0 不过滤", async () => {
await setMin(32);
const body = { items: items().map(({ root, path }) => ({ root, path })) };
const r = await h.api("POST", "/files/identify", body, READ_KEY);
expect(r.status).toBe(200);
expect(r.body.data.excludedBySize).toBe(1);
const r0 = await h.api("POST", "/files/identify", { ...body, minSizeBytes: 0 }, READ_KEY);
expect(r0.body.data.excludedBySize).toBe(0);
});
});