evolcore 0.0.25 → 0.0.26

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 (231) hide show
  1. package/CHANGELOG.md +607 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +160 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent.js +96 -46
  10. package/dist/agents/claude-runner.js +50 -12
  11. package/dist/agents/codex/app-server-client.js +13 -5
  12. package/dist/agents/codex/catalog.js +1 -1
  13. package/dist/agents/codex/diagnostics.js +34 -1
  14. package/dist/agents/codex-runner.js +121 -38
  15. package/dist/agents/ecagent-runner.js +4 -9
  16. package/dist/agents/gemini-runner.js +2 -7
  17. package/dist/agents/runner-types.js +25 -17
  18. package/dist/aun/aid/agentmd.js +81 -8
  19. package/dist/aun/aid/client.js +1 -1
  20. package/dist/aun/aid/identity.js +1 -1
  21. package/dist/aun/aid/index.js +1 -1
  22. package/dist/aun/aid/store.js +17 -13
  23. package/dist/aun/menu-client.js +74 -0
  24. package/dist/aun/msg/group.js +1 -1
  25. package/dist/aun/msg/managed-operation.js +1 -1
  26. package/dist/aun/msg/p2p.js +6 -1
  27. package/dist/aun/msg/upload.js +30 -22
  28. package/dist/channels/aun.js +329 -94
  29. package/dist/cli/agent-command.js +105 -84
  30. package/dist/cli/agent-create.js +112 -10
  31. package/dist/cli/agent.js +16 -5
  32. package/dist/cli/aun-commands.js +297 -296
  33. package/dist/cli/bench.js +20 -20
  34. package/dist/cli/config.js +47 -28
  35. package/dist/cli/contact.js +70 -13
  36. package/dist/cli/ctl-command.js +22 -22
  37. package/dist/cli/daemon-commands.js +105 -40
  38. package/dist/cli/diag.js +31 -0
  39. package/dist/cli/fs-command.js +67 -67
  40. package/dist/cli/handoff-command.js +41 -9
  41. package/dist/cli/index.js +83 -77
  42. package/dist/cli/link-rules.js +21 -21
  43. package/dist/cli/managed-command-guard.js +3 -0
  44. package/dist/cli/menu-client.js +46 -31
  45. package/dist/cli/net-check.js +22 -22
  46. package/dist/cli/queue-command.js +29 -29
  47. package/dist/cli/response.js +30 -30
  48. package/dist/cli/restart-monitor.js +87 -40
  49. package/dist/cli/stats.js +51 -51
  50. package/dist/cli/trigger-command.js +56 -56
  51. package/dist/config/builtin-roles.js +1 -0
  52. package/dist/config/config-operation-service.js +145 -0
  53. package/dist/config/contact-operation-service.js +96 -7
  54. package/dist/config/resolved-config-op.js +27 -7
  55. package/dist/core/agent-application-service.js +2 -1
  56. package/dist/core/auth/authorization-audit.js +20 -12
  57. package/dist/core/auth/operation-authorizer.js +25 -4
  58. package/dist/core/auth/operation-catalog.js +35 -4
  59. package/dist/core/auth/remote-access-store.js +40 -1
  60. package/dist/core/bootstrap-service.js +1 -1
  61. package/dist/core/command/command-handler.js +83 -26
  62. package/dist/core/command/diag-service.js +118 -0
  63. package/dist/core/command/menu-catalog.js +12 -3
  64. package/dist/core/command/menu-handler.js +47 -26
  65. package/dist/core/command/menu-protocol.js +5 -0
  66. package/dist/core/command/menu-token-store.js +41 -8
  67. package/dist/core/command/slash-gate.js +34 -4
  68. package/dist/core/command/slash-handler.js +86 -21
  69. package/dist/core/handoff/runtime.js +205 -27
  70. package/dist/core/handoff/store.js +193 -4
  71. package/dist/core/message/im-renderer.js +72 -21
  72. package/dist/core/message/message-bridge.js +69 -9
  73. package/dist/core/message/message-queue.js +26 -0
  74. package/dist/core/message/message-utils.js +1 -0
  75. package/dist/core/message/response-engine.js +47 -28
  76. package/dist/core/model/catalog-service.js +79 -49
  77. package/dist/core/model/context-window-suffix.js +33 -0
  78. package/dist/core/model/model-catalog.js +1 -1
  79. package/dist/core/model/model-permission.js +12 -3
  80. package/dist/core/permission/ec-command-parser.js +51 -12
  81. package/dist/core/permission/sandbox-runtime.js +36 -14
  82. package/dist/core/permission/tool-policy.js +2 -1
  83. package/dist/index.js +280 -163
  84. package/dist/ipc.js +54 -2
  85. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  86. package/dist/stats/billing.js +41 -15
  87. package/dist/stats/db.js +159 -159
  88. package/dist/stats/eck-vars.js +9 -9
  89. package/dist/stats/price-resolver.js +28 -10
  90. package/dist/stats/query.js +139 -139
  91. package/dist/stats/role-budget.js +6 -6
  92. package/dist/stats/writer.js +51 -51
  93. package/dist/utils/cross-platform.js +30 -1
  94. package/dist/utils/error-dict.json +160 -160
  95. package/dist/utils/error-utils.js +21 -0
  96. package/dist/utils/evolcore-version.js +129 -0
  97. package/dist/utils/logger.js +3 -1
  98. package/dist/utils/macos-autostart.js +22 -22
  99. package/dist/utils/model-prices.jsonl +23 -23
  100. package/dist/utils/npm-ops.js +176 -35
  101. package/dist/utils/welcome.js +22 -22
  102. package/ecagent/LICENSE +21 -21
  103. package/ecagent/NOTICE +11 -11
  104. package/ecagent/README.md +61 -61
  105. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  106. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  107. package/ecagent/dist/harness/messages.js +7 -7
  108. package/kits/docs/GUIDE.md +20 -20
  109. package/kits/docs/INDEX.md +66 -66
  110. package/kits/docs/aun/CHEATSHEET.md +19 -19
  111. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  112. package/kits/docs/channels/aun.md +56 -56
  113. package/kits/docs/channels/feishu.md +56 -56
  114. package/kits/docs/context-assembly.md +364 -364
  115. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  116. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  117. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  118. package/kits/docs/eck_templates/runtime.template.md +19 -19
  119. package/kits/docs/evolcore/INDEX.md +71 -71
  120. package/kits/docs/evolcore/agent-create.md +170 -165
  121. package/kits/docs/evolcore/agent.md +66 -66
  122. package/kits/docs/evolcore/aid.md +60 -60
  123. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  124. package/kits/docs/evolcore/config.md +202 -195
  125. package/kits/docs/evolcore/contact.md +75 -68
  126. package/kits/docs/evolcore/ctl.md +46 -46
  127. package/kits/docs/evolcore/event.md +216 -216
  128. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  129. package/kits/docs/evolcore/fs.md +110 -110
  130. package/kits/docs/evolcore/group-collaboration.md +243 -243
  131. package/kits/docs/evolcore/group-fs.md +17 -17
  132. package/kits/docs/evolcore/group-rules.md +251 -251
  133. package/kits/docs/evolcore/group.md +152 -152
  134. package/kits/docs/evolcore/menu-client.md +42 -34
  135. package/kits/docs/evolcore/model.md +50 -50
  136. package/kits/docs/evolcore/msg.md +166 -165
  137. package/kits/docs/evolcore/response.md +75 -75
  138. package/kits/docs/evolcore/rpc.md +37 -37
  139. package/kits/docs/evolcore/self-summary.md +29 -29
  140. package/kits/docs/evolcore/stats.md +83 -83
  141. package/kits/docs/evolcore/storage.md +50 -50
  142. package/kits/docs/evolcore/trigger.md +575 -575
  143. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  144. package/kits/docs/identity/PATH_OPS.md +16 -16
  145. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  146. package/kits/docs/identity/identity-tools.md +26 -26
  147. package/kits/docs/path-registry.md +62 -62
  148. package/kits/docs/prompt-loading-architecture.md +269 -269
  149. package/kits/docs/venues/aun-group.md +45 -45
  150. package/kits/docs/venues/aun-private.md +10 -10
  151. package/kits/docs/venues/client-desktop.md +10 -10
  152. package/kits/docs/venues/client-mobile.md +10 -10
  153. package/kits/docs/venues/feishu-group.md +13 -13
  154. package/kits/docs/venues/feishu-private.md +9 -9
  155. package/kits/docs/venues/group.md +37 -25
  156. package/kits/docs/venues/private.md +10 -10
  157. package/kits/eck_manifest.auxiliary.json +43 -43
  158. package/kits/eck_manifest.json +190 -190
  159. package/kits/eck_message_manifest.json +74 -74
  160. package/kits/migrations/README-role-config-v4.md +32 -32
  161. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  162. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  163. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  164. package/kits/migrations/rename-config-file.mjs +99 -99
  165. package/kits/rules/01-overview.md +162 -162
  166. package/kits/rules/02-navigation.md +96 -96
  167. package/kits/rules/03-identity.md +36 -36
  168. package/kits/rules/04-relation.md +75 -75
  169. package/kits/rules/05-venue.md +60 -60
  170. package/kits/rules/06-channel.md +63 -63
  171. package/kits/schemas/_meta.json +53 -53
  172. package/kits/schemas/agent-config.schema.1.json +177 -177
  173. package/kits/schemas/agent-config.schema.10.json +395 -395
  174. package/kits/schemas/agent-config.schema.11.json +421 -421
  175. package/kits/schemas/agent-config.schema.12.json +427 -427
  176. package/kits/schemas/agent-config.schema.2.json +239 -239
  177. package/kits/schemas/agent-config.schema.3.json +119 -119
  178. package/kits/schemas/agent-config.schema.4.json +208 -208
  179. package/kits/schemas/agent-config.schema.5.json +326 -326
  180. package/kits/schemas/agent-config.schema.6.json +322 -322
  181. package/kits/schemas/agent-config.schema.7.json +303 -303
  182. package/kits/schemas/agent-config.schema.8.json +304 -304
  183. package/kits/schemas/agent-config.schema.9.json +364 -364
  184. package/kits/schemas/contact-book.schema.1.json +36 -36
  185. package/kits/schemas/contact-book.schema.2.json +43 -43
  186. package/kits/schemas/contact-book.schema.3.json +67 -67
  187. package/kits/schemas/daemon.schema.1.json +91 -91
  188. package/kits/schemas/daemon.schema.2.json +101 -101
  189. package/kits/schemas/daemon.schema.3.json +123 -123
  190. package/kits/schemas/daemon.schema.4.json +136 -136
  191. package/kits/schemas/daemon.schema.5.json +135 -135
  192. package/kits/schemas/daemon.schema.6.json +131 -131
  193. package/kits/schemas/defaults.schema.1.json +81 -81
  194. package/kits/schemas/defaults.schema.2.json +85 -85
  195. package/kits/schemas/defaults.schema.3.json +73 -73
  196. package/kits/schemas/defaults.schema.4.json +107 -107
  197. package/kits/schemas/defaults.schema.5.json +119 -119
  198. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  199. package/kits/schemas/migrations/README.md +30 -30
  200. package/kits/schemas/relation-config.schema.1.json +158 -158
  201. package/kits/schemas/relation-config.schema.2.json +73 -73
  202. package/kits/schemas/relation-config.schema.3.json +50 -50
  203. package/kits/schemas/relation-config.schema.4.json +47 -47
  204. package/kits/schemas/relation-config.schema.5.json +47 -47
  205. package/kits/schemas/relation-config.schema.6.json +46 -46
  206. package/kits/schemas/relation-config.schema.7.json +59 -59
  207. package/kits/schemas/relation-config.schema.8.json +103 -103
  208. package/kits/schemas/role-config.schema.1.json +201 -201
  209. package/kits/schemas/role-registry.schema.1.json +35 -35
  210. package/kits/schemas/single-session.schema.1.json +31 -31
  211. package/kits/schemas/single-session.schema.2.json +57 -57
  212. package/kits/schemas/single-session.schema.3.json +32 -32
  213. package/kits/templates/bootstrap-welcome.md +17 -17
  214. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  215. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  216. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  217. package/kits/templates/message-fragments/inject-default.md +2 -2
  218. package/kits/templates/message-fragments/item.md +2 -2
  219. package/kits/templates/roles/admin.json +92 -92
  220. package/kits/templates/roles/member.json +96 -96
  221. package/kits/templates/roles/owner.json +9 -9
  222. package/kits/templates/roles/visitor.json +85 -85
  223. package/kits/templates/system-fragments/baseagent.md +14 -14
  224. package/kits/templates/system-fragments/bootstrap.md +31 -29
  225. package/kits/templates/system-fragments/channel.md +64 -64
  226. package/kits/templates/system-fragments/commands.md +28 -28
  227. package/kits/templates/system-fragments/identity.md +13 -13
  228. package/kits/templates/system-fragments/relation.md +19 -19
  229. package/kits/templates/system-fragments/session.md +59 -59
  230. package/kits/templates/system-fragments/venue.md +31 -31
  231. package/package.json +100 -98
package/dist/ipc.js CHANGED
@@ -76,7 +76,7 @@ function requiresLocalControlToken(cmd) {
76
76
  // Managed bootstrap completion is authenticated by the session delegation
77
77
  // ticket. A standalone CLI invocation still needs the daemon control token.
78
78
  return LOCAL_CONTROL_COMMANDS.has(String(cmd.type ?? ''))
79
- || (cmd.type === 'evolagent.bootstrapComplete' && typeof cmd.sessionId !== 'string');
79
+ || ((cmd.type === 'evolagent.bootstrapComplete' || cmd.type === 'diag') && typeof cmd.sessionId !== 'string');
80
80
  }
81
81
  export function summarizeMonitorAgents(agents, aids) {
82
82
  const connectedAgents = new Set(aids.filter(aid => aid.status === 'connected').map(aid => aid.agentName || aid.aid));
@@ -116,7 +116,10 @@ export class IpcServer {
116
116
  taskRuntimeContextProvider;
117
117
  codexPreToolUseExecutor;
118
118
  aunMsgSender;
119
+ diagExecutor;
120
+ menuClientExecutor;
119
121
  handoffReturnExecutor;
122
+ handoffRecoverReturnExecutor;
120
123
  handoffStatusExecutor;
121
124
  handoffListExecutor;
122
125
  handoffTraceExecutor;
@@ -135,6 +138,8 @@ export class IpcServer {
135
138
  controlToken;
136
139
  daemonRuntimeEpoch;
137
140
  remoteAccessService = remoteAccessService;
141
+ /** Current daemon owner used to bind locally issued ECWeb pairing codes. */
142
+ remoteAccessOwnerProvider;
138
143
  // CPU 占用追踪:IPC handler 是一次性同步调用,无法在响应里做 200ms 异步采样,
139
144
  // 故用后台 1s interval 累积 process.cpuUsage() 增量,handler 直接读最近值。
140
145
  // procCpuPercent = 本 daemon 进程占单核的百分比(可 >100% 仅当多核,已 clamp 到 100);
@@ -189,6 +194,15 @@ export class IpcServer {
189
194
  setRemoteAccessService(service) {
190
195
  this.remoteAccessService = service;
191
196
  }
197
+ /**
198
+ * Supply the live process-level owner for pairing requests that do not carry
199
+ * an explicit owner (for example `ec watch web` and the ECWeb supervisor).
200
+ * The provider is evaluated for every request so owner updates take effect
201
+ * without restarting ECWeb.
202
+ */
203
+ setRemoteAccessOwnerProvider(provider) {
204
+ this.remoteAccessOwnerProvider = provider;
205
+ }
192
206
  /** Set the process-lifetime marker used to reject stale managed children. */
193
207
  setDaemonRuntimeEpoch(epoch) {
194
208
  this.daemonRuntimeEpoch = epoch;
@@ -362,10 +376,15 @@ export class IpcServer {
362
376
  setAunMsgSender(sender) {
363
377
  this.aunMsgSender = sender;
364
378
  }
379
+ setDiagExecutor(executor) { this.diagExecutor = executor; }
380
+ setMenuClientExecutor(executor) { this.menuClientExecutor = executor; }
365
381
  /** Inject internal handoff result return executor for `ec handoff return`. */
366
382
  setHandoffReturnExecutor(executor) {
367
383
  this.handoffReturnExecutor = executor;
368
384
  }
385
+ setHandoffRecoverReturnExecutor(executor) {
386
+ this.handoffRecoverReturnExecutor = executor;
387
+ }
369
388
  setHandoffStatusExecutor(executor) {
370
389
  this.handoffStatusExecutor = executor;
371
390
  }
@@ -807,6 +826,10 @@ export class IpcServer {
807
826
  };
808
827
  }
809
828
  switch (cmd.type) {
829
+ case 'diag':
830
+ return this.diagExecutor ? this.diagExecutor(cmd) : { ok: false, code: 'NOT_CONFIGURED', message: '诊断入口未配置' };
831
+ case 'menu-client.request':
832
+ return this.menuClientExecutor ? this.menuClientExecutor(cmd) : { ok: false, code: 'NOT_CONFIGURED', error: 'Managed Menu client is not configured' };
810
833
  case 'model.catalog': {
811
834
  if ((cmd.self !== undefined && typeof cmd.self !== 'string')
812
835
  || (cmd.baseagent !== undefined && !['claude', 'codex', 'ecagent', 'gemini'].includes(cmd.baseagent))) {
@@ -887,7 +910,9 @@ export class IpcServer {
887
910
  });
888
911
  }
889
912
  case 'remote-auth.pair-code': {
890
- const pair = this.remoteAccessService.getPairingCode(typeof cmd.authorizedBy === 'string' ? cmd.authorizedBy : undefined);
913
+ const requestedOwner = typeof cmd.authorizedBy === 'string' ? cmd.authorizedBy.trim() : undefined;
914
+ const authorizedBy = requestedOwner || this.remoteAccessOwnerProvider?.();
915
+ const pair = this.remoteAccessService.getPairingCode(authorizedBy);
891
916
  return { ok: true, ...pair };
892
917
  }
893
918
  case 'remote-auth.issue-local': {
@@ -1094,6 +1119,10 @@ export class IpcServer {
1094
1119
  return { ok: true, context };
1095
1120
  }
1096
1121
  case 'aun-msg-send': {
1122
+ if (cmd.handoffContext !== undefined && (typeof cmd.handoffContext !== 'string'
1123
+ || !cmd.handoffContext.trim() || cmd.handoffContext.length > 64 * 1024 || cmd.scope === 'group')) {
1124
+ return { ok: false, code: 'INVALID_HANDOFF_CONTEXT', error: 'Private handoff context must contain 1–65536 characters' };
1125
+ }
1097
1126
  if (!this.aunMsgSender)
1098
1127
  return { ok: false, error: 'aun msg sender not configured' };
1099
1128
  if (!cmd.aid || typeof cmd.aid !== 'string')
@@ -1148,6 +1177,7 @@ export class IpcServer {
1148
1177
  encrypt: typeof cmd.encrypt === 'boolean' ? cmd.encrypt : undefined,
1149
1178
  thread: typeof cmd.thread === 'string' ? cmd.thread : undefined,
1150
1179
  returnPolicy: cmd.returnPolicy === 'required' || cmd.returnPolicy === 'none' ? cmd.returnPolicy : undefined,
1180
+ handoffContext: cmd.handoffContext,
1151
1181
  originSessionId: typeof cmd.originSessionId === 'string' ? cmd.originSessionId : undefined,
1152
1182
  originMessageId: typeof cmd.originMessageId === 'string' ? cmd.originMessageId : undefined,
1153
1183
  delegationToken: typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined,
@@ -1177,6 +1207,28 @@ export class IpcServer {
1177
1207
  return { ok: false, error: e?.message || String(e) };
1178
1208
  }
1179
1209
  }
1210
+ case 'handoff-recover-return': {
1211
+ if (!this.handoffRecoverReturnExecutor)
1212
+ return { ok: false, error: 'handoff recovery not configured' };
1213
+ if (typeof cmd.sessionId !== 'string' || !cmd.sessionId)
1214
+ return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'missing sessionId' };
1215
+ if (typeof cmd.handoffId !== 'string' || !cmd.handoffId)
1216
+ return { ok: false, code: 'INVALID_HANDOFF_ID', error: 'missing handoff_id' };
1217
+ if (typeof cmd.content !== 'string')
1218
+ return { ok: false, code: 'HANDOFF_RETURN_CONTENT_REQUIRED', error: 'missing content' };
1219
+ try {
1220
+ return await this.handoffRecoverReturnExecutor({
1221
+ sessionId: cmd.sessionId,
1222
+ handoffId: cmd.handoffId,
1223
+ content: cmd.content,
1224
+ delegationToken: typeof cmd.delegationToken === 'string' ? cmd.delegationToken : undefined,
1225
+ delegationCommandHash: typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined,
1226
+ });
1227
+ }
1228
+ catch (e) {
1229
+ return { ok: false, error: e?.message || String(e) };
1230
+ }
1231
+ }
1180
1232
  case 'handoff-status': {
1181
1233
  if (!this.handoffStatusExecutor)
1182
1234
  return { ok: false, code: 'HANDOFF_STATUS_UNAVAILABLE', error: 'handoff status not configured' };
@@ -12,7 +12,18 @@ const STATE_KEY = 'proactive';
12
12
  const TOOL_REPORT_INTERVAL = 10;
13
13
  const SEND_RECEIPT_RE = /["']?(?:message[_ -]?id|handoff[_ -]?id)["']?\s*[:=]\s*["']?([A-Za-z0-9][A-Za-z0-9._:-]*)/gi;
14
14
  const HANDOFF_TEXT_RECEIPT_RE = /\bhandoff\s+(h-[A-Za-z0-9._-]+)/i;
15
- function hasSuccessfulSendReceipt(result, isError) {
15
+ const RETURN_CONTENT_RE = /["']?(?:return[_ -]?content|returnContent)["']?\s*[:=]\s*["']?([^}\n,]*)/i;
16
+ function hasRequiredReturnFlag(toolInput) {
17
+ if (!toolInput || typeof toolInput !== 'object')
18
+ return false;
19
+ const input = toolInput;
20
+ const commandValues = ['command', 'cmd', 'script'].flatMap(key => {
21
+ const value = input[key];
22
+ return typeof value === 'string' ? [value] : [];
23
+ });
24
+ return commandValues.some(command => /(?:^|\s)--return(?:=|\s+)['"]?required(?:['"]?)(?=\s|$)/i.test(command));
25
+ }
26
+ function hasSuccessfulSendReceipt(result, isError, requiresBusinessReturn = false) {
16
27
  if (isError === true)
17
28
  return false;
18
29
  const hasReceiptValue = (value) => {
@@ -40,6 +51,41 @@ function hasSuccessfulSendReceipt(result, isError) {
40
51
  };
41
52
  if (hasExplicitFailure(result, new Set()))
42
53
  return false;
54
+ const isBusinessReturn = (value) => {
55
+ if (typeof value !== 'string')
56
+ return false;
57
+ const normalized = value.trim();
58
+ if (!normalized || /^(?:null|undefined|-)$/.test(normalized))
59
+ return false;
60
+ // Contact admission/refusal is a real reply, but it is not task completion.
61
+ if (/(?:当前不接受陌生人消息|可发送\s*\/request|contact\s+(?:required|request)|stranger message)/i.test(normalized))
62
+ return false;
63
+ return true;
64
+ };
65
+ const inspectBusinessReturn = (value, seen) => {
66
+ if (!value || typeof value !== 'object')
67
+ return false;
68
+ if (seen.has(value))
69
+ return false;
70
+ seen.add(value);
71
+ if (Array.isArray(value))
72
+ return value.some(item => inspectBusinessReturn(item, seen));
73
+ for (const [key, item] of Object.entries(value)) {
74
+ if ((key === 'return_content' || key === 'returnContent') && isBusinessReturn(item))
75
+ return true;
76
+ if (inspectBusinessReturn(item, seen))
77
+ return true;
78
+ }
79
+ return false;
80
+ };
81
+ if (requiresBusinessReturn) {
82
+ if (inspectBusinessReturn(result, new Set()))
83
+ return true;
84
+ const returnText = typeof result === 'string'
85
+ ? result.match(RETURN_CONTENT_RE)?.[1]
86
+ : undefined;
87
+ return isBusinessReturn(returnText);
88
+ }
43
89
  const inspect = (value, seen) => {
44
90
  if (!value || typeof value !== 'object')
45
91
  return false;
@@ -153,6 +199,11 @@ export const proactiveFlow = {
153
199
  // Codex only invokes this hook for local tool paths. Hosted tools stay
154
200
  // enabled and do not satisfy or clear the pending first-send gate.
155
201
  const isSendCommand = ctx.isSendCommand(toolName, toolInput);
202
+ if (!isSendCommand && ((state.firstSendRequired && !state.firstToolDone) || state.toolReportPending)) {
203
+ const sendFailure = ctx.describeSendCommandFailure?.(toolName, toolInput);
204
+ if (sendFailure)
205
+ return { block: true, policyCode: 'proactive_send_invalid', reason: sendFailure };
206
+ }
156
207
  if (state.firstSendRequired && !state.firstToolDone) {
157
208
  if (!isSendCommand) {
158
209
  const cmdHint = state.chatType === 'group' ? 'ec group send' : 'ec msg send';
@@ -221,7 +272,13 @@ export const proactiveFlow = {
221
272
  return;
222
273
  if (!ctx.isSendCommand(ctx.toolName, ctx.toolInput))
223
274
  return;
224
- if (!hasSuccessfulSendReceipt(ctx.result, ctx.isError || !!ctx.error))
275
+ const requiresBusinessReturn = hasRequiredReturnFlag(ctx.toolInput)
276
+ && (typeof ctx.result === 'string'
277
+ ? HANDOFF_TEXT_RECEIPT_RE.test(ctx.result)
278
+ : !!ctx.result && typeof ctx.result === 'object'
279
+ && ('handoff_id' in ctx.result || 'handoffId' in ctx.result));
280
+ HANDOFF_TEXT_RECEIPT_RE.lastIndex = 0;
281
+ if (!hasSuccessfulSendReceipt(ctx.result, ctx.isError || !!ctx.error, requiresBusinessReturn))
225
282
  return;
226
283
  if (state.firstSendRequired && !state.firstToolDone)
227
284
  state.firstToolDone = true;
@@ -5,6 +5,7 @@
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
7
  import { getPackageRoot } from '../paths.js';
8
+ import { parseModelId, stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
8
9
  // 模型参数缓存(进程内,5 分钟 TTL)
9
10
  let _parameterCache = null;
10
11
  let _parameterCacheTs = 0;
@@ -79,21 +80,36 @@ function loadPrices(evolcoreHome) {
79
80
  _priceCacheTs = now;
80
81
  return _priceCache;
81
82
  }
83
+ /**
84
+ * 按优先级列出一个模型 ID 的查表候选键。
85
+ *
86
+ * 上下文窗口变体(`claude-opus-5[1m]`)与基础模型同价,价表只需一条基础条目;
87
+ * 但仍先试精确匹配,好让用户能给某个变体单独定价。
88
+ */
89
+ export function modelLookupKeys(evolcoreHome, model) {
90
+ const keys = [];
91
+ const push = (key) => { if (key && !keys.includes(key))
92
+ keys.push(key); };
93
+ push(model);
94
+ push(resolveCanonicalModel(evolcoreHome, model));
95
+ const base = stripContextWindowSuffix(model);
96
+ if (base !== model) {
97
+ push(base);
98
+ push(resolveCanonicalModel(evolcoreHome, base));
99
+ }
100
+ return keys;
101
+ }
82
102
  /** 取 model 在 ts 时刻生效的价格行(effective_from <= ts 中最新的一条)。
83
- * 精确匹配失败时,通过 model-aliases.jsonl 映射到规范 ID 再查一次。 */
103
+ * 精确匹配失败时,依次回落到 alias 规范 ID 和去掉窗口后缀的基础 ID。 */
84
104
  export function resolvePriceRow(evolcoreHome, model, ts) {
85
105
  const prices = loadPrices(evolcoreHome);
86
- let candidates = prices.filter(p => p.model === model && p.effective_from <= ts);
87
- if (!candidates.length) {
88
- // fallback: alias → canonical
89
- const canonical = resolveCanonicalModel(evolcoreHome, model);
90
- if (canonical !== model) {
91
- candidates = prices.filter(p => p.model === canonical && p.effective_from <= ts);
106
+ for (const key of modelLookupKeys(evolcoreHome, model)) {
107
+ const candidates = prices.filter(p => p.model === key && p.effective_from <= ts);
108
+ if (candidates.length) {
109
+ return candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
92
110
  }
93
111
  }
94
- if (!candidates.length)
95
- return null;
96
- return candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
112
+ return null;
97
113
  }
98
114
  export const BILLING_FNS = {
99
115
  // 通用 per-token(Claude / OpenAI 兼容 / Kimi / MiniMax)
@@ -164,14 +180,24 @@ function loadSpecs(evolcoreHome) {
164
180
  _specCacheTs = now;
165
181
  return _specCache;
166
182
  }
167
- /** 取模型在 ts 时刻的能力参数。找不到时返回默认值。 */
183
+ /** 取模型在 ts 时刻的能力参数。找不到时返回默认值。
184
+ * 带窗口后缀的 ID 复用基础模型的参数行,但窗口由后缀决定(网关的权威声明)。 */
168
185
  export function resolveModelSpec(evolcoreHome, model, ts) {
169
186
  const specs = loadSpecs(evolcoreHome);
170
187
  const t = ts ?? Date.now();
171
- const candidates = specs.filter(s => s.model === model && s.effective_from <= t);
172
- if (candidates.length) {
173
- return candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
188
+ const suffixWindow = parseModelId(model).contextWindow;
189
+ for (const key of modelLookupKeys(evolcoreHome, model)) {
190
+ const candidates = specs.filter(s => s.model === key && s.effective_from <= t);
191
+ if (!candidates.length)
192
+ continue;
193
+ const row = candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
194
+ if (suffixWindow === undefined || suffixWindow === row.context_window)
195
+ return row;
196
+ // 后缀声明的窗口覆盖基础行,输入上限按原行的预留比例等比缩放。
197
+ const ratio = row.context_window > 0 ? row.max_input_tokens / row.context_window : 0.9;
198
+ return { ...row, model, context_window: suffixWindow, max_input_tokens: Math.floor(suffixWindow * ratio) };
174
199
  }
175
200
  // 默认值
176
- return { model, effective_from: 0, context_window: 200000, max_input_tokens: 180000, max_output_tokens: 8192 };
201
+ const contextWindow = suffixWindow ?? 200000;
202
+ return { model, effective_from: 0, context_window: contextWindow, max_input_tokens: Math.floor(contextWindow * 0.9), max_output_tokens: 8192 };
177
203
  }
package/dist/stats/db.js CHANGED
@@ -85,134 +85,134 @@ export function openReadonlyDb(dbPath) {
85
85
  }
86
86
  }
87
87
  function _initTables(db) {
88
- db.exec(`
89
- CREATE TABLE IF NOT EXISTS usage_events (
90
- id INTEGER PRIMARY KEY AUTOINCREMENT,
91
- ts INTEGER NOT NULL,
92
- agent_aid TEXT NOT NULL,
93
- peer_key TEXT NOT NULL,
94
- usage_subject_key TEXT NOT NULL DEFAULT '',
95
- role TEXT NOT NULL DEFAULT '',
96
- peer_type TEXT,
97
- session_id TEXT,
98
- model TEXT NOT NULL,
99
- billing_fn TEXT NOT NULL,
100
- input_tokens INTEGER NOT NULL DEFAULT 0,
101
- output_tokens INTEGER NOT NULL DEFAULT 0,
102
- cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
103
- cache_read_tokens INTEGER NOT NULL DEFAULT 0,
104
- cache_hit_tokens INTEGER,
105
- cache_miss_tokens INTEGER,
106
- image_tokens INTEGER,
107
- total_context_tokens INTEGER,
108
- turns INTEGER NOT NULL DEFAULT 1,
109
- duration_ms INTEGER,
110
- context_window_pct REAL,
111
- cost_official_usd REAL,
112
- cost_official_cny REAL,
113
- cost_gateway_usd REAL,
114
- cost_gateway_cny REAL
115
- );
116
- CREATE INDEX IF NOT EXISTS idx_ue_ts ON usage_events(ts);
117
- CREATE INDEX IF NOT EXISTS idx_ue_agent_ts ON usage_events(agent_aid, ts);
118
- CREATE INDEX IF NOT EXISTS idx_ue_peer_ts ON usage_events(agent_aid, peer_key, ts);
119
- CREATE INDEX IF NOT EXISTS idx_ue_model_ts ON usage_events(model, ts);
120
- CREATE INDEX IF NOT EXISTS idx_ue_session_ts ON usage_events(session_id, ts);
121
-
122
- CREATE TABLE IF NOT EXISTS context_breakdown (
123
- id INTEGER PRIMARY KEY AUTOINCREMENT,
124
- ts INTEGER NOT NULL,
125
- agent_aid TEXT NOT NULL,
126
- session_id TEXT NOT NULL,
127
- turn_count INTEGER NOT NULL,
128
- model TEXT NOT NULL,
129
- max_tokens INTEGER NOT NULL,
130
- system_prompt INTEGER,
131
- system_tools INTEGER,
132
- mcp_tools INTEGER,
133
- custom_agents INTEGER,
134
- memory_files INTEGER,
135
- skills INTEGER,
136
- messages INTEGER,
137
- free_space INTEGER,
138
- total_estimated INTEGER
139
- );
140
- CREATE INDEX IF NOT EXISTS idx_cb_session ON context_breakdown(session_id, ts);
141
- CREATE INDEX IF NOT EXISTS idx_cb_agent ON context_breakdown(agent_aid, ts);
142
-
143
- CREATE TABLE IF NOT EXISTS message_events (
144
- id INTEGER PRIMARY KEY AUTOINCREMENT,
145
- ts INTEGER NOT NULL,
146
- agent_aid TEXT NOT NULL,
147
- peer_key TEXT NOT NULL,
148
- direction TEXT NOT NULL,
149
- msg_type TEXT,
150
- bytes INTEGER NOT NULL DEFAULT 0,
151
- encrypted INTEGER DEFAULT 0,
152
- chatmode TEXT
153
- );
154
- CREATE INDEX IF NOT EXISTS idx_me_ts ON message_events(ts);
155
- CREATE INDEX IF NOT EXISTS idx_me_agent_ts ON message_events(agent_aid, ts);
156
- CREATE INDEX IF NOT EXISTS idx_me_peer_ts ON message_events(agent_aid, peer_key, ts);
157
-
158
- -- 按天预聚合表:grain = 天 × agent × peer × session × model × billing_fn。
159
- -- 由 writer.ts 写时增量 UPSERT 维护,rebuildDailyRollup() 全量重建纠偏。
160
- -- 保留 model+billing_fn 维度以便查询时仍按 calcCost 现算成本。
161
- -- 日级行很小,永留主库、不参与年度归档。
162
- CREATE TABLE IF NOT EXISTS usage_daily (
163
- day TEXT NOT NULL, -- 'YYYY-MM-DD' localtime
164
- agent_aid TEXT NOT NULL,
165
- peer_key TEXT NOT NULL,
166
- peer_type TEXT NOT NULL DEFAULT '',-- 'private' | 'group',由 peer_key 唯一决定
167
- session_id TEXT NOT NULL DEFAULT '',
168
- model TEXT NOT NULL,
169
- billing_fn TEXT NOT NULL,
170
- input_tokens INTEGER NOT NULL DEFAULT 0,
171
- output_tokens INTEGER NOT NULL DEFAULT 0,
172
- cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
173
- cache_read_tokens INTEGER NOT NULL DEFAULT 0,
174
- cache_hit_tokens INTEGER NOT NULL DEFAULT 0,
175
- cache_miss_tokens INTEGER NOT NULL DEFAULT 0,
176
- image_tokens INTEGER NOT NULL DEFAULT 0,
177
- total_context_tokens INTEGER NOT NULL DEFAULT 0,
178
- turns INTEGER NOT NULL DEFAULT 0,
179
- calls INTEGER NOT NULL DEFAULT 0,
180
- cost_official_usd REAL,
181
- cost_official_cny REAL,
182
- cost_gateway_usd REAL,
183
- cost_gateway_cny REAL,
184
- PRIMARY KEY (day, agent_aid, peer_key, session_id, model, billing_fn)
185
- );
186
- CREATE INDEX IF NOT EXISTS idx_ud_day ON usage_daily(day);
187
- CREATE INDEX IF NOT EXISTS idx_ud_session ON usage_daily(session_id);
188
-
189
- -- 大模型调用明细:每次大模型调用一行(尽力而为;非 Claude 拿不到逐次时降级为单行累计)。
190
- -- 关联三个 ID:task_id(一次 runQuery)、session_id(evolcore 会话)、agent_session_id(SDK 会话)。
191
- CREATE TABLE IF NOT EXISTS model_calls (
192
- id INTEGER PRIMARY KEY AUTOINCREMENT,
193
- ts INTEGER NOT NULL,
194
- task_id TEXT NOT NULL,
195
- session_id TEXT,
196
- agent_session_id TEXT,
197
- agent_aid TEXT NOT NULL,
198
- peer_key TEXT NOT NULL,
199
- call_index INTEGER NOT NULL,
200
- model TEXT NOT NULL,
201
- request_id TEXT,
202
- message_id TEXT,
203
- input_tokens INTEGER NOT NULL DEFAULT 0,
204
- output_tokens INTEGER NOT NULL DEFAULT 0,
205
- cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
206
- cache_read_tokens INTEGER NOT NULL DEFAULT 0,
207
- context_tokens INTEGER,
208
- max_tokens INTEGER,
209
- auto_compact_tokens INTEGER,
210
- degraded INTEGER NOT NULL DEFAULT 0
211
- );
212
- CREATE INDEX IF NOT EXISTS idx_mc_task ON model_calls(task_id);
213
- CREATE INDEX IF NOT EXISTS idx_mc_session ON model_calls(session_id, ts);
214
- CREATE INDEX IF NOT EXISTS idx_mc_agentsession ON model_calls(agent_session_id);
215
- CREATE INDEX IF NOT EXISTS idx_mc_ts ON model_calls(ts);
88
+ db.exec(`
89
+ CREATE TABLE IF NOT EXISTS usage_events (
90
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
91
+ ts INTEGER NOT NULL,
92
+ agent_aid TEXT NOT NULL,
93
+ peer_key TEXT NOT NULL,
94
+ usage_subject_key TEXT NOT NULL DEFAULT '',
95
+ role TEXT NOT NULL DEFAULT '',
96
+ peer_type TEXT,
97
+ session_id TEXT,
98
+ model TEXT NOT NULL,
99
+ billing_fn TEXT NOT NULL,
100
+ input_tokens INTEGER NOT NULL DEFAULT 0,
101
+ output_tokens INTEGER NOT NULL DEFAULT 0,
102
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
103
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
104
+ cache_hit_tokens INTEGER,
105
+ cache_miss_tokens INTEGER,
106
+ image_tokens INTEGER,
107
+ total_context_tokens INTEGER,
108
+ turns INTEGER NOT NULL DEFAULT 1,
109
+ duration_ms INTEGER,
110
+ context_window_pct REAL,
111
+ cost_official_usd REAL,
112
+ cost_official_cny REAL,
113
+ cost_gateway_usd REAL,
114
+ cost_gateway_cny REAL
115
+ );
116
+ CREATE INDEX IF NOT EXISTS idx_ue_ts ON usage_events(ts);
117
+ CREATE INDEX IF NOT EXISTS idx_ue_agent_ts ON usage_events(agent_aid, ts);
118
+ CREATE INDEX IF NOT EXISTS idx_ue_peer_ts ON usage_events(agent_aid, peer_key, ts);
119
+ CREATE INDEX IF NOT EXISTS idx_ue_model_ts ON usage_events(model, ts);
120
+ CREATE INDEX IF NOT EXISTS idx_ue_session_ts ON usage_events(session_id, ts);
121
+
122
+ CREATE TABLE IF NOT EXISTS context_breakdown (
123
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
124
+ ts INTEGER NOT NULL,
125
+ agent_aid TEXT NOT NULL,
126
+ session_id TEXT NOT NULL,
127
+ turn_count INTEGER NOT NULL,
128
+ model TEXT NOT NULL,
129
+ max_tokens INTEGER NOT NULL,
130
+ system_prompt INTEGER,
131
+ system_tools INTEGER,
132
+ mcp_tools INTEGER,
133
+ custom_agents INTEGER,
134
+ memory_files INTEGER,
135
+ skills INTEGER,
136
+ messages INTEGER,
137
+ free_space INTEGER,
138
+ total_estimated INTEGER
139
+ );
140
+ CREATE INDEX IF NOT EXISTS idx_cb_session ON context_breakdown(session_id, ts);
141
+ CREATE INDEX IF NOT EXISTS idx_cb_agent ON context_breakdown(agent_aid, ts);
142
+
143
+ CREATE TABLE IF NOT EXISTS message_events (
144
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
145
+ ts INTEGER NOT NULL,
146
+ agent_aid TEXT NOT NULL,
147
+ peer_key TEXT NOT NULL,
148
+ direction TEXT NOT NULL,
149
+ msg_type TEXT,
150
+ bytes INTEGER NOT NULL DEFAULT 0,
151
+ encrypted INTEGER DEFAULT 0,
152
+ chatmode TEXT
153
+ );
154
+ CREATE INDEX IF NOT EXISTS idx_me_ts ON message_events(ts);
155
+ CREATE INDEX IF NOT EXISTS idx_me_agent_ts ON message_events(agent_aid, ts);
156
+ CREATE INDEX IF NOT EXISTS idx_me_peer_ts ON message_events(agent_aid, peer_key, ts);
157
+
158
+ -- 按天预聚合表:grain = 天 × agent × peer × session × model × billing_fn。
159
+ -- 由 writer.ts 写时增量 UPSERT 维护,rebuildDailyRollup() 全量重建纠偏。
160
+ -- 保留 model+billing_fn 维度以便查询时仍按 calcCost 现算成本。
161
+ -- 日级行很小,永留主库、不参与年度归档。
162
+ CREATE TABLE IF NOT EXISTS usage_daily (
163
+ day TEXT NOT NULL, -- 'YYYY-MM-DD' localtime
164
+ agent_aid TEXT NOT NULL,
165
+ peer_key TEXT NOT NULL,
166
+ peer_type TEXT NOT NULL DEFAULT '',-- 'private' | 'group',由 peer_key 唯一决定
167
+ session_id TEXT NOT NULL DEFAULT '',
168
+ model TEXT NOT NULL,
169
+ billing_fn TEXT NOT NULL,
170
+ input_tokens INTEGER NOT NULL DEFAULT 0,
171
+ output_tokens INTEGER NOT NULL DEFAULT 0,
172
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
173
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
174
+ cache_hit_tokens INTEGER NOT NULL DEFAULT 0,
175
+ cache_miss_tokens INTEGER NOT NULL DEFAULT 0,
176
+ image_tokens INTEGER NOT NULL DEFAULT 0,
177
+ total_context_tokens INTEGER NOT NULL DEFAULT 0,
178
+ turns INTEGER NOT NULL DEFAULT 0,
179
+ calls INTEGER NOT NULL DEFAULT 0,
180
+ cost_official_usd REAL,
181
+ cost_official_cny REAL,
182
+ cost_gateway_usd REAL,
183
+ cost_gateway_cny REAL,
184
+ PRIMARY KEY (day, agent_aid, peer_key, session_id, model, billing_fn)
185
+ );
186
+ CREATE INDEX IF NOT EXISTS idx_ud_day ON usage_daily(day);
187
+ CREATE INDEX IF NOT EXISTS idx_ud_session ON usage_daily(session_id);
188
+
189
+ -- 大模型调用明细:每次大模型调用一行(尽力而为;非 Claude 拿不到逐次时降级为单行累计)。
190
+ -- 关联三个 ID:task_id(一次 runQuery)、session_id(evolcore 会话)、agent_session_id(SDK 会话)。
191
+ CREATE TABLE IF NOT EXISTS model_calls (
192
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
193
+ ts INTEGER NOT NULL,
194
+ task_id TEXT NOT NULL,
195
+ session_id TEXT,
196
+ agent_session_id TEXT,
197
+ agent_aid TEXT NOT NULL,
198
+ peer_key TEXT NOT NULL,
199
+ call_index INTEGER NOT NULL,
200
+ model TEXT NOT NULL,
201
+ request_id TEXT,
202
+ message_id TEXT,
203
+ input_tokens INTEGER NOT NULL DEFAULT 0,
204
+ output_tokens INTEGER NOT NULL DEFAULT 0,
205
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
206
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
207
+ context_tokens INTEGER,
208
+ max_tokens INTEGER,
209
+ auto_compact_tokens INTEGER,
210
+ degraded INTEGER NOT NULL DEFAULT 0
211
+ );
212
+ CREATE INDEX IF NOT EXISTS idx_mc_task ON model_calls(task_id);
213
+ CREATE INDEX IF NOT EXISTS idx_mc_session ON model_calls(session_id, ts);
214
+ CREATE INDEX IF NOT EXISTS idx_mc_agentsession ON model_calls(agent_session_id);
215
+ CREATE INDEX IF NOT EXISTS idx_mc_ts ON model_calls(ts);
216
216
  `);
217
217
  // ── Schema Migration: 添加 cost 列 ───────────────────────────────────────
218
218
  try {
@@ -220,17 +220,17 @@ function _initTables(db) {
220
220
  const hasCostCols = cols.some((c) => c.name === 'cost_official_usd');
221
221
  if (!hasCostCols) {
222
222
  logger.info('[StatsDB] Migrating schema: adding cost columns to usage_events and usage_daily...');
223
- db.exec(`
224
- ALTER TABLE usage_events ADD COLUMN cost_official_usd REAL;
225
- ALTER TABLE usage_events ADD COLUMN cost_official_cny REAL;
226
- ALTER TABLE usage_events ADD COLUMN cost_gateway_usd REAL;
227
- ALTER TABLE usage_events ADD COLUMN cost_gateway_cny REAL;
223
+ db.exec(`
224
+ ALTER TABLE usage_events ADD COLUMN cost_official_usd REAL;
225
+ ALTER TABLE usage_events ADD COLUMN cost_official_cny REAL;
226
+ ALTER TABLE usage_events ADD COLUMN cost_gateway_usd REAL;
227
+ ALTER TABLE usage_events ADD COLUMN cost_gateway_cny REAL;
228
228
  `);
229
- db.exec(`
230
- ALTER TABLE usage_daily ADD COLUMN cost_official_usd REAL;
231
- ALTER TABLE usage_daily ADD COLUMN cost_official_cny REAL;
232
- ALTER TABLE usage_daily ADD COLUMN cost_gateway_usd REAL;
233
- ALTER TABLE usage_daily ADD COLUMN cost_gateway_cny REAL;
229
+ db.exec(`
230
+ ALTER TABLE usage_daily ADD COLUMN cost_official_usd REAL;
231
+ ALTER TABLE usage_daily ADD COLUMN cost_official_cny REAL;
232
+ ALTER TABLE usage_daily ADD COLUMN cost_gateway_usd REAL;
233
+ ALTER TABLE usage_daily ADD COLUMN cost_gateway_cny REAL;
234
234
  `);
235
235
  logger.info('[StatsDB] Migration complete. Run "ec stats --rebuild" to backfill historical costs.');
236
236
  }
@@ -295,26 +295,26 @@ export function rebuildDailyRollup(evolcoreHome) {
295
295
  try {
296
296
  db.exec('BEGIN');
297
297
  db.exec('DELETE FROM usage_daily');
298
- db.exec(`
299
- INSERT INTO usage_daily
300
- (day, agent_aid, peer_key, peer_type, session_id, model, billing_fn,
301
- input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
302
- cache_hit_tokens, cache_miss_tokens, image_tokens, total_context_tokens,
303
- turns, calls,
304
- cost_official_usd, cost_official_cny, cost_gateway_usd, cost_gateway_cny)
305
- SELECT
306
- strftime('%Y-%m-%d', ts/1000, 'unixepoch', 'localtime') AS day,
307
- agent_aid, peer_key, MAX(COALESCE(peer_type, '')),
308
- COALESCE(session_id, ''), model, billing_fn,
309
- SUM(input_tokens), SUM(output_tokens),
310
- SUM(cache_creation_tokens), SUM(cache_read_tokens),
311
- SUM(COALESCE(cache_hit_tokens, 0)), SUM(COALESCE(cache_miss_tokens, 0)),
312
- SUM(COALESCE(image_tokens, 0)), SUM(COALESCE(total_context_tokens, 0)),
313
- SUM(turns), COUNT(*),
314
- SUM(COALESCE(cost_official_usd, 0)), SUM(COALESCE(cost_official_cny, 0)),
315
- SUM(COALESCE(cost_gateway_usd, 0)), SUM(COALESCE(cost_gateway_cny, 0))
316
- FROM usage_events
317
- GROUP BY day, agent_aid, peer_key, COALESCE(session_id, ''), model, billing_fn
298
+ db.exec(`
299
+ INSERT INTO usage_daily
300
+ (day, agent_aid, peer_key, peer_type, session_id, model, billing_fn,
301
+ input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
302
+ cache_hit_tokens, cache_miss_tokens, image_tokens, total_context_tokens,
303
+ turns, calls,
304
+ cost_official_usd, cost_official_cny, cost_gateway_usd, cost_gateway_cny)
305
+ SELECT
306
+ strftime('%Y-%m-%d', ts/1000, 'unixepoch', 'localtime') AS day,
307
+ agent_aid, peer_key, MAX(COALESCE(peer_type, '')),
308
+ COALESCE(session_id, ''), model, billing_fn,
309
+ SUM(input_tokens), SUM(output_tokens),
310
+ SUM(cache_creation_tokens), SUM(cache_read_tokens),
311
+ SUM(COALESCE(cache_hit_tokens, 0)), SUM(COALESCE(cache_miss_tokens, 0)),
312
+ SUM(COALESCE(image_tokens, 0)), SUM(COALESCE(total_context_tokens, 0)),
313
+ SUM(turns), COUNT(*),
314
+ SUM(COALESCE(cost_official_usd, 0)), SUM(COALESCE(cost_official_cny, 0)),
315
+ SUM(COALESCE(cost_gateway_usd, 0)), SUM(COALESCE(cost_gateway_cny, 0))
316
+ FROM usage_events
317
+ GROUP BY day, agent_aid, peer_key, COALESCE(session_id, ''), model, billing_fn
318
318
  `);
319
319
  db.exec('COMMIT');
320
320
  const row = db.prepare('SELECT COUNT(*) AS n FROM usage_daily').get();
@@ -345,7 +345,7 @@ export function archiveOldData(evolcoreHome) {
345
345
  const cutoffYear = now.getFullYear(); // e.g. 2026
346
346
  const cutoffTs = Date.UTC(cutoffYear, 0, 1); // 2026-01-01 00:00:00 UTC
347
347
  // 找出主库中所有早于 cutoff 的年份
348
- const years = db.prepare(`SELECT DISTINCT CAST(strftime('%Y', ts/1000, 'unixepoch') AS INTEGER) AS y
348
+ const years = db.prepare(`SELECT DISTINCT CAST(strftime('%Y', ts/1000, 'unixepoch') AS INTEGER) AS y
349
349
  FROM usage_events WHERE ts < ? ORDER BY y`).all(cutoffTs).map((r) => r.y);
350
350
  if (years.length === 0)
351
351
  return;