feat(complexity): 新增 AUTO·由模型判断 复杂度档位 (tsk_zacmwH3chZpG)

新建任务时除手选 hard/medium/easy 外,新增 AUTO 档:先以 medium 落库
占位,建任务后异步用 sonnet 起一次轻量分类调用回填三档结果并记录理由,
不阻塞建任务返回;模型不可用/解析失败一律兜底 medium,绝不报错。

- web 看板:复杂度 seg 增加第四档 AUTO(默认选中,cyan 样式),提交 complexity='auto'
- src/api/server.ts:POST tasks 接受 'auto' → medium 占位 + 异步 classify 回填(可注入 classify 测试)
- src/executor/classify.ts(新增):buildClassifyPrompt/parseClassification/classifyComplexity
  复用 cc.ts/models.ts,sonnet 省额度,无工具、限轮限时
- src/executor/models.ts:pickClassifierModel(复用 executor easy 档 + MAESTRO_MODEL_EASY 覆盖)
- src/store/store.ts:applyAutoComplexity 回填复杂度+重置状态+理由入产出字段+广播 task.updated
- test/classify.test.ts:解析映射/兜底、prompt、回填、建任务集成(13 例)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 10:46:37 +08:00
parent 872354cf7c
commit 13572a5416
7 changed files with 348 additions and 4 deletions
+17 -3
View File
@@ -7,6 +7,7 @@ import type { TaskStatus } from '../model/status.js';
import type { Project, Autonomy } from '../model/types.js';
import { syncProject, hasTodoJson } from '../sync/todo-sync.js';
import { resolvedExecutorModels } from '../executor/models.js';
import { classifyComplexity, type ClassifierFn } from '../executor/classify.js';
import { mergeBranch } from '../executor/merge.js';
import { git, removeWorktree } from '../executor/worktree.js';
import { resolveLogo, LOGO_MIME } from './logo.js';
@@ -23,6 +24,8 @@ export interface ApiOptions {
logger?: boolean;
/** Claude 订阅额度查询(测试注入;默认直连 OAuth usage API60s 缓存) */
getUsage?: () => Promise<UsageInfo | null>;
/** AUTO 复杂度分类器(测试注入;默认 classifyComplexity,用 sonnet 起一次轻量调用) */
classify?: ClassifierFn;
}
/**
@@ -31,6 +34,7 @@ export interface ApiOptions {
*/
export function buildServer(opts: ApiOptions): FastifyInstance {
const { store } = opts;
const classify = opts.classify ?? ((title, description, cwd) => classifyComplexity(title, description, { cwd }));
const app = Fastify({ logger: opts.logger ?? false });
// Store 错误 → 400(业务校验),其余 → 500
@@ -152,13 +156,23 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
const { id } = req.params as { id: string };
const b = req.body as Record<string, unknown>;
if (!b?.title) throw new StoreError('title 必填');
if (!isComplexity(b.complexity)) throw new StoreError('complexity 必须是 hard|medium|easy');
return store.createTask({
projectId: id, title: String(b.title), complexity: b.complexity as Complexity,
// 'auto' = 由模型判定:先以 medium 落库占位,建任务后异步回填(不阻塞返回)
const auto = b.complexity === 'auto';
if (!auto && !isComplexity(b.complexity)) throw new StoreError('complexity 必须是 auto|hard|medium|easy');
const task = store.createTask({
projectId: id, title: String(b.title), complexity: auto ? 'medium' : (b.complexity as Complexity),
parentId: b.parentId ? String(b.parentId) : null,
priority: b.priority === undefined ? undefined : Number(b.priority),
deps: Array.isArray(b.deps) ? (b.deps as string[]) : undefined,
});
if (auto) {
// 异步分类回填:classifyComplexity 自身永不抛错(失败兜底 medium),外层再兜一层防御
const cwd = store.getProject(id)?.repoPath ?? process.cwd();
void classify(task.title, undefined, cwd)
.then(({ complexity, reason }) => store.applyAutoComplexity(task.id, complexity, reason))
.catch((e: unknown) => app.log.error(`任务 ${task.id} AUTO 复杂度分类失败(保留占位 medium):${(e as Error).message}`));
}
return task;
});
app.get('/api/tasks/:id', (req) => {