evolclaw 3.2.0 → 3.4.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 +53 -0
- package/README.md +7 -4
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -31
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1152 -140
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +58 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/outbox.js +14 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +869 -358
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +125 -154
- 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-command.js +591 -0
- package/dist/cli/agent.js +23 -8
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +23 -4905
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +88 -83
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +82 -0
- package/dist/core/evolagent.js +17 -1
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +63 -1
- package/dist/core/message/im-renderer.js +91 -51
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +73 -24
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +432 -94
- package/dist/core/message/message-queue.js +70 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +25 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +86 -26
- 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 +334 -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/data/error-dict.json +7 -0
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +174 -9
- package/dist/ipc.js +116 -1
- package/dist/utils/cross-platform.js +58 -5
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +77 -6
- 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_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/package.json +5 -6
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/command-handler.js +0 -3876
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/evolclaw-config.js +0 -11
- package/dist/utils/channel-helpers.js +0 -46
- /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
- /package/dist/{agents → eck}/kit-renderer.js +0 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* query.ts — 聚合查询(只读,供 CLI 和 ecweb 共用)。
|
|
3
|
+
* 支持跨年查询:自动附加归档库。
|
|
4
|
+
*/
|
|
5
|
+
import { openReadonlyDb, getDbPath, listArchivePaths } from './db.js';
|
|
6
|
+
import { calcCost } from './billing.js';
|
|
7
|
+
const GRAN_FMT = {
|
|
8
|
+
hour: '%Y-%m-%d %H:00',
|
|
9
|
+
day: '%Y-%m-%d',
|
|
10
|
+
week: '%Y-W%W',
|
|
11
|
+
month: '%Y-%m',
|
|
12
|
+
};
|
|
13
|
+
// 非时间维度:按字段分组
|
|
14
|
+
const GRAN_GROUP_COL = {
|
|
15
|
+
model: 'model',
|
|
16
|
+
peer: 'peer_key',
|
|
17
|
+
agent: 'agent_aid',
|
|
18
|
+
};
|
|
19
|
+
function _buildWhere(f) {
|
|
20
|
+
const conds = [];
|
|
21
|
+
const params = [];
|
|
22
|
+
if (f.from_ts) {
|
|
23
|
+
conds.push('ts >= ?');
|
|
24
|
+
params.push(f.from_ts);
|
|
25
|
+
}
|
|
26
|
+
if (f.to_ts) {
|
|
27
|
+
conds.push('ts < ?');
|
|
28
|
+
params.push(f.to_ts);
|
|
29
|
+
}
|
|
30
|
+
if (f.agent_aid) {
|
|
31
|
+
conds.push('agent_aid = ?');
|
|
32
|
+
params.push(f.agent_aid);
|
|
33
|
+
}
|
|
34
|
+
if (f.peer_key) {
|
|
35
|
+
conds.push('peer_key = ?');
|
|
36
|
+
params.push(f.peer_key);
|
|
37
|
+
}
|
|
38
|
+
if (f.model) {
|
|
39
|
+
conds.push('model = ?');
|
|
40
|
+
params.push(f.model);
|
|
41
|
+
}
|
|
42
|
+
if (f.session_id) {
|
|
43
|
+
conds.push('session_id = ?');
|
|
44
|
+
params.push(f.session_id);
|
|
45
|
+
}
|
|
46
|
+
if (f.billing_fn) {
|
|
47
|
+
conds.push('billing_fn = ?');
|
|
48
|
+
params.push(f.billing_fn);
|
|
49
|
+
}
|
|
50
|
+
return { clause: conds.length ? 'WHERE ' + conds.join(' AND ') : '', params };
|
|
51
|
+
}
|
|
52
|
+
/** 查询哪些归档库需要参与(给定时间范围)。 */
|
|
53
|
+
function _relevantDbs(evolclawHome, f) {
|
|
54
|
+
const dbPaths = [getDbPath(evolclawHome)];
|
|
55
|
+
if (!f.from_ts)
|
|
56
|
+
return dbPaths; // 无时间下界,只查主库(最近数据)
|
|
57
|
+
const fromYear = new Date(f.from_ts).getUTCFullYear();
|
|
58
|
+
const toYear = f.to_ts ? new Date(f.to_ts).getUTCFullYear() : new Date().getUTCFullYear();
|
|
59
|
+
for (const { year, path: p } of listArchivePaths(evolclawHome)) {
|
|
60
|
+
if (year >= fromYear && year <= toYear)
|
|
61
|
+
dbPaths.push(p);
|
|
62
|
+
}
|
|
63
|
+
return dbPaths;
|
|
64
|
+
}
|
|
65
|
+
function _sumRows(rows) {
|
|
66
|
+
if (!rows.length)
|
|
67
|
+
return null;
|
|
68
|
+
const sum = { ...rows[0] };
|
|
69
|
+
for (let i = 1; i < rows.length; i++) {
|
|
70
|
+
for (const k of Object.keys(rows[i])) {
|
|
71
|
+
if (k === 'period')
|
|
72
|
+
continue;
|
|
73
|
+
sum[k] = (sum[k] ?? 0) + (rows[i][k] ?? 0);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return sum;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 聚合统计(按粒度分组)。
|
|
80
|
+
* hour 粒度仍扫明细 usage_events(只查近期,本就快,且明细才有小时粒度)。
|
|
81
|
+
* 其余粒度(day/week/month/model/peer/agent)改读预聚合表 usage_daily:
|
|
82
|
+
* 工作量从"全表明细"降到"天×维度"小表。rollup 永留主库,无需跨归档库 union。
|
|
83
|
+
* 成本仍按 (model, billing_fn) 分组现算 calcCost,口径不变。
|
|
84
|
+
*/
|
|
85
|
+
export function queryAggregated(evolclawHome, granularity, filter) {
|
|
86
|
+
if (granularity === 'hour')
|
|
87
|
+
return _queryAggregatedEvents(evolclawHome, granularity, filter);
|
|
88
|
+
return _queryAggregatedDaily(evolclawHome, granularity, filter);
|
|
89
|
+
}
|
|
90
|
+
// rollup 时间粒度的 period 表达式(day 列是 'YYYY-MM-DD' 字符串,strftime 可直接解析)。
|
|
91
|
+
const DAILY_PERIOD_EXPR = {
|
|
92
|
+
day: 'day',
|
|
93
|
+
week: `strftime('%Y-W%W', day)`,
|
|
94
|
+
month: `substr(day,1,7)`,
|
|
95
|
+
};
|
|
96
|
+
/** 针对 usage_daily 构造 WHERE:时间过滤转成对 day 列的字符串比较,维度过滤直接相等。 */
|
|
97
|
+
function _buildDailyWhere(f) {
|
|
98
|
+
const conds = [];
|
|
99
|
+
const params = [];
|
|
100
|
+
// from_ts/to_ts(ms,含 to_ts 排他)转成 localtime 日期串,与 day 列同口径比较。
|
|
101
|
+
if (f.from_ts) {
|
|
102
|
+
conds.push(`day >= strftime('%Y-%m-%d', ?/1000, 'unixepoch', 'localtime')`);
|
|
103
|
+
params.push(f.from_ts);
|
|
104
|
+
}
|
|
105
|
+
if (f.to_ts) {
|
|
106
|
+
conds.push(`day < strftime('%Y-%m-%d', ?/1000, 'unixepoch', 'localtime')`);
|
|
107
|
+
params.push(f.to_ts);
|
|
108
|
+
}
|
|
109
|
+
if (f.agent_aid) {
|
|
110
|
+
conds.push('agent_aid = ?');
|
|
111
|
+
params.push(f.agent_aid);
|
|
112
|
+
}
|
|
113
|
+
if (f.peer_key) {
|
|
114
|
+
conds.push('peer_key = ?');
|
|
115
|
+
params.push(f.peer_key);
|
|
116
|
+
}
|
|
117
|
+
if (f.model) {
|
|
118
|
+
conds.push('model = ?');
|
|
119
|
+
params.push(f.model);
|
|
120
|
+
}
|
|
121
|
+
if (f.session_id) {
|
|
122
|
+
conds.push('session_id = ?');
|
|
123
|
+
params.push(f.session_id);
|
|
124
|
+
}
|
|
125
|
+
if (f.billing_fn) {
|
|
126
|
+
conds.push('billing_fn = ?');
|
|
127
|
+
params.push(f.billing_fn);
|
|
128
|
+
}
|
|
129
|
+
return { clause: conds.length ? 'WHERE ' + conds.join(' AND ') : '', params };
|
|
130
|
+
}
|
|
131
|
+
/** 走预聚合表 usage_daily 的聚合(day/week/month/model/peer/agent)。 */
|
|
132
|
+
function _queryAggregatedDaily(evolclawHome, granularity, filter) {
|
|
133
|
+
const { clause, params } = _buildDailyWhere(filter);
|
|
134
|
+
const groupCol = GRAN_GROUP_COL[granularity];
|
|
135
|
+
const periodExpr = groupCol || DAILY_PERIOD_EXPR[granularity] || 'day';
|
|
136
|
+
const db = openReadonlyDb(getDbPath(evolclawHome));
|
|
137
|
+
if (!db)
|
|
138
|
+
return [];
|
|
139
|
+
try {
|
|
140
|
+
const sql = `
|
|
141
|
+
SELECT
|
|
142
|
+
${periodExpr} AS period,
|
|
143
|
+
SUM(input_tokens) AS input_tokens,
|
|
144
|
+
SUM(output_tokens) AS output_tokens,
|
|
145
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens,
|
|
146
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
147
|
+
SUM(cache_hit_tokens) AS cache_hit_tokens,
|
|
148
|
+
SUM(cache_miss_tokens) AS cache_miss_tokens,
|
|
149
|
+
SUM(image_tokens) AS image_tokens,
|
|
150
|
+
SUM(total_context_tokens) AS total_context_tokens,
|
|
151
|
+
SUM(turns) AS turns,
|
|
152
|
+
SUM(calls) AS call_count
|
|
153
|
+
FROM usage_daily ${clause}
|
|
154
|
+
GROUP BY ${periodExpr}
|
|
155
|
+
`;
|
|
156
|
+
const periodMap = new Map();
|
|
157
|
+
for (const r of db.prepare(sql).all(...params))
|
|
158
|
+
periodMap.set(r.period, r);
|
|
159
|
+
// 按 period + model + billing_fn 分组精确计费(口径与原明细路径一致)。
|
|
160
|
+
const costSql = `
|
|
161
|
+
SELECT ${periodExpr} AS period, model, COALESCE(billing_fn,'') AS billing_fn,
|
|
162
|
+
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
|
|
163
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens, SUM(cache_read_tokens) AS cache_read_tokens,
|
|
164
|
+
SUM(cache_hit_tokens) AS cache_hit_tokens, SUM(cache_miss_tokens) AS cache_miss_tokens,
|
|
165
|
+
SUM(image_tokens) AS image_tokens, SUM(total_context_tokens) AS total_context_tokens
|
|
166
|
+
FROM usage_daily ${clause}
|
|
167
|
+
GROUP BY ${periodExpr}, model, billing_fn
|
|
168
|
+
`;
|
|
169
|
+
const costMap = new Map();
|
|
170
|
+
for (const r of db.prepare(costSql).all(...params)) {
|
|
171
|
+
const cost = calcCost(evolclawHome, {
|
|
172
|
+
model: r.model || 'unknown',
|
|
173
|
+
billing_fn: r.billing_fn || 'per_token_v1',
|
|
174
|
+
ts: Date.now(),
|
|
175
|
+
input_tokens: r.input_tokens ?? 0,
|
|
176
|
+
output_tokens: r.output_tokens ?? 0,
|
|
177
|
+
cache_creation_tokens: r.cache_creation_tokens ?? 0,
|
|
178
|
+
cache_read_tokens: r.cache_read_tokens ?? 0,
|
|
179
|
+
cache_hit_tokens: r.cache_hit_tokens ?? 0,
|
|
180
|
+
cache_miss_tokens: r.cache_miss_tokens ?? 0,
|
|
181
|
+
image_tokens: r.image_tokens ?? 0,
|
|
182
|
+
total_context_tokens: r.total_context_tokens ?? 0,
|
|
183
|
+
});
|
|
184
|
+
const e = costMap.get(r.period) ?? { usd: 0, cny: 0 };
|
|
185
|
+
e.usd += cost.usd ?? 0;
|
|
186
|
+
e.cny += cost.cny ?? 0;
|
|
187
|
+
costMap.set(r.period, e);
|
|
188
|
+
}
|
|
189
|
+
const sorted = Array.from(periodMap.entries());
|
|
190
|
+
if (groupCol) {
|
|
191
|
+
sorted.sort((a, b) => ((b[1].input_tokens ?? 0) + (b[1].output_tokens ?? 0)) - ((a[1].input_tokens ?? 0) + (a[1].output_tokens ?? 0)));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
sorted.sort((a, b) => a[0].localeCompare(b[0]));
|
|
195
|
+
}
|
|
196
|
+
return sorted.map(([, r]) => _enrichRow(evolclawHome, r, costMap.get(r.period)));
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
db.close();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 走明细 usage_events 的聚合(仅 hour 粒度)。
|
|
204
|
+
* 跨年时合并多个 DB 的结果,再按 period 聚合。
|
|
205
|
+
*/
|
|
206
|
+
function _queryAggregatedEvents(evolclawHome, granularity, filter) {
|
|
207
|
+
const { clause, params } = _buildWhere(filter);
|
|
208
|
+
const groupCol = GRAN_GROUP_COL[granularity];
|
|
209
|
+
let sql;
|
|
210
|
+
if (groupCol) {
|
|
211
|
+
// 非时间维度:按字段分组
|
|
212
|
+
sql = `
|
|
213
|
+
SELECT
|
|
214
|
+
${groupCol} AS period,
|
|
215
|
+
SUM(input_tokens) AS input_tokens,
|
|
216
|
+
SUM(output_tokens) AS output_tokens,
|
|
217
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens,
|
|
218
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
219
|
+
SUM(COALESCE(cache_hit_tokens,0)) AS cache_hit_tokens,
|
|
220
|
+
SUM(COALESCE(cache_miss_tokens,0)) AS cache_miss_tokens,
|
|
221
|
+
SUM(COALESCE(image_tokens,0)) AS image_tokens,
|
|
222
|
+
SUM(COALESCE(total_context_tokens,0)) AS total_context_tokens,
|
|
223
|
+
SUM(turns) AS turns,
|
|
224
|
+
COUNT(*) AS call_count
|
|
225
|
+
FROM usage_events ${clause}
|
|
226
|
+
GROUP BY ${groupCol} ORDER BY (input_tokens + output_tokens) DESC
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// 时间维度:strftime 分组
|
|
231
|
+
const fmt = GRAN_FMT[granularity] || GRAN_FMT.day;
|
|
232
|
+
sql = `
|
|
233
|
+
SELECT
|
|
234
|
+
strftime('${fmt}', ts/1000, 'unixepoch', 'localtime') AS period,
|
|
235
|
+
SUM(input_tokens) AS input_tokens,
|
|
236
|
+
SUM(output_tokens) AS output_tokens,
|
|
237
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens,
|
|
238
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
239
|
+
SUM(COALESCE(cache_hit_tokens,0)) AS cache_hit_tokens,
|
|
240
|
+
SUM(COALESCE(cache_miss_tokens,0)) AS cache_miss_tokens,
|
|
241
|
+
SUM(COALESCE(image_tokens,0)) AS image_tokens,
|
|
242
|
+
SUM(COALESCE(total_context_tokens,0)) AS total_context_tokens,
|
|
243
|
+
SUM(turns) AS turns,
|
|
244
|
+
COUNT(*) AS call_count
|
|
245
|
+
FROM usage_events ${clause}
|
|
246
|
+
GROUP BY period ORDER BY period
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
// 收集所有相关 DB 的原始行,再按 period 合并
|
|
250
|
+
const periodMap = new Map();
|
|
251
|
+
for (const dbPath of _relevantDbs(evolclawHome, filter)) {
|
|
252
|
+
const db = openReadonlyDb(dbPath);
|
|
253
|
+
if (!db)
|
|
254
|
+
continue;
|
|
255
|
+
try {
|
|
256
|
+
const rows = db.prepare(sql).all(...params);
|
|
257
|
+
for (const r of rows) {
|
|
258
|
+
const existing = periodMap.get(r.period);
|
|
259
|
+
periodMap.set(r.period, existing ? _sumRows([existing, r]) : r);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
db.close();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const sorted = Array.from(periodMap.entries());
|
|
267
|
+
if (groupCol) {
|
|
268
|
+
// 非时间维度按 total tokens 降序
|
|
269
|
+
sorted.sort((a, b) => ((b[1].input_tokens ?? 0) + (b[1].output_tokens ?? 0)) - ((a[1].input_tokens ?? 0) + (a[1].output_tokens ?? 0)));
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
sorted.sort((a, b) => a[0].localeCompare(b[0]));
|
|
273
|
+
}
|
|
274
|
+
// ── 方案 B:按 period + model + billing_fn 分组精确计费 ──
|
|
275
|
+
// 辅助 SQL:保留 model/billing_fn 维度用于逐组调用 calcCost
|
|
276
|
+
const costSql = groupCol
|
|
277
|
+
? `SELECT ${groupCol} AS period, model, COALESCE(billing_fn,'') AS billing_fn,
|
|
278
|
+
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
|
|
279
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens, SUM(cache_read_tokens) AS cache_read_tokens,
|
|
280
|
+
SUM(COALESCE(image_tokens,0)) AS image_tokens
|
|
281
|
+
FROM usage_events ${clause}
|
|
282
|
+
GROUP BY ${groupCol}, model, billing_fn`
|
|
283
|
+
: `SELECT strftime('${GRAN_FMT[granularity] || GRAN_FMT.day}', ts/1000, 'unixepoch', 'localtime') AS period,
|
|
284
|
+
model, COALESCE(billing_fn,'') AS billing_fn,
|
|
285
|
+
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
|
|
286
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens, SUM(cache_read_tokens) AS cache_read_tokens,
|
|
287
|
+
SUM(COALESCE(image_tokens,0)) AS image_tokens
|
|
288
|
+
FROM usage_events ${clause}
|
|
289
|
+
GROUP BY period, model, billing_fn`;
|
|
290
|
+
const costMap = new Map();
|
|
291
|
+
for (const dbPath of _relevantDbs(evolclawHome, filter)) {
|
|
292
|
+
const db = openReadonlyDb(dbPath);
|
|
293
|
+
if (!db)
|
|
294
|
+
continue;
|
|
295
|
+
try {
|
|
296
|
+
const rows = db.prepare(costSql).all(...params);
|
|
297
|
+
for (const r of rows) {
|
|
298
|
+
const cost = calcCost(evolclawHome, {
|
|
299
|
+
model: r.model || 'unknown',
|
|
300
|
+
billing_fn: r.billing_fn || 'per_token_v1',
|
|
301
|
+
ts: Date.now(),
|
|
302
|
+
input_tokens: r.input_tokens ?? 0,
|
|
303
|
+
output_tokens: r.output_tokens ?? 0,
|
|
304
|
+
cache_creation_tokens: r.cache_creation_tokens ?? 0,
|
|
305
|
+
cache_read_tokens: r.cache_read_tokens ?? 0,
|
|
306
|
+
image_tokens: r.image_tokens ?? 0,
|
|
307
|
+
});
|
|
308
|
+
const existing = costMap.get(r.period) ?? { usd: 0, cny: 0 };
|
|
309
|
+
existing.usd += cost.usd ?? 0;
|
|
310
|
+
existing.cny += cost.cny ?? 0;
|
|
311
|
+
costMap.set(r.period, existing);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
db.close();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return sorted.map(([, r]) => _enrichRow(evolclawHome, r, costMap.get(r.period)));
|
|
319
|
+
}
|
|
320
|
+
/** 查今日概览(单行汇总)。 */
|
|
321
|
+
export function queryTodaySummary(evolclawHome, agentAid) {
|
|
322
|
+
const now = new Date();
|
|
323
|
+
const from_ts = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
324
|
+
const rows = queryAggregated(evolclawHome, 'day', { from_ts, agent_aid: agentAid });
|
|
325
|
+
return rows[0] ?? null;
|
|
326
|
+
}
|
|
327
|
+
/** 查单个会话的每轮明细。 */
|
|
328
|
+
export function querySessionTurns(evolclawHome, sessionId) {
|
|
329
|
+
const { clause, params } = _buildWhere({ session_id: sessionId });
|
|
330
|
+
const sql = `SELECT * FROM usage_events ${clause} ORDER BY ts`;
|
|
331
|
+
const result = [];
|
|
332
|
+
for (const dbPath of _relevantDbs(evolclawHome, {})) {
|
|
333
|
+
const db = openReadonlyDb(dbPath);
|
|
334
|
+
if (!db)
|
|
335
|
+
continue;
|
|
336
|
+
try {
|
|
337
|
+
const rows = db.prepare(sql).all(...params);
|
|
338
|
+
for (const r of rows) {
|
|
339
|
+
const cost = calcCost(evolclawHome, r);
|
|
340
|
+
result.push({ ...r, usd: cost.usd ?? 0, cny: cost.cny ?? 0 });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
db.close();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return result.sort((a, b) => a.ts - b.ts);
|
|
348
|
+
}
|
|
349
|
+
/** 查会话 context_breakdown 细目(每轮各段 token)。 */
|
|
350
|
+
export function queryContextBreakdown(evolclawHome, sessionId) {
|
|
351
|
+
const sql = `SELECT * FROM context_breakdown WHERE session_id = ? ORDER BY ts`;
|
|
352
|
+
const result = [];
|
|
353
|
+
for (const dbPath of _relevantDbs(evolclawHome, {})) {
|
|
354
|
+
const db = openReadonlyDb(dbPath);
|
|
355
|
+
if (!db)
|
|
356
|
+
continue;
|
|
357
|
+
try {
|
|
358
|
+
result.push(...db.prepare(sql).all(sessionId));
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
db.close();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return result.sort((a, b) => a.ts - b.ts);
|
|
365
|
+
}
|
|
366
|
+
/** Top-N 对端(按 token 总量排序)。读预聚合表 usage_daily。 */
|
|
367
|
+
export function queryTopPeers(evolclawHome, filter, limit = 10) {
|
|
368
|
+
const { clause, params } = _buildDailyWhere(filter);
|
|
369
|
+
const sql = `
|
|
370
|
+
SELECT peer_key, SUM(input_tokens+output_tokens) AS total_tokens,
|
|
371
|
+
SUM(calls) AS call_count
|
|
372
|
+
FROM usage_daily ${clause}
|
|
373
|
+
GROUP BY peer_key ORDER BY total_tokens DESC LIMIT ${limit}
|
|
374
|
+
`;
|
|
375
|
+
const db = openReadonlyDb(getDbPath(evolclawHome));
|
|
376
|
+
if (!db)
|
|
377
|
+
return [];
|
|
378
|
+
try {
|
|
379
|
+
return db.prepare(sql).all(...params).map(r => ({ ...r }));
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
db.close();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/** Top-N 模型(按 token 总量排序)。读预聚合表 usage_daily。 */
|
|
386
|
+
export function queryTopModels(evolclawHome, filter, limit = 10) {
|
|
387
|
+
const { clause, params } = _buildDailyWhere(filter);
|
|
388
|
+
const sql = `
|
|
389
|
+
SELECT model, SUM(input_tokens+output_tokens) AS total_tokens,
|
|
390
|
+
SUM(calls) AS call_count
|
|
391
|
+
FROM usage_daily ${clause}
|
|
392
|
+
GROUP BY model ORDER BY total_tokens DESC LIMIT ${limit}
|
|
393
|
+
`;
|
|
394
|
+
const db = openReadonlyDb(getDbPath(evolclawHome));
|
|
395
|
+
if (!db)
|
|
396
|
+
return [];
|
|
397
|
+
try {
|
|
398
|
+
return db.prepare(sql).all(...params).map(r => ({ ...r }));
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
db.close();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// ── 私聊/群聊列表 + 汇总 + 对端按天明细(owner 前端用)────────────────────────
|
|
405
|
+
/** 解析 peer_key 末段为裸 peer_id(对端 AID / 群 ID)。
|
|
406
|
+
* peer_key = aun#<selfAID>#main#<encodeURIComponent(peer)>,取第 4 段起 decode。 */
|
|
407
|
+
function _parsePeerId(peerKey) {
|
|
408
|
+
const parts = peerKey.split('#');
|
|
409
|
+
if (parts.length < 4) {
|
|
410
|
+
// 兼容两段式 aun#peer:取末段
|
|
411
|
+
try {
|
|
412
|
+
return decodeURIComponent(parts[parts.length - 1] ?? '');
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return parts[parts.length - 1] ?? '';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
return decodeURIComponent(parts.slice(3).join('#'));
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return parts.slice(3).join('#');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/** 对一组按 (model,billing_fn) 分组的行调用 calcCost 并累加,返回 {usd,cny}。 */
|
|
426
|
+
function _accumCost(evolclawHome, rows) {
|
|
427
|
+
let usd = 0, cny = 0;
|
|
428
|
+
for (const r of rows) {
|
|
429
|
+
const c = calcCost(evolclawHome, {
|
|
430
|
+
model: r.model || 'unknown',
|
|
431
|
+
billing_fn: r.billing_fn || 'per_token_v1',
|
|
432
|
+
ts: Date.now(),
|
|
433
|
+
input_tokens: r.input_tokens ?? 0,
|
|
434
|
+
output_tokens: r.output_tokens ?? 0,
|
|
435
|
+
cache_creation_tokens: r.cache_creation_tokens ?? 0,
|
|
436
|
+
cache_read_tokens: r.cache_read_tokens ?? 0,
|
|
437
|
+
cache_hit_tokens: r.cache_hit_tokens ?? 0,
|
|
438
|
+
cache_miss_tokens: r.cache_miss_tokens ?? 0,
|
|
439
|
+
image_tokens: r.image_tokens ?? 0,
|
|
440
|
+
total_context_tokens: r.total_context_tokens ?? 0,
|
|
441
|
+
});
|
|
442
|
+
usd += c.usd ?? 0;
|
|
443
|
+
cny += c.cny ?? 0;
|
|
444
|
+
}
|
|
445
|
+
return { usd, cny };
|
|
446
|
+
}
|
|
447
|
+
/** 私聊(peer_type='private')或群聊('group')列表,每项带累计汇总。读 usage_daily。 */
|
|
448
|
+
export function queryPeerList(evolclawHome, opts) {
|
|
449
|
+
const limit = opts.limit ?? 50;
|
|
450
|
+
const { clause, params } = _buildDailyWhere({
|
|
451
|
+
from_ts: opts.from_ts, to_ts: opts.to_ts, agent_aid: opts.agent_aid,
|
|
452
|
+
});
|
|
453
|
+
// peer_type 过滤拼到 WHERE 上(_buildDailyWhere 不含该字段)。
|
|
454
|
+
const peerCond = clause ? `${clause} AND peer_type = ?` : 'WHERE peer_type = ?';
|
|
455
|
+
const listParams = [...params, opts.peer_type];
|
|
456
|
+
const db = openReadonlyDb(getDbPath(evolclawHome));
|
|
457
|
+
if (!db)
|
|
458
|
+
return [];
|
|
459
|
+
try {
|
|
460
|
+
const listSql = `
|
|
461
|
+
SELECT peer_key,
|
|
462
|
+
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
|
|
463
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens,
|
|
464
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
465
|
+
SUM(input_tokens+output_tokens+cache_read_tokens+cache_creation_tokens) AS total_tokens,
|
|
466
|
+
SUM(calls) AS calls,
|
|
467
|
+
COUNT(DISTINCT session_id) AS session_count,
|
|
468
|
+
MIN(day) AS first_day, MAX(day) AS last_day,
|
|
469
|
+
MAX(peer_type) AS peer_type
|
|
470
|
+
FROM usage_daily ${peerCond}
|
|
471
|
+
GROUP BY peer_key ORDER BY total_tokens DESC LIMIT ${limit}
|
|
472
|
+
`;
|
|
473
|
+
const rows = db.prepare(listSql).all(...listParams);
|
|
474
|
+
// 每个 peer 的成本:按 (peer_key, model, billing_fn) 分组算 calcCost 累加。
|
|
475
|
+
const costSql = `
|
|
476
|
+
SELECT peer_key, model, COALESCE(billing_fn,'') AS billing_fn,
|
|
477
|
+
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
|
|
478
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens, SUM(cache_read_tokens) AS cache_read_tokens,
|
|
479
|
+
SUM(cache_hit_tokens) AS cache_hit_tokens, SUM(cache_miss_tokens) AS cache_miss_tokens,
|
|
480
|
+
SUM(image_tokens) AS image_tokens, SUM(total_context_tokens) AS total_context_tokens
|
|
481
|
+
FROM usage_daily ${peerCond}
|
|
482
|
+
GROUP BY peer_key, model, billing_fn
|
|
483
|
+
`;
|
|
484
|
+
const costByPeer = new Map();
|
|
485
|
+
for (const r of db.prepare(costSql).all(...listParams)) {
|
|
486
|
+
const arr = costByPeer.get(r.peer_key) ?? [];
|
|
487
|
+
arr.push(r);
|
|
488
|
+
costByPeer.set(r.peer_key, arr);
|
|
489
|
+
}
|
|
490
|
+
return rows.map(r => {
|
|
491
|
+
const cost = _accumCost(evolclawHome, costByPeer.get(r.peer_key) ?? []);
|
|
492
|
+
return {
|
|
493
|
+
peer_key: r.peer_key,
|
|
494
|
+
peer_id: _parsePeerId(r.peer_key),
|
|
495
|
+
peer_type: r.peer_type ?? opts.peer_type,
|
|
496
|
+
input_tokens: r.input_tokens ?? 0,
|
|
497
|
+
output_tokens: r.output_tokens ?? 0,
|
|
498
|
+
cache_creation_tokens: r.cache_creation_tokens ?? 0,
|
|
499
|
+
cache_read_tokens: r.cache_read_tokens ?? 0,
|
|
500
|
+
total_tokens: r.total_tokens ?? 0,
|
|
501
|
+
calls: r.calls ?? 0,
|
|
502
|
+
session_count: r.session_count ?? 0,
|
|
503
|
+
first_day: r.first_day ?? '',
|
|
504
|
+
last_day: r.last_day ?? '',
|
|
505
|
+
usd: cost.usd,
|
|
506
|
+
cny: cost.cny,
|
|
507
|
+
};
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
db.close();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/** 指定时间范围(可选对端)的总消耗汇总,单行。读 usage_daily。 */
|
|
515
|
+
export function querySummary(evolclawHome, opts) {
|
|
516
|
+
const { clause, params } = _buildDailyWhere({
|
|
517
|
+
from_ts: opts.from_ts, to_ts: opts.to_ts, agent_aid: opts.agent_aid, peer_key: opts.peer_key,
|
|
518
|
+
});
|
|
519
|
+
const empty = {
|
|
520
|
+
input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0,
|
|
521
|
+
total_tokens: 0, calls: 0, cache_hit_rate: 0, usd: 0, cny: 0,
|
|
522
|
+
};
|
|
523
|
+
const db = openReadonlyDb(getDbPath(evolclawHome));
|
|
524
|
+
if (!db)
|
|
525
|
+
return empty;
|
|
526
|
+
try {
|
|
527
|
+
const row = db.prepare(`
|
|
528
|
+
SELECT
|
|
529
|
+
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
|
|
530
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens,
|
|
531
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
532
|
+
SUM(calls) AS calls
|
|
533
|
+
FROM usage_daily ${clause}
|
|
534
|
+
`).get(...params);
|
|
535
|
+
if (!row || row.calls == null)
|
|
536
|
+
return empty;
|
|
537
|
+
const costRows = db.prepare(`
|
|
538
|
+
SELECT model, COALESCE(billing_fn,'') AS billing_fn,
|
|
539
|
+
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
|
|
540
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens, SUM(cache_read_tokens) AS cache_read_tokens,
|
|
541
|
+
SUM(cache_hit_tokens) AS cache_hit_tokens, SUM(cache_miss_tokens) AS cache_miss_tokens,
|
|
542
|
+
SUM(image_tokens) AS image_tokens, SUM(total_context_tokens) AS total_context_tokens
|
|
543
|
+
FROM usage_daily ${clause}
|
|
544
|
+
GROUP BY model, billing_fn
|
|
545
|
+
`).all(...params);
|
|
546
|
+
const cost = _accumCost(evolclawHome, costRows);
|
|
547
|
+
const inTok = row.input_tokens ?? 0;
|
|
548
|
+
const cacheRead = row.cache_read_tokens ?? 0;
|
|
549
|
+
const totalIn = inTok + cacheRead;
|
|
550
|
+
return {
|
|
551
|
+
input_tokens: inTok,
|
|
552
|
+
output_tokens: row.output_tokens ?? 0,
|
|
553
|
+
cache_creation_tokens: row.cache_creation_tokens ?? 0,
|
|
554
|
+
cache_read_tokens: cacheRead,
|
|
555
|
+
total_tokens: inTok + (row.output_tokens ?? 0) + cacheRead + (row.cache_creation_tokens ?? 0),
|
|
556
|
+
calls: row.calls ?? 0,
|
|
557
|
+
cache_hit_rate: totalIn > 0 ? cacheRead / totalIn : 0,
|
|
558
|
+
usd: cost.usd,
|
|
559
|
+
cny: cost.cny,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
db.close();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** 指定对端(peer_key 或 peer_id),按天返回消耗明细。读 usage_daily。 */
|
|
567
|
+
export function queryPeerDaily(evolclawHome, opts) {
|
|
568
|
+
const { clause, params } = _buildDailyWhere({
|
|
569
|
+
from_ts: opts.from_ts, to_ts: opts.to_ts, agent_aid: opts.agent_aid,
|
|
570
|
+
peer_key: opts.peer_key,
|
|
571
|
+
});
|
|
572
|
+
// peer_id:按末段 LIKE 收窄(与 peer_key 精确匹配二选一)。
|
|
573
|
+
let where = clause;
|
|
574
|
+
const qParams = [...params];
|
|
575
|
+
if (!opts.peer_key && opts.peer_id) {
|
|
576
|
+
where = where ? `${where} AND peer_key LIKE ?` : 'WHERE peer_key LIKE ?';
|
|
577
|
+
qParams.push(`aun#%#main#${encodeURIComponent(opts.peer_id)}`);
|
|
578
|
+
}
|
|
579
|
+
const db = openReadonlyDb(getDbPath(evolclawHome));
|
|
580
|
+
if (!db)
|
|
581
|
+
return [];
|
|
582
|
+
try {
|
|
583
|
+
const sql = `
|
|
584
|
+
SELECT day AS period,
|
|
585
|
+
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
|
|
586
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens,
|
|
587
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
588
|
+
SUM(cache_hit_tokens) AS cache_hit_tokens, SUM(cache_miss_tokens) AS cache_miss_tokens,
|
|
589
|
+
SUM(image_tokens) AS image_tokens, SUM(total_context_tokens) AS total_context_tokens,
|
|
590
|
+
SUM(turns) AS turns, SUM(calls) AS call_count
|
|
591
|
+
FROM usage_daily ${where}
|
|
592
|
+
GROUP BY day ORDER BY day
|
|
593
|
+
`;
|
|
594
|
+
const periodMap = new Map();
|
|
595
|
+
for (const r of db.prepare(sql).all(...qParams))
|
|
596
|
+
periodMap.set(r.period, r);
|
|
597
|
+
const costSql = `
|
|
598
|
+
SELECT day AS period, model, COALESCE(billing_fn,'') AS billing_fn,
|
|
599
|
+
SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens,
|
|
600
|
+
SUM(cache_creation_tokens) AS cache_creation_tokens, SUM(cache_read_tokens) AS cache_read_tokens,
|
|
601
|
+
SUM(cache_hit_tokens) AS cache_hit_tokens, SUM(cache_miss_tokens) AS cache_miss_tokens,
|
|
602
|
+
SUM(image_tokens) AS image_tokens, SUM(total_context_tokens) AS total_context_tokens
|
|
603
|
+
FROM usage_daily ${where}
|
|
604
|
+
GROUP BY day, model, billing_fn
|
|
605
|
+
`;
|
|
606
|
+
const costMap = new Map();
|
|
607
|
+
const byPeriod = new Map();
|
|
608
|
+
for (const r of db.prepare(costSql).all(...qParams)) {
|
|
609
|
+
const arr = byPeriod.get(r.period) ?? [];
|
|
610
|
+
arr.push(r);
|
|
611
|
+
byPeriod.set(r.period, arr);
|
|
612
|
+
}
|
|
613
|
+
for (const [period, rows] of byPeriod)
|
|
614
|
+
costMap.set(period, _accumCost(evolclawHome, rows));
|
|
615
|
+
return Array.from(periodMap.entries())
|
|
616
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
617
|
+
.map(([, r]) => _enrichRow(evolclawHome, r, costMap.get(r.period)));
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
db.close();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/** 查一个 task 的所有大模型调用明细,按 call_index 排序。 */
|
|
624
|
+
export function queryTaskModelCalls(evolclawHome, taskId) {
|
|
625
|
+
const db = openReadonlyDb(getDbPath(evolclawHome));
|
|
626
|
+
if (!db)
|
|
627
|
+
return [];
|
|
628
|
+
try {
|
|
629
|
+
return db.prepare(`SELECT * FROM model_calls WHERE task_id = ? ORDER BY call_index`).all(taskId);
|
|
630
|
+
}
|
|
631
|
+
finally {
|
|
632
|
+
db.close();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/** 查一个 evolclaw session 的所有大模型调用明细,按时间 + call_index 排序。 */
|
|
636
|
+
export function querySessionModelCalls(evolclawHome, sessionId) {
|
|
637
|
+
const db = openReadonlyDb(getDbPath(evolclawHome));
|
|
638
|
+
if (!db)
|
|
639
|
+
return [];
|
|
640
|
+
try {
|
|
641
|
+
return db.prepare(`SELECT * FROM model_calls WHERE session_id = ? ORDER BY ts, call_index`).all(sessionId);
|
|
642
|
+
}
|
|
643
|
+
finally {
|
|
644
|
+
db.close();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function _enrichRow(evolclawHome, r, cost) {
|
|
648
|
+
const totalIn = (r.input_tokens ?? 0) + (r.cache_read_tokens ?? 0);
|
|
649
|
+
const cacheHitRate = totalIn > 0 ? (r.cache_read_tokens ?? 0) / totalIn : 0;
|
|
650
|
+
return {
|
|
651
|
+
period: r.period,
|
|
652
|
+
input_tokens: r.input_tokens ?? 0,
|
|
653
|
+
output_tokens: r.output_tokens ?? 0,
|
|
654
|
+
cache_creation_tokens: r.cache_creation_tokens ?? 0,
|
|
655
|
+
cache_read_tokens: r.cache_read_tokens ?? 0,
|
|
656
|
+
cache_hit_tokens: r.cache_hit_tokens ?? 0,
|
|
657
|
+
cache_miss_tokens: r.cache_miss_tokens ?? 0,
|
|
658
|
+
image_tokens: r.image_tokens ?? 0,
|
|
659
|
+
total_context_tokens: r.total_context_tokens ?? 0,
|
|
660
|
+
turns: r.turns ?? 0,
|
|
661
|
+
call_count: r.call_count ?? 0,
|
|
662
|
+
usd: cost?.usd ?? 0,
|
|
663
|
+
cny: cost?.cny ?? 0,
|
|
664
|
+
cache_hit_rate: cacheHitRate,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
/** 流量聚合统计(按时间/agent/peer 分组)。 */
|
|
668
|
+
export function queryMessageAggregated(evolclawHome, granularity, filter) {
|
|
669
|
+
const groupCol = GRAN_GROUP_COL[granularity];
|
|
670
|
+
const { clause, params } = _buildMsgWhere(filter);
|
|
671
|
+
let sql;
|
|
672
|
+
if (groupCol) {
|
|
673
|
+
const col = groupCol === 'model' ? 'agent_aid' : groupCol; // model 无意义,降级 agent
|
|
674
|
+
sql = `
|
|
675
|
+
SELECT ${col} AS period,
|
|
676
|
+
SUM(CASE WHEN direction='in' THEN 1 ELSE 0 END) AS msg_in,
|
|
677
|
+
SUM(CASE WHEN direction='out' THEN 1 ELSE 0 END) AS msg_out,
|
|
678
|
+
SUM(CASE WHEN direction='in' THEN bytes ELSE 0 END) AS bytes_in,
|
|
679
|
+
SUM(CASE WHEN direction='out' THEN bytes ELSE 0 END) AS bytes_out
|
|
680
|
+
FROM message_events ${clause}
|
|
681
|
+
GROUP BY ${col} ORDER BY (bytes_in + bytes_out) DESC
|
|
682
|
+
`;
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
const fmt = GRAN_FMT[granularity] || GRAN_FMT.day;
|
|
686
|
+
sql = `
|
|
687
|
+
SELECT strftime('${fmt}', ts/1000, 'unixepoch', 'localtime') AS period,
|
|
688
|
+
SUM(CASE WHEN direction='in' THEN 1 ELSE 0 END) AS msg_in,
|
|
689
|
+
SUM(CASE WHEN direction='out' THEN 1 ELSE 0 END) AS msg_out,
|
|
690
|
+
SUM(CASE WHEN direction='in' THEN bytes ELSE 0 END) AS bytes_in,
|
|
691
|
+
SUM(CASE WHEN direction='out' THEN bytes ELSE 0 END) AS bytes_out
|
|
692
|
+
FROM message_events ${clause}
|
|
693
|
+
GROUP BY period ORDER BY period
|
|
694
|
+
`;
|
|
695
|
+
}
|
|
696
|
+
const periodMap = new Map();
|
|
697
|
+
for (const dbPath of _relevantDbs(evolclawHome, filter)) {
|
|
698
|
+
const db = openReadonlyDb(dbPath);
|
|
699
|
+
if (!db)
|
|
700
|
+
continue;
|
|
701
|
+
try {
|
|
702
|
+
const rows = db.prepare(sql).all(...params);
|
|
703
|
+
for (const r of rows) {
|
|
704
|
+
const existing = periodMap.get(r.period);
|
|
705
|
+
if (existing) {
|
|
706
|
+
existing.msg_in += r.msg_in ?? 0;
|
|
707
|
+
existing.msg_out += r.msg_out ?? 0;
|
|
708
|
+
existing.bytes_in += r.bytes_in ?? 0;
|
|
709
|
+
existing.bytes_out += r.bytes_out ?? 0;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
periodMap.set(r.period, {
|
|
713
|
+
period: r.period,
|
|
714
|
+
msg_in: r.msg_in ?? 0, msg_out: r.msg_out ?? 0,
|
|
715
|
+
bytes_in: r.bytes_in ?? 0, bytes_out: r.bytes_out ?? 0,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
finally {
|
|
721
|
+
db.close();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const sorted = Array.from(periodMap.values());
|
|
725
|
+
if (groupCol) {
|
|
726
|
+
sorted.sort((a, b) => (b.bytes_in + b.bytes_out) - (a.bytes_in + a.bytes_out));
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
sorted.sort((a, b) => a.period.localeCompare(b.period));
|
|
730
|
+
}
|
|
731
|
+
return sorted;
|
|
732
|
+
}
|
|
733
|
+
/** 今日流量概览。 */
|
|
734
|
+
export function queryMessageTodaySummary(evolclawHome, agentAid) {
|
|
735
|
+
const now = new Date();
|
|
736
|
+
const from_ts = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
737
|
+
const rows = queryMessageAggregated(evolclawHome, 'day', { from_ts, agent_aid: agentAid });
|
|
738
|
+
return rows[0] ?? null;
|
|
739
|
+
}
|
|
740
|
+
function _buildMsgWhere(f) {
|
|
741
|
+
const conds = [];
|
|
742
|
+
const params = [];
|
|
743
|
+
if (f.from_ts) {
|
|
744
|
+
conds.push('ts >= ?');
|
|
745
|
+
params.push(f.from_ts);
|
|
746
|
+
}
|
|
747
|
+
if (f.to_ts) {
|
|
748
|
+
conds.push('ts < ?');
|
|
749
|
+
params.push(f.to_ts);
|
|
750
|
+
}
|
|
751
|
+
if (f.agent_aid) {
|
|
752
|
+
conds.push('agent_aid = ?');
|
|
753
|
+
params.push(f.agent_aid);
|
|
754
|
+
}
|
|
755
|
+
if (f.peer_key) {
|
|
756
|
+
conds.push('peer_key = ?');
|
|
757
|
+
params.push(f.peer_key);
|
|
758
|
+
}
|
|
759
|
+
return { clause: conds.length ? 'WHERE ' + conds.join(' AND ') : '', params };
|
|
760
|
+
}
|