evolclaw 2.2.0 → 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 +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +247 -84
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +132 -50
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +750 -209
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +57 -11
- package/dist/index.js +138 -54
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/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 +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
package/dist/channels/wechat.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { resolvePaths } from '../paths.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { sanitizeFileName, saveToUploads, safeFetch } from '../utils/media-cache.js';
|
|
6
7
|
const CHANNEL_VERSION = '1.0.0';
|
|
7
8
|
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
8
9
|
const DEFAULT_API_TIMEOUT_MS = 15_000;
|
|
@@ -61,10 +62,7 @@ async function downloadMedia(cdnMedia, hexKey) {
|
|
|
61
62
|
if (!cdnMedia.encrypt_query_param)
|
|
62
63
|
throw new Error('No encrypt_query_param');
|
|
63
64
|
const url = `${CDN_BASE_URL}/download?encrypted_query_param=${encodeURIComponent(cdnMedia.encrypt_query_param)}`;
|
|
64
|
-
const
|
|
65
|
-
if (!res.ok)
|
|
66
|
-
throw new Error(`CDN download failed: ${res.status}`);
|
|
67
|
-
const encrypted = Buffer.from(await res.arrayBuffer());
|
|
65
|
+
const encrypted = await safeFetch(url);
|
|
68
66
|
if (!aesKeyBase64)
|
|
69
67
|
return encrypted; // 无 key = 明文
|
|
70
68
|
return decryptAesEcb(encrypted, parseAesKey(aesKeyBase64));
|
|
@@ -142,6 +140,7 @@ export class WechatChannel {
|
|
|
142
140
|
// Session expired 状态
|
|
143
141
|
sessionPausedUntil = 0;
|
|
144
142
|
onSessionExpired;
|
|
143
|
+
eventBus;
|
|
145
144
|
// Project path resolver(用于保存文件到 uploads 目录)
|
|
146
145
|
projectPathResolver;
|
|
147
146
|
constructor(config) {
|
|
@@ -158,6 +157,10 @@ export class WechatChannel {
|
|
|
158
157
|
onSessionExpiredNotify(handler) {
|
|
159
158
|
this.onSessionExpired = handler;
|
|
160
159
|
}
|
|
160
|
+
/** 注册事件总线(推荐,替代 onSessionExpiredNotify) */
|
|
161
|
+
setEventBus(bus) {
|
|
162
|
+
this.eventBus = bus;
|
|
163
|
+
}
|
|
161
164
|
/** 当前是否处于 session 暂停状态 */
|
|
162
165
|
isSessionPaused() {
|
|
163
166
|
return Date.now() < this.sessionPausedUntil;
|
|
@@ -385,9 +388,19 @@ export class WechatChannel {
|
|
|
385
388
|
const pauseMin = SESSION_PAUSE_DURATION_MS / 60_000;
|
|
386
389
|
this.sessionPausedUntil = Date.now() + SESSION_PAUSE_DURATION_MS;
|
|
387
390
|
logger.error(`[WeChat] Session still expired, pausing for ${pauseMin}min`);
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
+
// 通知用户(通过事件总线或回调)
|
|
392
|
+
const authMsg = `⚠️ 微信 token 已过期,通道暂停 ${pauseMin} 分钟后自动重试。\n如需立即恢复,请运行: evolclaw init wechat`;
|
|
393
|
+
if (this.eventBus) {
|
|
394
|
+
this.eventBus.publish({
|
|
395
|
+
type: 'channel:health',
|
|
396
|
+
channel: 'wechat',
|
|
397
|
+
status: 'auth_error',
|
|
398
|
+
message: authMsg,
|
|
399
|
+
timestamp: Date.now(),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
else if (this.onSessionExpired) {
|
|
403
|
+
this.onSessionExpired(authMsg);
|
|
391
404
|
}
|
|
392
405
|
await this.sleep(SESSION_PAUSE_DURATION_MS, signal);
|
|
393
406
|
if (signal.aborted)
|
|
@@ -527,14 +540,14 @@ export class WechatChannel {
|
|
|
527
540
|
}
|
|
528
541
|
else if (item.type === MSG_ITEM_FILE && item.file_item?.media) {
|
|
529
542
|
const buf = await downloadMedia(item.file_item.media);
|
|
530
|
-
const fileName =
|
|
531
|
-
const savePath = await this.
|
|
543
|
+
const fileName = sanitizeFileName(item.file_item.file_name || `file_${Date.now()}`);
|
|
544
|
+
const savePath = await this.saveToUploadsLocal(buf, fileName, channelId);
|
|
532
545
|
prompts.push(`用户发送了文件:${fileName}\n文件已保存到:${savePath}\n请使用 Read 工具读取并分析文件内容。`);
|
|
533
546
|
}
|
|
534
547
|
else if (item.type === MSG_ITEM_VIDEO && item.video_item?.media) {
|
|
535
548
|
const buf = await downloadMedia(item.video_item.media);
|
|
536
549
|
const fileName = `video_${Date.now()}.mp4`;
|
|
537
|
-
const savePath = await this.
|
|
550
|
+
const savePath = await this.saveToUploadsLocal(buf, fileName, channelId);
|
|
538
551
|
prompts.push(`用户发送了视频:${fileName}\n文件已保存到:${savePath}`);
|
|
539
552
|
}
|
|
540
553
|
}
|
|
@@ -544,19 +557,12 @@ export class WechatChannel {
|
|
|
544
557
|
}
|
|
545
558
|
return { prompt: prompts.join('\n\n'), images };
|
|
546
559
|
}
|
|
547
|
-
async
|
|
560
|
+
async saveToUploadsLocal(buf, fileName, channelId) {
|
|
548
561
|
const projectPath = this.projectPathResolver
|
|
549
562
|
? await this.projectPathResolver(channelId)
|
|
550
563
|
: process.cwd();
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
const savePath = path.join(uploadsDir, fileName);
|
|
554
|
-
fs.writeFileSync(savePath, buf);
|
|
555
|
-
return savePath;
|
|
556
|
-
}
|
|
557
|
-
/** 清理文件名:移除路径穿越字符,只保留 basename */
|
|
558
|
-
sanitizeFileName(name) {
|
|
559
|
-
return path.basename(name).replace(/[<>:"|?*\x00-\x1f]/g, '_') || `file_${Date.now()}`;
|
|
564
|
+
const { filePath } = saveToUploads(buf, fileName, projectPath);
|
|
565
|
+
return filePath;
|
|
560
566
|
}
|
|
561
567
|
// ── Media Upload (Outbound) ──────────────────────────────────────────
|
|
562
568
|
async getUploadUrl(params) {
|
|
@@ -701,66 +707,84 @@ export class WechatChannel {
|
|
|
701
707
|
});
|
|
702
708
|
}
|
|
703
709
|
}
|
|
710
|
+
import { normalizeChannelInstances } from '../config.js';
|
|
704
711
|
export class WechatChannelPlugin {
|
|
705
712
|
name = 'wechat';
|
|
706
713
|
isEnabled(config) {
|
|
707
|
-
|
|
714
|
+
const raw = config.channels?.wechat;
|
|
715
|
+
if (!raw)
|
|
716
|
+
return false;
|
|
717
|
+
if (Array.isArray(raw)) {
|
|
718
|
+
return raw.some(inst => inst.enabled !== false && !!inst.token);
|
|
719
|
+
}
|
|
720
|
+
return raw.enabled === true && !!raw.token;
|
|
721
|
+
}
|
|
722
|
+
async createChannels(config) {
|
|
723
|
+
const instances = normalizeChannelInstances(config.channels?.wechat, 'wechat');
|
|
724
|
+
const result = [];
|
|
725
|
+
for (const inst of instances) {
|
|
726
|
+
if (inst.enabled === false || !inst.token)
|
|
727
|
+
continue;
|
|
728
|
+
const channel = new WechatChannel({
|
|
729
|
+
baseUrl: inst.baseUrl || 'https://ilinkai.weixin.qq.com',
|
|
730
|
+
token: inst.token,
|
|
731
|
+
});
|
|
732
|
+
const adapter = {
|
|
733
|
+
channelName: inst.name,
|
|
734
|
+
sendText: (id, text) => channel.sendMessage(id, text),
|
|
735
|
+
sendFile: (id, filePath) => channel.sendFile(id, filePath),
|
|
736
|
+
};
|
|
737
|
+
const policy = {
|
|
738
|
+
canSwitchProject: (chatType, identity) => identity === 'owner',
|
|
739
|
+
canListProjects: (chatType, identity) => identity === 'owner',
|
|
740
|
+
canCreateSession: (chatType, identity) => true,
|
|
741
|
+
canDeleteSession: (chatType, identity) => true,
|
|
742
|
+
canImportCliSession: (chatType, identity) => identity === 'owner',
|
|
743
|
+
messagePrefix: (chatType, peerName) => '',
|
|
744
|
+
showMiddleResult: (chatType, identity) => {
|
|
745
|
+
const mode = inst.showActivities ?? config.showActivities ?? 'all';
|
|
746
|
+
if (mode === 'none')
|
|
747
|
+
return false;
|
|
748
|
+
if (mode === 'dm-only')
|
|
749
|
+
return chatType === 'private';
|
|
750
|
+
if (mode === 'owner-dm-only')
|
|
751
|
+
return chatType === 'private' && identity === 'owner';
|
|
752
|
+
return true;
|
|
753
|
+
},
|
|
754
|
+
showIdleMonitor: (chatType, identity) => {
|
|
755
|
+
const mode = inst.showActivities ?? config.showActivities ?? 'all';
|
|
756
|
+
if (mode === 'none')
|
|
757
|
+
return false;
|
|
758
|
+
if (mode === 'dm-only')
|
|
759
|
+
return chatType === 'private';
|
|
760
|
+
if (mode === 'owner-dm-only')
|
|
761
|
+
return chatType === 'private' && identity === 'owner';
|
|
762
|
+
return true;
|
|
763
|
+
},
|
|
764
|
+
accumulateErrors: (chatType, identity) => true,
|
|
765
|
+
};
|
|
766
|
+
const options = {
|
|
767
|
+
fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
|
|
768
|
+
flushDelay: inst.flushDelay ?? 3, // WeChat 默认 3s
|
|
769
|
+
};
|
|
770
|
+
result.push({
|
|
771
|
+
channelType: 'wechat',
|
|
772
|
+
adapter,
|
|
773
|
+
channel,
|
|
774
|
+
policy,
|
|
775
|
+
options,
|
|
776
|
+
connect: () => channel.connect(),
|
|
777
|
+
disconnect: () => channel.disconnect(),
|
|
778
|
+
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return result;
|
|
708
782
|
}
|
|
709
783
|
async createChannel(config) {
|
|
710
|
-
const
|
|
711
|
-
if (
|
|
784
|
+
const instances = await this.createChannels(config);
|
|
785
|
+
if (instances.length === 0) {
|
|
712
786
|
throw new Error('WeChat config missing');
|
|
713
787
|
}
|
|
714
|
-
|
|
715
|
-
baseUrl: wechatConfig.baseUrl || 'https://ilinkai.weixin.qq.com',
|
|
716
|
-
token: wechatConfig.token,
|
|
717
|
-
});
|
|
718
|
-
const adapter = {
|
|
719
|
-
name: 'wechat',
|
|
720
|
-
sendText: (id, text) => channel.sendMessage(id, text),
|
|
721
|
-
sendFile: (id, filePath) => channel.sendFile(id, filePath),
|
|
722
|
-
};
|
|
723
|
-
const policy = {
|
|
724
|
-
canSwitchProject: (chatType, identity) => identity === 'owner',
|
|
725
|
-
canListProjects: (chatType, identity) => identity === 'owner',
|
|
726
|
-
canCreateSession: (chatType, identity) => true,
|
|
727
|
-
canDeleteSession: (chatType, identity) => true,
|
|
728
|
-
canImportCliSession: (chatType, identity) => identity === 'owner',
|
|
729
|
-
messagePrefix: (chatType, peerName) => '',
|
|
730
|
-
showMiddleResult: (chatType, identity) => {
|
|
731
|
-
const mode = wechatConfig.showActivities ?? config.showActivities ?? 'all';
|
|
732
|
-
if (mode === 'none')
|
|
733
|
-
return false;
|
|
734
|
-
if (mode === 'dm-only')
|
|
735
|
-
return chatType === 'private';
|
|
736
|
-
if (mode === 'owner-dm-only')
|
|
737
|
-
return chatType === 'private' && identity === 'owner';
|
|
738
|
-
return true;
|
|
739
|
-
},
|
|
740
|
-
showIdleMonitor: (chatType, identity) => {
|
|
741
|
-
const mode = wechatConfig.showActivities ?? config.showActivities ?? 'all';
|
|
742
|
-
if (mode === 'none')
|
|
743
|
-
return false;
|
|
744
|
-
if (mode === 'dm-only')
|
|
745
|
-
return chatType === 'private';
|
|
746
|
-
if (mode === 'owner-dm-only')
|
|
747
|
-
return chatType === 'private' && identity === 'owner';
|
|
748
|
-
return true;
|
|
749
|
-
},
|
|
750
|
-
accumulateErrors: (chatType, identity) => true,
|
|
751
|
-
};
|
|
752
|
-
const options = {
|
|
753
|
-
fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
|
|
754
|
-
flushDelay: wechatConfig.flushDelay ?? 3, // WeChat 默认 3s
|
|
755
|
-
};
|
|
756
|
-
return {
|
|
757
|
-
adapter,
|
|
758
|
-
channel,
|
|
759
|
-
policy,
|
|
760
|
-
options,
|
|
761
|
-
connect: () => channel.connect(),
|
|
762
|
-
disconnect: () => channel.disconnect(),
|
|
763
|
-
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
764
|
-
};
|
|
788
|
+
return instances[0];
|
|
765
789
|
}
|
|
766
790
|
}
|
package/dist/cli.js
CHANGED
|
@@ -6,11 +6,9 @@ import { promisify } from 'util';
|
|
|
6
6
|
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
|
|
7
7
|
import { loadConfig, validateConfigIntegrity, resolveAnthropicConfig } from './config.js';
|
|
8
8
|
import { migrateProject } from './utils/migrate-project.js';
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { cmdInitWechat } from './utils/init-wechat.js';
|
|
13
|
-
import { cmdInitFeishu } from './utils/init-feishu.js';
|
|
9
|
+
import { cmdInit } from './utils/init.js';
|
|
10
|
+
import { ipcQuery } from './ipc.js';
|
|
11
|
+
import { cmdInitWechat, cmdInitFeishu, cmdInitAun } from './utils/init-channel.js';
|
|
14
12
|
import * as platform from './utils/cross-platform.js';
|
|
15
13
|
import { EventBus } from './core/event-bus.js';
|
|
16
14
|
// Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
|
|
@@ -200,9 +198,11 @@ async function cmdStart() {
|
|
|
200
198
|
process.exit(1);
|
|
201
199
|
}
|
|
202
200
|
// 检查是否有残留进程(PID 文件已丢失但进程还在)
|
|
201
|
+
// 只清理属于当前 EVOLCLAW_HOME 的进程,避免误杀其他实例
|
|
203
202
|
let hasOrphan = false;
|
|
204
203
|
const evolclawMain = path.join(getPackageRoot(), 'dist', 'index.js');
|
|
205
|
-
const
|
|
204
|
+
const allPids = platform.findProcesses(evolclawMain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
205
|
+
const orphanPids = allPids.filter(pid => platform.getProcessEnv(pid, 'EVOLCLAW_HOME') === p.root);
|
|
206
206
|
if (orphanPids.length > 0) {
|
|
207
207
|
console.log(`⚠ 发现 ${orphanPids.length} 个残留进程,正在清理...`);
|
|
208
208
|
for (const p of orphanPids) {
|
|
@@ -231,6 +231,7 @@ async function cmdStart() {
|
|
|
231
231
|
stdio: ['ignore', out, err],
|
|
232
232
|
env: {
|
|
233
233
|
...process.env,
|
|
234
|
+
EVOLCLAW_HOME: p.root,
|
|
234
235
|
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
|
235
236
|
MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
|
|
236
237
|
EVENT_LOG: process.env.EVENT_LOG || 'true',
|
|
@@ -385,27 +386,37 @@ function formatTimeAgo(ms) {
|
|
|
385
386
|
return `${day}天前`;
|
|
386
387
|
}
|
|
387
388
|
function showConfigChannels(config) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
+
}
|
|
401
407
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
+
}
|
|
406
417
|
}
|
|
407
418
|
else {
|
|
408
|
-
console.log('
|
|
419
|
+
console.log(' (no channels configured)');
|
|
409
420
|
}
|
|
410
421
|
}
|
|
411
422
|
async function cmdStatus() {
|
|
@@ -421,8 +432,11 @@ async function cmdStatus() {
|
|
|
421
432
|
console.log(` Uptime: ${info.uptime}`);
|
|
422
433
|
if (info.cpu)
|
|
423
434
|
console.log(` CPU: ${info.cpu}%`);
|
|
424
|
-
if (info.memory)
|
|
425
|
-
|
|
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
|
+
}
|
|
426
440
|
}
|
|
427
441
|
catch { }
|
|
428
442
|
console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
|
|
@@ -493,13 +507,29 @@ async function cmdStatus() {
|
|
|
493
507
|
const status = await ipcQuery(p.socket, { type: 'status' });
|
|
494
508
|
if (status) {
|
|
495
509
|
console.log('🔌 Channels (live):');
|
|
510
|
+
// Group channels by channelType
|
|
511
|
+
const groups = new Map();
|
|
496
512
|
for (const [name, ch] of Object.entries(status.channels)) {
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
513
|
+
const type = ch.channelType || name;
|
|
514
|
+
if (!groups.has(type))
|
|
515
|
+
groups.set(type, []);
|
|
516
|
+
groups.get(type).push({ name, ch: ch });
|
|
517
|
+
}
|
|
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}`);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
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(', ')}]`);
|
|
532
|
+
}
|
|
503
533
|
}
|
|
504
534
|
if (status.stats) {
|
|
505
535
|
console.log('');
|
|
@@ -530,9 +560,9 @@ async function cmdStatus() {
|
|
|
530
560
|
const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
|
|
531
561
|
console.log(` Main log: ${mainLog} (${sizeMB} MB)`);
|
|
532
562
|
console.log('');
|
|
533
|
-
console.log('📝 Recent activity (last
|
|
563
|
+
console.log('📝 Recent activity (last 30 lines):');
|
|
534
564
|
const content = fs.readFileSync(mainLog, 'utf-8').trim().split('\n');
|
|
535
|
-
console.log(content.slice(-
|
|
565
|
+
console.log(content.slice(-30).map(l => ` ${l}`).join('\n'));
|
|
536
566
|
}
|
|
537
567
|
else {
|
|
538
568
|
console.log(' (no log file yet)');
|
|
@@ -617,6 +647,13 @@ async function cmdRestartMonitor() {
|
|
|
617
647
|
// 通知由新进程自行发送(channel-agnostic),此处不再调用 notifyChannel
|
|
618
648
|
process.exit(0);
|
|
619
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
|
+
}
|
|
620
657
|
// 启动失败,进入 self-heal 循环
|
|
621
658
|
log('❌ Service failed to start, entering self-heal loop');
|
|
622
659
|
eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
|
|
@@ -719,7 +756,13 @@ async function spawnAndWaitReady(p, log, timeout) {
|
|
|
719
756
|
fs.unlinkSync(p.readySignal);
|
|
720
757
|
}
|
|
721
758
|
catch { }
|
|
722
|
-
//
|
|
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 { }
|
|
723
766
|
try {
|
|
724
767
|
fs.unlinkSync(p.pid);
|
|
725
768
|
}
|
|
@@ -734,6 +777,7 @@ async function spawnAndWaitReady(p, log, timeout) {
|
|
|
734
777
|
stdio: ['ignore', out, err],
|
|
735
778
|
env: {
|
|
736
779
|
...process.env,
|
|
780
|
+
EVOLCLAW_HOME: p.root,
|
|
737
781
|
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
|
|
738
782
|
MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
|
|
739
783
|
EVENT_LOG: process.env.EVENT_LOG || 'true',
|
|
@@ -813,6 +857,7 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
|
813
857
|
'-p', prompt,
|
|
814
858
|
'--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
|
|
815
859
|
'--output-format', 'text',
|
|
860
|
+
'--no-session-persistence',
|
|
816
861
|
], {
|
|
817
862
|
cwd: projectDir,
|
|
818
863
|
timeout,
|
|
@@ -854,6 +899,28 @@ function archiveSelfHealLog(p, log) {
|
|
|
854
899
|
fs.renameSync(p.selfHealLog, archivePath);
|
|
855
900
|
log(`Archived self-heal log to ${archivePath}`);
|
|
856
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
|
+
}
|
|
857
924
|
/**
|
|
858
925
|
* 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
|
|
859
926
|
* 支持 feishu / wechat,根据 pendingInfo.channel 路由
|
|
@@ -865,14 +932,20 @@ async function notifyChannel(p, pendingInfo, message, log) {
|
|
|
865
932
|
if (!fs.existsSync(configPath))
|
|
866
933
|
return;
|
|
867
934
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
868
|
-
|
|
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') {
|
|
869
941
|
try {
|
|
870
|
-
|
|
942
|
+
const inst = resolved.config;
|
|
943
|
+
if (!inst.appId || !inst.appSecret)
|
|
871
944
|
return;
|
|
872
945
|
const lark = await import('@larksuiteoapi/node-sdk');
|
|
873
946
|
const client = new lark.Client({
|
|
874
|
-
appId:
|
|
875
|
-
appSecret:
|
|
947
|
+
appId: inst.appId,
|
|
948
|
+
appSecret: inst.appSecret,
|
|
876
949
|
});
|
|
877
950
|
if (pendingInfo.rootId) {
|
|
878
951
|
await client.im.message.reply({
|
|
@@ -900,13 +973,14 @@ async function notifyChannel(p, pendingInfo, message, log) {
|
|
|
900
973
|
log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
901
974
|
}
|
|
902
975
|
}
|
|
903
|
-
else if (
|
|
976
|
+
else if (resolved.type === 'wechat') {
|
|
904
977
|
try {
|
|
905
|
-
|
|
978
|
+
const inst = resolved.config;
|
|
979
|
+
if (!inst.token)
|
|
906
980
|
return;
|
|
907
981
|
const crypto = await import('node:crypto');
|
|
908
|
-
const baseUrl = (
|
|
909
|
-
const token =
|
|
982
|
+
const baseUrl = (inst.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
|
|
983
|
+
const token = inst.token;
|
|
910
984
|
// 读取缓存的 context_token
|
|
911
985
|
const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
|
|
912
986
|
let contextToken;
|
|
@@ -1030,7 +1104,7 @@ async function cmdDiagnose() {
|
|
|
1030
1104
|
}
|
|
1031
1105
|
// 4. 检查数据库
|
|
1032
1106
|
try {
|
|
1033
|
-
const { SessionManager } = await import('./core/session-manager.js');
|
|
1107
|
+
const { SessionManager } = await import('./core/session/session-manager.js');
|
|
1034
1108
|
const eventBus = new EventBus();
|
|
1035
1109
|
new SessionManager(p.db, eventBus);
|
|
1036
1110
|
console.log(`[diagnose] ✓ 数据库初始化成功: ${p.db}`);
|
|
@@ -1071,20 +1145,28 @@ async function cmdDiagnose() {
|
|
|
1071
1145
|
}
|
|
1072
1146
|
async function cmdTui() {
|
|
1073
1147
|
const config = loadConfig();
|
|
1074
|
-
|
|
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;
|
|
1075
1151
|
if (!aun?.owner || !aun?.aid) {
|
|
1076
1152
|
console.error('[tui] AUN 未配置,请先运行: evolclaw init aun');
|
|
1077
1153
|
process.exit(1);
|
|
1078
1154
|
}
|
|
1079
|
-
//
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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');
|
|
1084
1160
|
process.exit(1);
|
|
1085
1161
|
}
|
|
1086
1162
|
const pythonBin = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
|
|
1087
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
|
+
}
|
|
1088
1170
|
const child = spawn(pythonBin, [cliScript, '-a', aun.owner, '-t', aun.aid], { stdio: 'inherit' });
|
|
1089
1171
|
child.on('exit', (code) => process.exit(code ?? 0));
|
|
1090
1172
|
}
|