modules/{auth-settings,sources-identity,acquisition-organize,agent} placeholders listed in
SERVER_MODULES; ServerModule registers routes (under /api/v1, authenticated), tasks,
outbox routes, capabilities and background loops. bootstrap() builds ServerContext (db,
config, logger, events, engine, outbox writer, secrets placeholder, settings reader,
capability registry), fills unregistered tasks with stubs and rejects duplicate task or
outbox ownership. worker starts the OpenWorkflow worker, OutboxRelay and module start().
--role migrate also migrates the engine schema; api/worker connect without migrating.
tasks: TaskRegistry.has, migrateEngineSchema.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
83 lines
3.0 KiB
TypeScript
83 lines
3.0 KiB
TypeScript
import type { z } from "zod";
|
|
import { UnknownTaskError } from "./errors.ts";
|
|
import type { RetryPolicy, TaskContext } from "./types.ts";
|
|
|
|
export interface TaskDefinition<S extends z.ZodType = z.ZodType, O = unknown> {
|
|
name: string;
|
|
/** 正整数;不兼容改动 +1,并在 worker 上同时注册新旧版本直到旧 run 清空 */
|
|
version: number;
|
|
inputSchema: S;
|
|
/** 步骤默认重试策略(步骤可单独覆盖)。外部网络类建议 5 次 1s→60s,文件类 3 次 */
|
|
retryPolicy?: Partial<RetryPolicy>;
|
|
handler: (ctx: TaskContext<z.output<S>>) => Promise<O>;
|
|
}
|
|
|
|
export function defineTask<S extends z.ZodType, O>(def: TaskDefinition<S, O>): TaskDefinition<S, O> {
|
|
if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(def.name)) throw new Error(`invalid task name: ${def.name}`);
|
|
if (!Number.isInteger(def.version) || def.version < 1) throw new Error(`invalid task version: ${def.version}`);
|
|
return def;
|
|
}
|
|
|
|
export const engineVersion = (version: number) => `v${version}`;
|
|
export const parseEngineVersion = (v: string | null): number | null => {
|
|
const m = v ? /^v(\d+)$/.exec(v) : null;
|
|
return m ? Number(m[1]) : null;
|
|
};
|
|
|
|
export class TaskRegistry {
|
|
private readonly defs = new Map<string, Map<number, TaskDefinition>>();
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: 注册表按名存放异构的任务定义
|
|
register(...defs: TaskDefinition<any, any>[]): this {
|
|
for (const def of defs) {
|
|
let byVersion = this.defs.get(def.name);
|
|
if (!byVersion) {
|
|
byVersion = new Map();
|
|
this.defs.set(def.name, byVersion);
|
|
}
|
|
if (byVersion.has(def.version)) throw new Error(`task ${def.name}@${def.version} already registered`);
|
|
byVersion.set(def.version, def as TaskDefinition);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
get(name: string, version: number): TaskDefinition {
|
|
const def = this.defs.get(name)?.get(version);
|
|
if (!def) throw new UnknownTaskError(name, version);
|
|
return def;
|
|
}
|
|
|
|
/** 新 run 一律用最高版本 */
|
|
latest(name: string): TaskDefinition {
|
|
const byVersion = this.defs.get(name);
|
|
if (!byVersion || byVersion.size === 0) throw new UnknownTaskError(name);
|
|
return byVersion.get(Math.max(...byVersion.keys()))!;
|
|
}
|
|
|
|
/** 是否已注册该任务(任一版本) */
|
|
has(name: string): boolean {
|
|
return (this.defs.get(name)?.size ?? 0) > 0;
|
|
}
|
|
|
|
names(): string[] {
|
|
return [...this.defs.keys()].sort();
|
|
}
|
|
|
|
all(): TaskDefinition[] {
|
|
return [...this.defs.values()].flatMap((m) => [...m.values()]);
|
|
}
|
|
|
|
/** 按 StartWorkerOptions.versions 选择:"name@2" / "@1" / "name"(该任务全部版本) */
|
|
select(selectors?: string[]): TaskDefinition[] {
|
|
if (!selectors || selectors.length === 0) return this.all();
|
|
const picked = this.all().filter((d) =>
|
|
selectors.some((s) => {
|
|
const [name, ver] = s.split("@");
|
|
return (!name || name === d.name) && (ver === undefined || ver === "" || Number(ver) === d.version);
|
|
}),
|
|
);
|
|
if (picked.length === 0) throw new Error(`no task definitions match ${selectors.join(",")}`);
|
|
return picked;
|
|
}
|
|
}
|