evolclaw 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +283 -95
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +232 -57
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +803 -247
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +217 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +411 -124
- 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} +61 -11
- package/dist/index.js +140 -57
- 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
|
}
|