@dougen/dsh-deepseek-usage 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +268 -268
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1,268 +1,268 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-deepseek-usage — host half.
|
|
3
|
-
*
|
|
4
|
-
* 注册一个 HTTP 路由:
|
|
5
|
-
*
|
|
6
|
-
* GET /api/deepseek-usage[?lang=zh|en]
|
|
7
|
-
* GET /api/deepseek-usage/holidays
|
|
8
|
-
*
|
|
9
|
-
* 返回(JSON):
|
|
10
|
-
* {
|
|
11
|
-
* ok: true,
|
|
12
|
-
* balance: { isAvailable, total, currency, granted, toppedUp }, // 官方余额接口(API Key)
|
|
13
|
-
* tier: { input, cacheRead, cacheWrite, output, model, known }, // 当前模型当前档单价
|
|
14
|
-
* next: { peak, at, ms }, // 距下次峰谷切换(界面倒计时)
|
|
15
|
-
* peak, peakHours, // 峰谷时段(北京时间)
|
|
16
|
-
* updatedAt, errors,
|
|
17
|
-
* pricing: { via, requested, used, known }, // 【内部诊断】界面不再消费
|
|
18
|
-
* }
|
|
19
|
-
*
|
|
20
|
-
* 界面只使用 balance / tier / next / peak / errors:插件内部的取价路径属于诊断信息,
|
|
21
|
-
* 保留在响应里供排查与测试,但不出现在 UI 上。
|
|
22
|
-
* errors 只放"用户能理解且需要行动"的余额类失败,不再包含"模型不在价格表"这类内部细节。
|
|
23
|
-
*
|
|
24
|
-
* `lang` 参数(zh 默认)控制错误消息与峰谷文案的语言;UI 其余文案由
|
|
25
|
-
* client 端(DSH locale 服务)本地化。
|
|
26
|
-
*
|
|
27
|
-
* 单价数据源:lib/pricing.js 的内置 PRICE_TABLE(DeepSeek 官方峰谷定价快照)。
|
|
28
|
-
* 官方调价时随插件版本更新该表——不联网抓取、不读写本地价格配置文件。
|
|
29
|
-
* 官方改名的旧模型 id(如 deepseek-v4-flash)按 MODEL_ALIASES 映射到现名计价。
|
|
30
|
-
* 峰谷时段(周一至周五、不含法定节假日 9–12、14–18,北京时间)见 lib/pricing.js。
|
|
31
|
-
*
|
|
32
|
-
* 数据源:余额来自 DeepSeek 官方 GET /user/balance(API Key 经凭证服务解析,
|
|
33
|
-
* 与 llm-deepseek 同一引用,不出本机)。本插件仅支持 DeepSeek 官方平台余额显示。
|
|
34
|
-
*
|
|
35
|
-
* 安全:路由仅接受本机 Host(127.0.0.1 / localhost / ::1),拦截 DNS
|
|
36
|
-
* rebinding 与 0.0.0.0 绑定下的局域网访问。
|
|
37
|
-
*/
|
|
38
|
-
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
39
|
-
import {
|
|
40
|
-
SHANGHAI_OFFSET_MS,
|
|
41
|
-
DEFAULT_MODEL,
|
|
42
|
-
PEAK_HOURS_TEXT,
|
|
43
|
-
PEAK_HOURS_TEXT_EN,
|
|
44
|
-
HOLIDAY_RANGES,
|
|
45
|
-
hasHolidayYear,
|
|
46
|
-
isPeak,
|
|
47
|
-
nextChange,
|
|
48
|
-
unitAt,
|
|
49
|
-
} from "./pricing.js";
|
|
50
|
-
|
|
51
|
-
const name = "dsh-deepseek-usage";
|
|
52
|
-
const inject = ["credentials", "webServer"];
|
|
53
|
-
|
|
54
|
-
const PUBLIC_BASE_URL = "https://api.deepseek.com";
|
|
55
|
-
/** 与 llm-deepseek 适配器对齐的环境变量覆盖。注意:指向第三方网关时 API Key 会发给该地址。 */
|
|
56
|
-
const BASE_URL_ENV = "DEEPSEEK_BASE_URL";
|
|
57
|
-
const API_KEY_REF = credentialRef("DEEPSEEK_API_KEY");
|
|
58
|
-
const BALANCE_PATH = "/user/balance";
|
|
59
|
-
const ROUTE_PATH = "/api/deepseek-usage";
|
|
60
|
-
/** 放假日期路由(浏览器侧启动拉一次,保证两边同一份日期表)。 */
|
|
61
|
-
const HOLIDAYS_PATH = "/api/deepseek-usage/holidays";
|
|
62
|
-
const TIMEOUT_MS = 15000;
|
|
63
|
-
|
|
64
|
-
const JSON_HEADERS = {
|
|
65
|
-
"content-type": "application/json; charset=utf-8",
|
|
66
|
-
"cache-control": "no-store",
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/** 错误消息字典(zh/en)。只保留"余额为什么拿不到"这类用户能理解、且需要行动的信息;
|
|
70
|
-
* 插件内部细节(计价来源、同步状态等)不再作为错误推给界面。 */
|
|
71
|
-
const MESSAGES = {
|
|
72
|
-
zh: {
|
|
73
|
-
noKey: "未配置 DEEPSEEK_API_KEY",
|
|
74
|
-
balanceHttp: (status) => `余额接口 HTTP ${status}`,
|
|
75
|
-
balanceFail: (detail) => `余额获取失败: ${detail}`,
|
|
76
|
-
},
|
|
77
|
-
en: {
|
|
78
|
-
noKey: "DEEPSEEK_API_KEY is not configured",
|
|
79
|
-
balanceHttp: (status) => `Balance API HTTP ${status}`,
|
|
80
|
-
balanceFail: (detail) => `Failed to fetch balance: ${detail}`,
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/** 北京时间年份。 */
|
|
85
|
-
function beijingYear(ms = Date.now()) {
|
|
86
|
-
return new Date(ms + SHANGHAI_OFFSET_MS).getUTCFullYear();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 放假日期表是否覆盖了今年:没覆盖就告警一次——否则节假日白天会被当成高峰时段,
|
|
91
|
-
* 卡片显示的高峰价比实际高,且无从察觉(见 lib/pricing.js 的 HOLIDAY_RANGES)。
|
|
92
|
-
*/
|
|
93
|
-
function warnHolidayCoverage(ctx) {
|
|
94
|
-
const year = beijingYear();
|
|
95
|
-
if (hasHolidayYear(year)) return;
|
|
96
|
-
ctx.logger.warn(
|
|
97
|
-
`dsh-deepseek-usage: 内置放假日期表未覆盖 ${year} 年(仅收录到 ${HOLIDAY_RANGES.at(-1)?.[1] ?? "未知"})`
|
|
98
|
-
+ `,${year} 年法定节假日白天会被当作高峰时段:请按国务院办公厅通知更新 lib/pricing.js 的 HOLIDAY_RANGES`
|
|
99
|
-
+ `(并同步 lib/client.js 的镜像表)`,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function sendJson(res, status, body) {
|
|
104
|
-
res.writeHead(status, JSON_HEADERS);
|
|
105
|
-
res.end(JSON.stringify(body));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function balanceUrl() {
|
|
109
|
-
const base = process.env[BASE_URL_ENV] ?? PUBLIC_BASE_URL;
|
|
110
|
-
return `${base.replace(/\/+$/, "")}${BALANCE_PATH}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** 容忍字符串/数字,返回有限数或 NaN。 */
|
|
114
|
-
function toFinite(value) {
|
|
115
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
|
|
116
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
117
|
-
const n = Number(value);
|
|
118
|
-
return Number.isFinite(n) ? n : NaN;
|
|
119
|
-
}
|
|
120
|
-
return NaN;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** 仅接受本机 Host(127.0.0.1 / localhost / ::1),拦截 DNS rebinding 与局域网访问。 */
|
|
124
|
-
function isLocalHost(req) {
|
|
125
|
-
const raw = req.headers.host;
|
|
126
|
-
if (typeof raw !== "string" || raw === "") return false;
|
|
127
|
-
let host = raw.toLowerCase();
|
|
128
|
-
if (host.startsWith("[")) {
|
|
129
|
-
const end = host.indexOf("]");
|
|
130
|
-
if (end === -1) return false;
|
|
131
|
-
host = host.slice(1, end);
|
|
132
|
-
} else {
|
|
133
|
-
const idx = host.lastIndexOf(":");
|
|
134
|
-
if (idx !== -1) host = host.slice(0, idx);
|
|
135
|
-
}
|
|
136
|
-
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** 解析 ?lang= 参数。 */
|
|
140
|
-
function resolveQuery(req) {
|
|
141
|
-
const url = req.url ?? "";
|
|
142
|
-
const q = url.indexOf("?");
|
|
143
|
-
const kv = q === -1 ? [] : url.slice(q + 1).split("&");
|
|
144
|
-
let lang = "zh";
|
|
145
|
-
for (const item of kv) {
|
|
146
|
-
if (item === "lang=en") lang = "en";
|
|
147
|
-
}
|
|
148
|
-
return { lang };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function apply(ctx) {
|
|
152
|
-
try {
|
|
153
|
-
warnHolidayCoverage(ctx);
|
|
154
|
-
} catch { /* 告警失败不影响卡片 */ }
|
|
155
|
-
|
|
156
|
-
ctx.effect(
|
|
157
|
-
() => ctx.webServer.register({
|
|
158
|
-
kind: "exact",
|
|
159
|
-
path: HOLIDAYS_PATH,
|
|
160
|
-
handler: async (req, res) => {
|
|
161
|
-
if (!isLocalHost(req)) return sendJson(res, 403, { ok: false, error: "forbidden", message: "仅允许本机访问" });
|
|
162
|
-
sendJson(res, 200, { ok: true, ranges: HOLIDAY_RANGES });
|
|
163
|
-
},
|
|
164
|
-
}),
|
|
165
|
-
"dsh-deepseek-usage: holidays route",
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
ctx.effect(
|
|
169
|
-
() => ctx.webServer.register({
|
|
170
|
-
kind: "exact",
|
|
171
|
-
path: ROUTE_PATH,
|
|
172
|
-
handler: async (req, res) => {
|
|
173
|
-
try {
|
|
174
|
-
// ---- 仅本机访问:拦截 DNS rebinding 与局域网直接访问 ----
|
|
175
|
-
if (!isLocalHost(req)) {
|
|
176
|
-
return sendJson(res, 403, { ok: false, error: "forbidden", message: "仅允许本机访问" });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const { lang } = resolveQuery(req);
|
|
180
|
-
const msg = MESSAGES[lang] || MESSAGES.zh;
|
|
181
|
-
|
|
182
|
-
// ---- 当前模型当前档单价(展示用)----
|
|
183
|
-
const now = Date.now();
|
|
184
|
-
let model = DEFAULT_MODEL;
|
|
185
|
-
try {
|
|
186
|
-
const adm = ctx.get("agentDefaultModel");
|
|
187
|
-
const selection = adm && typeof adm.currentSelection === "function" ? adm.currentSelection() : void 0;
|
|
188
|
-
if (selection && typeof selection.model === "string") model = selection.model;
|
|
189
|
-
} catch {}
|
|
190
|
-
const unit = unitAt(model, now);
|
|
191
|
-
|
|
192
|
-
const errors = [];
|
|
193
|
-
|
|
194
|
-
// ---- 余额(官方接口,API Key)----
|
|
195
|
-
const balance = { isAvailable: null, total: null, currency: null, granted: null, toppedUp: null };
|
|
196
|
-
try {
|
|
197
|
-
const hit = await ctx.credentials.resolve(API_KEY_REF);
|
|
198
|
-
if (hit === void 0) {
|
|
199
|
-
errors.push(msg.noKey);
|
|
200
|
-
} else {
|
|
201
|
-
const response = await fetch(balanceUrl(), {
|
|
202
|
-
headers: { Authorization: `Bearer ${hit.value}`, Accept: "application/json" },
|
|
203
|
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
204
|
-
});
|
|
205
|
-
const text = await response.text();
|
|
206
|
-
if (!response.ok) {
|
|
207
|
-
errors.push(msg.balanceHttp(response.status));
|
|
208
|
-
} else {
|
|
209
|
-
const body = JSON.parse(text);
|
|
210
|
-
const infos = body && Array.isArray(body.balance_infos) ? body.balance_infos : [];
|
|
211
|
-
const first = infos[0] || {};
|
|
212
|
-
balance.isAvailable = body?.is_available !== false;
|
|
213
|
-
balance.total = toFinite(first.total_balance);
|
|
214
|
-
balance.currency = typeof first.currency === "string" ? first.currency : null;
|
|
215
|
-
balance.granted = toFinite(first.granted_balance);
|
|
216
|
-
balance.toppedUp = toFinite(first.topped_up_balance);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} catch (error) {
|
|
220
|
-
errors.push(msg.balanceFail(error instanceof Error ? error.message : String(error)));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ---- 档单价:未知模型静默回退 chat 档(属内部兜底,不再作为错误推给界面)----
|
|
224
|
-
const next = nextChange(now);
|
|
225
|
-
|
|
226
|
-
sendJson(res, 200, {
|
|
227
|
-
ok: true,
|
|
228
|
-
balance,
|
|
229
|
-
peak: isPeak(now),
|
|
230
|
-
peakHours: lang === "en" ? PEAK_HOURS_TEXT_EN : PEAK_HOURS_TEXT,
|
|
231
|
-
// 距下一次峰谷切换的剩余时间(界面用它在指示灯旁显示倒计时):
|
|
232
|
-
// peak = 切换后所处的时段,at = 切换时刻,ms = 剩余毫秒
|
|
233
|
-
next,
|
|
234
|
-
tier: {
|
|
235
|
-
input: unit.input,
|
|
236
|
-
cacheRead: unit.cacheRead,
|
|
237
|
-
cacheWrite: unit.cacheWrite,
|
|
238
|
-
output: unit.output,
|
|
239
|
-
model: unit.model,
|
|
240
|
-
known: unit.known,
|
|
241
|
-
},
|
|
242
|
-
// 【内部诊断】取价路径:界面不再展示(卡片去技术化),保留字段
|
|
243
|
-
// 供排查问题与回归测试使用。
|
|
244
|
-
pricing: {
|
|
245
|
-
via: unit.via, // exact | alias | family | fallback
|
|
246
|
-
requested: unit.requested, // 当前启用的模型 id
|
|
247
|
-
used: unit.model, // 实际取价使用的模型 id
|
|
248
|
-
known: unit.known, // false = 不在内置表里,按 deepseek-chat 计价
|
|
249
|
-
},
|
|
250
|
-
updatedAt: now,
|
|
251
|
-
errors,
|
|
252
|
-
});
|
|
253
|
-
} catch (error) {
|
|
254
|
-
ctx.logger.warn("dsh-deepseek-usage: request failed");
|
|
255
|
-
ctx.logger.warn(error);
|
|
256
|
-
sendJson(res, 502, {
|
|
257
|
-
ok: false,
|
|
258
|
-
error: "internal",
|
|
259
|
-
message: error instanceof Error ? error.message : String(error),
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
}),
|
|
264
|
-
"dsh-deepseek-usage: route",
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export { name, inject, apply };
|
|
1
|
+
/**
|
|
2
|
+
* dsh-deepseek-usage — host half.
|
|
3
|
+
*
|
|
4
|
+
* 注册一个 HTTP 路由:
|
|
5
|
+
*
|
|
6
|
+
* GET /api/deepseek-usage[?lang=zh|en]
|
|
7
|
+
* GET /api/deepseek-usage/holidays
|
|
8
|
+
*
|
|
9
|
+
* 返回(JSON):
|
|
10
|
+
* {
|
|
11
|
+
* ok: true,
|
|
12
|
+
* balance: { isAvailable, total, currency, granted, toppedUp }, // 官方余额接口(API Key)
|
|
13
|
+
* tier: { input, cacheRead, cacheWrite, output, model, known }, // 当前模型当前档单价
|
|
14
|
+
* next: { peak, at, ms }, // 距下次峰谷切换(界面倒计时)
|
|
15
|
+
* peak, peakHours, // 峰谷时段(北京时间)
|
|
16
|
+
* updatedAt, errors,
|
|
17
|
+
* pricing: { via, requested, used, known }, // 【内部诊断】界面不再消费
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* 界面只使用 balance / tier / next / peak / errors:插件内部的取价路径属于诊断信息,
|
|
21
|
+
* 保留在响应里供排查与测试,但不出现在 UI 上。
|
|
22
|
+
* errors 只放"用户能理解且需要行动"的余额类失败,不再包含"模型不在价格表"这类内部细节。
|
|
23
|
+
*
|
|
24
|
+
* `lang` 参数(zh 默认)控制错误消息与峰谷文案的语言;UI 其余文案由
|
|
25
|
+
* client 端(DSH locale 服务)本地化。
|
|
26
|
+
*
|
|
27
|
+
* 单价数据源:lib/pricing.js 的内置 PRICE_TABLE(DeepSeek 官方峰谷定价快照)。
|
|
28
|
+
* 官方调价时随插件版本更新该表——不联网抓取、不读写本地价格配置文件。
|
|
29
|
+
* 官方改名的旧模型 id(如 deepseek-v4-flash)按 MODEL_ALIASES 映射到现名计价。
|
|
30
|
+
* 峰谷时段(周一至周五、不含法定节假日 9–12、14–18,北京时间)见 lib/pricing.js。
|
|
31
|
+
*
|
|
32
|
+
* 数据源:余额来自 DeepSeek 官方 GET /user/balance(API Key 经凭证服务解析,
|
|
33
|
+
* 与 llm-deepseek 同一引用,不出本机)。本插件仅支持 DeepSeek 官方平台余额显示。
|
|
34
|
+
*
|
|
35
|
+
* 安全:路由仅接受本机 Host(127.0.0.1 / localhost / ::1),拦截 DNS
|
|
36
|
+
* rebinding 与 0.0.0.0 绑定下的局域网访问。
|
|
37
|
+
*/
|
|
38
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
39
|
+
import {
|
|
40
|
+
SHANGHAI_OFFSET_MS,
|
|
41
|
+
DEFAULT_MODEL,
|
|
42
|
+
PEAK_HOURS_TEXT,
|
|
43
|
+
PEAK_HOURS_TEXT_EN,
|
|
44
|
+
HOLIDAY_RANGES,
|
|
45
|
+
hasHolidayYear,
|
|
46
|
+
isPeak,
|
|
47
|
+
nextChange,
|
|
48
|
+
unitAt,
|
|
49
|
+
} from "./pricing.js";
|
|
50
|
+
|
|
51
|
+
const name = "dsh-deepseek-usage";
|
|
52
|
+
const inject = ["credentials", "webServer"];
|
|
53
|
+
|
|
54
|
+
const PUBLIC_BASE_URL = "https://api.deepseek.com";
|
|
55
|
+
/** 与 llm-deepseek 适配器对齐的环境变量覆盖。注意:指向第三方网关时 API Key 会发给该地址。 */
|
|
56
|
+
const BASE_URL_ENV = "DEEPSEEK_BASE_URL";
|
|
57
|
+
const API_KEY_REF = credentialRef("DEEPSEEK_API_KEY");
|
|
58
|
+
const BALANCE_PATH = "/user/balance";
|
|
59
|
+
const ROUTE_PATH = "/api/deepseek-usage";
|
|
60
|
+
/** 放假日期路由(浏览器侧启动拉一次,保证两边同一份日期表)。 */
|
|
61
|
+
const HOLIDAYS_PATH = "/api/deepseek-usage/holidays";
|
|
62
|
+
const TIMEOUT_MS = 15000;
|
|
63
|
+
|
|
64
|
+
const JSON_HEADERS = {
|
|
65
|
+
"content-type": "application/json; charset=utf-8",
|
|
66
|
+
"cache-control": "no-store",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** 错误消息字典(zh/en)。只保留"余额为什么拿不到"这类用户能理解、且需要行动的信息;
|
|
70
|
+
* 插件内部细节(计价来源、同步状态等)不再作为错误推给界面。 */
|
|
71
|
+
const MESSAGES = {
|
|
72
|
+
zh: {
|
|
73
|
+
noKey: "未配置 DEEPSEEK_API_KEY",
|
|
74
|
+
balanceHttp: (status) => `余额接口 HTTP ${status}`,
|
|
75
|
+
balanceFail: (detail) => `余额获取失败: ${detail}`,
|
|
76
|
+
},
|
|
77
|
+
en: {
|
|
78
|
+
noKey: "DEEPSEEK_API_KEY is not configured",
|
|
79
|
+
balanceHttp: (status) => `Balance API HTTP ${status}`,
|
|
80
|
+
balanceFail: (detail) => `Failed to fetch balance: ${detail}`,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** 北京时间年份。 */
|
|
85
|
+
function beijingYear(ms = Date.now()) {
|
|
86
|
+
return new Date(ms + SHANGHAI_OFFSET_MS).getUTCFullYear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 放假日期表是否覆盖了今年:没覆盖就告警一次——否则节假日白天会被当成高峰时段,
|
|
91
|
+
* 卡片显示的高峰价比实际高,且无从察觉(见 lib/pricing.js 的 HOLIDAY_RANGES)。
|
|
92
|
+
*/
|
|
93
|
+
function warnHolidayCoverage(ctx) {
|
|
94
|
+
const year = beijingYear();
|
|
95
|
+
if (hasHolidayYear(year)) return;
|
|
96
|
+
ctx.logger.warn(
|
|
97
|
+
`dsh-deepseek-usage: 内置放假日期表未覆盖 ${year} 年(仅收录到 ${HOLIDAY_RANGES.at(-1)?.[1] ?? "未知"})`
|
|
98
|
+
+ `,${year} 年法定节假日白天会被当作高峰时段:请按国务院办公厅通知更新 lib/pricing.js 的 HOLIDAY_RANGES`
|
|
99
|
+
+ `(并同步 lib/client.js 的镜像表)`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sendJson(res, status, body) {
|
|
104
|
+
res.writeHead(status, JSON_HEADERS);
|
|
105
|
+
res.end(JSON.stringify(body));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function balanceUrl() {
|
|
109
|
+
const base = process.env[BASE_URL_ENV] ?? PUBLIC_BASE_URL;
|
|
110
|
+
return `${base.replace(/\/+$/, "")}${BALANCE_PATH}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 容忍字符串/数字,返回有限数或 NaN。 */
|
|
114
|
+
function toFinite(value) {
|
|
115
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
|
|
116
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
117
|
+
const n = Number(value);
|
|
118
|
+
return Number.isFinite(n) ? n : NaN;
|
|
119
|
+
}
|
|
120
|
+
return NaN;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 仅接受本机 Host(127.0.0.1 / localhost / ::1),拦截 DNS rebinding 与局域网访问。 */
|
|
124
|
+
function isLocalHost(req) {
|
|
125
|
+
const raw = req.headers.host;
|
|
126
|
+
if (typeof raw !== "string" || raw === "") return false;
|
|
127
|
+
let host = raw.toLowerCase();
|
|
128
|
+
if (host.startsWith("[")) {
|
|
129
|
+
const end = host.indexOf("]");
|
|
130
|
+
if (end === -1) return false;
|
|
131
|
+
host = host.slice(1, end);
|
|
132
|
+
} else {
|
|
133
|
+
const idx = host.lastIndexOf(":");
|
|
134
|
+
if (idx !== -1) host = host.slice(0, idx);
|
|
135
|
+
}
|
|
136
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 解析 ?lang= 参数。 */
|
|
140
|
+
function resolveQuery(req) {
|
|
141
|
+
const url = req.url ?? "";
|
|
142
|
+
const q = url.indexOf("?");
|
|
143
|
+
const kv = q === -1 ? [] : url.slice(q + 1).split("&");
|
|
144
|
+
let lang = "zh";
|
|
145
|
+
for (const item of kv) {
|
|
146
|
+
if (item === "lang=en") lang = "en";
|
|
147
|
+
}
|
|
148
|
+
return { lang };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function apply(ctx) {
|
|
152
|
+
try {
|
|
153
|
+
warnHolidayCoverage(ctx);
|
|
154
|
+
} catch { /* 告警失败不影响卡片 */ }
|
|
155
|
+
|
|
156
|
+
ctx.effect(
|
|
157
|
+
() => ctx.webServer.register({
|
|
158
|
+
kind: "exact",
|
|
159
|
+
path: HOLIDAYS_PATH,
|
|
160
|
+
handler: async (req, res) => {
|
|
161
|
+
if (!isLocalHost(req)) return sendJson(res, 403, { ok: false, error: "forbidden", message: "仅允许本机访问" });
|
|
162
|
+
sendJson(res, 200, { ok: true, ranges: HOLIDAY_RANGES });
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
"dsh-deepseek-usage: holidays route",
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
ctx.effect(
|
|
169
|
+
() => ctx.webServer.register({
|
|
170
|
+
kind: "exact",
|
|
171
|
+
path: ROUTE_PATH,
|
|
172
|
+
handler: async (req, res) => {
|
|
173
|
+
try {
|
|
174
|
+
// ---- 仅本机访问:拦截 DNS rebinding 与局域网直接访问 ----
|
|
175
|
+
if (!isLocalHost(req)) {
|
|
176
|
+
return sendJson(res, 403, { ok: false, error: "forbidden", message: "仅允许本机访问" });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { lang } = resolveQuery(req);
|
|
180
|
+
const msg = MESSAGES[lang] || MESSAGES.zh;
|
|
181
|
+
|
|
182
|
+
// ---- 当前模型当前档单价(展示用)----
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
let model = DEFAULT_MODEL;
|
|
185
|
+
try {
|
|
186
|
+
const adm = ctx.get("agentDefaultModel");
|
|
187
|
+
const selection = adm && typeof adm.currentSelection === "function" ? adm.currentSelection() : void 0;
|
|
188
|
+
if (selection && typeof selection.model === "string") model = selection.model;
|
|
189
|
+
} catch {}
|
|
190
|
+
const unit = unitAt(model, now);
|
|
191
|
+
|
|
192
|
+
const errors = [];
|
|
193
|
+
|
|
194
|
+
// ---- 余额(官方接口,API Key)----
|
|
195
|
+
const balance = { isAvailable: null, total: null, currency: null, granted: null, toppedUp: null };
|
|
196
|
+
try {
|
|
197
|
+
const hit = await ctx.credentials.resolve(API_KEY_REF);
|
|
198
|
+
if (hit === void 0) {
|
|
199
|
+
errors.push(msg.noKey);
|
|
200
|
+
} else {
|
|
201
|
+
const response = await fetch(balanceUrl(), {
|
|
202
|
+
headers: { Authorization: `Bearer ${hit.value}`, Accept: "application/json" },
|
|
203
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
204
|
+
});
|
|
205
|
+
const text = await response.text();
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
errors.push(msg.balanceHttp(response.status));
|
|
208
|
+
} else {
|
|
209
|
+
const body = JSON.parse(text);
|
|
210
|
+
const infos = body && Array.isArray(body.balance_infos) ? body.balance_infos : [];
|
|
211
|
+
const first = infos[0] || {};
|
|
212
|
+
balance.isAvailable = body?.is_available !== false;
|
|
213
|
+
balance.total = toFinite(first.total_balance);
|
|
214
|
+
balance.currency = typeof first.currency === "string" ? first.currency : null;
|
|
215
|
+
balance.granted = toFinite(first.granted_balance);
|
|
216
|
+
balance.toppedUp = toFinite(first.topped_up_balance);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
errors.push(msg.balanceFail(error instanceof Error ? error.message : String(error)));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- 档单价:未知模型静默回退 chat 档(属内部兜底,不再作为错误推给界面)----
|
|
224
|
+
const next = nextChange(now);
|
|
225
|
+
|
|
226
|
+
sendJson(res, 200, {
|
|
227
|
+
ok: true,
|
|
228
|
+
balance,
|
|
229
|
+
peak: isPeak(now),
|
|
230
|
+
peakHours: lang === "en" ? PEAK_HOURS_TEXT_EN : PEAK_HOURS_TEXT,
|
|
231
|
+
// 距下一次峰谷切换的剩余时间(界面用它在指示灯旁显示倒计时):
|
|
232
|
+
// peak = 切换后所处的时段,at = 切换时刻,ms = 剩余毫秒
|
|
233
|
+
next,
|
|
234
|
+
tier: {
|
|
235
|
+
input: unit.input,
|
|
236
|
+
cacheRead: unit.cacheRead,
|
|
237
|
+
cacheWrite: unit.cacheWrite,
|
|
238
|
+
output: unit.output,
|
|
239
|
+
model: unit.model,
|
|
240
|
+
known: unit.known,
|
|
241
|
+
},
|
|
242
|
+
// 【内部诊断】取价路径:界面不再展示(卡片去技术化),保留字段
|
|
243
|
+
// 供排查问题与回归测试使用。
|
|
244
|
+
pricing: {
|
|
245
|
+
via: unit.via, // exact | alias | family | fallback
|
|
246
|
+
requested: unit.requested, // 当前启用的模型 id
|
|
247
|
+
used: unit.model, // 实际取价使用的模型 id
|
|
248
|
+
known: unit.known, // false = 不在内置表里,按 deepseek-chat 计价
|
|
249
|
+
},
|
|
250
|
+
updatedAt: now,
|
|
251
|
+
errors,
|
|
252
|
+
});
|
|
253
|
+
} catch (error) {
|
|
254
|
+
ctx.logger.warn("dsh-deepseek-usage: request failed");
|
|
255
|
+
ctx.logger.warn(error);
|
|
256
|
+
sendJson(res, 502, {
|
|
257
|
+
ok: false,
|
|
258
|
+
error: "internal",
|
|
259
|
+
message: error instanceof Error ? error.message : String(error),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
"dsh-deepseek-usage: route",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export { name, inject, apply };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougen/dsh-deepseek-usage",
|
|
3
|
-
"version": "0.10.
|
|
4
|
-
"description": "DeepSeek
|
|
3
|
+
"version": "0.10.2",
|
|
4
|
+
"description": "A DeepSeek Harness sidebar plugin: your balance (official API), the active model's unit prices and a peak/off-peak indicator, in a Chinese/English UI. / DeepSeek Harness 侧边栏插件:余额(官方 API)、当前模型档单价与峰谷时段指示灯,界面支持中英文。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"repository": {
|