evolcore 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/MIGRATION-0.5.0.md +378 -0
- package/README.md +318 -14
- package/ROLE_ACCESS_CONTROL.md +174 -0
- package/assets/.env.template +4 -0
- package/bin/ec-safe-output.js +161 -0
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent.js +163 -0
- package/dist/agents/claude-runner.js +2565 -0
- package/dist/agents/codex-app-server-client.js +448 -0
- package/dist/agents/codex-runner.js +2682 -0
- package/dist/agents/gemini-runner.js +666 -0
- package/dist/agents/runner-types.js +75 -0
- package/dist/aun/aid/agentmd.js +216 -0
- package/dist/aun/aid/client.js +132 -0
- package/dist/aun/aid/control-aid.js +91 -0
- package/dist/aun/aid/identity.js +518 -0
- package/dist/aun/aid/index.js +4 -0
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/group-identity.js +10 -0
- package/dist/aun/msg/group-index.js +6 -0
- package/dist/aun/msg/group.js +1231 -0
- package/dist/aun/msg/history.js +123 -0
- package/dist/aun/msg/index.js +5 -0
- package/dist/aun/msg/p2p.js +393 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +137 -0
- package/dist/aun/outbox.js +168 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +25 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/service-proxy.js +225 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +68 -0
- package/dist/channels/aun.js +4164 -0
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/daemon.js +422 -0
- package/dist/channels/dingtalk.js +1479 -0
- package/dist/channels/feishu.js +1865 -0
- package/dist/channels/qqbot.js +409 -0
- package/dist/channels/wechat.js +817 -0
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +1157 -0
- package/dist/cli/agent-command.js +642 -0
- package/dist/cli/agent.js +1059 -0
- package/dist/cli/aun-commands.js +2003 -0
- package/dist/cli/bench.js +1228 -0
- package/dist/cli/cli-argv.js +66 -0
- package/dist/cli/code-stats.js +329 -0
- package/dist/cli/command-log.js +82 -0
- package/dist/cli/config-selector.js +69 -0
- package/dist/cli/config.js +261 -0
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +62 -0
- package/dist/cli/daemon-commands.js +2750 -0
- package/dist/cli/fs-command.js +1447 -0
- package/dist/cli/handoff-command.js +302 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/index.js +374 -0
- package/dist/cli/init-channel.js +1372 -0
- package/dist/cli/init.js +590 -0
- package/dist/cli/link-rules.js +240 -0
- package/dist/cli/model.js +591 -0
- package/dist/cli/net-check.js +723 -0
- package/dist/cli/queue-command.js +150 -0
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +344 -0
- package/dist/cli/restart-monitor.js +480 -0
- package/dist/cli/stats.js +609 -0
- package/dist/cli/task-context.js +80 -0
- package/dist/cli/trigger-command.js +545 -0
- package/dist/cli/version.js +93 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +673 -0
- package/dist/config/boot-log.js +266 -0
- package/dist/config/builtin-role-templates.js +42 -0
- package/dist/config/builtin-roles.js +91 -0
- package/dist/config/config-batch-get.js +11 -0
- package/dist/config/config-field-policy.js +261 -0
- package/dist/config/config-manager.js +1120 -0
- package/dist/config/config-operation-service.js +384 -0
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +224 -0
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/gateway-config.js +858 -0
- package/dist/config/lifecycle.js +17 -0
- package/dist/config/mention-mode.js +27 -0
- package/dist/config/merge.js +161 -0
- package/dist/config/owner-policy.js +4 -0
- package/dist/config/peer-role-resolver.js +218 -0
- package/dist/config/resolved-config-op.js +483 -0
- package/dist/config/role-config-v4-startup.js +32 -0
- package/dist/config/role-config-v5-startup.js +27 -0
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-schema.js +105 -0
- package/dist/config/role-service.js +156 -0
- package/dist/config/role-store.js +215 -0
- package/dist/config/roles.js +64 -0
- package/dist/config/schema-registry.js +154 -0
- package/dist/config/snapshot.js +598 -0
- package/dist/config-store.js +501 -0
- package/dist/core/auth/agent-delegation.js +111 -0
- package/dist/core/auth/auth-gateway.js +166 -0
- package/dist/core/auth/authenticated-actor.js +6 -0
- package/dist/core/auth/authorization-audit.js +119 -0
- package/dist/core/auth/operation-authorizer.js +720 -0
- package/dist/core/auth/operation-catalog.js +731 -0
- package/dist/core/baseagent-loader.js +54 -0
- package/dist/core/bootstrap-service.js +175 -0
- package/dist/core/capability/capability-manager.js +316 -0
- package/dist/core/capability/providers/claude-capability-provider.js +176 -0
- package/dist/core/capability/providers/codex-capability-provider.js +148 -0
- package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
- package/dist/core/capability/types.js +27 -0
- package/dist/core/causation/audit.js +103 -0
- package/dist/core/causation/aun-association.js +111 -0
- package/dist/core/causation/context.js +93 -0
- package/dist/core/causation/index.js +4 -0
- package/dist/core/causation/types.js +2 -0
- package/dist/core/channel-loader.js +277 -0
- package/dist/core/command/agent-control.js +616 -0
- package/dist/core/command/cli-intent-parser.js +225 -0
- package/dist/core/command/command-handler.js +1733 -0
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +3452 -0
- package/dist/core/command/menu-protocol.js +247 -0
- package/dist/core/command/role-menu.js +1623 -0
- package/dist/core/command/slash-gate.js +148 -0
- package/dist/core/command/slash-handler.js +3066 -0
- package/dist/core/daemon-file-cache.js +222 -0
- package/dist/core/event-bus.js +32 -0
- package/dist/core/event-catalog.js +810 -0
- package/dist/core/evolagent-registry.js +545 -0
- package/dist/core/evolagent.js +342 -0
- package/dist/core/handoff/dispatcher.js +229 -0
- package/dist/core/handoff/mutex.js +46 -0
- package/dist/core/handoff/runtime.js +324 -0
- package/dist/core/handoff/store.js +537 -0
- package/dist/core/handoff/types.js +12 -0
- package/dist/core/inference/text-inference.js +173 -0
- package/dist/core/interaction-registration.js +10 -0
- package/dist/core/interaction-router.js +278 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +659 -0
- package/dist/core/message/items-formatter.js +76 -0
- package/dist/core/message/logical-queue-bridge.js +123 -0
- package/dist/core/message/message-bridge.js +874 -0
- package/dist/core/message/message-cache.js +56 -0
- package/dist/core/message/message-log.js +339 -0
- package/dist/core/message/message-processor.js +4 -0
- package/dist/core/message/message-queue.js +1446 -0
- package/dist/core/message/message-utils.js +76 -0
- package/dist/core/message/peer-mode.js +105 -0
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +33 -0
- package/dist/core/message/response-engine.js +4020 -0
- package/dist/core/message/response-snapshot.js +83 -0
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +139 -0
- package/dist/core/message/stream-idle-monitor.js +124 -0
- package/dist/core/model/config-scope.js +162 -0
- package/dist/core/model/field-scope.js +78 -0
- package/dist/core/model/model-catalog.js +227 -0
- package/dist/core/model/model-diagnostics.js +182 -0
- package/dist/core/model/model-permission.js +90 -0
- package/dist/core/permission/approval-gateway.js +1017 -0
- package/dist/core/permission/ec-command-parser.js +347 -0
- package/dist/core/permission/execution-sandbox.js +16 -0
- package/dist/core/permission/index.js +6 -0
- package/dist/core/permission/mode.js +24 -0
- package/dist/core/permission/sandbox-runtime.js +265 -0
- package/dist/core/permission/tool-policy.js +1019 -0
- package/dist/core/permission/unix-socket-policy.js +99 -0
- package/dist/core/protected-paths.js +332 -0
- package/dist/core/relation/peer-identity.js +222 -0
- package/dist/core/relation/peer-key.js +1 -0
- package/dist/core/role/runtime-policy.js +141 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-fs-store.js +273 -0
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +1643 -0
- package/dist/core/session/session-mapper.js +100 -0
- package/dist/core/session/session-renew.js +314 -0
- package/dist/core/session/session-title.js +128 -0
- package/dist/core/session/session-turn-coordinator.js +205 -0
- package/dist/core/session/session-turns.js +67 -0
- package/dist/core/system-channels.js +29 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/group-rules-sync.js +345 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/kit-renderer.js +359 -0
- package/dist/eck/manifest-engine.js +446 -0
- package/dist/eck/message-renderer.js +199 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +2926 -4
- package/dist/ipc.js +777 -0
- package/dist/paths.js +262 -0
- package/dist/product.js +18 -0
- package/dist/response-system/context-builder.js +71 -0
- package/dist/response-system/coordinator.js +117 -0
- package/dist/response-system/decision-executor.js +86 -0
- package/dist/response-system/engines/v1/index.js +21 -0
- package/dist/response-system/engines/v1/interactive-flow.js +27 -0
- package/dist/response-system/engines/v1/proactive-flow.js +137 -0
- package/dist/response-system/engines/v1/types.js +1 -0
- package/dist/response-system/extensions.js +41 -0
- package/dist/response-system/index.js +6 -0
- package/dist/response-system/modes/index.js +7 -0
- package/dist/response-system/modes/single-session/index.js +72 -0
- package/dist/response-system/queues/fifo-queue.js +44 -0
- package/dist/response-system/queues/index.js +6 -0
- package/dist/response-system/queues/lifo-queue.js +42 -0
- package/dist/response-system/queues/priority-queue.js +63 -0
- package/dist/response-system/registry.js +97 -0
- package/dist/response-system/resolver.js +37 -0
- package/dist/response-system/selector.js +23 -0
- package/dist/response-system/types.js +7 -0
- package/dist/stats/billing.js +163 -0
- package/dist/stats/budget.js +93 -0
- package/dist/stats/db.js +403 -0
- package/dist/stats/eck-vars.js +89 -0
- package/dist/stats/index.js +11 -0
- package/dist/stats/normalizer.js +80 -0
- package/dist/stats/price-resolver.js +138 -0
- package/dist/stats/query.js +763 -0
- package/dist/stats/role-budget.js +168 -0
- package/dist/stats/writer.js +151 -0
- package/dist/trigger/anomaly-store.js +258 -0
- package/dist/trigger/audit.js +152 -0
- package/dist/trigger/event-source.js +119 -0
- package/dist/trigger/feedback.js +685 -0
- package/dist/trigger/history.js +290 -0
- package/dist/trigger/manager.js +294 -0
- package/dist/trigger/parser.js +595 -0
- package/dist/trigger/patch.js +155 -0
- package/dist/trigger/scheduler.js +1602 -0
- package/dist/trigger/script-executor.js +155 -0
- package/dist/trigger/state.js +145 -0
- package/dist/trigger/types.js +1 -0
- package/dist/trigger/validation.js +634 -0
- package/dist/types.js +12 -0
- package/dist/utils/aid-bind.js +313 -0
- package/dist/utils/atomic-write.js +95 -0
- package/dist/utils/avatar-upload.js +123 -0
- package/dist/utils/cross-platform.js +297 -0
- package/dist/utils/ecweb-utils.js +73 -0
- package/dist/utils/error-dict.json +153 -0
- package/dist/utils/error-utils.js +349 -0
- package/dist/utils/instance-registry.js +444 -0
- package/dist/utils/locale.js +21 -0
- package/dist/utils/log-writer.js +270 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/markdown-to-plain-text.js +20 -0
- package/dist/utils/media-cache.js +274 -0
- package/dist/utils/model-prices.jsonl +20 -0
- package/dist/utils/npm-ops.js +210 -0
- package/dist/utils/process-introspect.js +133 -0
- package/dist/utils/process-tree-stats.js +271 -0
- package/dist/utils/project-path.js +74 -0
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stats.js +410 -0
- package/dist/utils/system-memory.js +62 -0
- package/dist/utils/tool-summary.js +284 -0
- package/dist/utils/welcome.js +268 -0
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +66 -0
- package/kits/docs/aun/CHEATSHEET.md +19 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +65 -0
- package/kits/docs/channels/feishu.md +56 -0
- package/kits/docs/context-assembly.md +366 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolcore/INDEX.md +68 -0
- package/kits/docs/evolcore/agent.md +77 -0
- package/kits/docs/evolcore/aid.md +52 -0
- package/kits/docs/evolcore/config.md +149 -0
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/ctl.md +46 -0
- package/kits/docs/evolcore/event.md +216 -0
- package/kits/docs/evolcore/fs-architecture.md +1215 -0
- package/kits/docs/evolcore/fs.md +110 -0
- package/kits/docs/evolcore/group-fs.md +17 -0
- package/kits/docs/evolcore/group-rules.md +226 -0
- package/kits/docs/evolcore/group.md +150 -0
- package/kits/docs/evolcore/model.md +50 -0
- package/kits/docs/evolcore/msg.md +136 -0
- package/kits/docs/evolcore/response.md +75 -0
- package/kits/docs/evolcore/rpc.md +37 -0
- package/kits/docs/evolcore/self-summary.md +29 -0
- package/kits/docs/evolcore/stats.md +83 -0
- package/kits/docs/evolcore/storage.md +50 -0
- package/kits/docs/evolcore/trigger.md +539 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +23 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/docs/prompt-loading-architecture.md +266 -0
- package/kits/docs/venues/aun-group.md +45 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +25 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.auxiliary.json +43 -0
- package/kits/eck_manifest.json +203 -0
- package/kits/eck_message_manifest.json +63 -0
- package/kits/migrations/README-role-config-v4.md +32 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/migrations/migrate-role-config-v4.mjs +623 -0
- package/kits/migrations/migrate-role-config-v5.mjs +346 -0
- package/kits/migrations/rename-config-file.mjs +99 -0
- package/kits/rules/01-overview.md +142 -0
- package/kits/rules/02-navigation.md +76 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +59 -0
- package/kits/rules/05-venue.md +44 -0
- package/kits/rules/06-channel.md +59 -0
- package/kits/schemas/_meta.json +32 -0
- package/kits/schemas/agent-config.schema.1.json +177 -0
- package/kits/schemas/agent-config.schema.2.json +239 -0
- package/kits/schemas/agent-config.schema.3.json +119 -0
- package/kits/schemas/agent-config.schema.4.json +208 -0
- package/kits/schemas/agent-config.schema.5.json +326 -0
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.1.json +36 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/daemon.schema.1.json +90 -0
- package/kits/schemas/defaults.schema.1.json +81 -0
- package/kits/schemas/menu-exec-schema-commands.md +208 -0
- package/kits/schemas/migrations/README.md +28 -0
- package/kits/schemas/relation-config.schema.1.json +158 -0
- package/kits/schemas/relation-config.schema.2.json +73 -0
- package/kits/schemas/relation-config.schema.3.json +50 -0
- package/kits/schemas/relation-config.schema.4.json +47 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +201 -0
- package/kits/schemas/role-registry.schema.1.json +35 -0
- package/kits/schemas/single-session.schema.1.json +31 -0
- package/kits/templates/bootstrap-welcome.md +15 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
- package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/roles/admin.json +9 -0
- package/kits/templates/roles/member.json +41 -0
- package/kits/templates/roles/owner.json +9 -0
- package/kits/templates/roles/visitor.json +40 -0
- package/kits/templates/system-fragments/baseagent.md +14 -0
- package/kits/templates/system-fragments/bootstrap.md +16 -0
- package/kits/templates/system-fragments/channel.md +48 -0
- package/kits/templates/system-fragments/commands.md +28 -0
- package/kits/templates/system-fragments/identity.md +11 -0
- package/kits/templates/system-fragments/relation.md +19 -0
- package/kits/templates/system-fragments/session.md +53 -0
- package/kits/templates/system-fragments/venue.md +31 -0
- package/package.json +50 -15
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* billing.ts — 读 model-prices.jsonl,按 billing_fn 调对应算法函数计算费用。
|
|
3
|
+
* 费用查询时实时计算,不存入 DB(价格可能变动)。
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { getPackageRoot } from '../paths.js';
|
|
8
|
+
// 价格表缓存(进程内,5 分钟 TTL)
|
|
9
|
+
let _priceCache = null;
|
|
10
|
+
let _priceCacheTs = 0;
|
|
11
|
+
const PRICE_CACHE_TTL = 5 * 60 * 1000;
|
|
12
|
+
/**
|
|
13
|
+
* 从包路径 + 用户路径合并读取 JSONL 文件。
|
|
14
|
+
* 包基线随构建产物发布在 dist/utils,源码运行时回退到 src/utils;
|
|
15
|
+
* 用户路径($EVOLCORE_HOME/data/stats/)为追加/覆盖。
|
|
16
|
+
* 两层合并(append),用户层行追加在包层之后——查价时 effective_from 越大越优先,天然正确。
|
|
17
|
+
*/
|
|
18
|
+
function _loadJsonlMerged(evolcoreHome, filename) {
|
|
19
|
+
const results = [];
|
|
20
|
+
// 1. 包路径(基线)
|
|
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) {
|
|
28
|
+
try {
|
|
29
|
+
const lines = fs.readFileSync(pkgFile, 'utf-8').split('\n').filter(Boolean);
|
|
30
|
+
for (const l of lines)
|
|
31
|
+
results.push(JSON.parse(l));
|
|
32
|
+
}
|
|
33
|
+
catch { /* skip */ }
|
|
34
|
+
}
|
|
35
|
+
// 2. 用户路径(追加/覆盖)
|
|
36
|
+
const userFile = path.join(evolcoreHome, 'data', 'stats', filename);
|
|
37
|
+
if (fs.existsSync(userFile) && (!pkgFile || path.resolve(userFile) !== path.resolve(pkgFile))) {
|
|
38
|
+
try {
|
|
39
|
+
const lines = fs.readFileSync(userFile, 'utf-8').split('\n').filter(Boolean);
|
|
40
|
+
for (const l of lines)
|
|
41
|
+
results.push(JSON.parse(l));
|
|
42
|
+
}
|
|
43
|
+
catch { /* skip */ }
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
let _aliasCache = null;
|
|
48
|
+
let _aliasCacheTs = 0;
|
|
49
|
+
function loadAliases(evolcoreHome) {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
if (_aliasCache && now - _aliasCacheTs < PRICE_CACHE_TTL)
|
|
52
|
+
return _aliasCache;
|
|
53
|
+
_aliasCache = _loadJsonlMerged(evolcoreHome, 'model-aliases.jsonl');
|
|
54
|
+
_aliasCacheTs = now;
|
|
55
|
+
return _aliasCache;
|
|
56
|
+
}
|
|
57
|
+
/** 解析模型 ID → 定价表规范 ID。精确匹配 alias 表,找不到返回原 ID。 */
|
|
58
|
+
export function resolveCanonicalModel(evolcoreHome, model) {
|
|
59
|
+
const aliases = loadAliases(evolcoreHome);
|
|
60
|
+
const entry = aliases.find(a => a.alias === model);
|
|
61
|
+
return entry ? entry.canonical : model;
|
|
62
|
+
}
|
|
63
|
+
function loadPrices(evolcoreHome) {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
if (_priceCache && now - _priceCacheTs < PRICE_CACHE_TTL)
|
|
66
|
+
return _priceCache;
|
|
67
|
+
_priceCache = _loadJsonlMerged(evolcoreHome, 'model-prices.jsonl');
|
|
68
|
+
_priceCacheTs = now;
|
|
69
|
+
return _priceCache;
|
|
70
|
+
}
|
|
71
|
+
/** 取 model 在 ts 时刻生效的价格行(effective_from <= ts 中最新的一条)。
|
|
72
|
+
* 精确匹配失败时,通过 model-aliases.jsonl 映射到规范 ID 再查一次。 */
|
|
73
|
+
export function resolvePriceRow(evolcoreHome, model, ts) {
|
|
74
|
+
const prices = loadPrices(evolcoreHome);
|
|
75
|
+
let candidates = prices.filter(p => p.model === model && p.effective_from <= ts);
|
|
76
|
+
if (!candidates.length) {
|
|
77
|
+
// fallback: alias → canonical
|
|
78
|
+
const canonical = resolveCanonicalModel(evolcoreHome, model);
|
|
79
|
+
if (canonical !== model) {
|
|
80
|
+
candidates = prices.filter(p => p.model === canonical && p.effective_from <= ts);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!candidates.length)
|
|
84
|
+
return null;
|
|
85
|
+
return candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
|
|
86
|
+
}
|
|
87
|
+
export const BILLING_FNS = {
|
|
88
|
+
// 通用 per-token(Claude / OpenAI 兼容 / Kimi / MiniMax)
|
|
89
|
+
per_token_v1: (e, p) => {
|
|
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;
|
|
100
|
+
return p.currency === 'CNY' ? { cny: r } : { usd: r };
|
|
101
|
+
},
|
|
102
|
+
// DeepSeek cache_hit / cache_miss 口径
|
|
103
|
+
per_token_deepseek_v1: (e, p) => {
|
|
104
|
+
const r = (p.price_cache_hit ?? 0) * (e.cache_hit_tokens ?? 0) / 1e6
|
|
105
|
+
+ (p.price_cache_miss ?? 0) * (e.cache_miss_tokens ?? 0) / 1e6
|
|
106
|
+
+ (p.price_output ?? 0) * e.output_tokens / 1e6;
|
|
107
|
+
return { cny: r };
|
|
108
|
+
},
|
|
109
|
+
// Gemini 分档(按 total_context_tokens 确定所在档位)
|
|
110
|
+
per_token_tiered_v1: (e, p) => {
|
|
111
|
+
const tiers = p.tiers;
|
|
112
|
+
if (!Array.isArray(tiers))
|
|
113
|
+
return {};
|
|
114
|
+
const ctx = e.total_context_tokens ?? e.input_tokens;
|
|
115
|
+
const tier = tiers.find(t => t.up_to_tokens == null || ctx <= t.up_to_tokens) ?? tiers[tiers.length - 1];
|
|
116
|
+
const r = (tier.price_input ?? 0) * e.input_tokens / 1e6
|
|
117
|
+
+ (tier.price_output ?? 0) * e.output_tokens / 1e6
|
|
118
|
+
+ (tier.price_cache_read ?? 0) * e.cache_read_tokens / 1e6;
|
|
119
|
+
return p.currency === 'CNY' ? { cny: r } : { usd: r };
|
|
120
|
+
},
|
|
121
|
+
// 视觉模型(含 image_tokens 单独计费)
|
|
122
|
+
per_token_image_v1: (e, p) => {
|
|
123
|
+
const r = (p.price_input ?? 0) * e.input_tokens / 1e6
|
|
124
|
+
+ (p.price_output ?? 0) * e.output_tokens / 1e6
|
|
125
|
+
+ (p.price_image ?? 0) * (e.image_tokens ?? 0) / 1e6;
|
|
126
|
+
return p.currency === 'CNY' ? { cny: r } : { usd: r };
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
/** 注册新计费函数(扩展点)。 */
|
|
130
|
+
export function registerBillingFn(id, fn) {
|
|
131
|
+
BILLING_FNS[id] = fn;
|
|
132
|
+
}
|
|
133
|
+
/** 计算一条 usage_event 的费用。找不到价格行时返回 {}。 */
|
|
134
|
+
export function calcCost(evolcoreHome, event) {
|
|
135
|
+
const priceRow = resolvePriceRow(evolcoreHome, event.model, event.ts);
|
|
136
|
+
if (!priceRow)
|
|
137
|
+
return {};
|
|
138
|
+
const fn = BILLING_FNS[event.billing_fn] ?? BILLING_FNS[priceRow.billing_fn];
|
|
139
|
+
if (!fn)
|
|
140
|
+
return {};
|
|
141
|
+
return fn(event, priceRow);
|
|
142
|
+
}
|
|
143
|
+
let _specCache = null;
|
|
144
|
+
let _specCacheTs = 0;
|
|
145
|
+
function loadSpecs(evolcoreHome) {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
if (_specCache && now - _specCacheTs < PRICE_CACHE_TTL)
|
|
148
|
+
return _specCache;
|
|
149
|
+
_specCache = _loadJsonlMerged(evolcoreHome, 'model-specs.jsonl');
|
|
150
|
+
_specCacheTs = now;
|
|
151
|
+
return _specCache;
|
|
152
|
+
}
|
|
153
|
+
/** 取模型在 ts 时刻的能力参数。找不到时返回默认值。 */
|
|
154
|
+
export function resolveModelSpec(evolcoreHome, model, ts) {
|
|
155
|
+
const specs = loadSpecs(evolcoreHome);
|
|
156
|
+
const t = ts ?? Date.now();
|
|
157
|
+
const candidates = specs.filter(s => s.model === model && s.effective_from <= t);
|
|
158
|
+
if (candidates.length) {
|
|
159
|
+
return candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
|
|
160
|
+
}
|
|
161
|
+
// 默认值
|
|
162
|
+
return { model, effective_from: 0, context_window: 200000, max_input_tokens: 180000, max_output_tokens: 8192 };
|
|
163
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* budget.ts — 三档预算控制(硬上限 / 软上限 / 自主上限)。
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { calcCost } from './billing.js';
|
|
7
|
+
import { openReadonlyDb, getDbPath } from './db.js';
|
|
8
|
+
function loadBudgets(evolcoreHome) {
|
|
9
|
+
const file = path.join(evolcoreHome, 'data', 'stats', 'budgets.json');
|
|
10
|
+
if (!fs.existsSync(file))
|
|
11
|
+
return {};
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function _resolveCfg(budgets, agentAid, peerKey) {
|
|
20
|
+
const g = budgets.global ?? {};
|
|
21
|
+
const a = agentAid ? (budgets.agents?.[agentAid] ?? {}) : {};
|
|
22
|
+
const p = peerKey ? (budgets.peers?.[peerKey] ?? {}) : {};
|
|
23
|
+
// 取最严格的限额
|
|
24
|
+
const daily_usd = Math.min(g.daily_usd ?? Infinity, a.daily_usd ?? Infinity, p.daily_usd ?? Infinity);
|
|
25
|
+
return {
|
|
26
|
+
daily_usd: isFinite(daily_usd) ? daily_usd : undefined,
|
|
27
|
+
monthly_usd: g.monthly_usd,
|
|
28
|
+
hard_limit_pct: g.hard_limit_pct ?? 100,
|
|
29
|
+
soft_limit_pct: g.soft_limit_pct ?? 80,
|
|
30
|
+
auto_limit_pct: g.auto_limit_pct ?? 60,
|
|
31
|
+
on_hard_limit: g.on_hard_limit ?? 'block',
|
|
32
|
+
downgrade_model: g.downgrade_model,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** 计算今日实际 USD 消耗(逐行算 cost,以避免聚合误差)。 */
|
|
36
|
+
function _calcTodayUsd(evolcoreHome, agentAid) {
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const from_ts = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
39
|
+
return _calcRangeUsd(evolcoreHome, from_ts, agentAid);
|
|
40
|
+
}
|
|
41
|
+
/** 计算本月实际 USD 消耗。 */
|
|
42
|
+
function _calcMonthUsd(evolcoreHome, agentAid) {
|
|
43
|
+
const now = new Date();
|
|
44
|
+
const from_ts = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
|
|
45
|
+
return _calcRangeUsd(evolcoreHome, from_ts, agentAid);
|
|
46
|
+
}
|
|
47
|
+
/** 计算指定时间范围的 USD 消耗(逐行算 cost)。 */
|
|
48
|
+
function _calcRangeUsd(evolcoreHome, from_ts, agentAid) {
|
|
49
|
+
const db = openReadonlyDb(getDbPath(evolcoreHome));
|
|
50
|
+
if (!db)
|
|
51
|
+
return 0;
|
|
52
|
+
let total = 0;
|
|
53
|
+
try {
|
|
54
|
+
const clause = agentAid ? 'WHERE ts >= ? AND agent_aid = ?' : 'WHERE ts >= ?';
|
|
55
|
+
const params = agentAid ? [from_ts, agentAid] : [from_ts];
|
|
56
|
+
const rows = db.prepare(`SELECT * FROM usage_events ${clause}`).all(...params);
|
|
57
|
+
for (const r of rows) {
|
|
58
|
+
const cost = calcCost(evolcoreHome, r);
|
|
59
|
+
total += cost.usd ?? 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
db.close();
|
|
64
|
+
}
|
|
65
|
+
return total;
|
|
66
|
+
}
|
|
67
|
+
export function getBudgetStatus(evolcoreHome, agentAid, peerKey) {
|
|
68
|
+
const budgets = loadBudgets(evolcoreHome);
|
|
69
|
+
const cfg = _resolveCfg(budgets, agentAid, peerKey);
|
|
70
|
+
const dailyLimit = cfg.daily_usd ?? Infinity;
|
|
71
|
+
const dailyUsed = _calcTodayUsd(evolcoreHome, agentAid);
|
|
72
|
+
const dailyRemaining = Math.max(0, dailyLimit - dailyUsed);
|
|
73
|
+
const dailyPct = isFinite(dailyLimit) && dailyLimit > 0 ? (dailyUsed / dailyLimit) * 100 : 0;
|
|
74
|
+
const monthlyLimit = cfg.monthly_usd ?? Infinity;
|
|
75
|
+
const monthlyUsed = isFinite(monthlyLimit) ? _calcMonthUsd(evolcoreHome, agentAid) : 0;
|
|
76
|
+
const monthlyRemaining = Math.max(0, monthlyLimit - monthlyUsed);
|
|
77
|
+
const monthlyPct = isFinite(monthlyLimit) && monthlyLimit > 0 ? (monthlyUsed / monthlyLimit) * 100 : 0;
|
|
78
|
+
// 取 daily/monthly 中更高的百分比作为整体判断依据
|
|
79
|
+
const pct = Math.max(dailyPct, monthlyPct);
|
|
80
|
+
return {
|
|
81
|
+
daily_limit_usd: isFinite(dailyLimit) ? dailyLimit : -1,
|
|
82
|
+
daily_used_usd: dailyUsed,
|
|
83
|
+
daily_remaining_usd: isFinite(dailyLimit) ? dailyRemaining : -1,
|
|
84
|
+
monthly_limit_usd: isFinite(monthlyLimit) ? monthlyLimit : -1,
|
|
85
|
+
monthly_used_usd: monthlyUsed,
|
|
86
|
+
monthly_remaining_usd: isFinite(monthlyLimit) ? monthlyRemaining : -1,
|
|
87
|
+
pct_used: pct,
|
|
88
|
+
hard_blocked: pct >= (cfg.hard_limit_pct ?? 100),
|
|
89
|
+
soft_warn: pct >= (cfg.soft_limit_pct ?? 80),
|
|
90
|
+
auto_warn: pct >= (cfg.auto_limit_pct ?? 60),
|
|
91
|
+
downgrade_model: cfg.downgrade_model,
|
|
92
|
+
};
|
|
93
|
+
}
|
package/dist/stats/db.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats DB — SQLite 初始化、WAL、建表、索引、归档。
|
|
3
|
+
* 技术选型:node:sqlite (Node 22.5+),与主包 config-store.ts 保持一致。
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
10
|
+
let sqliteModule; // undefined=未尝试, null=不可用
|
|
11
|
+
function loadSqlite() {
|
|
12
|
+
if (sqliteModule !== undefined)
|
|
13
|
+
return sqliteModule;
|
|
14
|
+
try {
|
|
15
|
+
sqliteModule = requireFromHere('node:sqlite');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
logger.warn(`[StatsDB] node:sqlite unavailable (Node < 22.5?). Stats disabled.`);
|
|
19
|
+
sqliteModule = null;
|
|
20
|
+
}
|
|
21
|
+
return sqliteModule;
|
|
22
|
+
}
|
|
23
|
+
// 单例:写者(daemon)持有一个 read-write 实例
|
|
24
|
+
let _db = null;
|
|
25
|
+
export function getStatsDir(evolcoreHome) {
|
|
26
|
+
return path.join(evolcoreHome, 'data', 'stats');
|
|
27
|
+
}
|
|
28
|
+
export function getDbPath(evolcoreHome) {
|
|
29
|
+
return path.join(getStatsDir(evolcoreHome), 'usage.db');
|
|
30
|
+
}
|
|
31
|
+
export function closeStatsDb() {
|
|
32
|
+
if (!_db)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
_db.close();
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
_db = null;
|
|
39
|
+
}
|
|
40
|
+
export function resetStatsDb(evolcoreHome) {
|
|
41
|
+
closeStatsDb();
|
|
42
|
+
return getDb(evolcoreHome);
|
|
43
|
+
}
|
|
44
|
+
/** 获取写者单例(daemon 调用)。首次调用时建表。 */
|
|
45
|
+
export function getDb(evolcoreHome) {
|
|
46
|
+
if (_db)
|
|
47
|
+
return _db;
|
|
48
|
+
const sqlite = loadSqlite();
|
|
49
|
+
if (!sqlite)
|
|
50
|
+
return null;
|
|
51
|
+
const dir = getStatsDir(evolcoreHome);
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
const dbPath = getDbPath(evolcoreHome);
|
|
54
|
+
try {
|
|
55
|
+
_db = new sqlite.DatabaseSync(dbPath);
|
|
56
|
+
_db.exec('PRAGMA journal_mode=WAL');
|
|
57
|
+
_db.exec('PRAGMA synchronous=NORMAL');
|
|
58
|
+
_initTables(_db);
|
|
59
|
+
logger.info(`[StatsDB] Opened: ${dbPath}`);
|
|
60
|
+
return _db;
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
try {
|
|
64
|
+
_db?.close();
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
_db = null;
|
|
68
|
+
logger.error(`[StatsDB] Failed to open DB: ${e}`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** 只读连接(CLI / ecweb 调用)。每次返回新连接,调用方负责 close()。 */
|
|
73
|
+
export function openReadonlyDb(dbPath) {
|
|
74
|
+
const sqlite = loadSqlite();
|
|
75
|
+
if (!sqlite)
|
|
76
|
+
return null;
|
|
77
|
+
if (!fs.existsSync(dbPath))
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
return new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
logger.warn(`[StatsDB] Failed to open readonly DB ${dbPath}: ${e}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
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);
|
|
216
|
+
`);
|
|
217
|
+
// ── Schema Migration: 添加 cost 列 ───────────────────────────────────────
|
|
218
|
+
try {
|
|
219
|
+
const cols = db.prepare(`PRAGMA table_info(usage_events)`).all();
|
|
220
|
+
const hasCostCols = cols.some((c) => c.name === 'cost_official_usd');
|
|
221
|
+
if (!hasCostCols) {
|
|
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;
|
|
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;
|
|
234
|
+
`);
|
|
235
|
+
logger.info('[StatsDB] Migration complete. Run "ec stats --rebuild" to backfill historical costs.');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
logger.warn(`[StatsDB] Cost column migration failed (non-fatal): ${e}`);
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
let cols = db.prepare(`PRAGMA table_info(usage_events)`).all();
|
|
243
|
+
if (!cols.some(c => c.name === 'usage_subject_key')) {
|
|
244
|
+
db.exec(`ALTER TABLE usage_events ADD COLUMN usage_subject_key TEXT NOT NULL DEFAULT ''`);
|
|
245
|
+
cols = db.prepare(`PRAGMA table_info(usage_events)`).all();
|
|
246
|
+
}
|
|
247
|
+
if (!cols.some(c => c.name === 'role')) {
|
|
248
|
+
db.exec(`ALTER TABLE usage_events ADD COLUMN role TEXT NOT NULL DEFAULT ''`);
|
|
249
|
+
}
|
|
250
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_ue_role_budget ON usage_events(agent_aid, role, usage_subject_key, ts)`);
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
logger.warn(`[StatsDB] usage_events role budget migration failed: ${e}`);
|
|
254
|
+
}
|
|
255
|
+
// 轻量迁移:旧库的 usage_daily 可能缺 peer_type 列(无 migration 机制,CREATE IF NOT EXISTS
|
|
256
|
+
// 不会补列)。检测后 ALTER 补上;补列后旧行 peer_type 为空,需跑一次 rebuildDailyRollup 回填。
|
|
257
|
+
try {
|
|
258
|
+
const cols = db.prepare(`PRAGMA table_info(usage_daily)`).all();
|
|
259
|
+
if (!cols.some(c => c.name === 'peer_type')) {
|
|
260
|
+
db.exec(`ALTER TABLE usage_daily ADD COLUMN peer_type TEXT NOT NULL DEFAULT ''`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
logger.warn(`[StatsDB] usage_daily peer_type 迁移检测失败: ${e}`);
|
|
265
|
+
}
|
|
266
|
+
// 轻量迁移:model_calls 早期只记录 usage 原始 token,task.start 自动压缩需要
|
|
267
|
+
// 使用上轮真实 context/max/compact window,避免按模型名猜窗口。
|
|
268
|
+
try {
|
|
269
|
+
const cols = db.prepare(`PRAGMA table_info(model_calls)`).all();
|
|
270
|
+
if (!cols.some(c => c.name === 'context_tokens')) {
|
|
271
|
+
db.exec(`ALTER TABLE model_calls ADD COLUMN context_tokens INTEGER`);
|
|
272
|
+
}
|
|
273
|
+
if (!cols.some(c => c.name === 'max_tokens')) {
|
|
274
|
+
db.exec(`ALTER TABLE model_calls ADD COLUMN max_tokens INTEGER`);
|
|
275
|
+
}
|
|
276
|
+
if (!cols.some(c => c.name === 'auto_compact_tokens')) {
|
|
277
|
+
db.exec(`ALTER TABLE model_calls ADD COLUMN auto_compact_tokens INTEGER`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
logger.warn(`[StatsDB] model_calls context window 迁移检测失败: ${e}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* 全量重建 usage_daily:从 usage_events 明细按天聚合重算。
|
|
286
|
+
* 用途:首次回填历史数据、每日自愈纠正写时漂移、手动 `ec stats --rebuild`。
|
|
287
|
+
* 单事务内 DELETE + INSERT...SELECT,失败回滚不破坏现有 rollup。
|
|
288
|
+
* 仅扫主库明细(往年明细已归档,但其 rollup 行已在归档前写入并永留主库)。
|
|
289
|
+
* @returns 重建后的行数,DB 不可用时返回 -1。
|
|
290
|
+
*/
|
|
291
|
+
export function rebuildDailyRollup(evolcoreHome) {
|
|
292
|
+
const db = getDb(evolcoreHome);
|
|
293
|
+
if (!db)
|
|
294
|
+
return -1;
|
|
295
|
+
try {
|
|
296
|
+
db.exec('BEGIN');
|
|
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
|
|
318
|
+
`);
|
|
319
|
+
db.exec('COMMIT');
|
|
320
|
+
const row = db.prepare('SELECT COUNT(*) AS n FROM usage_daily').get();
|
|
321
|
+
logger.info(`[StatsDB] Rebuilt usage_daily: ${row.n} rows`);
|
|
322
|
+
return row.n;
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
try {
|
|
326
|
+
db.exec('ROLLBACK');
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
329
|
+
logger.error(`[StatsDB] rebuildDailyRollup failed: ${e}`);
|
|
330
|
+
return -1;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// ── 归档 ────────────────────────────────────────────────────────────────────
|
|
334
|
+
/**
|
|
335
|
+
* 归档超过一整个自然年的数据到 usage-{year}.db,然后从主库删除。
|
|
336
|
+
* 例:当前 2026-05 → 归档所有 ts < 2025-01-01 00:00:00 的数据(即 2024 年及更早)。
|
|
337
|
+
* 安全:归档失败不删除主库数据。
|
|
338
|
+
*/
|
|
339
|
+
export function archiveOldData(evolcoreHome) {
|
|
340
|
+
const db = getDb(evolcoreHome);
|
|
341
|
+
if (!db)
|
|
342
|
+
return;
|
|
343
|
+
const now = new Date();
|
|
344
|
+
// 归档截止:当前年份第一天 00:00:00
|
|
345
|
+
const cutoffYear = now.getFullYear(); // e.g. 2026
|
|
346
|
+
const cutoffTs = Date.UTC(cutoffYear, 0, 1); // 2026-01-01 00:00:00 UTC
|
|
347
|
+
// 找出主库中所有早于 cutoff 的年份
|
|
348
|
+
const years = db.prepare(`SELECT DISTINCT CAST(strftime('%Y', ts/1000, 'unixepoch') AS INTEGER) AS y
|
|
349
|
+
FROM usage_events WHERE ts < ? ORDER BY y`).all(cutoffTs).map((r) => r.y);
|
|
350
|
+
if (years.length === 0)
|
|
351
|
+
return;
|
|
352
|
+
const statsDir = getStatsDir(evolcoreHome);
|
|
353
|
+
const sqlite = loadSqlite();
|
|
354
|
+
if (!sqlite)
|
|
355
|
+
return;
|
|
356
|
+
for (const year of years) {
|
|
357
|
+
const archivePath = path.join(statsDir, `usage-${year}.db`);
|
|
358
|
+
const yearStart = Date.UTC(year, 0, 1);
|
|
359
|
+
const yearEnd = Date.UTC(year + 1, 0, 1);
|
|
360
|
+
try {
|
|
361
|
+
// 打开/创建归档库,建同结构表
|
|
362
|
+
const archDb = new sqlite.DatabaseSync(archivePath);
|
|
363
|
+
archDb.exec('PRAGMA journal_mode=WAL');
|
|
364
|
+
_initTables(archDb);
|
|
365
|
+
archDb.close();
|
|
366
|
+
// ATTACH 方式批量迁移(同进程内最简洁)
|
|
367
|
+
db.exec(`ATTACH DATABASE '${archivePath.replace(/'/g, "''")}' AS arch`);
|
|
368
|
+
db.exec('BEGIN');
|
|
369
|
+
db.exec(`INSERT OR IGNORE INTO arch.usage_events SELECT * FROM main.usage_events WHERE ts >= ${yearStart} AND ts < ${yearEnd}`);
|
|
370
|
+
db.exec(`INSERT OR IGNORE INTO arch.context_breakdown SELECT * FROM main.context_breakdown WHERE ts >= ${yearStart} AND ts < ${yearEnd}`);
|
|
371
|
+
db.exec(`DELETE FROM main.usage_events WHERE ts >= ${yearStart} AND ts < ${yearEnd}`);
|
|
372
|
+
db.exec(`DELETE FROM main.context_breakdown WHERE ts >= ${yearStart} AND ts < ${yearEnd}`);
|
|
373
|
+
db.exec('COMMIT');
|
|
374
|
+
db.exec('DETACH DATABASE arch');
|
|
375
|
+
logger.info(`[StatsDB] Archived year ${year} → ${archivePath}`);
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
try {
|
|
379
|
+
db.exec('ROLLBACK');
|
|
380
|
+
}
|
|
381
|
+
catch { }
|
|
382
|
+
try {
|
|
383
|
+
db.exec('DETACH DATABASE arch');
|
|
384
|
+
}
|
|
385
|
+
catch { }
|
|
386
|
+
logger.error(`[StatsDB] Archive failed for year ${year}: ${e}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// VACUUM 回收空间(WAL 模式下安全)
|
|
390
|
+
try {
|
|
391
|
+
db.exec('VACUUM');
|
|
392
|
+
}
|
|
393
|
+
catch { }
|
|
394
|
+
}
|
|
395
|
+
/** 列出所有可用的归档库路径(按年份排序)。 */
|
|
396
|
+
export function listArchivePaths(evolcoreHome) {
|
|
397
|
+
const dir = getStatsDir(evolcoreHome);
|
|
398
|
+
if (!fs.existsSync(dir))
|
|
399
|
+
return [];
|
|
400
|
+
return fs.readdirSync(dir)
|
|
401
|
+
.map(f => { const m = f.match(/^usage-(\d{4})\.db$/); return m ? { year: +m[1], path: path.join(dir, f) } : null; })
|
|
402
|
+
.filter(Boolean);
|
|
403
|
+
}
|