@mokuyoaxis/dsh-iris 0.1.2 → 0.1.3
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 +34 -1
- package/README.en.md +16 -2
- package/README.md +16 -2
- package/bin/dsh-iris.js +23 -0
- package/docs/ARCHITECTURE.md +81 -0
- package/docs/FAULT_INJECTION.md +26 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +60 -0
- package/docs/ROADMAP.md +8 -8
- package/docs/SECURITY.md +53 -0
- package/docs/TASK_SEMANTICS.md +147 -0
- package/docs/releases/0.1.3.md +30 -0
- package/lib/actions.js +609 -175
- package/lib/api.js +80 -7
- package/lib/client.js +109 -14
- package/lib/doctor.js +243 -0
- package/lib/index.js +46 -45
- package/lib/provider-contract.js +234 -0
- package/lib/task-semantics.js +209 -0
- package/lib/tasks.js +473 -29
- package/package.json +7 -2
- package/user_guide.md +20 -0
package/lib/api.js
CHANGED
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
+
import crypto from 'node:crypto';
|
|
15
16
|
import { irisHome, capabilitiesOf, onChange as onConfigChange } from './config.js';
|
|
16
17
|
import { mimeOf, mediaUrl } from './media.js';
|
|
17
18
|
import * as tasks from './tasks.js';
|
|
18
19
|
import { runAction } from './actions.js';
|
|
19
20
|
import { chmodPrivateFile, ensurePrivateDir } from './private-storage.js';
|
|
20
21
|
import { inferMediaProtocol } from './provider-protocol.js';
|
|
22
|
+
import { deriveUserState } from './task-semantics.js';
|
|
21
23
|
import { optimizePrompt } from './prompt-optimizer.js';
|
|
22
24
|
import {
|
|
23
25
|
importPromptOptimizerConfig,
|
|
@@ -75,6 +77,21 @@ function mediaOf(task) {
|
|
|
75
77
|
}));
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
const V2_FACT_FIELDS = ['phase', 'acceptance', 'watchState', 'outcome', 'deliveryState', 'cancelState'];
|
|
81
|
+
|
|
82
|
+
function isTaskV2(t) {
|
|
83
|
+
return Boolean(t && t.schemaVersion === 2 && V2_FACT_FIELDS.every((field) => Object.prototype.hasOwnProperty.call(t, field)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 旧记录保持原四态;只有完整 v2 记录才使用事实轴派生用户态。 */
|
|
87
|
+
function taskUserState(t) {
|
|
88
|
+
if (isTaskV2(t)) return deriveUserState(t);
|
|
89
|
+
if (t && t.status === 'succeeded') return 'succeeded';
|
|
90
|
+
if (t && t.status === 'failed') return 'failed';
|
|
91
|
+
if (t && t.status === 'canceled') return 'canceled';
|
|
92
|
+
return 'running';
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
function taskPublic(t) {
|
|
79
96
|
const elapsedMs =
|
|
80
97
|
t.elapsedMs ||
|
|
@@ -83,6 +100,9 @@ function taskPublic(t) {
|
|
|
83
100
|
id: t.id,
|
|
84
101
|
cap: t.cap || '',
|
|
85
102
|
status: t.status || 'running',
|
|
103
|
+
userState: taskUserState(t),
|
|
104
|
+
schemaVersion: Number(t.schemaVersion || 1),
|
|
105
|
+
revision: Number(t.revision || 0),
|
|
86
106
|
progress: t.progress || '',
|
|
87
107
|
model: t.model || '',
|
|
88
108
|
providerName: t.providerName || '',
|
|
@@ -94,6 +114,8 @@ function taskPublic(t) {
|
|
|
94
114
|
saved: !!t.saved,
|
|
95
115
|
expired: !!t.expired,
|
|
96
116
|
mode: t.mode || '',
|
|
117
|
+
retryOf: t.retryOf || '',
|
|
118
|
+
attentionDisposition: tasks.attentionDisposition(t),
|
|
97
119
|
files: Array.isArray(t.files) ? t.files.map((f) => path.basename(f)) : [],
|
|
98
120
|
media: mediaOf(t)
|
|
99
121
|
};
|
|
@@ -110,21 +132,32 @@ function loadProviders() {
|
|
|
110
132
|
return Array.isArray(cache && cache.providers) ? cache.providers : [];
|
|
111
133
|
}
|
|
112
134
|
|
|
113
|
-
|
|
135
|
+
const STATE_EPOCH = crypto.randomUUID();
|
|
136
|
+
let stateRevision = 0;
|
|
137
|
+
|
|
138
|
+
/** 构建完整工作台状态(纯 JSON,可序列化);序号用于客户端拒绝迟到快照。 */
|
|
114
139
|
export function buildState() {
|
|
115
|
-
const
|
|
140
|
+
const taskRecords = loadTasks();
|
|
116
141
|
const providers = loadProviders();
|
|
117
142
|
|
|
118
|
-
const all = [...
|
|
119
|
-
const
|
|
120
|
-
const
|
|
143
|
+
const all = [...taskRecords].sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')));
|
|
144
|
+
const stateOf = (t) => taskUserState(t);
|
|
145
|
+
const needsAttention = (t) => ['watching_paused', 'needs_attention', 'artifact_unavailable'].includes(stateOf(t));
|
|
146
|
+
const acknowledged = (t) => tasks.attentionDisposition(t).status === 'acknowledged';
|
|
147
|
+
const running = all.filter((t) => ['queued', 'running'].includes(stateOf(t)) && !t.expired);
|
|
148
|
+
const attention = all.filter((t) => needsAttention(t) && !acknowledged(t) && !t.expired);
|
|
149
|
+
const terminal = all.filter((t) => ['succeeded', 'failed', 'canceled'].includes(stateOf(t))
|
|
150
|
+
|| (needsAttention(t) && acknowledged(t)) || t.expired);
|
|
121
151
|
const recent = terminal.slice(0, 100).map(taskPublic);
|
|
122
152
|
|
|
123
153
|
return {
|
|
154
|
+
stateEpoch: STATE_EPOCH,
|
|
155
|
+
stateRevision: ++stateRevision,
|
|
124
156
|
providers: providers.map(providerPublic),
|
|
125
157
|
tasks: {
|
|
126
158
|
recent,
|
|
127
159
|
recentTotal: terminal.length, // 泡泡「查看全部 (N)」计数用(不受 slice 截断)
|
|
160
|
+
attention: attention.slice(0, 20).map(taskPublic),
|
|
128
161
|
running: running.slice(0, 10).map(taskPublic)
|
|
129
162
|
}
|
|
130
163
|
};
|
|
@@ -138,15 +171,44 @@ export function sseClientCount() {
|
|
|
138
171
|
}
|
|
139
172
|
|
|
140
173
|
const sseClients = new Set(); // 活跃 res(长连接)
|
|
174
|
+
const sseBlocked = new Set(); // 背压中的连接
|
|
175
|
+
const sseLatest = new WeakMap(); // 每个慢连接只保留最新状态
|
|
176
|
+
const sseDrainBound = new WeakSet();
|
|
141
177
|
let sseHeartbeat = null; // 心跳定时器(15s,防代理/浏览器空闲断连)
|
|
142
178
|
let sseThrottleTimer = null; // 推送节流(400ms 合并,防盯守高频 update 刷屏)
|
|
143
179
|
let ssePending = false;
|
|
144
180
|
let sseBound = false;
|
|
145
181
|
|
|
182
|
+
function bindSseDrain(res) {
|
|
183
|
+
sseBlocked.add(res);
|
|
184
|
+
if (sseDrainBound.has(res)) return;
|
|
185
|
+
sseDrainBound.add(res);
|
|
186
|
+
res.once('drain', () => {
|
|
187
|
+
sseDrainBound.delete(res);
|
|
188
|
+
sseBlocked.delete(res);
|
|
189
|
+
const latest = sseLatest.get(res);
|
|
190
|
+
sseLatest.delete(res);
|
|
191
|
+
if (latest !== undefined) pushSse(res, latest);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
146
195
|
function pushSse(res, data) {
|
|
147
196
|
if (!res || res.writableEnded || res.destroyed) return;
|
|
197
|
+
if (sseBlocked.has(res)) {
|
|
198
|
+
sseLatest.set(res, data);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
if (res.write(`data: ${data}\n\n`) === false) bindSseDrain(res);
|
|
203
|
+
} catch (_) {
|
|
204
|
+
/* 连接已死,close 事件会清理 */
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function pushSseComment(res, comment) {
|
|
209
|
+
if (!res || res.writableEnded || res.destroyed || sseBlocked.has(res)) return;
|
|
148
210
|
try {
|
|
149
|
-
res.write(
|
|
211
|
+
if (res.write(`: ${comment}\n\n`) === false) bindSseDrain(res);
|
|
150
212
|
} catch (_) {
|
|
151
213
|
/* 连接已死,close 事件会清理 */
|
|
152
214
|
}
|
|
@@ -177,7 +239,7 @@ function scheduleBroadcast() {
|
|
|
177
239
|
function ensureHeartbeat() {
|
|
178
240
|
if (sseHeartbeat) return;
|
|
179
241
|
sseHeartbeat = setInterval(() => {
|
|
180
|
-
for (const res of [...sseClients])
|
|
242
|
+
for (const res of [...sseClients]) pushSseComment(res, 'ping'); // 注释帧不覆盖背压期间的最新状态
|
|
181
243
|
if (!sseClients.size) stopHeartbeat();
|
|
182
244
|
}, 15000);
|
|
183
245
|
if (sseHeartbeat && sseHeartbeat.unref) sseHeartbeat.unref();
|
|
@@ -239,6 +301,8 @@ function serveSse(res) {
|
|
|
239
301
|
pushSse(res, JSON.stringify(buildState())); // 立即推当前状态
|
|
240
302
|
const onClose = () => {
|
|
241
303
|
sseClients.delete(res);
|
|
304
|
+
sseBlocked.delete(res);
|
|
305
|
+
sseLatest.delete(res);
|
|
242
306
|
if (!sseClients.size) {
|
|
243
307
|
stopHeartbeat();
|
|
244
308
|
if (sseThrottleTimer) clearTimeout(sseThrottleTimer);
|
|
@@ -493,6 +557,10 @@ function serveTaskDetail(taskId, method, res) {
|
|
|
493
557
|
id: t.id,
|
|
494
558
|
cap: t.cap || '',
|
|
495
559
|
status: t.status || '',
|
|
560
|
+
userState: taskUserState(t),
|
|
561
|
+
schemaVersion: Number(t.schemaVersion || 1),
|
|
562
|
+
revision: Number(t.revision || 0),
|
|
563
|
+
...(isTaskV2(t) ? Object.fromEntries(V2_FACT_FIELDS.map((field) => [field, t[field]])) : {}),
|
|
496
564
|
progress: t.progress || '',
|
|
497
565
|
model: t.model || '',
|
|
498
566
|
providerName: t.providerName || '',
|
|
@@ -503,6 +571,11 @@ function serveTaskDetail(taskId, method, res) {
|
|
|
503
571
|
error: t.error || '',
|
|
504
572
|
mode: t.mode || '',
|
|
505
573
|
remoteTaskId: t.remoteTaskId || '',
|
|
574
|
+
retryOf: t.retryOf || '',
|
|
575
|
+
manualRetries: Array.isArray(t.manualRetries) ? t.manualRetries.filter((item) => item && item.taskId).map((item) => ({
|
|
576
|
+
taskId: String(item.taskId), createdAt: item.createdAt || ''
|
|
577
|
+
})) : [],
|
|
578
|
+
attentionDisposition: tasks.attentionDisposition(t),
|
|
506
579
|
files: Array.isArray(t.files) ? t.files.map((f) => path.basename(f)) : [],
|
|
507
580
|
media: mediaOf(t),
|
|
508
581
|
attachments: Array.isArray(t.attachments) ? t.attachments.map((a) => ({
|
package/lib/client.js
CHANGED
|
@@ -37,6 +37,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
37
37
|
'.iris-wb-badge { font-size: 11px; padding: 1px 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2, #32323c); }',
|
|
38
38
|
'.iris-wb-badge.ok { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
|
|
39
39
|
'.iris-wb-badge.err { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
|
|
40
|
+
'.iris-wb-badge.warn { color: var(--dsw-alias-state-warn-primary, #d9a941); }',
|
|
40
41
|
'.iris-wb-progress { margin-top: 6px; }',
|
|
41
42
|
'.iris-wb-bar { height: 5px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2, #32323c); overflow: hidden; }',
|
|
42
43
|
'.iris-wb-bar i { display: block; height: 100%; background: var(--dsw-alias-brand-primary, #7aa2f7); }',
|
|
@@ -252,6 +253,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
252
253
|
if (sharedState) setState(sharedState);
|
|
253
254
|
if (!sharedTimer && !sharedSource) {
|
|
254
255
|
function apply(data) {
|
|
256
|
+
if (sharedState && data && sharedState.stateEpoch && data.stateEpoch === sharedState.stateEpoch
|
|
257
|
+
&& Number(data.stateRevision || 0) <= Number(sharedState.stateRevision || 0)) return;
|
|
255
258
|
sharedState = data;
|
|
256
259
|
for (var i = 0; i < sharedListeners.length; i++) {
|
|
257
260
|
sharedListeners[i](data);
|
|
@@ -347,10 +350,38 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
347
350
|
return '更早';
|
|
348
351
|
}
|
|
349
352
|
|
|
353
|
+
var TASK_STATE_LABELS = {
|
|
354
|
+
queued: '排队中', running: '运行中', watching_paused: '观察暂停', needs_attention: '需要确认',
|
|
355
|
+
artifact_unavailable: '结果待取回', succeeded: '成功', failed: '失败', canceled: '已取消'
|
|
356
|
+
};
|
|
357
|
+
function taskState(task) {
|
|
358
|
+
if (task && task.userState) return task.userState;
|
|
359
|
+
if (task && task.status === 'succeeded') return 'succeeded';
|
|
360
|
+
if (task && task.status === 'failed') return 'failed';
|
|
361
|
+
if (task && task.status === 'canceled') return 'canceled';
|
|
362
|
+
return 'running';
|
|
363
|
+
}
|
|
364
|
+
function taskStateLabel(task) {
|
|
365
|
+
var state = taskState(task);
|
|
366
|
+
return TASK_STATE_LABELS[state] || state;
|
|
367
|
+
}
|
|
368
|
+
function taskStateClass(task) {
|
|
369
|
+
var state = taskState(task);
|
|
370
|
+
if (state === 'succeeded') return 'ok';
|
|
371
|
+
if (state === 'failed') return 'err';
|
|
372
|
+
if (state === 'watching_paused' || state === 'needs_attention' || state === 'artifact_unavailable') return 'warn';
|
|
373
|
+
return '';
|
|
374
|
+
}
|
|
375
|
+
function attentionDispositionLabel(task) {
|
|
376
|
+
var disposition = task && task.attentionDisposition;
|
|
377
|
+
if (!disposition || disposition.status !== 'acknowledged') return '';
|
|
378
|
+
return disposition.reason === 'retried' ? '已通过重试处理' : '提醒已读';
|
|
379
|
+
}
|
|
380
|
+
|
|
350
381
|
/* 紧凑任务行:泡泡「任务」标签用,单行摘要 */
|
|
351
382
|
function taskRowMini(task, selected, onSelect) {
|
|
352
|
-
var st = task
|
|
353
|
-
var label =
|
|
383
|
+
var st = taskStateClass(task);
|
|
384
|
+
var label = taskStateLabel(task);
|
|
354
385
|
return React.createElement('div', {
|
|
355
386
|
key: task.id, className: 'iris-task-mini' + (selected ? ' selected' : ''),
|
|
356
387
|
onClick: function () { onSelect(selected ? null : task.id); }, title: '点击展开详情'
|
|
@@ -361,6 +392,47 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
361
392
|
React.createElement('span', { className: 'iris-wb-muted' }, task.createdAt ? new Date(task.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''));
|
|
362
393
|
}
|
|
363
394
|
|
|
395
|
+
function TaskRecoveryActions({ detail }) {
|
|
396
|
+
var pair = React.useState('');
|
|
397
|
+
var note = pair[0];
|
|
398
|
+
var setNote = pair[1];
|
|
399
|
+
var busyPair = React.useState(false);
|
|
400
|
+
var busy = busyPair[0];
|
|
401
|
+
var setBusy = busyPair[1];
|
|
402
|
+
var state = taskState(detail);
|
|
403
|
+
var attentionState = ['watching_paused', 'needs_attention', 'artifact_unavailable'].indexOf(state) >= 0;
|
|
404
|
+
var acknowledged = detail.attentionDisposition && detail.attentionDisposition.status === 'acknowledged';
|
|
405
|
+
var canAcknowledge = detail.schemaVersion === 2 && attentionState && !acknowledged;
|
|
406
|
+
var canRestore = detail.schemaVersion === 2 && attentionState && acknowledged;
|
|
407
|
+
var canReobserve = detail.schemaVersion === 2 && !acknowledged && detail.acceptance === 'accepted' && detail.remoteTaskId
|
|
408
|
+
&& detail.outcome !== 'succeeded' && detail.outcome !== 'failed' && detail.outcome !== 'canceled';
|
|
409
|
+
var canRedeliver = detail.schemaVersion === 2 && !acknowledged && state === 'artifact_unavailable' && detail.remoteTaskId;
|
|
410
|
+
var canRetry = detail.schemaVersion === 2 && !acknowledged && ['needs_attention', 'failed', 'canceled'].indexOf(state) >= 0
|
|
411
|
+
&& (detail.cap === 'image' || (detail.cap === 'video' && detail.mode === 't2v'));
|
|
412
|
+
if (!canAcknowledge && !canRestore && !canReobserve && !canRedeliver && !canRetry) return null;
|
|
413
|
+
function run(action, body, confirmMessage) {
|
|
414
|
+
if (confirmMessage && !window.confirm(confirmMessage)) return;
|
|
415
|
+
setBusy(true);
|
|
416
|
+
setNote('处理中…');
|
|
417
|
+
postAction(action, body).then(function (r) {
|
|
418
|
+
setNote(r.ok ? ((r.d && r.d.text) || '操作已接受') : ('失败:' + ((r.d && r.d.error) || '未知错误')));
|
|
419
|
+
}).catch(function (error) { setNote('失败:' + String((error && error.message) || error)); })
|
|
420
|
+
.finally(function () { setBusy(false); });
|
|
421
|
+
}
|
|
422
|
+
return React.createElement('div', { className: 'iris-wb-row', onClick: function (e) { e.stopPropagation(); } },
|
|
423
|
+
canReobserve ? React.createElement('button', { className: 'iris-pm-btn', disabled: busy, onClick: function () { run('task_reobserve', { task_id: detail.id }); } }, '重新观察') : null,
|
|
424
|
+
canRedeliver ? React.createElement('button', { className: 'iris-pm-btn', disabled: busy, onClick: function () { run('task_redeliver', { task_id: detail.id }); } }, '重新交付') : null,
|
|
425
|
+
canRetry ? React.createElement('button', { className: 'iris-pm-btn danger', disabled: busy, onClick: function () {
|
|
426
|
+
run('task_manual_retry', { task_id: detail.id, confirm_duplicate_charge: true },
|
|
427
|
+
'远端任务可能已经产生,本次会创建新的生成任务,可能重复计费。确认知情重试?');
|
|
428
|
+
} }, '知情重试') : null,
|
|
429
|
+
canAcknowledge ? React.createElement('button', { className: 'iris-pm-btn', disabled: busy,
|
|
430
|
+
onClick: function () { run('task_ack_attention', { task_id: detail.id }); } }, '标为已读') : null,
|
|
431
|
+
canRestore ? React.createElement('button', { className: 'iris-pm-btn', disabled: busy,
|
|
432
|
+
onClick: function () { run('task_restore_attention', { task_id: detail.id }); } }, '恢复提醒') : null,
|
|
433
|
+
note ? React.createElement('span', { className: 'iris-clean-note' }, note) : null);
|
|
434
|
+
}
|
|
435
|
+
|
|
364
436
|
/* ---- 任务详情抽屉:点击任务卡片行展开 ---- */
|
|
365
437
|
function TaskDetailDrawer({ taskId }) {
|
|
366
438
|
var detail = useTaskDetail(taskId);
|
|
@@ -370,6 +442,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
370
442
|
var el = [];
|
|
371
443
|
el.push(React.createElement('div', { key: 'k', className: 'iris-wb-k' }, '提示词'));
|
|
372
444
|
el.push(React.createElement('div', { key: 'p', className: 'iris-wb-prompt' }, detail.prompt || '(空)'));
|
|
445
|
+
if (Number(detail.schemaVersion) === 1) {
|
|
446
|
+
el.push(React.createElement('div', { key: 'legacy', className: 'iris-clean-note' },
|
|
447
|
+
'旧版任务记录 · 仅支持查看。由于缺少可靠的远端受理与交付事实,不提供重新观察、重新交付或知情重试;0.1.3 新任务将使用可信任务语义。'));
|
|
448
|
+
}
|
|
449
|
+
var dispositionLabel = attentionDispositionLabel(detail);
|
|
450
|
+
if (dispositionLabel) {
|
|
451
|
+
var related = detail.attentionDisposition.relatedTaskId ? ' · 关联任务 ' + detail.attentionDisposition.relatedTaskId : '';
|
|
452
|
+
el.push(React.createElement('div', { key: 'attention-disposition', className: 'iris-clean-note' }, dispositionLabel + related));
|
|
453
|
+
}
|
|
373
454
|
if (detail.error) {
|
|
374
455
|
el.push(React.createElement('div', { key: 'ek', className: 'iris-wb-k' }, '错误'));
|
|
375
456
|
el.push(React.createElement('div', { key: 'e', className: 'iris-wb-err' }, detail.error));
|
|
@@ -377,11 +458,13 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
377
458
|
var kv = [
|
|
378
459
|
['ID', detail.id],
|
|
379
460
|
['能力', detail.cap],
|
|
380
|
-
['状态', detail
|
|
461
|
+
['状态', taskStateLabel(detail)],
|
|
381
462
|
['模型', detail.model],
|
|
382
463
|
['供应商', detail.providerName],
|
|
383
464
|
['模式', detail.mode || '—'],
|
|
384
465
|
['远端任务', detail.remoteTaskId || '—'],
|
|
466
|
+
['来源任务', detail.retryOf || '—'],
|
|
467
|
+
['后续重试', (detail.manualRetries || []).map(function (item) { return item.taskId; }).join('、') || '—'],
|
|
385
468
|
['发起', detail.createdAt ? fmtTime(detail.createdAt) : '—'],
|
|
386
469
|
['完成', detail.finishedAt ? fmtTime(detail.finishedAt) : '—'],
|
|
387
470
|
['耗时', detail.elapsedMs ? fmtElapsed(detail.elapsedMs) : '—']
|
|
@@ -418,24 +501,25 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
418
501
|
return React.createElement('span', { key: a.attachmentId, className: 'iris-wb-kv' }, a.attachmentId + (a.file ? ' (' + a.file + ')' : ''));
|
|
419
502
|
})));
|
|
420
503
|
}
|
|
504
|
+
el.push(React.createElement(TaskRecoveryActions, { key: 'recovery', detail: detail }));
|
|
421
505
|
return React.createElement('div', { className: 'iris-wb-drawer' }, ...el);
|
|
422
506
|
}
|
|
423
507
|
|
|
424
508
|
function taskRow(task, isRunning, selected, onSelect) {
|
|
425
|
-
var
|
|
426
|
-
|
|
427
|
-
badge = React.createElement('span', { className: 'iris-wb-badge ok' }, '成功');
|
|
428
|
-
} else if (task.status === 'failed') {
|
|
429
|
-
badge = React.createElement('span', { className: 'iris-wb-badge err' }, '失败');
|
|
430
|
-
} else {
|
|
431
|
-
badge = React.createElement('span', { className: 'iris-wb-badge' }, task.status);
|
|
432
|
-
}
|
|
509
|
+
var stateClass = taskStateClass(task);
|
|
510
|
+
var badge = React.createElement('span', { className: 'iris-wb-badge' + (stateClass ? ' ' + stateClass : '') }, taskStateLabel(task));
|
|
433
511
|
var cells = [
|
|
434
512
|
React.createElement('span', { className: 'iris-wb-kv' }, task.cap),
|
|
435
513
|
badge,
|
|
436
514
|
React.createElement('span', { className: 'iris-wb-kv' }, task.model || ''),
|
|
437
515
|
React.createElement('span', { className: 'iris-wb-muted' }, String(task.prompt || '').slice(0, 60))
|
|
438
516
|
];
|
|
517
|
+
if (Number(task.schemaVersion) === 1) {
|
|
518
|
+
cells.push(React.createElement('span', { className: 'iris-wb-badge', title: '旧版记录缺少可靠的远端受理与交付事实,仅支持查看' }, '旧任务 · 只读'));
|
|
519
|
+
}
|
|
520
|
+
var dispositionLabel = attentionDispositionLabel(task);
|
|
521
|
+
if (dispositionLabel) cells.push(React.createElement('span', { className: 'iris-wb-badge ok' }, dispositionLabel));
|
|
522
|
+
if (task.retryOf) cells.push(React.createElement('span', { className: 'iris-wb-badge' }, '重试任务'));
|
|
439
523
|
if (task.error) cells.push(React.createElement('span', { className: 'iris-wb-badge err' }, String(task.error).slice(0, 40)));
|
|
440
524
|
var links = (task.media || []).map(function (m) {
|
|
441
525
|
return React.createElement('a', {
|
|
@@ -1061,6 +1145,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1061
1145
|
var state = useIrisState(5000);
|
|
1062
1146
|
var running = (state && state.tasks && state.tasks.running) || [];
|
|
1063
1147
|
var recent = (state && state.tasks && state.tasks.recent) || [];
|
|
1148
|
+
var attention = (state && state.tasks && state.tasks.attention) || [];
|
|
1064
1149
|
var selPair = React.useState(null);
|
|
1065
1150
|
var selectedTask = selPair[0];
|
|
1066
1151
|
var setSelectedTask = selPair[1];
|
|
@@ -1081,6 +1166,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1081
1166
|
React.createElement('div', { className: 'iris-wb-box' },
|
|
1082
1167
|
running.length ? running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })
|
|
1083
1168
|
: React.createElement('div', { className: 'iris-wb-empty' }, '暂无进行中任务')),
|
|
1169
|
+
attention.length ? React.createElement('div', {},
|
|
1170
|
+
React.createElement('div', { className: 'iris-wb-sec' }, '需要处理'),
|
|
1171
|
+
React.createElement('div', { className: 'iris-wb-box' },
|
|
1172
|
+
attention.map(function (t) { return taskRow(t, false, selectedTask === t.id, onSelect); }))) : null,
|
|
1084
1173
|
React.createElement('div', { className: 'iris-wb-sec' }, '历史任务'),
|
|
1085
1174
|
React.createElement(CleanupBar, {}),
|
|
1086
1175
|
React.createElement(HistoryBrowser, { recent: recent, selected: selectedTask, onSelect: onSelect }),
|
|
@@ -1093,6 +1182,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1093
1182
|
var state = useIrisState(5000);
|
|
1094
1183
|
var running = (state && state.tasks && state.tasks.running) || [];
|
|
1095
1184
|
var recent = (state && state.tasks && state.tasks.recent) || [];
|
|
1185
|
+
var attention = (state && state.tasks && state.tasks.attention) || [];
|
|
1096
1186
|
var recentTotal = (state && state.tasks && state.tasks.recentTotal) || recent.length;
|
|
1097
1187
|
var cards = useBubbleCards()[0];
|
|
1098
1188
|
var tabPair = React.useState('tasks');
|
|
@@ -1104,7 +1194,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1104
1194
|
function onSelect(id) { setSelectedTask(id); }
|
|
1105
1195
|
var body;
|
|
1106
1196
|
if (tab === 'tasks') {
|
|
1107
|
-
//
|
|
1197
|
+
// 快捷面板承载当前任务、需要处理的异常事实与可直接查看的成功产物。
|
|
1108
1198
|
var succeeded = recent.filter(function (t) {
|
|
1109
1199
|
return t.status === 'succeeded' && Array.isArray(t.media) && t.media.length > 0;
|
|
1110
1200
|
});
|
|
@@ -1113,6 +1203,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1113
1203
|
running.length ? React.createElement('div', {},
|
|
1114
1204
|
React.createElement('div', { className: 'iris-wb-sec' }, '运行中'),
|
|
1115
1205
|
running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })) : null,
|
|
1206
|
+
attention.length ? React.createElement('div', {},
|
|
1207
|
+
React.createElement('div', { className: 'iris-wb-sec' }, '需要处理'),
|
|
1208
|
+
attention.slice(0, 4).map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); })) : null,
|
|
1116
1209
|
React.createElement('div', { className: 'iris-wb-sec' }, '最近完成'),
|
|
1117
1210
|
shown.length ? shown.map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); })
|
|
1118
1211
|
: React.createElement('div', { className: 'iris-wb-empty' }, '尚无成功产物'),
|
|
@@ -1379,6 +1472,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1379
1472
|
var providers = (state && state.providers) || [];
|
|
1380
1473
|
var configured = providers.some(function (p) { return p.enabled && p.apiKeyHint; });
|
|
1381
1474
|
var running = (state && state.tasks && state.tasks.running) || [];
|
|
1475
|
+
var attention = (state && state.tasks && state.tasks.attention) || [];
|
|
1382
1476
|
|
|
1383
1477
|
var posPair = React.useState(function () {
|
|
1384
1478
|
var saved = null;
|
|
@@ -1464,8 +1558,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
|
|
|
1464
1558
|
style = { right: DEFAULT_X + 'px', bottom: DEFAULT_Y + 'px' };
|
|
1465
1559
|
}
|
|
1466
1560
|
|
|
1467
|
-
var
|
|
1468
|
-
|
|
1561
|
+
var badgeCount = running.length + attention.length;
|
|
1562
|
+
var badge = badgeCount
|
|
1563
|
+
? React.createElement('span', { className: 'iris-bubble-badge', title: attention.length ? attention.length + ' 个任务需要处理' : '' }, badgeCount)
|
|
1469
1564
|
: null;
|
|
1470
1565
|
|
|
1471
1566
|
var panel = null;
|
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { attentionDisposition, deriveUserState, semanticViolations } from './task-semantics.js';
|
|
8
|
+
|
|
9
|
+
const PACKAGE_FILE = new URL('../package.json', import.meta.url);
|
|
10
|
+
|
|
11
|
+
function readPackageVersion() {
|
|
12
|
+
try { return JSON.parse(fs.readFileSync(PACKAGE_FILE, 'utf8')).version || 'unknown'; } catch (_) { return 'unknown'; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function result(id, status, summary, suggestion) {
|
|
16
|
+
return { id, status, summary, ...(suggestion ? { suggestion } : {}) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseVersion(value) {
|
|
20
|
+
const match = String(value || '').match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
21
|
+
return match ? match.slice(1).map(Number) : [0, 0, 0];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function atLeast(actual, required) {
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
if (actual[i] !== required[i]) return actual[i] > required[i];
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readJson(file) {
|
|
32
|
+
if (!fs.existsSync(file)) return { exists: false, value: null };
|
|
33
|
+
try { return { exists: true, value: JSON.parse(fs.readFileSync(file, 'utf8')) }; }
|
|
34
|
+
catch (_) { return { exists: true, value: null, error: 'JSON 无法解析' }; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function plain(value) {
|
|
38
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isGroupOrWorldAccessible(target) {
|
|
42
|
+
if (process.platform === 'win32' || !fs.existsSync(target)) return false;
|
|
43
|
+
try { return (fs.lstatSync(target).mode & 0o077) !== 0; } catch (_) { return false; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function privateFileModeProblems(dataDir) {
|
|
47
|
+
if (process.platform === 'win32') return [];
|
|
48
|
+
return ['providers.json', 'tasks.json', 'prompt-optimizer.json']
|
|
49
|
+
.filter((name) => isGroupOrWorldAccessible(path.join(dataDir, name)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function commandAvailable(command, runner) {
|
|
53
|
+
try {
|
|
54
|
+
const run = runner(command, ['-version']);
|
|
55
|
+
return Boolean(run && run.status === 0 && !run.error);
|
|
56
|
+
} catch (_) { return false; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function nearestExisting(target) {
|
|
60
|
+
let current = target;
|
|
61
|
+
for (;;) {
|
|
62
|
+
if (fs.existsSync(current)) return current;
|
|
63
|
+
const parent = path.dirname(current);
|
|
64
|
+
if (parent === current) return null;
|
|
65
|
+
current = parent;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writableProbe(dataDir) {
|
|
70
|
+
const base = fs.existsSync(dataDir) ? dataDir : nearestExisting(path.dirname(dataDir));
|
|
71
|
+
if (!base) return false;
|
|
72
|
+
const probe = path.join(base, `.iris-doctor-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
|
|
73
|
+
try {
|
|
74
|
+
fs.writeFileSync(probe, 'probe', { flag: 'wx', mode: 0o600 });
|
|
75
|
+
fs.rmSync(probe);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (_) {
|
|
78
|
+
try { fs.rmSync(probe, { force: true }); } catch (_) {}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function inspectConfig(dataDir, checks) {
|
|
84
|
+
const config = readJson(path.join(dataDir, 'providers.json'));
|
|
85
|
+
if (!config.exists) {
|
|
86
|
+
checks.push(result('config', 'warn', '尚未创建供应商配置', '启动 Iris 后在工作台添加供应商,或准备好 DSH_HOME 后再运行 doctor。'));
|
|
87
|
+
return { providers: [], assignments: {} };
|
|
88
|
+
}
|
|
89
|
+
if (config.error || !plain(config.value) || !Array.isArray(config.value.providers)
|
|
90
|
+
|| !plain(config.value.assignments || {})) {
|
|
91
|
+
checks.push(result('config', 'error', 'providers.json 结构无效或无法解析', '先备份该文件,再从 Iris 工作台重新配置;不要把密钥粘贴到诊断输出。'));
|
|
92
|
+
return { providers: [], assignments: {} };
|
|
93
|
+
}
|
|
94
|
+
const providers = config.value.providers;
|
|
95
|
+
let incomplete = 0;
|
|
96
|
+
let enabled = 0;
|
|
97
|
+
let models = 0;
|
|
98
|
+
for (const provider of providers) {
|
|
99
|
+
if (!plain(provider)) { incomplete++; continue; }
|
|
100
|
+
if (provider.enabled !== false) enabled++;
|
|
101
|
+
if (provider.enabled !== false && (!String(provider.baseUrl || '').trim() || !String(provider.apiKey || '').trim())) incomplete++;
|
|
102
|
+
models += Array.isArray(provider.models) ? provider.models.length : 0;
|
|
103
|
+
}
|
|
104
|
+
if (incomplete) checks.push(result('providers', 'warn', `${incomplete} 个启用供应商缺少 Base URL、API Key 或结构无效`, '在 Iris 工作台补全或停用对应供应商。'));
|
|
105
|
+
else if (!enabled) checks.push(result('providers', 'warn', '没有启用的供应商', '在 Iris 工作台启用至少一个供应商。'));
|
|
106
|
+
else checks.push(result('providers', 'ok', `${enabled} 个供应商已启用;凭据未输出`));
|
|
107
|
+
const assignments = config.value.assignments || {};
|
|
108
|
+
const assignmentCount = Object.keys(assignments).length;
|
|
109
|
+
if (!models) checks.push(result('models', 'warn', '模型池为空或尚未发现模型', '在工作台对供应商执行“发现模型”,再核对能力标签。'));
|
|
110
|
+
else checks.push(result('models', 'ok', `模型池记录 ${models} 个模型,能力分配 ${assignmentCount} 项`));
|
|
111
|
+
return { providers, assignments };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function inspectTasks(dataDir, checks) {
|
|
115
|
+
const stored = readJson(path.join(dataDir, 'tasks.json'));
|
|
116
|
+
if (!stored.exists) {
|
|
117
|
+
checks.push(result('tasks', 'ok', '尚无任务记录'));
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
if (stored.error || !plain(stored.value) || !Array.isArray(stored.value.tasks)) {
|
|
121
|
+
checks.push(result('tasks', 'error', 'tasks.json 结构无效或无法解析', '保留损坏文件作为证据;重新启动 Iris 会尝试隔离并建立新任务表。'));
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
let invalid = 0;
|
|
125
|
+
let attention = 0;
|
|
126
|
+
for (const task of stored.value.tasks) {
|
|
127
|
+
if (!plain(task) || !String(task.id || '').trim()) { invalid++; continue; }
|
|
128
|
+
if (task.schemaVersion === 2) {
|
|
129
|
+
if (semanticViolations(task).length) invalid++;
|
|
130
|
+
const userState = deriveUserState(task);
|
|
131
|
+
if (['watching_paused', 'needs_attention', 'artifact_unavailable'].includes(userState)
|
|
132
|
+
&& attentionDisposition(task).status !== 'acknowledged') attention++;
|
|
133
|
+
} else if (task.status === 'running' && !task.remoteTaskId) attention++;
|
|
134
|
+
}
|
|
135
|
+
if (invalid) checks.push(result('tasks', 'error', `${invalid} 条任务记录语义或结构无效`, '不要手工改写任务事实;先备份 tasks.json 并保留损坏证据。'));
|
|
136
|
+
else if (attention) checks.push(result('tasks', 'warn', `${stored.value.tasks.length} 条任务中有 ${attention} 条需要人工确认`, '在 Iris 工作台的“需要处理”分区查看,并选择重新观察、重新交付、标为已读或知情重试。'));
|
|
137
|
+
else checks.push(result('tasks', 'ok', `${stored.value.tasks.length} 条任务记录结构有效`));
|
|
138
|
+
return stored.value.tasks.filter(plain);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function inspectArtifacts(dataDir, tasks, checks) {
|
|
142
|
+
const outputs = path.join(dataDir, 'outputs');
|
|
143
|
+
const referenced = new Set();
|
|
144
|
+
let missing = 0;
|
|
145
|
+
for (const task of tasks) {
|
|
146
|
+
for (const name of task.files || []) {
|
|
147
|
+
const safe = path.basename(String(name));
|
|
148
|
+
referenced.add(safe);
|
|
149
|
+
try { if (!fs.statSync(path.join(outputs, safe)).isFile()) missing++; } catch (_) { missing++; }
|
|
150
|
+
}
|
|
151
|
+
for (const item of task.media || []) if (item && item.file) referenced.add(path.basename(String(item.file)));
|
|
152
|
+
for (const item of task.attachments || []) if (item && item.file) referenced.add(path.basename(String(item.file)));
|
|
153
|
+
}
|
|
154
|
+
let orphan = 0;
|
|
155
|
+
if (fs.existsSync(outputs)) {
|
|
156
|
+
for (const name of fs.readdirSync(outputs)) {
|
|
157
|
+
const target = path.join(outputs, name);
|
|
158
|
+
try { if (fs.lstatSync(target).isFile() && !referenced.has(name)) orphan++; } catch (_) {}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (missing) checks.push(result('artifacts', 'warn', `${missing} 个任务产物不可访问;另有 ${orphan} 个孤儿文件`, '在工作台检查任务详情并运行孤儿产物扫描。'));
|
|
162
|
+
else if (orphan) checks.push(result('artifacts', 'warn', `${orphan} 个产物文件没有任务引用`, '先在工作台运行孤儿扫描,确认后再删除。'));
|
|
163
|
+
else checks.push(result('artifacts', 'ok', `${referenced.size} 个已引用产物可核对,未发现孤儿文件`));
|
|
164
|
+
|
|
165
|
+
const uploads = path.join(dataDir, 'uploads');
|
|
166
|
+
let temporary = 0;
|
|
167
|
+
if (fs.existsSync(uploads)) {
|
|
168
|
+
for (const name of fs.readdirSync(uploads)) if (name.endsWith('.part')) temporary++;
|
|
169
|
+
}
|
|
170
|
+
checks.push(temporary
|
|
171
|
+
? result('temporary-files', 'warn', `${temporary} 个上传临时 .part 文件残留`, '确认没有上传进行中后重启 Iris,由启动清理逻辑处理。')
|
|
172
|
+
: result('temporary-files', 'ok', '未发现上传临时文件残留'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 零网络离线诊断。仅创建并立即删除一个写入探针;不启动 DSH、不读取或输出 API Key。
|
|
177
|
+
*/
|
|
178
|
+
export async function doctor(options = {}) {
|
|
179
|
+
const dshHome = path.resolve(options.dshHome || process.env.DSH_HOME || path.join(os.homedir(), '.dsh'));
|
|
180
|
+
const dataDir = path.join(dshHome, 'iris', 'v1');
|
|
181
|
+
const runner = options.commandRunner || ((command, args) => spawnSync(command, args, { stdio: 'ignore', shell: false }));
|
|
182
|
+
const sharpLoader = options.sharpLoader || (() => import('sharp'));
|
|
183
|
+
const nodeVersion = options.nodeVersion || process.versions.node;
|
|
184
|
+
const checks = [];
|
|
185
|
+
|
|
186
|
+
checks.push(atLeast(parseVersion(nodeVersion), [20, 10, 0])
|
|
187
|
+
? result('node', 'ok', `Node.js ${nodeVersion}`)
|
|
188
|
+
: result('node', 'error', `Node.js ${nodeVersion} 低于最低要求 20.10.0`, '升级 Node.js 后再运行 Iris。'));
|
|
189
|
+
try { await sharpLoader(); checks.push(result('sharp', 'ok', 'sharp 可加载')); }
|
|
190
|
+
catch (_) { checks.push(result('sharp', 'error', 'sharp 无法加载', '在当前平台重新安装 @mokuyoaxis/dsh-iris,确认 sharp 有可用的 ARM64/系统构建。')); }
|
|
191
|
+
for (const command of ['ffmpeg', 'ffprobe']) {
|
|
192
|
+
checks.push(commandAvailable(command, runner)
|
|
193
|
+
? result(command, 'ok', `${command} 可用`)
|
|
194
|
+
: result(command, 'warn', `${command} 不可用`, '视频抽帧与摘要需要 ffmpeg/ffprobe;其他 Iris 能力不受影响。'));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const dataExists = fs.existsSync(dataDir);
|
|
198
|
+
if (dataExists) {
|
|
199
|
+
let symlink = false;
|
|
200
|
+
try { symlink = fs.lstatSync(dataDir).isSymbolicLink(); } catch (_) {}
|
|
201
|
+
if (symlink) checks.push(result('storage', 'error', 'Iris 数据目录是符号链接,私有边界无法保证', '改用真实的私有目录并人工迁移数据。'));
|
|
202
|
+
else if (!writableProbe(dataDir)) checks.push(result('storage', 'error', 'Iris 数据目录不可写', '修复当前用户对 $DSH_HOME/iris/v1 的写权限。'));
|
|
203
|
+
else if (isGroupOrWorldAccessible(dataDir)) checks.push(result('storage', 'warn', 'Iris 数据目录允许组或其他用户访问', '在 POSIX 上将目录权限收紧为 0700、文件收紧为 0600。'));
|
|
204
|
+
else {
|
|
205
|
+
const exposedFiles = privateFileModeProblems(dataDir);
|
|
206
|
+
checks.push(exposedFiles.length
|
|
207
|
+
? result('storage', 'warn', `${exposedFiles.length} 个私有数据文件允许组或其他用户访问`, '在 POSIX 上将 providers.json、tasks.json 与提示词配置权限收紧为 0600。')
|
|
208
|
+
: result('storage', 'ok', 'Iris 数据目录可写、私有文件权限合格,临时探针已清理'));
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
checks.push(writableProbe(dataDir)
|
|
212
|
+
? result('storage', 'ok', 'Iris 数据目录尚未创建;父目录可写,临时探针已清理')
|
|
213
|
+
: result('storage', 'error', 'Iris 数据目录不存在且父目录不可写', '设置一个当前用户可写的 DSH_HOME。'));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
inspectConfig(dataDir, checks);
|
|
217
|
+
const taskRecords = inspectTasks(dataDir, checks);
|
|
218
|
+
inspectArtifacts(dataDir, taskRecords, checks);
|
|
219
|
+
checks.push(result('scope', 'ok', '离线模式未启动 DSH、未检查浏览器/工具注册,也未发送供应商请求'));
|
|
220
|
+
|
|
221
|
+
const errors = checks.filter((item) => item.status === 'error').length;
|
|
222
|
+
const warnings = checks.filter((item) => item.status === 'warn').length;
|
|
223
|
+
return {
|
|
224
|
+
schemaVersion: 1,
|
|
225
|
+
package: { name: '@mokuyoaxis/dsh-iris', version: options.packageVersion || readPackageVersion() },
|
|
226
|
+
mode: 'offline',
|
|
227
|
+
environment: { node: nodeVersion, platform: process.platform, arch: process.arch },
|
|
228
|
+
checks,
|
|
229
|
+
summary: { ok: checks.length - errors - warnings, warnings, errors },
|
|
230
|
+
exitCode: errors ? 2 : (warnings ? 1 : 0)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function formatDoctorReport(report) {
|
|
235
|
+
const mark = { ok: '✓', warn: '!', error: '✗' };
|
|
236
|
+
const lines = [`Iris Doctor ${report.package.version} · ${report.mode}`];
|
|
237
|
+
for (const check of report.checks) {
|
|
238
|
+
lines.push(`${mark[check.status] || '-'} ${check.id}: ${check.summary}`);
|
|
239
|
+
if (check.suggestion) lines.push(` 建议:${check.suggestion}`);
|
|
240
|
+
}
|
|
241
|
+
lines.push(`结果:${report.summary.errors} 错误,${report.summary.warnings} 警告`);
|
|
242
|
+
return lines.join('\n');
|
|
243
|
+
}
|