Skip to content

Token 仪表盘 / Token Meter Board: 本机 token 用量实时看板(逐次调用台账、turn 级对话明细;只读会话记录,不访问宿主内部存储) - #60

Closed
HaneWari wants to merge 2 commits into
MiniMax-AI:mainfrom
HaneWari:feat/token-meter-board
Closed

HaneWari wants to merge 2 commits into
MiniMax-AI:mainfrom
HaneWari:feat/token-meter-board

Conversation

@HaneWari

@HaneWari HaneWari commented Oct 22, 2026 •

Copy link
Copy Markdown

中文摘要

提交 Token 仪表盘(插件 ID token-meter-board):本机 MiniMax Code 的实时 token 用量看板。只读客户端为用户自己的对话写下的会话记录文件(v2/sessions/**/messages.jsonl),不打开运行时内部数据库、不扫观测日志、不读宿主配置。纯 Node,无需 Python,无子进程,全程只读、不出网、无遥测。

本版(2.0.0)按评审意见把对宿主内部存储的访问整条拿掉。 上一版直读内部数据库、观测日志与 config.yaml,本版全部移除,数据源换成画廊里已有先例所采用的会话记录文件。逐条对照见下方 「评审意见逐条回应」。

界面为中文,README 提供英文与中文两版。预览图为合成数据实拍,不含任何真实会话内容。


评审意见逐条回应

评审结论为 changes requested,7 条内联意见全部处理如下。

# 评审意见 2.0.0 的处理
1 DatabaseSync 直接打开内部数据库,"只读模式不能作为例外"(common.mjs:55) 整个 lib/common.mjs 已删除。包内不再有任何 import ... from 'node:sqlite'。回归测试对产物源码做静态断言,node:sqlite / DatabaseSync / v2/sqlite / runtime-state 一处都不出现。
2 从插件 dataDir 向上查找并定位宿主内部数据库文件(server.mjs:29) 向上查找的目标改为会话记录目录 v2/sessions,数据库定位逻辑整段移除。解析顺序:MINIMAX_DATA_DIR → 自宿主注入的 context.dataDir 向上最多 10 层找 v2/sessions → 回落 ~/.minimax/v2/sessions。
3 全表扫描宿主内部库消息行并取用户消息前 200 字(facts.mjs:53) 整个 lib/facts.mjs 已删除。逐轮用户消息预览已从页面移除,对话表改用「所属会话」标识一轮。
4 从宿主内部库会话表读 title / workspace_dir;绝对工作区路径不应经 API 带出(queries.mjs:67) 不再读宿主库。会话标题改取 transcript 中首条用户消息的前 80 字(与画廊 mcode-token-usage-board 同一规则,system reminder 会被剥离);会话排行与所有响应体不含任何绝对路径。
5 观测日志属宿主内部存储,请一并移除(common.mjs:304) 不再访问 v2/observability/logs/。模型显示名改用 transcript 记录的原始模型 id,不再去别处补齐。
6 整文件读取宿主 config.yaml(common.mjs:138) 不再读取 config.yaml。
7 建议:/api/health 返回绝对 dataDir,请只返回 basename(server.mjs:173) 已改为 basename:sessionsDir: "sessions"。查询 meta 不再携带任何路径字段。
8 建议:报错文案里的绝对路径应脱敏 入口统一走 redactPaths(),页面收到的错误文案中绝对路径被替换为 <path>;完整原文只进 Host 自己的日志。
9 建议:参考已有插件做 loopback Host / Origin 检查 已加 isLoopbackRequest(),与画廊 plugins/yujiachen-y/repo-xray 的实现逐行同款:Host 不指向回环地址、或 Origin 不指向本监听端口 → 403。
10 ENTRY_STDOUT warning 若随子进程路径一起删掉会自然消失 lib/db-child.mjs 已删除,warning 自然消失。官方校验器现为 0 errors / 0 warnings。

因此失去的能力(如实说明)

会话记录里没有耗时,也没有工作区路径,所以本版相应取消:

  • 输出速率、请求全程速率、P50/P90(原口径依赖 decode 耗时,transcript 里没有)
  • 会话排行里的绝对工作区路径
  • 每轮用户消息前 200 字预览
  • 来自 config.yaml 的模型显示名(表里写原始模型 id)

保留并改进的是:token 用量、缓存命中率、按模型分布、按天趋势、逐次调用台账、对话(turn)明细与会话排行。对话状态改由 transcript 的 stopReason 推导(stop → 已完成,toolUse → 未收尾,aborted → 已中止,尚无模型调用 → 进行中),KPI 新增「对话次数」。

为什么数据源换成会话记录

评审建议"例如画廊里已有、基于会话记录文件的做法",本包采用的就是这一条:画廊里已合入的 plugins/amszuidas/mcode-token-usage-board 读的就是 v2/sessions/**/messages.jsonl + manifest.json,路径解析与标题规则都与它一致。


What it does

A local, read-only token-usage dashboard for MiniMax Code. It reads the session records the client writes for your own conversations — the same source the gallery's mcode-token-usage-board uses — and nothing else. It never opens the runtime's internal database, its observability logs or its configuration file.

  • Usage trend — stacked bars per model, with the cache-hit-rate curve drawn on top. Buckets adapt from minutes to hours to days as the window grows.
  • Five detail tables — per-model breakdown, daily totals, a per-call ledger, a conversation (turn) table, and a session ranking.
  • Per-call ledger — every model call with its exact timestamp, model, all four token classes and how the call ended.
  • Conversation (turn) table — one row per question you asked, showing the session it belongs to, its status, how many model calls the turn took and the tokens. A running turn is marked as running and grows live; nothing has to be closed or archived first.
  • Time windows — last hour / today / last 7 days / last 30 days, or a custom range. The time bar is laid out in two rows so the native date picker can never cover the Apply button.
  • 5-second auto refresh, and switching a window never blanks the tables or waits.

Token Meter Board with synthetic data

Coverage: what the transcripts do not keep

Session records are not a complete accounting. When a long conversation is compacted, the runtime replaces the compacted part of the transcript with a summary, and the calls inside that part are no longer in any messages.jsonl.

Measured on a reference machine (2026-10-23): the board counts 8,640 calls across 151 sessions (162 transcript files, 343.8 MiB), covering 2026-06-17 onwards. The client's own internal ledger — which this board does not read — held 13,124 calls over the window it keeps (2026-09-28 onwards).

Restricting both sides to the same window:

Session records Internal ledger Coverage
Calls 8,422 13,124 64.2%
Total tokens 1,133,235,393 2,099,198,213 54.0%

The difference is calls that were compacted out, not calls that were mis-counted. It works the other way round too: the ledger's earliest row is 2026-09-28, more than three months after the transcripts begin — the ledger itself has passed its retention window, and those 220 calls survive only in the session records.

So these numbers are what your session records still contain, not a bill; billing and quota remain the product's own "usage" page.

How it differs from the two token boards already in the gallery

Token Meter Board (this one) mcode-token-usage-board mcode-usage-monitor
Source of truth Session transcript files (messages.jsonl) Session transcript files (messages.jsonl) Runtime database, read through Python 3.8+
Opens the Host's internal database No No Yes
Per-call ledger Yes No Recent requests only
Conversation (turn) table with live status Yes No No
Needs Python No No Yes
Extra process per query No No Yes
Incremental re-read Yes — byte cursor per file, ~170 ms No — full re-read —
Auto refresh 5 s 60 s 5 s / 10 s / 30 s

The practical difference against mcode-token-usage-board: same data source, but this board adds a per-call ledger, a turn-level table with live status, and incremental reading — the transcripts are ~344 MiB on a reference machine, so re-reading them on every refresh is what that board has to do.

Against mcode-usage-monitor: that board reads the runtime's internal database through Python, which is exactly the access the review rules out for community plugins. This board does not, and does not need Python.

Install and open

Copy plugins/hanewari/token-meter-board/ into your MiniMax Code data directory (by default ~/.minimax/plugins/token-meter-board/), keep the hidden .minimax-plugin directory, restart MiniMax Code, confirm the plugin is enabled, and open Token 仪表盘 — or just ask the Agent to open it. Full instructions are in the package README.

Data & access

Files read (read-only, never modified):

Path Used for
<dataDir>/v2/sessions/**/messages.jsonl The transcript of every session: token counts per model call, model id, timestamp, turn id and stop reason.
<dataDir>/v2/sessions/**/manifest.json The session id of a session directory.

Nothing else is read. The runtime's internal database (v2/sqlite/runtime-state.sqlite), its observability logs (v2/observability/logs/) and its configuration file (config.yaml) are not opened, in any mode.

What the page displays (so screenshots can carry more than you might expect): session titles — the first 80 characters of your first message in that session, with injected system reminders removed, the same rule the gallery's token-usage board uses — plus session ids and model ids. No message body, no per-turn preview, no absolute path.

Written: exactly one file — a scan cursor at <plugin data dir>/session-scan-state.json, written atomically (temp file + rename) when transcripts changed. It holds file paths, session ids, session titles and per-call counters so that a restart resumes instead of re-reading everything. It is a cache: deleting it only costs one full rescan. Browser storage holds exactly one thing: the theme choice, under the key tmb-theme.

Network: none. The Mini App serves one page on 127.0.0.1 and answers its own API routes; it makes no request other than to its own origin. Query parameters are an explicit per-endpoint allow-list, so a crafted query string cannot reach the query layer with an unexpected key — including anything that looks like a data-directory override. Requests whose Host header does not name the loopback interface, or whose Origin header does not name this listener, are refused with 403.

Subprocess: none. No process is spawned and no shell is used.

Node.js: any Node.js that ships node:fs/promises and node:http — no experimental flag and no native module is needed. The runtime used for testing is Node.js v24.18.0.

Tested environment

  • MiniMax Code Desktop 3.1.4 (build 186), Windows 11 (10.0.22631, x64)
  • Bundled Node.js v24.18.0
  • Verified in the desktop client: plugin install, window switching, custom start/end windows, per-call ledger, live turn growth, auto refresh, light and dark themes, and the error path when no session records are found.
  • macOS and Linux are not verified. Nothing in the code is platform-specific — the data paths come from context.dataDir — but please treat the other platforms as untested.

Validation results

  • npm run validate → plugins/hanewari/token-meter-board: 0 errors, 0 warnings, and the repository-level check reports repository: OK. The ENTRY_STDOUT warning is gone together with the child-process path that caused it.
  • npm test → 67 of 69 pass. The two failures are not caused by this package: they are the repository's own test/files.test.mjs cases that create a symbolic link in a temporary fixture to prove the validator rejects one. On this Windows account fs.symlink returns EPERM (Developer Mode is off), so the fixture cannot be created — reproduced on a pristine upstream checkout with no package of mine in it. CI runs on ubuntu-latest, where symlinks are available.
  • An end-to-end regression against the real session records on this machine passes 40/40, including a static guard for this review's red line: the shipped runtime code is scanned and must contain none of v2/sqlite, runtime-state, node:sqlite, DatabaseSync, v2/observability, config.yaml, workspace_dir, workspaceDir, userPreview.

Performance (162 files / 343.8 MiB, real HTTP):

Scenario Time
Server start (no scan) 26 ms
First query after a cold start (full scan) 1,957 ms
Incremental re-scan (after the 3 s TTL expires) 168 ms
TTL hit (repeat request within 3 s) 3 ms
Aggregate build for a new window 17 ms

The reader keeps a byte cursor and a head hash per file, so it only parses what was appended since the last scan. The cursor is persisted in the plugin's own data directory, so even a cold start resumes instead of re-reading everything. Endpoints answer in 2–17 ms once the aggregate is warm.

Details, including the full review-comment mapping table, are in the package's docs/validation.md.

Third-party components

Chart.js v4.4.3 — vendored at miniapp/client/vendor/chart.umd.min.js for offline use. MIT License, © Chart.js Contributors. The full licence text ships with the package at miniapp/client/vendor/LICENSE-chartjs.txt.

License

Apache-2.0. The vendored Chart.js copy keeps its own MIT licence, reproduced in miniapp/client/vendor/LICENSE-chartjs.txt.

A local, read-only token-usage dashboard for MiniMax Code. It reads the
runtime's own per-call usage table and observability logs, so it shows the
numbers the client actually recorded rather than an estimate reconstructed
from session transcript files.

- Usage trend with per-model stacked bars and cache-hit-rate / output-speed
  curves; five detail tables (per-model, daily, a 12-column per-call ledger,
  a turn-level conversation table, session ranking).
- Output speed uses the same basis as the client interface:
  Sigma output tokens / Sigma decode duration, time-weighted, with P50/P90.
- Every call is paired with its own decode and request durations. The pairing
  rule was verified against a token-signature ground truth over 16,723 calls
  with zero mismatches.
- No Python, no extra process in the normal path, no third-party runtime code
  except a vendored copy of Chart.js (MIT, full licence text included).
- Strictly read-only: no files written, no network, no telemetry. Query
  parameters are an explicit per-endpoint allow-list.

Includes the English README with the required "## Tested environment" and
"## Data & access" sections, a Chinese README, an Apache-2.0 LICENSE, a
synthetic-data preview image, and a validation record.

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @HaneWari,欢迎第一次向本仓库投稿!逐次调用台账、turn 级对话明细、以及和客户端同口径的输出速率,方向很清楚;文档也写得很完整(Data & access、与另外两块看板的对照、ENTRY_STDOUT 的说明、合成数据预览)。在 0414d527b9d9d53a2701ed7ab7153643e9588286 上本地看过:范围只有 plugins/hanewari/token-meter-board/ 和两个根 README 各一行;Apache-2.0 + 内置 Chart.js 的 MIT 文本齐全;代码路径上确认是 DatabaseSync(..., { readOnly: true }),没有对宿主文件的写入;npm run check 对本包是 0 errors, 1 warning(就是你说明过的 db-child.mjs stdout)。

不过有一项仓库红线,这一版没法合入。

必须修改

  1. 请去掉对 MiniMax Code 内部数据库的直接访问(只读也不行)

社区插件不能直接打开 MiniMax Code 的内部数据库文件,读、写都不允许。PR / README 里写的数据来源,和代码行为是一致的,并不是「文档写了、代码其实没开」:

  • miniapp/node/server.mjs:24-34:从 context.dataDir 向上最多 8 层,专门查找宿主数据目录里的内部数据库文件,再落到 resolveDataDir()。
  • miniapp/node/lib/common.mjs:39-55:dbPath / openDb 用 node:sqlite 的 DatabaseSync 打开该库(readOnly: true)。
  • 查询侧实际读到的表包括用量台账、消息行、会话行:common.mjs:74-78(local_runtime_token_usage)、facts.mjs:46-53(local_runtime_message_rows,并在 SQL 里 substr 出用户消息前 200 字)、queries.mjs:67(local_runtime_sessions 的 title / workspace_dir)。
  • 同一条数据路径还顺带读了宿主观测日志与配置:common.mjs:304(v2/observability/logs)、common.mjs:137-158(config.yaml,整文件读入再解析显示名)。

因此「只读」「不出网」「参数白名单挡住 dataDir」这些加固都成立,也改变不了:插件在打开并查询宿主内部库。请整条数据路径拿掉(数据库、为关联模型/状态而扫的观测日志、以及为显示名去读的宿主 config.yaml)。没有这条路径之后,看板需要改成不依赖宿主内部库的数据来源(例如画廊里已有、基于会话记录文件的做法),或先收缩到不碰宿主内部库的功能;README 的 Data & access 也请一并改到与真实行为一致。

页面实际会带出的内容(在去掉上述访问之前,也请心里有数——这些都来自那座库):对话表明细的 userPreview(用户消息前 200 字,facts.mjs:36 / queries.mjs:308)、会话标题与 id、会话排行 tooltip 里的绝对工作区路径(queries.mjs:392)。用户若把密钥或路径贴进对话,前 200 字会出现在本机页面的 JSON 响应里。

建议(不阻塞;当前红线未解决前可先放着)

  • /api/health 与查询 meta 带绝对 dataDir(server.mjs:173,queries.mjs:98):成功路径也会把宿主数据目录绝对路径送给页面。若以后有不依赖内部库的版本,建议改成 basename,或干脆不返回。
  • 报错文案里的绝对路径(例如 common.mjs:52 的 not found at ${file}):同样建议脱敏。
  • Host / Origin:入口目前只校验 GET。可参考仓库里已有插件对 loopback Host(以及写操作时的 Origin)的检查,降低 DNS rebinding / 跨站误打到本机端口的风险。
  • ENTRY_STDOUT:你对 db-child.mjs 的说明和 minimax-photo 的处理方式一致,这条 warning 可以保留;若整条子进程读库路径随红线一起删掉,warning 也会自然消失。

谢谢投稿。去掉对宿主内部数据库(以及同路径上的观测日志 / 宿主配置)的访问之后,很乐意再看一版。

if (envDir && fs.existsSync(envDir)) return path.resolve(envDir);
let dir = contextDataDir ? path.resolve(contextDataDir) : null;
for (let i = 0; dir && i < 8; i += 1) {
if (fs.existsSync(path.join(dir, 'v2', 'sqlite', 'runtime-state.sqlite'))) return dir;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 这里从插件 dataDir 向上查找并定位 MiniMax Code 的内部数据库文件。社区插件不能直接打开宿主内部数据库(只读也不行),请去掉整条数据路径。

return { db: null, error: `runtime state database not found at ${file} — is this a MiniMax Code data directory with recorded usage?` };
}
try {
return { db: new sqlite.DatabaseSync(file, { readOnly: true }), error: null };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: DatabaseSync 直接打开了 MiniMax Code 的内部数据库。请删除 openDb / dbPath 以及对宿主内部库的查询;只读模式不能作为例外。

${jsonField('$.usage.request_duration_ms', "IN ('integer','real')")} AS request_ms,
${jsonField('$.finish_reason', "= 'text'")} AS finish_reason,
substr(${jsonField('$.msg_content', "= 'text'")}, 1, ${USER_PREVIEW_CHARS}) AS msg_preview
FROM local_runtime_message_rows

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 这里全表扫描宿主内部库的消息行,并取出用户消息前 200 字。请不要读宿主内部库;对话预览也不能再依赖这条路径。

function loadSessionTitles(db) {
const titles = new Map();
try {
for (const r of db.prepare('SELECT session_id, title, agent_name, workspace_dir FROM local_runtime_sessions').all()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 从宿主内部库的会话表读取 title / workspace_dir。请去掉对宿主内部库的访问;绝对工作区路径也不应再经插件 API 带出。

const maxBytesPerFile = options.maxBytesPerFile ?? 12 * 1024 * 1024;
const now = options.now ?? Date.now();
const map = new Map();
const dir = path.join(dataDir, 'v2', 'observability', 'logs');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 观测日志属于宿主内部存储,且是为补齐内部库里没有的模型名而扫的。请与内部库访问一并移除。

let catalogCache = null;

export function loadCatalog(dataDir) {
const file = path.join(dataDir, 'config.yaml');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

必改: 这里整文件读取宿主 config.yaml(可能含自定义 provider 配置)。请不要读宿主配置文件;显示名需改用不依赖宿主内部文件的方式,或先去掉。

sqliteInProcess,
queryMode: childMode === true ? 'child' : 'in-process',
},
dataDir,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议:/api/health 返回了绝对 dataDir。若以后还有健康检查,请只返回 basename,或省略该字段。

按评审意见(changes requested)把整条数据路径拿掉。

社区插件不能打开 MiniMax Code 的内部数据库,读写都不允许。本版不再访问:

- v2/sqlite/runtime-state.sqlite
- v2/observability/logs/
- config.yaml

数据源改为客户端为用户自己的对话写下的会话记录:
v2/sessions/**/messages.jsonl + manifest.json。路径解析与画廊里已合入的
mcode-token-usage-board 同一套(自 context.dataDir 向上找 v2/sessions,
回落 ~/.minimax),标题规则也一致(首条用户消息前 80 字,剥离 system reminder)。

删除 lib/common.mjs、lib/trend.mjs、lib/facts.mjs、lib/db-child.mjs。
包内不再 import node:sqlite,不再派生任何子进程。随子进程回退一起消失的
ENTRY_STDOUT warning 自然消失:官方校验器现为 0 errors / 0 warnings。

新增 lib/sessions.mjs:按文件字节游标 + 首部哈希增量解析,message_id 全局去重,
扫描游标原子写入插件自己的 dataDir。重写 lib/queries.mjs 与 server.mjs:
逐端点参数白名单、loopback Host/Origin 校验(参照 repo-xray)、报错文案路径脱敏。

因此失去的能力(如实说明):会话记录里没有耗时,也没有工作区路径。
- 取消输出速率与 P50/P90
- 会话排行不再有绝对工作区路径
- 取消每轮用户消息前 200 字预览
- 模型显示名改用 transcript 里的原始模型 id

保留并改进:token 用量、缓存命中率、按模型分布、按天趋势、逐次调用台账、
对话明细与会话排行。对话状态改由 stopReason 推导(stop=已完成 /
toolUse=未收尾 / aborted=已中止 / 无调用=进行中),KPI 增加「对话次数」。

扫描游标持久化后,全量扫描约 2.0 秒,之后每次增量刷新约 170 毫秒。

覆盖率改为同窗对比:会话记录 8,422 次调用 / 1,133,235,393 tokens 对内部账本
13,124 次 / 2,099,198,213 tokens,覆盖率 64.2% / 54.0%;缺口是被压缩掉的调用,
且账本自身保留期更短(最早一行 2026-09-28)。

README(中英)重写 Data & access 并与真实行为对齐,根 README 作品表格那一行
同步更新;docs/validation.md 重写为 2.0.0 验证记录,含评审意见逐条对照表;
docs/preview.png 重新截取(合成数据,无速率列)。
@HaneWari HaneWari changed the title Token 仪表盘 / Token Meter Board: 本机 token 用量实时看板(逐次调用台账、turn 级对话明细、与客户端同口径的输出速率) Token 仪表盘 / Token Meter Board: 本机 token 用量实时看板(逐次调用台账、turn 级对话明细;只读会话记录,不访问宿主内部存储) Oct 23, 2026
@HaneWari

Copy link
Copy Markdown
Author

@MyPrototypeWhat 谢谢评审,这条红线是对的——社区插件确实不该打开宿主内部数据库,哪怕只读。已按意见把整条数据路径拿掉,新提交 46494d9(v2.0.0)。

改了什么

数据源换成会话记录文件,就是评审里建议的那条路:画廊里已合入的 plugins/amszuidas/mcode-token-usage-board 用的 v2/sessions/**/messages.jsonl + manifest.json。路径解析与它一致(自 context.dataDir 向上找 v2/sessions,回落 ~/.minimax),标题规则也一致(首条用户消息前 80 字,剥离 system reminder)。

于是这三个路径任何模式都不会被打开:

  • v2/sqlite/runtime-state.sqlite
  • v2/observability/logs/
  • config.yaml

lib/common.mjs、lib/facts.mjs、lib/trend.mjs、lib/db-child.mjs 四个文件已删除,包内不再 import node:sqlite,也不再派生任何子进程。ENTRY_STDOUT 那条 warning 随 db-child.mjs 一起消失了——官方校验器现在是 0 errors / 0 warnings。

七条内联意见逐条

评审定位 处理
common.mjs:55 DatabaseSync 打开内部库 文件删除,包内无 node:sqlite
server.mjs:29 向上查找宿主数据库 改为向上查找会话记录目录,数据库定位整段移除
facts.mjs:53 全表扫消息行 + 取前 200 字 文件删除;逐轮预览从页面移除,对话表改用「所属会话」标识
queries.mjs:67 读 title / workspace_dir 不再读宿主库;标题取 transcript 首条用户消息前 80 字;页面上没有任何绝对路径
common.mjs:304 观测日志 不再访问;模型显示名改用 transcript 的原始模型 id
common.mjs:138 整文件读 config.yaml 不再读取
server.mjs:173(建议)health 带绝对 dataDir 改为 basename:sessionsDir: "sessions"

另外三条建议也一并采纳了:报错文案统一脱敏(绝对路径在出页面前替换为 <path>,原文只进 Host 日志);入口加了 loopback Host / Origin 校验(参照画廊 repo-xray 的实现逐行同款,不回环 → 403)。

因此失去的能力

会话记录里没有耗时,也没有工作区路径,所以这几项取消了,不留空列:

  • 输出速率、请求全程速率、P50/P90
  • 会话排行的绝对工作区路径
  • 每轮用户消息前 200 字预览
  • 来自 config.yaml 的模型显示名(现在表里是原始模型 id)

保留并改进的是 token 用量、缓存命中率、按模型分布、按天趋势、逐次调用台账、对话明细与会话排行。对话状态改由 stopReason 推导:stop = 已完成,toolUse = 未收尾,aborted = 已中止,尚无调用 = 进行中;KPI 新增「对话次数」。

覆盖率(请重点看这个,它决定了看板还能不能叫"实时看板")

会话记录不是完整账本——长对话被压缩时,那一段会被换成摘要,其中的调用就不在任何 messages.jsonl 里了。

参考机器实测(2026-10-23):会话记录侧 8,640 次调用 / 151 个会话(162 个文件、343.8 MiB);内部账本侧在它自己保留的窗口(2026-09-28 至今)里有 13,124 次调用。限制在同一窗口比:

会话记录 内部账本 覆盖率
调用次数 8,422 13,124 64.2%
token 总量 1,133,235,393 2,099,198,213 54.0%

反方向也有:账本最早一行是 2026-09-28,比会话记录晚三个多月,它自己已经过了保留期,那段时间的 220 次调用只剩会话记录还留着。

所以 README 里写明了:这些数字是"你的会话记录里还留着多少",不是账单,计费与配额仍以客户端自己的"用量"页面为准。

验证

  • npm run validate → plugins/hanewari/token-meter-board: 0 errors, 0 warnings
  • npm test → 67/69 通过。那 2 个失败是仓库自己的 test/files.test.mjs 用 fs.symlink 在临时目录造符号链接的用例,本机 Windows 没开开发者模式(EPERM),与本包无关——我在不含本包的干净上游检出上复现了同样的两个失败。CI 跑在 ubuntu-latest 上不受影响。
  • 端到端回归对本机真实会话记录 40/40 通过,其中加了一条针对本次红线的静态守卫:直接扫描随包发布的运行时代码,以下字符串一处都不许出现——v2/sqlite、runtime-state、node:sqlite、DatabaseSync、v2/observability、config.yaml、workspace_dir、workspaceDir、userPreview。
  • 性能(162 文件 / 343.8 MiB,真实 HTTP):全量扫描 1,957 ms;之后的增量重扫 168 ms;各端点 2–17 ms。读取器为每个文件记字节游标 + 首部哈希,只解析新追加的内容,游标原子写进插件自己的 dataDir,所以冷启动也是接着上次走。

顺带更正上一版 README 里的一个不实表述:v1.9.0 写的"一次刷新 2 毫秒"其实是 3 秒 TTL 缓存命中、什么都没重扫,被当成了刷新成本。本版按实测改成了 168 ms。

完整的评审意见对照表、覆盖率与性能测量过程都写在包内 docs/validation.md,PR 正文也同步更新了(原来那段还在讲内部数据库、日志与速率,已经全部重写)。预览图也重新截了,是 v2.0 的界面。

麻烦再看一下,尤其是 §覆盖率 那段——如果 64% 的调用覆盖率在评审看来仍然偏低,也可以讨论是否该把这个功能再收缩一档。

@HaneWari

Copy link
Copy Markdown
Author

感谢 @MyPrototypeWhat 的评审。评审提出的红线是对的:社区插件不应打开宿主内部数据库,读写都不允许。

在收到评审后我已经按意见做了一版整改(本地版本号 2.0.0):数据源从 v2/sqlite/runtime-state.sqlite 换成客户端为用户对话写下的会话记录文件(v2/sessions/**/messages.jsonl + manifest.json),不再访问 v2/observability/logs/ 与宿主 config.yaml;lib/common.mjs、lib/facts.mjs、lib/trend.mjs、lib/db-child.mjs 已删除,包内不再 import node:sqlite,也不再派生任何子进程,官方校验器在该版本上为 0 errors / 0 warnings。同时补上了评审建议的三项加固:/api/health 只回 basename、报错文案脱敏、入口做 loopback Host / Origin 校验。

不过这次整改的代价是:会话记录里没有 decode / 请求耗时,也没有工作区目录,所以逐次调用台账从 12 列缩到 8 列,输出速率、P50/P90、decode / 请求全程时长、HTTP 状态码、逐轮用户消息预览全部无法保留;覆盖率相对内部账本约为调用 64.2% / token 54.0%。

考虑到这块看板的主要价值恰在其中一列,而这版又是在一个未公开的存储格式上做的权衡,作者决定不再推进市场投稿,改为本地自用,因此在此关闭本 PR。

已实现的这版整改不会浪费:它保留在作者自己的工作仓库里。如果日后有社区插件的合规路径被官方明确(例如官方提供只读 API 或允许的导出格式),可以再拿它来做讨论。评审中提到的几点——社区插件的数据访问边界、ENTRY_STDOUT 这类写 stdout 的检查规则、/api/health 不应回传宿主绝对路径——都很有价值,希望对仓库后续的贡献者也有参考意义。

再次感谢评审的时间与细致核对,尤其是逐条指出每一条访问路径的位置。

@HaneWari

Copy link
Copy Markdown
Author

Closing per the author's decision to keep this dashboard for local use.

@HaneWari HaneWari closed this Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants