opencode-metrics-plugin 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +108 -390
  2. package/dist/index.d.ts +5 -51
  3. package/dist/index.js +5 -61
  4. package/dist/metrics/dirs.d.ts +23 -0
  5. package/dist/{dirs.js → metrics/dirs.js} +4 -10
  6. package/dist/metrics/engine/engine.d.ts +15 -0
  7. package/dist/{metrics-engine.js → metrics/engine/engine.js} +33 -146
  8. package/dist/{metrics-handlers.d.ts → metrics/engine/handlers.d.ts} +1 -1
  9. package/dist/{metrics-handlers.js → metrics/engine/handlers.js} +2 -2
  10. package/dist/metrics/engine/state.d.ts +79 -0
  11. package/dist/{metrics-types.js → metrics/engine/state.js} +1 -19
  12. package/dist/{event-logger.d.ts → metrics/eventlog/event-logger.d.ts} +0 -5
  13. package/dist/{event-logger.js → metrics/eventlog/event-logger.js} +2 -18
  14. package/dist/metrics/index.d.ts +9 -0
  15. package/dist/metrics/index.js +6 -0
  16. package/dist/metrics/runtime.d.ts +25 -0
  17. package/dist/metrics/runtime.js +28 -0
  18. package/dist/metrics/snapshot/flush.d.ts +6 -0
  19. package/dist/{metrics-output.js → metrics/snapshot/flush.js} +6 -290
  20. package/dist/metrics/snapshot/merge.d.ts +9 -0
  21. package/dist/metrics/snapshot/merge.js +282 -0
  22. package/dist/{metrics-steps.d.ts → metrics/snapshot/steps.d.ts} +1 -1
  23. package/dist/{metrics-steps.js → metrics/snapshot/steps.js} +1 -1
  24. package/dist/{metrics-types.d.ts → metrics/types.d.ts} +3 -81
  25. package/dist/metrics/types.js +20 -0
  26. package/dist/plugin.d.ts +22 -0
  27. package/dist/plugin.js +49 -0
  28. package/dist/{logger.d.ts → shared/log.d.ts} +2 -0
  29. package/dist/{logger.js → shared/log.js} +9 -2
  30. package/package.json +8 -4
  31. package/dist/backfill-cli.d.ts +0 -1
  32. package/dist/backfill-cli.js +0 -74
  33. package/dist/backfill.d.ts +0 -58
  34. package/dist/backfill.js +0 -550
  35. package/dist/dirs.d.ts +0 -34
  36. package/dist/metrics-engine.d.ts +0 -37
  37. package/dist/metrics-output.d.ts +0 -11
  38. package/dist/summary-store.d.ts +0 -150
  39. package/dist/summary-store.js +0 -560
  40. /package/dist/{compile-analyzer.d.ts → metrics/analysis/hvigor.d.ts} +0 -0
  41. /package/dist/{compile-analyzer.js → metrics/analysis/hvigor.js} +0 -0
package/README.md CHANGED
@@ -1,390 +1,108 @@
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
- ```
1
+ # opencode-metrics-plugin
2
+
3
+ **opencode 会话指标插件**:挂上即自动记录每个会话的完整指标(tokens / rounds / steps 全文 / 子代理 / 工具调用 / hvigor 构建统计),每轮对话结束落盘一个快照 JSON。
4
+
5
+ - **开箱即用**:宿主只需在 `opencode.json` 加一行,无需任何配置
6
+ - **全量记录**:集成时刻起的所有会话(含子代理会话,自动并入父会话)
7
+ - **增量 merge**:同会话多轮持续追加,进程重启后 rounds/steps 不丢
8
+ - **dispose 兜底**:未走 `session.idle` 的会话在插件卸载时也会落盘
9
+ - **可编程 API**:`createMetricsEngine` / `createMetricsRuntime` 可脱离插件契约单独使用
10
+
11
+ ## 总览
12
+
13
+ ```
14
+ opencode 事件流
15
+ │ plugin event hook
16
+
17
+ ┌─ runtime(事件记录 + 指标引擎)────────────────────────────┐
18
+ │ events\<sessionId>.log 原始事件流(steps 全文数据源) │
19
+
20
+ │ 每轮 session.idle / dispose 兜底
21
+ ├─→ <metricsDir>\<sessionId>.json(快照,增量 merge)
22
+ └─→ onFlush(sessionId, snapshot)(宿主自定义消费)
23
+ └─────────────────────────────────────────────────────────────┘
24
+
25
+
26
+ session-viewer 扫描 metricsDir 解析展示(完全解耦)
27
+ ```
28
+
29
+ ## 快速开始
30
+
31
+ 宿主项目 `opencode.json`:
32
+
33
+ ```jsonc
34
+ // 最简
35
+ { "plugin": ["opencode-metrics-plugin"] }
36
+
37
+ // 带配置
38
+ {
39
+ "plugin": [
40
+ ["opencode-metrics-plugin", { "enabled": true, "eventLogging": true }]
41
+ ]
42
+ }
43
+ ```
44
+
45
+ 配置项(全部可选):
46
+
47
+ | 选项 | 类型 | 默认 | 说明 |
48
+ |---|---|---|---|
49
+ | `enabled` | `boolean` | `true` | 总开关,`false` 时不记录任何会话 |
50
+ | `eventLogging` | `boolean` | `true` | 原始事件写盘(`events/<sessionId>.log`);关闭后快照仍生成,但 `steps[].text/reasoning` 为空 |
51
+ | `dirs` | `Partial<MetricsDirs>` | env-paths | 自定义 `metricsDir` / `eventsDir` / `logFile`(多实例同用时建议各传各的) |
52
+ | `onFlush` | `(sessionId, snapshot) => void` | — | 快照落盘后回调(可在此上报自有系统) |
53
+
54
+ ## 输出
55
+
56
+ 默认目录(appName `opencode-metrics-plugin`,env-paths 规范):
57
+
58
+ | 平台 | 路径 |
59
+ |---|---|
60
+ | Windows | `%LOCALAPPDATA%\opencode-metrics-plugin\Log\metrics\`(events 同级) |
61
+ | Linux | `~/.local/state/opencode-metrics-plugin/metrics/` |
62
+ | macOS | `~/Library/Logs/opencode-metrics-plugin/metrics/` |
63
+
64
+ 每个会话一个 `<sessionId>.json`(`MetricsOutput` 结构),核心字段:
65
+
66
+ - `header`:sessionId / 工作目录 / agent / model 及切换分布
67
+ - `systemPrompts`:各模型 system prompt(经 `experimental.chat.system.transform` hook 记录,子代理跳过)
68
+ - `rounds[]`:每轮 duration、首 token 延迟、tokens、工具调用数、用户消息文本
69
+ - `steps[]`:每步全文(text / reasoning 来自事件日志)、tokens、cost、工具明细
70
+ - `subagents[]`:子代理(task/explore 等)steps、tokens、工具统计
71
+ - `codeStats`:hvigorw 构建统计(错误码 / 警告 / 模块耗时 / 修复周期;非 HarmonyOS 会话为空)
72
+ - `planning`:todowrite 规划统计
73
+
74
+ 配套可视化:[session-viewer](../session-viewer) 扫描 metricsDir 直接解析展示,无需任何服务端支持。
75
+
76
+ ## 可编程 API
77
+
78
+ ```ts
79
+ import { createMetricsRuntime, configureDirs } from 'opencode-metrics-plugin'
80
+
81
+ const rt = createMetricsRuntime({
82
+ dirs: { metricsDir: '/data/metrics', eventsDir: '/data/events' },
83
+ onFlush: (sessionId, snapshot) => upload(snapshot),
84
+ })
85
+
86
+ // 自托管事件源(非 opencode 插件场景)
87
+ rt.event({ event: { type: 'message.part.updated', properties: { ... } } })
88
+
89
+ // 结束时兜底
90
+ rt.eventLogger?.dispose()
91
+ rt.engine.dispose()
92
+ ```
93
+
94
+ 更低层可用 `createMetricsEngine`(无事件日志,仅指标累计)。
95
+
96
+ ## 开发
97
+
98
+ ```bash
99
+ npm run typecheck # tsc --noEmit
100
+ npm test # 冒烟测试(example/smoke.mts,27 断言)
101
+ npm run build # 清空 dist tsc
102
+ ```
103
+
104
+ 结构:`src/plugin.ts`(插件入口)→ `src/metrics/`(域:engine / snapshot / eventlog / analysis / runtime)→ `src/shared/log.ts`。`@opencode-ai/plugin` 仅作类型依赖(peer),运行时零依赖(除 env-paths)。
105
+
106
+ ## License
107
+
108
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,51 +1,5 @@
1
- import type { MetricsEngineOptions } from "./metrics-engine.js";
2
- import type { EventLoggerOptions } from "./event-logger.js";
3
- import type { MetricsDirs } from "./dirs.js";
4
- export { createMetricsEngine } from "./metrics-engine.js";
5
- export type { MetricsEngineOptions, MetricsScope, MetricsScopeMode, MetricsEngine } from "./metrics-engine.js";
6
- export { configureDirs, defaultDirs, getDirs, resolveDirs, getSummaryFile } from "./dirs.js";
7
- export type { MetricsDirs } from "./dirs.js";
8
- export { createEventLogger } from "./event-logger.js";
9
- export type { EventLogger, EventLoggerOptions } from "./event-logger.js";
10
- export { backfillFromOpencode } from "./backfill.js";
11
- export type { BackfillOptions, BackfillEngineRule, BackfillFilter, BackfillResult, BackfillEngineStat, BackfillMetricsOutput, } from "./backfill.js";
12
- export { querySummaries, countSummaries, getDetail, getSubagentSteps, getSessionRaw, setSessionTaskId, reindexDir, upsertSessionSnapshot, openSummaryDb, closeSummaryDbs, removeSession, extractSummaryFields, snapshotToSummary, detailFromRaw, detailFullFromRaw, sumTokens, subtractTokens, toIsoString, firstUserMessageOf, } from "./summary-store.js";
13
- export type { SummaryFilter, SessionSummary, SessionDetail, SubagentView, ReindexResult, UpsertMode, SummaryFields, RemoveSessionResult, } from "./summary-store.js";
14
- export type * from "./metrics-types.js";
15
- export interface MetricsRuntimeOptions extends Omit<MetricsEngineOptions, "enabled"> {
16
- dirs?: Partial<MetricsDirs>;
17
- enabled?: boolean;
18
- /** 事件写盘(events/<sessionId>.log);仅记录归属命中本引擎 scope 的会话 */
19
- eventLogging?: boolean;
20
- /** 事件记录器额外选项(如外部传入的 isTracked 门控,一般由 runtime 自动注入无需配置) */
21
- eventLoggerOptions?: Omit<EventLoggerOptions, "eventsDir" | "isTracked">;
22
- /**
23
- * 启动时增量回填本引擎归属的历史会话(读 opencode.db,水位存于 metricsDir/.backfill-state.json)。
24
- * 仅当 scope 可静态判定(mode all,或配置了 agents/cwdPrefixes)时生效,用于补齐"插件未运行期间"的间隙会话。
25
- */
26
- backfillOnStart?: boolean;
27
- }
28
- /**
29
- * 组装一整套"通用统计插件"的运行时:事件记录 + 指标引擎。
30
- * 目录为实例级(不产生全局副作用):defaultDirs → 进程级默认(configureDirs)→ opts.dirs 三层合并。
31
- * 同一进程可创建多个 runtime(不同插件各配各目录),scope 门控同时作用于引擎计数与事件写盘。
32
- */
33
- export declare function createMetricsRuntime(opts?: MetricsRuntimeOptions): {
34
- dirs: MetricsDirs;
35
- eventLogger: import("./event-logger.js").EventLogger | null;
36
- engine: import("./metrics-types.js").MetricsEngine & {
37
- claimSession: (sessionId: string, meta?: {
38
- agent?: string;
39
- }) => void;
40
- scopeOf: (sessionId: string) => boolean;
41
- shouldTrack: (sessionId: string, eventType: string, props: Record<string, unknown> | undefined) => boolean;
42
- };
43
- /** opencode event hook:`async ({ event }) => { runtime.event({ event }) }` */
44
- event(input: {
45
- event: {
46
- type: string;
47
- properties?: Record<string, unknown>;
48
- };
49
- }): void;
50
- };
51
- export type MetricsRuntime = ReturnType<typeof createMetricsRuntime>;
1
+ export { MetricsPlugin, default } from './plugin.js';
2
+ export type { MetricsPluginOptions } from './plugin.js';
3
+ export * from './metrics/index.js';
4
+ export { setLogLevel, setLogFile, flushLogs } from './shared/log.js';
5
+ export type { LogLevel } from './shared/log.js';
package/dist/index.js CHANGED
@@ -1,61 +1,5 @@
1
- import { createMetricsEngine } from "./metrics-engine.js";
2
- import { createEventLogger } from "./event-logger.js";
3
- import { resolveDirs } from "./dirs.js";
4
- import { backfillFromOpencode } from "./backfill.js";
5
- export { createMetricsEngine } from "./metrics-engine.js";
6
- export { configureDirs, defaultDirs, getDirs, resolveDirs, getSummaryFile } from "./dirs.js";
7
- export { createEventLogger } from "./event-logger.js";
8
- export { backfillFromOpencode } from "./backfill.js";
9
- export { querySummaries, countSummaries, getDetail, getSubagentSteps, getSessionRaw, setSessionTaskId, reindexDir, upsertSessionSnapshot, openSummaryDb, closeSummaryDbs, removeSession, extractSummaryFields, snapshotToSummary, detailFromRaw, detailFullFromRaw, sumTokens, subtractTokens, toIsoString, firstUserMessageOf, } from "./summary-store.js";
10
- /**
11
- * 组装一整套"通用统计插件"的运行时:事件记录 + 指标引擎。
12
- * 目录为实例级(不产生全局副作用):defaultDirs → 进程级默认(configureDirs)→ opts.dirs 三层合并。
13
- * 同一进程可创建多个 runtime(不同插件各配各目录),scope 门控同时作用于引擎计数与事件写盘。
14
- */
15
- export function createMetricsRuntime(opts = {}) {
16
- const dirs = resolveDirs(opts.dirs);
17
- const engine = createMetricsEngine({
18
- enabled: opts.enabled,
19
- scope: opts.scope,
20
- dirs,
21
- onFlush: opts.onFlush,
22
- });
23
- const eventLogger = (opts.eventLogging ?? true)
24
- ? createEventLogger(true, false, {
25
- eventsDir: dirs.eventsDir,
26
- // 事件记录与引擎共用同一归属判定:引擎不统计的会话不写事件日志
27
- isTracked: (sessionId, event) => engine.shouldTrack(sessionId, event.type, event.properties),
28
- })
29
- : null;
30
- if (opts.backfillOnStart) {
31
- const scope = opts.scope ?? { mode: "all" };
32
- const staticallyAttributable = scope.mode === "all" || (scope.agents?.length ?? 0) > 0 || (scope.cwdPrefixes?.length ?? 0) > 0;
33
- if (staticallyAttributable) {
34
- setImmediate(() => {
35
- backfillFromOpencode({
36
- engines: [{
37
- label: "self",
38
- metricsDir: dirs.metricsDir,
39
- agents: scope.agents,
40
- cwdPrefixes: scope.cwdPrefixes,
41
- summaryFile: dirs.summaryFile, // 与 live flush 同库(缺省全局共享库)
42
- }],
43
- skipExisting: true,
44
- incremental: true,
45
- }).catch((err) => {
46
- console.error("[opencode-metrics-plugin] 启动增量回填失败:", err);
47
- });
48
- });
49
- }
50
- }
51
- return {
52
- dirs,
53
- eventLogger,
54
- engine,
55
- /** opencode event hook:`async ({ event }) => { runtime.event({ event }) }` */
56
- event(input) {
57
- eventLogger?.log(input.event);
58
- engine.ingest(input.event);
59
- },
60
- };
61
- }
1
+ // opencode-metrics-plugin 包出口
2
+ // 默认导出即 opencode 插件入口(dist/index.js);metrics 域与 shared/log 为可编程 API
3
+ export { MetricsPlugin, default } from './plugin.js';
4
+ export * from './metrics/index.js';
5
+ export { setLogLevel, setLogFile, flushLogs } from './shared/log.js';