evolcore 0.0.1 → 0.0.2

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 (371) hide show
  1. package/CHANGELOG.md +813 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION-0.5.0.md +378 -0
  4. package/README.md +318 -14
  5. package/ROLE_ACCESS_CONTROL.md +174 -0
  6. package/assets/.env.template +4 -0
  7. package/assets/brand/evolcore/README.md +19 -0
  8. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  9. package/assets/brand/evolcore/evolcore-app-icon.svg +13 -0
  10. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  11. package/assets/brand/evolcore/evolcore-brand-board.svg +126 -0
  12. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  13. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  14. package/assets/brand/evolcore/evolcore-logo-reverse.svg +14 -0
  15. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  16. package/assets/brand/evolcore/evolcore-logo.svg +14 -0
  17. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  18. package/assets/brand/evolcore/evolcore-mark.svg +10 -0
  19. package/bin/ec-safe-output.js +161 -0
  20. package/bin/ec.js +29 -0
  21. package/dist/agents/baseagent.js +163 -0
  22. package/dist/agents/claude-runner.js +2385 -0
  23. package/dist/agents/codex-app-server-client.js +448 -0
  24. package/dist/agents/codex-runner.js +2639 -0
  25. package/dist/agents/gemini-runner.js +666 -0
  26. package/dist/agents/runner-types.js +75 -0
  27. package/dist/aun/aid/agentmd.js +216 -0
  28. package/dist/aun/aid/client.js +132 -0
  29. package/dist/aun/aid/control-aid.js +91 -0
  30. package/dist/aun/aid/identity.js +518 -0
  31. package/dist/aun/aid/index.js +4 -0
  32. package/dist/aun/aid/store.js +74 -0
  33. package/dist/aun/aid/types.js +1 -0
  34. package/dist/aun/aid/validation.js +21 -0
  35. package/dist/aun/group-identity.js +10 -0
  36. package/dist/aun/msg/group-index.js +6 -0
  37. package/dist/aun/msg/group.js +1231 -0
  38. package/dist/aun/msg/history.js +123 -0
  39. package/dist/aun/msg/index.js +5 -0
  40. package/dist/aun/msg/p2p.js +393 -0
  41. package/dist/aun/msg/payload-type.js +27 -0
  42. package/dist/aun/msg/upload.js +137 -0
  43. package/dist/aun/outbox.js +160 -0
  44. package/dist/aun/rpc/caller.js +42 -0
  45. package/dist/aun/rpc/connection.js +25 -0
  46. package/dist/aun/rpc/index.js +2 -0
  47. package/dist/aun/service-proxy.js +225 -0
  48. package/dist/aun/storage/download.js +29 -0
  49. package/dist/aun/storage/index.js +3 -0
  50. package/dist/aun/storage/manage.js +10 -0
  51. package/dist/aun/storage/upload.js +68 -0
  52. package/dist/channels/aun.js +4147 -0
  53. package/dist/channels/daemon.js +422 -0
  54. package/dist/channels/dingtalk.js +649 -0
  55. package/dist/channels/feishu.js +1789 -0
  56. package/dist/channels/qqbot.js +409 -0
  57. package/dist/channels/wechat.js +817 -0
  58. package/dist/channels/wecom.js +565 -0
  59. package/dist/cli/agent-command.js +641 -0
  60. package/dist/cli/agent.js +1059 -0
  61. package/dist/cli/aun-commands.js +1948 -0
  62. package/dist/cli/bench.js +1228 -0
  63. package/dist/cli/cli-argv.js +66 -0
  64. package/dist/cli/code-stats.js +329 -0
  65. package/dist/cli/command-log.js +82 -0
  66. package/dist/cli/config-selector.js +69 -0
  67. package/dist/cli/config.js +261 -0
  68. package/dist/cli/ctl-command.js +62 -0
  69. package/dist/cli/daemon-commands.js +2887 -0
  70. package/dist/cli/fs-command.js +1447 -0
  71. package/dist/cli/handoff-command.js +302 -0
  72. package/dist/cli/help.js +31 -0
  73. package/dist/cli/index.js +358 -0
  74. package/dist/cli/init-channel.js +1377 -0
  75. package/dist/cli/init.js +553 -0
  76. package/dist/cli/link-rules.js +240 -0
  77. package/dist/cli/model.js +590 -0
  78. package/dist/cli/net-check.js +723 -0
  79. package/dist/cli/queue-command.js +126 -0
  80. package/dist/cli/response.js +345 -0
  81. package/dist/cli/restart-monitor.js +456 -0
  82. package/dist/cli/stats.js +607 -0
  83. package/dist/cli/task-context.js +80 -0
  84. package/dist/cli/trigger-command.js +505 -0
  85. package/dist/cli/version.js +88 -0
  86. package/dist/cli/watch-logs.js +33 -0
  87. package/dist/cli/watch-msg.js +673 -0
  88. package/dist/config/boot-log.js +266 -0
  89. package/dist/config/builtin-role-templates.js +30 -0
  90. package/dist/config/builtin-roles.js +85 -0
  91. package/dist/config/config-batch-get.js +11 -0
  92. package/dist/config/config-field-policy.js +261 -0
  93. package/dist/config/config-manager.js +916 -0
  94. package/dist/config/config-operation-service.js +384 -0
  95. package/dist/config/contact-book.js +371 -0
  96. package/dist/config/gateway-config.js +858 -0
  97. package/dist/config/lifecycle.js +17 -0
  98. package/dist/config/mention-mode.js +27 -0
  99. package/dist/config/merge.js +161 -0
  100. package/dist/config/owner-policy.js +4 -0
  101. package/dist/config/peer-role-resolver.js +139 -0
  102. package/dist/config/resolved-config-op.js +483 -0
  103. package/dist/config/role-config-v4-startup.js +32 -0
  104. package/dist/config/role-config-v5-startup.js +27 -0
  105. package/dist/config/role-schema.js +105 -0
  106. package/dist/config/role-service.js +159 -0
  107. package/dist/config/role-store.js +204 -0
  108. package/dist/config/roles.js +55 -0
  109. package/dist/config/schema-registry.js +154 -0
  110. package/dist/config/snapshot.js +598 -0
  111. package/dist/config-store.js +503 -0
  112. package/dist/core/auth/agent-delegation.js +111 -0
  113. package/dist/core/auth/auth-gateway.js +166 -0
  114. package/dist/core/auth/authenticated-actor.js +6 -0
  115. package/dist/core/auth/authorization-audit.js +110 -0
  116. package/dist/core/auth/operation-authorizer.js +718 -0
  117. package/dist/core/auth/operation-catalog.js +675 -0
  118. package/dist/core/baseagent-loader.js +54 -0
  119. package/dist/core/bootstrap-service.js +175 -0
  120. package/dist/core/capability/capability-manager.js +316 -0
  121. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  122. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  123. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  124. package/dist/core/capability/types.js +27 -0
  125. package/dist/core/causation/audit.js +103 -0
  126. package/dist/core/causation/aun-association.js +111 -0
  127. package/dist/core/causation/context.js +93 -0
  128. package/dist/core/causation/index.js +4 -0
  129. package/dist/core/causation/types.js +2 -0
  130. package/dist/core/channel-loader.js +277 -0
  131. package/dist/core/command/agent-control.js +616 -0
  132. package/dist/core/command/cli-intent-parser.js +225 -0
  133. package/dist/core/command/command-handler.js +1638 -0
  134. package/dist/core/command/menu-handler.js +3354 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1524 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3056 -0
  139. package/dist/core/daemon-file-cache.js +216 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +740 -0
  142. package/dist/core/evolagent-registry.js +546 -0
  143. package/dist/core/evolagent.js +331 -0
  144. package/dist/core/handoff/dispatcher.js +229 -0
  145. package/dist/core/handoff/mutex.js +46 -0
  146. package/dist/core/handoff/runtime.js +324 -0
  147. package/dist/core/handoff/store.js +537 -0
  148. package/dist/core/handoff/types.js +12 -0
  149. package/dist/core/inference/text-inference.js +173 -0
  150. package/dist/core/interaction-registration.js +10 -0
  151. package/dist/core/interaction-router.js +278 -0
  152. package/dist/core/message/create-status.js +67 -0
  153. package/dist/core/message/im-renderer.js +657 -0
  154. package/dist/core/message/items-formatter.js +76 -0
  155. package/dist/core/message/logical-queue-bridge.js +123 -0
  156. package/dist/core/message/message-bridge.js +803 -0
  157. package/dist/core/message/message-cache.js +56 -0
  158. package/dist/core/message/message-log.js +339 -0
  159. package/dist/core/message/message-processor.js +4 -0
  160. package/dist/core/message/message-queue.js +1436 -0
  161. package/dist/core/message/message-utils.js +70 -0
  162. package/dist/core/message/peer-mode.js +105 -0
  163. package/dist/core/message/pending-hints.js +232 -0
  164. package/dist/core/message/response-depth.js +33 -0
  165. package/dist/core/message/response-engine.js +3776 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/stream-debouncer.js +130 -0
  168. package/dist/core/message/stream-idle-monitor.js +124 -0
  169. package/dist/core/model/config-scope.js +162 -0
  170. package/dist/core/model/field-scope.js +78 -0
  171. package/dist/core/model/model-catalog.js +227 -0
  172. package/dist/core/model/model-diagnostics.js +182 -0
  173. package/dist/core/model/model-permission.js +90 -0
  174. package/dist/core/permission/approval-gateway.js +1017 -0
  175. package/dist/core/permission/ec-command-parser.js +347 -0
  176. package/dist/core/permission/execution-sandbox.js +16 -0
  177. package/dist/core/permission/index.js +6 -0
  178. package/dist/core/permission/mode.js +24 -0
  179. package/dist/core/permission/sandbox-runtime.js +265 -0
  180. package/dist/core/permission/tool-policy.js +987 -0
  181. package/dist/core/permission/unix-socket-policy.js +99 -0
  182. package/dist/core/protected-paths.js +330 -0
  183. package/dist/core/relation/peer-identity.js +222 -0
  184. package/dist/core/relation/peer-key.js +1 -0
  185. package/dist/core/role/runtime-policy.js +141 -0
  186. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  187. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  188. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  189. package/dist/core/session/session-file-adapter.js +7 -0
  190. package/dist/core/session/session-file-health.js +45 -0
  191. package/dist/core/session/session-fs-store.js +232 -0
  192. package/dist/core/session/session-key.js +24 -0
  193. package/dist/core/session/session-manager.js +1587 -0
  194. package/dist/core/session/session-mapper.js +100 -0
  195. package/dist/core/session/session-renew.js +314 -0
  196. package/dist/core/session/session-title.js +128 -0
  197. package/dist/core/session/session-turn-coordinator.js +205 -0
  198. package/dist/core/session/session-turns.js +67 -0
  199. package/dist/core/system-channels.js +29 -0
  200. package/dist/eck/baseagent-caps.js +18 -0
  201. package/dist/eck/detect.js +47 -0
  202. package/dist/eck/group-rules-sync.js +345 -0
  203. package/dist/eck/init.js +77 -0
  204. package/dist/eck/kit-renderer.js +359 -0
  205. package/dist/eck/manifest-engine.js +446 -0
  206. package/dist/eck/message-renderer.js +199 -0
  207. package/dist/eck/rules-loader.js +28 -0
  208. package/dist/index.js +2870 -4
  209. package/dist/ipc.js +748 -0
  210. package/dist/paths.js +262 -0
  211. package/dist/product.js +18 -0
  212. package/dist/response-system/context-builder.js +71 -0
  213. package/dist/response-system/coordinator.js +117 -0
  214. package/dist/response-system/decision-executor.js +86 -0
  215. package/dist/response-system/engines/v1/index.js +21 -0
  216. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  217. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  218. package/dist/response-system/engines/v1/types.js +1 -0
  219. package/dist/response-system/extensions.js +41 -0
  220. package/dist/response-system/index.js +6 -0
  221. package/dist/response-system/modes/index.js +7 -0
  222. package/dist/response-system/modes/single-session/index.js +72 -0
  223. package/dist/response-system/queues/fifo-queue.js +44 -0
  224. package/dist/response-system/queues/index.js +6 -0
  225. package/dist/response-system/queues/lifo-queue.js +42 -0
  226. package/dist/response-system/queues/priority-queue.js +63 -0
  227. package/dist/response-system/registry.js +97 -0
  228. package/dist/response-system/resolver.js +37 -0
  229. package/dist/response-system/selector.js +23 -0
  230. package/dist/response-system/types.js +7 -0
  231. package/dist/stats/billing.js +151 -0
  232. package/dist/stats/budget.js +93 -0
  233. package/dist/stats/db.js +403 -0
  234. package/dist/stats/eck-vars.js +89 -0
  235. package/dist/stats/index.js +11 -0
  236. package/dist/stats/normalizer.js +80 -0
  237. package/dist/stats/price-resolver.js +138 -0
  238. package/dist/stats/query.js +763 -0
  239. package/dist/stats/role-budget.js +168 -0
  240. package/dist/stats/writer.js +151 -0
  241. package/dist/trigger/anomaly-store.js +258 -0
  242. package/dist/trigger/audit.js +152 -0
  243. package/dist/trigger/event-source.js +119 -0
  244. package/dist/trigger/feedback.js +685 -0
  245. package/dist/trigger/history.js +290 -0
  246. package/dist/trigger/manager.js +291 -0
  247. package/dist/trigger/parser.js +520 -0
  248. package/dist/trigger/patch.js +148 -0
  249. package/dist/trigger/scheduler.js +1600 -0
  250. package/dist/trigger/script-executor.js +155 -0
  251. package/dist/trigger/state.js +145 -0
  252. package/dist/trigger/types.js +1 -0
  253. package/dist/trigger/validation.js +621 -0
  254. package/dist/types.js +12 -0
  255. package/dist/utils/aid-bind.js +299 -0
  256. package/dist/utils/atomic-write.js +95 -0
  257. package/dist/utils/avatar-upload.js +123 -0
  258. package/dist/utils/cross-platform.js +297 -0
  259. package/dist/utils/ecweb-utils.js +73 -0
  260. package/dist/utils/error-dict.json +153 -0
  261. package/dist/utils/error-utils.js +349 -0
  262. package/dist/utils/instance-registry.js +437 -0
  263. package/dist/utils/locale.js +21 -0
  264. package/dist/utils/log-writer.js +224 -0
  265. package/dist/utils/logger.js +89 -0
  266. package/dist/utils/markdown-to-plain-text.js +20 -0
  267. package/dist/utils/media-cache.js +271 -0
  268. package/dist/utils/model-prices.jsonl +17 -0
  269. package/dist/utils/npm-ops.js +210 -0
  270. package/dist/utils/process-introspect.js +133 -0
  271. package/dist/utils/process-tree-stats.js +271 -0
  272. package/dist/utils/project-path.js +74 -0
  273. package/dist/utils/stats.js +410 -0
  274. package/dist/utils/tool-summary.js +284 -0
  275. package/dist/utils/welcome.js +268 -0
  276. package/kits/docs/GUIDE.md +20 -0
  277. package/kits/docs/INDEX.md +65 -0
  278. package/kits/docs/aun/CHEATSHEET.md +19 -0
  279. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  280. package/kits/docs/channels/aun.md +65 -0
  281. package/kits/docs/channels/feishu.md +56 -0
  282. package/kits/docs/context-assembly.md +366 -0
  283. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  284. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  285. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  286. package/kits/docs/eck_templates/runtime.template.md +19 -0
  287. package/kits/docs/evolcore/INDEX.md +66 -0
  288. package/kits/docs/evolcore/agent.md +69 -0
  289. package/kits/docs/evolcore/aid.md +49 -0
  290. package/kits/docs/evolcore/config.md +149 -0
  291. package/kits/docs/evolcore/ctl.md +46 -0
  292. package/kits/docs/evolcore/event.md +216 -0
  293. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  294. package/kits/docs/evolcore/fs.md +101 -0
  295. package/kits/docs/evolcore/group-fs.md +17 -0
  296. package/kits/docs/evolcore/group-rules.md +226 -0
  297. package/kits/docs/evolcore/group.md +141 -0
  298. package/kits/docs/evolcore/model.md +47 -0
  299. package/kits/docs/evolcore/msg.md +130 -0
  300. package/kits/docs/evolcore/response.md +80 -0
  301. package/kits/docs/evolcore/rpc.md +35 -0
  302. package/kits/docs/evolcore/self-summary.md +29 -0
  303. package/kits/docs/evolcore/stats.md +70 -0
  304. package/kits/docs/evolcore/storage.md +49 -0
  305. package/kits/docs/evolcore/trigger.md +524 -0
  306. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  307. package/kits/docs/identity/PATH_OPS.md +16 -0
  308. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  309. package/kits/docs/identity/identity-tools.md +26 -0
  310. package/kits/docs/path-registry.md +43 -0
  311. package/kits/docs/prompt-loading-architecture.md +266 -0
  312. package/kits/docs/venues/aun-group.md +45 -0
  313. package/kits/docs/venues/aun-private.md +10 -0
  314. package/kits/docs/venues/client-desktop.md +10 -0
  315. package/kits/docs/venues/client-mobile.md +10 -0
  316. package/kits/docs/venues/feishu-group.md +13 -0
  317. package/kits/docs/venues/feishu-private.md +9 -0
  318. package/kits/docs/venues/group.md +25 -0
  319. package/kits/docs/venues/private.md +10 -0
  320. package/kits/eck_manifest.auxiliary.json +43 -0
  321. package/kits/eck_manifest.json +191 -0
  322. package/kits/eck_message_manifest.json +63 -0
  323. package/kits/migrations/README-role-config-v4.md +32 -0
  324. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  325. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  326. package/kits/migrations/rename-config-file.mjs +99 -0
  327. package/kits/rules/01-overview.md +142 -0
  328. package/kits/rules/02-navigation.md +76 -0
  329. package/kits/rules/03-identity.md +34 -0
  330. package/kits/rules/04-relation.md +59 -0
  331. package/kits/rules/05-venue.md +44 -0
  332. package/kits/rules/06-channel.md +59 -0
  333. package/kits/schemas/_meta.json +29 -0
  334. package/kits/schemas/agent-config.schema.1.json +177 -0
  335. package/kits/schemas/agent-config.schema.2.json +239 -0
  336. package/kits/schemas/agent-config.schema.3.json +119 -0
  337. package/kits/schemas/agent-config.schema.4.json +208 -0
  338. package/kits/schemas/agent-config.schema.5.json +326 -0
  339. package/kits/schemas/contact-book.schema.1.json +36 -0
  340. package/kits/schemas/daemon.schema.1.json +90 -0
  341. package/kits/schemas/defaults.schema.1.json +81 -0
  342. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  343. package/kits/schemas/migrations/README.md +28 -0
  344. package/kits/schemas/relation-config.schema.1.json +158 -0
  345. package/kits/schemas/relation-config.schema.2.json +73 -0
  346. package/kits/schemas/relation-config.schema.3.json +50 -0
  347. package/kits/schemas/relation-config.schema.4.json +47 -0
  348. package/kits/schemas/role-config.schema.1.json +200 -0
  349. package/kits/schemas/role-registry.schema.1.json +35 -0
  350. package/kits/schemas/single-session.schema.1.json +31 -0
  351. package/kits/templates/bootstrap-welcome.md +15 -0
  352. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  353. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  354. package/kits/templates/message-fragments/inject-default.md +2 -0
  355. package/kits/templates/message-fragments/item.md +2 -0
  356. package/kits/templates/roles/admin.json +8 -0
  357. package/kits/templates/roles/member.json +40 -0
  358. package/kits/templates/roles/owner.json +8 -0
  359. package/kits/templates/roles/visitor.json +39 -0
  360. package/kits/templates/system-fragments/baseagent.md +14 -0
  361. package/kits/templates/system-fragments/bootstrap.md +16 -0
  362. package/kits/templates/system-fragments/channel.md +48 -0
  363. package/kits/templates/system-fragments/commands.md +26 -0
  364. package/kits/templates/system-fragments/identity.md +11 -0
  365. package/kits/templates/system-fragments/relation.md +19 -0
  366. package/kits/templates/system-fragments/session.md +53 -0
  367. package/kits/templates/system-fragments/venue.md +31 -0
  368. package/package.json +50 -15
  369. package/dist/index.d.ts +0 -7
  370. package/dist/index.d.ts.map +0 -1
  371. package/dist/index.js.map +0 -1
@@ -0,0 +1,817 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { resolvePaths } from '../paths.js';
5
+ import { logger } from '../utils/logger.js';
6
+ import { sanitizeFileName, saveToUploads, safeFetch, bufferToInboundImage } from '../utils/media-cache.js';
7
+ import { markdownToPlainText } from '../utils/markdown-to-plain-text.js';
8
+ import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
9
+ import { formatItemsAsText } from '../core/message/items-formatter.js';
10
+ import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
11
+ const CHANNEL_VERSION = '1.0.0';
12
+ const ILINK_APP_ID = 'bot';
13
+ // iLink-App-ClientVersion: major<<16 | minor<<8 | patch (uint32)
14
+ const ILINK_APP_CLIENT_VERSION = String((1 << 16) | (0 << 8) | 0); // 1.0.0 = 65536
15
+ const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
16
+ const DEFAULT_API_TIMEOUT_MS = 15_000;
17
+ const DEFAULT_CONFIG_TIMEOUT_MS = 10_000;
18
+ const MAX_CONSECUTIVE_FAILURES = 3;
19
+ const BACKOFF_DELAY_MS = 30_000;
20
+ const RETRY_DELAY_MS = 2_000;
21
+ const TYPING_TICKET_TTL_MS = 5 * 60 * 1000; // 5 min cache
22
+ const SESSION_EXPIRED_ERRCODE = -14;
23
+ const SESSION_RETRY_DELAY_MS = 30_000; // 短暂停:30s 后重试一次
24
+ const SESSION_PAUSE_DURATION_MS = 10 * 60 * 1000; // 长暂停:10 分钟
25
+ const MSG_TYPE_USER = 1;
26
+ const MSG_TYPE_BOT = 2;
27
+ const MSG_ITEM_TEXT = 1;
28
+ const MSG_ITEM_IMAGE = 2;
29
+ const MSG_ITEM_VOICE = 3;
30
+ const MSG_ITEM_FILE = 4;
31
+ const MSG_ITEM_VIDEO = 5;
32
+ const MSG_STATE_FINISH = 2;
33
+ // ── CDN + AES ───────────────────────────────────────────────────────────────
34
+ const CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c';
35
+ const MIME_MAP = {
36
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
37
+ '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp',
38
+ '.mp4': 'video/mp4', '.mov': 'video/quicktime', '.avi': 'video/x-msvideo',
39
+ '.pdf': 'application/pdf', '.doc': 'application/msword',
40
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
41
+ '.xls': 'application/vnd.ms-excel',
42
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
43
+ '.zip': 'application/zip', '.rar': 'application/x-rar-compressed',
44
+ '.txt': 'text/plain', '.csv': 'text/csv', '.md': 'text/markdown',
45
+ };
46
+ // Exported for unit testing
47
+ export function parseAesKey(aesKeyBase64) {
48
+ const decoded = Buffer.from(aesKeyBase64, 'base64');
49
+ if (decoded.length === 16)
50
+ return decoded;
51
+ if (decoded.length === 32 && /^[0-9a-fA-F]{32}$/.test(decoded.toString('ascii')))
52
+ return Buffer.from(decoded.toString('ascii'), 'hex');
53
+ throw new Error(`Invalid aes_key length: ${decoded.length}`);
54
+ }
55
+ // Exported for unit testing
56
+ export function decryptAesEcb(ciphertext, key) {
57
+ const decipher = crypto.createDecipheriv('aes-128-ecb', key, null);
58
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
59
+ }
60
+ // Exported for unit testing
61
+ export function encryptAesEcb(plaintext, key) {
62
+ const cipher = crypto.createCipheriv('aes-128-ecb', key, null);
63
+ return Buffer.concat([cipher.update(plaintext), cipher.final()]);
64
+ }
65
+ async function downloadMedia(cdnMedia, hexKey) {
66
+ const aesKeyBase64 = hexKey
67
+ ? Buffer.from(hexKey, 'hex').toString('base64')
68
+ : cdnMedia.aes_key;
69
+ if (!cdnMedia.encrypt_query_param)
70
+ throw new Error('No encrypt_query_param');
71
+ const url = `${CDN_BASE_URL}/download?encrypted_query_param=${encodeURIComponent(cdnMedia.encrypt_query_param)}`;
72
+ const encrypted = await safeFetch(url);
73
+ if (!aesKeyBase64)
74
+ return encrypted; // 无 key = 明文
75
+ return decryptAesEcb(encrypted, parseAesKey(aesKeyBase64));
76
+ }
77
+ // ── Message Text Extraction ─────────────────────────────────────────────────
78
+ function extractTextFromMessage(msg) {
79
+ if (!msg.item_list?.length)
80
+ return '';
81
+ for (const item of msg.item_list) {
82
+ if (item.type === MSG_ITEM_TEXT && item.text_item?.text) {
83
+ const text = item.text_item.text;
84
+ const ref = item.ref_msg;
85
+ if (!ref)
86
+ return text;
87
+ const parts = [];
88
+ if (ref.title)
89
+ parts.push(ref.title);
90
+ if (ref.message_item?.type === MSG_ITEM_TEXT && ref.message_item.text_item?.text) {
91
+ parts.push(ref.message_item.text_item.text);
92
+ }
93
+ if (!parts.length)
94
+ return text;
95
+ return `[引用: ${parts.join(' | ')}]\n${text}`;
96
+ }
97
+ if (item.type === MSG_ITEM_VOICE && item.voice_item?.text) {
98
+ return item.voice_item.text;
99
+ }
100
+ }
101
+ return '';
102
+ }
103
+ // ── WechatChannel ───────────────────────────────────────────────────────────
104
+ export class WechatChannel {
105
+ agentAid;
106
+ channelName;
107
+ config;
108
+ messageHandler;
109
+ recallHandler;
110
+ abortController;
111
+ connected = false;
112
+ // 内部状态(不外泄到核心层)
113
+ contextTokenCache = new Map();
114
+ typingTicketCache = new Map();
115
+ getUpdatesBuf = '';
116
+ syncBufPath;
117
+ contextTokensPath;
118
+ // Session expired 状态
119
+ sessionPausedUntil = 0;
120
+ onSessionExpired;
121
+ eventBus;
122
+ // Project path resolver(用于保存文件到 uploads 目录)
123
+ projectPathResolver;
124
+ // Welcome message manager
125
+ welcomeManager;
126
+ constructor(config, agentAid, channelName) {
127
+ this.agentAid = agentAid;
128
+ this.channelName = channelName;
129
+ this.config = config;
130
+ const dataDir = resolvePaths().dataDir;
131
+ this.syncBufPath = path.join(dataDir, 'wechat-sync-buf.txt');
132
+ this.contextTokensPath = path.join(dataDir, 'wechat-context-tokens.json');
133
+ // 初始化 welcomeManager(使用共享帮助函数)
134
+ if (agentAid && channelName) {
135
+ this.welcomeManager = initWelcomeManager('wechat', agentAid, channelName);
136
+ }
137
+ }
138
+ // ── Public API ──────────────────────────────────────────────────────────
139
+ onMessage(handler) {
140
+ this.messageHandler = handler;
141
+ }
142
+ onRecall(handler) {
143
+ this.recallHandler = handler;
144
+ }
145
+ /** 注册 session 过期通知回调(用于跨渠道通知用户) */
146
+ onSessionExpiredNotify(handler) {
147
+ this.onSessionExpired = handler;
148
+ }
149
+ /** 注册事件总线(推荐,替代 onSessionExpiredNotify) */
150
+ setEventBus(bus) {
151
+ this.eventBus = bus;
152
+ }
153
+ /** 当前是否处于 session 暂停状态 */
154
+ isSessionPaused() {
155
+ return Date.now() < this.sessionPausedUntil;
156
+ }
157
+ async connect() {
158
+ if (!this.config.token) {
159
+ throw new Error('WeChat token not configured');
160
+ }
161
+ // 恢复游标
162
+ try {
163
+ if (fs.existsSync(this.syncBufPath)) {
164
+ this.getUpdatesBuf = fs.readFileSync(this.syncBufPath, 'utf-8');
165
+ logger.info(`[WeChat] Restored sync cursor (${this.getUpdatesBuf.length} bytes)`);
166
+ }
167
+ }
168
+ catch {
169
+ // ignore
170
+ }
171
+ // 通知 ilink 后端:bot 上线
172
+ try {
173
+ await this.notifyLifecycle('notifystart');
174
+ }
175
+ catch (err) {
176
+ logger.warn('[WeChat] notifyStart failed during startup (ignored):', err);
177
+ }
178
+ this.abortController = new AbortController();
179
+ // 启动长轮询(不 await,后台运行)
180
+ this.pollLoop(this.abortController.signal).catch(err => {
181
+ if (this.abortController?.signal.aborted)
182
+ return;
183
+ logger.error('[WeChat] Poll loop fatal error:', err);
184
+ this.connected = false;
185
+ });
186
+ this.connected = true;
187
+ logger.info('[WeChat] Channel connected');
188
+ }
189
+ async disconnect() {
190
+ this.connected = false;
191
+ if (this.abortController) {
192
+ this.abortController.abort();
193
+ this.abortController = undefined;
194
+ }
195
+ // 通知 ilink 后端:bot 下线
196
+ try {
197
+ await this.notifyLifecycle('notifystop');
198
+ }
199
+ catch (err) {
200
+ logger.warn('[WeChat] notifyStop failed during shutdown (ignored):', err);
201
+ }
202
+ logger.info('[WeChat] Channel disconnected');
203
+ }
204
+ /** Get current connection status */
205
+ getStatus() {
206
+ return { connected: this.connected };
207
+ }
208
+ /** Reconnect: disconnect then connect again */
209
+ async reconnect() {
210
+ await this.disconnect();
211
+ try {
212
+ await this.connect();
213
+ return '重连成功';
214
+ }
215
+ catch (err) {
216
+ return `重连失败: ${err instanceof Error ? err.message : String(err)}`;
217
+ }
218
+ }
219
+ async sendMessage(to, text) {
220
+ if (!text || text.trim() === '') {
221
+ logger.warn('[WeChat] Attempted to send empty message, skipping');
222
+ return;
223
+ }
224
+ // Session 暂停期间拒绝发送
225
+ if (this.isSessionPaused()) {
226
+ const remainingMin = Math.ceil((this.sessionPausedUntil - Date.now()) / 60_000);
227
+ logger.warn(`[WeChat] Session paused, ${remainingMin}min remaining, dropping outbound to ${to}`);
228
+ return;
229
+ }
230
+ const contextToken = this.contextTokenCache.get(to);
231
+ if (!contextToken) {
232
+ logger.error(`[WeChat] No context_token for ${to}, cannot send message`);
233
+ return;
234
+ }
235
+ // Markdown → 纯文本
236
+ const plainText = markdownToPlainText(text);
237
+ const clientId = `evolcore-wechat:${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
238
+ const body = {
239
+ msg: {
240
+ from_user_id: '',
241
+ to_user_id: to,
242
+ client_id: clientId,
243
+ message_type: MSG_TYPE_BOT,
244
+ message_state: MSG_STATE_FINISH,
245
+ item_list: [{ type: MSG_ITEM_TEXT, text_item: { text: plainText } }],
246
+ context_token: contextToken,
247
+ },
248
+ base_info: { channel_version: CHANNEL_VERSION },
249
+ };
250
+ try {
251
+ await this.apiFetch('ilink/bot/sendmessage', JSON.stringify(body), DEFAULT_API_TIMEOUT_MS);
252
+ logger.debug(`[WeChat] Sent message to ${to}, clientId=${clientId}`);
253
+ }
254
+ catch (err) {
255
+ logger.error(`[WeChat] Failed to send message to ${to}:`, err);
256
+ throw err;
257
+ }
258
+ }
259
+ /** 注册 projectPath 解析器,用于保存接收的文件 */
260
+ onProjectPathRequest(resolver) {
261
+ this.projectPathResolver = resolver;
262
+ }
263
+ /** 发送文件(图片/视频/文件)给用户,通过 CDN 上传 */
264
+ async sendFile(to, filePath) {
265
+ // Session 暂停期间拒绝发送
266
+ if (this.isSessionPaused()) {
267
+ logger.warn(`[WeChat] Session paused, dropping file send to ${to}`);
268
+ return;
269
+ }
270
+ const contextToken = this.contextTokenCache.get(to);
271
+ if (!contextToken) {
272
+ logger.error(`[WeChat] No context_token for ${to}, cannot send file`);
273
+ return;
274
+ }
275
+ if (!fs.existsSync(filePath)) {
276
+ logger.error(`[WeChat] File not found: ${filePath}`);
277
+ return;
278
+ }
279
+ try {
280
+ const plaintext = Buffer.from(fs.readFileSync(filePath));
281
+ const rawsize = plaintext.length;
282
+ const rawfilemd5 = crypto.createHash('md5').update(plaintext).digest('hex');
283
+ const aeskey = crypto.randomBytes(16);
284
+ const filekey = crypto.randomBytes(16).toString('hex');
285
+ const filesize = Math.ceil((rawsize + 1) / 16) * 16;
286
+ // MIME → UploadMediaType
287
+ const ext = path.extname(filePath).toLowerCase();
288
+ const mime = MIME_MAP[ext] || 'application/octet-stream';
289
+ const uploadMediaType = mime.startsWith('image/') ? 1
290
+ : mime.startsWith('video/') ? 2 : 3;
291
+ // Step 1: getuploadurl
292
+ const uploadResp = await this.getUploadUrl({
293
+ filekey, media_type: uploadMediaType, to_user_id: to,
294
+ rawsize, rawfilemd5, filesize,
295
+ aeskey: aeskey.toString('hex'),
296
+ no_need_thumb: true,
297
+ });
298
+ // Step 2: encrypt + upload to CDN
299
+ const ciphertext = encryptAesEcb(plaintext, aeskey);
300
+ const downloadParam = await this.cdnUpload(uploadResp.upload_param, filekey, ciphertext);
301
+ // Step 3: sendmessage with CDN reference
302
+ const cdnMedia = {
303
+ encrypt_query_param: downloadParam,
304
+ aes_key: Buffer.from(aeskey.toString('hex')).toString('base64'),
305
+ encrypt_type: 1,
306
+ };
307
+ const itemType = mime.startsWith('image/') ? MSG_ITEM_IMAGE
308
+ : mime.startsWith('video/') ? MSG_ITEM_VIDEO : MSG_ITEM_FILE;
309
+ const item = this.buildMediaItem(itemType, cdnMedia, filePath, filesize, rawsize);
310
+ await this.sendMediaMessage(to, item, contextToken);
311
+ }
312
+ catch (err) {
313
+ logger.error(`[WeChat] Failed to send file ${filePath} to ${to}:`, err);
314
+ throw err;
315
+ }
316
+ }
317
+ // ── Long-Poll Loop ────────────────────────────────────────────────────
318
+ async pollLoop(signal) {
319
+ let consecutiveFailures = 0;
320
+ let nextTimeoutMs = DEFAULT_LONG_POLL_TIMEOUT_MS;
321
+ logger.info('[WeChat] Starting message polling...');
322
+ while (!signal.aborted) {
323
+ try {
324
+ const body = JSON.stringify({
325
+ get_updates_buf: this.getUpdatesBuf,
326
+ base_info: { channel_version: CHANNEL_VERSION },
327
+ });
328
+ let rawText;
329
+ try {
330
+ rawText = await this.apiFetch('ilink/bot/getupdates', body, nextTimeoutMs, signal);
331
+ }
332
+ catch (err) {
333
+ if (signal.aborted)
334
+ return;
335
+ // 长轮询超时是正常的
336
+ if (err instanceof Error && err.name === 'AbortError') {
337
+ continue;
338
+ }
339
+ throw err;
340
+ }
341
+ const resp = JSON.parse(rawText);
342
+ // 更新服务端建议的轮询超时
343
+ if (resp.longpolling_timeout_ms != null && resp.longpolling_timeout_ms > 0) {
344
+ nextTimeoutMs = resp.longpolling_timeout_ms;
345
+ }
346
+ // API 错误处理
347
+ const isError = (resp.ret !== undefined && resp.ret !== 0) ||
348
+ (resp.errcode !== undefined && resp.errcode !== 0);
349
+ if (isError) {
350
+ // Session expired 专用处理
351
+ const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
352
+ if (isSessionExpired) {
353
+ consecutiveFailures = 0;
354
+ logger.error(`[WeChat] Session expired (errcode=${resp.errcode}), retrying in ${SESSION_RETRY_DELAY_MS / 1000}s...`);
355
+ // 短暂停后重试一次
356
+ await this.sleep(SESSION_RETRY_DELAY_MS, signal);
357
+ if (signal.aborted)
358
+ return;
359
+ // 重试 getupdates
360
+ try {
361
+ const retryBody = JSON.stringify({
362
+ get_updates_buf: this.getUpdatesBuf,
363
+ base_info: { channel_version: CHANNEL_VERSION },
364
+ });
365
+ const retryRaw = await this.apiFetch('ilink/bot/getupdates', retryBody, nextTimeoutMs, signal);
366
+ const retryResp = JSON.parse(retryRaw);
367
+ const retryExpired = retryResp.errcode === SESSION_EXPIRED_ERRCODE || retryResp.ret === SESSION_EXPIRED_ERRCODE;
368
+ if (!retryExpired) {
369
+ // 恢复成功,静默继续
370
+ logger.info('[WeChat] Session recovered after retry');
371
+ // 把 retryResp 当正常响应处理(更新游标和消息)
372
+ if (retryResp.get_updates_buf) {
373
+ this.getUpdatesBuf = retryResp.get_updates_buf;
374
+ try {
375
+ fs.writeFileSync(this.syncBufPath, this.getUpdatesBuf, 'utf-8');
376
+ }
377
+ catch { }
378
+ }
379
+ for (const msg of retryResp.msgs ?? []) {
380
+ await this.handleInboundMessage(msg);
381
+ }
382
+ continue;
383
+ }
384
+ }
385
+ catch (retryErr) {
386
+ if (signal.aborted)
387
+ return;
388
+ logger.error('[WeChat] Retry after session expired also failed:', retryErr);
389
+ }
390
+ // 重试仍失败,进入长暂停
391
+ const pauseMin = SESSION_PAUSE_DURATION_MS / 60_000;
392
+ this.sessionPausedUntil = Date.now() + SESSION_PAUSE_DURATION_MS;
393
+ logger.error(`[WeChat] Session still expired, pausing for ${pauseMin}min`);
394
+ // 通知用户(通过事件总线或回调)
395
+ const authMsg = `⚠️ 微信 token 已过期,通道暂停 ${pauseMin} 分钟后自动重试。\n如需立即恢复,请运行: ec init wechat`;
396
+ if (this.eventBus) {
397
+ this.eventBus.publish({
398
+ type: 'channel:error',
399
+ channel: 'wechat',
400
+ status: 'auth_error',
401
+ message: authMsg,
402
+ timestamp: Date.now(),
403
+ });
404
+ }
405
+ else if (this.onSessionExpired) {
406
+ this.onSessionExpired(authMsg);
407
+ }
408
+ await this.sleep(SESSION_PAUSE_DURATION_MS, signal);
409
+ if (signal.aborted)
410
+ return;
411
+ // 长暂停结束,清除暂停状态,循环自动重试
412
+ this.sessionPausedUntil = 0;
413
+ logger.info('[WeChat] Session pause ended, resuming polling');
414
+ continue;
415
+ }
416
+ consecutiveFailures++;
417
+ logger.error(`[WeChat] getUpdates failed: ret=${resp.ret} errcode=${resp.errcode} errmsg=${resp.errmsg ?? ''}`);
418
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
419
+ logger.error(`[WeChat] ${MAX_CONSECUTIVE_FAILURES} consecutive failures, backing off ${BACKOFF_DELAY_MS / 1000}s`);
420
+ consecutiveFailures = 0;
421
+ await this.sleep(BACKOFF_DELAY_MS, signal);
422
+ }
423
+ else {
424
+ await this.sleep(RETRY_DELAY_MS, signal);
425
+ }
426
+ continue;
427
+ }
428
+ consecutiveFailures = 0;
429
+ // 保存游标
430
+ if (resp.get_updates_buf) {
431
+ this.getUpdatesBuf = resp.get_updates_buf;
432
+ try {
433
+ fs.writeFileSync(this.syncBufPath, this.getUpdatesBuf, 'utf-8');
434
+ }
435
+ catch {
436
+ // best-effort
437
+ }
438
+ }
439
+ // 处理消息
440
+ for (const msg of resp.msgs ?? []) {
441
+ await this.handleInboundMessage(msg);
442
+ }
443
+ }
444
+ catch (err) {
445
+ if (signal.aborted)
446
+ return;
447
+ consecutiveFailures++;
448
+ logger.error(`[WeChat] Poll error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}):`, err);
449
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
450
+ consecutiveFailures = 0;
451
+ await this.sleep(BACKOFF_DELAY_MS, signal);
452
+ }
453
+ else {
454
+ await this.sleep(RETRY_DELAY_MS, signal);
455
+ }
456
+ }
457
+ }
458
+ }
459
+ // ── Inbound Message Handler ──────────────────────────────────────────
460
+ async handleInboundMessage(msg) {
461
+ if (msg.message_type !== MSG_TYPE_USER)
462
+ return;
463
+ const fromUserId = msg.from_user_id ?? '';
464
+ // 缓存 context_token
465
+ if (msg.context_token) {
466
+ this.contextTokenCache.set(fromUserId, msg.context_token);
467
+ this.persistContextTokens();
468
+ }
469
+ // 提取文本(原有逻辑)
470
+ const text = extractTextFromMessage(msg);
471
+ // 提取媒体 → 下载
472
+ const media = await this.extractMedia(msg, fromUserId);
473
+ // 合成最终内容
474
+ const finalContent = media.prompt
475
+ ? (text ? `${text}\n\n${media.prompt}` : media.prompt)
476
+ : text;
477
+ if (!finalContent && !media.images.length)
478
+ return;
479
+ logger.info(`[WeChat] Received: from=${fromUserId} text=${(finalContent || '').slice(0, 50)} images=${media.images.length}...`);
480
+ // 发送 typing 指示器(异步,不阻塞)
481
+ this.acknowledgeMessage(fromUserId, msg.context_token).catch(() => { });
482
+ // 首次交互欢迎消息(使用共享帮助函数)
483
+ await sendWelcomeIfNeeded(this.welcomeManager, fromUserId, fromUserId, (id, text) => this.sendMessage(id, text), 'WeChat');
484
+ // 回调主流程
485
+ if (this.messageHandler) {
486
+ try {
487
+ await this.messageHandler(fromUserId, finalContent || '', fromUserId, media.images.length ? media.images : undefined, 'private');
488
+ }
489
+ catch (err) {
490
+ logger.error('[WeChat] Message handler error:', err);
491
+ }
492
+ }
493
+ }
494
+ // ── Acknowledge (sendTyping) ──────────────────────────────────────────
495
+ async acknowledgeMessage(fromUserId, contextToken) {
496
+ try {
497
+ const ticket = await this.getTypingTicket(fromUserId, contextToken);
498
+ if (!ticket)
499
+ return;
500
+ const body = JSON.stringify({
501
+ ilink_user_id: fromUserId,
502
+ typing_ticket: ticket,
503
+ status: 1, // typing
504
+ base_info: { channel_version: CHANNEL_VERSION },
505
+ });
506
+ await this.apiFetch('ilink/bot/sendtyping', body, DEFAULT_CONFIG_TIMEOUT_MS);
507
+ logger.debug(`[WeChat] Sent typing indicator to ${fromUserId}`);
508
+ }
509
+ catch {
510
+ // 静默失败,不阻塞主流程(和 Feishu addAckReaction 一致)
511
+ }
512
+ }
513
+ async getTypingTicket(userId, contextToken) {
514
+ const cached = this.typingTicketCache.get(userId);
515
+ if (cached && Date.now() - cached.fetchedAt < TYPING_TICKET_TTL_MS) {
516
+ return cached.ticket;
517
+ }
518
+ try {
519
+ const body = JSON.stringify({
520
+ ilink_user_id: userId,
521
+ context_token: contextToken,
522
+ base_info: { channel_version: CHANNEL_VERSION },
523
+ });
524
+ const rawText = await this.apiFetch('ilink/bot/getconfig', body, DEFAULT_CONFIG_TIMEOUT_MS);
525
+ const resp = JSON.parse(rawText);
526
+ if (resp.ret === 0 && resp.typing_ticket) {
527
+ this.typingTicketCache.set(userId, { ticket: resp.typing_ticket, fetchedAt: Date.now() });
528
+ return resp.typing_ticket;
529
+ }
530
+ }
531
+ catch {
532
+ // ignore
533
+ }
534
+ return undefined;
535
+ }
536
+ // ── Media Extraction (Inbound) ────────────────────────────────────────
537
+ async extractMedia(msg, channelId) {
538
+ const images = [];
539
+ const prompts = [];
540
+ for (const item of msg.item_list ?? []) {
541
+ try {
542
+ if (item.type === MSG_ITEM_IMAGE && item.image_item?.media) {
543
+ const buf = await downloadMedia(item.image_item.media, item.image_item.aeskey);
544
+ // 统一图片识别:magic bytes 优先正确区分 jpeg/png/gif/webp;
545
+ // 检测失败时回退到 image/jpeg(微信入站图片实际均为 jpeg,保留历史行为)。
546
+ const img = await bufferToInboundImage(buf, { contentType: 'image/jpeg' });
547
+ if (img)
548
+ images.push(img);
549
+ else
550
+ logger.warn('[WeChat] Image validation failed (not a supported image)');
551
+ }
552
+ else if (item.type === MSG_ITEM_FILE && item.file_item?.media) {
553
+ const buf = await downloadMedia(item.file_item.media);
554
+ const fileName = sanitizeFileName(item.file_item.file_name || `file_${Date.now()}`);
555
+ const savePath = await this.saveToUploadsLocal(buf, fileName, channelId);
556
+ prompts.push(`用户发送了文件:${fileName}\n文件已保存到:${savePath}\n请使用 Read 工具读取并分析文件内容。`);
557
+ }
558
+ else if (item.type === MSG_ITEM_VIDEO && item.video_item?.media) {
559
+ const buf = await downloadMedia(item.video_item.media);
560
+ const fileName = `video_${Date.now()}.mp4`;
561
+ const savePath = await this.saveToUploadsLocal(buf, fileName, channelId);
562
+ prompts.push(`用户发送了视频:${fileName}\n文件已保存到:${savePath}`);
563
+ }
564
+ }
565
+ catch (err) {
566
+ logger.error(`[WeChat] Failed to download media type=${item.type}:`, err);
567
+ }
568
+ }
569
+ return { prompt: prompts.join('\n\n'), images };
570
+ }
571
+ async saveToUploadsLocal(buf, fileName, channelId) {
572
+ const projectPath = this.projectPathResolver
573
+ ? await this.projectPathResolver(channelId)
574
+ : process.cwd();
575
+ const { filePath } = saveToUploads(buf, fileName, projectPath);
576
+ return filePath;
577
+ }
578
+ // ── Media Upload (Outbound) ──────────────────────────────────────────
579
+ async getUploadUrl(params) {
580
+ const body = JSON.stringify({
581
+ ...params,
582
+ base_info: { channel_version: CHANNEL_VERSION },
583
+ });
584
+ const raw = await this.apiFetch('ilink/bot/getuploadurl', body, DEFAULT_API_TIMEOUT_MS);
585
+ const resp = JSON.parse(raw);
586
+ if (!resp.upload_param)
587
+ throw new Error('getuploadurl: no upload_param');
588
+ return resp;
589
+ }
590
+ async cdnUpload(uploadParam, filekey, ciphertext) {
591
+ const url = `${CDN_BASE_URL}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${filekey}`;
592
+ let lastError;
593
+ for (let attempt = 0; attempt < 3; attempt++) {
594
+ try {
595
+ const res = await fetch(url, {
596
+ method: 'POST',
597
+ headers: { 'Content-Type': 'application/octet-stream' },
598
+ body: new Uint8Array(ciphertext),
599
+ });
600
+ if (res.status >= 400 && res.status < 500) {
601
+ throw new Error(`CDN upload client error: ${res.status}`);
602
+ }
603
+ if (!res.ok)
604
+ throw new Error(`CDN upload failed: ${res.status}`);
605
+ const downloadParam = res.headers.get('x-encrypted-param');
606
+ if (!downloadParam)
607
+ throw new Error('Missing x-encrypted-param header');
608
+ return downloadParam;
609
+ }
610
+ catch (err) {
611
+ lastError = err;
612
+ if (err.message?.includes('client error'))
613
+ throw err; // 4xx 不重试
614
+ }
615
+ }
616
+ throw lastError;
617
+ }
618
+ buildMediaItem(itemType, cdnMedia, filePath, ciphertextSize, plaintextSize) {
619
+ if (itemType === MSG_ITEM_IMAGE) {
620
+ return { type: MSG_ITEM_IMAGE, image_item: { media: cdnMedia, mid_size: ciphertextSize } };
621
+ }
622
+ if (itemType === MSG_ITEM_VIDEO) {
623
+ return { type: MSG_ITEM_VIDEO, video_item: { media: cdnMedia, video_size: ciphertextSize } };
624
+ }
625
+ return {
626
+ type: MSG_ITEM_FILE,
627
+ file_item: {
628
+ media: cdnMedia,
629
+ file_name: path.basename(filePath),
630
+ len: String(plaintextSize),
631
+ },
632
+ };
633
+ }
634
+ async sendMediaMessage(to, item, contextToken) {
635
+ const clientId = `evolcore-wechat:${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
636
+ const body = {
637
+ msg: {
638
+ from_user_id: '',
639
+ to_user_id: to,
640
+ client_id: clientId,
641
+ message_type: MSG_TYPE_BOT,
642
+ message_state: MSG_STATE_FINISH,
643
+ item_list: [item],
644
+ context_token: contextToken,
645
+ },
646
+ base_info: { channel_version: CHANNEL_VERSION },
647
+ };
648
+ await this.apiFetch('ilink/bot/sendmessage', JSON.stringify(body), DEFAULT_API_TIMEOUT_MS);
649
+ logger.info(`[WeChat] Sent media to ${to}, type=${item.type}`);
650
+ }
651
+ // ── ilink API Helpers ─────────────────────────────────────────────────
652
+ /** Notify ilink backend of bot lifecycle events (start/stop). */
653
+ async notifyLifecycle(action) {
654
+ const body = JSON.stringify({ base_info: { channel_version: CHANNEL_VERSION } });
655
+ await this.apiFetch(`ilink/bot/msg/${action}`, body, DEFAULT_CONFIG_TIMEOUT_MS);
656
+ logger.info(`[WeChat] ${action} succeeded`);
657
+ }
658
+ async apiFetch(endpoint, body, timeoutMs, externalSignal) {
659
+ const base = this.config.baseUrl.endsWith('/') ? this.config.baseUrl : `${this.config.baseUrl}/`;
660
+ const url = new URL(endpoint, base).toString();
661
+ const headers = this.buildHeaders(body);
662
+ const controller = new AbortController();
663
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
664
+ // 外部 signal(来自 disconnect)也要能中断
665
+ const onExternalAbort = () => controller.abort();
666
+ externalSignal?.addEventListener('abort', onExternalAbort, { once: true });
667
+ try {
668
+ const res = await fetch(url, {
669
+ method: 'POST',
670
+ headers,
671
+ body,
672
+ signal: controller.signal,
673
+ });
674
+ clearTimeout(timer);
675
+ const text = await res.text();
676
+ if (!res.ok)
677
+ throw new Error(`HTTP ${res.status}: ${text}`);
678
+ return text;
679
+ }
680
+ catch (err) {
681
+ clearTimeout(timer);
682
+ throw err;
683
+ }
684
+ finally {
685
+ externalSignal?.removeEventListener('abort', onExternalAbort);
686
+ }
687
+ }
688
+ buildHeaders(body) {
689
+ const uint32 = crypto.randomBytes(4).readUInt32BE(0);
690
+ const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
691
+ const headers = {
692
+ 'Content-Type': 'application/json',
693
+ 'AuthorizationType': 'ilink_bot_token',
694
+ 'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
695
+ 'X-WECHAT-UIN': wechatUin,
696
+ 'iLink-App-Id': ILINK_APP_ID,
697
+ 'iLink-App-ClientVersion': ILINK_APP_CLIENT_VERSION,
698
+ };
699
+ if (this.config.token?.trim()) {
700
+ headers['Authorization'] = `Bearer ${this.config.token.trim()}`;
701
+ }
702
+ return headers;
703
+ }
704
+ // ── Persistence ────────────────────────────────────────────────────
705
+ /** 持久化 context_token 到文件,供 restart-monitor 等外部进程读取 */
706
+ persistContextTokens() {
707
+ try {
708
+ const obj = {};
709
+ for (const [k, v] of this.contextTokenCache) {
710
+ obj[k] = v;
711
+ }
712
+ fs.writeFileSync(this.contextTokensPath, JSON.stringify(obj), 'utf-8');
713
+ }
714
+ catch {
715
+ // best-effort
716
+ }
717
+ }
718
+ // ── Utilities ─────────────────────────────────────────────────────────
719
+ sleep(ms, signal) {
720
+ return new Promise((resolve, reject) => {
721
+ const t = setTimeout(resolve, ms);
722
+ signal.addEventListener('abort', () => {
723
+ clearTimeout(t);
724
+ reject(new Error('aborted'));
725
+ }, { once: true });
726
+ });
727
+ }
728
+ }
729
+ import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
730
+ export class WechatChannelPlugin {
731
+ name = 'wechat';
732
+ async createInstance(inst, ctx) {
733
+ if (inst.enabled === false || !inst.token)
734
+ return null;
735
+ const channel = new WechatChannel({
736
+ baseUrl: inst.baseUrl || 'https://ilinkai.weixin.qq.com',
737
+ token: inst.token,
738
+ }, ctx.agentName, inst.name);
739
+ const mode = resolveShowActivities(inst);
740
+ const adapter = {
741
+ channelName: inst.name,
742
+ channelKey: inst.name,
743
+ capabilities: { file: false, image: false, interaction: false, markdown: false, thought: false, status: true, thread: false, authenticatedApproval: true },
744
+ send: async (envelope, payload) => {
745
+ const channelId = envelope.channelId;
746
+ switch (payload.kind) {
747
+ case 'result.text':
748
+ case 'command.result':
749
+ case 'command.error':
750
+ case 'system.notice':
751
+ case 'system.error':
752
+ case 'result.error':
753
+ await channel.sendMessage(channelId, payload.text);
754
+ return;
755
+ case 'result.file': {
756
+ const name = payload.fileName || payload.filePath;
757
+ await channel.sendMessage(channelId, `📎 文件已生成:${name}\n路径:${payload.filePath}`);
758
+ return;
759
+ }
760
+ case 'result.image': return;
761
+ case 'activity.batch': {
762
+ const filtered = payload.items.filter((i) => !(i.kind === 'tool_result' && i.ok));
763
+ const text = formatItemsAsText(filtered);
764
+ if (text)
765
+ await channel.sendMessage(channelId, text);
766
+ return;
767
+ }
768
+ case 'status.requires_action':
769
+ await channel.sendMessage(channelId, '等待 owner 审批');
770
+ return;
771
+ case 'status.error':
772
+ if (payload.metadata?.message)
773
+ await channel.sendMessage(channelId, payload.metadata.message);
774
+ return;
775
+ case 'interaction':
776
+ if (payload.fallbackText)
777
+ await channel.sendMessage(channelId, payload.fallbackText);
778
+ return;
779
+ default: return;
780
+ }
781
+ },
782
+ };
783
+ const policy = {
784
+ canSwitchProject: (_, identity) => identity === 'owner' || identity === 'admin',
785
+ canListProjects: (_, identity) => identity === 'owner' || identity === 'admin',
786
+ canCreateSession: () => true,
787
+ canDeleteSession: () => true,
788
+ canImportCliSession: (_, identity) => identity === 'owner' || identity === 'admin',
789
+ messagePrefix: () => '',
790
+ showMiddleResult: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
791
+ middleOutputMode: (chatType, identity) => middleOutputModePolicy(mode, chatType, identity),
792
+ showIdleMonitor: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
793
+ accumulateErrors: () => true,
794
+ };
795
+ return {
796
+ channelType: 'wechat', adapter, channel,
797
+ policy,
798
+ options: { fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g, flushDelay: inst.flushDelay ?? DEFAULT_FLUSH_DELAY_SECONDS },
799
+ connect: () => channel.connect(),
800
+ disconnect: () => channel.disconnect(),
801
+ onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
802
+ registerBridge(bridge, channelType) {
803
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async (channelId, content, peerId, images, chatType) => {
804
+ await handler({
805
+ channel: adapter.channelName, channelType, channelId,
806
+ selfAID: ctx.agentName, content, images,
807
+ chatType: chatType || 'private', peerId: peerId || '',
808
+ });
809
+ }), (channelId, text) => channel.sendMessage(channelId, text), adapter, channelType);
810
+ },
811
+ registerHooks(hookCtx) {
812
+ if (channel.setEventBus)
813
+ channel.setEventBus(hookCtx.eventBus);
814
+ },
815
+ };
816
+ }
817
+ }