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.
Files changed (54) hide show
  1. package/README.md +59 -30
  2. package/data/evolclaw.sample.json +15 -4
  3. package/dist/agents/claude-runner.js +685 -0
  4. package/dist/agents/codex-runner.js +315 -0
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +580 -10
  7. package/dist/channels/feishu.js +888 -135
  8. package/dist/channels/wechat.js +127 -21
  9. package/dist/cli.js +519 -136
  10. package/dist/config.js +277 -25
  11. package/dist/core/agent-loader.js +39 -0
  12. package/dist/core/channel-loader.js +67 -0
  13. package/dist/core/command-handler.js +1537 -392
  14. package/dist/core/event-bus.js +32 -0
  15. package/dist/core/interaction-router.js +68 -0
  16. package/dist/core/message/message-bridge.js +216 -0
  17. package/dist/core/message/message-processor.js +1028 -0
  18. package/dist/core/message/message-queue.js +240 -0
  19. package/dist/core/message/stream-debouncer.js +122 -0
  20. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  21. package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
  22. package/dist/core/permission.js +259 -0
  23. package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
  24. package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
  25. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  26. package/dist/core/session/session-file-adapter.js +7 -0
  27. package/dist/core/session/session-file-health.js +45 -0
  28. package/dist/core/session/session-manager.js +1072 -0
  29. package/dist/index.js +402 -252
  30. package/dist/ipc.js +106 -0
  31. package/dist/paths.js +1 -0
  32. package/dist/types.js +3 -0
  33. package/dist/utils/{platform.js → cross-platform.js} +38 -1
  34. package/dist/utils/error-utils.js +130 -5
  35. package/dist/utils/init-channel.js +649 -0
  36. package/dist/utils/init.js +190 -53
  37. package/dist/utils/logger.js +8 -3
  38. package/dist/utils/media-cache.js +207 -0
  39. package/dist/utils/migrate-project.js +122 -0
  40. package/dist/utils/rich-content-renderer.js +228 -0
  41. package/dist/utils/stats-collector.js +102 -0
  42. package/package.json +4 -2
  43. package/dist/core/agent-runner.js +0 -348
  44. package/dist/core/message-processor.js +0 -604
  45. package/dist/core/message-queue.js +0 -116
  46. package/dist/core/message-stream.js +0 -59
  47. package/dist/core/session-manager.js +0 -664
  48. package/dist/index.js.bak +0 -340
  49. package/dist/utils/init-feishu.js +0 -261
  50. package/dist/utils/init-wechat.js +0 -170
  51. package/dist/utils/markdown-to-feishu.js +0 -94
  52. package/dist/utils/permission.js +0 -43
  53. package/dist/utils/session-file-health.js +0 -68
  54. /package/dist/core/{message-cache.js → message/message-cache.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));
@@ -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
- if (this.onSessionExpired) {
371
- 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);
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 = this.sanitizeFileName(item.file_item.file_name || `file_${Date.now()}`);
512
- 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);
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.saveToUploads(buf, fileName, channelId);
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 saveToUploads(buf, fileName, channelId) {
560
+ async saveToUploadsLocal(buf, fileName, channelId) {
529
561
  const projectPath = this.projectPathResolver
530
562
  ? await this.projectPathResolver(channelId)
531
563
  : process.cwd();
532
- const uploadsDir = path.join(projectPath, '.claude', 'uploads');
533
- fs.mkdirSync(uploadsDir, { recursive: true });
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
+ }