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/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));
|
|
@@ -132,6 +130,7 @@ export class WechatChannel {
|
|
|
132
130
|
config;
|
|
133
131
|
messageHandler;
|
|
134
132
|
abortController;
|
|
133
|
+
connected = false;
|
|
135
134
|
// 内部状态(不外泄到核心层)
|
|
136
135
|
contextTokenCache = new Map();
|
|
137
136
|
typingTicketCache = new Map();
|
|
@@ -141,6 +140,7 @@ export class WechatChannel {
|
|
|
141
140
|
// Session expired 状态
|
|
142
141
|
sessionPausedUntil = 0;
|
|
143
142
|
onSessionExpired;
|
|
143
|
+
eventBus;
|
|
144
144
|
// Project path resolver(用于保存文件到 uploads 目录)
|
|
145
145
|
projectPathResolver;
|
|
146
146
|
constructor(config) {
|
|
@@ -157,6 +157,10 @@ export class WechatChannel {
|
|
|
157
157
|
onSessionExpiredNotify(handler) {
|
|
158
158
|
this.onSessionExpired = handler;
|
|
159
159
|
}
|
|
160
|
+
/** 注册事件总线(推荐,替代 onSessionExpiredNotify) */
|
|
161
|
+
setEventBus(bus) {
|
|
162
|
+
this.eventBus = bus;
|
|
163
|
+
}
|
|
160
164
|
/** 当前是否处于 session 暂停状态 */
|
|
161
165
|
isSessionPaused() {
|
|
162
166
|
return Date.now() < this.sessionPausedUntil;
|
|
@@ -181,16 +185,34 @@ export class WechatChannel {
|
|
|
181
185
|
if (this.abortController?.signal.aborted)
|
|
182
186
|
return;
|
|
183
187
|
logger.error('[WeChat] Poll loop fatal error:', err);
|
|
188
|
+
this.connected = false;
|
|
184
189
|
});
|
|
190
|
+
this.connected = true;
|
|
185
191
|
logger.info('[WeChat] Channel connected');
|
|
186
192
|
}
|
|
187
193
|
async disconnect() {
|
|
194
|
+
this.connected = false;
|
|
188
195
|
if (this.abortController) {
|
|
189
196
|
this.abortController.abort();
|
|
190
197
|
this.abortController = undefined;
|
|
191
198
|
}
|
|
192
199
|
logger.info('[WeChat] Channel disconnected');
|
|
193
200
|
}
|
|
201
|
+
/** Get current connection status */
|
|
202
|
+
getStatus() {
|
|
203
|
+
return { connected: this.connected };
|
|
204
|
+
}
|
|
205
|
+
/** Reconnect: disconnect then connect again */
|
|
206
|
+
async reconnect() {
|
|
207
|
+
await this.disconnect();
|
|
208
|
+
try {
|
|
209
|
+
await this.connect();
|
|
210
|
+
return '重连成功';
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
return `重连失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
194
216
|
async sendMessage(to, text) {
|
|
195
217
|
if (!text || text.trim() === '') {
|
|
196
218
|
logger.warn('[WeChat] Attempted to send empty message, skipping');
|
|
@@ -366,9 +388,19 @@ export class WechatChannel {
|
|
|
366
388
|
const pauseMin = SESSION_PAUSE_DURATION_MS / 60_000;
|
|
367
389
|
this.sessionPausedUntil = Date.now() + SESSION_PAUSE_DURATION_MS;
|
|
368
390
|
logger.error(`[WeChat] Session still expired, pausing for ${pauseMin}min`);
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
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);
|
|
372
404
|
}
|
|
373
405
|
await this.sleep(SESSION_PAUSE_DURATION_MS, signal);
|
|
374
406
|
if (signal.aborted)
|
|
@@ -447,7 +479,7 @@ export class WechatChannel {
|
|
|
447
479
|
// 回调主流程
|
|
448
480
|
if (this.messageHandler) {
|
|
449
481
|
try {
|
|
450
|
-
await this.messageHandler(fromUserId, finalContent || '', fromUserId, media.images.length ? media.images : undefined);
|
|
482
|
+
await this.messageHandler(fromUserId, finalContent || '', fromUserId, media.images.length ? media.images : undefined, 'private');
|
|
451
483
|
}
|
|
452
484
|
catch (err) {
|
|
453
485
|
logger.error('[WeChat] Message handler error:', err);
|
|
@@ -508,14 +540,14 @@ export class WechatChannel {
|
|
|
508
540
|
}
|
|
509
541
|
else if (item.type === MSG_ITEM_FILE && item.file_item?.media) {
|
|
510
542
|
const buf = await downloadMedia(item.file_item.media);
|
|
511
|
-
const fileName =
|
|
512
|
-
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);
|
|
513
545
|
prompts.push(`用户发送了文件:${fileName}\n文件已保存到:${savePath}\n请使用 Read 工具读取并分析文件内容。`);
|
|
514
546
|
}
|
|
515
547
|
else if (item.type === MSG_ITEM_VIDEO && item.video_item?.media) {
|
|
516
548
|
const buf = await downloadMedia(item.video_item.media);
|
|
517
549
|
const fileName = `video_${Date.now()}.mp4`;
|
|
518
|
-
const savePath = await this.
|
|
550
|
+
const savePath = await this.saveToUploadsLocal(buf, fileName, channelId);
|
|
519
551
|
prompts.push(`用户发送了视频:${fileName}\n文件已保存到:${savePath}`);
|
|
520
552
|
}
|
|
521
553
|
}
|
|
@@ -525,19 +557,12 @@ export class WechatChannel {
|
|
|
525
557
|
}
|
|
526
558
|
return { prompt: prompts.join('\n\n'), images };
|
|
527
559
|
}
|
|
528
|
-
async
|
|
560
|
+
async saveToUploadsLocal(buf, fileName, channelId) {
|
|
529
561
|
const projectPath = this.projectPathResolver
|
|
530
562
|
? await this.projectPathResolver(channelId)
|
|
531
563
|
: process.cwd();
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
const savePath = path.join(uploadsDir, fileName);
|
|
535
|
-
fs.writeFileSync(savePath, buf);
|
|
536
|
-
return savePath;
|
|
537
|
-
}
|
|
538
|
-
/** 清理文件名:移除路径穿越字符,只保留 basename */
|
|
539
|
-
sanitizeFileName(name) {
|
|
540
|
-
return path.basename(name).replace(/[<>:"|?*\x00-\x1f]/g, '_') || `file_${Date.now()}`;
|
|
564
|
+
const { filePath } = saveToUploads(buf, fileName, projectPath);
|
|
565
|
+
return filePath;
|
|
541
566
|
}
|
|
542
567
|
// ── Media Upload (Outbound) ──────────────────────────────────────────
|
|
543
568
|
async getUploadUrl(params) {
|
|
@@ -682,3 +707,84 @@ export class WechatChannel {
|
|
|
682
707
|
});
|
|
683
708
|
}
|
|
684
709
|
}
|
|
710
|
+
import { normalizeChannelInstances } from '../config.js';
|
|
711
|
+
export class WechatChannelPlugin {
|
|
712
|
+
name = 'wechat';
|
|
713
|
+
isEnabled(config) {
|
|
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;
|
|
782
|
+
}
|
|
783
|
+
async createChannel(config) {
|
|
784
|
+
const instances = await this.createChannels(config);
|
|
785
|
+
if (instances.length === 0) {
|
|
786
|
+
throw new Error('WeChat config missing');
|
|
787
|
+
}
|
|
788
|
+
return instances[0];
|
|
789
|
+
}
|
|
790
|
+
}
|