@mokuyoaxis/dsh-iris 0.1.4 → 0.2.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/CHANGELOG.md +54 -1
- package/README.en.md +72 -15
- package/README.md +72 -15
- package/bin/dsh-iris.js +392 -13
- package/docs/AGENT_QUICKSTART.md +35 -0
- package/docs/ARCHITECTURE.md +80 -64
- package/docs/ARTIFACT_MANIFEST.md +60 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +83 -0
- package/docs/DSH_CORE_MIGRATION.md +106 -0
- package/docs/DSH_RC2_ADAPTATION.md +36 -0
- package/docs/HEADLESS_CLI.md +217 -0
- package/docs/HOST_ADAPTER_CONTRACT.md +31 -12
- package/docs/HOST_DOCTOR.md +2 -0
- package/docs/MODEL_PORT_CONTRACT.md +300 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +51 -10
- package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +1 -1
- package/docs/ROADMAP.md +42 -86
- package/docs/SECURITY.md +4 -3
- package/docs/assets/logo/README.md +55 -0
- package/docs/assets/logo/iris-bubble-concept.svg +67 -0
- package/docs/assets/logo/iris-bubble.svg +14 -0
- package/docs/assets/logo/iris-interim-flower.png +0 -0
- package/docs/assets/logo/iris-logo-mono-inverse.svg +78 -0
- package/docs/assets/logo/iris-logo-mono.svg +78 -0
- package/docs/assets/logo/iris-logo.svg +78 -0
- package/docs/assets/logo/iris-mark-mono-inverse.svg +42 -0
- package/docs/assets/logo/iris-mark-mono.svg +42 -0
- package/docs/assets/logo/iris-mark.svg +42 -0
- package/docs/releases/0.2.0.md +48 -0
- package/lib/actions.js +374 -362
- package/lib/adapters.js +52 -15
- package/lib/api.js +152 -5
- package/lib/client.js +763 -48
- package/lib/command-service.js +420 -0
- package/lib/config.js +56 -43
- package/lib/core-artifact-store.js +598 -0
- package/lib/core-artifacts.js +4 -0
- package/lib/core-attention.js +103 -0
- package/lib/core-contract.js +105 -0
- package/lib/core-doctor.js +41 -0
- package/lib/core-lease-recovery.js +170 -0
- package/lib/core-runtime.js +232 -0
- package/lib/core-tasks.js +560 -0
- package/lib/core-user-projection.js +192 -0
- package/lib/doctor.js +92 -33
- package/lib/dsh-core-adapter.js +599 -0
- package/lib/dsh-host-adapter.js +58 -7
- package/lib/generation-input.js +95 -0
- package/lib/guard.js +3 -3
- package/lib/index.js +159 -54
- package/lib/model-invoker.js +218 -0
- package/lib/model-port-contract.js +333 -0
- package/lib/models.js +97 -11
- package/lib/provider-adapter.js +49 -23
- package/lib/provider-adapters.js +84 -21
- package/lib/provider-catalog.js +171 -0
- package/lib/provider-contract.js +72 -3
- package/lib/provider-protocol.js +51 -2
- package/lib/provider-task-runner.js +328 -0
- package/lib/tasks.js +16 -4
- package/lib/vision.js +28 -7
- package/package.json +8 -8
- package/user_guide.md +27 -12
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Core Task 的用户侧只读投影。
|
|
4
|
+
*
|
|
5
|
+
* Core 事实轴(lib/task-semantics.js)保持不变;这里只做一层只读映射,
|
|
6
|
+
* 把任务收敛成五类用户状态:running / observation_paused / attention /
|
|
7
|
+
* delivery_failed / succeeded。投影不读文件、不写事实、不接触 Provider,
|
|
8
|
+
* 也不包含 providerId、providerBinding、错误原文或任何路径。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const CORE_USER_STATES = Object.freeze([
|
|
12
|
+
'running',
|
|
13
|
+
'observation_paused',
|
|
14
|
+
'attention',
|
|
15
|
+
'delivery_failed',
|
|
16
|
+
'succeeded'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 五类状态的判定与文案。终态事实(成功/失败/取消)优先于观察姿态:
|
|
21
|
+
* 「已经明确失败」绝不能继续显示成「观察暂停」;观察暂停必须明确说明
|
|
22
|
+
* 远端可能仍在运行,避免误导用户以为任务已经终止。
|
|
23
|
+
*/
|
|
24
|
+
export function projectCoreTaskUserView(task) {
|
|
25
|
+
if (!task || typeof task !== 'object' || Array.isArray(task)
|
|
26
|
+
|| !['outcome', 'deliveryState', 'watchState', 'acceptance', 'phase'].some((axis) => axis in task)) {
|
|
27
|
+
return Object.freeze({ state: 'attention', label: '记录无法识别,需要检查' });
|
|
28
|
+
}
|
|
29
|
+
const outcome = String(task.outcome || 'none');
|
|
30
|
+
const delivery = String(task.deliveryState || 'none');
|
|
31
|
+
const watch = String(task.watchState || 'idle');
|
|
32
|
+
const acceptance = String(task.acceptance || 'none');
|
|
33
|
+
if (outcome === 'succeeded' && delivery === 'ready') {
|
|
34
|
+
return Object.freeze({ state: 'succeeded', label: '已完成,作品可用' });
|
|
35
|
+
}
|
|
36
|
+
if (outcome === 'succeeded' && delivery === 'failed') {
|
|
37
|
+
return Object.freeze({ state: 'delivery_failed', label: '已生成,作品取回失败' });
|
|
38
|
+
}
|
|
39
|
+
if (outcome === 'succeeded') {
|
|
40
|
+
return Object.freeze({ state: 'running', label: '已生成,正在保存作品' });
|
|
41
|
+
}
|
|
42
|
+
if (outcome === 'failed') {
|
|
43
|
+
return Object.freeze({ state: 'attention', label: '已失败' });
|
|
44
|
+
}
|
|
45
|
+
if (outcome === 'canceled') {
|
|
46
|
+
return Object.freeze({ state: 'attention', label: '已取消' });
|
|
47
|
+
}
|
|
48
|
+
if (outcome === 'unknown') {
|
|
49
|
+
return Object.freeze({ state: 'attention', label: '任务结果未知,需要检查' });
|
|
50
|
+
}
|
|
51
|
+
if (acceptance === 'unknown') {
|
|
52
|
+
return Object.freeze({ state: 'attention', label: '提交结果未知,请检查后处理' });
|
|
53
|
+
}
|
|
54
|
+
if (delivery === 'failed') {
|
|
55
|
+
return Object.freeze({ state: 'attention', label: '结果状态异常,需要检查' });
|
|
56
|
+
}
|
|
57
|
+
if (watch === 'suspended' || watch === 'exhausted') {
|
|
58
|
+
return Object.freeze({ state: 'observation_paused', label: '观察已暂停,远端可能仍在运行' });
|
|
59
|
+
}
|
|
60
|
+
return Object.freeze({ state: 'running', label: '运行中' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 只有状态时(如客户端角标归类)使用;完整视图见 projectCoreTaskUserView。 */
|
|
64
|
+
export function projectCoreTaskUserState(task) {
|
|
65
|
+
return projectCoreTaskUserView(task).state;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 与 task.observe / task.reobserve 同一组受理事实门:已受理、有远端 ID、
|
|
70
|
+
* 结果未定论且非终态。只读持久化事实轴,不解析 Provider、不接触网络;
|
|
71
|
+
* 服务端的真正门禁仍在 Command Service(含 binding 与能力校验)。
|
|
72
|
+
*/
|
|
73
|
+
export function coreTaskObservable(task) {
|
|
74
|
+
if (!task || typeof task !== 'object' || Array.isArray(task)) return false;
|
|
75
|
+
return String(task.acceptance || '') === 'accepted'
|
|
76
|
+
&& Boolean(String(task.remoteTaskId || '').trim())
|
|
77
|
+
&& ['none', 'unknown'].includes(String(task.outcome || 'none'))
|
|
78
|
+
&& String(task.phase || '') !== 'terminal';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 与 task.redeliver 同一组交付事实门:远端已成功、产物取回失败、远端 ID 仍在。
|
|
83
|
+
* 只读持久化事实轴,不解析 Provider、不接触网络;binding 与能力校验仍在
|
|
84
|
+
* Command Service。五类状态里只有 delivery_failed 会标记 redeliverable。
|
|
85
|
+
*/
|
|
86
|
+
export function coreTaskRedeliverable(task) {
|
|
87
|
+
if (!task || typeof task !== 'object' || Array.isArray(task)) return false;
|
|
88
|
+
return String(task.outcome || '') === 'succeeded'
|
|
89
|
+
&& String(task.deliveryState || '') === 'failed'
|
|
90
|
+
&& Boolean(String(task.remoteTaskId || '').trim());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 与 task.cancel 同一组受理事实门:可观察门的超集条件(外加从未请求过取消)。
|
|
95
|
+
* 只读持久化事实轴,不解析 Provider、不接触网络;Provider 是否支持远端取消
|
|
96
|
+
* 只有 Command 层用真实 adapter 才能回答,这里不预演。
|
|
97
|
+
*/
|
|
98
|
+
export function coreTaskCancelable(task) {
|
|
99
|
+
return coreTaskObservable(task) && String(task.cancelState || 'none') === 'none';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 与 task.retry 同一组门:终态且未成功交付(succeeded+ready 的任务不需要重试;
|
|
104
|
+
* delivery_failed 有 redeliver 但重试也合法)。重试产生新的真实计费,
|
|
105
|
+
* 必须显式确认——门只决定入口是否出现,确认在动作侧强制。
|
|
106
|
+
*/
|
|
107
|
+
export function coreTaskRetryable(task) {
|
|
108
|
+
if (!task || typeof task !== 'object' || Array.isArray(task)) return false;
|
|
109
|
+
return String(task.phase || '') === 'terminal'
|
|
110
|
+
&& !(String(task.outcome || '') === 'succeeded' && String(task.deliveryState || '') === 'ready');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function modelLabelOf(record) {
|
|
114
|
+
const ref = String(record && record.modelRef || '');
|
|
115
|
+
const separator = ref.indexOf('::');
|
|
116
|
+
if (separator >= 0 && ref.slice(separator + 2)) return ref.slice(separator + 2);
|
|
117
|
+
if (ref) return ref;
|
|
118
|
+
const attempts = Array.isArray(record && record.attempts) ? record.attempts : [];
|
|
119
|
+
const last = attempts[attempts.length - 1];
|
|
120
|
+
const attemptModel = last && typeof last.model === 'string' ? last.model : '';
|
|
121
|
+
const attemptSeparator = attemptModel.indexOf('::');
|
|
122
|
+
if (attemptSeparator >= 0 && attemptModel.slice(attemptSeparator + 2)) {
|
|
123
|
+
return attemptModel.slice(attemptSeparator + 2);
|
|
124
|
+
}
|
|
125
|
+
return attemptModel || '未记录';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 生成用户任务区消费的安全行 DTO。
|
|
130
|
+
* artifacts 是快照里已读出的 Artifact 摘要数组,用于把「作品可用」与实际的
|
|
131
|
+
* Artifact 存在性对齐;DTO 绝不携带 supplier 身份、binding 或错误原文。
|
|
132
|
+
*/
|
|
133
|
+
export function projectCoreTaskUserRow(record, artifacts = []) {
|
|
134
|
+
const view = projectCoreTaskUserView(record);
|
|
135
|
+
const artifactList = Array.isArray(artifacts) ? artifacts : [];
|
|
136
|
+
const knownIds = new Set(artifactList.map((item) => item && item.id));
|
|
137
|
+
const rawIds = Array.isArray(record && record.artifactIds) ? record.artifactIds : [];
|
|
138
|
+
const artifactIds = rawIds.filter((id) => typeof id === 'string');
|
|
139
|
+
const mediaReady = Boolean(record)
|
|
140
|
+
&& record.outcome === 'succeeded'
|
|
141
|
+
&& record.deliveryState === 'ready'
|
|
142
|
+
&& artifactIds.length > 0
|
|
143
|
+
&& artifactIds.every((id) => knownIds.has(id));
|
|
144
|
+
return Object.freeze({
|
|
145
|
+
id: String(record && record.id || ''),
|
|
146
|
+
capability: String(record && record.capability || 'image'),
|
|
147
|
+
userState: view.state,
|
|
148
|
+
label: view.label,
|
|
149
|
+
model: modelLabelOf(record),
|
|
150
|
+
updatedAt: String(record && record.updatedAt || ''),
|
|
151
|
+
revision: Number.isSafeInteger(record && record.revision) ? record.revision : 0,
|
|
152
|
+
artifactIds: Object.freeze(artifactIds),
|
|
153
|
+
mediaReady,
|
|
154
|
+
observable: coreTaskObservable(record),
|
|
155
|
+
redeliverable: coreTaskRedeliverable(record),
|
|
156
|
+
cancelable: coreTaskCancelable(record),
|
|
157
|
+
retryable: coreTaskRetryable(record)
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 完成提示防重的稳定键:同一条 Task 同一 revision 只提示一次。 */
|
|
162
|
+
export function coreCompletionKey(row) {
|
|
163
|
+
return 'core:' + String(row && row.id || '') + '@' + String(Number(row && row.revision) || 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 首次加载的基线:把既有完成项记为已见,避免会话开始时就刷一堆提示。 */
|
|
167
|
+
export function seedCoreCompletions(rows) {
|
|
168
|
+
const keys = new Set();
|
|
169
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
170
|
+
if (row && row.userState === 'succeeded') keys.add(coreCompletionKey(row));
|
|
171
|
+
}
|
|
172
|
+
return keys;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 同一份快照重复渲染不应重复提示:返回相对 seen 新出现的完成项,
|
|
177
|
+
* 以及合并后的键集合(调用方保存供下次比较)。提示不持久化,
|
|
178
|
+
* 刷新页面后由事实快照重新计算基线。
|
|
179
|
+
*/
|
|
180
|
+
export function diffCoreCompletions(rows, seen) {
|
|
181
|
+
const known = seen instanceof Set ? new Set(seen) : seedCoreCompletions(rows);
|
|
182
|
+
const added = [];
|
|
183
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
184
|
+
if (!row || row.userState !== 'succeeded') continue;
|
|
185
|
+
const key = coreCompletionKey(row);
|
|
186
|
+
if (!known.has(key)) {
|
|
187
|
+
known.add(key);
|
|
188
|
+
added.push(row);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return Object.freeze({ added: Object.freeze(added), keys: known });
|
|
192
|
+
}
|
package/lib/doctor.js
CHANGED
|
@@ -6,6 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { attentionDisposition, deriveUserState, semanticViolations } from './task-semantics.js';
|
|
8
8
|
import { HOST_CONTRACT_VERSION, hostCapabilitySnapshot } from './host-contract.js';
|
|
9
|
+
import { inspectCoreStorage } from './core-doctor.js';
|
|
10
|
+
import { providerMediaBaseUrl, providerApiKey } from './provider-protocol.js';
|
|
9
11
|
import {
|
|
10
12
|
EXPECTED_IRIS_CLIENT_SEATS, EXPECTED_IRIS_ROUTES, EXPECTED_IRIS_SKILLS, EXPECTED_IRIS_TOOLS,
|
|
11
13
|
HOST_CLIENT_PROTOCOL_VERSION, IRIS_PLUGIN_ID
|
|
@@ -103,10 +105,11 @@ function inspectConfig(dataDir, checks) {
|
|
|
103
105
|
for (const provider of providers) {
|
|
104
106
|
if (!plain(provider)) { incomplete++; continue; }
|
|
105
107
|
if (provider.enabled !== false) enabled++;
|
|
106
|
-
if (provider.enabled !== false && (!
|
|
108
|
+
if (provider.enabled !== false && (!providerMediaBaseUrl(provider)
|
|
109
|
+
|| (provider.auth !== 'none' && !providerApiKey(provider).trim()))) incomplete++;
|
|
107
110
|
models += Array.isArray(provider.models) ? provider.models.length : 0;
|
|
108
111
|
}
|
|
109
|
-
if (incomplete) checks.push(result('providers', 'warn', `${incomplete}
|
|
112
|
+
if (incomplete) checks.push(result('providers', 'warn', `${incomplete} 个启用供应商缺少端点、所需凭据或结构无效(auth:none 不要求密钥)`, '在 Iris 工作台补全或停用对应供应商。'));
|
|
110
113
|
else if (!enabled) checks.push(result('providers', 'warn', '没有启用的供应商', '在 Iris 工作台启用至少一个供应商。'));
|
|
111
114
|
else checks.push(result('providers', 'ok', `${enabled} 个供应商已启用;凭据未输出`));
|
|
112
115
|
const assignments = config.value.assignments || {};
|
|
@@ -202,9 +205,10 @@ function inspectArtifacts(dataDir, tasks, checks) {
|
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
/**
|
|
205
|
-
*
|
|
208
|
+
* 零网络离线诊断。默认 legacy 检查保留写入探针;显式 Core 模式全程只读。
|
|
206
209
|
*/
|
|
207
210
|
export async function doctor(options = {}) {
|
|
211
|
+
const explicitCore = options.dataRoot !== undefined;
|
|
208
212
|
const dshHome = path.resolve(options.dshHome || process.env.DSH_HOME || path.join(os.homedir(), '.dsh'));
|
|
209
213
|
const dataDir = path.join(dshHome, 'iris', 'v1');
|
|
210
214
|
const runner = options.commandRunner || ((command, args) => spawnSync(command, args, { stdio: 'ignore', shell: false }));
|
|
@@ -212,9 +216,9 @@ export async function doctor(options = {}) {
|
|
|
212
216
|
const nodeVersion = options.nodeVersion || process.versions.node;
|
|
213
217
|
const checks = [];
|
|
214
218
|
|
|
215
|
-
checks.push(atLeast(parseVersion(nodeVersion), [
|
|
219
|
+
checks.push(atLeast(parseVersion(nodeVersion), [22, 0, 0])
|
|
216
220
|
? result('node', 'ok', `Node.js ${nodeVersion}`)
|
|
217
|
-
: result('node', 'error', `Node.js ${nodeVersion} 低于最低要求
|
|
221
|
+
: result('node', 'error', `Node.js ${nodeVersion} 低于最低要求 22.0.0`, '升级 Node.js 后再运行 Iris。'));
|
|
218
222
|
try { await sharpLoader(); checks.push(result('sharp', 'ok', 'sharp 可加载')); }
|
|
219
223
|
catch (_) { checks.push(result('sharp', 'error', 'sharp 无法加载', '在当前平台重新安装 @mokuyoaxis/dsh-iris,确认 sharp 有可用的 ARM64/系统构建。')); }
|
|
220
224
|
for (const command of ['ffmpeg', 'ffprobe']) {
|
|
@@ -223,28 +227,39 @@ export async function doctor(options = {}) {
|
|
|
223
227
|
: result(command, 'warn', `${command} 不可用`, '视频抽帧与摘要需要 ffmpeg/ffprobe;其他 Iris 能力不受影响。'));
|
|
224
228
|
}
|
|
225
229
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
230
|
+
const core = inspectCoreStorage(explicitCore ? options.dataRoot : path.join(dataDir, 'core-v0'));
|
|
231
|
+
const storageComparison = {
|
|
232
|
+
legacy: legacyCounts(explicitCore ? options.dataRoot : dataDir),
|
|
233
|
+
core: { status: core.status, tasks: core.tasks?.total ?? null, artifacts: core.artifacts?.total ?? null }
|
|
234
|
+
};
|
|
235
|
+
appendCoreChecks(checks, core);
|
|
236
|
+
checks.push(result('storage-comparison', 'ok',
|
|
237
|
+
`Legacy:${storageComparison.legacy.tasks ?? '?'} 个任务、${storageComparison.legacy.artifacts ?? '?'} 个作品;Core:${storageComparison.core.tasks ?? '?'} 个任务、${storageComparison.core.artifacts ?? '?'} 个作品`));
|
|
238
|
+
|
|
239
|
+
if (!explicitCore) {
|
|
240
|
+
const dataExists = fs.existsSync(dataDir);
|
|
241
|
+
if (dataExists) {
|
|
242
|
+
let symlink = false;
|
|
243
|
+
try { symlink = fs.lstatSync(dataDir).isSymbolicLink(); } catch (_) {}
|
|
244
|
+
if (symlink) checks.push(result('storage', 'error', 'Iris 数据目录是符号链接,私有边界无法保证', '改用真实的私有目录并人工迁移数据。'));
|
|
245
|
+
else if (!writableProbe(dataDir)) checks.push(result('storage', 'error', 'Iris 数据目录不可写', '修复当前用户对 $DSH_HOME/iris/v1 的写权限。'));
|
|
246
|
+
else if (isGroupOrWorldAccessible(dataDir)) checks.push(result('storage', 'warn', 'Iris 数据目录允许组或其他用户访问', '在 POSIX 上将目录权限收紧为 0700、文件收紧为 0600。'));
|
|
247
|
+
else {
|
|
248
|
+
const exposedFiles = privateFileModeProblems(dataDir);
|
|
249
|
+
checks.push(exposedFiles.length
|
|
250
|
+
? result('storage', 'warn', `${exposedFiles.length} 个私有数据文件允许组或其他用户访问`, '在 POSIX 上将 providers.json、tasks.json 与提示词配置权限收紧为 0600。')
|
|
251
|
+
: result('storage', 'ok', 'Iris 数据目录可写、私有文件权限合格,临时探针已清理'));
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
checks.push(writableProbe(dataDir)
|
|
255
|
+
? result('storage', 'ok', 'Iris 数据目录尚未创建;父目录可写,临时探针已清理')
|
|
256
|
+
: result('storage', 'error', 'Iris 数据目录不存在且父目录不可写', '设置一个当前用户可写的 DSH_HOME。'));
|
|
238
257
|
}
|
|
239
|
-
} else {
|
|
240
|
-
checks.push(writableProbe(dataDir)
|
|
241
|
-
? result('storage', 'ok', 'Iris 数据目录尚未创建;父目录可写,临时探针已清理')
|
|
242
|
-
: result('storage', 'error', 'Iris 数据目录不存在且父目录不可写', '设置一个当前用户可写的 DSH_HOME。'));
|
|
243
|
-
}
|
|
244
258
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
259
|
+
inspectConfig(dataDir, checks);
|
|
260
|
+
const taskRecords = inspectTasks(dataDir, checks);
|
|
261
|
+
inspectArtifacts(dataDir, taskRecords, checks);
|
|
262
|
+
}
|
|
248
263
|
checks.push(result('scope', 'ok', '离线模式未启动 DSH、未检查浏览器/工具注册,也未发送供应商请求'));
|
|
249
264
|
|
|
250
265
|
const errors = checks.filter((item) => item.status === 'error').length;
|
|
@@ -252,14 +267,60 @@ export async function doctor(options = {}) {
|
|
|
252
267
|
return {
|
|
253
268
|
schemaVersion: 1,
|
|
254
269
|
package: { name: '@mokuyoaxis/dsh-iris', version: options.packageVersion || readPackageVersion() },
|
|
255
|
-
mode: 'offline',
|
|
270
|
+
mode: explicitCore ? 'core-offline' : 'offline',
|
|
256
271
|
environment: { node: nodeVersion, platform: process.platform, arch: process.arch },
|
|
257
272
|
checks,
|
|
273
|
+
core,
|
|
274
|
+
storageComparison,
|
|
258
275
|
summary: { ok: checks.length - errors - warnings, warnings, errors },
|
|
259
276
|
exitCode: errors ? 2 : (warnings ? 1 : 0)
|
|
260
277
|
};
|
|
261
278
|
}
|
|
262
279
|
|
|
280
|
+
function legacyCounts(directory) {
|
|
281
|
+
const count = (name, field) => {
|
|
282
|
+
try {
|
|
283
|
+
const file = path.join(directory, name);
|
|
284
|
+
const stat = fs.lstatSync(file);
|
|
285
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
286
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
287
|
+
return Array.isArray(value?.[field]) ? value[field].length : null;
|
|
288
|
+
} catch (error) { return error?.code === 'ENOENT' ? 0 : null; }
|
|
289
|
+
};
|
|
290
|
+
return { tasks: count('tasks.json', 'tasks'), artifacts: count('artifacts.json', 'artifacts') };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function appendCoreChecks(checks, core) {
|
|
294
|
+
checks.push(result('core-storage', core.status === 'invalid' ? 'error' : 'ok',
|
|
295
|
+
core.status === 'present' ? 'Core 数据根可读取(只读检查,未执行写入探针)'
|
|
296
|
+
: core.status === 'absent' ? 'Core 数据根尚未创建;未创建目录' : 'Core 数据根无法读取或不是目录'));
|
|
297
|
+
if (core.status !== 'present') return;
|
|
298
|
+
const lease = core.lease;
|
|
299
|
+
const leaseRemediation = lease.status === 'present' && lease.ownerStatus === 'missing'
|
|
300
|
+
? `确认没有跨主机写者后,显式运行 dsh-iris runtime recover --data-root <absolute-path> --confirm-stale-pid ${lease.pid}`
|
|
301
|
+
: lease.status === 'present' && lease.ownerStatus === 'alive'
|
|
302
|
+
? `先停止 PID ${lease.pid} 对应的写者,再重新运行 Doctor;活跃 owner 不可恢复。`
|
|
303
|
+
: '租约证据不完整或进程状态无法确认;不要删除或恢复,保留现场排查。';
|
|
304
|
+
checks.push(result('core-writer-lease', lease.status === 'absent' ? 'ok' : 'warn',
|
|
305
|
+
lease.status === 'absent' ? '未发现写者租约'
|
|
306
|
+
: lease.status === 'invalid' ? '写者租约证据不完整或不安全,已保留现场'
|
|
307
|
+
: `写者租约存在:PID ${lease.pid},进程状态 ${lease.ownerStatus},年龄 ${Math.floor(lease.ageMs / 1000)} 秒`,
|
|
308
|
+
lease.status === 'absent' ? undefined : leaseRemediation));
|
|
309
|
+
const tasks = core.tasks;
|
|
310
|
+
checks.push(result('core-tasks', tasks.status === 'invalid' ? 'error' : tasks.invalid || tasks.unresolved ? 'warn' : 'ok',
|
|
311
|
+
tasks.status === 'invalid' ? 'Core Task Store 结构不完整或不安全'
|
|
312
|
+
: `Core Task:${tasks.total} 个有效记录,${tasks.invalid} 个损坏记录,${tasks.unresolved} 个未解析条目`));
|
|
313
|
+
const artifacts = core.artifacts;
|
|
314
|
+
checks.push(result('core-artifacts', artifacts.status === 'invalid' ? 'error'
|
|
315
|
+
: artifacts.invalid || artifacts.orphanObjects || artifacts.orphanManifests || artifacts.unresolved ? 'warn' : 'ok',
|
|
316
|
+
artifacts.status === 'invalid' ? 'Core Artifact Store 结构不完整或不安全'
|
|
317
|
+
: `Core Artifact:${artifacts.total} 个有效记录,${artifacts.invalid} 个无效记录,${artifacts.orphanObjects} 个孤立对象,${artifacts.orphanManifests} 个未提交 Manifest,${artifacts.unresolved} 个未解析条目`));
|
|
318
|
+
if (artifacts.status !== 'invalid') checks.push(result('core-artifact-index', artifacts.index === 'invalid' ? 'warn' : 'ok',
|
|
319
|
+
artifacts.index === 'invalid' ? 'Core Artifact Index 缺失、校验失败或与提交记录不一致'
|
|
320
|
+
: artifacts.index === 'absent' ? 'Core Artifact Store 尚未创建' : 'Core Artifact Index 自校验与提交记录一致',
|
|
321
|
+
artifacts.index === 'invalid' ? '保留现场;确认租约可用后,显式执行 artifact rebuild。Doctor 不会修复索引。' : undefined));
|
|
322
|
+
}
|
|
323
|
+
|
|
263
324
|
|
|
264
325
|
function summarizeChecks(checks) {
|
|
265
326
|
const errors = checks.filter((item) => item.status === 'error').length;
|
|
@@ -271,12 +332,10 @@ function summarizeChecks(checks) {
|
|
|
271
332
|
}
|
|
272
333
|
|
|
273
334
|
function dshVersionSupported(value) {
|
|
274
|
-
/*
|
|
275
|
-
|
|
335
|
+
/* 当前 0.2 候选只支持经真实安装服务隔离验收的准确 rc.2。
|
|
336
|
+
Iris 0.1.4 的旧宿主窗口保留在历史发行中,不用于放行当前源码。 */
|
|
276
337
|
const version = String(value || '').trim().replace(/\s+/g, '');
|
|
277
|
-
|
|
278
|
-
const match = version.match(/^0\.1\.2(?:-rc\.(\d+))?(?:\+.*)?$/);
|
|
279
|
-
return Boolean(match && (match[1] === undefined || Number(match[1]) >= 1));
|
|
338
|
+
return /^0\.2\.0-rc\.2(?:\+.*)?$/.test(version);
|
|
280
339
|
}
|
|
281
340
|
|
|
282
341
|
function names(value) {
|
|
@@ -320,12 +379,12 @@ export function hostDoctor(host, evidence = {}, options = {}) {
|
|
|
320
379
|
if (snapshot.host.id === 'deepseek-harness') {
|
|
321
380
|
if (snapshot.host.version === 'unknown') {
|
|
322
381
|
checks.push(result('dsh-version', 'warn', '无法从当前进程识别 DSH 版本',
|
|
323
|
-
'使用 dsh --version
|
|
382
|
+
'使用 dsh --version 核对;当前候选仅支持 DSH 0.2.0-rc.2。'));
|
|
324
383
|
} else if (dshVersionSupported(snapshot.host.version)) {
|
|
325
384
|
checks.push(result('dsh-version', 'ok', 'DSH ' + snapshot.host.version + ' 位于已验证支持范围'));
|
|
326
385
|
} else {
|
|
327
386
|
checks.push(result('dsh-version', 'error', 'DSH ' + snapshot.host.version + ' 超出已验证支持范围',
|
|
328
|
-
'
|
|
387
|
+
'当前候选需要 DSH 0.2.0-rc.2;旧 DSH 0.1.2 线或 0.1.5-rc.1 使用 Iris @mokuyoaxis/dsh-iris@0.1.4。其他版本须另行验收。'));
|
|
329
388
|
}
|
|
330
389
|
}
|
|
331
390
|
|