evolclaw 2.8.3 → 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 +102 -38
- package/dist/agents/codex-runner.js +11 -14
- package/dist/agents/gemini-runner.js +10 -12
- 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 +1051 -288
- 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/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +858 -847
- package/dist/core/evolagent-registry.js +191 -371
- 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 +309 -142
- package/dist/core/message/message-queue.js +3 -3
- 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 +431 -275
- 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 -591
- 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
package/dist/cli.js
DELETED
|
@@ -1,2178 +0,0 @@
|
|
|
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 { loadConfig, validateConfigIntegrity, resolveAnthropicConfig, normalizeChannelInstances, channelTypes } from './config.js';
|
|
9
|
-
import { migrateProject } from './utils/migrate-project.js';
|
|
10
|
-
import readline from 'readline';
|
|
11
|
-
import { cmdInit } from './utils/init.js';
|
|
12
|
-
import { ipcQuery } from './ipc.js';
|
|
13
|
-
import { cmdInitWechat, cmdInitFeishu, cmdInitAun, cmdInitDingtalk, cmdInitQQBot, cmdInitWecom } from './utils/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/upgrade.js';
|
|
17
|
-
// Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
|
|
18
|
-
process.removeAllListeners('warning');
|
|
19
|
-
process.on('warning', (w) => { if (w.name === 'ExperimentalWarning')
|
|
20
|
-
return; process.stderr.write((w.stack ?? String(w)) + '\n'); });
|
|
21
|
-
const execFileAsync = promisify(execFile);
|
|
22
|
-
// 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
|
|
23
|
-
function cleanEnv() {
|
|
24
|
-
for (const key of [
|
|
25
|
-
'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT',
|
|
26
|
-
'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
|
27
|
-
'CLAUDE_CONFIG_DIR',
|
|
28
|
-
]) {
|
|
29
|
-
delete process.env[key];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function isRunning(pidFile) {
|
|
33
|
-
if (!fs.existsSync(pidFile))
|
|
34
|
-
return null;
|
|
35
|
-
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
36
|
-
if (platform.isProcessRunning(pid)) {
|
|
37
|
-
return pid;
|
|
38
|
-
}
|
|
39
|
-
fs.unlinkSync(pidFile);
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
function rotateLogs(logDir) {
|
|
43
|
-
if (!fs.existsSync(logDir))
|
|
44
|
-
return;
|
|
45
|
-
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
|
46
|
-
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
47
|
-
for (const file of fs.readdirSync(logDir)) {
|
|
48
|
-
const filePath = path.join(logDir, file);
|
|
49
|
-
if (file.endsWith('.log')) {
|
|
50
|
-
// 轮转超大日志
|
|
51
|
-
const stat = fs.statSync(filePath);
|
|
52
|
-
if (stat.size > MAX_SIZE) {
|
|
53
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
54
|
-
const newPath = `${filePath}.${timestamp}`;
|
|
55
|
-
fs.renameSync(filePath, newPath);
|
|
56
|
-
console.log(` Rotated: ${file} -> ${path.basename(newPath)}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
else if (file.includes('.log.') || /^aun-\d{8}\.log$/.test(file)) {
|
|
60
|
-
// 清理 7 天前的旧日志(含按日轮转的 aun-YYYYMMDD.log)
|
|
61
|
-
const stat = fs.statSync(filePath);
|
|
62
|
-
if (stat.mtimeMs < cutoff) {
|
|
63
|
-
fs.unlinkSync(filePath);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function countLines(pkgRoot, logDir) {
|
|
69
|
-
const srcDir = path.join(pkgRoot, 'src');
|
|
70
|
-
const statsFile = path.join(logDir, 'line-stats.log');
|
|
71
|
-
const countDir = (dir) => {
|
|
72
|
-
if (!fs.existsSync(dir))
|
|
73
|
-
return 0;
|
|
74
|
-
let total = 0;
|
|
75
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
76
|
-
const full = path.join(dir, entry.name);
|
|
77
|
-
if (entry.isDirectory()) {
|
|
78
|
-
total += countDir(full);
|
|
79
|
-
}
|
|
80
|
-
else if (entry.name.endsWith('.ts')) {
|
|
81
|
-
total += fs.readFileSync(full, 'utf-8').split('\n').length;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return total;
|
|
85
|
-
};
|
|
86
|
-
const countFile = (filePath) => {
|
|
87
|
-
if (!fs.existsSync(filePath))
|
|
88
|
-
return 0;
|
|
89
|
-
return fs.readFileSync(filePath, 'utf-8').split('\n').length;
|
|
90
|
-
};
|
|
91
|
-
console.log('\n[launcher] 正在统计代码行数...\n');
|
|
92
|
-
const core = countDir(path.join(srcDir, 'core'));
|
|
93
|
-
const agents = countDir(path.join(srcDir, 'agents'));
|
|
94
|
-
const channels = countDir(path.join(srcDir, 'channels'));
|
|
95
|
-
const utils = countDir(path.join(srcDir, 'utils'));
|
|
96
|
-
const entry = countFile(path.join(srcDir, 'index.ts'))
|
|
97
|
-
+ countFile(path.join(srcDir, 'config.ts'))
|
|
98
|
-
+ countFile(path.join(srcDir, 'types.ts'))
|
|
99
|
-
+ countFile(path.join(srcDir, 'cli.ts'))
|
|
100
|
-
+ countFile(path.join(srcDir, 'ipc.ts'))
|
|
101
|
-
+ countFile(path.join(srcDir, 'paths.ts'));
|
|
102
|
-
const total = core + agents + channels + utils + entry;
|
|
103
|
-
console.log('==================================================');
|
|
104
|
-
console.log('EvolClaw 代码统计');
|
|
105
|
-
console.log('==================================================');
|
|
106
|
-
console.log(`核心模块: ${String(core).padStart(8)} 行`);
|
|
107
|
-
console.log(`Agent 模块: ${String(agents).padStart(8)} 行`);
|
|
108
|
-
console.log(`渠道适配: ${String(channels).padStart(8)} 行`);
|
|
109
|
-
console.log(`工具库: ${String(utils).padStart(8)} 行`);
|
|
110
|
-
console.log(`入口与配置: ${String(entry).padStart(8)} 行`);
|
|
111
|
-
console.log('--------------------------------------------------');
|
|
112
|
-
console.log(`总计: ${String(total).padStart(8)} 行`);
|
|
113
|
-
console.log('==================================================');
|
|
114
|
-
// 追加历史记录(仅在数据变化时)
|
|
115
|
-
let shouldAppend = true;
|
|
116
|
-
if (fs.existsSync(statsFile)) {
|
|
117
|
-
const lines = fs.readFileSync(statsFile, 'utf-8').trim().split('\n');
|
|
118
|
-
if (lines.length > 0) {
|
|
119
|
-
const lastLine = lines[lines.length - 1];
|
|
120
|
-
const lastTotal = lastLine.split('\t').pop();
|
|
121
|
-
if (lastTotal === String(total)) {
|
|
122
|
-
shouldAppend = false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (shouldAppend) {
|
|
127
|
-
const _d = new Date();
|
|
128
|
-
const _p = (n) => String(n).padStart(2, '0');
|
|
129
|
-
const now = `${_d.getFullYear()}-${_p(_d.getMonth() + 1)}-${_p(_d.getDate())} ${_p(_d.getHours())}:${_p(_d.getMinutes())}:${_p(_d.getSeconds())}`;
|
|
130
|
-
fs.appendFileSync(statsFile, `${now}\t${core}\t${agents}\t${channels}\t${utils}\t${entry}\t${total}\n`);
|
|
131
|
-
}
|
|
132
|
-
showHistory(statsFile);
|
|
133
|
-
}
|
|
134
|
-
function showHistory(statsFile) {
|
|
135
|
-
if (!fs.existsSync(statsFile))
|
|
136
|
-
return;
|
|
137
|
-
const lines = fs.readFileSync(statsFile, 'utf-8').trim().split('\n');
|
|
138
|
-
if (lines.length < 2)
|
|
139
|
-
return;
|
|
140
|
-
const recent = lines.slice(-8);
|
|
141
|
-
console.log('\n==================================================');
|
|
142
|
-
console.log('历史记录(最近 8 次)');
|
|
143
|
-
console.log('==================================================');
|
|
144
|
-
console.log(`${'时间'.padEnd(20)} ${'核心'.padStart(6)} ${'Agent'.padStart(6)} ${'渠道'.padStart(6)} ${'工具'.padStart(6)} ${'入口'.padStart(6)} ${'总计'.padStart(6)} ${'变化'.padStart(8)}`);
|
|
145
|
-
console.log('--------------------------------------------------');
|
|
146
|
-
let prevTotal = null;
|
|
147
|
-
for (const line of recent) {
|
|
148
|
-
const parts = line.split('\t');
|
|
149
|
-
// 兼容旧格式(6列: time,core,ch,utils,entry,total)和新格式(7列: +agents)
|
|
150
|
-
let time, c, a, ch, u, e, t;
|
|
151
|
-
if (parts.length >= 7) {
|
|
152
|
-
[time, c, a, ch, u, e, t] = parts;
|
|
153
|
-
}
|
|
154
|
-
else if (parts.length >= 6) {
|
|
155
|
-
[time, c, ch, u, e, t] = parts;
|
|
156
|
-
a = '-';
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
const total = parseInt(t, 10);
|
|
162
|
-
let diff = '-';
|
|
163
|
-
if (prevTotal !== null) {
|
|
164
|
-
const change = total - prevTotal;
|
|
165
|
-
diff = change >= 0 ? `+${change}` : `${change}`;
|
|
166
|
-
}
|
|
167
|
-
console.log(`${time.padEnd(20)} ${c.padStart(6)} ${a.padStart(6)} ${ch.padStart(6)} ${u.padStart(6)} ${e.padStart(6)} ${t.padStart(6)} ${diff.padStart(8)}`);
|
|
168
|
-
prevTotal = total;
|
|
169
|
-
}
|
|
170
|
-
console.log('==================================================');
|
|
171
|
-
}
|
|
172
|
-
// ==================== Commands ====================
|
|
173
|
-
async function cmdStart() {
|
|
174
|
-
const p = resolvePaths();
|
|
175
|
-
ensureDataDirs();
|
|
176
|
-
// 检查配置文件
|
|
177
|
-
if (!fs.existsSync(p.config)) {
|
|
178
|
-
console.log('❌ 配置文件不存在,请先运行 evolclaw init');
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
// 配置完整性校验
|
|
182
|
-
try {
|
|
183
|
-
const config = loadConfig(p.config);
|
|
184
|
-
const integrity = validateConfigIntegrity(config);
|
|
185
|
-
if (!integrity.valid) {
|
|
186
|
-
console.log(`❌ 配置文件完整性校验失败:`);
|
|
187
|
-
for (const reason of integrity.reasons) {
|
|
188
|
-
console.log(` - ${reason}`);
|
|
189
|
-
}
|
|
190
|
-
console.log(`\n配置文件: ${p.config}`);
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch (e) {
|
|
195
|
-
console.log(`❌ 配置文件加载失败: ${e.message}`);
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
// 检查 PID 文件
|
|
199
|
-
const pid = isRunning(p.pid);
|
|
200
|
-
if (pid) {
|
|
201
|
-
console.log(`❌ EvolClaw is already running (PID: ${pid})`);
|
|
202
|
-
console.log(' 使用 evolclaw restart 重启,或 evolclaw stop 先停止');
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
205
|
-
// 检查是否有残留进程(PID 文件已丢失但进程还在)
|
|
206
|
-
// 只清理属于当前 EVOLCLAW_HOME 的进程,避免误杀其他实例
|
|
207
|
-
let hasOrphan = false;
|
|
208
|
-
const evolclawMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
209
|
-
const allPids = platform.findProcesses(evolclawMain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
210
|
-
const orphanPids = allPids.filter(pid => platform.getProcessEnv(pid, 'EVOLCLAW_HOME') === p.root);
|
|
211
|
-
if (orphanPids.length > 0) {
|
|
212
|
-
console.log(`⚠ 发现 ${orphanPids.length} 个残留进程,正在清理...`);
|
|
213
|
-
for (const p of orphanPids) {
|
|
214
|
-
platform.killProcess(p);
|
|
215
|
-
}
|
|
216
|
-
hasOrphan = true;
|
|
217
|
-
}
|
|
218
|
-
// 如果清理了残留进程,等待它们退出
|
|
219
|
-
if (hasOrphan) {
|
|
220
|
-
await sleep(2000);
|
|
221
|
-
}
|
|
222
|
-
console.log('🚀 Starting EvolClaw...');
|
|
223
|
-
rotateLogs(p.logs);
|
|
224
|
-
cleanEnv();
|
|
225
|
-
// 删除旧的 ready signal
|
|
226
|
-
try {
|
|
227
|
-
fs.unlinkSync(p.readySignal);
|
|
228
|
-
}
|
|
229
|
-
catch { }
|
|
230
|
-
const stdoutLog = path.join(p.logs, 'stdout.log');
|
|
231
|
-
const out = fs.openSync(stdoutLog, 'a');
|
|
232
|
-
const err = fs.openSync(stdoutLog, 'a');
|
|
233
|
-
const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
234
|
-
const child = spawn('node', ['--no-warnings=ExperimentalWarning', appMain], {
|
|
235
|
-
detached: true,
|
|
236
|
-
stdio: ['ignore', out, err],
|
|
237
|
-
env: {
|
|
238
|
-
...process.env,
|
|
239
|
-
EVOLCLAW_HOME: p.root,
|
|
240
|
-
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
|
241
|
-
MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
|
|
242
|
-
EVENT_LOG: process.env.EVENT_LOG || 'true',
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
fs.writeFileSync(p.pid, String(child.pid));
|
|
246
|
-
child.unref();
|
|
247
|
-
// 等待 ready signal(最多 30 秒,AUN sidecar 超时 15s + 其他通道连接)
|
|
248
|
-
const startTime = Date.now();
|
|
249
|
-
const checkReady = () => {
|
|
250
|
-
// ready signal 出现(优先检查,避免 Windows 上 isRunning 误判)
|
|
251
|
-
if (fs.existsSync(p.readySignal)) {
|
|
252
|
-
const pid = isRunning(p.pid);
|
|
253
|
-
console.log(`✓ EvolClaw started successfully (PID: ${pid})`);
|
|
254
|
-
console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
|
|
255
|
-
console.log(` Logs: ${p.logs}/`);
|
|
256
|
-
// 从主日志提取渠道连接摘要
|
|
257
|
-
const mainLog = path.join(p.logs, 'evolclaw.log');
|
|
258
|
-
if (fs.existsSync(mainLog)) {
|
|
259
|
-
const logLines = fs.readFileSync(mainLog, 'utf-8').split('\n');
|
|
260
|
-
// 从末尾往前找最近一次启动的摘要
|
|
261
|
-
let channelSummary = '';
|
|
262
|
-
for (let i = logLines.length - 1; i >= 0; i--) {
|
|
263
|
-
if (logLines[i].includes('EvolClaw is running with')) {
|
|
264
|
-
channelSummary = logLines[i];
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (channelSummary) {
|
|
269
|
-
const match = channelSummary.match(/running with .+/);
|
|
270
|
-
if (match)
|
|
271
|
-
console.log(` ${match[0]}`);
|
|
272
|
-
}
|
|
273
|
-
// 最近一次启动的失败信息
|
|
274
|
-
let lastReadyIdx = -1;
|
|
275
|
-
for (let i = logLines.length - 1; i >= 0; i--) {
|
|
276
|
-
if (logLines[i].includes('Ready signal written')) {
|
|
277
|
-
lastReadyIdx = i;
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (lastReadyIdx > 0) {
|
|
282
|
-
for (let i = Math.max(0, lastReadyIdx - 20); i < lastReadyIdx; i++) {
|
|
283
|
-
const line = logLines[i];
|
|
284
|
-
if (line.includes('failed to connect') || line.includes('Failed to create channel')) {
|
|
285
|
-
const match = line.match(/\[WARN\]\s*(.+)/);
|
|
286
|
-
console.log(` ⚠ ${match ? match[1] : line.trim()}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
console.log('');
|
|
292
|
-
// 代码统计仅在开发环境显示(EVOLCLAW_HOME 指向包目录)
|
|
293
|
-
if (resolveRoot() === getPackageRoot()) {
|
|
294
|
-
countLines(getPackageRoot(), p.logs);
|
|
295
|
-
}
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
// 超时
|
|
299
|
-
if (Date.now() - startTime > 30000) {
|
|
300
|
-
console.log('❌ Failed to start EvolClaw (ready signal timeout)');
|
|
301
|
-
console.log('');
|
|
302
|
-
console.log('📝 Error details (last 10 lines of stdout):');
|
|
303
|
-
if (fs.existsSync(stdoutLog)) {
|
|
304
|
-
const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
|
|
305
|
-
console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
|
|
306
|
-
}
|
|
307
|
-
process.exit(1);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
// 进程已退出且无 ready signal
|
|
311
|
-
if (!isRunning(p.pid)) {
|
|
312
|
-
// 给进程一点时间写 ready signal(可能刚好在写入中)
|
|
313
|
-
if (Date.now() - startTime > 3000) {
|
|
314
|
-
console.log('❌ Failed to start EvolClaw');
|
|
315
|
-
console.log('');
|
|
316
|
-
console.log('📝 Error details (last 10 lines of stdout):');
|
|
317
|
-
if (fs.existsSync(stdoutLog)) {
|
|
318
|
-
const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
|
|
319
|
-
console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
|
|
320
|
-
}
|
|
321
|
-
process.exit(1);
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
setTimeout(checkReady, 500);
|
|
326
|
-
};
|
|
327
|
-
setTimeout(checkReady, 1000);
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* 停止进程并等待退出,返回 Promise
|
|
331
|
-
*/
|
|
332
|
-
async function stopAndWait(pidFile) {
|
|
333
|
-
const pid = isRunning(pidFile);
|
|
334
|
-
if (!pid)
|
|
335
|
-
return;
|
|
336
|
-
console.log(`🛑 Stopping EvolClaw (PID: ${pid})...`);
|
|
337
|
-
platform.killProcess(pid);
|
|
338
|
-
await new Promise((resolve) => {
|
|
339
|
-
let waited = 0;
|
|
340
|
-
const check = setInterval(() => {
|
|
341
|
-
waited++;
|
|
342
|
-
if (!platform.isProcessRunning(pid)) {
|
|
343
|
-
clearInterval(check);
|
|
344
|
-
try {
|
|
345
|
-
fs.unlinkSync(pidFile);
|
|
346
|
-
}
|
|
347
|
-
catch { }
|
|
348
|
-
console.log('✓ EvolClaw stopped');
|
|
349
|
-
resolve();
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (waited >= 10) {
|
|
353
|
-
clearInterval(check);
|
|
354
|
-
platform.killProcess(pid, true);
|
|
355
|
-
try {
|
|
356
|
-
fs.unlinkSync(pidFile);
|
|
357
|
-
}
|
|
358
|
-
catch { }
|
|
359
|
-
console.log('✓ EvolClaw stopped (forced)');
|
|
360
|
-
resolve();
|
|
361
|
-
}
|
|
362
|
-
}, 1000);
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
async function cmdStop() {
|
|
366
|
-
const p = resolvePaths();
|
|
367
|
-
const pid = isRunning(p.pid);
|
|
368
|
-
if (!pid) {
|
|
369
|
-
console.log('⚠ EvolClaw is not running');
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
await stopAndWait(p.pid);
|
|
373
|
-
}
|
|
374
|
-
async function cmdRestart() {
|
|
375
|
-
console.log('🔄 Restarting EvolClaw...');
|
|
376
|
-
const p = resolvePaths();
|
|
377
|
-
// 版本检查与自动升级
|
|
378
|
-
console.log('📦 Checking for updates...');
|
|
379
|
-
const upgrade = await tryUpgrade();
|
|
380
|
-
switch (upgrade.status) {
|
|
381
|
-
case 'upgraded':
|
|
382
|
-
console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
|
|
383
|
-
break;
|
|
384
|
-
case 'no-update':
|
|
385
|
-
console.log(`✓ Already up to date (${upgrade.from})`);
|
|
386
|
-
break;
|
|
387
|
-
case 'skipped':
|
|
388
|
-
console.log(upgrade.error
|
|
389
|
-
? '⏭ Skipped upgrade (network unavailable)'
|
|
390
|
-
: '⏭ Skipped upgrade check (dev mode)');
|
|
391
|
-
break;
|
|
392
|
-
case 'failed':
|
|
393
|
-
console.log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}), continuing with current version`);
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
await stopAndWait(p.pid);
|
|
397
|
-
setTimeout(() => cmdStart(), 1000);
|
|
398
|
-
}
|
|
399
|
-
function formatTimeAgo(ms) {
|
|
400
|
-
const sec = Math.floor(ms / 1000);
|
|
401
|
-
if (sec < 60)
|
|
402
|
-
return '刚刚';
|
|
403
|
-
const min = Math.floor(sec / 60);
|
|
404
|
-
if (min < 60)
|
|
405
|
-
return `${min}分钟前`;
|
|
406
|
-
const hour = Math.floor(min / 60);
|
|
407
|
-
if (hour < 24)
|
|
408
|
-
return `${hour}小时前`;
|
|
409
|
-
const day = Math.floor(hour / 24);
|
|
410
|
-
return `${day}天前`;
|
|
411
|
-
}
|
|
412
|
-
function showConfigChannels(config) {
|
|
413
|
-
const groups = [];
|
|
414
|
-
const channelChecks = [
|
|
415
|
-
{ type: 'feishu', isValid: (inst) => !!inst.appId && inst.enabled !== false },
|
|
416
|
-
{ type: 'wechat', isValid: (inst) => !!inst.token && inst.enabled !== false },
|
|
417
|
-
{ type: 'aun', isValid: (inst) => !!inst.aid && inst.enabled !== false && !inst.aid.includes('your-') && !inst.aid.includes('placeholder') },
|
|
418
|
-
{ type: 'dingtalk', isValid: (inst) => !!inst.clientId && inst.enabled !== false && !inst.clientId.includes('your-') && !inst.clientId.includes('placeholder') },
|
|
419
|
-
{ type: 'qqbot', isValid: (inst) => !!inst.appId && inst.enabled !== false && !inst.appId.includes('your-') && !inst.appId.includes('placeholder') },
|
|
420
|
-
{ type: 'wecom', isValid: (inst) => !!inst.botId && inst.enabled !== false && !inst.botId.includes('your-') && !inst.botId.includes('placeholder') },
|
|
421
|
-
];
|
|
422
|
-
for (const { type, isValid } of channelChecks) {
|
|
423
|
-
const raw = config.channels?.[type];
|
|
424
|
-
if (!raw)
|
|
425
|
-
continue;
|
|
426
|
-
if (Array.isArray(raw)) {
|
|
427
|
-
const names = raw.filter(isValid).map((inst) => inst.name || type);
|
|
428
|
-
if (names.length > 0)
|
|
429
|
-
groups.push({ type, instances: names });
|
|
430
|
-
}
|
|
431
|
-
else if (isValid(raw)) {
|
|
432
|
-
groups.push({ type, instances: [raw.name || type] });
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
if (groups.length > 0) {
|
|
436
|
-
for (const g of groups) {
|
|
437
|
-
if (g.instances.length === 1) {
|
|
438
|
-
console.log(` ${g.instances[0]}: ✓ Configured`);
|
|
439
|
-
}
|
|
440
|
-
else {
|
|
441
|
-
console.log(` ${g.type}: [${g.instances.join(', ')}]`);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
console.log(' (no channels configured)');
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
async function cmdStatus() {
|
|
450
|
-
const p = resolvePaths();
|
|
451
|
-
const pid = isRunning(p.pid);
|
|
452
|
-
if (pid) {
|
|
453
|
-
console.log(`✓ EvolClaw is running (PID: ${pid})`);
|
|
454
|
-
console.log('');
|
|
455
|
-
console.log('📊 Process Info:');
|
|
456
|
-
try {
|
|
457
|
-
const info = platform.getProcessInfo(pid);
|
|
458
|
-
if (info.uptime)
|
|
459
|
-
console.log(` Uptime: ${info.uptime}`);
|
|
460
|
-
if (info.cpu)
|
|
461
|
-
console.log(` CPU: ${info.cpu}%`);
|
|
462
|
-
if (info.memory) {
|
|
463
|
-
const memKB = parseInt(info.memory, 10);
|
|
464
|
-
const memStr = memKB >= 1024 ? `${(memKB / 1024).toFixed(0)} MB` : `${memKB} KB`;
|
|
465
|
-
console.log(` Memory: ${memStr}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
catch { }
|
|
469
|
-
console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
|
|
470
|
-
// Runtime statistics (only when running)
|
|
471
|
-
if (fs.existsSync(p.db)) {
|
|
472
|
-
try {
|
|
473
|
-
const Database = await import('node:sqlite');
|
|
474
|
-
const db = new Database.DatabaseSync(p.db);
|
|
475
|
-
// Get recent active sessions (last 5)
|
|
476
|
-
const recentSessions = db.prepare(`
|
|
477
|
-
SELECT id, project_path, name, channel, chat_type, thread_id, agent_session_id, agent_id, metadata, updated_at
|
|
478
|
-
FROM sessions
|
|
479
|
-
WHERE deleted_at IS NULL
|
|
480
|
-
ORDER BY updated_at DESC
|
|
481
|
-
LIMIT 5
|
|
482
|
-
`).all();
|
|
483
|
-
// Detect orphan sessions (channel no longer in config)
|
|
484
|
-
let orphanCount = 0;
|
|
485
|
-
try {
|
|
486
|
-
const config = loadConfig(p.config);
|
|
487
|
-
const configChannelNames = new Set();
|
|
488
|
-
for (const type of channelTypes) {
|
|
489
|
-
const raw = config.channels?.[type];
|
|
490
|
-
const instances = normalizeChannelInstances(raw, type);
|
|
491
|
-
for (const inst of instances) {
|
|
492
|
-
configChannelNames.add(inst.name);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
const channelCounts = db.prepare(`
|
|
496
|
-
SELECT channel, COUNT(*) as c FROM sessions
|
|
497
|
-
WHERE deleted_at IS NULL
|
|
498
|
-
GROUP BY channel
|
|
499
|
-
`).all();
|
|
500
|
-
for (const row of channelCounts) {
|
|
501
|
-
if (!configChannelNames.has(row.channel)) {
|
|
502
|
-
orphanCount += row.c;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
catch { }
|
|
507
|
-
db.close();
|
|
508
|
-
if (recentSessions.length > 0) {
|
|
509
|
-
console.log('');
|
|
510
|
-
console.log('📋 Recent Active Sessions:');
|
|
511
|
-
for (const s of recentSessions) {
|
|
512
|
-
const projectName = path.basename(s.project_path);
|
|
513
|
-
const sessionType = s.thread_id ? '话题会话' : '主会话';
|
|
514
|
-
const chatType = s.chat_type === 'group' ? '群聊' : '单聊';
|
|
515
|
-
const sessionName = s.name || '默认会话';
|
|
516
|
-
const timeAgo = formatTimeAgo(Date.now() - s.updated_at);
|
|
517
|
-
const meta = s.metadata ? JSON.parse(s.metadata) : {};
|
|
518
|
-
const dot = meta.isActive ? '•' : '○';
|
|
519
|
-
const agentId = s.agent_session_id ? ` [${s.agent_session_id}]` : '';
|
|
520
|
-
const agentType = s.agent_id || 'claude';
|
|
521
|
-
console.log(` ${dot} [${agentType}] ${projectName} / ${sessionName} (${sessionType}, ${chatType})${agentId} - ${timeAgo}`);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (orphanCount > 0) {
|
|
525
|
-
console.log('');
|
|
526
|
-
console.log(`⚠ Orphan sessions: ${orphanCount}`);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
catch { }
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
console.log('⚠ EvolClaw is not running');
|
|
534
|
-
if (fs.existsSync(p.pid)) {
|
|
535
|
-
console.log(` Stale PID file found: ${p.pid}`);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
// Session & Project statistics (always show if DB exists)
|
|
539
|
-
if (fs.existsSync(p.db)) {
|
|
540
|
-
console.log('');
|
|
541
|
-
console.log('📦 Sessions & Projects:');
|
|
542
|
-
try {
|
|
543
|
-
const Database = await import('node:sqlite');
|
|
544
|
-
const db = new Database.DatabaseSync(p.db);
|
|
545
|
-
const totalSessions = db.prepare('SELECT count(*) as cnt FROM sessions WHERE deleted_at IS NULL').get();
|
|
546
|
-
const activeSessions = db.prepare("SELECT count(*) as cnt FROM sessions WHERE json_extract(metadata, '$.isActive') = true AND deleted_at IS NULL").get();
|
|
547
|
-
const uniqueChats = db.prepare('SELECT count(DISTINCT channel_id) as cnt FROM sessions WHERE deleted_at IS NULL').get();
|
|
548
|
-
const projects = db.prepare('SELECT count(DISTINCT project_path) as cnt FROM sessions WHERE deleted_at IS NULL').get();
|
|
549
|
-
db.close();
|
|
550
|
-
console.log(` Total sessions: ${totalSessions.cnt} (active: ${activeSessions.cnt})`);
|
|
551
|
-
console.log(` Unique chats: ${uniqueChats.cnt}`);
|
|
552
|
-
console.log(` Projects: ${projects.cnt}`);
|
|
553
|
-
}
|
|
554
|
-
catch { }
|
|
555
|
-
}
|
|
556
|
-
// Channel status
|
|
557
|
-
if (fs.existsSync(p.config)) {
|
|
558
|
-
console.log('');
|
|
559
|
-
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
560
|
-
if (pid) {
|
|
561
|
-
// Running: query IPC for real-time status
|
|
562
|
-
const status = await ipcQuery(p.socket, { type: 'status' });
|
|
563
|
-
if (status) {
|
|
564
|
-
console.log('🔌 Channels (live):');
|
|
565
|
-
// Group channels by channelType
|
|
566
|
-
const groups = new Map();
|
|
567
|
-
for (const [name, ch] of Object.entries(status.channels)) {
|
|
568
|
-
const type = ch.channelType || name;
|
|
569
|
-
if (!groups.has(type))
|
|
570
|
-
groups.set(type, []);
|
|
571
|
-
groups.get(type).push({ name, ch: ch });
|
|
572
|
-
}
|
|
573
|
-
for (const [type, instances] of groups) {
|
|
574
|
-
if (instances.length === 1) {
|
|
575
|
-
// Single instance: show instance name directly
|
|
576
|
-
const { name, ch } = instances[0];
|
|
577
|
-
const label = ch.connected ? '✓ Connected' : ch.reconnectAttempt ? `⏳ Reconnecting (${ch.reconnectAttempt}/${ch.maxAttempts})` : '✗ Disconnected';
|
|
578
|
-
console.log(` ${name}: ${label}`);
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
// Multi-instance: feishu [name1 ✓, name2 ✗]
|
|
582
|
-
const parts = instances.map(({ name, ch }) => {
|
|
583
|
-
const icon = ch.connected ? '✓' : ch.reconnectAttempt ? '⏳' : '✗';
|
|
584
|
-
return `${name} ${icon}`;
|
|
585
|
-
});
|
|
586
|
-
console.log(` ${type}: [${parts.join(', ')}]`);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
if (status.stats) {
|
|
590
|
-
console.log('');
|
|
591
|
-
console.log('📊 Last hour:');
|
|
592
|
-
console.log(` Messages: ${status.stats.received} received, ${status.stats.completed} completed`);
|
|
593
|
-
if (status.stats.errors > 0)
|
|
594
|
-
console.log(` Errors: ${status.stats.errors}`);
|
|
595
|
-
if (status.stats.completed > 0)
|
|
596
|
-
console.log(` Avg response: ${(status.stats.avgResponseMs / 1000).toFixed(1)}s`);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
else {
|
|
600
|
-
// IPC unreachable but PID exists — show config only
|
|
601
|
-
console.log('🔌 Channels (IPC unreachable):');
|
|
602
|
-
showConfigChannels(config);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
else {
|
|
606
|
-
console.log('🔌 Channel Configuration:');
|
|
607
|
-
showConfigChannels(config);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
// EvolAgent summary (via IPC, only when running)
|
|
611
|
-
if (pid) {
|
|
612
|
-
try {
|
|
613
|
-
const agentResult = await ipcQuery(p.socket, { type: 'evolagent.list' });
|
|
614
|
-
if (agentResult?.ok && agentResult.agents?.length > 0) {
|
|
615
|
-
const agents = agentResult.agents.filter((a) => !a.isDefault);
|
|
616
|
-
if (agents.length > 0) {
|
|
617
|
-
console.log('');
|
|
618
|
-
console.log('🤖 EvolAgents:');
|
|
619
|
-
for (const a of agents) {
|
|
620
|
-
const statusIcon = a.status === 'running' ? '●' : a.status === 'error' ? '✗' : a.status === 'disabled' ? '○' : '◌';
|
|
621
|
-
const channels = a.channels?.join(', ') || '—';
|
|
622
|
-
console.log(` ${statusIcon} ${a.name.padEnd(14)} ${a.status.padEnd(10)} ${channels}`);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
// IPC query for agents failed — skip section
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
// Log line pattern: [timestamp] [LEVEL] [Module?] message
|
|
633
|
-
const LOG_RE = /^(\[[^\]]+\]) (\[(?:INFO|WARN|ERROR|DEBUG)\]) ((?:\[[^\]]+\] )*)(.*)$/;
|
|
634
|
-
const MAX_MSG = 200; // truncate long messages
|
|
635
|
-
function makeColors(enabled) {
|
|
636
|
-
const e = (code) => enabled ? code : '';
|
|
637
|
-
return {
|
|
638
|
-
reset: e('\x1b[0m'), dim: e('\x1b[2m'), bold: e('\x1b[1m'),
|
|
639
|
-
red: e('\x1b[31m'), yellow: e('\x1b[33m'), cyan: e('\x1b[36m'),
|
|
640
|
-
magenta: e('\x1b[35m'), gray: e('\x1b[90m'),
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
function renderLogLine(line, opts) {
|
|
644
|
-
const m = line.match(LOG_RE);
|
|
645
|
-
if (!m)
|
|
646
|
-
return line; // passthrough non-standard lines (stack traces etc.)
|
|
647
|
-
const [, ts, levelTag, modulePart, msg] = m;
|
|
648
|
-
const level = levelTag.slice(1, -1); // strip brackets
|
|
649
|
-
// Level filter
|
|
650
|
-
if (opts.level) {
|
|
651
|
-
const want = opts.level.toUpperCase();
|
|
652
|
-
if (want === 'ERROR' && level !== 'ERROR')
|
|
653
|
-
return null;
|
|
654
|
-
if (want === 'WARN' && level !== 'WARN' && level !== 'ERROR')
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
// Module filter (case-insensitive substring match)
|
|
658
|
-
if (opts.module) {
|
|
659
|
-
const mod = modulePart.toLowerCase();
|
|
660
|
-
if (!mod.includes(opts.module.toLowerCase()))
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
// Truncate long messages (always, regardless of color)
|
|
664
|
-
const truncated = msg.length > MAX_MSG ? msg.slice(0, MAX_MSG) + '…' : msg;
|
|
665
|
-
const C = makeColors(opts.color);
|
|
666
|
-
// Color by level
|
|
667
|
-
const levelColor = level === 'ERROR' ? C.red : level === 'WARN' ? C.yellow : level === 'DEBUG' ? C.gray : '';
|
|
668
|
-
// Highlight user messages: [channel] channelId: text
|
|
669
|
-
const isUserMsg = modulePart && /^\S+: .+$/.test(truncated);
|
|
670
|
-
const renderedMsg = isUserMsg
|
|
671
|
-
? C.cyan + truncated + C.reset
|
|
672
|
-
: levelColor + truncated + C.reset;
|
|
673
|
-
return (C.dim + ts + C.reset + ' ' +
|
|
674
|
-
levelColor + C.bold + levelTag + C.reset + ' ' +
|
|
675
|
-
C.magenta + modulePart.trimEnd() + C.reset +
|
|
676
|
-
(modulePart ? ' ' : '') +
|
|
677
|
-
renderedMsg);
|
|
678
|
-
}
|
|
679
|
-
function cmdLogs(args) {
|
|
680
|
-
const raw = args.includes('--raw');
|
|
681
|
-
const noColor = args.includes('--no-color');
|
|
682
|
-
const levelIdx = args.indexOf('--level');
|
|
683
|
-
const moduleIdx = args.indexOf('--module');
|
|
684
|
-
const level = levelIdx !== -1 ? args[levelIdx + 1] : undefined;
|
|
685
|
-
const module = moduleIdx !== -1 ? args[moduleIdx + 1] : undefined;
|
|
686
|
-
const p = resolvePaths();
|
|
687
|
-
const mainLog = path.join(p.logs, 'evolclaw.log');
|
|
688
|
-
if (!fs.existsSync(mainLog)) {
|
|
689
|
-
console.log(`❌ Log file not found: ${mainLog}`);
|
|
690
|
-
process.exit(1);
|
|
691
|
-
}
|
|
692
|
-
if (raw) {
|
|
693
|
-
// Raw mode: plain tail -f, no rendering at all
|
|
694
|
-
if (platform.isWindows) {
|
|
695
|
-
const tail = platform.tailFile(mainLog);
|
|
696
|
-
platform.onShutdown(() => tail.abort());
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
const child = spawn('tail', ['-f', '-n', '50', mainLog], { stdio: 'inherit' });
|
|
700
|
-
child.on('exit', (code) => process.exit(code || 0));
|
|
701
|
-
}
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
// Rendered mode: always filter+truncate, color depends on TTY
|
|
705
|
-
const useColor = !noColor && !!process.stdout.isTTY;
|
|
706
|
-
const opts = { level, module, color: useColor };
|
|
707
|
-
function processLine(line) {
|
|
708
|
-
const rendered = renderLogLine(line, opts);
|
|
709
|
-
if (rendered !== null)
|
|
710
|
-
process.stdout.write(rendered + '\n');
|
|
711
|
-
}
|
|
712
|
-
if (platform.isWindows) {
|
|
713
|
-
// Windows: read existing content + watch
|
|
714
|
-
const existing = fs.readFileSync(mainLog, 'utf-8').split('\n').slice(-50);
|
|
715
|
-
existing.forEach(processLine);
|
|
716
|
-
let size = fs.statSync(mainLog).size;
|
|
717
|
-
const watcher = fs.watch(mainLog, () => {
|
|
718
|
-
const newSize = fs.statSync(mainLog).size;
|
|
719
|
-
if (newSize <= size)
|
|
720
|
-
return;
|
|
721
|
-
const buf = Buffer.alloc(newSize - size);
|
|
722
|
-
const fd = fs.openSync(mainLog, 'r');
|
|
723
|
-
fs.readSync(fd, buf, 0, buf.length, size);
|
|
724
|
-
fs.closeSync(fd);
|
|
725
|
-
size = newSize;
|
|
726
|
-
buf.toString().split('\n').forEach(l => l && processLine(l));
|
|
727
|
-
});
|
|
728
|
-
platform.onShutdown(() => watcher.close());
|
|
729
|
-
}
|
|
730
|
-
else {
|
|
731
|
-
// Unix: spawn tail -f, pipe through renderer
|
|
732
|
-
const child = spawn('tail', ['-f', '-n', '50', mainLog]);
|
|
733
|
-
const rl = readline.createInterface({ input: child.stdout });
|
|
734
|
-
rl.on('line', processLine);
|
|
735
|
-
child.on('exit', (code) => process.exit(code || 0));
|
|
736
|
-
platform.onShutdown(() => { child.kill(); });
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* restart-monitor: 内部命令,由 /restart 命令调用
|
|
741
|
-
* 支持 self-heal:启动失败时调用 claude CLI 自动修复,最多重试 3 次
|
|
742
|
-
*/
|
|
743
|
-
async function cmdRestartMonitor() {
|
|
744
|
-
const p = resolvePaths();
|
|
745
|
-
const restartLog = path.join(p.logs, 'restart.log');
|
|
746
|
-
const MAX_HEAL_ATTEMPTS = 3;
|
|
747
|
-
const READY_TIMEOUT = 30000; // 30s(AUN sidecar 10s + Feishu 连接 12s)
|
|
748
|
-
const HEAL_TIMEOUT = 30 * 60 * 1000; // 30 分钟,让 claude 自然结束
|
|
749
|
-
const eventBus = new EventBus();
|
|
750
|
-
const log = (msg) => {
|
|
751
|
-
const _d = new Date();
|
|
752
|
-
const _p = (n) => String(n).padStart(2, '0');
|
|
753
|
-
const ts = `${_d.getFullYear()}-${_p(_d.getMonth() + 1)}-${_p(_d.getDate())} ${_p(_d.getHours())}:${_p(_d.getMinutes())}:${_p(_d.getSeconds())}`;
|
|
754
|
-
const line = `[${ts}] ${msg}\n`;
|
|
755
|
-
fs.appendFileSync(restartLog, line);
|
|
756
|
-
};
|
|
757
|
-
/** 检查服务是否已经在运行(ready signal 存在 + 进程存活) */
|
|
758
|
-
const isServiceAlive = () => {
|
|
759
|
-
return fs.existsSync(p.readySignal) && isRunning(p.pid) !== null;
|
|
760
|
-
};
|
|
761
|
-
log('Restart monitor started');
|
|
762
|
-
// 读取 restart-pending.json 用于后续通知
|
|
763
|
-
const pendingFile = path.join(p.dataDir, 'restart-pending.json');
|
|
764
|
-
let pendingInfo = null;
|
|
765
|
-
try {
|
|
766
|
-
if (fs.existsSync(pendingFile)) {
|
|
767
|
-
pendingInfo = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
catch { }
|
|
771
|
-
// 等待旧进程退出
|
|
772
|
-
if (fs.existsSync(p.pid)) {
|
|
773
|
-
const oldPid = parseInt(fs.readFileSync(p.pid, 'utf-8').trim(), 10);
|
|
774
|
-
log(`Monitoring process PID: ${oldPid}`);
|
|
775
|
-
await new Promise((resolve) => {
|
|
776
|
-
let waited = 0;
|
|
777
|
-
const interval = setInterval(() => {
|
|
778
|
-
waited++;
|
|
779
|
-
if (!platform.isProcessRunning(oldPid)) {
|
|
780
|
-
clearInterval(interval);
|
|
781
|
-
log(`Process ${oldPid} has exited`);
|
|
782
|
-
resolve();
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
if (waited >= 30) {
|
|
786
|
-
clearInterval(interval);
|
|
787
|
-
log('ERROR: Process still running after 30s, force killing');
|
|
788
|
-
platform.killProcess(oldPid, true);
|
|
789
|
-
resolve();
|
|
790
|
-
}
|
|
791
|
-
}, 1000);
|
|
792
|
-
});
|
|
793
|
-
await sleep(3000);
|
|
794
|
-
}
|
|
795
|
-
// 版本检查与自动升级
|
|
796
|
-
log('Checking for updates...');
|
|
797
|
-
const upgrade = await tryUpgrade();
|
|
798
|
-
switch (upgrade.status) {
|
|
799
|
-
case 'upgraded':
|
|
800
|
-
log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
|
|
801
|
-
await notifyChannel(p, pendingInfo, `📦 已升级 ${upgrade.from} → ${upgrade.to}`, log);
|
|
802
|
-
break;
|
|
803
|
-
case 'no-update':
|
|
804
|
-
log(`Already up to date (${upgrade.from})`);
|
|
805
|
-
break;
|
|
806
|
-
case 'skipped':
|
|
807
|
-
log(upgrade.error
|
|
808
|
-
? 'Skipped upgrade (network unavailable)'
|
|
809
|
-
: 'Skipped upgrade check (dev mode)');
|
|
810
|
-
break;
|
|
811
|
-
case 'failed':
|
|
812
|
-
log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}): ${upgrade.error}`);
|
|
813
|
-
await notifyChannel(p, pendingInfo, `⚠️ 升级失败,使用当前版本继续`, log);
|
|
814
|
-
break;
|
|
815
|
-
}
|
|
816
|
-
// 启动并检测 ready signal
|
|
817
|
-
let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
|
|
818
|
-
if (started) {
|
|
819
|
-
log('✓ Service restarted successfully');
|
|
820
|
-
archiveSelfHealLog(p, log);
|
|
821
|
-
// 通知由新进程自行发送(channel-agnostic),此处不再调用 notifyChannel
|
|
822
|
-
process.exit(0);
|
|
823
|
-
}
|
|
824
|
-
// 启动失败 — 测试环境下跳过 self-heal(避免 claude -p 污染会话列表、误杀生产进程)
|
|
825
|
-
if (p.root.startsWith('/tmp/') || process.env.EVOLCLAW_TEST === '1') {
|
|
826
|
-
log('❌ Service failed to start (test environment detected, skipping self-heal)');
|
|
827
|
-
await notifyChannel(p, pendingInfo, '❌ 服务启动失败(测试环境,已跳过自动修复)', log);
|
|
828
|
-
cleanupPendingFile(pendingFile, log);
|
|
829
|
-
process.exit(1);
|
|
830
|
-
}
|
|
831
|
-
// 启动失败,进入 self-heal 循环
|
|
832
|
-
log('❌ Service failed to start, entering self-heal loop');
|
|
833
|
-
eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
|
|
834
|
-
await notifyChannel(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
|
|
835
|
-
for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
|
|
836
|
-
// 前置检查:服务可能已被上一轮 claude 修复并启动
|
|
837
|
-
if (isServiceAlive()) {
|
|
838
|
-
log(`✓ Service already running before attempt ${attempt}, skipping`);
|
|
839
|
-
await sendHealSummary(p, pendingInfo, attempt - 1, log);
|
|
840
|
-
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt - 1 });
|
|
841
|
-
archiveSelfHealLog(p, log);
|
|
842
|
-
cleanupPendingFile(pendingFile, log);
|
|
843
|
-
process.exit(0);
|
|
844
|
-
}
|
|
845
|
-
log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
|
|
846
|
-
eventBus.publish({ type: 'self-heal:attempt', attemptNumber: attempt, maxAttempts: MAX_HEAL_ATTEMPTS });
|
|
847
|
-
await notifyChannel(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
|
|
848
|
-
const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
|
|
849
|
-
// 后置检查:不管 invokeClaude 返回什么,都检查服务实际状态
|
|
850
|
-
if (isServiceAlive()) {
|
|
851
|
-
log(`✓ Service is running after attempt ${attempt}`);
|
|
852
|
-
await sendHealSummary(p, pendingInfo, attempt, log);
|
|
853
|
-
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
|
|
854
|
-
archiveSelfHealLog(p, log);
|
|
855
|
-
cleanupPendingFile(pendingFile, log);
|
|
856
|
-
process.exit(0);
|
|
857
|
-
}
|
|
858
|
-
if (!healed) {
|
|
859
|
-
log(`Self-heal attempt ${attempt} failed (claude invocation error)`);
|
|
860
|
-
continue;
|
|
861
|
-
}
|
|
862
|
-
// claude 正常完成但服务没自动启动,尝试 spawn
|
|
863
|
-
started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
|
|
864
|
-
if (started) {
|
|
865
|
-
log(`✓ Self-heal succeeded on attempt ${attempt}`);
|
|
866
|
-
await sendHealSummary(p, pendingInfo, attempt, log);
|
|
867
|
-
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
|
|
868
|
-
archiveSelfHealLog(p, log);
|
|
869
|
-
cleanupPendingFile(pendingFile, log);
|
|
870
|
-
process.exit(0);
|
|
871
|
-
}
|
|
872
|
-
log(`Attempt ${attempt}: still failing after fix`);
|
|
873
|
-
}
|
|
874
|
-
// 全部失败 — 最后再检查一次
|
|
875
|
-
if (isServiceAlive()) {
|
|
876
|
-
log('✓ Service recovered during final check');
|
|
877
|
-
await sendHealSummary(p, pendingInfo, MAX_HEAL_ATTEMPTS, log);
|
|
878
|
-
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: MAX_HEAL_ATTEMPTS });
|
|
879
|
-
archiveSelfHealLog(p, log);
|
|
880
|
-
cleanupPendingFile(pendingFile, log);
|
|
881
|
-
process.exit(0);
|
|
882
|
-
}
|
|
883
|
-
log(`❌ All ${MAX_HEAL_ATTEMPTS} self-heal attempts failed`);
|
|
884
|
-
eventBus.publish({ type: 'self-heal:completed', success: false, attempts: MAX_HEAL_ATTEMPTS });
|
|
885
|
-
await notifyChannel(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
|
|
886
|
-
cleanupPendingFile(pendingFile, log);
|
|
887
|
-
process.exit(1);
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* 发送 self-heal 修复成功小结(从 self-heal.md 提取摘要)
|
|
891
|
-
*/
|
|
892
|
-
async function sendHealSummary(p, pendingInfo, attempts, log) {
|
|
893
|
-
let summary = `✅ 自动修复成功(第 ${attempts || 1} 次尝试)`;
|
|
894
|
-
try {
|
|
895
|
-
if (fs.existsSync(p.selfHealLog)) {
|
|
896
|
-
const content = fs.readFileSync(p.selfHealLog, 'utf-8');
|
|
897
|
-
// 提取最后一个 ## 章节的要点
|
|
898
|
-
const sections = content.split(/^## /m).filter(Boolean);
|
|
899
|
-
const last = sections[sections.length - 1];
|
|
900
|
-
if (last) {
|
|
901
|
-
const lines = last.split('\n').filter(l => l.startsWith('- ')).map(l => l.trim());
|
|
902
|
-
if (lines.length > 0) {
|
|
903
|
-
summary += '\n' + lines.join('\n');
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
catch { }
|
|
909
|
-
summary += '\n\n⚠️ 修复前进行中的任务已中断,如需继续请重新发送。';
|
|
910
|
-
await notifyChannel(p, pendingInfo, summary, log);
|
|
911
|
-
}
|
|
912
|
-
function sleep(ms) {
|
|
913
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
914
|
-
}
|
|
915
|
-
function cleanupPendingFile(filePath, log) {
|
|
916
|
-
try {
|
|
917
|
-
if (fs.existsSync(filePath)) {
|
|
918
|
-
fs.unlinkSync(filePath);
|
|
919
|
-
log('Cleaned up restart-pending.json');
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
catch { }
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* 启动新进程并等待 ready.signal
|
|
926
|
-
*/
|
|
927
|
-
async function spawnAndWaitReady(p, log, timeout) {
|
|
928
|
-
// 删除旧的 ready signal
|
|
929
|
-
try {
|
|
930
|
-
fs.unlinkSync(p.readySignal);
|
|
931
|
-
}
|
|
932
|
-
catch { }
|
|
933
|
-
// 杀掉可能残留的进程(先读 PID 再删文件,避免数据库锁)
|
|
934
|
-
try {
|
|
935
|
-
const stalePid = parseInt(fs.readFileSync(p.pid, 'utf-8').trim(), 10);
|
|
936
|
-
if (!isNaN(stalePid))
|
|
937
|
-
platform.killProcess(stalePid, true);
|
|
938
|
-
}
|
|
939
|
-
catch { }
|
|
940
|
-
try {
|
|
941
|
-
fs.unlinkSync(p.pid);
|
|
942
|
-
}
|
|
943
|
-
catch { }
|
|
944
|
-
cleanEnv();
|
|
945
|
-
const stdoutLog = path.join(p.logs, 'stdout.log');
|
|
946
|
-
const out = fs.openSync(stdoutLog, 'a');
|
|
947
|
-
const err = fs.openSync(stdoutLog, 'a');
|
|
948
|
-
const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
949
|
-
const child = spawn('node', ['--no-warnings=ExperimentalWarning', appMain], {
|
|
950
|
-
detached: true,
|
|
951
|
-
stdio: ['ignore', out, err],
|
|
952
|
-
env: {
|
|
953
|
-
...process.env,
|
|
954
|
-
EVOLCLAW_HOME: p.root,
|
|
955
|
-
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
|
956
|
-
MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
|
|
957
|
-
EVENT_LOG: process.env.EVENT_LOG || 'true',
|
|
958
|
-
}
|
|
959
|
-
});
|
|
960
|
-
fs.writeFileSync(p.pid, String(child.pid));
|
|
961
|
-
child.unref();
|
|
962
|
-
log(`Spawned new process PID: ${child.pid}, waiting for ready signal...`);
|
|
963
|
-
// 轮询等待 ready.signal 出现
|
|
964
|
-
const start = Date.now();
|
|
965
|
-
while (Date.now() - start < timeout) {
|
|
966
|
-
await sleep(500);
|
|
967
|
-
// 进程已退出则提前失败
|
|
968
|
-
if (!isRunning(p.pid)) {
|
|
969
|
-
log('Process exited before ready signal');
|
|
970
|
-
return false;
|
|
971
|
-
}
|
|
972
|
-
if (fs.existsSync(p.readySignal)) {
|
|
973
|
-
log('Ready signal detected');
|
|
974
|
-
return true;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
log(`Ready signal not received within ${timeout / 1000}s`);
|
|
978
|
-
// 超时后杀掉进程
|
|
979
|
-
const pid = isRunning(p.pid);
|
|
980
|
-
if (pid) {
|
|
981
|
-
platform.killProcess(pid);
|
|
982
|
-
try {
|
|
983
|
-
fs.unlinkSync(p.pid);
|
|
984
|
-
}
|
|
985
|
-
catch { }
|
|
986
|
-
}
|
|
987
|
-
return false;
|
|
988
|
-
}
|
|
989
|
-
/**
|
|
990
|
-
* 调用 claude CLI 进行自动修复
|
|
991
|
-
*/
|
|
992
|
-
async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
993
|
-
const projectDir = getPackageRoot();
|
|
994
|
-
const selfHealLog = p.selfHealLog;
|
|
995
|
-
const stdoutLog = path.join(p.logs, 'stdout.log');
|
|
996
|
-
const selfHealExists = fs.existsSync(selfHealLog) ? '存在,请先阅读之前的修复记录' : '不存在(首次修复)';
|
|
997
|
-
const prompt = `EvolClaw 服务启动失败,需要你诊断并修复。这是第 ${attempt}/${maxAttempts} 次自动修复尝试。
|
|
998
|
-
|
|
999
|
-
关键信息:
|
|
1000
|
-
- 项目目录:${projectDir}
|
|
1001
|
-
- EVOLCLAW_HOME:${p.root}
|
|
1002
|
-
- 错误日志:${stdoutLog}
|
|
1003
|
-
- 主日志:${path.join(p.logs, 'evolclaw.log')}(logger 输出在这里,包含 config 校验失败等关键错误)
|
|
1004
|
-
- 修复记录:${selfHealLog}(${selfHealExists})
|
|
1005
|
-
|
|
1006
|
-
⚠️ 重要诊断技巧:
|
|
1007
|
-
- stdout.log 可能是空的(进程秒退时 logger 输出不会到 stdout),一定要同时读 evolclaw.log
|
|
1008
|
-
- 必须实际运行进程来复现错误:\`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\`,观察输出和退出码
|
|
1009
|
-
- 检查是否有旧进程仍在运行:\`ps aux | grep 'node.*dist/index.js' | grep -v grep\`,旧进程可能占用端口或锁文件
|
|
1010
|
-
- 可以运行 \`EVOLCLAW_HOME=${p.root} node dist/cli.js diagnose\` 快速检查配置和数据库
|
|
1011
|
-
- 如果进程无任何输出就 exit(1),说明是 process.exit(1) 被显式调用,搜索源码中所有 process.exit(1) 位置
|
|
1012
|
-
- evolclaw.json 有自动备份机制:运行时 config watch 检测到文件损坏会自动保存内存快照到 \`data/evolclaw-{timestamp}.json\`,同时 \`data/evolclaw.backup.json\` 是最近一次完整配置的备份。如果 evolclaw.json 损坏或缺失,可以从这些备份恢复
|
|
1013
|
-
|
|
1014
|
-
请执行以下步骤:
|
|
1015
|
-
1. 读取 ${stdoutLog} 和 ${path.join(p.logs, 'evolclaw.log')} 的最后 50 行
|
|
1016
|
-
2. 运行 \`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\` 复现错误(设置 10 秒超时)
|
|
1017
|
-
3. 如果 ${selfHealLog} 存在,先阅读之前的修复记录,避免重复尝试已失败的方案
|
|
1018
|
-
4. 根据实际复现的错误修复代码
|
|
1019
|
-
5. 执行 npm run build 确认编译通过
|
|
1020
|
-
6. 验证修复:启动服务确认 ready.signal 已写入,然后执行 \`EVOLCLAW_HOME=${p.root} node dist/cli.js stop\` 优雅停止(restart-monitor 会负责最终启动)
|
|
1021
|
-
7. 将本次修复内容追加到 ${selfHealLog},格式:
|
|
1022
|
-
## 第 ${attempt} 次修复 - {时间}
|
|
1023
|
-
- 错误原因:...
|
|
1024
|
-
- 修复方案:...
|
|
1025
|
-
- 修改文件:...
|
|
1026
|
-
|
|
1027
|
-
注意:只修复导致启动失败的问题,不要做额外的重构或优化。`;
|
|
1028
|
-
try {
|
|
1029
|
-
log(`Invoking claude CLI (attempt ${attempt}, timeout ${timeout / 60000}min)...`);
|
|
1030
|
-
const { stdout, stderr } = await execFileAsync('claude', [
|
|
1031
|
-
'-p', prompt,
|
|
1032
|
-
'--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
|
|
1033
|
-
'--output-format', 'text',
|
|
1034
|
-
'--no-session-persistence',
|
|
1035
|
-
], {
|
|
1036
|
-
cwd: projectDir,
|
|
1037
|
-
timeout,
|
|
1038
|
-
env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'cli' },
|
|
1039
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
1040
|
-
});
|
|
1041
|
-
if (stdout)
|
|
1042
|
-
log(`Claude output: ${stdout.slice(0, 500)}`);
|
|
1043
|
-
if (stderr)
|
|
1044
|
-
log(`Claude stderr: ${stderr.slice(0, 500)}`);
|
|
1045
|
-
log(`Claude CLI completed (attempt ${attempt})`);
|
|
1046
|
-
return true;
|
|
1047
|
-
}
|
|
1048
|
-
catch (error) {
|
|
1049
|
-
if (error.killed) {
|
|
1050
|
-
log(`Claude CLI timeout after ${timeout / 60000}min (attempt ${attempt})`);
|
|
1051
|
-
}
|
|
1052
|
-
else {
|
|
1053
|
-
log(`Claude CLI error: exit code ${error.code ?? 'unknown'} (attempt ${attempt})`);
|
|
1054
|
-
}
|
|
1055
|
-
if (error.stdout)
|
|
1056
|
-
log(`Claude output: ${String(error.stdout).slice(0, 500)}`);
|
|
1057
|
-
if (error.stderr) {
|
|
1058
|
-
const stderr = String(error.stderr).replace(/Warning: no stdin.*\n?/g, '').trim();
|
|
1059
|
-
if (stderr)
|
|
1060
|
-
log(`Claude stderr: ${stderr.slice(0, 300)}`);
|
|
1061
|
-
}
|
|
1062
|
-
return false;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
/**
|
|
1066
|
-
* 归档 self-heal.md
|
|
1067
|
-
*/
|
|
1068
|
-
function archiveSelfHealLog(p, log) {
|
|
1069
|
-
if (!fs.existsSync(p.selfHealLog))
|
|
1070
|
-
return;
|
|
1071
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
1072
|
-
const archivePath = path.join(p.logs, `self-heal-${timestamp}.md`);
|
|
1073
|
-
fs.renameSync(p.selfHealLog, archivePath);
|
|
1074
|
-
log(`Archived self-heal log to ${archivePath}`);
|
|
1075
|
-
}
|
|
1076
|
-
/**
|
|
1077
|
-
* Resolve a channel instance name to its type and config object.
|
|
1078
|
-
* Searches across all channel types (feishu, wechat, aun) for a matching instance.
|
|
1079
|
-
*/
|
|
1080
|
-
function resolveInstanceConfig(config, instanceName) {
|
|
1081
|
-
for (const type of ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot', 'wecom']) {
|
|
1082
|
-
const raw = config.channels?.[type];
|
|
1083
|
-
if (!raw)
|
|
1084
|
-
continue;
|
|
1085
|
-
if (Array.isArray(raw)) {
|
|
1086
|
-
const inst = raw.find((i) => i.name === instanceName);
|
|
1087
|
-
if (inst)
|
|
1088
|
-
return { type, config: inst };
|
|
1089
|
-
}
|
|
1090
|
-
else {
|
|
1091
|
-
const name = raw.name || type;
|
|
1092
|
-
if (name === instanceName)
|
|
1093
|
-
return { type, config: raw };
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
return null;
|
|
1097
|
-
}
|
|
1098
|
-
/**
|
|
1099
|
-
* 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
|
|
1100
|
-
* 支持 feishu / wechat,根据 pendingInfo.channel 路由
|
|
1101
|
-
*/
|
|
1102
|
-
async function notifyChannel(p, pendingInfo, message, log) {
|
|
1103
|
-
if (!pendingInfo)
|
|
1104
|
-
return;
|
|
1105
|
-
const configPath = path.join(p.dataDir, 'evolclaw.json');
|
|
1106
|
-
if (!fs.existsSync(configPath))
|
|
1107
|
-
return;
|
|
1108
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1109
|
-
const resolved = resolveInstanceConfig(config, pendingInfo.channel);
|
|
1110
|
-
if (!resolved) {
|
|
1111
|
-
log(`Channel instance "${pendingInfo.channel}" not found in config`);
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
1114
|
-
if (resolved.type === 'feishu') {
|
|
1115
|
-
try {
|
|
1116
|
-
const inst = resolved.config;
|
|
1117
|
-
if (!inst.appId || !inst.appSecret)
|
|
1118
|
-
return;
|
|
1119
|
-
const lark = await import('@larksuiteoapi/node-sdk');
|
|
1120
|
-
const client = new lark.Client({
|
|
1121
|
-
appId: inst.appId,
|
|
1122
|
-
appSecret: inst.appSecret,
|
|
1123
|
-
});
|
|
1124
|
-
if (pendingInfo.rootId) {
|
|
1125
|
-
await client.im.message.reply({
|
|
1126
|
-
path: { message_id: pendingInfo.rootId },
|
|
1127
|
-
data: {
|
|
1128
|
-
msg_type: 'text',
|
|
1129
|
-
content: JSON.stringify({ text: message }),
|
|
1130
|
-
reply_in_thread: true,
|
|
1131
|
-
},
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
else {
|
|
1135
|
-
await client.im.message.create({
|
|
1136
|
-
params: { receive_id_type: 'chat_id' },
|
|
1137
|
-
data: {
|
|
1138
|
-
receive_id: pendingInfo.channelId,
|
|
1139
|
-
msg_type: 'text',
|
|
1140
|
-
content: JSON.stringify({ text: message }),
|
|
1141
|
-
},
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
log(`Feishu notification sent: ${message.slice(0, 50)}`);
|
|
1145
|
-
}
|
|
1146
|
-
catch (error) {
|
|
1147
|
-
log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
else if (resolved.type === 'wechat') {
|
|
1151
|
-
try {
|
|
1152
|
-
const inst = resolved.config;
|
|
1153
|
-
if (!inst.token)
|
|
1154
|
-
return;
|
|
1155
|
-
const crypto = await import('node:crypto');
|
|
1156
|
-
const baseUrl = (inst.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
|
|
1157
|
-
const token = inst.token;
|
|
1158
|
-
// 读取缓存的 context_token
|
|
1159
|
-
const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
|
|
1160
|
-
let contextToken;
|
|
1161
|
-
try {
|
|
1162
|
-
if (fs.existsSync(syncBufPath)) {
|
|
1163
|
-
const tokens = JSON.parse(fs.readFileSync(syncBufPath, 'utf-8'));
|
|
1164
|
-
contextToken = tokens[pendingInfo.channelId];
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
catch { }
|
|
1168
|
-
if (!contextToken) {
|
|
1169
|
-
log(`WeChat notification skipped: no context_token for ${pendingInfo.channelId}`);
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
1173
|
-
const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
1174
|
-
const body = JSON.stringify({
|
|
1175
|
-
msg: {
|
|
1176
|
-
from_user_id: '',
|
|
1177
|
-
to_user_id: pendingInfo.channelId,
|
|
1178
|
-
client_id: `evolclaw-restart:${Date.now()}`,
|
|
1179
|
-
message_type: 2,
|
|
1180
|
-
message_state: 2,
|
|
1181
|
-
item_list: [{ type: 1, text_item: { text: message } }],
|
|
1182
|
-
context_token: contextToken,
|
|
1183
|
-
},
|
|
1184
|
-
base_info: { channel_version: '1.0.0' },
|
|
1185
|
-
});
|
|
1186
|
-
const res = await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
|
|
1187
|
-
method: 'POST',
|
|
1188
|
-
headers: {
|
|
1189
|
-
'Content-Type': 'application/json',
|
|
1190
|
-
'AuthorizationType': 'ilink_bot_token',
|
|
1191
|
-
'Authorization': `Bearer ${token.trim()}`,
|
|
1192
|
-
'X-WECHAT-UIN': wechatUin,
|
|
1193
|
-
'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
|
|
1194
|
-
},
|
|
1195
|
-
body,
|
|
1196
|
-
});
|
|
1197
|
-
if (res.ok) {
|
|
1198
|
-
log(`WeChat notification sent: ${message.slice(0, 50)}`);
|
|
1199
|
-
}
|
|
1200
|
-
else {
|
|
1201
|
-
log(`WeChat notification failed: HTTP ${res.status}`);
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
catch (error) {
|
|
1205
|
-
log(`WeChat notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
// ==================== Migrate ====================
|
|
1210
|
-
async function cmdMv(oldDir, newDir) {
|
|
1211
|
-
if (!oldDir || !newDir) {
|
|
1212
|
-
console.log('Usage: evolclaw mv <old_directory> <new_directory>');
|
|
1213
|
-
console.log('Example: evolclaw mv ~/projects/old-name ~/projects/new-name');
|
|
1214
|
-
process.exit(1);
|
|
1215
|
-
}
|
|
1216
|
-
const oldAbs = path.resolve(oldDir);
|
|
1217
|
-
const newAbs = path.resolve(newDir);
|
|
1218
|
-
console.log(`迁移项目: ${oldAbs} → ${newAbs}\n`);
|
|
1219
|
-
try {
|
|
1220
|
-
const r = await migrateProject(oldAbs, newAbs);
|
|
1221
|
-
if (r.claudeSessionsMoved)
|
|
1222
|
-
console.log('✓ Claude Code 会话目录已迁移');
|
|
1223
|
-
if (r.claudeHistoryUpdated)
|
|
1224
|
-
console.log('✓ Claude Code history.jsonl 已更新');
|
|
1225
|
-
if (r.codexUpdated > 0)
|
|
1226
|
-
console.log(`✓ Codex 数据库已更新 (${r.codexUpdated} 个会话)`);
|
|
1227
|
-
if (r.directoryMoved)
|
|
1228
|
-
console.log('✓ 项目目录已移动');
|
|
1229
|
-
if (r.evolclawDbUpdated > 0)
|
|
1230
|
-
console.log(`✓ EvolClaw sessions.db 已更新 (${r.evolclawDbUpdated} 个会话)`);
|
|
1231
|
-
if (r.evolclawConfigUpdated)
|
|
1232
|
-
console.log('✓ evolclaw.json projects.list 已更新');
|
|
1233
|
-
console.log('\n迁移完成!');
|
|
1234
|
-
}
|
|
1235
|
-
catch (e) {
|
|
1236
|
-
console.error(`迁移失败: ${e instanceof Error ? e.message : e}`);
|
|
1237
|
-
process.exit(1);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
// ==================== Diagnose ====================
|
|
1241
|
-
async function cmdDiagnose() {
|
|
1242
|
-
const p = resolvePaths();
|
|
1243
|
-
let hasError = false;
|
|
1244
|
-
// 1. 检查数据目录
|
|
1245
|
-
console.log(`[diagnose] EVOLCLAW_HOME = ${p.root}`);
|
|
1246
|
-
if (!fs.existsSync(p.root)) {
|
|
1247
|
-
console.error(`[diagnose] ❌ 数据目录不存在: ${p.root}`);
|
|
1248
|
-
hasError = true;
|
|
1249
|
-
}
|
|
1250
|
-
else {
|
|
1251
|
-
console.log(`[diagnose] ✓ 数据目录存在`);
|
|
1252
|
-
}
|
|
1253
|
-
// 2. 加载并校验配置
|
|
1254
|
-
try {
|
|
1255
|
-
const config = loadConfig();
|
|
1256
|
-
console.log(`[diagnose] ✓ 配置文件加载成功: ${p.config}`);
|
|
1257
|
-
const integrity = validateConfigIntegrity(config);
|
|
1258
|
-
if (!integrity.valid) {
|
|
1259
|
-
console.error(`[diagnose] ❌ 配置完整性校验失败:\n ${integrity.reasons.join('\n ')}`);
|
|
1260
|
-
hasError = true;
|
|
1261
|
-
}
|
|
1262
|
-
else {
|
|
1263
|
-
console.log(`[diagnose] ✓ 配置完整性校验通过`);
|
|
1264
|
-
}
|
|
1265
|
-
// 3. 检查 Anthropic 配置
|
|
1266
|
-
try {
|
|
1267
|
-
const anthropic = resolveAnthropicConfig(config);
|
|
1268
|
-
console.log(`[diagnose] ✓ Anthropic 配置解析成功 (apiKey: ${anthropic.apiKey ? '已设置' : '❌ 未设置'}, model: ${anthropic.model || 'default'})`);
|
|
1269
|
-
}
|
|
1270
|
-
catch (e) {
|
|
1271
|
-
console.error(`[diagnose] ❌ Anthropic 配置解析失败: ${e instanceof Error ? e.message : e}`);
|
|
1272
|
-
hasError = true;
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
catch (e) {
|
|
1276
|
-
console.error(`[diagnose] ❌ 配置文件加载失败: ${e instanceof Error ? e.message : e}`);
|
|
1277
|
-
hasError = true;
|
|
1278
|
-
}
|
|
1279
|
-
// 4. 检查数据库
|
|
1280
|
-
try {
|
|
1281
|
-
const { SessionManager } = await import('./core/session/session-manager.js');
|
|
1282
|
-
const eventBus = new EventBus();
|
|
1283
|
-
new SessionManager(p.db, eventBus);
|
|
1284
|
-
console.log(`[diagnose] ✓ 数据库初始化成功: ${p.db}`);
|
|
1285
|
-
}
|
|
1286
|
-
catch (e) {
|
|
1287
|
-
console.error(`[diagnose] ❌ 数据库初始化失败: ${e instanceof Error ? e.message : e}`);
|
|
1288
|
-
hasError = true;
|
|
1289
|
-
}
|
|
1290
|
-
// 5. 检查残留进程
|
|
1291
|
-
try {
|
|
1292
|
-
const pid = isRunning(p.pid);
|
|
1293
|
-
if (pid) {
|
|
1294
|
-
console.log(`[diagnose] ⚠️ 已有进程运行中: PID ${pid}`);
|
|
1295
|
-
}
|
|
1296
|
-
else {
|
|
1297
|
-
console.log(`[diagnose] ✓ 无残留进程`);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
catch {
|
|
1301
|
-
console.log(`[diagnose] ✓ 无 PID 文件`);
|
|
1302
|
-
}
|
|
1303
|
-
// 6. 检查关键文件
|
|
1304
|
-
const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
1305
|
-
if (!fs.existsSync(appMain)) {
|
|
1306
|
-
console.error(`[diagnose] ❌ 编译产物不存在: ${appMain}`);
|
|
1307
|
-
hasError = true;
|
|
1308
|
-
}
|
|
1309
|
-
else {
|
|
1310
|
-
console.log(`[diagnose] ✓ 编译产物存在: ${appMain}`);
|
|
1311
|
-
}
|
|
1312
|
-
if (hasError) {
|
|
1313
|
-
console.error('\n[diagnose] ❌ 诊断发现问题,请修复后重试');
|
|
1314
|
-
process.exit(1);
|
|
1315
|
-
}
|
|
1316
|
-
else {
|
|
1317
|
-
console.log('\n[diagnose] ✓ 所有检查通过');
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
// ==================== Ctl ====================
|
|
1321
|
-
async function cmdCtl(args) {
|
|
1322
|
-
if (args.length === 0) {
|
|
1323
|
-
console.error(`用法: evolclaw ctl <command> [args...]
|
|
1324
|
-
|
|
1325
|
-
查询:
|
|
1326
|
-
status 查看会话状态
|
|
1327
|
-
check 检查渠道健康状态
|
|
1328
|
-
help 显示帮助
|
|
1329
|
-
|
|
1330
|
-
配置:
|
|
1331
|
-
model [model-id] 查看/切换模型(如 opus, sonnet, haiku)
|
|
1332
|
-
effort [low|medium|high] 查看/切换推理强度
|
|
1333
|
-
compact 压缩当前会话上下文
|
|
1334
|
-
chatmode [mode] 查看/切换会话模式
|
|
1335
|
-
activity [all|dm|owner|none] 查看/控制中间输出显示模式
|
|
1336
|
-
perm [mode] 查看/切换权限模式
|
|
1337
|
-
|
|
1338
|
-
项目:
|
|
1339
|
-
bind <path> 注册项目目录(不切换当前会话)
|
|
1340
|
-
|
|
1341
|
-
消息:
|
|
1342
|
-
send <消息内容> 主动发送文本消息(proactive 模式)
|
|
1343
|
-
file [channel] <path> 发送项目内文件
|
|
1344
|
-
|
|
1345
|
-
运维:
|
|
1346
|
-
agentmd [put|set <内容>] 查看/管理 agent.md(仅 AUN 通道)
|
|
1347
|
-
restart [channel] 重启服务或重连指定渠道
|
|
1348
|
-
|
|
1349
|
-
示例:
|
|
1350
|
-
evolclaw ctl model sonnet
|
|
1351
|
-
evolclaw ctl effort high
|
|
1352
|
-
evolclaw ctl compact
|
|
1353
|
-
evolclaw ctl chatmode proactive`);
|
|
1354
|
-
process.exit(1);
|
|
1355
|
-
}
|
|
1356
|
-
// help 不需要连接服务,直接复用无参数时的帮助输出
|
|
1357
|
-
if (args[0] === 'help') {
|
|
1358
|
-
return cmdCtl([]);
|
|
1359
|
-
}
|
|
1360
|
-
const sessionId = process.env.EVOLCLAW_SESSION_ID;
|
|
1361
|
-
if (!sessionId) {
|
|
1362
|
-
console.error('错误: EVOLCLAW_SESSION_ID 未设置(仅在 evolclaw 托管环境中可用)');
|
|
1363
|
-
process.exit(1);
|
|
1364
|
-
}
|
|
1365
|
-
const cmd = '/' + args.join(' ');
|
|
1366
|
-
const socketPath = resolvePaths().socket;
|
|
1367
|
-
// compact/restart 等长时操作使用更长超时
|
|
1368
|
-
const longRunning = ['/compact', '/restart'];
|
|
1369
|
-
const timeout = longRunning.some(c => cmd.startsWith(c)) ? 60_000 : 10_000;
|
|
1370
|
-
const result = await ipcQuery(socketPath, {
|
|
1371
|
-
type: 'ctl',
|
|
1372
|
-
cmd,
|
|
1373
|
-
sessionId,
|
|
1374
|
-
}, timeout);
|
|
1375
|
-
if (!result) {
|
|
1376
|
-
console.error('错误: 无法连接 evolclaw 服务');
|
|
1377
|
-
process.exit(1);
|
|
1378
|
-
}
|
|
1379
|
-
const ctlResult = result;
|
|
1380
|
-
if (ctlResult.ok) {
|
|
1381
|
-
console.log(ctlResult.result);
|
|
1382
|
-
}
|
|
1383
|
-
else {
|
|
1384
|
-
console.error(ctlResult.error || '执行失败');
|
|
1385
|
-
process.exit(1);
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
// ==================== Agent ====================
|
|
1389
|
-
async function cmdAgent(args) {
|
|
1390
|
-
const sub = args[0];
|
|
1391
|
-
if (!sub) {
|
|
1392
|
-
await cmdAgentList();
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
if (sub === 'new') {
|
|
1396
|
-
const name = args[1];
|
|
1397
|
-
const nonInteractive = args.includes('--non-interactive');
|
|
1398
|
-
if (nonInteractive) {
|
|
1399
|
-
if (!name) {
|
|
1400
|
-
console.error('Usage: evolclaw agent new <name> --non-interactive ...');
|
|
1401
|
-
process.exit(1);
|
|
1402
|
-
}
|
|
1403
|
-
await cmdAgentNewNonInteractive(name, args.slice(2));
|
|
1404
|
-
}
|
|
1405
|
-
else {
|
|
1406
|
-
// Interactive mode: name from CLI is suggested default; user can override at prompt
|
|
1407
|
-
await cmdAgentNew(name);
|
|
1408
|
-
}
|
|
1409
|
-
return;
|
|
1410
|
-
}
|
|
1411
|
-
if (sub === 'reload') {
|
|
1412
|
-
const name = args[1];
|
|
1413
|
-
if (!name) {
|
|
1414
|
-
console.error('Usage: evolclaw agent reload <name>');
|
|
1415
|
-
process.exit(1);
|
|
1416
|
-
}
|
|
1417
|
-
const p = resolvePaths();
|
|
1418
|
-
try {
|
|
1419
|
-
const result = await ipcQuery(p.socket, { type: 'evolagent.reload', name });
|
|
1420
|
-
if (result && result.ok) {
|
|
1421
|
-
console.log(`✓ Agent "${name}" reloaded`);
|
|
1422
|
-
}
|
|
1423
|
-
else {
|
|
1424
|
-
console.error(`✗ Reload failed: ${result?.error || 'unknown error'}`);
|
|
1425
|
-
process.exit(1);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
catch {
|
|
1429
|
-
console.error('⚠ evolclaw 未运行,请先 evolclaw start 后再 reload');
|
|
1430
|
-
console.log(' 或直接 evolclaw restart 重新加载所有 agent');
|
|
1431
|
-
process.exit(1);
|
|
1432
|
-
}
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
// `evolclaw agent <name>` — show detail
|
|
1436
|
-
await cmdAgentShow(sub);
|
|
1437
|
-
}
|
|
1438
|
-
async function cmdAgentList() {
|
|
1439
|
-
const p = resolvePaths();
|
|
1440
|
-
// Try IPC first (running process has real status)
|
|
1441
|
-
try {
|
|
1442
|
-
const result = await ipcQuery(p.socket, { type: 'evolagent.list' });
|
|
1443
|
-
if (result && result.ok && result.agents) {
|
|
1444
|
-
printAgentTable(result.agents);
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
catch {
|
|
1449
|
-
// IPC unavailable — fall through to cold mode
|
|
1450
|
-
}
|
|
1451
|
-
// Cold mode: read from disk
|
|
1452
|
-
const { EvolAgentRegistry } = await import('./core/evolagent-registry.js');
|
|
1453
|
-
const { loadConfig } = await import('./config.js');
|
|
1454
|
-
let config;
|
|
1455
|
-
try {
|
|
1456
|
-
config = loadConfig(p.config);
|
|
1457
|
-
}
|
|
1458
|
-
catch {
|
|
1459
|
-
config = { agents: {}, channels: {}, projects: { defaultPath: process.cwd() } };
|
|
1460
|
-
}
|
|
1461
|
-
const registry = new EvolAgentRegistry(p.agentsDir);
|
|
1462
|
-
registry.loadAll(config);
|
|
1463
|
-
printAgentTable(registry.list());
|
|
1464
|
-
}
|
|
1465
|
-
function printAgentTable(list) {
|
|
1466
|
-
if (list.length === 0) {
|
|
1467
|
-
console.log('No agents configured.');
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
console.log('NAME'.padEnd(14) + 'STATUS'.padEnd(10) + 'CHANNELS'.padEnd(24) +
|
|
1471
|
-
'PROJECT'.padEnd(22) + 'BASEAGENT'.padEnd(11) + 'LAST ACTIVE');
|
|
1472
|
-
for (const info of list) {
|
|
1473
|
-
const name = info.isDefault ? '[default]' : info.name;
|
|
1474
|
-
const status = info.status || 'stopped';
|
|
1475
|
-
const channels = info.channels?.length > 0 ? info.channels.join(', ').slice(0, 22) : '—';
|
|
1476
|
-
const project = info.projectPath ? path.basename(info.projectPath) : '—';
|
|
1477
|
-
const baseagent = info.baseagent || '—';
|
|
1478
|
-
const lastActive = info.lastActivity
|
|
1479
|
-
? formatTimeAgo(Date.now() - info.lastActivity)
|
|
1480
|
-
: '—';
|
|
1481
|
-
console.log(name.padEnd(14) +
|
|
1482
|
-
status.padEnd(10) +
|
|
1483
|
-
channels.padEnd(24) +
|
|
1484
|
-
project.padEnd(22) +
|
|
1485
|
-
baseagent.padEnd(11) +
|
|
1486
|
-
lastActive);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
async function cmdAgentShow(name) {
|
|
1490
|
-
const p = resolvePaths();
|
|
1491
|
-
// Try IPC first
|
|
1492
|
-
try {
|
|
1493
|
-
const result = await ipcQuery(p.socket, { type: 'evolagent.show', name });
|
|
1494
|
-
if (result && result.ok && result.agent) {
|
|
1495
|
-
const info = result.agent;
|
|
1496
|
-
console.log(`${info.name} (${info.status})\n`);
|
|
1497
|
-
console.log(` Baseagent: ${info.baseagent}`);
|
|
1498
|
-
if (info.model)
|
|
1499
|
-
console.log(` Model: ${info.model}`);
|
|
1500
|
-
if (info.effort)
|
|
1501
|
-
console.log(` Effort: ${info.effort}`);
|
|
1502
|
-
console.log(` Project: ${info.projectPath}`);
|
|
1503
|
-
console.log(` Channels: ${info.channels?.join(', ') || '—'}`);
|
|
1504
|
-
if (info.activeSessions)
|
|
1505
|
-
console.log(` Sessions: ${info.activeSessions} active`);
|
|
1506
|
-
if (info.lastActivity)
|
|
1507
|
-
console.log(` Last active: ${formatTimeAgo(Date.now() - info.lastActivity)}`);
|
|
1508
|
-
if (info.error)
|
|
1509
|
-
console.log(` Error: ${info.error}`);
|
|
1510
|
-
return;
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
catch {
|
|
1514
|
-
// IPC unavailable — fall through to cold mode
|
|
1515
|
-
}
|
|
1516
|
-
// Cold mode
|
|
1517
|
-
const { EvolAgentRegistry } = await import('./core/evolagent-registry.js');
|
|
1518
|
-
const { loadConfig } = await import('./config.js');
|
|
1519
|
-
let config;
|
|
1520
|
-
try {
|
|
1521
|
-
config = loadConfig(p.config);
|
|
1522
|
-
}
|
|
1523
|
-
catch {
|
|
1524
|
-
config = { agents: {}, channels: {}, projects: { defaultPath: process.cwd() } };
|
|
1525
|
-
}
|
|
1526
|
-
const registry = new EvolAgentRegistry(p.agentsDir);
|
|
1527
|
-
registry.loadAll(config);
|
|
1528
|
-
const agent = registry.get(name);
|
|
1529
|
-
if (!agent) {
|
|
1530
|
-
console.error(`Agent "${name}" not found.`);
|
|
1531
|
-
const allList = registry.list().filter(i => !i.isDefault);
|
|
1532
|
-
if (allList.length > 0) {
|
|
1533
|
-
console.log(`Available: ${allList.map(i => i.name).join(', ')}`);
|
|
1534
|
-
}
|
|
1535
|
-
process.exit(1);
|
|
1536
|
-
}
|
|
1537
|
-
console.log(`${agent.name} (${agent.status})\n`);
|
|
1538
|
-
console.log(` Baseagent: ${agent.baseagent}`);
|
|
1539
|
-
if (agent.model)
|
|
1540
|
-
console.log(` Model: ${agent.model}`);
|
|
1541
|
-
if (agent.effort)
|
|
1542
|
-
console.log(` Effort: ${agent.effort}`);
|
|
1543
|
-
console.log(` Project: ${agent.projectPath}`);
|
|
1544
|
-
console.log(` Channels: ${agent.channelInstanceNames().join(', ') || '—'}`);
|
|
1545
|
-
if (agent.error)
|
|
1546
|
-
console.log(` Error: ${agent.error}`);
|
|
1547
|
-
if (agent.configPath)
|
|
1548
|
-
console.log(` Config: ${agent.configPath}`);
|
|
1549
|
-
}
|
|
1550
|
-
async function cmdAgentNew(suggestedName) {
|
|
1551
|
-
const p = resolvePaths();
|
|
1552
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1553
|
-
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
1554
|
-
let name;
|
|
1555
|
-
try {
|
|
1556
|
-
const prompt = suggestedName
|
|
1557
|
-
? `Agent name [${suggestedName}]: `
|
|
1558
|
-
: 'Agent name: ';
|
|
1559
|
-
const input = (await ask(prompt)).trim();
|
|
1560
|
-
name = input || suggestedName;
|
|
1561
|
-
if (!name) {
|
|
1562
|
-
console.error('Agent name is required.');
|
|
1563
|
-
process.exit(1);
|
|
1564
|
-
}
|
|
1565
|
-
// Disallow dots/slashes/spaces in agent name (used as filename + channel-name prefix)
|
|
1566
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
1567
|
-
console.error(`Invalid agent name "${name}": only letters/digits/_/- allowed`);
|
|
1568
|
-
process.exit(1);
|
|
1569
|
-
}
|
|
1570
|
-
const agentPath = path.join(p.agentsDir, `${name}.json`);
|
|
1571
|
-
if (fs.existsSync(agentPath)) {
|
|
1572
|
-
console.error(`Agent "${name}" already exists: ${agentPath}`);
|
|
1573
|
-
process.exit(1);
|
|
1574
|
-
}
|
|
1575
|
-
console.log(`\nCreating agent: ${name}\n`);
|
|
1576
|
-
// Suggest project path: sibling of evolclaw.json's defaultPath, named after agent
|
|
1577
|
-
let suggestedProjectPath = '';
|
|
1578
|
-
try {
|
|
1579
|
-
const cfg = loadConfig(p.config);
|
|
1580
|
-
const defaultProjectsRoot = cfg.projects?.defaultPath
|
|
1581
|
-
? path.dirname(cfg.projects.defaultPath)
|
|
1582
|
-
: path.join(os.homedir(), 'evolclaw-projects');
|
|
1583
|
-
suggestedProjectPath = path.join(defaultProjectsRoot, name);
|
|
1584
|
-
}
|
|
1585
|
-
catch {
|
|
1586
|
-
suggestedProjectPath = path.join(os.homedir(), 'evolclaw-projects', name);
|
|
1587
|
-
}
|
|
1588
|
-
const projectInput = (await ask(`Project path [${suggestedProjectPath}]: `)).trim();
|
|
1589
|
-
const projectPath = projectInput || suggestedProjectPath;
|
|
1590
|
-
if (!path.isAbsolute(projectPath)) {
|
|
1591
|
-
console.error('Project path must be an absolute path.');
|
|
1592
|
-
process.exit(1);
|
|
1593
|
-
}
|
|
1594
|
-
if (!fs.existsSync(projectPath)) {
|
|
1595
|
-
const create = (await ask(`Project path does not exist. Create ${projectPath}? [Y/n]: `)).trim().toLowerCase();
|
|
1596
|
-
if (create === '' || create === 'y' || create === 'yes') {
|
|
1597
|
-
try {
|
|
1598
|
-
fs.mkdirSync(projectPath, { recursive: true });
|
|
1599
|
-
console.log(` ✓ Created ${projectPath}`);
|
|
1600
|
-
}
|
|
1601
|
-
catch (e) {
|
|
1602
|
-
console.error(`Failed to create ${projectPath}: ${e?.message || e}`);
|
|
1603
|
-
process.exit(1);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
else {
|
|
1607
|
-
console.error('Aborted: project path does not exist.');
|
|
1608
|
-
process.exit(1);
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
const baseagentChoices = ['claude', 'codex', 'gemini', 'hermes'];
|
|
1612
|
-
const baseagent = (await ask(`Baseagent (${baseagentChoices.join('/')}) [claude]: `)).trim() || 'claude';
|
|
1613
|
-
if (!baseagentChoices.includes(baseagent)) {
|
|
1614
|
-
console.error(`Invalid baseagent: ${baseagent}`);
|
|
1615
|
-
process.exit(1);
|
|
1616
|
-
}
|
|
1617
|
-
const model = (await ask('Model (leave empty for default): ')).trim() || undefined;
|
|
1618
|
-
const effort = (await ask('Effort (low/medium/high/max) [high]: ')).trim() || 'high';
|
|
1619
|
-
const chatmodeChoices = ['interactive', 'proactive'];
|
|
1620
|
-
console.log('\nChat mode determines how the agent responds:');
|
|
1621
|
-
console.log(' interactive — replies to every message; agent output sent automatically');
|
|
1622
|
-
console.log(' proactive — silent by default; agent decides when/whether to send via ctl send');
|
|
1623
|
-
const chatmodePrivate = (await ask(`Private (1-on-1) chat mode (${chatmodeChoices.join('/')}) [interactive]: `)).trim() || 'interactive';
|
|
1624
|
-
if (!chatmodeChoices.includes(chatmodePrivate)) {
|
|
1625
|
-
console.error(`Invalid chat mode: ${chatmodePrivate}`);
|
|
1626
|
-
process.exit(1);
|
|
1627
|
-
}
|
|
1628
|
-
const chatmodeGroup = (await ask(`Group chat mode (${chatmodeChoices.join('/')}) [proactive]: `)).trim() || 'proactive';
|
|
1629
|
-
if (!chatmodeChoices.includes(chatmodeGroup)) {
|
|
1630
|
-
console.error(`Invalid chat mode: ${chatmodeGroup}`);
|
|
1631
|
-
process.exit(1);
|
|
1632
|
-
}
|
|
1633
|
-
// Channels (loop to allow multiple)
|
|
1634
|
-
const channelsConfig = {};
|
|
1635
|
-
const { getChannelCredentialCollector } = await import('./utils/init-channel.js');
|
|
1636
|
-
// Close outer rl before channel loop (collectors create their own readline)
|
|
1637
|
-
rl.close();
|
|
1638
|
-
while (true) {
|
|
1639
|
-
const loopRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1640
|
-
const loopAsk = (q) => new Promise(r => loopRl.question(q, r));
|
|
1641
|
-
const addChannel = (await loopAsk('\nAdd channel? (y/n) [n]: ')).trim().toLowerCase();
|
|
1642
|
-
if (addChannel !== 'y') {
|
|
1643
|
-
loopRl.close();
|
|
1644
|
-
break;
|
|
1645
|
-
}
|
|
1646
|
-
const channelType = (await loopAsk('Channel type (feishu/aun/wechat/wecom/dingtalk/qqbot): ')).trim();
|
|
1647
|
-
const collector = getChannelCredentialCollector(channelType);
|
|
1648
|
-
if (!collector) {
|
|
1649
|
-
console.error(`Unknown channel type: ${channelType}`);
|
|
1650
|
-
loopRl.close();
|
|
1651
|
-
continue;
|
|
1652
|
-
}
|
|
1653
|
-
// Close loop rl before collector opens its own
|
|
1654
|
-
loopRl.close();
|
|
1655
|
-
let creds = null;
|
|
1656
|
-
try {
|
|
1657
|
-
creds = await collector();
|
|
1658
|
-
}
|
|
1659
|
-
catch (e) {
|
|
1660
|
-
console.error(` Channel setup failed: ${e?.message || e}`);
|
|
1661
|
-
}
|
|
1662
|
-
if (!creds) {
|
|
1663
|
-
console.log(' Channel setup cancelled.');
|
|
1664
|
-
continue;
|
|
1665
|
-
}
|
|
1666
|
-
const nameRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1667
|
-
const defaultEffName = `${name}-${channelType}`;
|
|
1668
|
-
const instName = await new Promise(r => nameRl.question(` Channel instance name (leave empty for ${defaultEffName}): `, r));
|
|
1669
|
-
nameRl.close();
|
|
1670
|
-
const trimmedName = instName.trim();
|
|
1671
|
-
if (trimmedName)
|
|
1672
|
-
creds.name = trimmedName;
|
|
1673
|
-
// else: omit name; effective channel name will be ${agent.name}-${type} via EvolAgent.effectiveChannelName
|
|
1674
|
-
if (!channelsConfig[channelType])
|
|
1675
|
-
channelsConfig[channelType] = [];
|
|
1676
|
-
channelsConfig[channelType].push(creds);
|
|
1677
|
-
}
|
|
1678
|
-
// Simplify channels: if only one instance per type, unwrap from array
|
|
1679
|
-
const finalChannels = {};
|
|
1680
|
-
for (const [type, instances] of Object.entries(channelsConfig)) {
|
|
1681
|
-
finalChannels[type] = instances.length === 1 ? instances[0] : instances;
|
|
1682
|
-
}
|
|
1683
|
-
const agentConfig = {
|
|
1684
|
-
name,
|
|
1685
|
-
enabled: true,
|
|
1686
|
-
agents: { [baseagent]: { ...(model && { model }), effort } },
|
|
1687
|
-
channels: finalChannels,
|
|
1688
|
-
projects: { defaultPath: projectPath.trim() },
|
|
1689
|
-
chatmode: { private: chatmodePrivate, group: chatmodeGroup },
|
|
1690
|
-
};
|
|
1691
|
-
fs.mkdirSync(p.agentsDir, { recursive: true });
|
|
1692
|
-
fs.writeFileSync(agentPath, JSON.stringify(agentConfig, null, 2));
|
|
1693
|
-
console.log(`\n✓ Created: ${agentPath}`);
|
|
1694
|
-
console.log(' Run `evolclaw restart` to activate.');
|
|
1695
|
-
}
|
|
1696
|
-
finally {
|
|
1697
|
-
// rl may already be closed if channel collector was invoked
|
|
1698
|
-
try {
|
|
1699
|
-
rl.close();
|
|
1700
|
-
}
|
|
1701
|
-
catch { }
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
async function cmdAgentNewNonInteractive(name, args) {
|
|
1705
|
-
const p = resolvePaths();
|
|
1706
|
-
const agentPath = path.join(p.agentsDir, `${name}.json`);
|
|
1707
|
-
if (fs.existsSync(agentPath)) {
|
|
1708
|
-
console.error(`Agent "${name}" already exists: ${agentPath}`);
|
|
1709
|
-
process.exit(1);
|
|
1710
|
-
}
|
|
1711
|
-
// Helper: extract --flag value from args
|
|
1712
|
-
const getArg = (flag) => {
|
|
1713
|
-
const idx = args.indexOf(flag);
|
|
1714
|
-
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
1715
|
-
};
|
|
1716
|
-
// Required: baseagent + project
|
|
1717
|
-
const baseagent = getArg('--baseagent');
|
|
1718
|
-
if (!baseagent) {
|
|
1719
|
-
console.error('--baseagent is required (claude|codex|gemini|hermes)');
|
|
1720
|
-
process.exit(1);
|
|
1721
|
-
}
|
|
1722
|
-
const baseagentChoices = ['claude', 'codex', 'gemini', 'hermes'];
|
|
1723
|
-
if (!baseagentChoices.includes(baseagent)) {
|
|
1724
|
-
console.error(`Invalid --baseagent: ${baseagent}`);
|
|
1725
|
-
process.exit(1);
|
|
1726
|
-
}
|
|
1727
|
-
const project = getArg('--project');
|
|
1728
|
-
if (!project) {
|
|
1729
|
-
console.error('--project is required (absolute path)');
|
|
1730
|
-
process.exit(1);
|
|
1731
|
-
}
|
|
1732
|
-
if (!path.isAbsolute(project)) {
|
|
1733
|
-
console.error(`--project must be absolute: ${project}`);
|
|
1734
|
-
process.exit(1);
|
|
1735
|
-
}
|
|
1736
|
-
if (!fs.existsSync(project)) {
|
|
1737
|
-
try {
|
|
1738
|
-
fs.mkdirSync(project, { recursive: true });
|
|
1739
|
-
console.log(` ✓ Created ${project}`);
|
|
1740
|
-
}
|
|
1741
|
-
catch (e) {
|
|
1742
|
-
console.error(`Failed to create ${project}: ${e?.message || e}`);
|
|
1743
|
-
process.exit(1);
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
// Optional: chatmode
|
|
1747
|
-
const chatmodePrivate = getArg('--chatmode-private') || 'interactive';
|
|
1748
|
-
const chatmodeGroup = getArg('--chatmode-group') || 'proactive';
|
|
1749
|
-
const chatmodeValid = new Set(['interactive', 'proactive']);
|
|
1750
|
-
if (!chatmodeValid.has(chatmodePrivate)) {
|
|
1751
|
-
console.error(`Invalid --chatmode-private: ${chatmodePrivate}`);
|
|
1752
|
-
process.exit(1);
|
|
1753
|
-
}
|
|
1754
|
-
if (!chatmodeValid.has(chatmodeGroup)) {
|
|
1755
|
-
console.error(`Invalid --chatmode-group: ${chatmodeGroup}`);
|
|
1756
|
-
process.exit(1);
|
|
1757
|
-
}
|
|
1758
|
-
// Channels
|
|
1759
|
-
const channelsConfig = {};
|
|
1760
|
-
// AUN
|
|
1761
|
-
const aunAid = getArg('--aun-aid');
|
|
1762
|
-
const aunOwner = getArg('--aun-owner');
|
|
1763
|
-
if (aunAid || aunOwner) {
|
|
1764
|
-
if (!aunAid || !aunOwner) {
|
|
1765
|
-
console.error('--aun-aid and --aun-owner must both be provided');
|
|
1766
|
-
process.exit(1);
|
|
1767
|
-
}
|
|
1768
|
-
const { isValidAid, aidCreate } = await import('./channels/aun-ops.js');
|
|
1769
|
-
if (!isValidAid(aunAid)) {
|
|
1770
|
-
console.error(`Invalid --aun-aid: ${aunAid}`);
|
|
1771
|
-
process.exit(1);
|
|
1772
|
-
}
|
|
1773
|
-
if (!isValidAid(aunOwner)) {
|
|
1774
|
-
console.error(`Invalid --aun-owner: ${aunOwner}`);
|
|
1775
|
-
process.exit(1);
|
|
1776
|
-
}
|
|
1777
|
-
try {
|
|
1778
|
-
const result = await aidCreate(aunAid);
|
|
1779
|
-
try {
|
|
1780
|
-
await result.client.close();
|
|
1781
|
-
}
|
|
1782
|
-
catch { /* ignore */ }
|
|
1783
|
-
console.log(`✓ AID ${result.alreadyExisted ? 'reused' : 'created'}: ${aunAid}`);
|
|
1784
|
-
}
|
|
1785
|
-
catch (e) {
|
|
1786
|
-
console.error(`AID creation failed: ${e?.message || e}`);
|
|
1787
|
-
process.exit(1);
|
|
1788
|
-
}
|
|
1789
|
-
channelsConfig.aun = { enabled: true, aid: aunAid, owner: aunOwner };
|
|
1790
|
-
}
|
|
1791
|
-
// Feishu
|
|
1792
|
-
const feishuAppId = getArg('--feishu-app-id');
|
|
1793
|
-
const feishuAppSecret = getArg('--feishu-app-secret');
|
|
1794
|
-
if (feishuAppId || feishuAppSecret) {
|
|
1795
|
-
if (!feishuAppId || !feishuAppSecret) {
|
|
1796
|
-
console.error('--feishu-app-id and --feishu-app-secret must both be provided');
|
|
1797
|
-
process.exit(1);
|
|
1798
|
-
}
|
|
1799
|
-
channelsConfig.feishu = [{
|
|
1800
|
-
name: `feishu-${name}`,
|
|
1801
|
-
enabled: true,
|
|
1802
|
-
appId: feishuAppId,
|
|
1803
|
-
appSecret: feishuAppSecret,
|
|
1804
|
-
}];
|
|
1805
|
-
}
|
|
1806
|
-
// WeChat
|
|
1807
|
-
const wechatToken = getArg('--wechat-token');
|
|
1808
|
-
if (wechatToken) {
|
|
1809
|
-
channelsConfig.wechat = { enabled: true, token: wechatToken };
|
|
1810
|
-
}
|
|
1811
|
-
// WeCom
|
|
1812
|
-
const wecomBotId = getArg('--wecom-bot-id');
|
|
1813
|
-
const wecomSecret = getArg('--wecom-secret');
|
|
1814
|
-
if (wecomBotId || wecomSecret) {
|
|
1815
|
-
if (!wecomBotId || !wecomSecret) {
|
|
1816
|
-
console.error('--wecom-bot-id and --wecom-secret must both be provided');
|
|
1817
|
-
process.exit(1);
|
|
1818
|
-
}
|
|
1819
|
-
channelsConfig.wecom = { enabled: true, botId: wecomBotId, secret: wecomSecret };
|
|
1820
|
-
}
|
|
1821
|
-
// DingTalk
|
|
1822
|
-
const dingtalkClientId = getArg('--dingtalk-client-id');
|
|
1823
|
-
const dingtalkClientSecret = getArg('--dingtalk-client-secret');
|
|
1824
|
-
if (dingtalkClientId || dingtalkClientSecret) {
|
|
1825
|
-
if (!dingtalkClientId || !dingtalkClientSecret) {
|
|
1826
|
-
console.error('--dingtalk-client-id and --dingtalk-client-secret must both be provided');
|
|
1827
|
-
process.exit(1);
|
|
1828
|
-
}
|
|
1829
|
-
channelsConfig.dingtalk = { enabled: true, clientId: dingtalkClientId, clientSecret: dingtalkClientSecret };
|
|
1830
|
-
}
|
|
1831
|
-
// QQBot
|
|
1832
|
-
const qqbotAppId = getArg('--qqbot-app-id');
|
|
1833
|
-
const qqbotClientSecret = getArg('--qqbot-client-secret');
|
|
1834
|
-
if (qqbotAppId || qqbotClientSecret) {
|
|
1835
|
-
if (!qqbotAppId || !qqbotClientSecret) {
|
|
1836
|
-
console.error('--qqbot-app-id and --qqbot-client-secret must both be provided');
|
|
1837
|
-
process.exit(1);
|
|
1838
|
-
}
|
|
1839
|
-
channelsConfig.qqbot = { enabled: true, appId: qqbotAppId, clientSecret: qqbotClientSecret };
|
|
1840
|
-
}
|
|
1841
|
-
if (Object.keys(channelsConfig).length === 0) {
|
|
1842
|
-
console.error('At least one channel must be configured (aun / feishu / wechat / wecom / dingtalk / qqbot)');
|
|
1843
|
-
process.exit(1);
|
|
1844
|
-
}
|
|
1845
|
-
const agentConfig = {
|
|
1846
|
-
name,
|
|
1847
|
-
enabled: true,
|
|
1848
|
-
agents: { [baseagent]: {} },
|
|
1849
|
-
channels: channelsConfig,
|
|
1850
|
-
projects: { defaultPath: project },
|
|
1851
|
-
chatmode: { private: chatmodePrivate, group: chatmodeGroup },
|
|
1852
|
-
};
|
|
1853
|
-
fs.mkdirSync(p.agentsDir, { recursive: true });
|
|
1854
|
-
fs.writeFileSync(agentPath, JSON.stringify(agentConfig, null, 2));
|
|
1855
|
-
console.log(`✓ Created: ${agentPath}`);
|
|
1856
|
-
console.log(' Run `evolclaw restart` (or `evolclaw agent reload <name>`) to activate.');
|
|
1857
|
-
}
|
|
1858
|
-
// ==================== AID ====================
|
|
1859
|
-
async function cmdAid(args) {
|
|
1860
|
-
const sub = args[0] || 'list';
|
|
1861
|
-
if (sub === 'help') {
|
|
1862
|
-
console.log(`用法: evolclaw aid <command>
|
|
1863
|
-
|
|
1864
|
-
Commands:
|
|
1865
|
-
list 列出本地所有 AID
|
|
1866
|
-
new <aid> 创建新 AID 身份
|
|
1867
|
-
|
|
1868
|
-
示例:
|
|
1869
|
-
evolclaw aid list
|
|
1870
|
-
evolclaw aid new reviewer.agentid.pub`);
|
|
1871
|
-
return;
|
|
1872
|
-
}
|
|
1873
|
-
const { aidList, aidCreate, agentmdPut, buildInitialAgentMd, isValidAid } = await import('./channels/aun-ops.js');
|
|
1874
|
-
if (sub === 'list') {
|
|
1875
|
-
const aids = aidList();
|
|
1876
|
-
if (aids.length === 0) {
|
|
1877
|
-
console.log('本地无 AID');
|
|
1878
|
-
return;
|
|
1879
|
-
}
|
|
1880
|
-
console.log('本地 AID:');
|
|
1881
|
-
for (const a of aids) {
|
|
1882
|
-
const icons = [
|
|
1883
|
-
a.hasPrivateKey ? '🔑' : ' ',
|
|
1884
|
-
a.hasAgentMd ? '📄' : ' ',
|
|
1885
|
-
].join('');
|
|
1886
|
-
console.log(` ${icons} ${a.aid}`);
|
|
1887
|
-
}
|
|
1888
|
-
console.log('\n🔑=私钥 📄=agent.md');
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
if (sub === 'new') {
|
|
1892
|
-
const aid = args[1];
|
|
1893
|
-
if (!aid) {
|
|
1894
|
-
console.error('用法: evolclaw aid new <完整AID>\n例: evolclaw aid new reviewer.agentid.pub');
|
|
1895
|
-
process.exit(1);
|
|
1896
|
-
}
|
|
1897
|
-
if (!isValidAid(aid)) {
|
|
1898
|
-
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
1899
|
-
process.exit(1);
|
|
1900
|
-
}
|
|
1901
|
-
const result = await aidCreate(aid);
|
|
1902
|
-
if (!result.alreadyExisted) {
|
|
1903
|
-
const content = buildInitialAgentMd({ aid });
|
|
1904
|
-
try {
|
|
1905
|
-
await agentmdPut(content, { aid, client: result.client });
|
|
1906
|
-
console.log('✓ agent.md 已发布');
|
|
1907
|
-
}
|
|
1908
|
-
catch (e) {
|
|
1909
|
-
console.warn(`⚠ agent.md 发布失败(首次连接将自动重试): ${String(e.message || e).slice(0, 100)}`);
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
try {
|
|
1913
|
-
await result.client.close();
|
|
1914
|
-
}
|
|
1915
|
-
catch { }
|
|
1916
|
-
const verb = result.alreadyExisted ? '已存在' : '已创建';
|
|
1917
|
-
console.log(`✓ ${aid} ${verb}`);
|
|
1918
|
-
console.log(' 如需上线 AUN 通道,运行 evolclaw init aun');
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
console.error(`未知子命令: ${sub}\n用法: evolclaw aid [list|new <aid>]`);
|
|
1922
|
-
process.exit(1);
|
|
1923
|
-
}
|
|
1924
|
-
// ==================== AgentMd ====================
|
|
1925
|
-
async function cmdAgentmd(args) {
|
|
1926
|
-
if (args.length === 0 || args[0] === 'help') {
|
|
1927
|
-
console.log(`用法: evolclaw agentmd <command> <aid>
|
|
1928
|
-
|
|
1929
|
-
Commands:
|
|
1930
|
-
<aid> 查看指定 AID 的 agent.md
|
|
1931
|
-
put <aid> 上传本地 agent.md 到 AUN 网络
|
|
1932
|
-
set <aid> <内容> 设置并上传 agent.md
|
|
1933
|
-
|
|
1934
|
-
示例:
|
|
1935
|
-
evolclaw agentmd mybot.agentid.pub
|
|
1936
|
-
evolclaw agentmd put mybot.agentid.pub
|
|
1937
|
-
evolclaw agentmd set mybot.agentid.pub "---\\naid: mybot.agentid.pub\\n---"`);
|
|
1938
|
-
return;
|
|
1939
|
-
}
|
|
1940
|
-
const { agentmdGet, agentmdPut, isValidAid } = await import('./channels/aun-ops.js');
|
|
1941
|
-
if (args[0] === 'put') {
|
|
1942
|
-
const aid = args[1];
|
|
1943
|
-
if (!aid) {
|
|
1944
|
-
console.error('用法: evolclaw agentmd put <aid>');
|
|
1945
|
-
process.exit(1);
|
|
1946
|
-
}
|
|
1947
|
-
if (!isValidAid(aid)) {
|
|
1948
|
-
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
1949
|
-
process.exit(1);
|
|
1950
|
-
}
|
|
1951
|
-
// Read local file directly (put = push local → network)
|
|
1952
|
-
const localPath = path.join(os.homedir(), '.aun', 'AIDs', aid, 'agent.md');
|
|
1953
|
-
if (!fs.existsSync(localPath)) {
|
|
1954
|
-
console.error(`❌ 本地无 agent.md: ${aid}`);
|
|
1955
|
-
process.exit(1);
|
|
1956
|
-
}
|
|
1957
|
-
const content = fs.readFileSync(localPath, 'utf-8');
|
|
1958
|
-
await agentmdPut(content, { aid });
|
|
1959
|
-
console.log('✓ agent.md 已发布');
|
|
1960
|
-
return;
|
|
1961
|
-
}
|
|
1962
|
-
if (args[0] === 'set') {
|
|
1963
|
-
const aid = args[1];
|
|
1964
|
-
const content = args.slice(2).join(' ');
|
|
1965
|
-
if (!aid || !content) {
|
|
1966
|
-
console.error('用法: evolclaw agentmd set <aid> <内容>');
|
|
1967
|
-
process.exit(1);
|
|
1968
|
-
}
|
|
1969
|
-
if (!isValidAid(aid)) {
|
|
1970
|
-
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
1971
|
-
process.exit(1);
|
|
1972
|
-
}
|
|
1973
|
-
await agentmdPut(content, { aid });
|
|
1974
|
-
console.log('✓ agent.md 已更新并发布');
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
// Default: view
|
|
1978
|
-
const aid = args[0];
|
|
1979
|
-
if (!isValidAid(aid)) {
|
|
1980
|
-
console.error(`❌ 无效 AID 格式: ${aid}`);
|
|
1981
|
-
process.exit(1);
|
|
1982
|
-
}
|
|
1983
|
-
try {
|
|
1984
|
-
const md = await agentmdGet(aid);
|
|
1985
|
-
if (!md || !md.trim()) {
|
|
1986
|
-
console.log(`ℹ️ ${aid} 尚未设置 agent.md`);
|
|
1987
|
-
}
|
|
1988
|
-
else {
|
|
1989
|
-
console.log(md);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
catch (e) {
|
|
1993
|
-
const msg = String(e.message || e);
|
|
1994
|
-
if (msg.includes('not found') || msg.includes('404')) {
|
|
1995
|
-
console.log(`ℹ️ ${aid} 尚未设置 agent.md`);
|
|
1996
|
-
}
|
|
1997
|
-
else {
|
|
1998
|
-
console.error(`❌ 获取失败: ${msg.slice(0, 100)}`);
|
|
1999
|
-
process.exit(1);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
// ==================== Main ====================
|
|
2004
|
-
function getArgValue(args, flag) {
|
|
2005
|
-
const idx = args.indexOf(flag);
|
|
2006
|
-
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
2007
|
-
}
|
|
2008
|
-
export async function main(args) {
|
|
2009
|
-
const cmd = args[0] || 'start';
|
|
2010
|
-
if (cmd === '--version' || cmd === '-v' || cmd === '-V') {
|
|
2011
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
|
|
2012
|
-
console.log(pkg.version);
|
|
2013
|
-
return;
|
|
2014
|
-
}
|
|
2015
|
-
switch (cmd) {
|
|
2016
|
-
case 'init':
|
|
2017
|
-
if (args[1] === 'help') {
|
|
2018
|
-
console.log(`用法: evolclaw init [渠道] [选项]
|
|
2019
|
-
|
|
2020
|
-
交互式初始化:
|
|
2021
|
-
evolclaw init 创建基础配置文件(交互式)
|
|
2022
|
-
evolclaw init feishu 飞书扫码登录并写入配置
|
|
2023
|
-
evolclaw init wechat 微信扫码登录并写入配置
|
|
2024
|
-
evolclaw init dingtalk 钉钉扫码登录并写入配置
|
|
2025
|
-
evolclaw init qqbot QQ 机器人扫码绑定并写入配置
|
|
2026
|
-
evolclaw init wecom 企业微信 AI Bot 配置(手动输入)
|
|
2027
|
-
evolclaw init aun AUN 交互式配置(AID 创建 + Owner 绑定)
|
|
2028
|
-
|
|
2029
|
-
非交互式初始化:
|
|
2030
|
-
evolclaw init --non-interactive [选项]
|
|
2031
|
-
|
|
2032
|
-
选项:
|
|
2033
|
-
--default-path <path> 项目目录(默认: 当前目录)
|
|
2034
|
-
--channel <name> 渠道类型(默认: aun)
|
|
2035
|
-
--aun-aid <aid> AUN Agent ID(必填,如 mybot.agentid.pub)
|
|
2036
|
-
--aun-owner <aid> Owner AID(可选,如 alice.agentid.pub)
|
|
2037
|
-
|
|
2038
|
-
示例:
|
|
2039
|
-
evolclaw init --non-interactive --aun-aid mybot.agentid.pub --aun-owner alice.agentid.pub
|
|
2040
|
-
evolclaw init --non-interactive --default-path /home/user/project --aun-aid bot.agentid.pub`);
|
|
2041
|
-
}
|
|
2042
|
-
else if (args[1] === 'wechat') {
|
|
2043
|
-
await cmdInitWechat();
|
|
2044
|
-
}
|
|
2045
|
-
else if (args[1] === 'feishu') {
|
|
2046
|
-
await cmdInitFeishu();
|
|
2047
|
-
}
|
|
2048
|
-
else if (args[1] === 'aun') {
|
|
2049
|
-
await cmdInitAun();
|
|
2050
|
-
}
|
|
2051
|
-
else if (args[1] === 'dingtalk') {
|
|
2052
|
-
await cmdInitDingtalk();
|
|
2053
|
-
}
|
|
2054
|
-
else if (args[1] === 'qqbot') {
|
|
2055
|
-
await cmdInitQQBot();
|
|
2056
|
-
}
|
|
2057
|
-
else if (args[1] === 'wecom') {
|
|
2058
|
-
await cmdInitWecom();
|
|
2059
|
-
}
|
|
2060
|
-
else if (args[1] && !args[1].startsWith('-')) {
|
|
2061
|
-
const supported = ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot', 'wecom'];
|
|
2062
|
-
console.error(`❌ 不支持的渠道: ${args[1]}`);
|
|
2063
|
-
console.error(` 支持的渠道: ${supported.join(', ')}`);
|
|
2064
|
-
process.exit(1);
|
|
2065
|
-
}
|
|
2066
|
-
else {
|
|
2067
|
-
const nonInteractive = args.includes('--non-interactive');
|
|
2068
|
-
if (nonInteractive) {
|
|
2069
|
-
await cmdInit({
|
|
2070
|
-
nonInteractive: true,
|
|
2071
|
-
defaultPath: getArgValue(args, '--default-path') || path.join(os.homedir(), 'projects', 'default'),
|
|
2072
|
-
channel: getArgValue(args, '--channel') || 'aun',
|
|
2073
|
-
aunAid: getArgValue(args, '--aun-aid'),
|
|
2074
|
-
aunOwner: getArgValue(args, '--aun-owner'),
|
|
2075
|
-
});
|
|
2076
|
-
}
|
|
2077
|
-
else {
|
|
2078
|
-
await cmdInit();
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
break;
|
|
2082
|
-
case 'start':
|
|
2083
|
-
await cmdStart();
|
|
2084
|
-
break;
|
|
2085
|
-
case 'stop':
|
|
2086
|
-
await cmdStop();
|
|
2087
|
-
break;
|
|
2088
|
-
case 'restart':
|
|
2089
|
-
await cmdRestart();
|
|
2090
|
-
break;
|
|
2091
|
-
case 'status':
|
|
2092
|
-
await cmdStatus();
|
|
2093
|
-
break;
|
|
2094
|
-
case 'logs':
|
|
2095
|
-
cmdLogs(args.slice(1));
|
|
2096
|
-
break;
|
|
2097
|
-
case 'restart-monitor':
|
|
2098
|
-
await cmdRestartMonitor();
|
|
2099
|
-
break;
|
|
2100
|
-
case 'mv':
|
|
2101
|
-
await cmdMv(args[1], args[2]);
|
|
2102
|
-
break;
|
|
2103
|
-
case 'diagnose':
|
|
2104
|
-
await cmdDiagnose();
|
|
2105
|
-
break;
|
|
2106
|
-
case 'ctl':
|
|
2107
|
-
await cmdCtl(args.slice(1));
|
|
2108
|
-
break;
|
|
2109
|
-
case 'agent':
|
|
2110
|
-
await cmdAgent(args.slice(1));
|
|
2111
|
-
break;
|
|
2112
|
-
case 'aid':
|
|
2113
|
-
await cmdAid(args.slice(1));
|
|
2114
|
-
break;
|
|
2115
|
-
case 'agentmd':
|
|
2116
|
-
await cmdAgentmd(args.slice(1));
|
|
2117
|
-
break;
|
|
2118
|
-
default:
|
|
2119
|
-
console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|ctl|diagnose|mv}
|
|
2120
|
-
|
|
2121
|
-
Commands:
|
|
2122
|
-
init 创建配置文件 (${resolvePaths().config})
|
|
2123
|
-
init feishu 飞书扫码登录并写入配置
|
|
2124
|
-
init wechat 微信扫码登录并写入配置
|
|
2125
|
-
init dingtalk 钉钉扫码登录并写入配置
|
|
2126
|
-
init qqbot QQ 机器人扫码绑定并写入配置
|
|
2127
|
-
init wecom 企业微信 AI Bot 配置(手动输入 Bot ID + Secret)
|
|
2128
|
-
init aun AUN (AgentUnin.Network) 配置
|
|
2129
|
-
start 启动服务 (默认)
|
|
2130
|
-
stop 停止服务
|
|
2131
|
-
restart 重启服务
|
|
2132
|
-
status 查看状态
|
|
2133
|
-
logs 查看日志 (tail -f, 着色渲染)
|
|
2134
|
-
--level error|warn 只显示指定级别及以上
|
|
2135
|
-
--module <name> 只显示指定模块(如 feishu、AgentRunner)
|
|
2136
|
-
--raw 原始输出,不着色
|
|
2137
|
-
ctl 运行时自管理(模型切换、推理强度、压缩上下文等)
|
|
2138
|
-
evolclaw ctl help 查看完整命令列表
|
|
2139
|
-
agent 管理 EvolAgent
|
|
2140
|
-
agent 列出所有 agent
|
|
2141
|
-
agent <name> 查看指定 agent 详情
|
|
2142
|
-
agent new <name> 创建新 agent(交互式)
|
|
2143
|
-
agent new <name> --non-interactive ... 非交互创建(自动化)
|
|
2144
|
-
必填: --baseagent <claude|codex|gemini|hermes>
|
|
2145
|
-
--project <absolute path>
|
|
2146
|
-
可选 channel:
|
|
2147
|
-
--aun-aid <aid> --aun-owner <aid>
|
|
2148
|
-
--feishu-app-id xxx --feishu-app-secret yyy
|
|
2149
|
-
--wechat-token xxx
|
|
2150
|
-
--wecom-bot-id xxx --wecom-secret yyy
|
|
2151
|
-
--dingtalk-client-id xxx --dingtalk-client-secret yyy
|
|
2152
|
-
--qqbot-app-id xxx --qqbot-client-secret yyy
|
|
2153
|
-
可选行为:
|
|
2154
|
-
--chatmode-private <interactive|proactive> (默认 interactive)
|
|
2155
|
-
--chatmode-group <interactive|proactive> (默认 proactive)
|
|
2156
|
-
agent reload <n> 热重载 agent 配置
|
|
2157
|
-
aid AID 身份管理
|
|
2158
|
-
aid list 列出本地所有 AID
|
|
2159
|
-
aid new <aid> 创建新 AID 身份
|
|
2160
|
-
agentmd agent.md 管理
|
|
2161
|
-
agentmd <aid> 查看 agent.md
|
|
2162
|
-
agentmd put <aid> 上传本地 agent.md
|
|
2163
|
-
agentmd set <aid> <内容> 设置并上传
|
|
2164
|
-
diagnose 诊断启动环境(配置、数据库、进程)
|
|
2165
|
-
mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
|
|
2166
|
-
|
|
2167
|
-
Environment:
|
|
2168
|
-
EVOLCLAW_HOME 数据目录 (默认: ~/.evolclaw)
|
|
2169
|
-
LOG_LEVEL 日志级别 (默认: INFO)
|
|
2170
|
-
MESSAGE_LOG 消息日志 (默认: true)
|
|
2171
|
-
EVENT_LOG 事件日志 (默认: true)`);
|
|
2172
|
-
process.exit(1);
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
// 直接运行时自动执行(node dist/cli.js ...)
|
|
2176
|
-
if (platform.isMainScript(import.meta.url)) {
|
|
2177
|
-
main(process.argv.slice(2));
|
|
2178
|
-
}
|