Files
maestro/src/executor/cc.ts
T
wangjia f18db021c3 feat: Phase2 完整管线——score 调度 + 自动复审 + 模型分级 + 归档与详情
调度:
- model/scoring.ts: score = 自身分(P0=3/P1=2/P2=1) + 已完成依赖分(链条惯性)
  + 等待解锁的 blocked 任务分(解锁加权),编排器与 nextExecutable 同一打分
- createTask 校验 deps 存在且同项目(依赖图天然无环)
- daemon 重启中断自愈: executing 任务标 failed run 后重新入队(reconcileInterrupted)

执行管线:
- executor/cc.ts: 公共 headless CC 执行器(转录/超时/模型回退重试)
- executor/reviewer.ts: 执行后自动复审(只读 CC 审 diff),固定模板 summary
  (做了什么/怎么做/测试/CodeReview/安全Review/结论) + VERDICT 解析
- executor/models.ts: 按复杂度选模型(easy→sonnet/medium→opus/hard→fable5),
  env 可覆盖、project.model 最优先、不可用自动回退链
- runner: 测试/构建命令白名单(npm/go/shellcheck/make/pytest),prompt 要求实跑测试
- TaskResult 加 summary/verdict; RunKind 加 reviewer
- 容器收口: 已拆解 Hard 子任务全 done → 容器自动 done(afterDone 逐级向上)

看板:
- 五徽章组(待审批/待执行/执行中/被阻塞/总量,hover 展开,均不含已完成)
- 归档区: 深度1整树完成沉底,时间倒序分页(10/20/50/100 chip 选择)
- 归档详情对话框: 全属性/执行历史与时长/审批记录/状态流转时间线(含相关人或事)
- Agent 面板显示调度模式 + 各复杂度实际模型
- 结果闸展示复审 summary + 建议通过/拒绝徽章
- 筛选修复(组选与单选分离、已拆解移出进行中)、同步按钮收进配置面板、
  保存配置自动收起、预览全宽、被依赖阻塞→被阻塞
- API: GET /api/tasks/:id/events(任务级事件时间线)、/api/agents 带 scheduling/models

测试: 49/49(新增 scoring/复审/模型/容器收口/deps 校验/中断恢复)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:46:37 +08:00

133 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
/** 转录目录:<MAESTRO_DATA_DIR 或 ~/.maestro>/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<AttemptResult> {
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 CCrunner / reviewer 公用)。
* 模型可用性兜底:失败且错误信息像模型不可用(not_found/invalid/permission 等)时,
* 自动用回退链取一个 ≠ 原模型的模型在同一 run 内重试一次;回退记入 transcript 与 finalText。
*/
export async function runClaude(opts: CCOptions): Promise<CCResult> {
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<void>((resolve) => out.end(resolve));
}
}