evolclaw 3.3.0 → 3.4.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/CHANGELOG.md +36 -0
- package/README.md +7 -3
- package/dist/agents/claude-runner.js +23 -27
- package/dist/agents/codex-runner.js +90 -6
- package/dist/agents/runner-types.js +30 -0
- package/dist/aun/outbox.js +14 -2
- package/dist/channels/aun.js +506 -108
- package/dist/channels/feishu.js +29 -5
- package/dist/cli/agent-command.js +591 -0
- package/dist/cli/agent.js +15 -3
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +12 -5027
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/core/channel-loader.js +4 -1
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +81 -0
- package/dist/core/evolagent.js +16 -0
- package/dist/core/message/im-renderer.js +67 -49
- package/dist/core/message/message-bridge.js +30 -9
- package/dist/core/message/message-processor.js +200 -122
- package/dist/core/message/message-queue.js +68 -0
- package/dist/core/permission.js +16 -0
- package/dist/core/session/session-manager.js +59 -13
- package/dist/core/stats/db.js +20 -0
- package/dist/core/stats/writer.js +3 -3
- package/dist/data/error-dict.json +7 -0
- package/dist/index.js +49 -6
- package/dist/ipc.js +99 -0
- package/dist/utils/cross-platform.js +35 -0
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +63 -6
- package/kits/eck_manifest.json +0 -12
- package/package.json +2 -3
- package/dist/core/command-handler.js +0 -4235
- package/dist/core/message/response-depth.js +0 -56
- package/kits/templates/system-fragments/response-depth.md +0 -16
|
@@ -0,0 +1,2707 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { spawn, execFileSync, execFile } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot, agentMdPath } from '../paths.js';
|
|
8
|
+
import { loadDefaults, loadAllAgents, mergeForAgent, loadEvolclawConfig, saveEvolclawConfig } from '../config-store.js';
|
|
9
|
+
import { resolveAnthropicConfig } from '../agents/baseagent.js';
|
|
10
|
+
import { migrateProject } from '../config-store.js';
|
|
11
|
+
import { ipcQuery } from '../ipc.js';
|
|
12
|
+
import { isHelpFlag } from './help.js';
|
|
13
|
+
import { cmdInit, needsControlAidInit, initTail } from './init.js';
|
|
14
|
+
import * as platform from '../utils/cross-platform.js';
|
|
15
|
+
import { EventBus } from '../core/event-bus.js';
|
|
16
|
+
import { tryUpgrade, tryUpgradeAunSdk, tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-ops.js';
|
|
17
|
+
import { fetchEcwebPairCode } from '../utils/ecweb-pair.js';
|
|
18
|
+
import { resolveEcwebLaunchCommand } from '../utils/ecweb-launch.js';
|
|
19
|
+
import { resolveAunCoreSdkPkg, AUN_CORE_SDK_PKG } from '../aun/aid/client.js';
|
|
20
|
+
import { scanInstances, cleanupInstances, findOrphanProcesses, killOrphans } from '../utils/instance-registry.js';
|
|
21
|
+
import { filterLogFiles, deriveLogTypes, computePreChecked, validateLogTypes, shortLogName as shortLogNameLocal } from './watch-logs.js';
|
|
22
|
+
import { displaySessionTitle } from '../core/session/session-title.js';
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
24
|
+
// 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
|
|
25
|
+
export function cleanEnv() {
|
|
26
|
+
for (const key of [
|
|
27
|
+
'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT',
|
|
28
|
+
'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
|
29
|
+
'CLAUDE_CONFIG_DIR',
|
|
30
|
+
]) {
|
|
31
|
+
delete process.env[key];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 启动时归档过大的 stdout.log。其它 .log 文件由各自的 LogWriter 管理切片/清理,
|
|
36
|
+
* 不再扫描整个目录——LogWriter 模式下重复的 size 检查会和 hourly rotation 冲突。
|
|
37
|
+
*/
|
|
38
|
+
function rotateStdoutIfNeeded(logDir) {
|
|
39
|
+
if (!fs.existsSync(logDir))
|
|
40
|
+
return;
|
|
41
|
+
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
|
42
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
43
|
+
const stdoutLog = path.join(logDir, 'stdout.log');
|
|
44
|
+
// 归档当前 stdout.log(若超过 10MB)
|
|
45
|
+
try {
|
|
46
|
+
const stat = fs.statSync(stdoutLog);
|
|
47
|
+
if (stat.size > MAX_SIZE) {
|
|
48
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
49
|
+
const newPath = `${stdoutLog}.${timestamp}`;
|
|
50
|
+
fs.renameSync(stdoutLog, newPath);
|
|
51
|
+
console.log(` Rotated: stdout.log -> ${path.basename(newPath)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { /* file not exist */ }
|
|
55
|
+
// 清理 7 天前的 stdout.log.* 归档
|
|
56
|
+
try {
|
|
57
|
+
for (const file of fs.readdirSync(logDir)) {
|
|
58
|
+
if (!file.startsWith('stdout.log.'))
|
|
59
|
+
continue;
|
|
60
|
+
const full = path.join(logDir, file);
|
|
61
|
+
try {
|
|
62
|
+
if (fs.statSync(full).mtimeMs < cutoff)
|
|
63
|
+
fs.unlinkSync(full);
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
}
|
|
70
|
+
function countLines(pkgRoot, logDir) {
|
|
71
|
+
// 生产安装:pkgRoot/src/;开发模式:pkgRoot 是 dist/,src/ 在其兄弟目录
|
|
72
|
+
const srcCandidate = path.join(pkgRoot, 'src');
|
|
73
|
+
const srcDir = fs.existsSync(srcCandidate)
|
|
74
|
+
? srcCandidate
|
|
75
|
+
: path.join(pkgRoot, '..', 'src');
|
|
76
|
+
const statsFile = path.join(logDir, 'line-stats.log');
|
|
77
|
+
const countDir = (dir) => {
|
|
78
|
+
if (!fs.existsSync(dir))
|
|
79
|
+
return 0;
|
|
80
|
+
let total = 0;
|
|
81
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
82
|
+
const full = path.join(dir, entry.name);
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
total += countDir(full);
|
|
85
|
+
}
|
|
86
|
+
else if (entry.name.endsWith('.ts')) {
|
|
87
|
+
total += fs.readFileSync(full, 'utf-8').split('\n').length;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return total;
|
|
91
|
+
};
|
|
92
|
+
const countFile = (filePath) => {
|
|
93
|
+
if (!fs.existsSync(filePath))
|
|
94
|
+
return 0;
|
|
95
|
+
return fs.readFileSync(filePath, 'utf-8').split('\n').length;
|
|
96
|
+
};
|
|
97
|
+
console.log('\n[launcher] 正在统计代码行数...\n');
|
|
98
|
+
const core = countDir(path.join(srcDir, 'core'));
|
|
99
|
+
const agents = countDir(path.join(srcDir, 'agents'));
|
|
100
|
+
const channels = countDir(path.join(srcDir, 'channels'));
|
|
101
|
+
const utils = countDir(path.join(srcDir, 'utils'));
|
|
102
|
+
const cli = countDir(path.join(srcDir, 'cli'));
|
|
103
|
+
const aun = countDir(path.join(srcDir, 'aun'));
|
|
104
|
+
const entry = countFile(path.join(srcDir, 'index.ts'))
|
|
105
|
+
+ countFile(path.join(srcDir, 'config-store.ts'))
|
|
106
|
+
+ countFile(path.join(srcDir, 'types.ts'))
|
|
107
|
+
+ countFile(path.join(srcDir, 'ipc.ts'))
|
|
108
|
+
+ countFile(path.join(srcDir, 'paths.ts'));
|
|
109
|
+
const total = core + agents + channels + utils + cli + aun + entry;
|
|
110
|
+
console.log('==================================================');
|
|
111
|
+
console.log('EvolClaw 代码统计');
|
|
112
|
+
console.log('==================================================');
|
|
113
|
+
console.log(`核心模块: ${String(core).padStart(8)} 行`);
|
|
114
|
+
console.log(`Agent 模块: ${String(agents).padStart(8)} 行`);
|
|
115
|
+
console.log(`渠道适配: ${String(channels).padStart(8)} 行`);
|
|
116
|
+
console.log(`工具库: ${String(utils).padStart(8)} 行`);
|
|
117
|
+
console.log(`CLI: ${String(cli).padStart(8)} 行`);
|
|
118
|
+
console.log(`AUN 协议: ${String(aun).padStart(8)} 行`);
|
|
119
|
+
console.log(`入口与配置: ${String(entry).padStart(8)} 行`);
|
|
120
|
+
console.log('--------------------------------------------------');
|
|
121
|
+
console.log(`总计: ${String(total).padStart(8)} 行`);
|
|
122
|
+
console.log('==================================================');
|
|
123
|
+
// 追加历史记录(仅在数据变化时)
|
|
124
|
+
let shouldAppend = true;
|
|
125
|
+
let prevTotal = 0;
|
|
126
|
+
if (fs.existsSync(statsFile)) {
|
|
127
|
+
const lines = fs.readFileSync(statsFile, 'utf-8').trim().split('\n');
|
|
128
|
+
if (lines.length > 0) {
|
|
129
|
+
const lastLine = lines[lines.length - 1];
|
|
130
|
+
const parts = lastLine.split('\t');
|
|
131
|
+
// 旧格式8列: time core agents channels utils entry total delta → total at [6]
|
|
132
|
+
// 新格式10列: time core agents channels utils cli aun entry total delta → total at [8]
|
|
133
|
+
const lastTotalStr = parts.length >= 10 ? parts[8] : parts[6];
|
|
134
|
+
prevTotal = parseInt(lastTotalStr ?? parts[parts.length - 2], 10) || 0;
|
|
135
|
+
if (prevTotal === total) {
|
|
136
|
+
shouldAppend = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (shouldAppend) {
|
|
141
|
+
const _d = new Date();
|
|
142
|
+
const _p = (n) => String(n).padStart(2, '0');
|
|
143
|
+
const now = `${_d.getFullYear()}-${_p(_d.getMonth() + 1)}-${_p(_d.getDate())} ${_p(_d.getHours())}:${_p(_d.getMinutes())}:${_p(_d.getSeconds())}`;
|
|
144
|
+
const delta = total - prevTotal;
|
|
145
|
+
const deltaStr = delta >= 0 ? `+${delta}` : `${delta}`;
|
|
146
|
+
fs.appendFileSync(statsFile, `${now}\t${core}\t${agents}\t${channels}\t${utils}\t${cli}\t${aun}\t${entry}\t${total}\t${deltaStr}\n`);
|
|
147
|
+
}
|
|
148
|
+
showHistory(statsFile);
|
|
149
|
+
}
|
|
150
|
+
function showHistory(statsFile) {
|
|
151
|
+
if (!fs.existsSync(statsFile))
|
|
152
|
+
return;
|
|
153
|
+
const lines = fs.readFileSync(statsFile, 'utf-8').trim().split('\n');
|
|
154
|
+
if (lines.length < 2)
|
|
155
|
+
return;
|
|
156
|
+
const recent = lines.slice(-10);
|
|
157
|
+
console.log('\n==================================================');
|
|
158
|
+
console.log('历史记录(最近 10 次)');
|
|
159
|
+
console.log('==================================================');
|
|
160
|
+
console.log(`${'Time'.padEnd(19)} ${'Core'.padStart(6)} ${'Agent'.padStart(6)} ${'Chan'.padStart(6)} ${'Utils'.padStart(6)} ${'CLI'.padStart(6)} ${'AUN'.padStart(6)} ${'Entry'.padStart(6)} ${'Total'.padStart(6)} ${'Delta'.padStart(8)}`);
|
|
161
|
+
console.log('--------------------------------------------------');
|
|
162
|
+
let prevTotal = null;
|
|
163
|
+
for (const line of recent) {
|
|
164
|
+
const parts = line.split('\t');
|
|
165
|
+
// 格式演进:
|
|
166
|
+
// 旧6列: time core channels utils entry total
|
|
167
|
+
// 旧7列: time core agents channels utils entry total
|
|
168
|
+
// 旧8列: time core agents channels utils entry total delta
|
|
169
|
+
// 新10列: time core agents channels utils cli aun entry total delta
|
|
170
|
+
let time, c, a, ch, u, cl, au, e, t, d;
|
|
171
|
+
if (parts.length >= 10) {
|
|
172
|
+
[time, c, a, ch, u, cl, au, e, t, d] = parts;
|
|
173
|
+
}
|
|
174
|
+
else if (parts.length >= 8) {
|
|
175
|
+
// 旧8列: time core agents channels utils entry total delta
|
|
176
|
+
[time, c, a, ch, u, e, t, d] = parts;
|
|
177
|
+
cl = '-';
|
|
178
|
+
au = '-';
|
|
179
|
+
}
|
|
180
|
+
else if (parts.length >= 7) {
|
|
181
|
+
// 旧7列: time core agents channels utils entry total
|
|
182
|
+
[time, c, a, ch, u, e, t] = parts;
|
|
183
|
+
cl = '-';
|
|
184
|
+
au = '-';
|
|
185
|
+
}
|
|
186
|
+
else if (parts.length >= 6) {
|
|
187
|
+
[time, c, ch, u, e, t] = parts;
|
|
188
|
+
a = '-';
|
|
189
|
+
cl = '-';
|
|
190
|
+
au = '-';
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const total = parseInt(t, 10);
|
|
196
|
+
let diff;
|
|
197
|
+
if (d) {
|
|
198
|
+
diff = d;
|
|
199
|
+
}
|
|
200
|
+
else if (prevTotal !== null) {
|
|
201
|
+
const change = total - prevTotal;
|
|
202
|
+
diff = change >= 0 ? `+${change}` : `${change}`;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
diff = '-';
|
|
206
|
+
}
|
|
207
|
+
console.log(`${time.padEnd(19)} ${c.padStart(6)} ${a.padStart(6)} ${ch.padStart(6)} ${u.padStart(6)} ${cl.padStart(6)} ${au.padStart(6)} ${e.padStart(6)} ${t.padStart(6)} ${diff.padStart(8)}`);
|
|
208
|
+
prevTotal = total;
|
|
209
|
+
}
|
|
210
|
+
console.log('==================================================');
|
|
211
|
+
}
|
|
212
|
+
// ==================== Commands ====================
|
|
213
|
+
/**
|
|
214
|
+
* 检测并展示跨 HOME 残留的 evolclaw 主进程。
|
|
215
|
+
*
|
|
216
|
+
* 这些孤儿不在自己 HOME 的 instance/ 登记簿内,instance-registry 的常规清理
|
|
217
|
+
* (cleanupInstances)够不到。常见来源:
|
|
218
|
+
* - 测试套件 spawn 后未在 afterAll 杀子进程
|
|
219
|
+
* - 旧版本 pidfile 模式遗留(升级后 record 缺失)
|
|
220
|
+
*
|
|
221
|
+
* 仅打印提示,不主动杀;调用方决定是否清理。
|
|
222
|
+
*/
|
|
223
|
+
function reportOrphans(orphans) {
|
|
224
|
+
if (orphans.length === 0)
|
|
225
|
+
return;
|
|
226
|
+
console.log(`⚠ 检测到 ${orphans.length} 个未登记的 evolclaw 主进程(跨 HOME 残留):`);
|
|
227
|
+
for (const o of orphans) {
|
|
228
|
+
const home = o.evolclawHome ?? '未知';
|
|
229
|
+
console.log(` PID ${o.pid} EVOLCLAW_HOME=${home}`);
|
|
230
|
+
}
|
|
231
|
+
console.log(' 这些进程不属于当前 HOME 的实例登记簿,自动清理不会处理它们。');
|
|
232
|
+
console.log(' 使用 evolclaw restart --clear 一并清掉,或手动 kill。');
|
|
233
|
+
}
|
|
234
|
+
function formatLocalTime(ms) {
|
|
235
|
+
const d = new Date(ms);
|
|
236
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
|
|
237
|
+
}
|
|
238
|
+
function printStartupInfo(opts = {}) {
|
|
239
|
+
const pkgRoot = getPackageRoot();
|
|
240
|
+
const isNpmInstall = pkgRoot.includes('node_modules');
|
|
241
|
+
const cliRunsSource = !import.meta.url.includes('/dist/');
|
|
242
|
+
const daemonEntry = path.join(pkgRoot, 'dist', 'index.js');
|
|
243
|
+
const daemonRunsDist = fs.existsSync(daemonEntry);
|
|
244
|
+
const scanDir = path.join(pkgRoot, daemonRunsDist ? 'dist' : 'src');
|
|
245
|
+
let latestMtime = 0;
|
|
246
|
+
const scanRecursive = (dir) => {
|
|
247
|
+
try {
|
|
248
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
249
|
+
if (entry.name === 'node_modules')
|
|
250
|
+
continue;
|
|
251
|
+
const full = path.join(dir, entry.name);
|
|
252
|
+
if (entry.isDirectory()) {
|
|
253
|
+
scanRecursive(full);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
|
|
257
|
+
const mt = fs.statSync(full).mtimeMs;
|
|
258
|
+
if (mt > latestMtime)
|
|
259
|
+
latestMtime = mt;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch { }
|
|
264
|
+
};
|
|
265
|
+
scanRecursive(scanDir);
|
|
266
|
+
let version = '?';
|
|
267
|
+
try {
|
|
268
|
+
version = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8')).version;
|
|
269
|
+
}
|
|
270
|
+
catch { }
|
|
271
|
+
let aunVer = null;
|
|
272
|
+
try {
|
|
273
|
+
aunVer = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'node_modules', '@agentunion', 'fastaun', 'package.json'), 'utf-8')).version;
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
const pidPart = opts.pid ? ` (PID: ${opts.pid})` : '';
|
|
277
|
+
const aunPart = aunVer ? ` fastaun v${aunVer}` : '';
|
|
278
|
+
const prefix = opts.running ? '✓ EvolClaw is running , v' : ' EvolClaw v';
|
|
279
|
+
console.log(`${prefix}${version}${pidPart}${aunPart}`);
|
|
280
|
+
console.log(` 包路径: ${pkgRoot}`);
|
|
281
|
+
console.log(` 安装类型: ${isNpmInstall ? 'npm全局安装' : '开发仓(link)'}`);
|
|
282
|
+
console.log(` CLI执行: ${cliRunsSource ? '源码(tsx)' : '编译产物(dist)'}`);
|
|
283
|
+
console.log(` Daemon执行: ${daemonRunsDist ? '编译产物(dist)' : '未知'}`);
|
|
284
|
+
console.log(` 代码时间: ${latestMtime ? formatLocalTime(latestMtime) : '?'}`);
|
|
285
|
+
}
|
|
286
|
+
export async function cmdStart() {
|
|
287
|
+
const cmdStartedAt = Date.now();
|
|
288
|
+
printStartupInfo();
|
|
289
|
+
const p = resolvePaths();
|
|
290
|
+
ensureDataDirs();
|
|
291
|
+
// 旧配置自动迁移(evolclaw.json → 新结构)
|
|
292
|
+
const { autoMigrateIfNeeded } = await import('../config-store.js');
|
|
293
|
+
autoMigrateIfNeeded();
|
|
294
|
+
// 未初始化时自动引导
|
|
295
|
+
const defaults = loadDefaults();
|
|
296
|
+
if (!defaults || !defaults.baseagents || Object.keys(defaults.baseagents).length === 0) {
|
|
297
|
+
console.log('⚡ 未检测到初始化配置,自动启动初始化向导...\n');
|
|
298
|
+
await cmdInit();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// 控制 AID 门禁:缺 aid 且交互式 → 只补全控制 AID + owners(不重走 baseagent 向导)。
|
|
302
|
+
// 非 TTY(restart-monitor/systemd/管道)不补全(无法交互),只提示后继续启动,daemon 侧 warn 兜底。
|
|
303
|
+
const evolclawCfgStart = loadEvolclawConfig();
|
|
304
|
+
if (needsControlAidInit(evolclawCfgStart.aid, !!process.stdin.isTTY)) {
|
|
305
|
+
console.log('⚡ 控制 AID 未配置,自动补全...\n');
|
|
306
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
307
|
+
suppressSdkLogs();
|
|
308
|
+
await initTail();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (!evolclawCfgStart.aid) {
|
|
312
|
+
console.log('⚠ 控制 AID 未配置(非交互式启动,跳过补全)。如需进程身份/远程管理,请运行 evolclaw init');
|
|
313
|
+
}
|
|
314
|
+
else if (process.stdin.isTTY) {
|
|
315
|
+
// 证书缺失时在 CLI 侧提示,daemon 是后台进程无终端不做交互
|
|
316
|
+
const certKey = path.join(resolvePaths().root, 'AIDs', evolclawCfgStart.aid, 'private', 'key.json');
|
|
317
|
+
if (!fs.existsSync(certKey)) {
|
|
318
|
+
console.log(`⚠ 控制 AID 证书缺失:${evolclawCfgStart.aid}`);
|
|
319
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
320
|
+
const ans = await new Promise(res => rl.question(' [1] 继续启动 [2] 重新生成 AID [3] 退出 [1/2/3]: ', res));
|
|
321
|
+
rl.close();
|
|
322
|
+
if (ans.trim() === '3') {
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
if (ans.trim() === '2') {
|
|
326
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
327
|
+
suppressSdkLogs();
|
|
328
|
+
const { generateControlAid } = await import('../aun/aid/control-aid.js');
|
|
329
|
+
const result = await generateControlAid();
|
|
330
|
+
saveEvolclawConfig({ ...loadEvolclawConfig(), aid: result.aid });
|
|
331
|
+
console.log(`✓ 新控制 AID: ${result.aid}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// 检查至少有一个 self-agent
|
|
336
|
+
const { agents, skipped } = loadAllAgents();
|
|
337
|
+
if (agents.length === 0) {
|
|
338
|
+
console.log('❌ 未配置任何 self-agent。');
|
|
339
|
+
console.log('');
|
|
340
|
+
console.log('创建方式:');
|
|
341
|
+
console.log(' 1. 下载 Evol App(https://evolai.cn)→ 创建 Agent → 将引导文本输入给 baseagent 执行');
|
|
342
|
+
console.log(' 2. 手动创建:evolclaw agent new <your-aid>.agentid.pub');
|
|
343
|
+
console.log('');
|
|
344
|
+
if (skipped.length > 0) {
|
|
345
|
+
console.log(`跳过的目录:`);
|
|
346
|
+
for (const s of skipped)
|
|
347
|
+
console.log(` - ${s.dirName}: ${s.reason}`);
|
|
348
|
+
}
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
// 检查 instance 目录中的进程状态
|
|
352
|
+
const status = scanInstances();
|
|
353
|
+
const aliveMains = status.mains.filter(m => m.alive);
|
|
354
|
+
if (aliveMains.length > 0) {
|
|
355
|
+
const first = aliveMains[0];
|
|
356
|
+
console.log(` EvolClaw is already running (PID: ${aliveMains.map(m => m.record.pid).join(', ')})`);
|
|
357
|
+
console.log(` 启动于: ${new Date(first.record.startedAtIso).toLocaleString()}`);
|
|
358
|
+
console.log(` 启动方式: ${first.record.launchedBy}`);
|
|
359
|
+
// 报告 AID 状态
|
|
360
|
+
if (status.aidLastActivity.size > 0) {
|
|
361
|
+
console.log(' AID 状态:');
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
for (const [aid, info] of status.aidLastActivity) {
|
|
364
|
+
const ago = formatTimeAgo(now - info.ts);
|
|
365
|
+
const symbol = info.event === 'disconnected' ? '✗' : '✓';
|
|
366
|
+
console.log(` ${symbol} ${aid} — 最后活动 ${ago} (${info.event})`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
console.log(' 使用 evolclaw restart 重启,或 evolclaw stop 先停止');
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
// 清理残留进程和文件
|
|
373
|
+
if (status.mains.length > 0 || status.restartMonitors.length > 0) {
|
|
374
|
+
const killed = cleanupInstances();
|
|
375
|
+
if (killed.length > 0) {
|
|
376
|
+
console.log(`⚠ 清理了 ${killed.length} 个残留进程: ${killed.join(', ')}`);
|
|
377
|
+
await sleep(2000);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// 跨 HOME 孤儿(未登记进程)只警告,不动
|
|
381
|
+
reportOrphans(findOrphanProcesses());
|
|
382
|
+
console.log('🚀 Starting EvolClaw...');
|
|
383
|
+
rotateStdoutIfNeeded(p.logs);
|
|
384
|
+
cleanEnv();
|
|
385
|
+
// 删除旧的 ready signal
|
|
386
|
+
try {
|
|
387
|
+
fs.unlinkSync(p.readySignal);
|
|
388
|
+
}
|
|
389
|
+
catch { }
|
|
390
|
+
const stdoutLog = path.join(p.logs, 'stdout.log');
|
|
391
|
+
const out = fs.openSync(stdoutLog, 'a');
|
|
392
|
+
const err = fs.openSync(stdoutLog, 'a');
|
|
393
|
+
const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
394
|
+
const child = spawn('node', ['--no-warnings=ExperimentalWarning', appMain], {
|
|
395
|
+
detached: true,
|
|
396
|
+
stdio: ['ignore', out, err],
|
|
397
|
+
windowsHide: true,
|
|
398
|
+
env: {
|
|
399
|
+
...process.env,
|
|
400
|
+
EVOLCLAW_HOME: p.root,
|
|
401
|
+
EVOLCLAW_LAUNCHED_BY: 'start',
|
|
402
|
+
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
|
403
|
+
MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
|
|
404
|
+
EVENT_LOG: process.env.EVENT_LOG || 'true',
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
const childPid = child.pid;
|
|
408
|
+
child.unref();
|
|
409
|
+
// 等待 ready signal(最多 30 秒,AUN sidecar 超时 15s + 其他通道连接)
|
|
410
|
+
const startTime = Date.now();
|
|
411
|
+
const checkReady = async () => {
|
|
412
|
+
// ready signal 出现(优先检查,避免 Windows 上误判进程状态)
|
|
413
|
+
if (fs.existsSync(p.readySignal)) {
|
|
414
|
+
console.log(`✓ EvolClaw started successfully (PID: ${childPid})`);
|
|
415
|
+
console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
|
|
416
|
+
console.log(` Logs: ${p.logs}/`);
|
|
417
|
+
// 从主日志提取渠道连接摘要
|
|
418
|
+
const mainLog = findLatestLog(p.logs, 'evolclaw');
|
|
419
|
+
if (mainLog) {
|
|
420
|
+
const logLines = fs.readFileSync(mainLog, 'utf-8').split('\n');
|
|
421
|
+
// 从末尾往前找最近一次启动的摘要
|
|
422
|
+
let channelSummary = '';
|
|
423
|
+
for (let i = logLines.length - 1; i >= 0; i--) {
|
|
424
|
+
if (logLines[i].includes('EvolClaw is running with')) {
|
|
425
|
+
channelSummary = logLines[i];
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (channelSummary) {
|
|
430
|
+
const match = channelSummary.match(/running with .+/);
|
|
431
|
+
if (match)
|
|
432
|
+
console.log(` ${match[0]}`);
|
|
433
|
+
}
|
|
434
|
+
// 最近一次启动的失败信息
|
|
435
|
+
let lastReadyIdx = -1;
|
|
436
|
+
for (let i = logLines.length - 1; i >= 0; i--) {
|
|
437
|
+
if (logLines[i].includes('Ready signal written')) {
|
|
438
|
+
lastReadyIdx = i;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (lastReadyIdx > 0) {
|
|
443
|
+
for (let i = Math.max(0, lastReadyIdx - 20); i < lastReadyIdx; i++) {
|
|
444
|
+
const line = logLines[i];
|
|
445
|
+
if (line.includes('failed to connect') || line.includes('Failed to create channel')) {
|
|
446
|
+
const match = line.match(/\[WARN\]\s*(.+)/);
|
|
447
|
+
console.log(` ⚠ ${match ? match[1] : line.trim()}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
console.log('');
|
|
453
|
+
// 代码统计仅在开发环境显示(EVOLCLAW_HOME 指向包目录)
|
|
454
|
+
if (resolveRoot() === getPackageRoot()) {
|
|
455
|
+
countLines(getPackageRoot(), p.logs);
|
|
456
|
+
}
|
|
457
|
+
console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
|
|
458
|
+
// ECWeb 自动后台启动
|
|
459
|
+
await startEcwebIfEnabled(p);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// 超时
|
|
463
|
+
if (Date.now() - startTime > 30000) {
|
|
464
|
+
console.log('❌ Failed to start EvolClaw (ready signal timeout)');
|
|
465
|
+
console.log('');
|
|
466
|
+
console.log('📝 Error details (last 10 lines of stdout):');
|
|
467
|
+
if (fs.existsSync(stdoutLog)) {
|
|
468
|
+
const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
|
|
469
|
+
console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
|
|
470
|
+
}
|
|
471
|
+
console.log(`⏱ failed after ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
// 进程已退出且无 ready signal
|
|
476
|
+
if (!platform.isProcessRunning(childPid)) {
|
|
477
|
+
// 给进程一点时间写 ready signal(可能刚好在写入中)
|
|
478
|
+
if (Date.now() - startTime > 3000) {
|
|
479
|
+
console.log('❌ Failed to start EvolClaw');
|
|
480
|
+
console.log('');
|
|
481
|
+
console.log('📝 Error details (last 10 lines of stdout):');
|
|
482
|
+
if (fs.existsSync(stdoutLog)) {
|
|
483
|
+
const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
|
|
484
|
+
console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
|
|
485
|
+
}
|
|
486
|
+
console.log(`⏱ failed after ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
setTimeout(checkReady, 500);
|
|
492
|
+
};
|
|
493
|
+
setTimeout(checkReady, 1000);
|
|
494
|
+
}
|
|
495
|
+
async function stopPid(pid) {
|
|
496
|
+
console.log(`🛑 Stopping EvolClaw (PID: ${pid})...`);
|
|
497
|
+
platform.killProcess(pid);
|
|
498
|
+
await new Promise((resolve) => {
|
|
499
|
+
let waited = 0;
|
|
500
|
+
const check = setInterval(() => {
|
|
501
|
+
waited++;
|
|
502
|
+
if (!platform.isProcessRunning(pid)) {
|
|
503
|
+
clearInterval(check);
|
|
504
|
+
console.log('✓ EvolClaw stopped');
|
|
505
|
+
resolve();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (waited >= 10) {
|
|
509
|
+
clearInterval(check);
|
|
510
|
+
platform.killProcess(pid, true);
|
|
511
|
+
console.log('✓ EvolClaw stopped (forced)');
|
|
512
|
+
resolve();
|
|
513
|
+
}
|
|
514
|
+
}, 1000);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
export async function cmdStop() {
|
|
518
|
+
const status = scanInstances();
|
|
519
|
+
const aliveMains = status.mains.filter(m => m.alive);
|
|
520
|
+
if (aliveMains.length === 0) {
|
|
521
|
+
console.log('⚠ EvolClaw is not running');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
await Promise.all(aliveMains.map(m => stopPid(m.record.pid)));
|
|
525
|
+
await sleep(500);
|
|
526
|
+
cleanupInstances();
|
|
527
|
+
if (aliveMains.length > 1) {
|
|
528
|
+
console.log(`⚠ 停止了 ${aliveMains.length} 个 main 实例: ${aliveMains.map(m => m.record.pid).join(', ')}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
export async function cmdRestart(opts = {}) {
|
|
532
|
+
const cmdStartedAt = Date.now();
|
|
533
|
+
console.log('🔄 Restarting EvolClaw...');
|
|
534
|
+
// 版本检查与自动升级
|
|
535
|
+
console.log('📦 Checking for updates...');
|
|
536
|
+
const upgrade = await tryUpgrade();
|
|
537
|
+
switch (upgrade.status) {
|
|
538
|
+
case 'upgraded':
|
|
539
|
+
console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
|
|
540
|
+
break;
|
|
541
|
+
case 'no-update':
|
|
542
|
+
console.log(`✓ Already up to date (${upgrade.from})`);
|
|
543
|
+
break;
|
|
544
|
+
case 'skipped':
|
|
545
|
+
console.log(upgrade.error
|
|
546
|
+
? '⏭ Skipped upgrade (network unavailable)'
|
|
547
|
+
: '⏭ Skipped upgrade check (dev mode)');
|
|
548
|
+
break;
|
|
549
|
+
case 'failed':
|
|
550
|
+
console.log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}), continuing with current version`);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
// AUN SDK 版本检查与升级
|
|
554
|
+
const aunUpgrade = await tryUpgradeAunSdk(resolveAunCoreSdkPkg, AUN_CORE_SDK_PKG);
|
|
555
|
+
switch (aunUpgrade.status) {
|
|
556
|
+
case 'upgraded':
|
|
557
|
+
console.log(`✅ AUN SDK upgraded: ${aunUpgrade.from} → ${aunUpgrade.to}`);
|
|
558
|
+
break;
|
|
559
|
+
case 'no-update':
|
|
560
|
+
break; // silent
|
|
561
|
+
case 'failed':
|
|
562
|
+
console.log(`⚠ AUN SDK upgrade failed (${aunUpgrade.from} → ${aunUpgrade.to})`);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
// evolclaw-web 版本检查与升级(已安装才检查,与 AUN SDK 同级)
|
|
566
|
+
const ecwebUpgrade = await tryUpgradeGlobalPkg(() => resolveGlobalPkg('evolclaw-web'), 'evolclaw-web');
|
|
567
|
+
switch (ecwebUpgrade.status) {
|
|
568
|
+
case 'upgraded':
|
|
569
|
+
console.log(`✅ evolclaw-web upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
|
|
570
|
+
break;
|
|
571
|
+
case 'no-update':
|
|
572
|
+
break; // silent
|
|
573
|
+
case 'failed':
|
|
574
|
+
console.log(`⚠ evolclaw-web upgrade failed (${ecwebUpgrade.from} → ${ecwebUpgrade.to})`);
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
// 停止所有活 main 进程(可能不止一个)
|
|
578
|
+
const status = scanInstances();
|
|
579
|
+
const aliveMains = status.mains.filter(m => m.alive);
|
|
580
|
+
if (aliveMains.length > 0) {
|
|
581
|
+
if (aliveMains.length > 1) {
|
|
582
|
+
console.log(`⚠ 检测到 ${aliveMains.length} 个 main 实例,将一并停止: ${aliveMains.map(m => m.record.pid).join(', ')}`);
|
|
583
|
+
}
|
|
584
|
+
await Promise.all(aliveMains.map(m => stopPid(m.record.pid)));
|
|
585
|
+
await sleep(500);
|
|
586
|
+
}
|
|
587
|
+
cleanupInstances();
|
|
588
|
+
// 孤儿处理:同 HOME 的孤儿无条件 kill(restart 必须替换旧实例);
|
|
589
|
+
// 跨 HOME 的孤儿只在 --clear 时 kill,否则仅警告。
|
|
590
|
+
{
|
|
591
|
+
const orphans = findOrphanProcesses();
|
|
592
|
+
const currentHome = resolveRoot();
|
|
593
|
+
const sameHome = orphans.filter(o => o.evolclawHome === currentHome);
|
|
594
|
+
const otherHome = orphans.filter(o => o.evolclawHome !== currentHome);
|
|
595
|
+
if (sameHome.length > 0) {
|
|
596
|
+
const killed = killOrphans(sameHome);
|
|
597
|
+
console.log(`☠ 已 SIGKILL ${killed.length} 个同 HOME 孤儿进程: ${killed.join(', ')}`);
|
|
598
|
+
await sleep(500);
|
|
599
|
+
}
|
|
600
|
+
if (opts.clear && otherHome.length > 0) {
|
|
601
|
+
const killed = killOrphans(otherHome);
|
|
602
|
+
console.log(`☠ 已 SIGKILL ${killed.length} 个跨 HOME 孤儿进程: ${killed.join(', ')}`);
|
|
603
|
+
await sleep(500);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
console.log(`⏱ restart prep done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s, starting...`);
|
|
607
|
+
setTimeout(() => cmdStart(), 1000);
|
|
608
|
+
}
|
|
609
|
+
export async function cmdDev(args) {
|
|
610
|
+
const pkgRoot = getPackageRoot();
|
|
611
|
+
const isNpmInstall = pkgRoot.includes('node_modules');
|
|
612
|
+
const p = resolvePaths();
|
|
613
|
+
const devMarker = path.join(p.dataDir, 'dev-repo.path');
|
|
614
|
+
const sub = args[0];
|
|
615
|
+
if (!sub) {
|
|
616
|
+
if (!isNpmInstall) {
|
|
617
|
+
console.log(`当前: [dev] ${pkgRoot}`);
|
|
618
|
+
console.log('');
|
|
619
|
+
console.log('断开开发仓链接:');
|
|
620
|
+
console.log(' evolclaw dev off');
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
console.log(`当前: [pkg] ${pkgRoot}`);
|
|
624
|
+
console.log('');
|
|
625
|
+
let devPath = null;
|
|
626
|
+
try {
|
|
627
|
+
devPath = fs.readFileSync(devMarker, 'utf-8').trim();
|
|
628
|
+
}
|
|
629
|
+
catch { }
|
|
630
|
+
if (devPath && fs.existsSync(devPath)) {
|
|
631
|
+
console.log('链接到开发仓:');
|
|
632
|
+
console.log(` evolclaw dev on`);
|
|
633
|
+
console.log(` (已记录路径: ${devPath})`);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
console.log('链接到开发仓:');
|
|
637
|
+
console.log(' evolclaw dev <开发仓路径>');
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (sub === 'off') {
|
|
643
|
+
if (isNpmInstall) {
|
|
644
|
+
console.log('当前已是 [pkg] 模式,无需断开');
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
console.log('🔗 断开开发仓链接...');
|
|
648
|
+
let npmPrefix;
|
|
649
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
650
|
+
npmPrefix = path.join(process.env.APPDATA, 'npm');
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
npmPrefix = execFileSync('npm', ['prefix', '-g'], { encoding: 'utf-8', shell: true }).trim();
|
|
654
|
+
}
|
|
655
|
+
const linkPath = path.join(npmPrefix, 'node_modules', 'evolclaw');
|
|
656
|
+
const binPath = path.join(npmPrefix, 'evolclaw');
|
|
657
|
+
try {
|
|
658
|
+
fs.rmSync(linkPath, { recursive: true });
|
|
659
|
+
}
|
|
660
|
+
catch { }
|
|
661
|
+
try {
|
|
662
|
+
fs.unlinkSync(binPath);
|
|
663
|
+
}
|
|
664
|
+
catch { }
|
|
665
|
+
try {
|
|
666
|
+
fs.unlinkSync(binPath + '.cmd');
|
|
667
|
+
}
|
|
668
|
+
catch { }
|
|
669
|
+
try {
|
|
670
|
+
fs.unlinkSync(binPath + '.ps1');
|
|
671
|
+
}
|
|
672
|
+
catch { }
|
|
673
|
+
console.log('✓ 已断开');
|
|
674
|
+
console.log(` 已删除: ${linkPath}`);
|
|
675
|
+
console.log(` 如需恢复: evolclaw dev on(需从已安装的 evolclaw 执行)`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (sub === 'on') {
|
|
679
|
+
if (!isNpmInstall) {
|
|
680
|
+
console.log(`当前已是 [dev] 模式: ${pkgRoot}`);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
let devPath = null;
|
|
684
|
+
try {
|
|
685
|
+
devPath = fs.readFileSync(devMarker, 'utf-8').trim();
|
|
686
|
+
}
|
|
687
|
+
catch { }
|
|
688
|
+
if (!devPath || !fs.existsSync(devPath)) {
|
|
689
|
+
console.log('未记录开发仓路径');
|
|
690
|
+
console.log('');
|
|
691
|
+
console.log('用法: evolclaw dev <开发仓路径>');
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
console.log(`🔗 链接开发仓: ${devPath}`);
|
|
695
|
+
try {
|
|
696
|
+
execFileSync('npm', ['link'], { stdio: 'inherit', cwd: devPath, shell: true });
|
|
697
|
+
console.log(`✓ 已链接 [dev] ${devPath}`);
|
|
698
|
+
}
|
|
699
|
+
catch (e) {
|
|
700
|
+
console.error('❌ npm link 失败:', e.message);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
// evolclaw dev <path> — 记录路径 + 建立链接
|
|
706
|
+
const devPath = path.resolve(sub);
|
|
707
|
+
const pkgJson = path.join(devPath, 'package.json');
|
|
708
|
+
if (!fs.existsSync(pkgJson)) {
|
|
709
|
+
console.error(`❌ 路径不存在或无 package.json: ${devPath}`);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
let pkg;
|
|
713
|
+
try {
|
|
714
|
+
pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf-8'));
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
console.error(`❌ 无法解析 package.json: ${pkgJson}`);
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
if (pkg.name !== 'evolclaw') {
|
|
721
|
+
console.error(`❌ package.json name 不是 evolclaw(是 "${pkg.name}")`);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
// 已经链接到同一路径,只记录不重复 link
|
|
725
|
+
if (!isNpmInstall && path.resolve(pkgRoot) === devPath) {
|
|
726
|
+
fs.mkdirSync(p.dataDir, { recursive: true });
|
|
727
|
+
fs.writeFileSync(devMarker, devPath, 'utf-8');
|
|
728
|
+
console.log(`✓ 当前已链接到该路径,路径已记录`);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
console.log(`🔗 链接开发仓: ${devPath}`);
|
|
732
|
+
try {
|
|
733
|
+
execFileSync('npm', ['link'], { stdio: 'inherit', cwd: devPath, shell: true });
|
|
734
|
+
}
|
|
735
|
+
catch (e) {
|
|
736
|
+
console.error('❌ npm link 失败:', e.message);
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
fs.mkdirSync(p.dataDir, { recursive: true });
|
|
740
|
+
fs.writeFileSync(devMarker, devPath, 'utf-8');
|
|
741
|
+
console.log(`✓ 已链接 [dev] ${devPath}`);
|
|
742
|
+
console.log(` 路径已记录,下次可用 evolclaw dev on 快速切换`);
|
|
743
|
+
}
|
|
744
|
+
function formatTimeAgo(ms) {
|
|
745
|
+
const sec = Math.floor(ms / 1000);
|
|
746
|
+
if (sec < 60)
|
|
747
|
+
return '刚刚';
|
|
748
|
+
const min = Math.floor(sec / 60);
|
|
749
|
+
if (min < 60)
|
|
750
|
+
return `${min}分钟前`;
|
|
751
|
+
const hour = Math.floor(min / 60);
|
|
752
|
+
if (hour < 24)
|
|
753
|
+
return `${hour}小时前`;
|
|
754
|
+
const day = Math.floor(hour / 24);
|
|
755
|
+
return `${day}天前`;
|
|
756
|
+
}
|
|
757
|
+
/** 双字符宽字符 padding:中文/emoji 算 2 列,其他算 1 列 */
|
|
758
|
+
function visualWidth(s) {
|
|
759
|
+
// Strip ANSI escape sequences (color codes etc.) before measuring
|
|
760
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
761
|
+
let w = 0;
|
|
762
|
+
for (const ch of stripped) {
|
|
763
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
764
|
+
// CJK / Hangul / 全角符号 / Emoji 等宽字符
|
|
765
|
+
if ((code >= 0x1100 && code <= 0x115F) ||
|
|
766
|
+
(code >= 0x2E80 && code <= 0x9FFF) ||
|
|
767
|
+
(code >= 0xA000 && code <= 0xA4CF) ||
|
|
768
|
+
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
769
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
770
|
+
(code >= 0xFE30 && code <= 0xFE4F) ||
|
|
771
|
+
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
772
|
+
(code >= 0x1F300 && code <= 0x1FAFF)) {
|
|
773
|
+
w += 2;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
w += 1;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return w;
|
|
780
|
+
}
|
|
781
|
+
function padRight(s, width) {
|
|
782
|
+
const pad = Math.max(0, width - visualWidth(s));
|
|
783
|
+
return s + ' '.repeat(pad);
|
|
784
|
+
}
|
|
785
|
+
const AID_STATUS_LABELS = {
|
|
786
|
+
connected: '✓ Connected',
|
|
787
|
+
reconnecting: '⏳ Reconnecting',
|
|
788
|
+
aid_blocked: '🔒 AID Blocked',
|
|
789
|
+
kicked: '✗ Kicked',
|
|
790
|
+
failed: '✗ Failed',
|
|
791
|
+
disabled: '○ Disabled',
|
|
792
|
+
};
|
|
793
|
+
function renderAunAidsTable(aids) {
|
|
794
|
+
// Column widths(视觉宽度):AGENT 列按实际名字最长值动态扩展
|
|
795
|
+
const agentNames = aids.map(a => (a.agentName || '?').replace(/\.agentid\.pub$/, ''));
|
|
796
|
+
const COL_AGENT = Math.max(5, ...agentNames.map(n => n.length)) + 2;
|
|
797
|
+
const COL_AID = 32;
|
|
798
|
+
const COL_STATUS = 16;
|
|
799
|
+
const COL_RECONN = 8;
|
|
800
|
+
const COL_LAST = 14;
|
|
801
|
+
// 表头
|
|
802
|
+
console.log(' ' +
|
|
803
|
+
padRight('AGENT', COL_AGENT) +
|
|
804
|
+
padRight('AID', COL_AID) +
|
|
805
|
+
padRight('STATUS', COL_STATUS) +
|
|
806
|
+
padRight('RECONN', COL_RECONN) +
|
|
807
|
+
padRight('LAST ATTEMPT', COL_LAST) +
|
|
808
|
+
'NOTE');
|
|
809
|
+
for (let i = 0; i < aids.length; i++) {
|
|
810
|
+
const a = aids[i];
|
|
811
|
+
const agent = agentNames[i];
|
|
812
|
+
const aid = (a.aid || '?').slice(0, COL_AID - 1);
|
|
813
|
+
const statusLabel = AID_STATUS_LABELS[a.status] || a.status || '?';
|
|
814
|
+
const reconn = String(a.reconnectCount ?? 0);
|
|
815
|
+
const lastAttempt = a.lastAttemptAt
|
|
816
|
+
? formatTimeAgo(Date.now() - a.lastAttemptAt)
|
|
817
|
+
: '—';
|
|
818
|
+
let note = '';
|
|
819
|
+
if (a.status === 'connected' && a.lastConnectedAt) {
|
|
820
|
+
note = `uptime ${formatTimeAgo(Date.now() - a.lastConnectedAt).replace('前', '')}`;
|
|
821
|
+
}
|
|
822
|
+
else if (a.status === 'aid_blocked' && a.blockedBy) {
|
|
823
|
+
const home = (a.blockedBy.evolclawHome || '').replace(os.homedir(), '~');
|
|
824
|
+
const ag = a.blockedBy.agentName ? `, agent=${a.blockedBy.agentName}` : '';
|
|
825
|
+
note = `held by PID ${a.blockedBy.pid} (HOME=${home}${ag})`;
|
|
826
|
+
}
|
|
827
|
+
else if (a.lastError) {
|
|
828
|
+
note = a.lastError;
|
|
829
|
+
}
|
|
830
|
+
console.log(' ' +
|
|
831
|
+
padRight(agent, COL_AGENT) +
|
|
832
|
+
padRight(aid, COL_AID) +
|
|
833
|
+
padRight(statusLabel, COL_STATUS) +
|
|
834
|
+
padRight(reconn, COL_RECONN) +
|
|
835
|
+
padRight(lastAttempt, COL_LAST) +
|
|
836
|
+
note);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function showConfigChannels(config) {
|
|
840
|
+
const groups = [];
|
|
841
|
+
const channelChecks = [
|
|
842
|
+
{ type: 'feishu', isValid: (inst) => !!inst.appId && inst.enabled !== false },
|
|
843
|
+
{ type: 'wechat', isValid: (inst) => !!inst.token && inst.enabled !== false },
|
|
844
|
+
{ type: 'aun', isValid: (inst) => !!inst.aid && inst.enabled !== false && !inst.aid.includes('your-') && !inst.aid.includes('placeholder') },
|
|
845
|
+
{ type: 'dingtalk', isValid: (inst) => !!inst.clientId && inst.enabled !== false && !inst.clientId.includes('your-') && !inst.clientId.includes('placeholder') },
|
|
846
|
+
{ type: 'qqbot', isValid: (inst) => !!inst.appId && inst.enabled !== false && !inst.appId.includes('your-') && !inst.appId.includes('placeholder') },
|
|
847
|
+
{ type: 'wecom', isValid: (inst) => !!inst.botId && inst.enabled !== false && !inst.botId.includes('your-') && !inst.botId.includes('placeholder') },
|
|
848
|
+
];
|
|
849
|
+
for (const { type, isValid } of channelChecks) {
|
|
850
|
+
const raw = config.channels?.[type];
|
|
851
|
+
if (!raw)
|
|
852
|
+
continue;
|
|
853
|
+
if (Array.isArray(raw)) {
|
|
854
|
+
const names = raw.filter(isValid).map((inst) => inst.name || type);
|
|
855
|
+
if (names.length > 0)
|
|
856
|
+
groups.push({ type, instances: names });
|
|
857
|
+
}
|
|
858
|
+
else if (isValid(raw)) {
|
|
859
|
+
groups.push({ type, instances: [raw.name || type] });
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (groups.length > 0) {
|
|
863
|
+
for (const g of groups) {
|
|
864
|
+
if (g.instances.length === 1) {
|
|
865
|
+
console.log(` ${g.instances[0]}: ✓ Configured`);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
console.log(` ${g.type}: [${g.instances.join(', ')}]`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
console.log(' (no channels configured)');
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
export async function cmdStatus() {
|
|
877
|
+
const p = resolvePaths();
|
|
878
|
+
const status = scanInstances();
|
|
879
|
+
const aliveMains = status.mains.filter(m => m.alive);
|
|
880
|
+
const pid = aliveMains.length > 0 ? aliveMains[0].record.pid : null;
|
|
881
|
+
if (aliveMains.length > 1) {
|
|
882
|
+
console.log(`⚠ 检测到 ${aliveMains.length} 个 main 实例同时运行: ${aliveMains.map(m => m.record.pid).join(', ')}`);
|
|
883
|
+
console.log(' 这是异常状态,建议执行 evolclaw restart 让所有实例统一退出');
|
|
884
|
+
console.log('');
|
|
885
|
+
}
|
|
886
|
+
if (pid) {
|
|
887
|
+
printStartupInfo({ pid, running: true });
|
|
888
|
+
console.log('');
|
|
889
|
+
console.log('📊 Process Info:');
|
|
890
|
+
try {
|
|
891
|
+
const info = platform.getProcessInfo(pid);
|
|
892
|
+
if (info.uptime)
|
|
893
|
+
console.log(` Uptime: ${info.uptime}`);
|
|
894
|
+
if (info.cpu)
|
|
895
|
+
console.log(` CPU: ${info.cpu}%`);
|
|
896
|
+
if (info.memory) {
|
|
897
|
+
const memKB = parseInt(info.memory, 10);
|
|
898
|
+
const memStr = memKB >= 1024 ? `${(memKB / 1024).toFixed(0)} MB` : `${memKB} KB`;
|
|
899
|
+
console.log(` Memory: ${memStr}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
catch { }
|
|
903
|
+
console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
|
|
904
|
+
// Runtime statistics (read from sessions filesystem)
|
|
905
|
+
if (fs.existsSync(p.sessionsDir)) {
|
|
906
|
+
try {
|
|
907
|
+
const { scanChatDirs, scanMetaFiles, readJsonFile, readLastJsonlLine } = await import('../core/session/session-fs-store.js');
|
|
908
|
+
const chatDirs = scanChatDirs(p.sessionsDir);
|
|
909
|
+
const allSessions = [];
|
|
910
|
+
for (const { dirPath } of chatDirs) {
|
|
911
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
912
|
+
if (active)
|
|
913
|
+
allSessions.push({ ...active, isActive: true });
|
|
914
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
915
|
+
const meta = readLastJsonlLine(path.join(dirPath, metaFile));
|
|
916
|
+
if (!meta)
|
|
917
|
+
continue;
|
|
918
|
+
// 跳过同 id 的(active.json 已经是它的最新版)
|
|
919
|
+
if (active && active.id === meta.id)
|
|
920
|
+
continue;
|
|
921
|
+
allSessions.push({ ...meta, isActive: false });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
// 最近 5 个(按 updatedAt 倒排)
|
|
925
|
+
const recentSessions = [...allSessions].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 5);
|
|
926
|
+
// 检测 orphan:session 的 channel 实例名不在任何 self-agent 配置内
|
|
927
|
+
let orphanCount = 0;
|
|
928
|
+
try {
|
|
929
|
+
const { agents } = loadAllAgents();
|
|
930
|
+
const configChannelNames = new Set();
|
|
931
|
+
for (const cfg of agents) {
|
|
932
|
+
// AUN 是从 agent 自身 AID 隐式派生的内建通道,不出现在 cfg.channels 里。
|
|
933
|
+
// 不补这条,所有 aun#<aid>#main 会话都会被误判成 orphan。
|
|
934
|
+
configChannelNames.add(`aun#${cfg.aid}#main`);
|
|
935
|
+
for (const inst of cfg.channels) {
|
|
936
|
+
// effective key: <type>#<selfAID>#<name>
|
|
937
|
+
configChannelNames.add(`${inst.type}#${cfg.aid}#${inst.name}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
for (const s of allSessions) {
|
|
941
|
+
if (!configChannelNames.has(s.channel))
|
|
942
|
+
orphanCount++;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
catch { }
|
|
946
|
+
if (recentSessions.length > 0) {
|
|
947
|
+
console.log('');
|
|
948
|
+
console.log('📋 Recent Active Sessions:');
|
|
949
|
+
for (const s of recentSessions) {
|
|
950
|
+
const projectName = path.basename(s.projectPath);
|
|
951
|
+
const sessionType = s.threadId ? '话题会话' : '主会话';
|
|
952
|
+
const chatType = s.chatType === 'group' ? '群聊' : '单聊';
|
|
953
|
+
const sessionName = displaySessionTitle(s.name);
|
|
954
|
+
const timeAgo = formatTimeAgo(Date.now() - s.updatedAt);
|
|
955
|
+
const dot = s.isActive ? '•' : '○';
|
|
956
|
+
const agentSidLabel = s.agentSessionId ? ` [${s.agentSessionId}]` : '';
|
|
957
|
+
const agentType = s.agentType || 'claude';
|
|
958
|
+
console.log(` ${dot} [${agentType}] ${projectName} / ${sessionName} (${sessionType}, ${chatType})${agentSidLabel} - ${timeAgo}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (orphanCount > 0) {
|
|
962
|
+
console.log('');
|
|
963
|
+
console.log(`⚠ Orphan sessions: ${orphanCount}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
catch { }
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
console.log('⚠ EvolClaw is not running');
|
|
971
|
+
}
|
|
972
|
+
// Session & Project statistics (从文件系统读)
|
|
973
|
+
if (fs.existsSync(p.sessionsDir)) {
|
|
974
|
+
console.log('');
|
|
975
|
+
console.log('📦 Sessions & Projects:');
|
|
976
|
+
try {
|
|
977
|
+
const { scanChatDirs, scanMetaFiles, readJsonFile, readLastJsonlLine } = await import('../core/session/session-fs-store.js');
|
|
978
|
+
const chatDirs = scanChatDirs(p.sessionsDir);
|
|
979
|
+
let totalSessions = 0;
|
|
980
|
+
let activeSessions = 0;
|
|
981
|
+
const channelIdSet = new Set();
|
|
982
|
+
const projectSet = new Set();
|
|
983
|
+
for (const { channelId, dirPath } of chatDirs) {
|
|
984
|
+
channelIdSet.add(channelId);
|
|
985
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
986
|
+
if (active) {
|
|
987
|
+
activeSessions++;
|
|
988
|
+
projectSet.add(active.projectPath);
|
|
989
|
+
}
|
|
990
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
991
|
+
const meta = readLastJsonlLine(path.join(dirPath, metaFile));
|
|
992
|
+
if (!meta)
|
|
993
|
+
continue;
|
|
994
|
+
totalSessions++;
|
|
995
|
+
projectSet.add(meta.projectPath);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
console.log(` Total sessions: ${totalSessions} (active: ${activeSessions})`);
|
|
999
|
+
console.log(` Unique chats: ${channelIdSet.size}`);
|
|
1000
|
+
console.log(` Projects: ${projectSet.size}`);
|
|
1001
|
+
}
|
|
1002
|
+
catch { }
|
|
1003
|
+
}
|
|
1004
|
+
// Channel status
|
|
1005
|
+
if (fs.existsSync(p.defaultsConfig)) {
|
|
1006
|
+
console.log('');
|
|
1007
|
+
const config = JSON.parse(fs.readFileSync(p.defaultsConfig, 'utf-8'));
|
|
1008
|
+
if (pid) {
|
|
1009
|
+
// Running: query IPC for real-time status
|
|
1010
|
+
const status = await ipcQuery(p.socket, { type: 'status' });
|
|
1011
|
+
if (status) {
|
|
1012
|
+
// 🔑 AUN AIDs 表格(详细 AUN 实例状态)
|
|
1013
|
+
try {
|
|
1014
|
+
const aidsResp = await ipcQuery(p.socket, { type: 'aun-aids' });
|
|
1015
|
+
if (aidsResp?.ok && aidsResp.aids?.length > 0) {
|
|
1016
|
+
console.log('🔑 AUN AIDs:');
|
|
1017
|
+
renderAunAidsTable(aidsResp.aids);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
catch { /* ignore */ }
|
|
1021
|
+
// 控制 AID(daemon 进程身份)状态
|
|
1022
|
+
if (status.controlAid) {
|
|
1023
|
+
const state = status.controlAid.connected ? 'connected' : 'disconnected';
|
|
1024
|
+
console.log(`control: ${status.controlAid.aid} [${state}]`);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
console.log('control: not configured');
|
|
1028
|
+
}
|
|
1029
|
+
if (status.stats) {
|
|
1030
|
+
console.log('');
|
|
1031
|
+
console.log('📊 Last hour:');
|
|
1032
|
+
console.log(` Messages: ${status.stats.received} received, ${status.stats.completed} completed`);
|
|
1033
|
+
if (status.stats.errors > 0)
|
|
1034
|
+
console.log(` Errors: ${status.stats.errors}`);
|
|
1035
|
+
if (status.stats.completed > 0)
|
|
1036
|
+
console.log(` Avg response: ${(status.stats.avgResponseMs / 1000).toFixed(1)}s`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
// IPC unreachable but PID exists — show config only
|
|
1041
|
+
console.log('🔌 Channels (IPC unreachable):');
|
|
1042
|
+
showConfigChannels(config);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
console.log('🔌 Channel Configuration:');
|
|
1047
|
+
showConfigChannels(config);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
// EvolAgent summary (via IPC, only when running)
|
|
1051
|
+
if (pid) {
|
|
1052
|
+
try {
|
|
1053
|
+
const agentResult = await ipcQuery(p.socket, { type: 'evolagent.list' });
|
|
1054
|
+
if (agentResult?.ok && agentResult.agents?.length > 0) {
|
|
1055
|
+
const agents = agentResult.agents;
|
|
1056
|
+
if (agents.length > 0) {
|
|
1057
|
+
console.log('');
|
|
1058
|
+
console.log('🤖 EvolAgents:');
|
|
1059
|
+
for (const a of agents) {
|
|
1060
|
+
const statusIcon = a.status === 'running' ? '●' : a.status === 'error' ? '✗' : a.status === 'disabled' ? '○' : '◌';
|
|
1061
|
+
const channels = summarizeChannelFingerprints(a.channels || []);
|
|
1062
|
+
const shortName = a.name.replace(/\.agentid\.pub$/, '');
|
|
1063
|
+
console.log(` ${statusIcon} ${shortName.padEnd(20)} ${a.status.padEnd(10)} ${channels}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
// IPC query for agents failed — skip section
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
// ECWeb status(独立于 main 进程:ecweb 是单独 detached 进程)
|
|
1073
|
+
await printEcwebStatus(p);
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* 把 channel fingerprint 列表(`<type>#<selfAID>#<name>`)折叠成展示用摘要。
|
|
1077
|
+
*
|
|
1078
|
+
* 聚合规则:
|
|
1079
|
+
* - 按 type 分组
|
|
1080
|
+
* - 单实例:直接打 type(如 `aun`、`wechat`)
|
|
1081
|
+
* - 多实例:`type×N (name1, name2, ...)`
|
|
1082
|
+
* - 输出顺序保持首次出现的 type 顺序(aun 通常排第一,因为 channelInstanceNames 把它放头)
|
|
1083
|
+
*/
|
|
1084
|
+
function summarizeChannelFingerprints(fingerprints) {
|
|
1085
|
+
if (fingerprints.length === 0)
|
|
1086
|
+
return '—';
|
|
1087
|
+
const groups = new Map();
|
|
1088
|
+
const order = [];
|
|
1089
|
+
for (const fp of fingerprints) {
|
|
1090
|
+
const parts = fp.split('#');
|
|
1091
|
+
if (parts.length < 3) {
|
|
1092
|
+
if (!groups.has(fp)) {
|
|
1093
|
+
groups.set(fp, []);
|
|
1094
|
+
order.push(fp);
|
|
1095
|
+
}
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
const type = parts[0];
|
|
1099
|
+
const name = parts[2];
|
|
1100
|
+
if (!groups.has(type)) {
|
|
1101
|
+
groups.set(type, []);
|
|
1102
|
+
order.push(type);
|
|
1103
|
+
}
|
|
1104
|
+
groups.get(type).push(name);
|
|
1105
|
+
}
|
|
1106
|
+
return order.map(type => {
|
|
1107
|
+
const names = groups.get(type);
|
|
1108
|
+
if (names.length === 0)
|
|
1109
|
+
return type;
|
|
1110
|
+
if (names.length === 1)
|
|
1111
|
+
return type;
|
|
1112
|
+
return `${type}×${names.length} (${names.join(', ')})`;
|
|
1113
|
+
}).join(', ');
|
|
1114
|
+
}
|
|
1115
|
+
// Log line pattern: [timestamp] [LEVEL] [Module?] message
|
|
1116
|
+
const LOG_RE = /^(\[[^\]]+\]) (\[(?:INFO|WARN|ERROR|DEBUG)\]) ((?:\[[^\]]+\] )*)(.*)$/;
|
|
1117
|
+
const MAX_MSG = 200; // truncate long messages
|
|
1118
|
+
function makeColors(enabled) {
|
|
1119
|
+
const e = (code) => enabled ? code : '';
|
|
1120
|
+
return {
|
|
1121
|
+
reset: e('\x1b[0m'), dim: e('\x1b[2m'), bold: e('\x1b[1m'),
|
|
1122
|
+
red: e('\x1b[31m'), yellow: e('\x1b[33m'), cyan: e('\x1b[36m'),
|
|
1123
|
+
magenta: e('\x1b[35m'), gray: e('\x1b[90m'),
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function renderLogLine(line, opts) {
|
|
1127
|
+
const m = line.match(LOG_RE);
|
|
1128
|
+
if (!m)
|
|
1129
|
+
return line; // passthrough non-standard lines (stack traces etc.)
|
|
1130
|
+
const [, ts, levelTag, modulePart, msg] = m;
|
|
1131
|
+
const level = levelTag.slice(1, -1); // strip brackets
|
|
1132
|
+
// Level filter
|
|
1133
|
+
if (opts.level) {
|
|
1134
|
+
const want = opts.level.toUpperCase();
|
|
1135
|
+
if (want === 'ERROR' && level !== 'ERROR')
|
|
1136
|
+
return null;
|
|
1137
|
+
if (want === 'WARN' && level !== 'WARN' && level !== 'ERROR')
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
// Module filter (case-insensitive substring match)
|
|
1141
|
+
if (opts.module) {
|
|
1142
|
+
const mod = modulePart.toLowerCase();
|
|
1143
|
+
if (!mod.includes(opts.module.toLowerCase()))
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
// Truncate long messages (always, regardless of color)
|
|
1147
|
+
const truncated = msg.length > MAX_MSG ? msg.slice(0, MAX_MSG) + '…' : msg;
|
|
1148
|
+
const C = makeColors(opts.color);
|
|
1149
|
+
// Color by level
|
|
1150
|
+
const levelColor = level === 'ERROR' ? C.red : level === 'WARN' ? C.yellow : level === 'DEBUG' ? C.gray : '';
|
|
1151
|
+
// Highlight user messages: [channel] channelId: text
|
|
1152
|
+
const isUserMsg = modulePart && /^\S+: .+$/.test(truncated);
|
|
1153
|
+
const renderedMsg = isUserMsg
|
|
1154
|
+
? C.cyan + truncated + C.reset
|
|
1155
|
+
: levelColor + truncated + C.reset;
|
|
1156
|
+
return (C.dim + ts + C.reset + ' ' +
|
|
1157
|
+
levelColor + C.bold + levelTag + C.reset + ' ' +
|
|
1158
|
+
C.magenta + modulePart.trimEnd() + C.reset +
|
|
1159
|
+
(modulePart ? ' ' : '') +
|
|
1160
|
+
renderedMsg);
|
|
1161
|
+
}
|
|
1162
|
+
function findLatestLog(logDir, baseName) {
|
|
1163
|
+
if (!fs.existsSync(logDir))
|
|
1164
|
+
return null;
|
|
1165
|
+
// Try exact name first (legacy non-rotated)
|
|
1166
|
+
const exact = path.join(logDir, `${baseName}.log`);
|
|
1167
|
+
if (fs.existsSync(exact))
|
|
1168
|
+
return exact;
|
|
1169
|
+
// Find latest rotated file: baseName-YYYYMMDD-HH.log
|
|
1170
|
+
const files = fs.readdirSync(logDir)
|
|
1171
|
+
.filter(f => f.startsWith(`${baseName}-`) && f.endsWith('.log'))
|
|
1172
|
+
.sort();
|
|
1173
|
+
if (files.length === 0)
|
|
1174
|
+
return null;
|
|
1175
|
+
return path.join(logDir, files[files.length - 1]);
|
|
1176
|
+
}
|
|
1177
|
+
export function cmdLogs(args) {
|
|
1178
|
+
const raw = args.includes('--raw');
|
|
1179
|
+
const noColor = args.includes('--no-color');
|
|
1180
|
+
const levelIdx = args.indexOf('--level');
|
|
1181
|
+
const moduleIdx = args.indexOf('--module');
|
|
1182
|
+
const level = levelIdx !== -1 ? args[levelIdx + 1] : undefined;
|
|
1183
|
+
const module = moduleIdx !== -1 ? args[moduleIdx + 1] : undefined;
|
|
1184
|
+
const p = resolvePaths();
|
|
1185
|
+
const mainLog = findLatestLog(p.logs, 'evolclaw');
|
|
1186
|
+
if (!mainLog) {
|
|
1187
|
+
console.log(`❌ Log file not found in: ${p.logs}`);
|
|
1188
|
+
process.exit(1);
|
|
1189
|
+
}
|
|
1190
|
+
// Rendered mode: always filter+truncate, color depends on TTY
|
|
1191
|
+
const useColor = !noColor && !!process.stdout.isTTY;
|
|
1192
|
+
const opts = { level, module, color: useColor };
|
|
1193
|
+
function processLine(line) {
|
|
1194
|
+
const rendered = renderLogLine(line, opts);
|
|
1195
|
+
if (rendered !== null)
|
|
1196
|
+
process.stdout.write(rendered + '\n');
|
|
1197
|
+
}
|
|
1198
|
+
// Backfill last 50 lines from current file
|
|
1199
|
+
const existing = fs.readFileSync(mainLog, 'utf-8').split('\n').slice(-51);
|
|
1200
|
+
if (existing.length && existing[existing.length - 1] === '')
|
|
1201
|
+
existing.pop();
|
|
1202
|
+
existing.forEach(processLine);
|
|
1203
|
+
if (raw) {
|
|
1204
|
+
// Raw mode: poll without rendering
|
|
1205
|
+
let currentFile = mainLog;
|
|
1206
|
+
let position = fs.statSync(currentFile).size;
|
|
1207
|
+
let pending = '';
|
|
1208
|
+
const timer = setInterval(() => {
|
|
1209
|
+
const latest = findLatestLog(p.logs, 'evolclaw');
|
|
1210
|
+
if (latest && latest !== currentFile) {
|
|
1211
|
+
currentFile = latest;
|
|
1212
|
+
position = 0;
|
|
1213
|
+
pending = '';
|
|
1214
|
+
}
|
|
1215
|
+
let stat;
|
|
1216
|
+
try {
|
|
1217
|
+
stat = fs.statSync(currentFile);
|
|
1218
|
+
}
|
|
1219
|
+
catch {
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (stat.size < position) {
|
|
1223
|
+
position = 0;
|
|
1224
|
+
pending = '';
|
|
1225
|
+
}
|
|
1226
|
+
if (stat.size === position)
|
|
1227
|
+
return;
|
|
1228
|
+
const buf = Buffer.alloc(stat.size - position);
|
|
1229
|
+
try {
|
|
1230
|
+
const fd = fs.openSync(currentFile, 'r');
|
|
1231
|
+
fs.readSync(fd, buf, 0, buf.length, position);
|
|
1232
|
+
fs.closeSync(fd);
|
|
1233
|
+
}
|
|
1234
|
+
catch {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
position = stat.size;
|
|
1238
|
+
const parts = (pending + buf.toString('utf-8')).split('\n');
|
|
1239
|
+
pending = parts.pop() || '';
|
|
1240
|
+
for (const line of parts) {
|
|
1241
|
+
if (line)
|
|
1242
|
+
process.stdout.write(line + '\n');
|
|
1243
|
+
}
|
|
1244
|
+
}, 200);
|
|
1245
|
+
platform.onShutdown(() => { clearInterval(timer); process.exit(0); });
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
// Follow mode: poll with rendering, auto-switch on rotation
|
|
1249
|
+
let currentFile = mainLog;
|
|
1250
|
+
let position = fs.statSync(currentFile).size;
|
|
1251
|
+
let pending = '';
|
|
1252
|
+
const timer = setInterval(() => {
|
|
1253
|
+
const latest = findLatestLog(p.logs, 'evolclaw');
|
|
1254
|
+
if (latest && latest !== currentFile) {
|
|
1255
|
+
currentFile = latest;
|
|
1256
|
+
position = 0;
|
|
1257
|
+
pending = '';
|
|
1258
|
+
}
|
|
1259
|
+
let stat;
|
|
1260
|
+
try {
|
|
1261
|
+
stat = fs.statSync(currentFile);
|
|
1262
|
+
}
|
|
1263
|
+
catch {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (stat.size < position) {
|
|
1267
|
+
position = 0;
|
|
1268
|
+
pending = '';
|
|
1269
|
+
}
|
|
1270
|
+
if (stat.size === position)
|
|
1271
|
+
return;
|
|
1272
|
+
const buf = Buffer.alloc(stat.size - position);
|
|
1273
|
+
try {
|
|
1274
|
+
const fd = fs.openSync(currentFile, 'r');
|
|
1275
|
+
fs.readSync(fd, buf, 0, buf.length, position);
|
|
1276
|
+
fs.closeSync(fd);
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
position = stat.size;
|
|
1282
|
+
const parts = (pending + buf.toString('utf-8')).split('\n');
|
|
1283
|
+
pending = parts.pop() || '';
|
|
1284
|
+
for (const line of parts) {
|
|
1285
|
+
if (line)
|
|
1286
|
+
processLine(line);
|
|
1287
|
+
}
|
|
1288
|
+
}, 200);
|
|
1289
|
+
platform.onShutdown(() => { clearInterval(timer); process.exit(0); });
|
|
1290
|
+
}
|
|
1291
|
+
// ==================== Watch ====================
|
|
1292
|
+
let watchUseColor = false;
|
|
1293
|
+
const WATCH_BRACKET_TS_RE = /^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)\]/;
|
|
1294
|
+
const WATCH_JSON_TS_RE = /"ts"\s*:\s*"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)"/;
|
|
1295
|
+
function parseWatchTs(s) {
|
|
1296
|
+
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z)?$/);
|
|
1297
|
+
if (!m)
|
|
1298
|
+
return NaN;
|
|
1299
|
+
const [, y, mo, d, h, mi, se, ms, z] = m;
|
|
1300
|
+
const msNum = ms ? parseInt((ms + '000').slice(0, 3), 10) : 0;
|
|
1301
|
+
if (z)
|
|
1302
|
+
return Date.UTC(+y, +mo - 1, +d, +h, +mi, +se, msNum);
|
|
1303
|
+
return new Date(+y, +mo - 1, +d, +h, +mi, +se, msNum).getTime();
|
|
1304
|
+
}
|
|
1305
|
+
function extractWatchTs(line) {
|
|
1306
|
+
const m = line.match(WATCH_BRACKET_TS_RE) || line.match(WATCH_JSON_TS_RE);
|
|
1307
|
+
if (!m)
|
|
1308
|
+
return null;
|
|
1309
|
+
const t = parseWatchTs(m[1]);
|
|
1310
|
+
return isNaN(t) ? null : t;
|
|
1311
|
+
}
|
|
1312
|
+
function toLocalTimeStr(epoch) {
|
|
1313
|
+
const d = new Date(epoch);
|
|
1314
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
1315
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
1316
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
1317
|
+
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
1318
|
+
return `${hh}:${mm}:${ss}.${ms}`;
|
|
1319
|
+
}
|
|
1320
|
+
function stripTimestamp(line) {
|
|
1321
|
+
const m = line.match(WATCH_BRACKET_TS_RE);
|
|
1322
|
+
if (m)
|
|
1323
|
+
return line.slice(m[0].length).trimStart();
|
|
1324
|
+
return line;
|
|
1325
|
+
}
|
|
1326
|
+
function compactAunLog(line, color) {
|
|
1327
|
+
if (!color) {
|
|
1328
|
+
// No-color mode: just compact [LEVEL][module] [aid] → [LEVEL][module] aid:
|
|
1329
|
+
return line
|
|
1330
|
+
.replace(/^\[([A-Z]+)\]\[([^\]]+)\] \[([^\]]+)\] /, '[$1][$2] $3: ')
|
|
1331
|
+
.replace(/^\[([A-Z]+)\] /, '[$1] ');
|
|
1332
|
+
}
|
|
1333
|
+
// Pattern 1: [LEVEL][module] [aid] msg (AUN SDK logs in stdout)
|
|
1334
|
+
const m1 = line.match(/^\[([A-Z]+)\]\[([^\]]+)\](?: \[([^\]]+)\])? (.*)/s);
|
|
1335
|
+
if (m1) {
|
|
1336
|
+
const [, level, mod, aid, rest] = m1;
|
|
1337
|
+
const lc = LEVEL_COLORS[level] || '';
|
|
1338
|
+
const mc = assignModuleColor(mod);
|
|
1339
|
+
const aidPart = aid ? ` ${aid}:` : '';
|
|
1340
|
+
return `${lc}[${level}]${RST_CONST}${mc}[${mod}]${RST_CONST}${aidPart} ${rest}`;
|
|
1341
|
+
}
|
|
1342
|
+
// Pattern 2: [AiBotSDK] [LEVEL] msg (WeCom SDK logs)
|
|
1343
|
+
const m2 = line.match(/^\[([^\]]+)\] \[(DEBUG|INFO|WARN|ERROR)\] (.*)/s);
|
|
1344
|
+
if (m2) {
|
|
1345
|
+
const [, sdk, level, rest] = m2;
|
|
1346
|
+
const lc = LEVEL_COLORS[level] || '';
|
|
1347
|
+
const mc = assignModuleColor(sdk);
|
|
1348
|
+
return `${lc}[${level}]${RST_CONST}${mc}[${sdk}]${RST_CONST} ${rest}`;
|
|
1349
|
+
}
|
|
1350
|
+
// Pattern 3: [LEVEL] [Module] msg or [LEVEL] msg (evolclaw main log after stripTimestamp)
|
|
1351
|
+
const m3 = line.match(/^\[(DEBUG|INFO|WARN|ERROR)\] (?:\[([^\]]+)\] )?(.*)/s);
|
|
1352
|
+
if (m3) {
|
|
1353
|
+
const [, level, mod, rest] = m3;
|
|
1354
|
+
const lc = LEVEL_COLORS[level] || '';
|
|
1355
|
+
if (mod) {
|
|
1356
|
+
const mc = assignModuleColor(mod);
|
|
1357
|
+
return `${lc}[${level}]${RST_CONST} ${mc}[${mod}]${RST_CONST} ${rest}`;
|
|
1358
|
+
}
|
|
1359
|
+
return `${lc}[${level}]${RST_CONST} ${rest}`;
|
|
1360
|
+
}
|
|
1361
|
+
return line;
|
|
1362
|
+
}
|
|
1363
|
+
const RST_CONST = '\x1b[0m';
|
|
1364
|
+
const LEVEL_COLORS = {
|
|
1365
|
+
DEBUG: '\x1b[2m', // dim
|
|
1366
|
+
INFO: '\x1b[36m', // cyan
|
|
1367
|
+
WARN: '\x1b[33m', // yellow
|
|
1368
|
+
ERROR: '\x1b[31m', // red
|
|
1369
|
+
};
|
|
1370
|
+
const moduleColorPool = [
|
|
1371
|
+
'\x1b[35m', // magenta
|
|
1372
|
+
'\x1b[34m', // blue
|
|
1373
|
+
'\x1b[32m', // green
|
|
1374
|
+
'\x1b[96m', // bright cyan
|
|
1375
|
+
'\x1b[93m', // bright yellow
|
|
1376
|
+
'\x1b[95m', // bright magenta
|
|
1377
|
+
'\x1b[94m', // bright blue
|
|
1378
|
+
'\x1b[92m', // bright green
|
|
1379
|
+
];
|
|
1380
|
+
const moduleColorMap = new Map();
|
|
1381
|
+
let moduleColorIdx = 0;
|
|
1382
|
+
function assignModuleColor(mod) {
|
|
1383
|
+
let c = moduleColorMap.get(mod);
|
|
1384
|
+
if (!c) {
|
|
1385
|
+
c = moduleColorPool[moduleColorIdx++ % moduleColorPool.length];
|
|
1386
|
+
moduleColorMap.set(mod, c);
|
|
1387
|
+
}
|
|
1388
|
+
return c;
|
|
1389
|
+
}
|
|
1390
|
+
function formatWatchContent(line) {
|
|
1391
|
+
// JSON line: parse and format key fields
|
|
1392
|
+
if (line.startsWith('{') && line.endsWith('}')) {
|
|
1393
|
+
try {
|
|
1394
|
+
const obj = JSON.parse(line);
|
|
1395
|
+
// Message log format: { ts, msgId, sessionId, dir, status, duration? }
|
|
1396
|
+
if (obj.msgId && obj.status) {
|
|
1397
|
+
const dirLabel = obj.dir === 'inbound' ? '[IN]' : obj.dir === 'outbound' ? '[OUT]' : ' ';
|
|
1398
|
+
const peer = obj.msgId.replace(/_\d+(_reply)?$/, '').replace(/^[^_]+_/, '');
|
|
1399
|
+
const dur = obj.duration != null ? ` duration=${obj.duration}ms` : '';
|
|
1400
|
+
if (!watchUseColor)
|
|
1401
|
+
return `${dirLabel}[${obj.status}] ${peer}:${dur}`.trimEnd();
|
|
1402
|
+
const dc = obj.dir === 'inbound' ? '\x1b[32m' : '\x1b[33m'; // green in, yellow out
|
|
1403
|
+
const ec = assignModuleColor(obj.status);
|
|
1404
|
+
return `${dc}${dirLabel}${RST_CONST}${ec}[${obj.status}]${RST_CONST} ${peer}:${dur}`.trimEnd();
|
|
1405
|
+
}
|
|
1406
|
+
// Event log format: { ts, type, ... }
|
|
1407
|
+
if (obj.type && !obj.dir) {
|
|
1408
|
+
const parts = [];
|
|
1409
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1410
|
+
if (k === 'ts' || k === 'type')
|
|
1411
|
+
continue;
|
|
1412
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
1413
|
+
parts.push(`${k}=${v}`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (!watchUseColor)
|
|
1417
|
+
return `[${obj.type}] ${parts.join(' ')}`.trimEnd();
|
|
1418
|
+
const tc = assignModuleColor(obj.type.split(':')[0]);
|
|
1419
|
+
return `${tc}[${obj.type}]${RST_CONST} ${parts.join(' ')}`.trimEnd();
|
|
1420
|
+
}
|
|
1421
|
+
// AUN trace log format: { ts, dir, event, self_aid, data, ... }
|
|
1422
|
+
if (obj.dir && obj.event) {
|
|
1423
|
+
const aid = obj.self_aid || '';
|
|
1424
|
+
const data = obj.data;
|
|
1425
|
+
let dataStr = '';
|
|
1426
|
+
if (data) {
|
|
1427
|
+
const parts = [];
|
|
1428
|
+
for (const [k, v] of Object.entries(data)) {
|
|
1429
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
1430
|
+
parts.push(`${k}=${v}`);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
dataStr = parts.join(' ');
|
|
1434
|
+
}
|
|
1435
|
+
const dirLabel = obj.dir === 'IN' ? '[IN]' : obj.dir === 'OUT' ? '[OUT]' : ' ';
|
|
1436
|
+
const aidPart = aid ? `${aid}: ` : '';
|
|
1437
|
+
if (!watchUseColor)
|
|
1438
|
+
return `${dirLabel}[${obj.event}] ${aidPart}${dataStr}`.trimEnd();
|
|
1439
|
+
const dc = obj.dir === 'IN' ? '\x1b[32m' : '\x1b[33m'; // green in, yellow out
|
|
1440
|
+
const ec = assignModuleColor(obj.event.split('.')[0]);
|
|
1441
|
+
return `${dc}${dirLabel}${RST_CONST}${ec}[${obj.event}]${RST_CONST} ${aidPart}${dataStr}`.trimEnd();
|
|
1442
|
+
}
|
|
1443
|
+
// Unknown JSON format — show compact key=value summary
|
|
1444
|
+
const parts = [];
|
|
1445
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1446
|
+
if (k === 'ts')
|
|
1447
|
+
continue;
|
|
1448
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
1449
|
+
parts.push(`${k}=${v}`);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return parts.join(' ');
|
|
1453
|
+
}
|
|
1454
|
+
catch { /* fall through */ }
|
|
1455
|
+
}
|
|
1456
|
+
return compactAunLog(stripTimestamp(line), watchUseColor);
|
|
1457
|
+
}
|
|
1458
|
+
const WATCH_FILE_COLORS = [
|
|
1459
|
+
'\x1b[36m', // cyan
|
|
1460
|
+
'\x1b[33m', // yellow
|
|
1461
|
+
'\x1b[32m', // green
|
|
1462
|
+
'\x1b[35m', // magenta
|
|
1463
|
+
'\x1b[34m', // blue
|
|
1464
|
+
'\x1b[91m', // bright red
|
|
1465
|
+
'\x1b[92m', // bright green
|
|
1466
|
+
'\x1b[93m', // bright yellow
|
|
1467
|
+
'\x1b[94m', // bright blue
|
|
1468
|
+
'\x1b[95m', // bright magenta
|
|
1469
|
+
'\x1b[96m', // bright cyan
|
|
1470
|
+
];
|
|
1471
|
+
async function cmdWatchMenu() {
|
|
1472
|
+
const items = [
|
|
1473
|
+
{ key: 'log', label: 'log', desc: 'real-time log tail' },
|
|
1474
|
+
{ key: 'aid', label: 'aid', desc: 'AID connection stats' },
|
|
1475
|
+
{ key: 'msg', label: 'msg', desc: 'message inspector' },
|
|
1476
|
+
{ key: 'web', label: 'web', desc: 'browser dashboard (aid/msg/session)' },
|
|
1477
|
+
];
|
|
1478
|
+
let index = 0;
|
|
1479
|
+
const useColor = !!process.stdout.isTTY;
|
|
1480
|
+
const RST = useColor ? '\x1b[0m' : '';
|
|
1481
|
+
const DIM = useColor ? '\x1b[2m' : '';
|
|
1482
|
+
const BOLD = useColor ? '\x1b[1m' : '';
|
|
1483
|
+
const CYAN = useColor ? '\x1b[36m' : '';
|
|
1484
|
+
const pkgRoot = getPackageRoot();
|
|
1485
|
+
function render() {
|
|
1486
|
+
let buf = '\x1b[2J\x1b[H';
|
|
1487
|
+
buf += `${BOLD}evolclaw watch${RST} ${DIM}${pkgRoot}${RST}\n\n`;
|
|
1488
|
+
for (let i = 0; i < items.length; i++) {
|
|
1489
|
+
const sel = i === index;
|
|
1490
|
+
const marker = sel ? `${CYAN}${BOLD} ▸ ` : ' ';
|
|
1491
|
+
const label = sel ? `${items[i].label}${RST}` : `${DIM}${items[i].label}${RST}`;
|
|
1492
|
+
buf += `${marker}${label} ${DIM}${items[i].desc}${RST}\n`;
|
|
1493
|
+
}
|
|
1494
|
+
buf += `\n${DIM} ↑↓ select Enter confirm ESC exit${RST}\n`;
|
|
1495
|
+
process.stdout.write(buf);
|
|
1496
|
+
}
|
|
1497
|
+
render();
|
|
1498
|
+
return new Promise((resolve) => {
|
|
1499
|
+
if (!process.stdin.isTTY) {
|
|
1500
|
+
resolve();
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
process.stdin.setRawMode(true);
|
|
1504
|
+
process.stdin.resume();
|
|
1505
|
+
const onData = async (data) => {
|
|
1506
|
+
if (data[0] === 0x1b && data.length === 1) {
|
|
1507
|
+
cleanup();
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
if (data[0] === 0x03) {
|
|
1511
|
+
cleanup();
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (data[0] === 0x1b && data[1] === 0x5b) {
|
|
1515
|
+
if (data[2] === 0x41) {
|
|
1516
|
+
index = Math.max(0, index - 1);
|
|
1517
|
+
render();
|
|
1518
|
+
}
|
|
1519
|
+
if (data[2] === 0x42) {
|
|
1520
|
+
index = Math.min(items.length - 1, index + 1);
|
|
1521
|
+
render();
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (data[0] === 0x0d) {
|
|
1525
|
+
process.stdin.removeListener('data', onData);
|
|
1526
|
+
process.stdin.setRawMode(false);
|
|
1527
|
+
process.stdin.pause();
|
|
1528
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
1529
|
+
const chosen = items[index].key;
|
|
1530
|
+
if (chosen === 'log') {
|
|
1531
|
+
await cmdWatchLogsFlow();
|
|
1532
|
+
}
|
|
1533
|
+
else if (chosen === 'aid') {
|
|
1534
|
+
await cmdWatchAid();
|
|
1535
|
+
}
|
|
1536
|
+
else if (chosen === 'msg') {
|
|
1537
|
+
const { cmdWatchMsg } = await import('./watch-msg.js');
|
|
1538
|
+
await cmdWatchMsg();
|
|
1539
|
+
}
|
|
1540
|
+
else if (chosen === 'web') {
|
|
1541
|
+
await cmdWatchWeb();
|
|
1542
|
+
}
|
|
1543
|
+
resolve();
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
function cleanup() {
|
|
1547
|
+
process.stdin.removeListener('data', onData);
|
|
1548
|
+
if (process.stdin.isTTY)
|
|
1549
|
+
process.stdin.setRawMode(false);
|
|
1550
|
+
process.stdin.pause();
|
|
1551
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
1552
|
+
resolve();
|
|
1553
|
+
}
|
|
1554
|
+
process.stdin.on('data', onData);
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* 勾选要监控的日志类型。返回选中类型数组;ESC/Ctrl+C 取消返回 null。
|
|
1559
|
+
* types: 可用类型(字母序)。preChecked: 预勾集合。fileCount: 类型→当前文件数。
|
|
1560
|
+
*/
|
|
1561
|
+
async function cmdWatchLogsSelect(types, preChecked, fileCount) {
|
|
1562
|
+
let index = 0;
|
|
1563
|
+
const checked = new Set(preChecked);
|
|
1564
|
+
const useColor = !!process.stdout.isTTY;
|
|
1565
|
+
const RST = useColor ? '\x1b[0m' : '';
|
|
1566
|
+
const DIM = useColor ? '\x1b[2m' : '';
|
|
1567
|
+
const BOLD = useColor ? '\x1b[1m' : '';
|
|
1568
|
+
const CYAN = useColor ? '\x1b[36m' : '';
|
|
1569
|
+
let hint = '';
|
|
1570
|
+
function render() {
|
|
1571
|
+
let buf = '\x1b[2J\x1b[H';
|
|
1572
|
+
buf += `${BOLD}选择要监控的日志类型${RST}\n\n`;
|
|
1573
|
+
for (let i = 0; i < types.length; i++) {
|
|
1574
|
+
const sel = i === index;
|
|
1575
|
+
const mark = checked.has(types[i]) ? '✔' : ' ';
|
|
1576
|
+
const cursor = sel ? `${CYAN}${BOLD}▸ ` : ' ';
|
|
1577
|
+
const n = fileCount.get(types[i]) ?? 0;
|
|
1578
|
+
const label = sel ? `${types[i]}${RST}` : `${DIM}${types[i]}${RST}`;
|
|
1579
|
+
buf += `${cursor}[${mark}] ${label} ${DIM}(${n} file${n === 1 ? '' : 's'})${RST}\n`;
|
|
1580
|
+
}
|
|
1581
|
+
buf += `\n${DIM} ↑↓ 移动 空格 勾选 Enter 确认 ESC 取消${RST}\n`;
|
|
1582
|
+
if (hint)
|
|
1583
|
+
buf += `${CYAN} ${hint}${RST}\n`;
|
|
1584
|
+
process.stdout.write(buf);
|
|
1585
|
+
}
|
|
1586
|
+
render();
|
|
1587
|
+
return new Promise((resolve) => {
|
|
1588
|
+
if (!process.stdin.isTTY) {
|
|
1589
|
+
resolve(null);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
process.stdin.setRawMode(true);
|
|
1593
|
+
process.stdin.resume();
|
|
1594
|
+
const onData = (data) => {
|
|
1595
|
+
if ((data[0] === 0x1b && data.length === 1) || data[0] === 0x03) {
|
|
1596
|
+
finish(null);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (data[0] === 0x1b && data[1] === 0x5b) {
|
|
1600
|
+
if (data[2] === 0x41) {
|
|
1601
|
+
index = Math.max(0, index - 1);
|
|
1602
|
+
hint = '';
|
|
1603
|
+
render();
|
|
1604
|
+
}
|
|
1605
|
+
if (data[2] === 0x42) {
|
|
1606
|
+
index = Math.min(types.length - 1, index + 1);
|
|
1607
|
+
hint = '';
|
|
1608
|
+
render();
|
|
1609
|
+
}
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
if (data[0] === 0x20) { // space
|
|
1613
|
+
const t = types[index];
|
|
1614
|
+
if (checked.has(t))
|
|
1615
|
+
checked.delete(t);
|
|
1616
|
+
else
|
|
1617
|
+
checked.add(t);
|
|
1618
|
+
hint = '';
|
|
1619
|
+
render();
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (data[0] === 0x0d) { // enter
|
|
1623
|
+
if (checked.size === 0) {
|
|
1624
|
+
hint = '至少选择一项';
|
|
1625
|
+
render();
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
finish(types.filter(t => checked.has(t)));
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
function finish(result) {
|
|
1632
|
+
process.stdin.removeListener('data', onData);
|
|
1633
|
+
if (process.stdin.isTTY)
|
|
1634
|
+
process.stdin.setRawMode(false);
|
|
1635
|
+
process.stdin.pause();
|
|
1636
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
1637
|
+
resolve(result);
|
|
1638
|
+
}
|
|
1639
|
+
process.stdin.on('data', onData);
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
/** logs 勾选流程:扫描类型 → 预勾 → 菜单 → 保存 → 监控。 */
|
|
1643
|
+
async function cmdWatchLogsFlow() {
|
|
1644
|
+
const p = resolvePaths();
|
|
1645
|
+
if (!fs.existsSync(p.logs)) {
|
|
1646
|
+
console.log(`❌ Log directory not found: ${p.logs}`);
|
|
1647
|
+
process.exit(1);
|
|
1648
|
+
}
|
|
1649
|
+
const files = fs.readdirSync(p.logs).filter(f => f.endsWith('.log'));
|
|
1650
|
+
const types = deriveLogTypes(files);
|
|
1651
|
+
if (types.length === 0) {
|
|
1652
|
+
console.log(`⚠ ${p.logs} 下暂无 .log 文件`);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
const fileCount = new Map();
|
|
1656
|
+
for (const t of types)
|
|
1657
|
+
fileCount.set(t, files.filter(f => shortLogNameLocal(f) === t).length);
|
|
1658
|
+
const cfg = loadEvolclawConfig();
|
|
1659
|
+
const preChecked = computePreChecked(types, cfg.watch?.logTypes);
|
|
1660
|
+
if (!process.stdin.isTTY) {
|
|
1661
|
+
const fallback = cfg.watch?.logTypes && cfg.watch.logTypes.length > 0
|
|
1662
|
+
? new Set(cfg.watch.logTypes) : new Set(types);
|
|
1663
|
+
cmdWatch(fallback);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const selected = await cmdWatchLogsSelect(types, preChecked, fileCount);
|
|
1667
|
+
if (selected === null)
|
|
1668
|
+
return;
|
|
1669
|
+
saveEvolclawConfig({ ...cfg, watch: { ...cfg.watch, logTypes: selected } });
|
|
1670
|
+
cmdWatch(new Set(selected));
|
|
1671
|
+
}
|
|
1672
|
+
function cmdWatch(filterTypes) {
|
|
1673
|
+
const p = resolvePaths();
|
|
1674
|
+
if (!fs.existsSync(p.logs)) {
|
|
1675
|
+
console.log(`❌ Log directory not found: ${p.logs}`);
|
|
1676
|
+
process.exit(1);
|
|
1677
|
+
}
|
|
1678
|
+
// 清理残留的 watch 文件(旧 watch 进程被强杀时留下的)
|
|
1679
|
+
fs.mkdirSync(p.instanceDir, { recursive: true });
|
|
1680
|
+
for (const file of fs.readdirSync(p.instanceDir)) {
|
|
1681
|
+
if (!file.startsWith('watch-') || !file.endsWith('.json'))
|
|
1682
|
+
continue;
|
|
1683
|
+
const filePath = path.join(p.instanceDir, file);
|
|
1684
|
+
try {
|
|
1685
|
+
const rec = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1686
|
+
if (rec.pid && !platform.isProcessRunning(rec.pid)) {
|
|
1687
|
+
fs.unlinkSync(filePath);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
catch {
|
|
1691
|
+
try {
|
|
1692
|
+
fs.unlinkSync(filePath);
|
|
1693
|
+
}
|
|
1694
|
+
catch { }
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
// 注册 instance
|
|
1698
|
+
const instanceFile = path.join(p.instanceDir, `watch-${process.pid}.json`);
|
|
1699
|
+
const instanceRecord = {
|
|
1700
|
+
pid: process.pid,
|
|
1701
|
+
startedAt: Date.now(),
|
|
1702
|
+
startedAtIso: new Date().toISOString(),
|
|
1703
|
+
type: 'watch',
|
|
1704
|
+
};
|
|
1705
|
+
fs.writeFileSync(instanceFile, JSON.stringify(instanceRecord, null, 2));
|
|
1706
|
+
const useColor = !!process.stdout.isTTY;
|
|
1707
|
+
watchUseColor = useColor;
|
|
1708
|
+
const RST = useColor ? '\x1b[0m' : '';
|
|
1709
|
+
const DIM = useColor ? '\x1b[2m' : '';
|
|
1710
|
+
const colorMap = new Map();
|
|
1711
|
+
let colorIdx = 0;
|
|
1712
|
+
const getColor = (name) => {
|
|
1713
|
+
if (!useColor)
|
|
1714
|
+
return '';
|
|
1715
|
+
let c = colorMap.get(name);
|
|
1716
|
+
if (!c) {
|
|
1717
|
+
c = WATCH_FILE_COLORS[colorIdx++ % WATCH_FILE_COLORS.length];
|
|
1718
|
+
colorMap.set(name, c);
|
|
1719
|
+
}
|
|
1720
|
+
return c;
|
|
1721
|
+
};
|
|
1722
|
+
const listLogs = () => {
|
|
1723
|
+
const all = fs.readdirSync(p.logs).filter(f => f.endsWith('.log')).map(f => path.join(p.logs, f));
|
|
1724
|
+
return filterLogFiles(all, filterTypes);
|
|
1725
|
+
};
|
|
1726
|
+
// Strip rotation suffix (e.g., "evolclaw-20260518-21" → "evolclaw")
|
|
1727
|
+
const shortName = (f) => path.basename(f, '.log').replace(/-\d{8}-\d{2}$/, '');
|
|
1728
|
+
// 计算最长文件名用于对齐
|
|
1729
|
+
let maxNameLen = 0;
|
|
1730
|
+
const updateMaxName = () => {
|
|
1731
|
+
for (const file of listLogs()) {
|
|
1732
|
+
const len = shortName(file).length;
|
|
1733
|
+
if (len > maxNameLen)
|
|
1734
|
+
maxNameLen = len;
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
updateMaxName();
|
|
1738
|
+
const formatLine = (file, ts, line) => {
|
|
1739
|
+
const timeStr = `${DIM}${toLocalTimeStr(ts)}${RST}`;
|
|
1740
|
+
const name = shortName(file);
|
|
1741
|
+
const c = getColor(name);
|
|
1742
|
+
const paddedName = `${c}${name.padEnd(maxNameLen)}${RST}`;
|
|
1743
|
+
const content = formatWatchContent(line);
|
|
1744
|
+
return `${timeStr} ${paddedName} ${content}`;
|
|
1745
|
+
};
|
|
1746
|
+
console.log(`🔭 Watching ${p.logs}/*.log (ESC to stop)\n`);
|
|
1747
|
+
// 显示当前实例信息和 AID 状态
|
|
1748
|
+
const instStatus = scanInstances();
|
|
1749
|
+
const aliveMainEntries = instStatus.mains.filter(m => m.alive);
|
|
1750
|
+
if (aliveMainEntries.length > 0) {
|
|
1751
|
+
if (aliveMainEntries.length > 1) {
|
|
1752
|
+
console.log(`⚠ 检测到 ${aliveMainEntries.length} 个 main 实例: ${aliveMainEntries.map(m => m.record.pid).join(', ')}(异常)\n`);
|
|
1753
|
+
}
|
|
1754
|
+
const m = aliveMainEntries[0].record;
|
|
1755
|
+
const uptime = formatTimeAgo(Date.now() - m.startedAt);
|
|
1756
|
+
console.log(`📦 Instance: PID ${m.pid} | 启动于 ${new Date(m.startedAtIso).toLocaleString()} (${uptime}) | via ${m.launchedBy}`);
|
|
1757
|
+
if (instStatus.aidLastActivity.size > 0) {
|
|
1758
|
+
const now = Date.now();
|
|
1759
|
+
const aidLines = [];
|
|
1760
|
+
for (const [aid, info] of instStatus.aidLastActivity) {
|
|
1761
|
+
const ago = formatTimeAgo(now - info.ts);
|
|
1762
|
+
const symbol = info.event === 'disconnected' ? '✗' : '✓';
|
|
1763
|
+
aidLines.push(` ${symbol} ${aid} — ${info.event} ${ago}`);
|
|
1764
|
+
}
|
|
1765
|
+
console.log(`🔑 AIDs:\n${aidLines.join('\n')}`);
|
|
1766
|
+
}
|
|
1767
|
+
console.log('');
|
|
1768
|
+
}
|
|
1769
|
+
else {
|
|
1770
|
+
console.log('⚠ EvolClaw 主进程未运行\n');
|
|
1771
|
+
}
|
|
1772
|
+
// Backfill: 跨所有 .log 汇总最近 20 条带时间戳行;遇到无时间戳行就停止该文件向上追溯
|
|
1773
|
+
const TAIL_BYTES = 256 * 1024;
|
|
1774
|
+
const collected = [];
|
|
1775
|
+
for (const file of listLogs()) {
|
|
1776
|
+
let stat;
|
|
1777
|
+
try {
|
|
1778
|
+
stat = fs.statSync(file);
|
|
1779
|
+
}
|
|
1780
|
+
catch {
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
const start = Math.max(0, stat.size - TAIL_BYTES);
|
|
1784
|
+
const buf = Buffer.alloc(stat.size - start);
|
|
1785
|
+
try {
|
|
1786
|
+
const fd = fs.openSync(file, 'r');
|
|
1787
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
1788
|
+
fs.closeSync(fd);
|
|
1789
|
+
}
|
|
1790
|
+
catch {
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
const lines = buf.toString('utf-8').split('\n');
|
|
1794
|
+
if (start > 0)
|
|
1795
|
+
lines.shift();
|
|
1796
|
+
if (lines.length && lines[lines.length - 1] === '')
|
|
1797
|
+
lines.pop();
|
|
1798
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1799
|
+
const ts = extractWatchTs(lines[i]);
|
|
1800
|
+
if (ts === null)
|
|
1801
|
+
break;
|
|
1802
|
+
collected.push({ ts, file, line: lines[i] });
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
collected.sort((a, b) => a.ts - b.ts);
|
|
1806
|
+
for (const r of collected.slice(-20)) {
|
|
1807
|
+
process.stdout.write(formatLine(r.file, r.ts, r.line) + '\n');
|
|
1808
|
+
}
|
|
1809
|
+
if (collected.length > 0)
|
|
1810
|
+
process.stdout.write('\n');
|
|
1811
|
+
// 实时跟踪
|
|
1812
|
+
const state = new Map();
|
|
1813
|
+
for (const file of listLogs()) {
|
|
1814
|
+
try {
|
|
1815
|
+
state.set(file, { position: fs.statSync(file).size, pending: '' });
|
|
1816
|
+
}
|
|
1817
|
+
catch { }
|
|
1818
|
+
}
|
|
1819
|
+
const pumpFile = (file) => {
|
|
1820
|
+
const s = state.get(file);
|
|
1821
|
+
if (!s)
|
|
1822
|
+
return;
|
|
1823
|
+
let stat;
|
|
1824
|
+
try {
|
|
1825
|
+
stat = fs.statSync(file);
|
|
1826
|
+
}
|
|
1827
|
+
catch {
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
if (stat.size < s.position) {
|
|
1831
|
+
s.position = 0;
|
|
1832
|
+
s.pending = '';
|
|
1833
|
+
}
|
|
1834
|
+
if (stat.size === s.position)
|
|
1835
|
+
return;
|
|
1836
|
+
const buf = Buffer.alloc(stat.size - s.position);
|
|
1837
|
+
try {
|
|
1838
|
+
const fd = fs.openSync(file, 'r');
|
|
1839
|
+
fs.readSync(fd, buf, 0, buf.length, s.position);
|
|
1840
|
+
fs.closeSync(fd);
|
|
1841
|
+
}
|
|
1842
|
+
catch {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
s.position = stat.size;
|
|
1846
|
+
const parts = (s.pending + buf.toString('utf-8')).split('\n');
|
|
1847
|
+
s.pending = parts.pop() || '';
|
|
1848
|
+
for (const line of parts) {
|
|
1849
|
+
if (!line.trim())
|
|
1850
|
+
continue;
|
|
1851
|
+
const ts = extractWatchTs(line);
|
|
1852
|
+
if (ts !== null) {
|
|
1853
|
+
process.stdout.write(formatLine(file, ts, line) + '\n');
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
// 无时间戳行:对齐到内容列
|
|
1857
|
+
const pad = 12 + 1 + maxNameLen + 1; // "HH:MM:SS.mmm" + space + name + space
|
|
1858
|
+
process.stdout.write(' '.repeat(pad) + line + '\n');
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
const timer = setInterval(() => {
|
|
1863
|
+
for (const file of listLogs()) {
|
|
1864
|
+
if (!state.has(file))
|
|
1865
|
+
state.set(file, { position: 0, pending: '' });
|
|
1866
|
+
}
|
|
1867
|
+
updateMaxName();
|
|
1868
|
+
for (const file of state.keys())
|
|
1869
|
+
pumpFile(file);
|
|
1870
|
+
}, 200);
|
|
1871
|
+
const cleanup = () => { clearInterval(timer); try {
|
|
1872
|
+
fs.unlinkSync(instanceFile);
|
|
1873
|
+
}
|
|
1874
|
+
catch { } if (process.stdin.isTTY)
|
|
1875
|
+
process.stdin.setRawMode(false); process.exit(0); };
|
|
1876
|
+
process.on('exit', () => { try {
|
|
1877
|
+
fs.unlinkSync(instanceFile);
|
|
1878
|
+
}
|
|
1879
|
+
catch { } });
|
|
1880
|
+
// ESC key listener
|
|
1881
|
+
if (process.stdin.isTTY) {
|
|
1882
|
+
process.stdin.setRawMode(true);
|
|
1883
|
+
process.stdin.resume();
|
|
1884
|
+
process.stdin.on('data', (key) => {
|
|
1885
|
+
if (key[0] === 0x1b && key.length === 1)
|
|
1886
|
+
cleanup(); // ESC
|
|
1887
|
+
if (key[0] === 0x03)
|
|
1888
|
+
cleanup(); // Ctrl+C fallback
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
platform.onShutdown(cleanup);
|
|
1892
|
+
}
|
|
1893
|
+
// ==================== Watch AID (real-time stats table) ====================
|
|
1894
|
+
function formatBytes(bytes) {
|
|
1895
|
+
if (bytes === 0)
|
|
1896
|
+
return '0 B';
|
|
1897
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
1898
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
1899
|
+
const val = bytes / Math.pow(1024, i);
|
|
1900
|
+
return i === 0 ? `${bytes} B` : `${val.toFixed(1)} ${units[i]}`;
|
|
1901
|
+
}
|
|
1902
|
+
function formatTimeAgoShort(ms) {
|
|
1903
|
+
const sec = Math.floor(ms / 1000);
|
|
1904
|
+
if (sec < 60)
|
|
1905
|
+
return `${sec}s ago`;
|
|
1906
|
+
const min = Math.floor(sec / 60);
|
|
1907
|
+
if (min < 60)
|
|
1908
|
+
return `${min}m ago`;
|
|
1909
|
+
const hour = Math.floor(min / 60);
|
|
1910
|
+
if (hour < 24)
|
|
1911
|
+
return `${hour}h ago`;
|
|
1912
|
+
const day = Math.floor(hour / 24);
|
|
1913
|
+
return `${day}d ago`;
|
|
1914
|
+
}
|
|
1915
|
+
async function cmdWatchAid() {
|
|
1916
|
+
const p = resolvePaths();
|
|
1917
|
+
// Get version
|
|
1918
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
|
|
1919
|
+
const version = pkg.version;
|
|
1920
|
+
// Load AID names: first from local agent.md, then refresh from network
|
|
1921
|
+
const { aidList, aidLookup } = await import('../aun/aid/index.js');
|
|
1922
|
+
const localAids = aidList();
|
|
1923
|
+
const aidNameMap = new Map();
|
|
1924
|
+
const refreshedAids = new Set();
|
|
1925
|
+
function readLocalName(aid) {
|
|
1926
|
+
try {
|
|
1927
|
+
const mdPath = agentMdPath(aid);
|
|
1928
|
+
if (!fs.existsSync(mdPath))
|
|
1929
|
+
return undefined;
|
|
1930
|
+
const content = fs.readFileSync(mdPath, 'utf-8');
|
|
1931
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1932
|
+
if (!fmMatch)
|
|
1933
|
+
return undefined;
|
|
1934
|
+
const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
1935
|
+
return nameMatch?.[1]?.trim() || undefined;
|
|
1936
|
+
}
|
|
1937
|
+
catch {
|
|
1938
|
+
return undefined;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function parseNameFromContent(content) {
|
|
1942
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1943
|
+
if (!fmMatch)
|
|
1944
|
+
return undefined;
|
|
1945
|
+
const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
1946
|
+
return nameMatch?.[1]?.trim() || undefined;
|
|
1947
|
+
}
|
|
1948
|
+
// Phase 1: read cached local names
|
|
1949
|
+
for (const a of localAids) {
|
|
1950
|
+
if (!a.hasPrivateKey)
|
|
1951
|
+
continue;
|
|
1952
|
+
const name = readLocalName(a.aid);
|
|
1953
|
+
if (name)
|
|
1954
|
+
aidNameMap.set(a.aid, name);
|
|
1955
|
+
}
|
|
1956
|
+
// Phase 2: refresh names from network via aidLookup (async, non-blocking)
|
|
1957
|
+
const refreshNames = async () => {
|
|
1958
|
+
for (const a of localAids) {
|
|
1959
|
+
if (!a.hasPrivateKey)
|
|
1960
|
+
continue;
|
|
1961
|
+
try {
|
|
1962
|
+
const result = await aidLookup(a.aid);
|
|
1963
|
+
if (result.exists && result.content) {
|
|
1964
|
+
const name = parseNameFromContent(result.content);
|
|
1965
|
+
if (name)
|
|
1966
|
+
aidNameMap.set(a.aid, name);
|
|
1967
|
+
}
|
|
1968
|
+
refreshedAids.add(a.aid);
|
|
1969
|
+
}
|
|
1970
|
+
catch { /* ignore network errors */ }
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
refreshNames();
|
|
1974
|
+
// Register instance
|
|
1975
|
+
fs.mkdirSync(p.instanceDir, { recursive: true });
|
|
1976
|
+
const instanceFile = path.join(p.instanceDir, `watch-aid-${process.pid}.json`);
|
|
1977
|
+
fs.writeFileSync(instanceFile, JSON.stringify({
|
|
1978
|
+
pid: process.pid,
|
|
1979
|
+
startedAt: Date.now(),
|
|
1980
|
+
startedAtIso: new Date().toISOString(),
|
|
1981
|
+
type: 'watch-aid',
|
|
1982
|
+
}, null, 2));
|
|
1983
|
+
const useColor = !!process.stdout.isTTY;
|
|
1984
|
+
const RST = useColor ? '\x1b[0m' : '';
|
|
1985
|
+
const DIM = useColor ? '\x1b[2m' : '';
|
|
1986
|
+
const BOLD = useColor ? '\x1b[1m' : '';
|
|
1987
|
+
const CYAN = useColor ? '\x1b[36m' : '';
|
|
1988
|
+
const GREEN = useColor ? '\x1b[32m' : '';
|
|
1989
|
+
const RED = useColor ? '\x1b[31m' : '';
|
|
1990
|
+
const CLR_LINE = '\x1b[2K';
|
|
1991
|
+
const watchStartedAt = new Date();
|
|
1992
|
+
const watchStartStr = `${String(watchStartedAt.getHours()).padStart(2, '0')}:${String(watchStartedAt.getMinutes()).padStart(2, '0')}:${String(watchStartedAt.getSeconds()).padStart(2, '0')}`;
|
|
1993
|
+
const COL_AID = 28;
|
|
1994
|
+
const COL_STATUS = 14;
|
|
1995
|
+
const COL_UPTIME = 11;
|
|
1996
|
+
const COL_STATE = 11;
|
|
1997
|
+
const COL_RECONN = 6;
|
|
1998
|
+
const COL_RECV = 5;
|
|
1999
|
+
const COL_SENT = 5;
|
|
2000
|
+
const COL_SYS = 8;
|
|
2001
|
+
const COL_BIN = 9;
|
|
2002
|
+
const COL_BOUT = 9;
|
|
2003
|
+
const COL_LRECV = 10;
|
|
2004
|
+
const COL_LSENT = 10;
|
|
2005
|
+
const COL_PEERS = 5;
|
|
2006
|
+
// 表头跟随系统语言
|
|
2007
|
+
const isChinese = (process.env.LANG || process.env.LC_ALL || process.env.LANGUAGE || Intl.DateTimeFormat().resolvedOptions().locale || '').toLowerCase().includes('zh');
|
|
2008
|
+
const HEADERS = isChinese
|
|
2009
|
+
? { aid: 'AID', status: '状态', uptime: '运行', state: '工作', reconn: '重连', recv: '收', sent: '发', sys: '系统', bin: '入流量', bout: '出流量', lrecv: '最后收', lsent: '最后发', peers: '对端' }
|
|
2010
|
+
: { aid: 'AID', status: 'STATUS', uptime: 'UPTIME', state: 'STATE', reconn: 'RECONN', recv: 'RECV', sent: 'SENT', sys: 'SYS R/S', bin: 'BYTES IN', bout: 'BYTES OUT', lrecv: 'LAST RECV', lsent: 'LAST SENT', peers: 'PEERS' };
|
|
2011
|
+
function formatDuration(ms) {
|
|
2012
|
+
const sec = Math.floor(ms / 1000);
|
|
2013
|
+
if (sec < 60)
|
|
2014
|
+
return `${sec}s`;
|
|
2015
|
+
const min = Math.floor(sec / 60);
|
|
2016
|
+
const s = sec % 60;
|
|
2017
|
+
if (min < 60)
|
|
2018
|
+
return `${min}m${String(s).padStart(2, '0')}s`;
|
|
2019
|
+
const hour = Math.floor(min / 60);
|
|
2020
|
+
const m = min % 60;
|
|
2021
|
+
if (hour < 24)
|
|
2022
|
+
return `${hour}h${String(m).padStart(2, '0')}m${String(s).padStart(2, '0')}s`;
|
|
2023
|
+
const day = Math.floor(hour / 24);
|
|
2024
|
+
return `${day}d${hour % 24}h${String(m).padStart(2, '0')}m`;
|
|
2025
|
+
}
|
|
2026
|
+
function renderHeader() {
|
|
2027
|
+
return ' ' +
|
|
2028
|
+
padRight(HEADERS.aid, COL_AID) +
|
|
2029
|
+
padRight(HEADERS.status, COL_STATUS) +
|
|
2030
|
+
padRight(HEADERS.uptime, COL_UPTIME) +
|
|
2031
|
+
padRight(HEADERS.state, COL_STATE) +
|
|
2032
|
+
padRight(HEADERS.reconn, COL_RECONN) +
|
|
2033
|
+
padRight(HEADERS.recv, COL_RECV) +
|
|
2034
|
+
padRight(HEADERS.sent, COL_SENT) +
|
|
2035
|
+
padRight(HEADERS.sys, COL_SYS) +
|
|
2036
|
+
padRight(HEADERS.bin, COL_BIN) +
|
|
2037
|
+
padRight(HEADERS.bout, COL_BOUT) +
|
|
2038
|
+
padRight(HEADERS.lrecv, COL_LRECV) +
|
|
2039
|
+
padRight(HEADERS.lsent, COL_LSENT) +
|
|
2040
|
+
padRight(HEADERS.peers, COL_PEERS);
|
|
2041
|
+
}
|
|
2042
|
+
function renderRow(aid, stats, projectPath) {
|
|
2043
|
+
const aidLabel = aid.aid.length > COL_AID - 2 ? aid.aid.slice(0, COL_AID - 4) + '..' : aid.aid;
|
|
2044
|
+
const statusLabel = AID_STATUS_LABELS[aid.status] || aid.status;
|
|
2045
|
+
const now = Date.now();
|
|
2046
|
+
const lastRecv = stats?.lastReceivedAt ? formatTimeAgoShort(now - stats.lastReceivedAt) : '—';
|
|
2047
|
+
const lastSent = stats?.lastSentAt ? formatTimeAgoShort(now - stats.lastSentAt) : '—';
|
|
2048
|
+
const uptime = (aid.status === 'connected' && aid.lastConnectedAt)
|
|
2049
|
+
? formatDuration(now - aid.lastConnectedAt)
|
|
2050
|
+
: '—';
|
|
2051
|
+
// State: processing / queued / idle
|
|
2052
|
+
const YELLOW = useColor ? '\x1b[33m' : '';
|
|
2053
|
+
let stateLabel = 'idle';
|
|
2054
|
+
if (stats?.processing > 0) {
|
|
2055
|
+
stateLabel = `${YELLOW}working${RST}`;
|
|
2056
|
+
}
|
|
2057
|
+
else if (stats?.queued > 0) {
|
|
2058
|
+
stateLabel = `${YELLOW}queued(${stats.queued})${RST}`;
|
|
2059
|
+
}
|
|
2060
|
+
const mainLine = ' ' +
|
|
2061
|
+
padRight(aidLabel, COL_AID) +
|
|
2062
|
+
padRight(statusLabel, COL_STATUS) +
|
|
2063
|
+
padRight(uptime, COL_UPTIME) +
|
|
2064
|
+
padRight(stateLabel, COL_STATE) +
|
|
2065
|
+
padRight(String(aid.reconnectCount ?? 0), COL_RECONN) +
|
|
2066
|
+
padRight(String(stats?.messagesReceived ?? 0), COL_RECV) +
|
|
2067
|
+
padRight(String(stats?.messagesSent ?? 0), COL_SENT) +
|
|
2068
|
+
padRight(`${stats?.systemReceived ?? 0}/${stats?.systemSent ?? 0}`, COL_SYS) +
|
|
2069
|
+
padRight(formatBytes(stats?.bytesReceived ?? 0), COL_BIN) +
|
|
2070
|
+
padRight(formatBytes(stats?.bytesSent ?? 0), COL_BOUT) +
|
|
2071
|
+
padRight(lastRecv, COL_LRECV) +
|
|
2072
|
+
padRight(lastSent, COL_LSENT) +
|
|
2073
|
+
padRight(String(stats?.uniquePeerCount ?? 0), COL_PEERS);
|
|
2074
|
+
const namePart = aidNameMap.get(aid.aid) || stats?.selfName || aid.agentName || '';
|
|
2075
|
+
const nameColor = refreshedAids.has(aid.aid) ? '' : DIM;
|
|
2076
|
+
const nameReset = refreshedAids.has(aid.aid) ? '' : RST;
|
|
2077
|
+
const BLUE = useColor ? '\x1b[34m' : '';
|
|
2078
|
+
const ORANGE = useColor ? '\x1b[38;5;208m' : '';
|
|
2079
|
+
const MAGENTA = useColor ? '\x1b[35m' : '';
|
|
2080
|
+
// 标记生成:[明文/密文|自主/响应](紫色=工具渲染标记)
|
|
2081
|
+
const mkTags = (encrypt, chatmode) => {
|
|
2082
|
+
const enc = encrypt ? '密文' : '明文';
|
|
2083
|
+
const mode = chatmode === 'proactive' ? '自主' : '响应';
|
|
2084
|
+
return `${MAGENTA}[${enc}|${mode}]${RST}`;
|
|
2085
|
+
};
|
|
2086
|
+
let msgPreview = '';
|
|
2087
|
+
if (stats?.lastReceivedAt || stats?.lastSentAt) {
|
|
2088
|
+
const recvTs = stats.lastReceivedAt ?? 0;
|
|
2089
|
+
const sentTs = stats.lastSentAt ?? 0;
|
|
2090
|
+
if (recvTs >= sentTs && stats.lastReceivedText) {
|
|
2091
|
+
const fromShort = stats.lastReceivedFrom ? stats.lastReceivedFrom.split('.')[0] : '';
|
|
2092
|
+
msgPreview = `${GREEN}↓ ${fromShort ? `${ORANGE}${fromShort}${RST}${GREEN}: ` : ''}${stats.lastReceivedText.replace(/\n/g, ' ').slice(0, 60)}${RST}`;
|
|
2093
|
+
}
|
|
2094
|
+
else if (stats.lastSentText) {
|
|
2095
|
+
const toShort = stats.lastSentTo ? stats.lastSentTo.split('.')[0] : '';
|
|
2096
|
+
const tags = mkTags(stats.lastSentEncrypt, stats.lastSentChatmode);
|
|
2097
|
+
// task 进行中时也显示计数(processing > 0 说明还在跑)
|
|
2098
|
+
const isWorking = (stats.processing ?? 0) > 0;
|
|
2099
|
+
const taskEnd = stats?.lastTaskEnd;
|
|
2100
|
+
const counts = isWorking && taskEnd
|
|
2101
|
+
? `${MAGENTA}[大模型${taskEnd.numTurns}|调用${taskEnd.toolUseCount}|thought${taskEnd.thoughtPutCount}|msg${taskEnd.replyCount}]${RST}`
|
|
2102
|
+
: '';
|
|
2103
|
+
msgPreview = `${BLUE}↑${tags}${counts} ${toShort ? `${ORANGE}${toShort}${RST}${BLUE}: ` : ''}${stats.lastSentText.replace(/\n/g, ' ').slice(0, 60)}${RST}`;
|
|
2104
|
+
}
|
|
2105
|
+
else if (stats.lastReceivedText) {
|
|
2106
|
+
const fromShort = stats.lastReceivedFrom ? stats.lastReceivedFrom.split('.')[0] : '';
|
|
2107
|
+
msgPreview = `${GREEN}↓ ${fromShort ? `${ORANGE}${fromShort}${RST}${GREEN}: ` : ''}${stats.lastReceivedText.replace(/\n/g, ' ').slice(0, 60)}${RST}`;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
// 任务结束状态覆盖:仅当 taskEnd 比最后收发都新时才覆盖
|
|
2111
|
+
const taskEnd = stats?.lastTaskEnd;
|
|
2112
|
+
if (taskEnd && taskEnd.ts >= (stats?.lastSentAt ?? 0) && taskEnd.ts >= (stats?.lastReceivedAt ?? 0)) {
|
|
2113
|
+
const tags = mkTags(taskEnd.encrypt, taskEnd.chatmode);
|
|
2114
|
+
// 计数标记: [大模型N|调用N|thoughtN(streamN)|msgN]
|
|
2115
|
+
const thoughtLabel = taskEnd.thoughtPutCount > 0
|
|
2116
|
+
? `thought${taskEnd.numTurns}(stream${taskEnd.thoughtPutCount})`
|
|
2117
|
+
: `thought${taskEnd.numTurns}`;
|
|
2118
|
+
const counts = `${MAGENTA}[大模型${taskEnd.numTurns}|调用${taskEnd.toolUseCount}|${thoughtLabel}|msg${taskEnd.replyCount}]${RST}`;
|
|
2119
|
+
if (taskEnd.status === 'error') {
|
|
2120
|
+
msgPreview = `${RED}${tags}${counts} 错误: ${taskEnd.errorType ?? '未知错误'}${RST}`;
|
|
2121
|
+
}
|
|
2122
|
+
else if (taskEnd.sentDuringTask) {
|
|
2123
|
+
// 有 message.send:蓝色加粗 + 内容
|
|
2124
|
+
const toShort = stats?.lastSentTo ? stats.lastSentTo.split('.')[0] : '';
|
|
2125
|
+
const textPreview = stats?.lastSentText ? stats.lastSentText.replace(/\n/g, ' ').slice(0, 60) : '';
|
|
2126
|
+
msgPreview = `${BOLD}${BLUE}↑${tags}${counts} ${toShort ? `${ORANGE}${toShort}${RST}${BOLD}${BLUE}: ` : ''}${textPreview}${RST}`;
|
|
2127
|
+
}
|
|
2128
|
+
else if (taskEnd.thoughtDuringTask) {
|
|
2129
|
+
// 只有 thought:普通蓝色 + thought 内容
|
|
2130
|
+
const textPreview = taskEnd.lastThoughtText
|
|
2131
|
+
? taskEnd.lastThoughtText.replace(/\n/g, ' ').slice(0, 60)
|
|
2132
|
+
: (taskEnd.finalText ? taskEnd.finalText.replace(/\n/g, ' ').slice(0, 60) : '');
|
|
2133
|
+
msgPreview = `${BLUE}↑${tags}${counts} ${textPreview}${RST}`;
|
|
2134
|
+
}
|
|
2135
|
+
else {
|
|
2136
|
+
// 既没 send 也没 thought
|
|
2137
|
+
const textPreview = taskEnd.finalText
|
|
2138
|
+
? taskEnd.finalText.replace(/\n/g, ' ').slice(0, 60)
|
|
2139
|
+
: '(无输出)';
|
|
2140
|
+
msgPreview = `${ORANGE}${tags}${counts} ${textPreview}${RST}`;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
const subLine1 = ` ${nameColor}${namePart}${nameReset}${msgPreview ? ' ' + msgPreview : ''}`;
|
|
2144
|
+
const dirLabel = projectPath || '—';
|
|
2145
|
+
const subLine2 = `${DIM} ${dirLabel}${RST}`;
|
|
2146
|
+
const result = [mainLine, subLine1, subLine2];
|
|
2147
|
+
if (aid.status === 'failed' || aid.status === 'kicked' || aid.status === 'kicked_no_retry') {
|
|
2148
|
+
const parts = [];
|
|
2149
|
+
if (aid.lastAttemptAt) {
|
|
2150
|
+
const d = new Date(aid.lastAttemptAt);
|
|
2151
|
+
const ts = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
|
|
2152
|
+
parts.push(`last_attempt=${ts}`);
|
|
2153
|
+
}
|
|
2154
|
+
if (aid.kickDetail?.code) {
|
|
2155
|
+
parts.push(`code=${aid.kickDetail.code}`);
|
|
2156
|
+
}
|
|
2157
|
+
if (aid.kickDetail?.reason) {
|
|
2158
|
+
parts.push(`reason=${aid.kickDetail.reason}`);
|
|
2159
|
+
}
|
|
2160
|
+
if (aid.lastError) {
|
|
2161
|
+
parts.push(`error=${aid.lastError}`);
|
|
2162
|
+
}
|
|
2163
|
+
if (aid.gatewayUrl) {
|
|
2164
|
+
parts.push(`gateway=${aid.gatewayUrl}`);
|
|
2165
|
+
}
|
|
2166
|
+
if (parts.length > 0) {
|
|
2167
|
+
result.push(`${RED} ⚠ ${parts.join(' ')}${RST}`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
return result;
|
|
2171
|
+
}
|
|
2172
|
+
let lastLineCount = 0;
|
|
2173
|
+
async function render() {
|
|
2174
|
+
const lines = [];
|
|
2175
|
+
// Query daemon — may be offline
|
|
2176
|
+
const [aidsResp, statsResp, statusResp, agentsResp] = await Promise.all([
|
|
2177
|
+
ipcQuery(p.socket, { type: 'aun-aids' }),
|
|
2178
|
+
ipcQuery(p.socket, { type: 'aun-aid-stats' }),
|
|
2179
|
+
ipcQuery(p.socket, { type: 'status' }),
|
|
2180
|
+
ipcQuery(p.socket, { type: 'evolagent.list' }),
|
|
2181
|
+
]);
|
|
2182
|
+
const daemonOnline = statusResp !== null;
|
|
2183
|
+
const aids = aidsResp?.aids ?? [];
|
|
2184
|
+
const stats = statsResp?.stats ?? [];
|
|
2185
|
+
const statsMap = new Map();
|
|
2186
|
+
for (const s of stats)
|
|
2187
|
+
statsMap.set(s.aid, s);
|
|
2188
|
+
// Map agentName → projectPath
|
|
2189
|
+
const agents = agentsResp?.agents ?? [];
|
|
2190
|
+
const agentProjectMap = new Map();
|
|
2191
|
+
for (const a of agents) {
|
|
2192
|
+
if (a.name && a.projectPath)
|
|
2193
|
+
agentProjectMap.set(a.name, a.projectPath);
|
|
2194
|
+
}
|
|
2195
|
+
const now = new Date();
|
|
2196
|
+
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
2197
|
+
// Compute daemon start time
|
|
2198
|
+
let startedAtStr = '';
|
|
2199
|
+
if (daemonOnline && statusResp?.uptime) {
|
|
2200
|
+
const startedAt = new Date(Date.now() - statusResp.uptime);
|
|
2201
|
+
startedAtStr = `${String(startedAt.getHours()).padStart(2, '0')}:${String(startedAt.getMinutes()).padStart(2, '0')}:${String(startedAt.getSeconds()).padStart(2, '0')}`;
|
|
2202
|
+
}
|
|
2203
|
+
const statusIndicator = daemonOnline
|
|
2204
|
+
? `${GREEN}● Running${RST}`
|
|
2205
|
+
: `${RED}● Offline${RST}`;
|
|
2206
|
+
const startInfo = startedAtStr ? ` | Started: ${startedAtStr}` : '';
|
|
2207
|
+
lines.push(`${BOLD}${CYAN}📊 EvolClaw AID Monitor${RST} ${statusIndicator} ${DIM}${timeStr} | Watch: ${watchStartStr}${startInfo} | Refresh: 1s | ESC to exit${RST}`);
|
|
2208
|
+
lines.push('');
|
|
2209
|
+
if (!daemonOnline) {
|
|
2210
|
+
lines.push(` ${RED}EvolClaw is not running.${RST} Waiting for daemon to start...`);
|
|
2211
|
+
lines.push('');
|
|
2212
|
+
}
|
|
2213
|
+
else if (aids.length === 0) {
|
|
2214
|
+
lines.push(' No active AIDs');
|
|
2215
|
+
lines.push('');
|
|
2216
|
+
}
|
|
2217
|
+
else {
|
|
2218
|
+
lines.push(`${DIM}${renderHeader()}${RST}`);
|
|
2219
|
+
const lineWidth = COL_AID + COL_STATUS + COL_UPTIME + COL_STATE + COL_RECONN + COL_RECV + COL_SENT + COL_SYS + COL_BIN + COL_BOUT + COL_LRECV + COL_LSENT + COL_PEERS;
|
|
2220
|
+
lines.push(`${DIM} ${'─'.repeat(lineWidth)}${RST}`);
|
|
2221
|
+
for (const aid of aids) {
|
|
2222
|
+
const s = statsMap.get(aid.aid);
|
|
2223
|
+
const projPath = agentProjectMap.get(aid.agentName);
|
|
2224
|
+
lines.push(...renderRow(aid, s, projPath));
|
|
2225
|
+
}
|
|
2226
|
+
lines.push('');
|
|
2227
|
+
}
|
|
2228
|
+
// Status bar
|
|
2229
|
+
lines.push(`${DIM} ${'─'.repeat(80)}${RST}`);
|
|
2230
|
+
if (daemonOnline) {
|
|
2231
|
+
const connectedCount = aids.filter((a) => a.status === 'connected').length;
|
|
2232
|
+
const totalRecv = stats.reduce((sum, s) => sum + (s.messagesReceived ?? 0), 0);
|
|
2233
|
+
const totalSent = stats.reduce((sum, s) => sum + (s.messagesSent ?? 0), 0);
|
|
2234
|
+
const totalBytesIn = stats.reduce((sum, s) => sum + (s.bytesReceived ?? 0), 0);
|
|
2235
|
+
const totalBytesOut = stats.reduce((sum, s) => sum + (s.bytesSent ?? 0), 0);
|
|
2236
|
+
const gateways = [...new Set(aids.filter((a) => a.gatewayUrl).map((a) => a.gatewayUrl))];
|
|
2237
|
+
const gatewayStr = gateways.length > 0 ? gateways.join(', ') : '—';
|
|
2238
|
+
const daemonUptime = statusResp?.uptime ? formatDuration(statusResp.uptime) : '—';
|
|
2239
|
+
const daemonPid = statusResp?.pid ?? '—';
|
|
2240
|
+
lines.push(` ${GREEN}Gateway:${RST} ${gatewayStr}`);
|
|
2241
|
+
lines.push(` ${GREEN}AIDs:${RST} ${aids.length} total, ${connectedCount} connected | ${GREEN}Messages:${RST} ↓${totalRecv} ↑${totalSent} | ${GREEN}Traffic:${RST} ↓${formatBytes(totalBytesIn)} ↑${formatBytes(totalBytesOut)}`);
|
|
2242
|
+
lines.push(` ${GREEN}Version:${RST} ${version} | ${GREEN}PID:${RST} ${daemonPid} | ${GREEN}Uptime:${RST} ${daemonUptime}`);
|
|
2243
|
+
}
|
|
2244
|
+
else {
|
|
2245
|
+
lines.push(` ${GREEN}Version:${RST} ${version}`);
|
|
2246
|
+
}
|
|
2247
|
+
// Build frame buffer: cursor home, then each line with clear-to-EOL
|
|
2248
|
+
let buf = '\x1b[H';
|
|
2249
|
+
for (const line of lines) {
|
|
2250
|
+
buf += CLR_LINE + line + '\n';
|
|
2251
|
+
}
|
|
2252
|
+
// Clear any leftover lines from previous frame
|
|
2253
|
+
for (let i = lines.length; i < lastLineCount; i++) {
|
|
2254
|
+
buf += CLR_LINE + '\n';
|
|
2255
|
+
}
|
|
2256
|
+
lastLineCount = lines.length;
|
|
2257
|
+
process.stdout.write(buf);
|
|
2258
|
+
}
|
|
2259
|
+
// Initial clear screen
|
|
2260
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
2261
|
+
await render();
|
|
2262
|
+
const timer = setInterval(render, 1000);
|
|
2263
|
+
const cleanup = () => {
|
|
2264
|
+
clearInterval(timer);
|
|
2265
|
+
try {
|
|
2266
|
+
fs.unlinkSync(instanceFile);
|
|
2267
|
+
}
|
|
2268
|
+
catch { }
|
|
2269
|
+
process.exit(0);
|
|
2270
|
+
};
|
|
2271
|
+
// Listen for ESC key
|
|
2272
|
+
if (process.stdin.isTTY) {
|
|
2273
|
+
process.stdin.setRawMode(true);
|
|
2274
|
+
process.stdin.resume();
|
|
2275
|
+
process.stdin.on('data', (data) => {
|
|
2276
|
+
if (data[0] === 0x1b || data[0] === 0x03)
|
|
2277
|
+
cleanup();
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
platform.onShutdown(cleanup);
|
|
2281
|
+
}
|
|
2282
|
+
function isEcwebInstanceFile(file) {
|
|
2283
|
+
return /^(ecweb|watch-web)-\d+\.json$/.test(file);
|
|
2284
|
+
}
|
|
2285
|
+
/** 扫描 instance/ 目录,返回存活的 ecweb 实例(兼容 ecweb-*.json 和旧 watch-web-*.json)。 */
|
|
2286
|
+
function findAliveEcwebs(p) {
|
|
2287
|
+
const alive = [];
|
|
2288
|
+
if (!fs.existsSync(p.instanceDir))
|
|
2289
|
+
return alive;
|
|
2290
|
+
for (const file of fs.readdirSync(p.instanceDir)) {
|
|
2291
|
+
if (!isEcwebInstanceFile(file))
|
|
2292
|
+
continue;
|
|
2293
|
+
const filePath = path.join(p.instanceDir, file);
|
|
2294
|
+
try {
|
|
2295
|
+
const rec = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
2296
|
+
if (rec.pid && platform.isProcessRunning(rec.pid)) {
|
|
2297
|
+
alive.push({ pid: rec.pid, port: rec.port ?? 42705 });
|
|
2298
|
+
}
|
|
2299
|
+
else {
|
|
2300
|
+
fs.unlinkSync(filePath);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
catch { }
|
|
2304
|
+
}
|
|
2305
|
+
return alive;
|
|
2306
|
+
}
|
|
2307
|
+
function findAliveEcweb(p) {
|
|
2308
|
+
const alive = findAliveEcwebs(p);
|
|
2309
|
+
return alive[0] ?? null;
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* 轻量 HTTP 就绪探测:GET / 返回 2xx 即视为就绪。
|
|
2313
|
+
* 不走 /api/pair-code(会触发配对码刷新副作用),不走 /api/health(不存在)。
|
|
2314
|
+
* 根路径由 serveStatic 处理,返回 index.html,无副作用。
|
|
2315
|
+
*/
|
|
2316
|
+
async function probeEcwebReady(port) {
|
|
2317
|
+
try {
|
|
2318
|
+
const res = await fetch(`http://127.0.0.1:${port}/`, {
|
|
2319
|
+
method: 'GET',
|
|
2320
|
+
signal: AbortSignal.timeout(2000),
|
|
2321
|
+
});
|
|
2322
|
+
return res.ok;
|
|
2323
|
+
}
|
|
2324
|
+
catch {
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* 输出 ECWeb 状态。判定口径:
|
|
2330
|
+
* 1. 进程是否存活(instance/ 下的 pid 文件 + isProcessRunning)
|
|
2331
|
+
* 2. HTTP 是否就绪——探测根路径 /(serveStatic → index.html, 无副作用)。
|
|
2332
|
+
*/
|
|
2333
|
+
async function printEcwebStatus(p) {
|
|
2334
|
+
const cfg = loadEvolclawConfig();
|
|
2335
|
+
if (cfg.ecweb?.enabled === false) {
|
|
2336
|
+
console.log('');
|
|
2337
|
+
console.log('🔭 ECWeb: 已禁用 (evolclaw.json → ecweb.enabled: false)');
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
const inst = findAliveEcweb(p);
|
|
2341
|
+
if (!inst) {
|
|
2342
|
+
console.log('');
|
|
2343
|
+
console.log('🔭 ECWeb: 未运行');
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
// 进程存活,再确认 HTTP 真正就绪(探测根路径 /,无业务副作用)
|
|
2347
|
+
const ready = await probeEcwebReady(inst.port);
|
|
2348
|
+
console.log('');
|
|
2349
|
+
if (ready) {
|
|
2350
|
+
console.log(`🔭 ECWeb: 运行中 (PID: ${inst.pid}) http://localhost:${inst.port}`);
|
|
2351
|
+
}
|
|
2352
|
+
else {
|
|
2353
|
+
console.log(`🔭 ECWeb: 进程存活 (PID: ${inst.pid}) 但 HTTP 未就绪 (端口 ${inst.port}),查看 logs/watch-web.log`);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
function removeEcwebInstanceFiles(p) {
|
|
2357
|
+
if (!fs.existsSync(p.instanceDir))
|
|
2358
|
+
return;
|
|
2359
|
+
for (const file of fs.readdirSync(p.instanceDir)) {
|
|
2360
|
+
if (!isEcwebInstanceFile(file))
|
|
2361
|
+
continue;
|
|
2362
|
+
try {
|
|
2363
|
+
fs.unlinkSync(path.join(p.instanceDir, file));
|
|
2364
|
+
}
|
|
2365
|
+
catch { }
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
/** 若 ecweb 在运行则杀掉并清理 pid 文件,返回是否成功 kill。 */
|
|
2369
|
+
function stopEcwebIfRunning(p) {
|
|
2370
|
+
const alive = findAliveEcweb(p);
|
|
2371
|
+
let killed = false;
|
|
2372
|
+
if (alive) {
|
|
2373
|
+
try {
|
|
2374
|
+
platform.killProcess(alive.pid, true);
|
|
2375
|
+
}
|
|
2376
|
+
catch { }
|
|
2377
|
+
killed = true;
|
|
2378
|
+
}
|
|
2379
|
+
// 清理 pid 文件(仅 ecweb-<pid>.json / watch-web-<pid>.json,
|
|
2380
|
+
// 不碰 ecweb-tokens.json 等非 pid 文件,否则会清空配对 token 库导致每次重启都要重新配对)
|
|
2381
|
+
removeEcwebInstanceFiles(p);
|
|
2382
|
+
// 端口兜底:杀掉任何仍占用 ecweb 端口的残留进程(含手动启动、未登记 pid 文件的)。
|
|
2383
|
+
// 仅靠 pid 文件无法清理这类进程,会导致下次启动端口被占。
|
|
2384
|
+
const port = loadEvolclawConfig().ecweb?.port ?? 42705;
|
|
2385
|
+
for (const pid of platform.findProcessByPort(port)) {
|
|
2386
|
+
try {
|
|
2387
|
+
platform.killProcess(pid, true);
|
|
2388
|
+
killed = true;
|
|
2389
|
+
}
|
|
2390
|
+
catch { }
|
|
2391
|
+
}
|
|
2392
|
+
return killed;
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* 后台 detached 启动 ecweb;若已运行则先停再启(确保加载最新代码)。
|
|
2396
|
+
* 启动后轮询端口确认 HTTP 服务真正就绪,打印明确的成功/失败结论(而非模糊的「已在后台启动」状态描述)。
|
|
2397
|
+
* 返回 true=本次确实启动成功,false=未启用/未安装/启动失败。
|
|
2398
|
+
*/
|
|
2399
|
+
async function startEcwebIfEnabled(p) {
|
|
2400
|
+
const cfg = loadEvolclawConfig();
|
|
2401
|
+
if (!cfg.ecweb?.enabled)
|
|
2402
|
+
return false;
|
|
2403
|
+
stopEcwebIfRunning(p); // 先停旧进程(有则停),保证加载最新代码
|
|
2404
|
+
const port = cfg.ecweb.port ?? 42705;
|
|
2405
|
+
const args = ['--home', p.root, '--port', String(port)];
|
|
2406
|
+
const launch = resolveEcwebLaunchCommand(args);
|
|
2407
|
+
if (!launch) {
|
|
2408
|
+
console.log('⚠ ECWeb 未安装,跳过启动(运行 npm i -g evolclaw-web 后可用)');
|
|
2409
|
+
return false;
|
|
2410
|
+
}
|
|
2411
|
+
const child = spawn(launch.command, launch.args, {
|
|
2412
|
+
detached: true,
|
|
2413
|
+
stdio: 'ignore',
|
|
2414
|
+
windowsHide: true,
|
|
2415
|
+
});
|
|
2416
|
+
child.unref();
|
|
2417
|
+
const pid = child.pid;
|
|
2418
|
+
if (!pid) {
|
|
2419
|
+
console.log('❌ ECWeb 启动失败(进程创建失败)');
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2422
|
+
fs.mkdirSync(p.instanceDir, { recursive: true });
|
|
2423
|
+
fs.writeFileSync(path.join(p.instanceDir, `ecweb-${pid}.json`), JSON.stringify({ pid, port, startedAt: Date.now() }, null, 2));
|
|
2424
|
+
// 轮询端口确认 HTTP 服务真正就绪(spawn 成功 ≠ 端口绑定成功),顺便拿配对码
|
|
2425
|
+
let pair = null;
|
|
2426
|
+
for (let i = 0; i < 20; i++) {
|
|
2427
|
+
pair = await fetchEcwebPairCode(port);
|
|
2428
|
+
if (pair)
|
|
2429
|
+
break;
|
|
2430
|
+
await sleep(250);
|
|
2431
|
+
}
|
|
2432
|
+
if (pair) {
|
|
2433
|
+
const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
|
|
2434
|
+
console.log(`✓ ECWeb 启动成功 (PID: ${pid}) http://localhost:${port}`);
|
|
2435
|
+
console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,已配对过的浏览器无需重新配对)`);
|
|
2436
|
+
return true;
|
|
2437
|
+
}
|
|
2438
|
+
console.log(`❌ ECWeb 启动失败:端口 ${port} 未就绪(进程 PID ${pid} 可能已退出,查看 logs/watch-web.log)`);
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2441
|
+
/** 显示 ecweb 访问信息 + 配对码(启动后 ecweb 需要一点时间起 HTTP,故重试几次)。 */
|
|
2442
|
+
async function printEcwebAccess(port) {
|
|
2443
|
+
console.log(`🔭 ECWeb http://localhost:${port}`);
|
|
2444
|
+
let pair = null;
|
|
2445
|
+
for (let i = 0; i < 10 && !pair; i++) {
|
|
2446
|
+
pair = await fetchEcwebPairCode(port);
|
|
2447
|
+
if (!pair)
|
|
2448
|
+
await sleep(300);
|
|
2449
|
+
}
|
|
2450
|
+
if (pair) {
|
|
2451
|
+
const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
|
|
2452
|
+
console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,配对后 token 缓存 24h)`);
|
|
2453
|
+
}
|
|
2454
|
+
else {
|
|
2455
|
+
console.log(' 配对码: 暂不可用(稍后重试 ec watch web,或查看 logs/watch-web.log)');
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
async function cmdWatchWeb() {
|
|
2459
|
+
const p = resolvePaths();
|
|
2460
|
+
// 1. 检查安装
|
|
2461
|
+
if (!platform.commandExists('evolclaw-web')) {
|
|
2462
|
+
process.stdout.write('📦 evolclaw-web 未安装。');
|
|
2463
|
+
if (!process.stdin.isTTY) {
|
|
2464
|
+
process.stdout.write(' 请手动安装: npm install -g evolclaw-web\n');
|
|
2465
|
+
process.exit(1);
|
|
2466
|
+
}
|
|
2467
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2468
|
+
const ans = await new Promise(res => rl.question(' 立即安装?[Y/n] ', res));
|
|
2469
|
+
rl.close();
|
|
2470
|
+
if (ans.trim().toLowerCase() === 'n') {
|
|
2471
|
+
process.exit(0);
|
|
2472
|
+
}
|
|
2473
|
+
process.stdout.write('\n');
|
|
2474
|
+
const { npmInstallGlobal } = await import('../utils/npm-ops.js');
|
|
2475
|
+
try {
|
|
2476
|
+
await npmInstallGlobal('evolclaw-web@latest');
|
|
2477
|
+
}
|
|
2478
|
+
catch (e) {
|
|
2479
|
+
process.stderr.write(`❌ 安装失败: ${e?.stderr || e?.message || e}\n`);
|
|
2480
|
+
process.exit(1);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
else {
|
|
2484
|
+
// 已安装:检查并自动升级到最新版(参考 fastaun 自动升级机制)
|
|
2485
|
+
const upgrade = await tryUpgradeGlobalPkg(() => resolveGlobalPkg('evolclaw-web'), 'evolclaw-web');
|
|
2486
|
+
switch (upgrade.status) {
|
|
2487
|
+
case 'upgraded':
|
|
2488
|
+
process.stdout.write(`✅ evolclaw-web 已升级: ${upgrade.from} → ${upgrade.to}\n`);
|
|
2489
|
+
break;
|
|
2490
|
+
case 'failed':
|
|
2491
|
+
process.stdout.write(`⚠ evolclaw-web 升级失败 (${upgrade.from} → ${upgrade.to}),继续使用当前版本\n`);
|
|
2492
|
+
break;
|
|
2493
|
+
// no-update / skipped: 静默
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
// 2. 启动(后台)并同步配置。默认行为是替换旧实例,确保新配置/新版静态资源生效。
|
|
2497
|
+
const cfg = loadEvolclawConfig();
|
|
2498
|
+
const port = cfg.ecweb?.port ?? 42705;
|
|
2499
|
+
if (cfg.ecweb?.enabled === undefined) {
|
|
2500
|
+
// 首次手动启动时自动写入 enabled:true
|
|
2501
|
+
saveEvolclawConfig({ ...cfg, ecweb: { enabled: true, port } });
|
|
2502
|
+
}
|
|
2503
|
+
if (cfg.ecweb?.enabled === false) {
|
|
2504
|
+
const alive = findAliveEcweb(p);
|
|
2505
|
+
if (alive) {
|
|
2506
|
+
await printEcwebAccess(alive.port);
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
process.stderr.write('❌ ECWeb 已禁用。请在 evolclaw.json 中启用 ecweb.enabled,或删除该字段后重试。\n');
|
|
2510
|
+
process.exit(1);
|
|
2511
|
+
}
|
|
2512
|
+
const ok = await startEcwebIfEnabled(p);
|
|
2513
|
+
if (!ok)
|
|
2514
|
+
process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
|
|
2515
|
+
await printEcwebAccess(port);
|
|
2516
|
+
}
|
|
2517
|
+
function sleep(ms) {
|
|
2518
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
2519
|
+
}
|
|
2520
|
+
// ==================== Migrate ====================
|
|
2521
|
+
export async function cmdMv(oldDir, newDir) {
|
|
2522
|
+
if (!oldDir || !newDir) {
|
|
2523
|
+
console.log('Usage: evolclaw mv <old_directory> <new_directory>');
|
|
2524
|
+
console.log('Example: evolclaw mv ~/projects/old-name ~/projects/new-name');
|
|
2525
|
+
process.exit(1);
|
|
2526
|
+
}
|
|
2527
|
+
const oldAbs = path.resolve(oldDir);
|
|
2528
|
+
const newAbs = path.resolve(newDir);
|
|
2529
|
+
console.log(`迁移项目: ${oldAbs} → ${newAbs}\n`);
|
|
2530
|
+
try {
|
|
2531
|
+
const r = await migrateProject(oldAbs, newAbs);
|
|
2532
|
+
if (r.claudeSessionsMoved)
|
|
2533
|
+
console.log('✓ Claude Code 会话目录已迁移');
|
|
2534
|
+
if (r.claudeHistoryUpdated)
|
|
2535
|
+
console.log('✓ Claude Code history.jsonl 已更新');
|
|
2536
|
+
if (r.codexUpdated > 0)
|
|
2537
|
+
console.log(`✓ Codex 数据库已更新 (${r.codexUpdated} 个会话)`);
|
|
2538
|
+
if (r.directoryMoved)
|
|
2539
|
+
console.log('✓ 项目目录已移动');
|
|
2540
|
+
if (r.evolclawDbUpdated > 0)
|
|
2541
|
+
console.log(`✓ EvolClaw 会话存储已更新 (${r.evolclawDbUpdated} 条记录)`);
|
|
2542
|
+
console.log('\n迁移完成!');
|
|
2543
|
+
}
|
|
2544
|
+
catch (e) {
|
|
2545
|
+
console.error(`迁移失败: ${e instanceof Error ? e.message : e}`);
|
|
2546
|
+
process.exit(1);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
// ==================== Diagnose ====================
|
|
2550
|
+
export async function cmdDiagnose() {
|
|
2551
|
+
const p = resolvePaths();
|
|
2552
|
+
let hasError = false;
|
|
2553
|
+
// 1. 检查数据目录
|
|
2554
|
+
console.log(`[diagnose] EVOLCLAW_HOME = ${p.root}`);
|
|
2555
|
+
if (!fs.existsSync(p.root)) {
|
|
2556
|
+
console.error(`[diagnose] ❌ 数据目录不存在: ${p.root}`);
|
|
2557
|
+
hasError = true;
|
|
2558
|
+
}
|
|
2559
|
+
else {
|
|
2560
|
+
console.log(`[diagnose] ✓ 数据目录存在`);
|
|
2561
|
+
}
|
|
2562
|
+
// 2. 加载并校验配置
|
|
2563
|
+
try {
|
|
2564
|
+
// 2. 加载 self-agents
|
|
2565
|
+
const { agents, skipped } = loadAllAgents();
|
|
2566
|
+
if (agents.length === 0) {
|
|
2567
|
+
console.error(`[diagnose] ❌ 未配置 self-agent。请运行 \`evolclaw aid new <name>\``);
|
|
2568
|
+
hasError = true;
|
|
2569
|
+
if (skipped.length > 0) {
|
|
2570
|
+
console.error(`[diagnose] 跳过的目录:`);
|
|
2571
|
+
for (const s of skipped)
|
|
2572
|
+
console.error(` - ${s.dirName}: ${s.reason}`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
else {
|
|
2576
|
+
console.log(`[diagnose] ✓ 已加载 ${agents.length} 个 self-agent`);
|
|
2577
|
+
}
|
|
2578
|
+
// 3. 检查 Anthropic 配置(用首个 self-agent 的 effective config)
|
|
2579
|
+
if (agents.length > 0) {
|
|
2580
|
+
try {
|
|
2581
|
+
const defaults = loadDefaults();
|
|
2582
|
+
const merged = mergeForAgent(agents[0], defaults);
|
|
2583
|
+
const syntheticConfig = {
|
|
2584
|
+
agents: {
|
|
2585
|
+
claude: merged.baseagents?.claude,
|
|
2586
|
+
codex: merged.baseagents?.codex,
|
|
2587
|
+
gemini: merged.baseagents?.gemini,
|
|
2588
|
+
},
|
|
2589
|
+
channels: {},
|
|
2590
|
+
projects: merged.projects,
|
|
2591
|
+
};
|
|
2592
|
+
const anthropic = resolveAnthropicConfig(syntheticConfig);
|
|
2593
|
+
console.log(`[diagnose] ✓ Anthropic 配置解析成功 (apiKey: ${anthropic.apiKey ? '已设置' : '❌ 未设置'}, model: ${anthropic.model || 'default'})`);
|
|
2594
|
+
}
|
|
2595
|
+
catch (e) {
|
|
2596
|
+
console.error(`[diagnose] ❌ Anthropic 配置解析失败: ${e instanceof Error ? e.message : e}`);
|
|
2597
|
+
hasError = true;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
catch (e) {
|
|
2602
|
+
console.error(`[diagnose] ❌ 配置加载失败: ${e instanceof Error ? e.message : e}`);
|
|
2603
|
+
hasError = true;
|
|
2604
|
+
}
|
|
2605
|
+
// 4. 检查 Session 文件系统存储
|
|
2606
|
+
try {
|
|
2607
|
+
const { SessionManager } = await import('../core/session/session-manager.js');
|
|
2608
|
+
const eventBus = new EventBus();
|
|
2609
|
+
new SessionManager(p.sessionsDir, eventBus);
|
|
2610
|
+
console.log(`[diagnose] ✓ Session 存储初始化成功: ${p.sessionsDir}`);
|
|
2611
|
+
}
|
|
2612
|
+
catch (e) {
|
|
2613
|
+
console.error(`[diagnose] ❌ Session 存储初始化失败: ${e instanceof Error ? e.message : e}`);
|
|
2614
|
+
hasError = true;
|
|
2615
|
+
}
|
|
2616
|
+
// 5. 检查残留进程
|
|
2617
|
+
try {
|
|
2618
|
+
const instStatus = scanInstances();
|
|
2619
|
+
const aliveMains = instStatus.mains.filter(m => m.alive);
|
|
2620
|
+
if (aliveMains.length > 0) {
|
|
2621
|
+
console.log(`[diagnose] ⚠️ 已有进程运行中: PID ${aliveMains.map(m => m.record.pid).join(', ')}`);
|
|
2622
|
+
}
|
|
2623
|
+
else {
|
|
2624
|
+
console.log(`[diagnose] ✓ 无残留进程`);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
catch {
|
|
2628
|
+
console.log(`[diagnose] ✓ 无 instance 文件`);
|
|
2629
|
+
}
|
|
2630
|
+
// 6. 检查关键文件
|
|
2631
|
+
const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
2632
|
+
if (!fs.existsSync(appMain)) {
|
|
2633
|
+
console.error(`[diagnose] ❌ 编译产物不存在: ${appMain}`);
|
|
2634
|
+
hasError = true;
|
|
2635
|
+
}
|
|
2636
|
+
else {
|
|
2637
|
+
console.log(`[diagnose] ✓ 编译产物存在: ${appMain}`);
|
|
2638
|
+
}
|
|
2639
|
+
if (hasError) {
|
|
2640
|
+
console.error('\n[diagnose] ❌ 诊断发现问题,请修复后重试');
|
|
2641
|
+
process.exit(1);
|
|
2642
|
+
}
|
|
2643
|
+
else {
|
|
2644
|
+
console.log('\n[diagnose] ✓ 所有检查通过');
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
export async function cmdWatchCommand(args) {
|
|
2648
|
+
// watch 子命令(aid/msg)会调 AUN SDK(aidLookup 刷名片、对端探测等),
|
|
2649
|
+
// 与 aid/msg/group 等命令一致:进 case 先关掉 SDK 的 [aun_core] 日志,
|
|
2650
|
+
// 否则 SDK debug 日志会直喷终端、糊住 watch 的 TUI 面板。
|
|
2651
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
2652
|
+
suppressSdkLogs();
|
|
2653
|
+
if (args[0] === 'aid') {
|
|
2654
|
+
await cmdWatchAid();
|
|
2655
|
+
}
|
|
2656
|
+
else if (args[0] === 'msg') {
|
|
2657
|
+
if (isHelpFlag(args[1])) {
|
|
2658
|
+
console.log(`用法: evolclaw watch msg
|
|
2659
|
+
|
|
2660
|
+
三面板交互式消息监控 TUI。
|
|
2661
|
+
|
|
2662
|
+
面板:
|
|
2663
|
+
左 (Scope) 本地 AID 列表,显示收发统计和对端数量
|
|
2664
|
+
中 (Stats) 选中 AID 的对端列表(默认 All),显示 per-peer 收发数
|
|
2665
|
+
右 (Messages) 消息流,带滚动条
|
|
2666
|
+
|
|
2667
|
+
操作:
|
|
2668
|
+
↑↓ 当前面板内导航
|
|
2669
|
+
←→ / Tab 切换面板
|
|
2670
|
+
Enter 选中 AID / 选中对端
|
|
2671
|
+
Backspace 返回上一级
|
|
2672
|
+
Page Up/Down 消息滚动
|
|
2673
|
+
ESC 退出`);
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
const { cmdWatchMsg } = await import('./watch-msg.js');
|
|
2677
|
+
await cmdWatchMsg();
|
|
2678
|
+
}
|
|
2679
|
+
else if (args[0] === 'log' || args[0] === 'logs') {
|
|
2680
|
+
const requested = args.slice(1);
|
|
2681
|
+
if (requested.length > 0) {
|
|
2682
|
+
const p2 = resolvePaths();
|
|
2683
|
+
const avail = fs.existsSync(p2.logs)
|
|
2684
|
+
? deriveLogTypes(fs.readdirSync(p2.logs).filter(f => f.endsWith('.log')))
|
|
2685
|
+
: [];
|
|
2686
|
+
const invalid = validateLogTypes(requested, avail);
|
|
2687
|
+
if (invalid.length > 0) {
|
|
2688
|
+
console.log(`❌ 无效日志类型: ${invalid.join(', ')}`);
|
|
2689
|
+
console.log(`可用类型: ${avail.join(', ') || '(无)'}`);
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
cmdWatch(new Set(requested));
|
|
2693
|
+
}
|
|
2694
|
+
else {
|
|
2695
|
+
await cmdWatchLogsFlow();
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
else if (args[0] === 'web' || args[0] === 'session') {
|
|
2699
|
+
await cmdWatchWeb();
|
|
2700
|
+
}
|
|
2701
|
+
else if (!args[0]) {
|
|
2702
|
+
await cmdWatchMenu();
|
|
2703
|
+
}
|
|
2704
|
+
else {
|
|
2705
|
+
await cmdWatchLogsFlow();
|
|
2706
|
+
}
|
|
2707
|
+
}
|