Files
Agent-System/scripts/governance-console/index.html
T
weichen f9f055bee1 feat(agentctl): add governance console P1 serve and action APIs
Implement loopback HTTP console with claim new, integrate prepare, and pi packet sharing the same helpers as CLI; ship SPA and MCP brief/claim_archive wrappers.
2026-09-24 14:13:27 +00:00

200 lines
9.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>agentctl console</title>
<style>
:root { --bg:#0f1419; --panel:#1a2332; --text:#e7ecf3; --muted:#8b9bb4; --accent:#3d8bfd; --danger:#e35d6a; --ok:#3dd68c; --border:#2a3548; }
* { box-sizing: border-box; }
body { margin:0; font:14px/1.45 ui-sans-serif, system-ui, sans-serif; background:var(--bg); color:var(--text); }
header { display:flex; gap:1rem; align-items:center; padding:.75rem 1rem; border-bottom:1px solid var(--border); background:var(--panel); position:sticky; top:0; z-index:2; }
header h1 { font-size:1rem; margin:0; font-weight:600; letter-spacing:.02em; }
header .meta { color:var(--muted); flex:1; }
button { background:var(--accent); color:#fff; border:0; border-radius:6px; padding:.35rem .7rem; cursor:pointer; }
button.ghost { background:transparent; border:1px solid var(--border); color:var(--text); }
button.danger { background:var(--danger); }
main { display:grid; grid-template-columns:1fr 1fr; gap:1rem; padding:1rem; }
section { background:var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem 1rem; min-height:220px; }
section h2 { margin:0 0 .5rem; font-size:.95rem; }
table { width:100%; border-collapse:collapse; font-size:12px; }
th, td { text-align:left; padding:.3rem .25rem; border-bottom:1px solid var(--border); vertical-align:top; }
th { color:var(--muted); font-weight:500; }
.empty { color:var(--muted); font-style:italic; }
.toast { position:fixed; right:1rem; bottom:1rem; max-width:28rem; padding:.75rem 1rem; border-radius:8px; background:#222; border:1px solid var(--border); display:none; white-space:pre-wrap; }
.toast.err { border-color:var(--danger); color:#ffd0d4; }
.toast.ok { border-color:var(--ok); }
dialog { border:1px solid var(--border); border-radius:10px; background:var(--panel); color:var(--text); padding:1rem; max-width:36rem; width:90%; }
dialog textarea, dialog input, dialog select { width:100%; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:.4rem; margin:.25rem 0 .6rem; font:inherit; }
#brief { grid-column:1 / -1; }
pre.brief { margin:0; white-space:pre-wrap; font-size:12px; color:var(--muted); max-height:12rem; overflow:auto; }
@media (max-width:900px) { main { grid-template-columns:1fr; } }
</style>
</head>
<body>
<header>
<h1>agentctl console</h1>
<div class="meta" id="meta">loading…</div>
<label><input type="checkbox" id="auto" checked/> 10s</label>
<button class="ghost" id="refresh">刷新</button>
<button id="btnClaim">认领…</button>
<button id="btnPrepare">Prepare…</button>
<button class="ghost" id="btnArchive" title="dry-run by default in CLI; SPA 仅提示">归档指引</button>
</header>
<main>
<section>
<h2>1. 任务 / 租约</h2>
<div id="tasks"></div>
</section>
<section>
<h2>2. 队列与集成</h2>
<div id="queue"></div>
<p class="empty">Enqueue 请用 CLI:<code>agentctl queue enqueue --task … --expected-dev … --head …</code></p>
</section>
<section>
<h2>3. 文档漂移</h2>
<div id="drift"></div>
</section>
<section>
<h2>4. 清理积压</h2>
<div id="cleanup"></div>
</section>
<section id="brief">
<h2>brief</h2>
<pre class="brief" id="briefBody"></pre>
</section>
</main>
<div class="toast" id="toast"></div>
<dialog id="dlgClaim">
<form method="dialog" id="formClaim">
<h3>claim new</h3>
<label>task <input name="task" required/></label>
<label>owner <input name="owner" value="Cursor" required/></label>
<label>mutex <input name="mutex" value="-"/></label>
<label><input type="checkbox" name="replace"/> replace</label>
<p class="empty">须在已注册任务 worktree 上下文由 serve 代其 Repo 执行。</p>
<menu>
<button value="cancel" class="ghost">取消</button>
<button id="confirmClaim" value="default">确认</button>
</menu>
</form>
</dialog>
<dialog id="dlgPrepare">
<form method="dialog" id="formPrepare">
<h3>integrate prepare</h3>
<label>task <input name="task" required/></label>
<label>expected_dev <input name="expected_dev" required placeholder="full sha"/></label>
<label>integrate_id <input name="integrate_id" placeholder="optional"/></label>
<menu>
<button value="cancel" class="ghost">取消</button>
<button id="confirmPrepare" value="default">确认</button>
</menu>
</form>
</dialog>
<script>
const $ = (s) => document.querySelector(s);
const toast = (msg, ok) => {
const el = $('#toast');
el.textContent = msg;
el.className = 'toast ' + (ok ? 'ok' : 'err');
el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 6000);
};
async function api(path, opts) {
const r = await fetch(path, opts);
const ct = r.headers.get('content-type') || '';
const body = ct.includes('json') ? await r.json() : await r.text();
if (!r.ok) {
const code = body && body.code ? body.code : r.status;
const msg = body && body.message ? body.message : String(body);
throw new Error(code + ': ' + msg);
}
return body;
}
function table(headers, rows) {
if (!rows.length) return '<p class="empty">(empty)</p>';
let h = '<table><thead><tr>' + headers.map(x => '<th>'+x+'</th>').join('') + '</tr></thead><tbody>';
for (const row of rows) h += '<tr>' + row.map(c => '<td>'+esc(c)+'</td>').join('') + '</tr>';
return h + '</tbody></table>';
}
function esc(s) {
return String(s ?? '-').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
async function refresh() {
try {
const [status, queue, claims, cleanup, docs, cat, daemon, brief] = await Promise.all([
api('/api/status'), api('/api/queue'), api('/api/claims'), api('/api/cleanup'),
api('/api/docs/drift'), api('/api/catalog/drift'), api('/api/daemon'),
api('/api/brief?format=json'),
]);
const active = (status.tasks||[]).filter(t => t.state !== 'done' && t.state !== 'cancelled');
$('#meta').textContent = `daemon:${daemon.status||'?'} lock:${daemon.manager_lock&&daemon.manager_lock.held} queue:${(queue.items||[]).length} active:${active.length}`;
$('#tasks').innerHTML = table(['task','owner','state','worktree'], active.map(t => [t.id, t.owner, t.state, (t.worktree||'').split('/.worktrees/').pop()||t.worktree]))
+ '<h3 style="font-size:12px;color:var(--muted)">runs</h3>'
+ table(['run','task','state'], (status.runs||[]).map(r => [r.id, r.task, r.state]))
+ '<h3 style="font-size:12px;color:var(--muted)">claims</h3>'
+ table(['task','resource','state'], (claims.claims||[]).map(c => [c.task, c.resource, c.state]));
$('#queue').innerHTML = table(['task','state','pri','head'], (queue.items||[]).map(i => [i.task, i.state, i.priority, (i.head_sha||'').slice(0,12)]));
const regs = (docs.regressions||[]).concat(cat.regressions||[]);
$('#drift').innerHTML = table(['record','prev→cur','msg'], regs.map(r => [r.record_id, (r.previous||r.catalog||'')+'→'+(r.current||r.matrix||''), r.message||'']));
const trees = cleanup.worktrees||[];
$('#cleanup').innerHTML = table(['path','allowed','owner'], trees.map(t => {
const allowed = t.cleanup_allowed || t.status === 'allowed';
return [(t.path||'').split('/.worktrees/').pop()||t.path, allowed, t.cleanup_owner||'-'];
}));
$('#briefBody').textContent = brief.brief || '';
} catch (e) {
toast(String(e), false);
}
}
$('#refresh').onclick = refresh;
let timer = setInterval(refresh, 10000);
$('#auto').onchange = (e) => {
clearInterval(timer);
if (e.target.checked) timer = setInterval(refresh, 10000);
};
$('#btnClaim').onclick = () => $('#dlgClaim').showModal();
$('#btnPrepare').onclick = () => $('#dlgPrepare').showModal();
$('#btnArchive').onclick = () => toast(
'归档请用 CLI(需 docs:admin):\nagentctl claim archive --task <id> [--apply]\n或 MCP claim_archive',
true,
);
$('#confirmClaim').onclick = async (ev) => {
ev.preventDefault();
const f = $('#formClaim');
const body = {
task: f.task.value.trim(),
owner: f.owner.value.trim(),
mutex: f.mutex.value.trim() || '-',
replace: f.replace.checked,
};
try {
const r = await api('/api/claim/new', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
toast(JSON.stringify(r, null, 2), true);
$('#dlgClaim').close();
refresh();
} catch (e) { toast(String(e), false); }
};
$('#confirmPrepare').onclick = async (ev) => {
ev.preventDefault();
const f = $('#formPrepare');
const body = {
task: f.task.value.trim(),
expected_dev: f.expected_dev.value.trim(),
integrate_id: f.integrate_id.value.trim() || undefined,
};
try {
const r = await api('/api/integrate/prepare', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
toast(JSON.stringify(r, null, 2), true);
$('#dlgPrepare').close();
refresh();
} catch (e) { toast(String(e), false); }
};
refresh();
</script>
</body>
</html>