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/README.md +74 -0
- package/bin/cli.mjs +1380 -256
- package/package.json +1 -1
- package//344/275/277/347/224/250/350/257/264/346/230/216.md +73 -1
package/bin/cli.mjs
CHANGED
|
@@ -11,8 +11,19 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { exec, execSync, spawnSync } from 'child_process';
|
|
14
|
-
import {
|
|
15
|
-
|
|
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, {
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
225
|
-
if (
|
|
226
|
-
return { installed: true, name:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1925
|
+
if (strongMode) {
|
|
1926
|
+
log.hint('强力模式已启用:多轮复检 + 深度回收修复');
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
let activeCli = cliName;
|
|
911
1930
|
const fixed = [];
|
|
912
|
-
|
|
1931
|
+
let finalIssues = [];
|
|
1932
|
+
const totalPasses = strongMode && autoFix ? STRONG_FIX_MAX_PASSES : 1;
|
|
913
1933
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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('
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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('
|
|
1047
|
-
const
|
|
1048
|
-
if (
|
|
1049
|
-
|
|
1050
|
-
|
|
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(
|
|
1056
|
-
fixed.push(
|
|
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
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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(
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
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((
|
|
2245
|
+
fixed.forEach((item, index) => console.log(colors.green(` ${index + 1}. ${item}`)));
|
|
1177
2246
|
console.log('');
|
|
1178
2247
|
}
|
|
1179
2248
|
|
|
1180
|
-
if (
|
|
1181
|
-
|
|
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(`⚠ 发现 ${
|
|
2256
|
+
console.log(colors.bold(colors.yellow(`⚠ 发现 ${finalIssues.length} 个问题:`)));
|
|
1184
2257
|
console.log('='.repeat(50));
|
|
1185
2258
|
|
|
1186
|
-
|
|
2259
|
+
finalIssues.forEach((issue, index) => {
|
|
1187
2260
|
const icon = issue.level === 'error' ? colors.red('❌') : colors.yellow('⚠');
|
|
1188
|
-
console.log(`\n${icon} ${colors.bold(`问题 ${
|
|
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 &&
|
|
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
|
|
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')}.
|
|
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-
|
|
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
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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-
|
|
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
|
|
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
|
|