oh-my-knowledge 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/README.zh.md +39 -0
- package/dist/src/analysis/failure-clusterer.js +1 -1
- package/dist/src/analysis/failure-clusterer.js.map +1 -1
- package/dist/src/analysis/gap-analyzer.d.ts +8 -1
- package/dist/src/analysis/gap-analyzer.d.ts.map +1 -1
- package/dist/src/analysis/gap-analyzer.js +65 -33
- package/dist/src/analysis/gap-analyzer.js.map +1 -1
- package/dist/src/analysis/sample-diagnostics.js +14 -14
- package/dist/src/analysis/sample-diagnostics.js.map +1 -1
- package/dist/src/cli/commands/observe.d.ts.map +1 -1
- package/dist/src/cli/commands/observe.js +169 -0
- package/dist/src/cli/commands/observe.js.map +1 -1
- package/dist/src/cli/commands/registry.d.ts.map +1 -1
- package/dist/src/cli/commands/registry.js +9 -1
- package/dist/src/cli/commands/registry.js.map +1 -1
- package/dist/src/cli/commands/studio.d.ts.map +1 -1
- package/dist/src/cli/commands/studio.js +8 -0
- package/dist/src/cli/commands/studio.js.map +1 -1
- package/dist/src/cli/i18n-dict.d.ts +1 -1
- package/dist/src/cli/i18n-dict.d.ts.map +1 -1
- package/dist/src/cli/i18n-dict.js +125 -11
- package/dist/src/cli/i18n-dict.js.map +1 -1
- package/dist/src/grading/debias-validate.js +4 -4
- package/dist/src/grading/debias-validate.js.map +1 -1
- package/dist/src/grading/gold-cli.js +2 -2
- package/dist/src/grading/gold-cli.js.map +1 -1
- package/dist/src/observability/inbox-view-model.d.ts +17 -0
- package/dist/src/observability/inbox-view-model.d.ts.map +1 -0
- package/dist/src/observability/inbox-view-model.js +62 -0
- package/dist/src/observability/inbox-view-model.js.map +1 -0
- package/dist/src/observability/inbox.d.ts +100 -0
- package/dist/src/observability/inbox.d.ts.map +1 -0
- package/dist/src/observability/inbox.js +703 -0
- package/dist/src/observability/inbox.js.map +1 -0
- package/dist/src/observability/trace-adapter.d.ts +12 -64
- package/dist/src/observability/trace-adapter.d.ts.map +1 -1
- package/dist/src/observability/trace-adapter.js +9 -355
- package/dist/src/observability/trace-adapter.js.map +1 -1
- package/dist/src/observability/trace-attribution.d.ts +28 -0
- package/dist/src/observability/trace-attribution.d.ts.map +1 -0
- package/dist/src/observability/trace-attribution.js +104 -0
- package/dist/src/observability/trace-attribution.js.map +1 -0
- package/dist/src/observability/trace-segmenter.d.ts +42 -0
- package/dist/src/observability/trace-segmenter.d.ts.map +1 -0
- package/dist/src/observability/trace-segmenter.js +231 -0
- package/dist/src/observability/trace-segmenter.js.map +1 -0
- package/dist/src/observability/trace-source.d.ts +72 -0
- package/dist/src/observability/trace-source.d.ts.map +1 -0
- package/dist/src/observability/trace-source.js +142 -0
- package/dist/src/observability/trace-source.js.map +1 -0
- package/dist/src/renderer/html-renderer.d.ts.map +1 -1
- package/dist/src/renderer/html-renderer.js +107 -59
- package/dist/src/renderer/html-renderer.js.map +1 -1
- package/dist/src/renderer/layout.d.ts.map +1 -1
- package/dist/src/renderer/layout.js +58 -21
- package/dist/src/renderer/layout.js.map +1 -1
- package/dist/src/renderer/observation-inbox-renderer.d.ts +4 -0
- package/dist/src/renderer/observation-inbox-renderer.d.ts.map +1 -0
- package/dist/src/renderer/observation-inbox-renderer.js +900 -0
- package/dist/src/renderer/observation-inbox-renderer.js.map +1 -0
- package/dist/src/renderer/summary.d.ts +17 -0
- package/dist/src/renderer/summary.d.ts.map +1 -1
- package/dist/src/renderer/summary.js +277 -33
- package/dist/src/renderer/summary.js.map +1 -1
- package/dist/src/renderer/trends.d.ts.map +1 -1
- package/dist/src/renderer/trends.js +3 -1
- package/dist/src/renderer/trends.js.map +1 -1
- package/dist/src/server/report-server.d.ts +3 -1
- package/dist/src/server/report-server.d.ts.map +1 -1
- package/dist/src/server/report-server.js +35 -4
- package/dist/src/server/report-server.js.map +1 -1
- package/dist/src/shared/tool-search.d.ts +9 -0
- package/dist/src/shared/tool-search.d.ts.map +1 -0
- package/dist/src/shared/tool-search.js +104 -0
- package/dist/src/shared/tool-search.js.map +1 -0
- package/dist/src/types/executor.d.ts +4 -0
- package/dist/src/types/executor.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import { DEFAULT_LANG, layout, e } from './layout.js';
|
|
2
|
+
import { severityReasonFor } from '../observability/inbox.js';
|
|
3
|
+
export function renderObservationInboxPage(model, lang = DEFAULT_LANG) {
|
|
4
|
+
const { allItems, items, reports, skillInvocationCounts, skillSessionCounts, skillInvocationLastSeen, skillToolCallCounts, totalSkillInvocations, severitySkillCounts, skillCount, reportCount, latestSeenLabel, } = model;
|
|
5
|
+
const countSkillsBySeverity = (...severities) => new Set(allItems.filter((item) => severities.includes(item.severity)).map((item) => item.skillName)).size;
|
|
6
|
+
const skillAnchor = (skillName) => `skill-${skillName.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown'}`;
|
|
7
|
+
const renderJson = (value) => `<pre style="margin:8px 0 0;padding:10px;background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;white-space:pre-wrap;word-break:break-word;font-size:11px;line-height:1.45;max-height:520px;overflow:auto;text-align:left">${e(JSON.stringify(value, null, 2))}</pre>`;
|
|
8
|
+
const renderField = (label, value) => {
|
|
9
|
+
if (value == null || value === '')
|
|
10
|
+
return '';
|
|
11
|
+
return `<div style="margin:4px 0;text-align:left"><span style="color:var(--text-muted);font-size:11px">${e(label)}</span><div style="font-family:ui-monospace,monospace;font-size:11px;word-break:break-all;text-align:left;color:var(--text-secondary)">${e(String(value))}</div></div>`;
|
|
12
|
+
};
|
|
13
|
+
const renderArtifactVersion = (value) => {
|
|
14
|
+
if (value === 'unknown') {
|
|
15
|
+
return `<div style="margin:4px 0;text-align:left"><span style="color:var(--text-muted);font-size:11px">artifactVersion</span><div style="font-family:ui-monospace,monospace;font-size:11px;word-break:break-all;color:var(--yellow);font-weight:600;text-align:left">⚠ unknown</div></div>`;
|
|
16
|
+
}
|
|
17
|
+
return renderField('artifactVersion', value);
|
|
18
|
+
};
|
|
19
|
+
const reviewSeverityMeta = (item) => {
|
|
20
|
+
if (item.severity === 'high') {
|
|
21
|
+
return { label: '高风险/需关注', decision: '优先看,可能要补 SKILL.md 或改 skill 说明', color: 'var(--red)', bg: 'rgba(220,38,38,.08)' };
|
|
22
|
+
}
|
|
23
|
+
if (item.severity === 'medium') {
|
|
24
|
+
return { label: '低风险/抽样确认', decision: '通常不需要改 skill;抽样确认是否反复浪费时间', color: 'var(--yellow)', bg: 'rgba(202,138,4,.10)' };
|
|
25
|
+
}
|
|
26
|
+
if (item.severity === 'low') {
|
|
27
|
+
return { label: '不确定/低优先级', decision: '模型只是说不确定,不一定需要改 skill', color: 'var(--accent)', bg: 'rgba(37,99,235,.08)' };
|
|
28
|
+
}
|
|
29
|
+
return { label: '无异常/无需改 skill', decision: '更像路径、权限、文件太大或工具限制;先不当成 skill 内容缺失', color: 'var(--text-muted)', bg: 'var(--bg-muted)' };
|
|
30
|
+
};
|
|
31
|
+
const renderSeverityBadge = (item) => {
|
|
32
|
+
const meta = reviewSeverityMeta(item);
|
|
33
|
+
return `<div style="display:flex;flex-direction:column;gap:3px">
|
|
34
|
+
<span style="display:inline-flex;align-items:center;width:max-content;max-width:190px;padding:3px 7px;border-radius:999px;background:${meta.bg};color:${meta.color};font-weight:650;white-space:normal">${e(meta.label)}</span>
|
|
35
|
+
<span style="color:var(--text-muted);font-size:11px">${e(meta.decision)}</span>
|
|
36
|
+
</div>`;
|
|
37
|
+
};
|
|
38
|
+
const semanticEvidence = (item) => {
|
|
39
|
+
const tool = item.evidence.tool || 'Tool';
|
|
40
|
+
const target = item.evidence.query || item.evidence.path || item.evidence.assistantSnippet || '';
|
|
41
|
+
if (item.signalSubtype === 'tool_limit')
|
|
42
|
+
return `${tool} 触发工具限制:${item.evidence.outputSnippet || target}`;
|
|
43
|
+
if (item.signalSubtype === 'transient_file_missing')
|
|
44
|
+
return `${tool} 访问了临时文件但文件不存在:${item.evidence.path || target}`;
|
|
45
|
+
if (item.signalSubtype === 'skill_asset_read_failed')
|
|
46
|
+
return `${tool} 读取该 skill 自身资源失败:${item.evidence.path || target}`;
|
|
47
|
+
if (item.signalSubtype === 'not_found')
|
|
48
|
+
return `${tool} 访问了不存在的路径:${item.evidence.path || target}`;
|
|
49
|
+
if (item.signalSubtype === 'permission_denied')
|
|
50
|
+
return `${tool} 被权限拒绝:${item.evidence.path || target}`;
|
|
51
|
+
if (item.signalSubtype === 'bash_probe')
|
|
52
|
+
return `skill 运行过程中,agent 调用了一条 Bash 命令:${item.evidence.query || target}`;
|
|
53
|
+
if (item.signalSubtype === 'hard_miss')
|
|
54
|
+
return `${tool} 未命中且后续未找到同主题成功证据:${target}`;
|
|
55
|
+
if (item.signalSubtype === 'exploratory_miss')
|
|
56
|
+
return `${tool} 前序未命中,但后续有成功搜索证据:${target}`;
|
|
57
|
+
return target || item.evidence.outputSnippet || '';
|
|
58
|
+
};
|
|
59
|
+
const evidenceConclusion = (item) => {
|
|
60
|
+
if (item.signalSubtype === 'bash_probe')
|
|
61
|
+
return 'skill 运行过程中,agent 调用了一条 Bash 命令。';
|
|
62
|
+
if (item.signalSubtype === 'tool_limit')
|
|
63
|
+
return `${item.evidence.tool || '工具'} 触发了文件太长、token 或超时限制。`;
|
|
64
|
+
if (item.signalSubtype === 'transient_file_missing')
|
|
65
|
+
return `${item.evidence.tool || '工具'} 访问了临时文件,但文件不存在。`;
|
|
66
|
+
if (item.signalSubtype === 'skill_asset_read_failed')
|
|
67
|
+
return `${item.evidence.tool || '工具'} 读取该 skill 自身资源失败。`;
|
|
68
|
+
if (item.signalSubtype === 'not_found')
|
|
69
|
+
return `${item.evidence.tool || '工具'} 访问了不存在的路径。`;
|
|
70
|
+
if (item.signalSubtype === 'permission_denied')
|
|
71
|
+
return `${item.evidence.tool || '工具'} 被权限拒绝。`;
|
|
72
|
+
if (item.signalSubtype === 'hard_miss')
|
|
73
|
+
return `${item.evidence.tool || '工具'} 没有拿到有效结果,后续也没有看到同主题成功证据。`;
|
|
74
|
+
if (item.signalSubtype === 'exploratory_miss')
|
|
75
|
+
return `${item.evidence.tool || '工具'} 前面没有拿到结果,但后续找到了相关内容。`;
|
|
76
|
+
if (item.signalType === 'hedging')
|
|
77
|
+
return '模型文本里出现了不确定表达。';
|
|
78
|
+
if (item.signalType === 'explicit_marker')
|
|
79
|
+
return '模型文本里出现了显式标记。';
|
|
80
|
+
return semanticEvidence(item);
|
|
81
|
+
};
|
|
82
|
+
const evidenceQuote = (item) => {
|
|
83
|
+
return item.evidence.query || item.evidence.path || item.evidence.assistantSnippet || item.evidence.outputSnippet || '';
|
|
84
|
+
};
|
|
85
|
+
const quoteFromEvidence = (evidence) => evidence.query || evidence.path || evidence.assistantSnippet || evidence.outputSnippet || '';
|
|
86
|
+
const renderEvidenceDetailList = (item) => {
|
|
87
|
+
const entries = item.representativeEvidence.length > 0 ? item.representativeEvidence : [item.evidence];
|
|
88
|
+
return `<div style="display:flex;flex-direction:column;gap:8px">
|
|
89
|
+
${entries.map((ev, i) => {
|
|
90
|
+
const quote = quoteFromEvidence(ev);
|
|
91
|
+
const output = ev.outputSnippet && ev.outputSnippet !== quote ? ev.outputSnippet : '';
|
|
92
|
+
return `<div style="padding:9px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-surface);text-align:left">
|
|
93
|
+
<div style="display:flex;justify-content:space-between;gap:10px;align-items:center;margin-bottom:5px">
|
|
94
|
+
<span style="font-size:12px;color:var(--text-primary);font-weight:650">Evidence ${i + 1}</span>
|
|
95
|
+
<span style="font-size:11px;color:var(--text-muted);font-family:ui-monospace,monospace">${e(ev.tool || item.signalType)}${ev.markerToken ? ` · ${e(ev.markerToken)}` : ''}</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div style="font-size:12px;color:var(--text-secondary);line-height:1.45;margin-bottom:5px">${e(evidenceConclusion({ ...item, evidence: ev }))}</div>
|
|
98
|
+
${quote ? `<pre style="margin:0;padding:8px;background:var(--bg-muted);border-radius:5px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,monospace;font-size:11px;line-height:1.45;color:var(--text-primary);max-height:180px;overflow:auto">${e(quote)}</pre>` : ''}
|
|
99
|
+
${output ? `<div style="margin-top:6px;color:var(--text-muted);font-size:11px">输出片段</div><pre style="margin:3px 0 0;padding:8px;background:var(--bg-muted);border-radius:5px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,monospace;font-size:11px;line-height:1.45;color:var(--text-secondary);max-height:140px;overflow:auto">${e(output)}</pre>` : ''}
|
|
100
|
+
</div>`;
|
|
101
|
+
}).join('')}
|
|
102
|
+
</div>`;
|
|
103
|
+
};
|
|
104
|
+
const renderEvidenceCell = (item, max = 220) => {
|
|
105
|
+
const quote = evidenceQuote(item);
|
|
106
|
+
return `<div style="max-width:360px">
|
|
107
|
+
<div style="font-size:12px;line-height:1.45;color:var(--text-primary)">${e(evidenceConclusion(item))}</div>
|
|
108
|
+
${quote ? `<code style="display:block;margin-top:5px;font-family:ui-monospace,monospace;font-size:11px;line-height:1.45;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background:var(--bg-muted);padding:4px 6px;border-radius:4px">${e(quote.slice(0, max))}</code>` : ''}
|
|
109
|
+
</div>`;
|
|
110
|
+
};
|
|
111
|
+
const signalRuleDescription = (item) => {
|
|
112
|
+
const reason = severityReasonFor(item, lang);
|
|
113
|
+
const meta = reviewSeverityMeta(item);
|
|
114
|
+
const prefix = `${meta.label}: ${item.signalType}/${item.signalSubtype}, confidence=${item.confidence.toFixed(2)}.`;
|
|
115
|
+
return `${prefix} ${reason}`;
|
|
116
|
+
};
|
|
117
|
+
const renderSignalLabel = (item) => {
|
|
118
|
+
const desc = signalRuleDescription(item);
|
|
119
|
+
return `<span style="position:relative;display:inline-flex;align-items:center;gap:4px;overflow:visible">
|
|
120
|
+
<span>${e(item.signalType)}</span>
|
|
121
|
+
<span class="signal-help" tabindex="0" data-signal-title="${e(`${item.signalType}/${item.signalSubtype}`)}" data-signal-description="${e(desc)}" aria-label="${e(desc)}" style="display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;border:1px solid var(--border);border-radius:50%;font-size:10px;line-height:15px;color:var(--text-muted);cursor:help;vertical-align:top">?</span>
|
|
122
|
+
</span><br><span style="color:var(--text-muted);font-size:11px">OMK subtype: ${e(item.signalSubtype)}</span>`;
|
|
123
|
+
};
|
|
124
|
+
const renderOccurrences = (item) => {
|
|
125
|
+
const text = `出现次数=${item.occurrences}。这是按 skill + cwd + signal + subtype + query/path 归一化去重后的聚合次数,不是样本量。`;
|
|
126
|
+
return `<div style="text-align:right">
|
|
127
|
+
<div style="font-weight:700">${item.occurrences}</div>
|
|
128
|
+
<div title="${e(text)}" style="color:var(--text-muted);font-size:11px;line-height:1.35;max-width:92px;margin-left:auto">同类事件次数</div>
|
|
129
|
+
</div>`;
|
|
130
|
+
};
|
|
131
|
+
const renderSourceBadge = (item) => {
|
|
132
|
+
const label = item.sourceKind === 'markdown_log' ? 'Markdown log' : item.sourceKind === 'claude' ? 'Claude' : 'Unknown';
|
|
133
|
+
const color = item.sourceKind === 'markdown_log' ? 'var(--green)' : item.sourceKind === 'claude' ? 'var(--accent)' : 'var(--text-muted)';
|
|
134
|
+
return `<span title="调用日志来源:${e(label)}" style="display:inline-flex;width:max-content;margin-top:4px;padding:2px 6px;border-radius:999px;background:var(--bg-muted);color:${color};font-size:11px;font-weight:650">${e(label)}</span>`;
|
|
135
|
+
};
|
|
136
|
+
const confidenceHeaderHelp = '判断把握:OMK 对“这条 过程发现 是否需要处理/是否高风险/需关注”的规则判断有多确定。归属把握:OMK 把这条 过程发现 归到当前 skill 名下有多确定,例如明确调用 skill 通常更高。';
|
|
137
|
+
const renderConfidenceHeader = (padding = '8px 10px', border = '1px solid var(--border)') => `<th style="text-align:right;padding:${padding};border-bottom:${border}">
|
|
138
|
+
判断把握 / 归属把握
|
|
139
|
+
<span class="signal-help" tabindex="0" data-signal-title="判断把握 / 归属把握" data-signal-description="${e(confidenceHeaderHelp)}" aria-label="${e(confidenceHeaderHelp)}" style="display:inline-flex;align-items:center;justify-content:center;margin-left:4px;width:15px;height:15px;border:1px solid var(--border);border-radius:50%;font-size:10px;line-height:15px;color:var(--text-muted);cursor:help;vertical-align:middle">?</span>
|
|
140
|
+
</th>`;
|
|
141
|
+
const renderReviewRows = (groupItems, idPrefix) => groupItems.map((item, index) => {
|
|
142
|
+
const evidence = semanticEvidence(item);
|
|
143
|
+
const detailsId = `${idPrefix}-detail-${index}`;
|
|
144
|
+
const searchText = [item.severity, item.sourceKind, item.skillName, item.signalType, item.signalSubtype, evidence, item.cwd, item.sourceTrace].join(' ').toLowerCase();
|
|
145
|
+
return `<tr data-observe-row="review" data-severity="${e(item.severity)}" data-search="${e(searchText)}" data-detail-id="${detailsId}">
|
|
146
|
+
<td style="padding:8px 10px;min-width:180px">${renderSeverityBadge(item)}${renderSourceBadge(item)}</td>
|
|
147
|
+
<td style="padding:8px 10px">${renderSignalLabel(item)}</td>
|
|
148
|
+
<td class="num" style="padding:8px 10px;text-align:right">${renderOccurrences(item)}</td>
|
|
149
|
+
<td class="num" style="padding:8px 10px;text-align:right">${item.confidence.toFixed(2)} / ${item.attributionConfidence.toFixed(2)}</td>
|
|
150
|
+
<td style="padding:8px 10px;color:var(--text-muted);font-size:12px">${item.lastSeen.slice(0, 19).replace('T', ' ')}</td>
|
|
151
|
+
<td style="padding:8px 10px">${renderEvidenceCell(item)}</td>
|
|
152
|
+
<td class="num" style="padding:8px 10px;text-align:right"><button type="button" onclick="toggleObservationDetail('${detailsId}', this)" style="font-size:12px;padding:4px 8px;border:1px solid var(--border);background:var(--bg);border-radius:4px;cursor:pointer">${lang === 'zh' ? '展开' : 'Details'}</button></td>
|
|
153
|
+
</tr>
|
|
154
|
+
<tr id="${detailsId}" data-observe-detail-for="${detailsId}" style="display:none;background:var(--bg-muted)">
|
|
155
|
+
<td colspan="7" style="padding:14px 18px;border-bottom:1px solid var(--border);text-align:left">
|
|
156
|
+
<div style="display:grid;grid-template-columns:minmax(0,0.75fr) minmax(0,1.55fr);gap:18px;align-items:start;max-width:100%;overflow:hidden">
|
|
157
|
+
<section style="text-align:left;font-size:11px;color:var(--text-muted);min-width:0">
|
|
158
|
+
<h3 style="font-size:12px;margin:0 0 8px;color:var(--text-secondary)">这条记录的来源和判断依据</h3>
|
|
159
|
+
${renderField('id', item.id)}
|
|
160
|
+
${renderField('sourceTrace', item.sourceTrace)}
|
|
161
|
+
${renderField('sourceKind', item.sourceKind)}
|
|
162
|
+
${renderField('sessionId', item.sessionId)}
|
|
163
|
+
${renderField('cwd', item.cwd)}
|
|
164
|
+
${renderArtifactVersion(item.artifactVersion)}
|
|
165
|
+
${renderField('signalRule', signalRuleDescription(item))}
|
|
166
|
+
${renderField('signalMeaning', `原始信号=${item.signalType};OMK 判断出的失败原因=${item.signalSubtype}。例如 failed_search 表示工具搜索/读取失败,bash_probe/not_found/tool_limit 是 OMK 根据命令和输出内容进一步判断出来的原因。`)}
|
|
167
|
+
${renderField('occurrencesMeaning', `出现次数=${item.occurrences};按 skill/cwd/signal/subtype/query/path 聚合去重后的同类事件数量。`)}
|
|
168
|
+
${renderField('reviewDecision', reviewSeverityMeta(item).decision)}
|
|
169
|
+
${renderField('firstSeen', item.firstSeen)}
|
|
170
|
+
${renderField('lastSeen', item.lastSeen)}
|
|
171
|
+
${renderField('recentSessionIds', item.recentSessionIds.join(', '))}
|
|
172
|
+
<button type="button" onclick="openObservationTrace('${e(item.id)}', this)" style="margin-top:8px;font-size:12px;padding:5px 8px;border:1px solid var(--border);background:var(--bg);border-radius:4px;cursor:pointer">Open in trace</button>
|
|
173
|
+
<pre id="trace-${e(item.id)}" style="display:none;margin:8px 0 0;padding:9px;background:var(--bg-muted);border:1px solid var(--border);border-radius:6px;white-space:pre-wrap;word-break:break-word;font-size:11px;line-height:1.45;max-height:360px;overflow:auto;text-align:left"></pre>
|
|
174
|
+
</section>
|
|
175
|
+
<section style="text-align:left;min-width:0">
|
|
176
|
+
<h3 style="font-size:13px;margin:0 0 8px;color:var(--text-primary)">Evidence 明细</h3>
|
|
177
|
+
<div style="color:var(--text-muted);font-size:12px;margin-bottom:8px">这里展示这条聚合记录下面的原始明细。列表里每一条都是一次真实命中的证据;上方表格只展示第一条摘要。</div>
|
|
178
|
+
${renderEvidenceDetailList(item)}
|
|
179
|
+
<details style="margin-top:10px">
|
|
180
|
+
<summary style="cursor:pointer;color:var(--text-muted);font-size:12px">查看原始 JSON</summary>
|
|
181
|
+
${renderJson({ evidence: item.evidence, representativeEvidence: item.representativeEvidence })}
|
|
182
|
+
</details>
|
|
183
|
+
</section>
|
|
184
|
+
</div>
|
|
185
|
+
</td>
|
|
186
|
+
</tr>`;
|
|
187
|
+
}).join('');
|
|
188
|
+
const skillGroups = Array.from(items.reduce((map, item) => {
|
|
189
|
+
const existing = map.get(item.skillName) ?? [];
|
|
190
|
+
existing.push(item);
|
|
191
|
+
map.set(item.skillName, existing);
|
|
192
|
+
return map;
|
|
193
|
+
}, new Map()).entries())
|
|
194
|
+
.sort((a, b) => {
|
|
195
|
+
const aHigh = a[1].filter((item) => item.severity === 'high').length;
|
|
196
|
+
const bHigh = b[1].filter((item) => item.severity === 'high').length;
|
|
197
|
+
if (bHigh !== aHigh)
|
|
198
|
+
return bHigh - aHigh;
|
|
199
|
+
return b[1].length - a[1].length;
|
|
200
|
+
});
|
|
201
|
+
const severityOrder = ['high', 'medium', 'low', 'noise'];
|
|
202
|
+
const severityHeading = {
|
|
203
|
+
high: '高风险/需关注',
|
|
204
|
+
medium: '低风险/抽样确认',
|
|
205
|
+
low: '不确定/低优先级',
|
|
206
|
+
noise: '无异常/无需改 skill',
|
|
207
|
+
};
|
|
208
|
+
const severityHeadingZh = {
|
|
209
|
+
high: '异常需关注',
|
|
210
|
+
medium: '低风险:Bash 探测',
|
|
211
|
+
low: '不确定信号',
|
|
212
|
+
noise: '无异常:路径/权限/工具限制',
|
|
213
|
+
};
|
|
214
|
+
const skillSections = skillGroups.map(([skillName, groupItems], groupIndex) => {
|
|
215
|
+
const counts = {
|
|
216
|
+
high: groupItems.filter((item) => item.severity === 'high').length,
|
|
217
|
+
medium: groupItems.filter((item) => item.severity === 'medium').length,
|
|
218
|
+
low: groupItems.filter((item) => item.severity === 'low').length,
|
|
219
|
+
noise: groupItems.filter((item) => item.severity === 'noise').length,
|
|
220
|
+
};
|
|
221
|
+
const latestObservation = groupItems.reduce((value, item) => item.lastSeen > value ? item.lastSeen : value, '');
|
|
222
|
+
const latest = latestObservation || skillInvocationLastSeen[skillName] || '';
|
|
223
|
+
const invocationCount = skillInvocationCounts[skillName] ?? groupItems.reduce((sum, item) => sum + item.occurrences, 0);
|
|
224
|
+
const sessionCount = skillSessionCounts[skillName] ?? new Set(groupItems.flatMap((item) => item.recentSessionIds)).size;
|
|
225
|
+
const sourceKinds = Array.from(new Set(groupItems.map((item) => item.sourceKind))).sort();
|
|
226
|
+
const searchText = groupItems.map((item) => [item.sourceKind, item.skillName, item.signalType, item.signalSubtype, semanticEvidence(item), item.cwd, item.sourceTrace].join(' ')).join(' ').toLowerCase();
|
|
227
|
+
const buckets = severityOrder.map((severity) => {
|
|
228
|
+
const bucketItems = groupItems.filter((item) => item.severity === severity);
|
|
229
|
+
if (bucketItems.length === 0)
|
|
230
|
+
return '';
|
|
231
|
+
return `<section data-observe-skill-bucket="${e(severity)}" style="margin-top:12px">
|
|
232
|
+
<h3 style="font-size:13px;margin:0 0 6px;color:var(--text-primary)">${e(severityHeading[severity])} <span style="color:var(--text-muted);font-weight:400">/ ${e(severityHeadingZh[severity])} (${bucketItems.length})</span></h3>
|
|
233
|
+
<div style="width:100%;overflow-x:auto;overflow-y:visible">
|
|
234
|
+
<table style="border-collapse:collapse;width:100%;min-width:940px;font-size:13px;table-layout:fixed">
|
|
235
|
+
<colgroup>
|
|
236
|
+
<col style="width:190px">
|
|
237
|
+
<col style="width:160px">
|
|
238
|
+
<col style="width:96px">
|
|
239
|
+
<col style="width:126px">
|
|
240
|
+
<col style="width:132px">
|
|
241
|
+
<col style="width:auto">
|
|
242
|
+
<col style="width:82px">
|
|
243
|
+
</colgroup>
|
|
244
|
+
<thead><tr>
|
|
245
|
+
<th style="text-align:left;padding:8px 10px;border-bottom:1px solid var(--border)">Severity</th>
|
|
246
|
+
<th style="text-align:left;padding:8px 10px;border-bottom:1px solid var(--border)">Signal</th>
|
|
247
|
+
<th style="text-align:right;padding:8px 10px;border-bottom:1px solid var(--border)">出现次数<br><span style="color:var(--text-muted);font-weight:400;font-size:11px">dedup 后同类事件</span></th>
|
|
248
|
+
${renderConfidenceHeader()}
|
|
249
|
+
<th style="text-align:left;padding:8px 10px;border-bottom:1px solid var(--border)">Last seen</th>
|
|
250
|
+
<th style="text-align:left;padding:8px 10px;border-bottom:1px solid var(--border)">Evidence</th>
|
|
251
|
+
<th style="text-align:right;padding:8px 10px;border-bottom:1px solid var(--border)">Review</th>
|
|
252
|
+
</tr></thead>
|
|
253
|
+
<tbody>${renderReviewRows(bucketItems, `obs-skill-${groupIndex}-${severity}`)}</tbody>
|
|
254
|
+
</table>
|
|
255
|
+
</div>
|
|
256
|
+
</section>`;
|
|
257
|
+
}).join('');
|
|
258
|
+
return `<details id="${e(skillAnchor(skillName))}" data-observe-skill-group data-search="${e(searchText)}" open style="margin-top:14px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);overflow:visible;scroll-margin-top:16px">
|
|
259
|
+
<summary style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;cursor:pointer;list-style:none">
|
|
260
|
+
<div>
|
|
261
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
262
|
+
<span style="font-family:ui-monospace,monospace;font-size:14px;font-weight:700">${e(skillName)}</span>
|
|
263
|
+
<span title="这个 skill 在当前 trace 里被归因/触发的次数" style="padding:2px 7px;border-radius:999px;background:var(--info-bg);color:var(--accent);font-size:12px;font-weight:650">调用 ${invocationCount} 次</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div style="color:var(--text-muted);font-size:12px;margin-top:3px">${groupItems.length} 过程发现 · ${sessionCount} sessions · source ${e(sourceKinds.join(', '))} · latest ${e(latest.slice(0, 19).replace('T', ' '))}</div>
|
|
266
|
+
</div>
|
|
267
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">
|
|
268
|
+
<span title="高风险/需关注:优先看,可能需要补 SKILL.md 或改 skill 说明" style="padding:3px 7px;border-radius:999px;background:rgba(220,38,38,.08);color:var(--red);font-size:12px">高风险/需关注 ${counts.high}</span>
|
|
269
|
+
<span title="低风险/抽样确认:通常不需要改 skill;抽样确认是否反复浪费时间" style="padding:3px 7px;border-radius:999px;background:rgba(202,138,4,.10);color:var(--yellow);font-size:12px">低风险 ${counts.medium}</span>
|
|
270
|
+
<span title="不确定/低优先级:模型只是说不确定,不一定需要改 skill" style="padding:3px 7px;border-radius:999px;background:rgba(37,99,235,.08);color:var(--accent);font-size:12px">说不确定 ${counts.low}</span>
|
|
271
|
+
<span title="无异常/无需改 skill:更像文件不存在、文件太大、权限或工具限制" style="padding:3px 7px;border-radius:999px;background:var(--bg-muted);color:var(--text-muted);font-size:12px">路径/工具问题 ${counts.noise}</span>
|
|
272
|
+
</div>
|
|
273
|
+
</summary>
|
|
274
|
+
<div style="padding:0 14px 14px">${buckets}</div>
|
|
275
|
+
</details>`;
|
|
276
|
+
}).join('');
|
|
277
|
+
const itemsBySkill = allItems.reduce((map, item) => {
|
|
278
|
+
const existing = map.get(item.skillName) ?? [];
|
|
279
|
+
existing.push(item);
|
|
280
|
+
map.set(item.skillName, existing);
|
|
281
|
+
return map;
|
|
282
|
+
}, new Map());
|
|
283
|
+
const allSkillNames = Array.from(new Set([
|
|
284
|
+
...Object.keys(skillInvocationCounts),
|
|
285
|
+
...Array.from(itemsBySkill.keys()),
|
|
286
|
+
]));
|
|
287
|
+
const skillRollups = allSkillNames.map((skillName) => {
|
|
288
|
+
const groupItems = itemsBySkill.get(skillName) ?? [];
|
|
289
|
+
const counts = {
|
|
290
|
+
high: groupItems.filter((item) => item.severity === 'high').length,
|
|
291
|
+
medium: groupItems.filter((item) => item.severity === 'medium').length,
|
|
292
|
+
low: groupItems.filter((item) => item.severity === 'low').length,
|
|
293
|
+
noise: groupItems.filter((item) => item.severity === 'noise').length,
|
|
294
|
+
};
|
|
295
|
+
const invocationCount = skillInvocationCounts[skillName] ?? groupItems.reduce((sum, item) => sum + item.occurrences, 0);
|
|
296
|
+
const sessionCount = skillSessionCounts[skillName] ?? new Set(groupItems.flatMap((item) => item.recentSessionIds)).size;
|
|
297
|
+
const lastProblemSeen = groupItems.reduce((value, item) => item.lastSeen > value ? item.lastSeen : value, '');
|
|
298
|
+
const lastUsed = skillInvocationLastSeen[skillName] || lastProblemSeen || '';
|
|
299
|
+
const sources = Array.from(new Set(groupItems.map((item) => item.sourceKind))).sort();
|
|
300
|
+
const observationCount = groupItems.length;
|
|
301
|
+
const toolCounts = skillToolCallCounts[skillName] ?? {};
|
|
302
|
+
const metricCounts = {
|
|
303
|
+
bash: toolCounts.Bash ?? 0,
|
|
304
|
+
read: toolCounts.Read ?? 0,
|
|
305
|
+
grep: toolCounts.Grep ?? 0,
|
|
306
|
+
uncertainty: groupItems.filter((item) => item.signalType === 'hedging').reduce((sum, item) => sum + item.occurrences, 0),
|
|
307
|
+
explicitMarker: groupItems.filter((item) => item.signalType === 'explicit_marker').reduce((sum, item) => sum + item.occurrences, 0),
|
|
308
|
+
bashProbe: groupItems.filter((item) => item.signalSubtype === 'bash_probe').reduce((sum, item) => sum + item.occurrences, 0),
|
|
309
|
+
notFound: groupItems.filter((item) => item.signalSubtype === 'not_found').reduce((sum, item) => sum + item.occurrences, 0),
|
|
310
|
+
toolLimit: groupItems.filter((item) => item.signalSubtype === 'tool_limit').reduce((sum, item) => sum + item.occurrences, 0),
|
|
311
|
+
toolFailure: groupItems.filter((item) => item.signalSubtype === 'tool_failure').reduce((sum, item) => sum + item.occurrences, 0),
|
|
312
|
+
};
|
|
313
|
+
const actionableCount = counts.high;
|
|
314
|
+
const reviewLabel = actionableCount > 0
|
|
315
|
+
? '高风险'
|
|
316
|
+
: counts.medium > 0
|
|
317
|
+
? '低风险'
|
|
318
|
+
: counts.noise > 0
|
|
319
|
+
? '无异常'
|
|
320
|
+
: '无异常';
|
|
321
|
+
const reviewColor = actionableCount > 0
|
|
322
|
+
? 'var(--red)'
|
|
323
|
+
: counts.medium > 0
|
|
324
|
+
? 'var(--yellow)'
|
|
325
|
+
: counts.noise > 0
|
|
326
|
+
? 'var(--text-muted)'
|
|
327
|
+
: 'var(--green)';
|
|
328
|
+
return {
|
|
329
|
+
skillName,
|
|
330
|
+
invocationCount,
|
|
331
|
+
sessionCount,
|
|
332
|
+
observationCount,
|
|
333
|
+
counts,
|
|
334
|
+
lastProblemSeen,
|
|
335
|
+
lastUsed,
|
|
336
|
+
sources,
|
|
337
|
+
metricCounts,
|
|
338
|
+
reviewLabel,
|
|
339
|
+
reviewColor,
|
|
340
|
+
};
|
|
341
|
+
}).sort((a, b) => {
|
|
342
|
+
const aRisk = a.counts.high * 100 + a.counts.medium * 10 + a.counts.noise;
|
|
343
|
+
const bRisk = b.counts.high * 100 + b.counts.medium * 10 + b.counts.noise;
|
|
344
|
+
if (bRisk !== aRisk)
|
|
345
|
+
return bRisk - aRisk;
|
|
346
|
+
return b.invocationCount - a.invocationCount;
|
|
347
|
+
});
|
|
348
|
+
const skillRollupRows = skillRollups.map((row) => {
|
|
349
|
+
const hasDetail = (itemsBySkill.get(row.skillName)?.length ?? 0) > 0;
|
|
350
|
+
return `<tr data-observe-rollup-row data-skill-anchor="${e(skillAnchor(row.skillName))}" style="${hasDetail ? 'cursor:pointer' : ''}">
|
|
351
|
+
<td style="padding:9px 10px;font-family:ui-monospace,monospace;font-weight:650;word-break:break-all">${e(row.skillName)}</td>
|
|
352
|
+
<td class="num" style="padding:9px 10px;text-align:right;font-weight:700">${row.invocationCount}</td>
|
|
353
|
+
<td class="num" style="padding:9px 10px;text-align:right">${row.sessionCount}</td>
|
|
354
|
+
<td class="num" style="padding:9px 10px;text-align:right">${row.observationCount}</td>
|
|
355
|
+
<td style="padding:9px 10px;color:var(--text-secondary);font-size:12px;line-height:1.55">
|
|
356
|
+
<span title="完整 trace 中这个 skill 运行时调用 Bash 工具的次数">Bash调用 ${row.metricCounts.bash}</span> ·
|
|
357
|
+
<span title="完整 trace 中这个 skill 运行时调用 Read 工具的次数">Read ${row.metricCounts.read}</span> ·
|
|
358
|
+
<span title="完整 trace 中这个 skill 运行时调用 Grep 工具的次数">Grep ${row.metricCounts.grep}</span><br>
|
|
359
|
+
<span title="过程发现 中,agent 的回答出现“不确定、可能、需要确认”等表达的次数">回答不确定 ${row.metricCounts.uncertainty}</span> ·
|
|
360
|
+
<span title="过程发现 中,agent 明确说“未知/知识缺口/没覆盖”等内容的次数">明确说缺口 ${row.metricCounts.explicitMarker}</span> ·
|
|
361
|
+
<span title="过程发现 中,Bash 命令看起来是在试目录、试路径或探测环境的次数">Bash试探 ${row.metricCounts.bashProbe}</span><br>
|
|
362
|
+
<span title="过程发现 中,访问的文件或路径不存在的次数">路径不存在 ${row.metricCounts.notFound}</span> ·
|
|
363
|
+
<span title="过程发现 中,文件太长、token 上限或超时这类工具限制次数">工具限制 ${row.metricCounts.toolLimit}</span> ·
|
|
364
|
+
<span title="过程发现 中,工具调用失败但不属于路径不存在或工具限制的次数">工具失败 ${row.metricCounts.toolFailure}</span>
|
|
365
|
+
</td>
|
|
366
|
+
<td class="num" style="padding:9px 10px;text-align:right;color:var(--red);font-weight:650">${row.counts.high}</td>
|
|
367
|
+
<td class="num" style="padding:9px 10px;text-align:right;color:var(--yellow);font-weight:650">${row.counts.medium + row.counts.low}</td>
|
|
368
|
+
<td class="num" style="padding:9px 10px;text-align:right;color:var(--text-muted)">${row.counts.noise}</td>
|
|
369
|
+
<td style="padding:9px 10px;color:var(--text-muted);font-size:12px" title="${row.lastProblemSeen ? '最近一次产生 过程发现 的时间' : '当前没有发现问题或旧 report 没有 过程发现 时间'}">${row.lastProblemSeen ? e(row.lastProblemSeen.slice(0, 19).replace('T', ' ')) : '—'}</td>
|
|
370
|
+
<td style="padding:9px 10px;color:var(--text-muted);font-size:12px" title="${row.lastUsed ? '最近一次在 trace 中识别到 skill 调用的时间' : '当前 过程发现 report 中没有这个 skill 的调用时间信息;旧 report 需要重新 ingest 才会补齐'}">${row.lastUsed ? e(row.lastUsed.slice(0, 19).replace('T', ' ')) : '—'}</td>
|
|
371
|
+
<td style="padding:9px 10px;color:var(--text-muted);font-size:12px">${row.sources.length > 0 ? e(row.sources.join(', ')) : '—'}</td>
|
|
372
|
+
<td style="padding:9px 10px;text-align:right">
|
|
373
|
+
<span style="display:inline-flex;width:max-content;padding:3px 7px;border-radius:999px;background:var(--bg-muted);color:${row.reviewColor};font-size:12px;font-weight:650">${e(row.reviewLabel)}</span>
|
|
374
|
+
</td>
|
|
375
|
+
</tr>`;
|
|
376
|
+
}).join('');
|
|
377
|
+
const actionItems = skillRollups
|
|
378
|
+
.filter((row) => row.observationCount > 0)
|
|
379
|
+
.map((row) => {
|
|
380
|
+
const groupItems = itemsBySkill.get(row.skillName) ?? [];
|
|
381
|
+
const repeatedMedium = groupItems
|
|
382
|
+
.filter((item) => item.severity === 'medium')
|
|
383
|
+
.reduce((sum, item) => sum + item.occurrences, 0);
|
|
384
|
+
const noiseCount = groupItems
|
|
385
|
+
.filter((item) => item.severity === 'noise')
|
|
386
|
+
.reduce((sum, item) => sum + item.occurrences, 0);
|
|
387
|
+
let priority = 'P2';
|
|
388
|
+
let action = '打开明细看 1-2 条证据';
|
|
389
|
+
let reason = '这类记录说明运行中出现过异常信号,但现在还不能直接判断 skill 需要修改。';
|
|
390
|
+
let color = 'var(--text-muted)';
|
|
391
|
+
if (row.counts.high > 0) {
|
|
392
|
+
priority = 'P0';
|
|
393
|
+
action = '先看这个 skill 是否漏写了关键信息';
|
|
394
|
+
reason = '有高风险记录:agent 查找失败后,没有看到它在同一轮里找到替代结果,或回答里明确暴露了缺口。';
|
|
395
|
+
color = 'var(--red)';
|
|
396
|
+
}
|
|
397
|
+
else if (repeatedMedium >= 3) {
|
|
398
|
+
priority = 'P1';
|
|
399
|
+
action = '看是否要补一段“推荐查找路径”';
|
|
400
|
+
reason = `agent 在这个 skill 运行时反复试目录或路径,共 ${repeatedMedium} 次。单次不用改,但反复出现可能说明 skill 没告诉它该优先看哪里。`;
|
|
401
|
+
color = 'var(--yellow)';
|
|
402
|
+
}
|
|
403
|
+
else if (row.counts.medium > 0 || row.counts.low > 0) {
|
|
404
|
+
priority = 'P2';
|
|
405
|
+
action = '抽几条看看是否真的影响使用';
|
|
406
|
+
reason = '当前主要是低风险记录:可能只是 agent 正常探索路径,或回答里出现了不确定表达。先看样例,不要直接改 skill。';
|
|
407
|
+
color = 'var(--yellow)';
|
|
408
|
+
}
|
|
409
|
+
else if (noiseCount > 0) {
|
|
410
|
+
priority = 'P3';
|
|
411
|
+
action = '暂时不用改 skill';
|
|
412
|
+
reason = '当前只有文件不存在、权限、文件过大或工具失败这类记录。它们更像运行环境或工具限制,默认不作为 skill 修改依据。';
|
|
413
|
+
color = 'var(--text-muted)';
|
|
414
|
+
}
|
|
415
|
+
const topItem = groupItems[0];
|
|
416
|
+
return {
|
|
417
|
+
skillName: row.skillName,
|
|
418
|
+
priority,
|
|
419
|
+
action,
|
|
420
|
+
reason,
|
|
421
|
+
color,
|
|
422
|
+
evidenceCount: groupItems.reduce((sum, item) => sum + item.occurrences, 0),
|
|
423
|
+
anchor: skillAnchor(row.skillName),
|
|
424
|
+
sample: topItem ? evidenceConclusion(topItem) : '',
|
|
425
|
+
};
|
|
426
|
+
})
|
|
427
|
+
.sort((a, b) => {
|
|
428
|
+
const rank = { P0: 4, P1: 3, P2: 2, P3: 1 };
|
|
429
|
+
const byPriority = (rank[b.priority] ?? 0) - (rank[a.priority] ?? 0);
|
|
430
|
+
if (byPriority !== 0)
|
|
431
|
+
return byPriority;
|
|
432
|
+
return b.evidenceCount - a.evidenceCount;
|
|
433
|
+
});
|
|
434
|
+
const actionRows = actionItems.slice(0, 8).map((item) => `
|
|
435
|
+
<tr data-observe-rollup-row data-skill-anchor="${e(item.anchor)}" style="cursor:pointer">
|
|
436
|
+
<td style="padding:9px 10px;color:${item.color};font-weight:700">${e(item.priority)}</td>
|
|
437
|
+
<td style="padding:9px 10px;font-family:ui-monospace,monospace;font-weight:650;word-break:break-all">${e(item.skillName)}</td>
|
|
438
|
+
<td style="padding:9px 10px;color:var(--text-primary);font-weight:650">${e(item.action)}</td>
|
|
439
|
+
<td style="padding:9px 10px;color:var(--text-secondary);line-height:1.45">${e(item.reason)}</td>
|
|
440
|
+
<td class="num" style="padding:9px 10px;text-align:right">${item.evidenceCount}</td>
|
|
441
|
+
</tr>
|
|
442
|
+
`).join('');
|
|
443
|
+
const funnelRows = [
|
|
444
|
+
{
|
|
445
|
+
stage: '1. 调用 skill',
|
|
446
|
+
status: '当前能统计',
|
|
447
|
+
count: totalSkillInvocations,
|
|
448
|
+
desc: '用户或 agent 是否真的用了某个 skill。这里统计的是 trace 中识别到的 skill 调用次数,已排除没有归属到具体 skill 的 general 片段。',
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
stage: '2. 运行过程中',
|
|
452
|
+
status: '当前能统计',
|
|
453
|
+
count: allItems.filter((item) => item.signalType === 'failed_search' || item.signalType === 'repeated_failure').reduce((sum, item) => sum + item.occurrences, 0),
|
|
454
|
+
desc: 'skill 运行时,agent 调用了 Read、Grep、Bash 等工具。这里统计工具查找失败、读取失败、Bash 命令异常等记录。',
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
stage: '3. 回答阶段',
|
|
458
|
+
status: '只能部分统计',
|
|
459
|
+
count: allItems.filter((item) => item.signalType === 'hedging' || item.signalType === 'explicit_marker').reduce((sum, item) => sum + item.occurrences, 0),
|
|
460
|
+
desc: 'agent 给出回答时,是否出现“不确定、没找到、需要确认”等表达。它只能作为弱信号,不能单独证明 skill 有问题。',
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
stage: '4. skill 调用是否跑完',
|
|
464
|
+
status: '只能部分统计',
|
|
465
|
+
count: totalSkillInvocations,
|
|
466
|
+
desc: '当前只能确认 trace 里出现了完整的 skill 调用片段。tool_result 能说明单个工具是否成功,但还不能证明整个 skill 调用产出了正确结果。',
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
stage: '5. 用户目标是否完成',
|
|
470
|
+
status: '当前不能统计',
|
|
471
|
+
count: '—',
|
|
472
|
+
desc: '当前不统计。trace 里的 tool_result 只能说明某次工具调用成功或失败,不能证明用户目标已经完成、答案被接受,或用户没有换 session 继续问。',
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
stage: '6. 改版后是否变好',
|
|
476
|
+
status: '当前不能统计',
|
|
477
|
+
count: '—',
|
|
478
|
+
desc: '当前不统计。还没有把 SKILL.md 的版本、改动时间和 过程发现 趋势关联起来,所以不能判断 v2 是否比 v1 更好。',
|
|
479
|
+
},
|
|
480
|
+
];
|
|
481
|
+
const funnelHtml = funnelRows.map((row, index) => {
|
|
482
|
+
const color = row.status === '当前能统计' ? 'var(--green)' : row.status === '只能部分统计' ? 'var(--yellow)' : 'var(--text-muted)';
|
|
483
|
+
const width = Math.max(66, 100 - index * 6);
|
|
484
|
+
return `<div style="width:${width}%;min-width:230px;margin:0 auto;padding:7px 10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);position:relative">
|
|
485
|
+
<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">
|
|
486
|
+
<span style="font-weight:650;color:var(--text-primary);line-height:1.3;font-size:12px">${e(row.stage)}</span>
|
|
487
|
+
<span style="font-size:11px;color:${color};font-weight:650;white-space:nowrap">${e(row.status)}</span>
|
|
488
|
+
</div>
|
|
489
|
+
<div style="font-size:17px;font-weight:700;margin-top:3px;color:var(--text-primary)">${e(String(row.count))}</div>
|
|
490
|
+
<div
|
|
491
|
+
class="signal-help"
|
|
492
|
+
tabindex="0"
|
|
493
|
+
data-signal-title="${e(row.stage)}"
|
|
494
|
+
data-signal-description="${e(row.desc)}"
|
|
495
|
+
aria-label="${e(row.desc)}"
|
|
496
|
+
style="font-size:11px;color:var(--text-muted);line-height:1.35;margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;cursor:help"
|
|
497
|
+
>${e(row.desc)}</div>
|
|
498
|
+
</div>`;
|
|
499
|
+
}).join('');
|
|
500
|
+
const rawRows = reports.flatMap((report, reportIndex) => report.items.map((item, itemIndex) => {
|
|
501
|
+
const rawId = `obs-raw-${reportIndex}-${itemIndex}`;
|
|
502
|
+
const color = item.severity === 'high' ? 'var(--red)' : item.severity === 'medium' ? 'var(--yellow)' : item.severity === 'noise' ? 'var(--text-muted)' : 'var(--accent)';
|
|
503
|
+
return `<tr>
|
|
504
|
+
<td style="padding:8px 10px;color:${color};font-weight:600">${item.severity}</td>
|
|
505
|
+
<td style="padding:8px 10px">${renderSignalLabel(item)}</td>
|
|
506
|
+
<td style="padding:8px 10px;font-family:ui-monospace,monospace">${item.skillName}</td>
|
|
507
|
+
<td style="padding:8px 10px">${renderSourceBadge(item)}</td>
|
|
508
|
+
<td class="num" style="padding:8px 10px;text-align:right">${item.confidence.toFixed(2)} / ${item.attributionConfidence.toFixed(2)}</td>
|
|
509
|
+
<td style="padding:8px 10px">${renderEvidenceCell(item, 180)}</td>
|
|
510
|
+
<td class="num" style="padding:8px 10px;text-align:right"><button type="button" onclick="toggleObservationDetail('${rawId}', this)" style="font-size:12px;padding:4px 8px;border:1px solid var(--border);background:var(--bg);border-radius:4px;cursor:pointer">${lang === 'zh' ? '原始 JSON' : 'Raw JSON'}</button></td>
|
|
511
|
+
</tr>
|
|
512
|
+
<tr id="${rawId}" style="display:none;background:var(--bg-muted)">
|
|
513
|
+
<td colspan="7" style="padding:14px 18px;border-bottom:1px solid var(--border);text-align:left">
|
|
514
|
+
${renderJson(item)}
|
|
515
|
+
</td>
|
|
516
|
+
</tr>`;
|
|
517
|
+
})).join('');
|
|
518
|
+
const rawReportBlocks = reports.map((report, index) => `
|
|
519
|
+
<section style="margin-top:18px">
|
|
520
|
+
<h3 style="font-size:14px;margin:0 0 8px;color:var(--text-primary)">过程发现 JSON ${index + 1}</h3>
|
|
521
|
+
<div style="color:var(--text-muted);font-size:12px;margin-bottom:8px">
|
|
522
|
+
tracePath=${e(report.meta.tracePath)} · generatedAt=${e(report.meta.generatedAt)} · segments=${report.meta.segmentCount} · items=${report.meta.itemCount}
|
|
523
|
+
</div>
|
|
524
|
+
${renderJson(report)}
|
|
525
|
+
</section>
|
|
526
|
+
`).join('');
|
|
527
|
+
const empty = items.length === 0
|
|
528
|
+
? `<p style="color:var(--text-muted);margin-top:24px">${lang === 'zh' ? '暂无 inbox item。运行 omk observe ingest <sessions-dir> 生成。' : 'No inbox items. Run omk observe ingest <sessions-dir> first.'}</p>`
|
|
529
|
+
: '';
|
|
530
|
+
return layout('Observe Inbox', `
|
|
531
|
+
<main style="max-width:1120px;margin:0 auto;padding:24px">
|
|
532
|
+
<nav style="margin-bottom:12px"><a href="/analyses" style="color:var(--accent);text-decoration:none">${lang === 'zh' ? 'Skill 健康度日报' : 'Skill health reports'}</a></nav>
|
|
533
|
+
<h1 style="font-size:22px;margin:8px 0">Observe Inbox</h1>
|
|
534
|
+
<p style="color:var(--text-muted);font-size:13px">真实 session trace 中聚合出的待 review 问题。判断把握表示这条 过程发现 的规则判断有多确定;归属把握表示它归到当前 skill 名下有多确定。</p>
|
|
535
|
+
<style>
|
|
536
|
+
#signal-global-tooltip {
|
|
537
|
+
position: fixed;
|
|
538
|
+
z-index: 9999;
|
|
539
|
+
display: none;
|
|
540
|
+
width: min(360px, calc(100vw - 32px));
|
|
541
|
+
padding: 10px 12px;
|
|
542
|
+
background: var(--bg-surface);
|
|
543
|
+
border: 1px solid var(--border);
|
|
544
|
+
border-radius: 6px;
|
|
545
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.18);
|
|
546
|
+
color: var(--text-primary);
|
|
547
|
+
font-size: 12px;
|
|
548
|
+
line-height: 1.5;
|
|
549
|
+
white-space: normal;
|
|
550
|
+
pointer-events: none;
|
|
551
|
+
font-family: system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;
|
|
552
|
+
}
|
|
553
|
+
#observe-tab-review table td,
|
|
554
|
+
#observe-tab-review table th,
|
|
555
|
+
#observe-tab-raw table td,
|
|
556
|
+
#observe-tab-raw table th {
|
|
557
|
+
vertical-align: top;
|
|
558
|
+
}
|
|
559
|
+
#observe-tab-review table td,
|
|
560
|
+
#observe-tab-raw table td {
|
|
561
|
+
text-align: left;
|
|
562
|
+
}
|
|
563
|
+
#observe-tab-review table td.num,
|
|
564
|
+
#observe-tab-raw table td.num {
|
|
565
|
+
text-align: right;
|
|
566
|
+
}
|
|
567
|
+
</style>
|
|
568
|
+
<div id="signal-global-tooltip" role="tooltip"></div>
|
|
569
|
+
${empty}
|
|
570
|
+
<section style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:18px">
|
|
571
|
+
<div style="padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface)">
|
|
572
|
+
<div style="color:var(--text-muted);font-size:12px">需要优先 review</div>
|
|
573
|
+
<div style="font-size:24px;font-weight:700;color:var(--red);margin-top:4px">${severitySkillCounts.high}</div>
|
|
574
|
+
<div style="color:var(--text-muted);font-size:12px">个 skill 有高风险</div>
|
|
575
|
+
</div>
|
|
576
|
+
<div style="padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface)">
|
|
577
|
+
<div style="color:var(--text-muted);font-size:12px">低风险/抽样确认</div>
|
|
578
|
+
<div style="font-size:24px;font-weight:700;color:var(--yellow);margin-top:4px">${countSkillsBySeverity('medium', 'low')}</div>
|
|
579
|
+
<div style="color:var(--text-muted);font-size:12px">个 skill 是低风险或不确定</div>
|
|
580
|
+
</div>
|
|
581
|
+
<div style="padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface)">
|
|
582
|
+
<div style="color:var(--text-muted);font-size:12px">无异常/无需改 skill</div>
|
|
583
|
+
<div style="font-size:24px;font-weight:700;color:var(--text-muted);margin-top:4px">${severitySkillCounts.noise}</div>
|
|
584
|
+
<div style="color:var(--text-muted);font-size:12px">个 skill 无异常,仅路径/权限/工具问题</div>
|
|
585
|
+
</div>
|
|
586
|
+
<div style="padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface)">
|
|
587
|
+
<div style="color:var(--text-muted);font-size:12px">数据范围</div>
|
|
588
|
+
<div style="font-size:18px;font-weight:700;margin-top:6px">${skillCount} trace skills</div>
|
|
589
|
+
<div style="color:var(--text-muted);font-size:12px">${totalSkillInvocations} skill 调用 · ${allItems.length} 过程发现</div>
|
|
590
|
+
<div style="color:var(--text-muted);font-size:12px">${reportCount} reports · latest ${e(latestSeenLabel)}</div>
|
|
591
|
+
</div>
|
|
592
|
+
</section>
|
|
593
|
+
<section style="margin-top:16px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);overflow:hidden">
|
|
594
|
+
<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;padding:13px 14px;border-bottom:1px solid var(--border)">
|
|
595
|
+
<div>
|
|
596
|
+
<h2 style="font-size:15px;margin:0;color:var(--text-primary)">Skill 健康度看板</h2>
|
|
597
|
+
<div style="color:var(--text-muted);font-size:12px;margin-top:3px">一行一个 skill。子项指标同时汇总 trace 工具调用和 过程发现 信号:工具调用看运行行为,过程发现 看发现的问题类型。</div>
|
|
598
|
+
</div>
|
|
599
|
+
<div style="color:var(--text-muted);font-size:12px;white-space:nowrap">${skillRollups.length} trace skills</div>
|
|
600
|
+
</div>
|
|
601
|
+
<div style="width:100%;max-height:70vh;overflow:auto">
|
|
602
|
+
<table style="border-collapse:collapse;width:100%;min-width:1340px;font-size:13px;table-layout:fixed;border:0;border-radius:0;background:transparent">
|
|
603
|
+
<colgroup>
|
|
604
|
+
<col style="width:210px">
|
|
605
|
+
<col style="width:82px">
|
|
606
|
+
<col style="width:82px">
|
|
607
|
+
<col style="width:96px">
|
|
608
|
+
<col style="width:340px">
|
|
609
|
+
<col style="width:82px">
|
|
610
|
+
<col style="width:96px">
|
|
611
|
+
<col style="width:92px">
|
|
612
|
+
<col style="width:142px">
|
|
613
|
+
<col style="width:142px">
|
|
614
|
+
<col style="width:92px">
|
|
615
|
+
<col style="width:96px">
|
|
616
|
+
</colgroup>
|
|
617
|
+
<thead><tr>
|
|
618
|
+
<th style="text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)">Skill</th>
|
|
619
|
+
<th style="text-align:right;padding:9px 10px;border-bottom:1px solid var(--border)">调用</th>
|
|
620
|
+
<th style="text-align:right;padding:9px 10px;border-bottom:1px solid var(--border)">Session</th>
|
|
621
|
+
<th style="text-align:right;padding:9px 10px;border-bottom:1px solid var(--border)">过程发现</th>
|
|
622
|
+
<th style="text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)">子项指标</th>
|
|
623
|
+
<th style="text-align:right;padding:9px 10px;border-bottom:1px solid var(--border)">高风险</th>
|
|
624
|
+
<th style="text-align:right;padding:9px 10px;border-bottom:1px solid var(--border)">低风险</th>
|
|
625
|
+
<th style="text-align:right;padding:9px 10px;border-bottom:1px solid var(--border)">路径/工具</th>
|
|
626
|
+
<th style="text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)">最近发现问题</th>
|
|
627
|
+
<th style="text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)">最近使用</th>
|
|
628
|
+
<th style="text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)">来源</th>
|
|
629
|
+
<th style="text-align:right;padding:9px 10px;border-bottom:1px solid var(--border)">Review</th>
|
|
630
|
+
</tr></thead>
|
|
631
|
+
<tbody>${skillRollupRows}</tbody>
|
|
632
|
+
</table>
|
|
633
|
+
</div>
|
|
634
|
+
</section>
|
|
635
|
+
<section style="margin-top:16px;display:grid;grid-template-columns:minmax(0,1.25fr) minmax(280px,.75fr);gap:12px;align-items:start">
|
|
636
|
+
<section id="observe-action-panel" style="border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);overflow:hidden;display:flex;flex-direction:column">
|
|
637
|
+
<div style="padding:13px 14px;border-bottom:1px solid var(--border)">
|
|
638
|
+
<h2 style="font-size:15px;margin:0;color:var(--text-primary)">Reviewer 待办建议</h2>
|
|
639
|
+
<div style="color:var(--text-muted);font-size:12px;margin-top:3px">这张表回答“我现在该先看哪个 skill、看什么”。它只给 review 优先级,不自动判定必须改。点击行可跳到对应 skill 明细。</div>
|
|
640
|
+
</div>
|
|
641
|
+
${actionRows ? `<div style="width:100%;overflow:auto;flex:1;min-height:0">
|
|
642
|
+
<table style="border-collapse:collapse;width:100%;min-width:760px;font-size:13px;table-layout:fixed;border:0;border-radius:0;background:transparent">
|
|
643
|
+
<colgroup>
|
|
644
|
+
<col style="width:58px">
|
|
645
|
+
<col style="width:210px">
|
|
646
|
+
<col style="width:170px">
|
|
647
|
+
<col style="width:auto">
|
|
648
|
+
<col style="width:70px">
|
|
649
|
+
</colgroup>
|
|
650
|
+
<thead><tr>
|
|
651
|
+
<th style="text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)">P</th>
|
|
652
|
+
<th style="text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)">Skill</th>
|
|
653
|
+
<th style="text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)">现在要做什么</th>
|
|
654
|
+
<th style="text-align:left;padding:9px 10px;border-bottom:1px solid var(--border)">为什么这么建议</th>
|
|
655
|
+
<th style="text-align:right;padding:9px 10px;border-bottom:1px solid var(--border)">次数</th>
|
|
656
|
+
</tr></thead>
|
|
657
|
+
<tbody>${actionRows}</tbody>
|
|
658
|
+
</table>
|
|
659
|
+
</div>` : `<div style="padding:14px;color:var(--text-muted);font-size:13px">当前没有需要 review 的 过程发现。</div>`}
|
|
660
|
+
</section>
|
|
661
|
+
<section id="observe-funnel-panel" style="border:1px solid var(--border);border-radius:8px;background:var(--bg-muted);padding:13px 14px;box-sizing:border-box;overflow:hidden">
|
|
662
|
+
<h2 style="font-size:15px;margin:0;color:var(--text-primary)">当前可观测漏斗</h2>
|
|
663
|
+
<div style="color:var(--text-muted);font-size:12px;margin-top:3px">这张表说明 OMK 现在能统计用户使用 skill 的哪几步。不能统计的项不会在本报告里伪装成结论。</div>
|
|
664
|
+
<div style="display:grid;grid-template-columns:1fr;gap:6px;margin-top:10px">${funnelHtml}</div>
|
|
665
|
+
</section>
|
|
666
|
+
</section>
|
|
667
|
+
<section style="margin-top:14px;padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-muted);font-size:13px;line-height:1.6">
|
|
668
|
+
<strong>Reviewer path:</strong>
|
|
669
|
+
先看 <span style="color:var(--red);font-weight:650">高风险/需关注</span>;
|
|
670
|
+
“低风险”表示通常不需要改 skill,只需要抽样确认;
|
|
671
|
+
“无异常”表示更像环境、路径、权限或工具限制。
|
|
672
|
+
展开行看判断原因和原始 evidence,必要时到 过程发现 JSON / 打标 tab 查完整结构。
|
|
673
|
+
<div style="margin-top:6px;color:var(--text-muted)">
|
|
674
|
+
Signal 列第一行是原始信号类型,例如 failed_search;第二行是 OMK 判断出的失败原因,例如 bash_probe 表示 Bash 命令看起来只是在试目录或路径。
|
|
675
|
+
“出现次数”表示这类问题 dedup 后累计出现了几次。
|
|
676
|
+
</div>
|
|
677
|
+
<button type="button" onclick="toggleScoringGuide(this)" style="margin-top:10px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg);cursor:pointer;font-size:12px">查看判断标准</button>
|
|
678
|
+
<div id="observe-scoring-guide" style="display:none;margin-top:10px;padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);text-align:left">
|
|
679
|
+
<div style="font-weight:700;margin-bottom:8px">判断漏斗</div>
|
|
680
|
+
<ol style="margin:0 0 10px 18px;padding:0">
|
|
681
|
+
<li>先看发生了什么:工具查找、读取、Bash 命令、模型文本里是否出现失败或不确定。</li>
|
|
682
|
+
<li>再看为什么失败:文件太长、路径不存在、Bash 只是试目录、后续是否又找到了结果。</li>
|
|
683
|
+
<li>最后看要不要改 skill:只有像 skill 没写清楚、没覆盖路径/流程时,才进入“高风险/需关注”。</li>
|
|
684
|
+
</ol>
|
|
685
|
+
<table style="border-collapse:collapse;width:100%;font-size:12px">
|
|
686
|
+
<thead><tr>
|
|
687
|
+
<th style="text-align:left;padding:7px 8px;border-bottom:1px solid var(--border)">页面判断</th>
|
|
688
|
+
<th style="text-align:left;padding:7px 8px;border-bottom:1px solid var(--border)">规则来源</th>
|
|
689
|
+
<th style="text-align:left;padding:7px 8px;border-bottom:1px solid var(--border)">怎么处理</th>
|
|
690
|
+
</tr></thead>
|
|
691
|
+
<tbody>
|
|
692
|
+
<tr>
|
|
693
|
+
<td style="padding:7px 8px;color:var(--red);font-weight:650">高风险/需关注</td>
|
|
694
|
+
<td style="padding:7px 8px">hard_miss、repeated_failure、明确标了未知/缺口。通常表示查找失败后,没有看到后续找到同主题结果。</td>
|
|
695
|
+
<td style="padding:7px 8px">优先看。确认 skill 是否漏了入口、路径、流程、约束或常见问题。</td>
|
|
696
|
+
</tr>
|
|
697
|
+
<tr>
|
|
698
|
+
<td style="padding:7px 8px;color:var(--yellow);font-weight:650">低风险/抽样确认</td>
|
|
699
|
+
<td style="padding:7px 8px">Bash 里有 ls/find、2>/dev/null、|| true 等试目录/试路径写法,或前面没找到但后面又找到了。</td>
|
|
700
|
+
<td style="padding:7px 8px">通常不需要改 skill;抽样确认是否反复浪费时间。只有反复发生时,再考虑给 skill 补“推荐查找路径”。</td>
|
|
701
|
+
</tr>
|
|
702
|
+
<tr>
|
|
703
|
+
<td style="padding:7px 8px;color:var(--accent);font-weight:650">不确定/低优先级</td>
|
|
704
|
+
<td style="padding:7px 8px">模型文本里说“不确定/需要确认”等,但没有强工具证据。</td>
|
|
705
|
+
<td style="padding:7px 8px">低优先级看。只有它影响最终答案时,才考虑改 skill。</td>
|
|
706
|
+
</tr>
|
|
707
|
+
<tr>
|
|
708
|
+
<td style="padding:7px 8px;color:var(--text-muted);font-weight:650">无异常/无需改 skill:路径/工具问题</td>
|
|
709
|
+
<td style="padding:7px 8px">文件不存在、文件太长、权限失败、工具调用失败或超时。</td>
|
|
710
|
+
<td style="padding:7px 8px">通常不是 skill 内容缺失。先看环境、路径、权限、文件大小或工具调用方式。</td>
|
|
711
|
+
</tr>
|
|
712
|
+
</tbody>
|
|
713
|
+
</table>
|
|
714
|
+
</div>
|
|
715
|
+
</section>
|
|
716
|
+
<section style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:14px">
|
|
717
|
+
<input id="observe-filter-input" type="search" placeholder="Filter skill / signal / evidence / path" style="flex:1;min-width:260px;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text-primary);font-size:13px">
|
|
718
|
+
<button type="button" data-severity-filter="all" onclick="setObserveSeverityFilter('all')" style="padding:7px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-surface);cursor:pointer">All</button>
|
|
719
|
+
<button type="button" data-severity-filter="high" onclick="setObserveSeverityFilter('high')" style="padding:7px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg);cursor:pointer">高风险/需关注</button>
|
|
720
|
+
<button type="button" data-severity-filter="medium" onclick="setObserveSeverityFilter('medium')" style="padding:7px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg);cursor:pointer">低风险</button>
|
|
721
|
+
<button type="button" data-severity-filter="noise" onclick="setObserveSeverityFilter('noise')" style="padding:7px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg);cursor:pointer">路径/工具问题</button>
|
|
722
|
+
</section>
|
|
723
|
+
<div style="display:flex;gap:8px;margin-top:18px;border-bottom:1px solid var(--border)">
|
|
724
|
+
<button type="button" data-observe-tab-button="review" onclick="showObservationTab('review')" style="font-size:13px;padding:8px 12px;border:1px solid var(--border);border-bottom:0;background:var(--bg-surface);border-radius:6px 6px 0 0;cursor:pointer">${lang === 'zh' ? 'Skill 下钻明细' : 'Skill Details'}</button>
|
|
725
|
+
<button type="button" data-observe-tab-button="raw" onclick="showObservationTab('raw')" style="font-size:13px;padding:8px 12px;border:1px solid var(--border);border-bottom:0;background:var(--bg);border-radius:6px 6px 0 0;cursor:pointer">${lang === 'zh' ? '过程发现 JSON / 打标' : '过程发现 JSON / Tags'}</button>
|
|
726
|
+
</div>
|
|
727
|
+
<section id="observe-tab-review" style="margin-top:4px">
|
|
728
|
+
${items.length > 0 ? skillSections : ''}
|
|
729
|
+
</section>
|
|
730
|
+
<section id="observe-tab-raw" style="display:none">
|
|
731
|
+
<p style="color:var(--text-muted);font-size:13px;margin:16px 0 8px">这里展示 过程发现 JSON 文件里的原始结构,以及已经计算出的 severity / signal / subtype / confidence / attributionConfidence 等分类打标。</p>
|
|
732
|
+
${reports.length > 0 ? `<table style="border-collapse:collapse;width:100%;font-size:13px;margin-top:12px">
|
|
733
|
+
<thead><tr>
|
|
734
|
+
<th style="text-align:left;padding:10px;border-bottom:2px solid var(--border)">Severity</th>
|
|
735
|
+
<th style="text-align:left;padding:10px;border-bottom:2px solid var(--border)">Signal</th>
|
|
736
|
+
<th style="text-align:left;padding:10px;border-bottom:2px solid var(--border)">Skill</th>
|
|
737
|
+
<th style="text-align:left;padding:10px;border-bottom:2px solid var(--border)">Source</th>
|
|
738
|
+
${renderConfidenceHeader('10px', '2px solid var(--border)')}
|
|
739
|
+
<th style="text-align:left;padding:10px;border-bottom:2px solid var(--border)">Evidence</th>
|
|
740
|
+
<th style="text-align:right;padding:10px;border-bottom:2px solid var(--border)">JSON</th>
|
|
741
|
+
</tr></thead>
|
|
742
|
+
<tbody>${rawRows}</tbody>
|
|
743
|
+
</table>${rawReportBlocks}` : `<p style="color:var(--text-muted);margin-top:24px">${lang === 'zh' ? '暂无过程发现 JSON。' : 'No observation JSON yet.'}</p>`}
|
|
744
|
+
</section>
|
|
745
|
+
<script>
|
|
746
|
+
var observeSeverityFilter = 'all';
|
|
747
|
+
function showObservationTab(name) {
|
|
748
|
+
var review = document.getElementById('observe-tab-review');
|
|
749
|
+
var raw = document.getElementById('observe-tab-raw');
|
|
750
|
+
if (review) review.style.display = name === 'review' ? '' : 'none';
|
|
751
|
+
if (raw) raw.style.display = name === 'raw' ? '' : 'none';
|
|
752
|
+
var buttons = document.querySelectorAll('[data-observe-tab-button]');
|
|
753
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
754
|
+
var active = buttons[i].getAttribute('data-observe-tab-button') === name;
|
|
755
|
+
buttons[i].style.background = active ? 'var(--bg-surface)' : 'var(--bg)';
|
|
756
|
+
buttons[i].style.fontWeight = active ? '600' : '400';
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function toggleObservationDetail(id, btn) {
|
|
760
|
+
var row = document.getElementById(id);
|
|
761
|
+
if (!row) return;
|
|
762
|
+
var open = row.style.display !== 'none';
|
|
763
|
+
row.style.display = open ? 'none' : 'table-row';
|
|
764
|
+
if (btn) btn.textContent = open ? '${lang === 'zh' ? '展开' : 'Details'}' : '${lang === 'zh' ? '收起' : 'Hide'}';
|
|
765
|
+
}
|
|
766
|
+
function toggleScoringGuide(btn) {
|
|
767
|
+
var guide = document.getElementById('observe-scoring-guide');
|
|
768
|
+
if (!guide) return;
|
|
769
|
+
var open = guide.style.display !== 'none';
|
|
770
|
+
guide.style.display = open ? 'none' : 'block';
|
|
771
|
+
if (btn) btn.textContent = open ? '查看判断标准' : '收起判断标准';
|
|
772
|
+
}
|
|
773
|
+
async function openObservationTrace(id, btn) {
|
|
774
|
+
var target = document.getElementById('trace-' + id);
|
|
775
|
+
if (!target) return;
|
|
776
|
+
var open = target.style.display !== 'none';
|
|
777
|
+
if (open) {
|
|
778
|
+
target.style.display = 'none';
|
|
779
|
+
if (btn) btn.textContent = 'Open in trace';
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
target.style.display = 'block';
|
|
783
|
+
target.textContent = 'Loading...';
|
|
784
|
+
if (btn) btn.textContent = 'Hide trace';
|
|
785
|
+
try {
|
|
786
|
+
var res = await fetch('/api/observations/show?id=' + encodeURIComponent(id));
|
|
787
|
+
var data = await res.json();
|
|
788
|
+
target.textContent = data.text || data.error || '';
|
|
789
|
+
} catch (err) {
|
|
790
|
+
target.textContent = String(err && err.message ? err.message : err);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function setObserveSeverityFilter(value) {
|
|
794
|
+
observeSeverityFilter = value;
|
|
795
|
+
var buttons = document.querySelectorAll('[data-severity-filter]');
|
|
796
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
797
|
+
var active = buttons[i].getAttribute('data-severity-filter') === value;
|
|
798
|
+
buttons[i].style.background = active ? 'var(--bg-surface)' : 'var(--bg)';
|
|
799
|
+
buttons[i].style.fontWeight = active ? '650' : '400';
|
|
800
|
+
}
|
|
801
|
+
applyObserveFilters();
|
|
802
|
+
}
|
|
803
|
+
function applyObserveFilters() {
|
|
804
|
+
var input = document.getElementById('observe-filter-input');
|
|
805
|
+
var query = input ? String(input.value || '').toLowerCase().trim() : '';
|
|
806
|
+
var rows = document.querySelectorAll('[data-observe-row="review"]');
|
|
807
|
+
for (var i = 0; i < rows.length; i++) {
|
|
808
|
+
var row = rows[i];
|
|
809
|
+
var severity = row.getAttribute('data-severity') || '';
|
|
810
|
+
var search = row.getAttribute('data-search') || '';
|
|
811
|
+
var detailId = row.getAttribute('data-detail-id');
|
|
812
|
+
var detail = detailId ? document.querySelector('[data-observe-detail-for="' + detailId + '"]') : null;
|
|
813
|
+
var visible = (observeSeverityFilter === 'all' || severity === observeSeverityFilter) && (!query || search.indexOf(query) >= 0);
|
|
814
|
+
row.style.display = visible ? '' : 'none';
|
|
815
|
+
if (!visible && detail) detail.style.display = 'none';
|
|
816
|
+
}
|
|
817
|
+
var groups = document.querySelectorAll('[data-observe-skill-group]');
|
|
818
|
+
for (var g = 0; g < groups.length; g++) {
|
|
819
|
+
var group = groups[g];
|
|
820
|
+
var groupRows = group.querySelectorAll('[data-observe-row="review"]');
|
|
821
|
+
var anyVisible = false;
|
|
822
|
+
for (var r = 0; r < groupRows.length; r++) {
|
|
823
|
+
if (groupRows[r].style.display !== 'none') {
|
|
824
|
+
anyVisible = true;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
group.style.display = anyVisible ? '' : 'none';
|
|
829
|
+
if (query && anyVisible) group.open = true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
(function setupObserveFilters() {
|
|
833
|
+
var input = document.getElementById('observe-filter-input');
|
|
834
|
+
if (input) input.addEventListener('input', applyObserveFilters);
|
|
835
|
+
})();
|
|
836
|
+
(function setupSkillRollupRows() {
|
|
837
|
+
var rows = document.querySelectorAll('[data-observe-rollup-row]');
|
|
838
|
+
for (var i = 0; i < rows.length; i++) {
|
|
839
|
+
rows[i].addEventListener('click', function () {
|
|
840
|
+
var id = this.getAttribute('data-skill-anchor');
|
|
841
|
+
if (!id) return;
|
|
842
|
+
var target = document.getElementById(id);
|
|
843
|
+
if (!target) return;
|
|
844
|
+
target.open = true;
|
|
845
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
})();
|
|
849
|
+
(function syncActionPanelHeight() {
|
|
850
|
+
var action = document.getElementById('observe-action-panel');
|
|
851
|
+
var funnel = document.getElementById('observe-funnel-panel');
|
|
852
|
+
if (!action || !funnel) return;
|
|
853
|
+
function sync() {
|
|
854
|
+
action.style.height = '';
|
|
855
|
+
var h = Math.max(funnel.scrollHeight, funnel.offsetHeight);
|
|
856
|
+
if (h > 0) action.style.height = h + 'px';
|
|
857
|
+
}
|
|
858
|
+
sync();
|
|
859
|
+
window.addEventListener('resize', sync);
|
|
860
|
+
if (document.fonts && document.fonts.ready) document.fonts.ready.then(sync).catch(function () {});
|
|
861
|
+
})();
|
|
862
|
+
(function setupSignalTooltips() {
|
|
863
|
+
var tooltip = document.getElementById('signal-global-tooltip');
|
|
864
|
+
if (!tooltip) return;
|
|
865
|
+
function show(el) {
|
|
866
|
+
var title = el.getAttribute('data-signal-title') || '';
|
|
867
|
+
var description = el.getAttribute('data-signal-description') || '';
|
|
868
|
+
tooltip.innerHTML = '<strong style="display:block;margin-bottom:4px"></strong><div></div>';
|
|
869
|
+
tooltip.querySelector('strong').textContent = title;
|
|
870
|
+
tooltip.querySelector('div').textContent = description;
|
|
871
|
+
tooltip.style.display = 'block';
|
|
872
|
+
var rect = el.getBoundingClientRect();
|
|
873
|
+
var top = rect.bottom + 8;
|
|
874
|
+
var left = rect.left;
|
|
875
|
+
var width = Math.min(360, window.innerWidth - 32);
|
|
876
|
+
if (left + width > window.innerWidth - 16) left = window.innerWidth - width - 16;
|
|
877
|
+
if (left < 16) left = 16;
|
|
878
|
+
tooltip.style.left = left + 'px';
|
|
879
|
+
tooltip.style.top = top + 'px';
|
|
880
|
+
var tipRect = tooltip.getBoundingClientRect();
|
|
881
|
+
if (tipRect.bottom > window.innerHeight - 16) {
|
|
882
|
+
tooltip.style.top = Math.max(16, rect.top - tipRect.height - 8) + 'px';
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function hide() {
|
|
886
|
+
tooltip.style.display = 'none';
|
|
887
|
+
}
|
|
888
|
+
var helps = document.querySelectorAll('.signal-help');
|
|
889
|
+
for (var i = 0; i < helps.length; i++) {
|
|
890
|
+
helps[i].addEventListener('mouseenter', function () { show(this); });
|
|
891
|
+
helps[i].addEventListener('focus', function () { show(this); });
|
|
892
|
+
helps[i].addEventListener('mouseleave', hide);
|
|
893
|
+
helps[i].addEventListener('blur', hide);
|
|
894
|
+
}
|
|
895
|
+
})();
|
|
896
|
+
</script>
|
|
897
|
+
</main>
|
|
898
|
+
`, lang);
|
|
899
|
+
}
|
|
900
|
+
//# sourceMappingURL=observation-inbox-renderer.js.map
|