evolclaw 3.1.5 → 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 (46) hide show
  1. package/CHANGELOG.md +53 -3
  2. package/dist/agents/claude-runner.js +69 -24
  3. package/dist/agents/kit-renderer.js +15 -4
  4. package/dist/aun/aid/agentmd.js +10 -3
  5. package/dist/aun/msg/group.js +2 -2
  6. package/dist/channels/aun.js +98 -12
  7. package/dist/channels/dingtalk.js +1 -1
  8. package/dist/channels/feishu.js +31 -9
  9. package/dist/channels/qqbot.js +1 -1
  10. package/dist/channels/wechat.js +1 -1
  11. package/dist/channels/wecom.js +1 -1
  12. package/dist/cli/agent.js +10 -11
  13. package/dist/cli/bench.js +1 -5
  14. package/dist/cli/help.js +8 -0
  15. package/dist/cli/index.js +91 -128
  16. package/dist/cli/init.js +37 -21
  17. package/dist/cli/link-rules.js +1 -7
  18. package/dist/cli/model.js +231 -6
  19. package/dist/config-store.js +1 -22
  20. package/dist/core/command-handler.js +181 -48
  21. package/dist/core/evolagent.js +0 -18
  22. package/dist/core/message/im-renderer.js +9 -20
  23. package/dist/core/message/message-bridge.js +7 -3
  24. package/dist/core/message/message-processor.js +138 -35
  25. package/dist/core/relation/peer-identity.js +23 -11
  26. package/dist/core/trigger/parser.js +4 -4
  27. package/dist/core/trigger/scheduler.js +20 -6
  28. package/dist/index.js +55 -5
  29. package/dist/ipc.js +1 -1
  30. package/dist/utils/error-utils.js +6 -0
  31. package/dist/utils/process-introspect.js +7 -5
  32. package/kits/docs/INDEX.md +4 -8
  33. package/kits/docs/context-assembly.md +1 -0
  34. package/kits/docs/evolclaw/INDEX.md +43 -0
  35. package/kits/docs/evolclaw/group.md +13 -6
  36. package/kits/docs/evolclaw/model.md +51 -0
  37. package/kits/docs/evolclaw/msg.md +5 -0
  38. package/kits/docs/venues/group.md +13 -1
  39. package/kits/eck_manifest.json +9 -0
  40. package/kits/rules/06-channel.md +5 -1
  41. package/kits/templates/system-fragments/baseagent.md +7 -1
  42. package/kits/templates/system-fragments/channel.md +7 -5
  43. package/kits/templates/system-fragments/commands.md +19 -0
  44. package/kits/templates/system-fragments/session.md +9 -0
  45. package/kits/templates/system-fragments/venue.md +15 -0
  46. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,13 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.1.6 (2026-06-03)
4
+
5
+ ### New Features
6
+
7
+ - **AUN 图片附件支持** — AUN 私聊/群聊附件中的图片自动检测(元数据/文件名/magic bytes 三重检测),base64 编码后直接传给 Agent,不再要求 Read 工具读取
8
+ - **selfAID session 注入** — 从 channel key 解析 selfAID,透传至 MessageBridge → CommandHandler → `getOrCreateSession`,修复 AUN 多身份场景 session 归属错误
9
+ - **watch-web 单实例保护** — 启动前清理旧实例(按 instance 文件 + 端口兜底),端口冲突时自动切换并提示;前端新增 token 失效自动回配对页、退出按钮、配对码过期刷新
10
+
11
+ ### Improvements
12
+
13
+ - **init 流程优化** — `cmdInit` 一次性写入所有可用 baseagents(不只写选中的那个);新增 projects 默认目录询问步骤;默认项目路径改为 `EVOLCLAW_HOME/projects`;取消 init 后自动进入 `agent new`,改为打印提示
14
+ - **1M 上下文窗口** — `claude-opus-4-8` / `claude-sonnet-4-6` 自动追加 `[1m]` 后缀,`autoCompactWindow` 随之调整为 900000
15
+ - **上下文用量追踪** — complete 事件新增 `contextUsage`(totalTokens/maxTokens/percentage),`usage` 字段重命名为 `tokenUsage`,随 `status.completed` 元数据下发
16
+ - **模型别名缓存 TTL** — 从 1h 缩短至 5min
17
+ - **`/cli` 远程透传** — 经消息通道远程执行 CLI,仅 owner 可用,白名单只读+配置命令,spawn 无 shell,15s 超时 + 128KB 截断
18
+ - **proximity ECK 注入** — `same_device/same_network/same_egress_ip` 从 V2 E2EE 解密结果提取,注入 venue.md 条件块渲染
19
+ - **fastaun 0.4.9** — 含 SPKI 双格式指纹匹配(0.4.8)和 watch-web 单实例相关修复(0.4.9)
20
+
21
+ ### Bug Fixes
22
+
23
+ - **Feishu merge_forward 内容重复** — 直接转发时丢弃 quotedText,避免内容重复
24
+ - **Feishu seenMsg 文件写入** — 仅在有记录被 GC 清理时才重写文件;seenMessages 清空时改为 unlink
25
+ - **peer-identity 验签** — 新增 `agentmd-unverified` 来源;缓存命中时从 `verify_status` 恢复验签状态,修复 source 误降级
26
+ - **process-introspect** — 改用 `/proc/stat btime`(绝对 epoch)替代 uptime 计算进程启动时间,修复长时间运行后 PID 复用误判
27
+ - **evolagent.load IPC 超时** — AUN WebSocket 冷启动常 >3s,IPC 超时从默认值调整为 30s
28
+ - **交互路由 markWaiting/unmarkWaiting** — 新增提前占位方法,适配异步发卡片场景的等待计数
29
+
3
30
  ## v3.1.5 (2026-06-02)
4
31
 
32
+ ### New Features
33
+
34
+ - **Web 监控面板(watch-web)** — 全新 `evolclaw watch` Web 面板:实时会话视图、对话视图、AID 状态、消息流,含上下文大小与费用估算;新增 `src/cli/watch-web/` server + 静态前端 + session/msg/aid 数据源
35
+ - **CLI model 命令** — 新增 `src/cli/model.ts`:动态模型目录管理,`/models` 端点拉取(1h 缓存)+ 静态表 fallback,注入已验证但未上线 API 的模型
36
+ - **飞书交互卡片 JSON 2.0 + 表单** — `FeishuCardManager` 管理 CardKit 实体生命周期,`buildActionCardV2()` 表单卡片,动态输入框追加(card.element.create API)
37
+ - **proactive→interactive 模式切换提示** — 模式切换时注入提示信息
38
+
39
+ ### Improvements
40
+
41
+ - **AUN/AID 三主体模型重构** — fastaun 0.3.3 → 0.4.7 大版本跨越:适配三主体模型与 slot 隔离键,`createAid` 拆分为 `registerAid` + `authenticate`,新增 `AIDStore`,`uploadAgentMd` 迁移至 AIDStore
42
+ - **ECK 上下文组装体系对齐** — 四阶段改造:channelKey 第二段 `selfPeerId`→`selfAID`,sessions 目录统一三层化 `<channelType>/<selfAID>/<channelId>/`,新增 `peer-key.ts` helper,ECK manifest 诊断输出
43
+ - **Session 持久化重构** — 统一 `persistSession(session, intent, opts)` 原语替代 `writeSessionIfChanged`/快照模式,内置去重,`markProcessing`/`clearProcessing` 不再扫描全部 chat 目录
44
+ - **idle-check 消息队列可靠性** — 提升空闲检测与消息队列稳定性
45
+ - **飞书卡片 schema 2.0 统一** — 交互卡片统一为 schema 2.0 inline,用户交互期间暂停 idle monitor,修复 V2 卡片回调 schema 不一致(error 200830/200810)
46
+ - **IPC 出站统计** — 新增 `aun-aid-stats-record-outbound` IPC handler,p2p/group/thought.put 出站消息统计追踪
47
+ - **/model 列表逻辑简化** — `listModels` 走缓存,model 显示标签同时展示完整 ID 与短别名
48
+ - **清理冗余日志与代码** — 移除过时诊断日志和未使用的 skills 代码
49
+
5
50
  ### Bug Fixes
6
51
 
7
- - **Trigger channelType 传播** — Trigger 新增 `targetChannelType` 字段,`buildSyntheticMessage` 正确填充 `channelType`,修复触发器消息无法创建 session 的问题
8
- - **Session mapper 过滤 channelName** `sessionToFile` 不再将 `channelName` 写入 metadata
52
+ - **Trigger channelType 传播** — Trigger 新增 `targetChannelType` 字段,`buildSyntheticMessage` 正确填充 `channelType`,修复触发器消息无法创建 session
53
+ - **session selfAID/channelType 注入**修复 session 创建时 selfAID 与 channelType 注入缺失
54
+ - **/model 短别名解析** — `/model sonnet` 等短别名正确解析为完整 model ID
55
+ - **aidCreate 成功路径泄漏 AIDStore** — 修复 AID 创建成功后 AIDStore 资源泄漏
56
+ - **peer-identity type 解析** — 修复对端身份类型解析错误
57
+ - **飞书卡片标题重复 emoji** — 修复 resolved card 标题与 checkers summary 中的双 emoji
58
+ - **消息可靠性与 session 迁移** — 提升消息投递可靠性,改进 session 迁移逻辑
59
+ - **session-mapper 过滤 channelName** — `sessionToFile` 不再将 `channelName` 写入 metadata
9
60
  - **resolveChatDirFromSession 严格校验** — 缺失 `channelType` 时抛出明确错误而非静默 fallback
10
- - **/model 别名解析** — `/model sonnet` 等短别名正确解析为完整 model ID
11
61
  - **test 脚本修复** — `package.json` test 脚本改为 `vitest run`
12
62
 
13
63
  ## v3.1.4 (2026-05-27)
@@ -24,7 +24,7 @@ const STATIC_MODEL_ALIASES = {
24
24
  'sonnet': 'claude-sonnet-4-6',
25
25
  'haiku': 'claude-haiku-4-5-20251001',
26
26
  };
27
- const MODEL_ALIAS_TTL_MS = 60 * 60 * 1000; // 1h
27
+ const MODEL_ALIAS_TTL_MS = 5 * 60 * 1000; // 5min
28
28
  const modelAliasCache = new Map(); // key: baseUrl
29
29
  const modelAliasInFlight = new Set(); // 去重并发刷新
30
30
  /** 从模型 ID 列表中提取各 claude 系列的最新版本(按 major.minor 取最高) */
@@ -70,9 +70,9 @@ async function refreshModelAliases(baseUrl, apiKey) {
70
70
  ? json.data.map((m) => m?.id).filter((x) => typeof x === 'string')
71
71
  : [];
72
72
  const aliases = deriveAliasesFromModelIds(ids);
73
- if (Object.keys(aliases).length > 0) {
74
- modelAliasCache.set(baseUrl, { aliases, fetchedAt: Date.now() });
75
- logger.info(`[AgentRunner] Refreshed model aliases from ${url}: ${JSON.stringify(aliases)}`);
73
+ if (ids.length > 0 || Object.keys(aliases).length > 0) {
74
+ modelAliasCache.set(baseUrl, { aliases, ids, fetchedAt: Date.now() });
75
+ logger.info(`[AgentRunner] Refreshed models from ${url}: ${ids.length} ids, aliases ${JSON.stringify(aliases)}`);
76
76
  }
77
77
  }
78
78
  catch {
@@ -97,6 +97,27 @@ function resolveModelAlias(model, baseUrl) {
97
97
  // 回退静态表
98
98
  return STATIC_MODEL_ALIASES[model] || model;
99
99
  }
100
+ /** 支持 1M 上下文窗口的模型 ID 前缀(SDK 通过 `[1m]` 后缀启用)。 */
101
+ const ONE_M_CONTEXT_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
102
+ /**
103
+ * 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
104
+ * 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
105
+ */
106
+ function applyContextWindow(modelId) {
107
+ if (/\[1m\]$/.test(modelId))
108
+ return modelId; // 已带后缀
109
+ if (ONE_M_CONTEXT_PREFIXES.some(p => modelId === p))
110
+ return `${modelId}[1m]`;
111
+ return modelId;
112
+ }
113
+ /** 根据 SDK model 串(含 [1m] 后缀)返回合适的 autoCompactWindow 值。 */
114
+ function contextWindowFor(sdkModel) {
115
+ return /\[1m\]$/.test(sdkModel) ? 900000 : 200000;
116
+ }
117
+ /** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
118
+ function resolveSdkModel(model, baseUrl) {
119
+ return applyContextWindow(resolveModelAlias(model, baseUrl));
120
+ }
100
121
  class MessageStream {
101
122
  queue = [];
102
123
  waiting = null;
@@ -213,16 +234,23 @@ export class AgentRunner {
213
234
  getModel() {
214
235
  return this.model;
215
236
  }
216
- listModels() {
217
- // 触发异步刷新(不阻塞)
218
- if (this.baseUrl)
219
- refreshModelAliases(this.baseUrl, this.apiKey);
220
- // 有缓存时返回完整 ID 列表,否则返回短别名
237
+ async listModels() {
221
238
  if (this.baseUrl) {
222
- const cached = modelAliasCache.get(this.baseUrl);
223
- if (cached)
224
- return Object.values(cached.aliases);
239
+ let cached = modelAliasCache.get(this.baseUrl);
240
+ const stale = !cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS);
241
+ // 缓存为空(首次打开)→ 等待刷新;缓存仅过期 → 后台刷新不阻塞
242
+ if (!cached) {
243
+ await refreshModelAliases(this.baseUrl, this.apiKey);
244
+ cached = modelAliasCache.get(this.baseUrl);
245
+ }
246
+ else if (stale) {
247
+ refreshModelAliases(this.baseUrl, this.apiKey);
248
+ }
249
+ // 有缓存时返回网关 /models 的全量原始 ID
250
+ if (cached && cached.ids.length > 0)
251
+ return cached.ids;
225
252
  }
253
+ // 无 baseUrl / 刷新超时或失败 → 回退短别名
226
254
  return Object.values(STATIC_MODEL_ALIASES);
227
255
  }
228
256
  /** 将短别名解析为当前代理实际使用的完整 model ID(仅用于展示,不改变持久化值) */
@@ -413,11 +441,8 @@ export class AgentRunner {
413
441
  }
414
442
  continue;
415
443
  }
416
- // 等待用户交互(unmark 占位,register 接管计数)
417
- if (waitMarked) {
418
- permCtx?.interactionRouter?.unmarkWaiting(sessionId);
419
- waitMarked = false;
420
- }
444
+ // 等待用户交互:先 register 接管计数,再 unmark 占位,消除空窗期
445
+ // (unmark 必须在 register 之后,否则计数短暂降为 0 触发 onWaitEnd→resume,idle 时钟被重置)
421
446
  const answer = await new Promise((resolve) => {
422
447
  permCtx?.interactionRouter?.register(requestId, sessionId, (action, values) => {
423
448
  if (action === 'cancel') {
@@ -442,6 +467,11 @@ export class AgentRunner {
442
467
  resolve(action);
443
468
  }
444
469
  });
470
+ // register 已接管计数(计数 +1),现在才能安全释放 markWaiting 占位(计数 -1),避免空窗
471
+ if (waitMarked) {
472
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
473
+ waitMarked = false;
474
+ }
445
475
  });
446
476
  if (answer === null) {
447
477
  const firstLabel = q.options[0]?.label || '';
@@ -656,7 +686,7 @@ export class AgentRunner {
656
686
  * SDK 原始事件 → 标准 AgentEvent 转换
657
687
  * 所有 SDK 特有的事件类型引用封装在此方法内
658
688
  */
659
- async *transformStream(sdkStream, sessionId) {
689
+ async *transformStream(sdkStream, sessionId, callModel, callEffort, sdkModel) {
660
690
  let lastSessionId;
661
691
  // tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
662
692
  const toolUseNames = new Map();
@@ -760,6 +790,19 @@ export class AgentRunner {
760
790
  const cleanResult = typeof event.result === 'string'
761
791
  ? event.result.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim()
762
792
  : event.result;
793
+ // 从 usage 三项求和得到当前上下文占用(与 claude-hud getTotalTokens 相同算法)
794
+ const u = event.usage;
795
+ const totalTokens = u
796
+ ? (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0)
797
+ : 0;
798
+ const maxTokens = sdkModel ? contextWindowFor(sdkModel) : 200000;
799
+ const contextUsage = totalTokens > 0 ? {
800
+ totalTokens,
801
+ maxTokens,
802
+ percentage: Math.round((totalTokens / maxTokens) * 100),
803
+ model: callModel ?? this.model,
804
+ effort: callEffort ?? this.effort,
805
+ } : undefined;
763
806
  yield {
764
807
  type: 'complete',
765
808
  result: cleanResult,
@@ -772,7 +815,8 @@ export class AgentRunner {
772
815
  terminalReason: event.terminal_reason,
773
816
  sessionTitle: event.session_title,
774
817
  numTurns: event.num_turns,
775
- usage: event.usage,
818
+ tokenUsage: event.usage,
819
+ contextUsage,
776
820
  };
777
821
  // result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
778
822
  return;
@@ -980,12 +1024,13 @@ export class AgentRunner {
980
1024
  else {
981
1025
  logger.info(`[AgentRunner] systemPromptAppend: none`);
982
1026
  }
1027
+ const sdkModel = resolveSdkModel(callModel, this.baseUrl);
983
1028
  const commonOptions = {
984
1029
  cwd: projectPath,
985
- model: resolveModelAlias(callModel, this.baseUrl),
1030
+ model: sdkModel,
986
1031
  ...(callEffort ? { effort: callEffort } : {}),
987
1032
  ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
988
- autoCompactWindow: 200000,
1033
+ autoCompactWindow: contextWindowFor(sdkModel),
989
1034
  advisorModel: 'haiku',
990
1035
  canUseTool: canUseToolCallback,
991
1036
  permissionMode: sdkPermissionMode,
@@ -1110,7 +1155,7 @@ export class AgentRunner {
1110
1155
  }
1111
1156
  let sdkStream;
1112
1157
  if (images && images.length > 0) {
1113
- logger.debug('[AgentRunner] Creating query with images, images:', images.length);
1158
+ logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
1114
1159
  logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
1115
1160
  const stream = new MessageStream();
1116
1161
  stream.push(prompt, images);
@@ -1126,7 +1171,7 @@ export class AgentRunner {
1126
1171
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
1127
1172
  }
1128
1173
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
1129
- return this.transformStream(sdkStream, sessionId);
1174
+ return this.transformStream(sdkStream, sessionId, callModel, callEffort, sdkModel);
1130
1175
  }
1131
1176
  async interrupt(sessionId) {
1132
1177
  const fn = this.interruptFns.get(sessionId);
@@ -1165,7 +1210,7 @@ export class AgentRunner {
1165
1210
  prompt,
1166
1211
  options: {
1167
1212
  cwd: projectPath,
1168
- model: resolveModelAlias(this.model, this.baseUrl),
1213
+ model: resolveSdkModel(this.model, this.baseUrl),
1169
1214
  resume: agentSessionId,
1170
1215
  maxTurns: 1,
1171
1216
  permissionMode: this.toSdkPermissionMode(),
@@ -19,6 +19,9 @@ const PARAM_DESCRIPTIONS = {
19
19
  peerName: '对端显示名',
20
20
  peerRole: '对端角色(owner/admin/guest/anonymous)',
21
21
  peerType: '对端类型(human/agent)',
22
+ sameDevice: '对端与本端同一物理设备(E2EE 消息 proximity,仅加密消息有值)',
23
+ sameNetwork: '对端与本端在同一网络内',
24
+ sameEgressIp: '对端与本端共享同一出口 IP',
22
25
  groupId: '群组 ID(群聊时)',
23
26
  chatType: '聊天类型(private=私聊 / group=群聊 / null=本地开发)',
24
27
  channel: '渠道类型(aun/feishu/wechat/dingtalk/qqbot/wecom)',
@@ -32,12 +35,19 @@ const PARAM_DESCRIPTIONS = {
32
35
  sessionName: '会话名称',
33
36
  sessionKey: '会话路由键(channelType#urlEncode(channelId)#urlEncode(threadId))',
34
37
  sessionCreatedAt: '会话创建时间(ISO)',
38
+ timezone: 'IANA 时区名(把 ISO 时间戳转本地时间用,如 Asia/Shanghai)',
39
+ tzOffset: '当前 UTC 偏移(如 +08:00)',
40
+ osInfo: '操作系统及版本(如 Windows 11 Pro (win32 10.0.26200))',
35
41
  threadId: '话题 ID(多话题路由时)',
36
42
  chatMode: '会话模式(interactive=同步交互 / proactive=主动推送)',
37
43
  readonly: '是否只读模式',
44
+ evolclawMode: 'evolclaw 运行模式(dev=源码仓库可直接修改 | install=全局安装包只读)',
38
45
  baseAgent: 'base agent 规范值(claude/codex/gemini/hermes)',
39
46
  baseAgentName: 'base agent 显示名',
40
- baseAgentModel: 'base agent 使用的模型',
47
+ baseAgentModel: 'base agent 引擎底座模型(evolclaw 作用域无配置时的兜底)',
48
+ effectiveModel: '当前实际生效模型(关系级 > agent级 > 全局 优先级解析结果)',
49
+ modelFallbackActive: 'evolclaw 配置的模型不可用,当前正在使用降级模型',
50
+ modelFallbackModel: '当前降级使用的 base agent 模型名',
41
51
  agentSessionId: 'base agent 会话 ID',
42
52
  };
43
53
  function buildPathMappings(vars) {
@@ -355,9 +365,10 @@ function isTruthy(val) {
355
365
  // CHUNK_CONTINUE_6
356
366
  // ── Template rendering ──
357
367
  function resolveConditions(template, vars) {
358
- // Find innermost {{?...}}...{{/}} block (no nested {{? inside) and resolve it.
359
- // Repeat until no blocks remain.
360
- const inner = /\{\{\?(\w+)(?:(!=|=)([^}]*))?\}\}([^]*?)\{\{\/\}\}/;
368
+ // 只匹配**最内层** {{?...}}...{{/}} 块:body 内不允许再出现 {{?
369
+ // 否则非贪婪 ([^]*?) 会匹配到嵌套内层的 {{/}},导致外层提前闭合、残留多余 {{/}}。
370
+ // 逐字符负向前瞻 (?!\{\{\?) 排除嵌套起始,配合 do/while 由内向外逐层消解。
371
+ const inner = /\{\{\?(\w+)(?:(!=|=)([^}]*))?\}\}((?:(?!\{\{\?)[^])*?)\{\{\/\}\}/;
361
372
  let result = template;
362
373
  let prev;
363
374
  do {
@@ -110,6 +110,10 @@ export async function agentmdPut(content, opts) {
110
110
  * Check if agent.md is up-to-date (30-day TTL), fetch if changed.
111
111
  * Returns changed=true + content when a new version was downloaded.
112
112
  *
113
+ * verification 透传 SDK 的验签结果:
114
+ * - downloaded(changed=true)时为 SDK downloadAgentMd 的 verification
115
+ * - 命中本地缓存或网络失败 fallback 时为 undefined(调用方需自行离线验签或视为未验证)
116
+ *
113
117
  * Note: store.checkAgentMd tracks freshness via the store's in-memory cache,
114
118
  * so a freshly-built store reports local_found=false and will fetch.
115
119
  */
@@ -120,16 +124,19 @@ export async function agentmdSync(aid, opts) {
120
124
  const localPath = agentMdPath(aid);
121
125
  try {
122
126
  const check = await store.checkAgentMd(aid, 30);
123
- // In sync (cache fresh) — return local file content unchanged.
127
+ // In sync (cache fresh) — return local file content with cached verification status.
124
128
  if (check.ok && !check.data.needs_update && check.data.local_found) {
125
129
  const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
126
- return { changed: false, content };
130
+ const verification = check.data.verify_status
131
+ ? normalizeVerification({ status: check.data.verify_status, reason: check.data.verify_error || undefined })
132
+ : undefined;
133
+ return { changed: false, content, verification };
127
134
  }
128
135
  // Needs update (or check failed) — fetch fresh content.
129
136
  // SDK's downloadAgentMd persists to disk internally (AgentMdManager.saveRecord → writeContent).
130
137
  const fetched = await store.downloadAgentMd(aid);
131
138
  if (fetched.ok) {
132
- return { changed: true, content: fetched.data.content };
139
+ return { changed: true, content: fetched.data.content, verification: normalizeVerification(fetched.data.verification) };
133
140
  }
134
141
  // Fetch failed (network) — fall back to local file if present.
135
142
  const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
@@ -70,11 +70,11 @@ export async function groupPull(args) {
70
70
  export async function groupAck(args) {
71
71
  const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
72
72
  try {
73
- const result = await conn.call('group.ack', { group_id: args.groupId, seq: args.seq });
73
+ const result = await conn.call('group.ack', { group_id: args.groupId, msg_seq: args.seq });
74
74
  return {
75
75
  ok: true,
76
76
  group_id: result?.group_id ?? args.groupId,
77
- ack_seq: result?.ack_seq ?? args.seq,
77
+ ack_seq: result?.cursor ?? result?.ack_seq ?? args.seq,
78
78
  latest_message_seq: result?.latest_message_seq,
79
79
  };
80
80
  }
@@ -576,7 +576,7 @@ export class AUNChannel {
576
576
  const store = await getAidStore({
577
577
  slotId: SLOT.daemon,
578
578
  aunPath,
579
- debug: this.config.aunSdkLog ?? true,
579
+ debug: this.config.aunSdkLog ?? false,
580
580
  });
581
581
  this.store = store;
582
582
  const client = await loadClient(store, aidName);
@@ -790,18 +790,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
790
790
 
791
791
  📋 **日常使用方法**:
792
792
 
793
- 1. **绑定项目**:发送 \`/bind <项目路径>\` 绑定工作目录
794
- 2. **查看帮助**:发送 \`/help\` 查看所有可用命令
795
- 3. **切换项目**:发送 \`/project <项目名>\` 切换到其他项目
796
- 4. **查看状态**:发送 \`/status\` 查看当前会话状态
797
- 5. **会话管理**:发送 \`/session\` 查看和切换会话
793
+ 1. **查看帮助**:发送 \`/help\` 查看所有可用命令
794
+ 2. **查看状态**:发送 \`/status\` 查看当前会话状态
795
+ 3. **会话管理**:发送 \`/session\` 查看和切换会话
798
796
 
799
797
  💡 **提示**:
800
798
  - 直接发送消息即可与 Claude/Codex 对话
801
- - 支持多项目会话管理,每个项目独立会话
799
+ - 支持多会话管理,每个会话独立上下文
802
800
  - 所有命令以 \`/\` 开头
803
801
 
804
- 现在,请先使用 \`/bind\` 命令绑定您的项目目录,然后就可以开始工作了!`;
802
+ 现在就可以开始工作了!`;
805
803
  // First contact with Owner races against Owner's async cert fetch from
806
804
  // gateway PKI; a 3s pause lets the cert propagate. persist_required asks
807
805
  // the gateway to durably store the message so Owner can recover it via
@@ -818,6 +816,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
818
816
  persist_required: true,
819
817
  });
820
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}`));
821
821
  // Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
822
822
  try {
823
823
  const fresh = loadAgent(aidName);
@@ -835,7 +835,57 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
835
835
  logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
836
836
  }
837
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
+ }
838
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
+ }
839
889
  async downloadAttachment(att, channelId) {
840
890
  const ownerAid = att.owner_aid || this._aid || '';
841
891
  const objectKey = att.object_key;
@@ -844,7 +894,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
844
894
  return null;
845
895
  }
846
896
  const filename = att.filename || objectKey.split('/').pop() || 'unknown';
847
- let downloadUrl;
897
+ // 安全:始终通过受信任的 ticket 路径获取下载 URL。
898
+ // 不信任 att.url(来自对端消息 payload,可被构造为内网/元数据地址,SSRF)。
899
+ let downloadUrl = '';
848
900
  try {
849
901
  const ticket = await this.callAndTrace('storage.create_download_ticket', {
850
902
  owner_aid: ownerAid,
@@ -924,12 +976,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
924
976
  // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
925
977
  const rawAttachments = this.collectAllAttachments(payload);
926
978
  let finalText = text;
979
+ const inboundImages = [];
927
980
  if (rawAttachments.length > 0 && this.client) {
928
981
  const fileParts = [];
929
982
  for (const att of rawAttachments) {
930
983
  const filePath = await this.downloadAttachment(att, fromAid);
931
984
  if (filePath) {
932
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
+ }
933
994
  fileParts.push(`[文件: ${name} → ${filePath}]`);
934
995
  }
935
996
  }
@@ -938,9 +999,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
938
999
  if (text)
939
1000
  parts.push(text);
940
1001
  parts.push(...fileParts);
941
- parts.push('请使用 Read 工具读取文件内容。');
1002
+ if (inboundImages.length === 0)
1003
+ parts.push('请使用 Read 工具读取文件内容。');
942
1004
  finalText = parts.join('\n\n');
943
1005
  }
1006
+ logger.info(`${this.logPrefix()} [img-debug] private attachments=${rawAttachments.length} images=${inboundImages.length}`);
944
1007
  }
945
1008
  // 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
946
1009
  // device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
@@ -993,7 +1056,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
993
1056
  mentions,
994
1057
  peerName: displayName || undefined,
995
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,
996
1062
  replyContext,
1063
+ images: inboundImages.length > 0 ? inboundImages : undefined,
997
1064
  });
998
1065
  }
999
1066
  async handleIncomingGroupMessage(data) {
@@ -1144,12 +1211,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1144
1211
  : mentionedSelf && this._aid ? [this._aid] : [];
1145
1212
  // Process attachments
1146
1213
  let finalText = strippedText;
1214
+ const inboundImages = [];
1147
1215
  if (hasAttachments && this.client) {
1148
1216
  const fileParts = [];
1149
1217
  for (const att of rawAttachments) {
1150
1218
  const filePath = await this.downloadAttachment(att, groupId);
1151
1219
  if (filePath) {
1152
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
+ }
1153
1229
  fileParts.push(`[文件: ${name} → ${filePath}]`);
1154
1230
  }
1155
1231
  }
@@ -1158,7 +1234,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1158
1234
  if (strippedText)
1159
1235
  parts.push(strippedText);
1160
1236
  parts.push(...fileParts);
1161
- parts.push('请使用 Read 工具读取文件内容。');
1237
+ if (inboundImages.length === 0)
1238
+ parts.push('请使用 Read 工具读取文件内容。');
1162
1239
  finalText = parts.join('\n\n');
1163
1240
  }
1164
1241
  }
@@ -1191,6 +1268,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1191
1268
  userId: senderAid,
1192
1269
  peerName: displayName || undefined,
1193
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,
1194
1274
  text: finalText,
1195
1275
  chatType: 'group',
1196
1276
  messageId,
@@ -1198,6 +1278,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1198
1278
  threadId,
1199
1279
  mentions,
1200
1280
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1281
+ images: inboundImages.length > 0 ? inboundImages : undefined,
1201
1282
  });
1202
1283
  }
1203
1284
  dispatchMessage(event) {
@@ -1260,10 +1341,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1260
1341
  peerId: event.userId || event.channelId || '',
1261
1342
  peerName: event.peerName,
1262
1343
  peerType: event.peerType,
1344
+ sameDevice: event.sameDevice,
1345
+ sameNetwork: event.sameNetwork,
1346
+ sameEgressIp: event.sameEgressIp,
1263
1347
  messageId: event.messageId,
1264
1348
  threadId: event.threadId,
1265
1349
  mentions: mentionObjects,
1266
1350
  replyContext,
1351
+ images: event.images,
1267
1352
  }).catch(err => {
1268
1353
  logger.error(`${this.logPrefix()} Message handler error:`, err);
1269
1354
  });
@@ -2444,7 +2529,7 @@ export class AUNChannelPlugin {
2444
2529
  const adapter = {
2445
2530
  channelName: inst.name,
2446
2531
  channelKey: inst.name, // channelName 实际上就是 channelKey
2447
- 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 },
2448
2533
  send: async (envelope, payload) => {
2449
2534
  const ctx = envelope.replyContext;
2450
2535
  const channelId = envelope.channelId;
@@ -2637,6 +2722,7 @@ export class AUNChannelPlugin {
2637
2722
  threadId: opts.threadId,
2638
2723
  replyContext: opts.replyContext,
2639
2724
  source: opts.source,
2725
+ images: opts.images,
2640
2726
  });
2641
2727
  }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
2642
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;