oh-my-knowledge 0.37.0 → 0.38.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 +1 -1
- package/README.zh.md +1 -1
- package/dist/assets/agent-skills/omk/SKILL.md +1 -1
- package/dist/assets/agent-skills/omk/references/commands.md +21 -12
- package/dist/authoring/evolver.d.ts +4 -1
- package/dist/authoring/evolver.js +13 -8
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +26 -6
- package/dist/cli/commands/eval/gold/compare.js +7 -4
- package/dist/cli/commands/eval/index.d.ts +1 -0
- package/dist/cli/commands/eval/index.js +14 -3
- package/dist/cli/commands/evolve.d.ts +1 -0
- package/dist/cli/commands/evolve.js +45 -0
- package/dist/cli/commands/init.js +18 -6
- package/dist/cli/commands/list.d.ts +0 -2
- package/dist/cli/commands/list.js +2 -7
- package/dist/cli/commands/observe/inbox.d.ts +1 -0
- package/dist/cli/commands/observe/inbox.js +14 -4
- package/dist/cli/commands/observe/index.d.ts +11 -0
- package/dist/cli/commands/observe/index.js +35 -12
- package/dist/cli/commands/observe/ingest.d.ts +1 -0
- package/dist/cli/commands/observe/ingest.js +13 -4
- package/dist/cli/commands/observe/show.d.ts +1 -0
- package/dist/cli/commands/observe/show.js +12 -2
- package/dist/cli/commands/promote.js +1 -2
- package/dist/cli/commands/sample.js +15 -9
- package/dist/cli/commands/studio.d.ts +2 -0
- package/dist/cli/commands/studio.js +62 -12
- package/dist/cli/lib/cmd-flags.d.ts +8 -0
- package/dist/cli/lib/i18n-dict/common.js +1 -1
- package/dist/cli/lib/i18n-dict/evolve.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/evolve.js +8 -0
- package/dist/cli/lib/i18n-dict/help.js +27 -45
- package/dist/cli/lib/i18n-dict/init.js +2 -2
- package/dist/cli/lib/parse-run-config.d.ts +0 -1
- package/dist/cli/lib/parse-run-config.js +6 -4
- package/dist/cli/lib/record-evolve-outcome.d.ts +31 -0
- package/dist/cli/lib/record-evolve-outcome.js +110 -0
- package/dist/cli/lib/resolve-skill-input.d.ts +6 -0
- package/dist/cli/lib/resolve-skill-input.js +4 -2
- package/dist/diagnosis/types.js +1 -1
- package/dist/doctor/index.js +4 -1
- package/dist/eval-core/artifact-index.d.ts +106 -0
- package/dist/eval-core/artifact-index.js +277 -0
- package/dist/eval-core/default-dirs.d.ts +34 -0
- package/dist/eval-core/default-dirs.js +36 -0
- package/dist/eval-core/evaluation-execution.js +3 -3
- package/dist/eval-core/evaluation-reporting.d.ts +7 -0
- package/dist/eval-core/evaluation-reporting.js +20 -5
- package/dist/eval-core/execution-strategy.js +3 -3
- package/dist/eval-core/measurement-dirs.d.ts +18 -0
- package/dist/eval-core/measurement-dirs.js +76 -0
- package/dist/eval-workflows/batch-evaluation-workflow.js +2 -1
- package/dist/eval-workflows/evaluation-pipeline/run-state.js +2 -1
- package/dist/eval-workflows/run-evaluation.js +5 -2
- package/dist/executors/codex-cli.js +2 -2
- package/dist/executors/script.js +43 -4
- package/dist/inputs/materialize-copy.d.ts +1 -1
- package/dist/inputs/materialize-copy.js +3 -3
- package/dist/inputs/skill-loader.d.ts +1 -1
- package/dist/inputs/skill-loader.js +1 -1
- package/dist/managed/index.d.ts +2 -0
- package/dist/managed/index.js +2 -0
- package/dist/managed/list-query.d.ts +7 -0
- package/dist/managed/list-query.js +11 -0
- package/dist/managed/source-probe.d.ts +15 -0
- package/dist/managed/source-probe.js +129 -0
- package/dist/managed/store.d.ts +10 -0
- package/dist/managed/store.js +25 -2
- package/dist/observability/inbox.js +6 -3
- package/dist/observability/skill-health-analyzer.d.ts +3 -0
- package/dist/observability/skill-health-analyzer.js +2 -0
- package/dist/renderer/html-renderer.js +4 -2
- package/dist/renderer/managed-history-renderer.d.ts +5 -0
- package/dist/renderer/managed-history-renderer.js +255 -0
- package/dist/renderer/observation-inbox-renderer.js +19 -19
- package/dist/renderer/skill-detail-renderer.js +17 -8
- package/dist/renderer/skill-list-renderer.js +1 -0
- package/dist/server/indexed-report-store.d.ts +8 -0
- package/dist/server/indexed-report-store.js +130 -0
- package/dist/server/job-store.d.ts +0 -1
- package/dist/server/job-store.js +0 -2
- package/dist/server/report-server.d.ts +17 -3
- package/dist/server/report-server.js +231 -85
- package/dist/server/report-store.d.ts +14 -8
- package/dist/server/report-store.js +46 -0
- package/dist/server/skill-index.d.ts +7 -1
- package/dist/server/skill-index.js +48 -12
- package/dist/types/executor.d.ts +4 -2
- package/dist/types/report.d.ts +15 -0
- package/dist/types/skill-index.d.ts +3 -0
- package/package.json +1 -1
|
@@ -5,12 +5,26 @@ interface ReportServerOptions {
|
|
|
5
5
|
* 暴露到容器外 / 局域网用 '0.0.0.0'。也可走 OMK_REPORT_HOST 环境变量。 */
|
|
6
6
|
host?: string;
|
|
7
7
|
reportsDir?: string;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** observe-health 报告目录(项目 .omk/observe-health 或全局),或一个按请求动态解析它的函数。
|
|
9
|
+
* 传函数 → 每次请求重解析(长会话里项目首次 omk observe 后从 global 实时切回 project);
|
|
10
|
+
* 传字符串 → 固定到该目录(测试 / 显式覆盖);默认动态解析 project→global 权威目录。 */
|
|
11
|
+
analysesDir?: string | (() => string);
|
|
12
|
+
/** 体检报告目录(项目 .omk/doctors 或全局),或一个按请求动态解析它的函数。三模式同 analysesDir。 */
|
|
13
|
+
doctorsDir?: string | (() => string);
|
|
10
14
|
observationsDir?: string;
|
|
11
15
|
jobsDir?: string;
|
|
16
|
+
/** 受管目录(.omk/managed 或全局),或一个按请求动态解析它的函数。只读消费,供 /managed 决策史页。
|
|
17
|
+
* 传函数 → 每次请求重解析(长会话里跟 omk list 同口径:项目首次 install 后从 global 实时切回 project);
|
|
18
|
+
* 传字符串 → 固定到该目录(测试 / 显式覆盖);默认动态解析 project→global 权威目录。 */
|
|
19
|
+
managedDir?: string | (() => string);
|
|
12
20
|
store?: ReportStore;
|
|
13
21
|
jobStore?: JobStore;
|
|
22
|
+
/** 是否把别项目的 observe-health 索引卡片合进列表 / 详情 / skill 首页(机器级总览)。
|
|
23
|
+
* 默认 false(固定目录 / 测试 hermetic);studio 仅在「无 --analyses-dir 且无 --global」的默认机器级模式传 true。 */
|
|
24
|
+
includeObserveCards?: boolean;
|
|
25
|
+
/** 是否把别项目的 doctor 索引卡片合进 skill 首页 / 详情兜底(机器级总览)。
|
|
26
|
+
* 默认 false;studio 仅在「无 --doctors-dir 且无 --global」的默认机器级模式传 true。 */
|
|
27
|
+
includeDoctorCards?: boolean;
|
|
14
28
|
}
|
|
15
29
|
interface ReportServer {
|
|
16
30
|
start(): Promise<string>;
|
|
@@ -18,5 +32,5 @@ interface ReportServer {
|
|
|
18
32
|
getUrl(): string | null;
|
|
19
33
|
}
|
|
20
34
|
export declare function formatListenError(p: number, err: unknown): Error | null;
|
|
21
|
-
export declare function createReportServer({ port, host: hostOption, reportsDir, analysesDir, doctorsDir, observationsDir, jobsDir, store, jobStore }?: ReportServerOptions): ReportServer;
|
|
35
|
+
export declare function createReportServer({ port, host: hostOption, reportsDir, analysesDir, doctorsDir, observationsDir, jobsDir, managedDir, store, jobStore, includeObserveCards, includeDoctorCards }?: ReportServerOptions): ReportServer;
|
|
22
36
|
export {};
|
|
@@ -2,7 +2,6 @@ import { createServer } from 'node:http';
|
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
5
|
import { renderReportDocumentDetail, renderTrendsPage, renderRunList } from '../renderer/html-renderer.js';
|
|
7
6
|
import { renderSkillList } from '../renderer/skill-list-renderer.js';
|
|
8
7
|
import { renderSkillHealthReport } from '../renderer/skill-health-renderer.js';
|
|
@@ -10,9 +9,15 @@ import { renderDoctorDetail } from '../renderer/doctor-detail-renderer.js';
|
|
|
10
9
|
import { assessHealth } from '../renderer/skill-detail-renderer.js';
|
|
11
10
|
import { renderObservationInboxPage } from '../renderer/observation-inbox-renderer.js';
|
|
12
11
|
import { DEFAULT_LANG, t, layout } from '../renderer/layout.js';
|
|
12
|
+
import { loadAllManagedRecords, resolveManagedDir, managedDir as projectManagedDir, listManagedRows } from '../managed/index.js';
|
|
13
|
+
import { renderManagedList, renderManagedHistory } from '../renderer/managed-history-renderer.js';
|
|
14
|
+
import { DEFAULT_JOBS_DIR } from '../eval-core/default-dirs.js';
|
|
15
|
+
import { resolveObserveHealthDir, projectObserveHealthDir, resolveDoctorsDir, projectDoctorsDir, projectReportsDir, globalReportsDir } from '../eval-core/measurement-dirs.js';
|
|
16
|
+
import { listObserveCards, listDoctorCards, listLiveObserveCards } from '../eval-core/artifact-index.js';
|
|
13
17
|
import { buildSkillIndex } from './skill-index.js';
|
|
14
|
-
import { createFileJobStore
|
|
18
|
+
import { createFileJobStore } from './job-store.js';
|
|
15
19
|
import { createFileStore, queryJob, queryJobList, queryRun, queryRunList, queryTrend } from './report-store.js';
|
|
20
|
+
import { createIndexedReportStore } from './indexed-report-store.js';
|
|
16
21
|
import { confidenceOf } from '../observability/skill-health-analyzer.js';
|
|
17
22
|
import { DEFAULT_OBSERVATIONS_DIR, findObservationInboxItem, formatObservationShow, queryObservationInbox } from '../observability/inbox.js';
|
|
18
23
|
import { buildObservationInboxViewModel } from '../observability/inbox-view-model.js';
|
|
@@ -21,9 +26,6 @@ import { deleteObservationReviewState, loadObservationReviewState, updateObserva
|
|
|
21
26
|
import { updateSkillDerivedStandardStatus } from '../observability/soft-standards/index.js';
|
|
22
27
|
const DEFAULT_PORT = 7799;
|
|
23
28
|
const PORT_HINT = `OMK_REPORT_PORT=${DEFAULT_PORT} omk eval ...`;
|
|
24
|
-
const DEFAULT_REPORTS_DIR = join(homedir(), '.oh-my-knowledge', 'reports');
|
|
25
|
-
const DEFAULT_ANALYSES_DIR = join(homedir(), '.oh-my-knowledge', 'analyses');
|
|
26
|
-
const DEFAULT_DOCTORS_DIR = join(homedir(), '.oh-my-knowledge', 'doctors');
|
|
27
29
|
let cachedChartJsBytes;
|
|
28
30
|
function loadChartJsBundle() {
|
|
29
31
|
if (cachedChartJsBytes !== undefined)
|
|
@@ -42,75 +44,124 @@ function loadChartJsBundle() {
|
|
|
42
44
|
}
|
|
43
45
|
return cachedChartJsBytes;
|
|
44
46
|
}
|
|
45
|
-
function listAnalyses(dir) {
|
|
46
|
-
if (!existsSync(dir))
|
|
47
|
-
return [];
|
|
47
|
+
function listAnalyses(dir, includeCards = false) {
|
|
48
48
|
const items = [];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
// live 扫描 dir 存在才做;dir 不存在(默认机器级模式下当前项目还没 .omk/observe-health、全局也空)时 live 为空,
|
|
50
|
+
// 但**不能早退** —— 后面仍要按 includeCards 合并别项目卡片,否则 observe 列表会与合卡片的 /api/skills 口径分裂。
|
|
51
|
+
if (existsSync(dir)) {
|
|
52
|
+
for (const file of readdirSync(dir)) {
|
|
53
|
+
if (!file.endsWith('.json'))
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
const data = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
57
|
+
if (!data.meta || !data.overall)
|
|
58
|
+
continue;
|
|
59
|
+
items.push({
|
|
60
|
+
id: file.replace(/\.json$/, ''),
|
|
61
|
+
generatedAt: data.meta.generatedAt,
|
|
62
|
+
sessionCount: data.meta.sessionCount,
|
|
63
|
+
segmentCount: data.meta.segmentCount,
|
|
64
|
+
skillCount: Object.keys(data.bySkill || {}).length,
|
|
65
|
+
healthBand: data.overall.healthBand,
|
|
66
|
+
// 旧 JSON 缺 confidence 时按 segmentCount 兜底,跟 Studio / CLI 口径一致。
|
|
67
|
+
confidence: data.overall.confidence ?? confidenceOf(data.meta.segmentCount),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch { /* skip corrupt */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 别项目的 observe 卡片(当前 dir live 扫不到的项目)→ list item,dedup by id(live 盖卡片)。
|
|
74
|
+
// 仅机器级模式合并;固定 --analyses-dir / --global 时 includeCards=false,只看该目录(逃生舱语义)。
|
|
75
|
+
if (includeCards) {
|
|
76
|
+
const seen = new Set(items.map((i) => i.id));
|
|
77
|
+
for (const card of listLiveObserveCards()) {
|
|
78
|
+
if (seen.has(card.id))
|
|
55
79
|
continue;
|
|
56
80
|
items.push({
|
|
57
|
-
id:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
segmentCount: data.meta.segmentCount,
|
|
61
|
-
skillCount: Object.keys(data.bySkill || {}).length,
|
|
62
|
-
healthBand: data.overall.healthBand,
|
|
63
|
-
// 旧 JSON 缺 confidence 时按 segmentCount 兜底,跟 Studio / CLI 口径一致。
|
|
64
|
-
confidence: data.overall.confidence ?? confidenceOf(data.meta.segmentCount),
|
|
81
|
+
id: card.id, generatedAt: card.meta.generatedAt, sessionCount: card.meta.sessionCount,
|
|
82
|
+
segmentCount: card.meta.segmentCount, skillCount: Object.keys(card.bySkill).length,
|
|
83
|
+
healthBand: card.overall.healthBand, confidence: card.overall.confidence ?? confidenceOf(card.meta.segmentCount),
|
|
65
84
|
});
|
|
66
85
|
}
|
|
67
|
-
catch { /* skip corrupt */ }
|
|
68
86
|
}
|
|
69
87
|
// 最新在前
|
|
70
88
|
items.sort((a, b) => b.generatedAt.localeCompare(a.generatedAt));
|
|
71
89
|
return items;
|
|
72
90
|
}
|
|
73
|
-
function loadAnalysis(dir, id) {
|
|
91
|
+
function loadAnalysis(dir, id, includeCards = false) {
|
|
74
92
|
const path = join(dir, `${id}.json`);
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
if (existsSync(path)) {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
96
|
+
}
|
|
97
|
+
catch { /* fall through to card */ }
|
|
79
98
|
}
|
|
80
|
-
|
|
99
|
+
// 别项目:按 observe 卡片 path 读真身(含 signals 等完整详情)。悬空(项目被移走)→ null,详情页 404。
|
|
100
|
+
// 仅机器级模式兜底;固定目录 / --global 不回源别项目卡片(逃生舱语义)。
|
|
101
|
+
if (!includeCards)
|
|
81
102
|
return null;
|
|
103
|
+
const card = listObserveCards().find((c) => c.id === id);
|
|
104
|
+
if (card && existsSync(card.path)) {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(readFileSync(card.path, 'utf-8'));
|
|
107
|
+
}
|
|
108
|
+
catch { /* corrupt 真身 */ }
|
|
82
109
|
}
|
|
110
|
+
return null;
|
|
83
111
|
}
|
|
84
112
|
/** 扫 doctorsDir 找 id 匹配的 doctor 报告(文件名不一定等于 report id)。
|
|
85
113
|
* 批量 doctor 会按 skill 拆成多份共享同一 id 的 per-skill 文件,传 skillName 时
|
|
86
114
|
* 优先返回含该 skill 的那份;都不含时回退首个 id 命中(单 skill / 无参行为不变)。 */
|
|
87
|
-
function loadDoctorReport(dir, id, skillName) {
|
|
88
|
-
if (!existsSync(dir))
|
|
89
|
-
return null;
|
|
115
|
+
function loadDoctorReport(dir, id, skillName, includeCards = false) {
|
|
90
116
|
let fallback = null;
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
if (existsSync(dir)) {
|
|
118
|
+
for (const file of readdirSync(dir)) {
|
|
119
|
+
if (!file.endsWith('.json'))
|
|
120
|
+
continue;
|
|
121
|
+
try {
|
|
122
|
+
const data = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
123
|
+
if (data?.kind !== 'doctor' || data.id !== id)
|
|
124
|
+
continue;
|
|
125
|
+
if (!skillName || data.skills?.some((s) => s.skillName === skillName))
|
|
126
|
+
return data;
|
|
127
|
+
fallback ??= data;
|
|
128
|
+
}
|
|
129
|
+
catch { /* skip */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (fallback)
|
|
133
|
+
return fallback;
|
|
134
|
+
// 仅机器级模式兜底;固定 --doctors-dir / --global 不回源别项目卡片(逃生舱语义)。
|
|
135
|
+
if (!includeCards)
|
|
136
|
+
return null;
|
|
137
|
+
// 别项目:按 doctor 卡片(reportId 匹配 + 可选 skillName)的 path 读真身。detail 路由传的 id 是 reportId(非卡片 stem)。
|
|
138
|
+
for (const card of listDoctorCards()) {
|
|
139
|
+
if (card.reportId !== id)
|
|
140
|
+
continue;
|
|
141
|
+
if (skillName && card.skillName !== skillName)
|
|
142
|
+
continue;
|
|
143
|
+
if (!existsSync(card.path))
|
|
93
144
|
continue;
|
|
94
145
|
try {
|
|
95
|
-
const data = JSON.parse(readFileSync(
|
|
96
|
-
if (data?.kind
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
146
|
+
const data = JSON.parse(readFileSync(card.path, 'utf-8'));
|
|
147
|
+
if (data?.kind === 'doctor' && data.id === id) {
|
|
148
|
+
if (!skillName || data.skills?.some((s) => s.skillName === skillName))
|
|
149
|
+
return data;
|
|
150
|
+
fallback ??= data;
|
|
151
|
+
}
|
|
101
152
|
}
|
|
102
|
-
catch { /*
|
|
153
|
+
catch { /* corrupt 真身 */ }
|
|
103
154
|
}
|
|
104
155
|
return fallback;
|
|
105
156
|
}
|
|
106
157
|
/**
|
|
107
158
|
* 扫 analyses/ 所有 JSON,按 skillName 过滤,按时间排序成 trend points。
|
|
108
159
|
*/
|
|
109
|
-
function querySkillTrend(dir, skillName) {
|
|
110
|
-
const items = listAnalyses(dir);
|
|
160
|
+
function querySkillTrend(dir, skillName, includeCards = false) {
|
|
161
|
+
const items = listAnalyses(dir, includeCards);
|
|
111
162
|
const points = [];
|
|
112
163
|
for (const it of items) {
|
|
113
|
-
const report = loadAnalysis(dir, it.id);
|
|
164
|
+
const report = loadAnalysis(dir, it.id, includeCards);
|
|
114
165
|
if (!report)
|
|
115
166
|
continue;
|
|
116
167
|
const h = report.bySkill[skillName];
|
|
@@ -144,9 +195,9 @@ function querySkillTrend(dir, skillName) {
|
|
|
144
195
|
* 比较两份 skill health report. `from` 通常是较早的,`to` 是较晚的;
|
|
145
196
|
* 对于每个 skill, 显示前后值和 delta. 缺失一侧时 presence 标记。
|
|
146
197
|
*/
|
|
147
|
-
function querySkillDiff(dir, fromId, toId) {
|
|
148
|
-
const from = loadAnalysis(dir, fromId);
|
|
149
|
-
const to = loadAnalysis(dir, toId);
|
|
198
|
+
function querySkillDiff(dir, fromId, toId, includeCards = false) {
|
|
199
|
+
const from = loadAnalysis(dir, fromId, includeCards);
|
|
200
|
+
const to = loadAnalysis(dir, toId, includeCards);
|
|
150
201
|
if (!from || !to)
|
|
151
202
|
return null;
|
|
152
203
|
const allSkills = new Set([...Object.keys(from.bySkill), ...Object.keys(to.bySkill)]);
|
|
@@ -324,9 +375,9 @@ function renderSkillDiffPage(diff, lang = DEFAULT_LANG) {
|
|
|
324
375
|
const body = `
|
|
325
376
|
<main style="max-width:1000px;margin:0 auto;padding:24px">
|
|
326
377
|
<nav style="margin-bottom:8px">
|
|
327
|
-
<a href="/
|
|
328
|
-
<a href="/
|
|
329
|
-
<a href="/
|
|
378
|
+
<a href="/observe-health${langQ}" data-i18n="backToAnalyses" style="color:var(--accent);text-decoration:none;margin-right:12px">${t('backToAnalyses', lang)}</a>
|
|
379
|
+
<a href="/observe-health/${encodeURIComponent(fromId)}${langQ}" data-i18n="diffNavFrom" style="color:var(--accent);text-decoration:none;margin-right:12px">${t('diffNavFrom', lang)}</a>
|
|
380
|
+
<a href="/observe-health/${encodeURIComponent(toId)}${langQ}" data-i18n="diffNavTo" style="color:var(--accent);text-decoration:none">${t('diffNavTo', lang)}</a>
|
|
330
381
|
</nav>
|
|
331
382
|
<h1 data-i18n="skillDiffHeading" style="font-size:20px;margin:8px 0">${t('skillDiffHeading', lang)}</h1>
|
|
332
383
|
<div style="color:var(--text-muted);font-size:13px;margin-bottom:20px">
|
|
@@ -352,7 +403,7 @@ function renderSkillTrendPage(trend, lang = DEFAULT_LANG) {
|
|
|
352
403
|
if (points.length === 0) {
|
|
353
404
|
const emptyBody = `
|
|
354
405
|
<main style="max-width:900px;margin:0 auto;padding:24px">
|
|
355
|
-
<nav style="margin-bottom:12px"><a href="/
|
|
406
|
+
<nav style="margin-bottom:12px"><a href="/observe-health${langQ}" data-i18n="backToAnalyses" style="color:var(--accent);text-decoration:none">${t('backToAnalyses', lang)}</a></nav>
|
|
356
407
|
<h1 style="font-size:20px;margin:8px 0 4px"><span data-i18n="skillTrendHeading">${t('skillTrendHeading', lang)}</span> · ${skillName}</h1>
|
|
357
408
|
<p style="color:var(--text-muted)" data-i18n="noTrendData">${t('noTrendData', lang)}</p>
|
|
358
409
|
</main>`;
|
|
@@ -389,7 +440,7 @@ function renderSkillTrendPage(trend, lang = DEFAULT_LANG) {
|
|
|
389
440
|
<span style="color:#4ade80">● <span data-i18n="trendLegendCoverage">${t('trendLegendCoverage', lang)}</span></span>
|
|
390
441
|
</div>`;
|
|
391
442
|
const rows = points.map((p) => `<tr>
|
|
392
|
-
<td style="padding:6px 10px;font-family:ui-monospace,monospace;font-size:12px"><a href="/
|
|
443
|
+
<td style="padding:6px 10px;font-family:ui-monospace,monospace;font-size:12px"><a href="/observe-health/${encodeURIComponent(p.analysisId)}${langQ}" style="color:var(--accent);text-decoration:none">${p.generatedAt.slice(0, 19).replace('T', ' ')}</a></td>
|
|
393
444
|
<td style="padding:6px 10px;text-align:right">${p.segmentCount}</td>
|
|
394
445
|
<td style="padding:6px 10px;text-align:right;color:#f87171">${Math.round(p.gapRate * 100)}%</td>
|
|
395
446
|
<td style="padding:6px 10px;text-align:right;color:#fbbf24">${Math.round(p.weightedGapRate * 100)}%</td>
|
|
@@ -401,7 +452,7 @@ function renderSkillTrendPage(trend, lang = DEFAULT_LANG) {
|
|
|
401
452
|
const subtitle = `${points.length} <span data-i18n="trendNPoints">${t('trendNPoints', lang)}</span> · <span data-i18n="trendEarliest">${t('trendEarliest', lang)}</span> ${points[0].generatedAt.slice(0, 10)} · <span data-i18n="trendLatest">${t('trendLatest', lang)}</span> ${points[points.length - 1].generatedAt.slice(0, 10)}`;
|
|
402
453
|
const body = `
|
|
403
454
|
<main style="max-width:900px;margin:0 auto;padding:24px">
|
|
404
|
-
<nav style="margin-bottom:8px"><a href="/
|
|
455
|
+
<nav style="margin-bottom:8px"><a href="/observe-health${langQ}" data-i18n="backToAnalyses" style="color:var(--accent);text-decoration:none">${t('backToAnalyses', lang)}</a></nav>
|
|
405
456
|
<h1 style="font-size:20px;margin:8px 0 4px"><span data-i18n="skillTrendHeading">${t('skillTrendHeading', lang)}</span> · ${skillName}</h1>
|
|
406
457
|
<div style="color:var(--text-muted);font-size:13px;margin-bottom:16px">${subtitle}</div>
|
|
407
458
|
${svg}
|
|
@@ -443,7 +494,7 @@ function renderAnalysisList(items, lang = DEFAULT_LANG) {
|
|
|
443
494
|
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${badgeColor}"></span>
|
|
444
495
|
<label style="font-size:11px;color:var(--text-muted);display:flex;align-items:center;gap:3px"><input type="radio" name="from" value="${enc}" onchange="updateCompare()"> <span data-i18n="analysesFromLabel">${t('analysesFromLabel', lang)}</span></label>
|
|
445
496
|
<label style="font-size:11px;color:var(--text-muted);display:flex;align-items:center;gap:3px"><input type="radio" name="to" value="${enc}" onchange="updateCompare()"> <span data-i18n="analysesToLabel">${t('analysesToLabel', lang)}</span></label>
|
|
446
|
-
<a href="/
|
|
497
|
+
<a href="/observe-health/${enc}${langQ}" style="color:var(--accent);text-decoration:none;flex:1;font-family:ui-monospace,monospace">${it.id}</a>
|
|
447
498
|
<span style="color:var(--text-muted);font-size:12px">${it.sessionCount} <span data-i18n="analysesSessions">${t('analysesSessions', lang)}</span> · ${it.segmentCount} <span data-i18n="analysesSegs">${t('analysesSegs', lang)}</span> · ${it.skillCount} <span data-i18n="analysesSkills">${t('analysesSkills', lang)}</span>${underpowered ? ` · <span data-i18n="analysesLowN" style="color:var(--text-faint)">${t('analysesLowN', lang)}</span>` : ''}</span>
|
|
448
499
|
</li>`;
|
|
449
500
|
}).join('');
|
|
@@ -509,18 +560,51 @@ export function formatListenError(p, err) {
|
|
|
509
560
|
// EADDRINUSE or unknown — let caller try the omk-takeover flow.
|
|
510
561
|
return null;
|
|
511
562
|
}
|
|
512
|
-
export function createReportServer({ port, host: hostOption, reportsDir
|
|
563
|
+
export function createReportServer({ port, host: hostOption, reportsDir, analysesDir, doctorsDir, observationsDir = DEFAULT_OBSERVATIONS_DIR, jobsDir = DEFAULT_JOBS_DIR, managedDir, store, jobStore, includeObserveCards = false, includeDoctorCards = false } = {}) {
|
|
513
564
|
let server = null;
|
|
514
565
|
let serverUrl = null;
|
|
515
|
-
//
|
|
516
|
-
|
|
566
|
+
// reports store:显式注入 store > 显式 reportsDir(eval serve 的本次 outputDir / 测试 / 覆盖,固定单目录)
|
|
567
|
+
// > 缺省 indexed(机器级总览:当前项目 + 全局 live ∪ 别项目的索引卡片,按 id dedup)。indexed 在 store 层做
|
|
568
|
+
// merge,底层 createFileStore 的 mtime 指纹自动反映内容变化,一次构建即可。
|
|
569
|
+
const reportStore = store
|
|
570
|
+
?? (reportsDir !== undefined
|
|
571
|
+
? createFileStore(reportsDir)
|
|
572
|
+
: createIndexedReportStore({ projectDir: projectReportsDir(), globalDir: globalReportsDir() }));
|
|
517
573
|
const resolvedJobStore = jobStore || createFileJobStore(jobsDir);
|
|
574
|
+
// 受管根目录按**请求**解析,不在启动时冻结 —— 否则长会话里会跟 omk list 分叉:Studio 启动时项目 .omk/managed
|
|
575
|
+
// 还空、回退到 global,随后用户在项目里首次 omk install,omk list 下次会切到 project,而冻结了 root 的 Studio
|
|
576
|
+
// 仍盯着旧 global,页面与 CLI 不一致。cwd 在进程内不变,变的是目录里有没有记录,故每次请求重判。
|
|
577
|
+
// - 传函数 → 直接当解析器,每次请求调用(测试可注入受控解析器复现 project↔global 切换);
|
|
578
|
+
// - 传字符串 → 固定该目录(显式覆盖 / 测试);
|
|
579
|
+
// - 缺省 → 动态解析 project→global 权威目录,与 omk list 同口径。
|
|
580
|
+
const resolveManagedRoot = typeof managedDir === 'function'
|
|
581
|
+
? managedDir
|
|
582
|
+
: managedDir !== undefined
|
|
583
|
+
? () => managedDir
|
|
584
|
+
: () => resolveManagedDir(projectManagedDir());
|
|
585
|
+
// observe-health 目录同 managed 按**请求**解析(项目 .omk/observe-health 优先、空则全局兜底) ——
|
|
586
|
+
// 长会话里项目首次 omk observe 后,studio 下次请求即从 global 切回 project,不在启动时冻结。
|
|
587
|
+
// 三模式同 managedDir:函数透传 / 字符串固定 / 缺省动态解析。
|
|
588
|
+
const resolveAnalysesDir = typeof analysesDir === 'function'
|
|
589
|
+
? analysesDir
|
|
590
|
+
: analysesDir !== undefined
|
|
591
|
+
? () => analysesDir
|
|
592
|
+
: () => resolveObserveHealthDir(projectObserveHealthDir());
|
|
593
|
+
// doctors 目录同 analyses 按请求解析(项目优先→全局兜底),三模式同 managedDir / analysesDir。
|
|
594
|
+
const resolveDoctorsRoot = typeof doctorsDir === 'function'
|
|
595
|
+
? doctorsDir
|
|
596
|
+
: doctorsDir !== undefined
|
|
597
|
+
? () => doctorsDir
|
|
598
|
+
: () => resolveDoctorsDir(projectDoctorsDir());
|
|
518
599
|
async function handleRequest(req, res) {
|
|
519
600
|
try {
|
|
520
601
|
const parsed = new URL(req.url || '/', 'http://127.0.0.1');
|
|
521
602
|
const path = parsed.pathname;
|
|
522
603
|
const langParam = parsed.searchParams.get('lang');
|
|
523
604
|
const lang = langParam === 'en' ? 'en' : langParam === 'zh' ? 'zh' : DEFAULT_LANG;
|
|
605
|
+
// observe-health / doctors 目录每请求解析一次(项目优先→全局兜底),下面各 handler 用这两个 local 字符串。
|
|
606
|
+
const analysesDir = resolveAnalysesDir();
|
|
607
|
+
const doctorsDir = resolveDoctorsRoot();
|
|
524
608
|
if (path === '/health') {
|
|
525
609
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
526
610
|
res.end(JSON.stringify({ ok: true, service: 'omk' }));
|
|
@@ -553,24 +637,87 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
553
637
|
res.end(JSON.stringify(runs));
|
|
554
638
|
return;
|
|
555
639
|
}
|
|
556
|
-
|
|
640
|
+
// 旧 observe 路由 → observe-* 词根 canonical 的兜底(querystring 透传),防外链 / 书签 / 已打开页面的旧 fetch 失效。
|
|
641
|
+
// 页面用 302(临时);API 用 307 —— review-state 有 POST/DELETE,302 会被客户端降级成 GET,307 保留 method+body。
|
|
642
|
+
// 复合名 /analyses-diff、/api/analyses-diff、/skill-trend 维持原名,不在此重定向。
|
|
643
|
+
const legacyObserveRedirect = (() => {
|
|
644
|
+
if (path === '/analyses')
|
|
645
|
+
return { to: '/observe-health', status: 302 };
|
|
646
|
+
if (path === '/observations' || path === '/observations/inbox')
|
|
647
|
+
return { to: '/observe-inbox', status: 302 };
|
|
648
|
+
const detail = path.match(/^\/analyses\/(.+)$/);
|
|
649
|
+
if (detail)
|
|
650
|
+
return { to: `/observe-health/${detail[1]}`, status: 302 };
|
|
651
|
+
if (path === '/api/analyses')
|
|
652
|
+
return { to: '/api/observe-health', status: 307 };
|
|
653
|
+
const apiDetail = path.match(/^\/api\/analyses\/(.+)$/);
|
|
654
|
+
if (apiDetail)
|
|
655
|
+
return { to: `/api/observe-health/${apiDetail[1]}`, status: 307 };
|
|
656
|
+
if (path === '/api/observations/inbox')
|
|
657
|
+
return { to: '/api/observe-inbox', status: 307 };
|
|
658
|
+
if (path === '/api/observations/show')
|
|
659
|
+
return { to: '/api/observe-inbox/show', status: 307 };
|
|
660
|
+
if (path === '/api/observations/diagnostics')
|
|
661
|
+
return { to: '/api/observe-inbox/diagnostics', status: 307 };
|
|
662
|
+
if (path === '/api/observations/review-state')
|
|
663
|
+
return { to: '/api/observe-inbox/review-state', status: 307 };
|
|
664
|
+
return null;
|
|
665
|
+
})();
|
|
666
|
+
if (legacyObserveRedirect) {
|
|
667
|
+
res.writeHead(legacyObserveRedirect.status, { Location: legacyObserveRedirect.to + parsed.search });
|
|
668
|
+
res.end();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (path === '/api/observe-health') {
|
|
557
672
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
558
|
-
res.end(JSON.stringify(listAnalyses(analysesDir)));
|
|
673
|
+
res.end(JSON.stringify(listAnalyses(analysesDir, includeObserveCards)));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (path === '/observe-health') {
|
|
677
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
678
|
+
res.end(renderAnalysisList(listAnalyses(analysesDir, includeObserveCards), lang));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
// 受管 skill 决策史(#203 管理支柱可视化出口)。只读受管记录,口径同 omk list。
|
|
682
|
+
if (path === '/api/managed') {
|
|
683
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
684
|
+
res.end(JSON.stringify({ schemaVersion: 1, rows: listManagedRows(resolveManagedRoot()) }));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (path === '/managed') {
|
|
688
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
689
|
+
res.end(renderManagedList(listManagedRows(resolveManagedRoot()), lang));
|
|
559
690
|
return;
|
|
560
691
|
}
|
|
561
|
-
|
|
692
|
+
const managedDetailMatch = path.match(/^\/managed\/(.+)$/);
|
|
693
|
+
if (managedDetailMatch) {
|
|
694
|
+
let id;
|
|
695
|
+
try {
|
|
696
|
+
id = decodeURIComponent(managedDetailMatch[1]);
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
id = '';
|
|
700
|
+
}
|
|
701
|
+
// 按稳定 id(= hash(kind, name)) 精确查 —— 同名不同 kind(skill/review vs prompt/review)各有独立 id,
|
|
702
|
+
// 不会串到同一页;且只在已加载、已校验的记录里查,不拼文件路径、无路径穿越。
|
|
703
|
+
const record = id ? loadAllManagedRecords(resolveManagedRoot()).find((r) => r.id === id) : undefined;
|
|
704
|
+
if (!record) {
|
|
705
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
706
|
+
res.end(lang === 'en' ? 'managed record not found' : '受管记录不存在');
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
562
709
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
563
|
-
res.end(
|
|
710
|
+
res.end(renderManagedHistory(record, lang));
|
|
564
711
|
return;
|
|
565
712
|
}
|
|
566
|
-
if (path === '/
|
|
713
|
+
if (path === '/observe-inbox') {
|
|
567
714
|
const skill = parsed.searchParams.get('skill') || undefined;
|
|
568
715
|
const html = renderObservationInboxPage(buildObservationInboxViewModel(observationsDir, { skill }), lang);
|
|
569
716
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
570
717
|
res.end(html);
|
|
571
718
|
return;
|
|
572
719
|
}
|
|
573
|
-
if (path === '/api/
|
|
720
|
+
if (path === '/api/observe-inbox') {
|
|
574
721
|
const severity = parsed.searchParams.get('severity');
|
|
575
722
|
const skill = parsed.searchParams.get('skill');
|
|
576
723
|
const limitRaw = parsed.searchParams.get('limit');
|
|
@@ -588,9 +735,9 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
588
735
|
res.end(JSON.stringify(items));
|
|
589
736
|
return;
|
|
590
737
|
}
|
|
591
|
-
if (path === '/api/
|
|
738
|
+
if (path === '/api/observe-inbox/diagnostics') {
|
|
592
739
|
const runs = await reportStore.list();
|
|
593
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir);
|
|
740
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
|
|
594
741
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
595
742
|
res.end(JSON.stringify({
|
|
596
743
|
sourceCoverage: idx.diagnosisSummary.sourceCoverage,
|
|
@@ -605,14 +752,14 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
605
752
|
}));
|
|
606
753
|
return;
|
|
607
754
|
}
|
|
608
|
-
if (path === '/api/
|
|
755
|
+
if (path === '/api/observe-inbox/show') {
|
|
609
756
|
const id = parsed.searchParams.get('id') || '';
|
|
610
757
|
const item = id ? findObservationInboxItem(id, observationsDir) : null;
|
|
611
758
|
res.writeHead(item ? 200 : 404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
612
759
|
res.end(JSON.stringify(item ? { id, text: formatObservationShow(item) } : { error: 'observation not found' }));
|
|
613
760
|
return;
|
|
614
761
|
}
|
|
615
|
-
if (path === '/api/
|
|
762
|
+
if (path === '/api/observe-inbox/review-state') {
|
|
616
763
|
if (req.method === 'GET') {
|
|
617
764
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
618
765
|
res.end(JSON.stringify(loadObservationReviewState(observationsDir)));
|
|
@@ -667,7 +814,7 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
667
814
|
if (doctorDetailMatch) {
|
|
668
815
|
const id = decodeURIComponent(doctorDetailMatch[1]);
|
|
669
816
|
const skillName = parsed.searchParams.get('skill') ?? '';
|
|
670
|
-
const report = loadDoctorReport(doctorsDir, id, skillName || undefined);
|
|
817
|
+
const report = loadDoctorReport(doctorsDir, id, skillName || undefined, includeDoctorCards);
|
|
671
818
|
if (!report) {
|
|
672
819
|
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
673
820
|
res.end(lang === 'en' ? 'doctor report not found' : '体检报告不存在');
|
|
@@ -676,7 +823,7 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
676
823
|
let ctx;
|
|
677
824
|
if (skillName) {
|
|
678
825
|
const runs = await reportStore.list();
|
|
679
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir);
|
|
826
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
|
|
680
827
|
const entry = idx.entries.find((en) => en.skillName === skillName);
|
|
681
828
|
if (entry)
|
|
682
829
|
ctx = buildSkillContext(entry, 'doctor', id, idx.insightsBySkill.get(entry.skillName) ?? [], lang);
|
|
@@ -686,10 +833,10 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
686
833
|
res.end(renderDoctorDetail(report, skillName, langQ, lang, ctx));
|
|
687
834
|
return;
|
|
688
835
|
}
|
|
689
|
-
const analysisDetailMatch = path.match(/^\/
|
|
836
|
+
const analysisDetailMatch = path.match(/^\/observe-health\/(.+)$/);
|
|
690
837
|
if (analysisDetailMatch) {
|
|
691
838
|
const id = decodeURIComponent(analysisDetailMatch[1]);
|
|
692
|
-
const report = loadAnalysis(analysesDir, id);
|
|
839
|
+
const report = loadAnalysis(analysesDir, id, includeObserveCards);
|
|
693
840
|
if (!report) {
|
|
694
841
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
695
842
|
res.end('analysis not found');
|
|
@@ -699,10 +846,10 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
699
846
|
res.end(renderSkillHealthReport(report, lang));
|
|
700
847
|
return;
|
|
701
848
|
}
|
|
702
|
-
const analysisApiMatch = path.match(/^\/api\/
|
|
849
|
+
const analysisApiMatch = path.match(/^\/api\/observe-health\/(.+)$/);
|
|
703
850
|
if (analysisApiMatch) {
|
|
704
851
|
const id = decodeURIComponent(analysisApiMatch[1]);
|
|
705
|
-
const report = loadAnalysis(analysesDir, id);
|
|
852
|
+
const report = loadAnalysis(analysesDir, id, includeObserveCards);
|
|
706
853
|
if (!report) {
|
|
707
854
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
708
855
|
res.end(JSON.stringify({ error: 'analysis not found' }));
|
|
@@ -716,14 +863,14 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
716
863
|
if (skillTrendApiMatch) {
|
|
717
864
|
const skillName = decodeURIComponent(skillTrendApiMatch[1]);
|
|
718
865
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
719
|
-
res.end(JSON.stringify(querySkillTrend(analysesDir, skillName)));
|
|
866
|
+
res.end(JSON.stringify(querySkillTrend(analysesDir, skillName, includeObserveCards)));
|
|
720
867
|
return;
|
|
721
868
|
}
|
|
722
869
|
const skillTrendPageMatch = path.match(/^\/skill-trend\/(.+)$/);
|
|
723
870
|
if (skillTrendPageMatch) {
|
|
724
871
|
const skillName = decodeURIComponent(skillTrendPageMatch[1]);
|
|
725
872
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
726
|
-
res.end(renderSkillTrendPage(querySkillTrend(analysesDir, skillName), lang));
|
|
873
|
+
res.end(renderSkillTrendPage(querySkillTrend(analysesDir, skillName, includeObserveCards), lang));
|
|
727
874
|
return;
|
|
728
875
|
}
|
|
729
876
|
if (path === '/analyses-diff') {
|
|
@@ -734,7 +881,7 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
734
881
|
res.end('missing from/to query params');
|
|
735
882
|
return;
|
|
736
883
|
}
|
|
737
|
-
const diff = querySkillDiff(analysesDir, fromId, toId);
|
|
884
|
+
const diff = querySkillDiff(analysesDir, fromId, toId, includeObserveCards);
|
|
738
885
|
if (!diff) {
|
|
739
886
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
740
887
|
res.end('analysis not found');
|
|
@@ -752,7 +899,7 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
752
899
|
res.end(JSON.stringify({ error: 'missing from/to query params' }));
|
|
753
900
|
return;
|
|
754
901
|
}
|
|
755
|
-
const diff = querySkillDiff(analysesDir, fromId, toId);
|
|
902
|
+
const diff = querySkillDiff(analysesDir, fromId, toId, includeObserveCards);
|
|
756
903
|
if (!diff) {
|
|
757
904
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
758
905
|
res.end(JSON.stringify({ error: 'analysis not found' }));
|
|
@@ -838,7 +985,7 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
838
985
|
let ctx;
|
|
839
986
|
if (report && report.kind === 'evaluation') {
|
|
840
987
|
const runs = await reportStore.list();
|
|
841
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir);
|
|
988
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
|
|
842
989
|
// 按 evalHistory 匹配(非仅最新),历史 eval 报告也能定位到所属 skill。
|
|
843
990
|
const entry = idx.entries.find((en) => en.evalHistory.some((h) => h.reportId === reportId));
|
|
844
991
|
if (entry)
|
|
@@ -860,14 +1007,14 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
860
1007
|
// list renderer 直接消费,不再每请求 N×detectInsights 重算。
|
|
861
1008
|
if (path === '/') {
|
|
862
1009
|
const runs = await reportStore.list();
|
|
863
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir);
|
|
1010
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
|
|
864
1011
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
865
1012
|
res.end(renderSkillList(idx, lang));
|
|
866
1013
|
return;
|
|
867
1014
|
}
|
|
868
1015
|
if (path === '/api/skills') {
|
|
869
1016
|
const runs = await reportStore.list();
|
|
870
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir);
|
|
1017
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
|
|
871
1018
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
872
1019
|
res.end(JSON.stringify({
|
|
873
1020
|
entries: idx.entries.map((entry) => ({
|
|
@@ -888,7 +1035,7 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
888
1035
|
if (skillDiagnosticsApiMatch) {
|
|
889
1036
|
const skillName = decodeURIComponent(skillDiagnosticsApiMatch[1]);
|
|
890
1037
|
const runs = await reportStore.list();
|
|
891
|
-
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir);
|
|
1038
|
+
const idx = buildSkillIndex(runs, analysesDir, doctorsDir, observationsDir, { includeObserveCards, includeDoctorCards });
|
|
892
1039
|
const diagnostics = idx.diagnosticsBySkill.get(skillName);
|
|
893
1040
|
if (!diagnostics) {
|
|
894
1041
|
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -918,10 +1065,9 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
918
1065
|
async function start() {
|
|
919
1066
|
if (server)
|
|
920
1067
|
return serverUrl;
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
mkdirSync(analysesDir, { recursive: true });
|
|
1068
|
+
// reports / analyses / doctors 都不在启动时 mkdir —— studio 只读消费,readers 都 existsSync 守卫、
|
|
1069
|
+
// writer(omk eval / observe / doctor)各自建目录;reports 默认走 overlay(项目盖全局),启动时往随机
|
|
1070
|
+
// cwd 建空 .omk/reports 既无意义又是噪声。observationsDir / jobsDir 仍预建(既有行为)。
|
|
925
1071
|
if (!existsSync(observationsDir))
|
|
926
1072
|
mkdirSync(observationsDir, { recursive: true });
|
|
927
1073
|
if (!existsSync(jobsDir))
|