evolclaw 2.8.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -12
- package/dist/agents/claude-runner.js +105 -30
- package/dist/agents/codex-runner.js +15 -7
- package/dist/agents/gemini-runner.js +14 -5
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1064 -279
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/baseagent-loader.js +48 -0
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +1090 -838
- package/dist/core/evolagent-registry.js +191 -360
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +326 -145
- package/dist/core/message/message-queue.js +5 -5
- package/dist/core/permission.js +21 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +704 -775
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/{templates → data}/prompts.md +34 -1
- package/dist/index.js +437 -273
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -576
- package/dist/core/agent-loader.js +0 -39
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
|
@@ -0,0 +1,4253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { spawn, execFile } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from '../paths.js';
|
|
8
|
+
import { loadDefaults, loadAllAgents, mergeForAgent } from '../config-store.js';
|
|
9
|
+
import { resolveAnthropicConfig } from '../agents/resolve.js';
|
|
10
|
+
import { migrateProject } from '../config-store.js';
|
|
11
|
+
import { cmdInit } from './init.js';
|
|
12
|
+
import { ipcQuery } from '../ipc.js';
|
|
13
|
+
import { cmdInitWechat, cmdInitFeishu, cmdInitDingtalk, cmdInitQQBot, cmdInitWecom } from './init-channel.js';
|
|
14
|
+
import * as platform from '../utils/cross-platform.js';
|
|
15
|
+
import { EventBus } from '../core/event-bus.js';
|
|
16
|
+
import { tryUpgrade } from '../utils/npm-ops.js';
|
|
17
|
+
import { scanInstances, cleanupInstances, writeRestartMonitor, removeRestartMonitor, isRestartMonitorWinner, findOrphanProcesses, killOrphans } from '../utils/instance-registry.js';
|
|
18
|
+
// Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
|
|
19
|
+
process.removeAllListeners('warning');
|
|
20
|
+
process.on('warning', (w) => { if (w.name === 'ExperimentalWarning')
|
|
21
|
+
return; process.stderr.write((w.stack ?? String(w)) + '\n'); });
|
|
22
|
+
const execFileAsync = promisify(execFile);
|
|
23
|
+
// 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
|
|
24
|
+
function cleanEnv() {
|
|
25
|
+
for (const key of [
|
|
26
|
+
'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT',
|
|
27
|
+
'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
|
28
|
+
'CLAUDE_CONFIG_DIR',
|
|
29
|
+
]) {
|
|
30
|
+
delete process.env[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 启动时归档过大的 stdout.log。其它 .log 文件由各自的 LogWriter 管理切片/清理,
|
|
35
|
+
* 不再扫描整个目录——LogWriter 模式下重复的 size 检查会和 hourly rotation 冲突。
|
|
36
|
+
*/
|
|
37
|
+
function rotateStdoutIfNeeded(logDir) {
|
|
38
|
+
if (!fs.existsSync(logDir))
|
|
39
|
+
return;
|
|
40
|
+
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
|
41
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
42
|
+
const stdoutLog = path.join(logDir, 'stdout.log');
|
|
43
|
+
// 归档当前 stdout.log(若超过 10MB)
|
|
44
|
+
try {
|
|
45
|
+
const stat = fs.statSync(stdoutLog);
|
|
46
|
+
if (stat.size > MAX_SIZE) {
|
|
47
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
48
|
+
const newPath = `${stdoutLog}.${timestamp}`;
|
|
49
|
+
fs.renameSync(stdoutLog, newPath);
|
|
50
|
+
console.log(` Rotated: stdout.log -> ${path.basename(newPath)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* file not exist */ }
|
|
54
|
+
// 清理 7 天前的 stdout.log.* 归档
|
|
55
|
+
try {
|
|
56
|
+
for (const file of fs.readdirSync(logDir)) {
|
|
57
|
+
if (!file.startsWith('stdout.log.'))
|
|
58
|
+
continue;
|
|
59
|
+
const full = path.join(logDir, file);
|
|
60
|
+
try {
|
|
61
|
+
if (fs.statSync(full).mtimeMs < cutoff)
|
|
62
|
+
fs.unlinkSync(full);
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
}
|
|
69
|
+
function countLines(pkgRoot, logDir) {
|
|
70
|
+
// 生产安装:pkgRoot/src/;开发模式:pkgRoot 是 dist/,src/ 在其兄弟目录
|
|
71
|
+
const srcCandidate = path.join(pkgRoot, 'src');
|
|
72
|
+
const srcDir = fs.existsSync(srcCandidate)
|
|
73
|
+
? srcCandidate
|
|
74
|
+
: path.join(pkgRoot, '..', 'src');
|
|
75
|
+
const statsFile = path.join(logDir, 'line-stats.log');
|
|
76
|
+
const countDir = (dir) => {
|
|
77
|
+
if (!fs.existsSync(dir))
|
|
78
|
+
return 0;
|
|
79
|
+
let total = 0;
|
|
80
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
81
|
+
const full = path.join(dir, entry.name);
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
total += countDir(full);
|
|
84
|
+
}
|
|
85
|
+
else if (entry.name.endsWith('.ts')) {
|
|
86
|
+
total += fs.readFileSync(full, 'utf-8').split('\n').length;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return total;
|
|
90
|
+
};
|
|
91
|
+
const countFile = (filePath) => {
|
|
92
|
+
if (!fs.existsSync(filePath))
|
|
93
|
+
return 0;
|
|
94
|
+
return fs.readFileSync(filePath, 'utf-8').split('\n').length;
|
|
95
|
+
};
|
|
96
|
+
console.log('\n[launcher] 正在统计代码行数...\n');
|
|
97
|
+
const core = countDir(path.join(srcDir, 'core'));
|
|
98
|
+
const agents = countDir(path.join(srcDir, 'agents'));
|
|
99
|
+
const channels = countDir(path.join(srcDir, 'channels'));
|
|
100
|
+
const utils = countDir(path.join(srcDir, 'utils'));
|
|
101
|
+
const cli = countDir(path.join(srcDir, 'cli'));
|
|
102
|
+
const aun = countDir(path.join(srcDir, 'aun'));
|
|
103
|
+
const entry = countFile(path.join(srcDir, 'index.ts'))
|
|
104
|
+
+ countFile(path.join(srcDir, 'config-store.ts'))
|
|
105
|
+
+ countFile(path.join(srcDir, 'types.ts'))
|
|
106
|
+
+ countFile(path.join(srcDir, 'ipc.ts'))
|
|
107
|
+
+ countFile(path.join(srcDir, 'paths.ts'));
|
|
108
|
+
const total = core + agents + channels + utils + cli + aun + entry;
|
|
109
|
+
console.log('==================================================');
|
|
110
|
+
console.log('EvolClaw 代码统计');
|
|
111
|
+
console.log('==================================================');
|
|
112
|
+
console.log(`核心模块: ${String(core).padStart(8)} 行`);
|
|
113
|
+
console.log(`Agent 模块: ${String(agents).padStart(8)} 行`);
|
|
114
|
+
console.log(`渠道适配: ${String(channels).padStart(8)} 行`);
|
|
115
|
+
console.log(`工具库: ${String(utils).padStart(8)} 行`);
|
|
116
|
+
console.log(`CLI: ${String(cli).padStart(8)} 行`);
|
|
117
|
+
console.log(`AUN 协议: ${String(aun).padStart(8)} 行`);
|
|
118
|
+
console.log(`入口与配置: ${String(entry).padStart(8)} 行`);
|
|
119
|
+
console.log('--------------------------------------------------');
|
|
120
|
+
console.log(`总计: ${String(total).padStart(8)} 行`);
|
|
121
|
+
console.log('==================================================');
|
|
122
|
+
// 追加历史记录(仅在数据变化时)
|
|
123
|
+
let shouldAppend = true;
|
|
124
|
+
let prevTotal = 0;
|
|
125
|
+
if (fs.existsSync(statsFile)) {
|
|
126
|
+
const lines = fs.readFileSync(statsFile, 'utf-8').trim().split('\n');
|
|
127
|
+
if (lines.length > 0) {
|
|
128
|
+
const lastLine = lines[lines.length - 1];
|
|
129
|
+
const parts = lastLine.split('\t');
|
|
130
|
+
// 旧格式8列: time core agents channels utils entry total delta → total at [6]
|
|
131
|
+
// 新格式10列: time core agents channels utils cli aun entry total delta → total at [8]
|
|
132
|
+
const lastTotalStr = parts.length >= 10 ? parts[8] : parts[6];
|
|
133
|
+
prevTotal = parseInt(lastTotalStr ?? parts[parts.length - 2], 10) || 0;
|
|
134
|
+
if (prevTotal === total) {
|
|
135
|
+
shouldAppend = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (shouldAppend) {
|
|
140
|
+
const _d = new Date();
|
|
141
|
+
const _p = (n) => String(n).padStart(2, '0');
|
|
142
|
+
const now = `${_d.getFullYear()}-${_p(_d.getMonth() + 1)}-${_p(_d.getDate())} ${_p(_d.getHours())}:${_p(_d.getMinutes())}:${_p(_d.getSeconds())}`;
|
|
143
|
+
const delta = total - prevTotal;
|
|
144
|
+
const deltaStr = delta >= 0 ? `+${delta}` : `${delta}`;
|
|
145
|
+
fs.appendFileSync(statsFile, `${now}\t${core}\t${agents}\t${channels}\t${utils}\t${cli}\t${aun}\t${entry}\t${total}\t${deltaStr}\n`);
|
|
146
|
+
}
|
|
147
|
+
showHistory(statsFile);
|
|
148
|
+
}
|
|
149
|
+
function showHistory(statsFile) {
|
|
150
|
+
if (!fs.existsSync(statsFile))
|
|
151
|
+
return;
|
|
152
|
+
const lines = fs.readFileSync(statsFile, 'utf-8').trim().split('\n');
|
|
153
|
+
if (lines.length < 2)
|
|
154
|
+
return;
|
|
155
|
+
const recent = lines.slice(-10);
|
|
156
|
+
console.log('\n==================================================');
|
|
157
|
+
console.log('历史记录(最近 10 次)');
|
|
158
|
+
console.log('==================================================');
|
|
159
|
+
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)}`);
|
|
160
|
+
console.log('--------------------------------------------------');
|
|
161
|
+
let prevTotal = null;
|
|
162
|
+
for (const line of recent) {
|
|
163
|
+
const parts = line.split('\t');
|
|
164
|
+
// 格式演进:
|
|
165
|
+
// 旧6列: time core channels utils entry total
|
|
166
|
+
// 旧7列: time core agents channels utils entry total
|
|
167
|
+
// 旧8列: time core agents channels utils entry total delta
|
|
168
|
+
// 新10列: time core agents channels utils cli aun entry total delta
|
|
169
|
+
let time, c, a, ch, u, cl, au, e, t, d;
|
|
170
|
+
if (parts.length >= 10) {
|
|
171
|
+
[time, c, a, ch, u, cl, au, e, t, d] = parts;
|
|
172
|
+
}
|
|
173
|
+
else if (parts.length >= 8) {
|
|
174
|
+
// 旧8列: time core agents channels utils entry total delta
|
|
175
|
+
[time, c, a, ch, u, e, t, d] = parts;
|
|
176
|
+
cl = '-';
|
|
177
|
+
au = '-';
|
|
178
|
+
}
|
|
179
|
+
else if (parts.length >= 7) {
|
|
180
|
+
// 旧7列: time core agents channels utils entry total
|
|
181
|
+
[time, c, a, ch, u, e, t] = parts;
|
|
182
|
+
cl = '-';
|
|
183
|
+
au = '-';
|
|
184
|
+
}
|
|
185
|
+
else if (parts.length >= 6) {
|
|
186
|
+
[time, c, ch, u, e, t] = parts;
|
|
187
|
+
a = '-';
|
|
188
|
+
cl = '-';
|
|
189
|
+
au = '-';
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const total = parseInt(t, 10);
|
|
195
|
+
let diff;
|
|
196
|
+
if (d) {
|
|
197
|
+
diff = d;
|
|
198
|
+
}
|
|
199
|
+
else if (prevTotal !== null) {
|
|
200
|
+
const change = total - prevTotal;
|
|
201
|
+
diff = change >= 0 ? `+${change}` : `${change}`;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
diff = '-';
|
|
205
|
+
}
|
|
206
|
+
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)}`);
|
|
207
|
+
prevTotal = total;
|
|
208
|
+
}
|
|
209
|
+
console.log('==================================================');
|
|
210
|
+
}
|
|
211
|
+
// ==================== Commands ====================
|
|
212
|
+
/**
|
|
213
|
+
* 检测并展示跨 HOME 残留的 evolclaw 主进程。
|
|
214
|
+
*
|
|
215
|
+
* 这些孤儿不在自己 HOME 的 instance/ 登记簿内,instance-registry 的常规清理
|
|
216
|
+
* (cleanupInstances)够不到。常见来源:
|
|
217
|
+
* - 测试套件 spawn 后未在 afterAll 杀子进程
|
|
218
|
+
* - 旧版本 pidfile 模式遗留(升级后 record 缺失)
|
|
219
|
+
*
|
|
220
|
+
* 仅打印提示,不主动杀;调用方决定是否清理。
|
|
221
|
+
*/
|
|
222
|
+
function reportOrphans(orphans) {
|
|
223
|
+
if (orphans.length === 0)
|
|
224
|
+
return;
|
|
225
|
+
console.log(`⚠ 检测到 ${orphans.length} 个未登记的 evolclaw 主进程(跨 HOME 残留):`);
|
|
226
|
+
for (const o of orphans) {
|
|
227
|
+
const home = o.evolclawHome ?? '未知';
|
|
228
|
+
console.log(` PID ${o.pid} EVOLCLAW_HOME=${home}`);
|
|
229
|
+
}
|
|
230
|
+
console.log(' 这些进程不属于当前 HOME 的实例登记簿,自动清理不会处理它们。');
|
|
231
|
+
console.log(' 使用 evolclaw restart --clear 一并清掉,或手动 kill。');
|
|
232
|
+
}
|
|
233
|
+
async function cmdStart() {
|
|
234
|
+
const p = resolvePaths();
|
|
235
|
+
ensureDataDirs();
|
|
236
|
+
// 旧配置自动迁移(evolclaw.json → 新结构)
|
|
237
|
+
const { autoMigrateIfNeeded } = await import('../config-store.js');
|
|
238
|
+
autoMigrateIfNeeded();
|
|
239
|
+
// 检查至少有一个 self-agent
|
|
240
|
+
const { agents, skipped } = loadAllAgents();
|
|
241
|
+
if (agents.length === 0) {
|
|
242
|
+
console.log('❌ 未配置任何 self-agent。');
|
|
243
|
+
console.log('');
|
|
244
|
+
console.log('创建方式:');
|
|
245
|
+
console.log(' 1. 下载 Evol App(https://evolai.cn)→ 创建 Agent → 将引导文本输入给 baseagent 执行');
|
|
246
|
+
console.log(' 2. 手动创建:evolclaw agent new <your-aid>.agentid.pub');
|
|
247
|
+
console.log('');
|
|
248
|
+
if (skipped.length > 0) {
|
|
249
|
+
console.log(`跳过的目录:`);
|
|
250
|
+
for (const s of skipped)
|
|
251
|
+
console.log(` - ${s.dirName}: ${s.reason}`);
|
|
252
|
+
}
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
// 检查 instance 目录中的进程状态
|
|
256
|
+
const status = scanInstances();
|
|
257
|
+
const aliveMains = status.mains.filter(m => m.alive);
|
|
258
|
+
if (aliveMains.length > 0) {
|
|
259
|
+
const first = aliveMains[0];
|
|
260
|
+
console.log(`❌ EvolClaw is already running (PID: ${aliveMains.map(m => m.record.pid).join(', ')})`);
|
|
261
|
+
console.log(` 启动于: ${first.record.startedAtIso}`);
|
|
262
|
+
console.log(` 启动方式: ${first.record.launchedBy}`);
|
|
263
|
+
// 报告 AID 状态
|
|
264
|
+
if (status.aidLastActivity.size > 0) {
|
|
265
|
+
console.log(' AID 状态:');
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
for (const [aid, info] of status.aidLastActivity) {
|
|
268
|
+
const ago = formatTimeAgo(now - info.ts);
|
|
269
|
+
const symbol = info.event === 'disconnected' ? '✗' : '✓';
|
|
270
|
+
console.log(` ${symbol} ${aid} — 最后活动 ${ago} (${info.event})`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
console.log(' 使用 evolclaw restart 重启,或 evolclaw stop 先停止');
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
// 清理残留进程和文件
|
|
277
|
+
if (status.mains.length > 0 || status.restartMonitors.length > 0) {
|
|
278
|
+
const killed = cleanupInstances();
|
|
279
|
+
if (killed.length > 0) {
|
|
280
|
+
console.log(`⚠ 清理了 ${killed.length} 个残留进程: ${killed.join(', ')}`);
|
|
281
|
+
await sleep(2000);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// 跨 HOME 孤儿(未登记进程)只警告,不动
|
|
285
|
+
reportOrphans(findOrphanProcesses());
|
|
286
|
+
console.log('🚀 Starting EvolClaw...');
|
|
287
|
+
rotateStdoutIfNeeded(p.logs);
|
|
288
|
+
cleanEnv();
|
|
289
|
+
// 删除旧的 ready signal
|
|
290
|
+
try {
|
|
291
|
+
fs.unlinkSync(p.readySignal);
|
|
292
|
+
}
|
|
293
|
+
catch { }
|
|
294
|
+
const stdoutLog = path.join(p.logs, 'stdout.log');
|
|
295
|
+
const out = fs.openSync(stdoutLog, 'a');
|
|
296
|
+
const err = fs.openSync(stdoutLog, 'a');
|
|
297
|
+
const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
298
|
+
const child = spawn('node', ['--no-warnings=ExperimentalWarning', appMain], {
|
|
299
|
+
detached: true,
|
|
300
|
+
stdio: ['ignore', out, err],
|
|
301
|
+
windowsHide: true,
|
|
302
|
+
env: {
|
|
303
|
+
...process.env,
|
|
304
|
+
EVOLCLAW_HOME: p.root,
|
|
305
|
+
EVOLCLAW_LAUNCHED_BY: 'start',
|
|
306
|
+
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
|
307
|
+
MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
|
|
308
|
+
EVENT_LOG: process.env.EVENT_LOG || 'true',
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
const childPid = child.pid;
|
|
312
|
+
child.unref();
|
|
313
|
+
// 等待 ready signal(最多 30 秒,AUN sidecar 超时 15s + 其他通道连接)
|
|
314
|
+
const startTime = Date.now();
|
|
315
|
+
const checkReady = () => {
|
|
316
|
+
// ready signal 出现(优先检查,避免 Windows 上误判进程状态)
|
|
317
|
+
if (fs.existsSync(p.readySignal)) {
|
|
318
|
+
console.log(`✓ EvolClaw started successfully (PID: ${childPid})`);
|
|
319
|
+
console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
|
|
320
|
+
console.log(` Logs: ${p.logs}/`);
|
|
321
|
+
// 从主日志提取渠道连接摘要
|
|
322
|
+
const mainLog = findLatestLog(p.logs, 'evolclaw');
|
|
323
|
+
if (mainLog) {
|
|
324
|
+
const logLines = fs.readFileSync(mainLog, 'utf-8').split('\n');
|
|
325
|
+
// 从末尾往前找最近一次启动的摘要
|
|
326
|
+
let channelSummary = '';
|
|
327
|
+
for (let i = logLines.length - 1; i >= 0; i--) {
|
|
328
|
+
if (logLines[i].includes('EvolClaw is running with')) {
|
|
329
|
+
channelSummary = logLines[i];
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (channelSummary) {
|
|
334
|
+
const match = channelSummary.match(/running with .+/);
|
|
335
|
+
if (match)
|
|
336
|
+
console.log(` ${match[0]}`);
|
|
337
|
+
}
|
|
338
|
+
// 最近一次启动的失败信息
|
|
339
|
+
let lastReadyIdx = -1;
|
|
340
|
+
for (let i = logLines.length - 1; i >= 0; i--) {
|
|
341
|
+
if (logLines[i].includes('Ready signal written')) {
|
|
342
|
+
lastReadyIdx = i;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (lastReadyIdx > 0) {
|
|
347
|
+
for (let i = Math.max(0, lastReadyIdx - 20); i < lastReadyIdx; i++) {
|
|
348
|
+
const line = logLines[i];
|
|
349
|
+
if (line.includes('failed to connect') || line.includes('Failed to create channel')) {
|
|
350
|
+
const match = line.match(/\[WARN\]\s*(.+)/);
|
|
351
|
+
console.log(` ⚠ ${match ? match[1] : line.trim()}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
console.log('');
|
|
357
|
+
// 代码统计仅在开发环境显示(EVOLCLAW_HOME 指向包目录)
|
|
358
|
+
if (resolveRoot() === getPackageRoot()) {
|
|
359
|
+
countLines(getPackageRoot(), p.logs);
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// 超时
|
|
364
|
+
if (Date.now() - startTime > 30000) {
|
|
365
|
+
console.log('❌ Failed to start EvolClaw (ready signal timeout)');
|
|
366
|
+
console.log('');
|
|
367
|
+
console.log('📝 Error details (last 10 lines of stdout):');
|
|
368
|
+
if (fs.existsSync(stdoutLog)) {
|
|
369
|
+
const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
|
|
370
|
+
console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
|
|
371
|
+
}
|
|
372
|
+
process.exit(1);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// 进程已退出且无 ready signal
|
|
376
|
+
if (!platform.isProcessRunning(childPid)) {
|
|
377
|
+
// 给进程一点时间写 ready signal(可能刚好在写入中)
|
|
378
|
+
if (Date.now() - startTime > 3000) {
|
|
379
|
+
console.log('❌ Failed to start EvolClaw');
|
|
380
|
+
console.log('');
|
|
381
|
+
console.log('📝 Error details (last 10 lines of stdout):');
|
|
382
|
+
if (fs.existsSync(stdoutLog)) {
|
|
383
|
+
const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
|
|
384
|
+
console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
|
|
385
|
+
}
|
|
386
|
+
process.exit(1);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
setTimeout(checkReady, 500);
|
|
391
|
+
};
|
|
392
|
+
setTimeout(checkReady, 1000);
|
|
393
|
+
}
|
|
394
|
+
async function stopPid(pid) {
|
|
395
|
+
console.log(`🛑 Stopping EvolClaw (PID: ${pid})...`);
|
|
396
|
+
platform.killProcess(pid);
|
|
397
|
+
await new Promise((resolve) => {
|
|
398
|
+
let waited = 0;
|
|
399
|
+
const check = setInterval(() => {
|
|
400
|
+
waited++;
|
|
401
|
+
if (!platform.isProcessRunning(pid)) {
|
|
402
|
+
clearInterval(check);
|
|
403
|
+
console.log('✓ EvolClaw stopped');
|
|
404
|
+
resolve();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (waited >= 10) {
|
|
408
|
+
clearInterval(check);
|
|
409
|
+
platform.killProcess(pid, true);
|
|
410
|
+
console.log('✓ EvolClaw stopped (forced)');
|
|
411
|
+
resolve();
|
|
412
|
+
}
|
|
413
|
+
}, 1000);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async function cmdStop() {
|
|
417
|
+
const status = scanInstances();
|
|
418
|
+
const aliveMains = status.mains.filter(m => m.alive);
|
|
419
|
+
if (aliveMains.length === 0) {
|
|
420
|
+
console.log('⚠ EvolClaw is not running');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
await Promise.all(aliveMains.map(m => stopPid(m.record.pid)));
|
|
424
|
+
await sleep(500);
|
|
425
|
+
cleanupInstances();
|
|
426
|
+
if (aliveMains.length > 1) {
|
|
427
|
+
console.log(`⚠ 停止了 ${aliveMains.length} 个 main 实例: ${aliveMains.map(m => m.record.pid).join(', ')}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function cmdRestart(opts = {}) {
|
|
431
|
+
console.log('🔄 Restarting EvolClaw...');
|
|
432
|
+
// 版本检查与自动升级
|
|
433
|
+
console.log('📦 Checking for updates...');
|
|
434
|
+
const upgrade = await tryUpgrade();
|
|
435
|
+
switch (upgrade.status) {
|
|
436
|
+
case 'upgraded':
|
|
437
|
+
console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
|
|
438
|
+
break;
|
|
439
|
+
case 'no-update':
|
|
440
|
+
console.log(`✓ Already up to date (${upgrade.from})`);
|
|
441
|
+
break;
|
|
442
|
+
case 'skipped':
|
|
443
|
+
console.log(upgrade.error
|
|
444
|
+
? '⏭ Skipped upgrade (network unavailable)'
|
|
445
|
+
: '⏭ Skipped upgrade check (dev mode)');
|
|
446
|
+
break;
|
|
447
|
+
case 'failed':
|
|
448
|
+
console.log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}), continuing with current version`);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
// 停止所有活 main 进程(可能不止一个)
|
|
452
|
+
const status = scanInstances();
|
|
453
|
+
const aliveMains = status.mains.filter(m => m.alive);
|
|
454
|
+
if (aliveMains.length > 0) {
|
|
455
|
+
if (aliveMains.length > 1) {
|
|
456
|
+
console.log(`⚠ 检测到 ${aliveMains.length} 个 main 实例,将一并停止: ${aliveMains.map(m => m.record.pid).join(', ')}`);
|
|
457
|
+
}
|
|
458
|
+
await Promise.all(aliveMains.map(m => stopPid(m.record.pid)));
|
|
459
|
+
await sleep(500);
|
|
460
|
+
cleanupInstances();
|
|
461
|
+
}
|
|
462
|
+
// 跨 HOME 孤儿处理:只在 --clear 时主动 kill;
|
|
463
|
+
// 否则交给后续 cmdStart() 统一警告,避免重复输出。
|
|
464
|
+
if (opts.clear) {
|
|
465
|
+
const orphans = findOrphanProcesses();
|
|
466
|
+
if (orphans.length > 0) {
|
|
467
|
+
const killed = killOrphans(orphans);
|
|
468
|
+
console.log(`☠ 已 SIGKILL ${killed.length} 个孤儿进程: ${killed.join(', ')}`);
|
|
469
|
+
await sleep(500);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
setTimeout(() => cmdStart(), 1000);
|
|
473
|
+
}
|
|
474
|
+
function formatTimeAgo(ms) {
|
|
475
|
+
const sec = Math.floor(ms / 1000);
|
|
476
|
+
if (sec < 60)
|
|
477
|
+
return '刚刚';
|
|
478
|
+
const min = Math.floor(sec / 60);
|
|
479
|
+
if (min < 60)
|
|
480
|
+
return `${min}分钟前`;
|
|
481
|
+
const hour = Math.floor(min / 60);
|
|
482
|
+
if (hour < 24)
|
|
483
|
+
return `${hour}小时前`;
|
|
484
|
+
const day = Math.floor(hour / 24);
|
|
485
|
+
return `${day}天前`;
|
|
486
|
+
}
|
|
487
|
+
/** 双字符宽字符 padding:中文/emoji 算 2 列,其他算 1 列 */
|
|
488
|
+
function visualWidth(s) {
|
|
489
|
+
// Strip ANSI escape sequences (color codes etc.) before measuring
|
|
490
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
491
|
+
let w = 0;
|
|
492
|
+
for (const ch of stripped) {
|
|
493
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
494
|
+
// CJK / Hangul / 全角符号 / Emoji 等宽字符
|
|
495
|
+
if ((code >= 0x1100 && code <= 0x115F) ||
|
|
496
|
+
(code >= 0x2E80 && code <= 0x9FFF) ||
|
|
497
|
+
(code >= 0xA000 && code <= 0xA4CF) ||
|
|
498
|
+
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
499
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
500
|
+
(code >= 0xFE30 && code <= 0xFE4F) ||
|
|
501
|
+
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
502
|
+
(code >= 0x1F300 && code <= 0x1FAFF)) {
|
|
503
|
+
w += 2;
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
w += 1;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return w;
|
|
510
|
+
}
|
|
511
|
+
function padRight(s, width) {
|
|
512
|
+
const pad = Math.max(0, width - visualWidth(s));
|
|
513
|
+
return s + ' '.repeat(pad);
|
|
514
|
+
}
|
|
515
|
+
const AID_STATUS_LABELS = {
|
|
516
|
+
connected: '✓ Connected',
|
|
517
|
+
reconnecting: '⏳ Reconnecting',
|
|
518
|
+
aid_blocked: '🔒 AID Blocked',
|
|
519
|
+
kicked: '✗ Kicked',
|
|
520
|
+
failed: '✗ Failed',
|
|
521
|
+
disabled: '○ Disabled',
|
|
522
|
+
};
|
|
523
|
+
function renderAunAidsTable(aids) {
|
|
524
|
+
// Column widths(视觉宽度):AGENT 列按实际名字最长值动态扩展
|
|
525
|
+
const agentNames = aids.map(a => (a.agentName || '?').replace(/\.agentid\.pub$/, ''));
|
|
526
|
+
const COL_AGENT = Math.max(5, ...agentNames.map(n => n.length)) + 2;
|
|
527
|
+
const COL_AID = 32;
|
|
528
|
+
const COL_STATUS = 16;
|
|
529
|
+
const COL_RECONN = 8;
|
|
530
|
+
const COL_LAST = 14;
|
|
531
|
+
// 表头
|
|
532
|
+
console.log(' ' +
|
|
533
|
+
padRight('AGENT', COL_AGENT) +
|
|
534
|
+
padRight('AID', COL_AID) +
|
|
535
|
+
padRight('STATUS', COL_STATUS) +
|
|
536
|
+
padRight('RECONN', COL_RECONN) +
|
|
537
|
+
padRight('LAST ATTEMPT', COL_LAST) +
|
|
538
|
+
'NOTE');
|
|
539
|
+
for (let i = 0; i < aids.length; i++) {
|
|
540
|
+
const a = aids[i];
|
|
541
|
+
const agent = agentNames[i];
|
|
542
|
+
const aid = (a.aid || '?').slice(0, COL_AID - 1);
|
|
543
|
+
const statusLabel = AID_STATUS_LABELS[a.status] || a.status || '?';
|
|
544
|
+
const reconn = String(a.reconnectCount ?? 0);
|
|
545
|
+
const lastAttempt = a.lastAttemptAt
|
|
546
|
+
? formatTimeAgo(Date.now() - a.lastAttemptAt)
|
|
547
|
+
: '—';
|
|
548
|
+
let note = '';
|
|
549
|
+
if (a.status === 'connected' && a.lastConnectedAt) {
|
|
550
|
+
note = `uptime ${formatTimeAgo(Date.now() - a.lastConnectedAt).replace('前', '')}`;
|
|
551
|
+
}
|
|
552
|
+
else if (a.status === 'aid_blocked' && a.blockedBy) {
|
|
553
|
+
const home = (a.blockedBy.evolclawHome || '').replace(os.homedir(), '~');
|
|
554
|
+
const ag = a.blockedBy.agentName ? `, agent=${a.blockedBy.agentName}` : '';
|
|
555
|
+
note = `held by PID ${a.blockedBy.pid} (HOME=${home}${ag})`;
|
|
556
|
+
}
|
|
557
|
+
else if (a.lastError) {
|
|
558
|
+
note = a.lastError;
|
|
559
|
+
}
|
|
560
|
+
console.log(' ' +
|
|
561
|
+
padRight(agent, COL_AGENT) +
|
|
562
|
+
padRight(aid, COL_AID) +
|
|
563
|
+
padRight(statusLabel, COL_STATUS) +
|
|
564
|
+
padRight(reconn, COL_RECONN) +
|
|
565
|
+
padRight(lastAttempt, COL_LAST) +
|
|
566
|
+
note);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function showConfigChannels(config) {
|
|
570
|
+
const groups = [];
|
|
571
|
+
const channelChecks = [
|
|
572
|
+
{ type: 'feishu', isValid: (inst) => !!inst.appId && inst.enabled !== false },
|
|
573
|
+
{ type: 'wechat', isValid: (inst) => !!inst.token && inst.enabled !== false },
|
|
574
|
+
{ type: 'aun', isValid: (inst) => !!inst.aid && inst.enabled !== false && !inst.aid.includes('your-') && !inst.aid.includes('placeholder') },
|
|
575
|
+
{ type: 'dingtalk', isValid: (inst) => !!inst.clientId && inst.enabled !== false && !inst.clientId.includes('your-') && !inst.clientId.includes('placeholder') },
|
|
576
|
+
{ type: 'qqbot', isValid: (inst) => !!inst.appId && inst.enabled !== false && !inst.appId.includes('your-') && !inst.appId.includes('placeholder') },
|
|
577
|
+
{ type: 'wecom', isValid: (inst) => !!inst.botId && inst.enabled !== false && !inst.botId.includes('your-') && !inst.botId.includes('placeholder') },
|
|
578
|
+
];
|
|
579
|
+
for (const { type, isValid } of channelChecks) {
|
|
580
|
+
const raw = config.channels?.[type];
|
|
581
|
+
if (!raw)
|
|
582
|
+
continue;
|
|
583
|
+
if (Array.isArray(raw)) {
|
|
584
|
+
const names = raw.filter(isValid).map((inst) => inst.name || type);
|
|
585
|
+
if (names.length > 0)
|
|
586
|
+
groups.push({ type, instances: names });
|
|
587
|
+
}
|
|
588
|
+
else if (isValid(raw)) {
|
|
589
|
+
groups.push({ type, instances: [raw.name || type] });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (groups.length > 0) {
|
|
593
|
+
for (const g of groups) {
|
|
594
|
+
if (g.instances.length === 1) {
|
|
595
|
+
console.log(` ${g.instances[0]}: ✓ Configured`);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
console.log(` ${g.type}: [${g.instances.join(', ')}]`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
console.log(' (no channels configured)');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function cmdStatus() {
|
|
607
|
+
const p = resolvePaths();
|
|
608
|
+
const status = scanInstances();
|
|
609
|
+
const aliveMains = status.mains.filter(m => m.alive);
|
|
610
|
+
const pid = aliveMains.length > 0 ? aliveMains[0].record.pid : null;
|
|
611
|
+
if (aliveMains.length > 1) {
|
|
612
|
+
console.log(`⚠ 检测到 ${aliveMains.length} 个 main 实例同时运行: ${aliveMains.map(m => m.record.pid).join(', ')}`);
|
|
613
|
+
console.log(' 这是异常状态,建议执行 evolclaw restart 让所有实例统一退出');
|
|
614
|
+
console.log('');
|
|
615
|
+
}
|
|
616
|
+
if (pid) {
|
|
617
|
+
console.log(`✓ EvolClaw is running (PID: ${pid})`);
|
|
618
|
+
console.log('');
|
|
619
|
+
console.log('📊 Process Info:');
|
|
620
|
+
try {
|
|
621
|
+
const info = platform.getProcessInfo(pid);
|
|
622
|
+
if (info.uptime)
|
|
623
|
+
console.log(` Uptime: ${info.uptime}`);
|
|
624
|
+
if (info.cpu)
|
|
625
|
+
console.log(` CPU: ${info.cpu}%`);
|
|
626
|
+
if (info.memory) {
|
|
627
|
+
const memKB = parseInt(info.memory, 10);
|
|
628
|
+
const memStr = memKB >= 1024 ? `${(memKB / 1024).toFixed(0)} MB` : `${memKB} KB`;
|
|
629
|
+
console.log(` Memory: ${memStr}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch { }
|
|
633
|
+
console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
|
|
634
|
+
// Runtime statistics (read from sessions filesystem)
|
|
635
|
+
if (fs.existsSync(p.sessionsDir)) {
|
|
636
|
+
try {
|
|
637
|
+
const { scanChatDirs, scanMetaFiles, readJsonFile, readLastJsonlLine } = await import('../core/session/session-fs-store.js');
|
|
638
|
+
const chatDirs = scanChatDirs(p.sessionsDir);
|
|
639
|
+
const allSessions = [];
|
|
640
|
+
for (const { dirPath } of chatDirs) {
|
|
641
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
642
|
+
if (active)
|
|
643
|
+
allSessions.push({ ...active, isActive: true });
|
|
644
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
645
|
+
const meta = readLastJsonlLine(path.join(dirPath, metaFile));
|
|
646
|
+
if (!meta)
|
|
647
|
+
continue;
|
|
648
|
+
// 跳过同 id 的(active.json 已经是它的最新版)
|
|
649
|
+
if (active && active.id === meta.id)
|
|
650
|
+
continue;
|
|
651
|
+
allSessions.push({ ...meta, isActive: false });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// 最近 5 个(按 updatedAt 倒排)
|
|
655
|
+
const recentSessions = [...allSessions].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 5);
|
|
656
|
+
// 检测 orphan:session 的 channel 实例名不在任何 self-agent 配置内
|
|
657
|
+
let orphanCount = 0;
|
|
658
|
+
try {
|
|
659
|
+
const { agents } = loadAllAgents();
|
|
660
|
+
const configChannelNames = new Set();
|
|
661
|
+
for (const cfg of agents) {
|
|
662
|
+
for (const inst of cfg.channels) {
|
|
663
|
+
// effective key: <aid>#<type>#<name>
|
|
664
|
+
configChannelNames.add(`${cfg.aid}#${inst.type}#${inst.name}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (const s of allSessions) {
|
|
668
|
+
if (!configChannelNames.has(s.channel))
|
|
669
|
+
orphanCount++;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
catch { }
|
|
673
|
+
if (recentSessions.length > 0) {
|
|
674
|
+
console.log('');
|
|
675
|
+
console.log('📋 Recent Active Sessions:');
|
|
676
|
+
for (const s of recentSessions) {
|
|
677
|
+
const projectName = path.basename(s.projectPath);
|
|
678
|
+
const sessionType = s.threadId ? '话题会话' : '主会话';
|
|
679
|
+
const chatType = s.chatType === 'group' ? '群聊' : '单聊';
|
|
680
|
+
const sessionName = s.name || '默认会话';
|
|
681
|
+
const timeAgo = formatTimeAgo(Date.now() - s.updatedAt);
|
|
682
|
+
const dot = s.isActive ? '•' : '○';
|
|
683
|
+
const agentSidLabel = s.agentSessionId ? ` [${s.agentSessionId}]` : '';
|
|
684
|
+
const agentType = s.agentType || 'claude';
|
|
685
|
+
console.log(` ${dot} [${agentType}] ${projectName} / ${sessionName} (${sessionType}, ${chatType})${agentSidLabel} - ${timeAgo}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (orphanCount > 0) {
|
|
689
|
+
console.log('');
|
|
690
|
+
console.log(`⚠ Orphan sessions: ${orphanCount}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch { }
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
console.log('⚠ EvolClaw is not running');
|
|
698
|
+
}
|
|
699
|
+
// Session & Project statistics (从文件系统读)
|
|
700
|
+
if (fs.existsSync(p.sessionsDir)) {
|
|
701
|
+
console.log('');
|
|
702
|
+
console.log('📦 Sessions & Projects:');
|
|
703
|
+
try {
|
|
704
|
+
const { scanChatDirs, scanMetaFiles, readJsonFile, readLastJsonlLine } = await import('../core/session/session-fs-store.js');
|
|
705
|
+
const chatDirs = scanChatDirs(p.sessionsDir);
|
|
706
|
+
let totalSessions = 0;
|
|
707
|
+
let activeSessions = 0;
|
|
708
|
+
const channelIdSet = new Set();
|
|
709
|
+
const projectSet = new Set();
|
|
710
|
+
for (const { channelId, dirPath } of chatDirs) {
|
|
711
|
+
channelIdSet.add(channelId);
|
|
712
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
713
|
+
if (active) {
|
|
714
|
+
activeSessions++;
|
|
715
|
+
projectSet.add(active.projectPath);
|
|
716
|
+
}
|
|
717
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
718
|
+
const meta = readLastJsonlLine(path.join(dirPath, metaFile));
|
|
719
|
+
if (!meta)
|
|
720
|
+
continue;
|
|
721
|
+
totalSessions++;
|
|
722
|
+
projectSet.add(meta.projectPath);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
console.log(` Total sessions: ${totalSessions} (active: ${activeSessions})`);
|
|
726
|
+
console.log(` Unique chats: ${channelIdSet.size}`);
|
|
727
|
+
console.log(` Projects: ${projectSet.size}`);
|
|
728
|
+
}
|
|
729
|
+
catch { }
|
|
730
|
+
}
|
|
731
|
+
// Channel status
|
|
732
|
+
if (fs.existsSync(p.defaultsConfig)) {
|
|
733
|
+
console.log('');
|
|
734
|
+
const config = JSON.parse(fs.readFileSync(p.defaultsConfig, 'utf-8'));
|
|
735
|
+
if (pid) {
|
|
736
|
+
// Running: query IPC for real-time status
|
|
737
|
+
const status = await ipcQuery(p.socket, { type: 'status' });
|
|
738
|
+
if (status) {
|
|
739
|
+
// 🔑 AUN AIDs 表格(详细 AUN 实例状态)
|
|
740
|
+
try {
|
|
741
|
+
const aidsResp = await ipcQuery(p.socket, { type: 'aun-aids' });
|
|
742
|
+
if (aidsResp?.ok && aidsResp.aids?.length > 0) {
|
|
743
|
+
console.log('🔑 AUN AIDs:');
|
|
744
|
+
renderAunAidsTable(aidsResp.aids);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
catch { /* ignore */ }
|
|
748
|
+
if (status.stats) {
|
|
749
|
+
console.log('');
|
|
750
|
+
console.log('📊 Last hour:');
|
|
751
|
+
console.log(` Messages: ${status.stats.received} received, ${status.stats.completed} completed`);
|
|
752
|
+
if (status.stats.errors > 0)
|
|
753
|
+
console.log(` Errors: ${status.stats.errors}`);
|
|
754
|
+
if (status.stats.completed > 0)
|
|
755
|
+
console.log(` Avg response: ${(status.stats.avgResponseMs / 1000).toFixed(1)}s`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
// IPC unreachable but PID exists — show config only
|
|
760
|
+
console.log('🔌 Channels (IPC unreachable):');
|
|
761
|
+
showConfigChannels(config);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
console.log('🔌 Channel Configuration:');
|
|
766
|
+
showConfigChannels(config);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// EvolAgent summary (via IPC, only when running)
|
|
770
|
+
if (pid) {
|
|
771
|
+
try {
|
|
772
|
+
const agentResult = await ipcQuery(p.socket, { type: 'evolagent.list' });
|
|
773
|
+
if (agentResult?.ok && agentResult.agents?.length > 0) {
|
|
774
|
+
const agents = agentResult.agents;
|
|
775
|
+
if (agents.length > 0) {
|
|
776
|
+
console.log('');
|
|
777
|
+
console.log('🤖 EvolAgents:');
|
|
778
|
+
for (const a of agents) {
|
|
779
|
+
const statusIcon = a.status === 'running' ? '●' : a.status === 'error' ? '✗' : a.status === 'disabled' ? '○' : '◌';
|
|
780
|
+
const channels = summarizeChannelFingerprints(a.channels || []);
|
|
781
|
+
const shortName = a.name.replace(/\.agentid\.pub$/, '');
|
|
782
|
+
console.log(` ${statusIcon} ${shortName.padEnd(20)} ${a.status.padEnd(10)} ${channels}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
// IPC query for agents failed — skip section
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* 把 channel fingerprint 列表(`<aid>#<type>#<name>`)折叠成展示用摘要。
|
|
794
|
+
*
|
|
795
|
+
* 聚合规则:
|
|
796
|
+
* - 按 type 分组
|
|
797
|
+
* - 单实例:直接打 type(如 `aun`、`wechat`)
|
|
798
|
+
* - 多实例:`type×N (name1, name2, ...)`
|
|
799
|
+
* - 输出顺序保持首次出现的 type 顺序(aun 通常排第一,因为 channelInstanceNames 把它放头)
|
|
800
|
+
*/
|
|
801
|
+
function summarizeChannelFingerprints(fingerprints) {
|
|
802
|
+
if (fingerprints.length === 0)
|
|
803
|
+
return '—';
|
|
804
|
+
const groups = new Map();
|
|
805
|
+
const order = [];
|
|
806
|
+
for (const fp of fingerprints) {
|
|
807
|
+
const parts = fp.split('#');
|
|
808
|
+
if (parts.length < 3) {
|
|
809
|
+
if (!groups.has(fp)) {
|
|
810
|
+
groups.set(fp, []);
|
|
811
|
+
order.push(fp);
|
|
812
|
+
}
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const type = parts[1];
|
|
816
|
+
const name = parts.slice(2).join('#');
|
|
817
|
+
if (!groups.has(type)) {
|
|
818
|
+
groups.set(type, []);
|
|
819
|
+
order.push(type);
|
|
820
|
+
}
|
|
821
|
+
groups.get(type).push(name);
|
|
822
|
+
}
|
|
823
|
+
return order.map(type => {
|
|
824
|
+
const names = groups.get(type);
|
|
825
|
+
if (names.length === 0)
|
|
826
|
+
return type;
|
|
827
|
+
if (names.length === 1)
|
|
828
|
+
return type;
|
|
829
|
+
return `${type}×${names.length} (${names.join(', ')})`;
|
|
830
|
+
}).join(', ');
|
|
831
|
+
}
|
|
832
|
+
// Log line pattern: [timestamp] [LEVEL] [Module?] message
|
|
833
|
+
const LOG_RE = /^(\[[^\]]+\]) (\[(?:INFO|WARN|ERROR|DEBUG)\]) ((?:\[[^\]]+\] )*)(.*)$/;
|
|
834
|
+
const MAX_MSG = 200; // truncate long messages
|
|
835
|
+
function makeColors(enabled) {
|
|
836
|
+
const e = (code) => enabled ? code : '';
|
|
837
|
+
return {
|
|
838
|
+
reset: e('\x1b[0m'), dim: e('\x1b[2m'), bold: e('\x1b[1m'),
|
|
839
|
+
red: e('\x1b[31m'), yellow: e('\x1b[33m'), cyan: e('\x1b[36m'),
|
|
840
|
+
magenta: e('\x1b[35m'), gray: e('\x1b[90m'),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function renderLogLine(line, opts) {
|
|
844
|
+
const m = line.match(LOG_RE);
|
|
845
|
+
if (!m)
|
|
846
|
+
return line; // passthrough non-standard lines (stack traces etc.)
|
|
847
|
+
const [, ts, levelTag, modulePart, msg] = m;
|
|
848
|
+
const level = levelTag.slice(1, -1); // strip brackets
|
|
849
|
+
// Level filter
|
|
850
|
+
if (opts.level) {
|
|
851
|
+
const want = opts.level.toUpperCase();
|
|
852
|
+
if (want === 'ERROR' && level !== 'ERROR')
|
|
853
|
+
return null;
|
|
854
|
+
if (want === 'WARN' && level !== 'WARN' && level !== 'ERROR')
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
// Module filter (case-insensitive substring match)
|
|
858
|
+
if (opts.module) {
|
|
859
|
+
const mod = modulePart.toLowerCase();
|
|
860
|
+
if (!mod.includes(opts.module.toLowerCase()))
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
// Truncate long messages (always, regardless of color)
|
|
864
|
+
const truncated = msg.length > MAX_MSG ? msg.slice(0, MAX_MSG) + '…' : msg;
|
|
865
|
+
const C = makeColors(opts.color);
|
|
866
|
+
// Color by level
|
|
867
|
+
const levelColor = level === 'ERROR' ? C.red : level === 'WARN' ? C.yellow : level === 'DEBUG' ? C.gray : '';
|
|
868
|
+
// Highlight user messages: [channel] channelId: text
|
|
869
|
+
const isUserMsg = modulePart && /^\S+: .+$/.test(truncated);
|
|
870
|
+
const renderedMsg = isUserMsg
|
|
871
|
+
? C.cyan + truncated + C.reset
|
|
872
|
+
: levelColor + truncated + C.reset;
|
|
873
|
+
return (C.dim + ts + C.reset + ' ' +
|
|
874
|
+
levelColor + C.bold + levelTag + C.reset + ' ' +
|
|
875
|
+
C.magenta + modulePart.trimEnd() + C.reset +
|
|
876
|
+
(modulePart ? ' ' : '') +
|
|
877
|
+
renderedMsg);
|
|
878
|
+
}
|
|
879
|
+
function findLatestLog(logDir, baseName) {
|
|
880
|
+
if (!fs.existsSync(logDir))
|
|
881
|
+
return null;
|
|
882
|
+
// Try exact name first (legacy non-rotated)
|
|
883
|
+
const exact = path.join(logDir, `${baseName}.log`);
|
|
884
|
+
if (fs.existsSync(exact))
|
|
885
|
+
return exact;
|
|
886
|
+
// Find latest rotated file: baseName-YYYYMMDD-HH.log
|
|
887
|
+
const files = fs.readdirSync(logDir)
|
|
888
|
+
.filter(f => f.startsWith(`${baseName}-`) && f.endsWith('.log'))
|
|
889
|
+
.sort();
|
|
890
|
+
if (files.length === 0)
|
|
891
|
+
return null;
|
|
892
|
+
return path.join(logDir, files[files.length - 1]);
|
|
893
|
+
}
|
|
894
|
+
function cmdLogs(args) {
|
|
895
|
+
const raw = args.includes('--raw');
|
|
896
|
+
const noColor = args.includes('--no-color');
|
|
897
|
+
const levelIdx = args.indexOf('--level');
|
|
898
|
+
const moduleIdx = args.indexOf('--module');
|
|
899
|
+
const level = levelIdx !== -1 ? args[levelIdx + 1] : undefined;
|
|
900
|
+
const module = moduleIdx !== -1 ? args[moduleIdx + 1] : undefined;
|
|
901
|
+
const p = resolvePaths();
|
|
902
|
+
const mainLog = findLatestLog(p.logs, 'evolclaw');
|
|
903
|
+
if (!mainLog) {
|
|
904
|
+
console.log(`❌ Log file not found in: ${p.logs}`);
|
|
905
|
+
process.exit(1);
|
|
906
|
+
}
|
|
907
|
+
// Rendered mode: always filter+truncate, color depends on TTY
|
|
908
|
+
const useColor = !noColor && !!process.stdout.isTTY;
|
|
909
|
+
const opts = { level, module, color: useColor };
|
|
910
|
+
function processLine(line) {
|
|
911
|
+
const rendered = renderLogLine(line, opts);
|
|
912
|
+
if (rendered !== null)
|
|
913
|
+
process.stdout.write(rendered + '\n');
|
|
914
|
+
}
|
|
915
|
+
// Backfill last 50 lines from current file
|
|
916
|
+
const existing = fs.readFileSync(mainLog, 'utf-8').split('\n').slice(-51);
|
|
917
|
+
if (existing.length && existing[existing.length - 1] === '')
|
|
918
|
+
existing.pop();
|
|
919
|
+
existing.forEach(processLine);
|
|
920
|
+
if (raw) {
|
|
921
|
+
// Raw mode: poll without rendering
|
|
922
|
+
let currentFile = mainLog;
|
|
923
|
+
let position = fs.statSync(currentFile).size;
|
|
924
|
+
let pending = '';
|
|
925
|
+
const timer = setInterval(() => {
|
|
926
|
+
const latest = findLatestLog(p.logs, 'evolclaw');
|
|
927
|
+
if (latest && latest !== currentFile) {
|
|
928
|
+
currentFile = latest;
|
|
929
|
+
position = 0;
|
|
930
|
+
pending = '';
|
|
931
|
+
}
|
|
932
|
+
let stat;
|
|
933
|
+
try {
|
|
934
|
+
stat = fs.statSync(currentFile);
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (stat.size < position) {
|
|
940
|
+
position = 0;
|
|
941
|
+
pending = '';
|
|
942
|
+
}
|
|
943
|
+
if (stat.size === position)
|
|
944
|
+
return;
|
|
945
|
+
const buf = Buffer.alloc(stat.size - position);
|
|
946
|
+
try {
|
|
947
|
+
const fd = fs.openSync(currentFile, 'r');
|
|
948
|
+
fs.readSync(fd, buf, 0, buf.length, position);
|
|
949
|
+
fs.closeSync(fd);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
position = stat.size;
|
|
955
|
+
const parts = (pending + buf.toString('utf-8')).split('\n');
|
|
956
|
+
pending = parts.pop() || '';
|
|
957
|
+
for (const line of parts) {
|
|
958
|
+
if (line)
|
|
959
|
+
process.stdout.write(line + '\n');
|
|
960
|
+
}
|
|
961
|
+
}, 200);
|
|
962
|
+
platform.onShutdown(() => { clearInterval(timer); process.exit(0); });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
// Follow mode: poll with rendering, auto-switch on rotation
|
|
966
|
+
let currentFile = mainLog;
|
|
967
|
+
let position = fs.statSync(currentFile).size;
|
|
968
|
+
let pending = '';
|
|
969
|
+
const timer = setInterval(() => {
|
|
970
|
+
const latest = findLatestLog(p.logs, 'evolclaw');
|
|
971
|
+
if (latest && latest !== currentFile) {
|
|
972
|
+
currentFile = latest;
|
|
973
|
+
position = 0;
|
|
974
|
+
pending = '';
|
|
975
|
+
}
|
|
976
|
+
let stat;
|
|
977
|
+
try {
|
|
978
|
+
stat = fs.statSync(currentFile);
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (stat.size < position) {
|
|
984
|
+
position = 0;
|
|
985
|
+
pending = '';
|
|
986
|
+
}
|
|
987
|
+
if (stat.size === position)
|
|
988
|
+
return;
|
|
989
|
+
const buf = Buffer.alloc(stat.size - position);
|
|
990
|
+
try {
|
|
991
|
+
const fd = fs.openSync(currentFile, 'r');
|
|
992
|
+
fs.readSync(fd, buf, 0, buf.length, position);
|
|
993
|
+
fs.closeSync(fd);
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
position = stat.size;
|
|
999
|
+
const parts = (pending + buf.toString('utf-8')).split('\n');
|
|
1000
|
+
pending = parts.pop() || '';
|
|
1001
|
+
for (const line of parts) {
|
|
1002
|
+
if (line)
|
|
1003
|
+
processLine(line);
|
|
1004
|
+
}
|
|
1005
|
+
}, 200);
|
|
1006
|
+
platform.onShutdown(() => { clearInterval(timer); process.exit(0); });
|
|
1007
|
+
}
|
|
1008
|
+
// ==================== Watch ====================
|
|
1009
|
+
let watchUseColor = false;
|
|
1010
|
+
const WATCH_BRACKET_TS_RE = /^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)\]/;
|
|
1011
|
+
const WATCH_JSON_TS_RE = /"ts"\s*:\s*"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)"/;
|
|
1012
|
+
function parseWatchTs(s) {
|
|
1013
|
+
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z)?$/);
|
|
1014
|
+
if (!m)
|
|
1015
|
+
return NaN;
|
|
1016
|
+
const [, y, mo, d, h, mi, se, ms, z] = m;
|
|
1017
|
+
const msNum = ms ? parseInt((ms + '000').slice(0, 3), 10) : 0;
|
|
1018
|
+
if (z)
|
|
1019
|
+
return Date.UTC(+y, +mo - 1, +d, +h, +mi, +se, msNum);
|
|
1020
|
+
return new Date(+y, +mo - 1, +d, +h, +mi, +se, msNum).getTime();
|
|
1021
|
+
}
|
|
1022
|
+
function extractWatchTs(line) {
|
|
1023
|
+
const m = line.match(WATCH_BRACKET_TS_RE) || line.match(WATCH_JSON_TS_RE);
|
|
1024
|
+
if (!m)
|
|
1025
|
+
return null;
|
|
1026
|
+
const t = parseWatchTs(m[1]);
|
|
1027
|
+
return isNaN(t) ? null : t;
|
|
1028
|
+
}
|
|
1029
|
+
function toLocalTimeStr(epoch) {
|
|
1030
|
+
const d = new Date(epoch);
|
|
1031
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
1032
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
1033
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
1034
|
+
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
1035
|
+
return `${hh}:${mm}:${ss}.${ms}`;
|
|
1036
|
+
}
|
|
1037
|
+
function stripTimestamp(line) {
|
|
1038
|
+
const m = line.match(WATCH_BRACKET_TS_RE);
|
|
1039
|
+
if (m)
|
|
1040
|
+
return line.slice(m[0].length).trimStart();
|
|
1041
|
+
return line;
|
|
1042
|
+
}
|
|
1043
|
+
function compactAunLog(line, color) {
|
|
1044
|
+
if (!color) {
|
|
1045
|
+
// No-color mode: just compact [LEVEL][module] [aid] → [LEVEL][module] aid:
|
|
1046
|
+
return line
|
|
1047
|
+
.replace(/^\[([A-Z]+)\]\[([^\]]+)\] \[([^\]]+)\] /, '[$1][$2] $3: ')
|
|
1048
|
+
.replace(/^\[([A-Z]+)\] /, '[$1] ');
|
|
1049
|
+
}
|
|
1050
|
+
// Pattern 1: [LEVEL][module] [aid] msg (AUN SDK logs in stdout)
|
|
1051
|
+
const m1 = line.match(/^\[([A-Z]+)\]\[([^\]]+)\](?: \[([^\]]+)\])? (.*)/s);
|
|
1052
|
+
if (m1) {
|
|
1053
|
+
const [, level, mod, aid, rest] = m1;
|
|
1054
|
+
const lc = LEVEL_COLORS[level] || '';
|
|
1055
|
+
const mc = assignModuleColor(mod);
|
|
1056
|
+
const aidPart = aid ? ` ${aid}:` : '';
|
|
1057
|
+
return `${lc}[${level}]${RST_CONST}${mc}[${mod}]${RST_CONST}${aidPart} ${rest}`;
|
|
1058
|
+
}
|
|
1059
|
+
// Pattern 2: [AiBotSDK] [LEVEL] msg (WeCom SDK logs)
|
|
1060
|
+
const m2 = line.match(/^\[([^\]]+)\] \[(DEBUG|INFO|WARN|ERROR)\] (.*)/s);
|
|
1061
|
+
if (m2) {
|
|
1062
|
+
const [, sdk, level, rest] = m2;
|
|
1063
|
+
const lc = LEVEL_COLORS[level] || '';
|
|
1064
|
+
const mc = assignModuleColor(sdk);
|
|
1065
|
+
return `${lc}[${level}]${RST_CONST}${mc}[${sdk}]${RST_CONST} ${rest}`;
|
|
1066
|
+
}
|
|
1067
|
+
// Pattern 3: [LEVEL] [Module] msg or [LEVEL] msg (evolclaw main log after stripTimestamp)
|
|
1068
|
+
const m3 = line.match(/^\[(DEBUG|INFO|WARN|ERROR)\] (?:\[([^\]]+)\] )?(.*)/s);
|
|
1069
|
+
if (m3) {
|
|
1070
|
+
const [, level, mod, rest] = m3;
|
|
1071
|
+
const lc = LEVEL_COLORS[level] || '';
|
|
1072
|
+
if (mod) {
|
|
1073
|
+
const mc = assignModuleColor(mod);
|
|
1074
|
+
return `${lc}[${level}]${RST_CONST} ${mc}[${mod}]${RST_CONST} ${rest}`;
|
|
1075
|
+
}
|
|
1076
|
+
return `${lc}[${level}]${RST_CONST} ${rest}`;
|
|
1077
|
+
}
|
|
1078
|
+
return line;
|
|
1079
|
+
}
|
|
1080
|
+
const RST_CONST = '\x1b[0m';
|
|
1081
|
+
const LEVEL_COLORS = {
|
|
1082
|
+
DEBUG: '\x1b[2m', // dim
|
|
1083
|
+
INFO: '\x1b[36m', // cyan
|
|
1084
|
+
WARN: '\x1b[33m', // yellow
|
|
1085
|
+
ERROR: '\x1b[31m', // red
|
|
1086
|
+
};
|
|
1087
|
+
const moduleColorPool = [
|
|
1088
|
+
'\x1b[35m', // magenta
|
|
1089
|
+
'\x1b[34m', // blue
|
|
1090
|
+
'\x1b[32m', // green
|
|
1091
|
+
'\x1b[96m', // bright cyan
|
|
1092
|
+
'\x1b[93m', // bright yellow
|
|
1093
|
+
'\x1b[95m', // bright magenta
|
|
1094
|
+
'\x1b[94m', // bright blue
|
|
1095
|
+
'\x1b[92m', // bright green
|
|
1096
|
+
];
|
|
1097
|
+
const moduleColorMap = new Map();
|
|
1098
|
+
let moduleColorIdx = 0;
|
|
1099
|
+
function assignModuleColor(mod) {
|
|
1100
|
+
let c = moduleColorMap.get(mod);
|
|
1101
|
+
if (!c) {
|
|
1102
|
+
c = moduleColorPool[moduleColorIdx++ % moduleColorPool.length];
|
|
1103
|
+
moduleColorMap.set(mod, c);
|
|
1104
|
+
}
|
|
1105
|
+
return c;
|
|
1106
|
+
}
|
|
1107
|
+
function formatWatchContent(line) {
|
|
1108
|
+
// JSON line: parse and format key fields
|
|
1109
|
+
if (line.startsWith('{') && line.endsWith('}')) {
|
|
1110
|
+
try {
|
|
1111
|
+
const obj = JSON.parse(line);
|
|
1112
|
+
// Message log format: { ts, msgId, sessionId, dir, status, duration? }
|
|
1113
|
+
if (obj.msgId && obj.status) {
|
|
1114
|
+
const dirLabel = obj.dir === 'inbound' ? '[IN]' : obj.dir === 'outbound' ? '[OUT]' : ' ';
|
|
1115
|
+
const peer = obj.msgId.replace(/_\d+(_reply)?$/, '').replace(/^[^_]+_/, '');
|
|
1116
|
+
const dur = obj.duration != null ? ` duration=${obj.duration}ms` : '';
|
|
1117
|
+
if (!watchUseColor)
|
|
1118
|
+
return `${dirLabel}[${obj.status}] ${peer}:${dur}`.trimEnd();
|
|
1119
|
+
const dc = obj.dir === 'inbound' ? '\x1b[32m' : '\x1b[33m'; // green in, yellow out
|
|
1120
|
+
const ec = assignModuleColor(obj.status);
|
|
1121
|
+
return `${dc}${dirLabel}${RST_CONST}${ec}[${obj.status}]${RST_CONST} ${peer}:${dur}`.trimEnd();
|
|
1122
|
+
}
|
|
1123
|
+
// Event log format: { ts, type, ... }
|
|
1124
|
+
if (obj.type && !obj.dir) {
|
|
1125
|
+
const parts = [];
|
|
1126
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1127
|
+
if (k === 'ts' || k === 'type')
|
|
1128
|
+
continue;
|
|
1129
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
1130
|
+
parts.push(`${k}=${v}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (!watchUseColor)
|
|
1134
|
+
return `[${obj.type}] ${parts.join(' ')}`.trimEnd();
|
|
1135
|
+
const tc = assignModuleColor(obj.type.split(':')[0]);
|
|
1136
|
+
return `${tc}[${obj.type}]${RST_CONST} ${parts.join(' ')}`.trimEnd();
|
|
1137
|
+
}
|
|
1138
|
+
// AUN trace log format: { ts, dir, event, self_aid, data, ... }
|
|
1139
|
+
if (obj.dir && obj.event) {
|
|
1140
|
+
const aid = obj.self_aid || '';
|
|
1141
|
+
const data = obj.data;
|
|
1142
|
+
let dataStr = '';
|
|
1143
|
+
if (data) {
|
|
1144
|
+
const parts = [];
|
|
1145
|
+
for (const [k, v] of Object.entries(data)) {
|
|
1146
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
1147
|
+
parts.push(`${k}=${v}`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
dataStr = parts.join(' ');
|
|
1151
|
+
}
|
|
1152
|
+
const dirLabel = obj.dir === 'IN' ? '[IN]' : obj.dir === 'OUT' ? '[OUT]' : ' ';
|
|
1153
|
+
const aidPart = aid ? `${aid}: ` : '';
|
|
1154
|
+
if (!watchUseColor)
|
|
1155
|
+
return `${dirLabel}[${obj.event}] ${aidPart}${dataStr}`.trimEnd();
|
|
1156
|
+
const dc = obj.dir === 'IN' ? '\x1b[32m' : '\x1b[33m'; // green in, yellow out
|
|
1157
|
+
const ec = assignModuleColor(obj.event.split('.')[0]);
|
|
1158
|
+
return `${dc}${dirLabel}${RST_CONST}${ec}[${obj.event}]${RST_CONST} ${aidPart}${dataStr}`.trimEnd();
|
|
1159
|
+
}
|
|
1160
|
+
// Unknown JSON format — show compact key=value summary
|
|
1161
|
+
const parts = [];
|
|
1162
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1163
|
+
if (k === 'ts')
|
|
1164
|
+
continue;
|
|
1165
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
1166
|
+
parts.push(`${k}=${v}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return parts.join(' ');
|
|
1170
|
+
}
|
|
1171
|
+
catch { /* fall through */ }
|
|
1172
|
+
}
|
|
1173
|
+
return compactAunLog(stripTimestamp(line), watchUseColor);
|
|
1174
|
+
}
|
|
1175
|
+
const WATCH_FILE_COLORS = [
|
|
1176
|
+
'\x1b[36m', // cyan
|
|
1177
|
+
'\x1b[33m', // yellow
|
|
1178
|
+
'\x1b[32m', // green
|
|
1179
|
+
'\x1b[35m', // magenta
|
|
1180
|
+
'\x1b[34m', // blue
|
|
1181
|
+
'\x1b[91m', // bright red
|
|
1182
|
+
'\x1b[92m', // bright green
|
|
1183
|
+
'\x1b[93m', // bright yellow
|
|
1184
|
+
'\x1b[94m', // bright blue
|
|
1185
|
+
'\x1b[95m', // bright magenta
|
|
1186
|
+
'\x1b[96m', // bright cyan
|
|
1187
|
+
];
|
|
1188
|
+
async function cmdWatchMenu() {
|
|
1189
|
+
const items = [
|
|
1190
|
+
{ key: 'log', label: 'log', desc: 'real-time log tail' },
|
|
1191
|
+
{ key: 'aid', label: 'aid', desc: 'AID connection stats' },
|
|
1192
|
+
{ key: 'msg', label: 'msg', desc: 'message inspector' },
|
|
1193
|
+
];
|
|
1194
|
+
let index = 0;
|
|
1195
|
+
const useColor = !!process.stdout.isTTY;
|
|
1196
|
+
const RST = useColor ? '\x1b[0m' : '';
|
|
1197
|
+
const DIM = useColor ? '\x1b[2m' : '';
|
|
1198
|
+
const BOLD = useColor ? '\x1b[1m' : '';
|
|
1199
|
+
const CYAN = useColor ? '\x1b[36m' : '';
|
|
1200
|
+
const pkgRoot = getPackageRoot();
|
|
1201
|
+
function render() {
|
|
1202
|
+
let buf = '\x1b[2J\x1b[H';
|
|
1203
|
+
buf += `${BOLD}evolclaw watch${RST} ${DIM}${pkgRoot}${RST}\n\n`;
|
|
1204
|
+
for (let i = 0; i < items.length; i++) {
|
|
1205
|
+
const sel = i === index;
|
|
1206
|
+
const marker = sel ? `${CYAN}${BOLD} ▸ ` : ' ';
|
|
1207
|
+
const label = sel ? `${items[i].label}${RST}` : `${DIM}${items[i].label}${RST}`;
|
|
1208
|
+
buf += `${marker}${label} ${DIM}${items[i].desc}${RST}\n`;
|
|
1209
|
+
}
|
|
1210
|
+
buf += `\n${DIM} ↑↓ select Enter confirm ESC exit${RST}\n`;
|
|
1211
|
+
process.stdout.write(buf);
|
|
1212
|
+
}
|
|
1213
|
+
render();
|
|
1214
|
+
return new Promise((resolve) => {
|
|
1215
|
+
if (!process.stdin.isTTY) {
|
|
1216
|
+
resolve();
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
process.stdin.setRawMode(true);
|
|
1220
|
+
process.stdin.resume();
|
|
1221
|
+
const onData = async (data) => {
|
|
1222
|
+
if (data[0] === 0x1b && data.length === 1) {
|
|
1223
|
+
cleanup();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (data[0] === 0x03) {
|
|
1227
|
+
cleanup();
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (data[0] === 0x1b && data[1] === 0x5b) {
|
|
1231
|
+
if (data[2] === 0x41) {
|
|
1232
|
+
index = Math.max(0, index - 1);
|
|
1233
|
+
render();
|
|
1234
|
+
}
|
|
1235
|
+
if (data[2] === 0x42) {
|
|
1236
|
+
index = Math.min(items.length - 1, index + 1);
|
|
1237
|
+
render();
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (data[0] === 0x0d) {
|
|
1241
|
+
process.stdin.removeListener('data', onData);
|
|
1242
|
+
process.stdin.setRawMode(false);
|
|
1243
|
+
process.stdin.pause();
|
|
1244
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
1245
|
+
const chosen = items[index].key;
|
|
1246
|
+
if (chosen === 'log') {
|
|
1247
|
+
cmdWatch();
|
|
1248
|
+
}
|
|
1249
|
+
else if (chosen === 'aid') {
|
|
1250
|
+
await cmdWatchAid();
|
|
1251
|
+
}
|
|
1252
|
+
else if (chosen === 'msg') {
|
|
1253
|
+
const { cmdWatchMsg } = await import('../watch-msg.js');
|
|
1254
|
+
await cmdWatchMsg();
|
|
1255
|
+
}
|
|
1256
|
+
resolve();
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
function cleanup() {
|
|
1260
|
+
process.stdin.removeListener('data', onData);
|
|
1261
|
+
if (process.stdin.isTTY)
|
|
1262
|
+
process.stdin.setRawMode(false);
|
|
1263
|
+
process.stdin.pause();
|
|
1264
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
1265
|
+
resolve();
|
|
1266
|
+
}
|
|
1267
|
+
process.stdin.on('data', onData);
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
function cmdWatch() {
|
|
1271
|
+
const p = resolvePaths();
|
|
1272
|
+
if (!fs.existsSync(p.logs)) {
|
|
1273
|
+
console.log(`❌ Log directory not found: ${p.logs}`);
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
// 清理残留的 watch 文件(旧 watch 进程被强杀时留下的)
|
|
1277
|
+
fs.mkdirSync(p.instanceDir, { recursive: true });
|
|
1278
|
+
for (const file of fs.readdirSync(p.instanceDir)) {
|
|
1279
|
+
if (!file.startsWith('watch-') || !file.endsWith('.json'))
|
|
1280
|
+
continue;
|
|
1281
|
+
const filePath = path.join(p.instanceDir, file);
|
|
1282
|
+
try {
|
|
1283
|
+
const rec = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1284
|
+
if (rec.pid && !platform.isProcessRunning(rec.pid)) {
|
|
1285
|
+
fs.unlinkSync(filePath);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
catch {
|
|
1289
|
+
try {
|
|
1290
|
+
fs.unlinkSync(filePath);
|
|
1291
|
+
}
|
|
1292
|
+
catch { }
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// 注册 instance
|
|
1296
|
+
const instanceFile = path.join(p.instanceDir, `watch-${process.pid}.json`);
|
|
1297
|
+
const instanceRecord = {
|
|
1298
|
+
pid: process.pid,
|
|
1299
|
+
startedAt: Date.now(),
|
|
1300
|
+
startedAtIso: new Date().toISOString(),
|
|
1301
|
+
type: 'watch',
|
|
1302
|
+
};
|
|
1303
|
+
fs.writeFileSync(instanceFile, JSON.stringify(instanceRecord, null, 2));
|
|
1304
|
+
const useColor = !!process.stdout.isTTY;
|
|
1305
|
+
watchUseColor = useColor;
|
|
1306
|
+
const RST = useColor ? '\x1b[0m' : '';
|
|
1307
|
+
const DIM = useColor ? '\x1b[2m' : '';
|
|
1308
|
+
const colorMap = new Map();
|
|
1309
|
+
let colorIdx = 0;
|
|
1310
|
+
const getColor = (name) => {
|
|
1311
|
+
if (!useColor)
|
|
1312
|
+
return '';
|
|
1313
|
+
let c = colorMap.get(name);
|
|
1314
|
+
if (!c) {
|
|
1315
|
+
c = WATCH_FILE_COLORS[colorIdx++ % WATCH_FILE_COLORS.length];
|
|
1316
|
+
colorMap.set(name, c);
|
|
1317
|
+
}
|
|
1318
|
+
return c;
|
|
1319
|
+
};
|
|
1320
|
+
const listLogs = () => fs.readdirSync(p.logs).filter(f => f.endsWith('.log')).map(f => path.join(p.logs, f));
|
|
1321
|
+
// Strip rotation suffix (e.g., "evolclaw-20260518-21" → "evolclaw")
|
|
1322
|
+
const shortName = (f) => path.basename(f, '.log').replace(/-\d{8}-\d{2}$/, '');
|
|
1323
|
+
// 计算最长文件名用于对齐
|
|
1324
|
+
let maxNameLen = 0;
|
|
1325
|
+
const updateMaxName = () => {
|
|
1326
|
+
for (const file of listLogs()) {
|
|
1327
|
+
const len = shortName(file).length;
|
|
1328
|
+
if (len > maxNameLen)
|
|
1329
|
+
maxNameLen = len;
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
updateMaxName();
|
|
1333
|
+
const formatLine = (file, ts, line) => {
|
|
1334
|
+
const timeStr = `${DIM}${toLocalTimeStr(ts)}${RST}`;
|
|
1335
|
+
const name = shortName(file);
|
|
1336
|
+
const c = getColor(name);
|
|
1337
|
+
const paddedName = `${c}${name.padEnd(maxNameLen)}${RST}`;
|
|
1338
|
+
const content = formatWatchContent(line);
|
|
1339
|
+
return `${timeStr} ${paddedName} ${content}`;
|
|
1340
|
+
};
|
|
1341
|
+
console.log(`🔭 Watching ${p.logs}/*.log (ESC to stop)\n`);
|
|
1342
|
+
// 显示当前实例信息和 AID 状态
|
|
1343
|
+
const instStatus = scanInstances();
|
|
1344
|
+
const aliveMainEntries = instStatus.mains.filter(m => m.alive);
|
|
1345
|
+
if (aliveMainEntries.length > 0) {
|
|
1346
|
+
if (aliveMainEntries.length > 1) {
|
|
1347
|
+
console.log(`⚠ 检测到 ${aliveMainEntries.length} 个 main 实例: ${aliveMainEntries.map(m => m.record.pid).join(', ')}(异常)\n`);
|
|
1348
|
+
}
|
|
1349
|
+
const m = aliveMainEntries[0].record;
|
|
1350
|
+
const uptime = formatTimeAgo(Date.now() - m.startedAt);
|
|
1351
|
+
console.log(`📦 Instance: PID ${m.pid} | 启动于 ${m.startedAtIso} (${uptime}) | via ${m.launchedBy}`);
|
|
1352
|
+
if (instStatus.aidLastActivity.size > 0) {
|
|
1353
|
+
const now = Date.now();
|
|
1354
|
+
const aidLines = [];
|
|
1355
|
+
for (const [aid, info] of instStatus.aidLastActivity) {
|
|
1356
|
+
const ago = formatTimeAgo(now - info.ts);
|
|
1357
|
+
const symbol = info.event === 'disconnected' ? '✗' : '✓';
|
|
1358
|
+
aidLines.push(` ${symbol} ${aid} — ${info.event} ${ago}`);
|
|
1359
|
+
}
|
|
1360
|
+
console.log(`🔑 AIDs:\n${aidLines.join('\n')}`);
|
|
1361
|
+
}
|
|
1362
|
+
console.log('');
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
console.log('⚠ EvolClaw 主进程未运行\n');
|
|
1366
|
+
}
|
|
1367
|
+
// Backfill: 跨所有 .log 汇总最近 20 条带时间戳行;遇到无时间戳行就停止该文件向上追溯
|
|
1368
|
+
const TAIL_BYTES = 256 * 1024;
|
|
1369
|
+
const collected = [];
|
|
1370
|
+
for (const file of listLogs()) {
|
|
1371
|
+
let stat;
|
|
1372
|
+
try {
|
|
1373
|
+
stat = fs.statSync(file);
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
const start = Math.max(0, stat.size - TAIL_BYTES);
|
|
1379
|
+
const buf = Buffer.alloc(stat.size - start);
|
|
1380
|
+
try {
|
|
1381
|
+
const fd = fs.openSync(file, 'r');
|
|
1382
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
1383
|
+
fs.closeSync(fd);
|
|
1384
|
+
}
|
|
1385
|
+
catch {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
const lines = buf.toString('utf-8').split('\n');
|
|
1389
|
+
if (start > 0)
|
|
1390
|
+
lines.shift();
|
|
1391
|
+
if (lines.length && lines[lines.length - 1] === '')
|
|
1392
|
+
lines.pop();
|
|
1393
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1394
|
+
const ts = extractWatchTs(lines[i]);
|
|
1395
|
+
if (ts === null)
|
|
1396
|
+
break;
|
|
1397
|
+
collected.push({ ts, file, line: lines[i] });
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
collected.sort((a, b) => a.ts - b.ts);
|
|
1401
|
+
for (const r of collected.slice(-20)) {
|
|
1402
|
+
process.stdout.write(formatLine(r.file, r.ts, r.line) + '\n');
|
|
1403
|
+
}
|
|
1404
|
+
if (collected.length > 0)
|
|
1405
|
+
process.stdout.write('\n');
|
|
1406
|
+
// 实时跟踪
|
|
1407
|
+
const state = new Map();
|
|
1408
|
+
for (const file of listLogs()) {
|
|
1409
|
+
try {
|
|
1410
|
+
state.set(file, { position: fs.statSync(file).size, pending: '' });
|
|
1411
|
+
}
|
|
1412
|
+
catch { }
|
|
1413
|
+
}
|
|
1414
|
+
const pumpFile = (file) => {
|
|
1415
|
+
const s = state.get(file);
|
|
1416
|
+
if (!s)
|
|
1417
|
+
return;
|
|
1418
|
+
let stat;
|
|
1419
|
+
try {
|
|
1420
|
+
stat = fs.statSync(file);
|
|
1421
|
+
}
|
|
1422
|
+
catch {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (stat.size < s.position) {
|
|
1426
|
+
s.position = 0;
|
|
1427
|
+
s.pending = '';
|
|
1428
|
+
}
|
|
1429
|
+
if (stat.size === s.position)
|
|
1430
|
+
return;
|
|
1431
|
+
const buf = Buffer.alloc(stat.size - s.position);
|
|
1432
|
+
try {
|
|
1433
|
+
const fd = fs.openSync(file, 'r');
|
|
1434
|
+
fs.readSync(fd, buf, 0, buf.length, s.position);
|
|
1435
|
+
fs.closeSync(fd);
|
|
1436
|
+
}
|
|
1437
|
+
catch {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
s.position = stat.size;
|
|
1441
|
+
const parts = (s.pending + buf.toString('utf-8')).split('\n');
|
|
1442
|
+
s.pending = parts.pop() || '';
|
|
1443
|
+
for (const line of parts) {
|
|
1444
|
+
if (!line.trim())
|
|
1445
|
+
continue;
|
|
1446
|
+
const ts = extractWatchTs(line);
|
|
1447
|
+
if (ts !== null) {
|
|
1448
|
+
process.stdout.write(formatLine(file, ts, line) + '\n');
|
|
1449
|
+
}
|
|
1450
|
+
else {
|
|
1451
|
+
// 无时间戳行:对齐到内容列
|
|
1452
|
+
const pad = 12 + 1 + maxNameLen + 1; // "HH:MM:SS.mmm" + space + name + space
|
|
1453
|
+
process.stdout.write(' '.repeat(pad) + line + '\n');
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
const timer = setInterval(() => {
|
|
1458
|
+
for (const file of listLogs()) {
|
|
1459
|
+
if (!state.has(file))
|
|
1460
|
+
state.set(file, { position: 0, pending: '' });
|
|
1461
|
+
}
|
|
1462
|
+
updateMaxName();
|
|
1463
|
+
for (const file of state.keys())
|
|
1464
|
+
pumpFile(file);
|
|
1465
|
+
}, 200);
|
|
1466
|
+
const cleanup = () => { clearInterval(timer); try {
|
|
1467
|
+
fs.unlinkSync(instanceFile);
|
|
1468
|
+
}
|
|
1469
|
+
catch { } if (process.stdin.isTTY)
|
|
1470
|
+
process.stdin.setRawMode(false); process.exit(0); };
|
|
1471
|
+
process.on('exit', () => { try {
|
|
1472
|
+
fs.unlinkSync(instanceFile);
|
|
1473
|
+
}
|
|
1474
|
+
catch { } });
|
|
1475
|
+
// ESC key listener
|
|
1476
|
+
if (process.stdin.isTTY) {
|
|
1477
|
+
process.stdin.setRawMode(true);
|
|
1478
|
+
process.stdin.resume();
|
|
1479
|
+
process.stdin.on('data', (key) => {
|
|
1480
|
+
if (key[0] === 0x1b && key.length === 1)
|
|
1481
|
+
cleanup(); // ESC
|
|
1482
|
+
if (key[0] === 0x03)
|
|
1483
|
+
cleanup(); // Ctrl+C fallback
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
platform.onShutdown(cleanup);
|
|
1487
|
+
}
|
|
1488
|
+
// ==================== Watch AID (real-time stats table) ====================
|
|
1489
|
+
function formatBytes(bytes) {
|
|
1490
|
+
if (bytes === 0)
|
|
1491
|
+
return '0 B';
|
|
1492
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
1493
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
1494
|
+
const val = bytes / Math.pow(1024, i);
|
|
1495
|
+
return i === 0 ? `${bytes} B` : `${val.toFixed(1)} ${units[i]}`;
|
|
1496
|
+
}
|
|
1497
|
+
function formatTimeAgoShort(ms) {
|
|
1498
|
+
const sec = Math.floor(ms / 1000);
|
|
1499
|
+
if (sec < 60)
|
|
1500
|
+
return `${sec}s ago`;
|
|
1501
|
+
const min = Math.floor(sec / 60);
|
|
1502
|
+
if (min < 60)
|
|
1503
|
+
return `${min}m ago`;
|
|
1504
|
+
const hour = Math.floor(min / 60);
|
|
1505
|
+
if (hour < 24)
|
|
1506
|
+
return `${hour}h ago`;
|
|
1507
|
+
const day = Math.floor(hour / 24);
|
|
1508
|
+
return `${day}d ago`;
|
|
1509
|
+
}
|
|
1510
|
+
async function cmdWatchAid() {
|
|
1511
|
+
const p = resolvePaths();
|
|
1512
|
+
// Get version
|
|
1513
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
|
|
1514
|
+
const version = pkg.version;
|
|
1515
|
+
// Load AID names: first from local agent.md, then refresh from network
|
|
1516
|
+
const { aidList, aidLookup } = await import('../aun/aid/index.js');
|
|
1517
|
+
const localAids = aidList();
|
|
1518
|
+
const aidNameMap = new Map();
|
|
1519
|
+
const refreshedAids = new Set();
|
|
1520
|
+
function readLocalName(aid) {
|
|
1521
|
+
try {
|
|
1522
|
+
const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aid, 'agent.md');
|
|
1523
|
+
if (!fs.existsSync(agentMdPath))
|
|
1524
|
+
return undefined;
|
|
1525
|
+
const content = fs.readFileSync(agentMdPath, 'utf-8');
|
|
1526
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1527
|
+
if (!fmMatch)
|
|
1528
|
+
return undefined;
|
|
1529
|
+
const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
1530
|
+
return nameMatch?.[1]?.trim() || undefined;
|
|
1531
|
+
}
|
|
1532
|
+
catch {
|
|
1533
|
+
return undefined;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function parseNameFromContent(content) {
|
|
1537
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1538
|
+
if (!fmMatch)
|
|
1539
|
+
return undefined;
|
|
1540
|
+
const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
1541
|
+
return nameMatch?.[1]?.trim() || undefined;
|
|
1542
|
+
}
|
|
1543
|
+
// Phase 1: read cached local names
|
|
1544
|
+
for (const a of localAids) {
|
|
1545
|
+
if (!a.hasPrivateKey)
|
|
1546
|
+
continue;
|
|
1547
|
+
const name = readLocalName(a.aid);
|
|
1548
|
+
if (name)
|
|
1549
|
+
aidNameMap.set(a.aid, name);
|
|
1550
|
+
}
|
|
1551
|
+
// Phase 2: refresh names from network via aidLookup (async, non-blocking)
|
|
1552
|
+
const refreshNames = async () => {
|
|
1553
|
+
for (const a of localAids) {
|
|
1554
|
+
if (!a.hasPrivateKey)
|
|
1555
|
+
continue;
|
|
1556
|
+
try {
|
|
1557
|
+
const result = await aidLookup(a.aid);
|
|
1558
|
+
if (result.exists && result.content) {
|
|
1559
|
+
const name = parseNameFromContent(result.content);
|
|
1560
|
+
if (name)
|
|
1561
|
+
aidNameMap.set(a.aid, name);
|
|
1562
|
+
}
|
|
1563
|
+
refreshedAids.add(a.aid);
|
|
1564
|
+
}
|
|
1565
|
+
catch { /* ignore network errors */ }
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
refreshNames();
|
|
1569
|
+
// Register instance
|
|
1570
|
+
fs.mkdirSync(p.instanceDir, { recursive: true });
|
|
1571
|
+
const instanceFile = path.join(p.instanceDir, `watch-aid-${process.pid}.json`);
|
|
1572
|
+
fs.writeFileSync(instanceFile, JSON.stringify({
|
|
1573
|
+
pid: process.pid,
|
|
1574
|
+
startedAt: Date.now(),
|
|
1575
|
+
startedAtIso: new Date().toISOString(),
|
|
1576
|
+
type: 'watch-aid',
|
|
1577
|
+
}, null, 2));
|
|
1578
|
+
const useColor = !!process.stdout.isTTY;
|
|
1579
|
+
const RST = useColor ? '\x1b[0m' : '';
|
|
1580
|
+
const DIM = useColor ? '\x1b[2m' : '';
|
|
1581
|
+
const BOLD = useColor ? '\x1b[1m' : '';
|
|
1582
|
+
const CYAN = useColor ? '\x1b[36m' : '';
|
|
1583
|
+
const GREEN = useColor ? '\x1b[32m' : '';
|
|
1584
|
+
const RED = useColor ? '\x1b[31m' : '';
|
|
1585
|
+
const CLR_LINE = '\x1b[2K';
|
|
1586
|
+
const watchStartedAt = new Date();
|
|
1587
|
+
const watchStartStr = `${String(watchStartedAt.getHours()).padStart(2, '0')}:${String(watchStartedAt.getMinutes()).padStart(2, '0')}:${String(watchStartedAt.getSeconds()).padStart(2, '0')}`;
|
|
1588
|
+
const COL_AID = 28;
|
|
1589
|
+
const COL_STATUS = 14;
|
|
1590
|
+
const COL_UPTIME = 11;
|
|
1591
|
+
const COL_STATE = 11;
|
|
1592
|
+
const COL_RECONN = 6;
|
|
1593
|
+
const COL_RECV = 5;
|
|
1594
|
+
const COL_SENT = 5;
|
|
1595
|
+
const COL_SYS = 8;
|
|
1596
|
+
const COL_BIN = 9;
|
|
1597
|
+
const COL_BOUT = 9;
|
|
1598
|
+
const COL_LRECV = 10;
|
|
1599
|
+
const COL_LSENT = 10;
|
|
1600
|
+
const COL_PEERS = 5;
|
|
1601
|
+
function formatDuration(ms) {
|
|
1602
|
+
const sec = Math.floor(ms / 1000);
|
|
1603
|
+
if (sec < 60)
|
|
1604
|
+
return `${sec}s`;
|
|
1605
|
+
const min = Math.floor(sec / 60);
|
|
1606
|
+
const s = sec % 60;
|
|
1607
|
+
if (min < 60)
|
|
1608
|
+
return `${min}m${String(s).padStart(2, '0')}s`;
|
|
1609
|
+
const hour = Math.floor(min / 60);
|
|
1610
|
+
const m = min % 60;
|
|
1611
|
+
if (hour < 24)
|
|
1612
|
+
return `${hour}h${String(m).padStart(2, '0')}m${String(s).padStart(2, '0')}s`;
|
|
1613
|
+
const day = Math.floor(hour / 24);
|
|
1614
|
+
return `${day}d${hour % 24}h${String(m).padStart(2, '0')}m`;
|
|
1615
|
+
}
|
|
1616
|
+
function renderHeader() {
|
|
1617
|
+
return ' ' +
|
|
1618
|
+
padRight('AID', COL_AID) +
|
|
1619
|
+
padRight('STATUS', COL_STATUS) +
|
|
1620
|
+
padRight('UPTIME', COL_UPTIME) +
|
|
1621
|
+
padRight('STATE', COL_STATE) +
|
|
1622
|
+
padRight('RECONN', COL_RECONN) +
|
|
1623
|
+
padRight('RECV', COL_RECV) +
|
|
1624
|
+
padRight('SENT', COL_SENT) +
|
|
1625
|
+
padRight('SYS R/S', COL_SYS) +
|
|
1626
|
+
padRight('BYTES IN', COL_BIN) +
|
|
1627
|
+
padRight('BYTES OUT', COL_BOUT) +
|
|
1628
|
+
padRight('LAST RECV', COL_LRECV) +
|
|
1629
|
+
padRight('LAST SENT', COL_LSENT) +
|
|
1630
|
+
padRight('PEERS', COL_PEERS);
|
|
1631
|
+
}
|
|
1632
|
+
function renderRow(aid, stats, projectPath) {
|
|
1633
|
+
const aidLabel = aid.aid.length > COL_AID - 2 ? aid.aid.slice(0, COL_AID - 4) + '..' : aid.aid;
|
|
1634
|
+
const statusLabel = AID_STATUS_LABELS[aid.status] || aid.status;
|
|
1635
|
+
const now = Date.now();
|
|
1636
|
+
const lastRecv = stats?.lastReceivedAt ? formatTimeAgoShort(now - stats.lastReceivedAt) : '—';
|
|
1637
|
+
const lastSent = stats?.lastSentAt ? formatTimeAgoShort(now - stats.lastSentAt) : '—';
|
|
1638
|
+
const uptime = (aid.status === 'connected' && aid.lastConnectedAt)
|
|
1639
|
+
? formatDuration(now - aid.lastConnectedAt)
|
|
1640
|
+
: '—';
|
|
1641
|
+
// State: processing / queued / idle
|
|
1642
|
+
const YELLOW = useColor ? '\x1b[33m' : '';
|
|
1643
|
+
let stateLabel = 'idle';
|
|
1644
|
+
if (stats?.processing > 0) {
|
|
1645
|
+
stateLabel = `${YELLOW}working${RST}`;
|
|
1646
|
+
}
|
|
1647
|
+
else if (stats?.queued > 0) {
|
|
1648
|
+
stateLabel = `${YELLOW}queued(${stats.queued})${RST}`;
|
|
1649
|
+
}
|
|
1650
|
+
const mainLine = ' ' +
|
|
1651
|
+
padRight(aidLabel, COL_AID) +
|
|
1652
|
+
padRight(statusLabel, COL_STATUS) +
|
|
1653
|
+
padRight(uptime, COL_UPTIME) +
|
|
1654
|
+
padRight(stateLabel, COL_STATE) +
|
|
1655
|
+
padRight(String(aid.reconnectCount ?? 0), COL_RECONN) +
|
|
1656
|
+
padRight(String(stats?.messagesReceived ?? 0), COL_RECV) +
|
|
1657
|
+
padRight(String(stats?.messagesSent ?? 0), COL_SENT) +
|
|
1658
|
+
padRight(`${stats?.systemReceived ?? 0}/${stats?.systemSent ?? 0}`, COL_SYS) +
|
|
1659
|
+
padRight(formatBytes(stats?.bytesReceived ?? 0), COL_BIN) +
|
|
1660
|
+
padRight(formatBytes(stats?.bytesSent ?? 0), COL_BOUT) +
|
|
1661
|
+
padRight(lastRecv, COL_LRECV) +
|
|
1662
|
+
padRight(lastSent, COL_LSENT) +
|
|
1663
|
+
padRight(String(stats?.uniquePeerCount ?? 0), COL_PEERS);
|
|
1664
|
+
const namePart = aidNameMap.get(aid.aid) || stats?.selfName || aid.agentName || '';
|
|
1665
|
+
const nameColor = refreshedAids.has(aid.aid) ? '' : DIM;
|
|
1666
|
+
const nameReset = refreshedAids.has(aid.aid) ? '' : RST;
|
|
1667
|
+
const BLUE = useColor ? '\x1b[34m' : '';
|
|
1668
|
+
const ORANGE = useColor ? '\x1b[38;5;208m' : '';
|
|
1669
|
+
let msgPreview = '';
|
|
1670
|
+
if (stats?.lastReceivedAt || stats?.lastSentAt) {
|
|
1671
|
+
const recvTs = stats.lastReceivedAt ?? 0;
|
|
1672
|
+
const sentTs = stats.lastSentAt ?? 0;
|
|
1673
|
+
if (recvTs >= sentTs && stats.lastReceivedText) {
|
|
1674
|
+
const fromShort = stats.lastReceivedFrom ? stats.lastReceivedFrom.split('.')[0] : '';
|
|
1675
|
+
msgPreview = `${GREEN}↓ ${fromShort ? `${ORANGE}${fromShort}${RST}${GREEN}: ` : ''}${stats.lastReceivedText.replace(/\n/g, ' ').slice(0, 60)}${RST}`;
|
|
1676
|
+
}
|
|
1677
|
+
else if (stats.lastSentText) {
|
|
1678
|
+
const toShort = stats.lastSentTo ? stats.lastSentTo.split('.')[0] : '';
|
|
1679
|
+
msgPreview = `${BLUE}↑ ${toShort ? `${ORANGE}${toShort}${RST}${BLUE}: ` : ''}${stats.lastSentText.replace(/\n/g, ' ').slice(0, 60)}${RST}`;
|
|
1680
|
+
}
|
|
1681
|
+
else if (stats.lastReceivedText) {
|
|
1682
|
+
const fromShort = stats.lastReceivedFrom ? stats.lastReceivedFrom.split('.')[0] : '';
|
|
1683
|
+
msgPreview = `${GREEN}↓ ${fromShort ? `${ORANGE}${fromShort}${RST}${GREEN}: ` : ''}${stats.lastReceivedText.replace(/\n/g, ' ').slice(0, 60)}${RST}`;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
const subLine1 = ` ${nameColor}${namePart}${nameReset}${msgPreview ? ' ' + msgPreview : ''}`;
|
|
1687
|
+
const dirLabel = projectPath || '—';
|
|
1688
|
+
const subLine2 = `${DIM} ${dirLabel}${RST}`;
|
|
1689
|
+
return [mainLine, subLine1, subLine2];
|
|
1690
|
+
}
|
|
1691
|
+
let lastLineCount = 0;
|
|
1692
|
+
async function render() {
|
|
1693
|
+
const lines = [];
|
|
1694
|
+
// Query daemon — may be offline
|
|
1695
|
+
const [aidsResp, statsResp, statusResp, agentsResp] = await Promise.all([
|
|
1696
|
+
ipcQuery(p.socket, { type: 'aun-aids' }),
|
|
1697
|
+
ipcQuery(p.socket, { type: 'aun-aid-stats' }),
|
|
1698
|
+
ipcQuery(p.socket, { type: 'status' }),
|
|
1699
|
+
ipcQuery(p.socket, { type: 'evolagent.list' }),
|
|
1700
|
+
]);
|
|
1701
|
+
const daemonOnline = statusResp !== null;
|
|
1702
|
+
const aids = aidsResp?.aids ?? [];
|
|
1703
|
+
const stats = statsResp?.stats ?? [];
|
|
1704
|
+
const statsMap = new Map();
|
|
1705
|
+
for (const s of stats)
|
|
1706
|
+
statsMap.set(s.aid, s);
|
|
1707
|
+
// Map agentName → projectPath
|
|
1708
|
+
const agents = agentsResp?.agents ?? [];
|
|
1709
|
+
const agentProjectMap = new Map();
|
|
1710
|
+
for (const a of agents) {
|
|
1711
|
+
if (a.name && a.projectPath)
|
|
1712
|
+
agentProjectMap.set(a.name, a.projectPath);
|
|
1713
|
+
}
|
|
1714
|
+
const now = new Date();
|
|
1715
|
+
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
1716
|
+
// Compute daemon start time
|
|
1717
|
+
let startedAtStr = '';
|
|
1718
|
+
if (daemonOnline && statusResp?.uptime) {
|
|
1719
|
+
const startedAt = new Date(Date.now() - statusResp.uptime);
|
|
1720
|
+
startedAtStr = `${String(startedAt.getHours()).padStart(2, '0')}:${String(startedAt.getMinutes()).padStart(2, '0')}:${String(startedAt.getSeconds()).padStart(2, '0')}`;
|
|
1721
|
+
}
|
|
1722
|
+
const statusIndicator = daemonOnline
|
|
1723
|
+
? `${GREEN}● Running${RST}`
|
|
1724
|
+
: `${RED}● Offline${RST}`;
|
|
1725
|
+
const startInfo = startedAtStr ? ` | Started: ${startedAtStr}` : '';
|
|
1726
|
+
lines.push(`${BOLD}${CYAN}📊 EvolClaw AID Monitor${RST} ${statusIndicator} ${DIM}${timeStr} | Watch: ${watchStartStr}${startInfo} | Refresh: 1s | ESC to exit${RST}`);
|
|
1727
|
+
lines.push('');
|
|
1728
|
+
if (!daemonOnline) {
|
|
1729
|
+
lines.push(` ${RED}EvolClaw is not running.${RST} Waiting for daemon to start...`);
|
|
1730
|
+
lines.push('');
|
|
1731
|
+
}
|
|
1732
|
+
else if (aids.length === 0) {
|
|
1733
|
+
lines.push(' No active AIDs');
|
|
1734
|
+
lines.push('');
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
lines.push(`${DIM}${renderHeader()}${RST}`);
|
|
1738
|
+
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;
|
|
1739
|
+
lines.push(`${DIM} ${'─'.repeat(lineWidth)}${RST}`);
|
|
1740
|
+
for (const aid of aids) {
|
|
1741
|
+
const s = statsMap.get(aid.aid);
|
|
1742
|
+
const projPath = agentProjectMap.get(aid.agentName);
|
|
1743
|
+
lines.push(...renderRow(aid, s, projPath));
|
|
1744
|
+
}
|
|
1745
|
+
lines.push('');
|
|
1746
|
+
}
|
|
1747
|
+
// Status bar
|
|
1748
|
+
lines.push(`${DIM} ${'─'.repeat(80)}${RST}`);
|
|
1749
|
+
if (daemonOnline) {
|
|
1750
|
+
const connectedCount = aids.filter((a) => a.status === 'connected').length;
|
|
1751
|
+
const totalRecv = stats.reduce((sum, s) => sum + (s.messagesReceived ?? 0), 0);
|
|
1752
|
+
const totalSent = stats.reduce((sum, s) => sum + (s.messagesSent ?? 0), 0);
|
|
1753
|
+
const totalBytesIn = stats.reduce((sum, s) => sum + (s.bytesReceived ?? 0), 0);
|
|
1754
|
+
const totalBytesOut = stats.reduce((sum, s) => sum + (s.bytesSent ?? 0), 0);
|
|
1755
|
+
const gateways = [...new Set(aids.filter((a) => a.gatewayUrl).map((a) => a.gatewayUrl))];
|
|
1756
|
+
const gatewayStr = gateways.length > 0 ? gateways.join(', ') : '—';
|
|
1757
|
+
const daemonUptime = statusResp?.uptime ? formatDuration(statusResp.uptime) : '—';
|
|
1758
|
+
const daemonPid = statusResp?.pid ?? '—';
|
|
1759
|
+
lines.push(` ${GREEN}Gateway:${RST} ${gatewayStr}`);
|
|
1760
|
+
lines.push(` ${GREEN}AIDs:${RST} ${aids.length} total, ${connectedCount} connected | ${GREEN}Messages:${RST} ↓${totalRecv} ↑${totalSent} | ${GREEN}Traffic:${RST} ↓${formatBytes(totalBytesIn)} ↑${formatBytes(totalBytesOut)}`);
|
|
1761
|
+
lines.push(` ${GREEN}Version:${RST} ${version} | ${GREEN}PID:${RST} ${daemonPid} | ${GREEN}Uptime:${RST} ${daemonUptime}`);
|
|
1762
|
+
}
|
|
1763
|
+
else {
|
|
1764
|
+
lines.push(` ${GREEN}Version:${RST} ${version}`);
|
|
1765
|
+
}
|
|
1766
|
+
// Build frame buffer: cursor home, then each line with clear-to-EOL
|
|
1767
|
+
let buf = '\x1b[H';
|
|
1768
|
+
for (const line of lines) {
|
|
1769
|
+
buf += CLR_LINE + line + '\n';
|
|
1770
|
+
}
|
|
1771
|
+
// Clear any leftover lines from previous frame
|
|
1772
|
+
for (let i = lines.length; i < lastLineCount; i++) {
|
|
1773
|
+
buf += CLR_LINE + '\n';
|
|
1774
|
+
}
|
|
1775
|
+
lastLineCount = lines.length;
|
|
1776
|
+
process.stdout.write(buf);
|
|
1777
|
+
}
|
|
1778
|
+
// Initial clear screen
|
|
1779
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
1780
|
+
await render();
|
|
1781
|
+
const timer = setInterval(render, 1000);
|
|
1782
|
+
const cleanup = () => {
|
|
1783
|
+
clearInterval(timer);
|
|
1784
|
+
try {
|
|
1785
|
+
fs.unlinkSync(instanceFile);
|
|
1786
|
+
}
|
|
1787
|
+
catch { }
|
|
1788
|
+
process.exit(0);
|
|
1789
|
+
};
|
|
1790
|
+
// Listen for ESC key
|
|
1791
|
+
if (process.stdin.isTTY) {
|
|
1792
|
+
process.stdin.setRawMode(true);
|
|
1793
|
+
process.stdin.resume();
|
|
1794
|
+
process.stdin.on('data', (data) => {
|
|
1795
|
+
if (data[0] === 0x1b || data[0] === 0x03)
|
|
1796
|
+
cleanup();
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
platform.onShutdown(cleanup);
|
|
1800
|
+
}
|
|
1801
|
+
async function cmdRestartMonitor() {
|
|
1802
|
+
const p = resolvePaths();
|
|
1803
|
+
const restartLog = path.join(p.logs, 'restart.log');
|
|
1804
|
+
const MAX_HEAL_ATTEMPTS = 3;
|
|
1805
|
+
const READY_TIMEOUT = 30000; // 30s(AUN sidecar 10s + Feishu 连接 12s)
|
|
1806
|
+
const HEAL_TIMEOUT = 30 * 60 * 1000; // 30 分钟,让 claude 自然结束
|
|
1807
|
+
const eventBus = new EventBus();
|
|
1808
|
+
const log = (msg) => {
|
|
1809
|
+
const _d = new Date();
|
|
1810
|
+
const _p = (n) => String(n).padStart(2, '0');
|
|
1811
|
+
const ts = `${_d.getFullYear()}-${_p(_d.getMonth() + 1)}-${_p(_d.getDate())} ${_p(_d.getHours())}:${_p(_d.getMinutes())}:${_p(_d.getSeconds())}`;
|
|
1812
|
+
const line = `[${ts}] ${msg}\n`;
|
|
1813
|
+
fs.appendFileSync(restartLog, line);
|
|
1814
|
+
};
|
|
1815
|
+
// 单实例保护:pre-check + post-write 自检(同 main 进程)
|
|
1816
|
+
{
|
|
1817
|
+
const pre = scanInstances();
|
|
1818
|
+
const aliveOthers = pre.restartMonitors.filter(m => m.alive && m.record.pid !== process.pid);
|
|
1819
|
+
if (aliveOthers.length > 0) {
|
|
1820
|
+
log(`Another restart-monitor already running (PID: ${aliveOthers.map(m => m.record.pid).join(', ')}), exiting`);
|
|
1821
|
+
process.exit(0);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
// 立即登记自己;exit 路径上自动清理 record
|
|
1825
|
+
writeRestartMonitor();
|
|
1826
|
+
process.on('exit', () => removeRestartMonitor());
|
|
1827
|
+
// post-write 自检:(startedAt, pid) 选最早赢家
|
|
1828
|
+
{
|
|
1829
|
+
const verdict = isRestartMonitorWinner();
|
|
1830
|
+
if (!verdict.winner) {
|
|
1831
|
+
log(`Lost restart-monitor election to PID ${verdict.conflictingPid}, yielding`);
|
|
1832
|
+
removeRestartMonitor();
|
|
1833
|
+
process.exit(0);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
/** 检查服务是否已经在运行(ready signal 存在 + 至少一个活 main) */
|
|
1837
|
+
const isServiceAlive = () => {
|
|
1838
|
+
if (!fs.existsSync(p.readySignal))
|
|
1839
|
+
return false;
|
|
1840
|
+
const s = scanInstances();
|
|
1841
|
+
return s.mains.some(m => m.alive);
|
|
1842
|
+
};
|
|
1843
|
+
log('Restart monitor started');
|
|
1844
|
+
// 读取 restart-pending.json 用于后续通知
|
|
1845
|
+
const pendingFile = path.join(p.dataDir, 'restart-pending.json');
|
|
1846
|
+
let pendingInfo = null;
|
|
1847
|
+
try {
|
|
1848
|
+
if (fs.existsSync(pendingFile)) {
|
|
1849
|
+
pendingInfo = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
catch { }
|
|
1853
|
+
// 等待所有活 main 进程退出(可能不止一个)
|
|
1854
|
+
const oldStatus = scanInstances();
|
|
1855
|
+
const aliveMains = oldStatus.mains.filter(m => m.alive);
|
|
1856
|
+
if (aliveMains.length > 0) {
|
|
1857
|
+
const oldPids = aliveMains.map(m => m.record.pid);
|
|
1858
|
+
log(`Monitoring ${oldPids.length} main process(es): ${oldPids.join(', ')}`);
|
|
1859
|
+
// 先并行 SIGTERM 通知所有活 main
|
|
1860
|
+
for (const pid of oldPids) {
|
|
1861
|
+
try {
|
|
1862
|
+
platform.killProcess(pid, false);
|
|
1863
|
+
}
|
|
1864
|
+
catch { }
|
|
1865
|
+
}
|
|
1866
|
+
await Promise.all(oldPids.map(oldPid => new Promise((resolve) => {
|
|
1867
|
+
let waited = 0;
|
|
1868
|
+
const interval = setInterval(() => {
|
|
1869
|
+
waited++;
|
|
1870
|
+
if (!platform.isProcessRunning(oldPid)) {
|
|
1871
|
+
clearInterval(interval);
|
|
1872
|
+
log(`Process ${oldPid} has exited`);
|
|
1873
|
+
resolve();
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (waited >= 30) {
|
|
1877
|
+
clearInterval(interval);
|
|
1878
|
+
log(`ERROR: Process ${oldPid} still running after 30s, force killing`);
|
|
1879
|
+
platform.killProcess(oldPid, true);
|
|
1880
|
+
resolve();
|
|
1881
|
+
}
|
|
1882
|
+
}, 1000);
|
|
1883
|
+
})));
|
|
1884
|
+
await sleep(3000);
|
|
1885
|
+
cleanupInstances();
|
|
1886
|
+
}
|
|
1887
|
+
// 版本检查与自动升级
|
|
1888
|
+
log('Checking for updates...');
|
|
1889
|
+
const upgrade = await tryUpgrade();
|
|
1890
|
+
switch (upgrade.status) {
|
|
1891
|
+
case 'upgraded':
|
|
1892
|
+
log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
|
|
1893
|
+
await notifyChannel(p, pendingInfo, `📦 已升级 ${upgrade.from} → ${upgrade.to}`, log);
|
|
1894
|
+
break;
|
|
1895
|
+
case 'no-update':
|
|
1896
|
+
log(`Already up to date (${upgrade.from})`);
|
|
1897
|
+
break;
|
|
1898
|
+
case 'skipped':
|
|
1899
|
+
log(upgrade.error
|
|
1900
|
+
? 'Skipped upgrade (network unavailable)'
|
|
1901
|
+
: 'Skipped upgrade check (dev mode)');
|
|
1902
|
+
break;
|
|
1903
|
+
case 'failed':
|
|
1904
|
+
log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}): ${upgrade.error}`);
|
|
1905
|
+
await notifyChannel(p, pendingInfo, `⚠️ 升级失败,使用当前版本继续`, log);
|
|
1906
|
+
break;
|
|
1907
|
+
}
|
|
1908
|
+
// 启动并检测 ready signal
|
|
1909
|
+
let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
|
|
1910
|
+
if (started) {
|
|
1911
|
+
log('✓ Service restarted successfully');
|
|
1912
|
+
archiveSelfHealLog(p, log);
|
|
1913
|
+
// 通知由新进程自行发送(channel-agnostic),此处不再调用 notifyChannel
|
|
1914
|
+
process.exit(0);
|
|
1915
|
+
}
|
|
1916
|
+
// 启动失败 — 测试环境下跳过 self-heal(避免 claude -p 污染会话列表、误杀生产进程)
|
|
1917
|
+
if (p.root.startsWith('/tmp/') || process.env.EVOLCLAW_TEST === '1') {
|
|
1918
|
+
log('❌ Service failed to start (test environment detected, skipping self-heal)');
|
|
1919
|
+
await notifyChannel(p, pendingInfo, '❌ 服务启动失败(测试环境,已跳过自动修复)', log);
|
|
1920
|
+
cleanupPendingFile(pendingFile, log);
|
|
1921
|
+
process.exit(1);
|
|
1922
|
+
}
|
|
1923
|
+
// 启动失败,进入 self-heal 循环
|
|
1924
|
+
log('❌ Service failed to start, entering self-heal loop');
|
|
1925
|
+
eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
|
|
1926
|
+
await notifyChannel(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
|
|
1927
|
+
for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
|
|
1928
|
+
// 前置检查:服务可能已被上一轮 claude 修复并启动
|
|
1929
|
+
if (isServiceAlive()) {
|
|
1930
|
+
log(`✓ Service already running before attempt ${attempt}, skipping`);
|
|
1931
|
+
await sendHealSummary(p, pendingInfo, attempt - 1, log);
|
|
1932
|
+
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt - 1 });
|
|
1933
|
+
archiveSelfHealLog(p, log);
|
|
1934
|
+
cleanupPendingFile(pendingFile, log);
|
|
1935
|
+
process.exit(0);
|
|
1936
|
+
}
|
|
1937
|
+
log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
|
|
1938
|
+
eventBus.publish({ type: 'self-heal:attempt', attemptNumber: attempt, maxAttempts: MAX_HEAL_ATTEMPTS });
|
|
1939
|
+
await notifyChannel(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
|
|
1940
|
+
const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
|
|
1941
|
+
// 后置检查:不管 invokeClaude 返回什么,都检查服务实际状态
|
|
1942
|
+
if (isServiceAlive()) {
|
|
1943
|
+
log(`✓ Service is running after attempt ${attempt}`);
|
|
1944
|
+
await sendHealSummary(p, pendingInfo, attempt, log);
|
|
1945
|
+
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
|
|
1946
|
+
archiveSelfHealLog(p, log);
|
|
1947
|
+
cleanupPendingFile(pendingFile, log);
|
|
1948
|
+
process.exit(0);
|
|
1949
|
+
}
|
|
1950
|
+
if (!healed) {
|
|
1951
|
+
log(`Self-heal attempt ${attempt} failed (claude invocation error)`);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
// claude 正常完成但服务没自动启动,尝试 spawn
|
|
1955
|
+
started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
|
|
1956
|
+
if (started) {
|
|
1957
|
+
log(`✓ Self-heal succeeded on attempt ${attempt}`);
|
|
1958
|
+
await sendHealSummary(p, pendingInfo, attempt, log);
|
|
1959
|
+
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
|
|
1960
|
+
archiveSelfHealLog(p, log);
|
|
1961
|
+
cleanupPendingFile(pendingFile, log);
|
|
1962
|
+
process.exit(0);
|
|
1963
|
+
}
|
|
1964
|
+
log(`Attempt ${attempt}: still failing after fix`);
|
|
1965
|
+
}
|
|
1966
|
+
// 全部失败 — 最后再检查一次
|
|
1967
|
+
if (isServiceAlive()) {
|
|
1968
|
+
log('✓ Service recovered during final check');
|
|
1969
|
+
await sendHealSummary(p, pendingInfo, MAX_HEAL_ATTEMPTS, log);
|
|
1970
|
+
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: MAX_HEAL_ATTEMPTS });
|
|
1971
|
+
archiveSelfHealLog(p, log);
|
|
1972
|
+
cleanupPendingFile(pendingFile, log);
|
|
1973
|
+
process.exit(0);
|
|
1974
|
+
}
|
|
1975
|
+
log(`❌ All ${MAX_HEAL_ATTEMPTS} self-heal attempts failed`);
|
|
1976
|
+
eventBus.publish({ type: 'self-heal:completed', success: false, attempts: MAX_HEAL_ATTEMPTS });
|
|
1977
|
+
await notifyChannel(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
|
|
1978
|
+
cleanupPendingFile(pendingFile, log);
|
|
1979
|
+
process.exit(1);
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* 发送 self-heal 修复成功小结(从 self-heal.md 提取摘要)
|
|
1983
|
+
*/
|
|
1984
|
+
async function sendHealSummary(p, pendingInfo, attempts, log) {
|
|
1985
|
+
let summary = `✅ 自动修复成功(第 ${attempts || 1} 次尝试)`;
|
|
1986
|
+
try {
|
|
1987
|
+
if (fs.existsSync(p.selfHealLog)) {
|
|
1988
|
+
const content = fs.readFileSync(p.selfHealLog, 'utf-8');
|
|
1989
|
+
// 提取最后一个 ## 章节的要点
|
|
1990
|
+
const sections = content.split(/^## /m).filter(Boolean);
|
|
1991
|
+
const last = sections[sections.length - 1];
|
|
1992
|
+
if (last) {
|
|
1993
|
+
const lines = last.split('\n').filter(l => l.startsWith('- ')).map(l => l.trim());
|
|
1994
|
+
if (lines.length > 0) {
|
|
1995
|
+
summary += '\n' + lines.join('\n');
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
catch { }
|
|
2001
|
+
summary += '\n\n⚠️ 修复前进行中的任务已中断,如需继续请重新发送。';
|
|
2002
|
+
await notifyChannel(p, pendingInfo, summary, log);
|
|
2003
|
+
}
|
|
2004
|
+
function sleep(ms) {
|
|
2005
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
2006
|
+
}
|
|
2007
|
+
function cleanupPendingFile(filePath, log) {
|
|
2008
|
+
try {
|
|
2009
|
+
if (fs.existsSync(filePath)) {
|
|
2010
|
+
fs.unlinkSync(filePath);
|
|
2011
|
+
log('Cleaned up restart-pending.json');
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
catch { }
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* 启动新进程并等待 ready.signal
|
|
2018
|
+
*/
|
|
2019
|
+
async function spawnAndWaitReady(p, log, timeout) {
|
|
2020
|
+
// 删除旧的 ready signal
|
|
2021
|
+
try {
|
|
2022
|
+
fs.unlinkSync(p.readySignal);
|
|
2023
|
+
}
|
|
2024
|
+
catch { }
|
|
2025
|
+
// 清理残留 instance 文件和进程
|
|
2026
|
+
cleanupInstances();
|
|
2027
|
+
cleanEnv();
|
|
2028
|
+
const stdoutLog = path.join(p.logs, 'stdout.log');
|
|
2029
|
+
const out = fs.openSync(stdoutLog, 'a');
|
|
2030
|
+
const err = fs.openSync(stdoutLog, 'a');
|
|
2031
|
+
const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
2032
|
+
const child = spawn('node', ['--no-warnings=ExperimentalWarning', appMain], {
|
|
2033
|
+
detached: true,
|
|
2034
|
+
stdio: ['ignore', out, err],
|
|
2035
|
+
windowsHide: true,
|
|
2036
|
+
env: {
|
|
2037
|
+
...process.env,
|
|
2038
|
+
EVOLCLAW_HOME: p.root,
|
|
2039
|
+
EVOLCLAW_LAUNCHED_BY: 'restart-monitor',
|
|
2040
|
+
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
|
2041
|
+
MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
|
|
2042
|
+
EVENT_LOG: process.env.EVENT_LOG || 'true',
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
const childPid = child.pid;
|
|
2046
|
+
child.unref();
|
|
2047
|
+
log(`Spawned new process PID: ${childPid}, waiting for ready signal...`);
|
|
2048
|
+
// 轮询等待 ready.signal 出现
|
|
2049
|
+
const start = Date.now();
|
|
2050
|
+
while (Date.now() - start < timeout) {
|
|
2051
|
+
await sleep(500);
|
|
2052
|
+
// 进程已退出则提前失败
|
|
2053
|
+
if (!platform.isProcessRunning(childPid)) {
|
|
2054
|
+
log('Process exited before ready signal');
|
|
2055
|
+
return false;
|
|
2056
|
+
}
|
|
2057
|
+
if (fs.existsSync(p.readySignal)) {
|
|
2058
|
+
log('Ready signal detected');
|
|
2059
|
+
return true;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
log(`Ready signal not received within ${timeout / 1000}s`);
|
|
2063
|
+
// 超时后杀掉进程
|
|
2064
|
+
if (platform.isProcessRunning(childPid)) {
|
|
2065
|
+
platform.killProcess(childPid);
|
|
2066
|
+
}
|
|
2067
|
+
cleanupInstances();
|
|
2068
|
+
return false;
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* 调用 claude CLI 进行自动修复
|
|
2072
|
+
*/
|
|
2073
|
+
async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
2074
|
+
const projectDir = getPackageRoot();
|
|
2075
|
+
const selfHealLog = p.selfHealLog;
|
|
2076
|
+
const stdoutLog = path.join(p.logs, 'stdout.log');
|
|
2077
|
+
const selfHealExists = fs.existsSync(selfHealLog) ? '存在,请先阅读之前的修复记录' : '不存在(首次修复)';
|
|
2078
|
+
const prompt = `EvolClaw 服务启动失败,需要你诊断并修复。这是第 ${attempt}/${maxAttempts} 次自动修复尝试。
|
|
2079
|
+
|
|
2080
|
+
关键信息:
|
|
2081
|
+
- 项目目录:${projectDir}
|
|
2082
|
+
- EVOLCLAW_HOME:${p.root}
|
|
2083
|
+
- 错误日志:${stdoutLog}
|
|
2084
|
+
- 主日志:${p.logs}/evolclaw-*.log(按小时切片,读最新的那个,包含 config 校验失败等关键错误)
|
|
2085
|
+
- 修复记录:${selfHealLog}(${selfHealExists})
|
|
2086
|
+
|
|
2087
|
+
⚠️ 重要诊断技巧:
|
|
2088
|
+
- stdout.log 可能是空的(进程秒退时 logger 输出不会到 stdout),一定要同时读 evolclaw-*.log 最新文件
|
|
2089
|
+
- 必须实际运行进程来复现错误:\`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\`,观察输出和退出码
|
|
2090
|
+
- 检查是否有旧进程仍在运行:\`ps aux | grep 'node.*dist/index.js' | grep -v grep\`,旧进程可能占用端口或锁文件
|
|
2091
|
+
- 可以运行 \`EVOLCLAW_HOME=${p.root} node dist/cli/index.js diagnose\` 快速检查配置和数据库
|
|
2092
|
+
- 如果进程无任何输出就 exit(1),说明是 process.exit(1) 被显式调用,搜索源码中所有 process.exit(1) 位置
|
|
2093
|
+
- 配置文件使用双 rename 原子写(foo.json → foo.json_ → foo.json__),崩溃时可从 foo.json_ 恢复
|
|
2094
|
+
|
|
2095
|
+
请执行以下步骤:
|
|
2096
|
+
1. 读取 ${stdoutLog} 和 ${p.logs}/evolclaw-*.log(最新文件)的最后 50 行
|
|
2097
|
+
2. 运行 \`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\` 复现错误(设置 10 秒超时)
|
|
2098
|
+
3. 如果 ${selfHealLog} 存在,先阅读之前的修复记录,避免重复尝试已失败的方案
|
|
2099
|
+
4. 根据实际复现的错误修复代码
|
|
2100
|
+
5. 执行 npm run build 确认编译通过
|
|
2101
|
+
6. 验证修复:启动服务确认 ready.signal 已写入,然后执行 \`EVOLCLAW_HOME=${p.root} node dist/cli/index.js stop\` 优雅停止(restart-monitor 会负责最终启动)
|
|
2102
|
+
7. 将本次修复内容追加到 ${selfHealLog},格式:
|
|
2103
|
+
## 第 ${attempt} 次修复 - {时间}
|
|
2104
|
+
- 错误原因:...
|
|
2105
|
+
- 修复方案:...
|
|
2106
|
+
- 修改文件:...
|
|
2107
|
+
|
|
2108
|
+
注意:只修复导致启动失败的问题,不要做额外的重构或优化。`;
|
|
2109
|
+
try {
|
|
2110
|
+
log(`Invoking claude CLI (attempt ${attempt}, timeout ${timeout / 60000}min)...`);
|
|
2111
|
+
const { stdout, stderr } = await execFileAsync('claude', [
|
|
2112
|
+
'-p', prompt,
|
|
2113
|
+
'--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
|
|
2114
|
+
'--output-format', 'text',
|
|
2115
|
+
'--no-session-persistence',
|
|
2116
|
+
], {
|
|
2117
|
+
cwd: projectDir,
|
|
2118
|
+
timeout,
|
|
2119
|
+
env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'cli' },
|
|
2120
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2121
|
+
});
|
|
2122
|
+
if (stdout)
|
|
2123
|
+
log(`Claude output: ${stdout.slice(0, 500)}`);
|
|
2124
|
+
if (stderr)
|
|
2125
|
+
log(`Claude stderr: ${stderr.slice(0, 500)}`);
|
|
2126
|
+
log(`Claude CLI completed (attempt ${attempt})`);
|
|
2127
|
+
return true;
|
|
2128
|
+
}
|
|
2129
|
+
catch (error) {
|
|
2130
|
+
if (error.killed) {
|
|
2131
|
+
log(`Claude CLI timeout after ${timeout / 60000}min (attempt ${attempt})`);
|
|
2132
|
+
}
|
|
2133
|
+
else {
|
|
2134
|
+
log(`Claude CLI error: exit code ${error.code ?? 'unknown'} (attempt ${attempt})`);
|
|
2135
|
+
}
|
|
2136
|
+
if (error.stdout)
|
|
2137
|
+
log(`Claude output: ${String(error.stdout).slice(0, 500)}`);
|
|
2138
|
+
if (error.stderr) {
|
|
2139
|
+
const stderr = String(error.stderr).replace(/Warning: no stdin.*\n?/g, '').trim();
|
|
2140
|
+
if (stderr)
|
|
2141
|
+
log(`Claude stderr: ${stderr.slice(0, 300)}`);
|
|
2142
|
+
}
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* 归档 self-heal.md
|
|
2148
|
+
*/
|
|
2149
|
+
function archiveSelfHealLog(p, log) {
|
|
2150
|
+
if (!fs.existsSync(p.selfHealLog))
|
|
2151
|
+
return;
|
|
2152
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
2153
|
+
const archivePath = path.join(p.logs, `self-heal-${timestamp}.md`);
|
|
2154
|
+
fs.renameSync(p.selfHealLog, archivePath);
|
|
2155
|
+
log(`Archived self-heal log to ${archivePath}`);
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Resolve a channel instance name to its type and config object.
|
|
2159
|
+
* Searches across all channel types (feishu, wechat, aun) for a matching instance.
|
|
2160
|
+
*/
|
|
2161
|
+
function resolveInstanceConfig(instanceName) {
|
|
2162
|
+
// 新结构:channel key 是 <aid>#<type>#<name>,解析后从对应 agent 的 channels[] 找
|
|
2163
|
+
const parts = instanceName.split('#');
|
|
2164
|
+
if (parts.length === 3) {
|
|
2165
|
+
const [aid, type, name] = parts;
|
|
2166
|
+
const { agents } = loadAllAgents();
|
|
2167
|
+
const agent = agents.find(a => a.aid === aid);
|
|
2168
|
+
if (!agent)
|
|
2169
|
+
return null;
|
|
2170
|
+
const inst = agent.channels.find((c) => c.type === type && c.name === name);
|
|
2171
|
+
if (inst)
|
|
2172
|
+
return { type, config: inst };
|
|
2173
|
+
}
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
|
|
2178
|
+
* 支持 feishu / wechat,根据 pendingInfo.channel 路由
|
|
2179
|
+
*
|
|
2180
|
+
* Phase 3 例外说明(出站消息统一计划,docs/outbound-message-unification.md):
|
|
2181
|
+
* 主网关进程出站系统通知(上线 / 重启完成 / channel:error 等)已迁到
|
|
2182
|
+
* `adapter.send(envelope, { kind: 'system.notice' | 'system.error', ... })` 统一入口。
|
|
2183
|
+
* 但 cli.ts 在 restart-monitor 子进程里跑,不持有 EvolAgent / ChannelAdapter 实例
|
|
2184
|
+
* (主网关进程已退出,新进程还没起或起不来),只能直连协议 SDK 自发。
|
|
2185
|
+
* 因此 self-heal 全流程(启动失败 / 修复中 / 修复成功 / 全部失败)和升级失败通知
|
|
2186
|
+
* 留在这里直发,**不属于** Phase 3 改造范围。
|
|
2187
|
+
*/
|
|
2188
|
+
async function notifyChannel(p, pendingInfo, message, log) {
|
|
2189
|
+
if (!pendingInfo)
|
|
2190
|
+
return;
|
|
2191
|
+
const resolved = resolveInstanceConfig(pendingInfo.channel);
|
|
2192
|
+
if (!resolved) {
|
|
2193
|
+
log(`Channel instance "${pendingInfo.channel}" not found in any agent config`);
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (resolved.type === 'feishu') {
|
|
2197
|
+
try {
|
|
2198
|
+
const inst = resolved.config;
|
|
2199
|
+
if (!inst.appId || !inst.appSecret)
|
|
2200
|
+
return;
|
|
2201
|
+
const lark = await import('@larksuiteoapi/node-sdk');
|
|
2202
|
+
const client = new lark.Client({
|
|
2203
|
+
appId: inst.appId,
|
|
2204
|
+
appSecret: inst.appSecret,
|
|
2205
|
+
});
|
|
2206
|
+
if (pendingInfo.rootId) {
|
|
2207
|
+
await client.im.message.reply({
|
|
2208
|
+
path: { message_id: pendingInfo.rootId },
|
|
2209
|
+
data: {
|
|
2210
|
+
msg_type: 'text',
|
|
2211
|
+
content: JSON.stringify({ text: message }),
|
|
2212
|
+
reply_in_thread: true,
|
|
2213
|
+
},
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
else {
|
|
2217
|
+
await client.im.message.create({
|
|
2218
|
+
params: { receive_id_type: 'chat_id' },
|
|
2219
|
+
data: {
|
|
2220
|
+
receive_id: pendingInfo.channelId,
|
|
2221
|
+
msg_type: 'text',
|
|
2222
|
+
content: JSON.stringify({ text: message }),
|
|
2223
|
+
},
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
log(`Feishu notification sent: ${message.slice(0, 50)}`);
|
|
2227
|
+
}
|
|
2228
|
+
catch (error) {
|
|
2229
|
+
log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
else if (resolved.type === 'wechat') {
|
|
2233
|
+
try {
|
|
2234
|
+
const inst = resolved.config;
|
|
2235
|
+
if (!inst.token)
|
|
2236
|
+
return;
|
|
2237
|
+
const crypto = await import('node:crypto');
|
|
2238
|
+
const baseUrl = (inst.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
|
|
2239
|
+
const token = inst.token;
|
|
2240
|
+
// 读取缓存的 context_token
|
|
2241
|
+
const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
|
|
2242
|
+
let contextToken;
|
|
2243
|
+
try {
|
|
2244
|
+
if (fs.existsSync(syncBufPath)) {
|
|
2245
|
+
const tokens = JSON.parse(fs.readFileSync(syncBufPath, 'utf-8'));
|
|
2246
|
+
contextToken = tokens[pendingInfo.channelId];
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
catch { }
|
|
2250
|
+
if (!contextToken) {
|
|
2251
|
+
log(`WeChat notification skipped: no context_token for ${pendingInfo.channelId}`);
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
2255
|
+
const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
2256
|
+
const body = JSON.stringify({
|
|
2257
|
+
msg: {
|
|
2258
|
+
from_user_id: '',
|
|
2259
|
+
to_user_id: pendingInfo.channelId,
|
|
2260
|
+
client_id: `evolclaw-restart:${Date.now()}`,
|
|
2261
|
+
message_type: 2,
|
|
2262
|
+
message_state: 2,
|
|
2263
|
+
item_list: [{ type: 1, text_item: { text: message } }],
|
|
2264
|
+
context_token: contextToken,
|
|
2265
|
+
},
|
|
2266
|
+
base_info: { channel_version: '1.0.0' },
|
|
2267
|
+
});
|
|
2268
|
+
const res = await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
|
|
2269
|
+
method: 'POST',
|
|
2270
|
+
headers: {
|
|
2271
|
+
'Content-Type': 'application/json',
|
|
2272
|
+
'AuthorizationType': 'ilink_bot_token',
|
|
2273
|
+
'Authorization': `Bearer ${token.trim()}`,
|
|
2274
|
+
'X-WECHAT-UIN': wechatUin,
|
|
2275
|
+
'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
|
|
2276
|
+
},
|
|
2277
|
+
body,
|
|
2278
|
+
});
|
|
2279
|
+
if (res.ok) {
|
|
2280
|
+
log(`WeChat notification sent: ${message.slice(0, 50)}`);
|
|
2281
|
+
}
|
|
2282
|
+
else {
|
|
2283
|
+
log(`WeChat notification failed: HTTP ${res.status}`);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
catch (error) {
|
|
2287
|
+
log(`WeChat notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
// ==================== Migrate ====================
|
|
2292
|
+
async function cmdMv(oldDir, newDir) {
|
|
2293
|
+
if (!oldDir || !newDir) {
|
|
2294
|
+
console.log('Usage: evolclaw mv <old_directory> <new_directory>');
|
|
2295
|
+
console.log('Example: evolclaw mv ~/projects/old-name ~/projects/new-name');
|
|
2296
|
+
process.exit(1);
|
|
2297
|
+
}
|
|
2298
|
+
const oldAbs = path.resolve(oldDir);
|
|
2299
|
+
const newAbs = path.resolve(newDir);
|
|
2300
|
+
console.log(`迁移项目: ${oldAbs} → ${newAbs}\n`);
|
|
2301
|
+
try {
|
|
2302
|
+
const r = await migrateProject(oldAbs, newAbs);
|
|
2303
|
+
if (r.claudeSessionsMoved)
|
|
2304
|
+
console.log('✓ Claude Code 会话目录已迁移');
|
|
2305
|
+
if (r.claudeHistoryUpdated)
|
|
2306
|
+
console.log('✓ Claude Code history.jsonl 已更新');
|
|
2307
|
+
if (r.codexUpdated > 0)
|
|
2308
|
+
console.log(`✓ Codex 数据库已更新 (${r.codexUpdated} 个会话)`);
|
|
2309
|
+
if (r.directoryMoved)
|
|
2310
|
+
console.log('✓ 项目目录已移动');
|
|
2311
|
+
if (r.evolclawDbUpdated > 0)
|
|
2312
|
+
console.log(`✓ EvolClaw 会话存储已更新 (${r.evolclawDbUpdated} 条记录)`);
|
|
2313
|
+
if (r.evolclawConfigUpdated)
|
|
2314
|
+
console.log('✓ agent config projects.list 已更新');
|
|
2315
|
+
console.log('\n迁移完成!');
|
|
2316
|
+
}
|
|
2317
|
+
catch (e) {
|
|
2318
|
+
console.error(`迁移失败: ${e instanceof Error ? e.message : e}`);
|
|
2319
|
+
process.exit(1);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
// ==================== Diagnose ====================
|
|
2323
|
+
async function cmdDiagnose() {
|
|
2324
|
+
const p = resolvePaths();
|
|
2325
|
+
let hasError = false;
|
|
2326
|
+
// 1. 检查数据目录
|
|
2327
|
+
console.log(`[diagnose] EVOLCLAW_HOME = ${p.root}`);
|
|
2328
|
+
if (!fs.existsSync(p.root)) {
|
|
2329
|
+
console.error(`[diagnose] ❌ 数据目录不存在: ${p.root}`);
|
|
2330
|
+
hasError = true;
|
|
2331
|
+
}
|
|
2332
|
+
else {
|
|
2333
|
+
console.log(`[diagnose] ✓ 数据目录存在`);
|
|
2334
|
+
}
|
|
2335
|
+
// 2. 加载并校验配置
|
|
2336
|
+
try {
|
|
2337
|
+
// 2. 加载 self-agents
|
|
2338
|
+
const { agents, skipped } = loadAllAgents();
|
|
2339
|
+
if (agents.length === 0) {
|
|
2340
|
+
console.error(`[diagnose] ❌ 未配置 self-agent。请运行 \`evolclaw aid new <name>\``);
|
|
2341
|
+
hasError = true;
|
|
2342
|
+
if (skipped.length > 0) {
|
|
2343
|
+
console.error(`[diagnose] 跳过的目录:`);
|
|
2344
|
+
for (const s of skipped)
|
|
2345
|
+
console.error(` - ${s.dirName}: ${s.reason}`);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
else {
|
|
2349
|
+
console.log(`[diagnose] ✓ 已加载 ${agents.length} 个 self-agent`);
|
|
2350
|
+
}
|
|
2351
|
+
// 3. 检查 Anthropic 配置(用首个 self-agent 的 effective config)
|
|
2352
|
+
if (agents.length > 0) {
|
|
2353
|
+
try {
|
|
2354
|
+
const defaults = loadDefaults();
|
|
2355
|
+
const merged = mergeForAgent(agents[0], defaults);
|
|
2356
|
+
const syntheticConfig = {
|
|
2357
|
+
agents: {
|
|
2358
|
+
claude: merged.baseagents?.claude,
|
|
2359
|
+
codex: merged.baseagents?.codex,
|
|
2360
|
+
gemini: merged.baseagents?.gemini,
|
|
2361
|
+
},
|
|
2362
|
+
channels: {},
|
|
2363
|
+
projects: merged.projects,
|
|
2364
|
+
};
|
|
2365
|
+
const anthropic = resolveAnthropicConfig(syntheticConfig);
|
|
2366
|
+
console.log(`[diagnose] ✓ Anthropic 配置解析成功 (apiKey: ${anthropic.apiKey ? '已设置' : '❌ 未设置'}, model: ${anthropic.model || 'default'})`);
|
|
2367
|
+
}
|
|
2368
|
+
catch (e) {
|
|
2369
|
+
console.error(`[diagnose] ❌ Anthropic 配置解析失败: ${e instanceof Error ? e.message : e}`);
|
|
2370
|
+
hasError = true;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
catch (e) {
|
|
2375
|
+
console.error(`[diagnose] ❌ 配置加载失败: ${e instanceof Error ? e.message : e}`);
|
|
2376
|
+
hasError = true;
|
|
2377
|
+
}
|
|
2378
|
+
// 4. 检查 Session 文件系统存储
|
|
2379
|
+
try {
|
|
2380
|
+
const { SessionManager } = await import('../core/session/session-manager.js');
|
|
2381
|
+
const eventBus = new EventBus();
|
|
2382
|
+
new SessionManager(p.sessionsDir, eventBus);
|
|
2383
|
+
console.log(`[diagnose] ✓ Session 存储初始化成功: ${p.sessionsDir}`);
|
|
2384
|
+
}
|
|
2385
|
+
catch (e) {
|
|
2386
|
+
console.error(`[diagnose] ❌ Session 存储初始化失败: ${e instanceof Error ? e.message : e}`);
|
|
2387
|
+
hasError = true;
|
|
2388
|
+
}
|
|
2389
|
+
// 5. 检查残留进程
|
|
2390
|
+
try {
|
|
2391
|
+
const instStatus = scanInstances();
|
|
2392
|
+
const aliveMains = instStatus.mains.filter(m => m.alive);
|
|
2393
|
+
if (aliveMains.length > 0) {
|
|
2394
|
+
console.log(`[diagnose] ⚠️ 已有进程运行中: PID ${aliveMains.map(m => m.record.pid).join(', ')}`);
|
|
2395
|
+
}
|
|
2396
|
+
else {
|
|
2397
|
+
console.log(`[diagnose] ✓ 无残留进程`);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
catch {
|
|
2401
|
+
console.log(`[diagnose] ✓ 无 instance 文件`);
|
|
2402
|
+
}
|
|
2403
|
+
// 6. 检查关键文件
|
|
2404
|
+
const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
2405
|
+
if (!fs.existsSync(appMain)) {
|
|
2406
|
+
console.error(`[diagnose] ❌ 编译产物不存在: ${appMain}`);
|
|
2407
|
+
hasError = true;
|
|
2408
|
+
}
|
|
2409
|
+
else {
|
|
2410
|
+
console.log(`[diagnose] ✓ 编译产物存在: ${appMain}`);
|
|
2411
|
+
}
|
|
2412
|
+
if (hasError) {
|
|
2413
|
+
console.error('\n[diagnose] ❌ 诊断发现问题,请修复后重试');
|
|
2414
|
+
process.exit(1);
|
|
2415
|
+
}
|
|
2416
|
+
else {
|
|
2417
|
+
console.log('\n[diagnose] ✓ 所有检查通过');
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
// ==================== Ctl ====================
|
|
2421
|
+
async function cmdCtl(args) {
|
|
2422
|
+
if (args.length === 0) {
|
|
2423
|
+
console.error(`用法: evolclaw ctl <command> [args...]
|
|
2424
|
+
|
|
2425
|
+
查询:
|
|
2426
|
+
status 查看会话状态
|
|
2427
|
+
check 检查渠道健康状态
|
|
2428
|
+
help 显示帮助
|
|
2429
|
+
|
|
2430
|
+
配置:
|
|
2431
|
+
model [model-id] 查看/切换模型(如 opus, sonnet, haiku)
|
|
2432
|
+
effort [low|medium|high] 查看/切换推理强度
|
|
2433
|
+
compact 压缩当前会话上下文
|
|
2434
|
+
perm [mode] 查看/切换权限模式
|
|
2435
|
+
|
|
2436
|
+
项目:
|
|
2437
|
+
bind <path> 注册项目目录(不切换当前会话)
|
|
2438
|
+
|
|
2439
|
+
消息:
|
|
2440
|
+
send <消息内容> 主动发送文本消息(proactive 模式)
|
|
2441
|
+
file [channel] <path> 发送项目内文件
|
|
2442
|
+
|
|
2443
|
+
运维:
|
|
2444
|
+
agentmd [put|set <内容>] 查看/管理 agent.md(仅 AUN 通道)
|
|
2445
|
+
restart [channel] 重启服务或重连指定渠道
|
|
2446
|
+
|
|
2447
|
+
示例:
|
|
2448
|
+
evolclaw ctl model sonnet
|
|
2449
|
+
evolclaw ctl effort high
|
|
2450
|
+
evolclaw ctl compact`);
|
|
2451
|
+
process.exit(1);
|
|
2452
|
+
}
|
|
2453
|
+
// help 不需要连接服务,直接复用无参数时的帮助输出
|
|
2454
|
+
if (args[0] === 'help') {
|
|
2455
|
+
return cmdCtl([]);
|
|
2456
|
+
}
|
|
2457
|
+
const sessionId = process.env.EVOLCLAW_SESSION_ID;
|
|
2458
|
+
if (!sessionId) {
|
|
2459
|
+
console.error('错误: EVOLCLAW_SESSION_ID 未设置(仅在 evolclaw 托管环境中可用)');
|
|
2460
|
+
process.exit(1);
|
|
2461
|
+
}
|
|
2462
|
+
const cmd = '/' + args.join(' ');
|
|
2463
|
+
const socketPath = resolvePaths().socket;
|
|
2464
|
+
// compact/restart 等长时操作使用更长超时
|
|
2465
|
+
const longRunning = ['/compact', '/restart'];
|
|
2466
|
+
const timeout = longRunning.some(c => cmd.startsWith(c)) ? 60_000 : 10_000;
|
|
2467
|
+
const result = await ipcQuery(socketPath, {
|
|
2468
|
+
type: 'ctl',
|
|
2469
|
+
cmd,
|
|
2470
|
+
sessionId,
|
|
2471
|
+
}, timeout);
|
|
2472
|
+
if (!result) {
|
|
2473
|
+
console.error('错误: 无法连接 evolclaw 服务');
|
|
2474
|
+
process.exit(1);
|
|
2475
|
+
}
|
|
2476
|
+
const ctlResult = result;
|
|
2477
|
+
if (ctlResult.ok) {
|
|
2478
|
+
console.log(ctlResult.result);
|
|
2479
|
+
}
|
|
2480
|
+
else {
|
|
2481
|
+
console.error(ctlResult.error || '执行失败');
|
|
2482
|
+
process.exit(1);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
// ==================== Agent ====================
|
|
2486
|
+
async function cmdAgent(args) {
|
|
2487
|
+
const sub = args[0];
|
|
2488
|
+
const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
|
|
2489
|
+
if (sub === 'help') {
|
|
2490
|
+
console.log(`用法: evolclaw agent <command>
|
|
2491
|
+
|
|
2492
|
+
Commands:
|
|
2493
|
+
list 列出所有 agent
|
|
2494
|
+
show <aid> 查看 agent 详情(身份 + 配置 + 连接 + 会话 + 路径)
|
|
2495
|
+
new [aid] 交互式创建 agent
|
|
2496
|
+
new <aid> --non-interactive ... 非交互式创建
|
|
2497
|
+
sync-aids 从本地 AID 批量创建 agent
|
|
2498
|
+
enable <aid> 启用 agent
|
|
2499
|
+
disable <aid> 停用 agent
|
|
2500
|
+
get <aid> <key> 读取单个配置字段(支持点路径)
|
|
2501
|
+
set <aid> <key> <val> 修改单个配置字段(支持点路径)
|
|
2502
|
+
rename <aid> <name> 修改 agent 名称(更新 agent.md 并重新上传)
|
|
2503
|
+
reload [aid] 热重载配置(无参数=全量 resync)
|
|
2504
|
+
delete <aid> [--purge] 删除 agent
|
|
2505
|
+
|
|
2506
|
+
Options:
|
|
2507
|
+
--format json 输出 JSON 格式
|
|
2508
|
+
|
|
2509
|
+
示例:
|
|
2510
|
+
evolclaw agent list
|
|
2511
|
+
evolclaw agent show mybot.agentid.pub
|
|
2512
|
+
evolclaw agent new mybot.agentid.pub
|
|
2513
|
+
evolclaw agent enable mybot.agentid.pub
|
|
2514
|
+
evolclaw agent get mybot.agentid.pub active_baseagent
|
|
2515
|
+
evolclaw agent set mybot.agentid.pub active_baseagent codex
|
|
2516
|
+
evolclaw agent rename mybot.agentid.pub "My Bot"
|
|
2517
|
+
evolclaw agent delete mybot.agentid.pub --purge`);
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
const { agentList, agentShow, agentCreateInteractive, agentCreateNonInteractive, agentSyncAids, agentReload, agentEnable, agentDisable, agentGet, agentSet, agentDelete, agentRename, } = await import('./agent.js');
|
|
2521
|
+
// --- list ---
|
|
2522
|
+
if (!sub || sub === 'list') {
|
|
2523
|
+
const result = await agentList();
|
|
2524
|
+
if (!result.ok) {
|
|
2525
|
+
if (formatJson) {
|
|
2526
|
+
console.log(JSON.stringify(result));
|
|
2527
|
+
}
|
|
2528
|
+
else {
|
|
2529
|
+
console.error(`❌ ${result.error}`);
|
|
2530
|
+
}
|
|
2531
|
+
process.exit(1);
|
|
2532
|
+
}
|
|
2533
|
+
if (formatJson) {
|
|
2534
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
if (result.agents.length === 0) {
|
|
2538
|
+
console.log('No agents configured.');
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
console.log('NAME'.padEnd(14) + 'STATUS'.padEnd(10) + 'CHANNELS'.padEnd(24) +
|
|
2542
|
+
'PROJECT'.padEnd(22) + 'BASEAGENT'.padEnd(11) + 'LAST ACTIVE');
|
|
2543
|
+
for (const info of result.agents) {
|
|
2544
|
+
const name = info.name;
|
|
2545
|
+
const status = info.status || 'stopped';
|
|
2546
|
+
const channels = info.channels?.length > 0 ? info.channels.join(', ').slice(0, 22) : '—';
|
|
2547
|
+
const project = info.projectPath ? path.basename(info.projectPath) : '—';
|
|
2548
|
+
const baseagent = info.baseagent || '—';
|
|
2549
|
+
const lastActive = info.lastActivity ? formatTimeAgo(Date.now() - info.lastActivity) : '—';
|
|
2550
|
+
console.log(name.padEnd(14) + status.padEnd(10) + channels.padEnd(24) +
|
|
2551
|
+
project.padEnd(22) + baseagent.padEnd(11) + lastActive);
|
|
2552
|
+
}
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
// --- new ---
|
|
2556
|
+
if (sub === 'new') {
|
|
2557
|
+
const name = args[1];
|
|
2558
|
+
const nonInteractive = args.includes('--non-interactive');
|
|
2559
|
+
if (nonInteractive) {
|
|
2560
|
+
if (!name) {
|
|
2561
|
+
console.error('Usage: evolclaw agent new <aid> --non-interactive ...');
|
|
2562
|
+
process.exit(1);
|
|
2563
|
+
}
|
|
2564
|
+
const getArg = (flag) => {
|
|
2565
|
+
const idx = args.indexOf(flag);
|
|
2566
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
2567
|
+
};
|
|
2568
|
+
const result = await agentCreateNonInteractive({
|
|
2569
|
+
aid: name,
|
|
2570
|
+
baseagent: getArg('--baseagent'),
|
|
2571
|
+
project: getArg('--project') || '',
|
|
2572
|
+
owner: getArg('--owner'),
|
|
2573
|
+
name: getArg('--name'),
|
|
2574
|
+
description: getArg('--description'),
|
|
2575
|
+
force: args.includes('--force'),
|
|
2576
|
+
});
|
|
2577
|
+
if (!result.ok) {
|
|
2578
|
+
if (formatJson) {
|
|
2579
|
+
console.log(JSON.stringify(result));
|
|
2580
|
+
}
|
|
2581
|
+
else {
|
|
2582
|
+
console.error(`❌ ${result.error}`);
|
|
2583
|
+
}
|
|
2584
|
+
process.exit(1);
|
|
2585
|
+
}
|
|
2586
|
+
if (formatJson) {
|
|
2587
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2588
|
+
}
|
|
2589
|
+
else {
|
|
2590
|
+
console.log(`✓ Created: ${result.configPath}`);
|
|
2591
|
+
console.log(result.agentmdUploaded
|
|
2592
|
+
? ' ✓ agent.md 已发布'
|
|
2593
|
+
: ' ⚠ agent.md 上传失败(可用 evolclaw aid agentmd put 重试)');
|
|
2594
|
+
console.log(' Run `evolclaw restart` to activate.');
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
else {
|
|
2598
|
+
const result = await agentCreateInteractive({ suggestedName: name });
|
|
2599
|
+
if (!result.ok) {
|
|
2600
|
+
if (formatJson) {
|
|
2601
|
+
console.log(JSON.stringify(result));
|
|
2602
|
+
}
|
|
2603
|
+
else {
|
|
2604
|
+
console.error(`❌ ${result.error}`);
|
|
2605
|
+
}
|
|
2606
|
+
process.exit(1);
|
|
2607
|
+
}
|
|
2608
|
+
if (formatJson) {
|
|
2609
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2610
|
+
}
|
|
2611
|
+
else {
|
|
2612
|
+
console.log(`\n✓ Created: ${result.configPath}`);
|
|
2613
|
+
console.log(result.agentmdUploaded
|
|
2614
|
+
? ' ✓ agent.md 已发布'
|
|
2615
|
+
: ' ⚠ agent.md 上传失败(可用 evolclaw aid agentmd put 重试)');
|
|
2616
|
+
console.log(' Run `evolclaw restart` to activate.');
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
// --- sync-aids ---
|
|
2622
|
+
if (sub === 'sync-aids') {
|
|
2623
|
+
const result = await agentSyncAids();
|
|
2624
|
+
if (!result.ok) {
|
|
2625
|
+
if (formatJson) {
|
|
2626
|
+
console.log(JSON.stringify(result));
|
|
2627
|
+
}
|
|
2628
|
+
else {
|
|
2629
|
+
console.error(`❌ ${result.error}`);
|
|
2630
|
+
}
|
|
2631
|
+
process.exit(1);
|
|
2632
|
+
}
|
|
2633
|
+
if (formatJson) {
|
|
2634
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
if (result.created.length === 0) {
|
|
2638
|
+
console.log('所有本地 AID 都已有对应 agent,无需同步。');
|
|
2639
|
+
}
|
|
2640
|
+
else {
|
|
2641
|
+
console.log(`✓ 同步完成:新建 ${result.created.length} 个 agent(模板: ${result.template})`);
|
|
2642
|
+
for (const aid of result.created)
|
|
2643
|
+
console.log(` ✓ ${aid}`);
|
|
2644
|
+
if (result.hotReloaded)
|
|
2645
|
+
console.log(' ✓ 已热加载到运行中的进程');
|
|
2646
|
+
else
|
|
2647
|
+
console.log(' evolclaw 未运行,新 agent 将在下次启动时加载。');
|
|
2648
|
+
}
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
// --- reload ---
|
|
2652
|
+
if (sub === 'reload') {
|
|
2653
|
+
const target = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
2654
|
+
const result = await agentReload(target);
|
|
2655
|
+
if (!result.ok) {
|
|
2656
|
+
if (formatJson) {
|
|
2657
|
+
console.log(JSON.stringify(result));
|
|
2658
|
+
}
|
|
2659
|
+
else {
|
|
2660
|
+
console.error(`✗ ${result.error}`);
|
|
2661
|
+
}
|
|
2662
|
+
process.exit(1);
|
|
2663
|
+
}
|
|
2664
|
+
if (formatJson) {
|
|
2665
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
if (target) {
|
|
2669
|
+
console.log(`✓ Agent "${target}" reloaded`);
|
|
2670
|
+
}
|
|
2671
|
+
else {
|
|
2672
|
+
console.log('✓ Agent resync 完成:');
|
|
2673
|
+
for (const line of (result.results || []))
|
|
2674
|
+
console.log(` ${line}`);
|
|
2675
|
+
}
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
// --- enable ---
|
|
2679
|
+
if (sub === 'enable') {
|
|
2680
|
+
const aid = args[1];
|
|
2681
|
+
if (!aid) {
|
|
2682
|
+
console.error('用法: evolclaw agent enable <aid>');
|
|
2683
|
+
process.exit(1);
|
|
2684
|
+
}
|
|
2685
|
+
const result = await agentEnable(aid);
|
|
2686
|
+
if (!result.ok) {
|
|
2687
|
+
if (formatJson) {
|
|
2688
|
+
console.log(JSON.stringify(result));
|
|
2689
|
+
}
|
|
2690
|
+
else {
|
|
2691
|
+
console.error(`❌ ${result.error}`);
|
|
2692
|
+
}
|
|
2693
|
+
process.exit(1);
|
|
2694
|
+
}
|
|
2695
|
+
if (formatJson) {
|
|
2696
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2697
|
+
}
|
|
2698
|
+
else {
|
|
2699
|
+
console.log(`✓ ${aid} enabled${result.reloaded ? ' (hot-reloaded)' : ''}`);
|
|
2700
|
+
}
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
// --- disable ---
|
|
2704
|
+
if (sub === 'disable') {
|
|
2705
|
+
const aid = args[1];
|
|
2706
|
+
if (!aid) {
|
|
2707
|
+
console.error('用法: evolclaw agent disable <aid>');
|
|
2708
|
+
process.exit(1);
|
|
2709
|
+
}
|
|
2710
|
+
const result = await agentDisable(aid);
|
|
2711
|
+
if (!result.ok) {
|
|
2712
|
+
if (formatJson) {
|
|
2713
|
+
console.log(JSON.stringify(result));
|
|
2714
|
+
}
|
|
2715
|
+
else {
|
|
2716
|
+
console.error(`❌ ${result.error}`);
|
|
2717
|
+
}
|
|
2718
|
+
process.exit(1);
|
|
2719
|
+
}
|
|
2720
|
+
if (formatJson) {
|
|
2721
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2722
|
+
}
|
|
2723
|
+
else {
|
|
2724
|
+
console.log(`✓ ${aid} disabled${result.reloaded ? ' (hot-reloaded)' : ''}`);
|
|
2725
|
+
}
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
// --- get ---
|
|
2729
|
+
if (sub === 'get') {
|
|
2730
|
+
const aid = args[1];
|
|
2731
|
+
const key = args[2];
|
|
2732
|
+
if (!aid || !key) {
|
|
2733
|
+
console.error('用法: evolclaw agent get <aid> <key>');
|
|
2734
|
+
process.exit(1);
|
|
2735
|
+
}
|
|
2736
|
+
const result = await agentGet(aid, key);
|
|
2737
|
+
if (!result.ok) {
|
|
2738
|
+
if (formatJson) {
|
|
2739
|
+
console.log(JSON.stringify(result));
|
|
2740
|
+
}
|
|
2741
|
+
else {
|
|
2742
|
+
console.error(`❌ ${result.error}`);
|
|
2743
|
+
}
|
|
2744
|
+
process.exit(1);
|
|
2745
|
+
}
|
|
2746
|
+
if (formatJson) {
|
|
2747
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2748
|
+
}
|
|
2749
|
+
else {
|
|
2750
|
+
const val = result.value;
|
|
2751
|
+
console.log(typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val));
|
|
2752
|
+
}
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
// --- set ---
|
|
2756
|
+
if (sub === 'set') {
|
|
2757
|
+
const aid = args[1];
|
|
2758
|
+
const key = args[2];
|
|
2759
|
+
const val = args[3];
|
|
2760
|
+
if (!aid || !key || val === undefined) {
|
|
2761
|
+
console.error('用法: evolclaw agent set <aid> <key> <value>');
|
|
2762
|
+
process.exit(1);
|
|
2763
|
+
}
|
|
2764
|
+
const result = await agentSet(aid, key, val);
|
|
2765
|
+
if (!result.ok) {
|
|
2766
|
+
if (formatJson) {
|
|
2767
|
+
console.log(JSON.stringify(result));
|
|
2768
|
+
}
|
|
2769
|
+
else {
|
|
2770
|
+
console.error(`❌ ${result.error}`);
|
|
2771
|
+
}
|
|
2772
|
+
process.exit(1);
|
|
2773
|
+
}
|
|
2774
|
+
if (formatJson) {
|
|
2775
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2776
|
+
}
|
|
2777
|
+
else {
|
|
2778
|
+
console.log(`✓ ${aid} ${key} = ${JSON.stringify(result.value)}${result.reloaded ? ' (hot-reloaded)' : ''}`);
|
|
2779
|
+
}
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
// --- rename ---
|
|
2783
|
+
if (sub === 'rename') {
|
|
2784
|
+
const aid = args[1];
|
|
2785
|
+
const newName = args[2];
|
|
2786
|
+
if (!aid || !newName) {
|
|
2787
|
+
console.error('用法: evolclaw agent rename <aid> <name>');
|
|
2788
|
+
process.exit(1);
|
|
2789
|
+
}
|
|
2790
|
+
const result = await agentRename(aid, newName);
|
|
2791
|
+
if (!result.ok) {
|
|
2792
|
+
if (formatJson) {
|
|
2793
|
+
console.log(JSON.stringify(result));
|
|
2794
|
+
}
|
|
2795
|
+
else {
|
|
2796
|
+
console.error(`❌ ${result.error}`);
|
|
2797
|
+
}
|
|
2798
|
+
process.exit(1);
|
|
2799
|
+
}
|
|
2800
|
+
if (formatJson) {
|
|
2801
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2802
|
+
}
|
|
2803
|
+
else {
|
|
2804
|
+
console.log(`✓ ${aid} renamed to "${newName}"${result.uploaded ? ' (uploaded)' : ' (local only, upload failed)'}`);
|
|
2805
|
+
}
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
// --- delete ---
|
|
2809
|
+
if (sub === 'delete') {
|
|
2810
|
+
const aid = args[1];
|
|
2811
|
+
if (!aid) {
|
|
2812
|
+
console.error('用法: evolclaw agent delete <aid> [--purge]');
|
|
2813
|
+
process.exit(1);
|
|
2814
|
+
}
|
|
2815
|
+
const purge = args.includes('--purge');
|
|
2816
|
+
const result = await agentDelete(aid, purge);
|
|
2817
|
+
if (!result.ok) {
|
|
2818
|
+
if (formatJson) {
|
|
2819
|
+
console.log(JSON.stringify(result));
|
|
2820
|
+
}
|
|
2821
|
+
else {
|
|
2822
|
+
console.error(`❌ ${result.error}`);
|
|
2823
|
+
}
|
|
2824
|
+
process.exit(1);
|
|
2825
|
+
}
|
|
2826
|
+
if (formatJson) {
|
|
2827
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2828
|
+
}
|
|
2829
|
+
else {
|
|
2830
|
+
console.log(`✓ ${aid} deleted${purge ? ' (purged)' : ''}`);
|
|
2831
|
+
}
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
// --- show ---
|
|
2835
|
+
if (sub === 'show') {
|
|
2836
|
+
const aid = args[1];
|
|
2837
|
+
if (!aid) {
|
|
2838
|
+
console.error('用法: evolclaw agent show <aid>');
|
|
2839
|
+
process.exit(1);
|
|
2840
|
+
}
|
|
2841
|
+
const result = await agentShow(aid);
|
|
2842
|
+
if (!result.ok) {
|
|
2843
|
+
if (formatJson) {
|
|
2844
|
+
console.log(JSON.stringify(result));
|
|
2845
|
+
}
|
|
2846
|
+
else {
|
|
2847
|
+
console.error(result.error);
|
|
2848
|
+
}
|
|
2849
|
+
process.exit(1);
|
|
2850
|
+
}
|
|
2851
|
+
if (formatJson) {
|
|
2852
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
printAgentShowHuman(result);
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
// --- default: `evolclaw agent <aid>` (shorthand for show) ---
|
|
2859
|
+
const result = await agentShow(sub);
|
|
2860
|
+
if (!result.ok) {
|
|
2861
|
+
if (formatJson) {
|
|
2862
|
+
console.log(JSON.stringify(result));
|
|
2863
|
+
}
|
|
2864
|
+
else {
|
|
2865
|
+
console.error(result.error);
|
|
2866
|
+
}
|
|
2867
|
+
process.exit(1);
|
|
2868
|
+
}
|
|
2869
|
+
if (formatJson) {
|
|
2870
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
printAgentShowHuman(result);
|
|
2874
|
+
}
|
|
2875
|
+
function printAgentShowHuman(result) {
|
|
2876
|
+
console.log(`${result.aid} (${result.status})\n`);
|
|
2877
|
+
if (result.identity.name || result.identity.description) {
|
|
2878
|
+
console.log(' Identity');
|
|
2879
|
+
if (result.identity.name)
|
|
2880
|
+
console.log(` Name: ${result.identity.name}`);
|
|
2881
|
+
if (result.identity.description)
|
|
2882
|
+
console.log(` Description: ${result.identity.description}`);
|
|
2883
|
+
console.log('');
|
|
2884
|
+
}
|
|
2885
|
+
console.log(' Config');
|
|
2886
|
+
console.log(` Baseagent: ${result.config.baseagent || '—'}`);
|
|
2887
|
+
if (result.config.model)
|
|
2888
|
+
console.log(` Model: ${result.config.model}`);
|
|
2889
|
+
if (result.config.effort)
|
|
2890
|
+
console.log(` Effort: ${result.config.effort}`);
|
|
2891
|
+
if (result.config.chatmode)
|
|
2892
|
+
console.log(` Chatmode: private=${result.config.chatmode.private} group=${result.config.chatmode.group}`);
|
|
2893
|
+
if (result.config.owners.length)
|
|
2894
|
+
console.log(` Owners: ${result.config.owners.join(', ')}`);
|
|
2895
|
+
console.log(` Channels: ${result.config.channels.length > 0 ? result.config.channels.join(', ') : '—'}`);
|
|
2896
|
+
console.log('');
|
|
2897
|
+
if (result.connection) {
|
|
2898
|
+
const c = result.connection;
|
|
2899
|
+
console.log(' Connection');
|
|
2900
|
+
console.log(` Status: ${c.status}`);
|
|
2901
|
+
console.log(` Uptime: ${c.uptime_ms != null ? formatDurationMs(c.uptime_ms) : '—'}`);
|
|
2902
|
+
console.log(` Reconnects: ${c.reconnect_count}`);
|
|
2903
|
+
console.log(` Msgs recv: ${c.messages_received}`);
|
|
2904
|
+
console.log(` Msgs sent: ${c.messages_sent}`);
|
|
2905
|
+
console.log(` Bytes in: ${formatBytes(c.bytes_received)}`);
|
|
2906
|
+
console.log(` Bytes out: ${formatBytes(c.bytes_sent)}`);
|
|
2907
|
+
console.log(` Last recv: ${c.last_received_at ? formatTimeAgo(Date.now() - new Date(c.last_received_at).getTime()) : '—'}`);
|
|
2908
|
+
console.log(` Last sent: ${c.last_sent_at ? formatTimeAgo(Date.now() - new Date(c.last_sent_at).getTime()) : '—'}`);
|
|
2909
|
+
console.log(` Peers: ${c.unique_peer_count}`);
|
|
2910
|
+
console.log('');
|
|
2911
|
+
}
|
|
2912
|
+
else {
|
|
2913
|
+
console.log(' Connection (daemon offline)');
|
|
2914
|
+
console.log('');
|
|
2915
|
+
}
|
|
2916
|
+
console.log(' Sessions');
|
|
2917
|
+
console.log(` Active: ${result.sessions.active}`);
|
|
2918
|
+
console.log(` Last active: ${result.sessions.last_activity ? formatTimeAgo(Date.now() - new Date(result.sessions.last_activity).getTime()) : '—'}`);
|
|
2919
|
+
console.log('');
|
|
2920
|
+
console.log(' Paths');
|
|
2921
|
+
console.log(` Config: ${result.paths.config}`);
|
|
2922
|
+
console.log(` Agent.md: ${result.paths.agent_md}`);
|
|
2923
|
+
console.log(` Project: ${result.paths.project || '—'}`);
|
|
2924
|
+
console.log(` Data: ${result.paths.data}`);
|
|
2925
|
+
}
|
|
2926
|
+
function formatDurationMs(ms) {
|
|
2927
|
+
const sec = Math.floor(ms / 1000);
|
|
2928
|
+
if (sec < 60)
|
|
2929
|
+
return `${sec}s`;
|
|
2930
|
+
const min = Math.floor(sec / 60);
|
|
2931
|
+
const s = sec % 60;
|
|
2932
|
+
if (min < 60)
|
|
2933
|
+
return `${min}m${String(s).padStart(2, '0')}s`;
|
|
2934
|
+
const hour = Math.floor(min / 60);
|
|
2935
|
+
const m = min % 60;
|
|
2936
|
+
if (hour < 24)
|
|
2937
|
+
return `${hour}h${String(m).padStart(2, '0')}m${String(s).padStart(2, '0')}s`;
|
|
2938
|
+
const day = Math.floor(hour / 24);
|
|
2939
|
+
return `${day}d${hour % 24}h${String(m).padStart(2, '0')}m`;
|
|
2940
|
+
}
|
|
2941
|
+
// ==================== AID ====================
|
|
2942
|
+
function resolveAunPath(args) {
|
|
2943
|
+
const idx = args.indexOf('--aun-path');
|
|
2944
|
+
if (idx !== -1 && idx + 1 < args.length)
|
|
2945
|
+
return args[idx + 1];
|
|
2946
|
+
return process.env.AUN_HOME || undefined;
|
|
2947
|
+
}
|
|
2948
|
+
async function cmdAid(args) {
|
|
2949
|
+
const sub = args[0] || 'list';
|
|
2950
|
+
const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
|
|
2951
|
+
const aunPath = resolveAunPath(args);
|
|
2952
|
+
if (sub === 'help') {
|
|
2953
|
+
console.log(`用法: evolclaw aid <command>
|
|
2954
|
+
|
|
2955
|
+
Commands:
|
|
2956
|
+
list 列出本地所有 AID
|
|
2957
|
+
show <aid> 查看本地 AID 详情(证书有效期、私钥状态)
|
|
2958
|
+
new <aid> 创建新 AID 身份
|
|
2959
|
+
delete <aid> 删除本地 AID(无网络注销)
|
|
2960
|
+
lookup <aid> 远程探测 AID(是否存在 + 网关 + agent.md)
|
|
2961
|
+
agentmd put <aid> 读本地 agent.md → 签名 → 上传
|
|
2962
|
+
agentmd get <aid> 下载 agent.md → 验签 → 本地持久化
|
|
2963
|
+
|
|
2964
|
+
Options:
|
|
2965
|
+
--format json 输出 JSON 格式
|
|
2966
|
+
|
|
2967
|
+
示例:
|
|
2968
|
+
evolclaw aid list
|
|
2969
|
+
evolclaw aid show toleiliang2.agentid.pub
|
|
2970
|
+
evolclaw aid new reviewer.agentid.pub
|
|
2971
|
+
evolclaw aid delete old.agentid.pub
|
|
2972
|
+
evolclaw aid lookup someone.agentid.pub
|
|
2973
|
+
evolclaw aid agentmd put mybot.agentid.pub
|
|
2974
|
+
evolclaw aid agentmd get someone.agentid.pub`);
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
const { aidList, aidCreate, aidShow, aidDelete, aidLookup, agentmdPut, agentmdGet, buildInitialAgentMd, isValidAid } = await import('../aun/aid/index.js');
|
|
2978
|
+
if (sub === 'list') {
|
|
2979
|
+
const aids = aidList(aunPath);
|
|
2980
|
+
if (formatJson) {
|
|
2981
|
+
console.log(JSON.stringify(aids, null, 2));
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
if (aids.length === 0) {
|
|
2985
|
+
console.log('本地无 AID');
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
console.log('本地 AID:');
|
|
2989
|
+
for (const a of aids) {
|
|
2990
|
+
const icons = [
|
|
2991
|
+
a.hasPrivateKey ? '🔑' : ' ',
|
|
2992
|
+
a.hasAgentMd ? '📄' : ' ',
|
|
2993
|
+
].join('');
|
|
2994
|
+
console.log(` ${icons} ${a.aid}`);
|
|
2995
|
+
}
|
|
2996
|
+
console.log('\n🔑=私钥 📄=agent.md');
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
if (sub === 'show') {
|
|
3000
|
+
const aid = args[1];
|
|
3001
|
+
if (!aid) {
|
|
3002
|
+
console.error('用法: evolclaw aid show <aid>');
|
|
3003
|
+
process.exit(1);
|
|
3004
|
+
}
|
|
3005
|
+
const info = aidShow(aid, { aunPath });
|
|
3006
|
+
if (formatJson) {
|
|
3007
|
+
console.log(JSON.stringify(info, null, 2));
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
console.log(`AID: ${info.aid}`);
|
|
3011
|
+
console.log(` 私钥: ${info.hasPrivateKey ? '有' : '无'}`);
|
|
3012
|
+
console.log(` agent.md: ${info.hasAgentMd ? '有' : '无'}`);
|
|
3013
|
+
console.log(` 证书到期: ${info.certExpiresAt ?? '无证书'}`);
|
|
3014
|
+
if (info.certSubject)
|
|
3015
|
+
console.log(` 证书主体: ${info.certSubject}`);
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
if (sub === 'new') {
|
|
3019
|
+
const aid = args[1];
|
|
3020
|
+
if (!aid) {
|
|
3021
|
+
console.error('用法: evolclaw aid new <完整AID>\n例: evolclaw aid new reviewer.agentid.pub');
|
|
3022
|
+
process.exit(1);
|
|
3023
|
+
}
|
|
3024
|
+
if (!isValidAid(aid)) {
|
|
3025
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
3026
|
+
process.exit(1);
|
|
3027
|
+
}
|
|
3028
|
+
const result = await aidCreate(aid, { aunPath });
|
|
3029
|
+
if (!result.alreadyExisted) {
|
|
3030
|
+
const content = buildInitialAgentMd({ aid });
|
|
3031
|
+
try {
|
|
3032
|
+
await agentmdPut(content, { aid, client: result.client, aunPath });
|
|
3033
|
+
console.log('✓ agent.md 已发布');
|
|
3034
|
+
}
|
|
3035
|
+
catch (e) {
|
|
3036
|
+
console.warn(`⚠ agent.md 发布失败(首次连接将自动重试): ${String(e.message || e).slice(0, 100)}`);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
try {
|
|
3040
|
+
await result.client.close();
|
|
3041
|
+
}
|
|
3042
|
+
catch { }
|
|
3043
|
+
const verb = result.alreadyExisted ? '已存在' : '已创建';
|
|
3044
|
+
console.log(`✓ ${aid} ${verb}`);
|
|
3045
|
+
console.log(' 如需上线 AUN 通道,运行 evolclaw agent new ' + aid);
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
if (sub === 'delete') {
|
|
3049
|
+
const aid = args[1];
|
|
3050
|
+
if (!aid) {
|
|
3051
|
+
console.error('用法: evolclaw aid delete <aid>');
|
|
3052
|
+
process.exit(1);
|
|
3053
|
+
}
|
|
3054
|
+
const deleted = aidDelete(aid, { aunPath });
|
|
3055
|
+
if (deleted) {
|
|
3056
|
+
console.log(`✓ ${aid} 已删除`);
|
|
3057
|
+
}
|
|
3058
|
+
else {
|
|
3059
|
+
console.error(`❌ 本地不存在: ${aid}`);
|
|
3060
|
+
process.exit(1);
|
|
3061
|
+
}
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
if (sub === 'lookup') {
|
|
3065
|
+
const aid = args[1];
|
|
3066
|
+
if (!aid) {
|
|
3067
|
+
console.error('用法: evolclaw aid lookup <aid>');
|
|
3068
|
+
process.exit(1);
|
|
3069
|
+
}
|
|
3070
|
+
if (!isValidAid(aid)) {
|
|
3071
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
3072
|
+
process.exit(1);
|
|
3073
|
+
}
|
|
3074
|
+
const result = await aidLookup(aid);
|
|
3075
|
+
if (formatJson) {
|
|
3076
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
if (result.exists) {
|
|
3080
|
+
console.log(`✓ ${aid} 已注册`);
|
|
3081
|
+
if (result.gateway)
|
|
3082
|
+
console.log(` 网关: ${result.gateway}`);
|
|
3083
|
+
if (result.content) {
|
|
3084
|
+
const hasSig = result.content.includes('AUN-SIGNATURE');
|
|
3085
|
+
console.log(` 签名: ${hasSig ? '有(未验证,如需验证请用 evolclaw aid agentmd get ' + aid + ')' : '无'}`);
|
|
3086
|
+
console.log('');
|
|
3087
|
+
console.log(result.content);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
else {
|
|
3091
|
+
console.log(`✗ ${aid} 未注册`);
|
|
3092
|
+
if (result.gateway)
|
|
3093
|
+
console.log(` 网关: ${result.gateway}`);
|
|
3094
|
+
if (result.error)
|
|
3095
|
+
console.log(` 原因: ${result.error}`);
|
|
3096
|
+
}
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
if (sub === 'agentmd') {
|
|
3100
|
+
const verb = args[1];
|
|
3101
|
+
const aid = args[2];
|
|
3102
|
+
if (verb === 'put') {
|
|
3103
|
+
if (!aid) {
|
|
3104
|
+
console.error('用法: evolclaw aid agentmd put <aid>');
|
|
3105
|
+
process.exit(1);
|
|
3106
|
+
}
|
|
3107
|
+
if (!isValidAid(aid)) {
|
|
3108
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
3109
|
+
process.exit(1);
|
|
3110
|
+
}
|
|
3111
|
+
const aunBase = aunPath ?? path.join(os.homedir(), '.aun');
|
|
3112
|
+
const localPath = path.join(aunBase, 'AIDs', aid, 'agent.md');
|
|
3113
|
+
if (!fs.existsSync(localPath)) {
|
|
3114
|
+
console.error(`❌ 本地无 agent.md: ${aid}`);
|
|
3115
|
+
process.exit(1);
|
|
3116
|
+
}
|
|
3117
|
+
const content = fs.readFileSync(localPath, 'utf-8');
|
|
3118
|
+
await agentmdPut(content, { aid, aunPath });
|
|
3119
|
+
console.log('✓ agent.md 已发布');
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (verb === 'get') {
|
|
3123
|
+
if (!aid) {
|
|
3124
|
+
console.error('用法: evolclaw aid agentmd get <aid>');
|
|
3125
|
+
process.exit(1);
|
|
3126
|
+
}
|
|
3127
|
+
if (!isValidAid(aid)) {
|
|
3128
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
3129
|
+
process.exit(1);
|
|
3130
|
+
}
|
|
3131
|
+
try {
|
|
3132
|
+
const result = await agentmdGet(aid, { withVerification: true, aunPath });
|
|
3133
|
+
if (!result.content || !result.content.trim()) {
|
|
3134
|
+
console.log(`ℹ️ ${aid} 尚未设置 agent.md`);
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
if (formatJson) {
|
|
3138
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3139
|
+
}
|
|
3140
|
+
else {
|
|
3141
|
+
console.log(result.content);
|
|
3142
|
+
const v = result.verification;
|
|
3143
|
+
if (v.status === 'verified') {
|
|
3144
|
+
console.error(`✓ 签名验证通过`);
|
|
3145
|
+
}
|
|
3146
|
+
else if (v.status === 'invalid') {
|
|
3147
|
+
console.error(`⚠ 签名验证失败: ${v.reason ?? '未知原因'}`);
|
|
3148
|
+
}
|
|
3149
|
+
else {
|
|
3150
|
+
console.error(`ℹ️ 未签名`);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
catch (e) {
|
|
3155
|
+
const msg = String(e.message || e);
|
|
3156
|
+
if (msg.includes('not found') || msg.includes('404')) {
|
|
3157
|
+
console.log(`ℹ️ ${aid} 尚未设置 agent.md`);
|
|
3158
|
+
}
|
|
3159
|
+
else {
|
|
3160
|
+
console.error(`❌ 获取失败: ${msg.slice(0, 100)}`);
|
|
3161
|
+
process.exit(1);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
console.error(`未知子命令: aid agentmd ${verb ?? ''}\n用法: evolclaw aid agentmd [put|get] <aid>`);
|
|
3167
|
+
process.exit(1);
|
|
3168
|
+
}
|
|
3169
|
+
console.error(`未知子命令: ${sub}\n用法: evolclaw aid [list|show|new|delete|lookup|agentmd] <aid>`);
|
|
3170
|
+
process.exit(1);
|
|
3171
|
+
}
|
|
3172
|
+
// ==================== RPC ====================
|
|
3173
|
+
async function cmdRpc(args) {
|
|
3174
|
+
if (args[0] === 'help' || args.length === 0) {
|
|
3175
|
+
console.log(`用法: evolclaw rpc --as <aid> --params <params>
|
|
3176
|
+
|
|
3177
|
+
通用 AUN RPC 调用。
|
|
3178
|
+
|
|
3179
|
+
--params 自动判断输入形式:
|
|
3180
|
+
单行 JSON (以 { 开头) → 单次调用
|
|
3181
|
+
多行 JSONL → 逐行执行,失败即停
|
|
3182
|
+
文件路径 (文件存在) → 读取文件内容作为 JSONL
|
|
3183
|
+
|
|
3184
|
+
每行 JSON 格式: {"method":"<namespace.method>","params":{...}}
|
|
3185
|
+
|
|
3186
|
+
示例:
|
|
3187
|
+
evolclaw rpc --as alice.agentid.pub --params '{"method":"message.send","params":{"to":"bob.agentid.pub","payload":{"type":"text","text":"hello"}}}'
|
|
3188
|
+
evolclaw rpc --as alice.agentid.pub --params calls.jsonl`);
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
const asIdx = args.indexOf('--as');
|
|
3192
|
+
const paramsIdx = args.indexOf('--params');
|
|
3193
|
+
const aunPath = resolveAunPath(args);
|
|
3194
|
+
if (asIdx === -1 || asIdx + 1 >= args.length) {
|
|
3195
|
+
console.error('❌ 缺少 --as <aid>');
|
|
3196
|
+
process.exit(1);
|
|
3197
|
+
}
|
|
3198
|
+
if (paramsIdx === -1 || paramsIdx + 1 >= args.length) {
|
|
3199
|
+
console.error('❌ 缺少 --params <params>');
|
|
3200
|
+
process.exit(1);
|
|
3201
|
+
}
|
|
3202
|
+
const aid = args[asIdx + 1];
|
|
3203
|
+
const paramsRaw = args[paramsIdx + 1];
|
|
3204
|
+
const { isValidAid } = await import('../aun/aid/index.js');
|
|
3205
|
+
if (!isValidAid(aid)) {
|
|
3206
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
3207
|
+
process.exit(1);
|
|
3208
|
+
}
|
|
3209
|
+
// Determine input: file, single JSON, or multi-line JSONL
|
|
3210
|
+
let lines;
|
|
3211
|
+
if (fs.existsSync(paramsRaw)) {
|
|
3212
|
+
lines = fs.readFileSync(paramsRaw, 'utf-8').split('\n').filter(l => l.trim());
|
|
3213
|
+
}
|
|
3214
|
+
else if (paramsRaw.includes('\n')) {
|
|
3215
|
+
lines = paramsRaw.split('\n').filter(l => l.trim());
|
|
3216
|
+
}
|
|
3217
|
+
else {
|
|
3218
|
+
lines = [paramsRaw];
|
|
3219
|
+
}
|
|
3220
|
+
// Parse calls
|
|
3221
|
+
const calls = [];
|
|
3222
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3223
|
+
try {
|
|
3224
|
+
const parsed = JSON.parse(lines[i]);
|
|
3225
|
+
if (!parsed.method) {
|
|
3226
|
+
console.error(`❌ 第 ${i + 1} 行缺少 "method" 字段`);
|
|
3227
|
+
process.exit(1);
|
|
3228
|
+
}
|
|
3229
|
+
calls.push({ method: parsed.method, params: parsed.params ?? {} });
|
|
3230
|
+
}
|
|
3231
|
+
catch (e) {
|
|
3232
|
+
console.error(`❌ 第 ${i + 1} 行 JSON 解析失败: ${e.message}`);
|
|
3233
|
+
process.exit(1);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
const { rpcCall, rpcBatch } = await import('../aun/rpc/index.js');
|
|
3237
|
+
if (calls.length === 1) {
|
|
3238
|
+
const result = await rpcCall(aid, calls[0].method, calls[0].params, { aunPath });
|
|
3239
|
+
console.log(JSON.stringify(result));
|
|
3240
|
+
}
|
|
3241
|
+
else {
|
|
3242
|
+
const results = await rpcBatch(aid, calls, { aunPath });
|
|
3243
|
+
for (const r of results) {
|
|
3244
|
+
console.log(JSON.stringify(r));
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
// ==================== Storage ====================
|
|
3249
|
+
async function cmdStorage(args) {
|
|
3250
|
+
const sub = args[0];
|
|
3251
|
+
const aunPath = resolveAunPath(args);
|
|
3252
|
+
const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
|
|
3253
|
+
if (!sub || sub === 'help') {
|
|
3254
|
+
console.log(`用法: evolclaw storage <command> <aid> [options]
|
|
3255
|
+
|
|
3256
|
+
Commands:
|
|
3257
|
+
upload <aid> <local-file> <remote-path> [--public] 上传文件(默认私有)
|
|
3258
|
+
download <aid> <url> [local-path] 下载文件
|
|
3259
|
+
ls <aid> [prefix] 列文件
|
|
3260
|
+
rm <aid> <remote-path> 删文件
|
|
3261
|
+
quota <aid> 查配额
|
|
3262
|
+
|
|
3263
|
+
<url> 格式: [https://]<owner-aid>/<path>
|
|
3264
|
+
|
|
3265
|
+
示例:
|
|
3266
|
+
evolclaw storage upload myaid.agentid.pub ./doc.txt notes/doc.txt
|
|
3267
|
+
evolclaw storage upload myaid.agentid.pub ./pic.png images/pic.png --public
|
|
3268
|
+
evolclaw storage download myaid.agentid.pub myaid.agentid.pub/notes/doc.txt ./doc.txt
|
|
3269
|
+
evolclaw storage download myaid.agentid.pub bob.agentid.pub/public/file.pdf ./file.pdf
|
|
3270
|
+
evolclaw storage ls myaid.agentid.pub notes/
|
|
3271
|
+
evolclaw storage rm myaid.agentid.pub notes/doc.txt
|
|
3272
|
+
evolclaw storage quota myaid.agentid.pub`);
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
const aid = args[1];
|
|
3276
|
+
if (!aid) {
|
|
3277
|
+
console.error('❌ 缺少 <aid> 参数');
|
|
3278
|
+
process.exit(1);
|
|
3279
|
+
}
|
|
3280
|
+
const { isValidAid } = await import('../aun/aid/index.js');
|
|
3281
|
+
if (!isValidAid(aid)) {
|
|
3282
|
+
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
3283
|
+
process.exit(1);
|
|
3284
|
+
}
|
|
3285
|
+
const { storageUpload, storageDownload, storageLs, storageRm, storageQuota } = await import('../aun/storage/index.js');
|
|
3286
|
+
if (sub === 'upload') {
|
|
3287
|
+
const localFile = args[2];
|
|
3288
|
+
const remotePath = args[3];
|
|
3289
|
+
const isPublic = args.includes('--public');
|
|
3290
|
+
if (!localFile || !remotePath) {
|
|
3291
|
+
console.error('用法: evolclaw storage upload <aid> <local-file> <remote-path> [--public]');
|
|
3292
|
+
process.exit(1);
|
|
3293
|
+
}
|
|
3294
|
+
if (!fs.existsSync(localFile)) {
|
|
3295
|
+
console.error(`❌ 文件不存在: ${localFile}`);
|
|
3296
|
+
process.exit(1);
|
|
3297
|
+
}
|
|
3298
|
+
const result = await storageUpload(aid, localFile, remotePath, { isPublic, aunPath });
|
|
3299
|
+
if (!result.ok) {
|
|
3300
|
+
if (formatJson) {
|
|
3301
|
+
console.log(JSON.stringify({ ok: false, error: result.error }));
|
|
3302
|
+
}
|
|
3303
|
+
else {
|
|
3304
|
+
console.error(`❌ 上传失败: ${result.error}`);
|
|
3305
|
+
}
|
|
3306
|
+
process.exit(1);
|
|
3307
|
+
}
|
|
3308
|
+
if (formatJson) {
|
|
3309
|
+
console.log(JSON.stringify({ ok: true, objectKey: remotePath, isPublic, ref: `${aid}/${remotePath}` }));
|
|
3310
|
+
}
|
|
3311
|
+
else {
|
|
3312
|
+
console.log(`✓ 已上传: ${remotePath}${isPublic ? ' (公开)' : ''}`);
|
|
3313
|
+
console.log(` 引用: ${aid}/${remotePath}`);
|
|
3314
|
+
console.log(` 下载: evolclaw storage download ${aid} ${aid}/${remotePath}`);
|
|
3315
|
+
}
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
if (sub === 'download') {
|
|
3319
|
+
const url = args[2];
|
|
3320
|
+
const localPath = args[3];
|
|
3321
|
+
if (!url) {
|
|
3322
|
+
console.error('用法: evolclaw storage download <aid> <url> [local-path]');
|
|
3323
|
+
process.exit(1);
|
|
3324
|
+
}
|
|
3325
|
+
const result = await storageDownload(aid, url, localPath, { aunPath });
|
|
3326
|
+
if (!result.ok) {
|
|
3327
|
+
if (formatJson) {
|
|
3328
|
+
console.log(JSON.stringify({ ok: false, error: result.error }));
|
|
3329
|
+
}
|
|
3330
|
+
else {
|
|
3331
|
+
console.error(`❌ 下载失败: ${result.error}`);
|
|
3332
|
+
}
|
|
3333
|
+
process.exit(1);
|
|
3334
|
+
}
|
|
3335
|
+
if (formatJson) {
|
|
3336
|
+
console.log(JSON.stringify({ ok: true, localPath: result.localPath, size: result.size }));
|
|
3337
|
+
}
|
|
3338
|
+
else {
|
|
3339
|
+
console.log(`✓ 已下载: ${result.localPath} (${result.size} bytes)`);
|
|
3340
|
+
}
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
if (sub === 'ls') {
|
|
3344
|
+
const prefix = args[2] || '';
|
|
3345
|
+
const result = await storageLs(aid, prefix, { aunPath });
|
|
3346
|
+
if (!result.ok) {
|
|
3347
|
+
console.error(`❌ 列文件失败: ${JSON.stringify(result.error)}`);
|
|
3348
|
+
process.exit(1);
|
|
3349
|
+
}
|
|
3350
|
+
const objects = result.result?.objects || result.result || [];
|
|
3351
|
+
if (Array.isArray(objects) && objects.length === 0) {
|
|
3352
|
+
console.log('(空)');
|
|
3353
|
+
}
|
|
3354
|
+
else {
|
|
3355
|
+
console.log(JSON.stringify(objects, null, 2));
|
|
3356
|
+
}
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
if (sub === 'rm') {
|
|
3360
|
+
const remotePath = args[2];
|
|
3361
|
+
if (!remotePath) {
|
|
3362
|
+
console.error('用法: evolclaw storage rm <aid> <remote-path>');
|
|
3363
|
+
process.exit(1);
|
|
3364
|
+
}
|
|
3365
|
+
const result = await storageRm(aid, remotePath, { aunPath });
|
|
3366
|
+
if (!result.ok) {
|
|
3367
|
+
if (formatJson) {
|
|
3368
|
+
console.log(JSON.stringify({ ok: false, error: result.error }));
|
|
3369
|
+
}
|
|
3370
|
+
else {
|
|
3371
|
+
console.error(`❌ 删除失败: ${JSON.stringify(result.error)}`);
|
|
3372
|
+
}
|
|
3373
|
+
process.exit(1);
|
|
3374
|
+
}
|
|
3375
|
+
if (formatJson) {
|
|
3376
|
+
console.log(JSON.stringify({ ok: true, objectKey: remotePath }));
|
|
3377
|
+
}
|
|
3378
|
+
else {
|
|
3379
|
+
console.log(`✓ 已删除: ${remotePath}`);
|
|
3380
|
+
}
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
if (sub === 'quota') {
|
|
3384
|
+
const result = await storageQuota(aid, { aunPath });
|
|
3385
|
+
if (!result.ok) {
|
|
3386
|
+
console.error(`❌ 查询配额失败: ${JSON.stringify(result.error)}`);
|
|
3387
|
+
process.exit(1);
|
|
3388
|
+
}
|
|
3389
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
console.error(`未知子命令: ${sub}\n用法: evolclaw storage [upload|download|ls|rm|quota]`);
|
|
3393
|
+
process.exit(1);
|
|
3394
|
+
}
|
|
3395
|
+
// ==================== Msg ====================
|
|
3396
|
+
async function cmdMsg(args) {
|
|
3397
|
+
const sub = args[0];
|
|
3398
|
+
const aunPath = resolveAunPath(args);
|
|
3399
|
+
const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
|
|
3400
|
+
const appIdx = args.indexOf('--app');
|
|
3401
|
+
const appSlot = appIdx >= 0 ? args[appIdx + 1] : undefined;
|
|
3402
|
+
const asDaemon = args.includes('--as-daemon');
|
|
3403
|
+
if (!sub || sub === 'help') {
|
|
3404
|
+
console.log(`用法: evolclaw msg <command> <from-aid> [args...] [options]
|
|
3405
|
+
|
|
3406
|
+
Commands:
|
|
3407
|
+
send <from> <to> <text> 发送文本
|
|
3408
|
+
send <from> <to> --file <path> [--as <type>] 发送文件(image|video|voice|file)
|
|
3409
|
+
send <from> <to> --link <url> [--title T] 发送链接卡片
|
|
3410
|
+
send <from> <to> --payload <json> 发送自定义 payload
|
|
3411
|
+
pull <from> [--after-seq N] [--limit N] 拉取收件箱
|
|
3412
|
+
ack <from> <seq> --app <name> 确认已读(必须传 --app)
|
|
3413
|
+
recall <from> <message-id> [<message-id>...] 撤回消息
|
|
3414
|
+
online <from> <target-aid> [<target-aid>...] 查询在线状态
|
|
3415
|
+
|
|
3416
|
+
Options:
|
|
3417
|
+
--app <name> 指定应用 slot(隔离 ack 游标)
|
|
3418
|
+
--as-daemon ack 时显式以 daemon 身份(高危,会污染 daemon 游标)
|
|
3419
|
+
--format json 输出 JSON 格式
|
|
3420
|
+
--content-type <mime> 显式覆盖 MIME(仅 --file 模式)
|
|
3421
|
+
--text <说明> 附件说明文字(仅 --file 模式)
|
|
3422
|
+
--transcript <text> 语音转写(仅 --as voice)
|
|
3423
|
+
|
|
3424
|
+
示例:
|
|
3425
|
+
evolclaw msg send alice.agentid.pub bob.agentid.pub "hello"
|
|
3426
|
+
evolclaw msg send alice.agentid.pub bob.agentid.pub --file ./pic.png
|
|
3427
|
+
evolclaw msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video
|
|
3428
|
+
evolclaw msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN"
|
|
3429
|
+
evolclaw msg pull alice.agentid.pub --app my-bot
|
|
3430
|
+
evolclaw msg ack alice.agentid.pub 42 --app my-bot
|
|
3431
|
+
evolclaw msg recall alice.agentid.pub msg-uuid-1 msg-uuid-2
|
|
3432
|
+
evolclaw msg online alice.agentid.pub bob.agentid.pub carol.agentid.pub`);
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
const from = args[1];
|
|
3436
|
+
if (!from) {
|
|
3437
|
+
console.error('❌ 缺少 <from-aid> 参数');
|
|
3438
|
+
process.exit(1);
|
|
3439
|
+
}
|
|
3440
|
+
const { isValidAid } = await import('../aun/aid/index.js');
|
|
3441
|
+
if (!isValidAid(from)) {
|
|
3442
|
+
console.error(`❌ 无效 AID 格式: ${from}`);
|
|
3443
|
+
process.exit(1);
|
|
3444
|
+
}
|
|
3445
|
+
const { msgSend, msgPull, msgAck, msgRecall, msgOnline } = await import('../aun/msg/index.js');
|
|
3446
|
+
const commonOpts = { aunPath, slotId: appSlot };
|
|
3447
|
+
if (sub === 'send') {
|
|
3448
|
+
const to = args[2];
|
|
3449
|
+
if (!to) {
|
|
3450
|
+
console.error('用法: evolclaw msg send <from> <to> <text|--file ...|--link ...|--payload ...>');
|
|
3451
|
+
process.exit(1);
|
|
3452
|
+
}
|
|
3453
|
+
if (!isValidAid(to)) {
|
|
3454
|
+
console.error(`❌ 无效目标 AID: ${to}`);
|
|
3455
|
+
process.exit(1);
|
|
3456
|
+
}
|
|
3457
|
+
const fileVal = getArgValue(args, '--file');
|
|
3458
|
+
const linkVal = getArgValue(args, '--link');
|
|
3459
|
+
const payloadVal = getArgValue(args, '--payload');
|
|
3460
|
+
let body;
|
|
3461
|
+
if (fileVal) {
|
|
3462
|
+
body = {
|
|
3463
|
+
mode: 'file',
|
|
3464
|
+
filePath: fileVal,
|
|
3465
|
+
as: getArgValue(args, '--as'),
|
|
3466
|
+
contentType: getArgValue(args, '--content-type'),
|
|
3467
|
+
text: getArgValue(args, '--text'),
|
|
3468
|
+
transcript: getArgValue(args, '--transcript'),
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
else if (linkVal) {
|
|
3472
|
+
body = {
|
|
3473
|
+
mode: 'link',
|
|
3474
|
+
url: linkVal,
|
|
3475
|
+
title: getArgValue(args, '--title'),
|
|
3476
|
+
description: getArgValue(args, '--description'),
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
else if (payloadVal) {
|
|
3480
|
+
let parsed;
|
|
3481
|
+
try {
|
|
3482
|
+
parsed = JSON.parse(payloadVal);
|
|
3483
|
+
}
|
|
3484
|
+
catch (e) {
|
|
3485
|
+
console.error(`❌ --payload 解析失败: ${e.message}`);
|
|
3486
|
+
process.exit(1);
|
|
3487
|
+
}
|
|
3488
|
+
body = { mode: 'payload', payload: parsed };
|
|
3489
|
+
}
|
|
3490
|
+
else {
|
|
3491
|
+
const text = collectPositional(args, 3).join(' ');
|
|
3492
|
+
if (!text) {
|
|
3493
|
+
console.error('❌ 缺少消息内容(文本或 --file/--link/--payload)');
|
|
3494
|
+
process.exit(1);
|
|
3495
|
+
}
|
|
3496
|
+
body = { mode: 'text', text };
|
|
3497
|
+
}
|
|
3498
|
+
const result = await msgSend({ from, to, body, ...commonOpts });
|
|
3499
|
+
if (!result.ok) {
|
|
3500
|
+
if (formatJson) {
|
|
3501
|
+
console.log(JSON.stringify(result));
|
|
3502
|
+
}
|
|
3503
|
+
else {
|
|
3504
|
+
console.error(`❌ 发送失败: ${result.error}`);
|
|
3505
|
+
}
|
|
3506
|
+
process.exit(1);
|
|
3507
|
+
}
|
|
3508
|
+
if (formatJson) {
|
|
3509
|
+
console.log(JSON.stringify(result));
|
|
3510
|
+
}
|
|
3511
|
+
else {
|
|
3512
|
+
console.log(`✓ 已发送 ${result.message_id ?? ''} seq=${result.seq ?? '-'} status=${result.status ?? '-'}`);
|
|
3513
|
+
}
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
if (sub === 'pull') {
|
|
3517
|
+
if (!appSlot) {
|
|
3518
|
+
console.error('⚠ 警告: 未传 --app,将使用 daemon 共享 slot(可能与 daemon 看到同一批消息)');
|
|
3519
|
+
}
|
|
3520
|
+
const afterSeqStr = getArgValue(args, '--after-seq');
|
|
3521
|
+
const limitStr = getArgValue(args, '--limit');
|
|
3522
|
+
const afterSeq = afterSeqStr !== undefined ? Number(afterSeqStr) : undefined;
|
|
3523
|
+
const limit = limitStr !== undefined ? Number(limitStr) : undefined;
|
|
3524
|
+
if (afterSeq !== undefined && !Number.isFinite(afterSeq)) {
|
|
3525
|
+
console.error(`❌ --after-seq 必须是数字: ${afterSeqStr}`);
|
|
3526
|
+
process.exit(1);
|
|
3527
|
+
}
|
|
3528
|
+
if (limit !== undefined && !Number.isFinite(limit)) {
|
|
3529
|
+
console.error(`❌ --limit 必须是数字: ${limitStr}`);
|
|
3530
|
+
process.exit(1);
|
|
3531
|
+
}
|
|
3532
|
+
const result = await msgPull({ from, afterSeq, limit, ...commonOpts });
|
|
3533
|
+
if (!result.ok) {
|
|
3534
|
+
if (formatJson) {
|
|
3535
|
+
console.log(JSON.stringify(result));
|
|
3536
|
+
}
|
|
3537
|
+
else {
|
|
3538
|
+
console.error(`❌ 拉取失败: ${result.error}`);
|
|
3539
|
+
}
|
|
3540
|
+
process.exit(1);
|
|
3541
|
+
}
|
|
3542
|
+
if (formatJson) {
|
|
3543
|
+
console.log(JSON.stringify(result));
|
|
3544
|
+
}
|
|
3545
|
+
else {
|
|
3546
|
+
console.log(`✓ ${result.count} 条消息,latest_seq=${result.latest_seq}`);
|
|
3547
|
+
for (const m of result.messages) {
|
|
3548
|
+
const text = m.payload?.text ?? JSON.stringify(m.payload).slice(0, 80);
|
|
3549
|
+
console.log(` [${m.seq}] ${m.from}: ${text}`);
|
|
3550
|
+
}
|
|
3551
|
+
if (result.ephemeral_dropped_count && result.ephemeral_dropped_count > 0) {
|
|
3552
|
+
console.log(` (临时消息淘汰: ${result.ephemeral_dropped_count} 条)`);
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
return;
|
|
3556
|
+
}
|
|
3557
|
+
if (sub === 'ack') {
|
|
3558
|
+
const seqStr = args[2];
|
|
3559
|
+
if (!seqStr) {
|
|
3560
|
+
console.error('用法: evolclaw msg ack <from> <seq> --app <name>');
|
|
3561
|
+
process.exit(1);
|
|
3562
|
+
}
|
|
3563
|
+
const seq = Number(seqStr);
|
|
3564
|
+
if (!Number.isFinite(seq)) {
|
|
3565
|
+
console.error(`❌ seq 必须是数字: ${seqStr}`);
|
|
3566
|
+
process.exit(1);
|
|
3567
|
+
}
|
|
3568
|
+
if (!appSlot && !asDaemon) {
|
|
3569
|
+
console.error('❌ ack 必须传 --app <name>(或 --as-daemon 显式以 daemon 身份,高危)');
|
|
3570
|
+
console.error(' 理由: 不传 --app 会推进 daemon 共享的 ack 游标,导致 daemon 丢消息');
|
|
3571
|
+
process.exit(1);
|
|
3572
|
+
}
|
|
3573
|
+
const result = await msgAck({ from, seq, ...commonOpts });
|
|
3574
|
+
if (!result.ok) {
|
|
3575
|
+
if (formatJson) {
|
|
3576
|
+
console.log(JSON.stringify(result));
|
|
3577
|
+
}
|
|
3578
|
+
else {
|
|
3579
|
+
console.error(`❌ ack 失败: ${result.error}`);
|
|
3580
|
+
}
|
|
3581
|
+
process.exit(1);
|
|
3582
|
+
}
|
|
3583
|
+
if (formatJson) {
|
|
3584
|
+
console.log(JSON.stringify(result));
|
|
3585
|
+
}
|
|
3586
|
+
else {
|
|
3587
|
+
console.log(`✓ ack_seq=${result.ack_seq}`);
|
|
3588
|
+
}
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
if (sub === 'recall') {
|
|
3592
|
+
const messageIds = collectPositional(args, 2);
|
|
3593
|
+
if (messageIds.length === 0) {
|
|
3594
|
+
console.error('用法: evolclaw msg recall <from> <message-id> [<message-id>...]');
|
|
3595
|
+
process.exit(1);
|
|
3596
|
+
}
|
|
3597
|
+
const result = await msgRecall({ from, messageIds, ...commonOpts });
|
|
3598
|
+
if (!result.ok) {
|
|
3599
|
+
if (formatJson) {
|
|
3600
|
+
console.log(JSON.stringify(result));
|
|
3601
|
+
}
|
|
3602
|
+
else {
|
|
3603
|
+
console.error(`❌ recall 失败: ${result.error}`);
|
|
3604
|
+
}
|
|
3605
|
+
process.exit(1);
|
|
3606
|
+
}
|
|
3607
|
+
if (formatJson) {
|
|
3608
|
+
console.log(JSON.stringify(result));
|
|
3609
|
+
}
|
|
3610
|
+
else {
|
|
3611
|
+
console.log(`✓ 受理 ${result.accepted},撤回 ${result.recalled}`);
|
|
3612
|
+
if (result.errors && result.errors.length > 0) {
|
|
3613
|
+
for (const e of result.errors) {
|
|
3614
|
+
console.log(` 失败 ${e.message_id}: ${e.error}`);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3620
|
+
if (sub === 'online') {
|
|
3621
|
+
const targets = collectPositional(args, 2);
|
|
3622
|
+
if (targets.length === 0) {
|
|
3623
|
+
console.error('用法: evolclaw msg online <from> <target-aid> [<target-aid>...]');
|
|
3624
|
+
process.exit(1);
|
|
3625
|
+
}
|
|
3626
|
+
for (const t of targets) {
|
|
3627
|
+
if (!isValidAid(t)) {
|
|
3628
|
+
console.error(`❌ 无效 AID: ${t}`);
|
|
3629
|
+
process.exit(1);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
const result = await msgOnline({ from, targets, ...commonOpts });
|
|
3633
|
+
if (!result.ok) {
|
|
3634
|
+
if (formatJson) {
|
|
3635
|
+
console.log(JSON.stringify(result));
|
|
3636
|
+
}
|
|
3637
|
+
else {
|
|
3638
|
+
console.error(`❌ 查询失败: ${result.error}`);
|
|
3639
|
+
}
|
|
3640
|
+
process.exit(1);
|
|
3641
|
+
}
|
|
3642
|
+
if (formatJson) {
|
|
3643
|
+
console.log(JSON.stringify(result));
|
|
3644
|
+
}
|
|
3645
|
+
else {
|
|
3646
|
+
for (const [aid, online] of Object.entries(result.online)) {
|
|
3647
|
+
console.log(` ${online ? '🟢' : '⚫'} ${aid}`);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
return;
|
|
3651
|
+
}
|
|
3652
|
+
console.error(`未知子命令: ${sub}\n用法: evolclaw msg [send|pull|ack|recall|online]`);
|
|
3653
|
+
process.exit(1);
|
|
3654
|
+
}
|
|
3655
|
+
// ==================== Group ====================
|
|
3656
|
+
async function cmdGroup(args) {
|
|
3657
|
+
const sub = args[0];
|
|
3658
|
+
const aunPath = resolveAunPath(args);
|
|
3659
|
+
const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
|
|
3660
|
+
const appIdx = args.indexOf('--app');
|
|
3661
|
+
const appSlot = appIdx >= 0 ? args[appIdx + 1] : undefined;
|
|
3662
|
+
const asDaemon = args.includes('--as-daemon');
|
|
3663
|
+
if (!sub || sub === 'help') {
|
|
3664
|
+
console.log(`用法: evolclaw group <command> <from-aid> [args...] [options]
|
|
3665
|
+
|
|
3666
|
+
消息:
|
|
3667
|
+
send <from> <group-id> <text> 发送群文本
|
|
3668
|
+
send <from> <group-id> --file <path> [--as <type>] 发送群文件
|
|
3669
|
+
send <from> <group-id> --payload <json> 发送自定义 payload
|
|
3670
|
+
pull <from> <group-id> [--after-seq N] [--limit N] 拉取群消息
|
|
3671
|
+
ack <from> <group-id> <seq> --app <name> 确认已读(必须传 --app)
|
|
3672
|
+
|
|
3673
|
+
群管理:
|
|
3674
|
+
create <from> <name> [--visibility public|private] [--description D] [--join-mode M] 创建群
|
|
3675
|
+
list <from> [--size N] 列出我加入的群
|
|
3676
|
+
info <from> <group-id> 查看群详情
|
|
3677
|
+
update <from> <group-id> [--name N] [--description D] 修改群信息
|
|
3678
|
+
dissolve <from> <group-id> 解散群
|
|
3679
|
+
|
|
3680
|
+
成员:
|
|
3681
|
+
join <from> <group-id> [--message M] [--answer A] 申请加入
|
|
3682
|
+
leave <from> <group-id> 退出群
|
|
3683
|
+
invite <from> <group-id> <member-aid> [<member-aid>...] 邀请成员
|
|
3684
|
+
kick <from> <group-id> <member-aid> 踢出成员
|
|
3685
|
+
members <from> <group-id> [--page N] [--size N] 列出群成员
|
|
3686
|
+
online <from> <group-id> 查看在线成员
|
|
3687
|
+
|
|
3688
|
+
Options:
|
|
3689
|
+
--app <name> 指定应用 slot(隔离 ack 游标)
|
|
3690
|
+
--as-daemon ack 时显式以 daemon 身份(高危)
|
|
3691
|
+
--format json 输出 JSON 格式
|
|
3692
|
+
--mention <aid> 发送时 @ 某个成员(可多次)
|
|
3693
|
+
--mention-all 发送时 @ 所有人
|
|
3694
|
+
|
|
3695
|
+
示例:
|
|
3696
|
+
evolclaw group create alice.agentid.pub "Dev Team" --visibility private
|
|
3697
|
+
evolclaw group send alice.agentid.pub g-dev.agentid.pub "hello team"
|
|
3698
|
+
evolclaw group send alice.agentid.pub g-dev.agentid.pub "@bob 看下 PR" --mention bob.agentid.pub
|
|
3699
|
+
evolclaw group send alice.agentid.pub g-dev.agentid.pub --file ./arch.png
|
|
3700
|
+
evolclaw group invite alice.agentid.pub g-dev.agentid.pub bob.agentid.pub carol.agentid.pub
|
|
3701
|
+
evolclaw group members alice.agentid.pub g-dev.agentid.pub`);
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
const from = args[1];
|
|
3705
|
+
if (!from) {
|
|
3706
|
+
console.error('❌ 缺少 <from-aid> 参数');
|
|
3707
|
+
process.exit(1);
|
|
3708
|
+
}
|
|
3709
|
+
const { isValidAid } = await import('../aun/aid/index.js');
|
|
3710
|
+
if (!isValidAid(from)) {
|
|
3711
|
+
console.error(`❌ 无效 AID 格式: ${from}`);
|
|
3712
|
+
process.exit(1);
|
|
3713
|
+
}
|
|
3714
|
+
const { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupJoin, groupLeave, groupInvite, groupKick, groupMembers, groupOnline, } = await import('../aun/msg/index.js');
|
|
3715
|
+
const commonOpts = { aunPath, slotId: appSlot };
|
|
3716
|
+
// 通用 group_id 提取(第三参数)
|
|
3717
|
+
const requireGroupId = () => {
|
|
3718
|
+
const gid = args[2];
|
|
3719
|
+
if (!gid) {
|
|
3720
|
+
console.error(`❌ 缺少 <group-id> 参数`);
|
|
3721
|
+
process.exit(1);
|
|
3722
|
+
}
|
|
3723
|
+
return gid;
|
|
3724
|
+
};
|
|
3725
|
+
// 收集 --mention(可多次)
|
|
3726
|
+
const collectMentions = () => {
|
|
3727
|
+
const mentions = [];
|
|
3728
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
3729
|
+
if (args[i] === '--mention') {
|
|
3730
|
+
mentions.push({ aid: args[i + 1] });
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
if (args.includes('--mention-all')) {
|
|
3734
|
+
mentions.push({ scope: 'all' });
|
|
3735
|
+
}
|
|
3736
|
+
return mentions;
|
|
3737
|
+
};
|
|
3738
|
+
// 输出辅助
|
|
3739
|
+
const outputResult = (result, successHuman) => {
|
|
3740
|
+
if (!result.ok) {
|
|
3741
|
+
if (formatJson) {
|
|
3742
|
+
console.log(JSON.stringify(result));
|
|
3743
|
+
}
|
|
3744
|
+
else {
|
|
3745
|
+
console.error(`❌ ${result.error}`);
|
|
3746
|
+
}
|
|
3747
|
+
process.exit(1);
|
|
3748
|
+
}
|
|
3749
|
+
if (formatJson) {
|
|
3750
|
+
console.log(JSON.stringify(result));
|
|
3751
|
+
}
|
|
3752
|
+
else {
|
|
3753
|
+
successHuman();
|
|
3754
|
+
}
|
|
3755
|
+
};
|
|
3756
|
+
// ---- 消息 ----
|
|
3757
|
+
if (sub === 'send') {
|
|
3758
|
+
const groupId = requireGroupId();
|
|
3759
|
+
const fileVal = getArgValue(args, '--file');
|
|
3760
|
+
const payloadVal = getArgValue(args, '--payload');
|
|
3761
|
+
let body;
|
|
3762
|
+
if (fileVal) {
|
|
3763
|
+
body = {
|
|
3764
|
+
mode: 'file',
|
|
3765
|
+
filePath: fileVal,
|
|
3766
|
+
as: getArgValue(args, '--as'),
|
|
3767
|
+
contentType: getArgValue(args, '--content-type'),
|
|
3768
|
+
text: getArgValue(args, '--text'),
|
|
3769
|
+
transcript: getArgValue(args, '--transcript'),
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
else if (payloadVal) {
|
|
3773
|
+
let parsed;
|
|
3774
|
+
try {
|
|
3775
|
+
parsed = JSON.parse(payloadVal);
|
|
3776
|
+
}
|
|
3777
|
+
catch (e) {
|
|
3778
|
+
console.error(`❌ --payload 解析失败: ${e.message}`);
|
|
3779
|
+
process.exit(1);
|
|
3780
|
+
}
|
|
3781
|
+
body = { mode: 'payload', payload: parsed };
|
|
3782
|
+
}
|
|
3783
|
+
else {
|
|
3784
|
+
const text = collectPositional(args, 3).join(' ');
|
|
3785
|
+
if (!text) {
|
|
3786
|
+
console.error('❌ 缺少消息内容(文本或 --file/--payload)');
|
|
3787
|
+
process.exit(1);
|
|
3788
|
+
}
|
|
3789
|
+
body = { mode: 'text', text };
|
|
3790
|
+
}
|
|
3791
|
+
const mentions = collectMentions();
|
|
3792
|
+
const result = await groupSend({ from, groupId, body, mentions: mentions.length ? mentions : undefined, ...commonOpts });
|
|
3793
|
+
outputResult(result, () => {
|
|
3794
|
+
const r = result;
|
|
3795
|
+
console.log(`✓ 已发送 message_id=${r.message?.message_id ?? '-'} seq=${r.message?.seq ?? '-'}`);
|
|
3796
|
+
});
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
if (sub === 'pull') {
|
|
3800
|
+
const groupId = requireGroupId();
|
|
3801
|
+
if (!appSlot) {
|
|
3802
|
+
console.error('⚠ 警告: 未传 --app,将使用 daemon 共享 slot');
|
|
3803
|
+
}
|
|
3804
|
+
const afterSeqStr = getArgValue(args, '--after-seq');
|
|
3805
|
+
const limitStr = getArgValue(args, '--limit');
|
|
3806
|
+
const afterSeq = afterSeqStr !== undefined ? Number(afterSeqStr) : undefined;
|
|
3807
|
+
const limit = limitStr !== undefined ? Number(limitStr) : undefined;
|
|
3808
|
+
const result = await groupPull({ from, groupId, afterSeq, limit, ...commonOpts });
|
|
3809
|
+
outputResult(result, () => {
|
|
3810
|
+
const r = result;
|
|
3811
|
+
console.log(`✓ ${r.messages.length} 条消息,latest_seq=${r.latest_message_seq}${r.has_more ? '(还有更多)' : ''}`);
|
|
3812
|
+
for (const m of r.messages) {
|
|
3813
|
+
const text = m.payload?.text ?? JSON.stringify(m.payload).slice(0, 80);
|
|
3814
|
+
console.log(` [${m.seq}] ${m.sender_aid}: ${text}`);
|
|
3815
|
+
}
|
|
3816
|
+
});
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
if (sub === 'ack') {
|
|
3820
|
+
const groupId = requireGroupId();
|
|
3821
|
+
const seqStr = args[3];
|
|
3822
|
+
if (!seqStr) {
|
|
3823
|
+
console.error('用法: evolclaw group ack <from> <group-id> <seq> --app <name>');
|
|
3824
|
+
process.exit(1);
|
|
3825
|
+
}
|
|
3826
|
+
const seq = Number(seqStr);
|
|
3827
|
+
if (!Number.isFinite(seq)) {
|
|
3828
|
+
console.error(`❌ seq 必须是数字: ${seqStr}`);
|
|
3829
|
+
process.exit(1);
|
|
3830
|
+
}
|
|
3831
|
+
if (!appSlot && !asDaemon) {
|
|
3832
|
+
console.error('❌ group ack 必须传 --app <name>(或 --as-daemon 显式以 daemon 身份,高危)');
|
|
3833
|
+
process.exit(1);
|
|
3834
|
+
}
|
|
3835
|
+
const result = await groupAck({ from, groupId, seq, ...commonOpts });
|
|
3836
|
+
outputResult(result, () => {
|
|
3837
|
+
const r = result;
|
|
3838
|
+
console.log(`✓ ack_seq=${r.ack_seq}`);
|
|
3839
|
+
});
|
|
3840
|
+
return;
|
|
3841
|
+
}
|
|
3842
|
+
// ---- 群管理 ----
|
|
3843
|
+
if (sub === 'create') {
|
|
3844
|
+
const name = args[2];
|
|
3845
|
+
if (!name) {
|
|
3846
|
+
console.error('用法: evolclaw group create <from> <name> [--visibility ...] [--description ...]');
|
|
3847
|
+
process.exit(1);
|
|
3848
|
+
}
|
|
3849
|
+
const visibility = getArgValue(args, '--visibility');
|
|
3850
|
+
if (visibility && visibility !== 'public' && visibility !== 'private') {
|
|
3851
|
+
console.error(`❌ --visibility 必须是 public 或 private`);
|
|
3852
|
+
process.exit(1);
|
|
3853
|
+
}
|
|
3854
|
+
const result = await groupCreate({
|
|
3855
|
+
from,
|
|
3856
|
+
name,
|
|
3857
|
+
visibility,
|
|
3858
|
+
description: getArgValue(args, '--description'),
|
|
3859
|
+
joinMode: getArgValue(args, '--join-mode'),
|
|
3860
|
+
groupId: getArgValue(args, '--group-id'),
|
|
3861
|
+
...commonOpts,
|
|
3862
|
+
});
|
|
3863
|
+
outputResult(result, () => {
|
|
3864
|
+
const r = result;
|
|
3865
|
+
console.log(`✓ 已创建群 ${r.group?.group_id}`);
|
|
3866
|
+
console.log(` 名称: ${r.group?.name}`);
|
|
3867
|
+
console.log(` 可见性: ${r.group?.visibility}`);
|
|
3868
|
+
});
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
if (sub === 'list') {
|
|
3872
|
+
const sizeStr = getArgValue(args, '--size');
|
|
3873
|
+
const size = sizeStr !== undefined ? Number(sizeStr) : undefined;
|
|
3874
|
+
const result = await groupList({ from, size, ...commonOpts });
|
|
3875
|
+
outputResult(result, () => {
|
|
3876
|
+
const r = result;
|
|
3877
|
+
if (r.items.length === 0) {
|
|
3878
|
+
console.log('(没有加入任何群)');
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
console.log(`共 ${r.total} 个群:`);
|
|
3882
|
+
for (const g of r.items) {
|
|
3883
|
+
console.log(` ${g.group_id} ${g.name} (${g.member_count ?? '?'} 人)`);
|
|
3884
|
+
}
|
|
3885
|
+
});
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
if (sub === 'info') {
|
|
3889
|
+
const groupId = requireGroupId();
|
|
3890
|
+
const result = await groupInfo({ from, groupId, ...commonOpts });
|
|
3891
|
+
outputResult(result, () => {
|
|
3892
|
+
const g = result.group;
|
|
3893
|
+
console.log(`Group: ${g.group_id}`);
|
|
3894
|
+
console.log(` 名称: ${g.name}`);
|
|
3895
|
+
console.log(` 群主: ${g.owner_aid}`);
|
|
3896
|
+
console.log(` 可见性: ${g.visibility ?? '-'}`);
|
|
3897
|
+
console.log(` 状态: ${g.status ?? '-'}`);
|
|
3898
|
+
console.log(` 成员数: ${g.member_count ?? '-'}`);
|
|
3899
|
+
console.log(` 最新 seq: ${g.message_seq ?? '-'}`);
|
|
3900
|
+
if (g.description)
|
|
3901
|
+
console.log(` 描述: ${g.description}`);
|
|
3902
|
+
});
|
|
3903
|
+
return;
|
|
3904
|
+
}
|
|
3905
|
+
if (sub === 'update') {
|
|
3906
|
+
const groupId = requireGroupId();
|
|
3907
|
+
const name = getArgValue(args, '--name');
|
|
3908
|
+
const description = getArgValue(args, '--description');
|
|
3909
|
+
if (name === undefined && description === undefined) {
|
|
3910
|
+
console.error('❌ 至少需要 --name 或 --description 之一');
|
|
3911
|
+
process.exit(1);
|
|
3912
|
+
}
|
|
3913
|
+
const result = await groupUpdate({ from, groupId, name, description, ...commonOpts });
|
|
3914
|
+
outputResult(result, () => {
|
|
3915
|
+
const g = result.group;
|
|
3916
|
+
console.log(`✓ 已更新 ${g.group_id}`);
|
|
3917
|
+
console.log(` 名称: ${g.name}`);
|
|
3918
|
+
});
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
if (sub === 'dissolve') {
|
|
3922
|
+
const groupId = requireGroupId();
|
|
3923
|
+
const result = await groupDissolve({ from, groupId, ...commonOpts });
|
|
3924
|
+
outputResult(result, () => {
|
|
3925
|
+
const r = result;
|
|
3926
|
+
console.log(`✓ 已解散 ${r.group_id} (${r.status})`);
|
|
3927
|
+
});
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3930
|
+
// ---- 成员 ----
|
|
3931
|
+
if (sub === 'join') {
|
|
3932
|
+
const groupId = requireGroupId();
|
|
3933
|
+
const result = await groupJoin({
|
|
3934
|
+
from, groupId,
|
|
3935
|
+
message: getArgValue(args, '--message'),
|
|
3936
|
+
answer: getArgValue(args, '--answer'),
|
|
3937
|
+
...commonOpts,
|
|
3938
|
+
});
|
|
3939
|
+
outputResult(result, () => {
|
|
3940
|
+
console.log(`✓ 已提交入群申请`);
|
|
3941
|
+
});
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
if (sub === 'leave') {
|
|
3945
|
+
const groupId = requireGroupId();
|
|
3946
|
+
const result = await groupLeave({ from, groupId, ...commonOpts });
|
|
3947
|
+
outputResult(result, () => {
|
|
3948
|
+
console.log(`✓ 已退出 ${groupId}`);
|
|
3949
|
+
});
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
if (sub === 'invite') {
|
|
3953
|
+
const groupId = requireGroupId();
|
|
3954
|
+
const members = collectPositional(args, 3);
|
|
3955
|
+
if (members.length === 0) {
|
|
3956
|
+
console.error('用法: evolclaw group invite <from> <group-id> <member-aid> [<member-aid>...]');
|
|
3957
|
+
process.exit(1);
|
|
3958
|
+
}
|
|
3959
|
+
for (const m of members) {
|
|
3960
|
+
if (!isValidAid(m)) {
|
|
3961
|
+
console.error(`❌ 无效 AID: ${m}`);
|
|
3962
|
+
process.exit(1);
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
const result = await groupInvite({ from, groupId, members, ...commonOpts });
|
|
3966
|
+
outputResult(result, () => {
|
|
3967
|
+
const r = result;
|
|
3968
|
+
console.log(`✓ 成功 ${r.added.length},失败 ${r.failed.length}`);
|
|
3969
|
+
for (const a of r.added)
|
|
3970
|
+
console.log(` + ${a}`);
|
|
3971
|
+
for (const f of r.failed)
|
|
3972
|
+
console.log(` ✗ ${f.aid}: ${f.error}`);
|
|
3973
|
+
});
|
|
3974
|
+
return;
|
|
3975
|
+
}
|
|
3976
|
+
if (sub === 'kick') {
|
|
3977
|
+
const groupId = requireGroupId();
|
|
3978
|
+
const memberAid = args[3];
|
|
3979
|
+
if (!memberAid) {
|
|
3980
|
+
console.error('用法: evolclaw group kick <from> <group-id> <member-aid>');
|
|
3981
|
+
process.exit(1);
|
|
3982
|
+
}
|
|
3983
|
+
const result = await groupKick({ from, groupId, memberAid, ...commonOpts });
|
|
3984
|
+
outputResult(result, () => {
|
|
3985
|
+
console.log(`✓ 已踢出 ${memberAid}`);
|
|
3986
|
+
});
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
if (sub === 'members') {
|
|
3990
|
+
const groupId = requireGroupId();
|
|
3991
|
+
const pageStr = getArgValue(args, '--page');
|
|
3992
|
+
const sizeStr = getArgValue(args, '--size');
|
|
3993
|
+
const result = await groupMembers({
|
|
3994
|
+
from, groupId,
|
|
3995
|
+
page: pageStr !== undefined ? Number(pageStr) : undefined,
|
|
3996
|
+
size: sizeStr !== undefined ? Number(sizeStr) : undefined,
|
|
3997
|
+
...commonOpts,
|
|
3998
|
+
});
|
|
3999
|
+
outputResult(result, () => {
|
|
4000
|
+
const r = result;
|
|
4001
|
+
console.log(`共 ${r.total} 名成员(第 ${r.page} 页):`);
|
|
4002
|
+
for (const m of r.members) {
|
|
4003
|
+
console.log(` [${m.role}] ${m.aid}`);
|
|
4004
|
+
}
|
|
4005
|
+
});
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
if (sub === 'online') {
|
|
4009
|
+
const groupId = requireGroupId();
|
|
4010
|
+
const result = await groupOnline({ from, groupId, ...commonOpts });
|
|
4011
|
+
outputResult(result, () => {
|
|
4012
|
+
const r = result;
|
|
4013
|
+
console.log(`在线 ${r.online_count}/${r.total}:`);
|
|
4014
|
+
for (const m of r.members) {
|
|
4015
|
+
console.log(` 🟢 ${m.aid}`);
|
|
4016
|
+
}
|
|
4017
|
+
});
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
console.error(`未知子命令: ${sub}\n用法: evolclaw group [send|pull|ack|create|list|info|update|dissolve|join|leave|invite|kick|members|online]`);
|
|
4021
|
+
process.exit(1);
|
|
4022
|
+
}
|
|
4023
|
+
// ==================== Main ====================
|
|
4024
|
+
function getArgValue(args, flag) {
|
|
4025
|
+
const idx = args.indexOf(flag);
|
|
4026
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
4027
|
+
}
|
|
4028
|
+
/**
|
|
4029
|
+
* 收集位置参数(从 startIdx 开始),跳过 flag 及其值。
|
|
4030
|
+
* 已知"取值"的 flag 会消耗下一个 arg;已知"开关"的 flag 只占自身。
|
|
4031
|
+
*/
|
|
4032
|
+
function collectPositional(args, startIdx) {
|
|
4033
|
+
const VALUE_FLAGS = new Set([
|
|
4034
|
+
'--format', '--app', '--after-seq', '--limit', '--file', '--link',
|
|
4035
|
+
'--payload', '--title', '--description', '--text', '--transcript',
|
|
4036
|
+
'--as', '--content-type', '--mention', '--visibility', '--join-mode',
|
|
4037
|
+
'--group-id', '--name', '--message', '--answer', '--page', '--size',
|
|
4038
|
+
'--aun-path',
|
|
4039
|
+
]);
|
|
4040
|
+
const out = [];
|
|
4041
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
4042
|
+
const a = args[i];
|
|
4043
|
+
if (a.startsWith('--')) {
|
|
4044
|
+
if (VALUE_FLAGS.has(a))
|
|
4045
|
+
i++; // 跳过 flag 的值
|
|
4046
|
+
// else: 开关 flag,自身已被跳过
|
|
4047
|
+
continue;
|
|
4048
|
+
}
|
|
4049
|
+
out.push(a);
|
|
4050
|
+
}
|
|
4051
|
+
return out;
|
|
4052
|
+
}
|
|
4053
|
+
export async function main(args) {
|
|
4054
|
+
const cmd = args[0] || 'start';
|
|
4055
|
+
if (cmd === '--version' || cmd === '-v' || cmd === '-V') {
|
|
4056
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
|
|
4057
|
+
console.log(pkg.version);
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
4060
|
+
switch (cmd) {
|
|
4061
|
+
case 'init':
|
|
4062
|
+
if (args[1] === 'help') {
|
|
4063
|
+
console.log(`用法: evolclaw init [渠道] [选项]
|
|
4064
|
+
|
|
4065
|
+
仅初始化 defaults.json:
|
|
4066
|
+
evolclaw init 交互式(写完 defaults.json 后嵌套 agent new)
|
|
4067
|
+
evolclaw init --non-interactive [选项]
|
|
4068
|
+
--baseagent <claude|codex|gemini> 默认: PATH 中第一个可用项
|
|
4069
|
+
--force 已存在 defaults.json 时覆盖
|
|
4070
|
+
|
|
4071
|
+
配置渠道(先 evolclaw agent new 创建 agent):
|
|
4072
|
+
evolclaw init feishu 飞书扫码登录
|
|
4073
|
+
evolclaw init wechat 微信扫码登录
|
|
4074
|
+
evolclaw init dingtalk 钉钉扫码登录
|
|
4075
|
+
evolclaw init qqbot QQ 机器人扫码绑定
|
|
4076
|
+
evolclaw init wecom 企业微信 AI Bot 配置(手动输入)`);
|
|
4077
|
+
}
|
|
4078
|
+
else if (args[1] === 'wechat') {
|
|
4079
|
+
await cmdInitWechat();
|
|
4080
|
+
}
|
|
4081
|
+
else if (args[1] === 'feishu') {
|
|
4082
|
+
await cmdInitFeishu();
|
|
4083
|
+
}
|
|
4084
|
+
else if (args[1] === 'dingtalk') {
|
|
4085
|
+
await cmdInitDingtalk();
|
|
4086
|
+
}
|
|
4087
|
+
else if (args[1] === 'qqbot') {
|
|
4088
|
+
await cmdInitQQBot();
|
|
4089
|
+
}
|
|
4090
|
+
else if (args[1] === 'wecom') {
|
|
4091
|
+
await cmdInitWecom();
|
|
4092
|
+
}
|
|
4093
|
+
else if (args[1] && !args[1].startsWith('-')) {
|
|
4094
|
+
const supported = ['feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom'];
|
|
4095
|
+
console.error(`❌ 不支持的渠道: ${args[1]}`);
|
|
4096
|
+
console.error(` 支持的渠道: ${supported.join(', ')}`);
|
|
4097
|
+
process.exit(1);
|
|
4098
|
+
}
|
|
4099
|
+
else {
|
|
4100
|
+
const nonInteractive = args.includes('--non-interactive');
|
|
4101
|
+
await cmdInit({
|
|
4102
|
+
nonInteractive,
|
|
4103
|
+
baseagent: getArgValue(args, '--baseagent'),
|
|
4104
|
+
force: args.includes('--force'),
|
|
4105
|
+
});
|
|
4106
|
+
}
|
|
4107
|
+
break;
|
|
4108
|
+
case 'start':
|
|
4109
|
+
await cmdStart();
|
|
4110
|
+
break;
|
|
4111
|
+
case 'stop':
|
|
4112
|
+
await cmdStop();
|
|
4113
|
+
break;
|
|
4114
|
+
case 'restart':
|
|
4115
|
+
await cmdRestart({ clear: args.includes('--clear') });
|
|
4116
|
+
break;
|
|
4117
|
+
case 'status':
|
|
4118
|
+
await cmdStatus();
|
|
4119
|
+
break;
|
|
4120
|
+
case 'logs':
|
|
4121
|
+
cmdLogs(args.slice(1));
|
|
4122
|
+
break;
|
|
4123
|
+
case 'watch':
|
|
4124
|
+
if (args[1] === 'aid') {
|
|
4125
|
+
await cmdWatchAid();
|
|
4126
|
+
}
|
|
4127
|
+
else if (args[1] === 'msg') {
|
|
4128
|
+
const { cmdWatchMsg } = await import('../watch-msg.js');
|
|
4129
|
+
await cmdWatchMsg();
|
|
4130
|
+
}
|
|
4131
|
+
else if (args[1] === 'log') {
|
|
4132
|
+
cmdWatch();
|
|
4133
|
+
}
|
|
4134
|
+
else if (!args[1]) {
|
|
4135
|
+
await cmdWatchMenu();
|
|
4136
|
+
}
|
|
4137
|
+
else {
|
|
4138
|
+
cmdWatch();
|
|
4139
|
+
}
|
|
4140
|
+
break;
|
|
4141
|
+
case 'restart-monitor':
|
|
4142
|
+
await cmdRestartMonitor();
|
|
4143
|
+
break;
|
|
4144
|
+
case 'mv':
|
|
4145
|
+
await cmdMv(args[1], args[2]);
|
|
4146
|
+
break;
|
|
4147
|
+
case 'diagnose':
|
|
4148
|
+
await cmdDiagnose();
|
|
4149
|
+
break;
|
|
4150
|
+
case 'ctl':
|
|
4151
|
+
await cmdCtl(args.slice(1));
|
|
4152
|
+
break;
|
|
4153
|
+
case 'agent':
|
|
4154
|
+
await cmdAgent(args.slice(1));
|
|
4155
|
+
break;
|
|
4156
|
+
case 'aid': {
|
|
4157
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4158
|
+
suppressSdkLogs();
|
|
4159
|
+
await cmdAid(args.slice(1));
|
|
4160
|
+
break;
|
|
4161
|
+
}
|
|
4162
|
+
case 'rpc': {
|
|
4163
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4164
|
+
suppressSdkLogs();
|
|
4165
|
+
await cmdRpc(args.slice(1));
|
|
4166
|
+
break;
|
|
4167
|
+
}
|
|
4168
|
+
case 'storage': {
|
|
4169
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4170
|
+
suppressSdkLogs();
|
|
4171
|
+
await cmdStorage(args.slice(1));
|
|
4172
|
+
break;
|
|
4173
|
+
}
|
|
4174
|
+
case 'msg': {
|
|
4175
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4176
|
+
suppressSdkLogs();
|
|
4177
|
+
await cmdMsg(args.slice(1));
|
|
4178
|
+
break;
|
|
4179
|
+
}
|
|
4180
|
+
case 'group': {
|
|
4181
|
+
const { suppressSdkLogs } = await import('../aun/aid/index.js');
|
|
4182
|
+
suppressSdkLogs();
|
|
4183
|
+
await cmdGroup(args.slice(1));
|
|
4184
|
+
break;
|
|
4185
|
+
}
|
|
4186
|
+
default:
|
|
4187
|
+
console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|watch|ctl|diagnose|mv}
|
|
4188
|
+
|
|
4189
|
+
Commands:
|
|
4190
|
+
init 初始化 evolclaw home (${resolvePaths().defaultsConfig})
|
|
4191
|
+
init feishu 飞书扫码登录并写入配置
|
|
4192
|
+
init wechat 微信扫码登录并写入配置
|
|
4193
|
+
init dingtalk 钉钉扫码登录并写入配置
|
|
4194
|
+
init qqbot QQ 机器人扫码绑定并写入配置
|
|
4195
|
+
init wecom 企业微信 AI Bot 配置(手动输入 Bot ID + Secret)
|
|
4196
|
+
start 启动服务 (默认)
|
|
4197
|
+
stop 停止服务
|
|
4198
|
+
restart 重启服务
|
|
4199
|
+
--clear 顺带 SIGKILL 跨 HOME 残留的 evolclaw 主进程
|
|
4200
|
+
status 查看状态
|
|
4201
|
+
logs 查看日志 (tail -f, 着色渲染)
|
|
4202
|
+
--level error|warn 只显示指定级别及以上
|
|
4203
|
+
--module <name> 只显示指定模块(如 feishu、AgentRunner)
|
|
4204
|
+
--raw 原始输出,不着色
|
|
4205
|
+
watch 监控 logs/ 下所有 .log 文件(汇总实时输出,启动时显示最近 20 条)
|
|
4206
|
+
watch aid AID 连接状态实时监控(显示各 AID 在线/离线/重连状态)
|
|
4207
|
+
ctl 运行时自管理(模型切换、推理强度、压缩上下文等)
|
|
4208
|
+
evolclaw ctl help 查看完整命令列表
|
|
4209
|
+
agent 管理 EvolAgent
|
|
4210
|
+
agent 列出所有 agent
|
|
4211
|
+
agent <name> 查看指定 agent 详情
|
|
4212
|
+
agent new <name> 创建新 agent(交互式)
|
|
4213
|
+
agent new <name> --non-interactive ... 非交互创建(自动化)
|
|
4214
|
+
必填: --project <absolute path>
|
|
4215
|
+
可选: --baseagent <claude|codex|gemini> (默认: PATH 中第一个可用)
|
|
4216
|
+
--owner <aid>
|
|
4217
|
+
--name <display-name>
|
|
4218
|
+
--description <text>
|
|
4219
|
+
--force (覆盖已有 config.json)
|
|
4220
|
+
agent sync-aids 从本地 AID 批量同步创建 agent(以最早 agent 为模板)
|
|
4221
|
+
agent reload 全量 resync(扫磁盘,新增上线、删除下线、修改热更新)
|
|
4222
|
+
agent reload <n> 热重载指定 agent 配置
|
|
4223
|
+
aid AID 身份管理
|
|
4224
|
+
aid list 列出本地所有 AID
|
|
4225
|
+
aid show <aid> 查看本地 AID 详情(证书有效期、私钥状态)
|
|
4226
|
+
aid new <aid> 创建新 AID 身份
|
|
4227
|
+
aid delete <aid> 删除本地 AID
|
|
4228
|
+
aid lookup <aid> 远程探测 AID(是否存在 + 网关 + agent.md)
|
|
4229
|
+
aid agentmd put <aid> 签名并上传 agent.md
|
|
4230
|
+
aid agentmd get <aid> 下载并验签 agent.md
|
|
4231
|
+
rpc AUN RPC 调用
|
|
4232
|
+
rpc --as <aid> --params <json|jsonl|file>
|
|
4233
|
+
storage 文件存储
|
|
4234
|
+
storage upload <aid> <file> <path> [--public]
|
|
4235
|
+
storage download <aid> <url> [local-path]
|
|
4236
|
+
storage ls <aid> [prefix]
|
|
4237
|
+
storage rm <aid> <path>
|
|
4238
|
+
storage quota <aid>
|
|
4239
|
+
diagnose 诊断启动环境(配置、数据库、进程)
|
|
4240
|
+
mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
|
|
4241
|
+
|
|
4242
|
+
Environment:
|
|
4243
|
+
EVOLCLAW_HOME 数据目录 (默认: ~/.evolclaw)
|
|
4244
|
+
LOG_LEVEL 日志级别 (默认: INFO)
|
|
4245
|
+
MESSAGE_LOG 消息日志 (默认: true)
|
|
4246
|
+
EVENT_LOG 事件日志 (默认: true)`);
|
|
4247
|
+
process.exit(1);
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
// 直接运行时自动执行(node dist/cli/index.js ...)
|
|
4251
|
+
if (platform.isMainScript(import.meta.url)) {
|
|
4252
|
+
main(process.argv.slice(2));
|
|
4253
|
+
}
|