@dsh-bio/dsh-bio-gem 0.1.13 → 0.1.15
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/package.json +66 -52
- package/src/capabilities.js +17 -4
- package/src/integration.js +511 -507
package/package.json
CHANGED
|
@@ -1,52 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dsh-bio/dsh-bio-gem",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "基因组尺度代谢模型(GEM)构建插件:输入细菌全基因组(蛋白FASTA,支持多质粒/多染色体),自动构建+验证+补洞+出报告(SBML + 模型卡),供 dsh-bio-genie 消费工具加载使用 | Genome-scale metabolic model builder for dsh",
|
|
5
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "https://github.com/moonbowterfly/dsh-bio-gem.git"
|
|
8
|
-
},
|
|
9
|
-
"homepage": "https://github.com/moonbowterfly/dsh-bio-gem",
|
|
10
|
-
"bugs": {
|
|
11
|
-
"url": "https://github.com/moonbowterfly/dsh-bio-gem/issues"
|
|
12
|
-
},
|
|
13
|
-
"publishConfig": {
|
|
14
|
-
"access": "public"
|
|
15
|
-
},
|
|
16
|
-
"type": "module",
|
|
17
|
-
"main": "index.js",
|
|
18
|
-
"exports": {
|
|
19
|
-
".": "./index.js",
|
|
20
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
21
|
-
"./package.json": "./package.json"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"index.js",
|
|
25
|
-
"cordis.patch.yml",
|
|
26
|
-
"src/**/*.js",
|
|
27
|
-
"python/**/*.py",
|
|
28
|
-
"skills/**/*.md",
|
|
29
|
-
"docs/**/*.md",
|
|
30
|
-
"README.md",
|
|
31
|
-
"LICENSE"
|
|
32
|
-
],
|
|
33
|
-
"keywords": [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-bio/dsh-bio-gem",
|
|
3
|
+
"version": "0.1.15",
|
|
4
|
+
"description": "基因组尺度代谢模型(GEM)构建插件:输入细菌全基因组(蛋白FASTA,支持多质粒/多染色体),自动构建+验证+补洞+出报告(SBML + 模型卡),供 dsh-bio-genie 消费工具加载使用 | Genome-scale metabolic model builder for dsh",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/moonbowterfly/dsh-bio-gem.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/moonbowterfly/dsh-bio-gem",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/moonbowterfly/dsh-bio-gem/issues"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "index.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./index.js",
|
|
20
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"cordis.patch.yml",
|
|
26
|
+
"src/**/*.js",
|
|
27
|
+
"python/**/*.py",
|
|
28
|
+
"skills/**/*.md",
|
|
29
|
+
"docs/**/*.md",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"keywords": [
|
|
34
|
+
"dsh",
|
|
35
|
+
"deepseek-harness",
|
|
36
|
+
"dsh-plugin",
|
|
37
|
+
"metabolic-model",
|
|
38
|
+
"GEM",
|
|
39
|
+
"SBML",
|
|
40
|
+
"carveme",
|
|
41
|
+
"cobra",
|
|
42
|
+
"bioinformatics"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": "^22.19 || >=24"
|
|
47
|
+
},
|
|
48
|
+
"dsh": {
|
|
49
|
+
"bundle": {
|
|
50
|
+
"patch": "./cordis.patch.yml"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"smoke": "node test/smoke.js",
|
|
55
|
+
"test": "node test/check-version-source.mjs && node test/smoke.js --skip-build && node test/integration.js && node test/capabilities.js && node --import ./test/register-dsh-tools.mjs test/optional-injection.js && node --import ./test/register-dsh-tools.mjs test/check-capabilities.mjs && node test/check-counts.mjs && node test/capabilities-warn-state.mjs",
|
|
56
|
+
"test:full": "node test/smoke.js && node test/integration.js && node test/capabilities.js && node --import ./test/register-dsh-tools.mjs test/optional-injection.js && node --import ./test/register-dsh-tools.mjs test/check-capabilities.mjs && node test/check-counts.mjs"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@deepseek-ai/dsh-tools": "*"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"@deepseek-ai/dsh-tools": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/capabilities.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// cost_class: light (<10s) | medium (10s–2min) | heavy (>2min)
|
|
13
13
|
// network: none | optional | required
|
|
14
14
|
// mutability: read_only | writes_output | model_mutating
|
|
15
|
-
// status: ready | experimental | data-not-initialized (依赖级状态由 integration
|
|
15
|
+
// status: ready | experimental | data-not-initialized (依赖级状态由 integration 动态覆盖为 unavailable / unknown)
|
|
16
16
|
// requires: 动态依赖 id 列表(对齐 integration status 的 check id;缺省即 ready)
|
|
17
17
|
|
|
18
18
|
export const TOOLS_MANIFEST = [
|
|
@@ -124,11 +124,19 @@ export const CONTRACT_VERSION = '1'
|
|
|
124
124
|
export function buildCapabilitiesReport({ pluginVersion, checks = [] } = {}) {
|
|
125
125
|
const checkStatus = new Map(checks.map((c) => [c.id, c.status]))
|
|
126
126
|
const tools = TOOLS_MANIFEST.map((t) => {
|
|
127
|
-
const
|
|
127
|
+
const required = t.requires ?? []
|
|
128
|
+
// 三态与 status 层对齐:missing = 确认缺;warn = 探测未完成或进行中
|
|
129
|
+
// (runtime.gapseq 在 WSL 冷启动期间就是 warn);未出现在 checks = 未观测到。
|
|
130
|
+
// warn 不可归入 missing——否则"还在探测"会被说成"gapseq 不可用"。
|
|
131
|
+
const missingDeps = required.filter((id) => checkStatus.get(id) === 'missing')
|
|
132
|
+
const unprobedDeps = required.filter((id) => {
|
|
128
133
|
const st = checkStatus.get(id)
|
|
129
|
-
return st !== undefined && st !== 'ok'
|
|
134
|
+
return st !== undefined && st !== 'ok' && st !== 'missing'
|
|
130
135
|
})
|
|
131
|
-
const
|
|
136
|
+
const unknownDeps = required.filter((id) => !checkStatus.has(id))
|
|
137
|
+
const effectiveStatus = missingDeps.length > 0 ? 'unavailable'
|
|
138
|
+
: unprobedDeps.length > 0 ? 'unknown'
|
|
139
|
+
: unknownDeps.length > 0 ? 'unknown' : (t.status ?? 'ready')
|
|
132
140
|
return {
|
|
133
141
|
name: t.name,
|
|
134
142
|
capability: t.capability,
|
|
@@ -140,6 +148,11 @@ export function buildCapabilitiesReport({ pluginVersion, checks = [] } = {}) {
|
|
|
140
148
|
summary: t.summary,
|
|
141
149
|
...(t.requires ? { requires: t.requires } : {}),
|
|
142
150
|
...(missingDeps.length > 0 ? { missing_dependencies: missingDeps } : {}),
|
|
151
|
+
// 未观测到(check缺席)与探测未完成(warn)都归入 unknown_dependencies:
|
|
152
|
+
// 两者都无法证明依赖缺失,前端据此提示"待确认"而非"不可用"。
|
|
153
|
+
...(unknownDeps.length + unprobedDeps.length > 0
|
|
154
|
+
? { unknown_dependencies: [...unprobedDeps, ...unknownDeps] }
|
|
155
|
+
: {}),
|
|
143
156
|
}
|
|
144
157
|
})
|
|
145
158
|
return {
|
package/src/integration.js
CHANGED
|
@@ -1,507 +1,511 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { pythonCandidates } from './python.js'
|
|
6
|
-
import { TOOLS_MANIFEST, buildCapabilitiesReport } from './capabilities.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* dsh-bio-gem — hosted-domain integration protocol v1 (read-only batch).
|
|
10
|
-
*
|
|
11
|
-
* This module deliberately has no import-time probes: loading the plugin and
|
|
12
|
-
* serving /health must never spawn Python or modify the data directory.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export const INTEGRATION_PREFIX = '/api/dsh-bio-gem/integration'
|
|
16
|
-
export const PROTOCOL_MAJOR = 1
|
|
17
|
-
export const PROTOCOL_MINORS = [0]
|
|
18
|
-
export const RUNTIME_PROBE_CACHE_MS = 60_000
|
|
19
|
-
/** WSL/gapseq 探测更重(wsl.exe 冷启动可达十秒级),缓存更久且**非阻塞**。 */
|
|
20
|
-
export const GAPSEQ_PROBE_CACHE_MS = 300_000
|
|
21
|
-
export const INTEGRATION_FEATURES = [
|
|
22
|
-
'status',
|
|
23
|
-
'capabilities',
|
|
24
|
-
'model-store',
|
|
25
|
-
'ledger',
|
|
26
|
-
'exports',
|
|
27
|
-
'carveme-runtime',
|
|
28
|
-
'gapseq-probe',
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
const PLUGIN_ID = 'dsh-bio-gem'
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
entries,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
child
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
})
|
|
207
|
-
child.
|
|
208
|
-
child.on('
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
res.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
*
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
let
|
|
393
|
-
let
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
checks,
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { pythonCandidates } from './python.js'
|
|
6
|
+
import { TOOLS_MANIFEST, buildCapabilitiesReport } from './capabilities.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* dsh-bio-gem — hosted-domain integration protocol v1 (read-only batch).
|
|
10
|
+
*
|
|
11
|
+
* This module deliberately has no import-time probes: loading the plugin and
|
|
12
|
+
* serving /health must never spawn Python or modify the data directory.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const INTEGRATION_PREFIX = '/api/dsh-bio-gem/integration'
|
|
16
|
+
export const PROTOCOL_MAJOR = 1
|
|
17
|
+
export const PROTOCOL_MINORS = [0]
|
|
18
|
+
export const RUNTIME_PROBE_CACHE_MS = 60_000
|
|
19
|
+
/** WSL/gapseq 探测更重(wsl.exe 冷启动可达十秒级),缓存更久且**非阻塞**。 */
|
|
20
|
+
export const GAPSEQ_PROBE_CACHE_MS = 300_000
|
|
21
|
+
export const INTEGRATION_FEATURES = [
|
|
22
|
+
'status',
|
|
23
|
+
'capabilities',
|
|
24
|
+
'model-store',
|
|
25
|
+
'ledger',
|
|
26
|
+
'exports',
|
|
27
|
+
'carveme-runtime',
|
|
28
|
+
'gapseq-probe',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const PLUGIN_ID = 'dsh-bio-gem'
|
|
32
|
+
// 版本号从 package.json 实时读,避免 bump 版本时漏改此处导致 /health 自报旧版本
|
|
33
|
+
// (第二真值源曾导致 galatea 报 0.1.2 而磁盘已是 0.1.3;重启无效、非缓存)。
|
|
34
|
+
const PLUGIN_VERSION = JSON.parse(
|
|
35
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
36
|
+
).version
|
|
37
|
+
|
|
38
|
+
function defaultDataRoot() {
|
|
39
|
+
const dshHome = process.env.DSH_HOME ?? join(os.homedir(), '.dsh')
|
|
40
|
+
return join(dshHome, 'dsh-bio-gem')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function listFiles(dir, predicate = () => true) {
|
|
44
|
+
try {
|
|
45
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
46
|
+
.filter((entry) => entry.isFile() && predicate(entry.name))
|
|
47
|
+
.map((entry) => {
|
|
48
|
+
const fullPath = join(dir, entry.name)
|
|
49
|
+
const stat = statSync(fullPath)
|
|
50
|
+
return {
|
|
51
|
+
name: entry.name,
|
|
52
|
+
sizeBytes: stat.size,
|
|
53
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt))
|
|
57
|
+
} catch {
|
|
58
|
+
return []
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function boundedSummary(dir, predicate) {
|
|
63
|
+
const all = listFiles(dir, predicate)
|
|
64
|
+
return { count: all.length, items: all.slice(0, 50) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ledgerSummary(root) {
|
|
68
|
+
const dir = join(root, 'ledger')
|
|
69
|
+
const files = listFiles(dir, (name) => name.endsWith('.jsonl'))
|
|
70
|
+
const ledgers = files.map((file) => {
|
|
71
|
+
let entries = 0
|
|
72
|
+
try {
|
|
73
|
+
entries = readFileSync(join(dir, file.name), 'utf8').split(/\r?\n/).filter((line) => line.trim()).length
|
|
74
|
+
} catch { /* unreadable ledgers are represented as zero entries */ }
|
|
75
|
+
return {
|
|
76
|
+
model: file.name.replace(/\.jsonl$/, ''),
|
|
77
|
+
entries,
|
|
78
|
+
modifiedAt: file.modifiedAt,
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
return {
|
|
82
|
+
count: ledgers.length,
|
|
83
|
+
totalEntries: ledgers.reduce((total, ledger) => total + ledger.entries, 0),
|
|
84
|
+
dir,
|
|
85
|
+
ledgers: ledgers.slice(0, 50),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function carvemeStatus(root) {
|
|
90
|
+
const scripts = join(root, 'venv-carveme', 'Scripts')
|
|
91
|
+
const carve = existsSync(join(scripts, 'carve.exe'))
|
|
92
|
+
const diamond = existsSync(join(scripts, 'diamond.exe'))
|
|
93
|
+
return {
|
|
94
|
+
available: carve && diamond,
|
|
95
|
+
hint: carve && diamond
|
|
96
|
+
? 'carve.exe 与 diamond.exe 均可读。'
|
|
97
|
+
: 'gem_build(carveme) 需要私有 venv 中的 carve.exe 与 diamond.exe。',
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function statusCheck(id, status, detail) {
|
|
102
|
+
return { id, status, detail }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function remediationsFor(checks) {
|
|
106
|
+
const codes = new Set(checks.filter((check) => check.status !== 'ok').map((check) => check.id))
|
|
107
|
+
const remediations = []
|
|
108
|
+
if (codes.has('python.cobra')) {
|
|
109
|
+
remediations.push({
|
|
110
|
+
code: 'genie.bootstrap-python', owner: 'genie',
|
|
111
|
+
detail: '准备 BioGenie 共享 Python/cobra 环境后重新探测。',
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
if (codes.has('runtime.carveme')) {
|
|
115
|
+
remediations.push({
|
|
116
|
+
code: 'gem.install-carveme-runtime', owner: 'gem',
|
|
117
|
+
detail: '准备 gem 私有 CarveMe 运行时(含 diamond)后重新探测。',
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
if (codes.has('runtime.gapseq')) {
|
|
121
|
+
remediations.push({
|
|
122
|
+
code: 'genie.install-wsl-gapseq', owner: 'genie',
|
|
123
|
+
detail: '准备 WSL/gapseq 共享前置能力后重新探测。',
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
return remediations
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Read only the installed cobra version; output and failures stay local. */
|
|
130
|
+
function probeCobraVersion(executable) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
let settled = false
|
|
133
|
+
let timer = null
|
|
134
|
+
const finish = (value) => {
|
|
135
|
+
if (settled) return
|
|
136
|
+
settled = true
|
|
137
|
+
if (timer) clearTimeout(timer)
|
|
138
|
+
resolve(value)
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const child = spawn(executable, ['-I', '-c', 'import cobra; print(cobra.__version__)'], {
|
|
142
|
+
windowsHide: true,
|
|
143
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
144
|
+
})
|
|
145
|
+
let stdout = ''
|
|
146
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString('utf8') })
|
|
147
|
+
child.on('error', () => finish(null))
|
|
148
|
+
child.on('close', (code) => finish(code === 0 ? stdout.trim() || 'available' : null))
|
|
149
|
+
timer = setTimeout(() => {
|
|
150
|
+
try { child.kill() } catch { /* already exited */ }
|
|
151
|
+
finish(null)
|
|
152
|
+
}, 20_000)
|
|
153
|
+
} catch {
|
|
154
|
+
finish(null)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function probePythonEnvironment(candidateProvider, fileExists, probeCobra) {
|
|
160
|
+
const candidates = candidateProvider().map((candidate) => ({
|
|
161
|
+
...candidate,
|
|
162
|
+
exists: candidate.path === 'python' ? true : fileExists(candidate.path),
|
|
163
|
+
}))
|
|
164
|
+
let selected = null
|
|
165
|
+
for (const candidate of candidates) {
|
|
166
|
+
if (!candidate.exists) continue
|
|
167
|
+
let cobraVersion = null
|
|
168
|
+
try {
|
|
169
|
+
cobraVersion = await probeCobra(candidate.path)
|
|
170
|
+
} catch { /* individual candidate failures are an expected degraded state */ }
|
|
171
|
+
if (cobraVersion) {
|
|
172
|
+
selected = {
|
|
173
|
+
path: candidate.path,
|
|
174
|
+
source: candidate.source,
|
|
175
|
+
cobraVersion,
|
|
176
|
+
}
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
selected,
|
|
182
|
+
candidates,
|
|
183
|
+
note: selected ? undefined : '所有候选均未通过 import cobra 的只读探测。',
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Execute a fixed, read-only WSL command and retain only a tiny version response. */
|
|
188
|
+
function runGapseqVersionCommand(command, args) {
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
let settled = false
|
|
191
|
+
let stdout = ''
|
|
192
|
+
let timer = null
|
|
193
|
+
const finish = (result) => {
|
|
194
|
+
if (settled) return
|
|
195
|
+
settled = true
|
|
196
|
+
if (timer) clearTimeout(timer)
|
|
197
|
+
resolve(result)
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
// stdin 必须保持 pipe(并立即 end):实测 wsl.exe 在 stdin=ignore 下会极慢
|
|
201
|
+
// (同一条 `echo ok`:ignore 14.9s vs pipe 0.26s,约 50 倍),
|
|
202
|
+
// 这是之前 gapseq 探测频繁超时的真正根因。
|
|
203
|
+
const child = spawn(command, args, {
|
|
204
|
+
windowsHide: true,
|
|
205
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
206
|
+
})
|
|
207
|
+
child.stdin?.end()
|
|
208
|
+
child.stdout?.on('data', (chunk) => {
|
|
209
|
+
if (stdout.length < 512) stdout += chunk.toString('utf8').slice(0, 512 - stdout.length)
|
|
210
|
+
})
|
|
211
|
+
child.on('error', () => finish({ ok: false }))
|
|
212
|
+
child.on('close', (code) => finish({ ok: code === 0, stdout }))
|
|
213
|
+
timer = setTimeout(() => {
|
|
214
|
+
try { child.kill() } catch { /* already exited */ }
|
|
215
|
+
finish({ ok: false, timeout: true })
|
|
216
|
+
}, 30_000)
|
|
217
|
+
} catch {
|
|
218
|
+
finish({ ok: false })
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function probeGapseqEnvironment({ isWindows, distro, runner }) {
|
|
224
|
+
if (!isWindows) {
|
|
225
|
+
return { available: false, detail: 'gapseq 仅支持 Windows WSL 的只读探测。' }
|
|
226
|
+
}
|
|
227
|
+
// 快速预检:先确认目标发行版存在(wsl.exe -l -q 亚秒级),避免发行版缺失时
|
|
228
|
+
// 白等一次 bash 长命令直到超时刹车(真实运行时实测:本机 bash 启动即 ~3s)。
|
|
229
|
+
try {
|
|
230
|
+
const listed = await runner('wsl.exe', ['-l', '-q'])
|
|
231
|
+
const names = String(listed?.stdout ?? '')
|
|
232
|
+
.replace(/\u0000/g, '')
|
|
233
|
+
.split(/\r?\n/)
|
|
234
|
+
.map((name) => name.trim())
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
if (listed?.ok && !names.includes(distro)) {
|
|
237
|
+
return {
|
|
238
|
+
available: false,
|
|
239
|
+
detail: names.length > 0
|
|
240
|
+
? `WSL 发行版 ${distro} 不存在(现有:${names.join(', ')})。`
|
|
241
|
+
: `WSL 未发现发行版(期望 ${distro})。`,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch { /* 预检失败不阻断后续只读探测 */ }
|
|
245
|
+
let result
|
|
246
|
+
try {
|
|
247
|
+
result = await runner('wsl.exe', [
|
|
248
|
+
'-d', distro, '-u', 'root', '--', 'bash', '-lc',
|
|
249
|
+
'source /opt/miniforge3/etc/profile.d/conda.sh && conda activate gapseq && gapseq -v',
|
|
250
|
+
])
|
|
251
|
+
} catch {
|
|
252
|
+
return { available: false, detail: 'WSL/gapseq 只读探测未完成。' }
|
|
253
|
+
}
|
|
254
|
+
const output = String(result?.stdout ?? '').slice(0, 512)
|
|
255
|
+
if (!result?.ok || !/\bgapseq\b/i.test(output)) {
|
|
256
|
+
return {
|
|
257
|
+
available: false,
|
|
258
|
+
detail: result?.timeout
|
|
259
|
+
? 'gapseq 探测超时(WSL 冷启动可能超过 30s),后台将自动重试。'
|
|
260
|
+
: 'WSL/gapseq 只读探测未就绪。',
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const version = output.match(/gapseq(?:\s+version)?\s*[:v]?\s*([0-9][0-9.]*)/i)?.[1]
|
|
264
|
+
return {
|
|
265
|
+
available: true,
|
|
266
|
+
detail: version ? `gapseq ${version}(WSL 只读探测通过)。` : 'gapseq(WSL 只读探测通过)。',
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function writeJson(res, status, body) {
|
|
271
|
+
res.writeHead(status, {
|
|
272
|
+
'content-type': 'application/json; charset=utf-8',
|
|
273
|
+
'referrer-policy': 'no-referrer',
|
|
274
|
+
})
|
|
275
|
+
res.end(JSON.stringify(body))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Loopback + same-origin guard copied from the host integration boundary.
|
|
280
|
+
*
|
|
281
|
+
* It independently verifies socket address, Host, browser cross-site intent,
|
|
282
|
+
* and Origin when one is present. Loopback alone is not treated as a general
|
|
283
|
+
* authorization mechanism for future write routes.
|
|
284
|
+
*/
|
|
285
|
+
export function isLoopbackRequest(req) {
|
|
286
|
+
const address = req.socket?.remoteAddress
|
|
287
|
+
if (address !== '127.0.0.1' && address !== '::1' && address !== '::ffff:127.0.0.1') return false
|
|
288
|
+
const host = req.headers?.host
|
|
289
|
+
if (typeof host !== 'string') return false
|
|
290
|
+
let hostUrl
|
|
291
|
+
try {
|
|
292
|
+
hostUrl = new URL(`http://${host}`)
|
|
293
|
+
} catch {
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
if (hostUrl.hostname !== '127.0.0.1' && hostUrl.hostname !== 'localhost' && hostUrl.hostname !== '[::1]') return false
|
|
297
|
+
if (req.headers?.['sec-fetch-site'] === 'cross-site') return false
|
|
298
|
+
const origin = req.headers?.origin
|
|
299
|
+
if (origin === undefined) return true
|
|
300
|
+
try {
|
|
301
|
+
return new URL(origin).host === hostUrl.host
|
|
302
|
+
} catch {
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function guardedGet(handler) {
|
|
308
|
+
return async (req, res) => {
|
|
309
|
+
if (!isLoopbackRequest(req)) {
|
|
310
|
+
return writeJson(res, 403, {
|
|
311
|
+
ok: false,
|
|
312
|
+
code: 'loopback-required',
|
|
313
|
+
message: 'loopback requests only',
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
if (req.method !== 'GET') {
|
|
317
|
+
return writeJson(res, 405, {
|
|
318
|
+
ok: false,
|
|
319
|
+
code: 'method-not-allowed',
|
|
320
|
+
message: `method not allowed: ${req.method}`,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const response = await handler()
|
|
325
|
+
if (response?.ok !== true) {
|
|
326
|
+
return writeJson(res, 500, {
|
|
327
|
+
ok: false,
|
|
328
|
+
code: 'internal',
|
|
329
|
+
message: 'integration endpoint failed',
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
return writeJson(res, 200, response)
|
|
333
|
+
} catch {
|
|
334
|
+
return writeJson(res, 500, {
|
|
335
|
+
ok: false,
|
|
336
|
+
code: 'internal',
|
|
337
|
+
message: 'integration endpoint failed',
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Register the two fixed read-only integration routes when webServer exists. */
|
|
344
|
+
export function registerIntegrationRoutes(ctx, options = {}) {
|
|
345
|
+
const webServer = options.webServer ?? ctx?.webServer
|
|
346
|
+
if (!webServer?.register) return () => {}
|
|
347
|
+
const service = options.service ?? createIntegrationService(options)
|
|
348
|
+
const routes = [
|
|
349
|
+
{
|
|
350
|
+
kind: 'exact',
|
|
351
|
+
path: `${INTEGRATION_PREFIX}/health`,
|
|
352
|
+
handler: guardedGet(() => service.health()),
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
kind: 'exact',
|
|
356
|
+
path: `${INTEGRATION_PREFIX}/v1/status`,
|
|
357
|
+
handler: guardedGet(() => service.status()),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
kind: 'exact',
|
|
361
|
+
path: `${INTEGRATION_PREFIX}/v1/capabilities`,
|
|
362
|
+
handler: guardedGet(() => service.capabilities()),
|
|
363
|
+
},
|
|
364
|
+
]
|
|
365
|
+
const disposers = routes.map((route) => webServer.register(route))
|
|
366
|
+
return () => {
|
|
367
|
+
for (const dispose of disposers) dispose?.()
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Create the stateless portion of the integration API.
|
|
373
|
+
*
|
|
374
|
+
* Options exist solely to make protocol behavior testable without a dsh host;
|
|
375
|
+
* production callers use the package defaults.
|
|
376
|
+
*/
|
|
377
|
+
export function createIntegrationService(options = {}) {
|
|
378
|
+
const pluginVersion = options.pluginVersion ?? PLUGIN_VERSION
|
|
379
|
+
const dataRoot = options.dataRoot ?? defaultDataRoot()
|
|
380
|
+
const now = options.now ?? Date.now
|
|
381
|
+
const candidateProvider = options.pythonCandidates ?? pythonCandidates
|
|
382
|
+
const fileExists = options.fileExists ?? existsSync
|
|
383
|
+
const probeCobra = options.probeCobra ?? probeCobraVersion
|
|
384
|
+
const probePython = options.probePython
|
|
385
|
+
?? (() => probePythonEnvironment(candidateProvider, fileExists, probeCobra))
|
|
386
|
+
const probeGapseq = options.probeGapseq
|
|
387
|
+
?? (() => probeGapseqEnvironment({
|
|
388
|
+
isWindows: options.isWindows ?? process.platform === 'win32',
|
|
389
|
+
distro: options.gapseqDistro ?? process.env.GEM_GAPSEQ_DISTRO ?? 'Ubuntu-22.04',
|
|
390
|
+
runner: options.runGapseqProbe ?? runGapseqVersionCommand,
|
|
391
|
+
}))
|
|
392
|
+
let cachedPython = null
|
|
393
|
+
let cachedPythonAt = 0
|
|
394
|
+
let pendingPython = null
|
|
395
|
+
let cachedGapseq = null
|
|
396
|
+
let cachedGapseqAt = 0
|
|
397
|
+
let pendingGapseq = null
|
|
398
|
+
|
|
399
|
+
/** Python 探测快(≈3s)且有 60s 缓存:保持同步等待,语义简单。 */
|
|
400
|
+
function readPython() {
|
|
401
|
+
const current = now()
|
|
402
|
+
if (cachedPython && current - cachedPythonAt < RUNTIME_PROBE_CACHE_MS) return Promise.resolve(cachedPython)
|
|
403
|
+
if (pendingPython) return pendingPython
|
|
404
|
+
pendingPython = Promise.resolve()
|
|
405
|
+
.then(probePython)
|
|
406
|
+
.then((value) => {
|
|
407
|
+
cachedPython = value
|
|
408
|
+
cachedPythonAt = now()
|
|
409
|
+
return value
|
|
410
|
+
})
|
|
411
|
+
.finally(() => { pendingPython = null })
|
|
412
|
+
return pendingPython
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* gapseq 探测**非阻塞**(stale-while-revalidate):缓存未过期直接返回;
|
|
417
|
+
* 过期时立即返回旧值并后台刷新;从未探测过则返回 `available: null` 占位
|
|
418
|
+
* (契约语义:null = 尚未探测,探测在后台进行,后续请求即得布尔结果)。
|
|
419
|
+
* 这样 status 永远不会被 WSL 冷启动拖到消费端超时。
|
|
420
|
+
*/
|
|
421
|
+
function readGapseq() {
|
|
422
|
+
const current = now()
|
|
423
|
+
// 失败结果(含超时)只缓存 60s,让后台尽快重试(WSL 冷启动是暂时性状态)。
|
|
424
|
+
const ttl = cachedGapseq?.available === false ? 60_000 : GAPSEQ_PROBE_CACHE_MS
|
|
425
|
+
if (cachedGapseq && current - cachedGapseqAt < ttl) return cachedGapseq
|
|
426
|
+
if (!pendingGapseq) {
|
|
427
|
+
pendingGapseq = Promise.resolve()
|
|
428
|
+
.then(probeGapseq)
|
|
429
|
+
.then((value) => {
|
|
430
|
+
cachedGapseq = value
|
|
431
|
+
cachedGapseqAt = now()
|
|
432
|
+
return value
|
|
433
|
+
})
|
|
434
|
+
.catch(() => cachedGapseq)
|
|
435
|
+
.finally(() => { pendingGapseq = null })
|
|
436
|
+
}
|
|
437
|
+
return cachedGapseq ?? {
|
|
438
|
+
available: null,
|
|
439
|
+
probing: true,
|
|
440
|
+
detail: 'gapseq 只读探测进行中(WSL 冷启动可能需数秒),稍后刷新可见结果。',
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** 依赖检查(status 与 capabilities 共用;语义与既有 status 一致)。 */
|
|
445
|
+
async function collectChecks() {
|
|
446
|
+
const python = await readPython()
|
|
447
|
+
const gapseq = readGapseq()
|
|
448
|
+
const carveme = carvemeStatus(dataRoot)
|
|
449
|
+
const checks = [
|
|
450
|
+
statusCheck(
|
|
451
|
+
'python.cobra',
|
|
452
|
+
python.selected ? 'ok' : 'missing',
|
|
453
|
+
python.selected
|
|
454
|
+
? `cobra ${python.selected.cobraVersion ?? 'available'} @ ${python.selected.path}`
|
|
455
|
+
: '未找到可 import cobra 的 Python 解释器。',
|
|
456
|
+
),
|
|
457
|
+
statusCheck(
|
|
458
|
+
'runtime.carveme',
|
|
459
|
+
carveme.available ? 'ok' : 'missing',
|
|
460
|
+
carveme.hint,
|
|
461
|
+
),
|
|
462
|
+
statusCheck(
|
|
463
|
+
'runtime.gapseq',
|
|
464
|
+
gapseq.available === true ? 'ok' : gapseq.available === null ? 'warn' : 'missing',
|
|
465
|
+
gapseq.detail ?? (gapseq.available ? 'gapseq 只读探测通过。' : 'gapseq 只读探测未就绪。'),
|
|
466
|
+
),
|
|
467
|
+
]
|
|
468
|
+
return { python, gapseq, carveme, checks }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
async health() {
|
|
473
|
+
return {
|
|
474
|
+
ok: true,
|
|
475
|
+
value: {
|
|
476
|
+
pluginId: PLUGIN_ID,
|
|
477
|
+
pluginVersion,
|
|
478
|
+
protocolMajor: PROTOCOL_MAJOR,
|
|
479
|
+
protocolMinors: PROTOCOL_MINORS,
|
|
480
|
+
features: INTEGRATION_FEATURES,
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
async capabilities() {
|
|
486
|
+
const { checks } = await collectChecks()
|
|
487
|
+
return { ok: true, value: buildCapabilitiesReport({ pluginVersion, checks }) }
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
async status() {
|
|
491
|
+
const { python, gapseq, carveme, checks } = await collectChecks()
|
|
492
|
+
return {
|
|
493
|
+
ok: true,
|
|
494
|
+
value: {
|
|
495
|
+
state: checks.every((check) => check.status === 'ok') ? 'ready' : 'degraded',
|
|
496
|
+
generatedAt: new Date(now()).toISOString(),
|
|
497
|
+
pluginVersion,
|
|
498
|
+
features: INTEGRATION_FEATURES,
|
|
499
|
+
checks,
|
|
500
|
+
data: {
|
|
501
|
+
models: boundedSummary(join(dataRoot, 'models'), (name) => name.endsWith('.xml')),
|
|
502
|
+
ledger: ledgerSummary(dataRoot),
|
|
503
|
+
exports: boundedSummary(join(dataRoot, 'exports')),
|
|
504
|
+
},
|
|
505
|
+
env: { python, engines: { carveme, gapseq } },
|
|
506
|
+
remediations: remediationsFor(checks),
|
|
507
|
+
},
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
}
|
|
511
|
+
}
|