oh-my-knowledge 0.36.0 → 0.37.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 +5 -3
- package/README.zh.md +5 -3
- package/dist/assets/agent-skills/omk/SKILL.md +4 -2
- package/dist/assets/agent-skills/omk/references/commands.md +86 -3
- package/dist/cli/commands/doctor.js +19 -4
- package/dist/cli/commands/evolve.d.ts +11 -0
- package/dist/cli/commands/evolve.js +105 -5
- package/dist/cli/commands/list.d.ts +5 -26
- package/dist/cli/commands/list.js +17 -142
- package/dist/cli/commands/promote.d.ts +24 -0
- package/dist/cli/commands/promote.js +158 -0
- package/dist/cli/commands/rollback.d.ts +20 -0
- package/dist/cli/commands/rollback.js +95 -0
- package/dist/cli/commands/sample.d.ts +1 -0
- package/dist/cli/commands/sample.js +10 -4
- package/dist/cli/lib/cell-format.d.ts +17 -0
- package/dist/cli/lib/cell-format.js +19 -0
- package/dist/cli/lib/cmd-flags.d.ts +1 -0
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +8 -0
- package/dist/cli/lib/i18n-dict/list.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/list.js +4 -0
- package/dist/cli/lib/i18n-dict/promote.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/promote.js +54 -0
- package/dist/cli/lib/i18n-dict/rollback.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/rollback.js +22 -0
- package/dist/cli/lib/i18n-dict.d.ts +3 -1
- package/dist/cli/lib/i18n-dict.js +4 -0
- package/dist/cli/lib/progress.d.ts +3 -0
- package/dist/cli/lib/progress.js +22 -0
- package/dist/cli/lib/source-probe.d.ts +15 -0
- package/dist/cli/lib/source-probe.js +129 -0
- package/dist/doctor/endpoint-rule.d.ts +60 -0
- package/dist/doctor/endpoint-rule.js +381 -0
- package/dist/doctor/health/load-custom-dimensions.d.ts +6 -0
- package/dist/doctor/health/load-custom-dimensions.js +43 -3
- package/dist/doctor/index.js +8 -1
- package/dist/executors/codex-cli.js +5 -1
- package/dist/managed/index.d.ts +1 -0
- package/dist/managed/index.js +1 -0
- package/dist/managed/list-view.d.ts +3 -0
- package/dist/managed/list-view.js +8 -4
- package/dist/managed/promote-gate.d.ts +40 -0
- package/dist/managed/promote-gate.js +37 -0
- package/dist/managed/store.d.ts +25 -2
- package/dist/managed/store.js +92 -3
- package/dist/renderer/doctor-detail-renderer.d.ts +9 -0
- package/dist/renderer/doctor-detail-renderer.js +114 -0
- package/dist/renderer/html-renderer.d.ts +3 -2
- package/dist/renderer/html-renderer.js +99 -99
- package/dist/renderer/icons.d.ts +29 -0
- package/dist/renderer/icons.js +66 -0
- package/dist/renderer/layout.js +81 -68
- package/dist/renderer/observation-inbox/styles.d.ts +1 -1
- package/dist/renderer/observation-inbox/styles.js +53 -53
- package/dist/renderer/report-shell.d.ts +77 -0
- package/dist/renderer/report-shell.js +223 -0
- package/dist/renderer/skill-detail-renderer.js +147 -352
- package/dist/renderer/skill-health-renderer.js +50 -73
- package/dist/renderer/skill-list-renderer.js +358 -306
- package/dist/renderer/summary.js +272 -162
- package/dist/renderer/test-view.d.ts +4 -3
- package/dist/renderer/test-view.js +386 -135
- package/dist/server/report-server.js +142 -28
- package/dist/types/doctor.d.ts +22 -0
- package/dist/types/managed.d.ts +17 -3
- package/package.json +1 -1
package/dist/managed/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ArtifactKind, DeriveManagedStateInput, DerivedManagedState, ManagedArtifactRecord, ManagedArtifactSource, ManagedDistributionTarget, ManagedEvidenceRef } from '../types/index.js';
|
|
1
|
+
import type { ArtifactKind, DeriveManagedStateInput, DerivedManagedState, ManagedArtifactRecord, ManagedArtifactSource, ManagedDecision, ManagedDistributionTarget, ManagedEvidenceRef } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* 受管记录的 per-record 文件存储。一条记录一个 `.omk/managed/<id>.json`,镜像 report-store 的
|
|
4
4
|
* 成熟模式(原子 tmp+rename):每次 install 只碰自己那个文件,independent write、不丢别人、
|
|
@@ -44,6 +44,28 @@ export declare function upsertManagedRecord(dir: string, record: ManagedArtifact
|
|
|
44
44
|
* null —— 这是"管理是 install 显式 opt-in"的体现:eval 绝不为未纳管的 skill 凭空建记录。
|
|
45
45
|
*/
|
|
46
46
|
export declare function appendManagedEvidence(dir: string, recordId: string, evidence: ManagedEvidenceRef): ManagedArtifactRecord | null;
|
|
47
|
+
/**
|
|
48
|
+
* 当前内容(contentHash === record.contentHash)是否处于 promoted 态。决定是 append-only 事件流,
|
|
49
|
+
* promote 与 rollback 互为反操作,故不能看「是否存在过 promote」,而要看**当前内容最近一条** promote/rollback
|
|
50
|
+
* 决定是不是 promote —— rollback 之后再 promote 仍能恢复 promoted,反之亦然。换了内容(contentHash 变)后
|
|
51
|
+
* 旧内容的决定一概不算数。
|
|
52
|
+
*
|
|
53
|
+
* 「最近」按 decidedAt 真实时刻取,与 `latestCurrentEvidence` 同口径:omk 自写恒 UTC `Z`、字典序即时间序;
|
|
54
|
+
* 但记录可手改 / 随仓库分发,异偏移 ISO 串字典序会乱 → 两端可解析才用解析时刻,否则退字典序;并列取后出现的
|
|
55
|
+
* (数组追加序 = 事件序)。
|
|
56
|
+
*/
|
|
57
|
+
export declare function isCurrentlyPromoted(record: ManagedArtifactRecord): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* 追加一条人工管理决定(append-only,promote/reject/rollback 走此路)。与 evidence 同样**不能走 upsert**
|
|
60
|
+
* (`mergeManagedRecord` 刻意保留旧 decisions、丢弃 next.decisions),必须独立 load→push→原子重写。
|
|
61
|
+
*
|
|
62
|
+
* 幂等:对**当前内容**的 promote/rollback,看是否会改变当前 promoted 态——已 promoted 再 promote、未 promoted
|
|
63
|
+
* 再 rollback 都不追加、原样返回(由 CLI 提示「已 promoted」/「未 promoted」)。这让重复 `omk promote` /
|
|
64
|
+
* `omk rollback` 不堆冗余事件,但 promote↔rollback 的真实切换、以及换了内容(contentHash 变)后的新决定仍会
|
|
65
|
+
* 追加——正是要的版本史。其它 decisionKind(如 reject)仍按「同 kind + 同 contentHash 即 dedup」。记录不存在
|
|
66
|
+
* (未 install)返回 null,与 evidence 同口径(管理是 install 显式 opt-in,绝不为未纳管 skill 凭空建记录)。
|
|
67
|
+
*/
|
|
68
|
+
export declare function appendManagedDecision(dir: string, recordId: string, decision: ManagedDecision): ManagedArtifactRecord | null;
|
|
47
69
|
/**
|
|
48
70
|
* 统一的记录构造点——所有消费方(CLI / 未来 server / SDK)经此组装,保证 schema 一致。
|
|
49
71
|
* 只接收事实(身份、源、hash、分发落点);evidence/decisions 恒为空(eval/promote 的地盘)。
|
|
@@ -64,9 +86,10 @@ export declare function recordManagedArtifact(record: ManagedArtifactRecord, opt
|
|
|
64
86
|
/**
|
|
65
87
|
* 读时推导生命周期标签。
|
|
66
88
|
* - 源缺失或 hash 漂(当前内容 ≠ 记录 contentHash)→ stale;
|
|
89
|
+
* - 否则当前内容已有 promote 决定 → promoted;
|
|
67
90
|
* - 否则有**当前**证据 / samples → measurable;
|
|
68
91
|
* - 否则 installed。
|
|
69
|
-
*
|
|
92
|
+
* 「当前证据 / 当前决定」= contentHash 与记录当前 contentHash 匹配的 evidence / decision —— 旧内容的不算数,
|
|
70
93
|
* 这正是 #203「证据必须跟 artifact 一起走」的读时保证。`'discovered'` 留给无分发的记录,此函数不产生。
|
|
71
94
|
*/
|
|
72
95
|
export declare function deriveManagedState(input: DeriveManagedStateInput): DerivedManagedState;
|
package/dist/managed/store.js
CHANGED
|
@@ -92,8 +92,29 @@ function isManagedArtifactRecord(value) {
|
|
|
92
92
|
}
|
|
93
93
|
return true;
|
|
94
94
|
});
|
|
95
|
-
const okDec = r.decisions.every((d) =>
|
|
96
|
-
|
|
95
|
+
const okDec = r.decisions.every((d) => {
|
|
96
|
+
if (!d || typeof d !== 'object')
|
|
97
|
+
return false;
|
|
98
|
+
const dec = d;
|
|
99
|
+
if (!isStringField(dec.decisionKind))
|
|
100
|
+
return false;
|
|
101
|
+
// promote/reject/rollback 的证据指针(promote 写)同样收窄:任意类型脏值不得穿过 validator 进
|
|
102
|
+
// deriveManagedState / promote 消费方。actor / decidedAt / reason 是 install 后才追加的可选展示字段,
|
|
103
|
+
// 旧记录(install 时 decisions 恒空)无,按 optional 读。
|
|
104
|
+
if (!isOptionalString(dec.actor) || !isOptionalString(dec.decidedAt) || !isOptionalString(dec.reason))
|
|
105
|
+
return false;
|
|
106
|
+
if (!isOptionalString(dec.contentHash) || !isOptionalString(dec.reportId))
|
|
107
|
+
return false;
|
|
108
|
+
if (dec.override !== undefined) {
|
|
109
|
+
const o = dec.override;
|
|
110
|
+
if (!o || typeof o !== 'object' || !isStringField(o.verdict))
|
|
111
|
+
return false;
|
|
112
|
+
if (o.overriddenBlocks !== undefined
|
|
113
|
+
&& !(Array.isArray(o.overriddenBlocks) && o.overriddenBlocks.every((b) => typeof b === 'string')))
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
97
118
|
return okDist && okEv && okDec;
|
|
98
119
|
}
|
|
99
120
|
export function loadManagedRecord(dir, id) {
|
|
@@ -218,6 +239,69 @@ export function appendManagedEvidence(dir, recordId, evidence) {
|
|
|
218
239
|
}
|
|
219
240
|
return merged;
|
|
220
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* 当前内容(contentHash === record.contentHash)是否处于 promoted 态。决定是 append-only 事件流,
|
|
244
|
+
* promote 与 rollback 互为反操作,故不能看「是否存在过 promote」,而要看**当前内容最近一条** promote/rollback
|
|
245
|
+
* 决定是不是 promote —— rollback 之后再 promote 仍能恢复 promoted,反之亦然。换了内容(contentHash 变)后
|
|
246
|
+
* 旧内容的决定一概不算数。
|
|
247
|
+
*
|
|
248
|
+
* 「最近」按 decidedAt 真实时刻取,与 `latestCurrentEvidence` 同口径:omk 自写恒 UTC `Z`、字典序即时间序;
|
|
249
|
+
* 但记录可手改 / 随仓库分发,异偏移 ISO 串字典序会乱 → 两端可解析才用解析时刻,否则退字典序;并列取后出现的
|
|
250
|
+
* (数组追加序 = 事件序)。
|
|
251
|
+
*/
|
|
252
|
+
export function isCurrentlyPromoted(record) {
|
|
253
|
+
const ms = (s) => { const n = Date.parse(s); return Number.isNaN(n) ? null : n; };
|
|
254
|
+
let latest;
|
|
255
|
+
for (const d of record.decisions) {
|
|
256
|
+
if (d.contentHash !== record.contentHash)
|
|
257
|
+
continue;
|
|
258
|
+
if (d.decisionKind !== 'promote' && d.decisionKind !== 'rollback')
|
|
259
|
+
continue;
|
|
260
|
+
if (latest === undefined) {
|
|
261
|
+
latest = d;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const tcur = ms(d.decidedAt);
|
|
265
|
+
const tlat = ms(latest.decidedAt);
|
|
266
|
+
const newer = tcur !== null && tlat !== null ? tcur >= tlat : d.decidedAt >= latest.decidedAt;
|
|
267
|
+
if (newer)
|
|
268
|
+
latest = d;
|
|
269
|
+
}
|
|
270
|
+
return latest?.decisionKind === 'promote';
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 追加一条人工管理决定(append-only,promote/reject/rollback 走此路)。与 evidence 同样**不能走 upsert**
|
|
274
|
+
* (`mergeManagedRecord` 刻意保留旧 decisions、丢弃 next.decisions),必须独立 load→push→原子重写。
|
|
275
|
+
*
|
|
276
|
+
* 幂等:对**当前内容**的 promote/rollback,看是否会改变当前 promoted 态——已 promoted 再 promote、未 promoted
|
|
277
|
+
* 再 rollback 都不追加、原样返回(由 CLI 提示「已 promoted」/「未 promoted」)。这让重复 `omk promote` /
|
|
278
|
+
* `omk rollback` 不堆冗余事件,但 promote↔rollback 的真实切换、以及换了内容(contentHash 变)后的新决定仍会
|
|
279
|
+
* 追加——正是要的版本史。其它 decisionKind(如 reject)仍按「同 kind + 同 contentHash 即 dedup」。记录不存在
|
|
280
|
+
* (未 install)返回 null,与 evidence 同口径(管理是 install 显式 opt-in,绝不为未纳管 skill 凭空建记录)。
|
|
281
|
+
*/
|
|
282
|
+
export function appendManagedDecision(dir, recordId, decision) {
|
|
283
|
+
const prev = loadManagedRecord(dir, recordId);
|
|
284
|
+
if (!prev)
|
|
285
|
+
return null;
|
|
286
|
+
const isPromotion = decision.decisionKind === 'promote' || decision.decisionKind === 'rollback';
|
|
287
|
+
const onCurrent = decision.contentHash !== undefined && decision.contentHash === prev.contentHash;
|
|
288
|
+
const dup = isPromotion && onCurrent
|
|
289
|
+
// promote/rollback 当前内容:不改变 promoted 态 → no-op(promote 当已 promoted、rollback 当未 promoted)。
|
|
290
|
+
? (decision.decisionKind === 'promote') === isCurrentlyPromoted(prev)
|
|
291
|
+
: decision.contentHash !== undefined
|
|
292
|
+
&& prev.decisions.some((d) => d.decisionKind === decision.decisionKind && d.contentHash === decision.contentHash);
|
|
293
|
+
const merged = dup
|
|
294
|
+
? prev
|
|
295
|
+
: { ...prev, decisions: [...prev.decisions, decision] };
|
|
296
|
+
if (!dup) {
|
|
297
|
+
mkdirSync(dir, { recursive: true });
|
|
298
|
+
const path = recordPath(dir, recordId);
|
|
299
|
+
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
300
|
+
writeFileSync(tmp, JSON.stringify(merged, null, 2));
|
|
301
|
+
renameSync(tmp, path);
|
|
302
|
+
}
|
|
303
|
+
return merged;
|
|
304
|
+
}
|
|
221
305
|
/**
|
|
222
306
|
* 统一的记录构造点——所有消费方(CLI / 未来 server / SDK)经此组装,保证 schema 一致。
|
|
223
307
|
* 只接收事实(身份、源、hash、分发落点);evidence/decisions 恒为空(eval/promote 的地盘)。
|
|
@@ -244,9 +328,10 @@ export function recordManagedArtifact(record, opts = {}) {
|
|
|
244
328
|
/**
|
|
245
329
|
* 读时推导生命周期标签。
|
|
246
330
|
* - 源缺失或 hash 漂(当前内容 ≠ 记录 contentHash)→ stale;
|
|
331
|
+
* - 否则当前内容已有 promote 决定 → promoted;
|
|
247
332
|
* - 否则有**当前**证据 / samples → measurable;
|
|
248
333
|
* - 否则 installed。
|
|
249
|
-
*
|
|
334
|
+
* 「当前证据 / 当前决定」= contentHash 与记录当前 contentHash 匹配的 evidence / decision —— 旧内容的不算数,
|
|
250
335
|
* 这正是 #203「证据必须跟 artifact 一起走」的读时保证。`'discovered'` 留给无分发的记录,此函数不产生。
|
|
251
336
|
*/
|
|
252
337
|
export function deriveManagedState(input) {
|
|
@@ -255,6 +340,10 @@ export function deriveManagedState(input) {
|
|
|
255
340
|
const drifted = currentContentHash === undefined || currentContentHash !== record.contentHash;
|
|
256
341
|
if (drifted)
|
|
257
342
|
return { label: 'stale', drifted: true, hasEvidence };
|
|
343
|
+
// 当前内容(contentHash 匹配)最近一条 promote/rollback 决定是 promote → 已人工接受当前版本,排在 measurable
|
|
344
|
+
// 之上。rollback 之后落回 measurable/installed;换了内容(contentHash 变)后旧决定不冒充当前。
|
|
345
|
+
if (isCurrentlyPromoted(record))
|
|
346
|
+
return { label: 'promoted', drifted: false, hasEvidence };
|
|
258
347
|
if (hasEvidence || hasSamplesOrDoctorPass)
|
|
259
348
|
return { label: 'measurable', drifted: false, hasEvidence };
|
|
260
349
|
return { label: 'installed', drifted: false, hasEvidence };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor 报告详情页 — 逐条规则的健康检查结果。
|
|
3
|
+
*
|
|
4
|
+
* 主次:Hero 综合健康分 → 统计条(通过/警告/失败)→ 规则列表(失败/警告高亮在前,
|
|
5
|
+
* 通过项折叠)。每条规则展开显示 finding 描述 + 修复建议。
|
|
6
|
+
*/
|
|
7
|
+
import { type SkillReportContext } from './report-shell.js';
|
|
8
|
+
import type { Lang, DoctorReport } from '../types/index.js';
|
|
9
|
+
export declare function renderDoctorDetail(report: DoctorReport, skillName: string, langQ: string, lang?: Lang, skillContext?: SkillReportContext): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor 报告详情页 — 逐条规则的健康检查结果。
|
|
3
|
+
*
|
|
4
|
+
* 主次:Hero 综合健康分 → 统计条(通过/警告/失败)→ 规则列表(失败/警告高亮在前,
|
|
5
|
+
* 通过项折叠)。每条规则展开显示 finding 描述 + 修复建议。
|
|
6
|
+
*/
|
|
7
|
+
import { reportShell, healthColor } from './report-shell.js';
|
|
8
|
+
import { icon as svgIcon } from './icons.js';
|
|
9
|
+
import { e, DEFAULT_LANG } from './layout.js';
|
|
10
|
+
function getDetail(r) {
|
|
11
|
+
return (r.detail ?? {});
|
|
12
|
+
}
|
|
13
|
+
const SORT_ORDER = { fail: 0, warn: 1, pass: 2, skipped: 3 };
|
|
14
|
+
function fmtTs(ts) {
|
|
15
|
+
try {
|
|
16
|
+
const d = new Date(ts);
|
|
17
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return ts;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function renderRuleCard(r, lang, forceOpen = false) {
|
|
24
|
+
const det = getDetail(r);
|
|
25
|
+
const name = det.displayName ?? r.ruleId;
|
|
26
|
+
const findings = det.findings ?? [];
|
|
27
|
+
const errors = findings.filter((f) => f.level === '错误');
|
|
28
|
+
const warns = findings.filter((f) => f.level === '警告');
|
|
29
|
+
const tips = findings.filter((f) => f.level === '建议');
|
|
30
|
+
const cardCls = r.status === 'fail' ? 'fail' : r.status === 'warn' ? 'warn' : 'pass';
|
|
31
|
+
const dotCls = r.status === 'fail' ? 'fail' : r.status === 'warn' ? 'warn' : r.status === 'skipped' ? 'skip' : 'pass';
|
|
32
|
+
const badges = [];
|
|
33
|
+
if (errors.length)
|
|
34
|
+
badges.push(`<span class="rs-badge rs-badge--fail">${errors.length} ${lang === 'zh' ? '错误' : 'err'}</span>`);
|
|
35
|
+
if (warns.length)
|
|
36
|
+
badges.push(`<span class="rs-badge rs-badge--warn">${warns.length} ${lang === 'zh' ? '警告' : 'warn'}</span>`);
|
|
37
|
+
if (!errors.length && !warns.length)
|
|
38
|
+
badges.push(`<span class="rs-badge rs-badge--pass">${lang === 'zh' ? '通过' : 'pass'}</span>`);
|
|
39
|
+
const head = `<span class="rs-card-dot rs-card-dot--${dotCls}"></span><span class="rs-card-name">${e(name)}</span><span class="rs-card-badges">${badges.join('')}</span>`;
|
|
40
|
+
const allFindings = [...errors, ...warns, ...tips];
|
|
41
|
+
if (allFindings.length === 0) {
|
|
42
|
+
return `<div class="rs-card rs-card--${cardCls}"><div class="rs-card-head" style="cursor:default">${head}</div></div>`;
|
|
43
|
+
}
|
|
44
|
+
const findingHtml = allFindings.map((f) => {
|
|
45
|
+
const isErr = f.level === '错误';
|
|
46
|
+
const mIcon = isErr
|
|
47
|
+
? svgIcon('x', { size: 14, style: 'color:#dc2626;vertical-align:-2px;margin-right:5px' })
|
|
48
|
+
: svgIcon('warn', { size: 14, style: `color:${f.level === '警告' ? '#d97706' : '#637083'};vertical-align:-2px;margin-right:5px` });
|
|
49
|
+
return `<div class="rs-finding rs-finding--${isErr ? 'err' : 'warn'}">
|
|
50
|
+
<div class="rs-finding-desc">${mIcon}${e(f.description ?? '')}</div>
|
|
51
|
+
${f.suggestion ? `<div class="rs-finding-sug">${svgIcon('bulb', { size: 13, style: 'vertical-align:-2px;margin-right:5px' })}${e(f.suggestion)}</div>` : ''}
|
|
52
|
+
</div>`;
|
|
53
|
+
}).join('');
|
|
54
|
+
return `<details class="rs-card rs-card--${cardCls}" ${errors.length || forceOpen ? 'open' : ''}>
|
|
55
|
+
<summary class="rs-card-head">${head}</summary>
|
|
56
|
+
<div class="rs-card-body">${findingHtml}</div>
|
|
57
|
+
</details>`;
|
|
58
|
+
}
|
|
59
|
+
function renderSkillBlock(sk, lang) {
|
|
60
|
+
const rules = sk.results.filter((r) => !r.ruleId.endsWith(':_summary'));
|
|
61
|
+
const pass = rules.filter((r) => r.status === 'pass').length;
|
|
62
|
+
const warn = rules.filter((r) => r.status === 'warn').length;
|
|
63
|
+
const fail = rules.filter((r) => r.status === 'fail').length;
|
|
64
|
+
const total = rules.length;
|
|
65
|
+
const sorted = [...rules].sort((a, b) => (SORT_ORDER[a.status] ?? 9) - (SORT_ORDER[b.status] ?? 9));
|
|
66
|
+
const issues = sorted.filter((r) => r.status === 'fail' || r.status === 'warn');
|
|
67
|
+
const oks = sorted.filter((r) => r.status === 'pass' || r.status === 'skipped');
|
|
68
|
+
const zh = lang === 'zh';
|
|
69
|
+
const stats = `<div class="rs-stats">
|
|
70
|
+
<div class="rs-stat rs-stat--pass"><span class="rs-stat-num">${pass}</span><span class="rs-stat-label">${zh ? '通过' : 'Pass'}</span></div>
|
|
71
|
+
<div class="rs-stat rs-stat--warn"><span class="rs-stat-num">${warn}</span><span class="rs-stat-label">${zh ? '警告' : 'Warn'}</span></div>
|
|
72
|
+
<div class="rs-stat rs-stat--fail"><span class="rs-stat-num">${fail}</span><span class="rs-stat-label">${zh ? '失败' : 'Fail'}</span></div>
|
|
73
|
+
<div class="rs-stat rs-stat--total"><span class="rs-stat-num">${total}</span><span class="rs-stat-label">${zh ? '规则总数' : 'Rules'}</span></div>
|
|
74
|
+
</div>`;
|
|
75
|
+
const issuesHtml = issues.length
|
|
76
|
+
? `<div class="rs-panel"><div class="rs-panel-title">${zh ? '待处理项' : 'Issues'} <small>${issues.length}</small></div>${issues.map((r, i) => renderRuleCard(r, lang, i === 0)).join('')}</div>`
|
|
77
|
+
: `<div class="rs-panel"><div class="rs-empty">${zh ? '✓ 所有规则通过,无待处理项' : '✓ All rules passed'}</div></div>`;
|
|
78
|
+
const oksHtml = oks.length
|
|
79
|
+
? `<div class="rs-panel"><details class="rs-fold"><summary>${zh ? `展开 ${oks.length} 条通过的规则` : `Show ${oks.length} passed rules`}</summary>${oks.map((r) => renderRuleCard(r, lang)).join('')}</details></div>`
|
|
80
|
+
: '';
|
|
81
|
+
return stats + issuesHtml + oksHtml;
|
|
82
|
+
}
|
|
83
|
+
export function renderDoctorDetail(report, skillName, langQ, lang = DEFAULT_LANG, skillContext) {
|
|
84
|
+
const zh = lang === 'zh';
|
|
85
|
+
// 优先展示点进来的 skill;找不到就展示全部
|
|
86
|
+
const targetSkill = report.skills.find((s) => s.skillName === skillName) ?? report.skills[0];
|
|
87
|
+
const rules = (targetSkill?.results ?? []).filter((r) => !r.ruleId.endsWith(':_summary'));
|
|
88
|
+
const pass = rules.filter((r) => r.status === 'pass').length;
|
|
89
|
+
const warn = rules.filter((r) => r.status === 'warn').length;
|
|
90
|
+
const total = rules.length;
|
|
91
|
+
const score = total > 0 ? Math.round(((pass + warn * 0.5) / total) * 100) : null;
|
|
92
|
+
// 与 eval「综合分 / 5」统一成「X分 / 满分」形式;圆心显原生分数,等级由颜色传达。
|
|
93
|
+
const scoreLabel = zh ? '健康分 / 100' : 'Health / 100';
|
|
94
|
+
const body = targetSkill
|
|
95
|
+
? renderSkillBlock(targetSkill, lang)
|
|
96
|
+
: `<div class="rs-panel"><div class="rs-empty">${zh ? '报告无 skill 数据' : 'No skill data'}</div></div>`;
|
|
97
|
+
return reportShell({
|
|
98
|
+
dim: 'doctor',
|
|
99
|
+
icon: '🩺',
|
|
100
|
+
kindTitle: zh ? '健康体检报告' : 'Doctor Report',
|
|
101
|
+
skillName: targetSkill?.skillName ?? skillName,
|
|
102
|
+
metaItems: [`${svgIcon('clock', { size: 13 })} ${e(fmtTs(report.timestamp))}`, `${svgIcon('chip', { size: 13 })} ${e(report.model)}`, e(report.id)],
|
|
103
|
+
// Hero 展示「综合健康」总分(跨维度,与首页同口径),切 tab 不变;本次体检明细(规则/计数)在 body。
|
|
104
|
+
// 无 skillContext 时回退显本次体检健康分。
|
|
105
|
+
score: skillContext ? skillContext.overall.score : score,
|
|
106
|
+
scoreText: skillContext && skillContext.overall.score != null ? String(skillContext.overall.score) : undefined,
|
|
107
|
+
ringColor: skillContext ? healthColor(skillContext.overall.score) : undefined,
|
|
108
|
+
scoreLabel: skillContext ? (zh ? '综合健康' : 'Health') : scoreLabel,
|
|
109
|
+
backHref: `/${langQ}`,
|
|
110
|
+
backLabel: zh ? '返回列表' : 'Back to list',
|
|
111
|
+
body,
|
|
112
|
+
skillContext,
|
|
113
|
+
}, lang);
|
|
114
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML report renderer — orchestrates sub-modules.
|
|
3
3
|
*/
|
|
4
|
+
import { type SkillReportContext } from './report-shell.js';
|
|
4
5
|
import type { BatchEvaluationReport, EvaluationReport, Report, ReportDocument, Lang } from '../types/index.js';
|
|
5
6
|
export declare function renderRunList(runs: ReportDocument[], lang?: Lang): string;
|
|
6
|
-
export declare function renderRunDetail(report: EvaluationReport | null, lang?: Lang): string;
|
|
7
|
+
export declare function renderRunDetail(report: EvaluationReport | null, lang?: Lang, skillContext?: SkillReportContext): string;
|
|
7
8
|
export declare function renderBatchEvaluationDetail(report: BatchEvaluationReport | null, lang?: Lang): string;
|
|
8
|
-
export declare function renderReportDocumentDetail(report: ReportDocument | null, lang?: Lang): string;
|
|
9
|
+
export declare function renderReportDocumentDetail(report: ReportDocument | null, lang?: Lang, skillContext?: SkillReportContext): string;
|
|
9
10
|
export declare function renderTrendsPage(variantName: string, runs: Report[], lang?: Lang): string;
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { e, fmtCost, fmtDuration, fmtKnownCost, COLORS, DEFAULT_LANG, t, layout } from './layout.js';
|
|
5
5
|
import { renderAgentOverview, renderAnalysis, renderKnowledgeInteractionSection, renderMethodologyAudit, renderScoringModal, renderSummaryCards, renderVarianceComparisons, renderVerdictPill, levelLabel, levelTooltip, } from './summary.js';
|
|
6
|
-
import { renderSampleTable } from './table.js';
|
|
7
6
|
import { renderTestView, TEST_VIEW_CSS, TEST_VIEW_JS } from './test-view.js';
|
|
8
7
|
import { renderTrendsBody } from './trends.js';
|
|
8
|
+
import { reportShell, healthColor } from './report-shell.js';
|
|
9
|
+
import { icon } from './icons.js';
|
|
9
10
|
import { computeVerdict } from '../eval-core/verdict.js';
|
|
10
11
|
// v0.21 B.4 — 列表页 status pill 用的 dot. PROGRESS/REGRESS 实心(强信号),
|
|
11
12
|
// CAUTIOUS 三角(警示),NOISE 空心圆(有信号但无效果),UNDERPOWERED 部分填充
|
|
@@ -316,7 +317,7 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
|
|
|
316
317
|
</main>
|
|
317
318
|
`, lang);
|
|
318
319
|
}
|
|
319
|
-
export function renderRunDetail(report, lang = DEFAULT_LANG) {
|
|
320
|
+
export function renderRunDetail(report, lang = DEFAULT_LANG, skillContext) {
|
|
320
321
|
const langQ = lang === DEFAULT_LANG ? '' : `?lang=${lang}`;
|
|
321
322
|
if (!report) {
|
|
322
323
|
return layout(t('title', lang), `
|
|
@@ -329,11 +330,24 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
|
|
|
329
330
|
const m = report.meta;
|
|
330
331
|
const variants = m.variants || [];
|
|
331
332
|
const summary = report.summary || {};
|
|
332
|
-
const results = report.results || [];
|
|
333
333
|
const cards = renderSummaryCards(variants, summary, lang, report.variance);
|
|
334
334
|
const methodologyAudit = renderMethodologyAudit(report, variants, summary, lang);
|
|
335
|
-
|
|
336
|
-
const
|
|
335
|
+
// 单 variant(SOLO)不显示 verdict — 对照才有的概念,单组是噪音/误导(见融合设计)。
|
|
336
|
+
const verdictPill = variants.length > 1 ? renderVerdictPill(report, lang) : '';
|
|
337
|
+
// Hero:综合分 ring(取 treatment = 最后一个 variant 的综合分)+ skill 名 + 时间戳。
|
|
338
|
+
const treatmentName = variants[variants.length - 1];
|
|
339
|
+
const heroComposite = treatmentName ? (summary[treatmentName]?.avgCompositeScore ?? null) : null;
|
|
340
|
+
const heroScore100 = heroComposite != null ? Math.round((heroComposite / 5) * 100) : null;
|
|
341
|
+
const heroSkillName = report.meta.evolve?.skillName ?? treatmentName ?? report.id;
|
|
342
|
+
const heroTs = (() => {
|
|
343
|
+
try {
|
|
344
|
+
const d = new Date(report.meta.timestamp);
|
|
345
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return '';
|
|
349
|
+
}
|
|
350
|
+
})();
|
|
337
351
|
const totalExecCost = Object.values(summary).reduce((s, v) => s + (v.totalExecCostUSD || 0), 0);
|
|
338
352
|
// 任一 variant exec cost 未报告 → 总 cost 不可靠,renderer 显示「—」+ tooltip。
|
|
339
353
|
// 全部 reported(undefined / true)才正常显示 USD 数字。
|
|
@@ -420,14 +434,6 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
|
|
|
420
434
|
` : '';
|
|
421
435
|
// v0.30 重设计 — 评分/单测各自独立 title + meta strip(不共享头部)。
|
|
422
436
|
// 顶部只保留极简身份带:返回链接 + run-id pill。完整 H1 + meta tags 移进各自 tab 面板。
|
|
423
|
-
const idStamp = (() => {
|
|
424
|
-
const idMatch = /^.+?-(\d{8})-(\d{4})$/.exec(report.id);
|
|
425
|
-
if (idMatch) {
|
|
426
|
-
const [, ymd, hm] = idMatch;
|
|
427
|
-
return `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)} ${hm.slice(0, 2)}:${hm.slice(2, 4)}`;
|
|
428
|
-
}
|
|
429
|
-
return '';
|
|
430
|
-
})();
|
|
431
437
|
// ──────────── 评分 tab 的 meta tags(原版全部留这里) ────────────
|
|
432
438
|
const scoreMetaTags = `<div class="meta-tags">
|
|
433
439
|
<span class="meta-tag">${t('model', lang)}: ${e(m.model)}</span>
|
|
@@ -469,71 +475,61 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
|
|
|
469
475
|
${Object.entries(m.blindMap || {}).map(([label, real]) => `<div style="font-size:13px;color:var(--text-secondary)"><strong>Variant ${e(label)}</strong> → ${e(real)}</div>`).join('')}
|
|
470
476
|
</div>
|
|
471
477
|
</div>` : '';
|
|
472
|
-
// ────────────
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
<nav class="nav"><a href="/${langQ}" data-i18n="backToList">${t('backToList', lang)}</a></nav>
|
|
499
|
-
<div class="report-id-line">
|
|
500
|
-
<code class="report-id-pill">${e(report.id)}</code>
|
|
501
|
-
${idStamp ? `<span class="report-id-stamp">${e(idStamp)}</span>` : ''}
|
|
502
|
-
</div>
|
|
503
|
-
|
|
504
|
-
<div class="omk-view-tabs" role="tablist">
|
|
505
|
-
<button type="button" class="omk-view-tab omk-view-tab--active" data-view="score" onclick="omkSwitchView('score')">${lang === 'zh' ? '📊 评测视角' : '📊 Score view'}</button>
|
|
506
|
-
<button type="button" class="omk-view-tab" data-view="test" onclick="omkSwitchView('test')">${lang === 'zh' ? '✅ 功能视角' : '✅ Functional view'}</button>
|
|
507
|
-
</div>
|
|
508
|
-
|
|
509
|
-
<div class="omk-view-panel" data-view="score">
|
|
510
|
-
<p class="subtitle">${lang === 'zh' ? '主观质量打分(judge)+ 客观断言聚合,产出综合评分与 verdict' : 'Subjective judge + objective assertions aggregated into composite + verdict'}</p>
|
|
511
|
-
${verdictPill}
|
|
512
|
-
${scoreMetaTags}
|
|
478
|
+
// ──────────── 融合单页:Hero → 结论(verdict+六维) → 折叠次要区 → 统一逐用例 ────────────
|
|
479
|
+
const agentOverview = renderAgentOverview(variants, summary, lang);
|
|
480
|
+
const varianceSection = renderVarianceComparisons(report.variance, lang, Boolean(report.meta.layeredStats), summary);
|
|
481
|
+
const analysisSection = renderAnalysis(report, lang);
|
|
482
|
+
const knowledgeSection = renderKnowledgeInteractionSection(report.analysis?.coverage, report.analysis?.gapReports, lang);
|
|
483
|
+
// 折叠区 A:实验如何跑的(配置 / 运行参数 / Agent 概览 / 审计指纹)— 复现/审计才看。
|
|
484
|
+
const setupInner = [
|
|
485
|
+
scoreMetaTags,
|
|
486
|
+
(variantConfigSection || agentOverview) ? `<div class="ctx-strip">${variantConfigSection}${agentOverview}</div>` : '',
|
|
487
|
+
auditFingerprints,
|
|
488
|
+
].filter(Boolean).join('');
|
|
489
|
+
const setupFold = setupInner
|
|
490
|
+
? `<details class="ev-fold"><summary>${lang === 'zh' ? '实验配置与运行参数' : 'Experiment setup & run params'}</summary><div class="ev-fold-body">${setupInner}</div></details>`
|
|
491
|
+
: '';
|
|
492
|
+
// 折叠区 B:深入分析(方法学审计 / 方差 / 自动分析 / 知识交互)— 想验证测量学时展开。
|
|
493
|
+
const analysisInner = [methodologyAudit, varianceSection, analysisSection, knowledgeSection].filter(Boolean).join('');
|
|
494
|
+
const analysisFold = analysisInner
|
|
495
|
+
? `<details class="ev-fold"><summary>${lang === 'zh' ? '方法学审计 · 方差 · 自动分析 · 知识交互' : 'Methodology audit · variance · analysis · knowledge'}</summary><div class="ev-fold-body">${analysisInner}</div></details>`
|
|
496
|
+
: '';
|
|
497
|
+
// 「结论」panel:多 variant = verdict 一句话领头 + 六维表;单 variant = 直接六维分布。
|
|
498
|
+
const conclusionPanel = `<section class="ev-conclusion">${verdictPill}${cards}</section>`;
|
|
499
|
+
const samplesSubtitle = lang === 'zh'
|
|
500
|
+
? '每条用例既是评分卡也是功能测试:综合分 + 三层分布、断言、诊断与执行轨迹合一'
|
|
501
|
+
: 'Each sample is both a score card and a functional test: composite + layered scores, assertions, diagnostics, and trace combined';
|
|
502
|
+
const body = `
|
|
503
|
+
${conclusionPanel}
|
|
513
504
|
${blindReveal}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
505
|
+
${setupFold}
|
|
506
|
+
${analysisFold}
|
|
507
|
+
<section class="ev-samples">
|
|
508
|
+
<h2 class="ev-samples-h">${lang === 'zh' ? '逐用例' : 'Per-sample'}</h2>
|
|
509
|
+
<p class="subtitle ev-samples-sub">${samplesSubtitle}</p>
|
|
510
|
+
${renderTestView(report, lang)}
|
|
511
|
+
</section>${TEST_VIEW_JS ? `<script>${TEST_VIEW_JS}</script>` : ''}`;
|
|
512
|
+
const evalCss = `${TEST_VIEW_CSS}
|
|
513
|
+
/* 结论 panel — verdict(多 variant)+ 六维,白底卡片成「答案」模块,与 hero / fold 同一视觉语言 */
|
|
514
|
+
.ev-conclusion{background:#fff;border:1px solid #e4e8f1;border-radius:12px;box-shadow:0 8px 24px rgba(31,41,55,4%);padding:18px 24px 20px;margin:0 0 16px}
|
|
515
|
+
.ev-conclusion>:first-child{margin-top:0}
|
|
516
|
+
.ev-conclusion>h2:first-child{margin-top:2px}
|
|
517
|
+
/* 次要折叠区 — 实验配置 / 方法学 / 方差 / 分析,默认收起,审计/复现时展开 */
|
|
518
|
+
.ev-fold{margin:10px 0;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius)}
|
|
519
|
+
.ev-fold>summary{cursor:pointer;list-style:none;font-size:13px;font-weight:500;color:var(--text-secondary);padding:12px 16px}
|
|
520
|
+
.ev-fold>summary::-webkit-details-marker{display:none}
|
|
521
|
+
.ev-fold>summary::before{content:'▸ ';color:var(--text-muted)}
|
|
522
|
+
.ev-fold[open]>summary::before{content:'▾ '}
|
|
523
|
+
.ev-fold[open]>summary{border-bottom:1px solid var(--border);color:var(--text-primary)}
|
|
524
|
+
.ev-fold>summary:hover{color:var(--text-primary)}
|
|
525
|
+
.ev-fold-body{padding:8px 16px 16px}
|
|
526
|
+
.ev-fold-body>section:first-child,.ev-fold-body>.audit-fingerprints:first-child{margin-top:0}
|
|
527
|
+
.ev-fold-body .meta-tags{margin-top:8px}
|
|
528
|
+
/* 逐用例 section 标题 */
|
|
529
|
+
.ev-samples{margin-top:28px}
|
|
530
|
+
.ev-samples-h{font-size:1.4rem;margin:0 0 4px;color:var(--text-primary)}
|
|
531
|
+
.ev-samples-sub{font-size:13.5px;color:var(--text-secondary);margin:0 0 8px;line-height:1.6}
|
|
532
|
+
/* Context strip — 实验配置 + Agent 概览并排,辅助元数据 */
|
|
537
533
|
.ctx-strip { display:grid;gap:14px;grid-template-columns:1fr 1fr;margin:14px 0 24px }
|
|
538
534
|
@media (max-width:760px) { .ctx-strip { grid-template-columns:1fr } }
|
|
539
535
|
.ctx-block { background:var(--bg-surface);border-radius:var(--radius);box-shadow:var(--shadow-sm);padding:12px 16px }
|
|
@@ -543,26 +539,30 @@ export function renderRunDetail(report, lang = DEFAULT_LANG) {
|
|
|
543
539
|
.ctx-row { padding:8px 12px;background:var(--bg-soft);border-radius:4px;display:flex;align-items:center;gap:10px;font-size:13px;line-height:1.5;flex-wrap:wrap }
|
|
544
540
|
.ctx-row-name { font-weight:600;color:var(--text-primary);font-family:"SF Mono",Menlo,monospace;font-size:12.5px;letter-spacing:-0.01em }
|
|
545
541
|
.ctx-row-bits { color:var(--text-secondary);flex:1;min-width:0;font-size:12.5px }
|
|
546
|
-
.ctx-sep { color:var(--text-muted);margin:0 2px;opacity:0.6 }
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
542
|
+
.ctx-sep { color:var(--text-muted);margin:0 2px;opacity:0.6 }`;
|
|
543
|
+
return reportShell({
|
|
544
|
+
dim: 'eval',
|
|
545
|
+
icon: '🧪',
|
|
546
|
+
kindTitle: lang === 'zh' ? '用例评测报告' : 'Eval Report',
|
|
547
|
+
// 头部用规范 skill 名(skill-index entry),与 doctor/observe 一致;无 context 时回退到 report 推导名。
|
|
548
|
+
skillName: skillContext?.skillName ?? heroSkillName,
|
|
549
|
+
metaItems: [`${icon('clock', { size: 13 })} ${e(heroTs)}`, `${icon('chip', { size: 13 })} ${e(m.model)}`, e(report.id)],
|
|
550
|
+
// Hero 展示「综合健康」总分(跨 doctor/eval/observe,与首页同口径),切 tab 不变;
|
|
551
|
+
// 本次 eval 的综合分(4.09/5)在下方结论卡。无 skillContext(batch/孤儿报告)时回退显 eval 综合分。
|
|
552
|
+
score: skillContext ? skillContext.overall.score : heroScore100,
|
|
553
|
+
scoreText: skillContext
|
|
554
|
+
? (skillContext.overall.score != null ? String(skillContext.overall.score) : undefined)
|
|
555
|
+
: (heroComposite != null ? heroComposite.toFixed(2) : undefined),
|
|
556
|
+
ringColor: skillContext
|
|
557
|
+
? healthColor(skillContext.overall.score)
|
|
558
|
+
: (heroComposite != null ? (heroComposite >= 4 ? '#1f9d63' : heroComposite >= 3 ? '#d97706' : '#dc2626') : undefined),
|
|
559
|
+
scoreLabel: skillContext ? (lang === 'zh' ? '综合健康' : 'Health') : (lang === 'zh' ? '综合分 / 5' : 'Composite / 5'),
|
|
560
|
+
backHref: `/${langQ}`,
|
|
561
|
+
backLabel: lang === 'zh' ? '返回列表' : 'Back to list',
|
|
562
|
+
body,
|
|
563
|
+
skillContext,
|
|
564
|
+
extraCss: evalCss,
|
|
565
|
+
}, lang);
|
|
566
566
|
}
|
|
567
567
|
export function renderBatchEvaluationDetail(report, lang = DEFAULT_LANG) {
|
|
568
568
|
const langQ = lang === DEFAULT_LANG ? '' : `?lang=${lang}`;
|
|
@@ -649,12 +649,12 @@ export function renderBatchEvaluationDetail(report, lang = DEFAULT_LANG) {
|
|
|
649
649
|
</main>
|
|
650
650
|
`, lang);
|
|
651
651
|
}
|
|
652
|
-
export function renderReportDocumentDetail(report, lang = DEFAULT_LANG) {
|
|
652
|
+
export function renderReportDocumentDetail(report, lang = DEFAULT_LANG, skillContext) {
|
|
653
653
|
if (!report)
|
|
654
654
|
return renderRunDetail(null, lang);
|
|
655
655
|
return report.kind === 'batch-evaluation'
|
|
656
656
|
? renderBatchEvaluationDetail(report, lang)
|
|
657
|
-
: renderRunDetail(report, lang);
|
|
657
|
+
: renderRunDetail(report, lang, skillContext);
|
|
658
658
|
}
|
|
659
659
|
export function renderTrendsPage(variantName, runs, lang = DEFAULT_LANG) {
|
|
660
660
|
const body = renderTrendsBody(variantName, runs, lang);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享单色线性 SVG 图标集(替代各处 emoji)。
|
|
3
|
+
*
|
|
4
|
+
* 设计:统一 24×24 viewBox、`stroke="currentColor"`、单一描边权重、圆角端点。
|
|
5
|
+
* 颜色由调用处的 CSS `color` 控制(图标自身不带色),尺寸/粗细可覆盖。
|
|
6
|
+
* 之所以自带一套而不用图标库:报告是离线自包含 HTML,内联 SVG 无需外链/字体。
|
|
7
|
+
*/
|
|
8
|
+
/** name → viewBox 内的 path/shape 串。 */
|
|
9
|
+
declare const PATHS: Record<string, string>;
|
|
10
|
+
export type IconName = keyof typeof PATHS;
|
|
11
|
+
export interface IconOpts {
|
|
12
|
+
/** 像素尺寸(宽高相同),默认 16。 */
|
|
13
|
+
size?: number;
|
|
14
|
+
/** 额外 class。 */
|
|
15
|
+
cls?: string;
|
|
16
|
+
/** 描边权重,默认 1.7。 */
|
|
17
|
+
sw?: number;
|
|
18
|
+
/** 内联 style。 */
|
|
19
|
+
style?: string;
|
|
20
|
+
}
|
|
21
|
+
/** 返回一个自包含 `<svg>` 串。未知 name 返回空串(不抛错,调用处可安全拼接)。 */
|
|
22
|
+
export declare function icon(name: string, opts?: IconOpts): string;
|
|
23
|
+
/** 维度 → 报告类型图标名。 */
|
|
24
|
+
export declare function dimIconName(dim: 'doctor' | 'eval' | 'observe'): IconName;
|
|
25
|
+
/** 品牌 logo 的完整 `<svg>`(给定像素尺寸)。 */
|
|
26
|
+
export declare function brandLogo(size?: number): string;
|
|
27
|
+
/** 品牌 logo 的原始 `<svg>`(无尺寸,供 favicon data URI 编码用)。 */
|
|
28
|
+
export declare const BRAND_LOGO_RAW = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 120 120\" fill=\"none\"><defs><linearGradient id=\"omkAccent\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\"><stop offset=\"0%\" stop-color=\"#60a5fa\"/><stop offset=\"100%\" stop-color=\"#a78bfa\"/></linearGradient></defs><circle cx=\"60\" cy=\"60\" r=\"56\" fill=\"#0f172a\" stroke=\"#1e293b\" stroke-width=\"1.5\"/><path d=\"M24 60 A12 12 0 1 1 36 72\" stroke=\"url(#omkAccent)\" stroke-width=\"3.5\" stroke-linecap=\"round\" fill=\"none\"/><path d=\"M56 72 V57 Q56 48 61.5 48 Q67 48 67 57 V72 M67 57 Q67 48 72.5 48 Q78 48 78 57 V72\" stroke=\"#e2e8f0\" stroke-width=\"3.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" fill=\"none\"/><path d=\"M86 48 V72 M86 60 L96 48 M86 60 L96 72\" stroke=\"#e2e8f0\" stroke-width=\"3.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" fill=\"none\"/></svg>";
|
|
29
|
+
export {};
|