openclawsetup 2.5.3 → 2.8.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/bin/cli.mjs CHANGED
@@ -11,8 +11,19 @@
11
11
  */
12
12
 
13
13
  import { exec, execSync, spawnSync } from 'child_process';
14
- import { existsSync, accessSync, constants as fsConstants, rmSync, readFileSync } from 'fs';
15
- import { homedir, platform } from 'os';
14
+ import {
15
+ existsSync,
16
+ accessSync,
17
+ constants as fsConstants,
18
+ rmSync,
19
+ readFileSync,
20
+ realpathSync,
21
+ renameSync,
22
+ mkdirSync,
23
+ writeFileSync,
24
+ } from 'fs';
25
+ import { createServer } from 'net';
26
+ import { homedir, platform, arch, release, hostname } from 'os';
16
27
  import { join } from 'path';
17
28
  import { createInterface } from 'readline';
18
29
 
@@ -70,40 +81,530 @@ const log = {
70
81
  guide: (msg) => console.log(colors.bgYellow(` 📖 ${msg} `)),
71
82
  };
72
83
 
84
+ const DEFAULT_GATEWAY_PORT = 18789;
85
+ const STRONG_FIX_MAX_PASSES = 3;
86
+ const STRONG_PORT_CANDIDATES = [
87
+ 18789,
88
+ 18790,
89
+ 18791,
90
+ 18792,
91
+ 18793,
92
+ 18794,
93
+ 18795,
94
+ 18889,
95
+ 18989,
96
+ ];
97
+
98
+ const EVIDENCE_DIR_NAME = 'openclaw-evidence';
99
+
73
100
  function safeExec(cmd, options = {}) {
74
101
  try {
75
- const output = execSync(cmd, { encoding: 'utf8', stdio: 'pipe', ...options });
102
+ const output = execSync(cmd, {
103
+ encoding: 'utf8',
104
+ stdio: 'pipe',
105
+ timeout: 30000,
106
+ maxBuffer: 8 * 1024 * 1024,
107
+ ...options,
108
+ });
76
109
  return { ok: true, output: output.trim() };
77
110
  } catch (e) {
78
- return { ok: false, error: e.message, stderr: e.stderr?.toString() || '' };
111
+ return {
112
+ ok: false,
113
+ error: e.message,
114
+ stderr: e.stderr?.toString() || '',
115
+ status: Number.isInteger(e?.status) ? e.status : null,
116
+ };
79
117
  }
80
118
  }
81
119
 
82
120
  function parseArgs() {
83
121
  const args = process.argv.slice(2);
122
+ const strongFix = args.includes('--strong-fix') || args.includes('--extreme-fix');
84
123
  return {
85
124
  update: args.includes('--update'),
86
125
  reinstall: args.includes('--reinstall'),
87
126
  uninstall: args.includes('--uninstall'),
88
127
  check: args.includes('--check'),
89
128
  fix: args.includes('--fix'),
129
+ strongFix,
130
+ strong: args.includes('--strong') || strongFix,
90
131
  manual: args.includes('--manual'),
91
132
  auto: args.includes('--auto'),
92
133
  withModel: args.includes('--with-model'),
93
134
  withChannel: args.includes('--with-channel'),
135
+ optimizeToken: args.includes('--optimize-token'),
136
+ collectEvidence: args.includes('--collect-evidence') || args.includes('--evidence'),
137
+ evidenceQuick: args.includes('--evidence-quick'),
94
138
  quiet: args.includes('--quiet') || args.includes('-q'),
95
139
  help: args.includes('--help') || args.includes('-h'),
96
140
  };
97
141
  }
98
142
 
143
+ function parseSemver(version) {
144
+ if (!version || typeof version !== 'string') return null;
145
+ const core = version.trim().replace(/^v/i, '').split('-')[0];
146
+ const parts = core.split('.').map((item) => Number.parseInt(item, 10));
147
+ if (parts.length < 2 || parts.some((item) => Number.isNaN(item))) return null;
148
+ while (parts.length < 3) parts.push(0);
149
+ return parts.slice(0, 3);
150
+ }
151
+
152
+ function compareSemver(currentVersion, targetVersion) {
153
+ const current = parseSemver(currentVersion);
154
+ const target = parseSemver(targetVersion);
155
+ if (!current || !target) return 0;
156
+
157
+ for (let index = 0; index < 3; index += 1) {
158
+ if (current[index] < target[index]) return -1;
159
+ if (current[index] > target[index]) return 1;
160
+ }
161
+ return 0;
162
+ }
163
+
164
+ function timestampForFile() {
165
+ const now = new Date();
166
+ const pad = (v) => String(v).padStart(2, '0');
167
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
168
+ }
169
+
170
+ function sanitizeText(input = '') {
171
+ if (!input) return '';
172
+ const patterns = [
173
+ /(token\s*[=:]\s*)([A-Za-z0-9._\-]{8,})/gi,
174
+ /("token"\s*:\s*")([^"]+)(")/gi,
175
+ /("api[_-]?key"\s*:\s*")([^"]+)(")/gi,
176
+ /(sk-[A-Za-z0-9]{12,})/g,
177
+ /(Bearer\s+)([A-Za-z0-9._\-]{8,})/gi,
178
+ /([?&]token=)([^&\s]+)/gi,
179
+ ];
180
+
181
+ let result = input;
182
+ for (const pattern of patterns) {
183
+ result = result.replace(pattern, (match, p1, p2, p3) => {
184
+ if (typeof p3 === 'string') {
185
+ return `${p1}<REDACTED>${p3}`;
186
+ }
187
+ return `${p1}<REDACTED>`;
188
+ });
189
+ }
190
+ return result;
191
+ }
192
+
193
+ function truncateText(input = '', limit = 20000) {
194
+ if (!input) return '';
195
+ if (input.length <= limit) return input;
196
+ return `${input.slice(0, limit)}\n\n...[TRUNCATED ${input.length - limit} chars]`;
197
+ }
198
+
199
+ function safeFileRead(path) {
200
+ try {
201
+ if (!path || !existsSync(path)) return '';
202
+ return readFileSync(path, 'utf8');
203
+ } catch {
204
+ return '';
205
+ }
206
+ }
207
+
208
+ function runCmdCapture(cmd, timeout = 20000) {
209
+ const result = safeExec(cmd, { timeout });
210
+ if (result.ok) {
211
+ return {
212
+ ok: true,
213
+ cmd,
214
+ output: sanitizeText(truncateText(result.output || '', 40000)),
215
+ };
216
+ }
217
+
218
+ return {
219
+ ok: false,
220
+ cmd,
221
+ error: sanitizeText(truncateText(result.stderr || result.error || 'unknown error', 12000)),
222
+ };
223
+ }
224
+
225
+ function detectDesktopPath() {
226
+ const home = homedir();
227
+ const candidates = [
228
+ join(home, 'Desktop'),
229
+ join(home, '桌面'),
230
+ ];
231
+
232
+ for (const path of candidates) {
233
+ if (existsSync(path)) return path;
234
+ }
235
+ return process.cwd();
236
+ }
237
+
238
+ function maskPathForUser(path) {
239
+ const home = homedir();
240
+ return path.startsWith(home) ? path.replace(home, '~') : path;
241
+ }
242
+
243
+ function gatherEnvironmentSnapshot() {
244
+ const config = getConfigInfo();
245
+ const existing = detectExistingInstall();
246
+
247
+ const snapshot = {
248
+ timestamp: new Date().toISOString(),
249
+ os: {
250
+ platform: platform(),
251
+ release: release(),
252
+ arch: arch(),
253
+ hostname: hostname(),
254
+ },
255
+ runtime: {
256
+ node: process.version,
257
+ npm: runCmdCapture('npm -v', 10000),
258
+ npx: runCmdCapture('npx -v', 10000),
259
+ },
260
+ openclaw: {
261
+ detectedInstall: existing,
262
+ configPath: config.configPath || '',
263
+ configDir: config.configDir || '',
264
+ gatewayPort: Number(config.port || DEFAULT_GATEWAY_PORT),
265
+ hasToken: Boolean(getDashboardToken(config)),
266
+ hasModelConfig: config.raw.includes('"models"') || config.raw.includes('"providers"') || config.raw.includes('"apiKey"') || config.raw.includes('"api_key"'),
267
+ },
268
+ };
269
+
270
+ return snapshot;
271
+ }
272
+
273
+ function jsonSafeParse(raw) {
274
+ try {
275
+ return JSON.parse(raw);
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ function detectConfigFormat(configPath = '') {
282
+ const lower = (configPath || '').toLowerCase();
283
+ if (lower.endsWith('.json5')) return 'json5';
284
+ return 'json';
285
+ }
286
+
287
+ function createTokenOptimizationPlan() {
288
+ return {
289
+ memory: {
290
+ enabled: true,
291
+ collections: {
292
+ conversations: {
293
+ retention_days: 14,
294
+ max_entries: 2000,
295
+ auto_summarize: true,
296
+ summarize_threshold: 25,
297
+ },
298
+ },
299
+ retrieval: {
300
+ default_limit: 4,
301
+ similarity_threshold: 0.82,
302
+ rerank: true,
303
+ },
304
+ },
305
+ };
306
+ }
307
+
308
+ function ensureObjectPath(base, pathParts) {
309
+ let cursor = base;
310
+ for (const part of pathParts) {
311
+ if (!cursor[part] || typeof cursor[part] !== 'object' || Array.isArray(cursor[part])) {
312
+ cursor[part] = {};
313
+ }
314
+ cursor = cursor[part];
315
+ }
316
+ return cursor;
317
+ }
318
+
319
+ function applyTokenOptimizationToJson(configJson) {
320
+ const root = configJson && typeof configJson === 'object' ? configJson : {};
321
+ const before = JSON.parse(JSON.stringify(root));
322
+
323
+ const memory = ensureObjectPath(root, ['memory']);
324
+ memory.enabled = true;
325
+
326
+ const collections = ensureObjectPath(memory, ['collections']);
327
+ const conversations = ensureObjectPath(collections, ['conversations']);
328
+ conversations.retention_days = 14;
329
+ conversations.max_entries = 2000;
330
+ conversations.auto_summarize = true;
331
+ conversations.summarize_threshold = 25;
332
+
333
+ const retrieval = ensureObjectPath(memory, ['retrieval']);
334
+ retrieval.default_limit = 4;
335
+ retrieval.similarity_threshold = 0.82;
336
+ retrieval.rerank = true;
337
+
338
+ return { before, after: root };
339
+ }
340
+
341
+ function applyTokenOptimizationToJson5Raw(raw) {
342
+ const addSnippet = [
343
+ '',
344
+ '// openclawsetup token optimization start',
345
+ 'memory: {',
346
+ ' enabled: true,',
347
+ ' collections: {',
348
+ ' conversations: {',
349
+ ' retention_days: 14,',
350
+ ' max_entries: 2000,',
351
+ ' auto_summarize: true,',
352
+ ' summarize_threshold: 25,',
353
+ ' },',
354
+ ' },',
355
+ ' retrieval: {',
356
+ ' default_limit: 4,',
357
+ ' similarity_threshold: 0.82,',
358
+ ' rerank: true,',
359
+ ' },',
360
+ '},',
361
+ '// openclawsetup token optimization end',
362
+ '',
363
+ ].join('\n');
364
+
365
+ if (!raw || typeof raw !== 'string') return addSnippet;
366
+ if (raw.includes('openclawsetup token optimization start')) {
367
+ return raw;
368
+ }
369
+
370
+ const trimmed = raw.trimEnd();
371
+ if (trimmed.endsWith('}')) {
372
+ const idx = raw.lastIndexOf('}');
373
+ return `${raw.slice(0, idx)}${addSnippet}\n}`;
374
+ }
375
+ return `${raw}\n${addSnippet}`;
376
+ }
377
+
378
+ function calculateTokenOptimizationDiff(beforeObj, afterObj) {
379
+ const get = (obj, path, fallback = null) => {
380
+ const keys = path.split('.');
381
+ let cursor = obj;
382
+ for (const key of keys) {
383
+ if (!cursor || typeof cursor !== 'object' || !(key in cursor)) return fallback;
384
+ cursor = cursor[key];
385
+ }
386
+ return cursor;
387
+ };
388
+
389
+ return {
390
+ conversationsRetentionDays: {
391
+ before: get(beforeObj, 'memory.collections.conversations.retention_days', '<unset>'),
392
+ after: get(afterObj, 'memory.collections.conversations.retention_days', '<unset>'),
393
+ },
394
+ conversationsMaxEntries: {
395
+ before: get(beforeObj, 'memory.collections.conversations.max_entries', '<unset>'),
396
+ after: get(afterObj, 'memory.collections.conversations.max_entries', '<unset>'),
397
+ },
398
+ summarizeThreshold: {
399
+ before: get(beforeObj, 'memory.collections.conversations.summarize_threshold', '<unset>'),
400
+ after: get(afterObj, 'memory.collections.conversations.summarize_threshold', '<unset>'),
401
+ },
402
+ retrievalLimit: {
403
+ before: get(beforeObj, 'memory.retrieval.default_limit', '<unset>'),
404
+ after: get(afterObj, 'memory.retrieval.default_limit', '<unset>'),
405
+ },
406
+ similarityThreshold: {
407
+ before: get(beforeObj, 'memory.retrieval.similarity_threshold', '<unset>'),
408
+ after: get(afterObj, 'memory.retrieval.similarity_threshold', '<unset>'),
409
+ },
410
+ };
411
+ }
412
+
413
+ async function optimizeTokenUsage(cliName = 'openclaw') {
414
+ console.log(colors.bold(colors.cyan('\n🪶 输入 Token 优化\n')));
415
+
416
+ const config = getConfigInfo();
417
+ if (!config.configPath || !existsSync(config.configPath)) {
418
+ log.warn('未找到配置文件,尝试先生成配置...');
419
+ const ensured = ensureConfigFilePresent(config, cliName);
420
+ if (!ensured.ok) {
421
+ log.error('无法生成配置文件,Token 优化中止');
422
+ return { ok: false, reason: 'config-missing' };
423
+ }
424
+ }
425
+
426
+ const freshConfig = getConfigInfo();
427
+ const raw = safeFileRead(freshConfig.configPath);
428
+ const format = detectConfigFormat(freshConfig.configPath);
429
+ const backupPath = `${freshConfig.configPath}.token-optimize.bak.${Date.now()}`;
430
+
431
+ try {
432
+ renameSync(freshConfig.configPath, backupPath);
433
+ } catch (e) {
434
+ log.error(`备份配置失败: ${e.message}`);
435
+ return { ok: false, reason: 'backup-failed' };
436
+ }
437
+
438
+ const optimizationPlan = createTokenOptimizationPlan();
439
+ let optimizationDiff = null;
440
+
441
+ try {
442
+ if (format === 'json') {
443
+ const parsed = jsonSafeParse(raw) || {};
444
+ const { before, after } = applyTokenOptimizationToJson(parsed);
445
+ optimizationDiff = calculateTokenOptimizationDiff(before, after);
446
+ writeFileSync(freshConfig.configPath, `${JSON.stringify(after, null, 2)}\n`, 'utf8');
447
+ } else {
448
+ const updatedRaw = applyTokenOptimizationToJson5Raw(raw);
449
+ writeFileSync(freshConfig.configPath, updatedRaw, 'utf8');
450
+ optimizationDiff = {
451
+ format: 'json5',
452
+ action: 'append-snippet',
453
+ };
454
+ }
455
+ } catch (e) {
456
+ writeFileSync(freshConfig.configPath, raw, 'utf8');
457
+ log.error(`写入优化配置失败,已恢复原配置: ${e.message}`);
458
+ return { ok: false, reason: 'write-failed' };
459
+ }
460
+
461
+ const restartResult = await ensureGatewayRunning(cliName, 'restart');
462
+ if (!restartResult.ok) {
463
+ log.warn('配置已更新,但 Gateway 重启失败,请手动执行 gateway restart');
464
+ }
465
+
466
+ log.success('Token 优化配置已应用');
467
+ log.hint(`原配置备份: ${maskPathForUser(backupPath)}`);
468
+ console.log(colors.cyan('优化目标: 降低长期记忆召回和历史膨胀导致的输入 token'));
469
+
470
+ if (optimizationDiff && optimizationDiff.format !== 'json5') {
471
+ console.log(colors.gray(` retention_days: ${optimizationDiff.conversationsRetentionDays.before} -> ${optimizationDiff.conversationsRetentionDays.after}`));
472
+ console.log(colors.gray(` max_entries: ${optimizationDiff.conversationsMaxEntries.before} -> ${optimizationDiff.conversationsMaxEntries.after}`));
473
+ console.log(colors.gray(` summarize_threshold: ${optimizationDiff.summarizeThreshold.before} -> ${optimizationDiff.summarizeThreshold.after}`));
474
+ console.log(colors.gray(` retrieval.default_limit: ${optimizationDiff.retrievalLimit.before} -> ${optimizationDiff.retrievalLimit.after}`));
475
+ console.log(colors.gray(` retrieval.similarity_threshold: ${optimizationDiff.similarityThreshold.before} -> ${optimizationDiff.similarityThreshold.after}`));
476
+ } else {
477
+ console.log(colors.gray(' 已为 JSON5 配置追加 token 优化片段(可手动微调)。'));
478
+ }
479
+
480
+ console.log('');
481
+ return { ok: true, backupPath, optimizationPlan, optimizationDiff };
482
+ }
483
+
484
+ async function collectEvidencePackage(options = {}) {
485
+ const quick = Boolean(options.quick);
486
+ const startedAt = Date.now();
487
+
488
+ console.log(colors.bold(colors.cyan('\n📦 生成 OpenClaw 排障证据包\n')));
489
+ log.info('正在采集环境信息、状态、日志(已自动脱敏)...');
490
+
491
+ const desktopPath = detectDesktopPath();
492
+ const stamp = timestampForFile();
493
+ const folderName = `${EVIDENCE_DIR_NAME}-${stamp}`;
494
+ const evidenceDir = join(desktopPath, folderName);
495
+ mkdirSync(evidenceDir, { recursive: true });
496
+
497
+ const snapshot = gatherEnvironmentSnapshot();
498
+ const tokenOptimizationPreview = createTokenOptimizationPlan();
499
+ const cliName = snapshot.openclaw.detectedInstall?.name || findWorkingCliName() || 'openclaw';
500
+
501
+ const captures = {
502
+ doctor: runCmdCapture(`${cliName} doctor`, quick ? 20000 : 90000),
503
+ status: runCmdCapture(`${cliName} status`, 20000),
504
+ gatewayLogs: runCmdCapture(`${cliName} gateway logs`, quick ? 20000 : 45000),
505
+ cliVersion: runCmdCapture(`${cliName} --version`, 15000),
506
+ npmListGlobalOpenclaw: runCmdCapture('npm ls -g openclaw --depth=0', 20000),
507
+ npmListGlobalClawdbot: runCmdCapture('npm ls -g clawdbot --depth=0', 20000),
508
+ };
509
+
510
+ const port = snapshot.openclaw.gatewayPort || DEFAULT_GATEWAY_PORT;
511
+ captures.portCheck = runCmdCapture(
512
+ platform() === 'win32'
513
+ ? `netstat -ano | findstr :${port}`
514
+ : `lsof -nP -iTCP:${port} -sTCP:LISTEN 2>/dev/null || netstat -tlnp 2>/dev/null | grep :${port}`,
515
+ 15000,
516
+ );
517
+ captures.health = runCmdCapture(`curl -sS --max-time 5 http://127.0.0.1:${port}/health`, 10000);
518
+
519
+ let strongCheckResult = null;
520
+ if (!quick && snapshot.openclaw.detectedInstall?.installed) {
521
+ log.hint('执行一次强力检测并附加结果...');
522
+ strongCheckResult = await runHealthCheck(cliName, false, true);
523
+ }
524
+
525
+ const configRaw = safeFileRead(snapshot.openclaw.configPath);
526
+ const safeConfigPreview = sanitizeText(truncateText(configRaw, 20000));
527
+
528
+ const summary = {
529
+ meta: {
530
+ generatedAt: new Date().toISOString(),
531
+ durationMs: Date.now() - startedAt,
532
+ quick,
533
+ evidenceVersion: '1.0.0',
534
+ },
535
+ snapshot,
536
+ tokenOptimizationPreview,
537
+ captures,
538
+ strongCheck: strongCheckResult
539
+ ? {
540
+ issuesCount: strongCheckResult.issues?.length || 0,
541
+ fixedCount: strongCheckResult.fixed?.length || 0,
542
+ issues: strongCheckResult.issues || [],
543
+ fixed: strongCheckResult.fixed || [],
544
+ }
545
+ : null,
546
+ };
547
+
548
+ writeFileSync(join(evidenceDir, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
549
+ writeFileSync(join(evidenceDir, 'config.preview.json.txt'), `${safeConfigPreview || '<empty>'}\n`, 'utf8');
550
+
551
+ const readme = [
552
+ '# OpenClaw 排障证据包',
553
+ '',
554
+ `生成时间: ${new Date().toLocaleString()}`,
555
+ `系统: ${snapshot.os.platform} ${snapshot.os.release} (${snapshot.os.arch})`,
556
+ `检测 CLI: ${cliName}`,
557
+ '',
558
+ '## 文件说明',
559
+ '- summary.json: 机器可解析的完整诊断结果(已脱敏)',
560
+ '- config.preview.json.txt: 配置文件预览(已脱敏)',
561
+ '',
562
+ '## 发给技术支持',
563
+ '请把整个证据包文件夹(或 zip)发给技术支持即可。',
564
+ ].join('\n');
565
+ writeFileSync(join(evidenceDir, 'README.txt'), `${readme}\n`, 'utf8');
566
+
567
+ let archivePath = '';
568
+ if (platform() !== 'win32') {
569
+ const tarPath = `${evidenceDir}.tar.gz`;
570
+ const tarResult = safeExec(`tar -czf "${tarPath}" -C "${desktopPath}" "${folderName}"`, { timeout: 30000 });
571
+ if (tarResult.ok && existsSync(tarPath)) {
572
+ archivePath = tarPath;
573
+ }
574
+ }
575
+
576
+ console.log('');
577
+ log.success('证据包已生成(内容已自动脱敏)');
578
+ console.log(colors.cyan(`目录: ${maskPathForUser(evidenceDir)}`));
579
+ if (archivePath) {
580
+ console.log(colors.cyan(`压缩包: ${maskPathForUser(archivePath)}`));
581
+ }
582
+ console.log(colors.yellow('请将该目录(或压缩包)直接发给技术支持。'));
583
+ console.log('');
584
+
585
+ return { evidenceDir, archivePath, summary };
586
+ }
587
+
99
588
  function showHelp() {
589
+ const pkgVersion = (() => {
590
+ try {
591
+ const pkgJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
592
+ return pkgJson.version || 'unknown';
593
+ } catch {
594
+ return 'unknown';
595
+ }
596
+ })();
597
+
100
598
  console.log(`
101
- ${colors.bold('OpenClaw 安装向导 v2.3')}
599
+ ${colors.bold(`OpenClaw 安装向导 v${pkgVersion}`)}
102
600
 
103
601
  ${colors.cyan('基础用法:')}
104
602
  npx openclawsetup 交互式菜单(已安装)/ 安装向导(未安装)
105
603
  npx openclawsetup --check 检查配置和服务状态
106
604
  npx openclawsetup --fix 检查并自动修复常见问题
605
+ npx openclawsetup --strong-fix 强力模式(多轮深度检查 + 强修复)
606
+ npx openclawsetup --optimize-token 一键优化输入 token(记忆/召回瘦身)
607
+ npx openclawsetup --collect-evidence 一键导出排障证据包(给技术支持)
107
608
  npx openclawsetup --update 更新已安装的 OpenClaw
108
609
  npx openclawsetup --reinstall 卸载后重新安装
109
610
  npx openclawsetup --uninstall 卸载 OpenClaw
@@ -116,6 +617,11 @@ ${colors.cyan('安装模式:')}
116
617
  ${colors.cyan('高级选项:')}
117
618
  --with-model 检测到模型配置时暂停自动选择
118
619
  --with-channel 检测到渠道配置时暂停自动选择
620
+ --strong 与 --check/--fix 搭配,启用强力策略
621
+ --strong-fix 直接执行强力检查修复
622
+ --optimize-token 直接执行 token 优化配置(无需进菜单)
623
+ --evidence 等价于 --collect-evidence
624
+ --evidence-quick 快速证据包(跳过深度检测)
119
625
 
120
626
  ${colors.cyan('安装后配置模型:')}
121
627
  npx openclawapi@latest preset-claude # 一键配置 Claude
@@ -204,26 +710,48 @@ function fixNpmCacheOwnership() {
204
710
  }
205
711
  }
206
712
 
713
+ function isRealGatewayCli(name) {
714
+ const result = safeExec(`${name} --version`);
715
+ if (!result.ok) return false;
716
+ if (result.output && result.output.toLowerCase().includes('openclawapi')) return false;
717
+
718
+ const whichResult = safeExec(platform() === 'win32' ? `where ${name}` : `command -v ${name}`);
719
+ if (whichResult.ok && whichResult.output) {
720
+ try {
721
+ const realPath = realpathSync(whichResult.output.trim());
722
+ if (realPath.toLowerCase().includes('openclawapi')) return false;
723
+ } catch {
724
+ // ignore
725
+ }
726
+ }
727
+ return true;
728
+ }
729
+
730
+ function findWorkingCliName() {
731
+ for (const name of ['openclaw', 'clawdbot', 'moltbot']) {
732
+ if (isRealGatewayCli(name)) return name;
733
+ }
734
+ return null;
735
+ }
736
+
207
737
  function detectExistingInstall() {
208
738
  const home = homedir();
209
739
  const openclawDir = join(home, '.openclaw');
210
740
  const clawdbotDir = join(home, '.clawdbot');
211
741
 
212
- if (existsSync(openclawDir)) {
213
- return { installed: true, configDir: openclawDir, name: 'openclaw' };
214
- }
215
- if (existsSync(clawdbotDir)) {
216
- return { installed: true, configDir: clawdbotDir, name: 'clawdbot' };
217
- }
218
-
219
- const openclawResult = safeExec('openclaw --version');
220
- if (openclawResult.ok) {
221
- return { installed: true, name: 'openclaw', version: openclawResult.output };
742
+ if (existsSync(openclawDir) || existsSync(clawdbotDir)) {
743
+ const configDir = existsSync(openclawDir) ? openclawDir : clawdbotDir;
744
+ const cliName = findWorkingCliName();
745
+ if (cliName) {
746
+ return { installed: true, configDir, name: cliName };
747
+ }
748
+ // Config dir exists but no working CLI found
749
+ return { installed: true, configDir, name: existsSync(openclawDir) ? 'openclaw' : 'clawdbot', cliMissing: true };
222
750
  }
223
751
 
224
- const clawdbotResult = safeExec('clawdbot --version');
225
- if (clawdbotResult.ok) {
226
- return { installed: true, name: 'clawdbot', version: clawdbotResult.output };
752
+ const cliName = findWorkingCliName();
753
+ if (cliName) {
754
+ return { installed: true, name: cliName };
227
755
  }
228
756
 
229
757
  return { installed: false };
@@ -268,6 +796,490 @@ function getConfigInfo() {
268
796
  return { configDir: '', configPath: '', token: '', port: 18789, bind: '', raw: '' };
269
797
  }
270
798
 
799
+ function sleep(ms) {
800
+ return new Promise((resolve) => setTimeout(resolve, ms));
801
+ }
802
+
803
+ function isPortInUse(port, host = '127.0.0.1') {
804
+ return new Promise((resolve) => {
805
+ const server = createServer();
806
+
807
+ server.once('error', (err) => {
808
+ if (err && err.code === 'EADDRINUSE') {
809
+ resolve(true);
810
+ } else {
811
+ resolve(false);
812
+ }
813
+ });
814
+
815
+ server.once('listening', () => {
816
+ server.close(() => resolve(false));
817
+ });
818
+
819
+ server.listen(port, host);
820
+ });
821
+ }
822
+
823
+ async function findAvailablePort(preferredPort = DEFAULT_GATEWAY_PORT) {
824
+ const candidates = [preferredPort, ...STRONG_PORT_CANDIDATES.filter((p) => p !== preferredPort)];
825
+ for (const port of candidates) {
826
+ const inUse = await isPortInUse(port);
827
+ if (!inUse) return port;
828
+ }
829
+
830
+ for (let dynamicPort = 19000; dynamicPort <= 19999; dynamicPort += 1) {
831
+ const inUse = await isPortInUse(dynamicPort);
832
+ if (!inUse) return dynamicPort;
833
+ }
834
+ return null;
835
+ }
836
+
837
+ function extractAuthToken(configRaw = '') {
838
+ if (!configRaw) return '';
839
+ const tokenPatterns = [
840
+ /"token"\s*:\s*"([^"]+)"/i,
841
+ /"gatewayToken"\s*:\s*"([^"]+)"/i,
842
+ /"auth"\s*:\s*\{[\s\S]*?"token"\s*:\s*"([^"]+)"/i,
843
+ ];
844
+ for (const pattern of tokenPatterns) {
845
+ const match = configRaw.match(pattern);
846
+ if (match?.[1]) return match[1];
847
+ }
848
+ return '';
849
+ }
850
+
851
+ function getDashboardToken(config) {
852
+ if (config?.token) return config.token;
853
+ return extractAuthToken(config?.raw || '') || '';
854
+ }
855
+
856
+ function parseStatusOutput(statusOutput) {
857
+ const text = (statusOutput || '').toLowerCase();
858
+ const runningKeywords = ['running', 'active', '已运行', '运行中', '在线'];
859
+ const stoppedKeywords = ['stopped', 'inactive', 'not running', '未运行', '停止'];
860
+
861
+ if (runningKeywords.some((word) => text.includes(word))) return 'running';
862
+ if (stoppedKeywords.some((word) => text.includes(word))) return 'stopped';
863
+ return 'unknown';
864
+ }
865
+
866
+ function getPortCheckOutput(port) {
867
+ const cmd = platform() === 'win32'
868
+ ? `netstat -ano | findstr :${port}`
869
+ : `lsof -nP -iTCP:${port} -sTCP:LISTEN 2>/dev/null || netstat -tlnp 2>/dev/null | grep :${port}`;
870
+ return safeExec(cmd);
871
+ }
872
+
873
+ function getPortConflictDetail(port) {
874
+ const cmd = platform() === 'win32'
875
+ ? `netstat -ano | findstr :${port} | findstr LISTENING`
876
+ : `lsof -nP -iTCP:${port} -sTCP:LISTEN 2>/dev/null | head -8`;
877
+ return safeExec(cmd);
878
+ }
879
+
880
+ function isLikelyOpenClawProcess(output = '') {
881
+ const text = output.toLowerCase();
882
+ return (
883
+ text.includes('openclaw') ||
884
+ text.includes('clawdbot') ||
885
+ text.includes('moltbot') ||
886
+ text.includes('gateway') ||
887
+ text.includes('node')
888
+ );
889
+ }
890
+
891
+ function gatewayHealthRequest(port, path = '/health') {
892
+ const endpoint = `http://127.0.0.1:${port}${path}`;
893
+ const curlCmd = `curl -sS --max-time 5 --connect-timeout 2 ${endpoint}`;
894
+ return safeExec(curlCmd, { timeout: 7000 });
895
+ }
896
+
897
+ function parseHealthOutput(raw = '') {
898
+ if (!raw) return { healthy: false, detail: '空响应' };
899
+
900
+ const lower = raw.toLowerCase();
901
+ if (lower.includes('ok') || lower.includes('healthy') || lower.includes('success')) {
902
+ return { healthy: true, detail: raw.slice(0, 300) };
903
+ }
904
+
905
+ try {
906
+ const parsed = JSON.parse(raw);
907
+ if (parsed?.status === 'ok' || parsed?.healthy === true || parsed?.ok === true) {
908
+ return { healthy: true, detail: JSON.stringify(parsed).slice(0, 300), parsed };
909
+ }
910
+ return { healthy: false, detail: JSON.stringify(parsed).slice(0, 300), parsed };
911
+ } catch {
912
+ return { healthy: false, detail: raw.slice(0, 300) };
913
+ }
914
+ }
915
+
916
+ function runGatewayCommand(cliName, action) {
917
+ return safeExec(`${cliName} gateway ${action}`);
918
+ }
919
+
920
+ async function ensureGatewayRunning(cliName, preferredAction = 'start') {
921
+ const actions = preferredAction === 'restart'
922
+ ? ['restart', 'start', 'stop', 'start']
923
+ : ['start', 'restart', 'stop', 'start'];
924
+
925
+ for (const action of actions) {
926
+ const result = runGatewayCommand(cliName, action);
927
+ if (result.ok) {
928
+ await sleep(2200);
929
+ const statusCheck = safeExec(`${cliName} status`);
930
+ if (statusCheck.ok && parseStatusOutput(statusCheck.output) === 'running') {
931
+ return { ok: true, action };
932
+ }
933
+ }
934
+ }
935
+ return { ok: false, action: actions[actions.length - 1] };
936
+ }
937
+
938
+ function parseDoctorProblems(output = '') {
939
+ const text = output.toLowerCase();
940
+ const keywords = ['error', 'fail', 'failed', 'problem', '问题', '失败', '错误'];
941
+ return keywords.some((word) => text.includes(word));
942
+ }
943
+
944
+ function detectNpmInstallState(packageName) {
945
+ const result = safeExec(`npm ls -g ${packageName} --depth=0`);
946
+ if (!result.ok) return { installed: false, output: result.stderr || result.error || '' };
947
+ return { installed: true, output: result.output || '' };
948
+ }
949
+
950
+ function installCliPackage(packageName, strongMode = false) {
951
+ const useSudo = needsSudo();
952
+ const baseInstall = useSudo
953
+ ? `sudo npm install -g ${packageName}@latest`
954
+ : `npm install -g ${packageName}@latest`;
955
+
956
+ const registries = strongMode
957
+ ? ['', 'https://registry.npmjs.org', 'https://registry.npmmirror.com']
958
+ : [''];
959
+
960
+ for (const registry of registries) {
961
+ const cmd = registry
962
+ ? `${baseInstall} --registry ${registry}`
963
+ : baseInstall;
964
+ const result = safeExec(cmd, { timeout: 240000 });
965
+ if (result.ok) {
966
+ return { ok: true, method: registry ? `registry:${registry}` : 'default' };
967
+ }
968
+ }
969
+
970
+ if (strongMode) {
971
+ safeExec('npm cache clean --force', { timeout: 120000 });
972
+ for (const registry of registries) {
973
+ const cmd = registry
974
+ ? `${baseInstall} --registry ${registry}`
975
+ : baseInstall;
976
+ const retry = safeExec(cmd, { timeout: 240000 });
977
+ if (retry.ok) {
978
+ return { ok: true, method: registry ? `cache-retry:${registry}` : 'cache-retry:default' };
979
+ }
980
+ }
981
+ }
982
+
983
+ if (strongMode) {
984
+ const tarball = safeExec(`npm view ${packageName}@latest dist.tarball --registry https://registry.npmjs.org`);
985
+ if (tarball.ok && tarball.output) {
986
+ const tarballInstall = useSudo
987
+ ? `sudo npm install -g ${tarball.output.trim()}`
988
+ : `npm install -g ${tarball.output.trim()}`;
989
+ const tarballResult = safeExec(tarballInstall, { timeout: 240000 });
990
+ if (tarballResult.ok) {
991
+ return { ok: true, method: 'tarball' };
992
+ }
993
+ }
994
+ }
995
+
996
+ return { ok: false, method: 'failed' };
997
+ }
998
+
999
+ function tryRecoverCliBinary(cliName, strongMode = false) {
1000
+ const candidates = [
1001
+ { bin: 'openclaw', pkg: 'openclaw' },
1002
+ { bin: 'clawdbot', pkg: 'clawdbot' },
1003
+ { bin: 'moltbot', pkg: 'moltbot' },
1004
+ ];
1005
+
1006
+ const preferred = candidates.find((item) => item.bin === cliName);
1007
+ const ordered = preferred
1008
+ ? [preferred, ...candidates.filter((item) => item.bin !== cliName)]
1009
+ : candidates;
1010
+
1011
+ for (const candidate of ordered) {
1012
+ const installedState = detectNpmInstallState(candidate.pkg);
1013
+ if (!installedState.installed) {
1014
+ const installResult = installCliPackage(candidate.pkg, strongMode);
1015
+ if (!installResult.ok) continue;
1016
+ }
1017
+
1018
+ if (isRealGatewayCli(candidate.bin)) {
1019
+ return { ok: true, cliName: candidate.bin, packageName: candidate.pkg };
1020
+ }
1021
+
1022
+ if (strongMode) {
1023
+ const uninstallCmd = needsSudo()
1024
+ ? `sudo npm uninstall -g ${candidate.pkg}`
1025
+ : `npm uninstall -g ${candidate.pkg}`;
1026
+ safeExec(uninstallCmd, { timeout: 120000 });
1027
+ }
1028
+
1029
+ const reinstallResult = installCliPackage(candidate.pkg, strongMode);
1030
+ if (reinstallResult.ok && isRealGatewayCli(candidate.bin)) {
1031
+ return { ok: true, cliName: candidate.bin, packageName: candidate.pkg };
1032
+ }
1033
+ }
1034
+
1035
+ return { ok: false, cliName };
1036
+ }
1037
+
1038
+ function ensureConfigFilePresent(config, cliName) {
1039
+ if (config.configPath && existsSync(config.configPath)) {
1040
+ return { ok: true, repaired: false, note: '配置文件存在' };
1041
+ }
1042
+ const result = safeExec(`${cliName} onboard --install-daemon`, { timeout: 240000 });
1043
+ if (!result.ok && process.stdin.isTTY && process.stdout.isTTY) {
1044
+ spawnSync(cliName, ['onboard'], { stdio: 'inherit', shell: true });
1045
+ }
1046
+ const recheck = getConfigInfo();
1047
+ if (recheck.configPath && existsSync(recheck.configPath)) {
1048
+ return { ok: true, repaired: true, note: '已重新生成配置文件' };
1049
+ }
1050
+ return { ok: false, repaired: false, note: '无法自动生成配置文件' };
1051
+ }
1052
+
1053
+ function backupBrokenConfig(configPath) {
1054
+ const backupPath = `${configPath}.bak.${Date.now()}`;
1055
+ renameSync(configPath, backupPath);
1056
+ return backupPath;
1057
+ }
1058
+
1059
+ async function repairBrokenConfig(config, cliName, strongMode = false) {
1060
+ if (!config.configPath || !existsSync(config.configPath)) {
1061
+ return { ok: false, repaired: false, note: '配置文件不存在' };
1062
+ }
1063
+
1064
+ try {
1065
+ JSON.parse(readFileSync(config.configPath, 'utf8'));
1066
+ return { ok: true, repaired: false, note: '配置文件 JSON 正常' };
1067
+ } catch {
1068
+ try {
1069
+ const backupPath = backupBrokenConfig(config.configPath);
1070
+ const onboardArgs = strongMode ? ['onboard', '--install-daemon'] : ['onboard'];
1071
+ spawnSync(cliName, onboardArgs, { stdio: 'inherit', shell: true });
1072
+ const recheck = getConfigInfo();
1073
+ if (recheck.configPath && existsSync(recheck.configPath)) {
1074
+ return {
1075
+ ok: true,
1076
+ repaired: true,
1077
+ note: `配置损坏已备份并重建 (${backupPath})`,
1078
+ backupPath,
1079
+ };
1080
+ }
1081
+ return { ok: false, repaired: false, note: `配置已备份但重建失败 (${backupPath})`, backupPath };
1082
+ } catch (e) {
1083
+ return { ok: false, repaired: false, note: `配置修复失败: ${e.message}` };
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ function selectHealthPort(config, strongMode = false) {
1089
+ const candidate = Number(config?.port || DEFAULT_GATEWAY_PORT);
1090
+ if (Number.isInteger(candidate) && candidate > 0) return candidate;
1091
+ return strongMode ? STRONG_PORT_CANDIDATES[0] : DEFAULT_GATEWAY_PORT;
1092
+ }
1093
+
1094
+ async function tryFixPortConflict(cliName, currentPort) {
1095
+ const availablePort = await findAvailablePort(currentPort);
1096
+ if (!availablePort || availablePort === currentPort) {
1097
+ return { ok: false, newPort: currentPort, note: '未找到可用替代端口' };
1098
+ }
1099
+
1100
+ const setResult = safeExec(`${cliName} config set gateway.port ${availablePort}`);
1101
+ if (!setResult.ok) {
1102
+ return { ok: false, newPort: currentPort, note: `端口切换失败: ${setResult.stderr || setResult.error}` };
1103
+ }
1104
+
1105
+ const restartResult = await ensureGatewayRunning(cliName, 'restart');
1106
+ if (!restartResult.ok) {
1107
+ return { ok: false, newPort: currentPort, note: '已切换端口但 Gateway 重启失败' };
1108
+ }
1109
+
1110
+ return { ok: true, newPort: availablePort, note: `端口已切换到 ${availablePort}` };
1111
+ }
1112
+
1113
+ function summarizeIssue(level, title, detail, solution, fixCmd = '') {
1114
+ return { level, title, detail, solution, fixCmd };
1115
+ }
1116
+
1117
+ function collectGatewayLogs(cliName, lines = 80) {
1118
+ const candidates = [
1119
+ `${cliName} gateway logs --lines ${lines}`,
1120
+ `${cliName} gateway logs -n ${lines}`,
1121
+ `${cliName} gateway logs`,
1122
+ ];
1123
+
1124
+ for (const cmd of candidates) {
1125
+ const result = safeExec(cmd, { timeout: 20000 });
1126
+ if (result.ok && result.output) {
1127
+ return { ok: true, output: result.output.slice(-2000) };
1128
+ }
1129
+ }
1130
+ return { ok: false, output: '' };
1131
+ }
1132
+
1133
+ async function runOfficialDoctor(cliName, autoFix = false, strongMode = false) {
1134
+ const doctorResult = safeExec(`${cliName} doctor`, { timeout: strongMode ? 120000 : 60000 });
1135
+
1136
+ if (!doctorResult.ok) {
1137
+ return {
1138
+ ok: false,
1139
+ issue: summarizeIssue(
1140
+ 'warning',
1141
+ '官方诊断执行失败',
1142
+ doctorResult.stderr || doctorResult.error || 'doctor 命令失败',
1143
+ `运行 ${cliName} doctor 查看详情`,
1144
+ `${cliName} doctor`,
1145
+ ),
1146
+ };
1147
+ }
1148
+
1149
+ if (!parseDoctorProblems(doctorResult.output)) {
1150
+ return { ok: true, fixed: false, issue: null };
1151
+ }
1152
+
1153
+ if (!autoFix) {
1154
+ return {
1155
+ ok: false,
1156
+ issue: summarizeIssue(
1157
+ 'warning',
1158
+ '官方诊断发现问题',
1159
+ doctorResult.output.slice(0, 260),
1160
+ `运行 ${cliName} doctor --fix 尝试自动修复`,
1161
+ `${cliName} doctor --fix`,
1162
+ ),
1163
+ };
1164
+ }
1165
+
1166
+ const fixResult = safeExec(`${cliName} doctor --fix`, { timeout: strongMode ? 180000 : 90000 });
1167
+ if (fixResult.ok) {
1168
+ return { ok: true, fixed: true, issue: null };
1169
+ }
1170
+
1171
+ return {
1172
+ ok: false,
1173
+ issue: summarizeIssue(
1174
+ 'warning',
1175
+ '官方自动修复失败',
1176
+ fixResult.stderr || fixResult.error || 'doctor --fix 执行失败',
1177
+ `查看日志: ${cliName} gateway logs`,
1178
+ `${cliName} doctor`,
1179
+ ),
1180
+ };
1181
+ }
1182
+
1183
+ async function verifyGatewayApi(port, cliName, autoFix = false, strongMode = false) {
1184
+ const paths = ['/health', '/api/health', '/status'];
1185
+
1186
+ for (const path of paths) {
1187
+ const healthResult = gatewayHealthRequest(port, path);
1188
+ if (!healthResult.ok) continue;
1189
+ const parsed = parseHealthOutput(healthResult.output);
1190
+ if (parsed.healthy) {
1191
+ return { ok: true, issue: null };
1192
+ }
1193
+ }
1194
+
1195
+ const issue = summarizeIssue(
1196
+ 'error',
1197
+ 'API 无响应',
1198
+ `无法连接到 Gateway API (127.0.0.1:${port})`,
1199
+ `重启服务: ${cliName} gateway restart`,
1200
+ `${cliName} gateway restart`,
1201
+ );
1202
+
1203
+ if (!autoFix) {
1204
+ return { ok: false, issue, fixed: false };
1205
+ }
1206
+
1207
+ const recovered = await ensureGatewayRunning(cliName, 'restart');
1208
+ if (!recovered.ok) {
1209
+ return { ok: false, issue, fixed: false };
1210
+ }
1211
+
1212
+ await sleep(strongMode ? 3500 : 2000);
1213
+
1214
+ for (const path of paths) {
1215
+ const retryResult = gatewayHealthRequest(port, path);
1216
+ if (!retryResult.ok) continue;
1217
+ const parsed = parseHealthOutput(retryResult.output);
1218
+ if (parsed.healthy) {
1219
+ return { ok: true, issue: null, fixed: true };
1220
+ }
1221
+ }
1222
+
1223
+ return { ok: false, issue, fixed: false };
1224
+ }
1225
+
1226
+ async function validateModelConfig(autoFix = false, strongMode = false) {
1227
+ const config = getConfigInfo();
1228
+ if (!config.raw) {
1229
+ return {
1230
+ ok: false,
1231
+ issue: summarizeIssue(
1232
+ 'warning',
1233
+ '模型配置状态未知',
1234
+ '未读取到配置文件内容',
1235
+ '运行 npx openclawapi@latest 配置模型',
1236
+ 'npx openclawapi@latest',
1237
+ ),
1238
+ fixed: false,
1239
+ };
1240
+ }
1241
+
1242
+ const hasModels = config.raw.includes('"models"') || config.raw.includes('"providers"');
1243
+ const hasApiKey = config.raw.includes('"apiKey"') || config.raw.includes('"api_key"');
1244
+ if (hasModels || hasApiKey) {
1245
+ return { ok: true, issue: null, fixed: false };
1246
+ }
1247
+
1248
+ const issue = summarizeIssue(
1249
+ 'warning',
1250
+ '未配置 AI 模型',
1251
+ '配置文件中未找到模型或 API Key 配置',
1252
+ '运行 npx openclawapi@latest 配置模型',
1253
+ 'npx openclawapi@latest',
1254
+ );
1255
+
1256
+ if (!autoFix) {
1257
+ return { ok: false, issue, fixed: false };
1258
+ }
1259
+
1260
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1261
+ return { ok: false, issue, fixed: false };
1262
+ }
1263
+
1264
+ if (strongMode) {
1265
+ const presetResult = safeExec('npx openclawapi@latest preset-claude', { timeout: 180000 });
1266
+ if (presetResult.ok) {
1267
+ return { ok: true, issue: null, fixed: true };
1268
+ }
1269
+ }
1270
+
1271
+ spawnSync('npx', ['openclawapi@latest'], { stdio: 'inherit', shell: true });
1272
+ const recheck = getConfigInfo();
1273
+ const recheckHasModels = recheck.raw.includes('"models"') || recheck.raw.includes('"providers"');
1274
+ const recheckHasApiKey = recheck.raw.includes('"apiKey"') || recheck.raw.includes('"api_key"');
1275
+
1276
+ if (recheckHasModels || recheckHasApiKey) {
1277
+ return { ok: true, issue: null, fixed: true };
1278
+ }
1279
+
1280
+ return { ok: false, issue, fixed: false };
1281
+ }
1282
+
271
1283
  function detectVps() {
272
1284
  if (platform() !== 'linux') return false;
273
1285
  // Cloud provider markers
@@ -833,7 +1845,7 @@ function showCompletionInfo(cliName) {
833
1845
  function showDashboardAccessInfo() {
834
1846
  const config = getConfigInfo();
835
1847
  const port = config.port || 18789;
836
- const token = config.token || '<你的token>';
1848
+ const token = getDashboardToken(config) || '<你的token>';
837
1849
  const dashboardUrl = `http://127.0.0.1:${port}/?token=${token}`;
838
1850
 
839
1851
  if (detectVps()) {
@@ -904,83 +1916,82 @@ async function uninstallOpenClaw(existing) {
904
1916
 
905
1917
  // ============ 健康检查 ============
906
1918
 
907
- async function runHealthCheck(cliName, autoFix = false) {
908
- console.log(colors.bold(colors.cyan('\n🔍 OpenClaw 健康检查\n')));
1919
+ async function runHealthCheck(cliName, autoFix = false, strongMode = false) {
1920
+ const title = strongMode
1921
+ ? '\n🔍 OpenClaw 健康检查(强力模式)\n'
1922
+ : '\n🔍 OpenClaw 健康检查\n';
1923
+ console.log(colors.bold(colors.cyan(title)));
909
1924
 
910
- const issues = [];
1925
+ if (strongMode) {
1926
+ log.hint('强力模式已启用:多轮复检 + 深度回收修复');
1927
+ }
1928
+
1929
+ let activeCli = cliName;
911
1930
  const fixed = [];
912
- const config = getConfigInfo();
1931
+ let finalIssues = [];
1932
+ const totalPasses = strongMode && autoFix ? STRONG_FIX_MAX_PASSES : 1;
913
1933
 
914
- // 1. 检查配置文件
915
- console.log(colors.cyan('检查配置文件...'));
916
- if (!config.configPath || !existsSync(config.configPath)) {
917
- const issue = {
918
- level: 'error',
919
- title: '配置文件不存在',
920
- detail: '未找到 openclaw.json 配置文件',
921
- solution: '运行 openclaw onboard 重新配置',
922
- fixCmd: `${cliName} onboard`,
923
- };
924
- if (autoFix) {
925
- console.log(colors.yellow(' 尝试运行 onboard 生成配置...'));
926
- spawnSync(cliName, ['onboard'], { stdio: 'inherit', shell: true });
927
- fixed.push('已运行 onboard 生成配置');
928
- } else {
929
- issues.push(issue);
1934
+ for (let pass = 1; pass <= totalPasses; pass += 1) {
1935
+ const issues = [];
1936
+ const fixedBeforePass = fixed.length;
1937
+
1938
+ if (totalPasses > 1) {
1939
+ console.log(colors.bold(colors.cyan(`\n--- 第 ${pass}/${totalPasses} 轮深度检查 ---`)));
930
1940
  }
931
- } else {
932
- try {
933
- const raw = readFileSync(config.configPath, 'utf8');
934
- JSON.parse(raw);
935
- log.success('配置文件格式正确');
936
- } catch (e) {
937
- const issue = {
938
- level: 'error',
939
- title: '配置文件 JSON 格式错误',
940
- detail: `解析失败: ${e.message}`,
941
- solution: '备份并重新生成配置文件',
942
- fixCmd: `mv ${config.configPath} ${config.configPath}.bak && ${cliName} onboard`,
943
- };
1941
+
1942
+ // 1. 检查 CLI
1943
+ console.log(colors.cyan('检查 CLI 命令可用性...'));
1944
+ if (!isRealGatewayCli(activeCli)) {
1945
+ const issue = summarizeIssue(
1946
+ 'error',
1947
+ 'OpenClaw CLI 命令不可用',
1948
+ `当前命令 ${activeCli} 不可执行或不是 Gateway CLI`,
1949
+ '重新安装 CLI 包并恢复命令链接',
1950
+ `npm install -g ${activeCli}@latest`,
1951
+ );
1952
+
944
1953
  if (autoFix) {
945
- console.log(colors.yellow(' 尝试修复配置文件...'));
946
- const backupPath = `${config.configPath}.bak.${Date.now()}`;
947
- try {
948
- const { renameSync } = await import('fs');
949
- renameSync(config.configPath, backupPath);
950
- log.success(`已备份损坏的配置到 ${backupPath}`);
951
- fixed.push('配置文件已备份,请重新运行 onboard');
952
- } catch {
1954
+ console.log(colors.yellow(` 尝试恢复 CLI 命令 (${activeCli})...`));
1955
+ const recovered = tryRecoverCliBinary(activeCli, strongMode);
1956
+ if (recovered.ok && recovered.cliName) {
1957
+ activeCli = recovered.cliName;
1958
+ log.success(`CLI 已恢复: ${activeCli}`);
1959
+ fixed.push(`CLI 已自动恢复(${activeCli})`);
1960
+ } else {
953
1961
  issues.push(issue);
954
1962
  }
955
1963
  } else {
956
1964
  issues.push(issue);
957
1965
  }
1966
+ } else {
1967
+ log.success(`CLI 可用: ${activeCli}`);
958
1968
  }
959
- }
960
1969
 
961
- // 2. 检查 Gateway 进程
962
- console.log(colors.cyan('检查 Gateway 进程...'));
963
- const statusResult = safeExec(`${cliName} status`);
964
- if (statusResult.ok) {
965
- const output = statusResult.output.toLowerCase();
966
- if (output.includes('running') || output.includes('active')) {
967
- log.success('Gateway 进程正在运行');
968
- } else if (output.includes('stopped') || output.includes('inactive') || output.includes('not running')) {
969
- const issue = {
970
- level: 'error',
971
- title: 'Gateway 未运行',
972
- detail: 'Gateway 服务已停止',
973
- solution: `运行 ${cliName} gateway start 启动服务`,
974
- fixCmd: `${cliName} gateway start`,
975
- };
1970
+ if (!isRealGatewayCli(activeCli)) {
1971
+ finalIssues = issues;
1972
+ break;
1973
+ }
1974
+
1975
+ // 2. 检查配置文件
1976
+ console.log(colors.cyan('检查配置文件...'));
1977
+ let config = getConfigInfo();
1978
+
1979
+ if (!config.configPath || !existsSync(config.configPath)) {
1980
+ const issue = summarizeIssue(
1981
+ 'error',
1982
+ '配置文件不存在',
1983
+ '未找到 openclaw/clawdbot 配置文件',
1984
+ `运行 ${activeCli} onboard 重新生成配置`,
1985
+ `${activeCli} onboard`,
1986
+ );
1987
+
976
1988
  if (autoFix) {
977
- console.log(colors.yellow(' 尝试启动 Gateway...'));
978
- const startResult = safeExec(`${cliName} gateway start`);
979
- if (startResult.ok) {
980
- console.log(colors.gray(' 等待 Gateway 启动...'));
981
- await new Promise(r => setTimeout(r, 5000));
982
- log.success('Gateway 已启动');
983
- fixed.push('Gateway 已自动启动');
1989
+ const recovered = ensureConfigFilePresent(config, activeCli);
1990
+ if (recovered.ok) {
1991
+ log.success(recovered.note);
1992
+ if (recovered.repaired) {
1993
+ fixed.push(recovered.note);
1994
+ }
984
1995
  } else {
985
1996
  issues.push(issue);
986
1997
  }
@@ -988,75 +1999,127 @@ async function runHealthCheck(cliName, autoFix = false) {
988
1999
  issues.push(issue);
989
2000
  }
990
2001
  }
991
- } else {
992
- issues.push({
993
- level: 'warning',
994
- title: '无法获取 Gateway 状态',
995
- detail: statusResult.error || '状态检查失败',
996
- solution: `尝试运行 ${cliName} status 查看详情`,
997
- });
998
- }
999
2002
 
1000
- // 3. 检查端口监听
1001
- console.log(colors.cyan('检查端口监听...'));
1002
- const port = config.port || 18789;
1003
- const portCheckCmd = platform() === 'win32'
1004
- ? `netstat -an | findstr :${port}`
1005
- : `lsof -i :${port} 2>/dev/null || netstat -tlnp 2>/dev/null | grep :${port}`;
1006
- const portResult = safeExec(portCheckCmd);
2003
+ config = getConfigInfo();
2004
+ if (config.configPath && existsSync(config.configPath)) {
2005
+ const configRepair = await repairBrokenConfig(config, activeCli, strongMode);
2006
+ if (configRepair.ok) {
2007
+ log.success(configRepair.note);
2008
+ if (configRepair.repaired) {
2009
+ fixed.push(configRepair.note);
2010
+ }
2011
+ } else {
2012
+ issues.push(
2013
+ summarizeIssue(
2014
+ 'error',
2015
+ '配置文件 JSON 格式错误',
2016
+ configRepair.note,
2017
+ `备份后重建配置: ${activeCli} onboard`,
2018
+ `${activeCli} onboard`,
2019
+ ),
2020
+ );
2021
+ }
2022
+ }
2023
+
2024
+ // 3. 检查 Gateway 进程
2025
+ console.log(colors.cyan('检查 Gateway 进程...'));
2026
+ const statusResult = safeExec(`${activeCli} status`);
2027
+ const statusState = statusResult.ok ? parseStatusOutput(statusResult.output) : 'unknown';
2028
+
2029
+ if (statusResult.ok && statusState === 'running') {
2030
+ log.success('Gateway 进程正在运行');
2031
+ } else {
2032
+ const issue = summarizeIssue(
2033
+ 'error',
2034
+ 'Gateway 未运行',
2035
+ statusResult.ok ? 'Gateway 状态为停止/未知' : (statusResult.stderr || statusResult.error || '状态检查失败'),
2036
+ `运行 ${activeCli} gateway start 启动服务`,
2037
+ `${activeCli} gateway start`,
2038
+ );
1007
2039
 
1008
- if (portResult.ok && portResult.output) {
1009
- log.success(`端口 ${port} 正在监听`);
1010
- } else {
1011
- // 检查是否有其他进程占用端口
1012
- const conflictCmd = platform() === 'win32'
1013
- ? `netstat -ano | findstr :${port} | findstr LISTENING`
1014
- : `lsof -i :${port} 2>/dev/null | head -5`;
1015
- const conflictCheck = safeExec(conflictCmd);
1016
- if (conflictCheck.ok && conflictCheck.output && !conflictCheck.output.includes('openclaw') && !conflictCheck.output.includes('node')) {
1017
- const issue = {
1018
- level: 'error',
1019
- title: `端口 ${port} 被其他程序占用`,
1020
- detail: conflictCheck.output.slice(0, 100),
1021
- solution: `更换端口: ${cliName} config set gateway.port 18790`,
1022
- fixCmd: `${cliName} config set gateway.port 18790 && ${cliName} gateway restart`,
1023
- };
1024
2040
  if (autoFix) {
1025
- console.log(colors.yellow(' 尝试更换端口到 18790...'));
1026
- const portResult = safeExec(`${cliName} config set gateway.port 18790`);
1027
- const restartResult = safeExec(`${cliName} gateway restart`);
1028
- if (portResult.ok && restartResult.ok) {
1029
- log.success('已更换端口到 18790 并重启 Gateway');
1030
- fixed.push('端口冲突已自动解决(更换到 18790)');
2041
+ console.log(colors.yellow(' 尝试恢复 Gateway 运行状态...'));
2042
+ const recovered = await ensureGatewayRunning(activeCli, 'start');
2043
+ if (recovered.ok) {
2044
+ log.success(`Gateway 已恢复运行(${recovered.action})`);
2045
+ fixed.push(`Gateway 已自动恢复(${recovered.action})`);
1031
2046
  } else {
1032
2047
  issues.push(issue);
1033
2048
  }
1034
2049
  } else {
1035
2050
  issues.push(issue);
1036
2051
  }
1037
- } else {
1038
- const issue = {
1039
- level: 'error',
1040
- title: `端口 ${port} 未监听`,
1041
- detail: 'Gateway 端口未开放,服务可能未正常启动',
1042
- solution: `运行 ${cliName} gateway restart`,
1043
- fixCmd: `${cliName} gateway restart`,
1044
- };
2052
+ }
2053
+
2054
+ // 4. 检查端口监听
2055
+ config = getConfigInfo();
2056
+ let port = selectHealthPort(config, strongMode);
2057
+ console.log(colors.cyan(`检查端口监听 (${port})...`));
2058
+ let portHealthy = false;
2059
+
2060
+ const portResult = getPortCheckOutput(port);
2061
+ if (portResult.ok && portResult.output) {
2062
+ if (isLikelyOpenClawProcess(portResult.output)) {
2063
+ log.success(`端口 ${port} 正在监听`);
2064
+ portHealthy = true;
2065
+ } else {
2066
+ const issue = summarizeIssue(
2067
+ 'error',
2068
+ `端口 ${port} 被其他程序占用`,
2069
+ portResult.output.slice(0, 180),
2070
+ `更换端口: ${activeCli} config set gateway.port <新端口>`,
2071
+ `${activeCli} config set gateway.port 18790 && ${activeCli} gateway restart`,
2072
+ );
2073
+
2074
+ if (autoFix) {
2075
+ const portFix = await tryFixPortConflict(activeCli, port);
2076
+ if (portFix.ok) {
2077
+ port = portFix.newPort;
2078
+ log.success(portFix.note);
2079
+ fixed.push(`端口冲突已自动修复(${port})`);
2080
+ portHealthy = true;
2081
+ } else {
2082
+ issues.push(issue);
2083
+ }
2084
+ } else {
2085
+ issues.push(issue);
2086
+ }
2087
+ }
2088
+ }
2089
+
2090
+ if (!portHealthy) {
2091
+ const conflictDetail = getPortConflictDetail(port);
2092
+ const conflictText = conflictDetail.ok ? conflictDetail.output : '';
2093
+ const issue = summarizeIssue(
2094
+ 'error',
2095
+ `端口 ${port} 未监听`,
2096
+ conflictText ? `端口状态异常: ${conflictText.slice(0, 180)}` : 'Gateway 端口未监听',
2097
+ `重启服务: ${activeCli} gateway restart`,
2098
+ `${activeCli} gateway restart`,
2099
+ );
2100
+
1045
2101
  if (autoFix) {
1046
- console.log(colors.yellow(' 尝试重启 Gateway...'));
1047
- const restartResult = safeExec(`${cliName} gateway restart`);
1048
- if (restartResult.ok) {
1049
- // 等待 Gateway 绑定端口
1050
- console.log(colors.gray(' 等待 Gateway 启动...'));
1051
- await new Promise(r => setTimeout(r, 5000));
1052
- // 重新检查端口
1053
- const recheck = safeExec(portCheckCmd);
2102
+ console.log(colors.yellow(' 尝试重启并恢复端口监听...'));
2103
+ const recovered = await ensureGatewayRunning(activeCli, 'restart');
2104
+ if (recovered.ok) {
2105
+ await sleep(2500);
2106
+ const recheck = getPortCheckOutput(port);
1054
2107
  if (recheck.ok && recheck.output) {
1055
- log.success('Gateway 已重启,端口正常');
1056
- fixed.push('Gateway 已自动重启');
2108
+ log.success(`端口 ${port} 已恢复监听`);
2109
+ fixed.push(`端口监听已恢复(${port})`);
2110
+ portHealthy = true;
2111
+ } else if (strongMode) {
2112
+ const portFix = await tryFixPortConflict(activeCli, port);
2113
+ if (portFix.ok) {
2114
+ port = portFix.newPort;
2115
+ log.success(portFix.note);
2116
+ fixed.push(`强力模式切换端口成功(${port})`);
2117
+ portHealthy = true;
2118
+ } else {
2119
+ issues.push(issue);
2120
+ }
1057
2121
  } else {
1058
- log.warn('Gateway 已重启但端口仍未监听');
1059
- issues.push({ ...issue, detail: 'Gateway 重启后端口仍未监听,可能配置有问题' });
2122
+ issues.push(issue);
1060
2123
  }
1061
2124
  } else {
1062
2125
  issues.push(issue);
@@ -1065,106 +2128,112 @@ async function runHealthCheck(cliName, autoFix = false) {
1065
2128
  issues.push(issue);
1066
2129
  }
1067
2130
  }
1068
- }
1069
2131
 
1070
- // 4. 检查 API 健康
1071
- console.log(colors.cyan('检查 API 健康状态...'));
1072
- const healthResult = safeExec(`curl -s --connect-timeout 3 http://127.0.0.1:${port}/health`);
1073
- if (healthResult.ok && healthResult.output) {
1074
- try {
1075
- const health = JSON.parse(healthResult.output);
1076
- if (health.status === 'ok' || health.healthy) {
1077
- log.success('API 健康检查通过');
1078
- } else {
1079
- issues.push({
1080
- level: 'warning',
1081
- title: 'API 健康状态异常',
1082
- detail: JSON.stringify(health),
1083
- solution: `运行 ${cliName} gateway logs 查看日志`,
1084
- });
2132
+ // 5. 检查 API 健康
2133
+ console.log(colors.cyan('检查 API 健康状态...'));
2134
+ const apiCheck = await verifyGatewayApi(port, activeCli, autoFix, strongMode);
2135
+ if (apiCheck.ok) {
2136
+ log.success('API 健康检查通过');
2137
+ if (apiCheck.fixed) {
2138
+ fixed.push('API 无响应问题已自动修复');
1085
2139
  }
1086
- } catch {
1087
- if (healthResult.output.includes('ok') || healthResult.output.includes('healthy')) {
1088
- log.success('API 健康检查通过');
1089
- } else {
1090
- log.warn('API 返回非标准格式');
2140
+ } else if (apiCheck.issue) {
2141
+ issues.push(apiCheck.issue);
2142
+ }
2143
+
2144
+ // 6. 检查模型配置
2145
+ console.log(colors.cyan('检查模型配置...'));
2146
+ const modelCheck = await validateModelConfig(autoFix, strongMode);
2147
+ if (modelCheck.ok) {
2148
+ log.success('模型配置检查通过');
2149
+ if (modelCheck.fixed) {
2150
+ fixed.push('模型配置已自动补全');
1091
2151
  }
2152
+ } else if (modelCheck.issue) {
2153
+ issues.push(modelCheck.issue);
1092
2154
  }
1093
- } else {
1094
- const issue = {
1095
- level: 'error',
1096
- title: 'API 无响应',
1097
- detail: '无法连接到 Gateway API',
1098
- solution: `重启服务: ${cliName} gateway restart`,
1099
- fixCmd: `${cliName} gateway restart`,
1100
- };
1101
- if (autoFix) {
1102
- console.log(colors.yellow(' 尝试重启 Gateway...'));
1103
- const restartResult = safeExec(`${cliName} gateway restart`);
1104
- if (restartResult.ok) {
1105
- console.log(colors.gray(' 等待 Gateway 启动...'));
1106
- await new Promise(r => setTimeout(r, 5000));
1107
- log.success('Gateway 已重启');
1108
- fixed.push('Gateway 已自动重启(API 无响应)');
2155
+
2156
+ // 6.5 强力模式:探测模型实际连通
2157
+ if (strongMode) {
2158
+ console.log(colors.cyan('强力模式:测试模型连通性...'));
2159
+ const chatProbe = await testModelChat(activeCli);
2160
+ if (chatProbe.success) {
2161
+ const modelHint = chatProbe.model
2162
+ ? `${chatProbe.provider || 'provider'}/${chatProbe.model}`
2163
+ : (chatProbe.provider || 'provider');
2164
+ log.success(`模型连通性通过 (${modelHint})`);
1109
2165
  } else {
1110
- issues.push(issue);
2166
+ issues.push(
2167
+ summarizeIssue(
2168
+ 'warning',
2169
+ '模型连通性测试失败',
2170
+ chatProbe.error || '模型未返回可用结果',
2171
+ '运行 npx openclawapi@latest 重新配置模型后再执行强力修复',
2172
+ 'npx openclawapi@latest',
2173
+ ),
2174
+ );
1111
2175
  }
1112
- } else {
1113
- issues.push(issue);
1114
2176
  }
1115
- }
1116
2177
 
1117
- // 5. 检查模型配置
1118
- console.log(colors.cyan('检查模型配置...'));
1119
- if (config.raw) {
1120
- const hasModels = config.raw.includes('"models"') || config.raw.includes('"providers"');
1121
- const hasApiKey = config.raw.includes('"apiKey"') || config.raw.includes('"api_key"');
1122
- if (!hasModels && !hasApiKey) {
1123
- const issue = {
1124
- level: 'warning',
1125
- title: '未配置 AI 模型',
1126
- detail: '配置文件中未找到模型或 API Key 配置',
1127
- solution: '运行 npx openclawapi@latest 配置模型',
1128
- fixCmd: 'npx openclawapi@latest',
1129
- };
1130
- if (autoFix) {
1131
- console.log(colors.yellow(' 启动模型配置...'));
1132
- spawnSync('npx', ['openclawapi@latest'], { stdio: 'inherit', shell: true });
1133
- fixed.push('已启动模型配置向导');
1134
- } else {
1135
- issues.push(issue);
2178
+ // 7. 运行官方诊断
2179
+ console.log(colors.cyan('运行官方诊断...'));
2180
+ const doctorCheck = await runOfficialDoctor(activeCli, autoFix, strongMode);
2181
+ if (doctorCheck.ok) {
2182
+ log.success('官方诊断通过');
2183
+ if (doctorCheck.fixed) {
2184
+ fixed.push('官方诊断问题已自动修复');
1136
2185
  }
1137
- } else {
1138
- log.success('已配置模型');
1139
- }
1140
- }
1141
-
1142
- // 6. 运行官方诊断
1143
- console.log(colors.cyan('运行官方诊断...'));
1144
- const doctorResult = safeExec(`${cliName} doctor`);
1145
- if (doctorResult.ok) {
1146
- const output = doctorResult.output.toLowerCase();
1147
- if (output.includes('error') || output.includes('fail') || output.includes('问题')) {
1148
- const issue = {
1149
- level: 'warning',
1150
- title: '官方诊断发现问题',
1151
- detail: doctorResult.output.slice(0, 200),
1152
- solution: `运行 ${cliName} doctor --fix 尝试自动修复`,
1153
- fixCmd: `${cliName} doctor --fix`,
1154
- };
1155
- if (autoFix) {
1156
- console.log(colors.yellow(' 运行官方自动修复...'));
1157
- const fixResult = safeExec(`${cliName} doctor --fix`);
1158
- if (fixResult.ok) {
1159
- fixed.push('官方诊断问题已尝试修复');
1160
- } else {
1161
- issues.push(issue);
2186
+ } else if (doctorCheck.issue) {
2187
+ issues.push(doctorCheck.issue);
2188
+ }
2189
+
2190
+ // 8. 强力模式深度回收
2191
+ if (strongMode && autoFix && issues.length > 0 && pass < totalPasses) {
2192
+ console.log(colors.cyan('执行强力回收修复...'));
2193
+ const deepActions = [];
2194
+
2195
+ const doctorFix = safeExec(`${activeCli} doctor --fix`, { timeout: 180000 });
2196
+ if (doctorFix.ok) deepActions.push('doctor --fix');
2197
+
2198
+ const onboardRecovery = runOnboardFlags(activeCli, { withModel: false, withChannel: false });
2199
+ if (onboardRecovery.ran) {
2200
+ if (onboardRecovery.ok) {
2201
+ deepActions.push('onboard 非交互重建');
2202
+ }
2203
+ } else if (process.stdin.isTTY && process.stdout.isTTY) {
2204
+ const autoOnboard = await runOnboardAuto(activeCli, { withModel: false, withChannel: false });
2205
+ if (autoOnboard.ok && autoOnboard.exitCode === 0) {
2206
+ deepActions.push('onboard 自动应答重建');
1162
2207
  }
1163
- } else {
1164
- issues.push(issue);
1165
2208
  }
1166
- } else {
1167
- log.success('官方诊断通过');
2209
+
2210
+ const restartRecovery = await ensureGatewayRunning(activeCli, 'restart');
2211
+ if (restartRecovery.ok) {
2212
+ deepActions.push(`gateway ${restartRecovery.action}`);
2213
+ }
2214
+
2215
+ if (deepActions.length) {
2216
+ deepActions.forEach((action) => fixed.push(`[强力] 已执行 ${action}`));
2217
+ }
2218
+
2219
+ await sleep(2500);
2220
+ }
2221
+
2222
+ finalIssues = issues;
2223
+
2224
+ const fixedThisPass = fixed.length - fixedBeforePass;
2225
+ const noIssueNow = finalIssues.length === 0;
2226
+ const shouldContinueStrongPass = strongMode && autoFix && pass < totalPasses;
2227
+
2228
+ if (noIssueNow) {
2229
+ if (!shouldContinueStrongPass || fixedThisPass === 0) {
2230
+ break;
2231
+ }
2232
+ continue;
2233
+ }
2234
+
2235
+ if (!shouldContinueStrongPass) {
2236
+ break;
1168
2237
  }
1169
2238
  }
1170
2239
 
@@ -1173,19 +2242,23 @@ async function runHealthCheck(cliName, autoFix = false) {
1173
2242
 
1174
2243
  if (fixed.length > 0) {
1175
2244
  console.log(colors.bold(colors.green(`🔧 已自动修复 ${fixed.length} 个问题:`)));
1176
- fixed.forEach((f, i) => console.log(colors.green(` ${i + 1}. ${f}`)));
2245
+ fixed.forEach((item, index) => console.log(colors.green(` ${index + 1}. ${item}`)));
1177
2246
  console.log('');
1178
2247
  }
1179
2248
 
1180
- if (issues.length === 0) {
1181
- console.log(colors.bold(colors.green('✅ 所有检查通过,OpenClaw 运行正常!')));
2249
+ if (finalIssues.length === 0) {
2250
+ if (strongMode) {
2251
+ console.log(colors.bold(colors.green('✅ 强力模式检查通过,系统状态已稳定!')));
2252
+ } else {
2253
+ console.log(colors.bold(colors.green('✅ 所有检查通过,OpenClaw 运行正常!')));
2254
+ }
1182
2255
  } else {
1183
- console.log(colors.bold(colors.yellow(`⚠ 发现 ${issues.length} 个问题:`)));
2256
+ console.log(colors.bold(colors.yellow(`⚠ 发现 ${finalIssues.length} 个问题:`)));
1184
2257
  console.log('='.repeat(50));
1185
2258
 
1186
- issues.forEach((issue, i) => {
2259
+ finalIssues.forEach((issue, index) => {
1187
2260
  const icon = issue.level === 'error' ? colors.red('❌') : colors.yellow('⚠');
1188
- console.log(`\n${icon} ${colors.bold(`问题 ${i + 1}: ${issue.title}`)}`);
2261
+ console.log(`\n${icon} ${colors.bold(`问题 ${index + 1}: ${issue.title}`)}`);
1189
2262
  console.log(colors.gray(` ${issue.detail}`));
1190
2263
  console.log(colors.cyan(` 解决方案: ${issue.solution}`));
1191
2264
  if (issue.fixCmd) {
@@ -1193,13 +2266,24 @@ async function runHealthCheck(cliName, autoFix = false) {
1193
2266
  }
1194
2267
  });
1195
2268
 
1196
- if (!autoFix && issues.some(i => i.fixCmd)) {
2269
+ if (!autoFix && finalIssues.some((item) => item.fixCmd)) {
1197
2270
  console.log(colors.cyan('\n💡 提示: 运行 npx openclawsetup --fix 尝试自动修复'));
1198
2271
  }
2272
+ if (!strongMode) {
2273
+ console.log(colors.cyan('💡 深度修复: 运行 npx openclawsetup --strong-fix'));
2274
+ }
2275
+
2276
+ if (strongMode && autoFix) {
2277
+ const logs = collectGatewayLogs(activeCli, 80);
2278
+ if (logs.ok && logs.output) {
2279
+ console.log(colors.gray('\n最近 Gateway 日志(截断):'));
2280
+ console.log(colors.gray(logs.output));
2281
+ }
2282
+ }
1199
2283
  }
1200
2284
  console.log('');
1201
2285
 
1202
- return { issues, fixed };
2286
+ return { issues: finalIssues, fixed, cliName: activeCli };
1203
2287
  }
1204
2288
 
1205
2289
  // ============ 交互式菜单 ============
@@ -1249,7 +2333,7 @@ function testModelChat(cliName) {
1249
2333
  async function showStatusInfo(cliName) {
1250
2334
  const config = getConfigInfo();
1251
2335
  const port = config.port || 18789;
1252
- const token = config.token || '<未配置>';
2336
+ const token = getDashboardToken(config) || '<未配置>';
1253
2337
  const dashboardUrl = `http://127.0.0.1:${port}/?token=${token}`;
1254
2338
 
1255
2339
  console.log(colors.bold(colors.cyan('\n📊 OpenClaw 状态信息\n')));
@@ -1383,16 +2467,19 @@ async function showInteractiveMenu(existing) {
1383
2467
 
1384
2468
  console.log(colors.cyan('\n请选择操作:'));
1385
2469
  console.log(` ${colors.yellow('1')}. 状态信息`);
1386
- console.log(` ${colors.yellow('2')}. 检查修复`);
1387
- console.log(` ${colors.yellow('3')}. 检查更新`);
1388
- console.log(` ${colors.yellow('4')}. 配置模型`);
1389
- console.log(` ${colors.yellow('5')}. 配置 Chat`);
1390
- console.log(` ${colors.yellow('6')}. 配置技能`);
1391
- console.log(` ${colors.yellow('7')}. 重新安装`);
1392
- console.log(` ${colors.yellow('8')}. 完全卸载`);
2470
+ console.log(` ${colors.yellow('2')}. 检查修复(标准)`);
2471
+ console.log(` ${colors.yellow('3')}. 强力检查修复(多轮深度)`);
2472
+ console.log(` ${colors.yellow('4')}. Token 优化(降低输入 token)`);
2473
+ console.log(` ${colors.yellow('5')}. 导出排障证据包(发给技术支持)`);
2474
+ console.log(` ${colors.yellow('6')}. 检查更新`);
2475
+ console.log(` ${colors.yellow('7')}. 配置模型`);
2476
+ console.log(` ${colors.yellow('8')}. 配置 Chat`);
2477
+ console.log(` ${colors.yellow('9')}. 配置技能`);
2478
+ console.log(` ${colors.yellow('10')}. 重新安装`);
2479
+ console.log(` ${colors.yellow('11')}. 完全卸载`);
1393
2480
  console.log(` ${colors.yellow('0')}. 退出`);
1394
2481
 
1395
- const choice = await askQuestion('\n请输入选项 (0-8): ');
2482
+ const choice = await askQuestion('\n请输入选项 (0-11): ');
1396
2483
 
1397
2484
  switch (choice.trim()) {
1398
2485
  case '1':
@@ -1400,14 +2487,26 @@ async function showInteractiveMenu(existing) {
1400
2487
  await waitForEnter('\n按回车返回菜单...');
1401
2488
  break;
1402
2489
  case '2':
1403
- await runHealthCheck(cliName, true);
2490
+ await runHealthCheck(cliName, true, false);
1404
2491
  await waitForEnter('\n按回车返回菜单...');
1405
2492
  break;
1406
2493
  case '3':
1407
- await updateOpenClaw(cliName);
2494
+ await runHealthCheck(cliName, true, true);
1408
2495
  await waitForEnter('\n按回车返回菜单...');
1409
2496
  break;
1410
2497
  case '4':
2498
+ await optimizeTokenUsage(cliName);
2499
+ await waitForEnter('\n按回车返回菜单...');
2500
+ break;
2501
+ case '5':
2502
+ await collectEvidencePackage({ quick: false });
2503
+ await waitForEnter('\n按回车返回菜单...');
2504
+ break;
2505
+ case '6':
2506
+ await updateOpenClaw(cliName);
2507
+ await waitForEnter('\n按回车返回菜单...');
2508
+ break;
2509
+ case '7':
1411
2510
  console.log(colors.cyan('\n启动模型配置...'));
1412
2511
  spawnSync('npx', ['openclawapi@latest'], {
1413
2512
  stdio: 'inherit',
@@ -1415,7 +2514,7 @@ async function showInteractiveMenu(existing) {
1415
2514
  });
1416
2515
  await waitForEnter('\n按回车返回菜单...');
1417
2516
  break;
1418
- case '5':
2517
+ case '8':
1419
2518
  console.log(colors.cyan('\n选择聊天渠道:'));
1420
2519
  console.log(` ${colors.yellow('a')}. Discord`);
1421
2520
  console.log(` ${colors.yellow('b')}. 飞书`);
@@ -1435,13 +2534,13 @@ async function showInteractiveMenu(existing) {
1435
2534
  }
1436
2535
  await waitForEnter('\n按回车返回菜单...');
1437
2536
  break;
1438
- case '6':
2537
+ case '9':
1439
2538
  console.log(colors.cyan('\n配置技能(即将支持)...'));
1440
2539
  // TODO: 等待技能地址提供后实现
1441
2540
  log.warn('技能配置功能即将上线,请稍后再试');
1442
2541
  await waitForEnter('\n按回车返回菜单...');
1443
2542
  break;
1444
- case '7':
2543
+ case '10':
1445
2544
  console.log(colors.yellow('\n即将重新安装 OpenClaw...'));
1446
2545
  const confirmReinstall = await askQuestion('确认重新安装?(y/N): ');
1447
2546
  if (confirmReinstall.toLowerCase() === 'y') {
@@ -1451,7 +2550,7 @@ async function showInteractiveMenu(existing) {
1451
2550
  showCompletionInfo(newCliName);
1452
2551
  }
1453
2552
  break;
1454
- case '8':
2553
+ case '11':
1455
2554
  console.log(colors.red('\n⚠ 警告:卸载将删除所有配置!'));
1456
2555
  const confirmUninstall = await askQuestion('确认卸载?(y/N): ');
1457
2556
  if (confirmUninstall.toLowerCase() === 'y') {
@@ -1465,7 +2564,7 @@ async function showInteractiveMenu(existing) {
1465
2564
  console.log(colors.gray('\n再见!'));
1466
2565
  process.exit(0);
1467
2566
  default:
1468
- log.warn('无效选项,请输入 0-8');
2567
+ log.warn('无效选项,请输入 0-11');
1469
2568
  }
1470
2569
  }
1471
2570
  }
@@ -1492,7 +2591,7 @@ async function main() {
1492
2591
  const latestResult = safeExec('npm view openclawsetup version 2>/dev/null');
1493
2592
  if (latestResult.ok && latestResult.output) {
1494
2593
  const latestVersion = latestResult.output.trim();
1495
- if (latestVersion && latestVersion !== currentVersion) {
2594
+ if (latestVersion && compareSemver(currentVersion, latestVersion) < 0) {
1496
2595
  console.log(colors.yellow(`\n⚠ 当前版本 ${currentVersion},最新版本 ${latestVersion}`));
1497
2596
  console.log(colors.yellow(' 正在更新到最新版本...\n'));
1498
2597
  const updateCmd = platform() === 'win32'
@@ -1525,12 +2624,27 @@ async function main() {
1525
2624
  console.log(colors.green(`\n✓ 检测到已安装: ${existing.name}`));
1526
2625
 
1527
2626
  if (options.check) {
1528
- await runHealthCheck(existing.name, false);
2627
+ await runHealthCheck(existing.name, false, options.strong);
2628
+ process.exit(0);
2629
+ }
2630
+
2631
+ if (options.optimizeToken) {
2632
+ await optimizeTokenUsage(existing.name);
2633
+ process.exit(0);
2634
+ }
2635
+
2636
+ if (options.collectEvidence || options.evidenceQuick) {
2637
+ await collectEvidencePackage({ quick: options.evidenceQuick });
1529
2638
  process.exit(0);
1530
2639
  }
1531
2640
 
1532
2641
  if (options.fix) {
1533
- await runHealthCheck(existing.name, true);
2642
+ await runHealthCheck(existing.name, true, options.strong);
2643
+ process.exit(0);
2644
+ }
2645
+
2646
+ if (options.strongFix) {
2647
+ await runHealthCheck(existing.name, true, true);
1534
2648
  process.exit(0);
1535
2649
  }
1536
2650
 
@@ -1552,6 +2666,16 @@ async function main() {
1552
2666
  }
1553
2667
  }
1554
2668
 
2669
+ if (options.collectEvidence || options.evidenceQuick) {
2670
+ await collectEvidencePackage({ quick: options.evidenceQuick });
2671
+ process.exit(0);
2672
+ }
2673
+
2674
+ if (options.optimizeToken) {
2675
+ await optimizeTokenUsage('openclaw');
2676
+ process.exit(0);
2677
+ }
2678
+
1555
2679
  // 安装 CLI
1556
2680
  const cliName = await installOpenClaw();
1557
2681