opencode-metrics-plugin 0.1.5
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/LICENSE +21 -0
- package/README.md +390 -0
- package/dist/backfill-cli.d.ts +1 -0
- package/dist/backfill-cli.js +74 -0
- package/dist/backfill.d.ts +58 -0
- package/dist/backfill.js +550 -0
- package/dist/compile-analyzer.d.ts +40 -0
- package/dist/compile-analyzer.js +161 -0
- package/dist/dirs.d.ts +34 -0
- package/dist/dirs.js +44 -0
- package/dist/event-logger.d.ts +20 -0
- package/dist/event-logger.js +329 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +61 -0
- package/dist/logger.d.ts +10 -0
- package/dist/logger.js +76 -0
- package/dist/metrics-engine.d.ts +37 -0
- package/dist/metrics-engine.js +331 -0
- package/dist/metrics-handlers.d.ts +9 -0
- package/dist/metrics-handlers.js +648 -0
- package/dist/metrics-output.d.ts +11 -0
- package/dist/metrics-output.js +673 -0
- package/dist/metrics-steps.d.ts +19 -0
- package/dist/metrics-steps.js +90 -0
- package/dist/metrics-types.d.ts +381 -0
- package/dist/metrics-types.js +102 -0
- package/dist/summary-store.d.ts +150 -0
- package/dist/summary-store.js +560 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dear-xml
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# opencode-metrics-plugin
|
|
2
|
+
|
|
3
|
+
**opencode 会话实时统计插件**(通用核心,发布于公共 npm)。
|
|
4
|
+
|
|
5
|
+
- **实时事件 → SessionMetrics 快照**:订阅 opencode 事件流(`session.*` / `message.*` / `part.*`),累加每会话状态,每轮 `session.idle` 产出完整快照 JSON(多轮增量 merge)
|
|
6
|
+
- **摘要/详情双表(全局 summary.db)**:本机多宿主共享一个 sqlite 双表库(表结构与 tnnotix 既有库一致,显示字段不变);live 与回填同拍写入,查询一处获得全部宿主会话
|
|
7
|
+
- **统计范围可控**(默认不采):支持 宿主声明 / agent 白名单 / 工作目录前缀 / 全量 四种策略
|
|
8
|
+
- **多引擎目录隔离**:快照 JSON 目录为引擎实例属性,同进程多个宿主插件各写各的,互不覆盖
|
|
9
|
+
- **历史回填**:从 opencode.db 重建历史会话快照,按归属规则路由到各引擎目录(CLI 全量 + 启动增量),摘要同样入全局库
|
|
10
|
+
- **无 UI、无编译专属依赖**(HarmonyOS 的 hvigor/ets 统计逻辑一并保留为可选路径,非 HarmonyOS 会话为空)
|
|
11
|
+
|
|
12
|
+
## 总览
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
opencode 事件流(广播给所有已加载插件)
|
|
16
|
+
│ 宿主 event hook → runtime.event({ event })
|
|
17
|
+
▼
|
|
18
|
+
┌─ 引擎(scope 门控)─────────────── 实时累计 ──────────────────┐
|
|
19
|
+
│ │
|
|
20
|
+
│ 每轮 session.idle / flush / dispose │
|
|
21
|
+
│ │ │
|
|
22
|
+
│ ├─→ 各宿主 <metricsDir>\<sessionId>.json(快照文件, │
|
|
23
|
+
│ │ 按宿主隔离——多写者会冲突,必须分目录) │
|
|
24
|
+
│ ├─→ 全局 summary.db 双表(force,与文件同拍) │
|
|
25
|
+
│ │ session_summaries(摘要索引行,多宿主聚合) │
|
|
26
|
+
│ │ session_details(full_data = 快照原文) │
|
|
27
|
+
│ └─→ onFlush(sessionId, snapshot) 宿主自定义消费 │
|
|
28
|
+
│ │
|
|
29
|
+
│ events\<sessionId>.log(原始事件流,仅 scope 内会话) │
|
|
30
|
+
└──────────────────────────────────────────────────────────────┘
|
|
31
|
+
backfillFromOpencode(opencode.db 历史重建,soft 入全局库,live 永远赢)
|
|
32
|
+
│
|
|
33
|
+
▼
|
|
34
|
+
查询:querySummaries / getDetail / getSubagentSteps / getSessionRaw
|
|
35
|
+
(缺省即查全局库 → 一次获得所有宿主的会话列表)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## 安装
|
|
40
|
+
|
|
41
|
+
公共包,直接安装,无需任何 registry / token 配置:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install opencode-metrics-plugin
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> 从旧 `@dear-xml` 私有包迁移的用户:**全局数据目录不变**——快照/摘要库仍落
|
|
48
|
+
> `%LOCALAPPDATA%/opencode-metrics-plugin/Log/`(`defaultDirs` 的 appName
|
|
49
|
+
> 默认值与包名解耦,已聚合的数据不受迁移/升级影响)。
|
|
50
|
+
|
|
51
|
+
## 快速开始
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { createMetricsRuntime } from 'opencode-metrics-plugin'
|
|
55
|
+
|
|
56
|
+
// 场景 A:全局记录(宿主指定目录,scope=all 记录一切会话)
|
|
57
|
+
const rt = createMetricsRuntime({
|
|
58
|
+
dirs: { metricsDir: '/data/agent-metrics', eventsDir: '/data/agent-events' },
|
|
59
|
+
scope: { mode: 'all' },
|
|
60
|
+
backfillOnStart: true, // 可选:启动时增量补齐插件未运行期间的会话
|
|
61
|
+
})
|
|
62
|
+
// 在宿主插件 hooks.event 里调用(注意包一层 { event }):
|
|
63
|
+
// async ({ event }) => rt.event({ event })
|
|
64
|
+
|
|
65
|
+
// 场景 B:按工作目录前缀统计
|
|
66
|
+
const rt2 = createMetricsRuntime({ scope: { mode: 'cwd', cwdPrefixes: ['/srv/frontend'] } })
|
|
67
|
+
|
|
68
|
+
// 场景 C:宿主声明模式——默认不采,宿主在目标会话创建时 claim
|
|
69
|
+
const rt3 = createMetricsRuntime({ scope: { mode: 'declared' } })
|
|
70
|
+
rt3.engine.claimSession(sessionId, { agent: 'my-plugin-agent' })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 策略
|
|
74
|
+
|
|
75
|
+
| mode | 行为 |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `all` | 统计所有会话(可加 `agents` 只统计白名单 agent) |
|
|
78
|
+
| `none` | 默认不统计;`claimSession` 的会话才统计 |
|
|
79
|
+
| `declared` | 仅宿主 `claimSession`(或 `agents` 命中) |
|
|
80
|
+
| `cwd` | 仅工作目录命中 `cwdPrefixes` 的会话 |
|
|
81
|
+
|
|
82
|
+
子代理(opencode 以 `parentID` 标识)自动随其父会话纳入,不单独计为独立会话。
|
|
83
|
+
|
|
84
|
+
## 历史回填(backfill)
|
|
85
|
+
|
|
86
|
+
从 `~/.local/share/opencode/opencode.db` 重建历史会话快照,按归属规则路由写入各引擎目录。
|
|
87
|
+
归属判定链:`session.agent` 列 →(null 时)首条含 agent 的 message → 引擎规则(`agents` / `cwdPrefixes`)→ 兜底目录。
|
|
88
|
+
空会话(无消息)跳过;快照带 `source: "backfill"` 字段与 live 区分;已有 live 快照不覆盖。
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { backfillFromOpencode } from 'opencode-metrics-plugin'
|
|
92
|
+
|
|
93
|
+
const result = await backfillFromOpencode({
|
|
94
|
+
dbPath: '可选,默认 ~/.local/share/opencode/opencode.db',
|
|
95
|
+
engines: [
|
|
96
|
+
// summaryFile 可选:缺省写全局共享库(与 live 同库);显式指定可路由到隔离库
|
|
97
|
+
{ label: 'harmony-code', metricsDir: '.../hm-plugin-nodejs/Log/metrics', agents: ['harmonyos-plugin'] },
|
|
98
|
+
{ label: 'tnnotix', metricsDir: '.../plugin-b/Log/metrics', agents: ['harmonyos-ascf'] },
|
|
99
|
+
],
|
|
100
|
+
fallback: { metricsDir: '.../default/Log/metrics' }, // 无人认领的会话
|
|
101
|
+
filter: { agents: ['harmonyos-convert'] }, // 只回填指定插件/agent 的会话
|
|
102
|
+
skipExisting: true, // live 优先(默认 true)
|
|
103
|
+
incremental: false, // true=按各目录 .backfill-state 水位增量
|
|
104
|
+
dryRun: false, // true=只统计不写盘
|
|
105
|
+
onSessionBackfilled: (sessionId, label, metricsDir) => {}, // 单会话完成回调
|
|
106
|
+
})
|
|
107
|
+
// result: { perEngine: [{label, metricsDir, written, skippedExisting, skippedEmpty}],
|
|
108
|
+
// fallback: 同上 | null, skippedByFilter, unattributed, total }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 触发时机
|
|
112
|
+
|
|
113
|
+
- **CLI 全量**:`node dist/backfill-cli.js --config engines.json [--agent <name>] [--since <ms|ISO>] [--dry-run] [--db <path>]`
|
|
114
|
+
配置文件(JSON,支持 `//` 注释):`{ "dbPath"?, "engines": [{ "label", "metricsDir", "agents"?, "cwdPrefixes"?, "summaryFile"? }], "fallback"?, "filter"? }`(summaryFile 缺省 = 全局共享库,与 live 同库)
|
|
115
|
+
- **启动增量**:`createMetricsRuntime({ backfillOnStart: true, ... })`,各引擎目录 `.backfill-state` 水位记录进度,插件加载时自动补齐"插件未运行期间"的本引擎会话(仅当 scope 可静态判定:`mode: all` 或配置了 `agents`/`cwdPrefixes` 时生效)
|
|
116
|
+
|
|
117
|
+
### 保真度
|
|
118
|
+
|
|
119
|
+
| 指标 | 回填可重建 |
|
|
120
|
+
|---|---|
|
|
121
|
+
| tokens / cacheHitRate / 轮次 / 时长 / 工具分布 / steps / skills / 子代理 | 完整 |
|
|
122
|
+
| 代码编译统计(hvigor)/ anomaly / compaction 细节 | 置零或缺省 |
|
|
123
|
+
|
|
124
|
+
## 摘要/详情双表(summary.db)
|
|
125
|
+
|
|
126
|
+
插件在快照文件之外自动维护一个 sqlite 摘要库:**缺省为全局共享路径**(`%LOCALAPPDATA%/opencode-metrics-plugin/Log/summary.db`,不随 metricsDir 派生——本机多宿主自动聚合到同一个库);需要隔离时经 `dirs.summaryFile` 精确指定。表结构与 tnnotix 既有库完全一致(显示字段不变):
|
|
127
|
+
|
|
128
|
+
- `session_summaries`:`session_id / task_id / agent / model / directory / title / first_user_message / step_count / tool_calls / total_tokens_{input,output,cache_read,cache_write,reasoning} / total_tokens / duration / created_at / source_file / ingested_at`(索引:`created_at` / `task_id` / `model`)
|
|
129
|
+
- `session_details`:`session_id / full_data`(完整快照 JSON 原文)
|
|
130
|
+
|
|
131
|
+
写入时机:
|
|
132
|
+
|
|
133
|
+
| 时机 | 模式 | 说明 |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| live flush(每轮 `session.idle`) | force | 双表与快照文件同拍最新;进行中会话每轮自动刷新,首轮结束即可查到 |
|
|
136
|
+
| `backfillFromOpencode` | soft | 已有 `total_tokens > 0` 的行不覆盖,live 永远赢 |
|
|
137
|
+
| `reindexDir(metricsDir)` | soft + prune | 扫描存量 JSON 重建;`prune: true` 清理文件已删的孤儿行 |
|
|
138
|
+
|
|
139
|
+
> 依赖 `node:sqlite`(Node >= 22.5)。不可用时实时统计与快照文件照常工作,仅摘要库停用(进程内只告警一次)。
|
|
140
|
+
|
|
141
|
+
## API 使用指导
|
|
142
|
+
|
|
143
|
+
### createMetricsRuntime —— 一体封装(推荐入口)
|
|
144
|
+
|
|
145
|
+
目录 + 事件日志 + 引擎 + 可选启动回填的一体组装,宿主通常只需这一个函数。
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { createMetricsRuntime } from 'opencode-metrics-plugin'
|
|
149
|
+
|
|
150
|
+
const rt = createMetricsRuntime({
|
|
151
|
+
enabled: true, // 总开关(默认 true)
|
|
152
|
+
scope: { mode: 'all' }, // 统计范围(见「策略」)
|
|
153
|
+
dirs: { // 可选,实例目录(三层解析)
|
|
154
|
+
metricsDir: '...', eventsDir: '...', logFile: '...', summaryFile: '...',
|
|
155
|
+
},
|
|
156
|
+
onFlush: (sessionId, snapshot) => {
|
|
157
|
+
// 每轮 flush 后回调(snapshot 为本轮快照;宿主可在此上报自有系统)
|
|
158
|
+
},
|
|
159
|
+
eventLogging: true, // 是否写 events/<id>.log(默认 true),与引擎共用 scope 门控
|
|
160
|
+
backfillOnStart: true, // 启动增量回填(见上)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
rt.event({ event }) // 挂到宿主 hooks.event:async ({ event }) => rt.event({ event })
|
|
164
|
+
rt.engine // 底层引擎(claimSession / flush / dispose ...)
|
|
165
|
+
rt.eventLogger // 事件记录器(null 当 eventLogging:false)
|
|
166
|
+
rt.dirs // 解析后的完整目录集(含派生的 summaryFile)
|
|
167
|
+
rt.eventLogger?.flush() // 退出前手动刷事件日志缓冲
|
|
168
|
+
rt.engine.dispose() // 退出前兜底刷所有未落盘会话(快照+双表)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### createMetricsEngine —— 手动组装引擎
|
|
172
|
+
|
|
173
|
+
需要自己控制事件日志/生命周期时使用(harmony-code 即此方式)。
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { createMetricsEngine } from 'opencode-metrics-plugin'
|
|
177
|
+
|
|
178
|
+
const engine = createMetricsEngine({
|
|
179
|
+
enabled: true,
|
|
180
|
+
scope: { mode: 'all', agents: ['harmonyos-plugin'] },
|
|
181
|
+
dirs: { metricsDir: '...', eventsDir: '...' }, // 构造时三层解析并固化;缺省回退进程级默认
|
|
182
|
+
onFlush: (sessionId, snapshot) => {},
|
|
183
|
+
})
|
|
184
|
+
// 兼容旧布尔入参:createMetricsEngine(true) === scope all;false === none
|
|
185
|
+
|
|
186
|
+
engine.ingest(event) // 喂事件:{ type, properties }
|
|
187
|
+
engine.ingestPrompt(sessionId, modelId, system) // 喂 system prompt(string[])
|
|
188
|
+
engine.claimSession(sessionId, { agent }) // declared/none 模式下显式纳入某会话
|
|
189
|
+
engine.scopeOf(sessionId) // 查询会话是否在统计范围
|
|
190
|
+
engine.shouldTrack(sessionId, type, props) // 归属判定(供 event-logger 等共用同一门控)
|
|
191
|
+
engine.isSubAgent(sessionId) // 是否子代理会话
|
|
192
|
+
engine.flush(sessionId) // 手动触发单会话落盘(快照文件 + summary.db)
|
|
193
|
+
engine.dispose() // 兜底刷全部未落盘会话并清空状态
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### createEventLogger —— 原始事件流记录
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { createEventLogger } from 'opencode-metrics-plugin'
|
|
200
|
+
|
|
201
|
+
const logger = createEventLogger(true, false, {
|
|
202
|
+
eventsDir: '.../events', // 实例目录;缺省回退进程级默认
|
|
203
|
+
isTracked: (sessionId, event) => engine.shouldTrack(sessionId, event.type, event.properties),
|
|
204
|
+
// 与引擎共用同一 scope 判定:引擎不统计的会话不写事件日志
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
logger.log(event) // 喂事件(子代理事件自动并入父日志)
|
|
208
|
+
logger.logPrompt(sessionId, type, content)
|
|
209
|
+
logger.isSubAgent(sessionId)
|
|
210
|
+
logger.flush() // 刷缓冲到磁盘
|
|
211
|
+
logger.dispose()
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### backfillFromOpencode —— 历史回填
|
|
215
|
+
|
|
216
|
+
见「历史回填」章节完整示例与选项说明。
|
|
217
|
+
|
|
218
|
+
### querySummaries / countSummaries —— 摘要筛选(列表数据源)
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { querySummaries, countSummaries } from 'opencode-metrics-plugin'
|
|
222
|
+
|
|
223
|
+
const rows = querySummaries(
|
|
224
|
+
{
|
|
225
|
+
agent: 'harmonyos-plugin', // 单值精确;多值用 agents: [...]
|
|
226
|
+
agents: ['harmonyos-plugin', 'harmonyos-convert'],
|
|
227
|
+
taskId: 'task_42', // 任务分组(先经 setSessionTaskId 标记)
|
|
228
|
+
directoryPrefix: 'D:/company/app2hm', // 反斜杠/大小写自动归一
|
|
229
|
+
model: 'glm-4.6',
|
|
230
|
+
sessionIds: ['ses_a', 'ses_b'], // 会话 id 名单(按集合聚合汇总用)
|
|
231
|
+
since: '2026-01-01', // created_at 下界(ISO 或毫秒时间戳)
|
|
232
|
+
until: Date.now(), // 上界
|
|
233
|
+
limit: 20,
|
|
234
|
+
offset: 0,
|
|
235
|
+
},
|
|
236
|
+
{ metricsDir: '.../Log/metrics' }, // 可选:指定实例目录(缺省走三层解析)
|
|
237
|
+
)
|
|
238
|
+
// rows: SessionSummary[](created_at 倒序)—— id / taskId / agent / model / directory / title /
|
|
239
|
+
// stepCount / toolCalls / totalTokens{input,output,cacheRead,cacheWrite,reasoning,total} /
|
|
240
|
+
// duration / createdAt / firstUserMessage / filePath ...
|
|
241
|
+
|
|
242
|
+
const total = countSummaries({ agent: 'harmonyos-plugin' }, { metricsDir: '.../Log/metrics' })
|
|
243
|
+
// 同条件总数,配合 limit/offset 做分页
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### getDetail —— 单会话详情(详情页数据源)
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { getDetail } from 'opencode-metrics-plugin'
|
|
250
|
+
|
|
251
|
+
const detail = getDetail('ses_xxx', { metricsDir: '.../Log/metrics' })
|
|
252
|
+
// detail: SessionDetail —— steps / rounds / tools / codeStats / planning / skills /
|
|
253
|
+
// systemPrompts;子代理默认摘要化(steps 剥离为 stepCount,载荷更轻)
|
|
254
|
+
|
|
255
|
+
const full = getDetail('ses_xxx', { metricsDir: '.../Log/metrics' }, { fullSubagents: true })
|
|
256
|
+
// fullSubagents: true 保留完整子代理 steps(需直接渲染子代理树的宿主用)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
数据源优先级:`session_details.full_data` → 快照文件回退;返回 `null` 表示会话不存在。
|
|
260
|
+
|
|
261
|
+
### getSubagentSteps —— 子代理 steps 懒加载
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const steps = getSubagentSteps('ses_xxx', 0, { metricsDir: '.../Log/metrics' })
|
|
265
|
+
// StepData[] | null:DB full_data → 文件回退;配合 getDetail 摘要化模式按需取
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### getSessionRaw —— 原始快照 JSON(下载能力)
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
const rawJson = getSessionRaw('ses_xxx', { metricsDir: '.../Log/metrics' })
|
|
272
|
+
// string | null:快照文件优先 → DB full_data 回退
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### setSessionTaskId —— 任务分组标记
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
setSessionTaskId('ses_xxx', 'task_42', { metricsDir: '.../Log/metrics' })
|
|
279
|
+
// boolean:是否命中行(false = 摘要库中无该会话);taskId 传 null 可清除分组
|
|
280
|
+
const group = querySummaries({ taskId: 'task_42' }, { metricsDir: '.../Log/metrics' })
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### removeSession —— 删除会话行(宿主删除链路)
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const { deleted, sourceFile } = removeSession('ses_xxx', { metricsDir: '.../Log/metrics' })
|
|
287
|
+
// 事务内删双表行;sourceFile 为被删行的 source_file,供宿主继续清理快照文件/事件日志
|
|
288
|
+
// 行不存在或库不可用时 deleted=false(幂等,可安全重试)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### reindexDir —— 存量重建 + 孤儿清理
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
const { scanned, upserted, pruned } = reindexDir('.../Log/metrics', {
|
|
295
|
+
summaryFile: '可选,缺省全局共享库',
|
|
296
|
+
prune: true, // 清理 source_file 已不存在于磁盘的孤儿行(双表同删)
|
|
297
|
+
})
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
典型场景:
|
|
301
|
+
- 升级接入后把历史快照文件一次性登记进摘要库
|
|
302
|
+
- 删除文件后清理双表孤儿行——注意共享库语义下 prune 清理的是**全库**孤儿(含其他宿主目录已消失的快照),属预期行为
|
|
303
|
+
|
|
304
|
+
### upsertSessionSnapshot —— 手动入库(自定义数据源)
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
upsertSessionSnapshot(summaryFile, snapshot, filePath, 'force')
|
|
308
|
+
// summaryFile: 库路径;snapshot: MetricsOutput;filePath: 记入 source_file 的快照路径
|
|
309
|
+
// 'force' 总是刷新(live 语义);'soft' 不覆盖已有 total_tokens>0 的行(backfill 语义)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
宿主从自有管道拿到快照对象时可直接入库;返回 boolean 表示是否成功。
|
|
313
|
+
|
|
314
|
+
### 目录 API
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { defaultDirs, configureDirs, getDirs, resolveDirs, getSummaryFile } from 'opencode-metrics-plugin'
|
|
318
|
+
|
|
319
|
+
defaultDirs('my-app') // 兜底目录集:envPaths('my-app').log 下 metrics/ events/ plugin.log summary.db
|
|
320
|
+
// 不传应用名则为 'opencode-metrics-plugin'(本包默认,即全局共享库所在)
|
|
321
|
+
configureDirs({ summaryFile: '...' }) // 只改摘要库落点(进程级);其余保持默认
|
|
322
|
+
configureDirs({ metricsDir: '...' }) // 改快照目录不影响 summaryFile(仍为全局库,除非显式配置)
|
|
323
|
+
getDirs() // 当前进程级默认(defaultDirs + configureDirs 合并结果)
|
|
324
|
+
resolveDirs(partial) // 三层合并:defaultDirs → configureDirs → partial;summaryFile 显式值优先,缺省全局
|
|
325
|
+
getSummaryFile() // 进程级默认摘要库路径
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
落点语义(关键):
|
|
329
|
+
```
|
|
330
|
+
宿主传了 metricsDir='.../hm-plugin/Log/metrics' 时:
|
|
331
|
+
.../hm-plugin/Log/metrics/<sessionId>.json ← 快照按宿主隔离
|
|
332
|
+
%LOCALAPPDATA%/opencode-metrics-plugin/Log/
|
|
333
|
+
├── summary.db ← 摘要库恒为全局(缺省),多宿主聚合
|
|
334
|
+
└── metrics/ events/ plugin.log ← 仅当宿主连 metricsDir 也没配时的兜底
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### 辅助函数
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import {
|
|
341
|
+
extractSummaryFields, // (snapshot, filePath) → SummaryFields:快照→摘要列提取(入库同口径)
|
|
342
|
+
snapshotToSummary, // (snapshot, filePath?) → SessionSummary:快照→列表摘要(含 main/subagent tokens 拆分)
|
|
343
|
+
detailFromRaw, // (raw, { keepSubagentSteps? }) → SessionDetail:快照→详情(默认子代理摘要化)
|
|
344
|
+
detailFullFromRaw, // (raw) → SessionDetail:保留完整子代理 steps 的详情
|
|
345
|
+
sumTokens, // (TokenUsage[]) → TokenUsage:逐元素累加
|
|
346
|
+
subtractTokens, // (a, b) → TokenUsage:相减(剥离子代理 token)
|
|
347
|
+
toIsoString, // (string|number|Date) → ISO 字符串 | null
|
|
348
|
+
firstUserMessageOf, // (rounds) → string | undefined:首条用户消息(兼容 string/string[])
|
|
349
|
+
openSummaryDb, // (file) → DatabaseSync | null:打开/复用摘要库连接(WAL + 幂等建表)
|
|
350
|
+
closeSummaryDbs, // () => void:关闭全部缓存连接(宿主删除目录前调用,避免 Windows 句柄占用)
|
|
351
|
+
} from 'opencode-metrics-plugin'
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## 类型导出
|
|
355
|
+
|
|
356
|
+
- 快照/状态:`MetricsOutput` / `SessionMetrics*` / `TokenUsage` / `StepData` / `RoundSnapshot` / `SubAgentOutput` / `PlanningMetrics` / `SessionMeta`
|
|
357
|
+
- 查询:`SessionSummary` / `SessionDetail` / `SubagentView` / `SummaryFilter` / `SummaryFields` / `ReindexResult` / `UpsertMode` / `RemoveSessionResult`
|
|
358
|
+
- 回填:`BackfillOptions` / `BackfillEngineRule` / `BackfillFilter` / `BackfillResult` / `BackfillEngineStat` / `BackfillMetricsOutput`
|
|
359
|
+
- 目录:`MetricsDirs`;引擎:`MetricsEngineOptions` / `MetricsScope` / `MetricsScopeMode` / `MetricsEngine`;事件:`EventLogger` / `EventLoggerOptions`;运行时:`MetricsRuntime` / `MetricsRuntimeOptions`
|
|
360
|
+
|
|
361
|
+
> 回填与摘要库功能依赖 `node:sqlite`(Node >= 22.5);实时统计与快照文件核心无此要求。
|
|
362
|
+
|
|
363
|
+
## 构建/发布
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
npm install
|
|
367
|
+
npm run build # tsc → dist/
|
|
368
|
+
npm test # example/smoke.mts(tsx,53 断言)
|
|
369
|
+
npm pack # 本地发包前检查
|
|
370
|
+
npm publish # 发布到 npm registry
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## 目录结构
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
src/
|
|
377
|
+
├── index.ts createMetricsRuntime / 查询 API / 全部类型导出
|
|
378
|
+
├── metrics-engine.ts createMetricsEngine(策略 + claimSession + onFlush + 实例目录)
|
|
379
|
+
├── metrics-types.ts 状态/快照类型(MetricsOutput/SessionMetrics*)
|
|
380
|
+
├── metrics-handlers.ts 事件→状态 累加
|
|
381
|
+
├── metrics-output.ts flush 组装快照 + 写盘 + 双表 force 入库(返回快照供 onFlush)
|
|
382
|
+
├── metrics-steps.ts 步骤内容回填(读 events 日志,实例目录)
|
|
383
|
+
├── event-logger.ts events/<sessionId>.log 记录/轮转(实例目录 + isTracked 门控)
|
|
384
|
+
├── compile-analyzer.ts hvigor 输出解析(可选,非编译会话为空)
|
|
385
|
+
├── backfill.ts opencode.db 历史回填(归属路由 + 水位 + source 标记 + 双表 soft 入库)
|
|
386
|
+
├── backfill-cli.ts 回填命令行入口
|
|
387
|
+
├── summary-store.ts 摘要/详情双表(summary.db)+ 全部查询 API + reindex
|
|
388
|
+
├── dirs.ts 目录参数化(defaultDirs/configureDirs/getDirs/resolveDirs/summaryFile 派生)
|
|
389
|
+
└── logger.ts 运行日志(文件 + console 回退)
|
|
390
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { backfillFromOpencode } from "./backfill.js";
|
|
4
|
+
function loadConfig(configPath) {
|
|
5
|
+
let raw = fs.readFileSync(configPath, "utf-8");
|
|
6
|
+
// 容忍 // 行注释与尾逗号外的常见 jsonc 写法(去掉整行注释)
|
|
7
|
+
raw = raw
|
|
8
|
+
.split("\n")
|
|
9
|
+
.map((l) => l.replace(/^\s*\/\/.*$/, ""))
|
|
10
|
+
.join("\n");
|
|
11
|
+
const cfg = JSON.parse(raw);
|
|
12
|
+
if (!Array.isArray(cfg.engines) || cfg.engines.length === 0) {
|
|
13
|
+
throw new Error("配置文件缺少 engines 数组");
|
|
14
|
+
}
|
|
15
|
+
return cfg;
|
|
16
|
+
}
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const out = { agents: [], dryRun: false };
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const a = argv[i];
|
|
21
|
+
if (a === "--config")
|
|
22
|
+
out.config = argv[++i];
|
|
23
|
+
else if (a === "--agent")
|
|
24
|
+
out.agents.push(argv[++i]);
|
|
25
|
+
else if (a === "--since") {
|
|
26
|
+
const v = argv[++i];
|
|
27
|
+
const n = Number(v);
|
|
28
|
+
out.since = Number.isFinite(n) && v.trim() !== "" ? n : Date.parse(v);
|
|
29
|
+
}
|
|
30
|
+
else if (a === "--dry-run")
|
|
31
|
+
out.dryRun = true;
|
|
32
|
+
else if (a === "--db")
|
|
33
|
+
out.dbPath = argv[++i];
|
|
34
|
+
else if (a === "--help" || a === "-h") {
|
|
35
|
+
console.log("用法: node dist/backfill-cli.js --config <engines.json> [--agent <name>...] [--since <ms|ISO>] [--db <path>] [--dry-run]");
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
async function main() {
|
|
42
|
+
const args = parseArgs(process.argv.slice(2));
|
|
43
|
+
if (!args.config) {
|
|
44
|
+
console.error("必须指定 --config <engines.json>");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const cfg = loadConfig(path.resolve(args.config));
|
|
48
|
+
const filter = { ...(cfg.filter ?? {}) };
|
|
49
|
+
if (args.agents.length > 0)
|
|
50
|
+
filter.agents = args.agents;
|
|
51
|
+
if (typeof args.since === "number" && Number.isFinite(args.since))
|
|
52
|
+
filter.since = args.since;
|
|
53
|
+
const opts = {
|
|
54
|
+
dbPath: args.dbPath ?? cfg.dbPath,
|
|
55
|
+
engines: cfg.engines,
|
|
56
|
+
fallback: cfg.fallback,
|
|
57
|
+
filter,
|
|
58
|
+
skipExisting: true,
|
|
59
|
+
dryRun: args.dryRun,
|
|
60
|
+
};
|
|
61
|
+
console.log(`回填开始: db=${opts.dbPath ?? "默认"} 引擎数=${opts.engines.length}${opts.dryRun ? " [dry-run]" : ""}`);
|
|
62
|
+
const result = await backfillFromOpencode(opts);
|
|
63
|
+
for (const s of result.perEngine) {
|
|
64
|
+
console.log(` [${s.label}] ${s.metricsDir} → 写入 ${s.written} | 已有跳过 ${s.skippedExisting} | 空会话 ${s.skippedEmpty}`);
|
|
65
|
+
}
|
|
66
|
+
if (result.fallback) {
|
|
67
|
+
console.log(` [fallback] ${result.fallback.metricsDir} → 写入 ${result.fallback.written} | 已有跳过 ${result.fallback.skippedExisting} | 空会话 ${result.fallback.skippedEmpty}`);
|
|
68
|
+
}
|
|
69
|
+
console.log(` 过滤跳过 ${result.skippedByFilter} | 无人认领 ${result.unattributed} | 顶层会话总数 ${result.total}`);
|
|
70
|
+
}
|
|
71
|
+
main().catch((err) => {
|
|
72
|
+
console.error("回填失败:", err);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { MetricsOutput } from "./metrics-types.js";
|
|
2
|
+
/** 引擎归属规则(与运行时 scope 同语义:agents 命中或 cwd 前缀命中即归属,首条命中优先) */
|
|
3
|
+
export interface BackfillEngineRule {
|
|
4
|
+
label: string;
|
|
5
|
+
/** 该引擎快照写入目录(<sessionId>.json 落这里) */
|
|
6
|
+
metricsDir: string;
|
|
7
|
+
/** agent 名单(session.agent 列,缺失时回退首条含 agent 的 message) */
|
|
8
|
+
agents?: string[];
|
|
9
|
+
/** 工作目录前缀(与 session.directory 前缀匹配,斜杠/大小写不敏感) */
|
|
10
|
+
cwdPrefixes?: string[];
|
|
11
|
+
/** 摘要库落点;缺省写全局共享库(与 live flush 一致,多宿主聚合) */
|
|
12
|
+
summaryFile?: string;
|
|
13
|
+
}
|
|
14
|
+
/** 回填会话选择过滤器(在归属判定之前应用) */
|
|
15
|
+
export interface BackfillFilter {
|
|
16
|
+
agents?: string[];
|
|
17
|
+
cwdPrefixes?: string[];
|
|
18
|
+
since?: number;
|
|
19
|
+
sessionIds?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface BackfillOptions {
|
|
22
|
+
/** opencode 数据库路径,默认 ~/.local/share/opencode/opencode.db */
|
|
23
|
+
dbPath?: string;
|
|
24
|
+
engines: BackfillEngineRule[];
|
|
25
|
+
/** 无人认领会话的兜底目录(不配则跳过不回填) */
|
|
26
|
+
fallback?: {
|
|
27
|
+
metricsDir: string;
|
|
28
|
+
summaryFile?: string;
|
|
29
|
+
};
|
|
30
|
+
filter?: BackfillFilter;
|
|
31
|
+
/** 目标目录已有快照(live 优先)则跳过,默认 true */
|
|
32
|
+
skipExisting?: boolean;
|
|
33
|
+
/** 增量模式:读各目录 .backfill-state.json 水位,只回填 time_created 更晚的会话 */
|
|
34
|
+
incremental?: boolean;
|
|
35
|
+
/** 只统计不写盘 */
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
/** 单会话回填完成回调 */
|
|
38
|
+
onSessionBackfilled?: (sessionId: string, label: string, metricsDir: string) => void;
|
|
39
|
+
}
|
|
40
|
+
export interface BackfillEngineStat {
|
|
41
|
+
label: string;
|
|
42
|
+
metricsDir: string;
|
|
43
|
+
written: number;
|
|
44
|
+
skippedExisting: number;
|
|
45
|
+
skippedEmpty: number;
|
|
46
|
+
}
|
|
47
|
+
export interface BackfillResult {
|
|
48
|
+
perEngine: BackfillEngineStat[];
|
|
49
|
+
fallback: BackfillEngineStat | null;
|
|
50
|
+
skippedByFilter: number;
|
|
51
|
+
unattributed: number;
|
|
52
|
+
total: number;
|
|
53
|
+
}
|
|
54
|
+
export type BackfillMetricsOutput = MetricsOutput & {
|
|
55
|
+
source: "backfill";
|
|
56
|
+
};
|
|
57
|
+
/** 从 opencode.db 回填历史会话统计:按归属规则路由写入各引擎目录(快照带 source:"backfill")。 */
|
|
58
|
+
export declare function backfillFromOpencode(opts: BackfillOptions): Promise<BackfillResult>;
|