evolclaw 3.1.11 → 3.3.0

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 (89) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +27 -2
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -27
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1069 -141
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +28 -0
  9. package/dist/aun/aid/control-aid.js +67 -0
  10. package/dist/aun/aid/identity.js +20 -7
  11. package/dist/aun/aid/store.js +2 -2
  12. package/dist/aun/storage/download.js +1 -1
  13. package/dist/aun/storage/upload.js +13 -1
  14. package/dist/channels/aun.js +538 -325
  15. package/dist/channels/dingtalk.js +77 -140
  16. package/dist/channels/feishu.js +98 -151
  17. package/dist/channels/qqbot.js +75 -138
  18. package/dist/channels/wechat.js +75 -136
  19. package/dist/channels/wecom.js +75 -138
  20. package/dist/cli/agent.js +44 -13
  21. package/dist/cli/index.js +207 -46
  22. package/dist/cli/init-channel.js +38 -148
  23. package/dist/cli/init.js +192 -85
  24. package/dist/cli/model.js +1 -1
  25. package/dist/cli/stats.js +558 -0
  26. package/dist/cli/version.js +87 -0
  27. package/dist/cli/watch-msg.js +5 -2
  28. package/dist/config-store.js +48 -11
  29. package/dist/core/channel-loader.js +84 -82
  30. package/dist/core/command-handler.js +754 -172
  31. package/dist/core/daemon-file-cache.js +216 -0
  32. package/dist/core/evolagent-registry.js +4 -0
  33. package/dist/core/evolagent.js +28 -23
  34. package/dist/core/interaction-router.js +8 -0
  35. package/dist/core/message/command-handler-agent-control.js +215 -0
  36. package/dist/core/message/create-status.js +67 -0
  37. package/dist/core/message/im-renderer.js +35 -13
  38. package/dist/core/message/items-formatter.js +9 -1
  39. package/dist/core/message/message-bridge.js +52 -22
  40. package/dist/core/message/message-log.js +1 -0
  41. package/dist/core/message/message-processor.js +336 -68
  42. package/dist/core/message/message-queue.js +15 -8
  43. package/dist/core/message/pending-hints.js +232 -0
  44. package/dist/core/message/response-depth.js +56 -0
  45. package/dist/core/model/model-catalog.js +1 -1
  46. package/dist/core/model/model-scope.js +40 -7
  47. package/dist/core/permission.js +9 -12
  48. package/dist/core/relation/peer-identity.js +16 -1
  49. package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  51. package/dist/core/session/session-manager.js +27 -13
  52. package/dist/core/session/session-title.js +26 -0
  53. package/dist/core/stats/billing.js +151 -0
  54. package/dist/core/stats/budget.js +93 -0
  55. package/dist/core/stats/db.js +314 -0
  56. package/dist/core/stats/eck-vars.js +84 -0
  57. package/dist/core/stats/index.js +10 -0
  58. package/dist/core/stats/normalizer.js +78 -0
  59. package/dist/core/stats/query.js +760 -0
  60. package/dist/core/stats/writer.js +115 -0
  61. package/dist/core/trigger/manager.js +34 -0
  62. package/dist/core/trigger/parser.js +9 -3
  63. package/dist/core/trigger/scheduler.js +20 -17
  64. package/dist/{agents → eck}/kit-renderer.js +5 -1
  65. package/dist/{agents → eck}/manifest-engine.js +127 -35
  66. package/dist/{agents → eck}/message-renderer.js +26 -1
  67. package/dist/index.js +185 -8
  68. package/dist/ipc.js +22 -0
  69. package/dist/paths.js +7 -3
  70. package/dist/utils/cross-platform.js +23 -5
  71. package/dist/utils/ecweb-pair.js +20 -0
  72. package/dist/utils/stats.js +14 -0
  73. package/kits/docs/evolclaw/INDEX.md +3 -1
  74. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  75. package/kits/docs/evolclaw/fs.md +131 -0
  76. package/kits/docs/evolclaw/group-fs.md +209 -0
  77. package/kits/docs/evolclaw/stats.md +70 -0
  78. package/kits/docs/venues/aun-group.md +29 -6
  79. package/kits/docs/venues/group.md +5 -4
  80. package/kits/eck_manifest.json +12 -0
  81. package/kits/eck_message_manifest.json +30 -3
  82. package/kits/rules/05-venue.md +1 -1
  83. package/kits/templates/message-fragments/inject-default.md +2 -0
  84. package/kits/templates/message-fragments/item.md +1 -1
  85. package/kits/templates/system-fragments/response-depth.md +16 -0
  86. package/package.json +4 -4
  87. package/dist/agents/baseagent-normalize.js +0 -19
  88. package/dist/core/relation/peer-key.js +0 -16
  89. package/dist/utils/channel-helpers.js +0 -46
@@ -69,6 +69,9 @@ export class SessionManager {
69
69
  return { role: 'admin', mode: 'interactive' };
70
70
  return { role: 'guest', mode: 'interactive' };
71
71
  }
72
+ resolvePermissionMode(role) {
73
+ return (role === 'owner' || role === 'admin') ? 'bypass' : 'readonly';
74
+ }
72
75
  async updateIdentity(sessionId, identity) {
73
76
  logger.debug(`[SessionManager] updateIdentity: sessionId=${sessionId}, role=${identity.role}`);
74
77
  }
@@ -479,10 +482,10 @@ export class SessionManager {
479
482
  throw new Error(`[SessionManager] getOrCreateSession requires channelType. channel="${channel}" channelId="${channelId}"`);
480
483
  }
481
484
  if (threadId) {
482
- const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfAID, channelType, peerType);
485
+ const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfAID, channelType, peerType, chatType);
483
486
  session.identity = this.resolveIdentity(channel, userId);
484
487
  if (session.metadata && !session.metadata.permissionMode) {
485
- session.metadata.permissionMode = DEFAULT_PERMISSION_MODE;
488
+ session.metadata.permissionMode = this.resolvePermissionMode(session.identity.role);
486
489
  this.persistSession(session, 'none');
487
490
  }
488
491
  return session;
@@ -564,8 +567,9 @@ export class SessionManager {
564
567
  }
565
568
  // Create new session
566
569
  const sessionMetadata = { ...(metadata || {}) };
570
+ const newIdentity = this.resolveIdentity(channel, userId);
567
571
  if (!sessionMetadata.permissionMode)
568
- sessionMetadata.permissionMode = DEFAULT_PERMISSION_MODE;
572
+ sessionMetadata.permissionMode = this.resolvePermissionMode(newIdentity.role);
569
573
  const session = {
570
574
  id: generateSessionId(),
571
575
  channel,
@@ -583,7 +587,7 @@ export class SessionManager {
583
587
  createdAt: Date.now(),
584
588
  updatedAt: Date.now(),
585
589
  };
586
- session.identity = this.resolveIdentity(channel, userId);
590
+ session.identity = newIdentity;
587
591
  this.persistSession(session, 'set');
588
592
  this.eventBus.publish({
589
593
  type: 'session:created',
@@ -614,7 +618,7 @@ export class SessionManager {
614
618
  current.agentSessionId = updates.agentSessionId ?? undefined;
615
619
  this.persistSession(current, 'sync');
616
620
  }
617
- getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfAID, channelType, peerType) {
621
+ getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfAID, channelType, peerType, chatType) {
618
622
  // 使用精确路径(channelType + selfAID)
619
623
  const chatDir = (channelType && selfAID)
620
624
  ? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
@@ -627,18 +631,22 @@ export class SessionManager {
627
631
  if (existing) {
628
632
  const validSessionId = this.validateSessionFile(existing);
629
633
  if (metadata) {
634
+ const creatorPeerId = existing.metadata?.peerId;
630
635
  existing.metadata = { ...(existing.metadata || {}), ...metadata };
636
+ if (creatorPeerId && existing.metadata)
637
+ existing.metadata.peerId = creatorPeerId;
631
638
  this.persistSession(existing, 'none');
632
639
  }
633
640
  return { ...existing, agentSessionId: validSessionId };
634
641
  }
635
642
  }
636
- // Inherit project path & chatType from active main session
637
- const activeMain = this.readActive(channel, channelId, channelType, selfAID);
638
- const projectPath = (activeMain && !activeMain.threadId ? activeMain.projectPath : undefined) || defaultProjectPath;
639
- const inheritedChatType = (activeMain && !activeMain.threadId ? activeMain.chatType : undefined) || 'private';
643
+ const projectPath = defaultProjectPath;
644
+ const effectiveChatType = chatType || 'private';
640
645
  const effectiveChannelType = channelType || channel;
641
646
  const sessionKey = formatSessionKey(effectiveChannelType, channelId, threadId);
647
+ // 继承主会话的 agentId(话题会话从属于主会话,应沿用相同 backend)
648
+ const mainActive = readJsonFile(path.join(chatDir, 'active.json'));
649
+ const inheritedAgentId = agentId || mainActive?.agentType || 'claude';
642
650
  const session = {
643
651
  id: generateSessionId(),
644
652
  channel,
@@ -647,10 +655,10 @@ export class SessionManager {
647
655
  selfAID: selfAID || '',
648
656
  projectPath,
649
657
  threadId,
650
- agentId: agentId || 'claude',
658
+ agentId: inheritedAgentId,
651
659
  sessionKey,
652
- chatType: inheritedChatType,
653
- sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType, peerType),
660
+ chatType: effectiveChatType,
661
+ sessionMode: this.resolveDefaultSessionMode(channel, effectiveChatType, peerType),
654
662
  metadata,
655
663
  name: name || '话题会话',
656
664
  createdAt: Date.now(),
@@ -1024,11 +1032,17 @@ export class SessionManager {
1024
1032
  const existingDir = this.findExistingChatDir(channel, channelId);
1025
1033
  let channelType = channel;
1026
1034
  let selfAID = '';
1035
+ let inheritedRole = 'guest';
1027
1036
  if (existingDir) {
1028
1037
  const active = readJsonFile(path.join(existingDir, 'active.json'));
1029
1038
  if (active) {
1030
1039
  channelType = active.channelType || channel;
1031
1040
  selfAID = active.selfAID || '';
1041
+ // 从现有 session 的 permissionMode 反推 role,bypass→owner,其余→guest
1042
+ if (active.permissionMode === 'bypass')
1043
+ inheritedRole = 'owner';
1044
+ if (!agentId && active.agentType)
1045
+ agentId = active.agentType;
1032
1046
  }
1033
1047
  }
1034
1048
  const session = {
@@ -1043,7 +1057,7 @@ export class SessionManager {
1043
1057
  sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
1044
1058
  chatType: inheritedChatType,
1045
1059
  sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
1046
- metadata: { permissionMode: DEFAULT_PERMISSION_MODE },
1060
+ metadata: { permissionMode: this.resolvePermissionMode(inheritedRole) },
1047
1061
  name: name || '默认会话',
1048
1062
  createdAt: Date.now(),
1049
1063
  updatedAt: Date.now(),
@@ -0,0 +1,26 @@
1
+ const SYSTEM_PROMPT_MARKERS = [
2
+ '--- [SYSTEM_PROMPT_END] ---',
3
+ '<system-reminder>',
4
+ 'EvolClaw Context Kit documents are shown below.',
5
+ ];
6
+ const MESSAGE_ROUTE_PREFIX_RE = /^‹[^›]*·\s*from:[^›]*→\s*self:[^›]*›\s*/;
7
+ export function sanitizeSessionTitle(title) {
8
+ if (typeof title !== 'string')
9
+ return undefined;
10
+ let cleanTitle = title;
11
+ for (const marker of SYSTEM_PROMPT_MARKERS) {
12
+ const markerIndex = cleanTitle.indexOf(marker);
13
+ if (markerIndex >= 0) {
14
+ cleanTitle = cleanTitle.slice(0, markerIndex);
15
+ }
16
+ }
17
+ cleanTitle = cleanTitle.trim().replace(MESSAGE_ROUTE_PREFIX_RE, '').replace(/\s+/g, ' ');
18
+ if (!cleanTitle)
19
+ return undefined;
20
+ if (/^\.+$/.test(cleanTitle))
21
+ return undefined;
22
+ return cleanTitle.length > 80 ? `${cleanTitle.slice(0, 80)}...` : cleanTitle;
23
+ }
24
+ export function displaySessionTitle(title, fallback = '默认会话') {
25
+ return sanitizeSessionTitle(title) || sanitizeSessionTitle(fallback) || fallback;
26
+ }
@@ -0,0 +1,151 @@
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
+ * 包路径($PACKAGE_ROOT/data/stats/)为基线,用户路径($EVOLCLAW_HOME/data/stats/)为追加/覆盖。
15
+ * 两层合并(append),用户层行追加在包层之后——查价时 effective_from 越大越优先,天然正确。
16
+ */
17
+ function _loadJsonlMerged(evolclawHome, filename) {
18
+ const results = [];
19
+ // 1. 包路径(基线)
20
+ const pkgFile = path.join(getPackageRoot(), 'data', 'stats', filename);
21
+ if (fs.existsSync(pkgFile)) {
22
+ try {
23
+ const lines = fs.readFileSync(pkgFile, 'utf-8').split('\n').filter(Boolean);
24
+ for (const l of lines)
25
+ results.push(JSON.parse(l));
26
+ }
27
+ catch { /* skip */ }
28
+ }
29
+ // 2. 用户路径(追加/覆盖)
30
+ const userFile = path.join(evolclawHome, 'data', 'stats', filename);
31
+ if (fs.existsSync(userFile)) {
32
+ try {
33
+ const lines = fs.readFileSync(userFile, 'utf-8').split('\n').filter(Boolean);
34
+ for (const l of lines)
35
+ results.push(JSON.parse(l));
36
+ }
37
+ catch { /* skip */ }
38
+ }
39
+ return results;
40
+ }
41
+ let _aliasCache = null;
42
+ let _aliasCacheTs = 0;
43
+ function loadAliases(evolclawHome) {
44
+ const now = Date.now();
45
+ if (_aliasCache && now - _aliasCacheTs < PRICE_CACHE_TTL)
46
+ return _aliasCache;
47
+ _aliasCache = _loadJsonlMerged(evolclawHome, 'model-aliases.jsonl');
48
+ _aliasCacheTs = now;
49
+ return _aliasCache;
50
+ }
51
+ /** 解析模型 ID → 定价表规范 ID。精确匹配 alias 表,找不到返回原 ID。 */
52
+ export function resolveCanonicalModel(evolclawHome, model) {
53
+ const aliases = loadAliases(evolclawHome);
54
+ const entry = aliases.find(a => a.alias === model);
55
+ return entry ? entry.canonical : model;
56
+ }
57
+ function loadPrices(evolclawHome) {
58
+ const now = Date.now();
59
+ if (_priceCache && now - _priceCacheTs < PRICE_CACHE_TTL)
60
+ return _priceCache;
61
+ _priceCache = _loadJsonlMerged(evolclawHome, 'model-prices.jsonl');
62
+ _priceCacheTs = now;
63
+ return _priceCache;
64
+ }
65
+ /** 取 model 在 ts 时刻生效的价格行(effective_from <= ts 中最新的一条)。
66
+ * 精确匹配失败时,通过 model-aliases.jsonl 映射到规范 ID 再查一次。 */
67
+ export function resolvePriceRow(evolclawHome, model, ts) {
68
+ const prices = loadPrices(evolclawHome);
69
+ let candidates = prices.filter(p => p.model === model && p.effective_from <= ts);
70
+ if (!candidates.length) {
71
+ // fallback: alias → canonical
72
+ const canonical = resolveCanonicalModel(evolclawHome, model);
73
+ if (canonical !== model) {
74
+ candidates = prices.filter(p => p.model === canonical && p.effective_from <= ts);
75
+ }
76
+ }
77
+ if (!candidates.length)
78
+ return null;
79
+ return candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
80
+ }
81
+ const BILLING_FNS = {
82
+ // 通用 per-token(Claude / OpenAI 兼容 / Kimi / MiniMax)
83
+ 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;
88
+ return p.currency === 'CNY' ? { cny: r } : { usd: r };
89
+ },
90
+ // DeepSeek cache_hit / cache_miss 口径
91
+ per_token_deepseek_v1: (e, p) => {
92
+ const r = (p.price_cache_hit ?? 0) * (e.cache_hit_tokens ?? 0) / 1e6
93
+ + (p.price_cache_miss ?? 0) * (e.cache_miss_tokens ?? 0) / 1e6
94
+ + (p.price_output ?? 0) * e.output_tokens / 1e6;
95
+ return { cny: r };
96
+ },
97
+ // Gemini 分档(按 total_context_tokens 确定所在档位)
98
+ per_token_tiered_v1: (e, p) => {
99
+ const tiers = p.tiers;
100
+ if (!Array.isArray(tiers))
101
+ return {};
102
+ const ctx = e.total_context_tokens ?? e.input_tokens;
103
+ const tier = tiers.find(t => t.up_to_tokens == null || ctx <= t.up_to_tokens) ?? tiers[tiers.length - 1];
104
+ const r = (tier.price_input ?? 0) * e.input_tokens / 1e6
105
+ + (tier.price_output ?? 0) * e.output_tokens / 1e6
106
+ + (tier.price_cache_read ?? 0) * e.cache_read_tokens / 1e6;
107
+ return p.currency === 'CNY' ? { cny: r } : { usd: r };
108
+ },
109
+ // 视觉模型(含 image_tokens 单独计费)
110
+ per_token_image_v1: (e, p) => {
111
+ const r = (p.price_input ?? 0) * e.input_tokens / 1e6
112
+ + (p.price_output ?? 0) * e.output_tokens / 1e6
113
+ + (p.price_image ?? 0) * (e.image_tokens ?? 0) / 1e6;
114
+ return p.currency === 'CNY' ? { cny: r } : { usd: r };
115
+ },
116
+ };
117
+ /** 注册新计费函数(扩展点)。 */
118
+ export function registerBillingFn(id, fn) {
119
+ BILLING_FNS[id] = fn;
120
+ }
121
+ /** 计算一条 usage_event 的费用。找不到价格行时返回 {}。 */
122
+ export function calcCost(evolclawHome, event) {
123
+ const priceRow = resolvePriceRow(evolclawHome, event.model, event.ts);
124
+ if (!priceRow)
125
+ return {};
126
+ const fn = BILLING_FNS[event.billing_fn] ?? BILLING_FNS[priceRow.billing_fn];
127
+ if (!fn)
128
+ return {};
129
+ return fn(event, priceRow);
130
+ }
131
+ let _specCache = null;
132
+ let _specCacheTs = 0;
133
+ function loadSpecs(evolclawHome) {
134
+ const now = Date.now();
135
+ if (_specCache && now - _specCacheTs < PRICE_CACHE_TTL)
136
+ return _specCache;
137
+ _specCache = _loadJsonlMerged(evolclawHome, 'model-specs.jsonl');
138
+ _specCacheTs = now;
139
+ return _specCache;
140
+ }
141
+ /** 取模型在 ts 时刻的能力参数。找不到时返回默认值。 */
142
+ export function resolveModelSpec(evolclawHome, model, ts) {
143
+ const specs = loadSpecs(evolclawHome);
144
+ const t = ts ?? Date.now();
145
+ const candidates = specs.filter(s => s.model === model && s.effective_from <= t);
146
+ if (candidates.length) {
147
+ return candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
148
+ }
149
+ // 默认值
150
+ return { model, effective_from: 0, context_window: 200000, max_input_tokens: 180000, max_output_tokens: 8192 };
151
+ }
@@ -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(evolclawHome) {
9
+ const file = path.join(evolclawHome, '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(evolclawHome, agentAid) {
37
+ const now = new Date();
38
+ const from_ts = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
39
+ return _calcRangeUsd(evolclawHome, from_ts, agentAid);
40
+ }
41
+ /** 计算本月实际 USD 消耗。 */
42
+ function _calcMonthUsd(evolclawHome, agentAid) {
43
+ const now = new Date();
44
+ const from_ts = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
45
+ return _calcRangeUsd(evolclawHome, from_ts, agentAid);
46
+ }
47
+ /** 计算指定时间范围的 USD 消耗(逐行算 cost)。 */
48
+ function _calcRangeUsd(evolclawHome, from_ts, agentAid) {
49
+ const db = openReadonlyDb(getDbPath(evolclawHome));
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(evolclawHome, r);
59
+ total += cost.usd ?? 0;
60
+ }
61
+ }
62
+ finally {
63
+ db.close();
64
+ }
65
+ return total;
66
+ }
67
+ export function getBudgetStatus(evolclawHome, agentAid, peerKey) {
68
+ const budgets = loadBudgets(evolclawHome);
69
+ const cfg = _resolveCfg(budgets, agentAid, peerKey);
70
+ const dailyLimit = cfg.daily_usd ?? Infinity;
71
+ const dailyUsed = _calcTodayUsd(evolclawHome, 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(evolclawHome, 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
+ }