evolcore 0.0.3 → 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 (143) hide show
  1. package/CHANGELOG.md +64 -0
  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 +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  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/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/handoff-command.js +2 -1
  22. package/dist/cli/init-channel.js +185 -67
  23. package/dist/cli/init.js +7 -5
  24. package/dist/cli/trigger-command.js +92 -11
  25. package/dist/config/access-policy-domain.js +38 -0
  26. package/dist/config/access-policy.js +134 -0
  27. package/dist/config/builtin-role-templates.js +27 -14
  28. package/dist/config/builtin-roles.js +25 -6
  29. package/dist/config/config-field-policy.js +17 -5
  30. package/dist/config/config-manager.js +198 -22
  31. package/dist/config/config-operation-service.js +35 -38
  32. package/dist/config/contact-bind-code.js +274 -0
  33. package/dist/config/contact-book-store.js +173 -5
  34. package/dist/config/contact-book.js +32 -0
  35. package/dist/config/contact-operation-service.js +9 -3
  36. package/dist/config/contact-request-service.js +331 -0
  37. package/dist/config/peer-role-resolver.js +3 -1
  38. package/dist/config/schema-registry.js +49 -24
  39. package/dist/config-store.js +14 -2
  40. package/dist/core/auth/agent-delegation.js +105 -11
  41. package/dist/core/auth/authorization-audit.js +6 -0
  42. package/dist/core/auth/operation-authorizer.js +8 -0
  43. package/dist/core/auth/operation-catalog.js +51 -3
  44. package/dist/core/auth/trigger-authorization.js +15 -0
  45. package/dist/core/bootstrap-service.js +2 -9
  46. package/dist/core/channel-loader.js +6 -2
  47. package/dist/core/command/command-handler.js +10 -13
  48. package/dist/core/command/connect-menu.js +249 -24
  49. package/dist/core/command/evol-menu-version-gate.js +38 -0
  50. package/dist/core/command/group-menu.js +421 -0
  51. package/dist/core/command/menu-handler.js +269 -85
  52. package/dist/core/command/menu-protocol.js +8 -2
  53. package/dist/core/command/menu-token-store.js +102 -0
  54. package/dist/core/command/slash-gate.js +1 -1
  55. package/dist/core/command/slash-handler.js +158 -31
  56. package/dist/core/daemon-file-cache.js +28 -0
  57. package/dist/core/event-catalog.js +29 -0
  58. package/dist/core/evolagent-registry.js +4 -39
  59. package/dist/core/handoff/runtime.js +162 -37
  60. package/dist/core/handoff/store.js +46 -2
  61. package/dist/core/handoff/types.js +1 -0
  62. package/dist/core/inference/text-inference.js +16 -74
  63. package/dist/core/message/file-markers.js +7 -0
  64. package/dist/core/message/im-renderer.js +18 -14
  65. package/dist/core/message/inbound-admission.js +124 -0
  66. package/dist/core/message/message-bridge.js +616 -74
  67. package/dist/core/message/message-log.js +1 -0
  68. package/dist/core/message/message-queue.js +185 -10
  69. package/dist/core/message/peer-mode.js +7 -8
  70. package/dist/core/message/response-engine.js +381 -79
  71. package/dist/core/permission/approval-gateway.js +17 -10
  72. package/dist/core/permission/ec-command-parser.js +101 -46
  73. package/dist/core/permission/tool-policy.js +69 -24
  74. package/dist/core/protected-paths.js +36 -11
  75. package/dist/core/session/session-manager.js +25 -3
  76. package/dist/core/session/session-mapper.js +2 -0
  77. package/dist/core/session/session-renew.js +125 -69
  78. package/dist/core/session/session-turn-coordinator.js +5 -1
  79. package/dist/eck/kit-renderer.js +12 -1
  80. package/dist/eck/message-renderer.js +79 -1
  81. package/dist/index.js +224 -89
  82. package/dist/ipc.js +81 -4
  83. package/dist/paths.js +3 -0
  84. package/dist/response-system/config-resolver.js +29 -0
  85. package/dist/response-system/coordinator.js +8 -32
  86. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  87. package/dist/response-system/index.js +1 -0
  88. package/dist/response-system/modes/single-session/index.js +7 -4
  89. package/dist/trigger/parser.js +55 -15
  90. package/dist/trigger/patch.js +4 -1
  91. package/dist/trigger/scheduler.js +441 -39
  92. package/dist/utils/error-dict.json +7 -0
  93. package/dist/utils/evolcore-version.js +21 -0
  94. package/dist/utils/logger.js +2 -2
  95. package/dist/utils/stable-semver.js +21 -0
  96. package/dist/utils/stats.js +33 -9
  97. package/kits/docs/channels/aun.md +4 -13
  98. package/kits/docs/evolcore/INDEX.md +1 -1
  99. package/kits/docs/evolcore/config.md +47 -2
  100. package/kits/docs/evolcore/contact.md +7 -3
  101. package/kits/docs/evolcore/group-rules.md +46 -4
  102. package/kits/docs/evolcore/group.md +4 -4
  103. package/kits/docs/evolcore/msg.md +5 -5
  104. package/kits/docs/evolcore/trigger.md +25 -8
  105. package/kits/docs/path-registry.md +36 -17
  106. package/kits/eck_message_manifest.json +12 -1
  107. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  108. package/kits/rules/01-overview.md +17 -7
  109. package/kits/rules/02-navigation.md +38 -18
  110. package/kits/rules/03-identity.md +28 -24
  111. package/kits/rules/04-relation.md +44 -28
  112. package/kits/rules/05-venue.md +31 -15
  113. package/kits/rules/06-channel.md +11 -7
  114. package/kits/schemas/_meta.json +18 -7
  115. package/kits/schemas/agent-config.schema.7.json +303 -0
  116. package/kits/schemas/agent-config.schema.8.json +304 -0
  117. package/kits/schemas/agent-config.schema.9.json +374 -0
  118. package/kits/schemas/contact-book.schema.3.json +67 -0
  119. package/kits/schemas/daemon.schema.1.json +3 -2
  120. package/kits/schemas/daemon.schema.2.json +101 -0
  121. package/kits/schemas/daemon.schema.3.json +123 -0
  122. package/kits/schemas/defaults.schema.2.json +85 -0
  123. package/kits/schemas/defaults.schema.3.json +73 -0
  124. package/kits/schemas/relation-config.schema.6.json +46 -0
  125. package/kits/schemas/relation-config.schema.7.json +59 -0
  126. package/kits/schemas/single-session.schema.2.json +57 -0
  127. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  128. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  129. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  130. package/kits/templates/roles/admin.json +46 -0
  131. package/kits/templates/roles/member.json +4 -0
  132. package/kits/templates/roles/visitor.json +4 -0
  133. package/kits/templates/system-fragments/channel.md +12 -2
  134. package/kits/templates/system-fragments/identity.md +3 -1
  135. package/kits/templates/system-fragments/relation.md +1 -1
  136. package/kits/templates/system-fragments/session.md +6 -2
  137. package/package.json +4 -2
  138. package/MIGRATION-0.5.0.md +0 -378
  139. package/ROLE_ACCESS_CONTROL.md +0 -174
  140. package/dist/channels/contact-bind-code.js +0 -134
  141. package/dist/channels/wecom-card.js +0 -101
  142. package/dist/channels/wecom-onboarding.js +0 -82
  143. package/dist/channels/wecom-state.js +0 -191
package/dist/index.js CHANGED
@@ -17,9 +17,12 @@ import { ConfigTarget, initConfigManager, onConfigWrite } from './config/config-
17
17
  import { ensureRoleConfigV4OnStartup } from './config/role-config-v4-startup.js';
18
18
  import { ensureRoleConfigV5OnStartup } from './config/role-config-v5-startup.js';
19
19
  import { ensureContactBookV2OnStartup } from './config/contact-book-v2-startup.js';
20
+ import { resolvePeerDisplayLabel } from './config/contact-book.js';
21
+ import { expirePendingContactRequests } from './config/contact-request-service.js';
20
22
  import { shouldFailFastForMissingOwners } from './config/owner-policy.js';
21
23
  import { isManagementRole } from './config/builtin-roles.js';
22
24
  import { checkRoleAccess, getFirstStaticAgentOwner, resolvePeerRoleDetail, roleToSessionIdentity } from './config/peer-role-resolver.js';
25
+ import { isCrossAgentTriggerOperationAllowed, isCrossAgentTriggerReadOperation } from './core/auth/trigger-authorization.js';
23
26
  import { ensureStartupConfigSnapshot, isConfigSnapshotsEnabled, retentionCleanup, readCurrent, readWVersion, diffWorkingVsVersion, paramDiff, incrementSuccessCount } from './config/snapshot.js';
24
27
  import { appendBootLog, selfDiagnose } from './config/boot-log.js';
25
28
  import { CONFIG_SCHEMA_VERSION } from './types.js';
@@ -27,19 +30,22 @@ import { SessionManager } from './core/session/session-manager.js';
27
30
  import { ClaudeAgentPlugin } from './agents/claude-runner.js';
28
31
  import { CodexAgentPlugin } from './agents/codex-runner.js';
29
32
  import { GeminiAgentPlugin } from './agents/gemini-runner.js';
30
- import { FeishuChannelPlugin } from './channels/feishu.js';
31
- import { WechatChannelPlugin } from './channels/wechat.js';
33
+ import { FeishuChannelPlugin, registerPendingFeishuContactBind } from './channels/feishu.js';
34
+ import { WechatChannelPlugin, registerPendingWechatContactBind } from './channels/wechat.js';
32
35
  import { AUN_HANDOFF_MARKER_FIELD, AUNChannel, AUNChannelPlugin } from './channels/aun.js';
36
+ import { assertAunEncryptionSeedPolicy } from './aun/aid/encryption-seed-policy.js';
33
37
  import { startServiceProxy } from './aun/service-proxy.js';
34
38
  import { BindService } from './utils/aid-bind.js';
35
39
  import { DingtalkChannelPlugin, registerPendingDingtalkContactBind } from './channels/dingtalk.js';
36
- import { QQBotChannelPlugin } from './channels/qqbot.js';
40
+ import { QQBotChannelPlugin, registerPendingQQBotContactBind } from './channels/qqbot.js';
37
41
  import { WecomChannelPlugin, registerPendingWecomContactBind } from './channels/wecom.js';
38
42
  import { buildEnvelope } from './core/message/message-utils.js';
39
43
  import { ResponseEngine } from './core/message/response-engine.js';
40
44
  import { MessageQueue } from './core/message/message-queue.js';
41
45
  import { MessageBridge } from './core/message/message-bridge.js';
42
46
  import { MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, parseMenuControl, validateMenuRequest } from './core/command/menu-protocol.js';
47
+ import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata } from './core/command/evol-menu-version-gate.js';
48
+ import { readInstalledEvolcoreVersion } from './utils/evolcore-version.js';
43
49
  import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
44
50
  import { HandoffRuntime } from './core/handoff/runtime.js';
45
51
  import { BootstrapService } from './core/bootstrap-service.js';
@@ -345,7 +351,7 @@ function removeLegacyAgentUpgradeCheck(manager) {
345
351
  export async function sendSystemPayload(adapter, envelope, payload) {
346
352
  await adapter.send(envelope, payload);
347
353
  }
348
- async function runBindBootstrapDaemon(daemonCfg, defaults) {
354
+ async function runBindBootstrapDaemon(daemonCfg) {
349
355
  logger.warn('[bind-bootstrap] starting control AID + IPC only');
350
356
  const bindService = new BindService({
351
357
  receiverAid: daemonCfg.aid,
@@ -359,8 +365,8 @@ async function runBindBootstrapDaemon(daemonCfg, defaults) {
359
365
  agentName: daemonCfg.aid,
360
366
  channelName: 'control',
361
367
  pureIdentity: true,
362
- aunTrace: daemonCfg.debug?.aunTrace ?? defaults.debug?.aunTrace,
363
- aunSdkLog: daemonCfg.debug?.aunSdkLog ?? defaults.debug?.aunSdkLog,
368
+ aunTrace: daemonCfg.debug?.aunTrace,
369
+ aunSdkLog: daemonCfg.debug?.aunSdkLog,
364
370
  });
365
371
  try {
366
372
  await controlChannel.connect();
@@ -417,13 +423,7 @@ async function runBindBootstrapDaemon(daemonCfg, defaults) {
417
423
  process.on('exit', () => removeAll());
418
424
  }
419
425
  export function readEvolcoreVersion() {
420
- try {
421
- const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
422
- return pkg.version || 'unknown';
423
- }
424
- catch {
425
- return 'unknown';
426
- }
426
+ return readInstalledEvolcoreVersion();
427
427
  }
428
428
  function detectAvailableBaseagentsForBind() {
429
429
  const out = [];
@@ -583,6 +583,18 @@ async function main() {
583
583
  logger.info(`EvolCore v${readEvolcoreVersion()} starting... (fastaun v${readFastaunVersion()})`);
584
584
  // 确保数据目录存在
585
585
  ensureDataDirs();
586
+ // AUN keystore seed is an EvolCore application constant. A custom value can
587
+ // make existing identities unreadable, so fail before migrations, snapshots,
588
+ // instance registration, and service startup.
589
+ try {
590
+ assertAunEncryptionSeedPolicy(loadDaemonConfig().aun?.encryptionSeed, process.env.AUN_ENCRYPTION_SEED);
591
+ }
592
+ catch (error) {
593
+ const msg = `❌ ${error instanceof Error ? error.message : String(error)}`;
594
+ logger.error(msg);
595
+ console.error(msg);
596
+ process.exit(1);
597
+ }
586
598
  // .env 文件已在模块顶部加载,此处不再重复加载
587
599
  // ── 单实例保护(pre-check + post-write self-check)──
588
600
  // pre-check:发现已有活 main 直接退出,避免起任何副作用
@@ -680,6 +692,9 @@ async function main() {
680
692
  }
681
693
  }
682
694
  const daemonCfg = loadDaemonConfig();
695
+ if (daemonCfg.debug?.logLevel) {
696
+ setLogLevel(daemonCfg.debug.logLevel);
697
+ }
683
698
  if (initializeEckSnapshotsConfig(daemonCfg)) {
684
699
  logger.warn(`[ECK] Debug snapshots enabled at ${eckDebugDir()}; files may contain sensitive context and local paths.`);
685
700
  }
@@ -701,12 +716,9 @@ async function main() {
701
716
  logger.warn('[startup] daemon.json.owners 未配置:进程级 menu 操作(/system /agent)将一律拒绝。' +
702
717
  '如需远程管理,请在 daemon.json 配置 owners: [<你的 AID>]');
703
718
  }
704
- // 应用配置中的日志级别(优先于环境变量)
705
- // logLevel 现在不在新结构中——若要保留,将来可加 defaults.debug.logLevel
706
- // 阶段 2c 暂跳过
707
719
  const paths = resolvePaths();
708
720
  if (bindBootstrapMode && daemonCfg.aid && loadAllAgents().agents.length === 0) {
709
- await runBindBootstrapDaemon(daemonCfg, defaults);
721
+ await runBindBootstrapDaemon(daemonCfg);
710
722
  return;
711
723
  }
712
724
  // ── EvolAgent Registry:加载 agents/<aid>/config.json ──
@@ -754,14 +766,11 @@ async function main() {
754
766
  agentRuntimeError = 'No runnable self-agent (all are error/disabled).';
755
767
  logger.warn(`[startup] ${agentRuntimeError} Control Plane will remain available.`);
756
768
  }
757
- // 进程级设置:idleMonitor 属于 daemon.json;debug 继续沿用 defaults 的现有行为。
769
+ // 进程级设置独立来自 daemon.json,不进入 Agent 覆盖链。
758
770
  const globalSettings = {
759
771
  idleMonitor: daemonCfg.idleMonitor,
760
- debug: defaults.debug,
772
+ debug: daemonCfg.debug,
761
773
  };
762
- if (globalSettings.debug?.logLevel) {
763
- setLogLevel(globalSettings.debug.logLevel);
764
- }
765
774
  // 启动期 anthropic 凭证校验已移除:runner 创建时由 AgentLoader 错误处理
766
775
  logger.info('✓ Config loaded');
767
776
  // Store for IPC access (T10 will wire this)
@@ -918,7 +927,7 @@ async function main() {
918
927
  messageCache.cleanupExpired();
919
928
  }, 60 * 60 * 1000);
920
929
  // 渠道插件系统
921
- const channelLoader = new ChannelLoader();
930
+ const channelLoader = new ChannelLoader(daemonCfg.debug);
922
931
  channelLoader.register(new FeishuChannelPlugin());
923
932
  channelLoader.register(new WechatChannelPlugin());
924
933
  channelLoader.register(new AUNChannelPlugin());
@@ -940,6 +949,14 @@ async function main() {
940
949
  }
941
950
  const channelInstances = evolagentInstances;
942
951
  logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
952
+ const contactRequestExpiryTimer = setInterval(() => {
953
+ for (const agent of agentRegistry.runnableAgents()) {
954
+ void expirePendingContactRequests(agent.aid).catch(error => {
955
+ logger.warn(`[contact-request] Pending expiry sweep failed: self=${agent.aid} error=${error instanceof Error ? error.message : String(error)}`);
956
+ });
957
+ }
958
+ }, 60_000);
959
+ contactRequestExpiryTimer.unref?.();
943
960
  const bindService = daemonCfg.aid
944
961
  ? new BindService({
945
962
  receiverAid: daemonCfg.aid,
@@ -1002,6 +1019,10 @@ async function main() {
1002
1019
  await responseEngine.interruptSession(sessionKey, reason);
1003
1020
  });
1004
1021
  messageQueue.setEventBus(eventBus);
1022
+ // Restore persisted input before Trigger schedulers start. Trigger-owned work
1023
+ // is deliberately dropped here and reclaimed from active.json by its scheduler.
1024
+ messageQueue.restorePersisted(false);
1025
+ const droppedTriggerSessionIds = new Set(messageQueue.consumeDroppedTriggerSessionIds());
1005
1026
  // 进程退出时立即刷盘队列状态(防止 debounce 期间的消息丢失)
1006
1027
  onShutdown(() => messageQueue.persistQueuesImmediate());
1007
1028
  // 回填 messageQueue 引用
@@ -1281,7 +1302,7 @@ async function main() {
1281
1302
  }
1282
1303
  return { allowed: true };
1283
1304
  };
1284
- const authenticatedTriggerActor = async (agentAid, actorSessionId, delegationToken, controlToken) => {
1305
+ const authenticatedTriggerActor = async (agentAid, actorSessionId, delegationToken, delegationCommandHash, controlToken) => {
1285
1306
  if (typeof actorSessionId !== 'string' || !actorSessionId) {
1286
1307
  const providedToken = typeof controlToken === 'string' ? Buffer.from(controlToken) : undefined;
1287
1308
  const expectedToken = Buffer.from(triggerControlToken);
@@ -1317,12 +1338,9 @@ async function main() {
1317
1338
  subject,
1318
1339
  };
1319
1340
  }
1320
- const validation = agentDelegationRegistry.validate(typeof delegationToken === 'string' ? delegationToken : undefined, actorSessionId);
1341
+ const validation = agentDelegationRegistry.validate(typeof delegationToken === 'string' ? delegationToken : undefined, actorSessionId, typeof delegationCommandHash === 'string' ? delegationCommandHash : undefined);
1321
1342
  if (!validation.ok)
1322
1343
  throw new Error(validation.reason);
1323
- if (validation.grant.selfAid !== agentAid) {
1324
- throw new Error('trigger agent does not match authenticated task agent');
1325
- }
1326
1344
  const actorSession = await sessionManager.getSessionById(actorSessionId);
1327
1345
  const actorOrigin = actorSession ? originFromActorSession(actorSession, validation.grant.actorId, validation.grant.channel, validation.grant.chatType) : undefined;
1328
1346
  if (!actorSession || !actorOrigin?.peerId)
@@ -1331,7 +1349,9 @@ async function main() {
1331
1349
  throw new Error('trigger delegation actor does not match session peer');
1332
1350
  }
1333
1351
  const roleDetail = resolvePeerRoleDetail({
1334
- selfAid: validation.grant.selfAid,
1352
+ // A cross-agent audit must be authorized by the target agent's role
1353
+ // configuration, not by the task agent that carried the delegation.
1354
+ selfAid: agentAid,
1335
1355
  channelKey: validation.grant.channel,
1336
1356
  channelType: validation.grant.channelType,
1337
1357
  chatType: 'private',
@@ -1339,11 +1359,11 @@ async function main() {
1339
1359
  conversationId: validation.grant.actorId,
1340
1360
  });
1341
1361
  const currentRole = roleDetail.effectiveRole;
1342
- if (!checkRoleAccess(currentRole, validation.grant.selfAid)) {
1362
+ if (!checkRoleAccess(currentRole, agentAid)) {
1343
1363
  throw new Error('trigger actor no longer has access to this agent');
1344
1364
  }
1345
1365
  const subject = buildAuthSubject({
1346
- selfAid: validation.grant.selfAid,
1366
+ selfAid: agentAid,
1347
1367
  actorId: validation.grant.actorId,
1348
1368
  channel: validation.grant.channel,
1349
1369
  channelType: validation.grant.channelType,
@@ -1436,18 +1456,36 @@ async function main() {
1436
1456
  }
1437
1457
  };
1438
1458
  cmdHandler.setTriggerSchedulerResolver((agentAid) => triggerSchedulers.get(agentAid));
1439
- const startedTriggerAgents = new Set();
1459
+ const triggerSchedulerStarts = new Map();
1440
1460
  const ensureTriggerSchedulerStarted = async (agent) => {
1441
- if (startedTriggerAgents.has(agent.aid))
1442
- return;
1443
- startedTriggerAgents.add(agent.aid);
1444
- await startTriggerScheduler(agent);
1461
+ const existing = triggerSchedulerStarts.get(agent.aid);
1462
+ if (existing)
1463
+ return await existing;
1464
+ const starting = startTriggerScheduler(agent);
1465
+ triggerSchedulerStarts.set(agent.aid, starting);
1466
+ try {
1467
+ await starting;
1468
+ }
1469
+ catch (error) {
1470
+ if (triggerSchedulerStarts.get(agent.aid) === starting)
1471
+ triggerSchedulerStarts.delete(agent.aid);
1472
+ throw error;
1473
+ }
1445
1474
  };
1446
1475
  const ensureDaemonTriggerSchedulerStarted = async () => {
1447
- if (startedTriggerAgents.has(daemonTriggerOwner.aid))
1448
- return;
1449
- startedTriggerAgents.add(daemonTriggerOwner.aid);
1450
- await startTriggerScheduler(daemonTriggerOwner, { seedUpgradeCheck: true });
1476
+ const existing = triggerSchedulerStarts.get(daemonTriggerOwner.aid);
1477
+ if (existing)
1478
+ return await existing;
1479
+ const starting = startTriggerScheduler(daemonTriggerOwner, { seedUpgradeCheck: true });
1480
+ triggerSchedulerStarts.set(daemonTriggerOwner.aid, starting);
1481
+ try {
1482
+ await starting;
1483
+ }
1484
+ catch (error) {
1485
+ if (triggerSchedulerStarts.get(daemonTriggerOwner.aid) === starting)
1486
+ triggerSchedulerStarts.delete(daemonTriggerOwner.aid);
1487
+ throw error;
1488
+ }
1451
1489
  };
1452
1490
  // 默认策略
1453
1491
  const defaultPolicy = {
@@ -1468,8 +1506,15 @@ async function main() {
1468
1506
  const defaultProjectPath = primaryAgent?.projectPath
1469
1507
  ?? defaults.projects?.defaultPath
1470
1508
  ?? path.join(paths.root, 'projects', 'default');
1509
+ const isContactBindChannelReady = (channelKey) => {
1510
+ const inst = channelInstances.find(candidate => candidate.adapter.channelKey === channelKey);
1511
+ return !!inst && inst.channel.getStatus?.().connected === true;
1512
+ };
1471
1513
  const msgBridge = new MessageBridge(defaultProjectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, primaryAgent?.config.debounce, () => processLevelOwners);
1472
1514
  msgBridge.setAgentRegistry(agentRegistry);
1515
+ msgBridge.setInteractionRouter(interactionRouter);
1516
+ msgBridge.setContactBindRuntimeChecker(isContactBindChannelReady);
1517
+ msgBridge.setAidStatsCollector(aidStatsCollector);
1473
1518
  const bootstrapService = new BootstrapService(agentRegistry, eventBus);
1474
1519
  msgBridge.setBootstrapService(bootstrapService);
1475
1520
  msgBridge.setHandoffRuntime(handoffRuntime);
@@ -1496,7 +1541,7 @@ async function main() {
1496
1541
  const isGroup = session.chatType === 'group';
1497
1542
  const target = isGroup
1498
1543
  ? (session.metadata?.groupId || session.channelId)
1499
- : (session.metadata?.peerId || envelope.channelId);
1544
+ : (envelope.replyContext?.peerId || session.metadata?.peerId || envelope.channelId);
1500
1545
  appendMessageLog(chatDir, buildOutboundEntry({
1501
1546
  from: owningAgent?.aid ?? envelope.agentName ?? session.selfAID ?? 'self',
1502
1547
  to: String(target || envelope.channelId),
@@ -1514,6 +1559,16 @@ async function main() {
1514
1559
  source: normalizeMessageSource(envelope.replyContext?.metadata?.source),
1515
1560
  chatmode: envelope.chatmode,
1516
1561
  }));
1562
+ // Per-AID 统计(预览行/tooltip 数据源)。AUN 在 channel 层自行记账,此处只补非 AUN。
1563
+ const statsAid = owningAgent?.aid ?? envelope.agentName ?? session.selfAID;
1564
+ if (statsAid) {
1565
+ aidStatsCollector.recordOutbound(statsAid, String(target || envelope.channelId), Buffer.byteLength(logPayload.text, 'utf-8'), logPayload.text, false, undefined, envelope.chatmode, 'send', {
1566
+ channelType: inst.channelType || inst.adapter.channelName,
1567
+ // 与入向同一套展示名解析:联系人簿 displayName 优先,兜底原生 ID
1568
+ peerLabel: resolvePeerDisplayLabel(statsAid, inst.channelType || inst.adapter.channelName, String(target || envelope.channelId), inst.adapter.channelKey) || session.metadata?.peerName || String(target || envelope.channelId),
1569
+ routeId: envelope.channelId,
1570
+ });
1571
+ }
1517
1572
  }
1518
1573
  }
1519
1574
  catch (err) {
@@ -1663,7 +1718,7 @@ async function main() {
1663
1718
  const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey) ?? agentRegistry.resolveByChannel(name);
1664
1719
  if (!agent)
1665
1720
  return;
1666
- if (!agent.config.debug?.upmsg)
1721
+ if (!daemonCfg.debug?.upmsg)
1667
1722
  return;
1668
1723
  const ownerAid = getFirstStaticAgentOwner(agent.aid);
1669
1724
  if (!ownerAid)
@@ -1838,15 +1893,15 @@ async function main() {
1838
1893
  // Trigger scheduler 与渠道连接解耦:cron 定时任务独立于渠道可用性运行
1839
1894
  // (触发时若渠道未连,发送侧自行排队/重试)。markChannelConnected 里的
1840
1895
  // ensureTriggerSchedulerStarted 仅作"尽早启动"优化,此处保证即使渠道首连
1841
- // 全部失败(如 gateway 宕机),trigger 仍无条件启动。Set 去重,不会重复。
1842
- ensureDaemonTriggerSchedulerStarted().catch((e) => {
1843
- logger.warn(`[startup] daemon trigger scheduler start failed for ${daemonTriggerOwner.aid}: ${e}`);
1844
- });
1845
- for (const agent of triggerStartupAgents) {
1846
- ensureTriggerSchedulerStarted(agent).catch((e) => {
1896
+ // 全部失败(如 gateway 宕机),trigger 仍无条件启动。Promise 去重,不会重复。
1897
+ await Promise.all([
1898
+ ensureDaemonTriggerSchedulerStarted().catch((e) => {
1899
+ logger.warn(`[startup] daemon trigger scheduler start failed for ${daemonTriggerOwner.aid}: ${e}`);
1900
+ }),
1901
+ ...triggerStartupAgents.map(agent => ensureTriggerSchedulerStarted(agent).catch((e) => {
1847
1902
  logger.warn(`[startup] trigger scheduler start failed for ${agent.aid}: ${e}`);
1848
- });
1849
- }
1903
+ })),
1904
+ ]);
1850
1905
  // ── 控制 AID(daemon 进程身份):pureIdentity 接入 AUN,独立于 evolagent ──
1851
1906
  // 证书缺失检测/生成在 CLI 侧(ec start)完成。daemon 是后台进程无终端,
1852
1907
  // 这里只做兜底:证书缺失时 warn 并继续(AUNChannel 内部后台重连),绝不阻塞。
@@ -1864,8 +1919,8 @@ async function main() {
1864
1919
  agentName: daemonCfg.aid,
1865
1920
  channelName: 'control',
1866
1921
  pureIdentity: true,
1867
- aunTrace: daemonCfg.debug?.aunTrace ?? defaults.debug?.aunTrace,
1868
- aunSdkLog: daemonCfg.debug?.aunSdkLog ?? defaults.debug?.aunSdkLog,
1922
+ aunTrace: daemonCfg.debug?.aunTrace,
1923
+ aunSdkLog: daemonCfg.debug?.aunSdkLog,
1869
1924
  });
1870
1925
  // connect() 失败不置空实例:AUNChannel 内部有无限重连(SDK auto_reconnect +
1871
1926
  // scheduleReconnect),首连失败后台会自愈;保留实例供 status 显示 disconnected。
@@ -1935,13 +1990,16 @@ async function main() {
1935
1990
  const menuControl = parseMenuControl(text);
1936
1991
  const isRoleMenu = menuControl.isMenu
1937
1992
  && (menuControl.name === 'role' || menuControl.request.cmd === '/role');
1993
+ const menuResponseContext = () => ({
1994
+ metadata: evolMenuResponseTransportMetadata(opts.encrypted),
1995
+ });
1938
1996
  if (menuControl.isMenu && !hasValidMenuId(menuControl)) {
1939
1997
  logger.warn(`[ControlMenu] dropped malformed request without id type=${menuControl.type}`);
1940
1998
  return;
1941
1999
  }
1942
2000
  if (menuControl.isMenu && !isRoleMenu && !isProcessLevelOwner(opts.peerId, processLevelOwners)) {
1943
2001
  const response = menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, { code: 'ROLE_ACCESS_DENIED', message: 'Current identity cannot access the control channel' });
1944
- await controlChannel.sendStructured(opts.channelId, response, { metadata: { encrypted: opts.encrypted } });
2002
+ await controlChannel.sendStructured(opts.channelId, response, menuResponseContext());
1945
2003
  return;
1946
2004
  }
1947
2005
  if (!isRoleMenu && !isProcessLevelOwner(opts.peerId, processLevelOwners)) {
@@ -1964,9 +2022,17 @@ async function main() {
1964
2022
  }
1965
2023
  // menu.* JSON 路由:owner 已在上方校验,转交 execMenuForControl(fromControlChannel=true)
1966
2024
  if (menuControl.isMenu) {
2025
+ const versionError = evaluateEvolMenuVersionGate({
2026
+ encrypted: opts.encrypted,
2027
+ protectedHeaders: opts.protectedHeaders,
2028
+ });
2029
+ if (versionError) {
2030
+ await controlChannel.sendStructured(opts.channelId, menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, versionError), menuResponseContext());
2031
+ return;
2032
+ }
1967
2033
  const validationError = validateMenuRequest(menuControl.request);
1968
2034
  if (validationError) {
1969
- await controlChannel.sendStructured(opts.channelId, menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, validationError), { metadata: { encrypted: opts.encrypted } });
2035
+ await controlChannel.sendStructured(opts.channelId, menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, validationError), menuResponseContext());
1970
2036
  return;
1971
2037
  }
1972
2038
  const deduped = await controlMenuDeduper.execute([opts.channelId, opts.peerId, menuControl.id].join('\u001f'), menuPayloadFingerprint(menuControl.raw), () => cmdHandler.execMenuForControl(parsed, opts.peerId));
@@ -1975,7 +2041,7 @@ async function main() {
1975
2041
  : deduped.value;
1976
2042
  // 同 bind.response:sendStructured 直发 typed payload(payload.type='menu.response'),
1977
2043
  // 不能用 sendMessage(会包成 {type:'text',...});encrypted 跟随入站请求保持对称。
1978
- await controlChannel.sendStructured(opts.channelId, response, { metadata: { encrypted: opts.encrypted } });
2044
+ await controlChannel.sendStructured(opts.channelId, response, menuResponseContext());
1979
2045
  return;
1980
2046
  }
1981
2047
  // owner 发的其他内容:提示可用指令
@@ -2042,9 +2108,7 @@ async function main() {
2042
2108
  });
2043
2109
  }
2044
2110
  });
2045
- // 先恢复消息队列。若某个 session 有原始 active/pending 消息,下面的泛化 resume 会跳过它。
2046
- messageQueue.restorePersisted(false);
2047
- const droppedTriggerSessionIds = new Set(messageQueue.consumeDroppedTriggerSessionIds());
2111
+ // Trigger scheduler 已在队列恢复之后启动;这里只协调其余持久化工作。
2048
2112
  // runnableAgents() only returns pre-start `stopped` agents. Reuse the stable
2049
2113
  // startup snapshot because channel connection may already mark them running.
2050
2114
  const recoveryAids = triggerStartupAgents.map(agent => agent.aid);
@@ -2080,8 +2144,13 @@ async function main() {
2080
2144
  logger.info(`[Resume] Found ${pendingSessions.length} pending session(s) from before restart`);
2081
2145
  for (const session of pendingSessions) {
2082
2146
  if (droppedTriggerSessionIds.has(session.id)) {
2083
- logger.info(`[Resume] session ${session.id}: dropped stale Trigger execution, clearing processing without resume`);
2084
- sessionManager.clearProcessing(session.id);
2147
+ if (messageQueue.isProcessing(session.id) || messageQueue.getQueueLength(session.id) > 0) {
2148
+ logger.info(`[Resume] session ${session.id}: Trigger scheduler already reclaimed execution, skipping generic resume`);
2149
+ }
2150
+ else {
2151
+ logger.info(`[Resume] session ${session.id}: dropped stale Trigger execution was not reclaimable, clearing processing`);
2152
+ sessionManager.clearProcessing(session.id);
2153
+ }
2085
2154
  continue;
2086
2155
  }
2087
2156
  if (messageQueue.isProcessing(session.id) || messageQueue.getQueueLength(session.id) > 0) {
@@ -2172,8 +2241,8 @@ async function main() {
2172
2241
  ? { aid: daemonCfg.aid, connected: controlChannel?.getAidState().status === 'connected' }
2173
2242
  : undefined,
2174
2243
  };
2175
- }, async (cmd, sessionId, delegationToken) => {
2176
- const delegation = agentDelegationRegistry.validate(delegationToken, sessionId);
2244
+ }, async (cmd, sessionId, delegationToken, delegationCommandHash) => {
2245
+ const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
2177
2246
  if (!delegation.ok)
2178
2247
  return { ok: false, code: delegation.code, error: delegation.reason };
2179
2248
  return cmdHandler.handleCtl(cmd, sessionId);
@@ -2182,13 +2251,27 @@ async function main() {
2182
2251
  ipcServer.setAgentRegistry(agentRegistry);
2183
2252
  ipcServer.setDingtalkContactBindExecutor({
2184
2253
  register: (cmd) => registerPendingDingtalkContactBind(cmd),
2254
+ isChannelReady: isContactBindChannelReady,
2255
+ });
2256
+ ipcServer.setFeishuContactBindExecutor({
2257
+ register: (cmd) => registerPendingFeishuContactBind(cmd),
2258
+ isChannelReady: isContactBindChannelReady,
2259
+ });
2260
+ ipcServer.setQQBotContactBindExecutor({
2261
+ register: (cmd) => registerPendingQQBotContactBind(cmd),
2262
+ isChannelReady: isContactBindChannelReady,
2185
2263
  });
2186
2264
  ipcServer.setWecomContactBindExecutor({
2187
2265
  register: (cmd) => registerPendingWecomContactBind(cmd),
2266
+ isChannelReady: isContactBindChannelReady,
2267
+ });
2268
+ ipcServer.setWechatContactBindExecutor({
2269
+ register: (cmd) => registerPendingWechatContactBind(cmd),
2270
+ isChannelReady: isContactBindChannelReady,
2188
2271
  });
2189
2272
  ipcServer.setMenuExecutor((payload, auth) => cmdHandler.execMenuForEcweb(payload, auth));
2190
- ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken));
2191
- ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken));
2273
+ ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash));
2274
+ ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash));
2192
2275
  cmdHandler.setDaemonStatusProvider(() => {
2193
2276
  const aidState = controlChannel?.getAidState?.();
2194
2277
  return {
@@ -2314,6 +2397,7 @@ async function main() {
2314
2397
  : undefined;
2315
2398
  const delegation = authorizeDelegatedAunMsgSend(agentDelegationRegistry, {
2316
2399
  delegationToken: params.delegationToken,
2400
+ delegationCommandHash: params.delegationCommandHash,
2317
2401
  sessionId: params.originSessionId,
2318
2402
  messageId: params.originMessageId,
2319
2403
  aid: params.aid,
@@ -2536,6 +2620,10 @@ async function main() {
2536
2620
  };
2537
2621
  // Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
2538
2622
  globalThis.__evolcore_resyncAgents = async () => {
2623
+ // 先清 kit 缓存:'kits' 组(manifest / fragment / schema / 角色模板)走 on-reload
2624
+ // 策略,平时不查盘。放在扫盘与 reload 之前,本轮上下线的 agent 才能读到磁盘上的
2625
+ // 最新版本——放在末尾的话,这一轮全用旧值,改动要等下一次 resync 才生效。
2626
+ invalidateKitCache();
2539
2627
  const { loadAllAgents: scanAgents, loadDefaults: readDefaults } = await import('./config-store.js');
2540
2628
  const { resolveEffective } = await import('./config/config-manager.js');
2541
2629
  const freshDefaults = readDefaults();
@@ -2557,6 +2645,7 @@ async function main() {
2557
2645
  }
2558
2646
  await triggerSchedulers.get(aid)?.stop();
2559
2647
  triggerSchedulers.delete(aid);
2648
+ triggerSchedulerStarts.delete(aid);
2560
2649
  // 断开所有 channels
2561
2650
  for (const chName of agent.channelInstanceNames()) {
2562
2651
  const inst = channelInstances.find(i => i.adapter.channelName === chName);
@@ -2609,8 +2698,7 @@ async function main() {
2609
2698
  results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
2610
2699
  }
2611
2700
  }
2612
- // 重建 channel index + 清除 kit 缓存
2613
- invalidateKitCache();
2701
+ // 重建 channel indexkit 缓存已在本轮开头清过)
2614
2702
  agentRegistry.channelIndex.clear();
2615
2703
  agentRegistry.buildChannelIndex();
2616
2704
  logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
@@ -2682,8 +2770,21 @@ async function main() {
2682
2770
  return agentAid;
2683
2771
  };
2684
2772
  const authorizeTrigger = async (agentAid, operation, triggerId) => {
2685
- const actor = await authenticatedTriggerActor(agentAid, cmd.actorSessionId, cmd.delegationToken, cmd.controlToken);
2686
- if (!actor.control && actor.selfAid !== agentAid) {
2773
+ const actor = await authenticatedTriggerActor(agentAid, cmd.actorSessionId, cmd.delegationToken, cmd.delegationCommandHash, cmd.controlToken);
2774
+ const isCrossAgent = !actor.control && actor.selfAid !== agentAid;
2775
+ if (!isCrossAgentTriggerOperationAllowed({
2776
+ control: actor.control,
2777
+ taskAgentAid: actor.selfAid,
2778
+ targetAgentAid: agentAid,
2779
+ targetManagement: actor.management,
2780
+ operation,
2781
+ })) {
2782
+ if (isCrossAgent && !actor.management) {
2783
+ throw new Error('cross-agent trigger audit requires owner/admin access on the target agent');
2784
+ }
2785
+ if (isCrossAgent && !isCrossAgentTriggerReadOperation(operation)) {
2786
+ throw new Error('cross-agent trigger operations are limited to list, show, and history');
2787
+ }
2687
2788
  throw new Error('trigger agent does not match authenticated task agent');
2688
2789
  }
2689
2790
  const operationDecision = authorizeOperation({
@@ -2783,7 +2884,7 @@ async function main() {
2783
2884
  if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
2784
2885
  throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
2785
2886
  }
2786
- const definition = applyTriggerPatch(existing, cmd.patch);
2887
+ const definition = applyTriggerPatch(existing, cmd.patch, { fromPromptFile: cmd.promptFile === true });
2787
2888
  validateTriggerDefinitionForActor(definition, actor);
2788
2889
  validateTriggerFeedbackChannels(definition);
2789
2890
  const scheduler = schedulerFor(agentAid);
@@ -2853,7 +2954,11 @@ async function main() {
2853
2954
  // 显式触发更可控。
2854
2955
  // 优雅关闭
2855
2956
  let shutdownSignal = 'unknown';
2957
+ let shutdownInProgress = false;
2856
2958
  const shutdown = async (signal) => {
2959
+ if (shutdownInProgress)
2960
+ return;
2961
+ shutdownInProgress = true;
2857
2962
  if (signal)
2858
2963
  shutdownSignal = signal;
2859
2964
  const pid = process.pid;
@@ -2862,30 +2967,60 @@ async function main() {
2862
2967
  ipcServer.stopCpuTracking();
2863
2968
  ipcServer.stop();
2864
2969
  bindService?.stopCleanup();
2865
- await permissionGateway.cancelAllPending('daemon_restart');
2866
- await Promise.all([...triggerSchedulers.values()].map(scheduler => scheduler.waitForIdle(5_000)));
2867
- await Promise.all([...triggerSchedulers.values()].map(scheduler => scheduler.stop()));
2868
- eventBus.publish({
2869
- type: 'system:shutdown',
2870
- timestamp: Date.now()
2871
- });
2872
- // 断开插件系统的渠道
2873
- await channelLoader.disconnectAll(channelInstances);
2874
- for (const inst of channelInstances) {
2875
- const type = inst.channelType || inst.adapter.channelName;
2876
- eventBus.publish({ type: 'channel:disconnected', channel: type, channelName: inst.adapter.channelName, reason: 'shutdown' });
2970
+ // 必须在第一个 await 之前同步冻结 active batch、发布 task:interrupted,
2971
+ // 否则 handler continuation 可能先提交完成并清掉待恢复的主消息。
2972
+ const shutdownStep = async (label, operation) => {
2973
+ try {
2974
+ await operation();
2975
+ }
2976
+ catch (error) {
2977
+ logger.error(`[Shutdown] ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
2978
+ }
2979
+ };
2980
+ try {
2981
+ const queueShutdown = messageQueue.shutdownGracefully();
2982
+ await shutdownStep('cancel pending permissions', () => permissionGateway.cancelAllPending('daemon_restart'));
2983
+ await shutdownStep('wait for trigger schedulers', async () => {
2984
+ await Promise.all([...triggerSchedulers.values()].map(scheduler => scheduler.waitForIdle(5_000)));
2985
+ });
2986
+ await shutdownStep('suspend trigger schedulers', async () => {
2987
+ await Promise.all([...triggerSchedulers.values()].map(scheduler => scheduler.suspendForDaemonExit()));
2988
+ });
2989
+ await shutdownStep('interrupt active message queue tasks', async () => { await queueShutdown; });
2990
+ eventBus.publish({
2991
+ type: 'system:shutdown',
2992
+ timestamp: Date.now()
2993
+ });
2994
+ // 断开插件系统的渠道
2995
+ await shutdownStep('disconnect channels', () => channelLoader.disconnectAll(channelInstances));
2996
+ for (const inst of channelInstances) {
2997
+ const type = inst.channelType || inst.adapter.channelName;
2998
+ eventBus.publish({ type: 'channel:disconnected', channel: type, channelName: inst.adapter.channelName, reason: 'shutdown' });
2999
+ }
3000
+ // 断开控制 AID(daemon 进程身份)
3001
+ if (controlChannel) {
3002
+ await shutdownStep('disconnect control AID', () => controlChannel.disconnect());
3003
+ }
2877
3004
  }
2878
- // 断开控制 AID(daemon 进程身份)
2879
- if (controlChannel) {
3005
+ catch (error) {
3006
+ logger.error(`[Shutdown] Unexpected shutdown failure: ${error instanceof Error ? error.stack || error.message : String(error)}`);
3007
+ }
3008
+ finally {
2880
3009
  try {
2881
- await controlChannel.disconnect();
3010
+ sessionManager.close();
2882
3011
  }
2883
- catch { /* ignore */ }
3012
+ catch (error) {
3013
+ logger.error(`[Shutdown] session manager close failed: ${error instanceof Error ? error.message : String(error)}`);
3014
+ }
3015
+ try {
3016
+ removeAll();
3017
+ }
3018
+ catch (error) {
3019
+ logger.error(`[Shutdown] instance cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
3020
+ }
3021
+ logger.info('✓ Shutdown complete');
3022
+ process.exit(0);
2884
3023
  }
2885
- sessionManager.close();
2886
- removeAll();
2887
- logger.info('✓ Shutdown complete');
2888
- process.exit(0);
2889
3024
  };
2890
3025
  process.on('SIGINT', () => shutdown('SIGINT'));
2891
3026
  process.on('SIGTERM', () => shutdown('SIGTERM'));