Files
curriculum-project-hub/hub/src/feishu/approval.ts
T
hongjr03 4736610cf3 feat: add interactive approval/confirmation cards
- sendApprovalCard: interactive card with configurable buttons
- resolveCard: patch card to show resolution state (green/red)
- ApprovalManager: register/resolve lifecycle with 5min timeout
- request_approval MCP tool: agent can ask user for confirmation
- Wire onCardAction in trigger handler and server startup
- Add unit tests (5) for card JSON, manager, and routing
2026-07-08 16:47:31 +08:00

55 lines
1.5 KiB
TypeScript

export interface ApprovalResult {
readonly action: string;
readonly resolvedBy: string;
}
export interface PendingApproval {
readonly messageId: string;
readonly chatId: string;
readonly resolve: (result: ApprovalResult) => void;
}
export interface ApprovalManagerOptions {
readonly timeoutMs?: number;
}
export class ApprovalManager {
private readonly timeoutMs: number;
private pending = new Map<string, PendingApproval>();
constructor(options: ApprovalManagerOptions = {}) {
this.timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
}
register(messageId: string, chatId: string): Promise<ApprovalResult> {
return new Promise<ApprovalResult>((resolve, reject) => {
const timeout = setTimeout(() => {
this.pending.delete(messageId);
reject(new Error(`approval timed out after ${this.timeoutMs}ms`));
}, this.timeoutMs);
const unref = (timeout as { unref?: () => void }).unref;
if (unref !== undefined) unref.call(timeout);
this.pending.set(messageId, {
messageId,
chatId,
resolve: (result) => {
clearTimeout(timeout);
resolve(result);
},
});
});
}
resolve(messageId: string, action: string, resolvedBy: string): void {
const pending = this.pending.get(messageId);
if (pending === undefined) return;
this.pending.delete(messageId);
pending.resolve({ action, resolvedBy });
}
hasPending(messageId: string): boolean {
return this.pending.has(messageId);
}
}