evolclaw 2.1.2 → 2.3.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 +59 -30
- package/data/evolclaw.sample.json +15 -4
- package/dist/agents/claude-runner.js +685 -0
- package/dist/agents/codex-runner.js +315 -0
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +580 -10
- package/dist/channels/feishu.js +888 -135
- package/dist/channels/wechat.js +127 -21
- package/dist/cli.js +519 -136
- package/dist/config.js +277 -25
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +67 -0
- package/dist/core/command-handler.js +1537 -392
- package/dist/core/event-bus.js +32 -0
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/message/message-processor.js +1028 -0
- package/dist/core/message/message-queue.js +240 -0
- package/dist/core/message/stream-debouncer.js +122 -0
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
- package/dist/core/permission.js +259 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-manager.js +1072 -0
- package/dist/index.js +402 -252
- package/dist/ipc.js +106 -0
- package/dist/paths.js +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/{platform.js → cross-platform.js} +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +190 -53
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/stats-collector.js +102 -0
- package/package.json +4 -2
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-processor.js +0 -604
- package/dist/core/message-queue.js +0 -116
- package/dist/core/message-stream.js +0 -59
- package/dist/core/session-manager.js +0 -664
- package/dist/index.js.bak +0 -340
- package/dist/utils/init-feishu.js +0 -261
- package/dist/utils/init-wechat.js +0 -170
- package/dist/utils/markdown-to-feishu.js +0 -94
- package/dist/utils/permission.js +0 -43
- package/dist/utils/session-file-health.js +0 -68
- /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,13 @@ import path from 'path';
|
|
|
4
4
|
import { spawn, execFile } from 'child_process';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
6
|
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
|
|
7
|
+
import { loadConfig, validateConfigIntegrity, resolveAnthropicConfig } from './config.js';
|
|
8
|
+
import { migrateProject } from './utils/migrate-project.js';
|
|
7
9
|
import { cmdInit } from './utils/init.js';
|
|
8
|
-
import {
|
|
9
|
-
import { cmdInitFeishu } from './utils/init-
|
|
10
|
-
import * as platform from './utils/platform.js';
|
|
10
|
+
import { ipcQuery } from './ipc.js';
|
|
11
|
+
import { cmdInitWechat, cmdInitFeishu, cmdInitAun } from './utils/init-channel.js';
|
|
12
|
+
import * as platform from './utils/cross-platform.js';
|
|
13
|
+
import { EventBus } from './core/event-bus.js';
|
|
11
14
|
// Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
|
|
12
15
|
process.removeAllListeners('warning');
|
|
13
16
|
process.on('warning', (w) => { if (w.name === 'ExperimentalWarning')
|
|
@@ -86,17 +89,19 @@ function countLines(pkgRoot, logDir) {
|
|
|
86
89
|
};
|
|
87
90
|
console.log('\n[launcher] 正在统计代码行数...\n');
|
|
88
91
|
const core = countDir(path.join(srcDir, 'core'));
|
|
92
|
+
const agents = countDir(path.join(srcDir, 'agents'));
|
|
89
93
|
const channels = countDir(path.join(srcDir, 'channels'), 'experimental');
|
|
90
94
|
const utils = countDir(path.join(srcDir, 'utils'));
|
|
91
95
|
const entry = countFile(path.join(srcDir, 'index.ts'))
|
|
92
96
|
+ countFile(path.join(srcDir, 'config.ts'))
|
|
93
97
|
+ countFile(path.join(srcDir, 'types.ts'))
|
|
94
98
|
+ countFile(path.join(srcDir, 'cli.ts'));
|
|
95
|
-
const total = core + channels + utils + entry;
|
|
99
|
+
const total = core + agents + channels + utils + entry;
|
|
96
100
|
console.log('==================================================');
|
|
97
101
|
console.log('EvolClaw 代码统计');
|
|
98
102
|
console.log('==================================================');
|
|
99
103
|
console.log(`核心模块: ${String(core).padStart(8)} 行`);
|
|
104
|
+
console.log(`Agent 模块: ${String(agents).padStart(8)} 行`);
|
|
100
105
|
console.log(`渠道适配: ${String(channels).padStart(8)} 行`);
|
|
101
106
|
console.log(`工具库: ${String(utils).padStart(8)} 行`);
|
|
102
107
|
console.log(`入口与配置: ${String(entry).padStart(8)} 行`);
|
|
@@ -117,7 +122,7 @@ function countLines(pkgRoot, logDir) {
|
|
|
117
122
|
}
|
|
118
123
|
if (shouldAppend) {
|
|
119
124
|
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
120
|
-
fs.appendFileSync(statsFile, `${now}\t${core}\t${channels}\t${utils}\t${entry}\t${total}\n`);
|
|
125
|
+
fs.appendFileSync(statsFile, `${now}\t${core}\t${agents}\t${channels}\t${utils}\t${entry}\t${total}\n`);
|
|
121
126
|
}
|
|
122
127
|
showHistory(statsFile);
|
|
123
128
|
}
|
|
@@ -131,21 +136,30 @@ function showHistory(statsFile) {
|
|
|
131
136
|
console.log('\n==================================================');
|
|
132
137
|
console.log('历史记录(最近 8 次)');
|
|
133
138
|
console.log('==================================================');
|
|
134
|
-
console.log(`${'时间'.padEnd(20)} ${'核心'.padStart(6)} ${'渠道'.padStart(6)} ${'工具'.padStart(6)} ${'入口'.padStart(6)} ${'总计'.padStart(6)} ${'变化'.padStart(8)}`);
|
|
139
|
+
console.log(`${'时间'.padEnd(20)} ${'核心'.padStart(6)} ${'Agent'.padStart(6)} ${'渠道'.padStart(6)} ${'工具'.padStart(6)} ${'入口'.padStart(6)} ${'总计'.padStart(6)} ${'变化'.padStart(8)}`);
|
|
135
140
|
console.log('--------------------------------------------------');
|
|
136
141
|
let prevTotal = null;
|
|
137
142
|
for (const line of recent) {
|
|
138
143
|
const parts = line.split('\t');
|
|
139
|
-
|
|
144
|
+
// 兼容旧格式(6列: time,core,ch,utils,entry,total)和新格式(7列: +agents)
|
|
145
|
+
let time, c, a, ch, u, e, t;
|
|
146
|
+
if (parts.length >= 7) {
|
|
147
|
+
[time, c, a, ch, u, e, t] = parts;
|
|
148
|
+
}
|
|
149
|
+
else if (parts.length >= 6) {
|
|
150
|
+
[time, c, ch, u, e, t] = parts;
|
|
151
|
+
a = '-';
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
140
154
|
continue;
|
|
141
|
-
|
|
155
|
+
}
|
|
142
156
|
const total = parseInt(t, 10);
|
|
143
157
|
let diff = '-';
|
|
144
158
|
if (prevTotal !== null) {
|
|
145
159
|
const change = total - prevTotal;
|
|
146
160
|
diff = change >= 0 ? `+${change}` : `${change}`;
|
|
147
161
|
}
|
|
148
|
-
console.log(`${time.padEnd(20)} ${c.padStart(6)} ${ch.padStart(6)} ${u.padStart(6)} ${e.padStart(6)} ${t.padStart(6)} ${diff.padStart(8)}`);
|
|
162
|
+
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)}`);
|
|
149
163
|
prevTotal = total;
|
|
150
164
|
}
|
|
151
165
|
console.log('==================================================');
|
|
@@ -159,6 +173,23 @@ async function cmdStart() {
|
|
|
159
173
|
console.log('❌ 配置文件不存在,请先运行 evolclaw init');
|
|
160
174
|
process.exit(1);
|
|
161
175
|
}
|
|
176
|
+
// 配置完整性校验
|
|
177
|
+
try {
|
|
178
|
+
const config = loadConfig(p.config);
|
|
179
|
+
const integrity = validateConfigIntegrity(config);
|
|
180
|
+
if (!integrity.valid) {
|
|
181
|
+
console.log(`❌ 配置文件完整性校验失败:`);
|
|
182
|
+
for (const reason of integrity.reasons) {
|
|
183
|
+
console.log(` - ${reason}`);
|
|
184
|
+
}
|
|
185
|
+
console.log(`\n配置文件: ${p.config}`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
console.log(`❌ 配置文件加载失败: ${e.message}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
162
193
|
// 检查 PID 文件
|
|
163
194
|
const pid = isRunning(p.pid);
|
|
164
195
|
if (pid) {
|
|
@@ -167,8 +198,11 @@ async function cmdStart() {
|
|
|
167
198
|
process.exit(1);
|
|
168
199
|
}
|
|
169
200
|
// 检查是否有残留进程(PID 文件已丢失但进程还在)
|
|
201
|
+
// 只清理属于当前 EVOLCLAW_HOME 的进程,避免误杀其他实例
|
|
170
202
|
let hasOrphan = false;
|
|
171
|
-
const
|
|
203
|
+
const evolclawMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
204
|
+
const allPids = platform.findProcesses(evolclawMain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
205
|
+
const orphanPids = allPids.filter(pid => platform.getProcessEnv(pid, 'EVOLCLAW_HOME') === p.root);
|
|
172
206
|
if (orphanPids.length > 0) {
|
|
173
207
|
console.log(`⚠ 发现 ${orphanPids.length} 个残留进程,正在清理...`);
|
|
174
208
|
for (const p of orphanPids) {
|
|
@@ -197,6 +231,7 @@ async function cmdStart() {
|
|
|
197
231
|
stdio: ['ignore', out, err],
|
|
198
232
|
env: {
|
|
199
233
|
...process.env,
|
|
234
|
+
EVOLCLAW_HOME: p.root,
|
|
200
235
|
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
|
201
236
|
MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
|
|
202
237
|
EVENT_LOG: process.env.EVENT_LOG || 'true',
|
|
@@ -204,7 +239,7 @@ async function cmdStart() {
|
|
|
204
239
|
});
|
|
205
240
|
fs.writeFileSync(p.pid, String(child.pid));
|
|
206
241
|
child.unref();
|
|
207
|
-
// 等待 ready signal(最多
|
|
242
|
+
// 等待 ready signal(最多 30 秒,AUN sidecar 超时 15s + 其他通道连接)
|
|
208
243
|
const startTime = Date.now();
|
|
209
244
|
const checkReady = () => {
|
|
210
245
|
// ready signal 出现(优先检查,避免 Windows 上 isRunning 误判)
|
|
@@ -213,6 +248,41 @@ async function cmdStart() {
|
|
|
213
248
|
console.log(`✓ EvolClaw started successfully (PID: ${pid})`);
|
|
214
249
|
console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
|
|
215
250
|
console.log(` Logs: ${p.logs}/`);
|
|
251
|
+
// 从主日志提取渠道连接摘要
|
|
252
|
+
const mainLog = path.join(p.logs, 'evolclaw.log');
|
|
253
|
+
if (fs.existsSync(mainLog)) {
|
|
254
|
+
const logLines = fs.readFileSync(mainLog, 'utf-8').split('\n');
|
|
255
|
+
// 从末尾往前找最近一次启动的摘要
|
|
256
|
+
let channelSummary = '';
|
|
257
|
+
for (let i = logLines.length - 1; i >= 0; i--) {
|
|
258
|
+
if (logLines[i].includes('EvolClaw is running with')) {
|
|
259
|
+
channelSummary = logLines[i];
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (channelSummary) {
|
|
264
|
+
const match = channelSummary.match(/running with .+/);
|
|
265
|
+
if (match)
|
|
266
|
+
console.log(` ${match[0]}`);
|
|
267
|
+
}
|
|
268
|
+
// 最近一次启动的失败信息
|
|
269
|
+
let lastReadyIdx = -1;
|
|
270
|
+
for (let i = logLines.length - 1; i >= 0; i--) {
|
|
271
|
+
if (logLines[i].includes('Ready signal written')) {
|
|
272
|
+
lastReadyIdx = i;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (lastReadyIdx > 0) {
|
|
277
|
+
for (let i = Math.max(0, lastReadyIdx - 20); i < lastReadyIdx; i++) {
|
|
278
|
+
const line = logLines[i];
|
|
279
|
+
if (line.includes('failed to connect') || line.includes('Failed to create channel')) {
|
|
280
|
+
const match = line.match(/\[WARN\]\s*(.+)/);
|
|
281
|
+
console.log(` ⚠ ${match ? match[1] : line.trim()}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
216
286
|
console.log('');
|
|
217
287
|
// 代码统计仅在开发环境显示(EVOLCLAW_HOME 指向包目录)
|
|
218
288
|
if (resolveRoot() === getPackageRoot()) {
|
|
@@ -221,7 +291,7 @@ async function cmdStart() {
|
|
|
221
291
|
return;
|
|
222
292
|
}
|
|
223
293
|
// 超时
|
|
224
|
-
if (Date.now() - startTime >
|
|
294
|
+
if (Date.now() - startTime > 30000) {
|
|
225
295
|
console.log('❌ Failed to start EvolClaw (ready signal timeout)');
|
|
226
296
|
console.log('');
|
|
227
297
|
console.log('📝 Error details (last 10 lines of stdout):');
|
|
@@ -302,6 +372,53 @@ async function cmdRestart() {
|
|
|
302
372
|
await stopAndWait(p.pid);
|
|
303
373
|
setTimeout(() => cmdStart(), 1000);
|
|
304
374
|
}
|
|
375
|
+
function formatTimeAgo(ms) {
|
|
376
|
+
const sec = Math.floor(ms / 1000);
|
|
377
|
+
if (sec < 60)
|
|
378
|
+
return '刚刚';
|
|
379
|
+
const min = Math.floor(sec / 60);
|
|
380
|
+
if (min < 60)
|
|
381
|
+
return `${min}分钟前`;
|
|
382
|
+
const hour = Math.floor(min / 60);
|
|
383
|
+
if (hour < 24)
|
|
384
|
+
return `${hour}小时前`;
|
|
385
|
+
const day = Math.floor(hour / 24);
|
|
386
|
+
return `${day}天前`;
|
|
387
|
+
}
|
|
388
|
+
function showConfigChannels(config) {
|
|
389
|
+
const groups = [];
|
|
390
|
+
const channelChecks = [
|
|
391
|
+
{ type: 'feishu', isValid: (inst) => !!inst.appId && inst.enabled !== false },
|
|
392
|
+
{ type: 'wechat', isValid: (inst) => !!inst.token && inst.enabled !== false },
|
|
393
|
+
{ type: 'aun', isValid: (inst) => !!inst.aid && inst.enabled !== false && !inst.aid.includes('your-') && !inst.aid.includes('placeholder') },
|
|
394
|
+
];
|
|
395
|
+
for (const { type, isValid } of channelChecks) {
|
|
396
|
+
const raw = config.channels?.[type];
|
|
397
|
+
if (!raw)
|
|
398
|
+
continue;
|
|
399
|
+
if (Array.isArray(raw)) {
|
|
400
|
+
const names = raw.filter(isValid).map((inst) => inst.name || type);
|
|
401
|
+
if (names.length > 0)
|
|
402
|
+
groups.push({ type, instances: names });
|
|
403
|
+
}
|
|
404
|
+
else if (isValid(raw)) {
|
|
405
|
+
groups.push({ type, instances: [raw.name || type] });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (groups.length > 0) {
|
|
409
|
+
for (const g of groups) {
|
|
410
|
+
if (g.instances.length === 1) {
|
|
411
|
+
console.log(` ${g.instances[0]}: ✓ Configured`);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
console.log(` ${g.type}: [${g.instances.join(', ')}]`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
console.log(' (no channels configured)');
|
|
420
|
+
}
|
|
421
|
+
}
|
|
305
422
|
async function cmdStatus() {
|
|
306
423
|
const p = resolvePaths();
|
|
307
424
|
const pid = isRunning(p.pid);
|
|
@@ -315,11 +432,47 @@ async function cmdStatus() {
|
|
|
315
432
|
console.log(` Uptime: ${info.uptime}`);
|
|
316
433
|
if (info.cpu)
|
|
317
434
|
console.log(` CPU: ${info.cpu}%`);
|
|
318
|
-
if (info.memory)
|
|
319
|
-
|
|
435
|
+
if (info.memory) {
|
|
436
|
+
const memKB = parseInt(info.memory, 10);
|
|
437
|
+
const memStr = memKB >= 1024 ? `${(memKB / 1024).toFixed(0)} MB` : `${memKB} KB`;
|
|
438
|
+
console.log(` Memory: ${memStr}`);
|
|
439
|
+
}
|
|
320
440
|
}
|
|
321
441
|
catch { }
|
|
322
442
|
console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
|
|
443
|
+
// Runtime statistics (only when running)
|
|
444
|
+
if (fs.existsSync(p.db)) {
|
|
445
|
+
try {
|
|
446
|
+
const Database = await import('node:sqlite');
|
|
447
|
+
const db = new Database.DatabaseSync(p.db);
|
|
448
|
+
// Get recent active sessions (last 5)
|
|
449
|
+
const recentSessions = db.prepare(`
|
|
450
|
+
SELECT id, project_path, name, channel, chat_type, thread_id, agent_session_id, agent_id, metadata, updated_at
|
|
451
|
+
FROM sessions
|
|
452
|
+
WHERE deleted_at IS NULL
|
|
453
|
+
ORDER BY updated_at DESC
|
|
454
|
+
LIMIT 5
|
|
455
|
+
`).all();
|
|
456
|
+
db.close();
|
|
457
|
+
if (recentSessions.length > 0) {
|
|
458
|
+
console.log('');
|
|
459
|
+
console.log('📋 Recent Active Sessions:');
|
|
460
|
+
for (const s of recentSessions) {
|
|
461
|
+
const projectName = path.basename(s.project_path);
|
|
462
|
+
const sessionType = s.thread_id ? '话题会话' : '主会话';
|
|
463
|
+
const chatType = s.chat_type === 'group' ? '群聊' : '单聊';
|
|
464
|
+
const sessionName = s.name || '默认会话';
|
|
465
|
+
const timeAgo = formatTimeAgo(Date.now() - s.updated_at);
|
|
466
|
+
const meta = s.metadata ? JSON.parse(s.metadata) : {};
|
|
467
|
+
const dot = meta.isActive ? '•' : '○';
|
|
468
|
+
const agentId = s.agent_session_id ? ` [${s.agent_session_id}]` : '';
|
|
469
|
+
const agentType = s.agent_id || 'claude';
|
|
470
|
+
console.log(` ${dot} [${agentType}] ${projectName} / ${sessionName} (${sessionType}, ${chatType})${agentId} - ${timeAgo}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch { }
|
|
475
|
+
}
|
|
323
476
|
}
|
|
324
477
|
else {
|
|
325
478
|
console.log('⚠ EvolClaw is not running');
|
|
@@ -327,16 +480,17 @@ async function cmdStatus() {
|
|
|
327
480
|
console.log(` Stale PID file found: ${p.pid}`);
|
|
328
481
|
}
|
|
329
482
|
}
|
|
483
|
+
// Session & Project statistics (always show if DB exists)
|
|
330
484
|
if (fs.existsSync(p.db)) {
|
|
331
485
|
console.log('');
|
|
332
486
|
console.log('📦 Sessions & Projects:');
|
|
333
487
|
try {
|
|
334
488
|
const Database = await import('node:sqlite');
|
|
335
489
|
const db = new Database.DatabaseSync(p.db);
|
|
336
|
-
const totalSessions = db.prepare('SELECT count(*) as cnt FROM sessions').get();
|
|
337
|
-
const activeSessions = db.prepare(
|
|
338
|
-
const uniqueChats = db.prepare('SELECT count(DISTINCT channel_id) as cnt FROM sessions').get();
|
|
339
|
-
const projects = db.prepare('SELECT count(DISTINCT project_path) as cnt FROM sessions').get();
|
|
490
|
+
const totalSessions = db.prepare('SELECT count(*) as cnt FROM sessions WHERE deleted_at IS NULL').get();
|
|
491
|
+
const activeSessions = db.prepare("SELECT count(*) as cnt FROM sessions WHERE json_extract(metadata, '$.isActive') = true AND deleted_at IS NULL").get();
|
|
492
|
+
const uniqueChats = db.prepare('SELECT count(DISTINCT channel_id) as cnt FROM sessions WHERE deleted_at IS NULL').get();
|
|
493
|
+
const projects = db.prepare('SELECT count(DISTINCT project_path) as cnt FROM sessions WHERE deleted_at IS NULL').get();
|
|
340
494
|
db.close();
|
|
341
495
|
console.log(` Total sessions: ${totalSessions.cnt} (active: ${activeSessions.cnt})`);
|
|
342
496
|
console.log(` Unique chats: ${uniqueChats.cnt}`);
|
|
@@ -344,102 +498,59 @@ async function cmdStatus() {
|
|
|
344
498
|
}
|
|
345
499
|
catch { }
|
|
346
500
|
}
|
|
347
|
-
// Channel
|
|
501
|
+
// Channel status
|
|
348
502
|
if (fs.existsSync(p.config)) {
|
|
349
503
|
console.log('');
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
console.log(` Feishu: ✗ Connection refused (${res.msg})`);
|
|
366
|
-
}
|
|
504
|
+
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
505
|
+
if (pid) {
|
|
506
|
+
// Running: query IPC for real-time status
|
|
507
|
+
const status = await ipcQuery(p.socket, { type: 'status' });
|
|
508
|
+
if (status) {
|
|
509
|
+
console.log('🔌 Channels (live):');
|
|
510
|
+
// Group channels by channelType
|
|
511
|
+
const groups = new Map();
|
|
512
|
+
for (const [name, ch] of Object.entries(status.channels)) {
|
|
513
|
+
const type = ch.channelType || name;
|
|
514
|
+
if (!groups.has(type))
|
|
515
|
+
groups.set(type, []);
|
|
516
|
+
groups.get(type).push({ name, ch: ch });
|
|
367
517
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
518
|
+
for (const [type, instances] of groups) {
|
|
519
|
+
if (instances.length === 1) {
|
|
520
|
+
// Single instance: show instance name directly
|
|
521
|
+
const { name, ch } = instances[0];
|
|
522
|
+
const label = ch.connected ? '✓ Connected' : ch.reconnectAttempt ? `⏳ Reconnecting (${ch.reconnectAttempt}/${ch.maxAttempts})` : '✗ Disconnected';
|
|
523
|
+
console.log(` ${name}: ${label}`);
|
|
372
524
|
}
|
|
373
525
|
else {
|
|
374
|
-
|
|
526
|
+
// Multi-instance: feishu [name1 ✓, name2 ✗]
|
|
527
|
+
const parts = instances.map(({ name, ch }) => {
|
|
528
|
+
const icon = ch.connected ? '✓' : ch.reconnectAttempt ? '⏳' : '✗';
|
|
529
|
+
return `${name} ${icon}`;
|
|
530
|
+
});
|
|
531
|
+
console.log(` ${type}: [${parts.join(', ')}]`);
|
|
375
532
|
}
|
|
376
533
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const baseUrl = (config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
|
|
386
|
-
const body = JSON.stringify({ base_info: { channel_version: '1.0.0' } });
|
|
387
|
-
const uint32 = (await import('node:crypto')).default.randomBytes(4).readUInt32BE(0);
|
|
388
|
-
const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
389
|
-
const res = await fetch(`${baseUrl}/ilink/bot/getconfig`, {
|
|
390
|
-
method: 'POST',
|
|
391
|
-
headers: {
|
|
392
|
-
'Content-Type': 'application/json',
|
|
393
|
-
'AuthorizationType': 'ilink_bot_token',
|
|
394
|
-
'Authorization': `Bearer ${config.channels.wechat.token.trim()}`,
|
|
395
|
-
'X-WECHAT-UIN': wechatUin,
|
|
396
|
-
},
|
|
397
|
-
body,
|
|
398
|
-
signal: AbortSignal.timeout(10_000),
|
|
399
|
-
});
|
|
400
|
-
const resp = JSON.parse(await res.text());
|
|
401
|
-
const isExpired = resp.errcode === -14 || resp.ret === -14;
|
|
402
|
-
if (isExpired) {
|
|
403
|
-
console.log(` WeChat: ✗ Token expired (Token: ${tokenPreview}...)`);
|
|
404
|
-
console.log(' Run: evolclaw init wechat && evolclaw restart');
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
console.log(` WeChat: ✓ Connected (Token: ${tokenPreview}...)`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
catch (e) {
|
|
411
|
-
const msg = e.message || '';
|
|
412
|
-
if (msg.includes('ETIMEDOUT') || msg.includes('ENETUNREACH') || msg.includes('ENOTFOUND')) {
|
|
413
|
-
console.log(` WeChat: ✗ Connection timeout (Token: ${tokenPreview}...)`);
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
console.log(` WeChat: ✓ Configured (Token: ${tokenPreview}...)`);
|
|
417
|
-
}
|
|
534
|
+
if (status.stats) {
|
|
535
|
+
console.log('');
|
|
536
|
+
console.log('📊 Last hour:');
|
|
537
|
+
console.log(` Messages: ${status.stats.received} received, ${status.stats.completed} completed`);
|
|
538
|
+
if (status.stats.errors > 0)
|
|
539
|
+
console.log(` Errors: ${status.stats.errors}`);
|
|
540
|
+
if (status.stats.completed > 0)
|
|
541
|
+
console.log(` Avg response: ${(status.stats.avgResponseMs / 1000).toFixed(1)}s`);
|
|
418
542
|
}
|
|
419
543
|
}
|
|
420
544
|
else {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const aunDomain = config.channels?.aun?.domain;
|
|
425
|
-
const aunAgent = config.channels?.aun?.agentName;
|
|
426
|
-
const isAunPlaceholder = !aunDomain || !aunAgent ||
|
|
427
|
-
aunDomain.includes('your-') || aunDomain.includes('placeholder') ||
|
|
428
|
-
aunAgent.includes('your-') || aunAgent.includes('placeholder');
|
|
429
|
-
if (aunDomain && aunAgent && !isAunPlaceholder) {
|
|
430
|
-
console.log(` AUN: ✓ Configured (${aunAgent}@${aunDomain})`);
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
console.log(' AUN: - Not configured');
|
|
434
|
-
}
|
|
435
|
-
if (config.agents?.anthropic?.model) {
|
|
436
|
-
console.log(` Model: ${config.agents.anthropic.model}`);
|
|
437
|
-
}
|
|
438
|
-
if (config.projects?.defaultPath) {
|
|
439
|
-
console.log(` Default project: ${config.projects.defaultPath}`);
|
|
545
|
+
// IPC unreachable but PID exists — show config only
|
|
546
|
+
console.log('🔌 Channels (IPC unreachable):');
|
|
547
|
+
showConfigChannels(config);
|
|
440
548
|
}
|
|
441
549
|
}
|
|
442
|
-
|
|
550
|
+
else {
|
|
551
|
+
console.log('🔌 Channel Configuration:');
|
|
552
|
+
showConfigChannels(config);
|
|
553
|
+
}
|
|
443
554
|
}
|
|
444
555
|
console.log('');
|
|
445
556
|
console.log('📁 Log Files:');
|
|
@@ -449,9 +560,9 @@ async function cmdStatus() {
|
|
|
449
560
|
const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
|
|
450
561
|
console.log(` Main log: ${mainLog} (${sizeMB} MB)`);
|
|
451
562
|
console.log('');
|
|
452
|
-
console.log('📝 Recent activity (last
|
|
563
|
+
console.log('📝 Recent activity (last 30 lines):');
|
|
453
564
|
const content = fs.readFileSync(mainLog, 'utf-8').trim().split('\n');
|
|
454
|
-
console.log(content.slice(-
|
|
565
|
+
console.log(content.slice(-30).map(l => ` ${l}`).join('\n'));
|
|
455
566
|
}
|
|
456
567
|
else {
|
|
457
568
|
console.log(' (no log file yet)');
|
|
@@ -483,11 +594,17 @@ async function cmdRestartMonitor() {
|
|
|
483
594
|
const p = resolvePaths();
|
|
484
595
|
const restartLog = path.join(p.logs, 'restart.log');
|
|
485
596
|
const MAX_HEAL_ATTEMPTS = 3;
|
|
486
|
-
const READY_TIMEOUT =
|
|
597
|
+
const READY_TIMEOUT = 30000; // 30s(AUN sidecar 10s + Feishu 连接 12s)
|
|
598
|
+
const HEAL_TIMEOUT = 30 * 60 * 1000; // 30 分钟,让 claude 自然结束
|
|
599
|
+
const eventBus = new EventBus();
|
|
487
600
|
const log = (msg) => {
|
|
488
601
|
const line = `[${new Date().toISOString().replace('T', ' ').slice(0, 19)}] ${msg}\n`;
|
|
489
602
|
fs.appendFileSync(restartLog, line);
|
|
490
603
|
};
|
|
604
|
+
/** 检查服务是否已经在运行(ready signal 存在 + 进程存活) */
|
|
605
|
+
const isServiceAlive = () => {
|
|
606
|
+
return fs.existsSync(p.readySignal) && isRunning(p.pid) !== null;
|
|
607
|
+
};
|
|
491
608
|
log('Restart monitor started');
|
|
492
609
|
// 读取 restart-pending.json 用于后续通知
|
|
493
610
|
const pendingFile = path.join(p.dataDir, 'restart-pending.json');
|
|
@@ -527,40 +644,97 @@ async function cmdRestartMonitor() {
|
|
|
527
644
|
if (started) {
|
|
528
645
|
log('✓ Service restarted successfully');
|
|
529
646
|
archiveSelfHealLog(p, log);
|
|
530
|
-
|
|
531
|
-
cleanupPendingFile(pendingFile, log);
|
|
647
|
+
// 通知由新进程自行发送(channel-agnostic),此处不再调用 notifyChannel
|
|
532
648
|
process.exit(0);
|
|
533
649
|
}
|
|
650
|
+
// 启动失败 — 测试环境下跳过 self-heal(避免 claude -p 污染会话列表、误杀生产进程)
|
|
651
|
+
if (p.root.startsWith('/tmp/') || process.env.EVOLCLAW_TEST === '1') {
|
|
652
|
+
log('❌ Service failed to start (test environment detected, skipping self-heal)');
|
|
653
|
+
await notifyChannel(p, pendingInfo, '❌ 服务启动失败(测试环境,已跳过自动修复)', log);
|
|
654
|
+
cleanupPendingFile(pendingFile, log);
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
534
657
|
// 启动失败,进入 self-heal 循环
|
|
535
658
|
log('❌ Service failed to start, entering self-heal loop');
|
|
659
|
+
eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
|
|
536
660
|
await notifyChannel(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
|
|
537
661
|
for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
|
|
662
|
+
// 前置检查:服务可能已被上一轮 claude 修复并启动
|
|
663
|
+
if (isServiceAlive()) {
|
|
664
|
+
log(`✓ Service already running before attempt ${attempt}, skipping`);
|
|
665
|
+
await sendHealSummary(p, pendingInfo, attempt - 1, log);
|
|
666
|
+
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt - 1 });
|
|
667
|
+
archiveSelfHealLog(p, log);
|
|
668
|
+
cleanupPendingFile(pendingFile, log);
|
|
669
|
+
process.exit(0);
|
|
670
|
+
}
|
|
538
671
|
log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
|
|
672
|
+
eventBus.publish({ type: 'self-heal:attempt', attemptNumber: attempt, maxAttempts: MAX_HEAL_ATTEMPTS });
|
|
539
673
|
await notifyChannel(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
674
|
+
const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
|
|
675
|
+
// 后置检查:不管 invokeClaude 返回什么,都检查服务实际状态
|
|
676
|
+
if (isServiceAlive()) {
|
|
677
|
+
log(`✓ Service is running after attempt ${attempt}`);
|
|
678
|
+
await sendHealSummary(p, pendingInfo, attempt, log);
|
|
679
|
+
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
|
|
680
|
+
archiveSelfHealLog(p, log);
|
|
681
|
+
cleanupPendingFile(pendingFile, log);
|
|
682
|
+
process.exit(0);
|
|
683
|
+
}
|
|
543
684
|
if (!healed) {
|
|
544
685
|
log(`Self-heal attempt ${attempt} failed (claude invocation error)`);
|
|
545
686
|
continue;
|
|
546
687
|
}
|
|
547
|
-
//
|
|
688
|
+
// claude 正常完成但服务没自动启动,尝试 spawn
|
|
548
689
|
started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
|
|
549
690
|
if (started) {
|
|
550
691
|
log(`✓ Self-heal succeeded on attempt ${attempt}`);
|
|
692
|
+
await sendHealSummary(p, pendingInfo, attempt, log);
|
|
693
|
+
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
|
|
551
694
|
archiveSelfHealLog(p, log);
|
|
552
|
-
await notifyChannel(p, pendingInfo, `✅ 自愈成功!(第 ${attempt} 次修复后恢复)`, log);
|
|
553
695
|
cleanupPendingFile(pendingFile, log);
|
|
554
696
|
process.exit(0);
|
|
555
697
|
}
|
|
556
698
|
log(`Attempt ${attempt}: still failing after fix`);
|
|
557
699
|
}
|
|
558
|
-
// 全部失败
|
|
700
|
+
// 全部失败 — 最后再检查一次
|
|
701
|
+
if (isServiceAlive()) {
|
|
702
|
+
log('✓ Service recovered during final check');
|
|
703
|
+
await sendHealSummary(p, pendingInfo, MAX_HEAL_ATTEMPTS, log);
|
|
704
|
+
eventBus.publish({ type: 'self-heal:completed', success: true, attempts: MAX_HEAL_ATTEMPTS });
|
|
705
|
+
archiveSelfHealLog(p, log);
|
|
706
|
+
cleanupPendingFile(pendingFile, log);
|
|
707
|
+
process.exit(0);
|
|
708
|
+
}
|
|
559
709
|
log(`❌ All ${MAX_HEAL_ATTEMPTS} self-heal attempts failed`);
|
|
710
|
+
eventBus.publish({ type: 'self-heal:completed', success: false, attempts: MAX_HEAL_ATTEMPTS });
|
|
560
711
|
await notifyChannel(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
|
|
561
712
|
cleanupPendingFile(pendingFile, log);
|
|
562
713
|
process.exit(1);
|
|
563
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* 发送 self-heal 修复成功小结(从 self-heal.md 提取摘要)
|
|
717
|
+
*/
|
|
718
|
+
async function sendHealSummary(p, pendingInfo, attempts, log) {
|
|
719
|
+
let summary = `✅ 自动修复成功(第 ${attempts || 1} 次尝试)`;
|
|
720
|
+
try {
|
|
721
|
+
if (fs.existsSync(p.selfHealLog)) {
|
|
722
|
+
const content = fs.readFileSync(p.selfHealLog, 'utf-8');
|
|
723
|
+
// 提取最后一个 ## 章节的要点
|
|
724
|
+
const sections = content.split(/^## /m).filter(Boolean);
|
|
725
|
+
const last = sections[sections.length - 1];
|
|
726
|
+
if (last) {
|
|
727
|
+
const lines = last.split('\n').filter(l => l.startsWith('- ')).map(l => l.trim());
|
|
728
|
+
if (lines.length > 0) {
|
|
729
|
+
summary += '\n' + lines.join('\n');
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
catch { }
|
|
735
|
+
summary += '\n\n⚠️ 修复前进行中的任务已中断,如需继续请重新发送。';
|
|
736
|
+
await notifyChannel(p, pendingInfo, summary, log);
|
|
737
|
+
}
|
|
564
738
|
function sleep(ms) {
|
|
565
739
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
566
740
|
}
|
|
@@ -582,7 +756,13 @@ async function spawnAndWaitReady(p, log, timeout) {
|
|
|
582
756
|
fs.unlinkSync(p.readySignal);
|
|
583
757
|
}
|
|
584
758
|
catch { }
|
|
585
|
-
//
|
|
759
|
+
// 杀掉可能残留的进程(先读 PID 再删文件,避免数据库锁)
|
|
760
|
+
try {
|
|
761
|
+
const stalePid = parseInt(fs.readFileSync(p.pid, 'utf-8').trim(), 10);
|
|
762
|
+
if (!isNaN(stalePid))
|
|
763
|
+
platform.killProcess(stalePid, true);
|
|
764
|
+
}
|
|
765
|
+
catch { }
|
|
586
766
|
try {
|
|
587
767
|
fs.unlinkSync(p.pid);
|
|
588
768
|
}
|
|
@@ -597,6 +777,7 @@ async function spawnAndWaitReady(p, log, timeout) {
|
|
|
597
777
|
stdio: ['ignore', out, err],
|
|
598
778
|
env: {
|
|
599
779
|
...process.env,
|
|
780
|
+
EVOLCLAW_HOME: p.root,
|
|
600
781
|
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
|
601
782
|
MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
|
|
602
783
|
EVENT_LOG: process.env.EVENT_LOG || 'true',
|
|
@@ -643,16 +824,27 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
|
643
824
|
|
|
644
825
|
关键信息:
|
|
645
826
|
- 项目目录:${projectDir}
|
|
646
|
-
-
|
|
647
|
-
-
|
|
827
|
+
- EVOLCLAW_HOME:${p.root}
|
|
828
|
+
- 错误日志:${stdoutLog}
|
|
829
|
+
- 主日志:${path.join(p.logs, 'evolclaw.log')}(logger 输出在这里,包含 config 校验失败等关键错误)
|
|
648
830
|
- 修复记录:${selfHealLog}(${selfHealExists})
|
|
649
831
|
|
|
832
|
+
⚠️ 重要诊断技巧:
|
|
833
|
+
- stdout.log 可能是空的(进程秒退时 logger 输出不会到 stdout),一定要同时读 evolclaw.log
|
|
834
|
+
- 必须实际运行进程来复现错误:\`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\`,观察输出和退出码
|
|
835
|
+
- 检查是否有旧进程仍在运行:\`ps aux | grep 'node.*dist/index.js' | grep -v grep\`,旧进程可能占用端口或锁文件
|
|
836
|
+
- 可以运行 \`EVOLCLAW_HOME=${p.root} node dist/cli.js diagnose\` 快速检查配置和数据库
|
|
837
|
+
- 如果进程无任何输出就 exit(1),说明是 process.exit(1) 被显式调用,搜索源码中所有 process.exit(1) 位置
|
|
838
|
+
- evolclaw.json 有自动备份机制:运行时 config watch 检测到文件损坏会自动保存内存快照到 \`data/evolclaw-{timestamp}.json\`,同时 \`data/evolclaw.backup.json\` 是最近一次完整配置的备份。如果 evolclaw.json 损坏或缺失,可以从这些备份恢复
|
|
839
|
+
|
|
650
840
|
请执行以下步骤:
|
|
651
|
-
1.
|
|
652
|
-
2.
|
|
653
|
-
3.
|
|
654
|
-
4.
|
|
655
|
-
5.
|
|
841
|
+
1. 读取 ${stdoutLog} 和 ${path.join(p.logs, 'evolclaw.log')} 的最后 50 行
|
|
842
|
+
2. 运行 \`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\` 复现错误(设置 10 秒超时)
|
|
843
|
+
3. 如果 ${selfHealLog} 存在,先阅读之前的修复记录,避免重复尝试已失败的方案
|
|
844
|
+
4. 根据实际复现的错误修复代码
|
|
845
|
+
5. 执行 npm run build 确认编译通过
|
|
846
|
+
6. 验证修复:启动服务确认 ready.signal 已写入,然后执行 \`EVOLCLAW_HOME=${p.root} node dist/cli.js stop\` 优雅停止(restart-monitor 会负责最终启动)
|
|
847
|
+
7. 将本次修复内容追加到 ${selfHealLog},格式:
|
|
656
848
|
## 第 ${attempt} 次修复 - {时间}
|
|
657
849
|
- 错误原因:...
|
|
658
850
|
- 修复方案:...
|
|
@@ -665,6 +857,7 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
|
665
857
|
'-p', prompt,
|
|
666
858
|
'--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
|
|
667
859
|
'--output-format', 'text',
|
|
860
|
+
'--no-session-persistence',
|
|
668
861
|
], {
|
|
669
862
|
cwd: projectDir,
|
|
670
863
|
timeout,
|
|
@@ -679,12 +872,19 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
|
679
872
|
return true;
|
|
680
873
|
}
|
|
681
874
|
catch (error) {
|
|
682
|
-
|
|
683
|
-
|
|
875
|
+
if (error.killed) {
|
|
876
|
+
log(`Claude CLI timeout after ${timeout / 60000}min (attempt ${attempt})`);
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
log(`Claude CLI error: exit code ${error.code ?? 'unknown'} (attempt ${attempt})`);
|
|
880
|
+
}
|
|
684
881
|
if (error.stdout)
|
|
685
|
-
log(`
|
|
686
|
-
if (error.stderr)
|
|
687
|
-
|
|
882
|
+
log(`Claude output: ${String(error.stdout).slice(0, 500)}`);
|
|
883
|
+
if (error.stderr) {
|
|
884
|
+
const stderr = String(error.stderr).replace(/Warning: no stdin.*\n?/g, '').trim();
|
|
885
|
+
if (stderr)
|
|
886
|
+
log(`Claude stderr: ${stderr.slice(0, 300)}`);
|
|
887
|
+
}
|
|
688
888
|
return false;
|
|
689
889
|
}
|
|
690
890
|
}
|
|
@@ -699,6 +899,28 @@ function archiveSelfHealLog(p, log) {
|
|
|
699
899
|
fs.renameSync(p.selfHealLog, archivePath);
|
|
700
900
|
log(`Archived self-heal log to ${archivePath}`);
|
|
701
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* Resolve a channel instance name to its type and config object.
|
|
904
|
+
* Searches across all channel types (feishu, wechat, aun) for a matching instance.
|
|
905
|
+
*/
|
|
906
|
+
function resolveInstanceConfig(config, instanceName) {
|
|
907
|
+
for (const type of ['feishu', 'wechat', 'aun']) {
|
|
908
|
+
const raw = config.channels?.[type];
|
|
909
|
+
if (!raw)
|
|
910
|
+
continue;
|
|
911
|
+
if (Array.isArray(raw)) {
|
|
912
|
+
const inst = raw.find((i) => i.name === instanceName);
|
|
913
|
+
if (inst)
|
|
914
|
+
return { type, config: inst };
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
const name = raw.name || type;
|
|
918
|
+
if (name === instanceName)
|
|
919
|
+
return { type, config: raw };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
702
924
|
/**
|
|
703
925
|
* 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
|
|
704
926
|
* 支持 feishu / wechat,根据 pendingInfo.channel 路由
|
|
@@ -710,14 +932,20 @@ async function notifyChannel(p, pendingInfo, message, log) {
|
|
|
710
932
|
if (!fs.existsSync(configPath))
|
|
711
933
|
return;
|
|
712
934
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
713
|
-
|
|
935
|
+
const resolved = resolveInstanceConfig(config, pendingInfo.channel);
|
|
936
|
+
if (!resolved) {
|
|
937
|
+
log(`Channel instance "${pendingInfo.channel}" not found in config`);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (resolved.type === 'feishu') {
|
|
714
941
|
try {
|
|
715
|
-
|
|
942
|
+
const inst = resolved.config;
|
|
943
|
+
if (!inst.appId || !inst.appSecret)
|
|
716
944
|
return;
|
|
717
945
|
const lark = await import('@larksuiteoapi/node-sdk');
|
|
718
946
|
const client = new lark.Client({
|
|
719
|
-
appId:
|
|
720
|
-
appSecret:
|
|
947
|
+
appId: inst.appId,
|
|
948
|
+
appSecret: inst.appSecret,
|
|
721
949
|
});
|
|
722
950
|
if (pendingInfo.rootId) {
|
|
723
951
|
await client.im.message.reply({
|
|
@@ -745,13 +973,14 @@ async function notifyChannel(p, pendingInfo, message, log) {
|
|
|
745
973
|
log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
746
974
|
}
|
|
747
975
|
}
|
|
748
|
-
else if (
|
|
976
|
+
else if (resolved.type === 'wechat') {
|
|
749
977
|
try {
|
|
750
|
-
|
|
978
|
+
const inst = resolved.config;
|
|
979
|
+
if (!inst.token)
|
|
751
980
|
return;
|
|
752
981
|
const crypto = await import('node:crypto');
|
|
753
|
-
const baseUrl = (
|
|
754
|
-
const token =
|
|
982
|
+
const baseUrl = (inst.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
|
|
983
|
+
const token = inst.token;
|
|
755
984
|
// 读取缓存的 context_token
|
|
756
985
|
const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
|
|
757
986
|
let contextToken;
|
|
@@ -803,6 +1032,144 @@ async function notifyChannel(p, pendingInfo, message, log) {
|
|
|
803
1032
|
}
|
|
804
1033
|
}
|
|
805
1034
|
}
|
|
1035
|
+
// ==================== Migrate ====================
|
|
1036
|
+
async function cmdMv(oldDir, newDir) {
|
|
1037
|
+
if (!oldDir || !newDir) {
|
|
1038
|
+
console.log('Usage: evolclaw mv <old_directory> <new_directory>');
|
|
1039
|
+
console.log('Example: evolclaw mv ~/projects/old-name ~/projects/new-name');
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
const oldAbs = path.resolve(oldDir);
|
|
1043
|
+
const newAbs = path.resolve(newDir);
|
|
1044
|
+
console.log(`迁移项目: ${oldAbs} → ${newAbs}\n`);
|
|
1045
|
+
try {
|
|
1046
|
+
const r = await migrateProject(oldAbs, newAbs);
|
|
1047
|
+
if (r.claudeSessionsMoved)
|
|
1048
|
+
console.log('✓ Claude Code 会话目录已迁移');
|
|
1049
|
+
if (r.claudeHistoryUpdated)
|
|
1050
|
+
console.log('✓ Claude Code history.jsonl 已更新');
|
|
1051
|
+
if (r.codexUpdated > 0)
|
|
1052
|
+
console.log(`✓ Codex 数据库已更新 (${r.codexUpdated} 个会话)`);
|
|
1053
|
+
if (r.directoryMoved)
|
|
1054
|
+
console.log('✓ 项目目录已移动');
|
|
1055
|
+
if (r.evolclawDbUpdated > 0)
|
|
1056
|
+
console.log(`✓ EvolClaw sessions.db 已更新 (${r.evolclawDbUpdated} 个会话)`);
|
|
1057
|
+
if (r.evolclawConfigUpdated)
|
|
1058
|
+
console.log('✓ evolclaw.json projects.list 已更新');
|
|
1059
|
+
console.log('\n迁移完成!');
|
|
1060
|
+
}
|
|
1061
|
+
catch (e) {
|
|
1062
|
+
console.error(`迁移失败: ${e instanceof Error ? e.message : e}`);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
// ==================== Diagnose ====================
|
|
1067
|
+
async function cmdDiagnose() {
|
|
1068
|
+
const p = resolvePaths();
|
|
1069
|
+
let hasError = false;
|
|
1070
|
+
// 1. 检查数据目录
|
|
1071
|
+
console.log(`[diagnose] EVOLCLAW_HOME = ${p.root}`);
|
|
1072
|
+
if (!fs.existsSync(p.root)) {
|
|
1073
|
+
console.error(`[diagnose] ❌ 数据目录不存在: ${p.root}`);
|
|
1074
|
+
hasError = true;
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
console.log(`[diagnose] ✓ 数据目录存在`);
|
|
1078
|
+
}
|
|
1079
|
+
// 2. 加载并校验配置
|
|
1080
|
+
try {
|
|
1081
|
+
const config = loadConfig();
|
|
1082
|
+
console.log(`[diagnose] ✓ 配置文件加载成功: ${p.config}`);
|
|
1083
|
+
const integrity = validateConfigIntegrity(config);
|
|
1084
|
+
if (!integrity.valid) {
|
|
1085
|
+
console.error(`[diagnose] ❌ 配置完整性校验失败:\n ${integrity.reasons.join('\n ')}`);
|
|
1086
|
+
hasError = true;
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
console.log(`[diagnose] ✓ 配置完整性校验通过`);
|
|
1090
|
+
}
|
|
1091
|
+
// 3. 检查 Anthropic 配置
|
|
1092
|
+
try {
|
|
1093
|
+
const anthropic = resolveAnthropicConfig(config);
|
|
1094
|
+
console.log(`[diagnose] ✓ Anthropic 配置解析成功 (apiKey: ${anthropic.apiKey ? '已设置' : '❌ 未设置'}, model: ${anthropic.model || 'default'})`);
|
|
1095
|
+
}
|
|
1096
|
+
catch (e) {
|
|
1097
|
+
console.error(`[diagnose] ❌ Anthropic 配置解析失败: ${e instanceof Error ? e.message : e}`);
|
|
1098
|
+
hasError = true;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
catch (e) {
|
|
1102
|
+
console.error(`[diagnose] ❌ 配置文件加载失败: ${e instanceof Error ? e.message : e}`);
|
|
1103
|
+
hasError = true;
|
|
1104
|
+
}
|
|
1105
|
+
// 4. 检查数据库
|
|
1106
|
+
try {
|
|
1107
|
+
const { SessionManager } = await import('./core/session/session-manager.js');
|
|
1108
|
+
const eventBus = new EventBus();
|
|
1109
|
+
new SessionManager(p.db, eventBus);
|
|
1110
|
+
console.log(`[diagnose] ✓ 数据库初始化成功: ${p.db}`);
|
|
1111
|
+
}
|
|
1112
|
+
catch (e) {
|
|
1113
|
+
console.error(`[diagnose] ❌ 数据库初始化失败: ${e instanceof Error ? e.message : e}`);
|
|
1114
|
+
hasError = true;
|
|
1115
|
+
}
|
|
1116
|
+
// 5. 检查残留进程
|
|
1117
|
+
try {
|
|
1118
|
+
const pid = isRunning(p.pid);
|
|
1119
|
+
if (pid) {
|
|
1120
|
+
console.log(`[diagnose] ⚠️ 已有进程运行中: PID ${pid}`);
|
|
1121
|
+
}
|
|
1122
|
+
else {
|
|
1123
|
+
console.log(`[diagnose] ✓ 无残留进程`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
catch {
|
|
1127
|
+
console.log(`[diagnose] ✓ 无 PID 文件`);
|
|
1128
|
+
}
|
|
1129
|
+
// 6. 检查关键文件
|
|
1130
|
+
const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
1131
|
+
if (!fs.existsSync(appMain)) {
|
|
1132
|
+
console.error(`[diagnose] ❌ 编译产物不存在: ${appMain}`);
|
|
1133
|
+
hasError = true;
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
console.log(`[diagnose] ✓ 编译产物存在: ${appMain}`);
|
|
1137
|
+
}
|
|
1138
|
+
if (hasError) {
|
|
1139
|
+
console.error('\n[diagnose] ❌ 诊断发现问题,请修复后重试');
|
|
1140
|
+
process.exit(1);
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
console.log('\n[diagnose] ✓ 所有检查通过');
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
async function cmdTui() {
|
|
1147
|
+
const config = loadConfig();
|
|
1148
|
+
// Find the first AUN instance (TUI connects to one AUN instance)
|
|
1149
|
+
const aunResolved = resolveInstanceConfig(config, 'aun');
|
|
1150
|
+
const aun = aunResolved?.type === 'aun' ? aunResolved.config : null;
|
|
1151
|
+
if (!aun?.owner || !aun?.aid) {
|
|
1152
|
+
console.error('[tui] AUN 未配置,请先运行: evolclaw init aun');
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
// TUI requires Python + aun_core (independent of init aun which is now pure TS)
|
|
1156
|
+
const pythonCheck = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
|
|
1157
|
+
if (!platform.commandExists(pythonCheck)) {
|
|
1158
|
+
console.error(`[tui] Python 未找到 (${pythonCheck})`);
|
|
1159
|
+
console.error(' → TUI 依赖 Python 和 aun-core: pip3 install aun-core');
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1162
|
+
const pythonBin = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
|
|
1163
|
+
const cliScript = path.join(getPackageRoot(), 'aun', 'aun_cli.py');
|
|
1164
|
+
if (!fs.existsSync(cliScript)) {
|
|
1165
|
+
console.error(`[tui] aun_cli.py 不存在: ${cliScript}`);
|
|
1166
|
+
console.error(' → TUI 需要 AUN CLI 工具,请确认源码目录包含 aun/aun_cli.py');
|
|
1167
|
+
console.error(' → 安装: pip3 install aun-core && 从源码仓库获取 aun_cli.py');
|
|
1168
|
+
process.exit(1);
|
|
1169
|
+
}
|
|
1170
|
+
const child = spawn(pythonBin, [cliScript, '-a', aun.owner, '-t', aun.aid], { stdio: 'inherit' });
|
|
1171
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
1172
|
+
}
|
|
806
1173
|
// ==================== Main ====================
|
|
807
1174
|
export async function main(args) {
|
|
808
1175
|
const cmd = args[0] || 'start';
|
|
@@ -814,6 +1181,9 @@ export async function main(args) {
|
|
|
814
1181
|
else if (args[1] === 'feishu') {
|
|
815
1182
|
await cmdInitFeishu();
|
|
816
1183
|
}
|
|
1184
|
+
else if (args[1] === 'aun') {
|
|
1185
|
+
await cmdInitAun();
|
|
1186
|
+
}
|
|
817
1187
|
else {
|
|
818
1188
|
await cmdInit();
|
|
819
1189
|
}
|
|
@@ -836,18 +1206,31 @@ export async function main(args) {
|
|
|
836
1206
|
case 'restart-monitor':
|
|
837
1207
|
await cmdRestartMonitor();
|
|
838
1208
|
break;
|
|
1209
|
+
case 'mv':
|
|
1210
|
+
await cmdMv(args[1], args[2]);
|
|
1211
|
+
break;
|
|
1212
|
+
case 'diagnose':
|
|
1213
|
+
await cmdDiagnose();
|
|
1214
|
+
break;
|
|
1215
|
+
case 'tui':
|
|
1216
|
+
await cmdTui();
|
|
1217
|
+
break;
|
|
839
1218
|
default:
|
|
840
|
-
console.log(`Usage: evolclaw {init|start|stop|restart|status|logs}
|
|
1219
|
+
console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|tui|diagnose|mv}
|
|
841
1220
|
|
|
842
1221
|
Commands:
|
|
843
1222
|
init 创建配置文件 (${resolvePaths().config})
|
|
844
|
-
init wechat 微信扫码登录并写入配置
|
|
845
1223
|
init feishu 飞书扫码登录并写入配置
|
|
1224
|
+
init wechat 微信扫码登录并写入配置
|
|
1225
|
+
init aun AUN (AgentUnin.Network) 配置
|
|
846
1226
|
start 启动服务 (默认)
|
|
847
1227
|
stop 停止服务
|
|
848
1228
|
restart 重启服务
|
|
849
1229
|
status 查看状态
|
|
850
1230
|
logs 查看日志 (tail -f)
|
|
1231
|
+
tui 启动 AUN TUI 客户端
|
|
1232
|
+
diagnose 诊断启动环境(配置、数据库、进程)
|
|
1233
|
+
mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
|
|
851
1234
|
|
|
852
1235
|
Environment:
|
|
853
1236
|
EVOLCLAW_HOME 数据目录 (默认: ~/.evolclaw)
|