export interface PromptDispatchInput { cwd: string; runId: string; phase: string; userText: string; now?: number; holdMs?: number; } export interface PromptDispatchDecision { admitted: boolean; key: string; fingerprint: string; reason?: string; } const DEFAULT_HOLD_MS = 3000; const recentDispatches = new Map(); export function admitPromptDispatch(input: PromptDispatchInput): PromptDispatchDecision { const now = input.now ?? Date.now(); const holdMs = input.holdMs ?? DEFAULT_HOLD_MS; pruneExpiredDispatches(now, holdMs); const key = dispatchKey(input); const fingerprint = stableHash([input.cwd, input.runId, input.phase, normalizeText(input.userText)].join("\n")); const previous = recentDispatches.get(key); if (previous?.fingerprint === fingerprint && now - previous.admittedAt <= holdMs) { return { admitted: false, key, fingerprint, reason: `重复内部 workflow prompt 已在 ${now - previous.admittedAt}ms 内登记,拒绝再次注入。`, }; } recentDispatches.set(key, { fingerprint, admittedAt: now }); return { admitted: true, key, fingerprint }; } export function resetPromptDispatchGateForTests(): void { recentDispatches.clear(); } function dispatchKey(input: Pick): string { return [normalizePath(input.cwd), input.runId.trim(), input.phase.trim()].join("::"); } function pruneExpiredDispatches(now: number, holdMs: number): void { for (const [key, value] of recentDispatches) { if (now - value.admittedAt > holdMs) recentDispatches.delete(key); } } function normalizePath(path: string): string { return path.trim().replace(/\\/g, "/").toLowerCase(); } function normalizeText(text: string): string { return text.trim().replace(/\s+/g, " "); } function stableHash(text: string): string { let hash = 2166136261; for (let index = 0; index < text.length; index++) { hash ^= text.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0).toString(16).padStart(8, "0"); }