evolcore 0.0.2 → 0.0.4

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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -16,7 +16,9 @@ import { ipcQuery } from '../ipc.js';
16
16
  import { cmdStart } from './daemon-commands.js';
17
17
  import * as platform from '../utils/cross-platform.js';
18
18
  import { cleanupInstances } from '../utils/instance-registry.js';
19
- import { bindContactAlias } from '../config/contact-book.js';
19
+ import { bindContactAlias, getContactSnapshot } from '../config/contact-book.js';
20
+ import { listStaticAgentAdmins } from '../config/peer-role-resolver.js';
21
+ import { setupRawKeyListener } from './raw-key-input.js';
20
22
  function ask(rl, question) {
21
23
  return new Promise(resolve => rl.question(question, resolve));
22
24
  }
@@ -198,26 +200,12 @@ async function pollBindStatusWithKeyboard(taskId, expiresAt) {
198
200
  let quit = false;
199
201
  let skip = false;
200
202
  const p = resolvePaths();
201
- const setupKeyListener = () => {
202
- if (!process.stdin.isTTY)
203
- return () => { };
204
- process.stdin.setRawMode(true);
205
- process.stdin.resume();
206
- process.stdin.setEncoding('utf8');
207
- const handler = (key) => {
208
- if (key === 'q' || key === '\u0003')
209
- quit = true;
210
- if (key === 's' || key === 'S')
211
- skip = true;
212
- };
213
- process.stdin.on('data', handler);
214
- return () => {
215
- process.stdin.removeListener('data', handler);
216
- process.stdin.setRawMode(false);
217
- // Don't pause stdin - let the next readline/prompt handle it naturally
218
- };
219
- };
220
- const cleanup = setupKeyListener();
203
+ const cleanup = setupRawKeyListener((key) => {
204
+ if (key === 'q' || key === '\u0003')
205
+ quit = true;
206
+ if (key === 's' || key === 'S')
207
+ skip = true;
208
+ });
221
209
  try {
222
210
  while (Date.now() < expiresAt) {
223
211
  if (quit)
@@ -305,26 +293,12 @@ async function runQrRegistrationFlow() {
305
293
  console.log('请用飞书/Lark 扫描上方二维码...\n');
306
294
  console.log('按 q 退出 | 按 s 跳过扫码手动输入 appId/appSecret\n');
307
295
  let userAction = null;
308
- const setupKeyListener = () => {
309
- if (!process.stdin.isTTY)
310
- return () => { };
311
- process.stdin.setRawMode(true);
312
- process.stdin.resume();
313
- process.stdin.setEncoding('utf8');
314
- const handler = (key) => {
315
- if (key === 'q' || key === '\u0003')
316
- userAction = QUIT;
317
- if (key === 's')
318
- userAction = SKIP;
319
- };
320
- process.stdin.on('data', handler);
321
- return () => {
322
- process.stdin.removeListener('data', handler);
323
- process.stdin.setRawMode(false);
324
- // Don't pause stdin - let the next readline/prompt handle it naturally
325
- };
326
- };
327
- const cleanup = setupKeyListener();
296
+ const cleanup = setupRawKeyListener((key) => {
297
+ if (key === 'q' || key === '\u0003')
298
+ userAction = QUIT;
299
+ if (key === 's')
300
+ userAction = SKIP;
301
+ });
328
302
  const startedAt = Date.now();
329
303
  let pollIntervalSeconds = Number(beginResult.interval ?? 5);
330
304
  const expireInSeconds = Number(beginResult.expires_in ?? beginResult.expire_in ?? 600);
@@ -410,6 +384,7 @@ export async function cmdInitFeishu() {
410
384
  let agentConfig = null;
411
385
  let choice = null;
412
386
  let contactBinding = null;
387
+ let operation = 'configure';
413
388
  try {
414
389
  aid = await pickAgentForChannel(rl);
415
390
  if (!aid)
@@ -420,16 +395,25 @@ export async function cmdInitFeishu() {
420
395
  return;
421
396
  }
422
397
  const existing = (agentConfig.channels || []).filter(c => c.type === 'feishu');
423
- choice = await pickInstanceWithinAgent(rl, 'feishu', existing);
398
+ choice = await pickInstanceWithinAgent(rl, 'feishu', existing, { confirmOverwrite: false });
424
399
  if (choice === null)
425
400
  return;
426
- contactBinding = await pickOwnerAdminForChannelBinding(rl, agentConfig, 'feishu');
401
+ if (choice.action === 'overwrite') {
402
+ operation = await pickExistingChannelOperation(rl, '飞书', choice.name);
403
+ if (operation === 'configure' && !await confirmOverwriteChannel(rl, '飞书', choice.name))
404
+ return;
405
+ }
406
+ contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'feishu');
427
407
  if (!contactBinding)
428
408
  return;
429
409
  }
430
410
  finally {
431
411
  rl.close();
432
412
  }
413
+ if (operation === 'rebind-contact') {
414
+ await registerFeishuContactBind(aid, contactBindChannelKey('feishu', aid, choice.name), contactBinding);
415
+ return;
416
+ }
433
417
  console.log('\n正在获取飞书登录二维码...\n');
434
418
  let result;
435
419
  try {
@@ -470,10 +454,18 @@ export async function cmdInitFeishu() {
470
454
  console.log(` Login Open ID: ${result.openId}`);
471
455
  if (result.domain !== 'unknown')
472
456
  console.log(` Domain: ${result.domain}`);
473
- bindQrActorToContact(aid, contactBinding, 'feishu', result.openId, commitResult.channelKey);
457
+ if (result.openId) {
458
+ bindQrActorToContact(aid, contactBinding, 'feishu', result.openId, commitResult.channelKey);
459
+ }
460
+ else if (commitResult.reloaded) {
461
+ await registerFeishuContactBind(aid, commitResult.channelKey, contactBinding);
462
+ }
463
+ else {
464
+ console.log(' ⚠ 服务未运行或热重载失败,无法注册飞书绑定码;请启动服务后重新执行 ec init feishu。');
465
+ }
474
466
  // Show unified init success message
475
467
  const { generateInitSuccessMessage } = await import('../utils/welcome.js');
476
- console.log(generateInitSuccessMessage('feishu', false));
468
+ console.log(generateInitSuccessMessage('feishu', contactBinding !== null));
477
469
  }
478
470
  // ==================== WeChat ====================
479
471
  const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
@@ -571,6 +563,7 @@ export async function cmdInitWechat() {
571
563
  let agentConfig = null;
572
564
  let choice = null;
573
565
  let contactBinding = null;
566
+ let operation = 'configure';
574
567
  try {
575
568
  aid = await pickAgentForChannel(rl);
576
569
  if (!aid)
@@ -581,16 +574,25 @@ export async function cmdInitWechat() {
581
574
  return;
582
575
  }
583
576
  const existing = (agentConfig.channels || []).filter(c => c.type === 'wechat');
584
- choice = await pickInstanceWithinAgent(rl, 'wechat', existing);
577
+ choice = await pickInstanceWithinAgent(rl, 'wechat', existing, { confirmOverwrite: false });
585
578
  if (choice === null)
586
579
  return;
587
- contactBinding = await pickOwnerAdminForChannelBinding(rl, agentConfig, 'wechat');
580
+ if (choice.action === 'overwrite') {
581
+ operation = await pickExistingChannelOperation(rl, '微信', choice.name);
582
+ if (operation === 'configure' && !await confirmOverwriteChannel(rl, '微信', choice.name))
583
+ return;
584
+ }
585
+ contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'wechat');
588
586
  if (!contactBinding)
589
587
  return;
590
588
  }
591
589
  finally {
592
590
  rl.close();
593
591
  }
592
+ if (operation === 'rebind-contact') {
593
+ await registerWechatContactBind(aid, contactBindChannelKey('wechat', aid, choice.name), contactBinding);
594
+ return;
595
+ }
594
596
  console.log('\n正在获取微信登录二维码...\n');
595
597
  const result = await runWechatQrFlow();
596
598
  if (!result) {
@@ -607,10 +609,18 @@ export async function cmdInitWechat() {
607
609
  const commitResult = await commitChannel(aid, channel, choice.action);
608
610
  if (!commitResult)
609
611
  return;
610
- bindQrActorToContact(aid, contactBinding, 'wechat', result.userId, commitResult.channelKey);
612
+ if (firstNonEmpty(result.userId)) {
613
+ bindQrActorToContact(aid, contactBinding, 'wechat', result.userId, commitResult.channelKey);
614
+ }
615
+ else if (commitResult.reloaded) {
616
+ await registerWechatContactBind(aid, commitResult.channelKey, contactBinding);
617
+ }
618
+ else {
619
+ console.log(' ⚠ 服务未运行或热重载失败,无法注册微信绑定码;请启动服务后重新执行 ec init wechat。');
620
+ }
611
621
  // Show unified init success message
612
622
  const { generateInitSuccessMessage } = await import('../utils/welcome.js');
613
- console.log(generateInitSuccessMessage('wechat', false));
623
+ console.log(generateInitSuccessMessage('wechat', contactBinding !== null));
614
624
  }
615
625
  // ==================== AUN ====================
616
626
  export async function cmdInitAun() {
@@ -752,26 +762,12 @@ async function runDingtalkQrFlow() {
752
762
  console.log('提示: 扫码页面标注 "OpenClaw" 是钉钉生态接入桥,可放心使用。\n');
753
763
  console.log('按 q 退出 | 按 s 跳过扫码手动输入\n');
754
764
  let userAction = null;
755
- const setupKeyListener = () => {
756
- if (!process.stdin.isTTY)
757
- return () => { };
758
- process.stdin.setRawMode(true);
759
- process.stdin.resume();
760
- process.stdin.setEncoding('utf8');
761
- const handler = (key) => {
762
- if (key === 'q' || key === '\u0003')
763
- userAction = QUIT;
764
- if (key === 's')
765
- userAction = SKIP;
766
- };
767
- process.stdin.on('data', handler);
768
- return () => {
769
- process.stdin.removeListener('data', handler);
770
- process.stdin.setRawMode(false);
771
- // Don't pause stdin - let the next readline/prompt handle it naturally
772
- };
773
- };
774
- const cleanup = setupKeyListener();
765
+ const cleanup = setupRawKeyListener((key) => {
766
+ if (key === 'q' || key === '\u0003')
767
+ userAction = QUIT;
768
+ if (key === 's')
769
+ userAction = SKIP;
770
+ });
775
771
  const pollInterval = Math.max(Number(beginData.interval ?? 3), 2);
776
772
  const expiresIn = Number(beginData.expires_in ?? 7200);
777
773
  const startedAt = Date.now();
@@ -828,6 +824,7 @@ export async function cmdInitDingtalk() {
828
824
  let agentConfig = null;
829
825
  let choice = null;
830
826
  let contactBinding = null;
827
+ let operation = 'configure';
831
828
  try {
832
829
  aid = await pickAgentForChannel(rl);
833
830
  if (!aid)
@@ -838,16 +835,25 @@ export async function cmdInitDingtalk() {
838
835
  return;
839
836
  }
840
837
  const existing = (agentConfig.channels || []).filter(c => c.type === 'dingtalk');
841
- choice = await pickInstanceWithinAgent(rl, 'dingtalk', existing);
838
+ choice = await pickInstanceWithinAgent(rl, 'dingtalk', existing, { confirmOverwrite: false });
842
839
  if (choice === null)
843
840
  return;
844
- contactBinding = await pickOwnerAdminForChannelBinding(rl, agentConfig, 'dingtalk');
841
+ if (choice.action === 'overwrite') {
842
+ operation = await pickExistingChannelOperation(rl, '钉钉', choice.name);
843
+ if (operation === 'configure' && !await confirmOverwriteChannel(rl, '钉钉', choice.name))
844
+ return;
845
+ }
846
+ contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'dingtalk');
845
847
  if (!contactBinding)
846
848
  return;
847
849
  }
848
850
  finally {
849
851
  rl.close();
850
852
  }
853
+ if (operation === 'rebind-contact') {
854
+ await registerDingtalkContactBind(aid, contactBindChannelKey('dingtalk', aid, choice.name), contactBinding);
855
+ return;
856
+ }
851
857
  console.log('\n正在获取钉钉登录二维码...\n');
852
858
  let result;
853
859
  try {
@@ -904,7 +910,7 @@ export async function cmdInitDingtalk() {
904
910
  console.log(' ⚠ 服务未运行或热重载失败,无法注册钉钉绑定码;请启动服务后重新执行 ec init dingtalk 并再次扫码绑定。');
905
911
  }
906
912
  const { generateInitSuccessMessage } = await import('../utils/welcome.js');
907
- console.log(generateInitSuccessMessage('dingtalk', false));
913
+ console.log(generateInitSuccessMessage('dingtalk', contactBinding !== null));
908
914
  }
909
915
  // ==================== QQBot ====================
910
916
  const QQBOT_PORTAL_HOST = 'q.qq.com';
@@ -968,26 +974,12 @@ async function runQQBotBindFlow() {
968
974
  console.log('请用 QQ 扫描上方二维码绑定机器人...\n');
969
975
  console.log('按 q 退出 | 按 s 跳过扫码手动输入\n');
970
976
  let userAction = null;
971
- const setupKeyListener = () => {
972
- if (!process.stdin.isTTY)
973
- return () => { };
974
- process.stdin.setRawMode(true);
975
- process.stdin.resume();
976
- process.stdin.setEncoding('utf8');
977
- const handler = (key) => {
978
- if (key === 'q' || key === '\u0003')
979
- userAction = QUIT;
980
- if (key === 's')
981
- userAction = SKIP;
982
- };
983
- process.stdin.on('data', handler);
984
- return () => {
985
- process.stdin.removeListener('data', handler);
986
- process.stdin.setRawMode(false);
987
- // Don't pause stdin - let the next readline/prompt handle it naturally
988
- };
989
- };
990
- const cleanup = setupKeyListener();
977
+ const cleanup = setupRawKeyListener((key) => {
978
+ if (key === 'q' || key === '\u0003')
979
+ userAction = QUIT;
980
+ if (key === 's')
981
+ userAction = SKIP;
982
+ });
991
983
  const startedAt = Date.now();
992
984
  try {
993
985
  // Step 3: Poll for bind result
@@ -1051,6 +1043,7 @@ export async function cmdInitQQBot() {
1051
1043
  let agentConfig = null;
1052
1044
  let choice = null;
1053
1045
  let contactBinding = null;
1046
+ let operation = 'configure';
1054
1047
  try {
1055
1048
  aid = await pickAgentForChannel(rl);
1056
1049
  if (!aid)
@@ -1061,16 +1054,25 @@ export async function cmdInitQQBot() {
1061
1054
  return;
1062
1055
  }
1063
1056
  const existing = (agentConfig.channels || []).filter(c => c.type === 'qqbot');
1064
- choice = await pickInstanceWithinAgent(rl, 'qqbot', existing);
1057
+ choice = await pickInstanceWithinAgent(rl, 'qqbot', existing, { confirmOverwrite: false });
1065
1058
  if (choice === null)
1066
1059
  return;
1067
- contactBinding = await pickOwnerAdminForChannelBinding(rl, agentConfig, 'qqbot');
1060
+ if (choice.action === 'overwrite') {
1061
+ operation = await pickExistingChannelOperation(rl, 'QQ', choice.name);
1062
+ if (operation === 'configure' && !await confirmOverwriteChannel(rl, 'QQ', choice.name))
1063
+ return;
1064
+ }
1065
+ contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'qqbot');
1068
1066
  if (!contactBinding)
1069
1067
  return;
1070
1068
  }
1071
1069
  finally {
1072
1070
  rl.close();
1073
1071
  }
1072
+ if (operation === 'rebind-contact') {
1073
+ await registerQQBotContactBind(aid, contactBindChannelKey('qqbot', aid, choice.name), contactBinding);
1074
+ return;
1075
+ }
1074
1076
  console.log('\n正在创建 QQ 机器人绑定任务...\n');
1075
1077
  let result;
1076
1078
  try {
@@ -1120,9 +1122,17 @@ export async function cmdInitQQBot() {
1120
1122
  if (!commitResult)
1121
1123
  return;
1122
1124
  console.log(` App ID: ${result.appId}`);
1123
- bindQrActorToContact(aid, contactBinding, 'qqbot', result.userOpenId, commitResult.channelKey);
1125
+ if (firstNonEmpty(result.userOpenId)) {
1126
+ bindQrActorToContact(aid, contactBinding, 'qqbot', result.userOpenId, commitResult.channelKey);
1127
+ }
1128
+ else if (commitResult.reloaded) {
1129
+ await registerQQBotContactBind(aid, commitResult.channelKey, contactBinding);
1130
+ }
1131
+ else {
1132
+ console.log(' ⚠ 服务未运行或热重载失败,无法注册 QQ 绑定码;请启动服务后重新执行 ec init qqbot。');
1133
+ }
1124
1134
  const { generateInitSuccessMessage } = await import('../utils/welcome.js');
1125
- console.log(generateInitSuccessMessage('qqbot', false));
1135
+ console.log(generateInitSuccessMessage('qqbot', contactBinding !== null));
1126
1136
  }
1127
1137
  // ==================== WeCom (企业微信) ====================
1128
1138
  export async function cmdInitWecom() {
@@ -1130,6 +1140,8 @@ export async function cmdInitWecom() {
1130
1140
  let aid = null;
1131
1141
  let agentConfig = null;
1132
1142
  let choice = null;
1143
+ let contactBinding = null;
1144
+ let operation = 'configure';
1133
1145
  let botId = '';
1134
1146
  let secret = '';
1135
1147
  try {
@@ -1142,25 +1154,71 @@ export async function cmdInitWecom() {
1142
1154
  return;
1143
1155
  }
1144
1156
  const existing = (agentConfig.channels || []).filter(c => c.type === 'wecom');
1145
- choice = await pickInstanceWithinAgent(rl, 'wecom', existing);
1157
+ choice = await pickInstanceWithinAgent(rl, 'wecom', existing, { confirmOverwrite: false });
1146
1158
  if (choice === null)
1147
1159
  return;
1148
- console.log('\n企业微信 AI Bot 配置');
1149
- console.log('请在企业微信管理后台 AI Bot 页面获取 Bot ID 和 Secret\n');
1150
- while (!botId) {
1151
- botId = (await ask(rl, ' Bot ID: ')).trim();
1152
- if (!botId)
1153
- console.log(' ⚠ 不能为空');
1160
+ if (choice.action === 'overwrite') {
1161
+ operation = await pickExistingChannelOperation(rl, '企微', choice.name);
1162
+ if (operation === 'configure' && !await confirmOverwriteChannel(rl, '企微', choice.name))
1163
+ return;
1154
1164
  }
1155
- while (!secret) {
1156
- secret = (await ask(rl, ' Secret: ')).trim();
1157
- if (!secret)
1158
- console.log(' ⚠ 不能为空');
1165
+ contactBinding = await pickContactForChannelBinding(rl, agentConfig, 'wecom');
1166
+ if (!contactBinding)
1167
+ return;
1168
+ if (operation !== 'rebind-contact') {
1169
+ console.log('\n企业微信 AI Bot 配置');
1170
+ const method = (await ask(rl, ' 接入方式:1. 扫码创建(推荐) 2. 手工输入 [1]: ')).trim() || '1';
1171
+ if (method === '1') {
1172
+ try {
1173
+ const { createWecomQrSession, pollWecomQrCredentials } = await import('../channels/wecom.js');
1174
+ const session = await createWecomQrSession();
1175
+ const qrterm = await import('qrcode-terminal');
1176
+ console.log('\n请使用企业微信扫描以下二维码:\n');
1177
+ await new Promise(resolve => {
1178
+ qrterm.default.generate(session.authUrl, { small: true }, (qr) => {
1179
+ console.log(qr);
1180
+ resolve();
1181
+ });
1182
+ });
1183
+ console.log(`也可在浏览器打开:${session.browserUrl}`);
1184
+ console.log('等待扫码确认,二维码 5 分钟内有效...\n');
1185
+ const credentials = await pollWecomQrCredentials(session.scode, {
1186
+ onStatus: status => {
1187
+ if (status !== 'waiting')
1188
+ console.log(` 扫码状态: ${status}`);
1189
+ },
1190
+ });
1191
+ botId = credentials.botId;
1192
+ secret = credentials.secret;
1193
+ console.log(' ✓ 已取得 Bot ID 和 Secret');
1194
+ }
1195
+ catch (error) {
1196
+ console.log(` ⚠ 扫码接入失败: ${error instanceof Error ? error.message : String(error)}`);
1197
+ console.log(' 将切换到手工输入模式。\n');
1198
+ }
1199
+ }
1200
+ if (!botId || !secret) {
1201
+ console.log('请在企业微信管理后台 → AI Bot 页面获取 Bot ID 和 Secret\n');
1202
+ }
1203
+ while (!botId) {
1204
+ botId = (await ask(rl, ' Bot ID: ')).trim();
1205
+ if (!botId)
1206
+ console.log(' ⚠ 不能为空');
1207
+ }
1208
+ while (!secret) {
1209
+ secret = (await ask(rl, ' Secret: ')).trim();
1210
+ if (!secret)
1211
+ console.log(' ⚠ 不能为空');
1212
+ }
1159
1213
  }
1160
1214
  }
1161
1215
  finally {
1162
1216
  rl.close();
1163
1217
  }
1218
+ if (operation === 'rebind-contact') {
1219
+ await registerWecomContactBind(aid, contactBindChannelKey('wecom', aid, choice.name), contactBinding);
1220
+ return;
1221
+ }
1164
1222
  const channel = {
1165
1223
  type: 'wecom',
1166
1224
  name: choice.name,
@@ -1168,10 +1226,18 @@ export async function cmdInitWecom() {
1168
1226
  botId,
1169
1227
  secret,
1170
1228
  };
1171
- await commitChannel(aid, channel, choice.action);
1229
+ const commitResult = await commitChannel(aid, channel, choice.action);
1230
+ if (!commitResult)
1231
+ return;
1172
1232
  console.log(` Bot ID: ${botId}`);
1233
+ if (commitResult.reloaded) {
1234
+ await registerWecomContactBind(aid, commitResult.channelKey, contactBinding);
1235
+ }
1236
+ else {
1237
+ console.log(' ⚠ 服务未运行或热重载失败,无法注册企微绑定码;请启动服务后重新执行 ec init wecom。');
1238
+ }
1173
1239
  const { generateInitSuccessMessage } = await import('../utils/welcome.js');
1174
- console.log(generateInitSuccessMessage('wecom', false));
1240
+ console.log(generateInitSuccessMessage('wecom', contactBinding !== null));
1175
1241
  }
1176
1242
  // ==================== Shared helpers for per-agent init <channel> ====================
1177
1243
  /**
@@ -1211,24 +1277,42 @@ async function pickAgentForChannel(rl) {
1211
1277
  *
1212
1278
  * If existing.length === 0 → returns { action:'add', name:'main' } directly.
1213
1279
  */
1214
- async function pickInstanceWithinAgent(rl, channelType, existing) {
1280
+ async function pickInstanceWithinAgent(rl, channelType, existing, options) {
1215
1281
  if (existing.length === 0) {
1216
1282
  return { action: 'add', name: 'main' };
1217
1283
  }
1218
1284
  const view = existing.map((c, i) => ({ ...c, name: c.name, originalIndex: i }));
1219
- return await selectInstance(rl, channelType, view);
1285
+ return await selectInstance(rl, channelType, view, options);
1220
1286
  }
1221
- async function pickOwnerAdminForChannelBinding(rl, agentConfig, channelType) {
1222
- const candidates = ownerAdminContactCandidates(agentConfig);
1223
- if (candidates.length === 0) {
1224
- console.log(`\n❌ agent ${agentConfig.aid} 尚未配置 owners/admins,无法为 ${channelType} 扫码账号建立身份映射。`);
1225
- console.log(' 请先通过 AUN 绑定 agent owner,或在 agent config.json 中配置 owners/admins 后重试。');
1226
- return null;
1287
+ async function pickExistingChannelOperation(rl, displayName, channelName) {
1288
+ console.log(`\n请选择对${displayName}实例 ${channelName} 的操作:`);
1289
+ console.log(' a. 覆盖机器人配置(重新扫码或输入 App ID / App Secret)');
1290
+ console.log(' b. 重新绑定联系人(保留机器人配置,生成一次性 /bind 验证码)');
1291
+ while (true) {
1292
+ const choice = (await ask(rl, '请选择 [a/b]: ')).trim().toLowerCase();
1293
+ if (choice === 'a')
1294
+ return 'configure';
1295
+ if (choice === 'b')
1296
+ return 'rebind-contact';
1297
+ console.log('无效选择,请输入 a 或 b');
1227
1298
  }
1228
- console.log(`\n请选择 ${channelType} 扫码账号要绑定到的 owner/admin AID:`);
1299
+ }
1300
+ async function confirmOverwriteChannel(rl, displayName, channelName) {
1301
+ const confirm = (await ask(rl, `⚠️ 即将覆盖${displayName}实例 ${channelName} 的机器人配置,确认?(y/N) `)).trim().toLowerCase();
1302
+ if (confirm === 'y' || confirm === 'yes')
1303
+ return true;
1304
+ console.log('已取消');
1305
+ return false;
1306
+ }
1307
+ async function pickContactForChannelBinding(rl, agentConfig, channelType) {
1308
+ const candidates = preferredContactBindingCandidates(agentConfig);
1309
+ const contacts = getContactSnapshot(agentConfig.aid).contacts;
1310
+ console.log(`\n请选择 ${channelType} 账号要绑定到的联系人 AID:`);
1229
1311
  candidates.forEach((candidate, index) => {
1230
1312
  console.log(` ${index + 1}. ${candidate.primaryId} (${candidate.label})`);
1231
1313
  });
1314
+ const manualChoice = candidates.length + 1;
1315
+ console.log(` ${manualChoice}. 手工输入已有联系人 AID`);
1232
1316
  const defaultIndex = candidates.length === 1 ? 1 : null;
1233
1317
  const prompt = defaultIndex ? `请选择 [${defaultIndex}]: ` : '请选择: ';
1234
1318
  while (true) {
@@ -1237,14 +1321,26 @@ async function pickOwnerAdminForChannelBinding(rl, agentConfig, channelType) {
1237
1321
  if (Number.isInteger(selected) && selected >= 1 && selected <= candidates.length) {
1238
1322
  return candidates[selected - 1];
1239
1323
  }
1240
- console.log(`无效选择,请输入 1-${candidates.length}`);
1324
+ if (selected === manualChoice) {
1325
+ const primaryId = (await ask(rl, '请输入要绑定的联系人 AID: ')).trim();
1326
+ if (isValidAid(primaryId) && contacts.has(primaryId))
1327
+ return { primaryId, label: 'contact' };
1328
+ if (isValidAid(primaryId)) {
1329
+ console.log('该 AID 不是当前 Agent 的联系人,请先添加联系人或将其设为 Owner。');
1330
+ continue;
1331
+ }
1332
+ console.log('AID 格式无效,请重新输入。');
1333
+ continue;
1334
+ }
1335
+ console.log(`无效选择,请输入 1-${manualChoice}`);
1241
1336
  }
1242
1337
  }
1243
- function ownerAdminContactCandidates(agentConfig) {
1338
+ function preferredContactBindingCandidates(agentConfig) {
1339
+ const contacts = getContactSnapshot(agentConfig.aid).contacts;
1244
1340
  const labels = new Map();
1245
1341
  const add = (value, label) => {
1246
1342
  const aid = String(value || '').trim();
1247
- if (!isValidAid(aid))
1343
+ if (!isValidAid(aid) || !contacts.has(aid))
1248
1344
  return;
1249
1345
  let set = labels.get(aid);
1250
1346
  if (!set) {
@@ -1255,7 +1351,7 @@ function ownerAdminContactCandidates(agentConfig) {
1255
1351
  };
1256
1352
  for (const owner of agentConfig.owners ?? [])
1257
1353
  add(owner, 'owner');
1258
- for (const admin of agentConfig.admins ?? [])
1354
+ for (const admin of listStaticAgentAdmins(agentConfig.aid))
1259
1355
  add(admin, 'admin');
1260
1356
  return [...labels.entries()].map(([primaryId, roles]) => ({
1261
1357
  primaryId,
@@ -1273,7 +1369,7 @@ function bindQrActorToContact(aid, binding, channelType, actorId, channelKey) {
1273
1369
  try {
1274
1370
  const changed = bindContactAlias(aid, binding.primaryId, channelType, normalizedActor, channelKey);
1275
1371
  const action = changed ? '已写入' : '已存在';
1276
- console.log(` ✓ 身份映射${action}: ${channelType}:${normalizedActor} -> ${binding.primaryId}`);
1372
+ console.log(` ✓ 身份映射${action}: ${channelKey}:${encodeURIComponent(normalizedActor)} -> ${binding.primaryId}`);
1277
1373
  }
1278
1374
  catch (error) {
1279
1375
  const message = error instanceof Error ? error.message : String(error);
@@ -1281,23 +1377,40 @@ function bindQrActorToContact(aid, binding, channelType, actorId, channelKey) {
1281
1377
  }
1282
1378
  }
1283
1379
  async function registerDingtalkContactBind(aid, channelName, binding) {
1380
+ await registerChannelContactBind('dingtalk', '钉钉', aid, channelName, binding);
1381
+ }
1382
+ function contactBindChannelKey(channelType, aid, channelName) {
1383
+ return `${channelType}#${aid}#${channelName}`;
1384
+ }
1385
+ async function registerFeishuContactBind(aid, channelName, binding) {
1386
+ await registerChannelContactBind('feishu', '飞书', aid, channelName, binding);
1387
+ }
1388
+ async function registerWechatContactBind(aid, channelName, binding) {
1389
+ await registerChannelContactBind('wechat', '微信', aid, channelName, binding);
1390
+ }
1391
+ async function registerQQBotContactBind(aid, channelName, binding) {
1392
+ await registerChannelContactBind('qqbot', 'QQ', aid, channelName, binding);
1393
+ }
1394
+ async function registerWecomContactBind(aid, channelName, binding) {
1395
+ await registerChannelContactBind('wecom', '企微', aid, channelName, binding);
1396
+ }
1397
+ async function registerChannelContactBind(channelType, displayName, aid, channelName, binding) {
1284
1398
  if (!binding)
1285
1399
  return;
1286
1400
  const result = await ipcQuery(resolvePaths().socket, {
1287
- type: 'dingtalk.contact-bind.register',
1401
+ type: `${channelType}.contact-bind.register`,
1288
1402
  selfAid: aid,
1289
1403
  channelName,
1290
1404
  primaryId: binding.primaryId,
1291
1405
  }, 3000);
1292
1406
  if (!result?.ok) {
1293
- console.log(` ⚠ 钉钉绑定码注册失败: ${result?.error || 'daemon 不可用'}`);
1294
- console.log(' 请重新执行 ec init dingtalk 并再次扫码绑定。');
1407
+ console.log(` ⚠ ${displayName}绑定码注册失败: ${result?.error || 'daemon 不可用'}`);
1408
+ console.log(` 请重新执行 ec init ${channelType} 并再次绑定。`);
1295
1409
  return;
1296
1410
  }
1297
1411
  const expiresAt = new Date(Number(result.expiresAt)).toLocaleString();
1298
- console.log(` ✓ 钉钉身份绑定码: ${result.code}`);
1299
- console.log(` 请在 10 分钟内私聊钉钉机器人,直接发送这 6 位数字绑定码。有效期至: ${expiresAt}`);
1300
- console.log(' 非纯数字或位数不对只会提醒格式,不计入错误次数;6 位数字输错最多 5 次。');
1412
+ console.log(` ✓ ${displayName}身份绑定码: ${result.code}`);
1413
+ console.log(` 请在 10 分钟内私聊${displayName}机器人,发送:/bind ${result.code}。有效期至: ${expiresAt}`);
1301
1414
  }
1302
1415
  function firstNonEmpty(...values) {
1303
1416
  for (const value of values) {