mirror of
https://git.openapi.site/https://github.com/desirecore/agent-desirecore.git
synced 2026-09-05 20:03:40 +08:00
feat: 增加人机共管场景决策树 (#8)
Co-authored-by: yige <yige@yigedeMacBook-Neo.local>
This commit is contained in:
@@ -88,7 +88,7 @@ body {
|
||||
}
|
||||
.rail-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 6px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@@ -355,7 +355,7 @@ body {
|
||||
}
|
||||
.kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
@@ -563,6 +563,248 @@ body {
|
||||
margin-top: 7px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.decision-tree-section {
|
||||
overflow: hidden;
|
||||
scroll-margin-top: 82px;
|
||||
background:
|
||||
radial-gradient(circle at 10% 0, rgba(35, 184, 178, 0.1), transparent 30%),
|
||||
#fff;
|
||||
}
|
||||
.tree-toolbar,
|
||||
.tree-proof {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
.tree-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.tree-legend,
|
||||
.tree-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
}
|
||||
.tree-legend span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: var(--muted);
|
||||
font-size: 9px;
|
||||
}
|
||||
.tree-legend span::before {
|
||||
content: '';
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--cyan);
|
||||
}
|
||||
.tree-legend .human::before {
|
||||
background: var(--violet);
|
||||
}
|
||||
.tree-legend .agent::before {
|
||||
background: var(--cyan);
|
||||
}
|
||||
.tree-legend .validator::before {
|
||||
background: var(--green);
|
||||
}
|
||||
.tree-legend .blocked::before {
|
||||
background: var(--red);
|
||||
}
|
||||
.tree-actions button {
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 8px 11px;
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tree-actions button:hover,
|
||||
.tree-actions button:focus-visible {
|
||||
border-color: #7ecac1;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(35, 184, 178, 0.12);
|
||||
}
|
||||
.tree-scroll {
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
padding: 8px 4px 18px;
|
||||
scrollbar-color: #aab8ca #edf1f6;
|
||||
outline: none;
|
||||
}
|
||||
.tree-scroll:focus-visible {
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 0 0 3px rgba(35, 184, 178, 0.24);
|
||||
}
|
||||
.tree-flow {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 200px;
|
||||
align-items: start;
|
||||
gap: 52px;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
.tree-stage {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
.tree-node {
|
||||
position: relative;
|
||||
min-height: 224px;
|
||||
padding: 15px;
|
||||
border: 1px solid var(--line);
|
||||
border-top: 5px solid var(--cyan);
|
||||
border-radius: 15px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 28px rgba(24, 42, 80, 0.08);
|
||||
}
|
||||
.tree-node.neutral {
|
||||
border-top-color: var(--violet);
|
||||
}
|
||||
.tree-node.guard {
|
||||
border-top-color: var(--amber);
|
||||
}
|
||||
.tree-node.blocked {
|
||||
border-top-color: var(--red);
|
||||
background: #fff9f8;
|
||||
}
|
||||
.tree-node-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
.tree-owner,
|
||||
.tree-kind {
|
||||
border-radius: 999px;
|
||||
padding: 4px 7px;
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tree-owner {
|
||||
background: #e9f8f5;
|
||||
color: #177369;
|
||||
font-weight: 800;
|
||||
}
|
||||
.tree-owner.human {
|
||||
background: #f0edff;
|
||||
color: #6251bd;
|
||||
}
|
||||
.tree-owner.validator {
|
||||
background: #eaf8f0;
|
||||
color: #247b53;
|
||||
}
|
||||
.tree-owner.platform {
|
||||
background: #eef2f8;
|
||||
color: #4d627e;
|
||||
}
|
||||
.tree-kind {
|
||||
background: var(--soft);
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tree-node h3 {
|
||||
margin: 14px 0 9px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42;
|
||||
}
|
||||
.tree-node p {
|
||||
margin: 0 0 14px;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.tree-node a {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
bottom: 14px;
|
||||
color: #167f75;
|
||||
font-size: 9px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.tree-connector {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: calc(100% + 5px);
|
||||
top: 74px;
|
||||
width: 42px;
|
||||
color: #2a9289;
|
||||
text-align: center;
|
||||
}
|
||||
.tree-connector span {
|
||||
display: block;
|
||||
width: 88px;
|
||||
margin-left: -23px;
|
||||
margin-bottom: 7px;
|
||||
color: var(--muted);
|
||||
font-size: 8px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.tree-connector i {
|
||||
display: block;
|
||||
font-size: 25px;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
}
|
||||
.tree-connector.guard {
|
||||
color: var(--amber);
|
||||
}
|
||||
.tree-connector.success {
|
||||
color: var(--green);
|
||||
}
|
||||
.tree-alternatives {
|
||||
position: relative;
|
||||
margin-top: 46px;
|
||||
}
|
||||
.tree-alternatives::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
bottom: 100%;
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
background: repeating-linear-gradient(to bottom, var(--red) 0 4px, transparent 4px 8px);
|
||||
}
|
||||
.tree-alternative > span {
|
||||
display: inline-block;
|
||||
margin: 0 0 7px 10px;
|
||||
border-radius: 999px;
|
||||
padding: 4px 7px;
|
||||
background: #fff1ec;
|
||||
color: #a34f43;
|
||||
font-size: 8px;
|
||||
}
|
||||
.tree-node.branch {
|
||||
min-height: 190px;
|
||||
border-style: dashed;
|
||||
box-shadow: none;
|
||||
}
|
||||
.decision-tree.path-only .tree-alternatives {
|
||||
display: none;
|
||||
}
|
||||
.tree-proof {
|
||||
align-items: flex-start;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 12px;
|
||||
color: var(--muted);
|
||||
font-size: 9px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tree-proof code {
|
||||
color: #477070;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: right;
|
||||
}
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -989,6 +1231,62 @@ body {
|
||||
.validation-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.decision-tree-section {
|
||||
padding: 22px 16px;
|
||||
}
|
||||
.tree-toolbar,
|
||||
.tree-proof {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
.tree-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.tree-actions button {
|
||||
min-height: 44px;
|
||||
}
|
||||
.tree-scroll {
|
||||
overflow-x: hidden;
|
||||
padding-inline: 1px;
|
||||
}
|
||||
.tree-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: 0;
|
||||
}
|
||||
.tree-stage {
|
||||
width: 100%;
|
||||
}
|
||||
.tree-node {
|
||||
min-height: 190px;
|
||||
}
|
||||
.tree-connector {
|
||||
position: static;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 9px 0;
|
||||
}
|
||||
.tree-connector span {
|
||||
width: auto;
|
||||
margin: 0 0 2px;
|
||||
}
|
||||
.tree-connector i {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.tree-alternatives {
|
||||
margin: 30px 0 4px 24px;
|
||||
}
|
||||
.tree-node.branch {
|
||||
min-height: 176px;
|
||||
}
|
||||
.tree-proof code {
|
||||
text-align: left;
|
||||
}
|
||||
.kpis {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
function renderShell() {
|
||||
$('#rail-summary').innerHTML =
|
||||
`<span><b>${manifest.counts.chapters}</b>场景</span><span><b>${manifest.counts.screenshots}</b>真机图</span><span><b>${manifest.counts.representativeStabilityPasses}</b>稳定运行</span>`
|
||||
`<span><b>${manifest.counts.chapters}</b>场景</span><span><b>${manifest.counts.decisionTrees}</b>决策树</span><span><b>${manifest.counts.screenshots}</b>真机图</span><span><b>${manifest.counts.representativeStabilityPasses}</b>稳定运行</span>`
|
||||
$('#build-id').textContent = manifest.buildId
|
||||
const families = ['ALL', ...new Set(manifest.chapters.map((item) => familyLabel(item.problemFamily)))]
|
||||
$('#filters').innerHTML = families
|
||||
@@ -173,6 +173,102 @@
|
||||
.join('')
|
||||
}
|
||||
|
||||
const ownerLabel = (owner) =>
|
||||
({ human: '人类确认', agent: 'Agent 推导', shared: '人 + Agent', validator: '独立校核', platform: '平台执行' })[
|
||||
owner
|
||||
] || owner
|
||||
const kindLabel = (kind) =>
|
||||
({ context: '决策背景', question: '判断门', action: '执行', check: '校核门', outcome: '结果' })[kind] || kind
|
||||
|
||||
function decisionTreePath(tree) {
|
||||
const nodes = new Map(tree.nodes.map((node) => [node.id, node]))
|
||||
const selected = new Map(tree.edges.filter((edge) => edge.selected).map((edge) => [edge.from, edge]))
|
||||
const path = []
|
||||
const visited = new Set()
|
||||
let current = tree.rootNodeId
|
||||
while (nodes.has(current) && !visited.has(current)) {
|
||||
visited.add(current)
|
||||
path.push(nodes.get(current))
|
||||
current = selected.get(current)?.to
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
function renderTreeNode(node, branch = false) {
|
||||
return `<article class="tree-node ${esc(node.status)}${branch ? ' branch' : ''}" data-tree-node="${esc(node.id)}">
|
||||
<div class="tree-node-top"><span class="tree-owner ${esc(node.owner)}">${esc(ownerLabel(node.owner))}</span><span class="tree-kind">${esc(kindLabel(node.kind))}</span></div>
|
||||
<h3>${esc(node.title)}</h3><p>${esc(node.detail)}</p>
|
||||
<a href="${esc(node.evidenceRef)}">查看关联证据 →</a>
|
||||
</article>`
|
||||
}
|
||||
|
||||
function renderDecisionTree(tree) {
|
||||
const path = decisionTreePath(tree)
|
||||
const nodes = new Map(tree.nodes.map((node) => [node.id, node]))
|
||||
const selectedEdges = new Map(tree.edges.filter((edge) => edge.selected).map((edge) => [edge.from, edge]))
|
||||
const stages = path
|
||||
.map((node, index) => {
|
||||
const nextEdge = selectedEdges.get(node.id)
|
||||
const alternatives = tree.edges
|
||||
.filter((edge) => edge.from === node.id && !edge.selected)
|
||||
.map(
|
||||
(edge) =>
|
||||
`<div class="tree-alternative"><span>${esc(edge.label)}</span>${renderTreeNode(nodes.get(edge.to), true)}</div>`
|
||||
)
|
||||
.join('')
|
||||
return `<div class="tree-stage" role="listitem">${renderTreeNode(node)}${alternatives ? `<div class="tree-alternatives">${alternatives}</div>` : ''}${index < path.length - 1 ? `<div class="tree-connector ${esc(nextEdge.tone)}"><span>${esc(nextEdge.label)}</span><i aria-hidden="true">→</i></div>` : ''}</div>`
|
||||
})
|
||||
.join('')
|
||||
return `<div class="decision-tree" data-decision-tree>
|
||||
<div class="tree-toolbar"><div class="tree-legend" aria-label="决策树职责图例"><span class="human">人类确认</span><span class="agent">Agent 推导</span><span class="validator">独立校核</span><span class="blocked">阻断分支</span></div><div class="tree-actions"><button type="button" data-tree-mode aria-pressed="false">只看实际路径</button><button type="button" data-tree-export>下载 SVG</button></div></div>
|
||||
<div class="tree-scroll" tabindex="0" aria-label="${esc(`场景决策树:${state.chapter.title}`)}"><div class="tree-flow" role="list">${stages}</div></div>
|
||||
<div class="tree-proof"><span>当前实际路径以实线连接;旁路展示系统何时必须补问或阻断。</span><code>PACK ${esc(shortHash(tree.evidenceBindings.scenarioPackSha256))} · RUN ${esc(shortHash(tree.evidenceBindings.runId))}</code></div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
const svgTextLines = (value, max = 18, lines = 3) => {
|
||||
const chars = [...String(value)]
|
||||
const result = []
|
||||
while (chars.length && result.length < lines) result.push(chars.splice(0, max).join(''))
|
||||
if (chars.length) result[result.length - 1] = `${result[result.length - 1].slice(0, -1)}…`
|
||||
return result
|
||||
}
|
||||
|
||||
function downloadDecisionTreeSvg(c) {
|
||||
const path = decisionTreePath(c.decisionTree)
|
||||
const width = Math.max(1200, path.length * 230 + 100)
|
||||
const height = 420
|
||||
const cards = path
|
||||
.map((node, index) => {
|
||||
const x = 50 + index * 230
|
||||
const title = svgTextLines(node.title)
|
||||
.map(
|
||||
(line, lineIndex) =>
|
||||
`<text x="${x + 16}" y="${112 + lineIndex * 22}" font-size="15" font-weight="700" fill="#172238">${esc(line)}</text>`
|
||||
)
|
||||
.join('')
|
||||
const detail = svgTextLines(node.detail, 22, 3)
|
||||
.map(
|
||||
(line, lineIndex) =>
|
||||
`<text x="${x + 16}" y="${194 + lineIndex * 18}" font-size="11" fill="#637089">${esc(line)}</text>`
|
||||
)
|
||||
.join('')
|
||||
const connector =
|
||||
index < path.length - 1
|
||||
? `<path d="M ${x + 190} 174 H ${x + 222}" stroke="#23b8b2" stroke-width="3"/><path d="M ${x + 216} 168 L ${x + 224} 174 L ${x + 216} 180" fill="none" stroke="#23b8b2" stroke-width="3"/>`
|
||||
: ''
|
||||
return `<g><rect x="${x}" y="64" width="190" height="210" rx="16" fill="#fff" stroke="#dce3ed"/><rect x="${x}" y="64" width="190" height="8" rx="4" fill="${node.status === 'blocked' ? '#d95c63' : '#23b8b2'}"/><text x="${x + 16}" y="94" font-size="11" font-weight="700" fill="#477070">${esc(ownerLabel(node.owner))}</text>${title}${detail}${connector}</g>`
|
||||
})
|
||||
.join('')
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="#eef2f7"/><text x="50" y="35" font-size="22" font-weight="800" fill="#172238">${esc(c.title)} · 人 + Agent 决策树</text>${cards}<text x="50" y="316" font-size="13" font-weight="700" fill="#172238">实际证据路径</text><text x="50" y="342" font-size="12" fill="#637089">场景 ${esc(c.scenarioId)} · ${esc(c.decisionTree.evidenceBindings.scenarioPackSha256)}</text><text x="50" y="370" font-size="12" fill="#637089">此 SVG 由验证书中的结构化决策树即时导出;完整旁路与证据链接请在 Web 页面审阅。</text></svg>`
|
||||
const url = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${c.scenarioId.replace(/[^a-z0-9._-]/gi, '-')}-decision-tree.svg`
|
||||
link.click()
|
||||
setTimeout(() => URL.revokeObjectURL(url), 0)
|
||||
}
|
||||
|
||||
function renderChapter() {
|
||||
const c = state.chapter
|
||||
const tone = statusTone(c.resultStatus)
|
||||
@@ -188,13 +284,15 @@
|
||||
<div class="kpi"><small>问题族</small><strong>${familyLabel(c.problemFamily)}</strong></div>
|
||||
<div class="kpi"><small>实际引擎</small><strong>${esc(c.engineId || '独立校核')}</strong></div>
|
||||
<div class="kpi"><small>结果 / 校核</small><strong class="status ${tone}">${statusLabel(c.resultStatus)} / ${esc(c.validationVerdict || '不适用')}</strong></div>
|
||||
<div class="kpi"><small>人机决策树</small><strong>${c.decisionTree.nodes.length} 节点 · 证据绑定</strong></div>
|
||||
<div class="kpi"><small>真实消息 / 工具</small><strong>${c.counts.rawConversationMessages} / ${c.counts.toolEvents}</strong></div>
|
||||
<div class="kpi"><small>真机截图</small><strong>${c.counts.screenshots} 张 · 2x</strong></div>
|
||||
<div class="kpi"><small>原生真机截图</small><strong>${c.counts.screenshots} 张 · 2x</strong></div>
|
||||
</section>
|
||||
<section class="section-shell"><div class="section-head"><div><h2>场景故事与意义</h2><p>为什么这个问题具有代表性,以及哪些业务规则决定可行性。</p></div><span class="evidence-tag">PACK ${esc(c.scenarioVersion)} · ${shortHash(c.contentSha256)}</span></div>
|
||||
<div class="hero-grid"><div class="validation-card"><small>目标</small><strong>${c.objectives.map(esc).join(';')}</strong></div><div class="validation-card"><small>基线</small><strong>${esc(c.baseline)}</strong></div></div>
|
||||
<div class="validation-card" style="margin-top:12px"><small>硬规则</small><strong>${c.constraints.map(esc).join(';')}</strong></div>
|
||||
</section>
|
||||
<section class="section-shell decision-tree-section" id="decision-tree"><div class="section-head"><div><h2>人 + Agent 场景决策树</h2><p>从业务确认到交付门禁,展示本次实际路径以及必须补问、拒绝或阻断的旁路。</p></div><span class="evidence-tag">EVIDENCE-BOUND · ${c.decisionTree.nodes.length} NODES</span></div>${renderDecisionTree(c.decisionTree)}</section>
|
||||
<section class="section-shell"><div class="section-head"><div><h2>真实多轮对话</h2><p>普通消息逐条来自 messages.jsonl;工具事件来自持久回执,未用离线脚本改写对话。</p></div><span class="evidence-tag">RUN ${shortHash(c.evidence.runId)}</span></div><div class="timeline">${renderTimeline(c)}</div></section>
|
||||
<section class="section-shell"><div class="section-head"><div><h2>智能体最终十部分报告</h2><p>十个 section ID、数量和顺序固定;正文由最终消息 Markdown 原样构建。</p></div><span class="evidence-tag">${c.formulaStats.rendered} FORMULAS · HTML+MATHML</span></div><div class="report-grid">${c.sections.map((section) => `<section class="report-part" id="${section.id}"><div class="part-number">${section.number}</div><div class="part-body"><h3>${esc(section.title)}</h3><div class="markdown">${section.html}</div></div></section>`).join('')}</div></section>
|
||||
<section class="section-shell"><div class="section-head"><div><h2>专业校核与稳定性</h2><p>求解结果、独立验证、等价 formulation 和重复运行分别留证。</p></div><span class="evidence-tag">${esc(c.stability.policy)}</span></div>
|
||||
@@ -214,6 +312,13 @@
|
||||
})
|
||||
})
|
||||
$('#chapter [data-open-regression]')?.addEventListener('click', openLatestRegression)
|
||||
$('#chapter [data-tree-mode]')?.addEventListener('click', (event) => {
|
||||
const tree = event.currentTarget.closest('[data-decision-tree]')
|
||||
const pathOnly = tree.classList.toggle('path-only')
|
||||
event.currentTarget.setAttribute('aria-pressed', String(pathOnly))
|
||||
event.currentTarget.textContent = pathOnly ? '显示完整分支' : '只看实际路径'
|
||||
})
|
||||
$('#chapter [data-tree-export]')?.addEventListener('click', () => downloadDecisionTreeSvg(c))
|
||||
document.title = `${String(c.scenarioOrder).padStart(2, '0')} · ${c.title} — ${manifest.title}`
|
||||
$('#app').setAttribute('aria-busy', 'false')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user