diff --git a/web/app.js b/web/app.js index e0f64ea..bebedc6 100644 --- a/web/app.js +++ b/web/app.js @@ -24,6 +24,15 @@ const I18N = { projLogo: 'Logo', logoPh: '图片 URL 或仓库内相对路径;留空=自动', verifyCmd: '校验命令', verifyPh: 'npm test · go test ./...(可空)', model: '项目 Model', modelPh: 'claude-opus-4-5(可空,留空用全局默认)', + cfgGates: '硬闸 / 自动放行', cfgChecks: '分项检查(lint / typecheck / build)', + cfgLint: 'lint 命令', cfgTypecheck: 'typecheck 命令', cfgBuild: 'build 命令', cfgCmdPh: '留空=跳过此项', + cfgAutoPlan: '自动放行 plan(全 easy 子任务跳过 plan_review)', + cfgAutoExec: '自动放行 exec(双复审 approve 后自动合并)', + cfgAgentRules: '项目 Agent 规范(注入执行/复审 prompt)', cfgAgentRulesPh: '项目专属执行约定、风格、禁忌……(可空)', + cfgBudget: '成本预算', cfgBudgetPh: 'USD 上限(可空=不限)', cfgBudgetDay: '按天', cfgBudgetMonth: '按月', + cfgModels: '角色模型(留空=默认 opus-4.8;可填模型 id 或 {"easy":..,"hard":..} JSON)', + cfgMdlExecutor: 'executor', cfgMdlPlanner: 'planner', cfgMdlReviewer: 'reviewer', cfgMdlConflict: 'conflict', + cfgModelsBad: 'models 角色「{role}」JSON 格式不对', autonomy: { manual: 'manual · 手动', 'auto-easy': 'auto-easy · 自动执行 Easy', 'auto-approved': 'auto-approved · 自动执行已批准' }, autonomyShort: { manual: '手动', 'auto-easy': '自动 · Easy', 'auto-approved': '自动 · 已批准' }, agentSection: 'Agent 执行', agentEmpty: '无 agent 在执行', running: 'RUNNING', since: '开始', @@ -157,6 +166,15 @@ const I18N = { projLogo: 'Logo', logoPh: 'Image URL or repo-relative path; empty = auto', verifyCmd: 'Verify command', verifyPh: 'npm test · go test ./... (optional)', model: 'Project Model', modelPh: 'claude-opus-4-5 (optional, empty = global default)', + cfgGates: 'Gates / auto-approve', cfgChecks: 'Per-step checks (lint / typecheck / build)', + cfgLint: 'lint command', cfgTypecheck: 'typecheck command', cfgBuild: 'build command', cfgCmdPh: 'empty = skip', + cfgAutoPlan: 'Auto-approve plan (skip plan_review when all-easy)', + cfgAutoExec: 'Auto-approve exec (auto-merge after dual review approves)', + cfgAgentRules: 'Project agent rules (injected into exec/review prompts)', cfgAgentRulesPh: 'Project-specific conventions, style, taboos… (optional)', + cfgBudget: 'Cost budget', cfgBudgetPh: 'USD cap (empty = unlimited)', cfgBudgetDay: 'per day', cfgBudgetMonth: 'per month', + cfgModels: 'Per-role models (empty = default opus-4.8; model id or {"easy":..,"hard":..} JSON)', + cfgMdlExecutor: 'executor', cfgMdlPlanner: 'planner', cfgMdlReviewer: 'reviewer', cfgMdlConflict: 'conflict', + cfgModelsBad: 'models role "{role}" has invalid JSON', autonomy: { manual: 'manual · manual', 'auto-easy': 'auto-easy · Auto Easy', 'auto-approved': 'auto-approved · Auto approved' }, autonomyShort: { manual: 'Manual', 'auto-easy': 'Auto · Easy', 'auto-approved': 'Auto · Approved' }, agentSection: 'Agent runs', agentEmpty: 'No agents running', running: 'RUNNING', since: '', @@ -1998,6 +2016,25 @@ document.addEventListener('click', (ev) => { $('#cfgLogo').value = p.logo ?? ''; $('#cfgVerifyCmd').value = p.verifyCmd ?? ''; $('#cfgModel').value = p.model ?? ''; + // 分项检查(checks 是 JSON 文本) + let checks = {}; + try { checks = p.checks ? JSON.parse(p.checks) : {}; } catch { checks = {}; } + $('#cfgLint').value = checks.lint ?? ''; + $('#cfgTypecheck').value = checks.typecheck ?? ''; + $('#cfgBuild').value = checks.build ?? ''; + // 自动放行 + $('#cfgAutoPlan').checked = !!p.autoApprovePlan; + $('#cfgAutoExec').checked = !!p.autoApproveExec; + // agent 规范 + 预算 + $('#cfgAgentRules').value = p.agentRules ?? ''; + $('#cfgBudget').value = p.budgetUsd == null ? '' : p.budgetUsd; + $('#cfgBudgetPeriod').value = p.budgetPeriod === 'day' ? 'day' : 'month'; + // 角色模型(字符串原样显示;对象形式显示 JSON) + const m = p.models || {}; + for (const [role, el] of [['executor', '#cfgMdlExecutor'], ['planner', '#cfgMdlPlanner'], ['reviewer', '#cfgMdlReviewer'], ['conflict', '#cfgMdlConflict']]) { + const v = m[role]; + $(el).value = v == null ? '' : (typeof v === 'string' ? v : JSON.stringify(v)); + } } } break; @@ -2010,9 +2047,32 @@ document.addEventListener('click', (ev) => { const logo = $('#cfgLogo').value.trim(); const verifyCmd = $('#cfgVerifyCmd').value.trim() || null; const model = $('#cfgModel').value.trim() || null; + // 分项检查 → checks JSON(全空=null) + const checksObj = {}; + for (const [k, el] of [['lint', '#cfgLint'], ['typecheck', '#cfgTypecheck'], ['build', '#cfgBuild']]) { + const v = $(el).value.trim(); if (v) checksObj[k] = v; + } + const checks = Object.keys(checksObj).length ? JSON.stringify(checksObj) : null; + // 角色模型 → models(字符串或 {..} JSON;全空=null) + const modelsObj = {}; + let modelsBad = null; + for (const [role, el] of [['executor', '#cfgMdlExecutor'], ['planner', '#cfgMdlPlanner'], ['reviewer', '#cfgMdlReviewer'], ['conflict', '#cfgMdlConflict']]) { + const v = $(el).value.trim(); if (!v) continue; + if (v[0] === '{') { try { modelsObj[role] = JSON.parse(v); } catch { modelsBad = role; } } + else modelsObj[role] = v; + } + if (modelsBad) { toast(t('cfgModelsBad', { role: modelsBad })); break; } + const models = Object.keys(modelsObj).length ? modelsObj : null; + const agentRules = $('#cfgAgentRules').value.trim() || null; + const budgetRaw = $('#cfgBudget').value.trim(); + const budgetUsd = budgetRaw === '' ? null : Number(budgetRaw); + if (budgetUsd !== null && (!Number.isFinite(budgetUsd) || budgetUsd < 0)) { toast(t('cfgModelsBad', { role: 'budget' })); break; } + const budgetPeriod = $('#cfgBudgetPeriod').value === 'day' ? 'day' : 'month'; + const autoApprovePlan = $('#cfgAutoPlan').checked; + const autoApproveExec = $('#cfgAutoExec').checked; act(async () => { await api(`/api/projects/${S.currentProjectId}`, { - method: 'PATCH', body: JSON.stringify({ concurrency: cc, autonomy, logo: logo || null, verifyCmd, model }), + method: 'PATCH', body: JSON.stringify({ concurrency: cc, autonomy, logo: logo || null, verifyCmd, model, checks, models, agentRules, budgetUsd, budgetPeriod, autoApprovePlan, autoApproveExec }), }); await loadProjects(); // 刷新当前值显示 + logo renderSidebar(); diff --git a/web/index.html b/web/index.html index 7916f7b..607e553 100644 --- a/web/index.html +++ b/web/index.html @@ -91,6 +91,59 @@ + +