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.
Files changed (95) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -4
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -31
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1152 -140
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +58 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/outbox.js +14 -2
  11. package/dist/aun/storage/download.js +1 -1
  12. package/dist/aun/storage/upload.js +13 -1
  13. package/dist/channels/aun.js +869 -358
  14. package/dist/channels/dingtalk.js +77 -140
  15. package/dist/channels/feishu.js +125 -154
  16. package/dist/channels/qqbot.js +75 -138
  17. package/dist/channels/wechat.js +75 -136
  18. package/dist/channels/wecom.js +75 -138
  19. package/dist/cli/agent-command.js +591 -0
  20. package/dist/cli/agent.js +23 -8
  21. package/dist/cli/aun-commands.js +1444 -0
  22. package/dist/cli/ctl-command.js +78 -0
  23. package/dist/cli/daemon-commands.js +2707 -0
  24. package/dist/cli/index.js +23 -4905
  25. package/dist/cli/init.js +33 -6
  26. package/dist/cli/model.js +1 -1
  27. package/dist/cli/restart-monitor.js +539 -0
  28. package/dist/cli/stats.js +558 -0
  29. package/dist/cli/version.js +87 -0
  30. package/dist/cli/watch-logs.js +33 -0
  31. package/dist/cli/watch-msg.js +5 -2
  32. package/dist/config-store.js +12 -6
  33. package/dist/core/channel-loader.js +88 -83
  34. package/dist/core/command/command-handler.js +1189 -0
  35. package/dist/core/command/menu-handler.js +1478 -0
  36. package/dist/core/command/slash-gate.js +142 -0
  37. package/dist/core/command/slash-handler.js +2090 -0
  38. package/dist/core/evolagent-registry.js +82 -0
  39. package/dist/core/evolagent.js +17 -1
  40. package/dist/core/interaction-router.js +8 -0
  41. package/dist/core/message/command-handler-agent-control.js +63 -1
  42. package/dist/core/message/im-renderer.js +91 -51
  43. package/dist/core/message/items-formatter.js +9 -1
  44. package/dist/core/message/message-bridge.js +73 -24
  45. package/dist/core/message/message-log.js +1 -0
  46. package/dist/core/message/message-processor.js +432 -94
  47. package/dist/core/message/message-queue.js +70 -2
  48. package/dist/core/message/pending-hints.js +232 -0
  49. package/dist/core/model/model-catalog.js +1 -1
  50. package/dist/core/model/model-scope.js +2 -2
  51. package/dist/core/permission.js +25 -12
  52. package/dist/core/relation/peer-identity.js +16 -1
  53. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  54. package/dist/core/session/session-manager.js +86 -26
  55. package/dist/core/session/session-title.js +26 -0
  56. package/dist/core/stats/billing.js +151 -0
  57. package/dist/core/stats/budget.js +93 -0
  58. package/dist/core/stats/db.js +334 -0
  59. package/dist/core/stats/eck-vars.js +84 -0
  60. package/dist/core/stats/index.js +10 -0
  61. package/dist/core/stats/normalizer.js +78 -0
  62. package/dist/core/stats/query.js +760 -0
  63. package/dist/core/stats/writer.js +115 -0
  64. package/dist/core/trigger/manager.js +34 -0
  65. package/dist/core/trigger/parser.js +9 -3
  66. package/dist/core/trigger/scheduler.js +20 -17
  67. package/dist/data/error-dict.json +7 -0
  68. package/dist/{agents → eck}/manifest-engine.js +20 -1
  69. package/dist/{agents → eck}/message-renderer.js +24 -1
  70. package/dist/index.js +174 -9
  71. package/dist/ipc.js +116 -1
  72. package/dist/utils/cross-platform.js +58 -5
  73. package/dist/utils/ecweb-launch.js +49 -0
  74. package/dist/utils/ecweb-pair.js +20 -0
  75. package/dist/utils/error-utils.js +18 -5
  76. package/dist/utils/npm-ops.js +38 -8
  77. package/dist/utils/stats.js +77 -6
  78. package/kits/docs/evolclaw/INDEX.md +3 -1
  79. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  80. package/kits/docs/evolclaw/fs.md +131 -0
  81. package/kits/docs/evolclaw/group-fs.md +209 -0
  82. package/kits/docs/evolclaw/stats.md +70 -0
  83. package/kits/docs/venues/aun-group.md +29 -6
  84. package/kits/docs/venues/group.md +5 -4
  85. package/kits/eck_message_manifest.json +30 -3
  86. package/kits/rules/05-venue.md +1 -1
  87. package/kits/templates/message-fragments/inject-default.md +2 -0
  88. package/package.json +5 -6
  89. package/dist/agents/baseagent-normalize.js +0 -19
  90. package/dist/core/command-handler.js +0 -3876
  91. package/dist/core/relation/peer-key.js +0 -16
  92. package/dist/evolclaw-config.js +0 -11
  93. package/dist/utils/channel-helpers.js +0 -46
  94. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  95. /package/dist/{agents → eck}/kit-renderer.js +0 -0
@@ -0,0 +1,558 @@
1
+ /**
2
+ * ec stats — Token 用量统计 CLI 命令。
3
+ */
4
+ import { resolveRoot } from '../paths.js';
5
+ import { wantsHelp } from './help.js';
6
+ import { queryAggregated, querySessionTurns, queryContextBreakdown, queryTopPeers, queryTopModels, queryMessageAggregated, queryPeerList, querySummary, queryPeerDaily, queryTaskModelCalls, querySessionModelCalls } from '../core/stats/query.js';
7
+ import { getBudgetStatus } from '../core/stats/budget.js';
8
+ const HELP = `ec stats — Token 用量与费用统计
9
+
10
+ Usage: ec stats [options]
11
+
12
+ 时间范围:
13
+ (无参数) 今日概览
14
+ --today 同上
15
+ --hour 最近 24 小时,按小时分组
16
+ --week 本周
17
+ --month 本月
18
+ --from <date> --to <date> 任意区间(YYYY-MM-DD)
19
+
20
+ 维度过滤(可组合):
21
+ --agent <aid> 指定 agent
22
+ --peer <X> 对端(裸 AID 自动前缀 aun#,或 channel#id 格式)
23
+ --model <model-id> 指定模型
24
+ --session <id> 指定会话
25
+
26
+ 聚合粒度:
27
+ --by hour|day|week|month|model|peer|agent
28
+
29
+ 快捷视图:
30
+ --session <id> 单个会话明细
31
+ --session <id> --last 会话最后一轮用量 + 累计(等价 status.completed)
32
+ --context <id> 会话 context breakdown 细目
33
+ --budget 预算状态
34
+ --top-peers [--limit N] 对端排行
35
+ --sql "<query>" 直接执行只读 SQL
36
+ --rebuild 全量重建日聚合表 usage_daily(运维兜底/排查)
37
+ --peers [--limit N] 私聊对端列表(带累计 token/calls/活跃日)
38
+ --groups [--limit N] 群聊列表(同上)
39
+ --summary 指定时间范围总消耗汇总(token/USD/CNY)
40
+ --peer-detail <id> 指定对端 AID 或 peer_key,按天返回消耗明细
41
+ --task-calls <taskId> 一个 task 的逐次大模型调用明细
42
+ --session-calls <id> 一个会话的逐次大模型调用明细
43
+
44
+ 输出格式:
45
+ --format json JSON 输出
46
+ --help, -h 显示此帮助
47
+
48
+ 示例:
49
+ ec stats
50
+ ec stats --month --agent bot.agentid.pub
51
+ ec stats --peer alice.aid.pub
52
+ ec stats --session abc123 --format json`;
53
+ const RESET = '\x1b[0m';
54
+ const BOLD = '\x1b[1m';
55
+ const DIM = '\x1b[2m';
56
+ const GREEN = '\x1b[32m';
57
+ const BLUE = '\x1b[34m';
58
+ const CYAN = '\x1b[36m';
59
+ const YELLOW = '\x1b[33m';
60
+ const RED = '\x1b[31m';
61
+ function fail(formatJson, code, message) {
62
+ if (formatJson) {
63
+ console.log(JSON.stringify({ ok: false, code, error: message }, null, 2));
64
+ }
65
+ else {
66
+ console.error(`❌ ${message} (${code})`);
67
+ }
68
+ process.exit(1);
69
+ }
70
+ function pad(s, n) { return s.padEnd(n); }
71
+ function padR(s, n) { return s.padStart(n); }
72
+ function fmtTokens(n) {
73
+ if (n >= 1_000_000)
74
+ return (n / 1_000_000).toFixed(1) + 'M';
75
+ if (n >= 1_000)
76
+ return (n / 1_000).toFixed(1) + 'k';
77
+ return String(n);
78
+ }
79
+ function fmtBytes(n) {
80
+ if (n >= 1_073_741_824)
81
+ return (n / 1_073_741_824).toFixed(1) + 'GB';
82
+ if (n >= 1_048_576)
83
+ return (n / 1_048_576).toFixed(1) + 'MB';
84
+ if (n >= 1_024)
85
+ return (n / 1_024).toFixed(1) + 'KB';
86
+ return n + 'B';
87
+ }
88
+ function printTable(headers, rows) {
89
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] || '').length)));
90
+ const sep = widths.map(w => '─'.repeat(w + 2)).join('┼');
91
+ console.log(headers.map((h, i) => ` ${pad(h, widths[i])} `).join('│'));
92
+ console.log(sep);
93
+ for (const row of rows) {
94
+ console.log(row.map((c, i) => ` ${pad(c || '', widths[i])} `).join('│'));
95
+ }
96
+ }
97
+ function parseDateArg(s) {
98
+ const d = new Date(s + 'T00:00:00Z');
99
+ if (isNaN(d.getTime()))
100
+ throw new Error(`Invalid date: ${s}`);
101
+ return d.getTime();
102
+ }
103
+ export async function handleStats(args) {
104
+ // Help
105
+ if (wantsHelp(args)) {
106
+ console.log(HELP);
107
+ return;
108
+ }
109
+ const home = resolveRoot();
110
+ const flags = {};
111
+ const positional = [];
112
+ // Parse args
113
+ for (let i = 0; i < args.length; i++) {
114
+ const a = args[i];
115
+ if (a.startsWith('--')) {
116
+ const key = a.slice(2);
117
+ const next = args[i + 1];
118
+ if (next && !next.startsWith('--')) {
119
+ flags[key] = next;
120
+ i++;
121
+ }
122
+ else {
123
+ flags[key] = true;
124
+ }
125
+ }
126
+ else {
127
+ positional.push(a);
128
+ }
129
+ }
130
+ const isJson = flags.format === 'json';
131
+ // 全量重建日聚合表(运维兜底/排查),不依赖时间范围。
132
+ if (flags.rebuild) {
133
+ const { rebuildDailyRollup } = await import('../core/stats/db.js');
134
+ const startedAt = Date.now();
135
+ const n = rebuildDailyRollup(home);
136
+ const ms = Date.now() - startedAt;
137
+ if (n < 0)
138
+ fail(isJson, 'REBUILD_FAILED', 'usage_daily 重建失败(DB 不可用或 SQL 错误)');
139
+ if (isJson) {
140
+ console.log(JSON.stringify({ ok: true, rows: n, duration_ms: ms }, null, 2));
141
+ }
142
+ else {
143
+ console.log(`✓ usage_daily 重建完成:${n} 行,耗时 ${ms}ms`);
144
+ }
145
+ return;
146
+ }
147
+ // Determine time range
148
+ let from_ts;
149
+ let to_ts;
150
+ let granularity = 'day';
151
+ const now = new Date();
152
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
153
+ if (flags.from && typeof flags.from === 'string') {
154
+ from_ts = parseDateArg(flags.from);
155
+ if (flags.to && typeof flags.to === 'string')
156
+ to_ts = parseDateArg(flags.to);
157
+ }
158
+ else if (flags.hour) {
159
+ from_ts = Date.now() - 24 * 60 * 60 * 1000;
160
+ granularity = 'hour';
161
+ }
162
+ else if (flags.week) {
163
+ const dayOfWeek = now.getDay();
164
+ from_ts = todayStart - dayOfWeek * 86400000;
165
+ granularity = 'day';
166
+ }
167
+ else if (flags.month) {
168
+ from_ts = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
169
+ granularity = 'day';
170
+ }
171
+ else {
172
+ // default: today
173
+ from_ts = todayStart;
174
+ granularity = 'hour';
175
+ }
176
+ // Granularity override
177
+ if (flags.by && typeof flags.by === 'string') {
178
+ if (['hour', 'day', 'week', 'month', 'model', 'peer', 'agent'].includes(flags.by)) {
179
+ granularity = flags.by;
180
+ }
181
+ }
182
+ // Filter
183
+ const filter = { from_ts, to_ts };
184
+ if (flags.agent && typeof flags.agent === 'string')
185
+ filter.agent_aid = flags.agent;
186
+ if (flags.peer && typeof flags.peer === 'string') {
187
+ // AUN 简写:不含 # 时默认 aun 渠道
188
+ filter.peer_key = flags.peer.includes('#') ? flags.peer : `aun#${encodeURIComponent(flags.peer)}`;
189
+ }
190
+ if (flags.model && typeof flags.model === 'string')
191
+ filter.model = flags.model;
192
+ if (flags.session && typeof flags.session === 'string')
193
+ filter.session_id = flags.session;
194
+ // ── 结构化查询:私聊/群聊列表、总消耗汇总、对端按天明细 ──────────────────
195
+ if (flags.peers || flags.groups) {
196
+ const peerType = flags.peers ? 'private' : 'group';
197
+ const limit = typeof flags.limit === 'string' ? parseInt(flags.limit) || 50 : 50;
198
+ const rows = queryPeerList(home, { peer_type: peerType, from_ts, to_ts, agent_aid: filter.agent_aid, limit });
199
+ if (isJson) {
200
+ console.log(JSON.stringify(rows, null, 2));
201
+ return;
202
+ }
203
+ console.log(`\n${BOLD}📊 ${peerType === 'private' ? '私聊' : '群聊'}列表${RESET}\n`);
204
+ if (!rows.length) {
205
+ console.log(' (无数据)');
206
+ return;
207
+ }
208
+ const headers = ['#', 'Peer ID', 'Tokens', 'Calls', 'Sessions', 'First', 'Last'];
209
+ const tableRows = rows.map((r, i) => [
210
+ String(i + 1), r.peer_id, fmtTokens(r.total_tokens), String(r.calls),
211
+ String(r.session_count), r.first_day, r.last_day,
212
+ ]);
213
+ printTable(headers, tableRows);
214
+ console.log();
215
+ return;
216
+ }
217
+ if (flags.summary) {
218
+ const result = querySummary(home, { from_ts, to_ts, agent_aid: filter.agent_aid, peer_key: filter.peer_key });
219
+ if (isJson) {
220
+ console.log(JSON.stringify(result, null, 2));
221
+ return;
222
+ }
223
+ console.log(`\n${BOLD}📊 用量汇总${RESET}\n`);
224
+ console.log(` Input: ${fmtTokens(result.input_tokens)}`);
225
+ console.log(` Output: ${fmtTokens(result.output_tokens)}`);
226
+ console.log(` Cache read: ${fmtTokens(result.cache_read_tokens)}`);
227
+ console.log(` Cache write: ${fmtTokens(result.cache_creation_tokens)}`);
228
+ console.log(` Total tokens: ${fmtTokens(result.total_tokens)}`);
229
+ console.log(` Calls: ${result.calls}`);
230
+ console.log(` Cache hit: ${(result.cache_hit_rate * 100).toFixed(1)}%`);
231
+ if (result.usd > 0)
232
+ console.log(` Cost USD: ${GREEN}$${result.usd.toFixed(4)}${RESET}`);
233
+ if (result.cny > 0)
234
+ console.log(` Cost CNY: ${GREEN}¥${result.cny.toFixed(4)}${RESET}`);
235
+ console.log();
236
+ return;
237
+ }
238
+ if (flags['peer-detail'] && typeof flags['peer-detail'] === 'string') {
239
+ const input = flags['peer-detail'];
240
+ // 含 # 视为完整 peer_key(精确匹配),否则视为裸 peer_id(LIKE 匹配)
241
+ const peerOpts = input.includes('#')
242
+ ? { peer_key: input, from_ts, to_ts, agent_aid: filter.agent_aid }
243
+ : { peer_id: input, from_ts, to_ts, agent_aid: filter.agent_aid };
244
+ const rows = queryPeerDaily(home, peerOpts);
245
+ if (isJson) {
246
+ console.log(JSON.stringify(rows, null, 2));
247
+ return;
248
+ }
249
+ if (!rows.length) {
250
+ console.log('No data for the specified peer and range.');
251
+ return;
252
+ }
253
+ console.log(`\n${BOLD}📊 对端明细 — ${input}${RESET}\n`);
254
+ const headers = ['Day', 'Input', 'Output', 'Cache', 'Calls', 'HitRate', 'USD', 'CNY'];
255
+ const tableRows = rows.map(r => [
256
+ r.period, fmtTokens(r.input_tokens), fmtTokens(r.output_tokens),
257
+ fmtTokens(r.cache_read_tokens), String(r.call_count),
258
+ (r.cache_hit_rate * 100).toFixed(0) + '%',
259
+ r.usd > 0 ? '$' + r.usd.toFixed(4) : '—',
260
+ r.cny > 0 ? '¥' + r.cny.toFixed(4) : '—',
261
+ ]);
262
+ printTable(headers, tableRows);
263
+ console.log();
264
+ return;
265
+ }
266
+ // 逐次大模型调用明细
267
+ if ((flags['task-calls'] && typeof flags['task-calls'] === 'string') ||
268
+ (flags['session-calls'] && typeof flags['session-calls'] === 'string')) {
269
+ const byTask = typeof flags['task-calls'] === 'string';
270
+ const key = (byTask ? flags['task-calls'] : flags['session-calls']);
271
+ const rows = byTask ? queryTaskModelCalls(home, key) : querySessionModelCalls(home, key);
272
+ if (isJson) {
273
+ console.log(JSON.stringify(rows, null, 2));
274
+ return;
275
+ }
276
+ if (!rows.length) {
277
+ console.log('No model calls found.');
278
+ return;
279
+ }
280
+ console.log(`\n${BOLD}📊 大模型调用明细 — ${key}${RESET}\n`);
281
+ const headers = ['#', 'Model', 'Input', 'Output', 'CacheR', 'CacheW', 'Task', 'Deg'];
282
+ const tableRows = rows.map(r => [
283
+ String(r.call_index),
284
+ r.model.split('-').slice(0, 3).join('-'),
285
+ fmtTokens(r.input_tokens), fmtTokens(r.output_tokens),
286
+ fmtTokens(r.cache_read_tokens), fmtTokens(r.cache_creation_tokens),
287
+ r.task_id, r.degraded ? 'Y' : '',
288
+ ]);
289
+ printTable(headers, tableRows);
290
+ console.log();
291
+ return;
292
+ }
293
+ // Special views
294
+ if (flags.sql && typeof flags.sql === 'string') {
295
+ // 直接执行 SQL(只读,仅 SELECT)
296
+ const sql = flags.sql.trim();
297
+ if (!/^\s*select/i.test(sql)) {
298
+ console.error('Only SELECT queries are allowed.');
299
+ return;
300
+ }
301
+ const { openReadonlyDb, getDbPath } = await import('../core/stats/db.js');
302
+ const db = openReadonlyDb(getDbPath(home));
303
+ if (!db) {
304
+ console.error('Stats DB not available.');
305
+ return;
306
+ }
307
+ try {
308
+ const rows = db.prepare(sql).all();
309
+ if (isJson) {
310
+ console.log(JSON.stringify(rows, null, 2));
311
+ }
312
+ else if (rows.length === 0) {
313
+ console.log('(empty result)');
314
+ }
315
+ else {
316
+ const keys = Object.keys(rows[0]);
317
+ printTable(keys, rows.map(r => keys.map(k => String(r[k] ?? ''))));
318
+ }
319
+ }
320
+ catch (e) {
321
+ console.error(`SQL error: ${e.message || e}`);
322
+ }
323
+ finally {
324
+ db.close();
325
+ }
326
+ return;
327
+ }
328
+ if (flags.budget) {
329
+ const status = getBudgetStatus(home, filter.agent_aid, filter.peer_key);
330
+ if (isJson) {
331
+ console.log(JSON.stringify(status, null, 2));
332
+ return;
333
+ }
334
+ console.log(`\n${BOLD}📊 Budget Status${RESET}\n`);
335
+ console.log(` Daily limit: ${status.daily_limit_usd >= 0 ? '$' + status.daily_limit_usd.toFixed(2) : 'unlimited'}`);
336
+ console.log(` Daily used: ${GREEN}$${status.daily_used_usd.toFixed(4)}${RESET}`);
337
+ console.log(` Daily remaining: ${status.daily_remaining_usd >= 0 ? '$' + status.daily_remaining_usd.toFixed(2) : 'unlimited'}`);
338
+ console.log(` Monthly limit: ${status.monthly_limit_usd >= 0 ? '$' + status.monthly_limit_usd.toFixed(2) : 'unlimited'}`);
339
+ console.log(` Monthly used: ${GREEN}$${status.monthly_used_usd.toFixed(4)}${RESET}`);
340
+ console.log(` Monthly remaining: ${status.monthly_remaining_usd >= 0 ? '$' + status.monthly_remaining_usd.toFixed(2) : 'unlimited'}`);
341
+ console.log(` Usage: ${status.pct_used > 80 ? RED : status.pct_used > 60 ? YELLOW : GREEN}${status.pct_used.toFixed(1)}%${RESET}`);
342
+ console.log(` Hard blocked: ${status.hard_blocked ? RED + 'YES' + RESET : 'no'}`);
343
+ console.log(` Soft warn: ${status.soft_warn ? YELLOW + 'yes' + RESET : 'no'}`);
344
+ console.log(` Auto warn: ${status.auto_warn ? YELLOW + 'yes' + RESET : 'no'}`);
345
+ console.log();
346
+ return;
347
+ }
348
+ if (flags.context && typeof flags.context === 'string') {
349
+ const rows = queryContextBreakdown(home, flags.context);
350
+ if (isJson) {
351
+ console.log(JSON.stringify(rows, null, 2));
352
+ return;
353
+ }
354
+ if (!rows.length) {
355
+ console.log('No context breakdown data for this session.');
356
+ return;
357
+ }
358
+ console.log(`\n${BOLD}📊 Context Breakdown — session ${flags.context}${RESET}\n`);
359
+ const headers = ['Turn', 'System', 'Tools', 'MCP', 'Agents', 'Memory', 'Skills', 'Messages', 'Free', 'Total', 'Max'];
360
+ const tableRows = rows.map(r => [
361
+ String(r.turn_count),
362
+ fmtTokens(r.system_prompt ?? 0), fmtTokens(r.system_tools ?? 0),
363
+ fmtTokens(r.mcp_tools ?? 0), fmtTokens(r.custom_agents ?? 0),
364
+ fmtTokens(r.memory_files ?? 0), fmtTokens(r.skills ?? 0),
365
+ fmtTokens(r.messages ?? 0), fmtTokens(r.free_space ?? 0),
366
+ fmtTokens(r.total_estimated ?? 0), fmtTokens(r.max_tokens ?? 0),
367
+ ]);
368
+ printTable(headers, tableRows);
369
+ console.log();
370
+ return;
371
+ }
372
+ if (flags['top-peers']) {
373
+ const limit = typeof flags.limit === 'string' ? parseInt(flags.limit) || 10 : 10;
374
+ const rows = queryTopPeers(home, filter, limit);
375
+ if (isJson) {
376
+ console.log(JSON.stringify(rows, null, 2));
377
+ return;
378
+ }
379
+ console.log(`\n${BOLD}📊 Top Peers${RESET}\n`);
380
+ const headers = ['#', 'Peer', 'Tokens', 'Calls'];
381
+ const tableRows = rows.map((r, i) => [
382
+ String(i + 1), r.peer_key, fmtTokens(r.total_tokens), String(r.call_count),
383
+ ]);
384
+ printTable(headers, tableRows);
385
+ console.log();
386
+ return;
387
+ }
388
+ if (flags['top-models']) {
389
+ const limit = typeof flags.limit === 'string' ? parseInt(flags.limit) || 10 : 10;
390
+ const rows = queryTopModels(home, filter, limit);
391
+ if (isJson) {
392
+ console.log(JSON.stringify(rows, null, 2));
393
+ return;
394
+ }
395
+ console.log(`\n${BOLD}📊 Top Models${RESET}\n`);
396
+ const headers = ['#', 'Model', 'Tokens', 'Calls'];
397
+ const tableRows = rows.map((r, i) => [
398
+ String(i + 1), r.model, fmtTokens(r.total_tokens), String(r.call_count),
399
+ ]);
400
+ printTable(headers, tableRows);
401
+ console.log();
402
+ return;
403
+ }
404
+ if (flags.traffic) {
405
+ const rows = queryMessageAggregated(home, granularity, filter);
406
+ if (isJson) {
407
+ console.log(JSON.stringify(rows, null, 2));
408
+ return;
409
+ }
410
+ if (!rows.length) {
411
+ console.log('No traffic data for the selected range.');
412
+ return;
413
+ }
414
+ const periodLabel = flags.hour ? 'Last 24h' : flags.week ? 'This Week' : flags.month ? 'This Month' : 'Today';
415
+ console.log(`\n${BOLD}📊 ${periodLabel} — Network Traffic${RESET}\n`);
416
+ const headers = ['Period', 'Msg In', 'Msg Out', 'Bytes In', 'Bytes Out'];
417
+ const tableRows = rows.map(r => [
418
+ r.period,
419
+ String(r.msg_in), String(r.msg_out),
420
+ fmtBytes(r.bytes_in), fmtBytes(r.bytes_out),
421
+ ]);
422
+ printTable(headers, tableRows);
423
+ const totIn = rows.reduce((s, r) => s + r.msg_in, 0);
424
+ const totOut = rows.reduce((s, r) => s + r.msg_out, 0);
425
+ const totBIn = rows.reduce((s, r) => s + r.bytes_in, 0);
426
+ const totBOut = rows.reduce((s, r) => s + r.bytes_out, 0);
427
+ console.log(`\n ${DIM}Total: in=${totIn} out=${totOut} bytes_in=${fmtBytes(totBIn)} bytes_out=${fmtBytes(totBOut)}${RESET}\n`);
428
+ return;
429
+ }
430
+ if (flags.session && typeof flags.session === 'string') {
431
+ const rows = querySessionTurns(home, flags.session);
432
+ // --last: 返回最后一轮用量 + 会话累计(与 status.completed metadata 等价)
433
+ if (flags.last) {
434
+ if (!rows.length) {
435
+ if (isJson) {
436
+ console.log(JSON.stringify({ ok: false, error: 'No usage data for this session' }, null, 2));
437
+ }
438
+ else {
439
+ console.log('No usage data for this session.');
440
+ }
441
+ return;
442
+ }
443
+ const last = rows[rows.length - 1];
444
+ const totIn = rows.reduce((s, r) => s + r.input_tokens, 0);
445
+ const totOut = rows.reduce((s, r) => s + r.output_tokens, 0);
446
+ const totCacheRead = rows.reduce((s, r) => s + r.cache_read_tokens, 0);
447
+ const totCacheCreation = rows.reduce((s, r) => s + r.cache_creation_tokens, 0);
448
+ const totUsd = rows.reduce((s, r) => s + r.usd, 0);
449
+ const totCny = rows.reduce((s, r) => s + r.cny, 0);
450
+ const totCacheAll = totIn + totCacheRead;
451
+ const sessionCacheHitRate = totCacheAll > 0 ? totCacheRead / totCacheAll : 0;
452
+ // model_spec
453
+ const { resolveModelSpec } = await import('../core/stats/billing.js');
454
+ const spec = resolveModelSpec(home, last.model);
455
+ const result = {
456
+ turn: {
457
+ input_tokens: last.input_tokens,
458
+ output_tokens: last.output_tokens,
459
+ cache_read_tokens: last.cache_read_tokens,
460
+ cache_creation_tokens: last.cache_creation_tokens,
461
+ model: last.model,
462
+ cost_usd: last.usd,
463
+ cost_cny: last.cny,
464
+ cache_hit_rate: last.cache_read_tokens / ((last.input_tokens + last.cache_read_tokens) || 1),
465
+ context_window_pct: last.context_window_pct ?? 0,
466
+ duration_ms: last.duration_ms ?? 0,
467
+ },
468
+ session_total: {
469
+ input_tokens: totIn,
470
+ output_tokens: totOut,
471
+ cache_read_tokens: totCacheRead,
472
+ cache_creation_tokens: totCacheCreation,
473
+ cost_usd: totUsd,
474
+ cost_cny: totCny,
475
+ call_count: rows.length,
476
+ cache_hit_rate: sessionCacheHitRate,
477
+ },
478
+ model_spec: {
479
+ context_window: spec.context_window,
480
+ max_input_tokens: spec.max_input_tokens,
481
+ max_output_tokens: spec.max_output_tokens,
482
+ },
483
+ };
484
+ if (isJson) {
485
+ console.log(JSON.stringify(result, null, 2));
486
+ return;
487
+ }
488
+ console.log(`\n${BOLD}📊 Session Last Turn — ${flags.session}${RESET}\n`);
489
+ console.log(` Model: ${CYAN}${result.turn.model}${RESET}`);
490
+ console.log(` Input: ${fmtTokens(result.turn.input_tokens)}`);
491
+ console.log(` Output: ${fmtTokens(result.turn.output_tokens)}`);
492
+ console.log(` Cache read: ${fmtTokens(result.turn.cache_read_tokens)}`);
493
+ console.log(` Cache hit: ${(result.turn.cache_hit_rate * 100).toFixed(1)}%`);
494
+ console.log(` Context: ${result.turn.context_window_pct}%`);
495
+ console.log(` Cost: ${result.turn.cost_usd > 0 ? GREEN + '$' + result.turn.cost_usd.toFixed(4) + RESET : ''}${result.turn.cost_cny > 0 ? GREEN + ' ¥' + result.turn.cost_cny.toFixed(4) + RESET : ''}`);
496
+ console.log(` Duration: ${result.turn.duration_ms}ms`);
497
+ console.log(`\n ${DIM}Session total: input=${fmtTokens(result.session_total.input_tokens)} output=${fmtTokens(result.session_total.output_tokens)} USD=$${totUsd.toFixed(4)} CNY=¥${totCny.toFixed(4)} calls=${result.session_total.call_count}${RESET}\n`);
498
+ return;
499
+ }
500
+ if (isJson) {
501
+ console.log(JSON.stringify(rows, null, 2));
502
+ return;
503
+ }
504
+ if (!rows.length) {
505
+ console.log('No usage data for this session.');
506
+ return;
507
+ }
508
+ console.log(`\n${BOLD}📊 Session ${flags.session}${RESET}\n`);
509
+ const headers = ['Time', 'Model', 'Input', 'Output', 'Cache', 'Ctx%', 'USD', 'CNY'];
510
+ const tableRows = rows.map(r => {
511
+ const t = new Date(r.ts).toLocaleTimeString();
512
+ return [
513
+ t, r.model.split('-').slice(0, 2).join('-'),
514
+ fmtTokens(r.input_tokens), fmtTokens(r.output_tokens),
515
+ fmtTokens(r.cache_read_tokens),
516
+ r.context_window_pct != null ? r.context_window_pct.toFixed(0) + '%' : '—',
517
+ r.usd > 0 ? '$' + r.usd.toFixed(4) : '—',
518
+ r.cny > 0 ? '¥' + r.cny.toFixed(4) : '—',
519
+ ];
520
+ });
521
+ printTable(headers, tableRows);
522
+ // Totals
523
+ const totIn = rows.reduce((s, r) => s + r.input_tokens, 0);
524
+ const totOut = rows.reduce((s, r) => s + r.output_tokens, 0);
525
+ const totUsd = rows.reduce((s, r) => s + r.usd, 0);
526
+ const totCny = rows.reduce((s, r) => s + r.cny, 0);
527
+ console.log(`\n ${DIM}Total: input=${fmtTokens(totIn)} output=${fmtTokens(totOut)} USD=$${totUsd.toFixed(4)} CNY=¥${totCny.toFixed(4)} turns=${rows.length}${RESET}\n`);
528
+ return;
529
+ }
530
+ // Default: aggregated view
531
+ const rows = queryAggregated(home, granularity, filter);
532
+ if (isJson) {
533
+ console.log(JSON.stringify(rows, null, 2));
534
+ return;
535
+ }
536
+ if (!rows.length) {
537
+ console.log('No usage data for the selected range.');
538
+ return;
539
+ }
540
+ const periodLabel = flags.hour ? 'Last 24h' : flags.week ? 'This Week' : flags.month ? 'This Month' : 'Today';
541
+ console.log(`\n${BOLD}📊 ${periodLabel} — Token Usage${RESET}\n`);
542
+ const headers = ['Period', 'Input', 'Output', 'Cache↑', 'CacheHit', 'Calls', 'HitRate'];
543
+ const tableRows = rows.map(r => [
544
+ r.period,
545
+ fmtTokens(r.input_tokens), fmtTokens(r.output_tokens),
546
+ fmtTokens(r.cache_creation_tokens), fmtTokens(r.cache_read_tokens),
547
+ String(r.call_count),
548
+ (r.cache_hit_rate * 100).toFixed(0) + '%',
549
+ ]);
550
+ printTable(headers, tableRows);
551
+ // Summary
552
+ const totIn = rows.reduce((s, r) => s + r.input_tokens, 0);
553
+ const totOut = rows.reduce((s, r) => s + r.output_tokens, 0);
554
+ const totCalls = rows.reduce((s, r) => s + r.call_count, 0);
555
+ const totCacheRead = rows.reduce((s, r) => s + r.cache_read_tokens, 0);
556
+ const overallHit = (totIn + totCacheRead) > 0 ? (totCacheRead / (totIn + totCacheRead) * 100).toFixed(1) : '0';
557
+ console.log(`\n ${DIM}Total: input=${fmtTokens(totIn)} output=${fmtTokens(totOut)} calls=${totCalls} cache_hit_rate=${overallHit}%${RESET}\n`);
558
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * ec version — 显示所有组件版本和构建时间戳,便于诊断。
3
+ */
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const PACKAGE_ROOT = path.resolve(__dirname, '../..');
9
+ const RESET = '\x1b[0m';
10
+ const BOLD = '\x1b[1m';
11
+ const DIM = '\x1b[2m';
12
+ const GREEN = '\x1b[32m';
13
+ const CYAN = '\x1b[36m';
14
+ function readPkgJson(dir) {
15
+ const f = path.join(dir, 'package.json');
16
+ if (!fs.existsSync(f))
17
+ return null;
18
+ try {
19
+ return JSON.parse(fs.readFileSync(f, 'utf-8'));
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function getFileModTime(filePath) {
26
+ try {
27
+ const stat = fs.statSync(filePath);
28
+ return stat.mtime.toISOString().replace('T', ' ').slice(0, 19);
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ function findInstalledVersion(pkgName) {
35
+ // 从 PACKAGE_ROOT/node_modules 查找
36
+ const dir = path.join(PACKAGE_ROOT, 'node_modules', ...pkgName.split('/'));
37
+ const pkg = readPkgJson(dir);
38
+ if (!pkg)
39
+ return null;
40
+ // 取 dist/index.js 修改时间作为构建时间戳参考
41
+ const mainFile = path.join(dir, 'dist', 'index.js');
42
+ const ts = getFileModTime(mainFile) || getFileModTime(path.join(dir, 'index.js'));
43
+ return { name: pkg.name || pkgName, version: pkg.version || '?', buildTs: ts || undefined, path: dir };
44
+ }
45
+ export function handleVersion(args) {
46
+ const isJson = args.includes('--format') && args.includes('json') || args.includes('--json');
47
+ // 主包
48
+ const mainPkg = readPkgJson(PACKAGE_ROOT);
49
+ const mainEntry = path.join(PACKAGE_ROOT, 'dist', 'cli', 'index.js');
50
+ const mainTs = getFileModTime(mainEntry);
51
+ // ecweb 包
52
+ const ecwebDir = path.join(PACKAGE_ROOT, 'ecweb');
53
+ const ecwebPkg = readPkgJson(ecwebDir);
54
+ const ecwebEntry = path.join(ecwebDir, 'dist', 'index.js');
55
+ const ecwebTs = getFileModTime(ecwebEntry);
56
+ const ecwebStaticTs = getFileModTime(path.join(ecwebDir, 'dist', 'static', 'index.html'));
57
+ // 关键依赖
58
+ const deps = [
59
+ '@agentunion/fastaun',
60
+ 'ws',
61
+ ];
62
+ const components = [
63
+ { name: 'evolclaw', version: mainPkg?.version || '?', built: mainTs || undefined, note: 'main package' },
64
+ { name: 'evolclaw-web', version: ecwebPkg?.version || '?', built: ecwebTs || undefined, note: `static: ${ecwebStaticTs || '?'}` },
65
+ ];
66
+ for (const dep of deps) {
67
+ const info = findInstalledVersion(dep);
68
+ if (info) {
69
+ components.push({ name: info.name, version: info.version, built: info.buildTs || undefined });
70
+ }
71
+ }
72
+ // Node.js 版本
73
+ components.push({ name: 'node', version: process.version, note: process.platform + '/' + process.arch });
74
+ if (isJson) {
75
+ console.log(JSON.stringify(components, null, 2));
76
+ return;
77
+ }
78
+ console.log(`\n${BOLD}🔧 EvolClaw Component Versions${RESET}\n`);
79
+ const nameW = Math.max(...components.map(c => c.name.length), 8);
80
+ const verW = Math.max(...components.map(c => c.version.length), 7);
81
+ for (const c of components) {
82
+ const builtStr = c.built ? `${DIM}built: ${c.built}${RESET}` : '';
83
+ const noteStr = c.note ? `${DIM}(${c.note})${RESET}` : '';
84
+ console.log(` ${CYAN}${c.name.padEnd(nameW)}${RESET} ${GREEN}${c.version.padEnd(verW)}${RESET} ${builtStr} ${noteStr}`);
85
+ }
86
+ console.log();
87
+ }
@@ -0,0 +1,33 @@
1
+ import path from 'path';
2
+ /** 去掉轮转后缀("evolclaw-20260518-21.log" → "evolclaw";"ts-sdk-2026-05-27.log" → "ts-sdk")。入参可为文件名或绝对路径。 */
3
+ export function shortLogName(file) {
4
+ return path.basename(file, '.log')
5
+ .replace(/-\d{8}-\d{2}$/, '') // -YYYYMMDD-HH(按小时轮转)
6
+ .replace(/-\d{4}-\d{2}-\d{2}$/, ''); // -YYYY-MM-DD(按日轮转,如 ts-sdk)
7
+ }
8
+ /** 从 .log 文件名列表推导去重、字母序的类型列表。 */
9
+ export function deriveLogTypes(files) {
10
+ const set = new Set();
11
+ for (const f of files) {
12
+ if (!f.endsWith('.log'))
13
+ continue;
14
+ set.add(shortLogName(f));
15
+ }
16
+ return [...set].sort();
17
+ }
18
+ /** 计算预勾集合:saved 为 undefined → 全勾;否则只勾命中 saved 的类型(新类型不勾)。 */
19
+ export function computePreChecked(types, saved) {
20
+ if (saved === undefined)
21
+ return new Set(types);
22
+ const savedSet = new Set(saved);
23
+ return new Set(types.filter(t => savedSet.has(t)));
24
+ }
25
+ /** 返回 requested 中不在 available 里的无效类型。 */
26
+ export function validateLogTypes(requested, available) {
27
+ const set = new Set(available);
28
+ return requested.filter(t => !set.has(t));
29
+ }
30
+ /** 只保留类型命中 filterTypes 的文件路径。 */
31
+ export function filterLogFiles(files, filterTypes) {
32
+ return files.filter(f => filterTypes.has(shortLogName(f)));
33
+ }