evolcore 0.0.8 → 0.0.10

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 (215) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +37 -3
  3. package/dist/agents/baseagent.js +111 -19
  4. package/dist/agents/claude-runner.js +30 -2
  5. package/dist/agents/codex-app-server-client.js +31 -0
  6. package/dist/agents/codex-runner.js +3 -0
  7. package/dist/aun/msg/group.js +9 -9
  8. package/dist/aun/msg/p2p.js +11 -6
  9. package/dist/aun/msg/upload.js +43 -29
  10. package/dist/aun/outbox.js +48 -5
  11. package/dist/channels/aun.js +199 -50
  12. package/dist/cli/aun-commands.js +10 -7
  13. package/dist/cli/daemon-commands.js +19 -4
  14. package/dist/cli/index.js +11 -1
  15. package/dist/cli/init.js +103 -4
  16. package/dist/cli/model.js +5 -5
  17. package/dist/config/builtin-role-templates.js +4 -3
  18. package/dist/config/config-field-policy.js +6 -2
  19. package/dist/config/config-manager.js +48 -11
  20. package/dist/config/contact-book-store.js +1 -1
  21. package/dist/config/role-schema.js +18 -3
  22. package/dist/config/role-service.js +3 -3
  23. package/dist/config/role-store.js +4 -3
  24. package/dist/config-store.js +4 -0
  25. package/dist/core/auth/operation-authorizer.js +3 -1
  26. package/dist/core/bootstrap-service.js +0 -12
  27. package/dist/core/channel-loader.js +5 -1
  28. package/dist/core/command/command-handler.js +1 -1
  29. package/dist/core/command/evol-menu-version-gate.js +3 -2
  30. package/dist/core/command/menu-handler.js +33 -22
  31. package/dist/core/command/menu-protocol.js +7 -4
  32. package/dist/core/command/slash-handler.js +4 -4
  33. package/dist/core/data-migration.js +79 -27
  34. package/dist/core/inference/text-inference.js +18 -5
  35. package/dist/core/message/message-bridge.js +17 -6
  36. package/dist/core/message/response-engine.js +22 -3
  37. package/dist/core/model/config-scope.js +3 -3
  38. package/dist/core/permission/tool-policy.js +18 -0
  39. package/dist/index.js +43 -26
  40. package/dist/ipc.js +1 -1
  41. package/dist/product.js +1 -0
  42. package/dist/utils/autostart.js +27 -0
  43. package/dist/utils/ecweb-utils.js +15 -2
  44. package/dist/utils/macos-autostart.js +179 -0
  45. package/dist/utils/windows-autostart.js +130 -0
  46. package/ecagent/dist/agent-loop.d.ts +1 -1
  47. package/ecagent/dist/agent-loop.js +1 -1
  48. package/ecagent/dist/agent.d.ts +1 -1
  49. package/ecagent/dist/agent.js +1 -1
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/compaction/branch-summarization.d.ts +1 -1
  52. package/ecagent/dist/harness/compaction/compaction.d.ts +1 -1
  53. package/ecagent/dist/harness/compaction/utils.d.ts +1 -1
  54. package/ecagent/dist/harness/messages.d.ts +1 -1
  55. package/ecagent/dist/harness/session/session.d.ts +1 -1
  56. package/ecagent/dist/harness/types.d.ts +1 -1
  57. package/ecagent/dist/proxy.d.ts +1 -1
  58. package/ecagent/dist/proxy.js +1 -1
  59. package/ecagent/dist/types.d.ts +1 -1
  60. package/kits/migrations/migrate-role-config-v5.mjs +27 -2
  61. package/kits/schemas/agent-config.schema.10.json +19 -1
  62. package/kits/schemas/daemon.schema.4.json +4 -0
  63. package/kits/schemas/defaults.schema.4.json +15 -1
  64. package/kits/schemas/relation-config.schema.8.json +22 -1
  65. package/kits/schemas/role-config.schema.1.json +2 -2
  66. package/package.json +2 -4
  67. package/ecagent/dist/agent-loop.d.ts.map +0 -1
  68. package/ecagent/dist/agent-loop.js.map +0 -1
  69. package/ecagent/dist/agent.d.ts.map +0 -1
  70. package/ecagent/dist/agent.js.map +0 -1
  71. package/ecagent/dist/harness/agent-harness.d.ts.map +0 -1
  72. package/ecagent/dist/harness/agent-harness.js.map +0 -1
  73. package/ecagent/dist/harness/compaction/branch-summarization.d.ts.map +0 -1
  74. package/ecagent/dist/harness/compaction/branch-summarization.js.map +0 -1
  75. package/ecagent/dist/harness/compaction/compaction.d.ts.map +0 -1
  76. package/ecagent/dist/harness/compaction/compaction.js.map +0 -1
  77. package/ecagent/dist/harness/compaction/utils.d.ts.map +0 -1
  78. package/ecagent/dist/harness/compaction/utils.js.map +0 -1
  79. package/ecagent/dist/harness/env/nodejs.d.ts.map +0 -1
  80. package/ecagent/dist/harness/env/nodejs.js.map +0 -1
  81. package/ecagent/dist/harness/messages.d.ts.map +0 -1
  82. package/ecagent/dist/harness/messages.js.map +0 -1
  83. package/ecagent/dist/harness/prompt-templates.d.ts.map +0 -1
  84. package/ecagent/dist/harness/prompt-templates.js.map +0 -1
  85. package/ecagent/dist/harness/session/jsonl-repo.d.ts.map +0 -1
  86. package/ecagent/dist/harness/session/jsonl-repo.js.map +0 -1
  87. package/ecagent/dist/harness/session/jsonl-storage.d.ts.map +0 -1
  88. package/ecagent/dist/harness/session/jsonl-storage.js.map +0 -1
  89. package/ecagent/dist/harness/session/memory-repo.d.ts.map +0 -1
  90. package/ecagent/dist/harness/session/memory-repo.js.map +0 -1
  91. package/ecagent/dist/harness/session/memory-storage.d.ts.map +0 -1
  92. package/ecagent/dist/harness/session/memory-storage.js.map +0 -1
  93. package/ecagent/dist/harness/session/repo-utils.d.ts.map +0 -1
  94. package/ecagent/dist/harness/session/repo-utils.js.map +0 -1
  95. package/ecagent/dist/harness/session/session.d.ts.map +0 -1
  96. package/ecagent/dist/harness/session/session.js.map +0 -1
  97. package/ecagent/dist/harness/session/uuid.d.ts.map +0 -1
  98. package/ecagent/dist/harness/session/uuid.js.map +0 -1
  99. package/ecagent/dist/harness/skills.d.ts.map +0 -1
  100. package/ecagent/dist/harness/skills.js.map +0 -1
  101. package/ecagent/dist/harness/system-prompt.d.ts.map +0 -1
  102. package/ecagent/dist/harness/system-prompt.js.map +0 -1
  103. package/ecagent/dist/harness/types.d.ts.map +0 -1
  104. package/ecagent/dist/harness/types.js.map +0 -1
  105. package/ecagent/dist/harness/utils/shell-output.d.ts.map +0 -1
  106. package/ecagent/dist/harness/utils/shell-output.js.map +0 -1
  107. package/ecagent/dist/harness/utils/truncate.d.ts.map +0 -1
  108. package/ecagent/dist/harness/utils/truncate.js.map +0 -1
  109. package/ecagent/dist/index.d.ts.map +0 -1
  110. package/ecagent/dist/index.js.map +0 -1
  111. package/ecagent/dist/node.d.ts.map +0 -1
  112. package/ecagent/dist/node.js.map +0 -1
  113. package/ecagent/dist/proxy.d.ts.map +0 -1
  114. package/ecagent/dist/proxy.js.map +0 -1
  115. package/ecagent/dist/runtime/api/lazy.d.ts.map +0 -1
  116. package/ecagent/dist/runtime/api/lazy.js.map +0 -1
  117. package/ecagent/dist/runtime/api/pi-messages.d.ts.map +0 -1
  118. package/ecagent/dist/runtime/api/pi-messages.js.map +0 -1
  119. package/ecagent/dist/runtime/api/pi-messages.lazy.d.ts.map +0 -1
  120. package/ecagent/dist/runtime/api/pi-messages.lazy.js.map +0 -1
  121. package/ecagent/dist/runtime/auth/context.d.ts.map +0 -1
  122. package/ecagent/dist/runtime/auth/context.js.map +0 -1
  123. package/ecagent/dist/runtime/auth/credential-store.d.ts.map +0 -1
  124. package/ecagent/dist/runtime/auth/credential-store.js.map +0 -1
  125. package/ecagent/dist/runtime/auth/helpers.d.ts.map +0 -1
  126. package/ecagent/dist/runtime/auth/helpers.js.map +0 -1
  127. package/ecagent/dist/runtime/auth/resolve.d.ts.map +0 -1
  128. package/ecagent/dist/runtime/auth/resolve.js.map +0 -1
  129. package/ecagent/dist/runtime/auth/types.d.ts.map +0 -1
  130. package/ecagent/dist/runtime/auth/types.js.map +0 -1
  131. package/ecagent/dist/runtime/compat.d.ts.map +0 -1
  132. package/ecagent/dist/runtime/compat.js.map +0 -1
  133. package/ecagent/dist/runtime/index.d.ts.map +0 -1
  134. package/ecagent/dist/runtime/index.js.map +0 -1
  135. package/ecagent/dist/runtime/models.d.ts.map +0 -1
  136. package/ecagent/dist/runtime/models.js.map +0 -1
  137. package/ecagent/dist/runtime/oauth.d.ts.map +0 -1
  138. package/ecagent/dist/runtime/oauth.js.map +0 -1
  139. package/ecagent/dist/runtime/providers/faux.d.ts.map +0 -1
  140. package/ecagent/dist/runtime/providers/faux.js.map +0 -1
  141. package/ecagent/dist/runtime/types.d.ts.map +0 -1
  142. package/ecagent/dist/runtime/types.js.map +0 -1
  143. package/ecagent/dist/runtime/utils/diagnostics.d.ts.map +0 -1
  144. package/ecagent/dist/runtime/utils/diagnostics.js.map +0 -1
  145. package/ecagent/dist/runtime/utils/event-stream.d.ts.map +0 -1
  146. package/ecagent/dist/runtime/utils/event-stream.js.map +0 -1
  147. package/ecagent/dist/runtime/utils/headers.d.ts.map +0 -1
  148. package/ecagent/dist/runtime/utils/headers.js.map +0 -1
  149. package/ecagent/dist/runtime/utils/json-parse.d.ts.map +0 -1
  150. package/ecagent/dist/runtime/utils/json-parse.js.map +0 -1
  151. package/ecagent/dist/runtime/utils/oauth/types.d.ts.map +0 -1
  152. package/ecagent/dist/runtime/utils/oauth/types.js.map +0 -1
  153. package/ecagent/dist/runtime/utils/overflow.d.ts.map +0 -1
  154. package/ecagent/dist/runtime/utils/overflow.js.map +0 -1
  155. package/ecagent/dist/runtime/utils/provider-env.d.ts.map +0 -1
  156. package/ecagent/dist/runtime/utils/provider-env.js.map +0 -1
  157. package/ecagent/dist/runtime/utils/retry.d.ts.map +0 -1
  158. package/ecagent/dist/runtime/utils/retry.js.map +0 -1
  159. package/ecagent/dist/runtime/utils/typebox-helpers.d.ts.map +0 -1
  160. package/ecagent/dist/runtime/utils/typebox-helpers.js.map +0 -1
  161. package/ecagent/dist/runtime/utils/validation.d.ts.map +0 -1
  162. package/ecagent/dist/runtime/utils/validation.js.map +0 -1
  163. package/ecagent/dist/types.d.ts.map +0 -1
  164. package/ecagent/dist/types.js.map +0 -1
  165. package/ecagent/package.json +0 -93
  166. package/ecagent/src/agent-loop.ts +0 -792
  167. package/ecagent/src/agent.ts +0 -575
  168. package/ecagent/src/harness/agent-harness.ts +0 -1029
  169. package/ecagent/src/harness/compaction/branch-summarization.ts +0 -261
  170. package/ecagent/src/harness/compaction/compaction.ts +0 -753
  171. package/ecagent/src/harness/compaction/utils.ts +0 -144
  172. package/ecagent/src/harness/env/nodejs.ts +0 -569
  173. package/ecagent/src/harness/messages.ts +0 -164
  174. package/ecagent/src/harness/prompt-templates.ts +0 -267
  175. package/ecagent/src/harness/session/jsonl-repo.ts +0 -179
  176. package/ecagent/src/harness/session/jsonl-storage.ts +0 -314
  177. package/ecagent/src/harness/session/memory-repo.ts +0 -50
  178. package/ecagent/src/harness/session/memory-storage.ts +0 -133
  179. package/ecagent/src/harness/session/repo-utils.ts +0 -51
  180. package/ecagent/src/harness/session/session.ts +0 -338
  181. package/ecagent/src/harness/session/uuid.ts +0 -54
  182. package/ecagent/src/harness/skills.ts +0 -375
  183. package/ecagent/src/harness/system-prompt.ts +0 -34
  184. package/ecagent/src/harness/types.ts +0 -838
  185. package/ecagent/src/harness/utils/shell-output.ts +0 -135
  186. package/ecagent/src/harness/utils/truncate.ts +0 -344
  187. package/ecagent/src/index.ts +0 -46
  188. package/ecagent/src/node.ts +0 -2
  189. package/ecagent/src/proxy.ts +0 -367
  190. package/ecagent/src/runtime/api/lazy.ts +0 -70
  191. package/ecagent/src/runtime/api/pi-messages.lazy.ts +0 -4
  192. package/ecagent/src/runtime/api/pi-messages.ts +0 -436
  193. package/ecagent/src/runtime/auth/context.ts +0 -45
  194. package/ecagent/src/runtime/auth/credential-store.ts +0 -47
  195. package/ecagent/src/runtime/auth/helpers.ts +0 -46
  196. package/ecagent/src/runtime/auth/resolve.ts +0 -141
  197. package/ecagent/src/runtime/auth/types.ts +0 -182
  198. package/ecagent/src/runtime/compat.ts +0 -158
  199. package/ecagent/src/runtime/index.ts +0 -31
  200. package/ecagent/src/runtime/models.ts +0 -452
  201. package/ecagent/src/runtime/oauth.ts +0 -1
  202. package/ecagent/src/runtime/providers/faux.ts +0 -538
  203. package/ecagent/src/runtime/types.ts +0 -718
  204. package/ecagent/src/runtime/utils/diagnostics.ts +0 -45
  205. package/ecagent/src/runtime/utils/event-stream.ts +0 -88
  206. package/ecagent/src/runtime/utils/headers.ts +0 -18
  207. package/ecagent/src/runtime/utils/json-parse.ts +0 -124
  208. package/ecagent/src/runtime/utils/oauth/types.ts +0 -79
  209. package/ecagent/src/runtime/utils/overflow.ts +0 -165
  210. package/ecagent/src/runtime/utils/provider-env.ts +0 -52
  211. package/ecagent/src/runtime/utils/retry.ts +0 -101
  212. package/ecagent/src/runtime/utils/typebox-helpers.ts +0 -24
  213. package/ecagent/src/runtime/utils/validation.ts +0 -310
  214. package/ecagent/src/types.ts +0 -430
  215. package/ecagent/tsconfig.build.json +0 -32
@@ -4,37 +4,30 @@ import path from 'path';
4
4
  import { guessMime, formatSize } from '../../utils/media-cache.js';
5
5
  import { inferPayloadType, isValidPayloadType } from './payload-type.js';
6
6
  /** 单次上传最大大小(与 daemon sendFile 一致)。 */
7
- const MAX_FILE_SIZE = 10 * 1024 * 1024;
7
+ export const MAX_AUN_ATTACHMENT_SIZE = 10 * 1024 * 1024;
8
8
  /** server 端 inline 上限错误的识别特征。优先匹配错误消息里的“inline 上限”字样。 */
9
9
  function isInlineLimitError(err) {
10
10
  const msg = err?.message ?? '';
11
11
  return /inline\s*上限|inline limit|create_upload_session/i.test(msg);
12
12
  }
13
13
  /**
14
- * 上传本地文件并构造发送用的 payload。
15
- *
16
- * 策略:先无脑 storage.put_object(内联 base64)。server 报“超过 inline 上限”
17
- * 时降级到 storage.create_upload_session + HTTP PUT + storage.complete_upload,
18
- * 此时通过 onProgress 上报字节级进度。
14
+ * 上传内存中的二进制内容并构造发送用的标准附件 payload。
19
15
  */
20
- export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts) {
21
- const absPath = path.resolve(filePath);
22
- if (!fs.existsSync(absPath)) {
23
- throw new Error(`文件不存在: ${absPath}`);
16
+ export async function uploadBufferAndBuildPayload(conn, ownerAid, data, filename, opts) {
17
+ const safeFilename = path.basename(filename.trim());
18
+ if (!safeFilename || safeFilename === '.' || safeFilename === '..') {
19
+ throw new Error('文件名不能为空');
24
20
  }
25
- const stat = fs.statSync(absPath);
26
- if (stat.size === 0) {
27
- throw new Error(`文件为空: ${absPath}`);
21
+ if (data.length === 0) {
22
+ throw new Error(`文件为空: ${safeFilename}`);
28
23
  }
29
- if (stat.size > MAX_FILE_SIZE) {
30
- throw new Error(`文件过大 (${formatSize(stat.size)}, 上限 ${formatSize(MAX_FILE_SIZE)}): ${absPath}`);
24
+ if (data.length > MAX_AUN_ATTACHMENT_SIZE) {
25
+ throw new Error(`文件过大 (${formatSize(data.length)}, 上限 ${formatSize(MAX_AUN_ATTACHMENT_SIZE)}): ${safeFilename}`);
31
26
  }
32
- const filename = path.basename(absPath);
33
- const fileData = fs.readFileSync(absPath);
34
- const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
35
- const contentType = opts?.contentType ?? guessMime(filename);
36
- const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
37
- const total = stat.size;
27
+ const sha256 = crypto.createHash('sha256').update(data).digest('hex');
28
+ const contentType = opts?.contentType ?? guessMime(safeFilename);
29
+ const objectKey = `shared/${crypto.randomUUID()}/${safeFilename}`;
30
+ const total = data.length;
38
31
  const report = (phase, bytes) => opts?.onProgress?.({ phase, bytes, total });
39
32
  // 1. 先按 inline 走
40
33
  let inlineRejected = false;
@@ -42,7 +35,7 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
42
35
  report('inline', 0);
43
36
  await conn.call('storage.put_object', {
44
37
  object_key: objectKey,
45
- content: fileData.toString('base64'),
38
+ content: data.toString('base64'),
46
39
  content_type: contentType,
47
40
  is_private: false,
48
41
  overwrite: true,
@@ -70,10 +63,9 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
70
63
  const PROGRESS_TICK_MS = 250;
71
64
  let lastBytes = 0;
72
65
  const tickerStart = Date.now();
73
- // 简单的“估算”进度:实际上一次性发,但在等待响应期间按 elapsed 模拟字节数,让用户看到在跑
66
+ // 实际上一次性发送;等待响应期间按 2MB/s 估算进度,且不超过 99%。
74
67
  const ticker = setInterval(() => {
75
68
  const elapsed = Date.now() - tickerStart;
76
- // 假设 2MB/s 估算;不超过 99%
77
69
  const estimated = Math.min(total - 1, Math.floor((elapsed / 1000) * 2 * 1024 * 1024));
78
70
  if (estimated > lastBytes) {
79
71
  lastBytes = estimated;
@@ -84,7 +76,7 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
84
76
  try {
85
77
  uploadResp = await fetch(uploadUrl, {
86
78
  method: 'PUT',
87
- body: new Blob([new Uint8Array(fileData)], { type: contentType }),
79
+ body: new Blob([new Uint8Array(data)], { type: contentType }),
88
80
  headers: { 'Content-Type': contentType },
89
81
  });
90
82
  }
@@ -107,12 +99,11 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
107
99
  const attachment = {
108
100
  owner_aid: ownerAid,
109
101
  object_key: objectKey,
110
- filename,
102
+ filename: safeFilename,
111
103
  size_bytes: total,
112
104
  sha256,
113
105
  content_type: contentType,
114
106
  };
115
- // 确定渲染类型
116
107
  let type;
117
108
  if (opts?.as) {
118
109
  if (!isValidPayloadType(opts.as)) {
@@ -121,7 +112,7 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
121
112
  type = opts.as;
122
113
  }
123
114
  else {
124
- type = inferPayloadType(filename);
115
+ type = inferPayloadType(safeFilename);
125
116
  }
126
117
  const payload = {
127
118
  type,
@@ -130,8 +121,31 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
130
121
  if (opts?.text)
131
122
  payload.text = opts.text;
132
123
  else if (type === 'file')
133
- payload.text = `📎 ${filename} (${formatSize(total)})`;
124
+ payload.text = `📎 ${safeFilename} (${formatSize(total)})`;
134
125
  if (type === 'voice' && opts?.transcript)
135
126
  payload.transcript = opts.transcript;
136
127
  return { payload, type, attachment };
137
128
  }
129
+ /**
130
+ * 上传本地文件并构造发送用的 payload。
131
+ *
132
+ * 策略:先无脑 storage.put_object(内联 base64)。server 报“超过 inline 上限”
133
+ * 时降级到 storage.create_upload_session + HTTP PUT + storage.complete_upload,
134
+ * 此时通过 onProgress 上报字节级进度。
135
+ */
136
+ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts) {
137
+ const absPath = path.resolve(filePath);
138
+ if (!fs.existsSync(absPath)) {
139
+ throw new Error(`文件不存在: ${absPath}`);
140
+ }
141
+ const stat = fs.statSync(absPath);
142
+ if (stat.size === 0) {
143
+ throw new Error(`文件为空: ${absPath}`);
144
+ }
145
+ if (stat.size > MAX_AUN_ATTACHMENT_SIZE) {
146
+ throw new Error(`文件过大 (${formatSize(stat.size)}, 上限 ${formatSize(MAX_AUN_ATTACHMENT_SIZE)}): ${absPath}`);
147
+ }
148
+ const filename = path.basename(absPath);
149
+ const fileData = fs.readFileSync(absPath);
150
+ return uploadBufferAndBuildPayload(conn, ownerAid, fileData, filename, opts);
151
+ }
@@ -59,6 +59,7 @@ export function enqueue(aid, opts) {
59
59
  payload: opts.payload,
60
60
  text: opts.text,
61
61
  filePath: opts.filePath,
62
+ image: opts.image,
62
63
  logText: opts.logText,
63
64
  context: opts.context,
64
65
  ttl: opts.ttl ?? DEFAULT_TTL,
@@ -81,6 +82,16 @@ export function remove(aid, id) {
81
82
  const entries = readEntries(aid).filter(e => e.id !== id);
82
83
  writeEntries(aid, entries);
83
84
  }
85
+ /** Replace an existing entry without consuming another outbox slot. */
86
+ export function replace(aid, entry) {
87
+ const entries = readEntries(aid);
88
+ const index = entries.findIndex(candidate => candidate.id === entry.id);
89
+ if (index < 0)
90
+ return false;
91
+ entries[index] = entry;
92
+ writeEntries(aid, entries);
93
+ return true;
94
+ }
84
95
  /** Remove durable interaction cards that were cancelled before delivery. */
85
96
  export function removeInteractionCards(aid, requestId) {
86
97
  const entries = readEntries(aid);
@@ -102,7 +113,11 @@ export function cleanup(aid) {
102
113
  writeEntries(aid, valid);
103
114
  return removed;
104
115
  }
105
- export async function drain(aid, sender) {
116
+ // Each caller gets its own pass over the current disk state. This prevents
117
+ // duplicate sends while allowing a later drain trigger to pick up entries
118
+ // enqueued during an active pass.
119
+ const activeDrainTails = new Map();
120
+ async function drainOnce(aid, sender) {
106
121
  const entries = readEntries(aid);
107
122
  if (entries.length === 0)
108
123
  return { sent: 0, expired: 0, failed: 0 };
@@ -135,12 +150,40 @@ export async function drain(aid, sender) {
135
150
  }
136
151
  }
137
152
  const current = readEntries(aid);
138
- const currentIds = new Set(current.map(e => e.id));
139
- const retainedNewEntries = current.filter(e => !drainedIds.has(e.id));
140
- const retainedFailedEntries = remaining.filter(e => currentIds.has(e.id));
141
- writeEntries(aid, [...retainedFailedEntries, ...retainedNewEntries]);
153
+ const failedById = new Map(remaining.map(entry => [entry.id, entry]));
154
+ const retained = current.flatMap(entry => {
155
+ if (!drainedIds.has(entry.id))
156
+ return [entry];
157
+ const failedEntry = failedById.get(entry.id);
158
+ if (!failedEntry)
159
+ return [];
160
+ return [{
161
+ ...failedEntry,
162
+ ...entry,
163
+ attempts: failedEntry.attempts,
164
+ lastError: failedEntry.lastError,
165
+ }];
166
+ });
167
+ writeEntries(aid, retained);
142
168
  return { sent, expired, failed };
143
169
  }
170
+ export async function drain(aid, sender) {
171
+ const previous = activeDrainTails.get(aid);
172
+ let releaseTurn;
173
+ const turn = new Promise(resolve => { releaseTurn = resolve; });
174
+ const tail = (previous ?? Promise.resolve()).then(() => turn);
175
+ activeDrainTails.set(aid, tail);
176
+ if (previous)
177
+ await previous;
178
+ try {
179
+ return await drainOnce(aid, sender);
180
+ }
181
+ finally {
182
+ releaseTurn();
183
+ if (activeDrainTails.get(aid) === tail)
184
+ activeDrainTails.delete(aid);
185
+ }
186
+ }
144
187
  export function hasPending(aid) {
145
188
  const file = outboxFile(aid);
146
189
  if (!fs.existsSync(file))
@@ -16,9 +16,8 @@ import { chatDirPath } from '../core/session/session-fs-store.js';
16
16
  import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
17
17
  import { appendAidLifecycle } from '../aun/aid/identity.js';
18
18
  import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
19
- import { uploadFileAndBuildPayload } from '../aun/msg/upload.js';
19
+ import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
20
20
  import { loadAgent, saveAgent } from '../config-store.js';
21
- import { activeBaseagent } from '../core/model/config-scope.js';
22
21
  import { resolveEffective } from '../config/config-manager.js';
23
22
  import { isManagementRole } from '../config/builtin-roles.js';
24
23
  import { mentionModeToDispatch } from '../config/mention-mode.js';
@@ -36,6 +35,18 @@ import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/age
36
35
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
36
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
38
37
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
38
+ const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
39
+ function normalizeImageMimeType(mimeType) {
40
+ const normalized = mimeType?.split(';', 1)[0].trim().toLowerCase();
41
+ if (!normalized)
42
+ return undefined;
43
+ if (!normalized.startsWith('image/'))
44
+ return undefined;
45
+ if (!IMAGE_MIME_TYPE.test(normalized)) {
46
+ throw new Error('result.image MIME type must be a valid image media type');
47
+ }
48
+ return normalized;
49
+ }
39
50
  /**
40
51
  * 构造 connect extra_info:自描述本进程身份。
41
52
  *
@@ -360,10 +371,11 @@ export class AUNChannel {
360
371
  || (channelId && !this.isGroupId(channelId) ? channelId : '')
361
372
  || '';
362
373
  if (cardInfo.isCommandCard) {
363
- // CommandCard:action_value 是完整 slash 命令,构造伪入站消息
364
- this.cardMessageIdMap.delete(cardMsgId);
374
+ // CommandCard:action_value 是完整 slash 命令,构造伪入站消息。
375
+ // 先完成点击者鉴权,再消费 mapping;否则群成员的一次误点会让发起者
376
+ // 无法继续操作同一张卡片。
377
+ const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
365
378
  if (this.messageHandler && actionValue.startsWith('/')) {
366
- const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
367
379
  // Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
368
380
  // 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
369
381
  if (chatType === 'group' && !cardClickerAid) {
@@ -377,6 +389,9 @@ export class AUNChannel {
377
389
  this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
378
390
  return '';
379
391
  }
392
+ // 已通过鉴权并即将进入处理流程;此时才消费 mapping,避免非发起者
393
+ // 或非法命令点击导致卡片提前失效。
394
+ this.cardMessageIdMap.delete(cardMsgId);
380
395
  Promise.resolve(this.messageHandler({
381
396
  channelId: channelId || '',
382
397
  channelType: 'aun',
@@ -580,6 +595,38 @@ export class AUNChannel {
580
595
  parts.push('---');
581
596
  return parts.join('\n');
582
597
  }
598
+ // 结构化 link/location 必须优先保留业务字段。部分客户端同时提供
599
+ // 顶层 text(仅供展示),若先返回该字段会丢失 URL、坐标和地址。
600
+ if (obj.type === 'link') {
601
+ const parts = [];
602
+ if (typeof obj.title === 'string' && obj.title)
603
+ parts.push(`标题: ${obj.title}`);
604
+ if (typeof obj.description === 'string' && obj.description)
605
+ parts.push(`描述: ${obj.description}`);
606
+ if (typeof obj.url === 'string' && obj.url)
607
+ parts.push(`URL: ${obj.url}`);
608
+ if (text && !parts.includes(text))
609
+ parts.push(text);
610
+ return parts.length > 0 ? parts.join('\n') : text || JSON.stringify(payload);
611
+ }
612
+ if (obj.type === 'location') {
613
+ const parts = [];
614
+ if (typeof obj.name === 'string' && obj.name)
615
+ parts.push(`地点: ${obj.name}`);
616
+ if (typeof obj.address === 'string' && obj.address)
617
+ parts.push(`地址: ${obj.address}`);
618
+ if (typeof obj.latitude === 'number' && Number.isFinite(obj.latitude))
619
+ parts.push(`纬度: ${obj.latitude}`);
620
+ if (typeof obj.longitude === 'number' && Number.isFinite(obj.longitude))
621
+ parts.push(`经度: ${obj.longitude}`);
622
+ if (typeof obj.precision_m === 'number' && Number.isFinite(obj.precision_m))
623
+ parts.push(`精度: ${obj.precision_m}m`);
624
+ if (typeof obj.map_url === 'string' && obj.map_url)
625
+ parts.push(`地图: ${obj.map_url}`);
626
+ if (text && !parts.includes(text))
627
+ parts.push(text);
628
+ return parts.length > 0 ? parts.join('\n') : text || JSON.stringify(payload);
629
+ }
583
630
  if (typeof obj.text === 'string')
584
631
  return text;
585
632
  return JSON.stringify(payload);
@@ -714,8 +761,11 @@ export class AUNChannel {
714
761
  this.messageSeqMap.delete(messageId);
715
762
  }
716
763
  shouldEncrypt(_peerId) {
717
- // Default to plaintext; only encrypt when session is explicitly marked encrypted
718
- return false;
764
+ return this.config.defaultEncrypt ?? true;
765
+ }
766
+ /** Effective channel default for callers that must persist a concrete transport choice. */
767
+ getDefaultEncrypt() {
768
+ return this.shouldEncrypt(this.config.aid);
719
769
  }
720
770
  _aid;
721
771
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
@@ -1143,13 +1193,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1143
1193
  await this.callAndTrace('message.send', {
1144
1194
  to: owner,
1145
1195
  payload: { type: 'text', text: welcomeText },
1146
- encrypt: true,
1196
+ encrypt: this.shouldEncrypt(owner),
1147
1197
  persist_required: true,
1148
1198
  });
1149
1199
  logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
1150
- // Send binding credential for Evol App to persist locally
1151
- // active_baseagent 经 config-scope 解析。
1152
- await this.sendBindingCredential(owner, agentDisplayName, activeBaseagent(aidName)).catch(e => logger.warn(`${this.logPrefix()} Binding credential failed: ${e}`));
1153
1200
  // Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
1154
1201
  try {
1155
1202
  const fresh = loadAgent(aidName);
@@ -1167,17 +1214,6 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1167
1214
  logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
1168
1215
  }
1169
1216
  }
1170
- async sendBindingCredential(owner, name, baseagent) {
1171
- if (!this.client)
1172
- return;
1173
- await this.callAndTrace('message.send', {
1174
- to: owner,
1175
- payload: { type: 'binding', aid: this.config.aid, name, owner, baseagent },
1176
- encrypt: true,
1177
- persist_required: true,
1178
- });
1179
- logger.info(`${this.logPrefix()} Binding credential sent to owner: ${owner}`);
1180
- }
1181
1217
  // ── Event handlers ──────────────────────────────────────────
1182
1218
  /**
1183
1219
  * 统一处理入站附件:下载 → 图片识别+base64 注入 → 拼接文本。
@@ -1926,7 +1962,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1926
1962
  //
1927
1963
  // observable=true 时,Agent 收发的每条 AUN 消息(原始信封 + 解密后明文
1928
1964
  // payload)镜像一份给 owners[]。入站在所有过滤之前转发;出站在真实发送
1929
- // 成功后转发。外层一律明文。详见 docs/observer-mode-design.md。
1965
+ // 成功后转发。Observer 使用 daemon 的全局 AUN 默认加密设置,业务合同保持不变。
1930
1966
  // observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
1931
1967
  // in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
1932
1968
  observerConfigResolver;
@@ -1989,7 +2025,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1989
2025
  this.emitForward('outbound', result, recipientOwners);
1990
2026
  }
1991
2027
  /**
1992
- * 实际投递 observer.forward 给每个 owner,外层一律明文。
2028
+ * 实际投递 observer.forward 给每个 owner,使用 daemon 的全局 AUN 默认加密设置。
1993
2029
  * original 为 SDK 给到的原始对象(入站回调 data / 出站 SendResult),整体透传,
1994
2030
  * 不挑字段、不改字段——SDK 加任何字段都会自动一并转发给 owner。
1995
2031
  */
@@ -2000,9 +2036,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2000
2036
  agent_aid: this.config.aid,
2001
2037
  original,
2002
2038
  };
2039
+ const encrypt = this.shouldEncrypt('observer.forward');
2003
2040
  for (const ownerAid of owners) {
2004
- this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt: false })
2005
- .catch(e => logger.debug(`${this.logPrefix()} observer.forward to ${ownerAid} failed: ${e}`));
2041
+ this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt })
2042
+ .catch(e => logger.warn(`${this.logPrefix()} observer.forward send to ${ownerAid} failed: ${e}`));
2006
2043
  }
2007
2044
  }
2008
2045
  // ── 观察者插话(Observer Insert,v0.3 待用上下文提示) ──────────────
@@ -2012,7 +2049,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2012
2049
  // (不 dispatch、不跑 LLM、不回 owner);下一条对端消息到达时由 message-processor
2013
2050
  // 回放消费、注入渲染层。action=add 加提示 / remove 撤销。
2014
2051
  // 详见 docs/observer-insert-design.md 第一部分。
2015
- /** 回 observer.inject.ack 给 owner(明文)。accepted 在成功写盘之后发出。 */
2052
+ /** 回 observer.inject.ack 给 owner;加密使用 daemon 的全局 AUN 默认设置。 */
2016
2053
  emitInjectAck(ownerAid, injectId, data, error) {
2017
2054
  if (!this.connected || !this.client)
2018
2055
  return;
@@ -2023,8 +2060,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2023
2060
  ackPayload.data = data;
2024
2061
  if (error)
2025
2062
  ackPayload.error = error;
2026
- this.callAndTrace('message.send', { to: ownerAid, payload: ackPayload, encrypt: false })
2027
- .catch(e => logger.debug(`${this.logPrefix()} observer.inject.ack to ${ownerAid} failed: ${e}`));
2063
+ this.callAndTrace('message.send', { to: ownerAid, payload: ackPayload, encrypt: this.shouldEncrypt(ownerAid) })
2064
+ .catch(e => logger.warn(`${this.logPrefix()} observer.inject.ack send to ${ownerAid} failed: ${e}`));
2028
2065
  }
2029
2066
  /** 处理 observer.inject:鉴权 + 校验 + 只落盘到 pending-hints(不触发处理、不回 owner)。 */
2030
2067
  handleObserverInject(fromAid, payload, displayName, peerType) {
@@ -2524,9 +2561,6 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2524
2561
  }
2525
2562
  }
2526
2563
  payloadByteLength(payload, logText) {
2527
- if (payload.type === 'image' && typeof payload.data_base64 === 'string') {
2528
- return Buffer.byteLength(payload.data_base64, 'utf-8');
2529
- }
2530
2564
  return Buffer.byteLength(logText, 'utf-8');
2531
2565
  }
2532
2566
  applyReplyContextToPayload(payload, context) {
@@ -2546,11 +2580,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2546
2580
  const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
2547
2581
  timer.unref?.();
2548
2582
  }
2549
- async recallInteractionCard(messageId) {
2583
+ async recallInteractionCard(messageId, channelId) {
2550
2584
  if (!this.client || !this.connected)
2551
2585
  return false;
2552
2586
  try {
2553
- await this.callAndTrace('message.recall', { message_ids: [messageId] }, { silentError: true });
2587
+ const isGroup = channelId ? this.isGroupId(channelId) : false;
2588
+ const method = isGroup ? 'group.recall' : 'message.recall';
2589
+ const params = isGroup
2590
+ ? { group_id: channelId, message_ids: [messageId] }
2591
+ : { message_ids: [messageId] };
2592
+ await this.callAndTrace(method, params, { silentError: true });
2554
2593
  return true;
2555
2594
  }
2556
2595
  catch (error) {
@@ -2571,8 +2610,9 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2571
2610
  }
2572
2611
  const messageIds = [...(this.interactionCardMessageIds.get(interactionId) ?? [])];
2573
2612
  for (const messageId of messageIds) {
2613
+ const channelId = this.cardMessageIdMap.get(messageId)?.channelId;
2574
2614
  this.forgetInteractionCard(messageId, interactionId);
2575
- await this.recallInteractionCard(messageId);
2615
+ await this.recallInteractionCard(messageId, channelId);
2576
2616
  }
2577
2617
  }
2578
2618
  forgetInteractionCard(messageId, interactionId) {
@@ -2582,13 +2622,14 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2582
2622
  if (ids && ids.size === 0)
2583
2623
  this.interactionCardMessageIds.delete(interactionId);
2584
2624
  }
2585
- registerCardPostSend(messageId, action) {
2625
+ registerCardPostSend(messageId, action, channelId) {
2586
2626
  if (action.type !== 'register_interaction_card')
2587
2627
  return;
2588
2628
  this.cardMessageIdMap.set(messageId, {
2589
2629
  requestId: action.requestId,
2590
2630
  isCommandCard: action.isCommandCard,
2591
2631
  initiatorAid: action.initiatorAid,
2632
+ channelId,
2592
2633
  });
2593
2634
  this.ownedCardMsgIds.add(messageId);
2594
2635
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
@@ -2614,14 +2655,14 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2614
2655
  const invalidationReason = this.invalidatedInteractions.get(action.requestId);
2615
2656
  if (invalidationReason) {
2616
2657
  this.cardMessageIdMap.delete(messageId);
2617
- void this.recallInteractionCard(messageId);
2658
+ void this.recallInteractionCard(messageId, channelId);
2618
2659
  }
2619
2660
  }
2620
2661
  runPostSend(entry, messageId) {
2621
2662
  if (!entry.postSend)
2622
2663
  return;
2623
2664
  if (entry.postSend.type === 'register_interaction_card') {
2624
- this.registerCardPostSend(messageId, entry.postSend);
2665
+ this.registerCardPostSend(messageId, entry.postSend, entry.channelId);
2625
2666
  }
2626
2667
  }
2627
2668
  async sendAunPayload(channelId, payload, context, label) {
@@ -2936,7 +2977,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2936
2977
  finalPayload._evolcore_causation_ref = causationRef;
2937
2978
  registerAunCausation(causationRef, this.config.aid, args.to, outboundCausation);
2938
2979
  }
2939
- const encrypt = args.encrypt === true;
2980
+ const encrypt = args.encrypt ?? this.shouldEncrypt(args.to);
2940
2981
  const sendParams = {
2941
2982
  to: args.to,
2942
2983
  payload: this.truncatePayloadIfNeeded(finalPayload, 'msg-send-ipc'),
@@ -3041,7 +3082,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3041
3082
  const finalPayload = this.stripUndefinedDeep({ ...args.payload });
3042
3083
  if (args.mentions?.length)
3043
3084
  finalPayload.mentions = args.mentions;
3044
- const encrypt = args.encrypt === true;
3085
+ const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
3045
3086
  try {
3046
3087
  const result = await this.callAndTrace('group.send', {
3047
3088
  group_id: args.groupId,
@@ -3534,6 +3575,117 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3534
3575
  outbox.remove(this.config.aid, entry.id);
3535
3576
  }
3536
3577
  }
3578
+ async sendImage(channelId, data, mimeType, alt, context) {
3579
+ if (!Buffer.isBuffer(data)) {
3580
+ throw new Error('result.image data must be a Buffer');
3581
+ }
3582
+ if (data.length === 0) {
3583
+ throw new Error('result.image data cannot be empty');
3584
+ }
3585
+ if (data.length > MAX_AUN_ATTACHMENT_SIZE) {
3586
+ throw new Error(`result.image is too large (${formatSize(data.length)}, max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`);
3587
+ }
3588
+ let normalizedMimeType = normalizeImageMimeType(mimeType);
3589
+ if (!normalizedMimeType) {
3590
+ const { default: imageType } = await import('image-type');
3591
+ const detected = await imageType(data).catch(() => undefined);
3592
+ normalizedMimeType = detected?.mime;
3593
+ if (!normalizedMimeType) {
3594
+ throw new Error('result.image requires a valid image MIME type or recognizable image data');
3595
+ }
3596
+ }
3597
+ const logText = alt ? `[image] ${alt}` : '[image]';
3598
+ const entry = outbox.enqueue(this.config.aid, {
3599
+ channelId,
3600
+ type: 'image',
3601
+ contentKind: 'image',
3602
+ image: { dataBase64: data.toString('base64'), mimeType: normalizedMimeType, alt },
3603
+ context,
3604
+ logText,
3605
+ });
3606
+ logger.debug(`${this.logPrefix()} Outbox enqueued image: id=${entry.id} channel=${channelId} bytes=${data.length}`);
3607
+ if (!this.connected || !this.client) {
3608
+ logger.warn(`${this.logPrefix()} Not connected, image send queued in outbox (id=${entry.id}). Triggering reconnect.`);
3609
+ if (!this.reconnectTimer && !this.client) {
3610
+ this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendImage failed: ${e}`));
3611
+ }
3612
+ return;
3613
+ }
3614
+ const ok = await this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), false);
3615
+ if (ok)
3616
+ outbox.remove(this.config.aid, entry.id);
3617
+ }
3618
+ async buildUploadedImagePayload(data, mimeType, alt) {
3619
+ if (!this.connected || !this.client) {
3620
+ throw new Error('AUN channel is not connected; image cannot be uploaded');
3621
+ }
3622
+ let contentType = normalizeImageMimeType(mimeType);
3623
+ let extension;
3624
+ if (!contentType) {
3625
+ const { default: imageType } = await import('image-type');
3626
+ const detected = await imageType(data).catch(() => undefined);
3627
+ contentType = detected?.mime;
3628
+ extension = detected?.ext;
3629
+ }
3630
+ if (!contentType) {
3631
+ throw new Error('result.image requires a valid image MIME type or recognizable image data');
3632
+ }
3633
+ if (!extension) {
3634
+ const subtype = contentType.slice('image/'.length);
3635
+ extension = subtype === 'jpeg'
3636
+ ? 'jpg'
3637
+ : subtype === 'svg+xml'
3638
+ ? 'svg'
3639
+ : subtype.replace(/[^a-z0-9]/g, '') || 'img';
3640
+ }
3641
+ const digest = crypto.createHash('sha256').update(data).digest('hex').slice(0, 12);
3642
+ const filename = `image-${digest}.${extension}`;
3643
+ const connection = {
3644
+ call: (method, params) => this.callAndTrace(method, params),
3645
+ close: async () => { },
3646
+ };
3647
+ const built = await uploadBufferAndBuildPayload(connection, this.config.aid, data, filename, { as: 'image', contentType });
3648
+ const imagePayload = { ...built.payload };
3649
+ setIfDefined(imagePayload, 'alt', alt);
3650
+ return imagePayload;
3651
+ }
3652
+ async deliverImageEntry(entry) {
3653
+ const image = entry.image;
3654
+ if (!image || typeof image.dataBase64 !== 'string') {
3655
+ logger.warn(`${this.logPrefix()} deliverImageEntry: missing image data (outbox id=${entry.id})`);
3656
+ return true;
3657
+ }
3658
+ const data = Buffer.from(image.dataBase64, 'base64');
3659
+ if (data.length === 0) {
3660
+ logger.warn(`${this.logPrefix()} deliverImageEntry: invalid image data (outbox id=${entry.id})`);
3661
+ return true;
3662
+ }
3663
+ const imagePayload = await this.buildUploadedImagePayload(data, image.mimeType, image.alt);
3664
+ entry.type = 'payload';
3665
+ entry.contentKind = 'image';
3666
+ entry.payload = this.applyReplyContextToPayload(imagePayload, entry.context);
3667
+ entry.logText ??= image.alt ? `[image] ${image.alt}` : '[image]';
3668
+ delete entry.image;
3669
+ // Persist the attachment reference before message.send. A failed send then
3670
+ // retries the compact wire payload without re-uploading the same image.
3671
+ let payloadEntry = entry;
3672
+ if (!outbox.replace(this.config.aid, entry)) {
3673
+ payloadEntry = outbox.enqueue(this.config.aid, {
3674
+ channelId: entry.channelId,
3675
+ type: 'payload',
3676
+ contentKind: 'image',
3677
+ payload: entry.payload,
3678
+ context: entry.context,
3679
+ logText: entry.logText,
3680
+ ttl: entry.ttl,
3681
+ });
3682
+ }
3683
+ const sent = await this.deliverPayloadEntry(payloadEntry);
3684
+ if (sent.ok && payloadEntry.id !== entry.id) {
3685
+ outbox.remove(this.config.aid, payloadEntry.id);
3686
+ }
3687
+ return sent.ok;
3688
+ }
3537
3689
  async deliverFileEntry(entry) {
3538
3690
  const channelId = entry.channelId;
3539
3691
  const absPath = entry.filePath;
@@ -3720,6 +3872,9 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3720
3872
  else if (entry.type === 'file') {
3721
3873
  return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), false);
3722
3874
  }
3875
+ else if (entry.type === 'image') {
3876
+ return this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), false);
3877
+ }
3723
3878
  else if (entry.type === 'payload') {
3724
3879
  const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false });
3725
3880
  return sent.ok;
@@ -4026,6 +4181,7 @@ export class AUNChannelPlugin {
4026
4181
  aid,
4027
4182
  keystorePath: inst.keystorePath,
4028
4183
  gatewayUrl: inst.gatewayUrl,
4184
+ defaultEncrypt: ctx.aunDefaultEncrypt,
4029
4185
  accessToken: inst.accessToken,
4030
4186
  flushDelay: inst.flushDelay,
4031
4187
  agentName: ctx.agentName,
@@ -4092,16 +4248,7 @@ export class AUNChannelPlugin {
4092
4248
  return;
4093
4249
  }
4094
4250
  case 'result.image': {
4095
- const buf = payload.data;
4096
- const b64 = buf.toString('base64');
4097
- const imagePayload = { type: 'image', data_base64: b64 };
4098
- setIfDefined(imagePayload, 'alt', payload.alt);
4099
- setIfDefined(imagePayload, 'mime_type', payload.mimeType);
4100
- await channel.sendContentPayload(channelId, imagePayload, {
4101
- contentKind: 'image',
4102
- context: replyCtx,
4103
- logText: payload.alt ? `[image] ${payload.alt}` : '[image]',
4104
- });
4251
+ await channel.sendImage(channelId, payload.data, payload.mimeType, payload.alt, replyCtx);
4105
4252
  return;
4106
4253
  }
4107
4254
  case 'activity.batch': {
@@ -4209,6 +4356,8 @@ export class AUNChannelPlugin {
4209
4356
  aunCard.text = card.body;
4210
4357
  setIfDefined(aunCard, 'format', card.bodyFormat);
4211
4358
  }
4359
+ if (req.initiatorId && channel.isGroupId(channelId))
4360
+ aunCard.initiator = req.initiatorId;
4212
4361
  if (replyCtx?.threadId)
4213
4362
  aunCard.thread_id = replyCtx.threadId;
4214
4363
  await channel.sendContentPayload(channelId, aunCard, {