dsh-key-usage 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xyl886
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 CHANGED
@@ -1,3 +1,188 @@
1
- # Temporary Holding Version
1
+ # dsh-key-usage
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **在 DeepSeek Harness 的侧边栏里看用量。** 装上之后,左侧导航会多一项「使用情况」,点开就是整页的用量面板:按通道看 Token、缓存命中率、成本与余额 / 订阅窗口,另外还有年度热力图、项目用量、会话用量。
4
+
5
+ - **本地为主**:用量、项目、会话数据全部来自本机台账、配置与宿主会话事件;**只有「余额与配额」会请求各通道官方接口**(实时优先,失败回落本机快照)。
6
+ - **不碰密钥**:凭据经宿主 `credentials` 服务解析(或进程环境变量),只用于拼 `Authorization` 请求头;不落盘、不进日志、不返回给前端,也从**不打开** `.credentials.yaml`。
7
+ - 只读官方会话句柄(`open("read") → read(0) → close`),不改写、不解压历史日志。
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ # web profile
13
+ dsh plugin --profile web add github:xyl886/dsh-key-usage
14
+
15
+ # tui / headless 等其它 profile 同理,换成对应名字
16
+ dsh plugin --profile tui add github:xyl886/dsh-key-usage
17
+ ```
18
+
19
+ 桌面端(`desktop` profile)由 Electron 应用独占,CLI 会拒绝安装,请在应用内的 **Plugin Hub → 安装插件** 里填 `github:xyl886/dsh-key-usage`。
20
+
21
+ 安装后:客户端改动刷新页面即生效;**服务端插件代码在宿主启动时加载,需要退出并重新打开应用**。
22
+
23
+ ## 它能做什么
24
+
25
+ | 页面 | 内容 |
26
+ | --- | --- |
27
+ | 总览 | Key 清单(通道 / 类型 / 今日 / 近 30 天 / 余额或窗口 / 30 天成本)+ 年度活跃热力图(按日 Tokens,悬停看当日各模型占比) |
28
+ | 使用明细 | 范围切换(今日 / 周 / 月 / 总计 / 自定义)+ 调用次数·Tokens·成本三卡 + 通道占比条 + 按天(单日按小时)堆叠图 + 每日细目(可展开到模型) |
29
+ | 项目用量 | 按工作目录聚合:调用 / Tokens / 占比 / 成本 / 最近活跃,可搜索、筛选通道、排序 |
30
+ | 会话用量 | 按会话列出时间 / 时长 / Tokens / 命中率 / 成本,按日分组,可搜索、筛选、排序 |
31
+ | 余额与配额 | 各通道余额与订阅窗口占用(实时查询官方接口,失败回落快照并标明来源) |
32
+
33
+ ## 界面预览
34
+
35
+ > 以下为演示数据(由设计稿原型渲染,界面元素与实现一致)。
36
+
37
+ **总览** —— Key 清单 + 年度热力图
38
+
39
+ ![总览](./docs/images/usage-overview.png)
40
+
41
+ **使用明细** —— 统计卡、通道占比、按天/按小时图表与每日细目
42
+
43
+ ![使用明细](./docs/images/usage-daily.png)
44
+
45
+ **余额与配额** —— 官方余额与订阅窗口
46
+
47
+ ![余额与配额](./docs/images/usage-quota.png)
48
+
49
+ ## 数据来源与权限
50
+
51
+ | 读取对象 | 用途 | 说明 |
52
+ | --- | --- | --- |
53
+ | `$DSH_HOME/dsh-usage/usage-ledger.json` | 用量台账 | 按天 × 通道 × 模型;台账没有记录的日期会回退到会话事件补齐 |
54
+ | `$DSH_HOME/dsh-usage/provider-snapshots.json` | 余额 / 订阅窗口**回落值** | 实时查询失败时才用,界面会标出「显示本机快照」 |
55
+ | `$DSH_HOME/profiles/<profile>/cordis.yml`(取不到时回退 `cordis.patch.yml`) | 通道清单 | 只做文本提取,**不执行** 文件里的 `!!js` 表达式 |
56
+ | 宿主 `sessionPersistence` 服务 | 项目 / 会话 / 按小时 | 只开只读句柄;服务缺失或读取失败时降级为「数据不可用」,不影响其它页面 |
57
+ | 宿主 `credentials` 服务 / 进程环境变量 | 查余额时的 API Key | 只拼 `Authorization` 请求头;不落盘、不进日志、不返回给前端 |
58
+
59
+ **会联网的唯一路径**:打开「余额与配额」时,服务端按通道请求官方接口,结果缓存 60 秒(点「刷新」则强制重查):
60
+
61
+ | 通道 | 接口 | 用途 |
62
+ | --- | --- | --- |
63
+ | `opencode-go` | `GET https://opencode.ai/zen/go/v1/usage` | 订阅窗口占用(rolling / weekly / monthly)与重置时间 |
64
+ | `deepseek-official` | `GET {baseURL}/user/balance` | 余额(总余额 / 赠送 / 充值) |
65
+ | `openrouter` / `moonshotai` / `zai` | 各自的官方余额接口 | 余额 |
66
+
67
+ 请求 6 秒超时、响应上限 64 KiB、只发 `Authorization` 与 `Accept` 两个头;任何失败(未配置 Key、网络不可达、401/403、非 JSON)都会**回落本机快照**并在卡片上标明原因。
68
+
69
+ - **凭据**:不读取任何凭据文件、不展示、不上传;界面只显示 `apiKeyEnv` 这类**变量名**做溯源提示。
70
+ - **写入**:不写任何文件,不改宿主配置。
71
+ - **端口**:服务端 7 个端点全部只读 GET,且只接受 `127.0.0.1` / `::1` 回环请求,其它来源一律 403。
72
+
73
+ ## 兼容与运行要求
74
+
75
+ - DeepSeek Harness 桌面端 0.2.x(客户端 slot:`sidebar.panellist` + `main`);web profile 亦可。
76
+ - 无需额外运行时依赖:`react` / `react/jsx-runtime` 由宿主注入,服务端只用 Node 内置模块(含内置 `fetch`)。
77
+ - 依赖宿主 `sessionPersistence` 服务提供项目 / 会话 / 按小时数据;该服务缺失时自动降级,其余功能正常。
78
+ - 余额 / 配额需要该通道的 API Key 能被宿主 `credentials` 解析(或存在于进程环境变量);解析不到就只显示快照。
79
+ - 成本:台账里带官方成本;由会话事件补齐的日期没有官方成本,界面会标注来源。
80
+
81
+ **能力分类**:界面与体验 | **许可证**:MIT(见 [LICENSE](./LICENSE))
82
+
83
+ ---
84
+
85
+ # 开发
86
+
87
+ ## 构建与测试
88
+
89
+ ```bash
90
+ # 在仓库根目录执行
91
+ node node_modules/tsdown/dist/run.mjs && node scripts/normalize-client-banner.mjs # 构建客户端 → client/client.js
92
+ node node_modules/typescript/bin/tsc -p tsconfig.server.json # 构建服务端 → lib/
93
+ node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit # 类型检查
94
+ node test/ledger.test.mjs # 其余:collect / channels / session-usage / sessions / merge / live-quota / routes
95
+ ```
96
+
97
+ `package.json` 的 `check` / `test` 脚本已覆盖全部 8 个 lib 文件与 8 个测试。
98
+
99
+ > 这类插件的 `node_modules` 常由较旧的 pnpm 装出,而宿主自带 pnpm 版本更新,
100
+ > 直接 `pnpm run` / `npx` 会因版本不匹配触发「清空 modules 目录」而中止 —— 所以上面用 `node` 直接调本地工具链。
101
+
102
+ ## 目录结构
103
+
104
+ ```
105
+ dsh-key-usage/
106
+ ├── src/
107
+ │ ├── client/ # 客户端 bundle 源码(entry: index.tsx)
108
+ │ │ ├── index.tsx # cordis 接线:注册 sidebar.panellist + main
109
+ │ │ ├── UsagePage.tsx # 主区域整页容器(含滚动)
110
+ │ │ ├── UsageIcon.tsx # 侧边栏导航图标(currentColor)
111
+ │ │ ├── KeyUsageSection.tsx # 页面内容:Tab 路由 + 取数 + 范围/筛选状态
112
+ │ │ ├── api.ts # 端点取数封装
113
+ │ │ ├── format.ts # 数字 / 时间 / 时长格式化
114
+ │ │ ├── channels.ts # 通道颜色、状态、计费模式文案
115
+ │ │ ├── tooltip.tsx # 共享浮层
116
+ │ │ ├── types.ts # 宿主 slot 契约 + 端点响应类型
117
+ │ │ ├── tabs/ # 每个 Tab 一个组件(Overview / Detail / Projects / Sessions / Quota)
118
+ │ │ ├── components/ # StatCard / ShareBar / Heatmap / TokenBars / DailyDetailTable
119
+ │ │ └── styles/ # shared / overview / detail / lists / quota / page(basename 全局唯一)
120
+ │ └── server/ # 服务端源码(编译到 lib/)
121
+ │ ├── index.ts # cordis apply:注入 webServer / 会话数据源 / 实时余额
122
+ │ ├── routes.ts # 7 个只读 GET 端点 + loopback 校验
123
+ │ ├── ledger.ts # 台账解析、范围聚合、均价表、成本估算
124
+ │ ├── collect.ts # cordis.yml 提取 + 快照归一化 + 状态/计费模式判定
125
+ │ ├── merge.ts # 台账 × 会话事件的按天合并
126
+ │ ├── live-quota.ts # 官方余额 / 订阅窗口实时查询(含缓存与错误分档)
127
+ │ ├── session-usage.ts # 会话事件 → 按天/小时/模型折叠(纯函数)
128
+ │ └── sessions.ts # sessionPersistence 适配 + revision 缓存 + 项目/会话聚合
129
+ ├── lib/ # 服务端构建产物(package.json main,需提交)
130
+ ├── client/client.js # 客户端构建产物(单文件,需提交)
131
+ ├── test/ # 8 个离线测试(直接 import lib/*.js)
132
+ ├── docs/ # 架构契约 / 差异清单 / 重构方案 / 设计稿原型
133
+ ├── tsdown.config.ts # 客户端打包 + CSS Modules 内联插件
134
+ └── tsconfig*.json # 类型检查与服务端编译
135
+ ```
136
+
137
+ ## 插件入口与 slot
138
+
139
+ 注册为 **侧边栏导航项 + 主区域整页**,与已装的 dsh-context「上下文洞察」同一套写法:
140
+
141
+ ```tsx
142
+ const PANEL_ID = 'dsh-key-usage'
143
+ ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: PANEL_ID }, UsagePage))
144
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
145
+ name: 'sidebar.panellist', id: PANEL_ID, order: 30, label: () => '使用情况',
146
+ }, (props) => h(UsageIcon, { size: props.size })))
147
+ ```
148
+
149
+ - `sidebar.panellist` 由宿主自持整行(标签、hover/active 配色),插件只提供**图标**与 `label`;`main` 用**同一个值放在 `key`** 上,宿主据此在主内容区渲染整页。
150
+ - 条目顺序由 `order` 控制;图标用 `currentColor`,跟随宿主的行状态。
151
+ - 页面组件 `UsagePage` 不绑定具体 slot,换容器只需改 `index.tsx`。
152
+
153
+ ## 三个坑(改动不生效时先看这里)
154
+
155
+ | 改动位置 | 生效方式 |
156
+ | --- | --- |
157
+ | `src/client/**` | 重新构建 → `client/client.js` → **刷新页面** |
158
+ | `src/server/**` | 重新编译 → `lib/*.js` → **退出并重开应用**(无热重载) |
159
+
160
+ 1. **只跑 `tsc --noEmit` 不会更新 `lib/`**:测试 import 的是 `lib/*.js`,改完必须执行 `tsconfig.server.json` 那次编译再跑测试。
161
+ 2. **服务端没有热重载**:插件代码在挂载时进入 Node 的 ESM 缓存,重新安装不会重新导入;Hub 的 `/restart` 在桌面端返回 `{ok:false, desktop:true}`。
162
+ 3. **`cordis.yml` 是应用重写的组合产物**:插件集变化期间会被短暂重写成空模板,因此通道提取先读它、取不到再回退 `cordis.patch.yml`;其中 `local` 通道不写 `apiKeyEnv`,判定顺序必须**先判本地端点、再判未配置**。
163
+
164
+ ## HTTP 端点(全部只读、仅回环)
165
+
166
+ 非 `127.0.0.1` / `::1` 请求一律返回 `403 {"error":"loopback only"}`。
167
+
168
+ | 端点 | 查询参数 | 响应 |
169
+ | --- | --- | --- |
170
+ | `/dsh-key-usage/channels` | — | `{ channels: Channel[] }`,含 `status` / `costMode` / `costLabel` / `quota*` / `balanceText` |
171
+ | `/dsh-key-usage/usage` | `from`、`to`、`provider` | `{ totals, days, sessionDates, availableFrom, availableTo, hours }`,`hours` 仅在单日范围非空 |
172
+ | `/dsh-key-usage/heatmap` | — | `{ days: [{ date, totalTokens, models[] }], sessionDates }` |
173
+ | `/dsh-key-usage/quota` | `fresh=1` 强制重查 | `{ providers: [{ provider, source, fetchedAt, error, balance, plan }] }` |
174
+ | `/dsh-key-usage/summary` | — | `{ channels, range, todaySource, sessionDates }`(总览「今日 / 近 30 天」) |
175
+ | `/dsh-key-usage/sessions` | `limit` | `{ rows, coverage, priced }` |
176
+ | `/dsh-key-usage/projects` | `limit` | `{ rows, coverage, priced }` |
177
+
178
+ 字段级契约、判定规则与统计口径见 [`docs/01-架构与数据契约.md`](./docs/01-架构与数据契约.md)。
179
+
180
+ ## 文档索引
181
+
182
+ | 文档 | 内容 |
183
+ | --- | --- |
184
+ | [`docs/01-架构与数据契约.md`](./docs/01-架构与数据契约.md) | 模块职责、端点字段级契约、会话服务契约、统计口径、判定规则 |
185
+ | [`docs/02-原型差异与优化清单.md`](./docs/02-原型差异与优化清单.md) | 截图 × 原型 × 源码差异对比与逐条落实情况 |
186
+ | [`docs/03-按Tab拆分重构方案.md`](./docs/03-按Tab拆分重构方案.md) | 拆分方案与实施记录 |
187
+ | [`docs/prototype/`](./docs/prototype/) | 设计稿原型(演示数据,界面预览图由它渲染) |
188
+ | [`AGENTS.md`](./AGENTS.md) | 接手本仓库的 agent 的必读清单、硬约束与汇报要求 |