feat(settings): 用户级全局默认配置(per-user 落库 + 新建项目套默认)

新增 settings 表(user_id PK,data JSON,单本地用户 'local')+ store.getSettings/putSettings
(内置默认=核心策略子集)+ GET/PUT /api/settings 端点;POST /api/projects 创建时以用户
默认填充未显式传的字段(body 优先,自动放行/预算经 patchProject 套用)。前端弹框另提交。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 09:19:18 +08:00
parent a50e45baba
commit f5ea32a600
4 changed files with 83 additions and 7 deletions
+34 -6
View File
@@ -1,6 +1,6 @@
import Fastify, { type FastifyInstance } from 'fastify';
import { WebSocketServer, type WebSocket } from 'ws';
import { Store, StoreError, type ActiveRun, type PatchProjectInput, type PatchTaskInput } from '../store/index.js';
import { Store, StoreError, type ActiveRun, type PatchProjectInput, type PatchTaskInput, type UserSettings } from '../store/index.js';
import type { Complexity } from '../model/complexity.js';
import { isComplexity } from '../model/complexity.js';
import type { TaskStatus } from '../model/status.js';
@@ -64,14 +64,25 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
app.post('/api/projects', (req) => {
const b = req.body as Record<string, unknown>;
if (!b?.name || !b?.repoPath) throw new StoreError('name 与 repoPath 必填');
return projectOut(store.createProject({
// 用户级全局默认(核心策略子集):body 显式给则用 body,否则套默认
const s = store.getSettings();
const pick = <T>(key: string, fallback: T): T => (b[key] !== undefined ? (b[key] as T) : fallback);
const created = store.createProject({
name: String(b.name), repoPath: String(b.repoPath),
defaultBranch: b.defaultBranch ? String(b.defaultBranch) : undefined,
verifyCmd: b.verifyCmd === undefined ? undefined : (b.verifyCmd === null ? null : String(b.verifyCmd)),
autonomy: b.autonomy as never, model: b.model === undefined ? undefined : (b.model === null ? null : String(b.model)),
concurrency: b.concurrency === undefined ? undefined : Number(b.concurrency),
maxRetries: b.maxRetries === undefined ? undefined : Number(b.maxRetries),
timeoutMs: b.timeoutMs === undefined ? undefined : Number(b.timeoutMs),
autonomy: pick('autonomy', s.autonomy) as never,
model: b.model !== undefined ? (b.model === null ? null : String(b.model)) : s.model,
concurrency: Number(pick('concurrency', s.concurrency)),
maxRetries: Number(pick('maxRetries', s.maxRetries)),
timeoutMs: Number(pick('timeoutMs', s.timeoutMs)),
});
// createProject 不接受的默认字段(自动放行 / 预算)经 patchProject 套用
return projectOut(store.patchProject(created.id, {
autoApprovePlan: Boolean(pick('autoApprovePlan', s.autoApprovePlan)),
autoApproveExec: Boolean(pick('autoApproveExec', s.autoApproveExec)),
budgetUsd: ((): number | null => { const v = pick<number | null>('budgetUsd', s.budgetUsd); return v === null ? null : Number(v); })(),
budgetPeriod: pick('budgetPeriod', s.budgetPeriod) as 'day' | 'month',
}));
});
@@ -112,6 +123,23 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
return store.reorderProjects(b.order.map(String)).map(projectOut);
});
// ---------- 用户级全局默认配置(per-user,新建项目默认值来源)----------
app.get('/api/settings', () => store.getSettings());
app.put('/api/settings', (req) => {
const b = (req.body ?? {}) as Record<string, unknown>;
const patch: Partial<UserSettings> = {};
if (b.autonomy !== undefined) patch.autonomy = b.autonomy as Autonomy;
if (b.concurrency !== undefined) patch.concurrency = Number(b.concurrency);
if (b.maxRetries !== undefined) patch.maxRetries = Number(b.maxRetries);
if (b.timeoutMs !== undefined) patch.timeoutMs = Number(b.timeoutMs);
if (b.autoApprovePlan !== undefined) patch.autoApprovePlan = Boolean(b.autoApprovePlan);
if (b.autoApproveExec !== undefined) patch.autoApproveExec = Boolean(b.autoApproveExec);
if (b.budgetUsd !== undefined) patch.budgetUsd = b.budgetUsd === null || b.budgetUsd === '' ? null : Number(b.budgetUsd);
if (b.budgetPeriod !== undefined) patch.budgetPeriod = b.budgetPeriod as 'day' | 'month';
if (b.model !== undefined) patch.model = b.model === null || b.model === '' ? null : String(b.model);
return store.putSettings(patch);
});
// 彻底删除项目(连带 tasks/runs/approvals/events,不可恢复)
app.delete('/api/projects/:id', (req) => {
const { id } = req.params as { id: string };