@dougen/dsh-deepseek-usage 0.1.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 +99 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +273 -0
- package/lib/index.js +300 -0
- package/lib/pricing.js +53 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# dsh-deepseek-usage
|
|
2
|
+
|
|
3
|
+
一个给 DeepSeek Harness(DSH)**网页界面**用的插件:在**侧边栏底部**常驻显示你的 **DeepSeek 账号用量**卡片 —— 余额、今日花费、今日 Token、今日 API 请求次数与高峰/空闲计费时段指示灯。
|
|
4
|
+
|
|
5
|
+
与动态插件不同,它是**正式的可安装 cordis 插件**:随 profile 组合启动,**刷新页面 / 重启服务后依然存在**。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- 第一行:`余额 ¥110.00`
|
|
10
|
+
- 第二行:`今日 ¥2.94 · 36.9M · 170`(今日花费 / 今日 Token / 今日 API 请求次数,带图标)
|
|
11
|
+
- 第三行:`↑1.5 · 💾0.05 · ↓4.5`(输入 / 缓存命中 / 输出 单价,按当前启用模型与峰谷时段自动切换)+ 右下角时间(刷新时显示旋转 ⟳ 图标,仅作刷新中的状态指示)
|
|
12
|
+
- 右上角:高峰/空闲状态指示灯(9:00–14:00 为高峰,橙色;其余空闲,绿色;悬停提示时段)
|
|
13
|
+
- 每 60 秒自动刷新;点击卡片任意位置也可刷新
|
|
14
|
+
- 折叠 rail 态显示紧凑余额 + 状态灯
|
|
15
|
+
- 跟随应用明暗主题(`--dsw-*` 变量,含兜底值)
|
|
16
|
+
- 错误状态明确(未配置 Key / 网络失败 / 接口报错)
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
需要 DSH CLI 与 [pnpm](https://pnpm.io/installation)。
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
# 1) 打包(把包真实安装进 profile 的 node_modules,保证 ESM 依赖解析正确)
|
|
24
|
+
cd dsh-deepseek-usage
|
|
25
|
+
pnpm pack # 生成 dsh-deepseek-usage-<version>.tgz
|
|
26
|
+
|
|
27
|
+
# 2) 安装
|
|
28
|
+
dsh plugin --profile web add D:\path\to\dsh-deepseek-usage-0.1.2.tgz
|
|
29
|
+
# 或直接从 npm 安装(发布到 npm 后):
|
|
30
|
+
# dsh plugin --profile web add @dougen/dsh-deepseek-usage
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> 注意:不要用目录路径 `dsh plugin add <目录>`——pnpm 会以 link 方式安装,包的真实路径在工作区外,ESM 无法解析到 peer 依赖(`@deepseek-ai/dsh-*`)。务必安装 tarball 或 npm 包(真实拷贝进 profile 的 node_modules)。
|
|
34
|
+
|
|
35
|
+
包声明了 `dsh.bundle`,`dsh plugin` 会自动把它加入 profile 的 bundle 层。之后:
|
|
36
|
+
|
|
37
|
+
1. 重启网页应用:`dsh web`(bundle 层在启动时读取)。
|
|
38
|
+
2. 打开 http://127.0.0.1:3080 。
|
|
39
|
+
3. 侧边栏底部(Cordis 面板按钮下方)即出现用量卡片,刷新页面也不会消失。
|
|
40
|
+
|
|
41
|
+
> 手动方式:把包放进 profile 的 `node_modules`,并在 `~/.dsh/profiles/web/cordis.patch.yml` 追加:
|
|
42
|
+
>
|
|
43
|
+
> ```yaml
|
|
44
|
+
> - insert:
|
|
45
|
+
> - id: deepseek-usage
|
|
46
|
+
> name: dsh-deepseek-usage
|
|
47
|
+
> ```
|
|
48
|
+
|
|
49
|
+
## 数据来源
|
|
50
|
+
|
|
51
|
+
| 数据 | 来源 |
|
|
52
|
+
|---|---|
|
|
53
|
+
| 余额 | DeepSeek 官方 `GET /user/balance`(API Key 经凭证服务 `DEEPSEEK_API_KEY` 解析,不出本机) |
|
|
54
|
+
| 今日花费 | ① 官方(优先):配置 `DEEPSEEK_PLATFORM_TOKEN` 后走平台用量接口;② 估算(兜底):本地会话日志 token × 官方峰谷价格表(估算值以 `≈` 前缀标识) |
|
|
55
|
+
| 今日 Token / API 次数 | 本地会话日志统计(官方 API 无用量查询接口),覆盖全部会话,含重启前历史 |
|
|
56
|
+
|
|
57
|
+
### 可选:官方今日消费(`DEEPSEEK_PLATFORM_TOKEN`)
|
|
58
|
+
|
|
59
|
+
DeepSeek 官方 API 不提供用量查询接口,但平台控制台的用量页有同源接口。登录 https://platform.deepseek.com ,打开 DevTools → Console 执行:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
JSON.parse(localStorage.getItem('userToken')).value
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
把返回值存为凭证(例如追加到 `~/.dsh/.credentials.yaml`):
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
DEEPSEEK_PLATFORM_TOKEN: <令牌>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
配置后卡片显示官方今日消费;令牌过期(或未配置)时自动回退为本地日志估算(带 `≈`)。
|
|
72
|
+
|
|
73
|
+
## 工作原理
|
|
74
|
+
|
|
75
|
+
| 部分 | 文件 | 作用 |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Host 半 | `lib/index.js` | Cordis 插件(`inject: credentials, webServer`),注册 `GET /api/deepseek-usage`;解析余额、平台今日消费、本地日志用量统计(按 `message.source.model` + 事件时刻峰谷单价计价,20s 缓存) |
|
|
78
|
+
| 定价 | `lib/pricing.js` | 官方价格表(2026-08-17 峰谷价),调整价格只改此文件 |
|
|
79
|
+
| 浏览器半 | `lib/client.js` | `dsh.client` web 包,注册 `sidebar.footer.action` 槽位卡片,60s 轮询 |
|
|
80
|
+
| 组合层 | `cordis.patch.yml` | `dsh.bundle` 补丁层,插入加载项 |
|
|
81
|
+
|
|
82
|
+
## 开发
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
# 修改 lib/index.js / lib/client.js / lib/pricing.js 后重新打包安装:
|
|
86
|
+
pnpm pack
|
|
87
|
+
dsh plugin --profile web add <生成的 .tgz>
|
|
88
|
+
# 重启 dsh web,硬刷新页面
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 卸载
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
dsh plugin --profile web remove dsh-deepseek-usage
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// dsh-deepseek-usage — browser half.
|
|
2
|
+
//
|
|
3
|
+
// 侧边栏底部用量卡片(注册进 `sidebar.footer.action` 槽位,独占一行,位于
|
|
4
|
+
// Cordis 面板按钮下方)。轮询宿主路由 `/api/deepseek-usage`(见 lib/index.js)
|
|
5
|
+
// 每 60 秒一次,点击卡片任意位置可立即刷新;时间旁的 ⟳ 图标仅作刷新中的状态显示。
|
|
6
|
+
// 样式仅使用 `--dsw-*` 主题变量(含兜底值),跟随明暗主题。
|
|
7
|
+
window.__ModuleLoader__.load({
|
|
8
|
+
id: "dsh-deepseek-usage",
|
|
9
|
+
factory: (require) => {
|
|
10
|
+
var module = { exports: {} };
|
|
11
|
+
var exports = module.exports;
|
|
12
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
let react = require("react");
|
|
14
|
+
const { useState, useEffect, useCallback, useRef } = react;
|
|
15
|
+
const h = react.createElement;
|
|
16
|
+
|
|
17
|
+
// ---- constants -------------------------------------------------
|
|
18
|
+
const POLL_MS = 60 * 1000;
|
|
19
|
+
const USAGE_PATH = "/api/deepseek-usage";
|
|
20
|
+
|
|
21
|
+
// ---- helpers ---------------------------------------------------
|
|
22
|
+
function money(value, currency) {
|
|
23
|
+
const n = Number(value);
|
|
24
|
+
if (!Number.isFinite(n)) return "—";
|
|
25
|
+
const symbol = currency === "USD" ? "$" : "¥";
|
|
26
|
+
return `${symbol}${n.toFixed(2)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fmtTokens(value) {
|
|
30
|
+
const n = Number(value);
|
|
31
|
+
if (!Number.isFinite(n)) return "—";
|
|
32
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
|
33
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
|
|
34
|
+
return String(n);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmtPrice(value) {
|
|
38
|
+
const n = Number(value);
|
|
39
|
+
if (!Number.isFinite(n)) return "—";
|
|
40
|
+
return String(parseFloat(n.toFixed(2)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fmtTime(ms) {
|
|
44
|
+
const d = new Date(ms);
|
|
45
|
+
const p = (x) => String(x).padStart(2, "0");
|
|
46
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 主题变量(带兜底值)
|
|
50
|
+
const C = {
|
|
51
|
+
bg: "var(--dsw-alias-bg-layer-1, rgba(127,127,127,.12))",
|
|
52
|
+
border: "var(--dsw-alias-border-l1, rgba(127,127,127,.35))",
|
|
53
|
+
label: "var(--dsw-alias-label-primary, #111)",
|
|
54
|
+
sub: "var(--dsw-alias-label-secondary, rgba(0,0,0,.55))",
|
|
55
|
+
err: "var(--dsw-alias-state-error-primary, rgb(200,40,40))",
|
|
56
|
+
peak: "var(--dsw-alias-state-warn-primary, rgb(230,120,0))",
|
|
57
|
+
idle: "var(--dsw-alias-state-success-primary, rgb(34,160,80))",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const svgBase = {
|
|
61
|
+
width: 11,
|
|
62
|
+
height: 11,
|
|
63
|
+
viewBox: "0 0 24 24",
|
|
64
|
+
fill: "none",
|
|
65
|
+
stroke: "currentColor",
|
|
66
|
+
strokeWidth: 2.2,
|
|
67
|
+
strokeLinecap: "round",
|
|
68
|
+
strokeLinejoin: "round",
|
|
69
|
+
style: { flex: "none", color: C.sub },
|
|
70
|
+
};
|
|
71
|
+
const IconMoney = () => h("svg", svgBase,
|
|
72
|
+
h("circle", { cx: 12, cy: 12, r: 9 }),
|
|
73
|
+
h("path", { d: "M12 6.5v11" }),
|
|
74
|
+
h("path", { d: "M9 9.5h6" }),
|
|
75
|
+
h("path", { d: "M9 14.5h6" }));
|
|
76
|
+
const IconToken = () => h("svg", svgBase,
|
|
77
|
+
h("rect", { x: 6.5, y: 6.5, width: 11, height: 11, rx: 2 }),
|
|
78
|
+
h("path", { d: "M9 2.5v4M15 2.5v4M9 17.5v4M15 17.5v4" }),
|
|
79
|
+
h("path", { d: "M2.5 9h4M2.5 15h4M17.5 9h4M17.5 15h4" }));
|
|
80
|
+
const IconApi = () => h("svg", svgBase, h("path", { d: "M13 2 3 14h7l-1 8 10-12h-7l1-8z" }));
|
|
81
|
+
const IconIn = () => h("svg", svgBase,
|
|
82
|
+
h("path", { d: "M12 19V5" }),
|
|
83
|
+
h("path", { d: "M5 12l7-7 7 7" }));
|
|
84
|
+
const IconOut = () => h("svg", svgBase,
|
|
85
|
+
h("path", { d: "M12 5v14" }),
|
|
86
|
+
h("path", { d: "M19 12l-7 7-7-7" }));
|
|
87
|
+
const IconCache = () => h("svg", svgBase,
|
|
88
|
+
h("ellipse", { cx: 12, cy: 5, rx: 8, ry: 3 }),
|
|
89
|
+
h("path", { d: "M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5" }),
|
|
90
|
+
h("path", { d: "M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3" }));
|
|
91
|
+
|
|
92
|
+
// ---- the widget -------------------------------------------------
|
|
93
|
+
function UsageCell(props) {
|
|
94
|
+
const wide = !!props.wide;
|
|
95
|
+
const [data, setData] = useState(null);
|
|
96
|
+
const [error, setError] = useState("");
|
|
97
|
+
const [spinning, setSpinning] = useState(false);
|
|
98
|
+
const mounted = useRef(true);
|
|
99
|
+
|
|
100
|
+
const load = useCallback(async () => {
|
|
101
|
+
setSpinning(true);
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(USAGE_PATH, { cache: "no-store" });
|
|
104
|
+
let body = null;
|
|
105
|
+
try {
|
|
106
|
+
body = await res.json();
|
|
107
|
+
} catch {}
|
|
108
|
+
if (!res.ok || body === null || typeof body !== "object" || body.ok !== true) {
|
|
109
|
+
throw new Error(body && typeof body.message === "string" ? body.message : `请求失败(HTTP ${res.status})`);
|
|
110
|
+
}
|
|
111
|
+
if (!mounted.current) return;
|
|
112
|
+
setData(body);
|
|
113
|
+
setError("");
|
|
114
|
+
} catch (e) {
|
|
115
|
+
if (!mounted.current) return;
|
|
116
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
117
|
+
} finally {
|
|
118
|
+
if (mounted.current) setSpinning(false);
|
|
119
|
+
}
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
mounted.current = true;
|
|
124
|
+
load();
|
|
125
|
+
const timer = setInterval(load, POLL_MS);
|
|
126
|
+
return () => {
|
|
127
|
+
mounted.current = false;
|
|
128
|
+
clearInterval(timer);
|
|
129
|
+
};
|
|
130
|
+
}, [load]);
|
|
131
|
+
|
|
132
|
+
const balance = data && data.balance ? data.balance : null;
|
|
133
|
+
const today = data && data.today ? data.today : null;
|
|
134
|
+
const tier = data && data.tier ? data.tier : null;
|
|
135
|
+
const currency = balance && balance.currency ? balance.currency : "CNY";
|
|
136
|
+
const errs = (Array.isArray(data && data.errors) ? data.errors : []).concat(error ? [error] : []);
|
|
137
|
+
const tok = today && today.tokens ? today.tokens : null;
|
|
138
|
+
const dot = {
|
|
139
|
+
width: 8,
|
|
140
|
+
height: 8,
|
|
141
|
+
borderRadius: "50%",
|
|
142
|
+
flex: "none",
|
|
143
|
+
background: data && data.peak ? C.peak : C.idle,
|
|
144
|
+
boxShadow: "0 0 4px rgba(0,0,0,.3)",
|
|
145
|
+
};
|
|
146
|
+
const title = "DeepSeek 用量 · 点击刷新" + (errs.length ? " · " + errs.join("; ") : "");
|
|
147
|
+
|
|
148
|
+
if (!wide) {
|
|
149
|
+
return h("div", {
|
|
150
|
+
style: {
|
|
151
|
+
boxSizing: "border-box",
|
|
152
|
+
flex: "none",
|
|
153
|
+
padding: "4px 8px",
|
|
154
|
+
borderRadius: 8,
|
|
155
|
+
border: "1px solid " + C.border,
|
|
156
|
+
background: C.bg,
|
|
157
|
+
color: C.label,
|
|
158
|
+
cursor: "pointer",
|
|
159
|
+
display: "flex",
|
|
160
|
+
flexDirection: "row",
|
|
161
|
+
alignItems: "center",
|
|
162
|
+
gap: 5,
|
|
163
|
+
userSelect: "none",
|
|
164
|
+
},
|
|
165
|
+
title,
|
|
166
|
+
onClick: load,
|
|
167
|
+
},
|
|
168
|
+
h("span", { style: { fontWeight: 700, fontSize: 12, color: C.label } }, money(balance && balance.total, currency)),
|
|
169
|
+
h("span", { style: dot, title: data && data.peak ? "高峰时段" : "空闲时段" }));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const grp = (label, icon, value) =>
|
|
173
|
+
h("span", { style: { display: "inline-flex", gap: 2, alignItems: "center", minWidth: 0 } },
|
|
174
|
+
label ? h("span", { style: { color: C.sub, fontSize: 11, marginRight: 5 } }, label) : null,
|
|
175
|
+
h("span", { style: { display: "inline-flex", alignItems: "center" } }, icon),
|
|
176
|
+
h("span", { style: { fontWeight: 600, whiteSpace: "nowrap" } }, value));
|
|
177
|
+
const pr = (icon, value) =>
|
|
178
|
+
h("span", { style: { display: "inline-flex", gap: 2, alignItems: "center" } },
|
|
179
|
+
h("span", { style: { display: "inline-flex", alignItems: "center" } }, icon),
|
|
180
|
+
h("span", { style: { fontWeight: 600, whiteSpace: "nowrap" } }, value));
|
|
181
|
+
const Sep = () => h("span", { style: { color: C.sub, margin: "0 1px", fontSize: 10 } }, "·");
|
|
182
|
+
|
|
183
|
+
const costText = money(today && today.cost, currency);
|
|
184
|
+
const costShown = costText;
|
|
185
|
+
|
|
186
|
+
return h("div", {
|
|
187
|
+
style: {
|
|
188
|
+
boxSizing: "border-box",
|
|
189
|
+
flex: "1 1 100%",
|
|
190
|
+
minWidth: 0,
|
|
191
|
+
padding: "8px 10px",
|
|
192
|
+
borderRadius: 10,
|
|
193
|
+
border: "1px solid " + C.border,
|
|
194
|
+
background: C.bg,
|
|
195
|
+
color: C.label,
|
|
196
|
+
cursor: "pointer",
|
|
197
|
+
userSelect: "none",
|
|
198
|
+
display: "flex",
|
|
199
|
+
flexDirection: "column",
|
|
200
|
+
gap: 4,
|
|
201
|
+
marginTop: 4,
|
|
202
|
+
position: "relative",
|
|
203
|
+
fontSize: "12px",
|
|
204
|
+
lineHeight: "15px",
|
|
205
|
+
},
|
|
206
|
+
title,
|
|
207
|
+
onClick: load,
|
|
208
|
+
},
|
|
209
|
+
h("div", { style: { display: "flex", alignItems: "baseline", minWidth: 0, paddingRight: 12 } },
|
|
210
|
+
grp("余额", h(IconMoney, null), money(balance && balance.total, currency))),
|
|
211
|
+
h("div", { style: { display: "flex", gap: 4, alignItems: "center", minWidth: 0, flexWrap: "nowrap" } },
|
|
212
|
+
grp("今日", h(IconMoney, null), costShown),
|
|
213
|
+
Sep(),
|
|
214
|
+
grp(null, h(IconToken, null), fmtTokens(tok && tok.total)),
|
|
215
|
+
Sep(),
|
|
216
|
+
grp(null, h(IconApi, null), today && today.calls !== null && today.calls !== undefined ? String(today.calls) : "—")),
|
|
217
|
+
h("span", {
|
|
218
|
+
style: { position: "absolute", top: 8, right: 9, ...dot },
|
|
219
|
+
title: data && data.peak ? "高峰时段 " + (data.peakHours || "") : "空闲时段",
|
|
220
|
+
}),
|
|
221
|
+
h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, minWidth: 0 } },
|
|
222
|
+
tier
|
|
223
|
+
? h("span", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: C.sub, fontSize: 10, whiteSpace: "nowrap", overflow: "hidden" } },
|
|
224
|
+
pr(h(IconIn, null), fmtPrice(tier.input)),
|
|
225
|
+
Sep(),
|
|
226
|
+
pr(h(IconCache, null), fmtPrice(tier.cacheRead)),
|
|
227
|
+
Sep(),
|
|
228
|
+
pr(h(IconOut, null), fmtPrice(tier.output)))
|
|
229
|
+
: null,
|
|
230
|
+
h("span", {
|
|
231
|
+
className: "dsw-usage-time",
|
|
232
|
+
style: { display: "inline-flex", alignItems: "center", gap: 4, color: C.sub, fontSize: 10, flex: "none" },
|
|
233
|
+
},
|
|
234
|
+
spinning
|
|
235
|
+
? h("span", {
|
|
236
|
+
className: "dsw-usage-refresh",
|
|
237
|
+
style: { color: C.label, display: "inline-flex", alignItems: "center" },
|
|
238
|
+
title: "刷新中",
|
|
239
|
+
}, h("svg", { width: 11, height: 11, viewBox: "0 0 16 16", fill: "none", style: { animation: "dsh-usage-spin .8s linear infinite" } },
|
|
240
|
+
h("path", { d: "M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 1.5v3h-3", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" })))
|
|
241
|
+
: null,
|
|
242
|
+
h("span", null, fmtTime(data ? data.updatedAt : Date.now())))),
|
|
243
|
+
errs.length
|
|
244
|
+
? h("div", { style: { color: C.err, fontSize: 10, whiteSpace: "pre-wrap" } }, errs.join(" | "))
|
|
245
|
+
: null);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---- client plugin body -----------------------------------------
|
|
249
|
+
const inject = ["slots"];
|
|
250
|
+
|
|
251
|
+
function apply(ctx) {
|
|
252
|
+
// 注入本包样式:footerActions 换行(卡片独占一行)+ 刷新旋转动画。
|
|
253
|
+
const tagId = "dsh-deepseek-usage";
|
|
254
|
+
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${tagId}"]`) === null) {
|
|
255
|
+
const style = document.createElement("style");
|
|
256
|
+
style.dataset.plugin = "dsh-deepseek-usage";
|
|
257
|
+
style.dataset.pluginCss = tagId;
|
|
258
|
+
style.textContent =
|
|
259
|
+
'div[class*="footerActions"]{flex-wrap:wrap}' +
|
|
260
|
+
"@keyframes dsh-usage-spin{to{transform:rotate(360deg)}}";
|
|
261
|
+
document.head.appendChild(style);
|
|
262
|
+
}
|
|
263
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register(
|
|
264
|
+
{ name: "sidebar.footer.action", id: "dsh-usage", order: 100, label: "DeepSeek 用量" },
|
|
265
|
+
(props) => h(UsageCell, { wide: props.wide }),
|
|
266
|
+
));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
exports.apply = apply;
|
|
270
|
+
exports.inject = inject;
|
|
271
|
+
return module.exports;
|
|
272
|
+
}
|
|
273
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-deepseek-usage — host half.
|
|
3
|
+
*
|
|
4
|
+
* 注册一个 HTTP 路由:
|
|
5
|
+
*
|
|
6
|
+
* GET /api/deepseek-usage
|
|
7
|
+
*
|
|
8
|
+
* 返回(JSON):
|
|
9
|
+
* {
|
|
10
|
+
* ok: true,
|
|
11
|
+
* balance: { available, total, currency, granted, toppedUp }, // 官方余额接口
|
|
12
|
+
* today: {
|
|
13
|
+
* cost, // 今日花费:官方平台数据(配置 DEEPSEEK_PLATFORM_TOKEN 时),否则本地日志估算
|
|
14
|
+
* costSource: 'official' | 'estimate' | null,
|
|
15
|
+
* tokens: { input, output, cacheRead, cacheWrite, total }, // 今日 token 数(本地日志)
|
|
16
|
+
* calls, // 今日 API 请求数(本地日志)
|
|
17
|
+
* sessions // 今日有活动的会话数
|
|
18
|
+
* },
|
|
19
|
+
* peak, peakHours, // 峰谷时段
|
|
20
|
+
* tier: { input, cacheRead, cacheWrite, output }, // 当前模型当前档单价
|
|
21
|
+
* updatedAt, errors
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* 余额来自 DeepSeek 官方 GET /user/balance(API Key 经凭证服务解析,与
|
|
25
|
+
* llm-deepseek 同一引用,不出本机)。今日 Token / API 次数只能从本地会话
|
|
26
|
+
* 日志统计(官方无用量查询接口);今日花费优先用官方平台 usage API
|
|
27
|
+
* (需可选的 DEEPSEEK_PLATFORM_TOKEN),否则按日志 token × 官方价格表估算。
|
|
28
|
+
*/
|
|
29
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
30
|
+
import { DEFAULT_MODEL, PEAK_HOURS_TEXT, costOf, isPeak, unitAt } from "./pricing.js";
|
|
31
|
+
|
|
32
|
+
const name = "dsh-deepseek-usage";
|
|
33
|
+
const inject = ["credentials", "webServer"];
|
|
34
|
+
|
|
35
|
+
const PUBLIC_BASE_URL = "https://api.deepseek.com";
|
|
36
|
+
/** 与 llm-deepseek 适配器对齐的环境变量覆盖。 */
|
|
37
|
+
const BASE_URL_ENV = "DEEPSEEK_BASE_URL";
|
|
38
|
+
const API_KEY_REF = credentialRef("DEEPSEEK_API_KEY");
|
|
39
|
+
/** 可选:平台会话令牌(platform.deepseek.com 的 userToken),解锁官方今日消费数据。 */
|
|
40
|
+
const PLATFORM_TOKEN_REF = credentialRef("DEEPSEEK_PLATFORM_TOKEN");
|
|
41
|
+
const BALANCE_PATH = "/user/balance";
|
|
42
|
+
const ROUTE_PATH = "/api/deepseek-usage";
|
|
43
|
+
const TIMEOUT_MS = 15000;
|
|
44
|
+
/** 平台用量接口(控制台同源,可按月按日筛选)。 */
|
|
45
|
+
const PLATFORM_USAGE_URL = "https://platform.deepseek.com/api/v0/usage/cost";
|
|
46
|
+
/** 本地用量扫描的短缓存,避免多标签页/轮询反复解码全部日志。 */
|
|
47
|
+
const USAGE_CACHE_MS = 20000;
|
|
48
|
+
|
|
49
|
+
const JSON_HEADERS = {
|
|
50
|
+
"content-type": "application/json; charset=utf-8",
|
|
51
|
+
"cache-control": "no-store",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function sendJson(res, status, body) {
|
|
55
|
+
res.writeHead(status, JSON_HEADERS);
|
|
56
|
+
res.end(JSON.stringify(body));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function balanceUrl() {
|
|
60
|
+
const base = process.env[BASE_URL_ENV] ?? PUBLIC_BASE_URL;
|
|
61
|
+
return `${base.replace(/\/+$/, "")}${BALANCE_PATH}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 本地日历日 `YYYY-MM-DD`(平台用量行按日期键控)。 */
|
|
65
|
+
function localDate(d = new Date()) {
|
|
66
|
+
const y = d.getFullYear();
|
|
67
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
68
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
69
|
+
return `${y}-${m}-${day}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 容忍字符串/数字,返回有限数或 NaN。 */
|
|
73
|
+
function toFinite(value) {
|
|
74
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
|
|
75
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
76
|
+
const n = Number(value);
|
|
77
|
+
return Number.isFinite(n) ? n : NaN;
|
|
78
|
+
}
|
|
79
|
+
return NaN;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 取平台用量接口"今天"行的花费(官方),找不到返回 null。 */
|
|
83
|
+
async function fetchPlatformTodayCost(token) {
|
|
84
|
+
const now = new Date();
|
|
85
|
+
const url = `${PLATFORM_USAGE_URL}?month=${now.getMonth() + 1}&year=${now.getFullYear()}`;
|
|
86
|
+
const response = await fetch(url, {
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${token}`,
|
|
89
|
+
Accept: "application/json",
|
|
90
|
+
"x-app-version": "1.0.0",
|
|
91
|
+
Origin: "https://platform.deepseek.com",
|
|
92
|
+
Referer: "https://platform.deepseek.com/usage",
|
|
93
|
+
},
|
|
94
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) throw new Error(`DeepSeek 平台用量接口返回 HTTP ${response.status}`);
|
|
97
|
+
const body = await response.json();
|
|
98
|
+
const biz = body && typeof body === "object" ? body.data : void 0;
|
|
99
|
+
if (body?.code !== 0 || biz === void 0 || biz.biz_code !== 0) {
|
|
100
|
+
const code = body?.code ?? biz?.biz_code;
|
|
101
|
+
if (code === 40002 || code === 40003) {
|
|
102
|
+
throw new Error("DEEPSEEK_PLATFORM_TOKEN 已过期:请重新登录 platform.deepseek.com 并更新 userToken");
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`DeepSeek 平台用量接口错误 (code ${code ?? "unknown"})`);
|
|
105
|
+
}
|
|
106
|
+
const bizData = biz.biz_data;
|
|
107
|
+
const container = Array.isArray(bizData) ? bizData[0] : bizData;
|
|
108
|
+
const days = container && typeof container === "object" ? container.days : void 0;
|
|
109
|
+
if (!Array.isArray(days)) return null;
|
|
110
|
+
const today = localDate();
|
|
111
|
+
const entry = days.find((d) => d && d.date === today);
|
|
112
|
+
if (!entry || !Array.isArray(entry.data)) return null;
|
|
113
|
+
let total = 0;
|
|
114
|
+
for (const modelEntry of entry.data) {
|
|
115
|
+
if (!modelEntry || typeof modelEntry !== "object" || !Array.isArray(modelEntry.usage)) continue;
|
|
116
|
+
for (const u of modelEntry.usage) {
|
|
117
|
+
if (!u || typeof u !== "object") continue;
|
|
118
|
+
const value = toFinite(u.cost ?? u.amount);
|
|
119
|
+
if (Number.isFinite(value)) total += value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return Math.round(total * 100) / 100;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 本地今日零点(unix ms)。 */
|
|
126
|
+
function startOfTodayMs() {
|
|
127
|
+
const d = new Date();
|
|
128
|
+
d.setHours(0, 0, 0, 0);
|
|
129
|
+
return d.getTime();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let usageCache = null;
|
|
133
|
+
let usageCacheAt = 0;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 扫描全部会话日志,统计今日(本地时区)token 用量 / API 请求数 / 花费估算。
|
|
137
|
+
* 每条 `assistant/message` 按消息自带的 model(data.message.source.model)
|
|
138
|
+
* 与事件时刻的峰谷单价计价。
|
|
139
|
+
*/
|
|
140
|
+
async function scanTodayUsage(ctx) {
|
|
141
|
+
const persistence = ctx.get("sessionPersistence");
|
|
142
|
+
if (persistence === void 0 || typeof persistence.list !== "function" || typeof persistence.readRaw !== "function") {
|
|
143
|
+
return { usable: false, error: "sessionPersistence 不可用" };
|
|
144
|
+
}
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
if (usageCache !== null && now - usageCacheAt < USAGE_CACHE_MS) return usageCache;
|
|
147
|
+
const dayStart = startOfTodayMs();
|
|
148
|
+
const acc = { usable: true, calls: 0, sessions: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, skipped: 0 };
|
|
149
|
+
try {
|
|
150
|
+
const headers = await persistence.list();
|
|
151
|
+
for (const header of headers) {
|
|
152
|
+
let raw;
|
|
153
|
+
try {
|
|
154
|
+
raw = await persistence.readRaw(header.id);
|
|
155
|
+
} catch {
|
|
156
|
+
acc.skipped += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (raw === void 0 || raw === null || typeof raw.content !== "string") continue;
|
|
160
|
+
let hit = false;
|
|
161
|
+
for (const line of raw.content.split("\n")) {
|
|
162
|
+
if (line === "") continue;
|
|
163
|
+
let event;
|
|
164
|
+
try {
|
|
165
|
+
event = JSON.parse(line);
|
|
166
|
+
} catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (event === null || typeof event !== "object" || event.type !== "assistant/message") continue;
|
|
170
|
+
const usage = event.data && event.data.usage;
|
|
171
|
+
if (usage === void 0 || usage === null || typeof event.time !== "number" || event.time < dayStart) continue;
|
|
172
|
+
hit = true;
|
|
173
|
+
const source = event.data.message && event.data.message.source;
|
|
174
|
+
const model = source && typeof source.model === "string" ? source.model : void 0;
|
|
175
|
+
const unit = unitAt(model, event.time);
|
|
176
|
+
acc.calls += 1;
|
|
177
|
+
acc.input += usage.inputTokens || 0;
|
|
178
|
+
acc.output += usage.outputTokens || 0;
|
|
179
|
+
acc.cacheRead += usage.cacheReadTokens || 0;
|
|
180
|
+
acc.cacheWrite += usage.cacheWriteTokens || 0;
|
|
181
|
+
acc.cost += costOf(usage, unit);
|
|
182
|
+
}
|
|
183
|
+
if (hit) acc.sessions += 1;
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
acc.usable = false;
|
|
187
|
+
acc.error = error instanceof Error ? error.message : String(error);
|
|
188
|
+
}
|
|
189
|
+
acc.cost = Math.round(acc.cost * 1e6) / 1e6;
|
|
190
|
+
usageCache = acc;
|
|
191
|
+
usageCacheAt = now;
|
|
192
|
+
return acc;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function apply(ctx) {
|
|
196
|
+
ctx.effect(
|
|
197
|
+
() => ctx.webServer.register({
|
|
198
|
+
kind: "exact",
|
|
199
|
+
path: ROUTE_PATH,
|
|
200
|
+
handler: async (req, res) => {
|
|
201
|
+
try {
|
|
202
|
+
const errors = [];
|
|
203
|
+
|
|
204
|
+
// ---- 余额(官方接口)----
|
|
205
|
+
const balance = { available: null, total: null, currency: null, granted: null, toppedUp: null };
|
|
206
|
+
try {
|
|
207
|
+
const hit = await ctx.credentials.resolve(API_KEY_REF);
|
|
208
|
+
if (hit === void 0) {
|
|
209
|
+
errors.push("未配置 DEEPSEEK_API_KEY");
|
|
210
|
+
} else {
|
|
211
|
+
const response = await fetch(balanceUrl(), {
|
|
212
|
+
headers: { Authorization: `Bearer ${hit.value}`, Accept: "application/json" },
|
|
213
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
214
|
+
});
|
|
215
|
+
const text = await response.text();
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
errors.push(`余额接口 HTTP ${response.status}`);
|
|
218
|
+
} else {
|
|
219
|
+
const body = JSON.parse(text);
|
|
220
|
+
const infos = body && Array.isArray(body.balance_infos) ? body.balance_infos : [];
|
|
221
|
+
const first = infos[0] || {};
|
|
222
|
+
balance.available = body?.is_available !== false;
|
|
223
|
+
balance.total = toFinite(first.total_balance);
|
|
224
|
+
balance.currency = typeof first.currency === "string" ? first.currency : null;
|
|
225
|
+
balance.granted = toFinite(first.granted_balance);
|
|
226
|
+
balance.toppedUp = toFinite(first.topped_up_balance);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
errors.push(`余额获取失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---- 今日花费:官方平台数据(可选令牌)----
|
|
234
|
+
let costOfficial = null;
|
|
235
|
+
try {
|
|
236
|
+
const platformHit = await ctx.credentials.resolve(PLATFORM_TOKEN_REF);
|
|
237
|
+
if (platformHit !== void 0) {
|
|
238
|
+
costOfficial = await fetchPlatformTodayCost(platformHit.value);
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
errors.push(`官方今日消费获取失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---- 今日 token / API 次数 / 花费估算(本地日志)----
|
|
245
|
+
const usage = await scanTodayUsage(ctx);
|
|
246
|
+
if (!usage.usable) errors.push(usage.error ?? "今日用量统计不可用");
|
|
247
|
+
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
let model = DEFAULT_MODEL;
|
|
250
|
+
try {
|
|
251
|
+
const adm = ctx.get("agentDefaultModel");
|
|
252
|
+
const selection = adm && typeof adm.currentSelection === "function" ? adm.currentSelection() : void 0;
|
|
253
|
+
if (selection && typeof selection.model === "string") model = selection.model;
|
|
254
|
+
} catch {}
|
|
255
|
+
const unit = unitAt(model, now);
|
|
256
|
+
|
|
257
|
+
const cost = costOfficial !== null ? costOfficial : (usage.usable ? usage.cost : null);
|
|
258
|
+
const costSource = costOfficial !== null ? "official" : (usage.usable ? "estimate" : null);
|
|
259
|
+
|
|
260
|
+
sendJson(res, 200, {
|
|
261
|
+
ok: true,
|
|
262
|
+
balance,
|
|
263
|
+
today: {
|
|
264
|
+
cost,
|
|
265
|
+
costSource,
|
|
266
|
+
tokens: usage.usable
|
|
267
|
+
? {
|
|
268
|
+
input: usage.input,
|
|
269
|
+
output: usage.output,
|
|
270
|
+
cacheRead: usage.cacheRead,
|
|
271
|
+
cacheWrite: usage.cacheWrite,
|
|
272
|
+
total: usage.input + usage.output + usage.cacheRead + usage.cacheWrite,
|
|
273
|
+
}
|
|
274
|
+
: null,
|
|
275
|
+
calls: usage.usable ? usage.calls : null,
|
|
276
|
+
sessions: usage.usable ? usage.sessions : null,
|
|
277
|
+
skipped: usage.usable ? usage.skipped : null,
|
|
278
|
+
},
|
|
279
|
+
peak: isPeak(now),
|
|
280
|
+
peakHours: PEAK_HOURS_TEXT,
|
|
281
|
+
tier: { input: unit.input, cacheRead: unit.cacheRead, cacheWrite: unit.cacheWrite, output: unit.output },
|
|
282
|
+
updatedAt: now,
|
|
283
|
+
errors,
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
ctx.logger.warn("dsh-deepseek-usage: request failed");
|
|
287
|
+
ctx.logger.warn(error);
|
|
288
|
+
sendJson(res, 502, {
|
|
289
|
+
ok: false,
|
|
290
|
+
error: "internal",
|
|
291
|
+
message: error instanceof Error ? error.message : String(error),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
"dsh-deepseek-usage: route",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export { name, inject, apply };
|
package/lib/pricing.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-deepseek-usage — 定价引擎(元 / 百万 tokens)。
|
|
3
|
+
*
|
|
4
|
+
* DeepSeek 官方价格快照(2026-08-17 起 V4 系列峰谷定价,北京时间):
|
|
5
|
+
* 高峰时段 9:00–12:00 与 14:00–18:00,其余为空闲时段(空闲=高峰半价)。
|
|
6
|
+
* 缓存写暂不计费(cacheWrite = 0);未知模型回退 chat 档。
|
|
7
|
+
* 价格调整时只需改 PRICE_TABLE 与 PEAK_WINDOWS。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 每模型价格:input/cacheRead/output 均为 [高峰, 空闲] 二元组;cacheWrite 为常量 0。 */
|
|
11
|
+
const PRICE_TABLE = {
|
|
12
|
+
'deepseek-v4-flash': { input: [3.0, 1.5], cacheRead: [0.1, 0.05], cacheWrite: 0, output: [9.0, 4.5] },
|
|
13
|
+
'deepseek-v4-pro': { input: [9.0, 4.5], cacheRead: [0.3, 0.15], cacheWrite: 0, output: [27.0, 13.5] },
|
|
14
|
+
'deepseek-chat': { input: [2, 2], cacheRead: [0.5, 0.5], cacheWrite: 0, output: [8, 8] },
|
|
15
|
+
'deepseek-reasoner': { input: [4, 4], cacheRead: [1, 1], cacheWrite: 0, output: [16, 16] },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** 默认模型(当 agentDefaultModel 不可用时)。 */
|
|
19
|
+
export const DEFAULT_MODEL = 'deepseek-v4-flash';
|
|
20
|
+
|
|
21
|
+
/** 高峰时段窗口(本地/北京时间,含起点不含终点)。 */
|
|
22
|
+
export const PEAK_WINDOWS = [[9, 12], [14, 18]];
|
|
23
|
+
/** 高峰时段展示文案。 */
|
|
24
|
+
export const PEAK_HOURS_TEXT = '9:00–12:00、14:00–18:00';
|
|
25
|
+
|
|
26
|
+
/** 某时刻是否为高峰时段。 */
|
|
27
|
+
export function isPeak(ms) {
|
|
28
|
+
const h = new Date(ms).getHours();
|
|
29
|
+
return PEAK_WINDOWS.some(([start, end]) => h >= start && h < end);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 某模型在某一时刻的单位价格(元/百万 tokens)。 */
|
|
33
|
+
export function unitAt(model, ms) {
|
|
34
|
+
const p = PRICE_TABLE[model] || PRICE_TABLE['deepseek-chat'];
|
|
35
|
+
const tier = isPeak(ms) ? 0 : 1;
|
|
36
|
+
return {
|
|
37
|
+
input: p.input[tier],
|
|
38
|
+
cacheRead: p.cacheRead[tier],
|
|
39
|
+
cacheWrite: p.cacheWrite,
|
|
40
|
+
output: p.output[tier],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 按单位价格计算一次 usage 的估算成本(元)。 */
|
|
45
|
+
export function costOf(usage, unit) {
|
|
46
|
+
return (
|
|
47
|
+
(usage.inputTokens * unit.input +
|
|
48
|
+
(usage.cacheReadTokens || 0) * unit.cacheRead +
|
|
49
|
+
(usage.cacheWriteTokens || 0) * unit.cacheWrite +
|
|
50
|
+
usage.outputTokens * unit.output) /
|
|
51
|
+
1e6
|
|
52
|
+
);
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dougen/dsh-deepseek-usage",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "DeepSeek 用量侧边栏插件:余额(官方 API)、今日花费(官方平台数据或本地日志估算)、今日 Token 与 API 次数(本地日志)、高峰/空闲时段指示灯。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"cordis.patch.yml",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./lib/index.js",
|
|
14
|
+
"./client": "./lib/client.js",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"dsh": {
|
|
18
|
+
"bundle": {
|
|
19
|
+
"patch": "./cordis.patch.yml"
|
|
20
|
+
},
|
|
21
|
+
"client": {
|
|
22
|
+
"platform": "web",
|
|
23
|
+
"inject": [
|
|
24
|
+
"slots"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"dsh",
|
|
33
|
+
"deepseek",
|
|
34
|
+
"deepseek-harness",
|
|
35
|
+
"plugin",
|
|
36
|
+
"usage",
|
|
37
|
+
"balance",
|
|
38
|
+
"tokens",
|
|
39
|
+
"web"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"registry": "https://registry.npmjs.org",
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|