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.
Files changed (43) hide show
  1. package/README.md +49 -27
  2. package/data/evolclaw.sample.json +6 -3
  3. package/dist/agents/claude-runner.js +125 -52
  4. package/dist/agents/codex-runner.js +10 -5
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +283 -95
  7. package/dist/channels/feishu.js +556 -96
  8. package/dist/channels/wechat.js +98 -74
  9. package/dist/cli.js +232 -57
  10. package/dist/config.js +185 -31
  11. package/dist/core/channel-loader.js +11 -4
  12. package/dist/core/command-handler.js +803 -247
  13. package/dist/core/interaction-router.js +68 -0
  14. package/dist/core/message/message-bridge.js +217 -0
  15. package/dist/core/{message-processor.js → message/message-processor.js} +411 -124
  16. package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
  17. package/dist/{utils → core/message}/stream-debouncer.js +1 -1
  18. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  19. package/dist/core/permission.js +212 -11
  20. package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
  21. package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
  22. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  23. package/dist/{utils → core/session}/session-file-health.js +1 -1
  24. package/dist/core/{session-manager.js → session/session-manager.js} +61 -11
  25. package/dist/index.js +140 -57
  26. package/dist/{core/ipc-server.js → ipc.js} +36 -1
  27. package/dist/types.js +3 -0
  28. package/dist/utils/cross-platform.js +38 -1
  29. package/dist/utils/error-utils.js +130 -5
  30. package/dist/utils/init-channel.js +649 -0
  31. package/dist/utils/init.js +55 -150
  32. package/dist/utils/logger.js +8 -3
  33. package/dist/utils/media-cache.js +207 -0
  34. package/dist/{core → utils}/stats-collector.js +16 -0
  35. package/package.json +3 -3
  36. package/dist/core/message-bridge.js +0 -187
  37. package/dist/utils/init-feishu.js +0 -263
  38. package/dist/utils/init-wechat.js +0 -172
  39. package/dist/utils/ipc-client.js +0 -36
  40. package/dist/utils/permission-utils.js +0 -71
  41. /package/dist/{utils → core/message}/message-cache.js +0 -0
  42. /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
  43. /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
@@ -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 res = await fetch(url);
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
- if (this.onSessionExpired) {
390
- this.onSessionExpired(`⚠️ 微信 token 已过期,通道暂停 ${pauseMin} 分钟后自动重试。\n如需立即恢复,请运行: evolclaw init wechat`);
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 = this.sanitizeFileName(item.file_item.file_name || `file_${Date.now()}`);
531
- const savePath = await this.saveToUploads(buf, fileName, channelId);
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.saveToUploads(buf, fileName, channelId);
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 saveToUploads(buf, fileName, channelId) {
560
+ async saveToUploadsLocal(buf, fileName, channelId) {
548
561
  const projectPath = this.projectPathResolver
549
562
  ? await this.projectPathResolver(channelId)
550
563
  : process.cwd();
551
- const uploadsDir = path.join(projectPath, '.evolclaw', 'uploads');
552
- fs.mkdirSync(uploadsDir, { recursive: true });
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
- return config.channels?.wechat?.enabled === true && !!config.channels?.wechat?.token;
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 wechatConfig = config.channels?.wechat;
711
- if (!wechatConfig?.token) {
784
+ const instances = await this.createChannels(config);
785
+ if (instances.length === 0) {
712
786
  throw new Error('WeChat config missing');
713
787
  }
714
- const channel = new WechatChannel({
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
  }