jishushell 0.0.1 → 0.4.2-beta2

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.
Files changed (139) hide show
  1. package/INSTALL-NOTICE +41 -0
  2. package/LICENSE +202 -0
  3. package/README.md +36 -0
  4. package/THIRD-PARTY-NOTICES +387 -0
  5. package/dist/auth.d.ts +6 -0
  6. package/dist/auth.js +88 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +290 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +24 -0
  12. package/dist/config.js +226 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/constants.d.ts +3 -0
  15. package/dist/constants.js +15 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/control.d.ts +44 -0
  18. package/dist/control.js +1359 -0
  19. package/dist/control.js.map +1 -0
  20. package/dist/crypto-shim.d.ts +1 -0
  21. package/dist/crypto-shim.js +2 -0
  22. package/dist/crypto-shim.js.map +1 -0
  23. package/dist/doctor.d.ts +46 -0
  24. package/dist/doctor.js +937 -0
  25. package/dist/doctor.js.map +1 -0
  26. package/dist/install.d.ts +27 -0
  27. package/dist/install.js +570 -0
  28. package/dist/install.js.map +1 -0
  29. package/dist/routes/auth.d.ts +4 -0
  30. package/dist/routes/auth.js +151 -0
  31. package/dist/routes/auth.js.map +1 -0
  32. package/dist/routes/instances.d.ts +2 -0
  33. package/dist/routes/instances.js +1303 -0
  34. package/dist/routes/instances.js.map +1 -0
  35. package/dist/routes/setup.d.ts +2 -0
  36. package/dist/routes/setup.js +139 -0
  37. package/dist/routes/setup.js.map +1 -0
  38. package/dist/routes/system.d.ts +2 -0
  39. package/dist/routes/system.js +102 -0
  40. package/dist/routes/system.js.map +1 -0
  41. package/dist/server.d.ts +6 -0
  42. package/dist/server.js +392 -0
  43. package/dist/server.js.map +1 -0
  44. package/dist/services/instance-manager.d.ts +67 -0
  45. package/dist/services/instance-manager.js +1319 -0
  46. package/dist/services/instance-manager.js.map +1 -0
  47. package/dist/services/llm-proxy/adapters.d.ts +3 -0
  48. package/dist/services/llm-proxy/adapters.js +309 -0
  49. package/dist/services/llm-proxy/adapters.js.map +1 -0
  50. package/dist/services/llm-proxy/circuit-breaker.d.ts +9 -0
  51. package/dist/services/llm-proxy/circuit-breaker.js +73 -0
  52. package/dist/services/llm-proxy/circuit-breaker.js.map +1 -0
  53. package/dist/services/llm-proxy/encryption.d.ts +6 -0
  54. package/dist/services/llm-proxy/encryption.js +61 -0
  55. package/dist/services/llm-proxy/encryption.js.map +1 -0
  56. package/dist/services/llm-proxy/index.d.ts +24 -0
  57. package/dist/services/llm-proxy/index.js +708 -0
  58. package/dist/services/llm-proxy/index.js.map +1 -0
  59. package/dist/services/llm-proxy/rate-limiter.d.ts +1 -0
  60. package/dist/services/llm-proxy/rate-limiter.js +39 -0
  61. package/dist/services/llm-proxy/rate-limiter.js.map +1 -0
  62. package/dist/services/llm-proxy/sse.d.ts +10 -0
  63. package/dist/services/llm-proxy/sse.js +378 -0
  64. package/dist/services/llm-proxy/sse.js.map +1 -0
  65. package/dist/services/llm-proxy/ssrf.d.ts +16 -0
  66. package/dist/services/llm-proxy/ssrf.js +185 -0
  67. package/dist/services/llm-proxy/ssrf.js.map +1 -0
  68. package/dist/services/llm-proxy/types.d.ts +52 -0
  69. package/dist/services/llm-proxy/types.js +2 -0
  70. package/dist/services/llm-proxy/types.js.map +1 -0
  71. package/dist/services/llm-proxy/usage.d.ts +12 -0
  72. package/dist/services/llm-proxy/usage.js +108 -0
  73. package/dist/services/llm-proxy/usage.js.map +1 -0
  74. package/dist/services/nomad-manager.d.ts +22 -0
  75. package/dist/services/nomad-manager.js +828 -0
  76. package/dist/services/nomad-manager.js.map +1 -0
  77. package/dist/services/plugin-installer.d.ts +22 -0
  78. package/dist/services/plugin-installer.js +102 -0
  79. package/dist/services/plugin-installer.js.map +1 -0
  80. package/dist/services/process-manager.d.ts +25 -0
  81. package/dist/services/process-manager.js +531 -0
  82. package/dist/services/process-manager.js.map +1 -0
  83. package/dist/services/setup-manager.d.ts +93 -0
  84. package/dist/services/setup-manager.js +1922 -0
  85. package/dist/services/setup-manager.js.map +1 -0
  86. package/dist/services/system-monitor.d.ts +1 -0
  87. package/dist/services/system-monitor.js +79 -0
  88. package/dist/services/system-monitor.js.map +1 -0
  89. package/dist/services/telemetry/activation.d.ts +12 -0
  90. package/dist/services/telemetry/activation.js +78 -0
  91. package/dist/services/telemetry/activation.js.map +1 -0
  92. package/dist/services/telemetry/client.d.ts +21 -0
  93. package/dist/services/telemetry/client.js +36 -0
  94. package/dist/services/telemetry/client.js.map +1 -0
  95. package/dist/services/telemetry/device-fingerprint.d.ts +18 -0
  96. package/dist/services/telemetry/device-fingerprint.js +123 -0
  97. package/dist/services/telemetry/device-fingerprint.js.map +1 -0
  98. package/dist/services/telemetry/heartbeat.d.ts +13 -0
  99. package/dist/services/telemetry/heartbeat.js +87 -0
  100. package/dist/services/telemetry/heartbeat.js.map +1 -0
  101. package/dist/services/telemetry/index.d.ts +3 -0
  102. package/dist/services/telemetry/index.js +4 -0
  103. package/dist/services/telemetry/index.js.map +1 -0
  104. package/dist/types.d.ts +51 -0
  105. package/dist/types.js +2 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/utils/safe-json.d.ts +2 -0
  108. package/dist/utils/safe-json.js +80 -0
  109. package/dist/utils/safe-json.js.map +1 -0
  110. package/dist/utils/ttl-cache.d.ts +29 -0
  111. package/dist/utils/ttl-cache.js +77 -0
  112. package/dist/utils/ttl-cache.js.map +1 -0
  113. package/install/jishu-install.sh +2920 -0
  114. package/install/jishu-uninstall.sh +811 -0
  115. package/install/post-install.sh +124 -0
  116. package/install/post-uninstall.sh +46 -0
  117. package/package.json +57 -8
  118. package/public/assets/Dashboard-Dxsq690N.js +1 -0
  119. package/public/assets/InitPassword-CslWYy8G.js +1 -0
  120. package/public/assets/InstanceDetail-DmEkMj-t.js +14 -0
  121. package/public/assets/Login-d45wtgVA.js +1 -0
  122. package/public/assets/NewInstance-Czp5-AJe.js +1 -0
  123. package/public/assets/Settings-BKMGck05.js +1 -0
  124. package/public/assets/Setup-D3rfLWjZ.js +1 -0
  125. package/public/assets/index-77Ug7feY.css +1 -0
  126. package/public/assets/index-DkDnIohs.js +16 -0
  127. package/public/assets/logo-black-theme-DywLAtFy.png +0 -0
  128. package/public/assets/logo-white-theme-DXffFAWw.png +0 -0
  129. package/public/assets/providers-lBSOjUWy.js +1 -0
  130. package/public/assets/usePolling-CqQ8hrNc.js +1 -0
  131. package/public/assets/vendor-i18n-Bvxxh8Di.js +9 -0
  132. package/public/assets/vendor-react-DONn7uBV.js +59 -0
  133. package/public/index.html +15 -0
  134. package/scripts/build-image.sh +55 -0
  135. package/scripts/run.sh +310 -0
  136. package/scripts/setup-pi.sh +80 -0
  137. package/scripts/start-feishu1.js +46 -0
  138. package/index.js +0 -0
  139. package/jishushell-0.0.1.tgz +0 -0
package/dist/doctor.js ADDED
@@ -0,0 +1,937 @@
1
+ /**
2
+ * JishuShell Doctor — 环境诊断与自动修复
3
+ *
4
+ * 用法:
5
+ * jishushell doctor 检查所有组件状态
6
+ * jishushell doctor --fix 检查并尝试自动修复
7
+ *
8
+ * Nomad 检查依据 install/jishu-install.sh 及 src/services/setup-manager.ts:
9
+ * - 二进制路径: ~/.jishushell/bin/nomad (v1.11.3)
10
+ * - 配置文件: ~/.jishushell/nomad/nomad.hcl
11
+ * - 数据目录: ~/.jishushell/nomad/data/
12
+ * - Alloc 目录: ~/.jishushell/nomad/data/alloc/
13
+ * - 日志文件: ~/.jishushell/nomad/nomad.log
14
+ * - HTTP API: localhost:4646 (ACL enabled)
15
+ * - systemd: /etc/systemd/system/nomad.service
16
+ * - macOS: ~/Library/LaunchAgents/com.jishushell.nomad.plist
17
+ */
18
+ import { execFileSync, execSync } from "child_process";
19
+ import { randomBytes } from "crypto";
20
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "fs";
21
+ import * as http from "http";
22
+ import { homedir } from "os";
23
+ import { join } from "path";
24
+ import { AUTH_FILE, INSTANCES_DIR, JISHUSHELL_HOME, PANEL_CONFIG_FILE, ensureDirs, getNomadToken, getPanelConfig, } from "./config.js";
25
+ import { loadNomadToken } from "./services/setup-manager.js";
26
+ // ── ANSI 颜色(非 TTY 时自动禁用)──────────────────────────────────────────
27
+ const isTTY = process.stdout.isTTY ?? false;
28
+ const c = {
29
+ bold: (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s,
30
+ green: (s) => isTTY ? `\x1b[32m${s}\x1b[0m` : s,
31
+ yellow: (s) => isTTY ? `\x1b[33m${s}\x1b[0m` : s,
32
+ red: (s) => isTTY ? `\x1b[31m${s}\x1b[0m` : s,
33
+ cyan: (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s,
34
+ dim: (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s,
35
+ };
36
+ function log(msg) { process.stdout.write(msg + "\n"); }
37
+ // ── 内部工具函数 ──────────────────────────────────────────────────────────────
38
+ function httpGet(url, timeoutMs = 3000) {
39
+ return new Promise((resolve, reject) => {
40
+ const req = http.get(url, { timeout: timeoutMs }, (res) => {
41
+ let body = "";
42
+ res.on("data", (d) => { body += d; });
43
+ res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
44
+ });
45
+ req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
46
+ req.on("error", reject);
47
+ });
48
+ }
49
+ function httpGetWithHeaders(url, headers, timeoutMs = 3000) {
50
+ return new Promise((resolve, reject) => {
51
+ const u = new URL(url);
52
+ const options = {
53
+ hostname: u.hostname,
54
+ port: u.port ? parseInt(u.port, 10) : 80,
55
+ path: u.pathname + u.search,
56
+ method: "GET",
57
+ headers,
58
+ timeout: timeoutMs,
59
+ };
60
+ const req = http.request(options, (res) => {
61
+ let body = "";
62
+ res.on("data", (d) => { body += d; });
63
+ res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
64
+ });
65
+ req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
66
+ req.on("error", reject);
67
+ req.end();
68
+ });
69
+ }
70
+ function commandExists(cmd) {
71
+ try {
72
+ execFileSync("which", [cmd], { stdio: "ignore", timeout: 2000 });
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ function isPortListening(port) {
80
+ try {
81
+ if (process.platform === "darwin") {
82
+ const out = execSync(`lsof -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null`, {
83
+ encoding: "utf-8", timeout: 3000,
84
+ });
85
+ return out.trim().length > 0;
86
+ }
87
+ const out = execSync(`ss -tlnp 2>/dev/null | grep ":${port} "`, {
88
+ encoding: "utf-8", timeout: 3000,
89
+ });
90
+ return out.trim().length > 0;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ // ── Nomad 路径常量(与 setup-manager.ts 保持一致)────────────────────────────
97
+ const NOMAD_BIN_DIR = join(JISHUSHELL_HOME, "bin");
98
+ const NOMAD_LOCAL_BIN = join(NOMAD_BIN_DIR, "nomad");
99
+ const NOMAD_DIR = join(JISHUSHELL_HOME, "nomad");
100
+ const NOMAD_CONFIG = join(NOMAD_DIR, "nomad.hcl");
101
+ const NOMAD_DATA_DIR = join(NOMAD_DIR, "data");
102
+ const NOMAD_ALLOC_DIR = join(NOMAD_DIR, "data", "alloc");
103
+ const NOMAD_LOG = join(NOMAD_DIR, "nomad.log");
104
+ const NOMAD_SERVICE = "/etc/systemd/system/nomad.service";
105
+ const NOMAD_MIN_VER = "1.11.3";
106
+ function nomadEnabled() {
107
+ return getPanelConfig().service_manager === "nomad";
108
+ }
109
+ // ════════════════════════════════════════════════════════════════════════════
110
+ // 类别: 系统环境
111
+ // ════════════════════════════════════════════════════════════════════════════
112
+ const checkNode = {
113
+ id: "node", label: "Node.js 版本", category: "系统环境", severity: "error",
114
+ async check() {
115
+ const version = process.version;
116
+ const major = parseInt(version.replace("v", "").split(".")[0], 10);
117
+ if (major >= 22)
118
+ return { ok: true, detail: version };
119
+ if (major >= 18)
120
+ return { ok: false, detail: `${version}(需要 ≥22)`, hint: "nvm install 22 && nvm use 22", fixable: false };
121
+ return { ok: false, detail: `${version}(版本过低,需要 ≥22)`, hint: "访问 https://nodejs.org 安装 Node.js 22+", fixable: false };
122
+ },
123
+ };
124
+ const checkDockerBin = {
125
+ id: "docker-bin", label: "Docker 已安装", category: "系统环境", severity: "error",
126
+ async check() {
127
+ if (commandExists("docker"))
128
+ return { ok: true, detail: "docker 命令可用" };
129
+ return { ok: false, detail: "未找到 docker 命令", hint: "curl -fsSL https://get.docker.com | sh", fixable: false };
130
+ },
131
+ };
132
+ const checkDockerDaemon = {
133
+ id: "docker-daemon", label: "Docker 守护进程", category: "系统环境", severity: "error",
134
+ async check() {
135
+ try {
136
+ execFileSync("docker", ["info"], { stdio: "ignore", timeout: 6000 });
137
+ const ver = execSync("docker --version 2>/dev/null", { encoding: "utf-8", timeout: 3000 }).trim();
138
+ return { ok: true, detail: ver.replace("Docker version ", "").split(",")[0] };
139
+ }
140
+ catch { }
141
+ try {
142
+ execFileSync("sudo", ["-n", "docker", "info"], { stdio: "ignore", timeout: 6000 });
143
+ return { ok: true, detail: "运行中(通过 sudo 访问 — 重新登录可激活 docker 组权限)" };
144
+ }
145
+ catch { }
146
+ return { ok: false, detail: "Docker 守护进程未运行", hint: "sudo systemctl start docker", fixable: true };
147
+ },
148
+ async fix() {
149
+ try {
150
+ execSync("sudo systemctl start docker", { stdio: "ignore", timeout: 30000 });
151
+ await new Promise((r) => setTimeout(r, 2000));
152
+ try {
153
+ execFileSync("docker", ["info"], { stdio: "ignore", timeout: 6000 });
154
+ return { ok: true, message: "Docker 守护进程已成功启动" };
155
+ }
156
+ catch { }
157
+ return { ok: true, message: "Docker 启动命令已执行(请手动验证)" };
158
+ }
159
+ catch (e) {
160
+ return { ok: false, message: `启动失败: ${e.message}` };
161
+ }
162
+ },
163
+ };
164
+ const checkDockerGroup = {
165
+ id: "docker-group", label: "Docker 用户权限", category: "系统环境", severity: "warning",
166
+ async check() {
167
+ if (!commandExists("docker"))
168
+ return { ok: true, detail: "跳过(docker 未安装)" };
169
+ try {
170
+ execFileSync("docker", ["info"], { stdio: "ignore", timeout: 4000 });
171
+ return { ok: true, detail: "无需 sudo 即可使用 Docker" };
172
+ }
173
+ catch { }
174
+ const user = process.env.USER || process.env.LOGNAME || "";
175
+ return { ok: false, detail: `${user || "当前用户"} 不在 docker 组中,需要 sudo`, hint: `sudo usermod -aG docker ${user || "$USER"} (执行后重新登录)`, fixable: true };
176
+ },
177
+ async fix() {
178
+ const user = process.env.USER || process.env.LOGNAME || "";
179
+ if (!user)
180
+ return { ok: false, message: "无法获取当前用户名(USER/LOGNAME 未设置)" };
181
+ try {
182
+ execSync(`sudo usermod -aG docker ${user}`, { stdio: "ignore", timeout: 10000 });
183
+ return { ok: true, message: `已将 ${user} 加入 docker 组。需要重新登录后生效。` };
184
+ }
185
+ catch (e) {
186
+ return { ok: false, message: `操作失败: ${e.message}` };
187
+ }
188
+ },
189
+ };
190
+ const checkDiskSpace = {
191
+ id: "disk-space", label: "磁盘空间", category: "系统环境", severity: "warning",
192
+ async check() {
193
+ try {
194
+ const raw = execSync("df -k / 2>/dev/null | awk 'NR==2{print $4, $5}'", { encoding: "utf-8", timeout: 3000 }).trim();
195
+ const parts = raw.split(/\s+/);
196
+ const availKB = parseInt(parts[0], 10);
197
+ const pct = parseInt((parts[1] || "0").replace("%", ""), 10);
198
+ const avail = availKB > 1024 * 1024 ? `${(availKB / 1024 / 1024).toFixed(1)} GB` : `${Math.round(availKB / 1024)} MB`;
199
+ if (pct >= 95)
200
+ return { ok: false, detail: `根分区已用 ${pct}%,可用 ${avail}(磁盘即将耗尽!)`, hint: "sudo journalctl --vacuum-size=100M && docker system prune -f", fixable: false };
201
+ if (pct >= 85)
202
+ return { ok: false, detail: `根分区已用 ${pct}%,可用 ${avail}(建议清理)`, hint: "docker system prune -f 或清理 /var/log", fixable: false };
203
+ return { ok: true, detail: `根分区已用 ${pct}%,可用 ${avail}` };
204
+ }
205
+ catch {
206
+ return { ok: true, detail: "无法读取磁盘信息(已跳过)" };
207
+ }
208
+ },
209
+ };
210
+ const checkMemory = {
211
+ id: "memory", label: "内存", category: "系统环境", severity: "warning",
212
+ async check() {
213
+ if (process.platform !== "linux")
214
+ return { ok: true, detail: "非 Linux 平台,已跳过" };
215
+ try {
216
+ const meminfo = readFileSync("/proc/meminfo", "utf-8");
217
+ const totalMatch = meminfo.match(/MemTotal:\s+(\d+)\s+kB/);
218
+ const availMatch = meminfo.match(/MemAvailable:\s+(\d+)\s+kB/);
219
+ if (!totalMatch || !availMatch)
220
+ return { ok: true, detail: "/proc/meminfo 格式异常,已跳过" };
221
+ const totalKB = parseInt(totalMatch[1], 10);
222
+ const availKB = parseInt(availMatch[1], 10);
223
+ const usedPct = Math.round((1 - availKB / totalKB) * 100);
224
+ const totalMB = Math.round(totalKB / 1024);
225
+ const availMB = Math.round(availKB / 1024);
226
+ if (availKB < 100 * 1024)
227
+ return { ok: false, detail: `可用内存不足(${availMB} MB / ${totalMB} MB,已用 ${usedPct}%)`, hint: "jishushell restart 或检查内存占用异常的进程", fixable: false };
228
+ return { ok: true, detail: `可用 ${availMB} MB / ${totalMB} MB(已用 ${usedPct}%)` };
229
+ }
230
+ catch {
231
+ return { ok: true, detail: "内存信息不可读(已跳过)" };
232
+ }
233
+ },
234
+ };
235
+ // ════════════════════════════════════════════════════════════════════════════
236
+ // 类别: 配置
237
+ // ════════════════════════════════════════════════════════════════════════════
238
+ const checkConfigDir = {
239
+ id: "config-dir", label: "配置目录", category: "配置", severity: "error",
240
+ async check() {
241
+ if (existsSync(JISHUSHELL_HOME) && existsSync(INSTANCES_DIR))
242
+ return { ok: true, detail: JISHUSHELL_HOME };
243
+ return { ok: false, detail: `目录不存在: ${!existsSync(JISHUSHELL_HOME) ? JISHUSHELL_HOME : INSTANCES_DIR}`, fixable: true };
244
+ },
245
+ async fix() {
246
+ try {
247
+ ensureDirs();
248
+ return { ok: true, message: `已创建目录: ${JISHUSHELL_HOME}` };
249
+ }
250
+ catch (e) {
251
+ return { ok: false, message: `创建失败: ${e.message}` };
252
+ }
253
+ },
254
+ };
255
+ const checkPanelConfig = {
256
+ id: "panel-config", label: "面板配置文件", category: "配置", severity: "warning",
257
+ async check() {
258
+ if (!existsSync(PANEL_CONFIG_FILE))
259
+ return { ok: false, detail: "panel.json 不存在", hint: "jishushell install", fixable: true };
260
+ try {
261
+ const cfg = getPanelConfig();
262
+ if (Object.keys(cfg).length === 0)
263
+ return { ok: false, detail: "panel.json 为空", hint: "jishushell install --force", fixable: true };
264
+ return { ok: true, detail: `service_manager=${cfg.service_manager ?? "process"}, port=${cfg.panel_port ?? 8090}` };
265
+ }
266
+ catch {
267
+ return { ok: false, detail: "panel.json 格式错误(JSON 解析失败)", hint: "jishushell install --force", fixable: true };
268
+ }
269
+ },
270
+ async fix() {
271
+ try {
272
+ ensureDirs();
273
+ if (!existsSync(PANEL_CONFIG_FILE)) {
274
+ writeFileSync(PANEL_CONFIG_FILE, JSON.stringify({ service_manager: "process", panel_port: 8090 }, null, 2), { mode: 0o600 });
275
+ return { ok: true, message: "已创建默认 panel.json(service_manager=process, port=8090)" };
276
+ }
277
+ return { ok: false, message: "panel.json 已存在但内容异常,请运行: jishushell install --force" };
278
+ }
279
+ catch (e) {
280
+ return { ok: false, message: `创建失败: ${e.message}` };
281
+ }
282
+ },
283
+ };
284
+ const checkJwtSecret = {
285
+ id: "jwt-secret", label: "JWT 签名密钥", category: "配置", severity: "error",
286
+ async check() {
287
+ const secretFile = join(JISHUSHELL_HOME, "jwt-secret");
288
+ if (!existsSync(secretFile))
289
+ return { ok: false, detail: "jwt-secret 文件不存在", fixable: true };
290
+ try {
291
+ const stored = readFileSync(secretFile, "utf-8").trim();
292
+ if (stored.length >= 32)
293
+ return { ok: true, detail: "密钥文件存在且有效" };
294
+ return { ok: false, detail: "密钥长度不足(应 ≥32 字符)", fixable: true };
295
+ }
296
+ catch (e) {
297
+ return { ok: false, detail: `读取失败: ${e.message}`, fixable: true };
298
+ }
299
+ },
300
+ async fix() {
301
+ const secretFile = join(JISHUSHELL_HOME, "jwt-secret");
302
+ try {
303
+ ensureDirs();
304
+ try {
305
+ unlinkSync(secretFile);
306
+ }
307
+ catch { }
308
+ writeFileSync(secretFile, randomBytes(48).toString("base64url"), { mode: 0o600 });
309
+ return { ok: true, message: "已重新生成 JWT 密钥(已有登录会话将失效,需重新登录)" };
310
+ }
311
+ catch (e) {
312
+ return { ok: false, message: `生成失败: ${e.message}` };
313
+ }
314
+ },
315
+ };
316
+ const checkAuth = {
317
+ id: "auth", label: "管理员密码", category: "配置", severity: "warning",
318
+ async check() {
319
+ if (!existsSync(AUTH_FILE))
320
+ return { ok: false, detail: "admin 密码未设置", hint: "jishushell onboard", fixable: false };
321
+ try {
322
+ const auth = JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
323
+ if (auth.password_hash)
324
+ return { ok: true, detail: "管理员密码已配置" };
325
+ return { ok: false, detail: "auth.json 无效(缺少 password_hash)", hint: "jishushell onboard", fixable: false };
326
+ }
327
+ catch {
328
+ return { ok: false, detail: "auth.json 格式错误", hint: "jishushell onboard", fixable: false };
329
+ }
330
+ },
331
+ };
332
+ const checkFilePermissions = {
333
+ id: "file-permissions", label: "配置文件权限", category: "配置", severity: "warning",
334
+ async check() {
335
+ if (process.platform === "win32")
336
+ return { ok: true, detail: "Windows 平台跳过权限检查" };
337
+ const targets = [
338
+ { path: join(JISHUSHELL_HOME, "jwt-secret"), label: "jwt-secret" },
339
+ { path: AUTH_FILE, label: "auth.json" },
340
+ { path: join(JISHUSHELL_HOME, "encryption-key"), label: "encryption-key" },
341
+ ];
342
+ const bad = [];
343
+ const extras = [];
344
+ for (const t of targets) {
345
+ if (!existsSync(t.path))
346
+ continue;
347
+ try {
348
+ const modeRaw = process.platform === "darwin"
349
+ ? execSync(`stat -f "%p" "${t.path}" 2>/dev/null`, { encoding: "utf-8", timeout: 2000 }).trim()
350
+ : execSync(`stat -c "%a" "${t.path}" 2>/dev/null`, { encoding: "utf-8", timeout: 2000 }).trim();
351
+ const modeStr = modeRaw.slice(-3);
352
+ const tooOpen = !modeStr.endsWith("00");
353
+ const icon = tooOpen ? c.yellow("!") : c.green("✓");
354
+ extras.push(c.dim(` ├─ ${icon} ${t.label.padEnd(18)} ${modeStr}`));
355
+ if (tooOpen)
356
+ bad.push(`${t.label}(${modeStr})`);
357
+ }
358
+ catch {
359
+ extras.push(c.dim(` ├─ ? ${t.label.padEnd(18)} 读取失败`));
360
+ }
361
+ }
362
+ if (extras.length > 0)
363
+ extras[extras.length - 1] = extras[extras.length - 1].replace("├─", "└─");
364
+ if (bad.length > 0) {
365
+ const badPaths = targets.filter((t) => bad.some((b) => b.startsWith(t.label))).map((t) => `"${t.path}"`).join(" ");
366
+ return { ok: false, detail: `文件权限过宽: ${bad.join(", ")}`, hint: `chmod 600 ${badPaths}`, fixable: true, extras };
367
+ }
368
+ return { ok: true, detail: "敏感文件权限正常(600)", extras: extras.length > 0 ? extras : undefined };
369
+ },
370
+ async fix() {
371
+ const targets = [join(JISHUSHELL_HOME, "jwt-secret"), AUTH_FILE, join(JISHUSHELL_HOME, "encryption-key")];
372
+ const fixed = [];
373
+ for (const t of targets) {
374
+ if (!existsSync(t))
375
+ continue;
376
+ try {
377
+ execSync(`chmod 600 "${t}"`, { stdio: "ignore", timeout: 3000 });
378
+ fixed.push(t.split("/").pop());
379
+ }
380
+ catch { }
381
+ }
382
+ return fixed.length > 0 ? { ok: true, message: `已修复权限: ${fixed.join(", ")}` } : { ok: false, message: "无可修复文件或操作失败" };
383
+ },
384
+ };
385
+ // ════════════════════════════════════════════════════════════════════════════
386
+ // 类别: 服务状态
387
+ // ════════════════════════════════════════════════════════════════════════════
388
+ const checkPanel = {
389
+ id: "panel", label: "面板服务", category: "服务状态", severity: "warning",
390
+ async check() {
391
+ const cfg = getPanelConfig();
392
+ const port = parseInt(String(cfg.panel_port ?? ""), 10) || 8090;
393
+ try {
394
+ const res = await httpGet(`http://localhost:${port}/api/auth/status`, 3000);
395
+ if (res.status > 0) {
396
+ try {
397
+ const body = JSON.parse(res.body);
398
+ const initialized = body.initialized ?? body.setup_complete ?? false;
399
+ return { ok: true, detail: initialized ? `运行中,端口 :${port}(已完成初始配置)` : `运行中,端口 :${port}(尚未完成初始密码设置,请运行 jishushell onboard)` };
400
+ }
401
+ catch {
402
+ return { ok: true, detail: `运行中,端口 :${port}(HTTP ${res.status})` };
403
+ }
404
+ }
405
+ }
406
+ catch { }
407
+ return { ok: false, detail: `端口 ${port} 无响应`, hint: "jishushell start", fixable: true };
408
+ },
409
+ async fix() {
410
+ try {
411
+ execFileSync(process.execPath, [process.argv[1], "start"], { stdio: "ignore", timeout: 15000 });
412
+ return { ok: true, message: "面板服务启动命令已执行" };
413
+ }
414
+ catch (e) {
415
+ return { ok: false, message: `启动失败: ${e.message}` };
416
+ }
417
+ },
418
+ };
419
+ const checkSystemdEnabled = {
420
+ id: "systemd-enabled", label: "面板开机自启", category: "服务状态", severity: "warning",
421
+ async check() {
422
+ if (process.platform === "darwin")
423
+ return { ok: true, detail: "macOS 平台,已跳过" };
424
+ try {
425
+ execSync("systemctl --version 2>/dev/null", { stdio: "ignore", timeout: 2000 });
426
+ }
427
+ catch {
428
+ return { ok: true, detail: "非 systemd 系统,已跳过" };
429
+ }
430
+ try {
431
+ const status = execSync("systemctl is-enabled jishushell 2>/dev/null", { encoding: "utf-8", timeout: 3000 }).trim();
432
+ if (status === "enabled")
433
+ return { ok: true, detail: "jishushell.service 已启用(开机自启)" };
434
+ if (status === "static" || status === "alias")
435
+ return { ok: true, detail: `jishushell.service: ${status}` };
436
+ return { ok: false, detail: `jishushell.service: ${status}(未开机自启)`, hint: "sudo systemctl enable jishushell", fixable: true };
437
+ }
438
+ catch {
439
+ return { ok: false, detail: "jishushell.service 未注册", hint: "jishushell install", fixable: false };
440
+ }
441
+ },
442
+ async fix() {
443
+ try {
444
+ execSync("sudo systemctl enable jishushell", { stdio: "ignore", timeout: 10000 });
445
+ return { ok: true, message: "已启用 jishushell.service 开机自启" };
446
+ }
447
+ catch (e) {
448
+ return { ok: false, message: `启用失败: ${e.message}` };
449
+ }
450
+ },
451
+ };
452
+ // ════════════════════════════════════════════════════════════════════════════
453
+ // 类别: Nomad
454
+ // ════════════════════════════════════════════════════════════════════════════
455
+ /**
456
+ * 检查 Nomad 二进制(~/.jishushell/bin/nomad)
457
+ * 依据: install/jishu-install.sh _install_nomad_binary()
458
+ * 安装路径: JISHUSHELL_BIN_DIR=${JISHUSHELL_HOME}/bin/nomad
459
+ * 要求版本: NOMAD_VERSION=1.11.3
460
+ */
461
+ const checkNomadBin = {
462
+ id: "nomad-bin", label: "Nomad 二进制", category: "Nomad", severity: "error",
463
+ async check() {
464
+ if (!nomadEnabled())
465
+ return { ok: true, detail: "未使用 Nomad(已跳过)" };
466
+ // 优先检查本地安装路径
467
+ const binPath = existsSync(NOMAD_LOCAL_BIN) ? NOMAD_LOCAL_BIN : "nomad";
468
+ try {
469
+ const versionOut = execFileSync(binPath, ["version"], { encoding: "utf-8", timeout: 5000 }).trim();
470
+ const match = versionOut.match(/(\d+\.\d+\.\d+)/);
471
+ const version = match ? match[1] : versionOut.split("\n")[0];
472
+ const inPath = (process.env.PATH || "").split(":").includes(NOMAD_BIN_DIR);
473
+ const pathMark = inPath ? c.green("✓ 在 PATH 中") : c.yellow("! 不在 PATH(需 source ~/.bashrc 或重新登录)");
474
+ const extras = [
475
+ c.dim(` ├─ 路径: ${binPath === "nomad" ? "(系统 PATH)" : NOMAD_LOCAL_BIN}`),
476
+ c.dim(` └─ ${NOMAD_BIN_DIR} → `) + pathMark,
477
+ ];
478
+ // 版本检查
479
+ const [maj, min, pat] = version.split(".").map(Number);
480
+ const [rMaj, rMin, rPat] = NOMAD_MIN_VER.split(".").map(Number);
481
+ const vOk = maj > rMaj || (maj === rMaj && (min > rMin || (min === rMin && pat >= rPat)));
482
+ if (!vOk) {
483
+ return { ok: false, detail: `v${version}(需要 ≥${NOMAD_MIN_VER},请升级)`, hint: "jishushell install --force", fixable: false, extras };
484
+ }
485
+ return { ok: true, detail: `v${version}`, extras };
486
+ }
487
+ catch {
488
+ if (!existsSync(NOMAD_LOCAL_BIN)) {
489
+ return { ok: false, detail: `${NOMAD_LOCAL_BIN} 不存在`, hint: "jishushell install(将下载 Nomad 二进制到 ~/.jishushell/bin/)", fixable: false };
490
+ }
491
+ return { ok: false, detail: `${NOMAD_LOCAL_BIN} 不可执行或架构不兼容`, hint: `chmod 755 ${NOMAD_LOCAL_BIN}`, fixable: true };
492
+ }
493
+ },
494
+ async fix() {
495
+ if (!existsSync(NOMAD_LOCAL_BIN))
496
+ return { ok: false, message: "Nomad 二进制不存在,请运行: jishushell install" };
497
+ try {
498
+ execSync(`chmod 755 "${NOMAD_LOCAL_BIN}"`, { stdio: "ignore", timeout: 3000 });
499
+ return { ok: true, message: `已修复 Nomad 二进制可执行权限: ${NOMAD_LOCAL_BIN}` };
500
+ }
501
+ catch (e) {
502
+ return { ok: false, message: `chmod 失败: ${e.message}` };
503
+ }
504
+ },
505
+ };
506
+ /**
507
+ * 检查 Nomad 配置文件与数据目录
508
+ * 依据: install/jishu-install.sh _ensure_nomad_hcl()
509
+ * nomad.hcl 关键字段: data_dir, bind_addr, server, client, acl { enabled = true }
510
+ */
511
+ const checkNomadConfig = {
512
+ id: "nomad-config", label: "Nomad 配置", category: "Nomad", severity: "error",
513
+ async check() {
514
+ if (!nomadEnabled())
515
+ return { ok: true, detail: "未使用 Nomad(已跳过)" };
516
+ const ck = (v) => v ? c.green("✓") : c.red("✗");
517
+ const extras = [
518
+ c.dim(` ├─ ${ck(existsSync(NOMAD_CONFIG))} nomad.hcl${existsSync(NOMAD_CONFIG) ? "" : "(不存在)"}`),
519
+ c.dim(` ├─ ${ck(existsSync(NOMAD_DATA_DIR))} nomad/data/${existsSync(NOMAD_DATA_DIR) ? "" : "(不存在)"}`),
520
+ c.dim(` ├─ ${ck(existsSync(NOMAD_ALLOC_DIR))} nomad/data/alloc/${existsSync(NOMAD_ALLOC_DIR) ? "" : "(不存在)"}`),
521
+ c.dim(` └─ ${existsSync(NOMAD_LOG) ? c.green("✓") : c.dim("·")} nomad.log${existsSync(NOMAD_LOG) ? "" : "(未启动过)"}`),
522
+ ];
523
+ if (!existsSync(NOMAD_CONFIG)) {
524
+ return { ok: false, detail: "nomad.hcl 不存在,Nomad 无法启动", hint: "jishushell install", fixable: true, extras };
525
+ }
526
+ if (!existsSync(NOMAD_DATA_DIR) || !existsSync(NOMAD_ALLOC_DIR)) {
527
+ return { ok: false, detail: "数据目录不完整", fixable: true, extras };
528
+ }
529
+ // 校验 ACL 配置
530
+ try {
531
+ const content = readFileSync(NOMAD_CONFIG, "utf-8");
532
+ if (!content.includes("acl") || !content.includes("enabled = true")) {
533
+ return { ok: false, detail: "nomad.hcl 缺少 ACL 配置(acl { enabled = true })", hint: "jishushell install --force", fixable: false, extras };
534
+ }
535
+ }
536
+ catch { }
537
+ return { ok: true, detail: "nomad.hcl 及数据目录正常", extras };
538
+ },
539
+ async fix() {
540
+ try {
541
+ mkdirSync(NOMAD_DATA_DIR, { recursive: true });
542
+ mkdirSync(NOMAD_ALLOC_DIR, { recursive: true });
543
+ if (!existsSync(NOMAD_CONFIG)) {
544
+ // 模板与 install/jishu-install.sh _ensure_nomad_hcl() 保持一致
545
+ const hcl = `data_dir = "${NOMAD_DATA_DIR}"
546
+
547
+ bind_addr = "127.0.0.1"
548
+
549
+ leave_on_terminate = true
550
+
551
+ advertise {
552
+ http = "127.0.0.1"
553
+ rpc = "127.0.0.1"
554
+ serf = "127.0.0.1"
555
+ }
556
+
557
+ server {
558
+ enabled = true
559
+ bootstrap_expect = 1
560
+ }
561
+
562
+ client {
563
+ enabled = true
564
+ servers = ["127.0.0.1:4647"]
565
+ alloc_dir = "${NOMAD_ALLOC_DIR}"
566
+
567
+ drain_on_shutdown {
568
+ deadline = "30s"
569
+ force = true
570
+ ignore_system_jobs = true
571
+ }
572
+ }
573
+
574
+ plugin "docker" {
575
+ config {
576
+ disable_log_collection = true
577
+ volumes {
578
+ enabled = true
579
+ }
580
+ }
581
+ }
582
+
583
+ acl {
584
+ enabled = true
585
+ }
586
+ `;
587
+ writeFileSync(NOMAD_CONFIG, hcl, { mode: 0o600 });
588
+ return { ok: true, message: `已创建 nomad.hcl 及数据目录` };
589
+ }
590
+ return { ok: true, message: "已创建缺失的数据目录" };
591
+ }
592
+ catch (e) {
593
+ return { ok: false, message: `创建失败: ${e.message}` };
594
+ }
595
+ },
596
+ };
597
+ /**
598
+ * 检查 Nomad Agent 运行状态(端口 4646 + HTTP API + ACL Token)
599
+ * 依据: install/jishu-install.sh start_nomad() / install_nomad_systemd()
600
+ * 启动命令: sudo nohup nomad agent -config=~/.jishushell/nomad/nomad.hcl > nomad.log 2>&1 &
601
+ */
602
+ const checkNomadAgent = {
603
+ id: "nomad-agent", label: "Nomad Agent", category: "Nomad", severity: "error",
604
+ async check() {
605
+ if (!nomadEnabled())
606
+ return { ok: true, detail: "未使用 Nomad(已跳过)" };
607
+ if (!isPortListening(4646)) {
608
+ return { ok: false, detail: "端口 4646 未监听(Agent 未运行)", hint: "jishushell doctor --fix 或 sudo systemctl start nomad", fixable: true };
609
+ }
610
+ loadNomadToken();
611
+ const nomadAddr = process.env.NOMAD_ADDR || "http://127.0.0.1:4646";
612
+ const token = getNomadToken();
613
+ try {
614
+ const result = token
615
+ ? await httpGetWithHeaders(`${nomadAddr}/v1/agent/self`, { "X-Nomad-Token": token }, 3000)
616
+ : await httpGet(`${nomadAddr}/v1/agent/self`, 3000);
617
+ if (result.status === 200) {
618
+ try {
619
+ const body = JSON.parse(result.body);
620
+ const nodeStatus = body?.stats?.client?.node_status ?? "ready";
621
+ const peers = body?.stats?.nomad?.peers ?? "?";
622
+ const extras = [
623
+ c.dim(` ├─ 节点状态: ${nodeStatus}`),
624
+ c.dim(` └─ Raft peers: ${peers}`),
625
+ ];
626
+ return { ok: true, detail: "运行中,ACL Token 有效", extras };
627
+ }
628
+ catch {
629
+ return { ok: true, detail: "运行中,ACL Token 有效" };
630
+ }
631
+ }
632
+ if (result.status === 403) {
633
+ return { ok: false, detail: "运行中,但 ACL Token 无效或已过期", hint: "在 panel.json 中更新 nomad_token 字段", fixable: false };
634
+ }
635
+ return { ok: true, detail: `运行中(HTTP ${result.status})` };
636
+ }
637
+ catch {
638
+ return { ok: true, detail: "运行中(验证 ACL Token 超时,已忽略)" };
639
+ }
640
+ },
641
+ async fix() {
642
+ // 1. 尝试 systemctl
643
+ try {
644
+ execSync("sudo systemctl start nomad", { stdio: "ignore", timeout: 15000 });
645
+ await new Promise((r) => setTimeout(r, 3000));
646
+ if (isPortListening(4646))
647
+ return { ok: true, message: "Nomad Agent 已通过 systemctl 启动" };
648
+ }
649
+ catch { }
650
+ // 2. 直接 nohup(与 install/jishu-install.sh start_nomad() 保持一致)
651
+ if (existsSync(NOMAD_LOCAL_BIN) && existsSync(NOMAD_CONFIG)) {
652
+ try {
653
+ execSync(`sudo nohup "${NOMAD_LOCAL_BIN}" agent -config="${NOMAD_CONFIG}" > "${NOMAD_LOG}" 2>&1 &`, { stdio: "ignore", timeout: 5000 });
654
+ await new Promise((r) => setTimeout(r, 5000));
655
+ if (isPortListening(4646))
656
+ return { ok: true, message: "Nomad Agent 已启动(nohup 模式)" };
657
+ return { ok: false, message: `启动命令已执行,但端口 4646 尚未就绪。查看日志: ${NOMAD_LOG}` };
658
+ }
659
+ catch (e) {
660
+ return { ok: false, message: `nohup 启动失败: ${e.message}` };
661
+ }
662
+ }
663
+ return { ok: false, message: "Nomad 未安装或配置缺失,请运行: jishushell install" };
664
+ },
665
+ };
666
+ /**
667
+ * 检查 nomad.service 是否注册并设为开机自启
668
+ * 依据: install/jishu-install.sh install_nomad_systemd() / _install_nomad_launchd()
669
+ * Linux 服务文件: /etc/systemd/system/nomad.service
670
+ * macOS plist: ~/Library/LaunchAgents/com.jishushell.nomad.plist
671
+ */
672
+ const checkNomadSystemd = {
673
+ id: "nomad-systemd", label: "Nomad 开机自启", category: "Nomad", severity: "warning",
674
+ async check() {
675
+ if (!nomadEnabled())
676
+ return { ok: true, detail: "未使用 Nomad(已跳过)" };
677
+ if (process.platform === "darwin") {
678
+ const plist = join(homedir(), "Library/LaunchAgents/com.jishushell.nomad.plist");
679
+ if (existsSync(plist))
680
+ return { ok: true, detail: "launchd: com.jishushell.nomad 已安装" };
681
+ return { ok: false, detail: "Nomad launchd plist 不存在", hint: "jishushell install", fixable: false };
682
+ }
683
+ if (!existsSync(NOMAD_SERVICE)) {
684
+ return { ok: false, detail: "nomad.service 未注册(运行 jishushell install 可注册)", hint: "jishushell install", fixable: false };
685
+ }
686
+ try {
687
+ const status = execSync("systemctl is-enabled nomad 2>/dev/null", { encoding: "utf-8", timeout: 3000 }).trim();
688
+ if (status === "enabled")
689
+ return { ok: true, detail: "nomad.service 已启用(开机自启)" };
690
+ return { ok: false, detail: `nomad.service: ${status}(未开机自启)`, hint: "sudo systemctl enable nomad", fixable: true };
691
+ }
692
+ catch {
693
+ return { ok: false, detail: "nomad.service 存在但 is-enabled 查询失败", hint: "sudo systemctl enable nomad", fixable: true };
694
+ }
695
+ },
696
+ async fix() {
697
+ try {
698
+ execSync("sudo systemctl enable nomad", { stdio: "ignore", timeout: 10000 });
699
+ return { ok: true, message: "已启用 nomad.service 开机自启" };
700
+ }
701
+ catch (e) {
702
+ return { ok: false, message: `启用失败: ${e.message}` };
703
+ }
704
+ },
705
+ };
706
+ /**
707
+ * 列出 Nomad 集群中的 Jobs 及其运行状态
708
+ * API: GET /v1/jobs (需 X-Nomad-Token)
709
+ */
710
+ const checkNomadJobs = {
711
+ id: "nomad-jobs", label: "Nomad Jobs", category: "Nomad", severity: "warning",
712
+ async check() {
713
+ if (!nomadEnabled())
714
+ return { ok: true, detail: "未使用 Nomad(已跳过)" };
715
+ if (!isPortListening(4646))
716
+ return { ok: false, detail: "Nomad 未运行,无法获取 Jobs 列表", fixable: false };
717
+ const nomadAddr = process.env.NOMAD_ADDR || "http://127.0.0.1:4646";
718
+ const token = getNomadToken();
719
+ try {
720
+ const res = token
721
+ ? await httpGetWithHeaders(`${nomadAddr}/v1/jobs`, { "X-Nomad-Token": token }, 5000)
722
+ : await httpGet(`${nomadAddr}/v1/jobs`, 5000);
723
+ if (res.status === 403)
724
+ return { ok: false, detail: "ACL Token 无效,无法列出 Jobs", hint: "在 panel.json 中更新 nomad_token", fixable: false };
725
+ if (res.status !== 200)
726
+ return { ok: false, detail: `Nomad API 返回 HTTP ${res.status}`, fixable: false };
727
+ const jobs = JSON.parse(res.body);
728
+ if (jobs.length === 0)
729
+ return { ok: true, detail: "暂无 Job(集群为空)" };
730
+ const unhealthy = jobs.filter((j) => j.Status !== "running");
731
+ const extras = jobs.map((j, idx) => {
732
+ const branch = idx === jobs.length - 1 ? "└─" : "├─";
733
+ const icon = j.Status === "running" ? c.green("✓") : c.yellow("!");
734
+ const status = j.Status === "running" ? c.green(j.Status) : c.yellow(j.Status);
735
+ return c.dim(` ${branch} ${icon} ${j.ID.padEnd(26)}`) + " " + status + c.dim(` [${j.Type}]`);
736
+ });
737
+ if (unhealthy.length > 0)
738
+ return { ok: false, detail: `${jobs.length} 个 job,${unhealthy.length} 个非 running 状态`, extras };
739
+ return { ok: true, detail: `${jobs.length} 个 job,全部 running`, extras };
740
+ }
741
+ catch (e) {
742
+ return { ok: false, detail: `查询失败: ${e.message}`, fixable: false };
743
+ }
744
+ },
745
+ };
746
+ // ════════════════════════════════════════════════════════════════════════════
747
+ // 类别: AI 组件
748
+ // ════════════════════════════════════════════════════════════════════════════
749
+ const checkOpenclawPkg = {
750
+ id: "openclaw-pkg", label: "OpenClaw 包", category: "AI 组件", severity: "warning",
751
+ async check() {
752
+ const oclawBin = join(JISHUSHELL_HOME, "packages/openclaw/bin/openclaw");
753
+ const oclawPkg = join(JISHUSHELL_HOME, "packages/openclaw/lib/node_modules/openclaw/package.json");
754
+ if (!existsSync(oclawBin))
755
+ return { ok: false, detail: "OpenClaw 未安装", hint: "jishushell install --skip-nomad --skip-docker", fixable: false };
756
+ try {
757
+ const pkg = JSON.parse(readFileSync(oclawPkg, "utf-8"));
758
+ return { ok: true, detail: `v${pkg.version}` };
759
+ }
760
+ catch {
761
+ return { ok: true, detail: "已安装(版本信息不可读)" };
762
+ }
763
+ },
764
+ };
765
+ const checkOpenclawImage = {
766
+ id: "openclaw-image", label: "OpenClaw Docker 镜像", category: "AI 组件", severity: "warning",
767
+ async check() {
768
+ const imageTag = getPanelConfig().openclaw_image || "openclaw:v1.0";
769
+ let dockerAccessible = false;
770
+ try {
771
+ execFileSync("docker", ["info"], { stdio: "ignore", timeout: 4000 });
772
+ dockerAccessible = true;
773
+ }
774
+ catch {
775
+ try {
776
+ execFileSync("sudo", ["-n", "docker", "info"], { stdio: "ignore", timeout: 4000 });
777
+ dockerAccessible = true;
778
+ }
779
+ catch { }
780
+ }
781
+ if (!dockerAccessible)
782
+ return { ok: false, detail: "Docker 不可访问,无法检查镜像", fixable: false };
783
+ const inspectOk = (useSudo) => {
784
+ const args = useSudo ? ["sudo", "-n", "docker", "image", "inspect", imageTag] : ["docker", "image", "inspect", imageTag];
785
+ try {
786
+ execFileSync(args[0], args.slice(1), { stdio: "ignore", timeout: 5000 });
787
+ return true;
788
+ }
789
+ catch {
790
+ return false;
791
+ }
792
+ };
793
+ if (inspectOk(false) || inspectOk(true))
794
+ return { ok: true, detail: imageTag };
795
+ return { ok: false, detail: `本地未找到镜像 ${imageTag}`, hint: `docker pull ${imageTag} 或 jishushell install`, fixable: false };
796
+ },
797
+ };
798
+ // ════════════════════════════════════════════════════════════════════════════
799
+ // 类别: 网络
800
+ // ════════════════════════════════════════════════════════════════════════════
801
+ const checkNetwork = {
802
+ id: "network", label: "网络连通性", category: "网络", severity: "warning",
803
+ async check() {
804
+ for (const ip of ["1.1.1.1", "8.8.8.8"]) {
805
+ try {
806
+ execSync(`ping -c 1 -W 2 ${ip} 2>/dev/null`, { stdio: "ignore", timeout: 5000 });
807
+ return { ok: true, detail: `外网可达(${ip})` };
808
+ }
809
+ catch { }
810
+ }
811
+ try {
812
+ execSync("nslookup registry.npmjs.org 2>/dev/null", { stdio: "ignore", timeout: 4000 });
813
+ return { ok: true, detail: "DNS 解析正常(ICMP 可能被防火墙阻断)" };
814
+ }
815
+ catch { }
816
+ return { ok: false, detail: "无法连通外网(ping 及 DNS 均失败)", hint: "检查网络: ip route && cat /etc/resolv.conf", fixable: false };
817
+ },
818
+ };
819
+ // ════════════════════════════════════════════════════════════════════════════
820
+ // 所有检查项(按执行顺序)
821
+ // ════════════════════════════════════════════════════════════════════════════
822
+ export const ALL_CHECKS = [
823
+ // 系统环境
824
+ checkNode,
825
+ checkDockerBin,
826
+ checkDockerDaemon,
827
+ checkDockerGroup,
828
+ checkDiskSpace,
829
+ checkMemory,
830
+ // 配置
831
+ checkConfigDir,
832
+ checkPanelConfig,
833
+ checkJwtSecret,
834
+ checkAuth,
835
+ checkFilePermissions,
836
+ // 服务状态
837
+ checkPanel,
838
+ checkSystemdEnabled,
839
+ // Nomad
840
+ checkNomadBin,
841
+ checkNomadConfig,
842
+ checkNomadAgent,
843
+ checkNomadSystemd,
844
+ checkNomadJobs,
845
+ // AI 组件
846
+ checkOpenclawPkg,
847
+ checkOpenclawImage,
848
+ // 网络
849
+ checkNetwork,
850
+ ];
851
+ // ════════════════════════════════════════════════════════════════════════════
852
+ // 输出格式化
853
+ // ════════════════════════════════════════════════════════════════════════════
854
+ function formatRow(check, result) {
855
+ const isWarn = !result.ok && check.severity === "warning";
856
+ const icon = result.ok ? c.green("✓") : isWarn ? c.yellow("!") : c.red("✗");
857
+ const tag = result.ok ? "" : isWarn ? c.yellow(" [WARN] ") : c.red(" [ERROR]");
858
+ const label = (result.ok ? c.green : isWarn ? c.yellow : c.red)(check.label.padEnd(22));
859
+ return ` ${icon} ${label}${tag} ${c.dim(result.detail)}`;
860
+ }
861
+ // ════════════════════════════════════════════════════════════════════════════
862
+ // 主入口
863
+ // ════════════════════════════════════════════════════════════════════════════
864
+ /**
865
+ * 运行所有诊断检查,返回是否全部 error 级别检查通过。
866
+ */
867
+ export async function runDoctorChecks(opts = {}) {
868
+ const { fix = false } = opts;
869
+ log("");
870
+ log(c.bold(c.cyan(" JishuShell Doctor")));
871
+ log(c.dim(" ────────────────────────────────────────────────"));
872
+ if (fix) {
873
+ log(c.dim(" 模式: ") + c.bold("检查 + 自动修复"));
874
+ }
875
+ else {
876
+ log(c.dim(" 运行 ") + c.bold("jishushell doctor --fix") + c.dim(" 可尝试自动修复问题"));
877
+ }
878
+ log("");
879
+ const categories = [...new Set(ALL_CHECKS.map((ch) => ch.category))];
880
+ const results = new Map();
881
+ for (const category of categories) {
882
+ log(c.bold(` ▸ ${category}`));
883
+ for (const check of ALL_CHECKS.filter((ch) => ch.category === category)) {
884
+ let result = await check.check();
885
+ results.set(check.id, result);
886
+ if (!result.ok && fix && result.fixable && check.fix) {
887
+ log(formatRow(check, result));
888
+ if (result.extras) {
889
+ for (const line of result.extras)
890
+ log(line);
891
+ }
892
+ process.stdout.write(c.dim(` ↻ 修复中: ${check.label}…`));
893
+ const fixResult = await check.fix();
894
+ process.stdout.write("\r" + " ".repeat(55) + "\r");
895
+ result = await check.check();
896
+ results.set(check.id, result);
897
+ log(formatRow(check, result));
898
+ if (result.extras) {
899
+ for (const line of result.extras)
900
+ log(line);
901
+ }
902
+ const fixIcon = fixResult.ok ? c.green("✓") : c.red("✗");
903
+ log(c.dim(` └─ ${fixIcon} ${fixResult.message}`));
904
+ }
905
+ else {
906
+ log(formatRow(check, result));
907
+ if (result.extras) {
908
+ for (const line of result.extras)
909
+ log(line);
910
+ }
911
+ if (!result.ok && result.hint) {
912
+ log(c.dim(` └─ 建议: ${result.hint}`));
913
+ }
914
+ }
915
+ }
916
+ log("");
917
+ }
918
+ // 汇总
919
+ const errors = ALL_CHECKS.filter((ch) => !results.get(ch.id).ok && ch.severity === "error");
920
+ const warnings = ALL_CHECKS.filter((ch) => !results.get(ch.id).ok && ch.severity === "warning");
921
+ log(c.dim(" ────────────────────────────────────────────────"));
922
+ if (errors.length === 0 && warnings.length === 0) {
923
+ log(c.bold(c.green(" ✓ 全部检查通过!")));
924
+ }
925
+ else {
926
+ if (errors.length > 0)
927
+ log(c.bold(c.red(` ✗ ${errors.length} 个错误(Error)`)));
928
+ if (warnings.length > 0)
929
+ log(c.bold(c.yellow(` ! ${warnings.length} 个警告(Warning)`)));
930
+ if (fix && (errors.length > 0 || warnings.length > 0)) {
931
+ log(c.dim(" 部分问题需要手动处理,请参考上方的「建议」提示。"));
932
+ }
933
+ }
934
+ log("");
935
+ return errors.length === 0;
936
+ }
937
+ //# sourceMappingURL=doctor.js.map