dsh-balance-plugin 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 +21 -0
- package/PUBLISHING.md +67 -0
- package/README.md +122 -0
- package/README.zh.md +122 -0
- package/cordis.patch.yml +3 -0
- package/lib/balance-core.js +97 -0
- package/lib/client.js +132 -0
- package/lib/index.js +171 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-balance-plugin contributors
|
|
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/PUBLISHING.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Publishing dsh-balance-plugin
|
|
2
|
+
|
|
3
|
+
Step-by-step release manual for the two official distribution channels (both are supported and can coexist).
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- [npm](https://docs.npmjs.com/cli/) account, logged in: `npm whoami`
|
|
8
|
+
- A GitHub account; the repository will be public.
|
|
9
|
+
|
|
10
|
+
## 1. Create the GitHub repository
|
|
11
|
+
|
|
12
|
+
1. On GitHub, **New repository** → name `dsh-balance-plugin`, public.
|
|
13
|
+
2. **Do not** initialize with README/.gitignore (this repo already has them).
|
|
14
|
+
3. Add the local remote and push:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
cd ~/Documents/dsh-balance-plugin
|
|
18
|
+
git remote add origin git@github.com:<you>/dsh-balance-plugin.git
|
|
19
|
+
git push -u origin main
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
4. On the repository page: **About → Topics → add `dsh-plugin`** (the official discovery topic from DeepSeek Harness's README). Also consider `deepseek-harness`.
|
|
23
|
+
|
|
24
|
+
## 2. Publish to npm
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# from ~/Documents/dsh-balance-plugin
|
|
28
|
+
pnpm run build && pnpm test # gates
|
|
29
|
+
npm version patch # bumps to 0.1.1, creates the git tag
|
|
30
|
+
pnpm pack # inspect the tarball contents
|
|
31
|
+
npm publish # publishes prebuilt lib/ — users install with zero friction
|
|
32
|
+
git push && git push --tags
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
npm is the **recommended** channel: users run `dsh plugin --profile <name> add dsh-balance-plugin` and get prebuilt code with no install-time build or permission prompt.
|
|
36
|
+
|
|
37
|
+
## 3. GitHub installs (optional, automatic once the repo exists)
|
|
38
|
+
|
|
39
|
+
Users can also install directly from GitHub. Two official caveats (from `docs/user/develop/basic/publish.md`):
|
|
40
|
+
|
|
41
|
+
- Git installs fetch **source**, not build output — the package ships a self-contained `prepare` script (`node scripts/build.mjs`, no project references), which pnpm runs after install;
|
|
42
|
+
- pnpm ≥ 10 refuses to run git dependencies' `prepare` until the user opts in via `allowBuilds` in the profile's `pnpm-workspace.yaml`. Document this in your README (already present).
|
|
43
|
+
|
|
44
|
+
## 4. Versioning and release notes
|
|
45
|
+
|
|
46
|
+
- Keep npm version and GitHub tag in sync (both come from `npm version`).
|
|
47
|
+
- Create a GitHub Release per tag with the changelog; reference the npm version.
|
|
48
|
+
- Semantic versioning: `patch` for fixes, `minor` for features, `major` for breaking changes.
|
|
49
|
+
|
|
50
|
+
## 5. Verification checklist (official inspection points)
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
# fresh install from the tarball
|
|
54
|
+
dsh plugin --profile demo add ./dsh-balance-plugin-0.1.0.tgz
|
|
55
|
+
dsh --profile demo --dump-config # expect "# == dsh-balance-plugin" layer
|
|
56
|
+
# then in the web profile with a real credential: the readout renders
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 6. Local development loop
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
pnpm install
|
|
63
|
+
pnpm run build # lib/index.js (host) + lib/client.js (browser) + lib/balance-core.js
|
|
64
|
+
pnpm test # node:test unit tests (no framework)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To test changes live in a profile: `dsh plugin --profile web add ./` (links the local checkout), restart `dsh web`, hard-refresh the browser.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# dsh-balance-plugin
|
|
2
|
+
|
|
3
|
+
DeepSeek wallet balance at the sidebar foot — a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin.
|
|
4
|
+
|
|
5
|
+
English | [中文](README.zh.md)
|
|
6
|
+
|
|
7
|
+
Shows your DeepSeek account balance as a **card icon + amount** in the sidebar footer (beside Settings), tinted by how much is left. Refreshes when a conversation turn ends, on click, and on mount — no polling.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Status-tinted amount**: only the amount carries the color, the icon stays neutral —
|
|
14
|
+
- ≥ `lowBalanceThreshold` (default **2**): green
|
|
15
|
+
- above zero: amber
|
|
16
|
+
- zero or below: red
|
|
17
|
+
- **Refresh on turn end**: the host pushes an SSE `refresh` event when any conversation turn ends — wallet spend settles with the turn.
|
|
18
|
+
- **Click to refresh**; in-flight refreshes are deduped (a click or turn-end tick during a pending query is a no-op).
|
|
19
|
+
- **Silent failure**: unconfigured credentials or a failed fetch render nothing — the readout is advisory and never surfaces an error.
|
|
20
|
+
- **Original currencies**: balances are displayed as the provider reports them; no conversion, no summing.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
Requires a DeepSeek account credential. The plugin resolves `DEEPSEEK_API_KEY` through the harness credential seam (`~/.dsh/.credentials.yaml` or the environment), exactly like the official DeepSeek adapter.
|
|
25
|
+
|
|
26
|
+
### From npm (recommended)
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
dsh plugin --profile <name> add dsh-balance-plugin
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### From GitHub
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
dsh plugin --profile <name> add github:<you>/dsh-balance-plugin#<commit-sha>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Git installs fetch source, so pnpm ≥ 10 refuses the install-time build until you allow it. Copy the exact package key pnpm prints into the profile's `pnpm-workspace.yaml`:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
allowBuilds:
|
|
42
|
+
dsh-balance-plugin: true
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
then re-run the `add` command. Only install packages whose source you trust, and pin the commit.
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
The plugin ships with sensible defaults; override via the bundle's plugin row:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
# profile's cordis.patch.yml (or your home-level one)
|
|
53
|
+
- id: balance
|
|
54
|
+
name: dsh-balance-plugin
|
|
55
|
+
config:
|
|
56
|
+
apiKeyEnv: DEEPSEEK_API_KEY # credential reference, then the environment
|
|
57
|
+
baseURL: https://api.deepseek.com # provider base URL
|
|
58
|
+
timeoutMs: 12000 # per-request timeout; abort maps to balance-timeout
|
|
59
|
+
lowBalanceThreshold: 2 # amount at or above which the tint reads healthy
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## How it works
|
|
63
|
+
|
|
64
|
+
| Half | What it does |
|
|
65
|
+
|---|---|
|
|
66
|
+
| **Host** (Node) | Registers `GET /dsh-balance` (the wallet readout as JSON) and `GET /dsh-balance/events` (an SSE stream). Listens on the harness's `session/event` broadcast and emits a `refresh` SSE event per `turn/end`. Resolves the credential **per request** through `ctx.credentials`, falling back to the environment — a changed key applies without a restart. |
|
|
67
|
+
| **Client** (browser) | Registers the `sidebar.footer.action` slot entry (the shell's reserved seat beside Settings; on the collapsed rail it stacks above the settings icon). Fetches the readout on mount, on `refresh` SSE events, and on click, with an in-flight guard. The card icon is self-contained (no dependency on the shell's icon library). |
|
|
68
|
+
|
|
69
|
+
Position and interaction follow the shell's own design: the `sidebar.footer.action` hole is the official extension point for "optional actions beside Settings".
|
|
70
|
+
|
|
71
|
+
## Frequently asked questions
|
|
72
|
+
|
|
73
|
+
**The balance looks stale right after a conversation ends.** Provider settlement lags the turn by tens of seconds. The turn-end refresh may briefly show the pre-settlement amount; clicking later shows the settled value. This matches the official DeepSeek client behavior — the readout is advisory.
|
|
74
|
+
|
|
75
|
+
**Nothing renders.** No credential resolves (`unconfigured`) or the fetch failed — by design. Check `dsh --profile <name> --dump-config` for the plugin row, and that `DEEPSEEK_API_KEY` resolves through the credential seam or the environment.
|
|
76
|
+
|
|
77
|
+
**How do I remove it?**
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
dsh plugin --profile <name> remove dsh-balance-plugin
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This removes the dependency and the bundle layer together.
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
src/
|
|
89
|
+
balance-core.ts # pure query logic (credential resolution, HTTP, normalization)
|
|
90
|
+
index.ts # host plugin: routes + SSE + turn-end listener
|
|
91
|
+
client.tsx # browser plugin: the sidebar-foot action
|
|
92
|
+
scripts/build.mjs # esbuild: host ESM + browser __ModuleLoader__ bundle
|
|
93
|
+
tests/ # node:test unit tests for balance-core
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
pnpm install
|
|
98
|
+
pnpm run build # emits lib/index.js + lib/client.js
|
|
99
|
+
pnpm test # balance-core unit tests
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Local install check (official verification points):
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
dsh plugin --profile demo add ./
|
|
106
|
+
dsh --profile demo --dump-config # expect a "# == dsh-balance-plugin" layer
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Release checklist
|
|
110
|
+
|
|
111
|
+
Before publishing a new version:
|
|
112
|
+
|
|
113
|
+
1. `pnpm run build` and `pnpm test` pass.
|
|
114
|
+
2. Local install check above passes (`--dump-config` shows the layer; the web profile renders the readout with a real credential).
|
|
115
|
+
3. Version bumped (`npm version patch/minor/major`); git tag matches.
|
|
116
|
+
4. `pnpm pack` — inspect the tarball: it must contain `lib/`, `cordis.patch.yml`, `README.md` (and nothing else heavy).
|
|
117
|
+
5. `npm publish` — publishes prebuilt code, so users install with zero friction.
|
|
118
|
+
6. Push the tag; keep the GitHub release notes in sync with the npm version.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
[MIT](LICENSE)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# dsh-balance-plugin
|
|
2
|
+
|
|
3
|
+
侧边栏底部的 DeepSeek 钱包余额——一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件。
|
|
4
|
+
|
|
5
|
+
[English](README.md) | 中文
|
|
6
|
+
|
|
7
|
+
在侧边栏底部(设置按钮旁)以**信用卡图标 + 金额**显示你的 DeepSeek 账户余额,并按剩余额度着色。在对话结束时、点击时与挂载时刷新——无轮询。
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## 功能
|
|
12
|
+
|
|
13
|
+
- **状态着色**:只有金额带颜色,图标保持中性——
|
|
14
|
+
- ≥ `lowBalanceThreshold`(默认 **2**):绿色
|
|
15
|
+
- 大于零:黄色
|
|
16
|
+
- 零或以下:红色
|
|
17
|
+
- **对话结束自动刷新**:宿主在任何对话轮次结束时推送 SSE `refresh` 事件——消耗随轮次结算。
|
|
18
|
+
- **点击刷新**;在途刷新去重(查询进行中时点击或轮次结束触发均为 no-op)。
|
|
19
|
+
- **失败静默**:凭证未配置或抓取失败时不渲染任何内容——读数仅供参考,绝不显示错误。
|
|
20
|
+
- **原始币种**:余额按提供方报告显示;不换算、不加总。
|
|
21
|
+
|
|
22
|
+
## 安装
|
|
23
|
+
|
|
24
|
+
需要一个 DeepSeek 账户凭证。插件通过 harness 凭证接缝(`~/.dsh/.credentials.yaml` 或环境变量)解析 `DEEPSEEK_API_KEY`,与官方 DeepSeek 适配器一致。
|
|
25
|
+
|
|
26
|
+
### 从 npm 安装(推荐)
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
dsh plugin --profile <名字> add dsh-balance-plugin
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 从 GitHub 安装
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
dsh plugin --profile <名字> add github:<你>/dsh-balance-plugin#<commit-sha>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Git 安装取到的是源码,pnpm ≥ 10 会拒绝安装时的构建,直到你允许:把 pnpm 打印的包键复制进 profile 的 `pnpm-workspace.yaml`:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
allowBuilds:
|
|
42
|
+
dsh-balance-plugin: true
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
然后重跑 `add`。只安装你信任源码的包,并钉住 commit。
|
|
46
|
+
|
|
47
|
+
## 配置
|
|
48
|
+
|
|
49
|
+
插件带合理默认值;可通过 bundle 的插件行覆盖:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
# profile 的 cordis.patch.yml(或 home 级)
|
|
53
|
+
- id: balance
|
|
54
|
+
name: dsh-balance-plugin
|
|
55
|
+
config:
|
|
56
|
+
apiKeyEnv: DEEPSEEK_API_KEY # 凭证引用,随后回退环境变量
|
|
57
|
+
baseURL: https://api.deepseek.com # 提供方基础 URL
|
|
58
|
+
timeoutMs: 12000 # 每次请求超时;中止映射为 balance-timeout
|
|
59
|
+
lowBalanceThreshold: 2 # 读数视为健康的最低金额
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 工作原理
|
|
63
|
+
|
|
64
|
+
| 半面 | 做什么 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| **宿主**(Node) | 注册 `GET /dsh-balance`(钱包读数 JSON)与 `GET /dsh-balance/events`(SSE 流)。监听 harness 的 `session/event` 广播,每个 `turn/end` 推送 `refresh` 事件。**每次请求**经 `ctx.credentials` 解析凭证并回退环境变量——改 key 无需重启。 |
|
|
67
|
+
| **客户端**(浏览器) | 注册 `sidebar.footer.action` 插槽条目(shell 预留的设置旁座位;收起 rail 时堆叠在设置图标上方)。在挂载时、`refresh` SSE 事件与点击时抓取读数,带 in-flight 守卫。信用卡图标自包含(不依赖 shell 的图标库)。 |
|
|
68
|
+
|
|
69
|
+
位置与交互遵循 shell 自身设计:`sidebar.footer.action` 是"设置旁可选操作"的官方扩展点。
|
|
70
|
+
|
|
71
|
+
## 常见问题
|
|
72
|
+
|
|
73
|
+
**对话刚结束余额看起来是旧的。** 提供方结算滞后于轮次数十秒。轮次结束的刷新可能短暂显示结算前金额;稍后点击显示结算后数值。这与官方 DeepSeek 客户端行为一致——读数仅供参考。
|
|
74
|
+
|
|
75
|
+
**什么都不显示。** 凭证未解析(`unconfigured`)或抓取失败——这是设计。用 `dsh --profile <名字> --dump-config` 检查插件行,确认 `DEEPSEEK_API_KEY` 能通过凭证接缝或环境变量解析。
|
|
76
|
+
|
|
77
|
+
**如何卸载?**
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
dsh plugin --profile <名字> remove dsh-balance-plugin
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
该命令同时移除依赖与层。
|
|
84
|
+
|
|
85
|
+
## 开发
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
src/
|
|
89
|
+
balance-core.ts # 纯查询逻辑(凭证解析、HTTP、归一化)
|
|
90
|
+
index.ts # 宿主插件:路由 + SSE + 轮次结束监听
|
|
91
|
+
client.tsx # 浏览器插件:侧边栏底部操作
|
|
92
|
+
scripts/build.mjs # esbuild:宿主 ESM + 浏览器 __ModuleLoader__ bundle
|
|
93
|
+
tests/ # balance-core 的 node:test 单元测试
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
pnpm install
|
|
98
|
+
pnpm run build # 生成 lib/index.js + lib/client.js
|
|
99
|
+
pnpm test # balance-core 单元测试
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
本地安装检查(官方检验点):
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
dsh plugin --profile demo add ./
|
|
106
|
+
dsh --profile demo --dump-config # 应出现 "# == dsh-balance-plugin" 层
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 发布检查清单
|
|
110
|
+
|
|
111
|
+
发布新版本前:
|
|
112
|
+
|
|
113
|
+
1. `pnpm run build` 与 `pnpm test` 通过。
|
|
114
|
+
2. 上述本地安装检查通过(`--dump-config` 显示层;web profile 用真实凭证渲染读数)。
|
|
115
|
+
3. 版本已提升(`npm version patch/minor/major`);git tag 对应。
|
|
116
|
+
4. `pnpm pack`——检查 tarball:必须包含 `lib/`、`cordis.patch.yml`、`README.md`(别无其他大文件)。
|
|
117
|
+
5. `npm publish`——发布预构建代码,用户零摩擦安装。
|
|
118
|
+
6. 推送 tag;GitHub release 说明与 npm 版本保持一致。
|
|
119
|
+
|
|
120
|
+
## 许可
|
|
121
|
+
|
|
122
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// src/balance-core.ts
|
|
2
|
+
function symbol(currency) {
|
|
3
|
+
switch (currency.toUpperCase()) {
|
|
4
|
+
case "CNY":
|
|
5
|
+
case "RMB":
|
|
6
|
+
return "\xA5";
|
|
7
|
+
case "USD":
|
|
8
|
+
return "$";
|
|
9
|
+
default:
|
|
10
|
+
return currency === "" ? "" : `${currency} `;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function normalizeCurrency(currency) {
|
|
14
|
+
switch (currency.toUpperCase().trim()) {
|
|
15
|
+
case "CNY":
|
|
16
|
+
case "RMB":
|
|
17
|
+
case "CNH":
|
|
18
|
+
case "\xA5":
|
|
19
|
+
case "\uFFE5":
|
|
20
|
+
return "CNY";
|
|
21
|
+
case "USD":
|
|
22
|
+
case "$":
|
|
23
|
+
return "USD";
|
|
24
|
+
default:
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function primaryInfo(infos) {
|
|
29
|
+
if (infos.length === 0) return void 0;
|
|
30
|
+
return infos.find((info) => normalizeCurrency(info.currency) === "CNY") ?? infos[0];
|
|
31
|
+
}
|
|
32
|
+
function displayFor(infos) {
|
|
33
|
+
const pick = primaryInfo(infos);
|
|
34
|
+
if (pick === void 0) return "";
|
|
35
|
+
return `${symbol(pick.currency)}${pick.total.trim()}`;
|
|
36
|
+
}
|
|
37
|
+
function primaryTotal(infos) {
|
|
38
|
+
const pick = primaryInfo(infos);
|
|
39
|
+
if (pick === void 0) return 0;
|
|
40
|
+
const parsed = Number.parseFloat(pick.total);
|
|
41
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
42
|
+
}
|
|
43
|
+
async function fetchBalance(credentials, env, config) {
|
|
44
|
+
const resolved = await credentials?.resolve(config.apiKeyEnv);
|
|
45
|
+
const apiKey = resolved?.value ?? env[config.apiKeyEnv];
|
|
46
|
+
if (!apiKey) {
|
|
47
|
+
return { status: "unconfigured" };
|
|
48
|
+
}
|
|
49
|
+
const timeout = AbortSignal.timeout(config.timeoutMs);
|
|
50
|
+
const url = `${config.baseURL.replace(/\/+$/, "")}/user/balance`;
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
headers: {
|
|
54
|
+
accept: "application/json",
|
|
55
|
+
authorization: `Bearer ${apiKey}`
|
|
56
|
+
},
|
|
57
|
+
signal: timeout
|
|
58
|
+
});
|
|
59
|
+
const body = await response.text();
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
return { status: "unavailable", reason: `balance-http-${String(response.status)}` };
|
|
62
|
+
}
|
|
63
|
+
const parsed = JSON.parse(body);
|
|
64
|
+
const infos = (parsed.balance_infos ?? []).map((info) => ({
|
|
65
|
+
currency: info.currency,
|
|
66
|
+
total: info.total_balance,
|
|
67
|
+
granted: info.granted_balance,
|
|
68
|
+
toppedUp: info.topped_up_balance
|
|
69
|
+
}));
|
|
70
|
+
if (infos.length === 0) {
|
|
71
|
+
return { status: "unavailable", reason: "balance-empty" };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
status: "ok",
|
|
75
|
+
available: parsed.is_available,
|
|
76
|
+
display: displayFor(infos),
|
|
77
|
+
total: primaryTotal(infos),
|
|
78
|
+
threshold: config.lowBalanceThreshold,
|
|
79
|
+
currencies: [...new Set(infos.map((info) => {
|
|
80
|
+
const normalized = normalizeCurrency(info.currency);
|
|
81
|
+
return normalized === "" ? info.currency.toUpperCase() : normalized;
|
|
82
|
+
}))].sort(),
|
|
83
|
+
infos
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
87
|
+
return { status: "unavailable", reason: "balance-timeout" };
|
|
88
|
+
}
|
|
89
|
+
if (error instanceof SyntaxError) {
|
|
90
|
+
return { status: "unavailable", reason: "balance-decode" };
|
|
91
|
+
}
|
|
92
|
+
return { status: "unavailable", reason: "balance-network" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
fetchBalance
|
|
97
|
+
};
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "dsh-balance-plugin", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name2 in all)
|
|
9
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/client.tsx
|
|
22
|
+
var client_exports = {};
|
|
23
|
+
__export(client_exports, {
|
|
24
|
+
BalanceAction: () => BalanceAction,
|
|
25
|
+
apply: () => apply,
|
|
26
|
+
inject: () => inject,
|
|
27
|
+
name: () => name
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(client_exports);
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
var name = "dsh-balance-plugin";
|
|
32
|
+
var inject = ["slots"];
|
|
33
|
+
var BALANCE_ENDPOINT = "/dsh-balance";
|
|
34
|
+
var EVENTS_ENDPOINT = "/dsh-balance/events";
|
|
35
|
+
function CardIcon({ size }) {
|
|
36
|
+
return (0, import_react.createElement)(
|
|
37
|
+
"svg",
|
|
38
|
+
{
|
|
39
|
+
width: size,
|
|
40
|
+
height: size,
|
|
41
|
+
viewBox: "0 0 16 16",
|
|
42
|
+
fill: "none",
|
|
43
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
44
|
+
style: { flex: "none" }
|
|
45
|
+
},
|
|
46
|
+
(0, import_react.createElement)("rect", { x: 1.3, y: 3.2, width: 13.4, height: 9.6, rx: 1.8, stroke: "currentColor", strokeWidth: 1.2 }),
|
|
47
|
+
(0, import_react.createElement)("path", { d: "M1.3 6.2 H14.7", stroke: "currentColor", strokeWidth: 1.1 }),
|
|
48
|
+
(0, import_react.createElement)("path", { d: "M3.8 10.7 H8", stroke: "currentColor", strokeWidth: 1, strokeLinecap: "round" }),
|
|
49
|
+
(0, import_react.createElement)("path", { d: "M9.2 10.7 H12.2", stroke: "currentColor", strokeWidth: 1, strokeLinecap: "round" })
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
var styles = {
|
|
53
|
+
button: {
|
|
54
|
+
flex: "none",
|
|
55
|
+
display: "inline-flex",
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
justifyContent: "center",
|
|
58
|
+
gap: 6,
|
|
59
|
+
minWidth: 28,
|
|
60
|
+
height: 28,
|
|
61
|
+
border: "none",
|
|
62
|
+
borderRadius: 999,
|
|
63
|
+
padding: "0 6px",
|
|
64
|
+
background: "transparent",
|
|
65
|
+
cursor: "pointer",
|
|
66
|
+
color: "var(--dsw-alias-label-secondary, #6b7280)"
|
|
67
|
+
},
|
|
68
|
+
amount: {
|
|
69
|
+
overflow: "hidden",
|
|
70
|
+
whiteSpace: "nowrap",
|
|
71
|
+
textOverflow: "ellipsis",
|
|
72
|
+
fontSize: 17,
|
|
73
|
+
fontWeight: 700,
|
|
74
|
+
fontVariantNumeric: "tabular-nums",
|
|
75
|
+
lineHeight: 1
|
|
76
|
+
},
|
|
77
|
+
success: { color: "var(--dsw-alias-state-success-primary, #16a34a)" },
|
|
78
|
+
warning: { color: "var(--dsw-static-amber-600, #d97706)" },
|
|
79
|
+
danger: { color: "var(--dsw-alias-state-error-primary, #dc2626)" }
|
|
80
|
+
};
|
|
81
|
+
function toneOf(readout) {
|
|
82
|
+
if (readout.total >= readout.threshold) return "success";
|
|
83
|
+
if (readout.total > 0) return "warning";
|
|
84
|
+
return "danger";
|
|
85
|
+
}
|
|
86
|
+
function BalanceAction({ wide }) {
|
|
87
|
+
const [readout, setReadout] = (0, import_react.useState)(void 0);
|
|
88
|
+
const [turnEpoch, setTurnEpoch] = (0, import_react.useState)(0);
|
|
89
|
+
const inFlight = (0, import_react.useRef)(false);
|
|
90
|
+
const refresh = (0, import_react.useCallback)(() => {
|
|
91
|
+
if (inFlight.current) return;
|
|
92
|
+
inFlight.current = true;
|
|
93
|
+
fetch(BALANCE_ENDPOINT, { cache: "no-store" }).then((response) => response.json()).then(setReadout).catch(() => setReadout(void 0)).finally(() => {
|
|
94
|
+
inFlight.current = false;
|
|
95
|
+
});
|
|
96
|
+
}, []);
|
|
97
|
+
(0, import_react.useEffect)(() => {
|
|
98
|
+
refresh();
|
|
99
|
+
}, [refresh, turnEpoch]);
|
|
100
|
+
(0, import_react.useEffect)(() => {
|
|
101
|
+
const source = new EventSource(EVENTS_ENDPOINT);
|
|
102
|
+
source.addEventListener("refresh", () => {
|
|
103
|
+
setTurnEpoch((epoch) => epoch + 1);
|
|
104
|
+
});
|
|
105
|
+
return () => {
|
|
106
|
+
source.close();
|
|
107
|
+
};
|
|
108
|
+
}, []);
|
|
109
|
+
if (readout === void 0 || readout.status !== "ok") return null;
|
|
110
|
+
const tone = toneOf(readout);
|
|
111
|
+
const label = `\u4F59\u989D ${readout.display}`;
|
|
112
|
+
return (0, import_react.createElement)(
|
|
113
|
+
"button",
|
|
114
|
+
{
|
|
115
|
+
type: "button",
|
|
116
|
+
style: styles.button,
|
|
117
|
+
"aria-label": "\u5237\u65B0\u4F59\u989D",
|
|
118
|
+
title: wide ? label : void 0,
|
|
119
|
+
onClick: refresh
|
|
120
|
+
},
|
|
121
|
+
(0, import_react.createElement)(CardIcon, { size: wide ? 16 : 18 }),
|
|
122
|
+
wide ? (0, import_react.createElement)("span", { style: { ...styles.amount, ...styles[tone] } }, readout.display) : null
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
function apply(ctx) {
|
|
126
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
127
|
+
name: "sidebar.footer.action",
|
|
128
|
+
id: "balance",
|
|
129
|
+
order: 0
|
|
130
|
+
}, BalanceAction));
|
|
131
|
+
}
|
|
132
|
+
return module.exports; } });
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
3
|
+
|
|
4
|
+
// src/balance-core.ts
|
|
5
|
+
function symbol(currency) {
|
|
6
|
+
switch (currency.toUpperCase()) {
|
|
7
|
+
case "CNY":
|
|
8
|
+
case "RMB":
|
|
9
|
+
return "\xA5";
|
|
10
|
+
case "USD":
|
|
11
|
+
return "$";
|
|
12
|
+
default:
|
|
13
|
+
return currency === "" ? "" : `${currency} `;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function normalizeCurrency(currency) {
|
|
17
|
+
switch (currency.toUpperCase().trim()) {
|
|
18
|
+
case "CNY":
|
|
19
|
+
case "RMB":
|
|
20
|
+
case "CNH":
|
|
21
|
+
case "\xA5":
|
|
22
|
+
case "\uFFE5":
|
|
23
|
+
return "CNY";
|
|
24
|
+
case "USD":
|
|
25
|
+
case "$":
|
|
26
|
+
return "USD";
|
|
27
|
+
default:
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function primaryInfo(infos) {
|
|
32
|
+
if (infos.length === 0) return void 0;
|
|
33
|
+
return infos.find((info) => normalizeCurrency(info.currency) === "CNY") ?? infos[0];
|
|
34
|
+
}
|
|
35
|
+
function displayFor(infos) {
|
|
36
|
+
const pick = primaryInfo(infos);
|
|
37
|
+
if (pick === void 0) return "";
|
|
38
|
+
return `${symbol(pick.currency)}${pick.total.trim()}`;
|
|
39
|
+
}
|
|
40
|
+
function primaryTotal(infos) {
|
|
41
|
+
const pick = primaryInfo(infos);
|
|
42
|
+
if (pick === void 0) return 0;
|
|
43
|
+
const parsed = Number.parseFloat(pick.total);
|
|
44
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
45
|
+
}
|
|
46
|
+
async function fetchBalance(credentials, env, config) {
|
|
47
|
+
const resolved = await credentials?.resolve(config.apiKeyEnv);
|
|
48
|
+
const apiKey = resolved?.value ?? env[config.apiKeyEnv];
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
return { status: "unconfigured" };
|
|
51
|
+
}
|
|
52
|
+
const timeout = AbortSignal.timeout(config.timeoutMs);
|
|
53
|
+
const url = `${config.baseURL.replace(/\/+$/, "")}/user/balance`;
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
headers: {
|
|
57
|
+
accept: "application/json",
|
|
58
|
+
authorization: `Bearer ${apiKey}`
|
|
59
|
+
},
|
|
60
|
+
signal: timeout
|
|
61
|
+
});
|
|
62
|
+
const body = await response.text();
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
return { status: "unavailable", reason: `balance-http-${String(response.status)}` };
|
|
65
|
+
}
|
|
66
|
+
const parsed = JSON.parse(body);
|
|
67
|
+
const infos = (parsed.balance_infos ?? []).map((info) => ({
|
|
68
|
+
currency: info.currency,
|
|
69
|
+
total: info.total_balance,
|
|
70
|
+
granted: info.granted_balance,
|
|
71
|
+
toppedUp: info.topped_up_balance
|
|
72
|
+
}));
|
|
73
|
+
if (infos.length === 0) {
|
|
74
|
+
return { status: "unavailable", reason: "balance-empty" };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
status: "ok",
|
|
78
|
+
available: parsed.is_available,
|
|
79
|
+
display: displayFor(infos),
|
|
80
|
+
total: primaryTotal(infos),
|
|
81
|
+
threshold: config.lowBalanceThreshold,
|
|
82
|
+
currencies: [...new Set(infos.map((info) => {
|
|
83
|
+
const normalized = normalizeCurrency(info.currency);
|
|
84
|
+
return normalized === "" ? info.currency.toUpperCase() : normalized;
|
|
85
|
+
}))].sort(),
|
|
86
|
+
infos
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
90
|
+
return { status: "unavailable", reason: "balance-timeout" };
|
|
91
|
+
}
|
|
92
|
+
if (error instanceof SyntaxError) {
|
|
93
|
+
return { status: "unavailable", reason: "balance-decode" };
|
|
94
|
+
}
|
|
95
|
+
return { status: "unavailable", reason: "balance-network" };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/index.ts
|
|
100
|
+
var name = "dsh-balance-plugin";
|
|
101
|
+
var inject = ["webServer"];
|
|
102
|
+
var Config = Schema.object({
|
|
103
|
+
apiKeyEnv: Schema.string().default("DEEPSEEK_API_KEY"),
|
|
104
|
+
baseURL: Schema.string().default("https://api.deepseek.com"),
|
|
105
|
+
timeoutMs: Schema.natural().min(1).max(12e4).default(12e3),
|
|
106
|
+
lowBalanceThreshold: Schema.number().min(0).default(2)
|
|
107
|
+
});
|
|
108
|
+
function apply(ctx, config) {
|
|
109
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
110
|
+
const broadcastRefresh = () => {
|
|
111
|
+
for (const res of sseClients) {
|
|
112
|
+
try {
|
|
113
|
+
res.write("event: refresh\ndata: {}\n\n");
|
|
114
|
+
} catch {
|
|
115
|
+
sseClients.delete(res);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const unregisterBalance = ctx.webServer.register({
|
|
120
|
+
kind: "exact",
|
|
121
|
+
path: "/dsh-balance",
|
|
122
|
+
handler: async (_req, res) => {
|
|
123
|
+
const credentials = ctx.get("credentials");
|
|
124
|
+
const resolver = credentials === void 0 ? void 0 : { resolve: (ref) => credentials.resolve(ref) };
|
|
125
|
+
const readout = await fetchBalance(resolver, process.env, config);
|
|
126
|
+
const body = JSON.stringify(readout);
|
|
127
|
+
res.writeHead(200, {
|
|
128
|
+
"content-type": "application/json",
|
|
129
|
+
"cache-control": "no-store",
|
|
130
|
+
"content-length": Buffer.byteLength(body)
|
|
131
|
+
});
|
|
132
|
+
res.end(body);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const unregisterEvents = ctx.webServer.register({
|
|
136
|
+
kind: "exact",
|
|
137
|
+
path: "/dsh-balance/events",
|
|
138
|
+
handler: (req, res) => {
|
|
139
|
+
res.writeHead(200, {
|
|
140
|
+
"content-type": "text/event-stream",
|
|
141
|
+
"cache-control": "no-cache",
|
|
142
|
+
connection: "keep-alive"
|
|
143
|
+
});
|
|
144
|
+
res.write(": connected\n\n");
|
|
145
|
+
sseClients.add(res);
|
|
146
|
+
req.on("close", () => {
|
|
147
|
+
sseClients.delete(res);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
ctx.on("session/event", (_session, event) => {
|
|
152
|
+
if (event.type === "turn/end") broadcastRefresh();
|
|
153
|
+
});
|
|
154
|
+
return () => {
|
|
155
|
+
unregisterBalance();
|
|
156
|
+
unregisterEvents();
|
|
157
|
+
for (const res of sseClients) {
|
|
158
|
+
try {
|
|
159
|
+
res.end();
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
sseClients.clear();
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
Config,
|
|
168
|
+
apply,
|
|
169
|
+
inject,
|
|
170
|
+
name
|
|
171
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-balance-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek wallet-balance readout at the sidebar foot: card icon + amount, status-tinted, refreshed on turn end and on click",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"cordis.patch.yml",
|
|
15
|
+
"README.md",
|
|
16
|
+
"README.zh.md",
|
|
17
|
+
"PUBLISHING.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"dsh": {
|
|
21
|
+
"bundle": {
|
|
22
|
+
"patch": "./cordis.patch.yml"
|
|
23
|
+
},
|
|
24
|
+
"client": {
|
|
25
|
+
"platform": "web",
|
|
26
|
+
"inject": [
|
|
27
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
28
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node scripts/build.mjs",
|
|
34
|
+
"prepare": "node scripts/build.mjs",
|
|
35
|
+
"test": "node --test tests/*.test.mjs"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
39
|
+
"@deepseek-ai/dsh-credentials": "0.0.1-rc.5",
|
|
40
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
41
|
+
"react": "^18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
45
|
+
"@deepseek-ai/dsh-credentials": "0.0.1-rc.5",
|
|
46
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
47
|
+
"@types/react": "^18.0.0",
|
|
48
|
+
"esbuild": "^0.25.0",
|
|
49
|
+
"react": "^18.0.0",
|
|
50
|
+
"typescript": "^5.0.0"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/luokai-demo/dsh-plugins.git"
|
|
56
|
+
},
|
|
57
|
+
"keywords": [
|
|
58
|
+
"deepseek",
|
|
59
|
+
"deepseek-harness",
|
|
60
|
+
"dsh-plugin",
|
|
61
|
+
"balance",
|
|
62
|
+
"wallet"
|
|
63
|
+
]
|
|
64
|
+
}
|