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.
- package/CHANGELOG.md +41 -0
- package/README.md +27 -2
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -27
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1069 -141
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +28 -0
- package/dist/aun/aid/control-aid.js +67 -0
- package/dist/aun/aid/identity.js +20 -7
- package/dist/aun/aid/store.js +2 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +538 -325
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +98 -151
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent.js +44 -13
- package/dist/cli/index.js +207 -46
- package/dist/cli/init-channel.js +38 -148
- package/dist/cli/init.js +192 -85
- package/dist/cli/model.js +1 -1
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +48 -11
- package/dist/core/channel-loader.js +84 -82
- package/dist/core/command-handler.js +754 -172
- package/dist/core/daemon-file-cache.js +216 -0
- package/dist/core/evolagent-registry.js +4 -0
- package/dist/core/evolagent.js +28 -23
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +215 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +35 -13
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +52 -22
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +336 -68
- package/dist/core/message/message-queue.js +15 -8
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +56 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +40 -7
- package/dist/core/permission.js +9 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +27 -13
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +314 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/{agents → eck}/kit-renderer.js +5 -1
- package/dist/{agents → eck}/manifest-engine.js +127 -35
- package/dist/{agents → eck}/message-renderer.js +26 -1
- package/dist/index.js +185 -8
- package/dist/ipc.js +22 -0
- package/dist/paths.js +7 -3
- package/dist/utils/cross-platform.js +23 -5
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/stats.js +14 -0
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/system-fragments/response-depth.md +16 -0
- package/package.json +4 -4
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/relation/peer-key.js +0 -16
- 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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
637
|
-
const
|
|
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:
|
|
658
|
+
agentId: inheritedAgentId,
|
|
651
659
|
sessionKey,
|
|
652
|
-
chatType:
|
|
653
|
-
sessionMode: this.resolveDefaultSessionMode(channel,
|
|
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:
|
|
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
|
+
}
|