import { createHash } from "node:crypto"; import type { ClusterConfig, ClusterLevel, LearningConfig, LevelConfig, ReviewerConfig } from "./types.ts"; const LEVELS: ClusterLevel[] = ["high", "medium", "low"]; const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]); const DEFAULT_REVIEW_TOOLS = ["read", "grep", "find", "ls"]; const DEFAULT_LEARNING_CONFIG = { enabled: true, taskTypeUpgradeThreshold: 2, historyRetentionDays: 30, } as const; export function workerLevelConfigSignature(config: ClusterConfig): string { const levels = Object.fromEntries( (["low", "medium", "high"] as const).map((level) => { const value = config.levels[level]; return [ level, { model: value.model, thinkingLevel: value.thinkingLevel ?? null, tools: value.tools ? [...value.tools].sort() : null, timeoutMs: value.timeoutMs ?? 900_000, }, ]; }), ); return createHash("sha256").update(JSON.stringify(levels)).digest("hex"); } export function createDefaultClusterConfig(model: string): ClusterConfig { return { levels: { high: { model, thinkingLevel: "high", tools: ["read", "grep", "find", "ls", "bash", "edit", "write"], timeoutMs: 1_800_000 }, medium: { model, thinkingLevel: "medium", tools: ["read", "grep", "find", "ls", "bash", "edit", "write"], timeoutMs: 1_200_000 }, low: { model, thinkingLevel: "low", tools: ["read", "grep", "find", "ls", "bash", "edit", "write"], timeoutMs: 900_000 }, }, reviewer: { thinkingLevel: "medium", tools: ["read", "grep", "find", "ls"], timeoutMs: 180_000 }, maxConcurrency: 4, maxTasks: 8, maxRetriesPerLevel: 1, learning: { ...DEFAULT_LEARNING_CONFIG }, }; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function positiveInteger(value: unknown, fallback: number): number { return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback; } function nonNegativeInteger(value: unknown, fallback: number): number { return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : fallback; } function parseLevelConfig(value: unknown, level: ClusterLevel): LevelConfig { if (!isRecord(value) || typeof value.model !== "string" || value.model.trim() === "") { throw new Error(`配置缺少 ${level}.model`); } const thinkingLevel = value.thinkingLevel; if (thinkingLevel !== undefined && (typeof thinkingLevel !== "string" || !THINKING_LEVELS.has(thinkingLevel))) { throw new Error(`${level}.thinkingLevel 不是有效的 Pi thinking level`); } const tools = value.tools; if (tools !== undefined && (!Array.isArray(tools) || tools.some((tool) => typeof tool !== "string"))) { throw new Error(`${level}.tools 必须是字符串数组`); } return { model: value.model, thinkingLevel: thinkingLevel as LevelConfig["thinkingLevel"], tools: tools as string[] | undefined, timeoutMs: positiveInteger(value.timeoutMs, 900_000), }; } function parseLearningConfig(value: unknown): LearningConfig { if (value === undefined) return { ...DEFAULT_LEARNING_CONFIG }; if (!isRecord(value)) throw new Error("learning 必须是对象"); if (typeof value.enabled !== "boolean") throw new Error("learning.enabled 必须是布尔值"); if (typeof value.taskTypeUpgradeThreshold !== "number" || !Number.isInteger(value.taskTypeUpgradeThreshold) || value.taskTypeUpgradeThreshold < 1) { throw new Error("learning.taskTypeUpgradeThreshold 必须是正整数"); } if (typeof value.historyRetentionDays !== "number" || !Number.isInteger(value.historyRetentionDays) || value.historyRetentionDays < 1) { throw new Error("learning.historyRetentionDays 必须是正整数"); } return { enabled: value.enabled, taskTypeUpgradeThreshold: value.taskTypeUpgradeThreshold, historyRetentionDays: value.historyRetentionDays, }; } function parseReviewerConfig(value: unknown): ReviewerConfig { if (value === undefined) { return { tools: DEFAULT_REVIEW_TOOLS, timeoutMs: 180_000 }; } if (!isRecord(value)) throw new Error("reviewer 必须是对象"); const thinkingLevel = value.thinkingLevel; if (thinkingLevel !== undefined && (typeof thinkingLevel !== "string" || !THINKING_LEVELS.has(thinkingLevel))) { throw new Error("reviewer.thinkingLevel 不是有效的 Pi thinking level"); } const tools = value.tools; if (tools !== undefined && (!Array.isArray(tools) || tools.some((tool) => typeof tool !== "string"))) { throw new Error("reviewer.tools 必须是字符串数组"); } const reviewerTools = (tools as string[] | undefined) ?? DEFAULT_REVIEW_TOOLS; if (reviewerTools.some((tool) => !DEFAULT_REVIEW_TOOLS.includes(tool))) { throw new Error("reviewer.tools 只能使用只读工具:read、grep、find、ls"); } const model = value.model; if (model !== undefined && (typeof model !== "string" || model.trim() === "")) { throw new Error("reviewer.model 必须是非空字符串"); } return { model: model as string | undefined, thinkingLevel: thinkingLevel as ReviewerConfig["thinkingLevel"], tools: reviewerTools, timeoutMs: positiveInteger(value.timeoutMs, 180_000), }; } export function parseClusterConfig(raw: unknown): ClusterConfig { if (!isRecord(raw)) throw new Error("集群配置必须是 JSON 对象"); const rawLevels = raw.levels; if (!isRecord(rawLevels)) throw new Error("配置缺少 levels 对象"); const levels = {} as Record; for (const level of LEVELS) levels[level] = parseLevelConfig(rawLevels[level], level); return { levels, reviewer: parseReviewerConfig(raw.reviewer), maxConcurrency: positiveInteger(raw.maxConcurrency, 4), maxTasks: positiveInteger(raw.maxTasks, 8), maxRetriesPerLevel: nonNegativeInteger(raw.maxRetriesPerLevel, 1), learning: parseLearningConfig(raw.learning), }; }