Upgrade dashboard v1.1 with auto-refresh, dual signoff, and justice routing
This commit is contained in:
@@ -10,3 +10,4 @@
|
|||||||
- 多智能体治理边界:Val 对“明显不应认可”的动作可直接否决;重大/不明确决策需上报谷老板最终拍板,谷老板保留最终否决权。
|
- 多智能体治理边界:Val 对“明显不应认可”的动作可直接否决;重大/不明确决策需上报谷老板最终拍板,谷老板保留最终否决权。
|
||||||
- 已根据《宪章v1》完成初版组织落地文档:`org/CONSTITUTION_v1.md`、`org/ROLE_CARDS.md`、`org/VAL_RUNBOOK.md`。
|
- 已根据《宪章v1》完成初版组织落地文档:`org/CONSTITUTION_v1.md`、`org/ROLE_CARDS.md`、`org/VAL_RUNBOOK.md`。
|
||||||
- 已完成“多 Agent 任务看板(状态流转可视化)”首版实现:`org/dashboard/index.html` + `tasks.sample.json` + `README.md`,支持样例加载与自定义 JSON 导入。
|
- 已完成“多 Agent 任务看板(状态流转可视化)”首版实现:`org/dashboard/index.html` + `tasks.sample.json` + `README.md`,支持样例加载与自定义 JSON 导入。
|
||||||
|
- 看板 v1.1 增强:自动刷新(5s/10s/30s)、门下+兵部双签标记、失败任务自动流入刑部(Audit/刑部)。
|
||||||
|
|||||||
@@ -32,4 +32,7 @@
|
|||||||
|
|
||||||
- 看板内置三省六部状态流转提示
|
- 看板内置三省六部状态流转提示
|
||||||
- 显示进行中、待审计、已完成、高风险四项统计
|
- 显示进行中、待审计、已完成、高风险四项统计
|
||||||
|
- 支持自动刷新(5s/10s/30s)与本地 JSON 上传
|
||||||
|
- 支持“门下+兵部”双签状态标记(双签✅/❌)
|
||||||
|
- 失败任务自动流入刑部(展示为 Audit/刑部)
|
||||||
- 适合作为 Val 进行任务总调度时的可视化面板
|
- 适合作为 Val 进行任务总调度时的可视化面板
|
||||||
|
|||||||
@@ -85,6 +85,14 @@
|
|||||||
<p id="updatedAt">等待加载数据...</p>
|
<p id="updatedAt">等待加载数据...</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
<label style="font-size:12px;color:var(--muted);display:flex;gap:6px;align-items:center;">
|
||||||
|
<input id="autoRefresh" type="checkbox" checked /> 自动刷新
|
||||||
|
</label>
|
||||||
|
<select id="refreshMs" style="background:#1a2550;color:#dce6ff;border:1px solid #3b4b86;border-radius:8px;padding:6px;">
|
||||||
|
<option value="5000">5s</option>
|
||||||
|
<option value="10000" selected>10s</option>
|
||||||
|
<option value="30000">30s</option>
|
||||||
|
</select>
|
||||||
<input id="fileInput" type="file" accept="application/json" />
|
<input id="fileInput" type="file" accept="application/json" />
|
||||||
<button id="loadSample">加载样例数据</button>
|
<button id="loadSample">加载样例数据</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,19 +126,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const state = { data: null };
|
const state = { data: null, timer: null, lastSource: 'sample' };
|
||||||
|
|
||||||
function riskClass(risk) {
|
function riskClass(risk) {
|
||||||
return risk === 'high' ? 'risk-high' : risk === 'medium' ? 'risk-medium' : 'risk-low';
|
return risk === 'high' ? 'risk-high' : risk === 'medium' ? 'risk-medium' : 'risk-low';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyWorkflowRules(tasks) {
|
||||||
|
// 规则1:失败任务自动流入刑部(Justice)
|
||||||
|
for (const t of tasks) {
|
||||||
|
if (t.failed === true) {
|
||||||
|
t.state = 'Audit';
|
||||||
|
t.owner = '刑部';
|
||||||
|
t.summary = `【自动流转】失败转刑部:${t.failReason || '待定位根因'}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDualSignoff(task) {
|
||||||
|
return !!(task.signoff?.auditor && task.signoff?.guardian);
|
||||||
|
}
|
||||||
|
|
||||||
function render(data) {
|
function render(data) {
|
||||||
state.data = data;
|
state.data = data;
|
||||||
const columns = data.columns || [];
|
const columns = data.columns || [];
|
||||||
const tasks = data.tasks || [];
|
const tasks = applyWorkflowRules([...(data.tasks || [])]);
|
||||||
|
|
||||||
|
const unsignedExec = tasks.filter(t => t.state === 'Execute' && !hasDualSignoff(t)).length;
|
||||||
document.getElementById('boardName').textContent = data.boardName || '多 Agent 任务看板';
|
document.getElementById('boardName').textContent = data.boardName || '多 Agent 任务看板';
|
||||||
document.getElementById('updatedAt').textContent = `更新时间:${data.updatedAt || '未知'} · 任务数 ${tasks.length}`;
|
document.getElementById('updatedAt').textContent = `更新时间:${data.updatedAt || '未知'} · 任务数 ${tasks.length} · 双签缺失执行项 ${unsignedExec}`;
|
||||||
|
|
||||||
const statsWrap = document.getElementById('stats');
|
const statsWrap = document.getElementById('stats');
|
||||||
const running = tasks.filter(t => ['Dispatch','Execute','Verify'].includes(t.state)).length;
|
const running = tasks.filter(t => ['Dispatch','Execute','Verify'].includes(t.state)).length;
|
||||||
@@ -151,37 +176,60 @@
|
|||||||
return `<div class="col">
|
return `<div class="col">
|
||||||
<div class="col-head"><span>${col}</span><span>${list.length}</span></div>
|
<div class="col-head"><span>${col}</span><span>${list.length}</span></div>
|
||||||
<div class="col-body">
|
<div class="col-body">
|
||||||
${list.length ? list.map(t => `
|
${list.length ? list.map(t => {
|
||||||
|
const dual = hasDualSignoff(t);
|
||||||
|
const signText = dual ? '双签✅' : '双签❌';
|
||||||
|
const signStyle = dual ? 'style="color:#22c55e"' : 'style="color:#ef4444"';
|
||||||
|
return `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h4>${t.id} · ${t.title}</h4>
|
<h4>${t.id} · ${t.title}</h4>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span class="pill">${t.owner || '未分配'}</span>
|
<span class="pill">${t.owner || '未分配'}</span>
|
||||||
<span class="pill">P:${t.priority || 'n/a'}</span>
|
<span class="pill">P:${t.priority || 'n/a'}</span>
|
||||||
<span class="pill ${riskClass(t.risk)}">Risk:${t.risk || 'low'}</span>
|
<span class="pill ${riskClass(t.risk)}">Risk:${t.risk || 'low'}</span>
|
||||||
|
<span class="pill" ${signStyle}>${signText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">${t.summary || ''}</div>
|
<div class="meta">${t.summary || ''}</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('') : `<div class="meta">暂无任务</div>`}
|
`}).join('') : `<div class="meta">暂无任务</div>`}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSample() {
|
async function loadSample() {
|
||||||
const res = await fetch('./tasks.sample.json');
|
const res = await fetch('./tasks.sample.json', { cache: 'no-store' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
render(data);
|
render(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loadSample').addEventListener('click', loadSample);
|
function setupAutoRefresh() {
|
||||||
|
if (state.timer) clearInterval(state.timer);
|
||||||
|
const enabled = document.getElementById('autoRefresh').checked;
|
||||||
|
const ms = Number(document.getElementById('refreshMs').value || 10000);
|
||||||
|
if (!enabled || state.lastSource !== 'sample') return;
|
||||||
|
state.timer = setInterval(loadSample, ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('loadSample').addEventListener('click', async () => {
|
||||||
|
state.lastSource = 'sample';
|
||||||
|
await loadSample();
|
||||||
|
setupAutoRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('fileInput').addEventListener('change', async (e) => {
|
document.getElementById('fileInput').addEventListener('change', async (e) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
state.lastSource = 'upload';
|
||||||
|
if (state.timer) clearInterval(state.timer);
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
render(JSON.parse(text));
|
render(JSON.parse(text));
|
||||||
});
|
});
|
||||||
|
|
||||||
loadSample();
|
document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh);
|
||||||
|
document.getElementById('refreshMs').addEventListener('change', setupAutoRefresh);
|
||||||
|
|
||||||
|
loadSample().then(setupAutoRefresh);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"priority": "high",
|
"priority": "high",
|
||||||
"state": "Execute",
|
"state": "Execute",
|
||||||
"risk": "low",
|
"risk": "low",
|
||||||
|
"signoff": { "auditor": true, "guardian": true },
|
||||||
"tags": ["dashboard", "workflow"],
|
"tags": ["dashboard", "workflow"],
|
||||||
"summary": "前端页面 + 状态统计 + 流转图",
|
"summary": "前端页面 + 状态统计 + 流转图",
|
||||||
"createdAt": "2026-03-08 11:42",
|
"createdAt": "2026-03-08 11:42",
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"priority": "high",
|
"priority": "high",
|
||||||
"state": "Audit",
|
"state": "Audit",
|
||||||
"risk": "medium",
|
"risk": "medium",
|
||||||
|
"signoff": { "auditor": true, "guardian": false },
|
||||||
"tags": ["security"],
|
"tags": ["security"],
|
||||||
"summary": "确认无外发、无不可逆破坏动作",
|
"summary": "确认无外发、无不可逆破坏动作",
|
||||||
"createdAt": "2026-03-08 11:43",
|
"createdAt": "2026-03-08 11:43",
|
||||||
@@ -44,10 +46,26 @@
|
|||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"state": "Verify",
|
"state": "Verify",
|
||||||
"risk": "low",
|
"risk": "low",
|
||||||
|
"signoff": { "auditor": true, "guardian": true },
|
||||||
"tags": ["style", "report"],
|
"tags": ["style", "report"],
|
||||||
"summary": "结论优先 + 风险提示 + 选项推荐",
|
"summary": "结论优先 + 风险提示 + 选项推荐",
|
||||||
"createdAt": "2026-03-08 11:44",
|
"createdAt": "2026-03-08 11:44",
|
||||||
"updatedAt": "2026-03-08 11:46"
|
"updatedAt": "2026-03-08 11:46"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "T-004",
|
||||||
|
"title": "批处理脚本执行异常",
|
||||||
|
"owner": "工部",
|
||||||
|
"priority": "high",
|
||||||
|
"state": "Execute",
|
||||||
|
"risk": "high",
|
||||||
|
"failed": true,
|
||||||
|
"failReason": "脚本在沙箱中权限不足",
|
||||||
|
"signoff": { "auditor": true, "guardian": true },
|
||||||
|
"tags": ["incident", "retry"],
|
||||||
|
"summary": "按宪章应自动流入刑部处理",
|
||||||
|
"createdAt": "2026-03-08 11:47",
|
||||||
|
"updatedAt": "2026-03-08 11:49"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user