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,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
|
+
}
|