@dougen/dsh-deepseek-usage 0.1.8 → 0.2.1

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
@@ -2,103 +2,66 @@
2
2
 
3
3
  DeepSeek 用量侧边栏插件(DeepSeek Harness Web UI)
4
4
 
5
- > **⚠️ 使用须知**
6
- > - 本插件界面文案仅提供**中文**,无多语言版本。
7
- > - **仅适用于 DSH 的 Web UI**:命令行等其他非WEB界面不支持。
8
- > - **今日花费为本地估算值**(非官方账单):按本地会话日志 token × 官方峰谷价格表计算。
9
- > - **峰谷时段按北京时间(Asia/Shanghai)固定计算**,与服务器本地时区无关。
10
-
11
5
  ## 简介
12
6
 
13
- DeepSeek Harness(DSH)**网页界面**用的插件:在**侧边栏底部**常驻显示你的 **DeepSeek 账号用量**卡片 —— 余额、今日花费、今日 Token、今日 API 调用次数与高峰/空闲计费时段指示灯。
14
-
15
- ## 界面截图
7
+ DSH **网页界面**的侧边栏底部常驻显示你的 **DeepSeek 账号用量**卡片:余额、今日消费金额、今日 Token、今日 API 请求次数,以及高峰/空闲计费时段指示灯。
16
8
 
17
9
  ![DeepSeek 用量卡片](https://cdn.jsdelivr.net/npm/@dougen/dsh-deepseek-usage/screenshot.png)
18
10
 
19
- 侧边栏底部常驻卡片:余额、今日花费、今日 Token、今日 API 次数、峰谷状态灯与当前档单价。
20
-
21
11
  ## 功能
22
12
 
23
13
  - 第一行:`余额 ¥110.00`
24
- - 第二行:`今日 ¥2.94 · 36.9M · 170`(今日花费为**本地估算**)
14
+ - 第二行:`今日 2.94 · 36.9M · 170`(今日消费金额 · 今日 Token · 今日 API 请求次数)
25
15
  - 第三行:`↑1.5 · 💾0.05 · ↓4.5`(输入 / 缓存命中 / 输出 单价,按当前启用模型与峰谷时段自动切换)
26
- - 右上角:高峰/空闲状态指示灯(高峰 9:00–12:00、14:00–18:00 **北京时间**为橙色;其余空闲为绿色;悬停提示时段)
27
- - 每 60 秒自动刷新;点击卡片任意位置也可手动刷新
28
- - 跟随应用明暗主题(`--dsw-*` 变量,含兜底值)
16
+ - 右上角:高峰/空闲状态指示灯(高峰 9:00–12:00、14:00–18:00 北京时间,橙色;其余空闲,绿色)
17
+ - 每 5 分钟自动刷新;点击卡片任意位置可立即刷新
18
+ - 跟随应用明暗主题
29
19
 
30
20
  ## 安装
31
21
 
32
- 需要 DSH CLI。**从 npm 直接安装(推荐)**:
22
+ 需要 DSH CLI,从 npm 安装:
33
23
 
34
24
  ```sh
35
25
  dsh plugin --profile web add @dougen/dsh-deepseek-usage
36
26
  ```
37
27
 
38
- > 包声明了 `dsh.bundle`,`dsh plugin` 会自动把它加入 profile 的 bundle 层。
28
+ 安装后重启网页应用:`dsh web`,打开 http://127.0.0.1:3080 ,侧边栏底部(Cordis 面板按钮下方)即出现用量卡片。
39
29
 
40
- 本地开发时也可以手动安装:
41
-
42
- ```sh
43
- cd dsh-deepseek-usage
44
- pnpm pack # 生成 dougen-dsh-deepseek-usage-<version>.tgz
45
- dsh plugin --profile web add <生成的 .tgz>
46
- ```
47
-
48
- 安装之后:
49
-
50
- 1. 重启网页应用:`dsh web`(bundle 层在启动时读取)。
51
- 2. 打开 http://127.0.0.1:3080 。
52
- 3. 侧边栏底部(Cordis 面板按钮下方)即出现用量卡片。
53
-
54
- > 手动方式:把包放进 profile 的 `node_modules`,并在 `~/.dsh/profiles/web/cordis.patch.yml` 追加:
55
- >
56
- > ```yaml
57
- > - insert:
58
- > - id: deepseek-usage
59
- > name: @dougen/dsh-deepseek-usage
60
- > ```
61
-
62
- ## 数据来源与统计口径
30
+ ## 数据来源
63
31
 
64
32
  | 数据 | 来源 |
65
33
  |---|---|
66
- | 余额 | DeepSeek 官方 `GET /user/balance`(API Key 经凭证服务 `DEEPSEEK_API_KEY` 解析,不出本机) |
67
- | 今日花费 | **本地估算**:本地会话日志 token × 官方峰谷价格表(非官方账单) |
68
- | 今日 Token / API 调用数 | 本地会话日志统计(官方 API 无用量查询接口),覆盖全部会话,含重启前历史 |
69
-
70
- 统计口径(v0.1.7 起,与 DSH 官方 token-meter 对齐):
34
+ | 余额 | DeepSeek 官方 API `GET /user/balance`(使用 DSH 中已配置的 `DEEPSEEK_API_KEY`) |
35
+ | 今日消费 / Token / API 请求次数 | DeepSeek **开放平台官方账单**(platform.deepseek.com 用量页面数据,约 5 分钟延迟) |
71
36
 
72
- - **按 (turn, step) 去重**:同一调用的流式 usage 与最终消息只计一次,绝不重复计数;
73
- - **失败请求的用量也计入**:请求失败、没有最终消息时,流式 usage 样本仍被统计;
74
- - **今日 = 北京时间(Asia/Shanghai)**的自然日,与服务器时区无关;
75
- - 模型不在内置价格表时按 `deepseek-chat` 档计价,并在卡片错误提示中明示(避免静默算错)。
37
+ **无需手动配置任何 Token**:插件通过浏览器的调试接口(CDP)自动读取你本机浏览器中 DeepSeek 开放平台的登录状态。首次使用请先在浏览器中登录 [platform.deepseek.com](https://platform.deepseek.com)
76
38
 
77
- ## 安全说明
39
+ > 需要你的浏览器开启**远程调试**(一次性设置,设置后长期有效):在地址栏打开 `chrome://inspect/#remote-debugging`,勾选"允许远程调试";或用命令行启动浏览器时加参数 `--remote-debugging-port=9222`。
78
40
 
79
- - 用量路由 `GET /api/deepseek-usage` **仅接受本机 Host**(`127.0.0.1` / `localhost` / `::1`),可拦截 DNS rebinding 与局域网直接访问。
80
- - ⚠️ 若将 DSH 配置为 `host: 0.0.0.0` 对外网开放,本插件会在入口拒绝非本机 Host 的请求,但**请勿依赖插件作为唯一防线**——余额为账号敏感信息,建议用反向代理 + 鉴权保护整个 DSH 服务。
81
- - ⚠️ 环境变量 `DEEPSEEK_BASE_URL`(与 llm-deepseek 适配器对齐)会把余额请求与 **API Key 一起发给该地址**;仅在你信任该网关/代理时配置。
41
+ ### 未登录时
82
42
 
83
- ## 变更记录
43
+ 如果检测不到平台登录,卡片第二行会显示 **`平台未登录,点击卡片进行登录`**:
84
44
 
85
- ### v0.1.8
45
+ 1. 点击卡片,浏览器会打开 DeepSeek 开放平台页面
46
+ 2. 登录完成后回到 DSH,再次点击卡片(或等待自动刷新)即可显示今日用量
86
47
 
87
- - **修复 0.1.7 回归:今日用量统计全为 0**。根因:`listSnapshots()` 返回的会话条目是 `{ header: SessionHeader, revision }`,id 位于 `header.id` 嵌套字段;0.1.7 误取顶层 `header.id`(undefined),导致所有会话 `readRaw` 失败被跳过(skipped>0、统计为 0)。已修正并从真实会话日志端到端验证:统计恢复、无读取失败。
48
+ ## 常见问题
88
49
 
89
- ### v0.1.7
50
+ **登录了平台但卡片仍显示未登录?**
51
+ - 请确认登录的是 [platform.deepseek.com](https://platform.deepseek.com)(开放平台),而不是 chat.deepseek.com(网页版对话)。
52
+ - 请确认浏览器已开启远程调试(见上文"数据来源"),设置后可点击卡片强制刷新一次。
53
+ - 支持的浏览器:Chromium 内核(Chromium / Chrome / Edge / CentBrowser 等)。
90
54
 
91
- - **统计口径修正**:按 (turn, step) 去重并吸收流式 usage 样本,失败请求的用量不再漏计(P1)。
92
- - **性能优化**:按会话修订值增量扫描,只重读变化的会话日志;跨北京日自动失效缓存(P2)。
93
- - **安全加固**:路由仅接受本机 Host,拦截 DNS rebinding 与局域网访问(P3)。
94
- - **字段语义修正**:余额响应 `available`(误用官方 `is_available`)改为 `isAvailable` 布尔(P4)。
95
- - **未知模型提示**:价格表外模型按 chat 档计价并明示,不再静默算错(P5)。
96
- - **时区修正**:峰谷判定与"今日"边界改为固定北京时间,不随服务器时区漂移(P6)。
97
- - **依赖声明**:补充 `@deepseek-ai/dsh-host-webserver` peer 依赖(P8)。
55
+ **为什么显示"平台未登录"?**
56
+ - 浏览器未开启远程调试:在地址栏打开 `chrome://inspect/#remote-debugging`,勾选"允许远程调试"后重启浏览器(设置会持久保留)。
57
+ - 或启动浏览器时加参数:`--remote-debugging-port=9222`。
58
+ - 登录状态已过期:点击卡片重新登录即可。
98
59
 
99
- ### v0.1.6
60
+ **今日数据与平台页面不一致?**
61
+ - 官方用量数据本身有约 5 分钟延迟,且按北京时间(GMT+8)统计。
100
62
 
101
- - 初始发布(审计基线)。
63
+ **支持哪些浏览器?**
64
+ - Chromium 内核浏览器均可(自动探测本机安装的 Chromium / Chrome / Edge / CentBrowser)。
102
65
 
103
66
  ## 卸载
104
67
 
@@ -0,0 +1,181 @@
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
+ }
package/lib/client.js CHANGED
@@ -15,15 +15,14 @@ window.__ModuleLoader__.load({
15
15
  const h = react.createElement;
16
16
 
17
17
  // ---- constants -------------------------------------------------
18
- const POLL_MS = 60 * 1000;
18
+ const POLL_MS = 5 * 60 * 1000;
19
19
  const USAGE_PATH = "/api/deepseek-usage";
20
20
 
21
21
  // ---- helpers ---------------------------------------------------
22
- function money(value, currency) {
22
+ function money(value) {
23
23
  const n = Number(value);
24
24
  if (!Number.isFinite(n)) return "—";
25
- const symbol = currency === "USD" ? "$" : "¥";
26
- return `${symbol}${n.toFixed(2)}`;
25
+ return n.toFixed(2);
27
26
  }
28
27
 
29
28
  function fmtTokens(value) {
@@ -90,11 +89,12 @@ window.__ModuleLoader__.load({
90
89
  const [error, setError] = useState("");
91
90
  const [spinning, setSpinning] = useState(false);
92
91
  const mounted = useRef(true);
92
+ const retryRef = useRef(null);
93
93
 
94
- const load = useCallback(async () => {
94
+ const load = useCallback(async (force) => {
95
95
  setSpinning(true);
96
96
  try {
97
- const res = await fetch(USAGE_PATH, { cache: "no-store" });
97
+ const res = await fetch(USAGE_PATH + (force ? "?refresh=1" : ""), { cache: "no-store" });
98
98
  let body = null;
99
99
  try {
100
100
  body = await res.json();
@@ -116,17 +116,20 @@ window.__ModuleLoader__.load({
116
116
  useEffect(() => {
117
117
  mounted.current = true;
118
118
  load();
119
- const timer = setInterval(load, POLL_MS);
119
+ const timer = setInterval(() => load(false), POLL_MS);
120
120
  return () => {
121
121
  mounted.current = false;
122
122
  clearInterval(timer);
123
+ if (retryRef.current) {
124
+ clearInterval(retryRef.current);
125
+ retryRef.current = null;
126
+ }
123
127
  };
124
128
  }, [load]);
125
129
 
126
130
  const balance = data && data.balance ? data.balance : null;
127
131
  const today = data && data.today ? data.today : null;
128
132
  const tier = data && data.tier ? data.tier : null;
129
- const currency = balance && balance.currency ? balance.currency : "CNY";
130
133
  const errs = (Array.isArray(data && data.errors) ? data.errors : []).concat(error ? [error] : []);
131
134
  const tok = today && today.tokens ? today.tokens : null;
132
135
  const dot = {
@@ -137,7 +140,28 @@ window.__ModuleLoader__.load({
137
140
  background: data && data.peak ? C.peak : C.idle,
138
141
  boxShadow: "0 0 4px rgba(0,0,0,.3)",
139
142
  };
140
- const title = "DeepSeek 用量 · 点击刷新" + (errs.length ? " · " + errs.join("; ") : "");
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("; ") : "");
141
165
 
142
166
  if (!wide) {
143
167
  return h("div", {
@@ -157,9 +181,9 @@ window.__ModuleLoader__.load({
157
181
  userSelect: "none",
158
182
  },
159
183
  title,
160
- onClick: load,
184
+ onClick: handleClick,
161
185
  },
162
- h("span", { style: { fontWeight: 700, fontSize: 12, color: C.label } }, money(balance && balance.total, currency)),
186
+ h("span", { style: { fontWeight: 700, fontSize: 12, color: C.label } }, money(balance && balance.total)),
163
187
  h("span", { style: dot, title: data && data.peak ? "高峰时段" : "空闲时段" }));
164
188
  }
165
189
 
@@ -174,7 +198,7 @@ window.__ModuleLoader__.load({
174
198
  h("span", { style: { fontWeight: 600, whiteSpace: "nowrap" } }, value));
175
199
  const Sep = () => h("span", { style: { color: C.sub, margin: "0 1px", fontSize: 10 } }, "·");
176
200
 
177
- const costText = money(today && today.cost, currency);
201
+ const costText = money(today && today.cost);
178
202
  const costShown = costText;
179
203
 
180
204
  return h("div", {
@@ -198,16 +222,19 @@ window.__ModuleLoader__.load({
198
222
  lineHeight: "15px",
199
223
  },
200
224
  title,
201
- onClick: load,
225
+ onClick: handleClick,
202
226
  },
203
227
  h("div", { style: { display: "flex", alignItems: "baseline", minWidth: 0, paddingRight: 12 } },
204
- grp("余额", h(IconMoney, null), money(balance && balance.total, currency))),
205
- h("div", { style: { display: "flex", gap: 4, alignItems: "center", minWidth: 0, flexWrap: "nowrap" } },
206
- grp("今日", h(IconMoney, null), costShown),
207
- Sep(),
208
- grp(null, h(IconToken, null), fmtTokens(tok && tok.total)),
209
- Sep(),
210
- grp(null, h(IconApi, null), today && today.calls !== null && today.calls !== undefined ? String(today.calls) : "—")),
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),
234
+ Sep(),
235
+ grp(null, h(IconToken, null), fmtTokens(tok && tok.total)),
236
+ Sep(),
237
+ grp(null, h(IconApi, null), today && today.calls !== null && today.calls !== undefined ? String(today.calls) : "—")),
211
238
  h("span", {
212
239
  style: { position: "absolute", top: 8, right: 9, ...dot },
213
240
  title: data && data.peak ? "高峰时段 " + (data.peakHours || "") : "空闲时段",
package/lib/index.js CHANGED
@@ -3,50 +3,42 @@
3
3
  *
4
4
  * 注册一个 HTTP 路由:
5
5
  *
6
- * GET /api/deepseek-usage
6
+ * GET /api/deepseek-usage[?refresh=1]
7
7
  *
8
8
  * 返回(JSON):
9
9
  * {
10
10
  * ok: true,
11
- * balance: { isAvailable, total, currency, granted, toppedUp }, // 官方余额接口
11
+ * balance: { isAvailable, total, currency, granted, toppedUp }, // 官方余额接口(API Key)
12
12
  * today: {
13
- * cost, // 今日花费:本地日志估算(日志 token × 官方峰谷价格表)
14
- * costSource: 'estimate' | null,
15
- * tokens: { input, output, cacheRead, cacheWrite, total }, // 今日 token 数(本地日志)
16
- * calls, // 今日有用量报告的 API 调用数(本地日志,按 turn/step 去重)
17
- * sessions // 今日有活动的会话数
13
+ * cost, costSource: 'official' | null, // 今日消费金额(官方账单)
14
+ * tokens: { input, output, cacheRead, cacheWrite, total }, // 今日 token(官方账单)
15
+ * calls, // 今日 API 请求次数(官方账单)
16
+ * platform: { loggedIn, loginHint? } // 平台登录态
18
17
  * },
19
- * peak, peakHours, // 峰谷时段(北京时间)
20
- * tier: { input, cacheRead, cacheWrite, output, model, known }, // 当前模型当前档单价
18
+ * peak, peakHours, // 峰谷时段(北京时间)
19
+ * tier: { input, cacheRead, cacheWrite, output, model, known }, // 当前模型当前档单价
21
20
  * updatedAt, errors
22
21
  * }
23
22
  *
24
- * 余额来自 DeepSeek 官方 GET /user/balance(API Key 经凭证服务解析,与
25
- * llm-deepseek 同一引用,不出本机)。今日 Token / API 次数只能从本地会话
26
- * 日志统计(官方无用量查询接口);今日花费为本地估算:
27
- * 日志 token × 官方峰谷价格表(见 lib/pricing.js)。
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
+ * 点击卡片前往登录页。
28
32
  *
29
- * 统计口径(0.1.7 起,与 DSH 官方 token-meter 对齐):
30
- * - (turn, step) 去重:同一调用的流式 usage chunk 与最终 assistant/message
31
- * 只计一次,后者覆盖前者(last-wins);
32
- * - 同时吸收 assistant/chunk 的 usage 样本,因此"请求失败、没有最终
33
- * assistant/message"的调用其用量仍被计入(chunk 样本存活);
34
- * - calls = 今日有用量报告的唯一调用数(近似 API 请求数,含失败请求)。
35
- *
36
- * 性能(0.1.7 起):按会话增量扫描——持久化后端会话修订值
37
- * (listSnapshots)未变的会话复用缓存的聚合结果,只重读变化的会话;
38
- * 跨北京日自动失效全部缓存。
39
- *
40
- * 安全(0.1.7 起):路由仅接受 Host 为 127.0.0.1 / localhost / ::1 的请求,
41
- * 拒绝 DNS rebinding 与 0.0.0.0 绑定下的局域网访问。
33
+ * 安全:路由仅接受本机 Host(127.0.0.1 / localhost / ::1),拦截 DNS
34
+ * rebinding 0.0.0.0 绑定下的局域网访问;userToken 仅在进程内存中使用。
42
35
  */
43
36
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
37
+ import { getPlatformToken } from "./browser-token.js";
44
38
  import {
45
39
  DEFAULT_MODEL,
46
40
  PEAK_HOURS_TEXT,
47
- costOf,
48
41
  isPeak,
49
- shanghaiDayKey,
50
42
  startOfTodayShanghaiMs,
51
43
  unitAt,
52
44
  } from "./pricing.js";
@@ -61,14 +53,26 @@ const API_KEY_REF = credentialRef("DEEPSEEK_API_KEY");
61
53
  const BALANCE_PATH = "/user/balance";
62
54
  const ROUTE_PATH = "/api/deepseek-usage";
63
55
  const TIMEOUT_MS = 15000;
64
- /** 本地用量扫描的短缓存,避免多标签页/轮询反复解码全部日志。 */
65
- const USAGE_CACHE_MS = 20000;
56
+
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 = "平台登录已过期,点击卡片重新登录";
66
68
 
67
69
  const JSON_HEADERS = {
68
70
  "content-type": "application/json; charset=utf-8",
69
71
  "cache-control": "no-store",
70
72
  };
71
73
 
74
+ let tokenCache = { token: null, at: 0 };
75
+
72
76
  function sendJson(res, status, body) {
73
77
  res.writeHead(status, JSON_HEADERS);
74
78
  res.end(JSON.stringify(body));
@@ -105,171 +109,120 @@ function isLocalHost(req) {
105
109
  return host === "127.0.0.1" || host === "localhost" || host === "::1";
106
110
  }
107
111
 
108
- /**
109
- * (turn, step) 去重键:同一调用的 usage chunk 与最终 message 共享 key,
110
- * 后者覆盖前者;缺少 turn/step 时退化为按事件 seq 独立计(罕见,日志
111
- * 规范下不该发生)。
112
- */
113
- function stepKey(turn, step, seq) {
114
- if (typeof turn === "number" && typeof step === "number") return `t:${turn}:s:${step}`;
115
- return `e:${seq}`;
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");
116
117
  }
117
118
 
118
- function freshAgg() {
119
- return { calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, unknownModels: new Set() };
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;
120
132
  }
121
133
 
122
- function addAgg(target, src) {
123
- target.calls += src.calls;
124
- target.input += src.input;
125
- target.output += src.output;
126
- target.cacheRead += src.cacheRead;
127
- target.cacheWrite += src.cacheWrite;
128
- target.cost += src.cost;
129
- for (const m of src.unknownModels) target.unknownModels.add(m);
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;
130
152
  }
131
153
 
132
154
  /**
133
- * 顺序扫描一个会话的原始日志,返回该会话的今日聚合;无今日样本返回 null。
134
- * token-meter 相同的 last-wins 语义:同一 (turn, step) usage 样本
135
- * (流式 chunk 或最终 message)只计最后一次,绝不重复计数。
155
+ * 聚合 by_api_key/cost 响应(与平台用量页面同源)。
156
+ * biz_data = { start, end, bucket, data: [{ currency, series: [{ api_key, model, buckets: [{ time, cost }] }] }] }
157
+ * 优先取 CNY 币种组(与余额币种一致),无 CNY 时取第一组。
136
158
  */
137
- function scanSessionRaw(content, dayStart, fallbackModel) {
138
- const agg = freshAgg();
139
- const steps = new Map();
140
- for (const line of content.split("\n")) {
141
- if (line === "") continue;
142
- let event;
143
- try {
144
- event = JSON.parse(line);
145
- } catch {
146
- continue;
147
- }
148
- if (event === null || typeof event !== "object" || typeof event.time !== "number" || event.time < dayStart) continue;
149
- const data = event.data;
150
- if (data === null || typeof data !== "object") continue;
151
- let usage;
152
- let model;
153
- if (
154
- event.type === "assistant/chunk" &&
155
- data.chunk !== null &&
156
- typeof data.chunk === "object" &&
157
- data.chunk.type === "usage" &&
158
- data.chunk.usage
159
- ) {
160
- // 流式 usage 样本:失败请求没有最终 message 时仍存活,必须计入。
161
- usage = data.chunk.usage;
162
- model = fallbackModel;
163
- } else if (event.type === "assistant/message" && data.usage) {
164
- // 最终样本:覆盖同 (turn, step) 的早期 chunk 样本。
165
- usage = data.usage;
166
- const source = data.message && data.message.source;
167
- model = source && typeof source.model === "string" ? source.model : fallbackModel;
168
- } else {
169
- continue;
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;
170
166
  }
171
- steps.set(stepKey(data.turn, data.step, event.seq), { usage, model, time: event.time });
172
167
  }
173
- if (steps.size === 0) return null;
174
- for (const { usage, model, time } of steps.values()) {
175
- const unit = unitAt(model, time);
176
- if (!unit.known) agg.unknownModels.add(model);
177
- agg.calls += 1;
178
- agg.input += usage.inputTokens || 0;
179
- agg.output += usage.outputTokens || 0;
180
- agg.cacheRead += usage.cacheReadTokens || 0;
181
- agg.cacheWrite += usage.cacheWriteTokens || 0;
182
- agg.cost += costOf(usage, unit);
183
- }
184
- return agg;
168
+ return cost;
185
169
  }
186
170
 
187
- let usageCache = null;
188
- let usageCacheAt = 0;
189
- let usageCacheDay = "";
190
- /** sessionId -> { revision, agg },per-session 增量聚合缓存(P2)。 */
191
- let sessionAggCache = new Map();
192
-
193
171
  /**
194
- * 扫描全部会话日志,统计今日(北京时间)token 用量 / API 调用数 / 花费估算。
195
- * 增量策略:会话修订值(listSnapshots)未变 复用缓存聚合;只重读变化的
196
- * 会话;跨北京日(shanghaiDayKey 变化)时全部失效重扫。
172
+ * 获取官方今日用量(北京时间今天 00:00–24:00,接口与平台用量页面同源)。
173
+ * @returns {{ ok: true, tokens, calls, cost } | { authError: true } | { ok: false, error }}
197
174
  */
198
- async function scanTodayUsage(ctx) {
199
- const persistence = ctx.get("sessionPersistence");
200
- if (persistence === void 0 || typeof persistence.list !== "function" || typeof persistence.readRaw !== "function") {
201
- return { usable: false, error: "sessionPersistence 不可用" };
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)}` };
202
194
  }
203
- const now = Date.now();
204
- const dayKey = shanghaiDayKey(now);
205
- if (usageCache !== null && usageCacheDay === dayKey && now - usageCacheAt < USAGE_CACHE_MS) return usageCache;
206
- if (usageCacheDay !== dayKey) {
207
- usageCacheDay = dayKey;
208
- sessionAggCache.clear();
195
+ if (amountRes.status === 401 || amountRes.status === 403 || costRes.status === 401 || costRes.status === 403) {
196
+ return { authError: true };
209
197
  }
210
- const dayStart = startOfTodayShanghaiMs(now);
211
- const acc = freshAgg();
212
- acc.usable = true;
213
- acc.sessions = 0;
214
- acc.skipped = 0;
215
- let defaultModel = DEFAULT_MODEL;
216
- try {
217
- const adm = ctx.get("agentDefaultModel");
218
- const selection = adm && typeof adm.currentSelection === "function" ? adm.currentSelection() : void 0;
219
- if (selection && typeof selection.model === "string") defaultModel = selection.model;
220
- } catch {}
198
+ if (!amountRes.ok || !costRes.ok) {
199
+ return { ok: false, error: `用量接口 HTTP ${amountRes.status}/${costRes.status}` };
200
+ }
201
+ let amountBody;
202
+ let costBody;
221
203
  try {
222
- const hasSnapshots = typeof persistence.listSnapshots === "function";
223
- const headers = hasSnapshots ? await persistence.listSnapshots() : await persistence.list();
224
- const seen = new Set();
225
- for (const header of headers) {
226
- // listSnapshots 返回 { header: SessionHeader, revision },id 在 header.header.id;
227
- // list 直接返回 SessionHeader[](顶层 id)。
228
- const id = hasSnapshots ? header.header.id : header.id;
229
- if (typeof id !== "string" || id === "") continue;
230
- seen.add(id);
231
- const revision = hasSnapshots ? header.revision : void 0;
232
- const cached = sessionAggCache.get(id);
233
- if (cached !== void 0 && revision !== void 0 && cached.revision === revision) {
234
- if (cached.agg !== null) {
235
- addAgg(acc, cached.agg);
236
- acc.sessions += 1;
237
- }
238
- continue;
239
- }
240
- let raw;
241
- try {
242
- raw = await persistence.readRaw(id);
243
- } catch {
244
- acc.skipped += 1;
245
- sessionAggCache.delete(id);
246
- continue;
247
- }
248
- if (raw === void 0 || raw === null || typeof raw.content !== "string") {
249
- sessionAggCache.delete(id);
250
- continue;
251
- }
252
- const agg = scanSessionRaw(raw.content, dayStart, defaultModel);
253
- sessionAggCache.set(id, { revision, agg });
254
- if (agg === null) continue;
255
- addAgg(acc, agg);
256
- acc.sessions += 1;
257
- }
258
- if (hasSnapshots) {
259
- // 修剪已被删除的会话缓存。
260
- for (const id of sessionAggCache.keys()) {
261
- if (!seen.has(id)) sessionAggCache.delete(id);
262
- }
263
- }
264
- } catch (error) {
265
- acc.usable = false;
266
- acc.error = error instanceof Error ? error.message : String(error);
204
+ amountBody = await amountRes.json();
205
+ costBody = await costRes.json();
206
+ } catch {
207
+ return { ok: false, error: "用量接口响应解析失败" };
267
208
  }
268
- acc.cost = Math.round(acc.cost * 1e6) / 1e6;
269
- usageCache = acc;
270
- usageCacheAt = now;
271
- usageCacheDay = dayKey;
272
- return acc;
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
+ };
273
226
  }
274
227
 
275
228
  function apply(ctx) {
@@ -279,14 +232,15 @@ function apply(ctx) {
279
232
  path: ROUTE_PATH,
280
233
  handler: async (req, res) => {
281
234
  try {
282
- // ---- 仅本机访问(P3):拦截 DNS rebinding 与局域网直接访问 ----
235
+ // ---- 仅本机访问:拦截 DNS rebinding 与局域网直接访问 ----
283
236
  if (!isLocalHost(req)) {
284
237
  return sendJson(res, 403, { ok: false, error: "forbidden", message: "仅允许本机访问" });
285
238
  }
286
239
 
287
240
  const errors = [];
241
+ const refresh = hasRefreshFlag(req);
288
242
 
289
- // ---- 余额(官方接口)----
243
+ // ---- 余额(官方接口,API Key)----
290
244
  const balance = { isAvailable: null, total: null, currency: null, granted: null, toppedUp: null };
291
245
  try {
292
246
  const hit = await ctx.credentials.resolve(API_KEY_REF);
@@ -315,13 +269,32 @@ function apply(ctx) {
315
269
  errors.push(`余额获取失败: ${error instanceof Error ? error.message : String(error)}`);
316
270
  }
317
271
 
318
- // ---- 今日 token / API 调用数 / 花费估算(本地日志)----
319
- const usage = await scanTodayUsage(ctx);
320
- if (!usage.usable) errors.push(usage.error ?? "今日用量统计不可用");
321
- if (usage.usable && usage.unknownModels.size > 0) {
322
- errors.push(`未知模型按 chat 档计价: ${[...usage.unknownModels].join(", ")}`);
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
+ }
323
295
  }
324
296
 
297
+ // ---- 当前模型当前档单价(展示用)----
325
298
  const now = Date.now();
326
299
  let model = DEFAULT_MODEL;
327
300
  try {
@@ -330,29 +303,12 @@ function apply(ctx) {
330
303
  if (selection && typeof selection.model === "string") model = selection.model;
331
304
  } catch {}
332
305
  const unit = unitAt(model, now);
333
-
334
- const cost = usage.usable ? usage.cost : null;
335
- const costSource = usage.usable ? "estimate" : null;
306
+ if (!unit.known) errors.push(`当前模型 ${model} 不在价格表,档单价按 deepseek-chat 展示`);
336
307
 
337
308
  sendJson(res, 200, {
338
309
  ok: true,
339
310
  balance,
340
- today: {
341
- cost,
342
- costSource,
343
- tokens: usage.usable
344
- ? {
345
- input: usage.input,
346
- output: usage.output,
347
- cacheRead: usage.cacheRead,
348
- cacheWrite: usage.cacheWrite,
349
- total: usage.input + usage.output + usage.cacheRead + usage.cacheWrite,
350
- }
351
- : null,
352
- calls: usage.usable ? usage.calls : null,
353
- sessions: usage.usable ? usage.sessions : null,
354
- skipped: usage.usable ? usage.skipped : null,
355
- },
311
+ today,
356
312
  peak: isPeak(now),
357
313
  peakHours: PEAK_HOURS_TEXT,
358
314
  tier: {
package/lib/pricing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dsh-deepseek-usage — 定价引擎(元 / 百万 tokens)。
2
+ * dsh-deepseek-usage — 定价表(元 / 百万 tokens)。
3
3
  *
4
4
  * DeepSeek 官方价格快照(2026-08-17 起 V4 系列峰谷定价,北京时间):
5
5
  * 高峰时段 9:00–12:00 与 14:00–18:00(北京时间),其余为空闲时段(空闲=高峰半价)。
@@ -7,6 +7,9 @@
7
7
  * 与 isKnownModel() 让调用方提示用户,而不是静默算错。
8
8
  * 价格调整时只需改 PRICE_TABLE 与 PEAK_WINDOWS。
9
9
  *
10
+ * 本文件仅用于卡片第三行"当前档单价"展示;今日消费金额已直接来自官方账单
11
+ * (platform.deepseek.com),不再本地按 token×价格估算。
12
+ *
10
13
  * 时区(0.1.7 起):峰谷判定与"今日"边界一律按北京时间(Asia/Shanghai,
11
14
  * 固定 UTC+8,无夏令时)计算,不随服务器本地时区变化——在任何时区部署的
12
15
  * DSH 上显示一致。
@@ -79,14 +82,3 @@ export function unitAt(model, ms) {
79
82
  known,
80
83
  };
81
84
  }
82
-
83
- /** 按单位价格计算一次 usage 的估算成本(元)。 */
84
- export function costOf(usage, unit) {
85
- return (
86
- (usage.inputTokens * unit.input +
87
- (usage.cacheReadTokens || 0) * unit.cacheRead +
88
- (usage.cacheWriteTokens || 0) * unit.cacheWrite +
89
- usage.outputTokens * unit.output) /
90
- 1e6
91
- );
92
- }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dougen/dsh-deepseek-usage",
3
- "version": "0.1.8",
4
- "description": "DeepSeek 用量侧边栏插件:余额(官方 API)、今日花费(本地日志估算)、今日 TokenAPI 次数(本地日志)、高峰/空闲时段指示灯。",
3
+ "version": "0.2.1",
4
+ "description": "DeepSeek 用量侧边栏插件:余额(官方 API)、今日消费金额/Token/API 请求次数(官方账单,自动获取浏览器登录态)、高峰/空闲时段指示灯。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "files": [
package/screenshot.png CHANGED
Binary file