dsh-any-agent 0.5.2 → 0.6.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/src/doctor.mjs ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * dsh_doctor:只读体检(DSH 可执行文件与版本、profile、状态文件、runs/locks 目录、
3
+ * 孤儿锁、会话数与 RSS、生效配置)。只报告,不修改任何东西。
4
+ *
5
+ * 从 tools.mjs 拆出来:它是处理器表里最长的一项,且只依赖 `hub` 的两个只读字段
6
+ * (conversations / hosts)与一批探测函数。返回值形状与原处理器一致:
7
+ * `{ structured: { ok, verdict, checks }, text }`。
8
+ */
9
+
10
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import {
13
+ DEFAULT_PERMISSION,
14
+ IDLE_TTL_MS,
15
+ MAX_LIVE,
16
+ MAX_RSS_MB,
17
+ REVIEWS_DIR,
18
+ RUNS_DIR,
19
+ SHARED_PROCESS,
20
+ STATE_FILE,
21
+ TEAM_ACTIVE,
22
+ TEAM_MODE,
23
+ DSH_PROFILE,
24
+ dshVersionStatus,
25
+ } from './config.mjs';
26
+ import {
27
+ SOURCE_LABEL,
28
+ discoverDsh,
29
+ isNpxCachePath,
30
+ probeRuntimeNode,
31
+ resolveDsh,
32
+ satisfiesNodeRange,
33
+ } from './dsh-locate.mjs';
34
+ import { inspectProfile } from './bootstrap.mjs';
35
+ import { listOrphans, LOCKS_DIR } from './locks.mjs';
36
+ import { rssMb } from './procstat.mjs';
37
+
38
+ export async function runDoctor(hub) {
39
+ const checks = [];
40
+ const add = (id, status, detail, fix = null) => checks.push({ id, status, detail, fix });
41
+
42
+ try {
43
+ // 体检总是重新探测(不吃 60 秒缓存):用户刚装/升级完桌面版就来查,必须看到新结果。
44
+ const all = discoverDsh({ fresh: true });
45
+ const dsh = resolveDsh();
46
+ const { bin, version } = dsh;
47
+ const fromNpx = isNpxCachePath(bin);
48
+ add(
49
+ 'dsh_bin',
50
+ fromNpx ? 'warn' : 'ok',
51
+ `${bin}${version ? ` (DSH ${version})` : ''};来源=${SOURCE_LABEL[dsh.source] ?? dsh.source}` +
52
+ `;运行时=${dsh.runtime === 'node' ? `node ${process.versions.node}` : dsh.command}` +
53
+ (fromNpx ? ' —— 位于 npx 缓存,清缓存后会失效' : ''),
54
+ fromNpx
55
+ ? '安装 DeepSeek Harness 桌面版(会被自动识别),或 npm i -g @deepseek-ai/dsh'
56
+ : null,
57
+ );
58
+
59
+ // DSH 的 engines.node 与实际拉起它的运行时是否匹配(0.2.2 起要求 Node >= 24.9)。
60
+ const runtimeNode = probeRuntimeNode(dsh);
61
+ const engOk = satisfiesNodeRange(dsh.enginesNode, runtimeNode);
62
+ add(
63
+ 'dsh_runtime',
64
+ engOk === false ? 'warn' : 'ok',
65
+ `运行时 Node ${runtimeNode ?? '未知'},DSH 要求 ${dsh.enginesNode ?? '未声明'}` +
66
+ (engOk === false ? ' —— 不满足,部分能力可能异常' : ''),
67
+ engOk === false
68
+ ? dsh.runtime === 'node'
69
+ ? '安装桌面版(自带满足要求的运行时,会被优先选用),或升级本机 Node,或用 DSH_RUNTIME 指定一个满足要求的 node'
70
+ : '更新桌面版,或用 DSH_RUNTIME 指定一个满足要求的 node'
71
+ : null,
72
+ );
73
+
74
+ // GUI(桌面版)与本服务是不是同一份 DSH:两边共用 ~/.dsh 会话存储。
75
+ const desktops = all.filter((c) => c.source === 'desktop');
76
+ if (desktops.length === 0) {
77
+ add('dsh_desktop', 'ok', '未发现 DeepSeek Harness 桌面版(不影响使用)');
78
+ } else {
79
+ const gui = desktops[0];
80
+ const same = gui.version === version;
81
+ add(
82
+ 'dsh_desktop',
83
+ same ? 'ok' : 'warn',
84
+ `桌面版 ${gui.version ?? '?'}(${gui.installDir})` +
85
+ (same ? ',与本服务所用 DSH 一致' : `;本服务用的是 ${version ?? '?'}(${SOURCE_LABEL[dsh.source]})—— 两者共用 ~/.dsh 会话,版本不一致时新版本写的会话可能被旧版本 resume`),
86
+ same ? null : dsh.source === 'env' ? '去掉 DSH_BIN 让服务自动选用桌面版,或把 DSH_BIN 指向同版本' : '升级/统一两边的 DSH 版本',
87
+ );
88
+ }
89
+ if (all.length > 1) {
90
+ add(
91
+ 'dsh_candidates',
92
+ 'ok',
93
+ all.map((c) => `${SOURCE_LABEL[c.source]} ${c.version ?? '?'}${c === all[0] ? '(自动选用)' : ''}`).join(';'),
94
+ );
95
+ }
96
+
97
+ const vs = dshVersionStatus(version);
98
+ add(
99
+ 'dsh_version',
100
+ vs.status,
101
+ `${vs.level}: ${vs.note}`,
102
+ vs.status === 'ok'
103
+ ? null
104
+ : vs.level === 'unverified'
105
+ ? '对照 docs/zh/compatibility.md;升级后跑一遍 npm test,必要时 npm run test:all'
106
+ : '升级到 DSH 0.1.7-rc.1 及以上',
107
+ );
108
+ } catch (e) {
109
+ add('dsh_bin', 'fail', e.message, '安装 DeepSeek Harness 桌面版(自动识别),或设置 DSH_BIN 指向 @deepseek-ai/dsh/lib/bin.js');
110
+ }
111
+
112
+ try {
113
+ const p = inspectProfile();
114
+ add(
115
+ 'profile',
116
+ p.exists ? (p.drifted ? 'warn' : 'ok') : 'warn',
117
+ `${p.dir} exists=${p.exists} managed=${p.managed} drifted=${p.drifted} routes=${p.routes.join(',') || 'none'}`,
118
+ p.exists ? null : '让服务首启自动创建,或运行 dsh-any-agent-setup',
119
+ );
120
+ } catch (e) {
121
+ add('profile', 'fail', e.message);
122
+ }
123
+
124
+ try {
125
+ const raw = JSON.parse(readFileSync(STATE_FILE, 'utf8'));
126
+ add(
127
+ 'state',
128
+ raw.version === 1 ? 'ok' : 'warn',
129
+ `${STATE_FILE} version=${raw.version} conversations=${raw.conversations?.length ?? 0}`,
130
+ );
131
+ } catch (e) {
132
+ add('state', 'warn', `${STATE_FILE}: ${e.message}`, '首次启动会自动创建');
133
+ }
134
+
135
+ for (const [id, dir] of [
136
+ ['runs_dir', RUNS_DIR],
137
+ ['reviews_dir', REVIEWS_DIR],
138
+ ['locks_dir', LOCKS_DIR],
139
+ ]) {
140
+ try {
141
+ mkdirSync(dir, { recursive: true });
142
+ const probe = join(dir, `.doctor-${process.pid}`);
143
+ writeFileSync(probe, '');
144
+ rmSync(probe, { force: true });
145
+ add(id, 'ok', dir);
146
+ } catch (e) {
147
+ add(id, 'fail', `${dir}: ${e.message}`, '检查权限,或用对应环境变量指向可写目录');
148
+ }
149
+ }
150
+
151
+ try {
152
+ const orphans = listOrphans();
153
+ add(
154
+ 'orphan_locks',
155
+ orphans.length ? 'warn' : 'ok',
156
+ orphans.length ? `${orphans.length} 个孤儿锁持有者(可 dsh_takeover)` : '没有孤儿锁',
157
+ );
158
+ } catch (e) {
159
+ add('orphan_locks', 'warn', e.message);
160
+ }
161
+
162
+ const all = [...hub.conversations.values()];
163
+ const alive = all.filter((c) => c.proc?.alive);
164
+ // ★ 共享进程下多个会话共用一个 PID:必须按 PID 去重,否则同一份 RSS 会被按会话
165
+ // 重复累加,"RSS 合计"就会凭空翻几倍,把这行诊断变成误导。
166
+ const livePids = new Set(alive.map((c) => c.proc?.pid).filter(Boolean));
167
+ let totalRss = 0;
168
+ for (const pid of livePids) totalRss += rssMb(pid) ?? 0;
169
+ add(
170
+ 'sessions',
171
+ 'ok',
172
+ `共 ${all.length} 个会话,活进程 ${livePids.size} 个` +
173
+ (SHARED_PROCESS ? `(共享宿主 ${hub.hosts?.size ?? 0} 个)` : '(一会话一进程)') +
174
+ `,RSS 合计 ${totalRss}MB`,
175
+ );
176
+ add(
177
+ 'config',
178
+ 'ok',
179
+ `profile=${DSH_PROFILE} team=${TEAM_ACTIVE ? 'on' : 'off'} permission=${DEFAULT_PERMISSION} ` +
180
+ `idle_ttl=${IDLE_TTL_MS}ms max_live=${MAX_LIVE} max_rss=${MAX_RSS_MB}MB shared=${SHARED_PROCESS}`,
181
+ );
182
+ add(
183
+ 'team',
184
+ TEAM_ACTIVE ? 'ok' : TEAM_MODE ? 'warn' : 'ok',
185
+ TEAM_ACTIVE
186
+ ? `on(profile=${DSH_PROFILE})`
187
+ : TEAM_MODE
188
+ ? 'requested but unavailable:DSH < 0.1.7-0,或该 profile 不含 team 包'
189
+ : 'off(opt-in:dsh-any-agent team on)',
190
+ TEAM_ACTIVE ? null : TEAM_MODE ? '检查 DSH 版本 / profile,或 dsh-any-agent team off' : null,
191
+ );
192
+
193
+ const ok = !checks.some((c) => c.status === 'fail');
194
+ const lines = checks.map(
195
+ (c) =>
196
+ `${c.status === 'ok' ? '✓' : c.status === 'warn' ? '⚠' : '✗'} ${c.id}: ${c.detail}` +
197
+ (c.fix ? ` → ${c.fix}` : ''),
198
+ );
199
+ return {
200
+ structured: { ok, verdict: ok ? 'ok' : 'fail', checks },
201
+ text: `${ok ? '体检通过' : '体检发现失败项'}\n${lines.join('\n')}`,
202
+ };
203
+ }