evolcore 0.0.2 → 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 (124) hide show
  1. package/CHANGELOG.md +44 -793
  2. package/dist/agents/claude-runner.js +197 -17
  3. package/dist/agents/codex-runner.js +46 -3
  4. package/dist/aun/outbox.js +8 -0
  5. package/dist/channels/aun.js +21 -4
  6. package/dist/channels/contact-bind-code.js +134 -0
  7. package/dist/channels/dingtalk.js +979 -149
  8. package/dist/channels/feishu.js +130 -54
  9. package/dist/channels/wecom-card.js +101 -0
  10. package/dist/channels/wecom-onboarding.js +82 -0
  11. package/dist/channels/wecom-state.js +191 -0
  12. package/dist/channels/wecom.js +755 -163
  13. package/dist/cli/agent-command.js +2 -1
  14. package/dist/cli/aun-commands.js +88 -33
  15. package/dist/cli/bench.js +2 -2
  16. package/dist/cli/contact.js +71 -0
  17. package/dist/cli/ctl-command.js +2 -2
  18. package/dist/cli/daemon-commands.js +77 -214
  19. package/dist/cli/handoff-command.js +2 -2
  20. package/dist/cli/help.js +9 -5
  21. package/dist/cli/index.js +132 -116
  22. package/dist/cli/init-channel.js +92 -97
  23. package/dist/cli/init.js +63 -26
  24. package/dist/cli/model.js +2 -1
  25. package/dist/cli/net-check.js +2 -2
  26. package/dist/cli/queue-command.js +30 -6
  27. package/dist/cli/raw-key-input.js +25 -0
  28. package/dist/cli/response.js +5 -6
  29. package/dist/cli/restart-monitor.js +25 -1
  30. package/dist/cli/stats.js +6 -4
  31. package/dist/cli/trigger-command.js +55 -15
  32. package/dist/cli/version.js +6 -1
  33. package/dist/config/builtin-role-templates.js +22 -10
  34. package/dist/config/builtin-roles.js +7 -1
  35. package/dist/config/config-manager.js +221 -17
  36. package/dist/config/contact-alias.js +68 -0
  37. package/dist/config/contact-book-store.js +454 -0
  38. package/dist/config/contact-book-v2-startup.js +35 -0
  39. package/dist/config/contact-book.js +156 -303
  40. package/dist/config/contact-operation-service.js +110 -0
  41. package/dist/config/peer-role-resolver.js +133 -54
  42. package/dist/config/role-ranks.js +18 -0
  43. package/dist/config/role-service.js +16 -19
  44. package/dist/config/role-store.js +16 -5
  45. package/dist/config/roles.js +10 -1
  46. package/dist/config-store.js +0 -2
  47. package/dist/core/auth/authorization-audit.js +10 -1
  48. package/dist/core/auth/operation-authorizer.js +2 -0
  49. package/dist/core/auth/operation-catalog.js +56 -0
  50. package/dist/core/command/command-handler.js +105 -10
  51. package/dist/core/command/connect-menu.js +374 -0
  52. package/dist/core/command/menu-handler.js +114 -16
  53. package/dist/core/command/role-menu.js +128 -29
  54. package/dist/core/command/slash-handler.js +28 -18
  55. package/dist/core/daemon-file-cache.js +12 -6
  56. package/dist/core/event-catalog.js +70 -0
  57. package/dist/core/evolagent-registry.js +0 -1
  58. package/dist/core/evolagent.js +20 -9
  59. package/dist/core/message/im-renderer.js +2 -0
  60. package/dist/core/message/message-bridge.js +79 -8
  61. package/dist/core/message/message-queue.js +10 -0
  62. package/dist/core/message/message-utils.js +8 -2
  63. package/dist/core/message/response-engine.js +269 -25
  64. package/dist/core/message/send-receipt.js +24 -0
  65. package/dist/core/message/stream-debouncer.js +11 -2
  66. package/dist/core/permission/tool-policy.js +47 -15
  67. package/dist/core/protected-paths.js +2 -0
  68. package/dist/core/session/session-fs-store.js +44 -3
  69. package/dist/core/session/session-manager.js +61 -5
  70. package/dist/index.js +62 -6
  71. package/dist/ipc.js +34 -5
  72. package/dist/stats/billing.js +20 -8
  73. package/dist/trigger/manager.js +3 -0
  74. package/dist/trigger/parser.js +77 -2
  75. package/dist/trigger/patch.js +8 -1
  76. package/dist/trigger/scheduler.js +3 -1
  77. package/dist/trigger/validation.js +13 -0
  78. package/dist/utils/aid-bind.js +43 -29
  79. package/dist/utils/instance-registry.js +14 -7
  80. package/dist/utils/log-writer.js +46 -0
  81. package/dist/utils/media-cache.js +4 -1
  82. package/dist/utils/model-prices.jsonl +6 -3
  83. package/dist/utils/restart-safety.js +31 -0
  84. package/dist/utils/system-memory.js +62 -0
  85. package/kits/docs/INDEX.md +2 -1
  86. package/kits/docs/evolcore/INDEX.md +5 -3
  87. package/kits/docs/evolcore/agent.md +9 -1
  88. package/kits/docs/evolcore/aid.md +5 -2
  89. package/kits/docs/evolcore/contact.md +57 -0
  90. package/kits/docs/evolcore/fs.md +9 -0
  91. package/kits/docs/evolcore/group.md +12 -3
  92. package/kits/docs/evolcore/model.md +4 -1
  93. package/kits/docs/evolcore/msg.md +9 -3
  94. package/kits/docs/evolcore/response.md +16 -21
  95. package/kits/docs/evolcore/rpc.md +2 -0
  96. package/kits/docs/evolcore/stats.md +15 -2
  97. package/kits/docs/evolcore/storage.md +1 -0
  98. package/kits/docs/evolcore/trigger.md +17 -2
  99. package/kits/eck_manifest.json +12 -0
  100. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  101. package/kits/schemas/_meta.json +7 -4
  102. package/kits/schemas/agent-config.schema.6.json +322 -0
  103. package/kits/schemas/contact-book.schema.2.json +43 -0
  104. package/kits/schemas/relation-config.schema.5.json +47 -0
  105. package/kits/schemas/role-config.schema.1.json +1 -0
  106. package/kits/schemas/role-registry.schema.1.json +2 -2
  107. package/kits/templates/roles/admin.json +1 -0
  108. package/kits/templates/roles/member.json +1 -0
  109. package/kits/templates/roles/owner.json +1 -0
  110. package/kits/templates/roles/visitor.json +1 -0
  111. package/kits/templates/system-fragments/commands.md +3 -1
  112. package/package.json +4 -4
  113. package/assets/brand/evolcore/README.md +0 -19
  114. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  115. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  116. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  117. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  118. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  119. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  120. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  121. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  122. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  123. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  124. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -13,12 +13,16 @@ function decodeSegment(s) {
13
13
  /**
14
14
  * 计算 chat 目录的完整路径。
15
15
  * - aun: sessionsDir/aun/<urlEncode(selfAID|'_unknown')>/<urlEncode(channelId)>/
16
+ * - wecom: sessionsDir/wecom/<urlEncode(channelKey)>/<urlEncode(channelId)>/
16
17
  * - 其它: sessionsDir/<channelType>/<urlEncode(channelId)>/
17
18
  */
18
- export function chatDirPath(sessionsDir, channelType, channelId, selfAID) {
19
+ export function chatDirPath(sessionsDir, channelType, channelId, selfAID, channelKey) {
19
20
  if (channelType === 'aun') {
20
21
  return path.join(sessionsDir, channelType, encodeSegment(selfAID || '_unknown'), encodeSegment(channelId));
21
22
  }
23
+ if (channelType === 'wecom' && channelKey?.startsWith('wecom#')) {
24
+ return path.join(sessionsDir, channelType, encodeSegment(channelKey), encodeSegment(channelId));
25
+ }
22
26
  return path.join(sessionsDir, channelType, encodeSegment(channelId));
23
27
  }
24
28
  /** 解码目录段(用于扫描时把目录名还原为原始 channelId/selfAID) */
@@ -126,6 +130,7 @@ export function readAllJsonlLines(filePath) {
126
130
  /**
127
131
  * 扫描所有 chat 目录。
128
132
  * - aun: channelType / selfAID / channelId (3层)
133
+ * - wecom: channelType / channelKey / channelId (3层;兼容旧的2层目录)
129
134
  * - 其它: channelType / channelId (2层)
130
135
  */
131
136
  export function scanChatDirs(sessionsDir) {
@@ -176,6 +181,42 @@ export function scanChatDirs(sessionsDir) {
176
181
  }
177
182
  }
178
183
  }
184
+ else if (channelType === 'wecom') {
185
+ for (const entry of level2Entries) {
186
+ if (!entry.isDirectory())
187
+ continue;
188
+ const decoded = decodeSegment(entry.name);
189
+ const entryDir = path.join(typeDir, entry.name);
190
+ if (decoded.startsWith('wecom#') && decoded.split('#').length === 3) {
191
+ let chatEntries;
192
+ try {
193
+ chatEntries = fs.readdirSync(entryDir, { withFileTypes: true });
194
+ }
195
+ catch {
196
+ continue;
197
+ }
198
+ for (const chatEntry of chatEntries) {
199
+ if (!chatEntry.isDirectory())
200
+ continue;
201
+ results.push({
202
+ channelType,
203
+ selfAID: '',
204
+ channelKey: decoded,
205
+ channelId: decodeSegment(chatEntry.name),
206
+ dirPath: path.join(entryDir, chatEntry.name),
207
+ });
208
+ }
209
+ }
210
+ else {
211
+ results.push({
212
+ channelType,
213
+ selfAID: '',
214
+ channelId: decoded,
215
+ dirPath: entryDir,
216
+ });
217
+ }
218
+ }
219
+ }
179
220
  else {
180
221
  // 2-layer: channelType/channelId
181
222
  for (const chatEntry of level2Entries) {
@@ -205,8 +246,8 @@ export function scanMetaFiles(chatDir) {
205
246
  throw e;
206
247
  }
207
248
  }
208
- export function ensureChatDir(sessionsDir, channelType, channelId, selfAID) {
209
- const dir = chatDirPath(sessionsDir, channelType, channelId, selfAID);
249
+ export function ensureChatDir(sessionsDir, channelType, channelId, selfAID, channelKey) {
250
+ const dir = chatDirPath(sessionsDir, channelType, channelId, selfAID, channelKey);
210
251
  fs.mkdirSync(dir, { recursive: true });
211
252
  fs.mkdirSync(path.join(dir, '_threads'), { recursive: true });
212
253
  fs.mkdirSync(path.join(dir, '_trash'), { recursive: true });
@@ -23,6 +23,7 @@ export class SessionManager {
23
23
  this.eventBus = eventBus;
24
24
  this.identityResolver = identityResolver;
25
25
  this.migrateChannelKeyFormat();
26
+ this.migrateWecomChannelDirs();
26
27
  }
27
28
  setIdentityResolver(resolver) {
28
29
  this.identityResolver = resolver;
@@ -69,20 +70,20 @@ export class SessionManager {
69
70
  * 需要明确的 channelType + selfAID 才能确定路径。
70
71
  */
71
72
  resolveChatDir(channel, channelId, channelType, selfAID) {
72
- return chatDirPath(this.sessionsDir, channelType, channelId, selfAID);
73
+ return chatDirPath(this.sessionsDir, channelType, channelId, selfAID, channel);
73
74
  }
74
75
  /**
75
76
  * 给定明确的 channelType + selfAID 时直接计算路径(不扫描)。
76
77
  * 用于 caller 已经知道完整路由信息的场景(如 message-bridge 透传)。
77
78
  */
78
79
  resolveChatDirExact(channel, channelId, channelType, selfAID) {
79
- return chatDirPath(this.sessionsDir, channelType, channelId, selfAID);
80
+ return chatDirPath(this.sessionsDir, channelType, channelId, selfAID, channel);
80
81
  }
81
82
  resolveChatDirFromSession(session) {
82
83
  if (!session.channelType) {
83
84
  throw new Error(`[SessionManager] missing channelType for session ${session.id}`);
84
85
  }
85
- return chatDirPath(this.sessionsDir, session.channelType, session.channelId, session.selfAID);
86
+ return chatDirPath(this.sessionsDir, session.channelType, session.channelId, session.selfAID, session.channel);
86
87
  }
87
88
  deriveChannelIdentity(channel, channelId) {
88
89
  const parsed = tryParseChannelKey(channel);
@@ -253,7 +254,7 @@ export class SessionManager {
253
254
  }
254
255
  ensureChatDirForSession(session) {
255
256
  const channelType = session.channelType || session.channel;
256
- return ensureChatDir(this.sessionsDir, channelType, session.channelId, session.selfAID);
257
+ return ensureChatDir(this.sessionsDir, channelType, session.channelId, session.selfAID, session.channel);
257
258
  }
258
259
  metaFilePath(chatDir, sessionId) {
259
260
  return path.join(chatDir, `${sessionId}.jsonl`);
@@ -678,7 +679,7 @@ export class SessionManager {
678
679
  getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, baseagent, selfAID, channelType, peerType, chatType) {
679
680
  // 使用精确路径(channelType + selfAID)
680
681
  const chatDir = (channelType && selfAID)
681
- ? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
682
+ ? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID, channel); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
682
683
  : this.ensureResolvedChatDirSafe(channel, channelId, channelType, selfAID);
683
684
  const threadIndex = readThreadIndex(chatDir);
684
685
  const existingEntry = threadIndex[threadId];
@@ -1584,4 +1585,59 @@ export class SessionManager {
1584
1585
  }
1585
1586
  return null;
1586
1587
  }
1588
+ /**
1589
+ * Move legacy `sessions/wecom/<chatId>` directories under their persisted
1590
+ * channelKey. The stored SessionFile.channel is authoritative; ambiguous
1591
+ * directories are deliberately left untouched for manual recovery.
1592
+ */
1593
+ migrateWecomChannelDirs() {
1594
+ const legacyDirs = scanChatDirs(this.sessionsDir)
1595
+ .filter(entry => entry.channelType === 'wecom' && !entry.channelKey);
1596
+ let migrated = 0;
1597
+ for (const entry of legacyDirs) {
1598
+ const candidates = [];
1599
+ const active = readJsonFile(path.join(entry.dirPath, 'active.json'));
1600
+ if (active)
1601
+ candidates.push(active);
1602
+ for (const metaFile of scanMetaFiles(entry.dirPath)) {
1603
+ const meta = readLastJsonlLine(path.join(entry.dirPath, metaFile));
1604
+ if (meta)
1605
+ candidates.push(meta);
1606
+ }
1607
+ const threadDir = path.join(entry.dirPath, '_threads');
1608
+ for (const metaFile of scanMetaFiles(threadDir)) {
1609
+ const meta = readLastJsonlLine(path.join(threadDir, metaFile));
1610
+ if (meta)
1611
+ candidates.push(meta);
1612
+ }
1613
+ const channelKeys = new Set(candidates
1614
+ .map(item => item.channel)
1615
+ .filter((value) => typeof value === 'string' && value.startsWith('wecom#')));
1616
+ if (channelKeys.size !== 1) {
1617
+ if (channelKeys.size > 1) {
1618
+ logger.error(`[SessionManager] Cannot auto-migrate mixed WeCom session directory: ${entry.dirPath}`);
1619
+ }
1620
+ continue;
1621
+ }
1622
+ const channelKey = [...channelKeys][0];
1623
+ const target = chatDirPath(this.sessionsDir, 'wecom', entry.channelId, undefined, channelKey);
1624
+ if (target === entry.dirPath)
1625
+ continue;
1626
+ if (fs.existsSync(target)) {
1627
+ logger.error(`[SessionManager] Cannot migrate WeCom session directory because target exists: ${target}`);
1628
+ continue;
1629
+ }
1630
+ try {
1631
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1632
+ fs.renameSync(entry.dirPath, target);
1633
+ migrated++;
1634
+ }
1635
+ catch (error) {
1636
+ logger.error(`[SessionManager] Failed to migrate WeCom session directory ${entry.dirPath}: ${String(error)}`);
1637
+ }
1638
+ }
1639
+ if (migrated > 0) {
1640
+ logger.info(`[SessionManager] Migrated ${migrated} WeCom session director${migrated === 1 ? 'y' : 'ies'} to channelKey isolation`);
1641
+ }
1642
+ }
1587
1643
  }
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { loadDefaults, loadAllAgents, migrateIdentitiesIfNeeded, loadDaemonConfi
16
16
  import { ConfigTarget, initConfigManager, onConfigWrite } from './config/config-manager.js';
17
17
  import { ensureRoleConfigV4OnStartup } from './config/role-config-v4-startup.js';
18
18
  import { ensureRoleConfigV5OnStartup } from './config/role-config-v5-startup.js';
19
+ import { ensureContactBookV2OnStartup } from './config/contact-book-v2-startup.js';
19
20
  import { shouldFailFastForMissingOwners } from './config/owner-policy.js';
20
21
  import { isManagementRole } from './config/builtin-roles.js';
21
22
  import { checkRoleAccess, getFirstStaticAgentOwner, resolvePeerRoleDetail, roleToSessionIdentity } from './config/peer-role-resolver.js';
@@ -33,7 +34,7 @@ import { startServiceProxy } from './aun/service-proxy.js';
33
34
  import { BindService } from './utils/aid-bind.js';
34
35
  import { DingtalkChannelPlugin, registerPendingDingtalkContactBind } from './channels/dingtalk.js';
35
36
  import { QQBotChannelPlugin } from './channels/qqbot.js';
36
- import { WecomChannelPlugin } from './channels/wecom.js';
37
+ import { WecomChannelPlugin, registerPendingWecomContactBind } from './channels/wecom.js';
37
38
  import { buildEnvelope } from './core/message/message-utils.js';
38
39
  import { ResponseEngine } from './core/message/response-engine.js';
39
40
  import { MessageQueue } from './core/message/message-queue.js';
@@ -598,6 +599,9 @@ async function main() {
598
599
  }
599
600
  // 在登记 daemon 实例前自动备份并迁移旧角色配置;失败才阻止启动。
600
601
  try {
602
+ // Crash recovery must precede schema gates and migrations so they never
603
+ // inspect or migrate a partially applied Role Menu transaction.
604
+ recoverAllRoleMutationsSync();
601
605
  const migration = await ensureRoleConfigV4OnStartup();
602
606
  if (migration) {
603
607
  const msg = `✓ Role config v4 migration applied automatically: ${migration.changedFiles} file(s)` +
@@ -612,6 +616,13 @@ async function main() {
612
616
  logger.info(msg);
613
617
  console.error(msg);
614
618
  }
619
+ const contactMigration = await ensureContactBookV2OnStartup();
620
+ if (contactMigration) {
621
+ const msg = `✓ Contact book v2 migration applied automatically: ${contactMigration.changedFiles} file(s)` +
622
+ (contactMigration.backup ? `; backup: ${contactMigration.backup}` : '');
623
+ logger.info(msg);
624
+ console.error(msg);
625
+ }
615
626
  }
616
627
  catch (e) {
617
628
  const msg = `❌ ${e instanceof Error ? e.message : String(e)}`;
@@ -639,7 +650,6 @@ async function main() {
639
650
  // ── 配置体系初始化(schema 字段不相交硬约束校验)──
640
651
  try {
641
652
  initConfigManager();
642
- recoverAllRoleMutationsSync();
643
653
  }
644
654
  catch (e) {
645
655
  const msg = `❌ 配置 schema 校验失败: ${e instanceof Error ? e.message : String(e)}`;
@@ -870,7 +880,9 @@ async function main() {
870
880
  const permissionGateway = new PermissionGateway();
871
881
  permissionGateway.setEventBus(eventBus);
872
882
  onConfigWrite(({ target, selector }) => {
873
- if ((target !== ConfigTarget.Agent && target !== ConfigTarget.Contact) || !selector.self)
883
+ if ((target !== ConfigTarget.Agent
884
+ && target !== ConfigTarget.Contact
885
+ && target !== ConfigTarget.Relation) || !selector.self)
874
886
  return;
875
887
  const cancelled = permissionGateway.revalidatePendingApprovals(selector.self);
876
888
  if (cancelled > 0) {
@@ -1061,6 +1073,43 @@ async function main() {
1061
1073
  baseagent: primaryAgent?.baseagent || 'codex',
1062
1074
  projectPath: primaryAgent?.projectPath || process.cwd(),
1063
1075
  };
1076
+ const triggerBaseagentsForAgent = (agentAid) => {
1077
+ const ownerName = agentRegistry.get(agentAid)?.name
1078
+ ?? (agentAid === daemonTriggerOwner.aid ? primaryAgent?.name : undefined);
1079
+ if (!ownerName)
1080
+ return [];
1081
+ const prefix = `${ownerName}::`;
1082
+ return [...agentMap.keys()]
1083
+ .filter(key => key.startsWith(prefix))
1084
+ .map(key => key.slice(prefix.length));
1085
+ };
1086
+ const materializeTriggerCreateBaseagent = (input, agentAid) => {
1087
+ const rawExecution = input.execution;
1088
+ if (!rawExecution || typeof rawExecution !== 'object' || Array.isArray(rawExecution))
1089
+ return input;
1090
+ const execution = rawExecution;
1091
+ if (execution.type !== 'trigger_session') {
1092
+ if (execution.baseagent !== undefined) {
1093
+ throw new Error('execution.baseagent is only allowed when execution.type=trigger_session');
1094
+ }
1095
+ return input;
1096
+ }
1097
+ if (execution.baseagent !== undefined
1098
+ && (typeof execution.baseagent !== 'string' || !execution.baseagent.trim())) {
1099
+ throw new Error('execution.baseagent must be a non-empty string');
1100
+ }
1101
+ const baseagent = typeof execution.baseagent === 'string'
1102
+ ? execution.baseagent.trim()
1103
+ : agentRegistry.get(agentAid)?.baseagent
1104
+ ?? (agentAid === daemonTriggerOwner.aid ? daemonTriggerOwner.baseagent : undefined);
1105
+ if (!baseagent)
1106
+ throw new Error('unable to resolve Trigger session baseagent');
1107
+ const available = triggerBaseagentsForAgent(agentAid);
1108
+ if (!available.includes(baseagent)) {
1109
+ throw new Error(`Trigger baseagent unavailable for ${agentAid}: ${baseagent} (available: ${available.join(', ') || 'none'})`);
1110
+ }
1111
+ return { ...input, execution: { ...execution, baseagent } };
1112
+ };
1064
1113
  const getTriggerChannel = (agentAid, channelKey) => {
1065
1114
  if (agentAid === daemonTriggerOwner.aid && channelKey === daemonChannel.channelKey) {
1066
1115
  return {
@@ -1117,7 +1166,7 @@ async function main() {
1117
1166
  const agent = agentRegistry.get(definition.agentAid);
1118
1167
  const modelDecision = validateModelSelectionForRole({
1119
1168
  role: actor.role,
1120
- baseagent: agent?.baseagent,
1169
+ baseagent: definition.execution.baseagent ?? agent?.baseagent,
1121
1170
  requestedModel: definition.execution.model,
1122
1171
  selfAid: definition.agentAid,
1123
1172
  });
@@ -1125,7 +1174,9 @@ async function main() {
1125
1174
  throw new Error(modelDecision.message || 'trigger model is not allowed by current role');
1126
1175
  }
1127
1176
  if (definition.execution.effort) {
1128
- const baseagent = agentRegistry.get(definition.agentAid)?.baseagent || 'claude';
1177
+ const baseagent = definition.execution.baseagent
1178
+ ?? agentRegistry.get(definition.agentAid)?.baseagent
1179
+ ?? 'claude';
1129
1180
  const effortDecision = validateRuntimeStringFieldOverride({
1130
1181
  selfAid: definition.agentAid,
1131
1182
  role: actor.role,
@@ -2132,8 +2183,12 @@ async function main() {
2132
2183
  ipcServer.setDingtalkContactBindExecutor({
2133
2184
  register: (cmd) => registerPendingDingtalkContactBind(cmd),
2134
2185
  });
2186
+ ipcServer.setWecomContactBindExecutor({
2187
+ register: (cmd) => registerPendingWecomContactBind(cmd),
2188
+ });
2135
2189
  ipcServer.setMenuExecutor((payload, auth) => cmdHandler.execMenuForEcweb(payload, auth));
2136
2190
  ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken));
2191
+ ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken));
2137
2192
  cmdHandler.setDaemonStatusProvider(() => {
2138
2193
  const aidState = controlChannel?.getAidState?.();
2139
2194
  return {
@@ -2708,7 +2763,8 @@ async function main() {
2708
2763
  }
2709
2764
  const agentAid = requireAgent(rawDefinition.agentAid);
2710
2765
  const actor = await authorizeTrigger(agentAid, 'trigger.create');
2711
- const definition = normalizeTriggerDefinition({ ...rawDefinition, origin: actor.origin });
2766
+ const materialized = materializeTriggerCreateBaseagent({ ...rawDefinition, origin: actor.origin }, agentAid);
2767
+ const definition = normalizeTriggerDefinition(materialized);
2712
2768
  validateTriggerDefinitionForActor(definition, actor);
2713
2769
  requireAgent(definition.agentAid);
2714
2770
  validateTriggerFeedbackChannels(definition);
package/dist/ipc.js CHANGED
@@ -8,6 +8,7 @@ import { HANDOFF_QUERY_MAX_LIMIT, HANDOFF_STATES } from './core/handoff/types.js
8
8
  import { resolvePaths } from './paths.js';
9
9
  import { getProcessStartTime, isSameOrOlderProcess } from './utils/process-introspect.js';
10
10
  import { classifyProcessTree, ProcessTreeSampler } from './utils/process-tree-stats.js';
11
+ import { readSystemMemoryUsage } from './utils/system-memory.js';
11
12
  const isWindows = process.platform === 'win32';
12
13
  const isNamedPipe = (p) => isWindows && p.startsWith('\\\\.\\pipe\\');
13
14
  export function summarizeMonitorAgents(agents, aids) {
@@ -45,7 +46,9 @@ export class IpcServer {
45
46
  handoffTraceExecutor;
46
47
  bindExecutor;
47
48
  configOperationExecutor;
49
+ contactOperationExecutor;
48
50
  dingtalkContactBindExecutor;
51
+ wecomContactBindExecutor;
49
52
  // CPU 占用追踪:IPC handler 是一次性同步调用,无法在响应里做 200ms 异步采样,
50
53
  // 故用后台 1s interval 累积 process.cpuUsage() 增量,handler 直接读最近值。
51
54
  // procCpuPercent = 本 daemon 进程占单核的百分比(可 >100% 仅当多核,已 clamp 到 100);
@@ -75,6 +78,9 @@ export class IpcServer {
75
78
  setConfigOperationExecutor(executor) {
76
79
  this.configOperationExecutor = executor;
77
80
  }
81
+ setContactOperationExecutor(executor) {
82
+ this.contactOperationExecutor = executor;
83
+ }
78
84
  /** Inject AUN AID state aggregator for aun-aids IPC handler */
79
85
  setAunAidProvider(provider) {
80
86
  this.aunAidProvider = provider;
@@ -136,6 +142,10 @@ export class IpcServer {
136
142
  setDingtalkContactBindExecutor(executor) {
137
143
  this.dingtalkContactBindExecutor = executor;
138
144
  }
145
+ /** Inject WeCom contact binding-code executor. */
146
+ setWecomContactBindExecutor(executor) {
147
+ this.wecomContactBindExecutor = executor;
148
+ }
139
149
  /** Start the 1s background CPU sampling loop (for monitor-snapshot). Call after start(). */
140
150
  startCpuTracking() {
141
151
  if (this.cpuTimer)
@@ -321,6 +331,16 @@ export class IpcServer {
321
331
  primaryId: cmd.primaryId,
322
332
  });
323
333
  }
334
+ case 'wecom.contact-bind.register': {
335
+ if (!this.wecomContactBindExecutor) {
336
+ return { ok: false, error: 'wecom contact bind executor not configured' };
337
+ }
338
+ return this.wecomContactBindExecutor.register({
339
+ selfAid: cmd.selfAid,
340
+ channelName: cmd.channelName,
341
+ primaryId: cmd.primaryId,
342
+ });
343
+ }
324
344
  case 'aun-aids': {
325
345
  const aids = this.aunAidProvider ? this.aunAidProvider() : [];
326
346
  return { ok: true, aids };
@@ -527,6 +547,18 @@ export class IpcServer {
527
547
  }
528
548
  return await this.configOperationExecutor(argv, sessionId, delegationToken);
529
549
  }
550
+ case 'contact.op': {
551
+ if (!this.contactOperationExecutor)
552
+ return { ok: false, code: 'NOT_CONFIGURED', error: 'contact.op not configured' };
553
+ const { argv, sessionId, delegationToken } = cmd;
554
+ if (!Array.isArray(argv) || argv.some(value => typeof value !== 'string') || !sessionId) {
555
+ return { ok: false, code: 'INVALID_REQUEST', error: 'missing argv or sessionId' };
556
+ }
557
+ if (delegationToken !== undefined && typeof delegationToken !== 'string') {
558
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegationToken must be a string' };
559
+ }
560
+ return await this.contactOperationExecutor(argv, sessionId, delegationToken);
561
+ }
530
562
  case 'trigger.list':
531
563
  case 'trigger.show':
532
564
  case 'trigger.history':
@@ -633,8 +665,7 @@ export class IpcServer {
633
665
  case 'monitor-snapshot': {
634
666
  // watch web Monitor 页用:进程级 + 系统级运行指标 + 全局 stats + per-agent 汇总。
635
667
  const mem = process.memoryUsage();
636
- const totalMem = os.totalmem();
637
- const freeMem = os.freemem();
668
+ const systemMemory = readSystemMemoryUsage();
638
669
  const aids = this.aunAidProvider ? this.aunAidProvider() : [];
639
670
  const aidStats = this.aunAidStatsProvider ? this.aunAidStatsProvider() : [];
640
671
  const statsMap = new Map(aidStats.map((s) => [s.aid, s]));
@@ -686,9 +717,7 @@ export class IpcServer {
686
717
  processTree: processTreeDetails,
687
718
  // 系统级:整机
688
719
  system: {
689
- memTotal: totalMem,
690
- memUsed: totalMem - freeMem,
691
- memFree: freeMem,
720
+ ...systemMemory,
692
721
  cpuPercent: Math.round(this.sysCpuPercent * 10) / 10,
693
722
  loadAvg: os.loadavg(), // [1m, 5m, 15m](Windows 恒 0)
694
723
  },
@@ -11,14 +11,20 @@ let _priceCacheTs = 0;
11
11
  const PRICE_CACHE_TTL = 5 * 60 * 1000;
12
12
  /**
13
13
  * 从包路径 + 用户路径合并读取 JSONL 文件。
14
- * 包路径($PACKAGE_ROOT/data/stats/)为基线,用户路径($EVOLCORE_HOME/data/stats/)为追加/覆盖。
14
+ * 包基线随构建产物发布在 dist/utils,源码运行时回退到 src/utils;
15
+ * 用户路径($EVOLCORE_HOME/data/stats/)为追加/覆盖。
15
16
  * 两层合并(append),用户层行追加在包层之后——查价时 effective_from 越大越优先,天然正确。
16
17
  */
17
18
  function _loadJsonlMerged(evolcoreHome, filename) {
18
19
  const results = [];
19
20
  // 1. 包路径(基线)
20
- const pkgFile = path.join(getPackageRoot(), 'data', 'stats', filename);
21
- if (fs.existsSync(pkgFile)) {
21
+ const packageRoot = getPackageRoot();
22
+ const pkgFile = [
23
+ path.join(packageRoot, 'dist', 'utils', filename),
24
+ path.join(packageRoot, 'src', 'utils', filename),
25
+ path.join(packageRoot, 'data', 'stats', filename),
26
+ ].find(candidate => fs.existsSync(candidate));
27
+ if (pkgFile) {
22
28
  try {
23
29
  const lines = fs.readFileSync(pkgFile, 'utf-8').split('\n').filter(Boolean);
24
30
  for (const l of lines)
@@ -28,7 +34,7 @@ function _loadJsonlMerged(evolcoreHome, filename) {
28
34
  }
29
35
  // 2. 用户路径(追加/覆盖)
30
36
  const userFile = path.join(evolcoreHome, 'data', 'stats', filename);
31
- if (fs.existsSync(userFile)) {
37
+ if (fs.existsSync(userFile) && (!pkgFile || path.resolve(userFile) !== path.resolve(pkgFile))) {
32
38
  try {
33
39
  const lines = fs.readFileSync(userFile, 'utf-8').split('\n').filter(Boolean);
34
40
  for (const l of lines)
@@ -81,10 +87,16 @@ export function resolvePriceRow(evolcoreHome, model, ts) {
81
87
  export const BILLING_FNS = {
82
88
  // 通用 per-token(Claude / OpenAI 兼容 / Kimi / MiniMax)
83
89
  per_token_v1: (e, p) => {
84
- const r = (p.price_input ?? 0) * e.input_tokens / 1e6
85
- + (p.price_output ?? 0) * e.output_tokens / 1e6
86
- + (p.price_cache_creation ?? 0) * e.cache_creation_tokens / 1e6
87
- + (p.price_cache_read ?? 0) * e.cache_read_tokens / 1e6;
90
+ const tiers = Array.isArray(p.tiers)
91
+ ? p.tiers
92
+ : [];
93
+ const promptTokens = e.input_tokens + e.cache_creation_tokens + e.cache_read_tokens;
94
+ const tier = tiers.find(t => t.up_to_input_tokens == null || promptTokens <= t.up_to_input_tokens);
95
+ const price = tier ?? p;
96
+ const r = (price.price_input ?? 0) * e.input_tokens / 1e6
97
+ + (price.price_output ?? 0) * e.output_tokens / 1e6
98
+ + (price.price_cache_creation ?? 0) * e.cache_creation_tokens / 1e6
99
+ + (price.price_cache_read ?? 0) * e.cache_read_tokens / 1e6;
88
100
  return p.currency === 'CNY' ? { cny: r } : { usd: r };
89
101
  },
90
102
  // DeepSeek cache_hit / cache_miss 口径
@@ -72,6 +72,9 @@ export class TriggerDefinitionManager {
72
72
  ...(origin ? { origin } : {}),
73
73
  }, { now: Date.now() });
74
74
  this.assertAgent(updated);
75
+ if (existing.execution.baseagent !== updated.execution.baseagent) {
76
+ throw new Error('execution.baseagent is immutable; create a new Trigger to use another baseagent');
77
+ }
75
78
  updated.createdAt = existing.createdAt;
76
79
  updated.updatedAt = Date.now();
77
80
  this.assertUnique(updated, triggerId);
@@ -5,6 +5,14 @@ const TRIGGER_UPDATE_FLAGS = new Set([
5
5
  'model', 'effort', 'permission',
6
6
  'max-runs', 'max-duration', 'concurrency', 'missed-policy',
7
7
  ]);
8
+ const TRIGGER_CREATE_FLAGS = new Set([
9
+ 'once', 'delay', 'at', 'cron', 'every', 'event', 'tz',
10
+ 'exec', 'prompt', 'name', 'agent',
11
+ 'script-path', 'script-runtime', 'script-args', 'script-timeout',
12
+ 'feedback', 'target-channel', 'target-channel-id', 'target-session', 'target-thread-id',
13
+ 'trigger-thread', 'baseagent',
14
+ 'model', 'effort', 'permission', 'max-runs', 'max-duration',
15
+ ]);
8
16
  // Note: unquoted multi-word values (e.g. --prompt=hello world) are not supported.
9
17
  // The second word would be treated as an unknown token. Always quote multi-word values:
10
18
  // --prompt "hello world" or --prompt='hello world'
@@ -103,6 +111,14 @@ function parseModelFlag(flags) {
103
111
  return { ok: false, error: '--model 不能为空' };
104
112
  return { ok: true, value: raw };
105
113
  }
114
+ function parseBaseagentFlag(flags) {
115
+ if (!flags.has('baseagent'))
116
+ return { ok: true };
117
+ const raw = flags.get('baseagent');
118
+ if (!raw || raw === true || !raw.trim())
119
+ return { ok: false, error: '--baseagent 不能为空' };
120
+ return { ok: true, value: raw.trim() };
121
+ }
106
122
  function parseEffortFlag(flags) {
107
123
  if (!flags.has('effort'))
108
124
  return { ok: true };
@@ -251,6 +267,12 @@ function commonParsed(flags, opts = {}) {
251
267
  const deprecated = rejectDeprecatedFlags(flags);
252
268
  if (deprecated)
253
269
  return { ok: false, error: deprecated };
270
+ if (!opts.update) {
271
+ for (const flag of flags.keys()) {
272
+ if (!TRIGGER_CREATE_FLAGS.has(flag))
273
+ return { ok: false, error: `create 不支持参数 --${flag}` };
274
+ }
275
+ }
254
276
  const source = parseSourceFlags(flags, opts);
255
277
  if (!source.ok)
256
278
  return source;
@@ -281,6 +303,9 @@ function commonParsed(flags, opts = {}) {
281
303
  const model = parseModelFlag(flags);
282
304
  if (!model.ok)
283
305
  return model;
306
+ const baseagent = parseBaseagentFlag(flags);
307
+ if (!baseagent.ok)
308
+ return baseagent;
284
309
  const effort = parseEffortFlag(flags);
285
310
  if (!effort.ok)
286
311
  return effort;
@@ -327,6 +352,9 @@ function commonParsed(flags, opts = {}) {
327
352
  return { ok: false, error: '--target-channel 与 --target-channel-id 必须同时指定或同时省略' };
328
353
  }
329
354
  const inferredExecution = execution.value ?? (scriptPath ? 'script' : 'target_session');
355
+ if (baseagent.value && inferredExecution !== 'trigger_session') {
356
+ return { ok: false, error: '--baseagent 仅适用于 --exec trigger-session' };
357
+ }
330
358
  if (inferredExecution === 'script') {
331
359
  if (!scriptPath && !opts.update)
332
360
  return { ok: false, error: '--exec script 需要 --script-path' };
@@ -357,6 +385,7 @@ function commonParsed(flags, opts = {}) {
357
385
  scriptArgs,
358
386
  scriptTimeoutMs: scriptTimeout.value,
359
387
  triggerThread: triggerThread.value,
388
+ baseagent: baseagent.value,
360
389
  maxRuns: maxRuns.value,
361
390
  maxDuration: maxDuration.value,
362
391
  model: model.value,
@@ -391,6 +420,46 @@ export function parseTriggerSet(args) {
391
420
  scriptArgs: value.scriptArgs,
392
421
  scriptTimeoutMs: value.scriptTimeoutMs,
393
422
  triggerThread: value.triggerThread,
423
+ baseagent: value.baseagent,
424
+ maxRuns: value.maxRuns,
425
+ maxDuration: value.maxDuration,
426
+ model: value.model,
427
+ effort: value.effort,
428
+ permissionMode: value.permissionMode,
429
+ },
430
+ };
431
+ }
432
+ /** CLI argv parser. Unlike the slash-command string parser, this preserves
433
+ * shell-quoted values such as `--prompt "multi word text"` as one token. */
434
+ export function parseTriggerSetArgv(args) {
435
+ const parsedFlags = flagsFromArgv(args, 'create');
436
+ if (!parsedFlags.ok)
437
+ return parsedFlags;
438
+ const parsed = commonParsed(parsedFlags.flags);
439
+ if (!parsed.ok)
440
+ return parsed;
441
+ const value = parsed.value;
442
+ return {
443
+ ok: true,
444
+ value: {
445
+ scheduleType: value.scheduleType,
446
+ scheduleValue: value.scheduleValue ?? '',
447
+ timezone: value.timezone,
448
+ executionType: value.inferredExecution,
449
+ feedbackStrategy: value.feedbackStrategy,
450
+ targetChannel: value.targetChannel,
451
+ targetChannelId: value.targetChannelId,
452
+ targetSession: value.targetSession,
453
+ targetThreadId: value.targetThreadId,
454
+ agentId: value.agentId,
455
+ name: value.name,
456
+ prompt: value.prompt,
457
+ scriptPath: value.scriptPath,
458
+ scriptRuntime: value.scriptRuntime,
459
+ scriptArgs: value.scriptArgs,
460
+ scriptTimeoutMs: value.scriptTimeoutMs,
461
+ triggerThread: value.triggerThread,
462
+ baseagent: value.baseagent,
394
463
  maxRuns: value.maxRuns,
395
464
  maxDuration: value.maxDuration,
396
465
  model: value.model,
@@ -426,11 +495,17 @@ export function parseTriggerUpdate(args) {
426
495
  return parseTriggerUpdateFlags(nameOrId, flags);
427
496
  }
428
497
  export function parseTriggerUpdateArgv(nameOrId, args) {
498
+ const parsedFlags = flagsFromArgv(args, 'update');
499
+ if (!parsedFlags.ok)
500
+ return parsedFlags;
501
+ return parseTriggerUpdateFlags(nameOrId, parsedFlags.flags);
502
+ }
503
+ function flagsFromArgv(args, command) {
429
504
  const flags = new Map();
430
505
  for (let i = 0; i < args.length; i++) {
431
506
  const arg = args[i];
432
507
  if (!arg.startsWith('--'))
433
- return { ok: false, error: `无法识别 update 参数: ${arg}` };
508
+ return { ok: false, error: `无法识别 ${command} 参数: ${arg}` };
434
509
  const equals = arg.indexOf('=');
435
510
  if (equals > 2) {
436
511
  flags.set(arg.slice(2, equals), arg.slice(equals + 1));
@@ -446,7 +521,7 @@ export function parseTriggerUpdateArgv(nameOrId, args) {
446
521
  flags.set(key, true);
447
522
  }
448
523
  }
449
- return parseTriggerUpdateFlags(nameOrId, flags);
524
+ return { ok: true, flags };
450
525
  }
451
526
  function parseTriggerUpdateFlags(nameOrId, flags) {
452
527
  if (flags.size === 0)
@@ -80,7 +80,7 @@ export function replaceEditableTriggerDefinition(existing, input) {
80
80
  throw new Error('trigger definition update must be an object');
81
81
  }
82
82
  const editable = input;
83
- return normalizeTriggerDefinition({
83
+ const replacement = normalizeTriggerDefinition({
84
84
  ...editable,
85
85
  $schema_version: TRIGGER_SCHEMA_VERSION,
86
86
  id: existing.id,
@@ -90,6 +90,13 @@ export function replaceEditableTriggerDefinition(existing, input) {
90
90
  updatedAt: existing.updatedAt,
91
91
  origin: existing.origin,
92
92
  });
93
+ assertBaseagentImmutable(existing, replacement);
94
+ return replacement;
95
+ }
96
+ function assertBaseagentImmutable(existing, updated) {
97
+ if (existing.execution.baseagent !== updated.execution.baseagent) {
98
+ throw new Error('execution.baseagent is immutable; create a new Trigger to use another baseagent');
99
+ }
93
100
  }
94
101
  function applySourcePatch(updated, patch) {
95
102
  const previous = updated.source;