import { createWriteStream, mkdirSync, type WriteStream } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { query } from '@anthropic-ai/claude-agent-sdk'; import { isModelError, pickFallbackModel } from './models.js'; /** 转录目录:/transcripts */ export function transcriptDir(): string { return join(process.env.MAESTRO_DATA_DIR ?? join(homedir(), '.maestro'), 'transcripts'); } export interface CCOptions { prompt: string; cwd: string; model: string; runId: string; maxTurns: number; timeoutMs: number; allowedTools: string[]; permissionMode?: 'acceptEdits' | 'default'; } export interface CCResult { ok: boolean; finalText: string; // SDK result 消息的文本(失败时为 '') sessionId: string | null; transcriptRef: string; modelUsed: string; // 实际使用的模型(发生回退时为回退模型) fellBack: boolean; error?: string; } interface AttemptResult { ok: boolean; finalText: string; sessionId: string | null; error?: string; } /** 单次 headless CC 会话:流式消息逐行写 transcript,不抛错(失败折叠进 error) */ async function attempt(opts: CCOptions, model: string, out: WriteStream): Promise { let sessionId: string | null = null; let finalText = ''; let resultOk = false; let resultError: string | undefined; let sawResult = false; const abort = new AbortController(); const killer = setTimeout(() => abort.abort(new Error('执行超时')), opts.timeoutMs); try { const q = query({ prompt: opts.prompt, options: { cwd: opts.cwd, permissionMode: opts.permissionMode ?? 'default', maxTurns: opts.maxTurns, settingSources: ['project'], // 读项目 CLAUDE.md / settings,不读用户全局 abortController: abort, model, allowedTools: opts.allowedTools, }, }); for await (const message of q) { out.write(`${JSON.stringify(message)}\n`); const sid = (message as { session_id?: unknown }).session_id; if (typeof sid === 'string' && sid) sessionId = sid; if (message.type === 'result') { sawResult = true; if (message.subtype === 'success' && !message.is_error) { resultOk = true; const txt = (message as { result?: unknown }).result; if (typeof txt === 'string') finalText = txt; } else { const errs = 'errors' in message && Array.isArray(message.errors) ? message.errors.join('; ') : ''; resultError = `Claude Code 结束于 ${message.subtype}${errs ? `:${errs}` : ''}`; } } } if (!sawResult && !resultError) resultError = '未收到 result 消息(会话异常结束)'; } catch (e) { resultError = `Claude Code 执行异常:${(e as Error).message}`; resultOk = false; } finally { clearTimeout(killer); } if (!resultOk) return { ok: false, finalText: '', sessionId, error: resultError ?? '未知错误' }; return { ok: true, finalText, sessionId }; } /** * 跑一次 headless CC(runner / reviewer 公用)。 * 模型可用性兜底:失败且错误信息像模型不可用(not_found/invalid/permission 等)时, * 自动用回退链取一个 ≠ 原模型的模型在同一 run 内重试一次;回退记入 transcript 与 finalText。 */ export async function runClaude(opts: CCOptions): Promise { const dir = transcriptDir(); mkdirSync(dir, { recursive: true }); const transcriptRef = join(dir, `${opts.runId}.jsonl`); const out = createWriteStream(transcriptRef, { flags: 'a' }); try { let r = await attempt(opts, opts.model, out); let modelUsed = opts.model; let fellBack = false; if (!r.ok && r.error && isModelError(r.error)) { const fb = pickFallbackModel(opts.model); if (fb) { out.write(`${JSON.stringify({ type: 'maestro.model_fallback', from: opts.model, to: fb, reason: r.error })}\n`); r = await attempt(opts, fb, out); modelUsed = fb; fellBack = true; } } const note = fellBack && r.ok ? `\n\n[模型回退] 原模型 ${opts.model} 不可用,实际使用 ${modelUsed}` : ''; return { ok: r.ok, finalText: r.finalText + note, sessionId: r.sessionId, transcriptRef, modelUsed, fellBack, ...(r.error ? { error: fellBack ? `${r.error}(已回退至 ${modelUsed} 重试)` : r.error } : {}), }; } finally { await new Promise((resolve) => out.end(resolve)); } }