@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 +12 -31
- package/lib/client.js +36 -99
- package/lib/index.js +7 -178
- package/lib/pricing.js +3 -20
- package/package.json +2 -2
- package/screenshot.png +0 -0
- package/lib/browser-token.js +0 -181
package/README.md
CHANGED
|
@@ -4,17 +4,16 @@ DeepSeek 用量侧边栏插件(DeepSeek Harness Web UI)
|
|
|
4
4
|
|
|
5
5
|
## 简介
|
|
6
6
|
|
|
7
|
-
在 DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek
|
|
7
|
+
在 DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号余额**(放大显示)与**当前模型档单价**卡片,并提供高峰/空闲计费时段指示灯。
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
## 功能
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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
|
-
|
|
|
34
|
+
| 档单价 / 峰谷时段 | 内置价格表(DeepSeek 官方峰谷定价快照,2026-08-17 起) |
|
|
36
35
|
|
|
37
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
// 每
|
|
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",
|
|
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 (
|
|
76
|
+
const load = useCallback(async () => {
|
|
95
77
|
setSpinning(true);
|
|
96
78
|
try {
|
|
97
|
-
const res = await fetch(USAGE_PATH
|
|
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(
|
|
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
|
|
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:
|
|
140
|
+
onClick: load,
|
|
185
141
|
},
|
|
186
|
-
h("span", { style: {
|
|
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
|
|
191
|
-
h("span", { style: { display: "inline-flex",
|
|
192
|
-
|
|
193
|
-
h("span", { style: {
|
|
194
|
-
|
|
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:
|
|
175
|
+
onClick: load,
|
|
226
176
|
},
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
184
|
+
Price(h(IconCache, null), "输入缓存", tier && tier.cacheRead),
|
|
236
185
|
Sep(),
|
|
237
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
// ----
|
|
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
|
|
14
|
-
*
|
|
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.
|
|
4
|
-
"description": "DeepSeek 用量侧边栏插件:余额(官方 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
|
package/lib/browser-token.js
DELETED
|
@@ -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
|
-
}
|