evolcore 0.0.1 → 0.0.3

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 (379) hide show
  1. package/CHANGELOG.md +64 -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/bin/ec-safe-output.js +161 -0
  8. package/bin/ec.js +29 -0
  9. package/dist/agents/baseagent.js +163 -0
  10. package/dist/agents/claude-runner.js +2565 -0
  11. package/dist/agents/codex-app-server-client.js +448 -0
  12. package/dist/agents/codex-runner.js +2682 -0
  13. package/dist/agents/gemini-runner.js +666 -0
  14. package/dist/agents/runner-types.js +75 -0
  15. package/dist/aun/aid/agentmd.js +216 -0
  16. package/dist/aun/aid/client.js +132 -0
  17. package/dist/aun/aid/control-aid.js +91 -0
  18. package/dist/aun/aid/identity.js +518 -0
  19. package/dist/aun/aid/index.js +4 -0
  20. package/dist/aun/aid/store.js +74 -0
  21. package/dist/aun/aid/types.js +1 -0
  22. package/dist/aun/aid/validation.js +21 -0
  23. package/dist/aun/group-identity.js +10 -0
  24. package/dist/aun/msg/group-index.js +6 -0
  25. package/dist/aun/msg/group.js +1231 -0
  26. package/dist/aun/msg/history.js +123 -0
  27. package/dist/aun/msg/index.js +5 -0
  28. package/dist/aun/msg/p2p.js +393 -0
  29. package/dist/aun/msg/payload-type.js +27 -0
  30. package/dist/aun/msg/upload.js +137 -0
  31. package/dist/aun/outbox.js +168 -0
  32. package/dist/aun/rpc/caller.js +42 -0
  33. package/dist/aun/rpc/connection.js +25 -0
  34. package/dist/aun/rpc/index.js +2 -0
  35. package/dist/aun/service-proxy.js +225 -0
  36. package/dist/aun/storage/download.js +29 -0
  37. package/dist/aun/storage/index.js +3 -0
  38. package/dist/aun/storage/manage.js +10 -0
  39. package/dist/aun/storage/upload.js +68 -0
  40. package/dist/channels/aun.js +4164 -0
  41. package/dist/channels/contact-bind-code.js +134 -0
  42. package/dist/channels/daemon.js +422 -0
  43. package/dist/channels/dingtalk.js +1479 -0
  44. package/dist/channels/feishu.js +1865 -0
  45. package/dist/channels/qqbot.js +409 -0
  46. package/dist/channels/wechat.js +817 -0
  47. package/dist/channels/wecom-card.js +101 -0
  48. package/dist/channels/wecom-onboarding.js +82 -0
  49. package/dist/channels/wecom-state.js +191 -0
  50. package/dist/channels/wecom.js +1157 -0
  51. package/dist/cli/agent-command.js +642 -0
  52. package/dist/cli/agent.js +1059 -0
  53. package/dist/cli/aun-commands.js +2003 -0
  54. package/dist/cli/bench.js +1228 -0
  55. package/dist/cli/cli-argv.js +66 -0
  56. package/dist/cli/code-stats.js +329 -0
  57. package/dist/cli/command-log.js +82 -0
  58. package/dist/cli/config-selector.js +69 -0
  59. package/dist/cli/config.js +261 -0
  60. package/dist/cli/contact.js +71 -0
  61. package/dist/cli/ctl-command.js +62 -0
  62. package/dist/cli/daemon-commands.js +2750 -0
  63. package/dist/cli/fs-command.js +1447 -0
  64. package/dist/cli/handoff-command.js +302 -0
  65. package/dist/cli/help.js +35 -0
  66. package/dist/cli/index.js +374 -0
  67. package/dist/cli/init-channel.js +1372 -0
  68. package/dist/cli/init.js +590 -0
  69. package/dist/cli/link-rules.js +240 -0
  70. package/dist/cli/model.js +591 -0
  71. package/dist/cli/net-check.js +723 -0
  72. package/dist/cli/queue-command.js +150 -0
  73. package/dist/cli/raw-key-input.js +25 -0
  74. package/dist/cli/response.js +344 -0
  75. package/dist/cli/restart-monitor.js +480 -0
  76. package/dist/cli/stats.js +609 -0
  77. package/dist/cli/task-context.js +80 -0
  78. package/dist/cli/trigger-command.js +545 -0
  79. package/dist/cli/version.js +93 -0
  80. package/dist/cli/watch-logs.js +33 -0
  81. package/dist/cli/watch-msg.js +673 -0
  82. package/dist/config/boot-log.js +266 -0
  83. package/dist/config/builtin-role-templates.js +42 -0
  84. package/dist/config/builtin-roles.js +91 -0
  85. package/dist/config/config-batch-get.js +11 -0
  86. package/dist/config/config-field-policy.js +261 -0
  87. package/dist/config/config-manager.js +1120 -0
  88. package/dist/config/config-operation-service.js +384 -0
  89. package/dist/config/contact-alias.js +68 -0
  90. package/dist/config/contact-book-store.js +454 -0
  91. package/dist/config/contact-book-v2-startup.js +35 -0
  92. package/dist/config/contact-book.js +224 -0
  93. package/dist/config/contact-operation-service.js +110 -0
  94. package/dist/config/gateway-config.js +858 -0
  95. package/dist/config/lifecycle.js +17 -0
  96. package/dist/config/mention-mode.js +27 -0
  97. package/dist/config/merge.js +161 -0
  98. package/dist/config/owner-policy.js +4 -0
  99. package/dist/config/peer-role-resolver.js +218 -0
  100. package/dist/config/resolved-config-op.js +483 -0
  101. package/dist/config/role-config-v4-startup.js +32 -0
  102. package/dist/config/role-config-v5-startup.js +27 -0
  103. package/dist/config/role-ranks.js +18 -0
  104. package/dist/config/role-schema.js +105 -0
  105. package/dist/config/role-service.js +156 -0
  106. package/dist/config/role-store.js +215 -0
  107. package/dist/config/roles.js +64 -0
  108. package/dist/config/schema-registry.js +154 -0
  109. package/dist/config/snapshot.js +598 -0
  110. package/dist/config-store.js +501 -0
  111. package/dist/core/auth/agent-delegation.js +111 -0
  112. package/dist/core/auth/auth-gateway.js +166 -0
  113. package/dist/core/auth/authenticated-actor.js +6 -0
  114. package/dist/core/auth/authorization-audit.js +119 -0
  115. package/dist/core/auth/operation-authorizer.js +720 -0
  116. package/dist/core/auth/operation-catalog.js +731 -0
  117. package/dist/core/baseagent-loader.js +54 -0
  118. package/dist/core/bootstrap-service.js +175 -0
  119. package/dist/core/capability/capability-manager.js +316 -0
  120. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  121. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  122. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  123. package/dist/core/capability/types.js +27 -0
  124. package/dist/core/causation/audit.js +103 -0
  125. package/dist/core/causation/aun-association.js +111 -0
  126. package/dist/core/causation/context.js +93 -0
  127. package/dist/core/causation/index.js +4 -0
  128. package/dist/core/causation/types.js +2 -0
  129. package/dist/core/channel-loader.js +277 -0
  130. package/dist/core/command/agent-control.js +616 -0
  131. package/dist/core/command/cli-intent-parser.js +225 -0
  132. package/dist/core/command/command-handler.js +1733 -0
  133. package/dist/core/command/connect-menu.js +374 -0
  134. package/dist/core/command/menu-handler.js +3452 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1623 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3066 -0
  139. package/dist/core/daemon-file-cache.js +222 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +810 -0
  142. package/dist/core/evolagent-registry.js +545 -0
  143. package/dist/core/evolagent.js +342 -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 +659 -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 +874 -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 +1446 -0
  161. package/dist/core/message/message-utils.js +76 -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 +4020 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/send-receipt.js +24 -0
  168. package/dist/core/message/stream-debouncer.js +139 -0
  169. package/dist/core/message/stream-idle-monitor.js +124 -0
  170. package/dist/core/model/config-scope.js +162 -0
  171. package/dist/core/model/field-scope.js +78 -0
  172. package/dist/core/model/model-catalog.js +227 -0
  173. package/dist/core/model/model-diagnostics.js +182 -0
  174. package/dist/core/model/model-permission.js +90 -0
  175. package/dist/core/permission/approval-gateway.js +1017 -0
  176. package/dist/core/permission/ec-command-parser.js +347 -0
  177. package/dist/core/permission/execution-sandbox.js +16 -0
  178. package/dist/core/permission/index.js +6 -0
  179. package/dist/core/permission/mode.js +24 -0
  180. package/dist/core/permission/sandbox-runtime.js +265 -0
  181. package/dist/core/permission/tool-policy.js +1019 -0
  182. package/dist/core/permission/unix-socket-policy.js +99 -0
  183. package/dist/core/protected-paths.js +332 -0
  184. package/dist/core/relation/peer-identity.js +222 -0
  185. package/dist/core/relation/peer-key.js +1 -0
  186. package/dist/core/role/runtime-policy.js +141 -0
  187. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  188. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  189. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  190. package/dist/core/session/session-file-adapter.js +7 -0
  191. package/dist/core/session/session-file-health.js +45 -0
  192. package/dist/core/session/session-fs-store.js +273 -0
  193. package/dist/core/session/session-key.js +24 -0
  194. package/dist/core/session/session-manager.js +1643 -0
  195. package/dist/core/session/session-mapper.js +100 -0
  196. package/dist/core/session/session-renew.js +314 -0
  197. package/dist/core/session/session-title.js +128 -0
  198. package/dist/core/session/session-turn-coordinator.js +205 -0
  199. package/dist/core/session/session-turns.js +67 -0
  200. package/dist/core/system-channels.js +29 -0
  201. package/dist/eck/baseagent-caps.js +18 -0
  202. package/dist/eck/detect.js +47 -0
  203. package/dist/eck/group-rules-sync.js +345 -0
  204. package/dist/eck/init.js +77 -0
  205. package/dist/eck/kit-renderer.js +359 -0
  206. package/dist/eck/manifest-engine.js +446 -0
  207. package/dist/eck/message-renderer.js +199 -0
  208. package/dist/eck/rules-loader.js +28 -0
  209. package/dist/index.js +2926 -4
  210. package/dist/ipc.js +777 -0
  211. package/dist/paths.js +262 -0
  212. package/dist/product.js +18 -0
  213. package/dist/response-system/context-builder.js +71 -0
  214. package/dist/response-system/coordinator.js +117 -0
  215. package/dist/response-system/decision-executor.js +86 -0
  216. package/dist/response-system/engines/v1/index.js +21 -0
  217. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  218. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  219. package/dist/response-system/engines/v1/types.js +1 -0
  220. package/dist/response-system/extensions.js +41 -0
  221. package/dist/response-system/index.js +6 -0
  222. package/dist/response-system/modes/index.js +7 -0
  223. package/dist/response-system/modes/single-session/index.js +72 -0
  224. package/dist/response-system/queues/fifo-queue.js +44 -0
  225. package/dist/response-system/queues/index.js +6 -0
  226. package/dist/response-system/queues/lifo-queue.js +42 -0
  227. package/dist/response-system/queues/priority-queue.js +63 -0
  228. package/dist/response-system/registry.js +97 -0
  229. package/dist/response-system/resolver.js +37 -0
  230. package/dist/response-system/selector.js +23 -0
  231. package/dist/response-system/types.js +7 -0
  232. package/dist/stats/billing.js +163 -0
  233. package/dist/stats/budget.js +93 -0
  234. package/dist/stats/db.js +403 -0
  235. package/dist/stats/eck-vars.js +89 -0
  236. package/dist/stats/index.js +11 -0
  237. package/dist/stats/normalizer.js +80 -0
  238. package/dist/stats/price-resolver.js +138 -0
  239. package/dist/stats/query.js +763 -0
  240. package/dist/stats/role-budget.js +168 -0
  241. package/dist/stats/writer.js +151 -0
  242. package/dist/trigger/anomaly-store.js +258 -0
  243. package/dist/trigger/audit.js +152 -0
  244. package/dist/trigger/event-source.js +119 -0
  245. package/dist/trigger/feedback.js +685 -0
  246. package/dist/trigger/history.js +290 -0
  247. package/dist/trigger/manager.js +294 -0
  248. package/dist/trigger/parser.js +595 -0
  249. package/dist/trigger/patch.js +155 -0
  250. package/dist/trigger/scheduler.js +1602 -0
  251. package/dist/trigger/script-executor.js +155 -0
  252. package/dist/trigger/state.js +145 -0
  253. package/dist/trigger/types.js +1 -0
  254. package/dist/trigger/validation.js +634 -0
  255. package/dist/types.js +12 -0
  256. package/dist/utils/aid-bind.js +313 -0
  257. package/dist/utils/atomic-write.js +95 -0
  258. package/dist/utils/avatar-upload.js +123 -0
  259. package/dist/utils/cross-platform.js +297 -0
  260. package/dist/utils/ecweb-utils.js +73 -0
  261. package/dist/utils/error-dict.json +153 -0
  262. package/dist/utils/error-utils.js +349 -0
  263. package/dist/utils/instance-registry.js +444 -0
  264. package/dist/utils/locale.js +21 -0
  265. package/dist/utils/log-writer.js +270 -0
  266. package/dist/utils/logger.js +89 -0
  267. package/dist/utils/markdown-to-plain-text.js +20 -0
  268. package/dist/utils/media-cache.js +274 -0
  269. package/dist/utils/model-prices.jsonl +20 -0
  270. package/dist/utils/npm-ops.js +210 -0
  271. package/dist/utils/process-introspect.js +133 -0
  272. package/dist/utils/process-tree-stats.js +271 -0
  273. package/dist/utils/project-path.js +74 -0
  274. package/dist/utils/restart-safety.js +31 -0
  275. package/dist/utils/stats.js +410 -0
  276. package/dist/utils/system-memory.js +62 -0
  277. package/dist/utils/tool-summary.js +284 -0
  278. package/dist/utils/welcome.js +268 -0
  279. package/kits/docs/GUIDE.md +20 -0
  280. package/kits/docs/INDEX.md +66 -0
  281. package/kits/docs/aun/CHEATSHEET.md +19 -0
  282. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  283. package/kits/docs/channels/aun.md +65 -0
  284. package/kits/docs/channels/feishu.md +56 -0
  285. package/kits/docs/context-assembly.md +366 -0
  286. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  287. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  288. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  289. package/kits/docs/eck_templates/runtime.template.md +19 -0
  290. package/kits/docs/evolcore/INDEX.md +68 -0
  291. package/kits/docs/evolcore/agent.md +77 -0
  292. package/kits/docs/evolcore/aid.md +52 -0
  293. package/kits/docs/evolcore/config.md +149 -0
  294. package/kits/docs/evolcore/contact.md +57 -0
  295. package/kits/docs/evolcore/ctl.md +46 -0
  296. package/kits/docs/evolcore/event.md +216 -0
  297. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  298. package/kits/docs/evolcore/fs.md +110 -0
  299. package/kits/docs/evolcore/group-fs.md +17 -0
  300. package/kits/docs/evolcore/group-rules.md +226 -0
  301. package/kits/docs/evolcore/group.md +150 -0
  302. package/kits/docs/evolcore/model.md +50 -0
  303. package/kits/docs/evolcore/msg.md +136 -0
  304. package/kits/docs/evolcore/response.md +75 -0
  305. package/kits/docs/evolcore/rpc.md +37 -0
  306. package/kits/docs/evolcore/self-summary.md +29 -0
  307. package/kits/docs/evolcore/stats.md +83 -0
  308. package/kits/docs/evolcore/storage.md +50 -0
  309. package/kits/docs/evolcore/trigger.md +539 -0
  310. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  311. package/kits/docs/identity/PATH_OPS.md +16 -0
  312. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  313. package/kits/docs/identity/identity-tools.md +26 -0
  314. package/kits/docs/path-registry.md +43 -0
  315. package/kits/docs/prompt-loading-architecture.md +266 -0
  316. package/kits/docs/venues/aun-group.md +45 -0
  317. package/kits/docs/venues/aun-private.md +10 -0
  318. package/kits/docs/venues/client-desktop.md +10 -0
  319. package/kits/docs/venues/client-mobile.md +10 -0
  320. package/kits/docs/venues/feishu-group.md +13 -0
  321. package/kits/docs/venues/feishu-private.md +9 -0
  322. package/kits/docs/venues/group.md +25 -0
  323. package/kits/docs/venues/private.md +10 -0
  324. package/kits/eck_manifest.auxiliary.json +43 -0
  325. package/kits/eck_manifest.json +203 -0
  326. package/kits/eck_message_manifest.json +63 -0
  327. package/kits/migrations/README-role-config-v4.md +32 -0
  328. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  329. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  330. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  331. package/kits/migrations/rename-config-file.mjs +99 -0
  332. package/kits/rules/01-overview.md +142 -0
  333. package/kits/rules/02-navigation.md +76 -0
  334. package/kits/rules/03-identity.md +34 -0
  335. package/kits/rules/04-relation.md +59 -0
  336. package/kits/rules/05-venue.md +44 -0
  337. package/kits/rules/06-channel.md +59 -0
  338. package/kits/schemas/_meta.json +32 -0
  339. package/kits/schemas/agent-config.schema.1.json +177 -0
  340. package/kits/schemas/agent-config.schema.2.json +239 -0
  341. package/kits/schemas/agent-config.schema.3.json +119 -0
  342. package/kits/schemas/agent-config.schema.4.json +208 -0
  343. package/kits/schemas/agent-config.schema.5.json +326 -0
  344. package/kits/schemas/agent-config.schema.6.json +322 -0
  345. package/kits/schemas/contact-book.schema.1.json +36 -0
  346. package/kits/schemas/contact-book.schema.2.json +43 -0
  347. package/kits/schemas/daemon.schema.1.json +90 -0
  348. package/kits/schemas/defaults.schema.1.json +81 -0
  349. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  350. package/kits/schemas/migrations/README.md +28 -0
  351. package/kits/schemas/relation-config.schema.1.json +158 -0
  352. package/kits/schemas/relation-config.schema.2.json +73 -0
  353. package/kits/schemas/relation-config.schema.3.json +50 -0
  354. package/kits/schemas/relation-config.schema.4.json +47 -0
  355. package/kits/schemas/relation-config.schema.5.json +47 -0
  356. package/kits/schemas/role-config.schema.1.json +201 -0
  357. package/kits/schemas/role-registry.schema.1.json +35 -0
  358. package/kits/schemas/single-session.schema.1.json +31 -0
  359. package/kits/templates/bootstrap-welcome.md +15 -0
  360. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  361. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  362. package/kits/templates/message-fragments/inject-default.md +2 -0
  363. package/kits/templates/message-fragments/item.md +2 -0
  364. package/kits/templates/roles/admin.json +9 -0
  365. package/kits/templates/roles/member.json +41 -0
  366. package/kits/templates/roles/owner.json +9 -0
  367. package/kits/templates/roles/visitor.json +40 -0
  368. package/kits/templates/system-fragments/baseagent.md +14 -0
  369. package/kits/templates/system-fragments/bootstrap.md +16 -0
  370. package/kits/templates/system-fragments/channel.md +48 -0
  371. package/kits/templates/system-fragments/commands.md +28 -0
  372. package/kits/templates/system-fragments/identity.md +11 -0
  373. package/kits/templates/system-fragments/relation.md +19 -0
  374. package/kits/templates/system-fragments/session.md +53 -0
  375. package/kits/templates/system-fragments/venue.md +31 -0
  376. package/package.json +50 -15
  377. package/dist/index.d.ts +0 -7
  378. package/dist/index.d.ts.map +0 -1
  379. package/dist/index.js.map +0 -1
@@ -0,0 +1,1372 @@
1
+ /**
2
+ * Channel-specific init flows: Feishu, WeChat, AUN
3
+ *
4
+ * Each channel provides:
5
+ * - cmdInit<Channel>() — standalone `ec init <channel>` entry
6
+ * - run<Channel>QrFlow() or setupAun*() — reusable primitives for the main init wizard
7
+ */
8
+ import readline from 'readline';
9
+ import crypto from 'crypto';
10
+ import { selectInstance } from './init.js';
11
+ import { loadAllAgents, loadAgent, saveAgent, loadDaemonConfig, saveDaemonConfig } from '../config-store.js';
12
+ import { agentChannelUpsert } from './agent.js';
13
+ import { isValidAid } from '../aun/aid/index.js';
14
+ import { resolvePaths } from '../paths.js';
15
+ import { ipcQuery } from '../ipc.js';
16
+ import { cmdStart } from './daemon-commands.js';
17
+ import * as platform from '../utils/cross-platform.js';
18
+ import { cleanupInstances } from '../utils/instance-registry.js';
19
+ import { bindContactAlias } from '../config/contact-book.js';
20
+ import { listStaticAgentAdmins } from '../config/peer-role-resolver.js';
21
+ import { setupRawKeyListener } from './raw-key-input.js';
22
+ function ask(rl, question) {
23
+ return new Promise(resolve => rl.question(question, resolve));
24
+ }
25
+ export async function runDaemonOwnerQrBindFlow(ownerMode = 'append') {
26
+ const p = resolvePaths();
27
+ const daemonConfig = loadDaemonConfig();
28
+ if (!daemonConfig.aid) {
29
+ console.log('❌ 未找到控制 AID,请先运行 ec init');
30
+ return null;
31
+ }
32
+ let status = await ipcQuery(p.socket, { type: 'status' }, 1000);
33
+ let startedBootstrapDaemon = false;
34
+ if (!status) {
35
+ await cmdStart({ bindBootstrap: true });
36
+ startedBootstrapDaemon = loadAllAgents().agents.length === 0;
37
+ status = await waitForIpcStatus(30_000);
38
+ }
39
+ try {
40
+ if (!status) {
41
+ console.log('❌ daemon 未就绪,无法创建绑定任务');
42
+ return null;
43
+ }
44
+ if (!status.controlAid?.connected) {
45
+ console.log('⚠ daemon 已启动,但 AUN 控制通道尚未连接;仍将生成二维码,daemon 会后台重连');
46
+ }
47
+ const begin = await ipcQuery(p.socket, {
48
+ type: 'bind.begin',
49
+ bindType: 'daemon',
50
+ ownerMode,
51
+ }, 3000);
52
+ if (!begin?.ok) {
53
+ console.log(`❌ 创建绑定任务失败: ${begin?.error || 'unknown error'}`);
54
+ return null;
55
+ }
56
+ await printQr(begin.qrData);
57
+ console.log('\n请用 evol app 扫描上方二维码完成绑定');
58
+ console.log('按 q 取消 | 10 分钟内有效\n');
59
+ const result = await pollBindStatusWithKeyboard(begin.taskId, begin.expiresAt);
60
+ if (result === 'quit') {
61
+ await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
62
+ console.log('已取消');
63
+ return null;
64
+ }
65
+ if (result === 'skip') {
66
+ await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
67
+ console.log('已跳过扫码');
68
+ return null;
69
+ }
70
+ if (!result) {
71
+ console.log('绑定超时,请重新生成二维码');
72
+ return null;
73
+ }
74
+ return { boundAid: result.boundAid, boundName: result.boundName };
75
+ }
76
+ finally {
77
+ if (startedBootstrapDaemon) {
78
+ await stopStartedBootstrapDaemon();
79
+ }
80
+ }
81
+ }
82
+ export async function runAgentOwnerQrBindFlow(targetAid, agentName, ownerMode = 'append') {
83
+ const p = resolvePaths();
84
+ const daemonConfig = loadDaemonConfig();
85
+ if (!daemonConfig.aid) {
86
+ console.log('❌ 未找到控制 AID,请先运行 ec init');
87
+ return null;
88
+ }
89
+ let status = await ipcQuery(p.socket, { type: 'status' }, 1000);
90
+ if (!status) {
91
+ await cmdStart();
92
+ status = await waitForIpcStatus(30_000);
93
+ }
94
+ if (!status) {
95
+ console.log('❌ daemon 未就绪,无法创建 agent 绑定任务');
96
+ return null;
97
+ }
98
+ if (!status.controlAid?.connected) {
99
+ console.log('⚠ daemon 已启动,但 AUN 控制通道尚未连接;仍将生成二维码,daemon 会后台重连');
100
+ }
101
+ const begin = await ipcQuery(p.socket, {
102
+ type: 'bind.begin',
103
+ bindType: 'agent',
104
+ targetAid,
105
+ agentName,
106
+ ownerMode,
107
+ }, 3000);
108
+ if (!begin?.ok) {
109
+ console.log(`❌ 创建 agent 绑定任务失败: ${begin?.error || 'unknown error'}`);
110
+ return null;
111
+ }
112
+ await printQr(begin.qrData);
113
+ console.log('\n请用 evol app 扫描上方二维码,10 分钟内有效');
114
+ console.log('按 q 退出 | 按 s 跳过扫码手动输入 agent owner AID\n');
115
+ const result = await pollBindStatusWithKeyboard(begin.taskId, begin.expiresAt);
116
+ if (result === 'quit') {
117
+ await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
118
+ console.log('已取消');
119
+ return null;
120
+ }
121
+ if (result === 'skip') {
122
+ await ipcQuery(p.socket, { type: 'bind.cancel', taskId: begin.taskId }, 1000);
123
+ console.log('已跳过扫码');
124
+ return null;
125
+ }
126
+ if (!result) {
127
+ console.log('绑定超时');
128
+ return null;
129
+ }
130
+ await reloadAgentAfterBind(targetAid);
131
+ return { boundAid: result.boundAid, boundName: result.boundName };
132
+ }
133
+ async function waitForIpcStatus(timeoutMs) {
134
+ const deadline = Date.now() + timeoutMs;
135
+ const p = resolvePaths();
136
+ while (Date.now() < deadline) {
137
+ const status = await ipcQuery(p.socket, { type: 'status' }, 1000);
138
+ if (status?.pid)
139
+ return status;
140
+ await new Promise(r => setTimeout(r, 500));
141
+ }
142
+ return null;
143
+ }
144
+ async function stopStartedBootstrapDaemon() {
145
+ const status = await ipcQuery(resolvePaths().socket, { type: 'status' }, 1000);
146
+ const pid = typeof status?.pid === 'number' ? status.pid : null;
147
+ if (!pid)
148
+ return;
149
+ platform.killProcess(pid);
150
+ if (!await waitForProcessExit(pid, 5_000)) {
151
+ platform.killProcess(pid, true);
152
+ await waitForProcessExit(pid, 3_000);
153
+ }
154
+ cleanupInstances();
155
+ const stale = await ipcQuery(resolvePaths().socket, { type: 'status' }, 500);
156
+ if (stale?.pid === pid) {
157
+ console.log(`⚠ bootstrap daemon PID ${pid} 仍在响应 IPC,后续 start 可能需要先执行 ec stop`);
158
+ }
159
+ }
160
+ async function waitForProcessExit(pid, timeoutMs) {
161
+ const deadline = Date.now() + timeoutMs;
162
+ while (Date.now() < deadline) {
163
+ if (!platform.isProcessRunning(pid)) {
164
+ return true;
165
+ }
166
+ await new Promise(r => setTimeout(r, 250));
167
+ }
168
+ return !platform.isProcessRunning(pid);
169
+ }
170
+ async function reloadAgentAfterBind(aid) {
171
+ try {
172
+ const result = await ipcQuery(resolvePaths().socket, { type: 'evolagent.reload', name: aid }, 30_000);
173
+ if (result?.ok) {
174
+ console.log(' ✓ agent owner 已热重载');
175
+ }
176
+ else if (result?.error) {
177
+ console.log(` ⚠ agent owner 已写入配置,热重载失败:${result.error}`);
178
+ }
179
+ }
180
+ catch {
181
+ console.log(' ⚠ agent owner 已写入配置,daemon 未响应热重载;下次启动生效');
182
+ }
183
+ }
184
+ async function printQr(qrData) {
185
+ const qrText = JSON.stringify(qrData);
186
+ try {
187
+ const qrterm = await import('qrcode-terminal');
188
+ await new Promise(resolve => {
189
+ qrterm.default.generate(qrText, { small: true }, (qr) => {
190
+ console.log(qr);
191
+ resolve();
192
+ });
193
+ });
194
+ }
195
+ catch {
196
+ console.log(qrText);
197
+ }
198
+ }
199
+ async function pollBindStatusWithKeyboard(taskId, expiresAt) {
200
+ let quit = false;
201
+ let skip = false;
202
+ const p = resolvePaths();
203
+ const cleanup = setupRawKeyListener((key) => {
204
+ if (key === 'q' || key === '\u0003')
205
+ quit = true;
206
+ if (key === 's' || key === 'S')
207
+ skip = true;
208
+ });
209
+ try {
210
+ while (Date.now() < expiresAt) {
211
+ if (quit)
212
+ return 'quit';
213
+ if (skip)
214
+ return 'skip';
215
+ const status = await ipcQuery(p.socket, { type: 'bind.status', taskId }, 1500);
216
+ if (status?.ok && status.status === 'bound') {
217
+ return { boundAid: status.boundAid, boundName: status.boundName };
218
+ }
219
+ if (status?.ok && (status.status === 'expired' || status.status === 'cancelled')) {
220
+ return null;
221
+ }
222
+ await new Promise(r => setTimeout(r, 1000));
223
+ }
224
+ return null;
225
+ }
226
+ finally {
227
+ cleanup();
228
+ }
229
+ }
230
+ // ==================== Feishu ====================
231
+ const FEISHU_PROD_URL = 'https://accounts.feishu.cn';
232
+ const LARK_PROD_URL = 'https://accounts.larksuite.com';
233
+ const SKIP = Symbol('SKIP');
234
+ const QUIT = Symbol('QUIT');
235
+ class FeishuQrRegistrationClient {
236
+ baseUrl;
237
+ constructor(isLark = false) {
238
+ this.baseUrl = isLark ? LARK_PROD_URL : FEISHU_PROD_URL;
239
+ }
240
+ setDomain(isLark) {
241
+ this.baseUrl = isLark ? LARK_PROD_URL : FEISHU_PROD_URL;
242
+ }
243
+ async init() {
244
+ return this.postRegistration('init', {});
245
+ }
246
+ async begin() {
247
+ return this.postRegistration('begin', {
248
+ archetype: 'PersonalAgent',
249
+ auth_method: 'client_secret',
250
+ request_user_info: 'open_id',
251
+ });
252
+ }
253
+ async poll(deviceCode) {
254
+ return this.postRegistration('poll', { device_code: deviceCode });
255
+ }
256
+ async postRegistration(action, extraParams) {
257
+ const body = new URLSearchParams({ action, ...extraParams }).toString();
258
+ const res = await fetch(`${this.baseUrl}/oauth/v1/app/registration`, {
259
+ method: 'POST',
260
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
261
+ body,
262
+ });
263
+ const text = await res.text();
264
+ if (!text)
265
+ return {};
266
+ return JSON.parse(text);
267
+ }
268
+ }
269
+ async function runQrRegistrationFlow() {
270
+ const client = new FeishuQrRegistrationClient();
271
+ const initResult = await client.init();
272
+ const authMethods = Array.isArray(initResult.supported_auth_methods) ? initResult.supported_auth_methods : [];
273
+ if (!authMethods.includes('client_secret')) {
274
+ throw new Error('当前环境不支持 client_secret 注册');
275
+ }
276
+ const beginResult = await client.begin();
277
+ if (!beginResult.verification_uri_complete || !beginResult.device_code) {
278
+ throw new Error('服务端未返回扫码链接或 device_code');
279
+ }
280
+ // 显示二维码
281
+ try {
282
+ const qrterm = await import('qrcode-terminal');
283
+ await new Promise(resolve => {
284
+ qrterm.default.generate(beginResult.verification_uri_complete, { small: true }, (qr) => {
285
+ console.log(qr);
286
+ resolve();
287
+ });
288
+ });
289
+ }
290
+ catch {
291
+ console.log(`请在浏览器中打开此链接扫码: ${beginResult.verification_uri_complete}\n`);
292
+ }
293
+ console.log('请用飞书/Lark 扫描上方二维码...\n');
294
+ console.log('按 q 退出 | 按 s 跳过扫码手动输入 appId/appSecret\n');
295
+ let userAction = null;
296
+ const cleanup = setupRawKeyListener((key) => {
297
+ if (key === 'q' || key === '\u0003')
298
+ userAction = QUIT;
299
+ if (key === 's')
300
+ userAction = SKIP;
301
+ });
302
+ const startedAt = Date.now();
303
+ let pollIntervalSeconds = Number(beginResult.interval ?? 5);
304
+ const expireInSeconds = Number(beginResult.expires_in ?? beginResult.expire_in ?? 600);
305
+ let domainResolved = false;
306
+ let currentDomain = 'feishu';
307
+ try {
308
+ while (Date.now() - startedAt < expireInSeconds * 1000) {
309
+ if (userAction === QUIT)
310
+ return QUIT;
311
+ if (userAction === SKIP)
312
+ return SKIP;
313
+ const pollResult = await client.poll(beginResult.device_code);
314
+ if (pollResult.user_info?.tenant_brand === 'lark' && !domainResolved) {
315
+ client.setDomain(true);
316
+ currentDomain = 'lark';
317
+ domainResolved = true;
318
+ }
319
+ if (pollResult.client_id && pollResult.client_secret) {
320
+ return {
321
+ appId: pollResult.client_id,
322
+ appSecret: pollResult.client_secret,
323
+ domain: currentDomain,
324
+ openId: pollResult.user_info?.open_id ?? '',
325
+ };
326
+ }
327
+ if (pollResult.error === 'authorization_pending') {
328
+ await new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
329
+ continue;
330
+ }
331
+ if (pollResult.error === 'slow_down') {
332
+ pollIntervalSeconds += 5;
333
+ await new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
334
+ continue;
335
+ }
336
+ if (pollResult.error === 'access_denied') {
337
+ throw new Error('用户拒绝了扫码授权');
338
+ }
339
+ if (pollResult.error === 'expired_token') {
340
+ throw new Error('扫码会话已过期');
341
+ }
342
+ if (pollResult.error) {
343
+ throw new Error(`扫码注册失败: ${pollResult.error}${pollResult.error_description ? ` - ${pollResult.error_description}` : ''}`);
344
+ }
345
+ await new Promise(r => setTimeout(r, pollIntervalSeconds * 1000));
346
+ }
347
+ throw new Error('等待扫码结果超时');
348
+ }
349
+ finally {
350
+ cleanup();
351
+ }
352
+ }
353
+ async function manualInput(rl) {
354
+ console.log('\n手动输入模式:\n');
355
+ let appId = '';
356
+ while (!appId) {
357
+ appId = (await ask(rl, ' 飞书 App ID: ')).trim();
358
+ if (!appId)
359
+ console.log(' ⚠ 不能为空');
360
+ }
361
+ let appSecret = '';
362
+ while (!appSecret) {
363
+ appSecret = (await ask(rl, ' 飞书 App Secret: ')).trim();
364
+ if (!appSecret)
365
+ console.log(' ⚠ 不能为空');
366
+ }
367
+ return { appId, appSecret, domain: 'unknown', openId: '' };
368
+ }
369
+ export async function runFeishuQrFlow() {
370
+ try {
371
+ const result = await runQrRegistrationFlow();
372
+ if (result === QUIT || result === SKIP)
373
+ return null;
374
+ return result;
375
+ }
376
+ catch (error) {
377
+ console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
378
+ return null;
379
+ }
380
+ }
381
+ export async function cmdInitFeishu() {
382
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
383
+ let aid = null;
384
+ let agentConfig = null;
385
+ let choice = null;
386
+ let contactBinding = null;
387
+ try {
388
+ aid = await pickAgentForChannel(rl);
389
+ if (!aid)
390
+ return;
391
+ agentConfig = loadAgent(aid);
392
+ if (!agentConfig) {
393
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
394
+ return;
395
+ }
396
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'feishu');
397
+ choice = await pickInstanceWithinAgent(rl, 'feishu', existing);
398
+ if (choice === null)
399
+ return;
400
+ contactBinding = await pickOwnerAdminForChannelBinding(rl, agentConfig, 'feishu');
401
+ if (!contactBinding)
402
+ return;
403
+ }
404
+ finally {
405
+ rl.close();
406
+ }
407
+ console.log('\n正在获取飞书登录二维码...\n');
408
+ let result;
409
+ try {
410
+ const flowResult = await runQrRegistrationFlow();
411
+ if (flowResult === QUIT) {
412
+ console.log('已退出');
413
+ return;
414
+ }
415
+ if (flowResult === SKIP) {
416
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
417
+ try {
418
+ result = await manualInput(rl2);
419
+ }
420
+ finally {
421
+ rl2.close();
422
+ }
423
+ }
424
+ else {
425
+ result = flowResult;
426
+ }
427
+ }
428
+ catch (error) {
429
+ console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
430
+ process.exit(1);
431
+ }
432
+ const channel = {
433
+ type: 'feishu',
434
+ name: choice.name,
435
+ enabled: true,
436
+ appId: result.appId,
437
+ appSecret: result.appSecret,
438
+ };
439
+ const commitResult = await commitChannel(aid, channel, choice.action);
440
+ if (!commitResult)
441
+ return;
442
+ console.log(` App ID: ${result.appId}`);
443
+ if (result.openId)
444
+ console.log(` Login Open ID: ${result.openId}`);
445
+ if (result.domain !== 'unknown')
446
+ console.log(` Domain: ${result.domain}`);
447
+ bindQrActorToContact(aid, contactBinding, 'feishu', result.openId, commitResult.channelKey);
448
+ // Show unified init success message
449
+ const { generateInitSuccessMessage } = await import('../utils/welcome.js');
450
+ console.log(generateInitSuccessMessage('feishu', contactBinding !== null));
451
+ }
452
+ // ==================== WeChat ====================
453
+ const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
454
+ const BOT_TYPE = '3';
455
+ const QR_POLL_TIMEOUT_MS = 35_000;
456
+ const LOGIN_TIMEOUT_MS = 480_000;
457
+ async function fetchQRCode(baseUrl) {
458
+ const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
459
+ const url = `${base}ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
460
+ const res = await fetch(url);
461
+ if (!res.ok)
462
+ throw new Error(`QR fetch failed: ${res.status}`);
463
+ return (await res.json());
464
+ }
465
+ async function pollQRStatus(baseUrl, qrcode) {
466
+ const base = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
467
+ const url = `${base}ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
468
+ const controller = new AbortController();
469
+ const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
470
+ try {
471
+ const res = await fetch(url, {
472
+ headers: { 'iLink-App-ClientVersion': '1' },
473
+ signal: controller.signal,
474
+ });
475
+ clearTimeout(timer);
476
+ if (!res.ok)
477
+ throw new Error(`QR status failed: ${res.status}`);
478
+ return (await res.json());
479
+ }
480
+ catch (err) {
481
+ clearTimeout(timer);
482
+ if (err instanceof Error && err.name === 'AbortError') {
483
+ return { status: 'wait' };
484
+ }
485
+ throw err;
486
+ }
487
+ }
488
+ export async function runWechatQrFlow() {
489
+ const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
490
+ try {
491
+ const qrterm = await import('qrcode-terminal');
492
+ await new Promise(resolve => {
493
+ qrterm.default.generate(qrResp.qrcode_img_content, { small: true }, (qr) => {
494
+ console.log(qr);
495
+ resolve();
496
+ });
497
+ });
498
+ }
499
+ catch {
500
+ console.log(`请在浏览器中打开此链接扫码: ${qrResp.qrcode_img_content}\n`);
501
+ }
502
+ console.log('请用微信扫描上方二维码...\n');
503
+ const deadline = Date.now() + LOGIN_TIMEOUT_MS;
504
+ let scannedPrinted = false;
505
+ let currentPollUrl = DEFAULT_BASE_URL;
506
+ while (Date.now() < deadline) {
507
+ const status = await pollQRStatus(currentPollUrl, qrResp.qrcode);
508
+ switch (status.status) {
509
+ case 'wait':
510
+ process.stdout.write('.');
511
+ break;
512
+ case 'scaned':
513
+ if (!scannedPrinted) {
514
+ console.log('\n👀 已扫码,请在微信中确认...');
515
+ scannedPrinted = true;
516
+ }
517
+ break;
518
+ case 'scaned_but_redirect':
519
+ if (status.redirect_host) {
520
+ currentPollUrl = `https://${status.redirect_host}`;
521
+ }
522
+ break;
523
+ case 'expired':
524
+ console.error('\n二维码已过期');
525
+ return null;
526
+ case 'confirmed':
527
+ if (!status.ilink_bot_id || !status.bot_token) {
528
+ console.error('\n登录失败:服务器未返回完整信息');
529
+ return null;
530
+ }
531
+ return {
532
+ baseUrl: status.baseurl || DEFAULT_BASE_URL,
533
+ token: status.bot_token,
534
+ userId: status.ilink_user_id,
535
+ };
536
+ }
537
+ await new Promise(r => setTimeout(r, 1000));
538
+ }
539
+ console.log('\n登录超时');
540
+ return null;
541
+ }
542
+ export async function cmdInitWechat() {
543
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
544
+ let aid = null;
545
+ let agentConfig = null;
546
+ let choice = null;
547
+ let contactBinding = null;
548
+ try {
549
+ aid = await pickAgentForChannel(rl);
550
+ if (!aid)
551
+ return;
552
+ agentConfig = loadAgent(aid);
553
+ if (!agentConfig) {
554
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
555
+ return;
556
+ }
557
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'wechat');
558
+ choice = await pickInstanceWithinAgent(rl, 'wechat', existing);
559
+ if (choice === null)
560
+ return;
561
+ contactBinding = await pickOwnerAdminForChannelBinding(rl, agentConfig, 'wechat');
562
+ if (!contactBinding)
563
+ return;
564
+ }
565
+ finally {
566
+ rl.close();
567
+ }
568
+ console.log('\n正在获取微信登录二维码...\n');
569
+ const result = await runWechatQrFlow();
570
+ if (!result) {
571
+ console.log('已取消');
572
+ return;
573
+ }
574
+ const channel = {
575
+ type: 'wechat',
576
+ name: choice.name,
577
+ enabled: true,
578
+ baseUrl: result.baseUrl,
579
+ token: result.token,
580
+ };
581
+ const commitResult = await commitChannel(aid, channel, choice.action);
582
+ if (!commitResult)
583
+ return;
584
+ bindQrActorToContact(aid, contactBinding, 'wechat', result.userId, commitResult.channelKey);
585
+ // Show unified init success message
586
+ const { generateInitSuccessMessage } = await import('../utils/welcome.js');
587
+ console.log(generateInitSuccessMessage('wechat', contactBinding !== null));
588
+ }
589
+ // ==================== AUN ====================
590
+ export async function cmdInitAun() {
591
+ const daemonConfig = loadDaemonConfig();
592
+ if (!daemonConfig.aid) {
593
+ console.log('❌ 未找到控制 AID,请先运行 ec init');
594
+ return;
595
+ }
596
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
597
+ try {
598
+ const target = await pickAunOwnerBindTarget(rl, daemonConfig.aid);
599
+ if (!target)
600
+ return;
601
+ if (target.kind === 'daemon') {
602
+ console.log(`\n📡 EvolCore daemon owner 绑定 — ${daemonConfig.aid}\n`);
603
+ const result = await runDaemonOwnerQrBindFlow('append');
604
+ if (result?.boundAid) {
605
+ console.log(`\n✅ daemon owner 已绑定: ${result.boundAid}`);
606
+ return;
607
+ }
608
+ await promptDaemonOwnerManually(rl);
609
+ return;
610
+ }
611
+ const agentConfig = loadAgent(target.aid);
612
+ console.log(`\n📡 Agent owner 绑定 — ${target.aid}\n`);
613
+ const result = await runAgentOwnerQrBindFlow(target.aid, agentConfig?.aid ?? target.aid, 'append');
614
+ if (result?.boundAid) {
615
+ console.log(`\n✅ agent owner 已绑定: ${result.boundAid}`);
616
+ return;
617
+ }
618
+ await promptAgentOwnerForAunManually(rl, target.aid);
619
+ }
620
+ finally {
621
+ rl.close();
622
+ }
623
+ }
624
+ async function pickAunOwnerBindTarget(rl, daemonAid) {
625
+ const { agents } = loadAllAgents();
626
+ console.log('\n请选择 AUN owner 绑定目标:');
627
+ console.log(` d. daemon owner (${daemonAid})`);
628
+ const letters = 'abcdefghijklmnopqrstuvwxyz';
629
+ for (let i = 0; i < agents.length; i++) {
630
+ console.log(` ${letters[i]}. agent owner (${agents[i].aid})`);
631
+ }
632
+ console.log(' q. 取消');
633
+ const validAgentLetters = letters.slice(0, agents.length).split('');
634
+ while (true) {
635
+ const input = (await ask(rl, '请选择 [d]: ')).trim().toLowerCase() || 'd';
636
+ if (input === 'q')
637
+ return null;
638
+ if (input === 'd')
639
+ return { kind: 'daemon' };
640
+ if (validAgentLetters.includes(input)) {
641
+ return { kind: 'agent', aid: agents[letters.indexOf(input)].aid };
642
+ }
643
+ console.log(`无效选择,请输入 d${validAgentLetters.length ? `/${validAgentLetters.join('/')}` : ''}/q`);
644
+ }
645
+ }
646
+ async function promptDaemonOwnerManually(rl) {
647
+ const fallback = (await ask(rl, '\n扫码未完成。手动输入 daemon owner AID(留空跳过): ')).trim();
648
+ if (!fallback)
649
+ return;
650
+ if (!isValidAid(fallback)) {
651
+ console.log(' ⚠ Owner AID 格式无效(需合法多级域名,如 alice.agentid.pub)');
652
+ return;
653
+ }
654
+ const owners = [fallback, ...(loadDaemonConfig().owners || []).filter(o => o !== fallback)];
655
+ saveDaemonConfig({ ...loadDaemonConfig(), owners });
656
+ console.log(`✅ daemon owner 已设置: ${fallback}`);
657
+ }
658
+ async function promptAgentOwnerForAunManually(rl, aid) {
659
+ const fallback = (await ask(rl, '\n扫码未完成。手动输入 agent owner AID(留空跳过): ')).trim();
660
+ if (!fallback)
661
+ return;
662
+ if (!isValidAid(fallback)) {
663
+ console.log(' ⚠ Owner AID 格式无效(需合法多级域名,如 alice.agentid.pub)');
664
+ return;
665
+ }
666
+ const agent = loadAgent(aid);
667
+ if (!agent) {
668
+ console.log(` ⚠ 无法加载 agent 配置: ${aid}`);
669
+ return;
670
+ }
671
+ const owners = [fallback, ...(agent.owners ?? []).filter(owner => owner !== fallback)];
672
+ saveAgent({ ...agent, owners });
673
+ try {
674
+ const result = await ipcQuery(resolvePaths().socket, { type: 'evolagent.reload', name: aid }, 30_000);
675
+ if (result?.ok)
676
+ console.log(' ✓ agent owner 已热重载');
677
+ }
678
+ catch { /* daemon not running */ }
679
+ console.log(`✅ agent owner 已设置: ${fallback}`);
680
+ }
681
+ // ==================== DingTalk ====================
682
+ const DINGTALK_BASE_URL = 'https://oapi.dingtalk.com';
683
+ const DINGTALK_SOURCE = 'openClaw';
684
+ async function dingtalkApiPost(path, payload) {
685
+ const res = await fetch(`${DINGTALK_BASE_URL}${path}`, {
686
+ method: 'POST',
687
+ headers: { 'Content-Type': 'application/json' },
688
+ body: JSON.stringify(payload),
689
+ });
690
+ if (!res.ok)
691
+ throw new Error(`DingTalk API ${path} failed: ${res.status}`);
692
+ const data = await res.json();
693
+ if (data.errcode && data.errcode !== 0) {
694
+ throw new Error(`DingTalk API ${path}: ${data.errmsg || 'unknown error'} (errcode=${data.errcode})`);
695
+ }
696
+ return data;
697
+ }
698
+ async function runDingtalkQrFlow() {
699
+ // Step 1: init → nonce
700
+ const initData = await dingtalkApiPost('/app/registration/init', { source: DINGTALK_SOURCE });
701
+ const nonce = initData.nonce?.trim();
702
+ if (!nonce)
703
+ throw new Error('DingTalk init: 未返回 nonce');
704
+ // Step 2: begin → device_code + verification_uri_complete
705
+ const beginData = await dingtalkApiPost('/app/registration/begin', { nonce });
706
+ const deviceCode = beginData.device_code?.trim();
707
+ const verificationUri = beginData.verification_uri_complete?.trim();
708
+ if (!deviceCode)
709
+ throw new Error('DingTalk begin: 未返回 device_code');
710
+ if (!verificationUri)
711
+ throw new Error('DingTalk begin: 未返回 verification_uri_complete');
712
+ // Display QR code
713
+ try {
714
+ const qrterm = await import('qrcode-terminal');
715
+ await new Promise(resolve => {
716
+ qrterm.default.generate(verificationUri, { small: true }, (qr) => {
717
+ console.log(qr);
718
+ resolve();
719
+ });
720
+ });
721
+ }
722
+ catch {
723
+ console.log(`请在浏览器中打开此链接扫码: ${verificationUri}\n`);
724
+ }
725
+ console.log('请用钉钉扫描上方二维码...\n');
726
+ console.log('提示: 扫码页面标注 "OpenClaw" 是钉钉生态接入桥,可放心使用。\n');
727
+ console.log('按 q 退出 | 按 s 跳过扫码手动输入\n');
728
+ let userAction = null;
729
+ const cleanup = setupRawKeyListener((key) => {
730
+ if (key === 'q' || key === '\u0003')
731
+ userAction = QUIT;
732
+ if (key === 's')
733
+ userAction = SKIP;
734
+ });
735
+ const pollInterval = Math.max(Number(beginData.interval ?? 3), 2);
736
+ const expiresIn = Number(beginData.expires_in ?? 7200);
737
+ const startedAt = Date.now();
738
+ try {
739
+ while (Date.now() - startedAt < expiresIn * 1000) {
740
+ if (userAction === QUIT)
741
+ return QUIT;
742
+ if (userAction === SKIP)
743
+ return SKIP;
744
+ await new Promise(r => setTimeout(r, pollInterval * 1000));
745
+ const pollData = await dingtalkApiPost('/app/registration/poll', { device_code: deviceCode });
746
+ const status = (pollData.status || '').trim().toUpperCase();
747
+ if (status === 'SUCCESS') {
748
+ if (!pollData.client_id || !pollData.client_secret) {
749
+ throw new Error('授权成功但未返回凭据');
750
+ }
751
+ return {
752
+ clientId: pollData.client_id.trim(),
753
+ clientSecret: pollData.client_secret.trim(),
754
+ };
755
+ }
756
+ if (status === 'WAITING') {
757
+ continue;
758
+ }
759
+ if (status === 'EXPIRED') {
760
+ throw new Error('扫码会话已过期');
761
+ }
762
+ if (status === 'FAIL') {
763
+ throw new Error(`授权失败: ${pollData.fail_reason || '未知原因'}`);
764
+ }
765
+ // Unknown status — keep polling
766
+ }
767
+ throw new Error('等待扫码结果超时');
768
+ }
769
+ finally {
770
+ cleanup();
771
+ }
772
+ }
773
+ export async function runDingtalkQrFlowSimple() {
774
+ try {
775
+ const result = await runDingtalkQrFlow();
776
+ if (result === QUIT || result === SKIP)
777
+ return null;
778
+ return result;
779
+ }
780
+ catch (error) {
781
+ console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
782
+ return null;
783
+ }
784
+ }
785
+ export async function cmdInitDingtalk() {
786
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
787
+ let aid = null;
788
+ let agentConfig = null;
789
+ let choice = null;
790
+ let contactBinding = null;
791
+ try {
792
+ aid = await pickAgentForChannel(rl);
793
+ if (!aid)
794
+ return;
795
+ agentConfig = loadAgent(aid);
796
+ if (!agentConfig) {
797
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
798
+ return;
799
+ }
800
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'dingtalk');
801
+ choice = await pickInstanceWithinAgent(rl, 'dingtalk', existing);
802
+ if (choice === null)
803
+ return;
804
+ contactBinding = await pickOwnerAdminForChannelBinding(rl, agentConfig, 'dingtalk');
805
+ if (!contactBinding)
806
+ return;
807
+ }
808
+ finally {
809
+ rl.close();
810
+ }
811
+ console.log('\n正在获取钉钉登录二维码...\n');
812
+ let result;
813
+ try {
814
+ const flowResult = await runDingtalkQrFlow();
815
+ if (flowResult === QUIT) {
816
+ console.log('已退出');
817
+ return;
818
+ }
819
+ if (flowResult === SKIP) {
820
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
821
+ try {
822
+ console.log('\n手动输入模式:\n');
823
+ let clientId = '';
824
+ while (!clientId) {
825
+ clientId = (await ask(rl2, ' 钉钉 Client ID (AppKey): ')).trim();
826
+ if (!clientId)
827
+ console.log(' ⚠ 不能为空');
828
+ }
829
+ let clientSecret = '';
830
+ while (!clientSecret) {
831
+ clientSecret = (await ask(rl2, ' 钉钉 Client Secret (AppSecret): ')).trim();
832
+ if (!clientSecret)
833
+ console.log(' ⚠ 不能为空');
834
+ }
835
+ result = { clientId, clientSecret };
836
+ }
837
+ finally {
838
+ rl2.close();
839
+ }
840
+ }
841
+ else {
842
+ result = flowResult;
843
+ }
844
+ }
845
+ catch (error) {
846
+ console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
847
+ process.exit(1);
848
+ }
849
+ const channel = {
850
+ type: 'dingtalk',
851
+ name: choice.name,
852
+ enabled: true,
853
+ clientId: result.clientId,
854
+ clientSecret: result.clientSecret,
855
+ };
856
+ const commitResult = await commitChannel(aid, channel, choice.action);
857
+ if (!commitResult)
858
+ return;
859
+ console.log(` Client ID: ${result.clientId}`);
860
+ if (commitResult.reloaded) {
861
+ await registerDingtalkContactBind(aid, commitResult.channelKey, contactBinding);
862
+ }
863
+ else {
864
+ console.log(' ⚠ 服务未运行或热重载失败,无法注册钉钉绑定码;请启动服务后重新执行 ec init dingtalk 并再次扫码绑定。');
865
+ }
866
+ const { generateInitSuccessMessage } = await import('../utils/welcome.js');
867
+ console.log(generateInitSuccessMessage('dingtalk', contactBinding !== null));
868
+ }
869
+ // ==================== QQBot ====================
870
+ const QQBOT_PORTAL_HOST = 'q.qq.com';
871
+ const QQBOT_CREATE_PATH = '/lite/create_bind_task';
872
+ const QQBOT_POLL_PATH = '/lite/poll_bind_result';
873
+ const QQBOT_QR_TEMPLATE = 'https://q.qq.com/qqbot/openclaw/connect.html?task_id={task_id}&_wv=2&source=hermes';
874
+ const QQBOT_POLL_INTERVAL_MS = 2000;
875
+ const QQBOT_POLL_TIMEOUT_MS = 600_000; // 10 minutes
876
+ function qqbotApiHeaders() {
877
+ return {
878
+ 'Content-Type': 'application/json',
879
+ 'Accept': 'application/json', // Required — without it, q.qq.com returns anti-bot HTML
880
+ 'User-Agent': 'EvolCore/QQBotInit',
881
+ };
882
+ }
883
+ function generateBindKey() {
884
+ const keyBuffer = crypto.randomBytes(32);
885
+ return { keyBase64: keyBuffer.toString('base64'), keyBuffer };
886
+ }
887
+ function decryptSecret(encryptedBase64, keyBuffer) {
888
+ const raw = Buffer.from(encryptedBase64, 'base64');
889
+ const iv = raw.subarray(0, 12);
890
+ const authTag = raw.subarray(raw.length - 16);
891
+ const ciphertext = raw.subarray(12, raw.length - 16);
892
+ const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, iv);
893
+ decipher.setAuthTag(authTag);
894
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
895
+ return decrypted.toString('utf-8');
896
+ }
897
+ async function runQQBotBindFlow() {
898
+ // Step 1: Generate AES key and create bind task
899
+ const { keyBase64, keyBuffer } = generateBindKey();
900
+ const createRes = await fetch(`https://${QQBOT_PORTAL_HOST}${QQBOT_CREATE_PATH}`, {
901
+ method: 'POST',
902
+ headers: qqbotApiHeaders(),
903
+ body: JSON.stringify({ key: keyBase64 }),
904
+ });
905
+ if (!createRes.ok)
906
+ throw new Error(`create_bind_task failed: ${createRes.status}`);
907
+ const createData = await createRes.json();
908
+ if (createData.retcode !== 0) {
909
+ throw new Error(`create_bind_task: ${createData.msg || 'unknown error'}`);
910
+ }
911
+ const taskId = createData.data?.task_id;
912
+ if (!taskId)
913
+ throw new Error('create_bind_task: 未返回 task_id');
914
+ // Step 2: Build QR URL and display
915
+ const qrUrl = QQBOT_QR_TEMPLATE.replace('{task_id}', encodeURIComponent(taskId));
916
+ try {
917
+ const qrterm = await import('qrcode-terminal');
918
+ await new Promise(resolve => {
919
+ qrterm.default.generate(qrUrl, { small: true }, (qr) => {
920
+ console.log(qr);
921
+ resolve();
922
+ });
923
+ });
924
+ }
925
+ catch {
926
+ console.log(`请在浏览器中打开此链接扫码: ${qrUrl}\n`);
927
+ }
928
+ console.log('请用 QQ 扫描上方二维码绑定机器人...\n');
929
+ console.log('按 q 退出 | 按 s 跳过扫码手动输入\n');
930
+ let userAction = null;
931
+ const cleanup = setupRawKeyListener((key) => {
932
+ if (key === 'q' || key === '\u0003')
933
+ userAction = QUIT;
934
+ if (key === 's')
935
+ userAction = SKIP;
936
+ });
937
+ const startedAt = Date.now();
938
+ try {
939
+ // Step 3: Poll for bind result
940
+ while (Date.now() - startedAt < QQBOT_POLL_TIMEOUT_MS) {
941
+ if (userAction === QUIT)
942
+ return QUIT;
943
+ if (userAction === SKIP)
944
+ return SKIP;
945
+ await new Promise(r => setTimeout(r, QQBOT_POLL_INTERVAL_MS));
946
+ const pollRes = await fetch(`https://${QQBOT_PORTAL_HOST}${QQBOT_POLL_PATH}`, {
947
+ method: 'POST',
948
+ headers: qqbotApiHeaders(),
949
+ body: JSON.stringify({ task_id: taskId }),
950
+ });
951
+ if (!pollRes.ok)
952
+ continue; // transient error, keep polling
953
+ const pollData = await pollRes.json();
954
+ if (pollData.retcode !== 0)
955
+ continue;
956
+ const status = pollData.data?.status ?? 0 /* QQBotBindStatus.NONE */;
957
+ if (status === 2 /* QQBotBindStatus.COMPLETED */) {
958
+ const botAppId = pollData.data?.bot_appid;
959
+ const encryptedSecret = pollData.data?.bot_encrypt_secret;
960
+ if (!botAppId || !encryptedSecret) {
961
+ throw new Error('绑定成功但未返回完整凭据');
962
+ }
963
+ // Step 4: Decrypt the secret
964
+ const clientSecret = decryptSecret(encryptedSecret, keyBuffer);
965
+ return {
966
+ appId: botAppId,
967
+ clientSecret,
968
+ userOpenId: pollData.data?.user_openid,
969
+ };
970
+ }
971
+ if (status === 3 /* QQBotBindStatus.EXPIRED */) {
972
+ throw new Error('二维码已过期');
973
+ }
974
+ // NONE or PENDING — keep polling
975
+ }
976
+ throw new Error('等待扫码结果超时');
977
+ }
978
+ finally {
979
+ cleanup();
980
+ }
981
+ }
982
+ export async function runQQBotBindFlowSimple() {
983
+ try {
984
+ const result = await runQQBotBindFlow();
985
+ if (result === QUIT || result === SKIP)
986
+ return null;
987
+ return result;
988
+ }
989
+ catch (error) {
990
+ console.error(`\n绑定失败: ${error instanceof Error ? error.message : error}`);
991
+ return null;
992
+ }
993
+ }
994
+ export async function cmdInitQQBot() {
995
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
996
+ let aid = null;
997
+ let agentConfig = null;
998
+ let choice = null;
999
+ let contactBinding = null;
1000
+ try {
1001
+ aid = await pickAgentForChannel(rl);
1002
+ if (!aid)
1003
+ return;
1004
+ agentConfig = loadAgent(aid);
1005
+ if (!agentConfig) {
1006
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
1007
+ return;
1008
+ }
1009
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'qqbot');
1010
+ choice = await pickInstanceWithinAgent(rl, 'qqbot', existing);
1011
+ if (choice === null)
1012
+ return;
1013
+ contactBinding = await pickOwnerAdminForChannelBinding(rl, agentConfig, 'qqbot');
1014
+ if (!contactBinding)
1015
+ return;
1016
+ }
1017
+ finally {
1018
+ rl.close();
1019
+ }
1020
+ console.log('\n正在创建 QQ 机器人绑定任务...\n');
1021
+ let result;
1022
+ try {
1023
+ const flowResult = await runQQBotBindFlow();
1024
+ if (flowResult === QUIT) {
1025
+ console.log('已退出');
1026
+ return;
1027
+ }
1028
+ if (flowResult === SKIP) {
1029
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
1030
+ try {
1031
+ console.log('\n手动输入模式:\n');
1032
+ let appId = '';
1033
+ while (!appId) {
1034
+ appId = (await ask(rl2, ' QQ 机器人 App ID: ')).trim();
1035
+ if (!appId)
1036
+ console.log(' ⚠ 不能为空');
1037
+ }
1038
+ let clientSecret = '';
1039
+ while (!clientSecret) {
1040
+ clientSecret = (await ask(rl2, ' QQ 机器人 Client Secret: ')).trim();
1041
+ if (!clientSecret)
1042
+ console.log(' ⚠ 不能为空');
1043
+ }
1044
+ result = { appId, clientSecret };
1045
+ }
1046
+ finally {
1047
+ rl2.close();
1048
+ }
1049
+ }
1050
+ else {
1051
+ result = flowResult;
1052
+ }
1053
+ }
1054
+ catch (error) {
1055
+ console.error(`\n绑定失败: ${error instanceof Error ? error.message : error}`);
1056
+ process.exit(1);
1057
+ }
1058
+ const channel = {
1059
+ type: 'qqbot',
1060
+ name: choice.name,
1061
+ enabled: true,
1062
+ appId: result.appId,
1063
+ clientSecret: result.clientSecret,
1064
+ };
1065
+ const commitResult = await commitChannel(aid, channel, choice.action);
1066
+ if (!commitResult)
1067
+ return;
1068
+ console.log(` App ID: ${result.appId}`);
1069
+ bindQrActorToContact(aid, contactBinding, 'qqbot', result.userOpenId, commitResult.channelKey);
1070
+ const { generateInitSuccessMessage } = await import('../utils/welcome.js');
1071
+ console.log(generateInitSuccessMessage('qqbot', contactBinding !== null));
1072
+ }
1073
+ // ==================== WeCom (企业微信) ====================
1074
+ export async function cmdInitWecom() {
1075
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1076
+ let aid = null;
1077
+ let agentConfig = null;
1078
+ let choice = null;
1079
+ let contactBinding = null;
1080
+ let botId = '';
1081
+ let secret = '';
1082
+ try {
1083
+ aid = await pickAgentForChannel(rl);
1084
+ if (!aid)
1085
+ return;
1086
+ agentConfig = loadAgent(aid);
1087
+ if (!agentConfig) {
1088
+ console.error(`❌ 无法加载 agent ${aid} 的配置`);
1089
+ return;
1090
+ }
1091
+ const existing = (agentConfig.channels || []).filter(c => c.type === 'wecom');
1092
+ choice = await pickInstanceWithinAgent(rl, 'wecom', existing);
1093
+ if (choice === null)
1094
+ return;
1095
+ contactBinding = await pickOwnerAdminForChannelBinding(rl, agentConfig, 'wecom');
1096
+ if (!contactBinding)
1097
+ return;
1098
+ console.log('\n企业微信 AI Bot 配置');
1099
+ const method = (await ask(rl, ' 接入方式:1. 扫码创建(推荐) 2. 手工输入 [1]: ')).trim() || '1';
1100
+ if (method === '1') {
1101
+ try {
1102
+ const { createWecomQrSession, pollWecomQrCredentials } = await import('../channels/wecom-onboarding.js');
1103
+ const session = await createWecomQrSession();
1104
+ const qrterm = await import('qrcode-terminal');
1105
+ console.log('\n请使用企业微信扫描以下二维码:\n');
1106
+ await new Promise(resolve => {
1107
+ qrterm.default.generate(session.authUrl, { small: true }, (qr) => {
1108
+ console.log(qr);
1109
+ resolve();
1110
+ });
1111
+ });
1112
+ console.log(`也可在浏览器打开:${session.browserUrl}`);
1113
+ console.log('等待扫码确认,二维码 5 分钟内有效...\n');
1114
+ const credentials = await pollWecomQrCredentials(session.scode, {
1115
+ onStatus: status => {
1116
+ if (status !== 'waiting')
1117
+ console.log(` 扫码状态: ${status}`);
1118
+ },
1119
+ });
1120
+ botId = credentials.botId;
1121
+ secret = credentials.secret;
1122
+ console.log(' ✓ 已取得 Bot ID 和 Secret');
1123
+ }
1124
+ catch (error) {
1125
+ console.log(` ⚠ 扫码接入失败: ${error instanceof Error ? error.message : String(error)}`);
1126
+ console.log(' 将切换到手工输入模式。\n');
1127
+ }
1128
+ }
1129
+ if (!botId || !secret) {
1130
+ console.log('请在企业微信管理后台 → AI Bot 页面获取 Bot ID 和 Secret\n');
1131
+ }
1132
+ while (!botId) {
1133
+ botId = (await ask(rl, ' Bot ID: ')).trim();
1134
+ if (!botId)
1135
+ console.log(' ⚠ 不能为空');
1136
+ }
1137
+ while (!secret) {
1138
+ secret = (await ask(rl, ' Secret: ')).trim();
1139
+ if (!secret)
1140
+ console.log(' ⚠ 不能为空');
1141
+ }
1142
+ }
1143
+ finally {
1144
+ rl.close();
1145
+ }
1146
+ const channel = {
1147
+ type: 'wecom',
1148
+ name: choice.name,
1149
+ enabled: true,
1150
+ botId,
1151
+ secret,
1152
+ };
1153
+ const commitResult = await commitChannel(aid, channel, choice.action);
1154
+ if (!commitResult)
1155
+ return;
1156
+ console.log(` Bot ID: ${botId}`);
1157
+ if (commitResult.reloaded) {
1158
+ await registerWecomContactBind(aid, commitResult.channelKey, contactBinding);
1159
+ }
1160
+ else {
1161
+ console.log(' ⚠ 服务未运行或热重载失败,无法注册企微绑定码;请启动服务后重新执行 ec init wecom。');
1162
+ }
1163
+ const { generateInitSuccessMessage } = await import('../utils/welcome.js');
1164
+ console.log(generateInitSuccessMessage('wecom', contactBinding !== null));
1165
+ }
1166
+ // ==================== Shared helpers for per-agent init <channel> ====================
1167
+ /**
1168
+ * Pick the target agent for an `ec init <channel>` flow.
1169
+ *
1170
+ * - 0 agents → print guidance and return null
1171
+ * - ≥1 → letter menu; with 1 agent the prompt accepts Enter (defaults to 'a')
1172
+ */
1173
+ async function pickAgentForChannel(rl) {
1174
+ const { agents } = loadAllAgents();
1175
+ if (agents.length === 0) {
1176
+ console.log('❌ 暂无 agent,请先创建:');
1177
+ console.log(' ec agent new <aid>.agentid.pub');
1178
+ return null;
1179
+ }
1180
+ const letters = 'abcdefghijklmnopqrstuvwxyz';
1181
+ console.log(`共 ${agents.length} 个 agent:`);
1182
+ for (let i = 0; i < agents.length; i++) {
1183
+ console.log(` ${letters[i]}. ${agents[i].aid}`);
1184
+ }
1185
+ const valid = letters.slice(0, agents.length).split('');
1186
+ const promptSuffix = agents.length === 1 ? ' [a]' : '';
1187
+ let choice = '';
1188
+ while (!valid.includes(choice)) {
1189
+ choice = (await ask(rl, `请选择${promptSuffix}: `)).trim().toLowerCase();
1190
+ if (agents.length === 1 && choice === '')
1191
+ choice = 'a';
1192
+ if (!valid.includes(choice)) {
1193
+ console.log(`无效选择,请输入 ${valid.join('/')}`);
1194
+ }
1195
+ }
1196
+ return agents[letters.indexOf(choice)].aid;
1197
+ }
1198
+ /**
1199
+ * Pick "add new instance" or "overwrite existing instance" within a single agent
1200
+ * for the given channel type. Returns null if user cancels.
1201
+ *
1202
+ * If existing.length === 0 → returns { action:'add', name:'main' } directly.
1203
+ */
1204
+ async function pickInstanceWithinAgent(rl, channelType, existing) {
1205
+ if (existing.length === 0) {
1206
+ return { action: 'add', name: 'main' };
1207
+ }
1208
+ const view = existing.map((c, i) => ({ ...c, name: c.name, originalIndex: i }));
1209
+ return await selectInstance(rl, channelType, view);
1210
+ }
1211
+ async function pickOwnerAdminForChannelBinding(rl, agentConfig, channelType) {
1212
+ const candidates = ownerAdminContactCandidates(agentConfig);
1213
+ if (candidates.length === 0) {
1214
+ console.log(`\n❌ agent ${agentConfig.aid} 尚未配置 owner/admin,无法为 ${channelType} 扫码账号建立身份映射。`);
1215
+ console.log(' 请先通过 AUN 绑定 agent owner,或由 owner 在 AUN 私聊 relation 中分配 admin 后重试。');
1216
+ return null;
1217
+ }
1218
+ console.log(`\n请选择 ${channelType} 扫码账号要绑定到的 owner/admin AID:`);
1219
+ candidates.forEach((candidate, index) => {
1220
+ console.log(` ${index + 1}. ${candidate.primaryId} (${candidate.label})`);
1221
+ });
1222
+ const defaultIndex = candidates.length === 1 ? 1 : null;
1223
+ const prompt = defaultIndex ? `请选择 [${defaultIndex}]: ` : '请选择: ';
1224
+ while (true) {
1225
+ const raw = (await ask(rl, prompt)).trim();
1226
+ const selected = raw === '' && defaultIndex ? defaultIndex : Number(raw);
1227
+ if (Number.isInteger(selected) && selected >= 1 && selected <= candidates.length) {
1228
+ return candidates[selected - 1];
1229
+ }
1230
+ console.log(`无效选择,请输入 1-${candidates.length}`);
1231
+ }
1232
+ }
1233
+ function ownerAdminContactCandidates(agentConfig) {
1234
+ const labels = new Map();
1235
+ const add = (value, label) => {
1236
+ const aid = String(value || '').trim();
1237
+ if (!isValidAid(aid))
1238
+ return;
1239
+ let set = labels.get(aid);
1240
+ if (!set) {
1241
+ set = new Set();
1242
+ labels.set(aid, set);
1243
+ }
1244
+ set.add(label);
1245
+ };
1246
+ for (const owner of agentConfig.owners ?? [])
1247
+ add(owner, 'owner');
1248
+ for (const admin of listStaticAgentAdmins(agentConfig.aid))
1249
+ add(admin, 'admin');
1250
+ return [...labels.entries()].map(([primaryId, roles]) => ({
1251
+ primaryId,
1252
+ label: [...roles].join('/'),
1253
+ }));
1254
+ }
1255
+ function bindQrActorToContact(aid, binding, channelType, actorId, channelKey) {
1256
+ if (!binding)
1257
+ return;
1258
+ const normalizedActor = firstNonEmpty(actorId);
1259
+ if (!normalizedActor) {
1260
+ console.log(` ⚠ ${channelType} 扫码流程未返回可绑定的用户 ID,未写入 contact.json`);
1261
+ return;
1262
+ }
1263
+ try {
1264
+ const changed = bindContactAlias(aid, binding.primaryId, channelType, normalizedActor, channelKey);
1265
+ const action = changed ? '已写入' : '已存在';
1266
+ console.log(` ✓ 身份映射${action}: ${channelKey}:${encodeURIComponent(normalizedActor)} -> ${binding.primaryId}`);
1267
+ }
1268
+ catch (error) {
1269
+ const message = error instanceof Error ? error.message : String(error);
1270
+ console.log(` ⚠ 身份映射写入失败: ${message}`);
1271
+ }
1272
+ }
1273
+ async function registerDingtalkContactBind(aid, channelName, binding) {
1274
+ await registerChannelContactBind('dingtalk', '钉钉', aid, channelName, binding);
1275
+ }
1276
+ async function registerWecomContactBind(aid, channelName, binding) {
1277
+ await registerChannelContactBind('wecom', '企微', aid, channelName, binding);
1278
+ }
1279
+ async function registerChannelContactBind(channelType, displayName, aid, channelName, binding) {
1280
+ if (!binding)
1281
+ return;
1282
+ const result = await ipcQuery(resolvePaths().socket, {
1283
+ type: `${channelType}.contact-bind.register`,
1284
+ selfAid: aid,
1285
+ channelName,
1286
+ primaryId: binding.primaryId,
1287
+ }, 3000);
1288
+ if (!result?.ok) {
1289
+ console.log(` ⚠ ${displayName}绑定码注册失败: ${result?.error || 'daemon 不可用'}`);
1290
+ console.log(` 请重新执行 ec init ${channelType} 并再次绑定。`);
1291
+ return;
1292
+ }
1293
+ const expiresAt = new Date(Number(result.expiresAt)).toLocaleString();
1294
+ console.log(` ✓ ${displayName}身份绑定码: ${result.code}`);
1295
+ console.log(` 请在 10 分钟内私聊${displayName}机器人,直接发送这 6 位数字绑定码。有效期至: ${expiresAt}`);
1296
+ }
1297
+ function firstNonEmpty(...values) {
1298
+ for (const value of values) {
1299
+ const text = String(value ?? '').trim();
1300
+ if (text)
1301
+ return text;
1302
+ }
1303
+ return undefined;
1304
+ }
1305
+ /**
1306
+ * Persist the new/overwritten channel and trigger hot-reload.
1307
+ */
1308
+ async function commitChannel(aid, channel, mode) {
1309
+ const result = await agentChannelUpsert({ aid, channel, mode });
1310
+ if (result.ok !== true) {
1311
+ console.error(`❌ ${result.error || 'channel upsert failed'}`);
1312
+ return null;
1313
+ }
1314
+ console.log(`\n✓ 已写入 agents/${aid}/config.json`);
1315
+ console.log(result.reloaded
1316
+ ? ' ✓ 已热重载'
1317
+ : ' ⚠ 服务未运行(或热重载失败),下次 ec start 时生效');
1318
+ return result;
1319
+ }
1320
+ export function getChannelCredentialCollector(type) {
1321
+ switch (type) {
1322
+ case 'feishu':
1323
+ return async () => {
1324
+ const result = await runFeishuQrFlow();
1325
+ if (!result)
1326
+ return null;
1327
+ return { appId: result.appId, appSecret: result.appSecret, enabled: true };
1328
+ };
1329
+ case 'wechat':
1330
+ return async () => {
1331
+ const result = await runWechatQrFlow();
1332
+ if (!result)
1333
+ return null;
1334
+ return { baseUrl: result.baseUrl, token: result.token, enabled: true };
1335
+ };
1336
+ case 'dingtalk':
1337
+ return async () => {
1338
+ const result = await runDingtalkQrFlowSimple();
1339
+ if (!result)
1340
+ return null;
1341
+ return { clientId: result.clientId, clientSecret: result.clientSecret, enabled: true };
1342
+ };
1343
+ case 'qqbot':
1344
+ return async () => {
1345
+ const result = await runQQBotBindFlowSimple();
1346
+ if (!result)
1347
+ return null;
1348
+ return { appId: result.appId, clientSecret: result.clientSecret, enabled: true };
1349
+ };
1350
+ case 'wecom':
1351
+ return async () => {
1352
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1353
+ const ask = (q) => new Promise(r => rl.question(q, r));
1354
+ try {
1355
+ console.log('企业微信 AI Bot 配置\n');
1356
+ console.log('请在企业微信管理后台 → AI Bot 页面获取 Bot ID 和 Secret\n');
1357
+ const botId = (await ask(' Bot ID: ')).trim();
1358
+ if (!botId)
1359
+ return null;
1360
+ const secret = (await ask(' Secret: ')).trim();
1361
+ if (!secret)
1362
+ return null;
1363
+ return { botId, secret, enabled: true };
1364
+ }
1365
+ finally {
1366
+ rl.close();
1367
+ }
1368
+ };
1369
+ default:
1370
+ return null;
1371
+ }
1372
+ }