merge: maestro/tsk_zacmwH3chZpG [tsk_zacmwH3chZpG]
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { Complexity } from '../model/complexity.js';
|
||||
import { isComplexity } from '../model/complexity.js';
|
||||
import { runClaude } from './cc.js';
|
||||
import { pickClassifierModel } from './models.js';
|
||||
|
||||
/** AUTO 复杂度判定结果:三档之一 + 一句理由(供用户复核) */
|
||||
export interface Classification {
|
||||
complexity: Complexity;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类器函数签名(server 注入点;测试传 mock,生产传 classifyComplexity 封装)。
|
||||
* 永不抛错:一切失败折叠成兜底 medium。
|
||||
*/
|
||||
export type ClassifierFn = (title: string, description: string | undefined, cwd: string) => Promise<Classification>;
|
||||
|
||||
/** 解析失败 / 模型不可用时的兜底档 */
|
||||
const FALLBACK: Complexity = 'medium';
|
||||
const CLASSIFY_MAX_TURNS = 2;
|
||||
const CLASSIFY_TIMEOUT_MS = 60_000; // 60s:一次轻量文本判定,超时即兜底
|
||||
|
||||
/** 组装分类提示词:给出三档判据,要求只回 COMPLEXITY + REASON 两行。 */
|
||||
export function buildClassifyPrompt(title: string, description?: string): string {
|
||||
return [
|
||||
'你是任务复杂度分级助手。请根据下述任务,判定它属于 hard / medium / easy 中的哪一档。',
|
||||
'',
|
||||
'## 三档判据',
|
||||
'- easy:单文件或少量机械改动,无需设计、无未知项(如改文案、调参数、加日志)。',
|
||||
'- medium:需要先写改动方案、会跨几处文件,但范围清晰、无需拆解成多个子任务。',
|
||||
'- hard:需要拆解成多个子任务,跨模块/跨子系统,或存在明显未知项与设计取舍。',
|
||||
'',
|
||||
'## 任务',
|
||||
`标题:${title}`,
|
||||
`说明:${description?.trim() || '(无)'}`,
|
||||
'',
|
||||
'## 输出格式(严格两行,不要任何多余内容、不要使用工具)',
|
||||
'COMPLEXITY: <hard|medium|easy>',
|
||||
'REASON: <一句话理由>',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析模型输出为三档 + 理由:
|
||||
* - 取最后一个匹配 `COMPLEXITY: hard|medium|easy` 的值(大小写/空白宽容);
|
||||
* - REASON 取第一行匹配;
|
||||
* - 解析不到合法档位 → 兜底 medium(理由说明原因,绝不抛错)。
|
||||
*/
|
||||
export function parseClassification(text: string): Classification {
|
||||
let complexity: Complexity | null = null;
|
||||
const re = /COMPLEXITY:\s*(hard|medium|easy)/gi;
|
||||
for (let m = re.exec(text); m !== null; m = re.exec(text)) {
|
||||
complexity = m[1].toLowerCase() as Complexity;
|
||||
}
|
||||
const rm = text.match(/REASON:\s*(.+)/i);
|
||||
const reason = rm ? rm[1].trim() : '';
|
||||
|
||||
if (!complexity || !isComplexity(complexity)) {
|
||||
return {
|
||||
complexity: FALLBACK,
|
||||
reason: reason
|
||||
? `无法解析复杂度档位,兜底 ${FALLBACK}(模型原话:${reason})`
|
||||
: `无法解析模型判定,兜底 ${FALLBACK}`,
|
||||
};
|
||||
}
|
||||
return { complexity, reason: reason || '(模型未给出理由)' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 生产分类器:复用 cc.ts 起一次轻量 headless CC(sonnet 省额度),无工具、限轮限时。
|
||||
* 解析失败 / 超时 / 模型不可用 → 兜底 medium,永不抛错。
|
||||
*/
|
||||
export async function classifyComplexity(
|
||||
title: string,
|
||||
description: string | undefined,
|
||||
opts: { cwd: string },
|
||||
): Promise<Classification> {
|
||||
try {
|
||||
const cc = await runClaude({
|
||||
prompt: buildClassifyPrompt(title, description),
|
||||
cwd: opts.cwd,
|
||||
model: pickClassifierModel(),
|
||||
runId: `classify_${nanoid(10)}`,
|
||||
maxTurns: CLASSIFY_MAX_TURNS,
|
||||
timeoutMs: CLASSIFY_TIMEOUT_MS,
|
||||
permissionMode: 'default',
|
||||
allowedTools: [], // 纯文本判定,不需要任何工具
|
||||
});
|
||||
if (!cc.ok || !cc.finalText.trim()) {
|
||||
return { complexity: FALLBACK, reason: `模型未产出有效判定,兜底 ${FALLBACK}${cc.error ? `(${cc.error})` : ''}` };
|
||||
}
|
||||
return parseClassification(cc.finalText);
|
||||
} catch (e) {
|
||||
return { complexity: FALLBACK, reason: `分类调用异常,兜底 ${FALLBACK}:${(e as Error).message}` };
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,15 @@ export function resolvedExecutorModels(project: Project): Record<Complexity, str
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* AUTO 复杂度分类器用的模型:复用 executor easy 档(claude-sonnet,省额度),
|
||||
* env MAESTRO_MODEL_EASY 可覆盖。单次轻量调用,不走 project.model。
|
||||
*/
|
||||
export function pickClassifierModel(): string {
|
||||
const [env, fallback] = MODEL_TABLE.executor.easy;
|
||||
return process.env[env]?.trim() || fallback;
|
||||
}
|
||||
|
||||
/** 错误信息是否像“模型不可用”(not_found / invalid / permission 等模式 + 提到 model) */
|
||||
export function isModelError(msg: string): boolean {
|
||||
if (!/model/i.test(msg)) return false;
|
||||
|
||||
Reference in New Issue
Block a user