feat(web): 项目配置面板补全——checks/自动放行/agentRules/预算/角色模型编辑

此前 checks、autoApprovePlan/Exec、agentRules、budgetUsd、models 只能经 API 改,
看板无编辑口。现扩展项目「配置」面板:
- 分项检查:lint / typecheck / build 三命令(拼成 checks JSON,全空=null)
- 硬闸/自动放行:autoApprovePlan / autoApproveExec 两个勾选
- 角色模型:executor/planner/reviewer/conflict 四个输入(填模型 id 字符串,
  或 {"easy":..,"hard":..} JSON;留空=默认 opus-4.8;非法 JSON 提示报错)
- 项目 Agent 规范(agentRules)textarea
- 成本预算:budgetUsd + budgetPeriod(按天/月)
toggle-config 回填全部新字段;save-config 一并 PATCH。新增 zh/en i18n 标签 +
.cfg-subhead/.cfg-check 样式。web/ 静态、无需 rebuild。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-25 07:34:49 +08:00
parent e8fb2bc664
commit 6bf2b3619c
3 changed files with 127 additions and 1 deletions
+61 -1
View File
@@ -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();
+53
View File
@@ -91,6 +91,59 @@
<input id="cfgModel" type="text" data-i18n-ph="modelPh" placeholder="claude-opus-4-5(可空,留空用全局默认)" autocomplete="off">
</label>
</div>
<div class="cfg-subhead" data-i18n="cfgChecks">分项检查(lint / typecheck / build</div>
<div class="form-row">
<label class="grow"><span data-i18n="cfgLint">lint 命令</span>
<input id="cfgLint" type="text" data-i18n-ph="cfgCmdPh" placeholder="留空=跳过此项" autocomplete="off">
</label>
<label class="grow"><span data-i18n="cfgTypecheck">typecheck 命令</span>
<input id="cfgTypecheck" type="text" data-i18n-ph="cfgCmdPh" placeholder="留空=跳过此项" autocomplete="off">
</label>
<label class="grow"><span data-i18n="cfgBuild">build 命令</span>
<input id="cfgBuild" type="text" data-i18n-ph="cfgCmdPh" placeholder="留空=跳过此项" autocomplete="off">
</label>
</div>
<div class="cfg-subhead" data-i18n="cfgGates">硬闸 / 自动放行</div>
<div class="form-row cfg-checks-row">
<label class="cfg-check"><input id="cfgAutoPlan" type="checkbox"> <span data-i18n="cfgAutoPlan">自动放行 plan</span></label>
<label class="cfg-check"><input id="cfgAutoExec" type="checkbox"> <span data-i18n="cfgAutoExec">自动放行 exec</span></label>
</div>
<div class="cfg-subhead" data-i18n="cfgModels">角色模型(留空=默认 opus-4.8)</div>
<div class="form-row">
<label class="grow"><span data-i18n="cfgMdlExecutor">executor</span>
<input id="cfgMdlExecutor" type="text" placeholder="claude-opus-4-8" autocomplete="off">
</label>
<label class="grow"><span data-i18n="cfgMdlPlanner">planner</span>
<input id="cfgMdlPlanner" type="text" placeholder="claude-opus-4-8" autocomplete="off">
</label>
<label class="grow"><span data-i18n="cfgMdlReviewer">reviewer</span>
<input id="cfgMdlReviewer" type="text" placeholder="claude-opus-4-8" autocomplete="off">
</label>
<label class="grow"><span data-i18n="cfgMdlConflict">conflict</span>
<input id="cfgMdlConflict" type="text" placeholder="claude-opus-4-8" autocomplete="off">
</label>
</div>
<div class="form-row">
<label class="grow"><span data-i18n="cfgAgentRules">项目 Agent 规范</span>
<textarea id="cfgAgentRules" rows="3" data-i18n-ph="cfgAgentRulesPh" placeholder="项目专属执行约定、风格、禁忌……(可空)" autocomplete="off"></textarea>
</label>
</div>
<div class="form-row">
<label class="narrow"><span data-i18n="cfgBudget">成本预算</span>
<input id="cfgBudget" type="number" min="0" step="0.5" data-i18n-ph="cfgBudgetPh" placeholder="USD 上限(可空=不限)" autocomplete="off">
</label>
<label class="narrow"><span>&nbsp;</span>
<select id="cfgBudgetPeriod">
<option value="month" data-i18n="cfgBudgetMonth">按月</option>
<option value="day" data-i18n="cfgBudgetDay">按天</option>
</select>
</label>
</div>
<div class="form-row cfg-sync-row">
<span id="syncMeta" class="sync-meta"></span>
<button id="btnSync" class="btn btn-xs" data-action="sync-todo"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5" stroke="var(--icon-accent,#5fdd7d)"/></svg> <span data-i18n="sync">同步 todo</span></button>
+13
View File
@@ -274,6 +274,19 @@ body::before {
font-size: 11px; color: var(--muted); letter-spacing: .06em;
margin-left: auto; padding-bottom: 7px;
}
/* 配置分组小标题 + 自动放行勾选 */
.cfg-subhead {
font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
color: var(--muted); margin: 16px 0 6px; padding-top: 10px;
border-top: 1px solid var(--line);
}
.cfg-checks-row { gap: 22px; }
label.cfg-check {
flex-direction: row; align-items: center; gap: 7px;
text-transform: none; letter-spacing: .02em; font-size: 12px; color: var(--ink);
cursor: pointer;
}
label.cfg-check input { width: auto; cursor: pointer; }
/* ── Agent 执行面板 ──────────────────────────────────────── */
.agent-total { color: var(--cyan); letter-spacing: .08em; }