evolclaw 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +1 -2
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -27
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1069 -141
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +28 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +406 -293
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +97 -150
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent.js +8 -5
- package/dist/cli/index.js +177 -44
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +84 -82
- package/dist/core/command-handler.js +473 -114
- package/dist/core/evolagent-registry.js +1 -0
- package/dist/core/evolagent.js +1 -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 +35 -13
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +49 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +295 -35
- package/dist/core/message/message-queue.js +2 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +56 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +9 -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 +27 -13
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +314 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +130 -8
- package/dist/ipc.js +17 -1
- package/dist/utils/cross-platform.js +23 -5
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/stats.js +14 -0
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/system-fragments/response-depth.md +16 -0
- package/package.json +4 -4
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/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
package/dist/cli/init.js
CHANGED
|
@@ -4,10 +4,9 @@ import readline from 'readline';
|
|
|
4
4
|
import { resolvePaths, ensureDataDirs } from '../paths.js';
|
|
5
5
|
import { commandExists } from '../utils/cross-platform.js';
|
|
6
6
|
import { scanInstances } from '../utils/instance-registry.js';
|
|
7
|
-
import { saveDefaultsSafe, loadAllAgents, migrateProcessConfigIfNeeded } from '../config-store.js';
|
|
8
|
-
import { loadEvolclawConfig, saveEvolclawConfig } from '../evolclaw-config.js';
|
|
7
|
+
import { saveDefaultsSafe, loadAllAgents, migrateProcessConfigIfNeeded, loadEvolclawConfig, saveEvolclawConfig } from '../config-store.js';
|
|
9
8
|
import { generateControlAid } from '../aun/aid/control-aid.js';
|
|
10
|
-
import {
|
|
9
|
+
import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
|
|
11
10
|
// ==================== Helpers ====================
|
|
12
11
|
function ask(rl, question) {
|
|
13
12
|
return new Promise(resolve => rl.question(question, resolve));
|
|
@@ -15,7 +14,7 @@ function ask(rl, question) {
|
|
|
15
14
|
const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
|
|
16
15
|
function isBaseagentAvailable(baseagent) {
|
|
17
16
|
if (baseagent === 'codex')
|
|
18
|
-
return
|
|
17
|
+
return isCodexAppServerAvailable();
|
|
19
18
|
return commandExists(baseagent);
|
|
20
19
|
}
|
|
21
20
|
function detectAvailable() {
|
|
@@ -80,7 +79,7 @@ export async function cmdInit(options) {
|
|
|
80
79
|
console.log('❌ 未检测到可用 baseagent。请安装至少一款:');
|
|
81
80
|
console.log(' - claude CLI');
|
|
82
81
|
console.log(' - gemini CLI');
|
|
83
|
-
console.log(' -
|
|
82
|
+
console.log(' - codex CLI with app-server');
|
|
84
83
|
console.log('\n安装后重新运行 evolclaw init');
|
|
85
84
|
return;
|
|
86
85
|
}
|
|
@@ -104,7 +103,10 @@ export async function cmdInit(options) {
|
|
|
104
103
|
return; // 硬错误:不落 tail
|
|
105
104
|
}
|
|
106
105
|
if (!available.includes(options.baseagent)) {
|
|
107
|
-
|
|
106
|
+
const reason = options.baseagent === 'codex'
|
|
107
|
+
? getCodexAppServerAvailability().reason
|
|
108
|
+
: undefined;
|
|
109
|
+
console.log(`❌ ${options.baseagent} 当前环境不可用${reason ? `:${reason}` : `(可用: ${available.join('/')})`}`);
|
|
108
110
|
return; // 硬错误:不落 tail
|
|
109
111
|
}
|
|
110
112
|
chosen = options.baseagent;
|
|
@@ -251,6 +253,31 @@ export async function initTail() {
|
|
|
251
253
|
catch { /* ignore */ }
|
|
252
254
|
}
|
|
253
255
|
}
|
|
256
|
+
// ECWeb:交互式 + 未配置时询问是否启用
|
|
257
|
+
if (process.stdin.isTTY) {
|
|
258
|
+
const evcEcweb = loadEvolclawConfig();
|
|
259
|
+
if (evcEcweb.ecweb?.enabled === undefined) {
|
|
260
|
+
const rlEcweb = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
261
|
+
try {
|
|
262
|
+
const ans = (await ask(rlEcweb, '\n是否在 evolclaw start 时自动启动 ECWeb 控制台?[y/N] ')).trim().toLowerCase();
|
|
263
|
+
if (ans === 'y' || ans === 'yes') {
|
|
264
|
+
saveEvolclawConfig({ ...loadEvolclawConfig(), ecweb: { enabled: true } });
|
|
265
|
+
console.log(' ✓ 已启用 ECWeb(evolclaw start 将自动在后台启动)');
|
|
266
|
+
console.log(' 提示:首次访问运行 ec watch web 查看配对码和 URL');
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
saveEvolclawConfig({ ...loadEvolclawConfig(), ecweb: { enabled: false } });
|
|
270
|
+
console.log(' 已跳过(可日后运行 ec watch web 手动启动,或编辑 evolclaw.json)');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
try {
|
|
275
|
+
rlEcweb.close();
|
|
276
|
+
}
|
|
277
|
+
catch { }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
254
281
|
}
|
|
255
282
|
/**
|
|
256
283
|
* Present instance selection menu when existing instances are found.
|
package/dist/cli/model.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
|
|
14
14
|
import { ModelScopeError, normalizePeer, determineScope, activeBaseagent, readScope, writeScope, clearScope, resolveEffectiveModel, } from '../core/model/model-scope.js';
|
|
15
15
|
import { loadDefaults, loadAgent } from '../config-store.js';
|
|
16
|
-
import { resolveAnthropicConfig } from '../agents/
|
|
16
|
+
import { resolveAnthropicConfig } from '../agents/baseagent.js';
|
|
17
17
|
import { getCatalog, getModelInfo } from '../core/model/model-catalog.js';
|
|
18
18
|
const ALL_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
|
|
19
19
|
const SCOPE_LABEL = {
|
|
@@ -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
|
+
}
|