dsh-bottom-info-bar 1.16.0 → 1.16.2

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/README.md CHANGED
@@ -1,9 +1,180 @@
1
- # dsh-bottom-info-bar
1
+ # Bottom Info Bar
2
2
 
3
- DeepSeek Harness 底部信息栏插件(静态 bundle 形态)。安装一次,每次打开 DSH 自动生效。
3
+ **English** | [**中文**](README.zh-CN.md)
4
4
 
5
- > 本包发布至 npm registry,也可通过仓库克隆后从本地安装(见仓库根 README)。
5
+ [![npm version](https://img.shields.io/npm/v/dsh-bottom-info-bar)](https://www.npmjs.com/package/dsh-bottom-info-bar)
6
+ [![License: MIT](https://img.shields.io/github/license/songoao25/dsh-bottom-info-bar)](https://github.com/songoao25/dsh-bottom-info-bar/blob/main/LICENSE)
6
7
 
7
- - 安装:`dsh plugin --profile web add dsh-bottom-info-bar`;或在仓库根执行 `./install.sh`,也可从本地目录安装
8
- - 构建:`npm run build`(生成 `lib/`,由 `src/host.js` + `src/client-bundle.js` 生成)
9
- - 完整文档见仓库根 `README.md` 与 `docs/INSTALL.md`
8
+ A DeepSeek Harness plugin that replaces the stats row under the composer with one line: provider and model, real balance or subscription quota, peak/off-peak pricing, and what this session has cost.
9
+
10
+ ![Bottom Info Bar in full mode](assets/bar-full.webp)
11
+
12
+ ## What it shows
13
+
14
+ | Group | Fields |
15
+ |---|---|
16
+ | Provider | the exact provider and model, mirroring DSH's model switcher |
17
+ | Native stats (kept) | turns and steps, model time, tool time, cache hit rate, input/output tokens, context usage |
18
+ | Money | real balance, subscription quota windows, or this month's cloud bill |
19
+ | Pricing | peak and off-peak prices, the current period, a countdown to the next price change |
20
+ | Spend | this session (including subagents), today, last 30 days, all time |
21
+ | Extras | main time, world time, custom text |
22
+
23
+ Click the bar to switch between **full** and **compact** mode; colors follow DSH's light or dark theme.
24
+
25
+ ![Bottom Info Bar in compact mode](assets/bar-compact.webp)
26
+
27
+ ## Three billing modes
28
+
29
+ The bar follows the active session and picks the mode from the provider. The three modes are mutually exclusive — there is no manual switch.
30
+
31
+ ### Balance
32
+
33
+ Shows the real balance from the provider's own API. It refetches when the bar opens, the page refreshes or the provider changes, then polls every 60 seconds; a failed refresh keeps the last known figure on screen. Below 20 (in the account's currency) the amount and a **Low** label turn red.
34
+
35
+ ![Balance mode](assets/bar-balance.webp)
36
+
37
+ ### Subscription quota
38
+
39
+ Shows the remaining quota per window (5-hour / weekly / monthly) and a countdown to the next reset — both always come from the same window, so they can never disagree. Windows show **remaining** percent by default and can be switched to **used** in settings; the low-quota warning always follows the remaining ≤ 20% rule. Compact mode prefers the shortest available window (5-hour → weekly → monthly).
40
+
41
+ ### Cloud billing
42
+
43
+ Shows this month's real spend from the provider's official billing API, for example `Together | This month $12.34` or `AWS Bedrock | This month $45.60 · Budget 46%`. Cloudflare also shows the daily free-quota remainder with a UTC-midnight reset countdown when the API actually reports a free allowance.
44
+
45
+ ## Dark mode
46
+
47
+ Colors adapt to DSH's theme — there is no separate setting.
48
+
49
+ ![Dark mode](assets/bar-dark.webp)
50
+
51
+ ## Install
52
+
53
+ Requires DeepSeek Harness with the web interface (`dsh web`) and pnpm.
54
+
55
+ **From npm** (recommended — installs the released version):
56
+
57
+ ```bash
58
+ dsh plugin --profile web add dsh-bottom-info-bar
59
+ ```
60
+
61
+ **From the GitHub repository** — [github.com/songoao25/dsh-bottom-info-bar](https://github.com/songoao25/dsh-bottom-info-bar) (tracks the default branch; `lib/` is committed, so no build runs on install):
62
+
63
+ ```bash
64
+ dsh plugin --profile web add https://github.com/songoao25/dsh-bottom-info-bar
65
+ ```
66
+
67
+ **With the local one-click script** (clone, build and install in one step):
68
+
69
+ ```bash
70
+ git clone https://github.com/songoao25/dsh-bottom-info-bar.git
71
+ cd dsh-bottom-info-bar
72
+ ./install.sh
73
+ ```
74
+
75
+ Then **restart `dsh web`** — plugins are composed when the host starts, so a page refresh is not enough. The plugin shows up in the Plugins list, enabled:
76
+
77
+ ![Plugins list with Bottom Info Bar installed](assets/plugins-list.webp)
78
+
79
+ More detail and troubleshooting: [docs/INSTALL.md](docs/INSTALL.md).
80
+
81
+ ## Settings
82
+
83
+ Everything lives on the plugin page — **Plugins → bottom-info-bar**. Changes save as you make them.
84
+
85
+ ![Plugin settings overview](assets/settings-overview.webp)
86
+
87
+ **Fields and colors** — one switch and one color per field, in two groups. Turn a field off and the bar drops it.
88
+
89
+ - **Native information** — the fields DSH's own stats row already showed.
90
+ - **Plugin information** — everything this bar adds: provider and model, subscriptions, spend, balance, pricing and quota.
91
+
92
+ ![Native information fields](assets/settings-native.webp)
93
+ ![Plugin information fields](assets/settings-plugin.webp)
94
+
95
+ **Subscription window percentage** — show quota windows as **remaining** (default) or **used**. The low-quota warning always follows remaining ≤ 20%.
96
+
97
+ ![Subscription window percentage set to remaining](assets/settings-quota.webp)
98
+ ![Subscription window percentage set to used](assets/settings-quota-used.webp)
99
+
100
+ **Time and date** — main and world time zones, plus which of year / month / day / hour / minute / second to display.
101
+
102
+ ![Time and date settings](assets/settings-time.webp)
103
+
104
+ **Custom text** — up to 64 characters, shown in the bar.
105
+
106
+ ![Custom text in the bar](assets/bar-custom.webp)
107
+
108
+ **Billing data** — export the ledger as CSV or JSON, or clear it after confirmation. Settings and sign-in information stay untouched.
109
+
110
+ ## Supported providers
111
+
112
+ The bar detects the provider from DSH's current model — no configuration. Set the key in DSH under **Settings → Models**.
113
+
114
+ ### Balance
115
+
116
+ | Provider | Display name | Credential / source |
117
+ |---|---|---|
118
+ | deepseek / deepseek-official | DeepSeek | `DEEPSEEK_API_KEY` |
119
+ | openai | OpenAI | `OPENAI_API_KEY` — estimated from your spending rate; there is no public balance API |
120
+ | moonshotai / moonshotai-cn / kimi-coding | Kimi | `MOONSHOT_API_KEY` (fallback `KIMI_API_KEY`) |
121
+ | openrouter | OpenRouter | `OPENROUTER_API_KEY` |
122
+ | stepfun | StepFun | `STEPFUN_API_KEY` |
123
+ | xiaomi | Xiaomi MiMo | `XIAOMI_API_KEY` |
124
+
125
+ ### Subscription quota
126
+
127
+ | Provider | Display name | Credential / source |
128
+ |---|---|---|
129
+ | codex / chatgpt / openai-codex | ChatGPT / Codex | `~/.codex/auth.json` (read-only, decoded locally) |
130
+ | opencode-go / opencode | OpenCode Go | `OPENCODE_GO_API_KEY` or the opencode CLI login |
131
+ | zai / zai-coding-cn | Zhipu | `ZAI_CODING_CN_API_KEY` (fallback `ZAI_API_KEY`) |
132
+ | xiaomi-token-plan-cn / -sgp / -ams | Xiaomi MiMo | `XIAOMI_TOKEN_PLAN_CN/SGP/AMS_API_KEY` (fallback `XIAOMI_API_KEY`) |
133
+ | command / command-code | Command Code | `COMMAND_CODE_API_KEY` or `CMD_API_KEY`, or `~/.commandcode/auth.json` |
134
+ | minimax / minimax-cn | MiniMax | `MINIMAX_API_KEY` (Global) / `MINIMAX_CN_API_KEY` (CN) — must be a **Subscription Key** |
135
+
136
+ ### Cloud billing
137
+
138
+ | Provider | Display name | Credential / source |
139
+ |---|---|---|
140
+ | together | Together | `TOGETHER_API_KEY` — official Usage API, this month's spend |
141
+ | fireworks | Fireworks | `FIREWORKS_API_KEY` — official Billing API, this period's spend |
142
+ | amazon-bedrock | AWS Bedrock | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` — Cost Explorer + Budgets |
143
+ | cloudflare-ai-gateway / cloudflare-workers-ai | Cloudflare | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` (token needs Billing read) |
144
+
145
+ Anything else shows a **Not supported** hint instead of borrowing another provider's numbers.
146
+
147
+ ## Spend tracking
148
+
149
+ Every model response is recorded (usage × unit price) and aggregated four ways: **this session** (including subagents), **today**, **last 30 days** and **all time**. The price is locked the moment a response completes, so later price-table updates never rewrite history. A model with no known price keeps its token counts but is excluded from money totals — no amount is ever invented. Records are written to disk before they count, so a restart loses nothing; if a write fails, the bar says **Spend not saved**.
150
+
151
+ ## Updating
152
+
153
+ Once per DSH startup the plugin checks npm for a newer version. When one exists, a red **Update available** label appears in the bar: click it to copy the update command, run it in a terminal, then restart `dsh web`.
154
+
155
+ | Installed via | Command |
156
+ |---|---|
157
+ | npm | `dsh plugin --profile <profile> add dsh-bottom-info-bar@latest` |
158
+ | GitHub address | `dsh plugin --profile <profile> add <the same address>` |
159
+ | local checkout or install script | `git -C <repo> fetch origin && git -C <repo> checkout main && git -C <repo> merge --ff-only origin/main && node <repo>/scripts/build.mjs` |
160
+
161
+ Nothing updates by itself — the bar only tells you that a newer version exists.
162
+
163
+ ## Privacy and security
164
+
165
+ - **Read-only credentials.** API keys stay in DSH; `~/.codex/auth.json` and `~/.commandcode/auth.json` are read locally and never written back. This plugin does not bind or refresh accounts.
166
+ - **No conversation content.** The ledger stores tokens, model, provider, currency and cost — never prompts, messages or keys.
167
+ - **Local only.** Data lives in `~/.dsh/dsh-bottom-info-bar/` (directory `0700`, files `0600`). Nothing leaves your machine except the provider API requests the bar itself makes.
168
+ - **Uninstalling keeps your data.** Export or clear it from the plugin page (Billing data) if you want a clean slate.
169
+
170
+ ## Development
171
+
172
+ - Build: `npm run build`
173
+ - Test: `node tests/run-all.mjs`
174
+ - Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
175
+
176
+ ## License
177
+
178
+ [MIT](LICENSE) © 2026 songoao25
179
+
180
+ 💬 Questions or ideas? Join our WeChat group **DeepThinking** — [QR code](assets/wechat-group.png).
@@ -0,0 +1,182 @@
1
+ # 底部信息栏
2
+
3
+ [**English**](README.md) | **中文**
4
+
5
+ [![npm 版本](https://img.shields.io/npm/v/dsh-bottom-info-bar)](https://www.npmjs.com/package/dsh-bottom-info-bar)
6
+ [![License: MIT](https://img.shields.io/github/license/songoao25/dsh-bottom-info-bar)](https://github.com/songoao25/dsh-bottom-info-bar/blob/main/LICENSE)
7
+
8
+ DeepSeek Harness 插件:把输入框下方那行统计栏换成一行信息栏——服务商与模型、真实余额或订阅额度、高峰/空闲定价,以及本会话已经花了多少。
9
+
10
+ ![信息栏完整模式](assets/bar-full.zh-CN.webp)
11
+
12
+ ## 它显示什么
13
+
14
+ | 分组 | 内容 |
15
+ |---|---|
16
+ | 服务商 | 当前会话的服务商与具体模型,跟随 DSH 模型切换器 |
17
+ | 原生统计(保留) | 轮次与步数、模型耗时、工具耗时、缓存命中率、输入/输出 token、上下文占用 |
18
+ | 金额 | 真实余额、订阅额度窗口,或本月云账单 |
19
+ | 定价 | 高峰价与空闲价、当前时段、下次价格切换倒计时 |
20
+ | 花费 | 本会话(含子代理)、今天、近 30 天、累计 |
21
+ | 其它 | 主时间、世界时间、自定义文字 |
22
+
23
+ 点击信息栏可在**完整**与**简洁**之间切换;配色跟随 DSH 的深色或浅色主题。
24
+
25
+ ![信息栏简洁模式](assets/bar-compact.zh-CN.webp)
26
+
27
+ ## 三种计费模式
28
+
29
+ 信息栏跟随当前会话,按服务商自动选择模式。三种模式互斥,无需手动切换。
30
+
31
+ ### 余额制
32
+
33
+ 显示服务商官方接口返回的真实余额。信息栏打开、页面刷新或切换服务商时立即重查,之后每 60 秒轮询一次;刷新失败时保留上一次的数值。余额低于 20(按账户币种)时,金额与**低**标记转红。
34
+
35
+ ![余额制](assets/bar-balance.zh-CN.webp)
36
+
37
+ ### 订阅额度制
38
+
39
+ 显示各窗口剩余额度(5 小时 / 周 / 月)与距下次重置的倒计时——两者始终取自同一个窗口,不会互相矛盾。窗口默认按**剩余**显示百分比,可在设置里切换为**已用**;低额度告警始终按剩余 ≤ 20% 判定。简洁模式优先显示时长最短的可用窗口(5 小时 → 周 → 月)。
40
+
41
+ ### 云账单制
42
+
43
+ 显示官方账单接口返回的本月真实花费,例如 `Together | 本月 $12.34`、`AWS Bedrock | 本月 $45.60 · 预算 46%`。Cloudflare 还会在接口确实返回免费额度时,显示每日免费额度剩余与 UTC 零点重置倒计时。
44
+
45
+ ## 深色模式
46
+
47
+ 配色跟随 DSH 主题,无需单独设置。
48
+
49
+ ![深色模式](assets/bar-dark.zh-CN.webp)
50
+
51
+ ## 安装
52
+
53
+ 需要带 Web 界面的 DeepSeek Harness(`dsh web`)与 pnpm。
54
+
55
+ **npm 安装**(推荐,装的是已发布版本):
56
+
57
+ ```bash
58
+ dsh plugin --profile web add dsh-bottom-info-bar
59
+ ```
60
+
61
+ **从 GitHub 仓库安装** —— [github.com/songoao25/dsh-bottom-info-bar](https://github.com/songoao25/dsh-bottom-info-bar)(跟随默认分支;`lib/` 已入库,安装时不跑构建):
62
+
63
+ ```bash
64
+ dsh plugin --profile web add https://github.com/songoao25/dsh-bottom-info-bar
65
+ ```
66
+
67
+ **本地一键脚本**(clone、构建、安装一步完成):
68
+
69
+ ```bash
70
+ git clone https://github.com/songoao25/dsh-bottom-info-bar.git
71
+ cd dsh-bottom-info-bar
72
+ ./install.sh
73
+ ```
74
+
75
+ 然后**重启 `dsh web`** —— 插件在宿主启动时组合,刷新页面不够。重启后插件出现在插件列表并处于启用状态:
76
+
77
+ ![插件列表:底部信息栏已安装](assets/plugins-list.zh-CN.webp)
78
+
79
+ 详细步骤与故障排查见 [docs/INSTALL.md](docs/INSTALL.md)。
80
+
81
+ ## 设置
82
+
83
+ 配置都在插件页(**插件 → bottom-info-bar**),改完自动保存。
84
+
85
+ ![插件设置页总览](assets/settings-overview.zh-CN.webp)
86
+
87
+ **字段与配色** —— 每个字段一个开关、一个颜色,分两组。关掉哪个字段,信息栏就不再显示它。
88
+
89
+ - **原生信息** —— DSH 原生统计栏本来就有的字段。
90
+ - **插件信息** —— 信息栏新增的全部内容:服务商与模型、订阅、花费、余额、定价与额度。
91
+
92
+ ![原生信息字段](assets/settings-native.zh-CN.webp)
93
+ ![插件信息字段](assets/settings-plugin.zh-CN.webp)
94
+
95
+ **订阅窗口百分比方向** —— 按**剩余**(默认)或**已用**显示额度窗口。低额度告警始终按剩余 ≤ 20% 判定。
96
+
97
+ ![订阅窗口百分比方向:剩余](assets/settings-quota.zh-CN.webp)
98
+ ![订阅窗口百分比方向:已用](assets/settings-quota-used.zh-CN.webp)
99
+
100
+ **时间与日期** —— 主时间与世界时间的时区,以及年 / 月 / 日 / 时 / 分 / 秒的显示格式。
101
+
102
+ ![时间与日期设置](assets/settings-time.zh-CN.webp)
103
+
104
+ **自定义文字** —— 最多 64 个字符,显示在信息栏中。
105
+
106
+ ![信息栏中的自定义文字](assets/bar-custom.zh-CN.webp)
107
+
108
+ **账单数据** —— 导出 CSV / JSON,或确认后清除记录。设置与登录信息不受影响。
109
+
110
+ ## 支持的服务商
111
+
112
+ 信息栏从 DSH 当前模型识别服务商,零配置;密钥在 DSH 的**设置 → 模型**里填写。
113
+
114
+ ### 余额制
115
+
116
+ | 服务商 | 显示名 | 凭据 / 数据来源 |
117
+ |---|---|---|
118
+ | deepseek / deepseek-official | DeepSeek | `DEEPSEEK_API_KEY` |
119
+ | openai | OpenAI | `OPENAI_API_KEY` —— 按消耗速度估算;官方没有公开余额接口 |
120
+ | moonshotai / moonshotai-cn / kimi-coding | Kimi | `MOONSHOT_API_KEY`(回退 `KIMI_API_KEY`) |
121
+ | openrouter | OpenRouter | `OPENROUTER_API_KEY` |
122
+ | stepfun | StepFun | `STEPFUN_API_KEY` |
123
+ | xiaomi | 小米 MiMo | `XIAOMI_API_KEY` |
124
+
125
+ ### 订阅额度制
126
+
127
+ | 服务商 | 显示名 | 凭据 / 数据来源 |
128
+ |---|---|---|
129
+ | codex / chatgpt / openai-codex | ChatGPT / Codex | `~/.codex/auth.json`(只读,本机解析) |
130
+ | opencode-go / opencode | OpenCode Go | `OPENCODE_GO_API_KEY` 或 opencode CLI 登录 |
131
+ | zai / zai-coding-cn | 智谱 | `ZAI_CODING_CN_API_KEY`(回退 `ZAI_API_KEY`) |
132
+ | xiaomi-token-plan-cn / -sgp / -ams | 小米 MiMo | `XIAOMI_TOKEN_PLAN_CN/SGP/AMS_API_KEY`(回退 `XIAOMI_API_KEY`) |
133
+ | command / command-code | Command Code | `COMMAND_CODE_API_KEY` 或 `CMD_API_KEY`,或 `~/.commandcode/auth.json` |
134
+ | minimax / minimax-cn | MiniMax | `MINIMAX_API_KEY`(Global)/ `MINIMAX_CN_API_KEY`(国内)—— 必须是 **Subscription Key** |
135
+
136
+ ### 云账单制
137
+
138
+ | 服务商 | 显示名 | 凭据 / 数据来源 |
139
+ |---|---|---|
140
+ | together | Together | `TOGETHER_API_KEY` —— 官方 Usage API,本月花费 |
141
+ | fireworks | Fireworks | `FIREWORKS_API_KEY` —— 官方 Billing API,本周期花费 |
142
+ | amazon-bedrock | AWS Bedrock | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` —— Cost Explorer + Budgets |
143
+ | cloudflare-ai-gateway / cloudflare-workers-ai | Cloudflare | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID`(令牌需 Billing 读权限) |
144
+
145
+ 不在列表中的服务商显示**未适配**提示,绝不借用别的服务商的数据。
146
+
147
+ ## 花费记账
148
+
149
+ 每次模型响应都会记录一条(用量 × 单价),并按四个口径聚合:**本会话**(含子代理)、**今天**、**近 30 天**、**累计**。单价在响应完成的那一刻锁定,之后的价目表更新不会改写历史金额。价目表里没有的模型保留 token 用量但不计入金额,绝不编造金额。记录先落盘再计入,重启不丢;写入失败时信息栏显示**账单未保存**。
150
+
151
+ ## 更新
152
+
153
+ 插件在一次完整的 DSH 启动后检查一次 npm 上的新版本。有新版时,信息栏出现红色**新版本提醒**标签:点击即可复制更新命令,到终端执行后重启 `dsh web`。
154
+
155
+ | 安装方式 | 命令 |
156
+ |---|---|
157
+ | npm | `dsh plugin --profile <profile> add dsh-bottom-info-bar@latest` |
158
+ | GitHub 地址 | `dsh plugin --profile <profile> add <同一个地址>` |
159
+ | 本地代码或一键脚本 | `git -C <仓库> fetch origin && git -C <仓库> checkout main && git -C <仓库> merge --ff-only origin/main && node <仓库>/scripts/build.mjs` |
160
+
161
+ 插件绝不自动更新——它只负责告诉你「有新版本」。
162
+
163
+ ## 隐私与安全
164
+
165
+ - **只读凭据。** API Key 始终留在 DSH 里;`~/.codex/auth.json` 与 `~/.commandcode/auth.json` 只在本机读取,绝不写回。本插件不负责绑定或续期账号。
166
+ - **不保存对话内容。** 账本只记录 token、模型、服务商、币种与花费——没有提示词、消息或密钥。
167
+ - **只存本地。** 数据在 `~/.dsh/dsh-bottom-info-bar/`(目录 `0700`、文件 `0600`)。除了信息栏自身发起的服务商接口请求,没有任何数据离开本机。
168
+ - **卸载不删数据。** 想彻底清零,在插件页的「账单数据」里导出或清除。
169
+
170
+ ## 开发
171
+
172
+ - 构建:`npm run build`
173
+ - 测试:`node tests/run-all.mjs`
174
+ - 贡献:[CONTRIBUTING.md](CONTRIBUTING.md)
175
+
176
+ ## 许可证
177
+
178
+ [MIT](LICENSE) © 2026 songoao25
179
+
180
+ 💬 有问题或想法?扫码加入微信群 **DeepThinking**:
181
+
182
+ <img src="assets/wechat-group.png" width="180" alt="微信群 DeepThinking 二维码">
@@ -0,0 +1,155 @@
1
+ # 安装 / 卸载 / 故障恢复
2
+
3
+ ## 前置条件
4
+
5
+ - 已安装 DeepSeek Harness(`dsh` CLI 在 PATH 中)
6
+ - 已安装 [pnpm](https://pnpm.io/)
7
+ - 使用 Web 界面(`dsh web`)
8
+
9
+ ## 安装
10
+
11
+ ### 方式一:NPM 安装(推荐)
12
+
13
+ ```bash
14
+ dsh plugin --profile web add dsh-bottom-info-bar
15
+ ```
16
+
17
+ 装的是已发布的版本,更新提醒给出的也是这条命令。
18
+
19
+ ### 方式二:在 DSH 插件页里添加
20
+
21
+ **插件 → 添加插件**,「包名或地址」填 `dsh-bottom-info-bar`。
22
+
23
+ 那一栏也可以直接填仓库地址 —— 包就在仓库根目录,所以填地址装的是默认分支的最新代码:
24
+
25
+ ```
26
+ https://github.com/songoao25/dsh-bottom-info-bar
27
+ ```
28
+
29
+ 这种方式不跑构建(`lib/` 已入库),也不需要额外授权。更新就是再执行一次同样的命令。
30
+
31
+ ### 方式三:一键脚本(产生 `link:` 安装)
32
+
33
+ ```bash
34
+ git clone https://github.com/songoao25/dsh-bottom-info-bar.git
35
+ cd dsh-bottom-info-bar
36
+ ./install.sh
37
+ # 默认安装到 web profile;其他 profile 需以 `dsh web` 方式使用:
38
+ ./install.sh --profile <profile名>
39
+ ```
40
+
41
+ ### 方式四:从本地代码安装(产生 `link:` 安装)
42
+
43
+ ```bash
44
+ git clone https://github.com/songoao25/dsh-bottom-info-bar.git
45
+ cd dsh-bottom-info-bar
46
+ node scripts/build.mjs
47
+ dsh plugin --profile web add /path/to/dsh-bottom-info-bar
48
+ ```
49
+
50
+ > 1.15.0 及更早的版本里,插件包放在仓库的 `plugin/` 子目录,安装路径末尾要写 `/plugin`。该目录已不存在 —— 如果你当初是这样装的,先 `dsh plugin --profile web remove dsh-bottom-info-bar`,再用上面的命令装一次。
51
+
52
+ ### 安装原理
53
+
54
+ `dsh plugin add` 会:
55
+
56
+ 1. 用 pnpm 把插件包安装到 profile 目录(`~/.dsh/profiles/<name>/`);
57
+ 2. 检测到包声明了 `dsh.bundle`(仓库根的 `cordis.patch.yml`),自动把包名加入 profile 的 bundle 层列表(`dsh.profile.bundles`);
58
+ 3. 下次启动 `dsh` 时,插件随 profile 自动加载——host 注册 HTTP 路由、client 注入页面信息栏。
59
+
60
+ **注意:安装后需要重启 `dsh web`(或重启 DSH)才会生效**——宿主进程在启动时组合插件。刷新页面不足以加载 host 端。
61
+
62
+ ### 验证安装成功
63
+
64
+ ```bash
65
+ dsh --profile web --dump-config | grep -A2 dsh-bottom-info-bar
66
+ # 应看到 dsh-bottom-info-bar 行(bundle 层已生效)
67
+ ```
68
+
69
+ 重启后页面底部输入框下方出现信息栏即安装成功。
70
+
71
+ ## 配置余额
72
+
73
+ 在 **设置 → 模型** 中配置 DeepSeek API Key(环境变量名 `DEEPSEEK_API_KEY`)。
74
+ 未配置时信息栏显示引导文案,其余功能(统计/定价/记账)不受影响。
75
+
76
+ ## 配置订阅额度(可选,v1.1.0)
77
+
78
+ 信息栏会自动检测当前模型所属模式:**订阅制**(Codex / OpenCode Go)显示三窗口额度,**余额制**(DeepSeek 等)显示余额。订阅额度数据源:
79
+
80
+ - **Codex / ChatGPT**:信息栏**只读** `~/.codex/auth.json` 中的登录令牌,解析真实套餐与到期信息;token 仅在本机内存中使用,不落盘、不记录、不续期、不写回。令牌的**绑定 / 续期**由独立插件 [**dsh-chatgpt-subscription**](https://github.com/songoao25)(独立仓库)负责——安装并绑定后,本信息栏即可显示订阅信息;令牌缺失时信息栏显示「未绑定」引导,相关字段缺失时只保留服务商和模型。
81
+ - **OpenCode Go**:在 **设置 → 模型** 配置 `OPENCODE_GO_API_KEY`(或先用 opencode CLI 登录其订阅,写入 `~/.local/share/opencode/auth.json` 的 `opencode-go` 条目)。未配置时信息栏显示"未配置 OpenCode Go"引导,不报错。
82
+
83
+ ## 更新版本
84
+
85
+ **最省事的方式:点信息栏上的红色「新版本提醒」标签**,更新命令会复制到剪贴板,粘到终端执行即可(见下一节)。命令会按你的安装方式自动给出,不用自己判断。
86
+
87
+ 手动更新时,按当初的安装方式二选一:
88
+
89
+ 如果最初使用 NPM 安装:
90
+
91
+ ```bash
92
+ dsh plugin --profile web add dsh-bottom-info-bar@latest
93
+ # 重启 dsh web
94
+ ```
95
+
96
+ 如果最初填的是 GitHub 地址(方式二),更新就是再执行一次同样的命令:
97
+
98
+ ```bash
99
+ dsh plugin --profile web add https://github.com/songoao25/dsh-bottom-info-bar
100
+ # 重启 dsh web
101
+ ```
102
+
103
+ 如果最初使用本地代码 / symlink 安装(方式三、方式四),要**三步一起做**:拉代码 → 切到默认分支 → 重新构建:
104
+
105
+ ```bash
106
+ cd dsh-bottom-info-bar
107
+ git fetch origin && git checkout main && git merge --ff-only origin/main
108
+ node scripts/build.mjs
109
+ # 重启 dsh web
110
+ ```
111
+
112
+ - **别只用 `git pull`**:分支没推到远端时它会直接失败(`no such ref was fetched`)——开发分支就是这种状态;而且 `link:` 安装加载的是构建产物 `lib/`,本地副本可能改过 `src/` 却没重建,只拉代码不重建,重启后跑的还是旧代码。
113
+ - 停在功能分支上时,更新前要先切回默认分支:信息栏加载的就是这份副本,快进一个功能分支不会改变任何东西。
114
+ - 三条命令都是 `--ff-only` / 非破坏性的:工作区不干净或分支上有本地提交时,git 会拒绝执行,而不是覆盖你的改动。
115
+
116
+ 本地 symlink 安装**不会**被 NPM 更新命令替换;想迁移到 NPM,先移除旧安装,再执行 NPM 安装命令。
117
+
118
+ > 无论哪种方式,**更新后都必须重启 `dsh web`** 才会生效。红色提醒标签消失得更早:悬浮提示里的版本号读的是磁盘上已安装的那一份,刷新页面就会更新。
119
+
120
+ ## 看到红色版本提醒怎么办
121
+
122
+ 信息栏出现红色的「新版本提醒」标签时,**直接点击该标签**即可把更新命令复制到剪贴板;粘到终端执行,然后重启 `dsh web`。
123
+
124
+ - 标签悬浮提示里同时给出了另一条路径:把更新交给有本机终端权限的 Agent 处理。
125
+ - 复制出来的命令与你的安装方式匹配(npm 装 → `dsh plugin … add …@latest`;GitHub 地址装 → 同一条地址重装;`link:` 装 → `git fetch` + 切默认分支 + `--ff-only` 快进 + 重建 `lib`),所以**不存在"用错命令把本地代码顶掉"的风险**。
126
+ - **插件不会自动更新自己**:它只负责提示,并准备好命令;机器上的一切改动都由你运行命令后才发生。
127
+
128
+ 如果想让 Agent 代劳,可以把下面这句话发给它:
129
+
130
+ > 我的 DSH 底部信息栏提示有新版本,请帮我安全更新到新版。请先判断当前是 NPM 安装还是本地 Git 安装;不要删除 `~/.dsh/dsh-bottom-info-bar/usage-records.json`,不要覆盖未提交代码,更新后提醒我重启 `dsh web`。
131
+
132
+ 普通网页聊天如果没有本机终端权限,不能直接完成更新;Agent 仍应在执行删除、覆盖或迁移安装前先征得用户确认。
133
+
134
+ ## 卸载
135
+
136
+ ```bash
137
+ cd dsh-bottom-info-bar
138
+ ./uninstall.sh
139
+ # 或手动:
140
+ dsh plugin --profile web remove dsh-bottom-info-bar
141
+ ```
142
+
143
+ 重启后原生统计栏自动恢复(插件 unload 时槽位自动退位,这是 DSH 插槽特性)。插件代码无残留;记账数据会保留,不会被卸载操作删除。如需清空统计,请在「插件页(插件 → bottom-info-bar)→ 账单数据」中先导出,再确认清除。
144
+
145
+ ## 故障排查
146
+
147
+ | 现象 | 原因与处理 |
148
+ |---|---|
149
+ | 信息栏不出现 | ① 没重启:需重启 `dsh web`;② 装错 profile:确认启动用的 profile 与安装目标一致;③ `dsh --profile web --dump-config` 里没有 dsh-bottom-info-bar:重新执行安装 |
150
+ | 安装报 `pnpm not found` | 安装 pnpm:`npm i -g pnpm` 或 `corepack enable` |
151
+ | 安装报 `dsh-bottom-info-bar` 找不到 | 确认包名拼写;本地目录安装时路径要指向**仓库根**(包在仓库根,不是子目录) |
152
+ | 插件页报「这个包没有声明组合包」(英文界面:declares no bundle) | 装到的是一个「仓库根不是包」的仓库——那是包移到仓库根之前的本仓库。插件页里改填包名 `dsh-bottom-info-bar`,或用包含该修复的版本上的仓库地址 |
153
+ | 插件页提示「待批准的构建脚本」 | 装的是 1.15.0 及更早的仓库地址(旧清单带 `prepack`)。改用包名安装,或装包含修复的版本 |
154
+ | 余额显示未配置/刷新失败 | 见 README「常见问题」 |
155
+ | 想彻底回到原生状态 | 卸载 + 重启,系统统计栏自动恢复 |
package/lib/client.js CHANGED
@@ -158,6 +158,9 @@ const RPC_TIMEOUT_MS = 20000;
158
158
  // 不必干等后台 60s 自动周期。覆盖窗口需容纳会话模型从空到确定的短暂翻转期。
159
159
  const BOOT_AT = Date.now();
160
160
  const FORCE_REFRESH_WINDOW_MS = 6000;
161
+ // 版本提醒的重读间隔:npm 侧由 host 按 TTL 缓存,这里只是读本地接口,
162
+ // 60 秒一次足够让「本地副本更新完」的提醒自己消失,也不会给宿主添负担。
163
+ const UPDATE_INFO_REFRESH_MS = 60000;
161
164
 
162
165
  // 复制文本到剪贴板。
163
166
  // 优先用异步剪贴板 API——它只在「安全上下文」可用:本 GUI 走 http://127.0.0.1 属安全上下文,
@@ -2723,13 +2726,30 @@ module.exports = {
2723
2726
  return function () { document.removeEventListener(BIB_LEDGER_EVENT, onLedgerChanged); };
2724
2727
  }, [load, activeSessionModel]);
2725
2728
 
2726
- // 版本检查由 host 在进程启动时完成;这里仅读取一次缓存,不轮询 NPM。
2729
+ // 版本信息:打开页面读一次,之后每 60 秒、以及页面重新可见/窗口重新获得焦点时重读。
2730
+ // 这样本地副本更新完(git pull + 重建,或 dsh plugin add …@latest)之后,红色提醒会自己消失,
2731
+ // 不必等用户刷新页面或重启 dsh web —— 2026-09-24 用户报的「更新完还显示提醒」就出在这里。
2732
+ // 每次读的都是本地 host 接口,NPM 只在 host 侧按 TTL 重查,这里不轮询 NPM。
2727
2733
  React.useEffect(function () {
2728
2734
  let active = true;
2729
- rpc('getUpdateInfo').then(function (info) {
2730
- if (active && info && typeof info.current === 'string') setUpdateInfo(info);
2731
- }).catch(function () { /* 版本检查失败静默,不影响信息栏 */ });
2732
- return function () { active = false; };
2735
+ const readUpdateInfo = function () {
2736
+ rpc('getUpdateInfo').then(function (info) {
2737
+ if (active && info && typeof info.current === 'string') setUpdateInfo(info);
2738
+ }).catch(function () { /* 版本检查失败静默,不影响信息栏 */ });
2739
+ };
2740
+ readUpdateInfo();
2741
+ const timer = window.setInterval(readUpdateInfo, UPDATE_INFO_REFRESH_MS);
2742
+ const onVisible = function () {
2743
+ if (document.visibilityState === 'visible') readUpdateInfo();
2744
+ };
2745
+ document.addEventListener('visibilitychange', onVisible);
2746
+ window.addEventListener('focus', onVisible);
2747
+ return function () {
2748
+ active = false;
2749
+ window.clearInterval(timer);
2750
+ document.removeEventListener('visibilitychange', onVisible);
2751
+ window.removeEventListener('focus', onVisible);
2752
+ };
2733
2753
  }, []);
2734
2754
 
2735
2755
  // 会话统计变化(回复中 turns/steps/tokens 增长,回复完成时停止)→ 防抖后即时刷新花费,
package/lib/index.js CHANGED
@@ -175,8 +175,8 @@ function defaultBranchOf(layout, remote) {
175
175
  return null
176
176
  }
177
177
 
178
- // link: 安装的 git 同步命令:目标常是仓库内的子目录(…/dsh-bottom-info-bar/plugin),
179
- // 一律以探测到的仓库根为准;路径可能含空格,必须做 shell 转义。
178
+ // link: 安装的 git 同步命令:目标就是仓库根(包在仓库根,见 README「安装」),
179
+ // 但仍以探测到的仓库根为准——历史安装可能指向仓库内的子目录;路径可能含空格,必须做 shell 转义。
180
180
  function linkGitSyncCommandFor(target) {
181
181
  const fallback = 'git -C ' + shellQuote(target) + ' pull --ff-only'
182
182
  const layout = findGitLayout(target)
@@ -194,8 +194,9 @@ function linkGitSyncCommandFor(target) {
194
194
  }
195
195
 
196
196
  // link: 的完整更新命令 = git 同步 + 重建 lib。
197
- // plugin/package.json 的 main 指向 lib/index.js,而 lib/ 是构建产物、不入 git;只拉代码不重建,
198
- // 用户重启后加载的仍是旧 lib —— 等于「更新了但没生效」(install.sh 里同样是先 build 再 add)。
197
+ // package.json 的 main 指向 lib/index.js;lib/ 已入库(保证直接装 GitHub 地址就能用),
198
+ // 但本地副本可能改过 src 没重建,重建一次才保证「拉到的代码」与「加载的产物」一致
199
+ // (install.sh 里同样是先 build 再 add)。
199
200
  function linkUpdateCommandFor(target) {
200
201
  const gitCommand = linkGitSyncCommandFor(target)
201
202
  const buildScript = join(target, 'scripts', 'build.mjs')
@@ -212,6 +213,15 @@ function shellQuote(value) {
212
213
  return "'" + text.replace(/'/g, "'\\''") + "'"
213
214
  }
214
215
 
216
+ // 非注册表来源的安装(DSH 插件页填 GitHub 地址 / tarball 地址):更新 = 用同一条 add 命令重装。
217
+ // 不能给 npm 那条命令:注册表安装路径把「已安装」当拒绝理由,用户照抄只会得到一句「该插件已安装」。
218
+ const REINSTALL_PREFIXES = ['github:', 'gitlab:', 'bitbucket:', 'gist:', 'git+', 'git@', 'http://', 'https://']
219
+
220
+ function isReinstallSpec(spec) {
221
+ const lower = spec.toLowerCase()
222
+ return REINSTALL_PREFIXES.some((prefix) => lower.startsWith(prefix))
223
+ }
224
+
215
225
  function updateCommandForInstall() {
216
226
  const profile = runningProfileName()
217
227
  const npmCommand = 'dsh plugin --profile ' + profile + ' add dsh-bottom-info-bar@latest'
@@ -219,32 +229,63 @@ function updateCommandForInstall() {
219
229
  const profileFile = join(dshHomeDir(), 'profiles', profile, 'package.json')
220
230
  const pkg = JSON.parse(readFileSync(profileFile, 'utf8'))
221
231
  const spec = pkg && pkg.dependencies && pkg.dependencies['dsh-bottom-info-bar']
222
- if (typeof spec === 'string' && spec.startsWith('link:')) {
223
- const target = spec.slice('link:'.length).trim()
224
- if (target.length > 0) return { installMode: 'link', updateCommand: linkUpdateCommandFor(target) }
232
+ if (typeof spec === 'string') {
233
+ const trimmed = spec.trim()
234
+ if (trimmed.startsWith('link:')) {
235
+ const target = trimmed.slice('link:'.length).trim()
236
+ if (target.length > 0) return { installMode: 'link', updateCommand: linkUpdateCommandFor(target) }
237
+ }
238
+ if (isReinstallSpec(trimmed)) {
239
+ return { installMode: 'git', updateCommand: 'dsh plugin --profile ' + profile + ' add ' + shellQuote(trimmed) }
240
+ }
225
241
  }
226
242
  } catch (err) { /* 读不到就按 npm 处理 */ }
227
243
  return { installMode: 'npm', updateCommand: npmCommand }
228
244
  }
229
245
 
230
- async function checkLatestVersion() {
231
- const current = packageVersion()
246
+ // npm 上的最新版本:进程内缓存 + TTL 定期重查。
247
+ // 两个「版本」的时间尺度必须分开,否则就会出现「本地更新完了,提醒还挂着」:
248
+ // · latest(npm 发布版)——变化慢,按 TTL 重查(默认 15 分钟),新版本不必等宿主重启才出现;
249
+ // · current(本机已安装版)——每次 RPC 都从磁盘重读,更新完本地副本后提醒会自己消失。
250
+ // 2026-09-24 用户报的正是这里:旧实现只在进程启动时查一次 npm,客户端也只读一次结果,
251
+ // 于是更新完本地副本后,提醒要等刷新页面(甚至重启宿主)才消失。
252
+ const UPDATE_LATEST_TTL_MS = 15 * 60 * 1000
253
+ const UPDATE_LATEST_RETRY_MS = 60 * 1000
254
+
255
+ // TTL 可被测试覆盖(生产不设置):0 表示每次都重查。
256
+ function updateLatestTtlMs() {
257
+ const raw = Number(process.env.DSH_BOTTOM_INFO_BAR_UPDATE_TTL_MS)
258
+ return Number.isFinite(raw) && raw >= 0 ? raw : UPDATE_LATEST_TTL_MS
259
+ }
260
+
261
+ const updateLatestCache = { value: null, expiresAt: 0, pending: null }
262
+
263
+ // 查一次 npm。任何失败都返回 null —— 提醒宁可不显示,也不误报。
264
+ function fetchLatestVersion() {
232
265
  const controller = new AbortController()
233
266
  const timer = setTimeout(() => controller.abort(), UPDATE_CHECK_TIMEOUT_MS)
234
- try {
235
- const response = await fetch(UPDATE_REGISTRY_URL, {
236
- headers: { accept: 'application/json' },
237
- signal: controller.signal,
238
- })
239
- if (!response.ok) return { available: false, current: current, latest: null }
240
- const body = await response.json()
241
- const latest = body && typeof body.version === 'string' ? body.version : null
242
- return { available: !!latest && compareVersions(latest, current) > 0, current: current, latest: latest }
243
- } catch {
244
- return { available: false, current: current, latest: null }
245
- } finally {
246
- clearTimeout(timer)
247
- }
267
+ return fetch(UPDATE_REGISTRY_URL, {
268
+ headers: { accept: 'application/json' },
269
+ signal: controller.signal,
270
+ }).then((response) => (response.ok ? response.json() : null))
271
+ .then((body) => (body && typeof body.version === 'string' ? body.version : null))
272
+ .catch(() => null)
273
+ .finally(() => clearTimeout(timer))
274
+ }
275
+
276
+ // 读最新版本:命中缓存直接返回;过期则重查(并发调用共用同一次请求)。
277
+ // 查失败时保留上一次的已知值(网络抖动不该让提醒闪烁),并把下次重试提前到 1 分钟。
278
+ function latestVersion() {
279
+ const now = Date.now()
280
+ if (now < updateLatestCache.expiresAt) return Promise.resolve(updateLatestCache.value)
281
+ if (updateLatestCache.pending) return updateLatestCache.pending
282
+ const pending = fetchLatestVersion().then((value) => {
283
+ if (value !== null) updateLatestCache.value = value
284
+ updateLatestCache.expiresAt = Date.now() + (value !== null ? updateLatestTtlMs() : UPDATE_LATEST_RETRY_MS)
285
+ return updateLatestCache.value
286
+ }).finally(() => { updateLatestCache.pending = null })
287
+ updateLatestCache.pending = pending
288
+ return pending
248
289
  }
249
290
 
250
291
  // ---------- 双模式(余额制 / 订阅制)配置 ----------
@@ -1654,8 +1695,8 @@ export default {
1654
1695
  const t = hostLocale.createHostTranslator(ctx);
1655
1696
  // 窗口标签:在 apply 内部按当前语言偏好重新计算(模块顶层 WINDOW_LABELS 仅作安全兜底)。
1656
1697
  const windowLabels = { five_hour: t('host.hour'), seven_day: t('ui.weekly'), monthly: t('ui.monthly') };
1657
- // 版本检查只在 host 进程启动时发起一次;客户端后续只读取这个缓存结果。
1658
- const updateInfoPromise = checkLatestVersion()
1698
+ // 预热一次 npm 版本查询(失败不影响信息栏);此后由 latestVersion() 按 TTL 自行重查。
1699
+ latestVersion()
1659
1700
  // 会话谱系列表是冷安全读取,但仍可能触发持久化查询;短暂缓存避免信息栏轮询
1660
1701
  // 每次都重新扫描全部会话。缓存失效时再次读取,保证新建子代理最终能被纳入。
1661
1702
  const SESSION_LINEAGE_CACHE_MS = 1000
@@ -4500,14 +4541,16 @@ export default {
4500
4541
  const ROUTE_PREFIX = '/_dsh/dsh-bottom-info-bar';
4501
4542
  const ROUTES = {
4502
4543
  getUpdateInfo: async function () {
4503
- // npm 的最新版本只在进程启动时查一次;但「已安装版本」每次都从磁盘重读——
4504
- // 用户更新完本地副本后刷新页面就能看到新版本、提醒随之消失,不必等宿主重启。
4544
+ // latest 按 TTL 重查、current 每次从磁盘重读(见 latestVersion):
4545
+ // 客户端每 60 秒读一次这个接口,所以本地副本更新完后提醒会自己消失,
4546
+ // 不必刷新页面或重启宿主。
4505
4547
  // 命令按安装形态区分,避免把 link: 安装的用户引导到 npm 命令而丢掉本地代码。
4506
- const info = await updateInfoPromise
4548
+ const latest = await latestVersion()
4507
4549
  const current = packageVersion()
4508
- return Object.assign({}, info, updateCommandForInstall(), {
4550
+ return Object.assign({}, updateCommandForInstall(), {
4509
4551
  current: current,
4510
- available: !!info.latest && compareVersions(info.latest, current) > 0,
4552
+ latest: latest,
4553
+ available: !!latest && compareVersions(latest, current) > 0,
4511
4554
  })
4512
4555
  },
4513
4556
  getBalanceSnapshot: async function (args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bottom-info-bar",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
4
4
  "description": "Shows the current model, balance and spend under the composer.",
5
5
  "keywords": [
6
6
  "deepseek",
@@ -35,12 +35,14 @@
35
35
  "cordis.patch.yml",
36
36
  "locale/*.json",
37
37
  "README.md",
38
+ "README.zh-CN.md",
39
+ "docs/INSTALL.md",
38
40
  "LICENSE"
39
41
  ],
40
42
  "scripts": {
41
43
  "build": "node scripts/build.mjs",
42
- "prepack": "npm run build",
43
- "test": "node ../tests/run-all.mjs"
44
+ "prepublishOnly": "npm run build",
45
+ "test": "node tests/run-all.mjs"
44
46
  },
45
47
  "dsh": {
46
48
  "bundle": {