import type { Quote } from "../types.js"; export interface ToolConfirmationRequest { sessionId: string; toolCallId: string; toolName: string; args: unknown; quote?: Quote; confirmType?: string; payload?: unknown; expiresAt: number; } export interface ToolConfirmationDecision { approved: boolean; content?: string; reason?: string; } interface PendingConfirmation { resolve: (decision: ToolConfirmationDecision) => void; timer: NodeJS.Timeout; cleanup: () => void; } function keyOf(sessionId: string, toolCallId: string): string { return `${sessionId}:${toolCallId}`; } export class ToolConfirmationManager { private readonly pending = new Map(); constructor(private readonly timeoutMs: number) {} request( input: Omit, onRequest: (request: ToolConfirmationRequest) => void, signal?: AbortSignal, ): Promise { if (signal?.aborted) { return Promise.resolve({ approved: false, reason: "请求已中止" }); } const key = keyOf(input.sessionId, input.toolCallId); if (this.pending.has(key)) { return Promise.resolve({ approved: false, reason: "该工具调用已在等待确认" }); } const request: ToolConfirmationRequest = { ...input, expiresAt: Date.now() + this.timeoutMs, }; return new Promise((resolve) => { let settled = false; const finish = (decision: ToolConfirmationDecision): void => { if (settled) return; settled = true; clearTimeout(timer); signal?.removeEventListener("abort", onAbort); this.pending.delete(key); resolve(decision); }; const onAbort = (): void => finish({ approved: false, reason: "请求已中止" }); const timer = setTimeout( () => finish({ approved: false, reason: "等待用户确认超时" }), this.timeoutMs, ); timer.unref?.(); this.pending.set(key, { resolve: finish, timer, cleanup: () => finish({ approved: false, reason: "会话已结束" }), }); signal?.addEventListener("abort", onAbort, { once: true }); try { onRequest(request); } catch (error) { finish({ approved: false, reason: error instanceof Error ? error.message : String(error) }); } }); } resolve( sessionId: string, toolCallId: string, decision: boolean | ToolConfirmationDecision, reason?: string, ): boolean { const pending = this.pending.get(keyOf(sessionId, toolCallId)); if (!pending) return false; pending.resolve(typeof decision === "boolean" ? { approved: decision, reason } : decision); return true; } clearSession(sessionId: string): void { const prefix = `${sessionId}:`; for (const [key, pending] of this.pending) { if (key.startsWith(prefix)) pending.cleanup(); } } count(): number { return this.pending.size; } }