evolclaw 3.1.4 → 3.1.6

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 (99) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/agents/claude-runner.js +398 -161
  3. package/dist/agents/kit-renderer.js +191 -25
  4. package/dist/aun/aid/agentmd.js +75 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/group.js +2 -2
  10. package/dist/aun/msg/p2p.js +26 -2
  11. package/dist/aun/rpc/connection.js +23 -30
  12. package/dist/channels/aun.js +174 -99
  13. package/dist/channels/dingtalk.js +2 -1
  14. package/dist/channels/feishu.js +301 -199
  15. package/dist/channels/qqbot.js +2 -1
  16. package/dist/channels/wechat.js +2 -1
  17. package/dist/channels/wecom.js +2 -1
  18. package/dist/cli/agent.js +21 -16
  19. package/dist/cli/bench.js +41 -28
  20. package/dist/cli/help.js +8 -0
  21. package/dist/cli/index.js +176 -87
  22. package/dist/cli/init-channel.js +5 -1
  23. package/dist/cli/init.js +37 -21
  24. package/dist/cli/link-rules.js +1 -7
  25. package/dist/cli/model.js +549 -0
  26. package/dist/cli/net-check.js +133 -50
  27. package/dist/cli/watch-msg.js +7 -7
  28. package/dist/cli/watch-web/debug-log.js +18 -0
  29. package/dist/cli/watch-web/server.js +306 -0
  30. package/dist/cli/watch-web/sources/aid.js +63 -0
  31. package/dist/cli/watch-web/sources/msg.js +70 -0
  32. package/dist/cli/watch-web/sources/session.js +638 -0
  33. package/dist/cli/watch-web/sources/types.js +10 -0
  34. package/dist/cli/watch-web/static/app.js +546 -0
  35. package/dist/cli/watch-web/static/index.html +54 -0
  36. package/dist/cli/watch-web/static/style.css +247 -0
  37. package/dist/config-store.js +1 -22
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +261 -133
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -22
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/im-renderer.js +9 -20
  44. package/dist/core/message/message-bridge.js +13 -9
  45. package/dist/core/message/message-log.js +2 -2
  46. package/dist/core/message/message-processor.js +211 -123
  47. package/dist/core/message/stream-idle-monitor.js +21 -0
  48. package/dist/core/model/model-catalog.js +215 -0
  49. package/dist/core/model/model-scope.js +250 -0
  50. package/dist/core/relation/peer-identity.js +58 -55
  51. package/dist/core/relation/peer-key.js +16 -0
  52. package/dist/core/session/session-fs-store.js +34 -55
  53. package/dist/core/session/session-key.js +24 -0
  54. package/dist/core/session/session-manager.js +308 -251
  55. package/dist/core/session/session-mapper.js +9 -4
  56. package/dist/core/trigger/manager.js +3 -3
  57. package/dist/core/trigger/parser.js +4 -4
  58. package/dist/core/trigger/scheduler.js +22 -7
  59. package/dist/index.js +61 -7
  60. package/dist/ipc.js +23 -1
  61. package/dist/utils/error-utils.js +6 -0
  62. package/dist/utils/process-introspect.js +7 -5
  63. package/kits/docs/GUIDE.md +2 -2
  64. package/kits/docs/INDEX.md +8 -8
  65. package/kits/docs/channels/aun.md +56 -17
  66. package/kits/docs/channels/feishu.md +41 -12
  67. package/kits/docs/context-assembly.md +182 -0
  68. package/kits/docs/evolclaw/INDEX.md +43 -0
  69. package/kits/docs/evolclaw/agent.md +49 -0
  70. package/kits/docs/evolclaw/aid.md +49 -0
  71. package/kits/docs/evolclaw/ctl.md +46 -0
  72. package/kits/docs/evolclaw/group.md +89 -0
  73. package/kits/docs/evolclaw/model.md +51 -0
  74. package/kits/docs/evolclaw/msg.md +91 -0
  75. package/kits/docs/evolclaw/rpc.md +35 -0
  76. package/kits/docs/evolclaw/storage.md +49 -0
  77. package/kits/docs/venues/aun-group.md +10 -0
  78. package/kits/docs/venues/aun-private.md +10 -0
  79. package/kits/docs/venues/client-desktop.md +10 -0
  80. package/kits/docs/venues/client-mobile.md +10 -0
  81. package/kits/docs/venues/feishu-group.md +13 -0
  82. package/kits/docs/venues/feishu-private.md +9 -0
  83. package/kits/docs/venues/group.md +23 -0
  84. package/kits/docs/venues/private.md +10 -0
  85. package/kits/eck_manifest.json +81 -36
  86. package/kits/rules/01-overview.md +20 -10
  87. package/kits/rules/06-channel.md +34 -27
  88. package/kits/templates/system-fragments/baseagent.md +7 -1
  89. package/kits/templates/system-fragments/channel.md +7 -5
  90. package/kits/templates/system-fragments/commands.md +19 -0
  91. package/kits/templates/system-fragments/session.md +19 -3
  92. package/kits/templates/system-fragments/venue.md +24 -0
  93. package/package.json +10 -5
  94. package/dist/aun/aid/lifecycle-log.js +0 -33
  95. package/dist/utils/aid-lifecycle-log.js +0 -33
  96. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  97. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  98. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  99. package/kits/docs/evolclaw/tools.md +0 -25
@@ -12,8 +12,8 @@ import { appendAidEvent } from '../utils/instance-registry.js';
12
12
  import { appendMessageLog, buildOutboundEntry } from '../core/message/message-log.js';
13
13
  import { chatDirPath } from '../core/session/session-fs-store.js';
14
14
  import { appendAidLifecycle } from '../aun/aid/identity.js';
15
- import { createAunClient } from '../aun/aid/client.js';
16
- import { loadAgent, saveAgent, loadProcessConfig } from '../config-store.js';
15
+ import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
16
+ import { loadAgent, saveAgent } from '../config-store.js';
17
17
  import { getProcessStartTime } from '../utils/process-introspect.js';
18
18
  import * as outbox from '../aun/outbox.js';
19
19
  import { guessMime, formatSize } from '../utils/media-cache.js';
@@ -61,6 +61,9 @@ function getEvolclawVersion() {
61
61
  export class AUNChannel {
62
62
  config;
63
63
  client = null;
64
+ store = null;
65
+ /** 实际连接的网关 URL(来自 authenticate() 返回值 / connection.state 事件),替代旧 (client as any)._gatewayUrl。 */
66
+ gatewayUrl = '';
64
67
  projectPathProvider;
65
68
  messageHandler;
66
69
  recallHandler;
@@ -115,11 +118,6 @@ export class AUNChannel {
115
118
  */
116
119
  async callAndTrace(method, params, opts) {
117
120
  this.trace('OUT', method, params);
118
- // [DIAG-STALE] 记录调用瞬间 SDK 内部 _state,证明是否在 reconnecting 中误发
119
- const sdkStateBefore = this.client?._state ?? 'no-client';
120
- if (sdkStateBefore !== 'connected') {
121
- logger.warn(`[AUN][DIAG-STALE] callAndTrace ${method} on non-connected SDK: sdk_state=${sdkStateBefore} evolclaw_connected=${this.connected}`);
122
- }
123
121
  try {
124
122
  const result = await this.client.call(method, params);
125
123
  if (!opts?.silentOk) {
@@ -137,9 +135,6 @@ export class AUNChannel {
137
135
  code: e?.code,
138
136
  name: e?.name,
139
137
  });
140
- // [DIAG-STALE] 失败时再记录一次 SDK _state,看错误类型是否为 ConnectionError
141
- const sdkStateAfter = this.client?._state ?? 'no-client';
142
- logger.warn(`[AUN][DIAG-STALE] callAndTrace ${method} FAILED: err_name=${e?.name ?? '?'} err_code=${e?.code ?? '?'} sdk_state_before=${sdkStateBefore} sdk_state_after=${sdkStateAfter} evolclaw_connected=${this.connected}`);
143
138
  logger.warn(`${this.logPrefix()} rpc ${method} failed: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
144
139
  throw e;
145
140
  }
@@ -545,12 +540,17 @@ export class AUNChannel {
545
540
  }
546
541
  this.client = null;
547
542
  }
543
+ if (this.store) {
544
+ try {
545
+ this.store.close();
546
+ }
547
+ catch { /* ignore */ }
548
+ this.store = null;
549
+ }
548
550
  this.connected = false;
549
551
  const aunPath = this.config.keystorePath || resolveRoot();
550
552
  const aidName = this.config.aid;
551
- const encryptionSeed = loadProcessConfig().aun?.encryptionSeed
552
- || process.env.AUN_ENCRYPTION_SEED
553
- || 'evol';
553
+ // encryptionSeed getAidStore 内部解析(config / env / 'evol')
554
554
  // Migration from ~/.aun is handled by ensureDataDirs() at startup with a marker file.
555
555
  // Gateway URL 解析:优先用配置的 gatewayUrl,否则通过 well-known 自动发现
556
556
  let gateway = this.config.gatewayUrl || '';
@@ -571,16 +571,18 @@ export class AUNChannel {
571
571
  throw new Error('Cannot resolve gateway URL from AID');
572
572
  }
573
573
  logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
574
- // Create client with FileSecretStore (AES-256-GCM)
575
- // 不传 encryption_seed 时,SDK 自动从 {aun_path}/.seed 文件派生密钥(与 aun_cli.py 对齐)
576
- const client = await createAunClient({
574
+ // 构造 AIDStore(slot=evolclaw daemon,与 cli/netcheck 共享 evolclaw 隔离键)
575
+ // encryptionSeed / rootCaPath getAidStore 内部注入
576
+ const store = await getAidStore({
577
+ slotId: SLOT.daemon,
577
578
  aunPath,
578
- encryptionSeed,
579
- aunSdkLog: this.config.aunSdkLog ?? true,
579
+ debug: this.config.aunSdkLog ?? false,
580
580
  });
581
+ this.store = store;
582
+ const client = await loadClient(store, aidName);
581
583
  this.client = client;
582
- // Set gateway URL (internal property, same as Python SDK)
583
- client._gatewayUrl = gateway;
584
+ // 记录应用层发现的 gateway 作为初始值(authenticate 后会用权威值覆盖)
585
+ this.gatewayUrl = gateway;
584
586
  // Register event handlers before connecting
585
587
  client.on('message.received', (data) => {
586
588
  this.trace('IN', 'message.received', data);
@@ -629,30 +631,16 @@ export class AUNChannel {
629
631
  const d = data;
630
632
  logger.warn(`${this.logPrefix()} Group message undecryptable: group=${d.group_id} from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
631
633
  });
632
- // Authenticate
633
- // Workaround: SDK 0.3.x _loadIdentityOrRaise doesn't set identity.aid from requested aid,
634
- // causing gateway "missing aid" error. Patch to backfill aid on loaded identity.
635
- const authFlow = client._auth;
636
- if (authFlow && typeof authFlow._loadIdentityOrRaise === 'function') {
637
- const origLoad = authFlow._loadIdentityOrRaise.bind(authFlow);
638
- authFlow._loadIdentityOrRaise = (aid) => {
639
- const identity = origLoad(aid);
640
- if (identity && !identity.aid)
641
- identity.aid = aid ?? authFlow._aid;
642
- return identity;
643
- };
644
- }
645
- let accessToken;
634
+ // Authenticate(拿权威 gateway 用于日志/状态;connect 内部也会复用 token)
646
635
  try {
647
636
  logger.info(`${this.logPrefix()} Authenticating as ${aidName}...`);
648
637
  this.trace('OUT', 'auth.authenticate', { aid: aidName });
649
- const auth = await client.auth.authenticate(aidName ? { aid: aidName } : undefined);
650
- this.trace('OUT', 'auth.authenticate.ok', { aid: auth.aid, gateway: auth.gateway, hasToken: !!auth.access_token });
651
- this.trace('IN', 'auth.result', { aid: auth.aid, gateway: auth.gateway, hasToken: !!auth.access_token });
652
- accessToken = auth.access_token;
653
- const resolvedGateway = auth.gateway || gateway;
654
- client._gatewayUrl = resolvedGateway;
655
- logger.info(`${this.logPrefix()} Authenticated as ${auth.aid ?? '?'}, gateway=${resolvedGateway}`);
638
+ const auth = await client.authenticate();
639
+ this.trace('OUT', 'auth.authenticate.ok', { aid: client.aid, gateway: auth?.gateway, hasToken: !!auth?.access_token });
640
+ this.trace('IN', 'auth.result', { aid: client.aid, gateway: auth?.gateway, hasToken: !!auth?.access_token });
641
+ const resolvedGateway = String(auth?.gateway ?? gateway);
642
+ this.gatewayUrl = resolvedGateway;
643
+ logger.info(`${this.logPrefix()} Authenticated as ${client.aid ?? '?'}, gateway=${resolvedGateway}`);
656
644
  }
657
645
  catch (e) {
658
646
  const errMsg = e.message || String(e);
@@ -661,15 +649,9 @@ export class AUNChannel {
661
649
  logger.error(`${this.logPrefix()} Authentication failed (${errName}): ${errMsg}`);
662
650
  if (e.stack)
663
651
  logger.debug(`${this.logPrefix()} Auth stack: ${e.stack}`);
664
- // Fallback: try direct token from env/config (legacy)
665
- accessToken = this.config.accessToken || process.env.AUN_ACCESS_TOKEN || '';
666
- if (!accessToken) {
667
- logger.error(`${this.logPrefix()} No accessToken fallback available, scheduling retry`);
668
- this.setAidStatus('failed', { lastError: `${errName}: ${errMsg}`.slice(0, 80) });
669
- this.scheduleReconnect();
670
- throw new Error('Authentication failed and no accessToken fallback available');
671
- }
672
- logger.warn(`${this.logPrefix()} Using accessToken fallback`);
652
+ this.setAidStatus('failed', { lastError: `${errName}: ${errMsg}`.slice(0, 80) });
653
+ this.scheduleReconnect();
654
+ throw new Error(`Authentication failed: ${errName}: ${errMsg}`);
673
655
  }
674
656
  // Connect (SDK auto_reconnect handles transient failures)
675
657
  try {
@@ -678,11 +660,18 @@ export class AUNChannel {
678
660
  agentName: this.config.agentName,
679
661
  channelName: this.config.channelName,
680
662
  });
681
- this.trace('OUT', 'client.connect', { gateway: client._gatewayUrl, extra_info: extraInfo });
682
- await client.connect({ access_token: accessToken, gateway: client._gatewayUrl, extra_info: extraInfo },
683
- // max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
684
- // initial_delay=1s,max_delay=300s(5min 封顶)
685
- { auto_reconnect: true, retry: { max_attempts: 0, initial_delay: 1.0, max_delay: 300.0 } });
663
+ this.trace('OUT', 'client.connect', { gateway: this.gatewayUrl, extra_info: extraInfo });
664
+ await client.connect({
665
+ // connection_kind 默认 long;slot 已由 AID 携带(evolclaw daemon)
666
+ // extra_info:互踢诊断名片(0.4.3 公开 connect 已支持透传)
667
+ extra_info: extraInfo,
668
+ // max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
669
+ // initial_delay=1s,max_delay=300s(5min 封顶)
670
+ auto_reconnect: true,
671
+ retry_max_attempts: 0,
672
+ retry_initial_delay: 1.0,
673
+ retry_max_delay: 300.0,
674
+ });
686
675
  this.trace('OUT', 'client.connect.ok', { aid: client.aid });
687
676
  this._aid = this.client.aid ?? undefined;
688
677
  const deviceId = this.client._device_id ?? '';
@@ -692,7 +681,7 @@ export class AUNChannel {
692
681
  this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
693
682
  this.connected = true;
694
683
  this.connectedAt = Date.now();
695
- this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.client?._gatewayUrl });
684
+ this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
696
685
  // Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
697
686
  // if cert is missing, it falls back to public key SPKI fingerprint which
698
687
  // causes peer cert lookup failures. Backfill from keystore if needed.
@@ -705,8 +694,8 @@ export class AUNChannel {
705
694
  }
706
695
  }
707
696
  logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
708
- appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.client._gatewayUrl });
709
- appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.client._gatewayUrl });
697
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
698
+ appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
710
699
  // Send welcome message to owner after first connection
711
700
  await this.sendWelcomeMessage();
712
701
  }
@@ -785,13 +774,10 @@ tags:
785
774
 
786
775
  EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
787
776
  `;
788
- // Write locally
789
- fs.mkdirSync(path.dirname(agentMdLocalPath), { recursive: true });
790
- fs.writeFileSync(agentMdLocalPath, newAgentMd, 'utf-8');
791
- logger.info(`${this.logPrefix()} Updated agent.md for ${aidName}`);
792
- // Publish to AUN network via publishAgentMd (auto-sign)
777
+ // Write locally and publish to AUN network (auto-sign)
793
778
  try {
794
- await this.client.publishAgentMd();
779
+ const { agentmdPut } = await import('../aun/aid/agentmd.js');
780
+ await agentmdPut(newAgentMd, { aid: aidName, store: this.store });
795
781
  logger.info(`${this.logPrefix()} Published agent.md to AUN network`);
796
782
  }
797
783
  catch (e) {
@@ -804,18 +790,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
804
790
 
805
791
  📋 **日常使用方法**:
806
792
 
807
- 1. **绑定项目**:发送 \`/bind <项目路径>\` 绑定工作目录
808
- 2. **查看帮助**:发送 \`/help\` 查看所有可用命令
809
- 3. **切换项目**:发送 \`/project <项目名>\` 切换到其他项目
810
- 4. **查看状态**:发送 \`/status\` 查看当前会话状态
811
- 5. **会话管理**:发送 \`/session\` 查看和切换会话
793
+ 1. **查看帮助**:发送 \`/help\` 查看所有可用命令
794
+ 2. **查看状态**:发送 \`/status\` 查看当前会话状态
795
+ 3. **会话管理**:发送 \`/session\` 查看和切换会话
812
796
 
813
797
  💡 **提示**:
814
798
  - 直接发送消息即可与 Claude/Codex 对话
815
- - 支持多项目会话管理,每个项目独立会话
799
+ - 支持多会话管理,每个会话独立上下文
816
800
  - 所有命令以 \`/\` 开头
817
801
 
818
- 现在,请先使用 \`/bind\` 命令绑定您的项目目录,然后就可以开始工作了!`;
802
+ 现在就可以开始工作了!`;
819
803
  // First contact with Owner races against Owner's async cert fetch from
820
804
  // gateway PKI; a 3s pause lets the cert propagate. persist_required asks
821
805
  // the gateway to durably store the message so Owner can recover it via
@@ -832,6 +816,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
832
816
  persist_required: true,
833
817
  });
834
818
  logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
819
+ // Send binding credential for Evol App to persist locally
820
+ await this.sendBindingCredential(owner, agentDisplayName, agentConfig.active_baseagent || 'claude').catch(e => logger.warn(`${this.logPrefix()} Binding credential failed: ${e}`));
835
821
  // Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
836
822
  try {
837
823
  const fresh = loadAgent(aidName);
@@ -849,7 +835,57 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
849
835
  logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
850
836
  }
851
837
  }
838
+ async sendBindingCredential(owner, name, baseagent) {
839
+ if (!this.client)
840
+ return;
841
+ await this.callAndTrace('message.send', {
842
+ to: owner,
843
+ payload: { type: 'binding', aid: this.config.aid, name, owner, baseagent },
844
+ encrypt: true,
845
+ persist_required: true,
846
+ });
847
+ logger.info(`${this.logPrefix()} Binding credential sent to owner: ${owner}`);
848
+ }
852
849
  // ── Event handlers ──────────────────────────────────────────
850
+ /**
851
+ * 判断附件是否为图片,返回 MIME 类型(非图片返回空)。
852
+ * 多重检测:附件元数据字段 → 文件名后缀 → 文件 magic bytes。
853
+ */
854
+ detectImageMime(att, filePath) {
855
+ const extToMime = {
856
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
857
+ '.gif': 'image/gif', '.webp': 'image/webp',
858
+ };
859
+ // 1. 附件元数据字段(content_type / mime_type / mimeType)
860
+ const metaCt = (att?.content_type || att?.mime_type || att?.mimeType || '');
861
+ if (typeof metaCt === 'string' && metaCt.startsWith('image/'))
862
+ return metaCt;
863
+ // 2. 文件名后缀
864
+ const name = (att?.filename || att?.object_key || filePath || '').toLowerCase();
865
+ for (const [ext, mime] of Object.entries(extToMime)) {
866
+ if (name.endsWith(ext))
867
+ return mime;
868
+ }
869
+ // 3. magic bytes
870
+ try {
871
+ const { openSync, readSync, closeSync } = require('node:fs');
872
+ const fd = openSync(filePath, 'r');
873
+ const head = Buffer.alloc(12);
874
+ readSync(fd, head, 0, 12, 0);
875
+ closeSync(fd);
876
+ if (head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47)
877
+ return 'image/png';
878
+ if (head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff)
879
+ return 'image/jpeg';
880
+ if (head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46)
881
+ return 'image/gif';
882
+ if (head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46 &&
883
+ head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50)
884
+ return 'image/webp';
885
+ }
886
+ catch { /* not readable, skip */ }
887
+ return '';
888
+ }
853
889
  async downloadAttachment(att, channelId) {
854
890
  const ownerAid = att.owner_aid || this._aid || '';
855
891
  const objectKey = att.object_key;
@@ -858,7 +894,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
858
894
  return null;
859
895
  }
860
896
  const filename = att.filename || objectKey.split('/').pop() || 'unknown';
861
- let downloadUrl;
897
+ // 安全:始终通过受信任的 ticket 路径获取下载 URL。
898
+ // 不信任 att.url(来自对端消息 payload,可被构造为内网/元数据地址,SSRF)。
899
+ let downloadUrl = '';
862
900
  try {
863
901
  const ticket = await this.callAndTrace('storage.create_download_ticket', {
864
902
  owner_aid: ownerAid,
@@ -938,12 +976,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
938
976
  // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
939
977
  const rawAttachments = this.collectAllAttachments(payload);
940
978
  let finalText = text;
979
+ const inboundImages = [];
941
980
  if (rawAttachments.length > 0 && this.client) {
942
981
  const fileParts = [];
943
982
  for (const att of rawAttachments) {
944
983
  const filePath = await this.downloadAttachment(att, fromAid);
945
984
  if (filePath) {
946
985
  const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
986
+ const mime = this.detectImageMime(att, filePath);
987
+ if (mime) {
988
+ try {
989
+ const { readFileSync } = await import('node:fs');
990
+ inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
991
+ }
992
+ catch { /* fallback to file path */ }
993
+ }
947
994
  fileParts.push(`[文件: ${name} → ${filePath}]`);
948
995
  }
949
996
  }
@@ -952,16 +999,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
952
999
  if (text)
953
1000
  parts.push(text);
954
1001
  parts.push(...fileParts);
955
- parts.push('请使用 Read 工具读取文件内容。');
1002
+ if (inboundImages.length === 0)
1003
+ parts.push('请使用 Read 工具读取文件内容。');
956
1004
  finalText = parts.join('\n\n');
957
1005
  }
1006
+ logger.info(`${this.logPrefix()} [img-debug] private attachments=${rawAttachments.length} images=${inboundImages.length}`);
958
1007
  }
959
1008
  // 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
960
1009
  // device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
961
1010
  const chatId = fromAid;
962
1011
  // 解析对端身份(30天缓存)
963
1012
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
964
- const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.client, false);
1013
+ const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false);
965
1014
  const shortAid = this.getShortAid(fromAid);
966
1015
  const displayName = peerIdentity.name || shortAid;
967
1016
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -1007,7 +1056,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1007
1056
  mentions,
1008
1057
  peerName: displayName || undefined,
1009
1058
  peerType: peerIdentity.type,
1059
+ sameDevice: msg.same_device === true || undefined,
1060
+ sameNetwork: msg.same_network === true || undefined,
1061
+ sameEgressIp: msg.same_egress_ip === true || undefined,
1010
1062
  replyContext,
1063
+ images: inboundImages.length > 0 ? inboundImages : undefined,
1011
1064
  });
1012
1065
  }
1013
1066
  async handleIncomingGroupMessage(data) {
@@ -1158,12 +1211,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1158
1211
  : mentionedSelf && this._aid ? [this._aid] : [];
1159
1212
  // Process attachments
1160
1213
  let finalText = strippedText;
1214
+ const inboundImages = [];
1161
1215
  if (hasAttachments && this.client) {
1162
1216
  const fileParts = [];
1163
1217
  for (const att of rawAttachments) {
1164
1218
  const filePath = await this.downloadAttachment(att, groupId);
1165
1219
  if (filePath) {
1166
1220
  const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
1221
+ const mime = this.detectImageMime(att, filePath);
1222
+ if (mime) {
1223
+ try {
1224
+ const { readFileSync } = await import('node:fs');
1225
+ inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
1226
+ }
1227
+ catch { /* fallback to file path */ }
1228
+ }
1167
1229
  fileParts.push(`[文件: ${name} → ${filePath}]`);
1168
1230
  }
1169
1231
  }
@@ -1172,12 +1234,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1172
1234
  if (strippedText)
1173
1235
  parts.push(strippedText);
1174
1236
  parts.push(...fileParts);
1175
- parts.push('请使用 Read 工具读取文件内容。');
1237
+ if (inboundImages.length === 0)
1238
+ parts.push('请使用 Read 工具读取文件内容。');
1176
1239
  finalText = parts.join('\n\n');
1177
1240
  }
1178
1241
  }
1179
1242
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
1180
- const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.client, false);
1243
+ const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
1181
1244
  const shortAid = this.getShortAid(senderAid);
1182
1245
  const displayName = peerIdentity.name || shortAid;
1183
1246
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -1205,6 +1268,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1205
1268
  userId: senderAid,
1206
1269
  peerName: displayName || undefined,
1207
1270
  peerType: peerIdentity.type,
1271
+ sameDevice: msg.same_device === true || undefined,
1272
+ sameNetwork: msg.same_network === true || undefined,
1273
+ sameEgressIp: msg.same_egress_ip === true || undefined,
1208
1274
  text: finalText,
1209
1275
  chatType: 'group',
1210
1276
  messageId,
@@ -1212,6 +1278,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1212
1278
  threadId,
1213
1279
  mentions,
1214
1280
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1281
+ images: inboundImages.length > 0 ? inboundImages : undefined,
1215
1282
  });
1216
1283
  }
1217
1284
  dispatchMessage(event) {
@@ -1268,16 +1335,20 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1268
1335
  channelId: event.channelId || '',
1269
1336
  channelType: 'aun',
1270
1337
  content: event.text || '',
1271
- selfId: this._aid,
1338
+ selfAID: this._aid,
1272
1339
  groupId: event.groupId,
1273
1340
  chatType: event.chatType,
1274
1341
  peerId: event.userId || event.channelId || '',
1275
1342
  peerName: event.peerName,
1276
1343
  peerType: event.peerType,
1344
+ sameDevice: event.sameDevice,
1345
+ sameNetwork: event.sameNetwork,
1346
+ sameEgressIp: event.sameEgressIp,
1277
1347
  messageId: event.messageId,
1278
1348
  threadId: event.threadId,
1279
1349
  mentions: mentionObjects,
1280
1350
  replyContext,
1351
+ images: event.images,
1281
1352
  }).catch(err => {
1282
1353
  logger.error(`${this.logPrefix()} Message handler error:`, err);
1283
1354
  });
@@ -1351,17 +1422,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1351
1422
  if (!data || typeof data !== 'object')
1352
1423
  return;
1353
1424
  const state = data.state ?? '';
1354
- // [DIAG-STALE] 记录状态切换瞬间 evolclaw 的 connected 标志和 SDK 的内部 _state,
1355
- // 用于证明"reconnecting 时 connected 保持 true,导致 sendMessage 误放行"的假设
1356
- const sdkState = this.client?._state ?? 'no-client';
1357
- const connectedBefore = this.connected;
1358
- logger.info(`[AUN][DIAG-STALE] connection.state event: state=${state} attempt=${data.attempt ?? '-'} | connected_before=${connectedBefore} sdk_state=${sdkState}`);
1359
1425
  if (state === 'connected') {
1360
1426
  this.connected = true;
1361
1427
  this.connectedAt = Date.now();
1362
1428
  this.lastReconnectLogTime = 0;
1363
1429
  this.lastReconnectLogAttempt = 0;
1364
- this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.client?._gatewayUrl });
1430
+ // connection.state 事件 payload 带实际连接的 gateway,更新本地缓存
1431
+ const evtGateway = data.gateway;
1432
+ if (typeof evtGateway === 'string' && evtGateway)
1433
+ this.gatewayUrl = evtGateway;
1434
+ this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
1365
1435
  this.trace('IN', 'connection.state', data);
1366
1436
  logger.info(`${this.logPrefix()} Connected`);
1367
1437
  // 不在这里清 flapCount —— 短命连接一上来就会触发本分支,
@@ -1816,11 +1886,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1816
1886
  appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text', source = 'daemon') {
1817
1887
  try {
1818
1888
  const sessionsDir = resolvePaths().sessionsDir;
1819
- const selfId = this.config.aid;
1820
- const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfId);
1889
+ const selfAID = this.config.aid;
1890
+ const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfAID);
1821
1891
  const chatmode = context?.metadata?.chatmode;
1822
1892
  appendMessageLog(chatDir, buildOutboundEntry({
1823
- from: selfId,
1893
+ from: selfAID,
1824
1894
  to: channelId,
1825
1895
  chatType: isGroup ? 'group' : 'private',
1826
1896
  groupId: isGroup ? channelId : null,
@@ -1891,8 +1961,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1891
1961
  const tid = putRes?.thought_id;
1892
1962
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1893
1963
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1894
- // 文本类 thought 写入 jsonl(只对有 text 的 item,过滤 tool 等结构化项)
1895
1964
  if (thoughtText) {
1965
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
1896
1966
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
1897
1967
  }
1898
1968
  }
@@ -1903,6 +1973,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1903
1973
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1904
1974
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1905
1975
  if (thoughtText) {
1976
+ this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
1906
1977
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
1907
1978
  }
1908
1979
  }
@@ -2280,6 +2351,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2280
2351
  }
2281
2352
  this.client = null;
2282
2353
  }
2354
+ if (this.store) {
2355
+ try {
2356
+ this.store.close();
2357
+ }
2358
+ catch { /* ignore */ }
2359
+ this.store = null;
2360
+ }
2283
2361
  this.connected = false;
2284
2362
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
2285
2363
  appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
@@ -2381,7 +2459,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2381
2459
  return { type: null };
2382
2460
  try {
2383
2461
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
2384
- const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.client, false);
2462
+ const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false);
2385
2463
  const type = identity.type === 'human' ? 'human' : 'ai';
2386
2464
  const name = identity.name || undefined;
2387
2465
  const info = { type, name };
@@ -2405,19 +2483,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2405
2483
  void this.fetchPeerInfo(aid).catch(() => { });
2406
2484
  }
2407
2485
  async uploadAgentMd(content) {
2408
- if (!this.client)
2486
+ if (!this.store)
2409
2487
  throw new Error('not connected');
2410
- const { agentMdPath } = await import('../paths.js');
2411
- const localPath = agentMdPath(this.config.aid);
2412
- fs.mkdirSync(path.dirname(localPath), { recursive: true });
2413
- fs.writeFileSync(localPath, content, 'utf-8');
2414
- await this.client.publishAgentMd();
2488
+ const { agentmdPut } = await import('../aun/aid/agentmd.js');
2489
+ await agentmdPut(content, { aid: this.config.aid, store: this.store });
2415
2490
  }
2416
2491
  async downloadAgentMd(aid) {
2417
- if (!this.client)
2492
+ if (!this.store)
2418
2493
  throw new Error('not connected');
2419
2494
  const { agentmdSync } = await import('../aun/aid/agentmd.js');
2420
- const result = await agentmdSync(aid, { client: this.client });
2495
+ const result = await agentmdSync(aid, { store: this.store ?? undefined });
2421
2496
  return result.content ?? '';
2422
2497
  }
2423
2498
  }
@@ -2445,7 +2520,6 @@ export class AUNChannelPlugin {
2445
2520
  gatewayUrl: inst.gatewayUrl,
2446
2521
  accessToken: inst.accessToken,
2447
2522
  flushDelay: inst.flushDelay,
2448
- encryptionSeed: inst.encryptionSeed,
2449
2523
  owner: inst.owner,
2450
2524
  agentName: inst.agentName,
2451
2525
  channelName: inst.name,
@@ -2455,7 +2529,7 @@ export class AUNChannelPlugin {
2455
2529
  const adapter = {
2456
2530
  channelName: inst.name,
2457
2531
  channelKey: inst.name, // channelName 实际上就是 channelKey
2458
- capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true },
2532
+ capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true },
2459
2533
  send: async (envelope, payload) => {
2460
2534
  const ctx = envelope.replyContext;
2461
2535
  const channelId = envelope.channelId;
@@ -2636,7 +2710,7 @@ export class AUNChannelPlugin {
2636
2710
  channel: adapter.channelName,
2637
2711
  channelType,
2638
2712
  channelId: opts.channelId,
2639
- selfId: opts.selfId,
2713
+ selfAID: opts.selfAID,
2640
2714
  groupId: opts.groupId,
2641
2715
  content: opts.content,
2642
2716
  chatType: opts.chatType || 'private',
@@ -2648,6 +2722,7 @@ export class AUNChannelPlugin {
2648
2722
  threadId: opts.threadId,
2649
2723
  replyContext: opts.replyContext,
2650
2724
  source: opts.source,
2725
+ images: opts.images,
2651
2726
  });
2652
2727
  }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
2653
2728
  },
@@ -448,7 +448,7 @@ export class DingtalkChannelPlugin {
448
448
  const adapter = {
449
449
  channelName: inst.name,
450
450
  channelKey: inst.name,
451
- capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
451
+ capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
452
452
  send: async (envelope, payload) => {
453
453
  const ctx = envelope.replyContext;
454
454
  const channelId = envelope.channelId;
@@ -540,6 +540,7 @@ export class DingtalkChannelPlugin {
540
540
  channel: adapter.channelName,
541
541
  channelType,
542
542
  channelId: event.channelId,
543
+ selfAID: inst.agentName,
543
544
  content: event.content,
544
545
  images: event.images,
545
546
  chatType: event.chatType || 'private',