io worker 进程内按任务声明的 lane 分文件 / 网络 / 维护三条道,各自并发(缺省 2/4/1)、 各自排队;维护道在文件道或网络道有排队/运行时让路(连续 10 分钟放行一个),调度器内联的 清理/回填/保留期裁剪也走维护道。并发在 设置 → 自动化 → 高级 热生效,env 覆盖;旧 WORKER_CONCURRENCY 映射到网络道。运行状态页按道显示运行中/并发/排队。归属表写进 docs/runbook/dev.md,ci 与注册表逐项核对。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
110 lines
4.4 KiB
TypeScript
110 lines
4.4 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { TmdbClient } from "@umt/adapters";
|
|
import { FilesIdentifyRequest } from "@umt/contracts";
|
|
import { defineTask } from "@umt/tasks";
|
|
import { z } from "zod";
|
|
import type { AoOverrides } from "../acquisition-organize/index.ts";
|
|
import { createPorts } from "../acquisition-organize/ports.ts";
|
|
import { registerScheduledJob } from "../schedules/registry.ts";
|
|
import { SourcesRuntime } from "../sources-identity/runtime.ts";
|
|
import type { ServerContext, ServerModule } from "../types.ts";
|
|
import { defaultOptions, type FmDeps, type FmOptions } from "./common.ts";
|
|
import { IDENTIFY_REMOTE_TASK, runIdentifyRemote } from "./identify.ts";
|
|
import { registerFileRoutes } from "./routes.ts";
|
|
import { purgeTrash } from "./trash.ts";
|
|
|
|
/**
|
|
* W5-A 文件管理(/files,docs/plan/06-file-manager-spec.md):只浏览已登记根(库根 + 下载器路径映射),
|
|
* 行内业务状态、改名同步资产与路径绑定修订、移动/复制两段式 planId、整理走 planImport、回收站 + 引用检查、识别/探测/刮削。
|
|
* 复用 acquisition-organize 的服务导出(planImport / 路径守卫 / Move 授权 / H&R),不改其文件。
|
|
* 持久任务 purgeTrash:每日清理过期回收站项(FILES_TRASH_RETENTION_DAYS,默认 7 天);周期登记为定时任务 trash.purge。
|
|
*/
|
|
export interface FmOverrides {
|
|
ao?: AoOverrides;
|
|
options?: Partial<FmOptions>;
|
|
/** W9-H:TMDB 客户端工厂(测试注入);缺省按设置 metadata.tmdb* 取(与 sources-identity 同一实现) */
|
|
tmdb?: (() => TmdbClient | null) | undefined;
|
|
}
|
|
|
|
export function createFileManagerModule(overrides: FmOverrides = {}): ServerModule & { deps(): FmDeps } {
|
|
let deps: FmDeps | null = null;
|
|
let sources: SourcesRuntime | null = null;
|
|
const lazy = (ctx: ServerContext): FmDeps => {
|
|
if (!deps) {
|
|
const { options: aoOptions, ...aoRest } = overrides.ao ?? {};
|
|
deps = {
|
|
ctx,
|
|
ao: { ctx, ports: createPorts(ctx, { ...aoRest, ...(aoOptions ? { options: aoOptions } : {}) }) },
|
|
options: { ...defaultOptions(ctx), ...overrides.options },
|
|
tmdb: () => {
|
|
sources ??= new SourcesRuntime(ctx, overrides.tmdb ? { tmdb: overrides.tmdb } : {});
|
|
return sources.tmdb();
|
|
},
|
|
};
|
|
}
|
|
return deps;
|
|
};
|
|
return {
|
|
name: "file-manager",
|
|
deps() {
|
|
if (!deps) throw new Error("file-manager 尚未装配");
|
|
return deps;
|
|
},
|
|
registerRoutes(app, ctx) {
|
|
registerFileRoutes(app, lazy(ctx));
|
|
},
|
|
registerTasks(registry, ctx) {
|
|
registry.register(
|
|
defineTask({
|
|
name: "purgeTrash",
|
|
version: 1,
|
|
lane: "maintenance",
|
|
inputSchema: z.object({}).passthrough(),
|
|
retryPolicy: { maximumAttempts: 3, initialInterval: "5s", backoffCoefficient: 2, maximumInterval: "60s" },
|
|
handler: async (t) =>
|
|
t.step.run("purge", async () => {
|
|
const r = await purgeTrash(lazy(ctx), undefined, {});
|
|
return { purged: r.purged.length, failed: r.failed.length };
|
|
}),
|
|
}),
|
|
// S-7:/files/identify 的远端(TMDB)查询;只读,失败发 status=failed 的 identify.completed,不重试
|
|
defineTask({
|
|
name: IDENTIFY_REMOTE_TASK,
|
|
version: 1,
|
|
lane: "network",
|
|
inputSchema: z.object({ body: FilesIdentifyRequest, actorId: z.string() }),
|
|
retryPolicy: { maximumAttempts: 1 },
|
|
handler: async (t) =>
|
|
t.step.run("identify", async () => {
|
|
const r = await runIdentifyRemote(lazy(ctx), t.runId, t.input, t.abortSignal);
|
|
return { status: r.status, items: r.items.length };
|
|
}),
|
|
}),
|
|
);
|
|
},
|
|
start(ctx) {
|
|
const d = lazy(ctx);
|
|
if (!d.options.backgroundLoops || d.options.purgeIntervalMs <= 0) return;
|
|
registerScheduledJob(ctx, {
|
|
key: "trash.purge",
|
|
defaults: {
|
|
name: "回收站清理",
|
|
description: "清除过期的回收站项",
|
|
group: "maintain",
|
|
schedule: { type: "interval", everyMs: d.options.purgeIntervalMs },
|
|
},
|
|
run: async (c) => {
|
|
const r = await d.ctx.engine.enqueue(
|
|
"purgeTrash",
|
|
{},
|
|
{ idempotencyKey: `purgeTrash:${c.slot}`, traceId: randomUUID() },
|
|
);
|
|
return { summary: r.deduplicated ? "本周期已入队" : "已入队清理" };
|
|
},
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export const fileManagerModule = createFileManagerModule();
|