dsh-subusage 0.8.3 → 0.9.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 +96 -6
- package/lib/client.js +163 -18
- package/lib/index.js +596 -17
- package/lib/volcengine.js +153 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// 火山方舟(Volcengine Ark)订阅额度适配:管控面 OpenAPI 的 AK/SK 签名与套餐窗口归一化。
|
|
2
|
+
//
|
|
3
|
+
// 额度接口只在管控面 OpenAPI 暴露,鉴权是 IAM Access Key 的 HMAC-SHA256 签名(V4 变体),
|
|
4
|
+
// 与推理用的方舟 API Key(Bearer / UUID 形态)是**两套完全不同的凭据**:
|
|
5
|
+
// 拿推理 Key 去签名只会得到 401/403,与接口可用性无关。
|
|
6
|
+
//
|
|
7
|
+
// 套餐额度分散在多个 Action 上,且官方 API 概览只公开了其中一部分(Team 系):
|
|
8
|
+
// - GetCodingPlanUsage 个人版 Coding Plan(Level + Percent,按次数计)
|
|
9
|
+
// - GetAFPUsage 个人版 Agent Plan(AFP 绝对值,5 小时/日/周/月四个滚动窗口)
|
|
10
|
+
// - GetSeatInfoUsage / ListSeatInfoUsages Coding Plan 企业版席位(需 SeatID)
|
|
11
|
+
// - GetSeatAFPUsage Agent Plan 企业版席位(需 SeatIDs)
|
|
12
|
+
// 本插件按 provider 固定调用其中一个 Action:路由由官方插件 ark-plan-api 注册,
|
|
13
|
+
// ark-coding-plan-* 走 Coding Plan,ark-agent-plan-* 走 Agent Plan。
|
|
14
|
+
import { createHash, createHmac } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
export const VOLC_HOST = "open.volcengineapi.com";
|
|
17
|
+
export const VOLC_REGION = "cn-beijing";
|
|
18
|
+
export const VOLC_SERVICE = "ark";
|
|
19
|
+
export const VOLC_VERSION = "2024-01-01";
|
|
20
|
+
|
|
21
|
+
const sha256Hex = value => createHash("sha256").update(value ?? "").digest("hex");
|
|
22
|
+
const hmac = (key, value, encoding) => createHmac("sha256", key).update(value).digest(encoding);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* RFC3986 严格转义。encodeURIComponent 会放过 ! ' ( ) * ,而签名要求把它们
|
|
26
|
+
* 也转成 %XX —— 少转一个字符,服务端算出的签名就不同,表现为 InvalidSignature。
|
|
27
|
+
*/
|
|
28
|
+
export function volcEscape(value) {
|
|
29
|
+
return encodeURIComponent(String(value)).replace(/[!'()*]/g, char => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** X-Date 格式:yyyyMMdd'T'HHmmss'Z'(UTC,无分隔符、无毫秒)。 */
|
|
33
|
+
export function volcDateFormat(ms) {
|
|
34
|
+
return new Date(ms).toISOString().replace(/[-:]|\.\d{3}/g, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 查询串按参数名升序,用严格转义拼接(Action 与 Version 走 query,不在 body 里)。 */
|
|
38
|
+
export function volcCanonicalQuery(params) {
|
|
39
|
+
return Object.keys(params).sort().map(key => `${volcEscape(key)}=${volcEscape(params[key])}`).join("&");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 生成一次调用的 URL 与签名头。签名密钥派生链:
|
|
44
|
+
* kDate = HMAC(SK, yyyyMMdd) → kRegion = HMAC(kDate, region)
|
|
45
|
+
* → kService = HMAC(kRegion, service) → kSigning = HMAC(kService, "request")
|
|
46
|
+
* 待签串与 AWS SigV4 同形,但算法名写 HMAC-SHA256,scope 末段固定为 request。
|
|
47
|
+
*/
|
|
48
|
+
export function volcSignature({ accessKeyId, secretAccessKey, action, version = VOLC_VERSION, body = "{}", date = Date.now(), host = VOLC_HOST, region = VOLC_REGION, service = VOLC_SERVICE }) {
|
|
49
|
+
const xDate = volcDateFormat(date);
|
|
50
|
+
const shortDate = xDate.slice(0, 8);
|
|
51
|
+
const payloadHash = sha256Hex(body);
|
|
52
|
+
const query = volcCanonicalQuery({ Action: action, Version: version });
|
|
53
|
+
const signedHeaders = "content-type;host;x-content-sha256;x-date";
|
|
54
|
+
const canonicalHeaders = [
|
|
55
|
+
"content-type:application/json",
|
|
56
|
+
`host:${host}`,
|
|
57
|
+
`x-content-sha256:${payloadHash}`,
|
|
58
|
+
`x-date:${xDate}`
|
|
59
|
+
].join("\n") + "\n";
|
|
60
|
+
const credentialScope = `${shortDate}/${region}/${service}/request`;
|
|
61
|
+
const canonicalRequest = ["POST", "/", query, canonicalHeaders, signedHeaders, payloadHash].join("\n");
|
|
62
|
+
const stringToSign = ["HMAC-SHA256", xDate, credentialScope, sha256Hex(canonicalRequest)].join("\n");
|
|
63
|
+
const signingKey = hmac(hmac(hmac(hmac(secretAccessKey, shortDate), region), service), "request");
|
|
64
|
+
return {
|
|
65
|
+
url: `https://${host}/?${query}`,
|
|
66
|
+
headers: {
|
|
67
|
+
"content-type": "application/json",
|
|
68
|
+
"x-content-sha256": payloadHash,
|
|
69
|
+
"x-date": xDate,
|
|
70
|
+
authorization: `HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${hmac(signingKey, stringToSign, "hex")}`
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 业务信封错误:ResponseMetadata.Error 比 HTTP 状态码更能说明拒绝原因。 */
|
|
76
|
+
export function volcError(body) {
|
|
77
|
+
const error = body?.ResponseMetadata?.Error;
|
|
78
|
+
if (!error || typeof error !== "object") return undefined;
|
|
79
|
+
const code = typeof error.Code === "string" && error.Code.trim() ? error.Code.trim() : "UnknownError";
|
|
80
|
+
const message = typeof error.Message === "string" && error.Message.trim() ? error.Message.trim() : "";
|
|
81
|
+
return { code, message };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function finite(value) {
|
|
85
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
86
|
+
}
|
|
87
|
+
/** 额度字段是字符串数字("50.0"),只接受有限非负数;非法值不当作 0。 */
|
|
88
|
+
function amount(value) {
|
|
89
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
|
|
90
|
+
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) {
|
|
91
|
+
const parsed = Number(value);
|
|
92
|
+
return parsed >= 0 ? parsed : undefined;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/** 重置时间戳单位不统一:Coding Plan 是秒,Agent Plan 是毫秒。 */
|
|
97
|
+
function fromEpoch(value, unit) {
|
|
98
|
+
if (!Number.isSafeInteger(value) && !Number.isFinite(value)) return undefined;
|
|
99
|
+
const ms = unit === "seconds" ? value * 1000 : value;
|
|
100
|
+
return Number.isFinite(ms) && ms > 0 && Number.isFinite(new Date(ms).getTime()) ? new Date(ms).toISOString() : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Level 是后端下发的窗口名;未登记的取值不产出窗口,不猜语义。
|
|
104
|
+
const CODING_LEVELS = {
|
|
105
|
+
session: "5h", "5h": "5h", five_hour: "5h", fivehour: "5h",
|
|
106
|
+
weekly: "week", week: "week", "7d": "week", seven_day: "week",
|
|
107
|
+
monthly: "month", month: "month", thirty_day: "month"
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 个人版 Coding Plan:Result.QuotaUsage[] 每项一个窗口。
|
|
112
|
+
* Percent 是**已用百分数**(0–100,实测常见 0.39 这类小值,不能按「≤1 即小数」放大)。
|
|
113
|
+
* 接口只给百分比,不返回绝对量,因此不产出 detail(Cap 字段未在任何来源证实存在,不编造)。
|
|
114
|
+
*/
|
|
115
|
+
export function parseVolcCodingPlan(body) {
|
|
116
|
+
// 正常走 OpenAPI 的 ResponseMetadata + Result 信封;个别抓包形态把字段放在顶层,两者都接受。
|
|
117
|
+
const result = body?.Result && typeof body.Result === "object" && !Array.isArray(body.Result) ? body.Result : body;
|
|
118
|
+
const rows = Array.isArray(result?.QuotaUsage) ? result.QuotaUsage : [];
|
|
119
|
+
const windows = [];
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
const kind = CODING_LEVELS[String(row?.Level ?? "").trim().toLowerCase()];
|
|
122
|
+
if (!kind) continue;
|
|
123
|
+
const percent = amount(row?.Percent);
|
|
124
|
+
if (percent === undefined) throw new Error("Volcengine Coding Plan percent is invalid");
|
|
125
|
+
windows.push({ kind, percent, resetsAt: fromEpoch(row?.ResetTimestamp, "seconds") });
|
|
126
|
+
}
|
|
127
|
+
return { subscribed: rows.length > 0, windows };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Agent Plan 的 AFP 窗口:Quota / Used 是绝对值,比例由两者算出。
|
|
131
|
+
const AFP_WINDOWS = [["AFPFiveHour", "5h"], ["AFPDaily", "day"], ["AFPWeekly", "week"], ["AFPMonthly", "month"]];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 个人版 Agent Plan:Result 下四个滚动窗口。
|
|
135
|
+
* Quota 为 0/缺失表示该窗口不适用(例如视觉模型不占 5 小时额度),不产出行、更不当成 0%。
|
|
136
|
+
*/
|
|
137
|
+
export function parseVolcAgentPlan(body) {
|
|
138
|
+
const result = body?.Result;
|
|
139
|
+
if (!result || typeof result !== "object") return { subscribed: false, windows: [], plan: undefined };
|
|
140
|
+
const windows = [];
|
|
141
|
+
let reported = false;
|
|
142
|
+
for (const [field, kind] of AFP_WINDOWS) {
|
|
143
|
+
const row = result[field];
|
|
144
|
+
if (!row || typeof row !== "object") continue;
|
|
145
|
+
reported = true;
|
|
146
|
+
const quota = amount(row.Quota), used = amount(row.Used);
|
|
147
|
+
if (quota === undefined || used === undefined) throw new Error("Volcengine AFP window is invalid");
|
|
148
|
+
if (quota <= 0) continue;
|
|
149
|
+
windows.push({ kind, percent: Math.min(100, used / quota * 100), resetsAt: fromEpoch(row.ResetTime, "milliseconds"), detail: { used, limit: quota, unit: "AFP" } });
|
|
150
|
+
}
|
|
151
|
+
const plan = typeof result.PlanType === "string" && result.PlanType.trim() ? result.PlanType.trim() : undefined;
|
|
152
|
+
return { subscribed: windows.length > 0 || reported, windows, plan };
|
|
153
|
+
}
|
package/locale/en.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Subscription usage",
|
|
3
|
-
"description": "Shows subscription quota for Z.ai, Kimi, MiMo, OpenCode Go, Command Code, MiniMax, Synthetic and NanoGPT, with
|
|
3
|
+
"description": "Shows subscription quota for Z.ai, Kimi, MiMo, OpenCode Go, Command Code, SuperGrok, MiniMax, Synthetic and NanoGPT, with shared refresh and credential management. Z.ai International, Synthetic and NanoGPT are disabled by default."
|
|
4
4
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "订阅用量显示",
|
|
3
|
-
"description": "显示 Z.ai / Kimi / MiMo / OpenCode Go / Command Code / MiniMax / Synthetic / NanoGPT
|
|
3
|
+
"description": "显示 Z.ai / Kimi / MiMo / OpenCode Go / Command Code / SuperGrok / MiniMax / Synthetic / NanoGPT 订阅余量;共享刷新与凭据管理,Z.ai 国际 / Synthetic / NanoGPT 默认关闭。"
|
|
4
4
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subusage",
|
|
3
3
|
"displayName": "订阅用量显示",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/KouzakiUmi/dsh-subusage.git"
|
|
12
12
|
},
|
|
13
|
-
"description": "显示 Z.ai(中国与国际)/ Kimi / MiMo / OpenCode Go / Command Code /
|
|
13
|
+
"description": "显示 DeepSeek 官方余额、Z.ai(中国与国际)/ Kimi / MiMo / OpenCode Go / Command Code / SuperGrok / Codex(ChatGPT 订阅)/ MiniMax(中国)/ Synthetic / NanoGPT / 火山方舟 Ark(Coding Plan 与 Agent Plan)/ SiliconFlow / OpenRouter / Novita / Hyperbolic / DeepInfra / Chutes / Ollama Cloud / Vercel AI Gateway / ZenMux / LiteLLM 的订阅余量与余额;共享刷新与凭据管理,默认关闭 17 家。",
|
|
14
14
|
"keywords": [
|
|
15
15
|
"dsh",
|
|
16
16
|
"dsh-plugin",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"@deepseek-ai/dsh": ">=0.2.0-rc.1 <0.3.0-0",
|
|
25
25
|
"@deepseek-ai/dsh-credentials": ">=0.2.0-rc.1 <0.3.0-0",
|
|
26
26
|
"@deepseek-ai/dsh-launch-environment": ">=0.2.0-rc.1 <0.3.0-0",
|
|
27
|
+
"@deepseek-ai/dsh-tools": ">=0.2.0-rc.1 <0.3.0-0",
|
|
27
28
|
"@deepseek-ai/dsh-typert-protocol": ">=0.2.0-rc.1 <0.3.0-0"
|
|
28
29
|
},
|
|
29
30
|
"type": "module",
|