@dougen/dsh-deepseek-usage 0.2.1 → 0.3.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/README.md CHANGED
@@ -4,17 +4,16 @@ DeepSeek 用量侧边栏插件(DeepSeek Harness Web UI)
4
4
 
5
5
  ## 简介
6
6
 
7
- 在 DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号用量**卡片:余额、今日消费金额、今日 Token、今日 API 请求次数,以及高峰/空闲计费时段指示灯。
7
+ 在 DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号余额**(放大显示)与**当前模型档单价**卡片,并提供高峰/空闲计费时段指示灯。
8
8
 
9
9
  ![DeepSeek 用量卡片](https://cdn.jsdelivr.net/npm/@dougen/dsh-deepseek-usage/screenshot.png)
10
10
 
11
11
  ## 功能
12
12
 
13
- - 第一行:`余额 ¥110.00`
14
- - 第二行:`今日 2.94 · 36.9M · 170`(今日消费金额 · 今日 Token · 今日 API 请求次数)
15
- - 第三行:`↑1.5 · 💾0.05 · ↓4.5`(输入 / 缓存命中 / 输出 单价,按当前启用模型与峰谷时段自动切换)
16
- - 右上角:高峰/空闲状态指示灯(高峰 9:00–12:00、14:00–18:00 北京时间,橙色;其余空闲,绿色)
17
- - 每 5 分钟自动刷新;点击卡片任意位置可立即刷新
13
+ - **余额**:`¥ 110.00` 大字显示(符号随账号币种自动切换:人民币 `¥` / 美元 `$`)
14
+ - **档单价**:`↑¥1.50 · 💾¥0.05 · ↓¥4.50`(输入 / 输入缓存 / 输出,随当前启用模型与峰谷时段自动切换,符号跟随余额币种)
15
+ - **峰谷指示灯**:卡片右上角,高峰(9:00–12:00、14:00–18:00 北京时间)橙色,空闲绿色
16
+ - **自动刷新**:每 5 分钟一次;点击卡片任意位置立即刷新,刷新时右下角显示旋转图标
18
17
  - 跟随应用明暗主题
19
18
 
20
19
  ## 安装
@@ -32,36 +31,18 @@ dsh plugin --profile web add @dougen/dsh-deepseek-usage
32
31
  | 数据 | 来源 |
33
32
  |---|---|
34
33
  | 余额 | DeepSeek 官方 API `GET /user/balance`(使用 DSH 中已配置的 `DEEPSEEK_API_KEY`) |
35
- | 今日消费 / Token / API 请求次数 | DeepSeek **开放平台官方账单**(platform.deepseek.com 用量页面数据,约 5 分钟延迟) |
34
+ | 档单价 / 峰谷时段 | 内置价格表(DeepSeek 官方峰谷定价快照,2026-08-17 起) |
36
35
 
37
- **无需手动配置任何 Token**:插件通过浏览器的调试接口(CDP)自动读取你本机浏览器中 DeepSeek 开放平台的登录状态。首次使用请先在浏览器中登录 [platform.deepseek.com](https://platform.deepseek.com)。
38
-
39
- > 需要你的浏览器开启**远程调试**(一次性设置,设置后长期有效):在地址栏打开 `chrome://inspect/#remote-debugging`,勾选"允许远程调试";或用命令行启动浏览器时加参数 `--remote-debugging-port=9222`。
40
-
41
- ### 未登录时
42
-
43
- 如果检测不到平台登录,卡片第二行会显示 **`平台未登录,点击卡片进行登录`**:
44
-
45
- 1. 点击卡片,浏览器会打开 DeepSeek 开放平台页面
46
- 2. 登录完成后回到 DSH,再次点击卡片(或等待自动刷新)即可显示今日用量
36
+ 使用前请确保 DSH 已配置 `DEEPSEEK_API_KEY`(可在 DSH 设置中的模型页面配置)。
47
37
 
48
38
  ## 常见问题
49
39
 
50
- **登录了平台但卡片仍显示未登录?**
51
- - 请确认登录的是 [platform.deepseek.com](https://platform.deepseek.com)(开放平台),而不是 chat.deepseek.com(网页版对话)。
52
- - 请确认浏览器已开启远程调试(见上文"数据来源"),设置后可点击卡片强制刷新一次。
53
- - 支持的浏览器:Chromium 内核(Chromium / Chrome / Edge / CentBrowser 等)。
54
-
55
- **为什么显示"平台未登录"?**
56
- - 浏览器未开启远程调试:在地址栏打开 `chrome://inspect/#remote-debugging`,勾选"允许远程调试"后重启浏览器(设置会持久保留)。
57
- - 或启动浏览器时加参数:`--remote-debugging-port=9222`。
58
- - 登录状态已过期:点击卡片重新登录即可。
59
-
60
- **今日数据与平台页面不一致?**
61
- - 官方用量数据本身有约 5 分钟延迟,且按北京时间(GMT+8)统计。
40
+ **余额显示"—"?**
41
+ - 未配置 `DEEPSEEK_API_KEY`:在 DSH 设置中配置后重启。
42
+ - 网络或接口异常:错误信息会显示在卡片下方。
62
43
 
63
- **支持哪些浏览器?**
64
- - Chromium 内核浏览器均可(自动探测本机安装的 Chromium / Chrome / Edge / CentBrowser)。
44
+ **单价与官方价格不一致?**
45
+ - 单价按**当前启用模型**与**峰谷时段**自动切换;若当前模型不在内置价格表,会按 `deepseek-chat` 档展示并给出提示。
65
46
 
66
47
  ## 卸载
67
48
 
package/lib/client.js CHANGED
@@ -2,7 +2,8 @@
2
2
  //
3
3
  // 侧边栏底部用量卡片(注册进 `sidebar.footer.action` 槽位,独占一行,位于
4
4
  // Cordis 面板按钮下方)。轮询宿主路由 `/api/deepseek-usage`(见 lib/index.js)
5
- // 每 60 秒一次,点击卡片任意位置可立即刷新;右下角在刷新时显示旋转图标,仅作刷新状态指示(无更新时间显示)。
5
+ // 每 5 分钟一次,点击卡片任意位置可立即刷新;右下角在刷新时显示旋转图标。
6
+ // 卡片显示:余额(放大显示,人民币符号)+ 当前模型档单价(输入/输入缓存/输出)。
6
7
  // 样式仅使用 `--dsw-*` 主题变量(含兜底值),跟随明暗主题。
7
8
  window.__ModuleLoader__.load({
8
9
  id: "@dougen/dsh-deepseek-usage",
@@ -25,14 +26,6 @@ window.__ModuleLoader__.load({
25
26
  return n.toFixed(2);
26
27
  }
27
28
 
28
- function fmtTokens(value) {
29
- const n = Number(value);
30
- if (!Number.isFinite(n)) return "—";
31
- if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
32
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
33
- return String(n);
34
- }
35
-
36
29
  function fmtPrice(value) {
37
30
  const n = Number(value);
38
31
  if (!Number.isFinite(n)) return "—";
@@ -59,18 +52,8 @@ window.__ModuleLoader__.load({
59
52
  strokeWidth: 2.2,
60
53
  strokeLinecap: "round",
61
54
  strokeLinejoin: "round",
62
- style: { flex: "none", color: C.sub },
55
+ style: { flex: "none", display: "block" },
63
56
  };
64
- const IconMoney = () => h("svg", svgBase,
65
- h("circle", { cx: 12, cy: 12, r: 9 }),
66
- h("path", { d: "M12 6.5v11" }),
67
- h("path", { d: "M9 9.5h6" }),
68
- h("path", { d: "M9 14.5h6" }));
69
- const IconToken = () => h("svg", svgBase,
70
- h("rect", { x: 6.5, y: 6.5, width: 11, height: 11, rx: 2 }),
71
- h("path", { d: "M9 2.5v4M15 2.5v4M9 17.5v4M15 17.5v4" }),
72
- h("path", { d: "M2.5 9h4M2.5 15h4M17.5 9h4M17.5 15h4" }));
73
- const IconApi = () => h("svg", svgBase, h("path", { d: "M13 2 3 14h7l-1 8 10-12h-7l1-8z" }));
74
57
  const IconIn = () => h("svg", svgBase,
75
58
  h("path", { d: "M12 19V5" }),
76
59
  h("path", { d: "M5 12l7-7 7 7" }));
@@ -89,12 +72,11 @@ window.__ModuleLoader__.load({
89
72
  const [error, setError] = useState("");
90
73
  const [spinning, setSpinning] = useState(false);
91
74
  const mounted = useRef(true);
92
- const retryRef = useRef(null);
93
75
 
94
- const load = useCallback(async (force) => {
76
+ const load = useCallback(async () => {
95
77
  setSpinning(true);
96
78
  try {
97
- const res = await fetch(USAGE_PATH + (force ? "?refresh=1" : ""), { cache: "no-store" });
79
+ const res = await fetch(USAGE_PATH, { cache: "no-store" });
98
80
  let body = null;
99
81
  try {
100
82
  body = await res.json();
@@ -116,22 +98,17 @@ window.__ModuleLoader__.load({
116
98
  useEffect(() => {
117
99
  mounted.current = true;
118
100
  load();
119
- const timer = setInterval(() => load(false), POLL_MS);
101
+ const timer = setInterval(load, POLL_MS);
120
102
  return () => {
121
103
  mounted.current = false;
122
104
  clearInterval(timer);
123
- if (retryRef.current) {
124
- clearInterval(retryRef.current);
125
- retryRef.current = null;
126
- }
127
105
  };
128
106
  }, [load]);
129
107
 
130
108
  const balance = data && data.balance ? data.balance : null;
131
- const today = data && data.today ? data.today : null;
132
109
  const tier = data && data.tier ? data.tier : null;
110
+ const symbol = balance && balance.currency === "USD" ? "$" : "¥";
133
111
  const errs = (Array.isArray(data && data.errors) ? data.errors : []).concat(error ? [error] : []);
134
- const tok = today && today.tokens ? today.tokens : null;
135
112
  const dot = {
136
113
  width: 8,
137
114
  height: 8,
@@ -140,28 +117,7 @@ window.__ModuleLoader__.load({
140
117
  background: data && data.peak ? C.peak : C.idle,
141
118
  boxShadow: "0 0 4px rgba(0,0,0,.3)",
142
119
  };
143
- const loggedIn = !!(today && today.platform && today.platform.loggedIn);
144
- const loginHint = (today && today.platform && today.platform.loginHint) || "平台未登录,点击卡片进行登录";
145
- const handleClick = useCallback(() => {
146
- if (loggedIn) {
147
- load(true);
148
- return;
149
- }
150
- // 未登录:打开 DeepSeek 开放平台登录页,并短暂高频重试等待登录完成。
151
- if (typeof window !== "undefined") window.open("https://platform.deepseek.com", "_blank");
152
- load(true);
153
- if (retryRef.current) return;
154
- let tries = 0;
155
- retryRef.current = setInterval(() => {
156
- tries += 1;
157
- load(true);
158
- if (tries >= 6) {
159
- clearInterval(retryRef.current);
160
- retryRef.current = null;
161
- }
162
- }, 10000);
163
- }, [loggedIn, load]);
164
- const title = "DeepSeek 用量 · " + (loggedIn ? "点击刷新" : "点击登录") + (errs.length ? " · " + errs.join("; ") : "");
120
+ const title = "DeepSeek 用量 · 点击刷新" + (errs.length ? " · " + errs.join("; ") : "");
165
121
 
166
122
  if (!wide) {
167
123
  return h("div", {
@@ -181,25 +137,19 @@ window.__ModuleLoader__.load({
181
137
  userSelect: "none",
182
138
  },
183
139
  title,
184
- onClick: handleClick,
140
+ onClick: load,
185
141
  },
186
- h("span", { style: { fontWeight: 700, fontSize: 12, color: C.label } }, money(balance && balance.total)),
142
+ h("span", { style: { display: "inline-flex", alignItems: "baseline", gap: 3 } },
143
+ h("span", { style: { fontWeight: 700, fontSize: 13, color: C.label } }, symbol),
144
+ h("span", { style: { fontWeight: 700, fontSize: 13, color: C.label } }, money(balance && balance.total))),
187
145
  h("span", { style: dot, title: data && data.peak ? "高峰时段" : "空闲时段" }));
188
146
  }
189
147
 
190
- const grp = (label, icon, value) =>
191
- h("span", { style: { display: "inline-flex", gap: 2, alignItems: "center", minWidth: 0 } },
192
- label ? h("span", { style: { color: C.sub, fontSize: 11, marginRight: 5 } }, label) : null,
193
- h("span", { style: { display: "inline-flex", alignItems: "center" } }, icon),
194
- h("span", { style: { fontWeight: 600, whiteSpace: "nowrap" } }, value));
195
- const pr = (icon, value) =>
196
- h("span", { style: { display: "inline-flex", gap: 2, alignItems: "center" } },
197
- h("span", { style: { display: "inline-flex", alignItems: "center" } }, icon),
198
- h("span", { style: { fontWeight: 600, whiteSpace: "nowrap" } }, value));
199
- const Sep = () => h("span", { style: { color: C.sub, margin: "0 1px", fontSize: 10 } }, "·");
200
-
201
- const costText = money(today && today.cost);
202
- const costShown = costText;
148
+ const Price = (icon, label, value) =>
149
+ h("span", { style: { display: "inline-flex", alignItems: "center", gap: 3, minWidth: 0, color: C.sub } },
150
+ h("span", { style: { display: "inline-flex", alignItems: "center" }, title: label }, icon),
151
+ h("span", { style: { fontWeight: 600, whiteSpace: "nowrap" } }, symbol + fmtPrice(value)));
152
+ const Sep = () => h("span", { style: { color: C.sub, margin: "0 3px", fontSize: 10 } }, "·");
203
153
 
204
154
  return h("div", {
205
155
  style: {
@@ -222,45 +172,32 @@ window.__ModuleLoader__.load({
222
172
  lineHeight: "15px",
223
173
  },
224
174
  title,
225
- onClick: handleClick,
175
+ onClick: load,
226
176
  },
227
- h("div", { style: { display: "flex", alignItems: "baseline", minWidth: 0, paddingRight: 12 } },
228
- grp("余额", h(IconMoney, null), money(balance && balance.total))),
229
- !loggedIn
230
- ? h("div", { style: { display: "flex", alignItems: "center", minWidth: 0, color: C.err, fontWeight: 600, gap: 4 } },
231
- h("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, loginHint))
232
- : h("div", { style: { display: "flex", gap: 4, alignItems: "center", minWidth: 0, flexWrap: "nowrap" } },
233
- grp("今日", h(IconMoney, null), costShown),
177
+ // 第一行:余额(24px,符号与数字 3px 间距)+ 当前档单价(灰色,跟在余额后,字号不变)
178
+ h("div", { style: { display: "flex", alignItems: "baseline", minWidth: 0, paddingRight: 14, gap: 6 } },
179
+ h("span", { style: { fontWeight: 700, fontSize: 24, color: C.label, lineHeight: "28px", marginRight: 3 } }, symbol),
180
+ h("span", { style: { fontWeight: 700, fontSize: 24, color: C.label, lineHeight: "28px" } }, money(balance && balance.total)),
181
+ h("span", { style: { display: "inline-flex", alignItems: "center", gap: 3, marginLeft: 10 } },
182
+ Price(h(IconIn, null), "输入", tier && tier.input),
234
183
  Sep(),
235
- grp(null, h(IconToken, null), fmtTokens(tok && tok.total)),
184
+ Price(h(IconCache, null), "输入缓存", tier && tier.cacheRead),
236
185
  Sep(),
237
- grp(null, h(IconApi, null), today && today.calls !== null && today.calls !== undefined ? String(today.calls) : "—")),
186
+ Price(h(IconOut, null), "输出", tier && tier.output))),
238
187
  h("span", {
239
188
  style: { position: "absolute", top: 8, right: 9, ...dot },
240
189
  title: data && data.peak ? "高峰时段 " + (data.peakHours || "") : "空闲时段",
241
190
  }),
242
- h("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, minWidth: 0 } },
243
- tier
244
- ? h("span", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: C.sub, fontSize: 10, whiteSpace: "nowrap", overflow: "hidden" } },
245
- h("span", { style: { color: C.sub, fontSize: 10, marginRight: 4 } }, "价格"),
246
- pr(h(IconIn, null), fmtPrice(tier.input)),
247
- Sep(),
248
- pr(h(IconCache, null), fmtPrice(tier.cacheRead)),
249
- Sep(),
250
- pr(h(IconOut, null), fmtPrice(tier.output)))
251
- : null,
252
- h("span", {
253
- className: "dsw-usage-time",
254
- style: { display: "inline-flex", alignItems: "center", gap: 4, color: C.sub, fontSize: 10, flex: "none" },
255
- },
256
- spinning
257
- ? h("span", {
258
- className: "dsw-usage-refresh",
259
- style: { color: C.label, display: "inline-flex", alignItems: "center" },
260
- title: "刷新中",
261
- }, h("svg", { width: 11, height: 11, viewBox: "0 0 16 16", fill: "none", style: { animation: "dsh-usage-spin .8s linear infinite" } },
262
- 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" })))
263
- : null)),
191
+ // 刷新指示:卡片右下角
192
+ h("span", {
193
+ className: "dsw-usage-refresh",
194
+ style: { position: "absolute", bottom: 8, right: 9, display: "inline-flex", alignItems: "center", color: C.sub },
195
+ title: "刷新中",
196
+ },
197
+ spinning
198
+ ? h("svg", { width: 11, height: 11, viewBox: "0 0 16 16", fill: "none", style: { animation: "dsh-usage-spin .8s linear infinite" } },
199
+ 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" }))
200
+ : null),
264
201
  errs.length
265
202
  ? h("div", { style: { color: C.err, fontSize: 10, whiteSpace: "pre-wrap" } }, errs.join(" | "))
266
203
  : null);
package/lib/index.js CHANGED
@@ -3,43 +3,29 @@
3
3
  *
4
4
  * 注册一个 HTTP 路由:
5
5
  *
6
- * GET /api/deepseek-usage[?refresh=1]
6
+ * GET /api/deepseek-usage
7
7
  *
8
8
  * 返回(JSON):
9
9
  * {
10
10
  * ok: true,
11
11
  * balance: { isAvailable, total, currency, granted, toppedUp }, // 官方余额接口(API Key)
12
- * today: {
13
- * cost, costSource: 'official' | null, // 今日消费金额(官方账单)
14
- * tokens: { input, output, cacheRead, cacheWrite, total }, // 今日 token(官方账单)
15
- * calls, // 今日 API 请求次数(官方账单)
16
- * platform: { loggedIn, loginHint? } // 平台登录态
17
- * },
18
- * peak, peakHours, // 峰谷时段(北京时间)
19
12
  * tier: { input, cacheRead, cacheWrite, output, model, known }, // 当前模型当前档单价
13
+ * peak, peakHours, // 峰谷时段(北京时间)
20
14
  * updatedAt, errors
21
15
  * }
22
16
  *
23
- * 数据源:
24
- * - 余额:DeepSeek 官方 GET /user/balance(API Key 经凭证服务解析,与 llm-deepseek
25
- * 同一引用,不出本机)。
26
- * - 今日消费/请求次数/tokens:platform.deepseek.com 官方账单接口
27
- * (/api/v0/usage/amount|cost?month=&year=),登录态 userToken 由
28
- * lib/browser-token.js 自动从本机浏览器 Local Storage 读取,无需用户配置。
29
- * 页面数据按 GMT+8,官方提示约 5 分钟延迟。
30
- * - 未检测到平台登录或登录过期:today.platform.loggedIn = false,前端提示用户
31
- * 点击卡片前往登录页。
17
+ * 数据源:余额来自 DeepSeek 官方 GET /user/balance(API Key 经凭证服务解析,
18
+ * llm-deepseek 同一引用,不出本机)。单价与峰谷时段来自内置价格表
19
+ * (lib/pricing.js,DeepSeek 官方峰谷定价快照)。
32
20
  *
33
21
  * 安全:路由仅接受本机 Host(127.0.0.1 / localhost / ::1),拦截 DNS
34
- * rebinding 与 0.0.0.0 绑定下的局域网访问;userToken 仅在进程内存中使用。
22
+ * rebinding 与 0.0.0.0 绑定下的局域网访问。
35
23
  */
36
24
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
37
- import { getPlatformToken } from "./browser-token.js";
38
25
  import {
39
26
  DEFAULT_MODEL,
40
27
  PEAK_HOURS_TEXT,
41
28
  isPeak,
42
- startOfTodayShanghaiMs,
43
29
  unitAt,
44
30
  } from "./pricing.js";
45
31
 
@@ -54,25 +40,11 @@ const BALANCE_PATH = "/user/balance";
54
40
  const ROUTE_PATH = "/api/deepseek-usage";
55
41
  const TIMEOUT_MS = 15000;
56
42
 
57
- const PLATFORM_BASE = "https://platform.deepseek.com";
58
- const PLATFORM_USAGE_HEADERS = {
59
- Accept: "application/json",
60
- Referer: "https://platform.deepseek.com/usage",
61
- Origin: "https://platform.deepseek.com",
62
- };
63
- /** userToken 内存缓存时长(浏览器 Local Storage 读取有复制开销,缓存避免高频重读)。 */
64
- const PLATFORM_TOKEN_CACHE_MS = 60000;
65
-
66
- const LOGIN_HINT = "平台未登录,点击卡片进行登录";
67
- const LOGIN_EXPIRED_HINT = "平台登录已过期,点击卡片重新登录";
68
-
69
43
  const JSON_HEADERS = {
70
44
  "content-type": "application/json; charset=utf-8",
71
45
  "cache-control": "no-store",
72
46
  };
73
47
 
74
- let tokenCache = { token: null, at: 0 };
75
-
76
48
  function sendJson(res, status, body) {
77
49
  res.writeHead(status, JSON_HEADERS);
78
50
  res.end(JSON.stringify(body));
@@ -109,122 +81,6 @@ function isLocalHost(req) {
109
81
  return host === "127.0.0.1" || host === "localhost" || host === "::1";
110
82
  }
111
83
 
112
- function hasRefreshFlag(req) {
113
- const url = req.url ?? "";
114
- const q = url.indexOf("?");
115
- if (q === -1) return false;
116
- return url.slice(q + 1).split("&").some((kv) => kv === "refresh=1");
117
- }
118
-
119
- /** 读取平台登录 token(带短缓存;refresh 时强制重读浏览器)。 */
120
- async function resolvePlatformToken(force) {
121
- const now = Date.now();
122
- if (!force && tokenCache.token !== null && now - tokenCache.at < PLATFORM_TOKEN_CACHE_MS) {
123
- return tokenCache.token;
124
- }
125
- const result = await getPlatformToken();
126
- if (result.token) {
127
- tokenCache = { token: result.token, at: now };
128
- return result.token;
129
- }
130
- tokenCache = { token: null, at: now };
131
- return null;
132
- }
133
-
134
- /**
135
- * 聚合 by_api_key/amount 响应(与平台用量页面同源)。
136
- * biz_data = { start, end, bucket, models, series: [{ api_key, model, buckets: [{ time, usage: { REQUEST, PROMPT_CACHE_HIT_TOKEN, PROMPT_CACHE_MISS_TOKEN, RESPONSE_TOKEN } }] }] }
137
- */
138
- function aggregateByApiKeyAmount(bizData) {
139
- const acc = { input: 0, cacheRead: 0, output: 0, requests: 0 };
140
- const series = bizData && Array.isArray(bizData.series) ? bizData.series : [];
141
- for (const s of series) {
142
- for (const b of s.buckets || []) {
143
- const u = b && b.usage;
144
- if (!u) continue;
145
- acc.input += Number(u.PROMPT_CACHE_MISS_TOKEN) || 0;
146
- acc.cacheRead += Number(u.PROMPT_CACHE_HIT_TOKEN) || 0;
147
- acc.output += Number(u.RESPONSE_TOKEN) || 0;
148
- acc.requests += Number(u.REQUEST) || 0;
149
- }
150
- }
151
- return acc;
152
- }
153
-
154
- /**
155
- * 聚合 by_api_key/cost 响应(与平台用量页面同源)。
156
- * biz_data = { start, end, bucket, data: [{ currency, series: [{ api_key, model, buckets: [{ time, cost }] }] }] }
157
- * 优先取 CNY 币种组(与余额币种一致),无 CNY 时取第一组。
158
- */
159
- function aggregateByApiKeyCost(bizData) {
160
- const groups = bizData && Array.isArray(bizData.data) ? bizData.data : [];
161
- const group = groups.find((g) => g && g.currency === "CNY") || groups[0];
162
- let cost = 0;
163
- for (const s of (group && group.series) || []) {
164
- for (const b of s.buckets || []) {
165
- cost += Number(b && b.cost) || 0;
166
- }
167
- }
168
- return cost;
169
- }
170
-
171
- /**
172
- * 获取官方今日用量(北京时间今天 00:00–24:00,接口与平台用量页面同源)。
173
- * @returns {{ ok: true, tokens, calls, cost } | { authError: true } | { ok: false, error }}
174
- */
175
- async function fetchOfficialToday(token) {
176
- const dayStart = Math.floor(startOfTodayShanghaiMs(Date.now()) / 1000);
177
- const dayEnd = dayStart + 86400;
178
- const headers = { ...PLATFORM_USAGE_HEADERS, Authorization: `Bearer ${token}` };
179
- let amountRes;
180
- let costRes;
181
- try {
182
- [amountRes, costRes] = await Promise.all([
183
- fetch(`${PLATFORM_BASE}/api/v0/usage/by_api_key/amount?start=${dayStart}&end=${dayEnd}&tz=28800`, {
184
- headers,
185
- signal: AbortSignal.timeout(TIMEOUT_MS),
186
- }),
187
- fetch(`${PLATFORM_BASE}/api/v0/usage/by_api_key/cost?start=${dayStart}&end=${dayEnd}&tz=28800`, {
188
- headers,
189
- signal: AbortSignal.timeout(TIMEOUT_MS),
190
- }),
191
- ]);
192
- } catch (error) {
193
- return { ok: false, error: `用量接口请求失败: ${error instanceof Error ? error.message : String(error)}` };
194
- }
195
- if (amountRes.status === 401 || amountRes.status === 403 || costRes.status === 401 || costRes.status === 403) {
196
- return { authError: true };
197
- }
198
- if (!amountRes.ok || !costRes.ok) {
199
- return { ok: false, error: `用量接口 HTTP ${amountRes.status}/${costRes.status}` };
200
- }
201
- let amountBody;
202
- let costBody;
203
- try {
204
- amountBody = await amountRes.json();
205
- costBody = await costRes.json();
206
- } catch {
207
- return { ok: false, error: "用量接口响应解析失败" };
208
- }
209
- if (amountBody.code === 40002 || amountBody.code === 40003 || costBody.code === 40002 || costBody.code === 40003) {
210
- return { authError: true };
211
- }
212
- const amounts = aggregateByApiKeyAmount(amountBody && amountBody.data && amountBody.data.biz_data);
213
- const cost = aggregateByApiKeyCost(costBody && costBody.data && costBody.data.biz_data);
214
- return {
215
- ok: true,
216
- tokens: {
217
- input: Math.round(amounts.input),
218
- cacheRead: Math.round(amounts.cacheRead),
219
- cacheWrite: 0,
220
- output: Math.round(amounts.output),
221
- total: Math.round(amounts.input + amounts.cacheRead + amounts.output),
222
- },
223
- calls: Math.round(amounts.requests),
224
- cost: Math.round(cost * 1e6) / 1e6,
225
- };
226
- }
227
-
228
84
  function apply(ctx) {
229
85
  ctx.effect(
230
86
  () => ctx.webServer.register({
@@ -238,7 +94,6 @@ function apply(ctx) {
238
94
  }
239
95
 
240
96
  const errors = [];
241
- const refresh = hasRefreshFlag(req);
242
97
 
243
98
  // ---- 余额(官方接口,API Key)----
244
99
  const balance = { isAvailable: null, total: null, currency: null, granted: null, toppedUp: null };
@@ -269,32 +124,7 @@ function apply(ctx) {
269
124
  errors.push(`余额获取失败: ${error instanceof Error ? error.message : String(error)}`);
270
125
  }
271
126
 
272
- // ---- 今日消费 / 请求次数 / tokens(官方账单,登录态自动获取)----
273
- let today = { cost: null, costSource: null, tokens: null, calls: null, platform: { loggedIn: false, loginHint: LOGIN_HINT } };
274
- const token = await resolvePlatformToken(refresh);
275
- if (token === null) {
276
- today.platform = { loggedIn: false, loginHint: LOGIN_HINT };
277
- } else {
278
- const usage = await fetchOfficialToday(token);
279
- if (usage.authError) {
280
- // 登录过期:清缓存,下次重读浏览器(用户重新登录后自动恢复)。
281
- tokenCache = { token: null, at: Date.now() };
282
- today.platform = { loggedIn: false, loginHint: LOGIN_EXPIRED_HINT };
283
- } else if (usage.ok) {
284
- today = {
285
- cost: usage.cost,
286
- costSource: "official",
287
- tokens: usage.tokens,
288
- calls: usage.calls,
289
- platform: { loggedIn: true },
290
- };
291
- } else {
292
- today.platform = { loggedIn: true };
293
- errors.push(usage.error);
294
- }
295
- }
296
-
297
- // ---- 当前模型当前档单价(展示用)----
127
+ // ---- 当前模型当前档单价 + 峰谷(展示用)----
298
128
  const now = Date.now();
299
129
  let model = DEFAULT_MODEL;
300
130
  try {
@@ -308,7 +138,6 @@ function apply(ctx) {
308
138
  sendJson(res, 200, {
309
139
  ok: true,
310
140
  balance,
311
- today,
312
141
  peak: isPeak(now),
313
142
  peakHours: PEAK_HOURS_TEXT,
314
143
  tier: {
package/lib/pricing.js CHANGED
@@ -7,12 +7,10 @@
7
7
  * 与 isKnownModel() 让调用方提示用户,而不是静默算错。
8
8
  * 价格调整时只需改 PRICE_TABLE 与 PEAK_WINDOWS。
9
9
  *
10
- * 本文件仅用于卡片第三行"当前档单价"展示;今日消费金额已直接来自官方账单
11
- * (platform.deepseek.com),不再本地按 token×价格估算。
10
+ * 本文件用于卡片"当前档单价"展示与峰谷指示灯。
12
11
  *
13
- * 时区(0.1.7 起):峰谷判定与"今日"边界一律按北京时间(Asia/Shanghai
14
- * 固定 UTC+8,无夏令时)计算,不随服务器本地时区变化——在任何时区部署的
15
- * DSH 上显示一致。
12
+ * 时区(0.1.7 起):峰谷判定按北京时间(Asia/Shanghai,固定 UTC+8,无夏令时)
13
+ * 计算,不随服务器本地时区变化——在任何时区部署的 DSH 上显示一致。
16
14
  */
17
15
 
18
16
  /** 北京时间固定偏移(上海无夏令时)。 */
@@ -39,21 +37,6 @@ export function shanghaiHour(ms) {
39
37
  return new Date(ms + SHANGHAI_OFFSET_MS).getUTCHours();
40
38
  }
41
39
 
42
- /** 某时刻所在北京日的零点(unix ms)。 */
43
- export function startOfTodayShanghaiMs(now) {
44
- const shifted = new Date(now + SHANGHAI_OFFSET_MS);
45
- return Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate()) - SHANGHAI_OFFSET_MS;
46
- }
47
-
48
- /** 某时刻所在北京日的日期键(YYYY-MM-DD),用于跨日失效。 */
49
- export function shanghaiDayKey(now) {
50
- const shifted = new Date(now + SHANGHAI_OFFSET_MS);
51
- const y = shifted.getUTCFullYear();
52
- const m = String(shifted.getUTCMonth() + 1).padStart(2, '0');
53
- const d = String(shifted.getUTCDate()).padStart(2, '0');
54
- return `${y}-${m}-${d}`;
55
- }
56
-
57
40
  /** 某时刻是否为高峰时段(按北京时间)。 */
58
41
  export function isPeak(ms) {
59
42
  const h = shanghaiHour(ms);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dougen/dsh-deepseek-usage",
3
- "version": "0.2.1",
4
- "description": "DeepSeek 用量侧边栏插件:余额(官方 API)、今日消费金额/Token/API 请求次数(官方账单,自动获取浏览器登录态)、高峰/空闲时段指示灯。",
3
+ "version": "0.3.0",
4
+ "description": "DeepSeek 用量侧边栏插件:余额(官方 API)、当前档单价与峰谷时段指示灯。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "files": [
package/screenshot.png CHANGED
Binary file
@@ -1,181 +0,0 @@
1
- /**
2
- * dsh-deepseek-usage — 本机浏览器登录态读取(零第三方依赖)。
3
- *
4
- * 通过 Chromium 系浏览器的 DevTools 调试服务(CDP,WebSocket)读取运行中
5
- * 浏览器里 platform.deepseek.com 的登录态 userToken,无需用户手动配置。
6
- *
7
- * 工作原理:
8
- * 1. 从候选浏览器的 User Data 目录读取 DevToolsActivePort 文件(浏览器开启
9
- * 远程调试时生成),得到调试端口与 WebSocket 路径;
10
- * 2. 用 Node 内置 WebSocket 连接浏览器级调试会话;
11
- * 3. 在 platform.deepseek.com 页面 target 上执行 Runtime.evaluate 读取
12
- * localStorage 中的 userToken(仅读取,不执行任何写入);
13
- * 4. 浏览器中没有该页面时自动创建一个(保留复用,读取后续登录状态)。
14
- *
15
- * 实现仅使用 Node 内置能力(WebSocket / fetch / fs),无第三方依赖;
16
- * 需要 Node 22+(原生 WebSocket)。
17
- *
18
- * 安全:token 仅在进程内存中使用,绝不落盘、不出本机(只用于向
19
- * platform.deepseek.com 官方接口发起 Bearer 认证请求)。
20
- */
21
- import { existsSync, readFileSync } from "node:fs";
22
- import { join } from "node:path";
23
-
24
- const PLATFORM_URL = "https://platform.deepseek.com";
25
- /** userToken 值的最小合理长度(防御性过滤误匹配)。 */
26
- const MIN_TOKEN_LEN = 20;
27
- /** 单个 CDP 请求超时。 */
28
- const RPC_TIMEOUT_MS = 8000;
29
- /** createTarget 后等待页面加载的时长。 */
30
- const PAGE_LOAD_WAIT_MS = 3000;
31
-
32
- /** %LOCALAPPDATA% 下候选 Chromium 系浏览器的 User Data 目录(按优先级)。 */
33
- function userDataDirs() {
34
- const local = process.env.LOCALAPPDATA;
35
- if (!local) return [];
36
- return ["Chromium", "CentBrowser", "Google\\Chrome", "Microsoft\\Edge"]
37
- .map((b) => join(local, b, "User Data"))
38
- .filter((p) => existsSync(p));
39
- }
40
-
41
- /** 读取一个 User Data 目录的 DevToolsActivePort,返回 { port, wsPath } 或 null。 */
42
- function readDevToolsEndpoint(userData) {
43
- const file = join(userData, "DevToolsActivePort");
44
- if (!existsSync(file)) return null;
45
- try {
46
- const lines = readFileSync(file, "utf8").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
47
- const port = Number(lines[0]);
48
- if (!Number.isInteger(port) || port <= 0 || port > 65535 || !lines[1]) return null;
49
- return { port, wsPath: lines[1] };
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- let conn = null; // { endpoint, ws, id, pending }
56
-
57
- /** 连接浏览器级调试会话(复用已有连接,端口/路径变化时重连)。 */
58
- async function connect(endpoint) {
59
- if (conn && conn.endpoint.port === endpoint.port && conn.endpoint.wsPath === endpoint.wsPath && conn.ws.readyState === 1) {
60
- return conn;
61
- }
62
- if (conn && conn.ws) {
63
- try { conn.ws.close(); } catch {}
64
- conn = null;
65
- }
66
- const ws = new WebSocket(`ws://127.0.0.1:${endpoint.port}${endpoint.wsPath}`);
67
- const opened = await Promise.race([
68
- new Promise((res) => { ws.onopen = () => res(true); }),
69
- new Promise((res) => { ws.onerror = () => res(false); }),
70
- new Promise((res) => setTimeout(() => res(false), 5000)),
71
- ]);
72
- if (!opened) {
73
- try { ws.close(); } catch {}
74
- return null;
75
- }
76
- conn = { endpoint, ws, id: 0, pending: new Map() };
77
- ws.onmessage = (ev) => {
78
- let msg;
79
- try { msg = JSON.parse(ev.data); } catch { return; }
80
- if (msg && msg.id && conn && conn.pending.has(msg.id)) {
81
- const { resolve, timer } = conn.pending.get(msg.id);
82
- conn.pending.delete(msg.id);
83
- clearTimeout(timer);
84
- resolve(msg);
85
- }
86
- };
87
- ws.onclose = () => { conn = null; };
88
- return conn;
89
- }
90
-
91
- /** 发送一条 CDP 命令,返回响应消息或 { timeout: true }。 */
92
- function send(conn, method, params, sessionId) {
93
- const mid = ++conn.id;
94
- const payload = sessionId !== undefined
95
- ? { id: mid, sessionId, method, params }
96
- : { id: mid, method, params };
97
- return new Promise((resolve) => {
98
- const timer = setTimeout(() => {
99
- if (conn.pending.has(mid)) {
100
- conn.pending.delete(mid);
101
- resolve({ timeout: true });
102
- }
103
- }, RPC_TIMEOUT_MS);
104
- conn.pending.set(mid, { resolve, timer });
105
- try {
106
- conn.ws.send(JSON.stringify(payload));
107
- } catch (e) {
108
- clearTimeout(timer);
109
- conn.pending.delete(mid);
110
- resolve({ error: { message: e.message } });
111
- }
112
- });
113
- }
114
-
115
- /** 在指定 target 上执行表达式,返回结果值(仅读取操作)。 */
116
- async function evalOnTarget(conn, targetId, expression) {
117
- const att = await send(conn, "Target.attachToTarget", { targetId, flatten: true });
118
- if (att.timeout || att.error || !att.result || !att.result.sessionId) return { error: "attach failed" };
119
- const sessionId = att.result.sessionId;
120
- try {
121
- const ev = await send(conn, "Runtime.evaluate", { expression, returnByValue: true }, sessionId);
122
- if (ev.timeout || ev.error) return { error: "evaluate failed" };
123
- const value = ev.result && ev.result.result ? ev.result.result.value : undefined;
124
- return { value: value === undefined ? null : value };
125
- } finally {
126
- await send(conn, "Target.detachFromTarget", { sessionId }).catch(() => {});
127
- }
128
- }
129
-
130
- /** 找到已打开的 platform 页面 target;没有则创建一个(保留复用)。 */
131
- async function resolvePlatformTarget(conn) {
132
- const r = await send(conn, "Target.getTargets");
133
- const infos = (r.result && r.result.targetInfos) || [];
134
- const existing = infos.find((t) => t.type === "page" && t.url.startsWith(PLATFORM_URL));
135
- if (existing) return existing.targetId;
136
- const created = await send(conn, "Target.createTarget", { url: PLATFORM_URL });
137
- if (created.timeout || created.error || !created.result || !created.result.targetId) return null;
138
- const targetId = created.result.targetId;
139
- // 等待页面完成加载后再读取。
140
- await new Promise((res) => setTimeout(res, PAGE_LOAD_WAIT_MS));
141
- return targetId;
142
- }
143
-
144
- /** 从一个浏览器调试端点读取 platform userToken;失败返回 null。 */
145
- async function readTokenFromEndpoint(endpoint) {
146
- const c = await connect(endpoint);
147
- if (!c) return null;
148
- const targetId = await resolvePlatformTarget(c);
149
- if (!targetId) return null;
150
- const expression =
151
- `(() => { try { const v = localStorage.getItem("userToken"); ` +
152
- `if (!v) return null; return JSON.parse(v).value || null; } catch (e) { return null; } })()`;
153
- const r = await evalOnTarget(c, targetId, expression);
154
- return r && typeof r.value === "string" && r.value.length >= MIN_TOKEN_LEN ? r.value : null;
155
- }
156
-
157
- /**
158
- * 获取 platform.deepseek.com 的登录态 userToken。
159
- * 对偶发的调试服务不可用做有限重试,避免误报"未登录"。
160
- * @returns {{ token: string } | { error: string }}
161
- */
162
- export async function getPlatformToken() {
163
- if (typeof WebSocket !== "function") {
164
- return { error: "当前 Node 版本不支持 WebSocket,无法读取浏览器登录态" };
165
- }
166
- const attempts = [0, 1];
167
- for (const attempt of attempts) {
168
- if (attempt > 0) await new Promise((res) => setTimeout(res, 800));
169
- for (const ud of userDataDirs()) {
170
- const endpoint = readDevToolsEndpoint(ud);
171
- if (!endpoint) continue;
172
- try {
173
- const token = await readTokenFromEndpoint(endpoint);
174
- if (token) return { token };
175
- } catch {
176
- // 该浏览器调试端点不可用,尝试下一个。
177
- }
178
- }
179
- }
180
- return { error: "未检测到 DeepSeek 开放平台登录" };
181
- }