dsh-subusage 0.10.7 → 0.10.9
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/LICENSE +21 -0
- package/README.md +61 -8
- package/lib/client.js +36 -25
- package/lib/index.js +34 -10
- package/package.json +40 -21
- package/locale/en.json +0 -4
- package/locale/zh.json +0 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KouzakiUmi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> **English.** dsh-subusage shows subscription quota and balance for the AI providers you already use, as a pill next to the model selector in DeepSeek Harness. Provider switches and credentials live in one settings page, and other plugins or agents can read the same snapshot through a read-only quota API.
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-subusage)
|
|
10
10
|
[](#license--security--许可与安全)
|
|
11
11
|
[](#compatibility--兼容性)
|
|
12
12
|
|
|
@@ -62,15 +62,53 @@
|
|
|
62
62
|
|
|
63
63
|
非公开控制台接口可能变更;缺失或非法百分比不会被当作零用量;「读取成功」只表示接口读取健康度,不保证仍有额度。
|
|
64
64
|
|
|
65
|
+
## Support status / 实测覆盖
|
|
66
|
+
|
|
67
|
+
### 已实测验证
|
|
68
|
+
|
|
69
|
+
下面这些在**真实账号**上跑通过(本仓库开发机 + 各家的官方 CLI / 插件):
|
|
70
|
+
|
|
71
|
+
| 提供商 | 验证范围 | 凭据来源 |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| 火山方舟 Agent Plan(个人版,`arkcli-agent-plan`) | 5 小时 / 周 / 月 AFP 绝对值 + 日限额 + 档位 | 本机 IAM AK/SK 直连 `GetAFPUsage`,HTTP 200 |
|
|
74
|
+
| 火山方舟 Coding Plan(个人版,`arkcli-coding-plan`) | session / weekly / monthly 三个窗口 | 同一组 AK/SK 直连 `GetCodingPlanUsage`,HTTP 200 |
|
|
75
|
+
| Kimi Coding | 5 小时 / 周 / 月窗口 | 真实 Key |
|
|
76
|
+
| 小米 MiMo | 余额 + 套餐额度 | 真实平台会话 Cookie(含自动登录) |
|
|
77
|
+
| Command Code | 月额度 | 提供方插件的凭据链 |
|
|
78
|
+
| SuperGrok | 统一周池 + 加量余额 | 真实 `~/.grok/auth.json` |
|
|
79
|
+
| Codex (ChatGPT) | 主 / 次窗口 + 档位 | 真实 `~/.codex/auth.json`(本插件默认关闭) |
|
|
80
|
+
|
|
81
|
+
### 未验证
|
|
82
|
+
|
|
83
|
+
其余提供商**只经过桩测试**——响应形态取自公开文档、第三方实现或抓包,**没有在真实账号上跑通过**:
|
|
84
|
+
|
|
85
|
+
> Z.ai(中国与国际)、OpenCode Go、MiniMax(中国与国际)、Synthetic、NanoGPT、SiliconFlow、DeepSeek、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM,以及火山方舟的**团队版席位**与 **BytePlus 站点**。
|
|
86
|
+
|
|
87
|
+
它们可能可用,也可能因为接口变更、字段差异或权限要求而失败。**本插件只保证上面那张表里的**。
|
|
88
|
+
|
|
89
|
+
如果你用了其中之一,不管成功还是失败,都欢迎[开 issue](https://github.com/KouzakiUmi/dsh-subusage/issues)——附上提供商名称、错误文案与 DSH 版本即可(**不要贴 Key 或 Cookie**)。失败和成功一样有用:失败帮我们修适配,成功帮我们把这一行挪进上表。
|
|
90
|
+
|
|
91
|
+
### 主要风险
|
|
92
|
+
|
|
93
|
+
- **额度数字仅供参考,不要当计费依据。** 各家的额度接口都是未公开或半公开的,字段语义可能随时变化;本插件不做任何换算,读到什么显示什么。
|
|
94
|
+
- **未验证的提供商会随接口变化而失效**(见上一节)。失效时在设置页关掉那一家的检测即可,其余不受影响;欢迎开 issue 帮我们补上。
|
|
95
|
+
- **凭据以明文落盘**:本插件的配置不是加密凭据库;Windows 上真正的访问边界取决于目录 ACL,`chmod` 不能替代它。
|
|
96
|
+
- **火山方舟是账号级读取**:一组 IAM AK/SK 能查到该账号名下**所有**套餐(Agent Plan、Coding Plan、团队席位)。把装了本插件的机器或配置文件交给别人时请注意这一点。
|
|
97
|
+
- **浏览器自动化**:MiMo 的自动登录会启动一个隔离的 Chromium 会话——不点「登录并自动导入」就不会触发。
|
|
98
|
+
- **收录不等于安全审计**:无论 DSH STORE 还是 awesome-dsh-plugin,收录只表示满足它们各自的清单规则,都不是对代码的安全审查。
|
|
99
|
+
|
|
65
100
|
## Compatibility / 兼容性
|
|
66
101
|
|
|
67
102
|
| 项目 | 值 | 来源 |
|
|
68
103
|
|---|---|---|
|
|
69
|
-
| 核心 peer 范围 | `@deepseek-ai/dsh` 及各核心包均为 `>=0.2.0-rc.1 <0.3.0-0
|
|
70
|
-
|
|
|
71
|
-
|
|
|
104
|
+
| 核心 peer 范围 | `@deepseek-ai/dsh` 及各核心包均为 `>=0.2.0-rc.1 <0.2.1-0 \|\| >=0.2.1-0 <0.3.0-0` | [`package.json`](package.json) 的 `peerDependencies` |
|
|
105
|
+
| Node.js | `>=22.19.0` | `engines` 与 `dsh.compatibility.node` |
|
|
106
|
+
| 已验证的 Core 版本 | `0.2.1-alpha.1` | 本机实机运行;逐版本状态见 `dsh.compatibility.dshReleases` |
|
|
107
|
+
| 最后验证日期 | `2026-10-10` | [开发与验证说明](docs/development.md) |
|
|
108
|
+
|
|
109
|
+
> **范围里那个 `||` 分支不是冗余。** node-semver 只有在范围中**某个比较符与该版本的 `major.minor.patch` 元组完全一致、且自身带预发布标签**时,才放行预发布版本。所以 `>=0.2.0-rc.1 <0.3.0-0` 这种看起来覆盖 0.2.x 的写法会**静默排除** `0.2.1-alpha.1`——而它正是当前活跃版本。必须为每个 minor 元组各写一个带预发布标签的分支。
|
|
72
110
|
|
|
73
|
-
|
|
111
|
+
`dsh.compatibility.dshReleases` 逐版本声明已知状态:**只有实际验证过的标 `compatible`**,其余一律 `unknown`——没有证据不猜。声明范围不代表范围内所有版本都已实机验证;实际验证范围(在线接口、真实账号、浏览器像素验收等)见 [开发与验证说明](docs/development.md)。这是树外 Host / Client bundle,通过 `cordis.patch.yml` 插入,不修改 DSH 核心、安装树或 ASAR。
|
|
74
112
|
|
|
75
113
|
## Install / 安装
|
|
76
114
|
|
|
@@ -81,10 +119,13 @@
|
|
|
81
119
|
| 本地开发目录 | 仓库路径 | 以链接方式使用工作副本 |
|
|
82
120
|
|
|
83
121
|
```console
|
|
84
|
-
# npm
|
|
122
|
+
# npm 包名(推荐:预构建,免构建授权)
|
|
85
123
|
dsh plugin --profile <profile> add dsh-subusage
|
|
86
124
|
|
|
87
|
-
# GitHub
|
|
125
|
+
# GitHub 固定 Commit(DSH STORE / awesome-dsh-plugin 只接受这种来源)
|
|
126
|
+
dsh plugin --profile <profile> add 'git+https://github.com/KouzakiUmi/dsh-subusage.git#<40 位完整 Commit>'
|
|
127
|
+
|
|
128
|
+
# GitHub Release tarball(资产名不带版本号,所以这个地址不会随发版而失效)
|
|
88
129
|
dsh plugin --profile <profile> add https://github.com/KouzakiUmi/dsh-subusage/releases/latest/download/dsh-subusage.tgz
|
|
89
130
|
|
|
90
131
|
# 本地开发目录(link 工作副本)
|
|
@@ -100,8 +141,10 @@ dsh plugin --profile <profile> remove dsh-subusage
|
|
|
100
141
|
```
|
|
101
142
|
|
|
102
143
|
- `<profile>` 换成目标 profile;**Desktop 请使用随包 DSH CLI 或应用内插件管理页**,PATH 上的 npm 全局 `dsh` 不一定是它。参数与 profile 名以目标部署的 `dsh plugin --help` 为准;桌面插件页同样接受包名、GitHub 地址或本地目录。
|
|
144
|
+
- **不要用浮动分支**(`#main`)安装:市场与商店只认固定到完整 40 位 Commit 的来源,浮动分支无法核实你装到的到底是哪份代码。
|
|
103
145
|
- 安装只把包放进 profile,**不等于已启用**;按部署方式重载或重启,让 Host 与 Client 一起生效(只刷新页面不保证 Host 升级)。
|
|
104
146
|
- 只取压缩包:`npm pack dsh-subusage`;核对发布结果:`npm view dsh-subusage version dist-tags`。
|
|
147
|
+
- 本包**没有** `preinstall` / `install` / `postinstall` / `prepare` 脚本,安装期不执行任何代码;也不需要 pnpm 的 `allowBuilds` 授权。
|
|
105
148
|
|
|
106
149
|
## Quick start / 快速开始
|
|
107
150
|
|
|
@@ -227,7 +270,17 @@ ark-coding-plan-byteplus ARK_CODING_PLAN_BYTEPLUS_API_KEY
|
|
|
227
270
|
| `~/.grok/auth.json`(回退 `~/.dsh/.xai-oauth-auth.json`) | Grok CLI / dsh-grok-kit 的 OAuth 登录 | **只读**,不保存、不刷新 |
|
|
228
271
|
| `~/.commandcode/auth.json` | Command Code CLI(`cmd login`)的 Key | **只读**兜底 |
|
|
229
272
|
|
|
230
|
-
**网络访问**:只连上面清单里各家自己的额度 / 余额端点,以及 `platform.xiaomimimo.com`(MiMo 用量与登录)、`api.commandcode.ai`、`cli-chat-proxy.grok.com`、`chatgpt.com/backend-api`、火山管控面 `open.volcengineapi.com
|
|
273
|
+
**网络访问**:只连上面清单里各家自己的额度 / 余额端点,以及 `platform.xiaomimimo.com`(MiMo 用量与登录)、`api.commandcode.ai`、`cli-chat-proxy.grok.com`、`chatgpt.com/backend-api`、火山管控面 `open.volcengineapi.com`,外加你自己填写的 LiteLLM proxy 地址。完整域名表见 [网络端点](docs/providers.md#网络端点)。MiMo 自动登录会另外打开官方平台页面,由你在该页面自行输入密码与验证码。**本插件没有自己的服务器,不做遥测,也不把你的数据发给任何第三方。**
|
|
274
|
+
|
|
275
|
+
**命令、进程与安装期行为**
|
|
276
|
+
|
|
277
|
+
| 项 | 情况 |
|
|
278
|
+
|---|---|
|
|
279
|
+
| 执行外部命令 | **不执行**。没有任何 shell 调用 |
|
|
280
|
+
| 生命周期脚本 | `preinstall` / `install` / `postinstall` / `prepare` **全部没有**;安装期不运行任何代码 |
|
|
281
|
+
| 运行依赖 | `playwright-core`(唯一一个)。只用于可选的 MiMo 自动登录;**不下载浏览器**,用你本机已有的 Chrome/Chromium |
|
|
282
|
+
| 浏览器 | 只有你点「登录并自动导入」时才启动一个**隔离的**临时会话,五分钟未完成自动超时;不读你日常浏览器的配置、Cookie 或 profile |
|
|
283
|
+
| DSH 核心 | 不修改源码树、不碰任何 `@deepseek-ai/*` 包、不遮蔽官方插件清单;只用公开 Host 服务与 `cordis.patch.yml` 插入自己的行 |
|
|
231
284
|
|
|
232
285
|
**密钥如何处理**
|
|
233
286
|
|
package/lib/client.js
CHANGED
|
@@ -282,8 +282,6 @@ window.__ModuleLoader__.load({
|
|
|
282
282
|
// 旧插件 @volcengine/ark-plan-api 的国内两条路由已从 Host 移除(实测与 arkcli 查同一份订阅、
|
|
283
283
|
// 同一组 AK/SK)。它们的 id 不在本表里,由 ROUTE_PROVIDER_ALIAS 在入口归并到 arkcli 路由,
|
|
284
284
|
// 装了旧插件的用户选中那些模型的药丸照常显示。
|
|
285
|
-
// BytePlus 是唯一「同 Action 不同 host」的:独立站点、独立订阅,所以保留自己的条目。
|
|
286
|
-
"ark-coding-plan-byteplus": { label: "Ark Coding Plan (BytePlus, legacy plugin)", short: "Ark BytePlus", envName: "ARK_CODING_PLAN_BYTEPLUS_API_KEY", volc: true, defaultEnabled: false },
|
|
287
285
|
// 余额型:接口给账户余额而不是订阅窗口,药丸用余额兜底文案显示。
|
|
288
286
|
"siliconflow": { label: "SiliconFlow", short: "SiliconFlow", envName: "SILICONFLOW_API_KEY", defaultEnabled: false },
|
|
289
287
|
// 余额端点只对 management / provisioning key 开放;普通 key 只显示限额与用量。
|
|
@@ -301,7 +299,7 @@ window.__ModuleLoader__.load({
|
|
|
301
299
|
"litellm": { label: "LiteLLM", short: "LiteLLM", envName: "LITELLM_API_KEY", defaultEnabled: false }
|
|
302
300
|
};
|
|
303
301
|
// 顺序只影响展示:DeepSeek 放在末尾以免打乱既有条目位置;药丸匹配靠 provider id,与顺序无关。
|
|
304
|
-
const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "openai-codex", "xai-oauth", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt", "
|
|
302
|
+
const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "openai-codex", "xai-oauth", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt", "siliconflow", "deepseek", "openrouter", "novita", "hyperbolic", "deepinfra", "chutes", "ollama-cloud", "vercel-ai-gateway", "zenmux", "litellm", "arkcli-agent-plan", "arkcli-coding-plan", "arkcli-agent-plan-team", "arkcli-coding-plan-team"];
|
|
305
303
|
// 火山方舟的 7 条路由共用同一组 IAM AK/SK,而一般用户只持有一个套餐,所以设置页只给一张卡片、
|
|
306
304
|
// 一个开关(组开关落在 Host 的 visibility.ark 上)。这些 id 不能真正合并——药丸靠 provider id 匹配路由。
|
|
307
305
|
const ARK_IDS = PROVIDER_ORDER.filter(id => PROVIDER_META[id]?.volc);
|
|
@@ -310,7 +308,7 @@ window.__ModuleLoader__.load({
|
|
|
310
308
|
* 查的是同一份订阅(同 Action、同 host、同 AK/SK,实测请求 URL 逐字相同),并列显示只会让人以为
|
|
311
309
|
* 买了两个套餐,数字还会因为各自缓存而差出几十秒的量。
|
|
312
310
|
* 但装了旧插件的用户,模型路由仍然是这两个 id —— 药丸必须在入口把它们归并到 arkcli 路由,
|
|
313
|
-
*
|
|
311
|
+
* 否则那些用户会突然失去用量显示。(第三条 BytePlus 已随实现一并移除:无 BytePlus 账号可验证。)
|
|
314
312
|
*/
|
|
315
313
|
const ROUTE_PROVIDER_ALIAS = { "ark-agent-plan-cn": "arkcli-agent-plan", "ark-coding-plan-cn": "arkcli-coding-plan" };
|
|
316
314
|
function providerEnabled(settings, id) {
|
|
@@ -686,8 +684,9 @@ window.__ModuleLoader__.load({
|
|
|
686
684
|
if (disposed) return snapshot;
|
|
687
685
|
const value = { ...incoming };
|
|
688
686
|
if (request && request.version < metadataSequence) { delete value.settings; delete value.configured; delete value.updatedAt; }
|
|
687
|
+
// 过期请求的 settings 已在上一行丢掉,所以这里若还带着 settings,就是「当前代」看到的 Host 配置。
|
|
688
|
+
// 另一窗口改过配置时采用它(开关跟着变),但不再 epoch++ / invalidate 全员——那会让非活跃条目永久 loading。
|
|
689
689
|
if (value.settings) metadataSequence = request ? request.version : ++sequence;
|
|
690
|
-
if (!localChange && snapshot.settings && value.settings && snapshot.settings.revision !== value.settings.revision) { epoch++; invalidate(PROVIDER_ORDER); }
|
|
691
690
|
const entries = new Map(snapshot.entries.map((e) => [e.providerId, e]));
|
|
692
691
|
for (const e of value.entries || []) if (!request || providerSequence.get(e.providerId) === request.version) entries.set(e.providerId, e);
|
|
693
692
|
snapshot = { ...snapshot, ...value, entries: [...entries.values()] };
|
|
@@ -696,17 +695,22 @@ window.__ModuleLoader__.load({
|
|
|
696
695
|
const refresh = (ids, force = false) => {
|
|
697
696
|
if (disposed) return Promise.reject(new Error("Client 已卸载"));
|
|
698
697
|
const unique = [...new Set(ids)].filter((id) => PROVIDER_ORDER.includes(id) && providerEnabled(snapshot.settings, id) && providerRoutable(snapshot, id));
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
const startedEpoch = epoch, request = markRequest(
|
|
702
|
-
const task = Promise.resolve().then(() => call("refresh", { providerIds:
|
|
698
|
+
const start = (id) => {
|
|
699
|
+
if (inflight.has(id)) return inflight.get(id);
|
|
700
|
+
const startedEpoch = epoch, request = markRequest([id]);
|
|
701
|
+
const task = Promise.resolve().then(() => call("refresh", { providerIds: [id], force: !!force, commandCodeAccount: ccAccount })).then((value) => startedEpoch === epoch ? merge(value, request) : snapshot).catch((error) => {
|
|
703
702
|
// 传输失败不能擅自 retainPrevious:只有 Host 明确标 stale 才保留。
|
|
704
|
-
if (startedEpoch === epoch) merge({ entries:
|
|
703
|
+
if (startedEpoch === epoch) merge({ entries: [{ providerId: id, state: "error", error: error instanceof Error ? error.message : String(error), freshness: "unknown", coverage: "partial", windows: [], extras: [] }] }, request);
|
|
705
704
|
throw error;
|
|
706
|
-
}).finally(() => {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
705
|
+
}).finally(() => { if (inflight.get(id) === task) inflight.delete(id); });
|
|
706
|
+
inflight.set(id, task);
|
|
707
|
+
return task;
|
|
708
|
+
};
|
|
709
|
+
const tasks = unique.map(start);
|
|
710
|
+
// 调用方点名多家(刷新全部)即使过滤后只剩一家,也不把失败抬成整页错误。
|
|
711
|
+
const tolerant = ids.length > 1;
|
|
712
|
+
if (unique.length <= 1 && !tolerant) return (tasks[0] ?? Promise.resolve(snapshot)).then(() => snapshot);
|
|
713
|
+
return Promise.all(tasks.map((task) => task.catch(() => null))).then(() => snapshot);
|
|
710
714
|
};
|
|
711
715
|
const visible = () => { if (typeof document === "undefined" || document.visibilityState !== "hidden") void refresh([...active.keys()]).catch(() => {}); };
|
|
712
716
|
const tick = () => {
|
|
@@ -747,7 +751,7 @@ window.__ModuleLoader__.load({
|
|
|
747
751
|
// invalidate 只重写快照不通知:补一次 emit 让药丸立即进入加载态,
|
|
748
752
|
// 避免强制刷新返回前「标注已是新账户、数字还是旧账户」的瞬态错标。
|
|
749
753
|
emit();
|
|
750
|
-
return refresh(["commandcode"], true);
|
|
754
|
+
return refresh(["commandcode"], true).catch(() => snapshot);
|
|
751
755
|
};
|
|
752
756
|
return {
|
|
753
757
|
subscribe, getSnapshot: () => snapshot, refresh, activate,
|
|
@@ -757,10 +761,16 @@ window.__ModuleLoader__.load({
|
|
|
757
761
|
cancelMimoLogin: async (request) => { if (loginJobId && request.jobId !== loginJobId) return null; const generation = ++loginGeneration, startedEpoch = epoch; return acceptLogin(await call("cancelMimoLogin", request), generation, startedEpoch, request.jobId); },
|
|
758
762
|
readAll: () => { if (!initial) { const startedEpoch = epoch, request = markRequest(PROVIDER_ORDER); initial = Promise.resolve().then(() => call("read", { providerIds: PROVIDER_ORDER, force: false, commandCodeAccount: ccAccount })).then((value) => startedEpoch === epoch ? merge(value, request) : snapshot).catch((error) => { initial = null; throw error; }); } return initial.then(() => snapshot); },
|
|
759
763
|
save: async (patch) => {
|
|
760
|
-
const value = await call("save", patch);
|
|
761
|
-
|
|
764
|
+
const value = await call("save", patch);
|
|
765
|
+
// 可见性开关只改 settings:不动 epoch、不重置在途请求序号——否则「刷新全部进行中拨开关」
|
|
766
|
+
// 会把所有在途用量结果整批丢弃(startedEpoch !== epoch 直接放弃 merge)。merge 内部会推进
|
|
767
|
+
// metadataSequence,让在途响应里的过期 settings 自动丢弃,本地新 revision 不被盖回。
|
|
768
|
+
if (patch.visibility) return merge({ ...value, entries: snapshot.entries }, null, true);
|
|
769
|
+
// 凭据变更改变用量语义:推进 epoch 丢弃在途。火山 AK/SK 是组内容共用的,必须整组失效。
|
|
770
|
+
epoch++;
|
|
762
771
|
const cookie = patch.cookieUpdate;
|
|
763
|
-
|
|
772
|
+
const affected = PROVIDER_META[patch.providerId]?.volc && patch.volc ? ARK_IDS : [patch.providerId];
|
|
773
|
+
invalidate(affected, !cookie || cookie.action === "keep"); return merge(value, null, true);
|
|
764
774
|
},
|
|
765
775
|
reader: (id) => {
|
|
766
776
|
if (!readers.has(id)) {
|
|
@@ -989,7 +999,7 @@ window.__ModuleLoader__.load({
|
|
|
989
999
|
// unavailable/loading 保持现状;store 内部对相同账户去重,不产生多余请求。
|
|
990
1000
|
react.useEffect(() => {
|
|
991
1001
|
if (providerId !== "commandcode" || !ccState || !setCommandCodeAccount) return;
|
|
992
|
-
if (ccState.status === "ready") void setCommandCodeAccount(ccState.activeId);
|
|
1002
|
+
if (ccState.status === "ready") void setCommandCodeAccount(ccState.activeId).catch(() => {});
|
|
993
1003
|
}, [providerId, setCommandCodeAccount, ccState && ccState.status, ccState && ccState.activeId]);
|
|
994
1004
|
react.useEffect(() => {
|
|
995
1005
|
let alive = true;
|
|
@@ -1199,6 +1209,7 @@ window.__ModuleLoader__.load({
|
|
|
1199
1209
|
const [draft, setDraft] = react.useState(() => draftFor(result.settings, tab));
|
|
1200
1210
|
const [dirty, setDirty] = react.useState(false);
|
|
1201
1211
|
const [busy, setBusy] = react.useState(false);
|
|
1212
|
+
const [refreshing, setRefreshing] = react.useState(false);
|
|
1202
1213
|
const [notice, setNotice] = react.useState(null);
|
|
1203
1214
|
const [loadError, setLoadError] = react.useState(null);
|
|
1204
1215
|
const [login, setLogin] = react.useState({ jobId: null, state: "idle" });
|
|
@@ -1283,10 +1294,10 @@ window.__ModuleLoader__.load({
|
|
|
1283
1294
|
try { window.localStorage.setItem("dsh-subusage:last-provider", id); } catch {}
|
|
1284
1295
|
};
|
|
1285
1296
|
const refresh = async (all) => {
|
|
1286
|
-
|
|
1297
|
+
setRefreshing(true);
|
|
1287
1298
|
try { if (!result.settings) await usageStore.readAll(); else await usageStore.refresh(all ? PROVIDER_ORDER : usageId ? [usageId] : [], true); if (aliveRef.current) setLoadError(null); }
|
|
1288
1299
|
catch (error) { if (aliveRef.current) setLoadError(String(error.message || error)); }
|
|
1289
|
-
finally { if (aliveRef.current)
|
|
1300
|
+
finally { if (aliveRef.current) setRefreshing(false); }
|
|
1290
1301
|
};
|
|
1291
1302
|
const saveVisibility = async (visibility) => {
|
|
1292
1303
|
if (busy || !result.settings || !result.settings.revision) return;
|
|
@@ -1321,7 +1332,7 @@ window.__ModuleLoader__.load({
|
|
|
1321
1332
|
if (!providerEnabled(saved.settings, tab)) return;
|
|
1322
1333
|
// 保存已成功落盘;独立验证失败不改写成“保存失败”。
|
|
1323
1334
|
try {
|
|
1324
|
-
const verified = await usageStore.refresh([tab], true);
|
|
1335
|
+
const verified = await usageStore.refresh(PROVIDER_META[tab]?.volc ? ARK_IDS : [tab], true);
|
|
1325
1336
|
const current = verified.entries.find((e) => e.providerId === tab);
|
|
1326
1337
|
if (aliveRef.current) setLoadError(current && current.state === "ok" ? null : text("已保存;验证获取失败:", "Saved; validation fetch failed: ") + (current && current.error || statusText(current, t)));
|
|
1327
1338
|
} catch (error) { if (aliveRef.current) setLoadError(text("已保存;验证获取失败:", "Saved; validation fetch failed: ") + String(error.message || error)); }
|
|
@@ -1475,8 +1486,8 @@ window.__ModuleLoader__.load({
|
|
|
1475
1486
|
"aria-label": text(`${success}/${visibleIds.length} 家数据获取成功;${troubledIds.length ? `点击跳到 ${PROVIDER_META[troubledIds[0]].short}` : "没有待处理项"}`, `${success}/${visibleIds.length} providers fetched; ${troubledIds.length ? `click to jump to ${PROVIDER_META[troubledIds[0]].short}` : "nothing needs attention"}`),
|
|
1476
1487
|
style: { ...btnStyle, padding: "2px 8px", fontSize: 12, opacity: .85, cursor: troubledIds.length ? "pointer" : "default", color: troubledIds.length ? "#b7791f" : "inherit" }
|
|
1477
1488
|
}, text(`${success}/${visibleIds.length} 家数据获取成功`, `${success}/${visibleIds.length} providers fetched`)),
|
|
1478
|
-
h("button", { type: "button", disabled: busy || !usageId, style: primary, onClick: () => refresh(false) }, text("刷新当前", "Refresh current")),
|
|
1479
|
-
h("button", { type: "button", disabled: busy, style: btnStyle, onClick: () => refresh(true) }, text("刷新全部", "Refresh all"))
|
|
1489
|
+
h("button", { type: "button", disabled: busy || refreshing || !usageId, style: { ...primary, cursor: busy || refreshing || !usageId ? "default" : "pointer" }, onClick: () => refresh(false) }, refreshing ? t("refreshing") : text("刷新当前", "Refresh current")),
|
|
1490
|
+
h("button", { type: "button", disabled: busy || refreshing, style: { ...btnStyle, cursor: busy || refreshing ? "default" : "pointer" }, onClick: () => refresh(true) }, refreshing ? t("refreshing") : text("刷新全部", "Refresh all"))
|
|
1480
1491
|
),
|
|
1481
1492
|
loadError ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, loadError) : null,
|
|
1482
1493
|
!visibleIds.length ? h("p", { role: "status" }, text("没有可显示的提供商,请展开下方「提供商管理」开启、配置 API,或关闭默认隐藏。", "No visible providers. Expand Provider management below to enable a provider, configure API credentials, or turn off auto-hide.")) : null,
|
|
@@ -1574,7 +1585,7 @@ window.__ModuleLoader__.load({
|
|
|
1574
1585
|
const provider = state.current && state.current.provider;
|
|
1575
1586
|
// 旧插件 @volcengine/ark-plan-api 的两个国内路由 id 不在本表里,但它们是**同一个 plan**:
|
|
1576
1587
|
// 不管模型路由由哪个插件提供,都归并到 arkcli 路由再查——所以只会有**一个**面板,
|
|
1577
|
-
// 数据来自同一份订阅(实测两条路由的请求 URL 逐字相同)。
|
|
1588
|
+
// 数据来自同一份订阅(实测两条路由的请求 URL 逐字相同)。
|
|
1578
1589
|
const resolved = ROUTE_PROVIDER_ALIAS[provider] ?? provider;
|
|
1579
1590
|
const info = PROVIDER_ORDER.includes(resolved) ? PROVIDER_META[resolved] : null;
|
|
1580
1591
|
const meta = info ? { label: resolved === "commandcode" ? info.label : info.short, id: resolved } : null;
|
package/lib/index.js
CHANGED
|
@@ -54,12 +54,13 @@ const PROVIDERS = {
|
|
|
54
54
|
// 企业版/团队版:额度挂在席位上,先 ListSeatInfos 取 SeatID 再查该席位(volcSeat 决定用哪对 Action)。
|
|
55
55
|
"arkcli-agent-plan-team": { label: "ARK Agent Plan Team (arkcli)", envName: "ARKCLI_AGENT_PLAN_TEAM_API_KEY", credentialKind: "volc", volcSeat: "agent", volcScene: "agent_plan_enterprise", volcHost: "open.volcengineapi.com", volcRegion: "cn-beijing", defaultEnabled: false },
|
|
56
56
|
"arkcli-coding-plan-team": { label: "ARK Coding Plan Team (arkcli)", envName: "ARKCLI_CODING_PLAN_TEAM_API_KEY", credentialKind: "volc", volcSeat: "coding", volcScene: "coding_plan_enterprise", volcHost: "open.volcengineapi.com", volcRegion: "cn-beijing", defaultEnabled: false },
|
|
57
|
-
// ── 旧插件 @volcengine/ark-plan-api
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
57
|
+
// ── 旧插件 @volcengine/ark-plan-api 的三条路由已全部移除 ──────────────────
|
|
58
|
+
// 国内两条(`ark-coding-plan-cn` / `ark-agent-plan-cn`)与 arkcli 的路由请求 URL 逐字相同(同 Action、
|
|
59
|
+
// 同 host、同 AK/SK、同一份订阅),并列显示只会让人以为买了两个套餐,数字还会因各自缓存而差出几十秒。
|
|
60
|
+
// BytePlus 那条(`ark-coding-plan-byteplus`)走 `ark.ap-southeast-1.byteplusapi.com`:域名与 Action
|
|
61
|
+
// **确实存在**(返回的是业务信封错误 NotFound.BillingType,不是路由不存在),但本机没有 BytePlus 账号
|
|
62
|
+
// 可验证,实现正确性无从确认,因此不再保留——取舍与证据记在 docs/provider-coverage.md。
|
|
63
|
+
// 国内那两个 id 药丸仍认:Client 的 ROUTE_PROVIDER_ALIAS 把它们归并到 arkcli 路由。
|
|
63
64
|
// DeepSeek 官方:余额型(不是订阅窗口),与推理**同一把** API Key,端点与响应见官方文档
|
|
64
65
|
// https://api-docs.deepseek.com/zh-cn/api/get-user-balance
|
|
65
66
|
"deepseek": { label: "DeepSeek", envName: "DEEPSEEK_API_KEY" },
|
|
@@ -386,6 +387,17 @@ function parseQuery(value) {
|
|
|
386
387
|
function failure(code, message, retryable = false, retryAfterMs) {
|
|
387
388
|
return new RemoteError(code, message, { retryable, retainPrevious: retryable, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}) });
|
|
388
389
|
}
|
|
390
|
+
// 与 fetchJson 的 AbortSignal.timeout 同语义的 promise 超时门;ms 非正数视为不限时。
|
|
391
|
+
// race 结束即清 timer:未触发的 setTimeout 否则会把测试进程挂到超时才退出;先 settle 的
|
|
392
|
+
// 原始 promise 之后才 reject 时,handler 已由 race 注册,不会变成 unhandledRejection。
|
|
393
|
+
function withTimeout(promise, ms, makeError) {
|
|
394
|
+
if (!Number.isFinite(ms) || ms <= 0) return promise;
|
|
395
|
+
let timer;
|
|
396
|
+
const work = Promise.resolve(promise);
|
|
397
|
+
work.catch(() => {});
|
|
398
|
+
const gate = new Promise((_, reject) => { timer = setTimeout(() => reject(makeError()), ms); });
|
|
399
|
+
return Promise.race([work, gate]).finally(() => { clearTimeout(timer); });
|
|
400
|
+
}
|
|
389
401
|
// 火山方舟把拒绝原因放在 ResponseMetadata.Error 业务信封里:401 是签名/凭据(服务端不知道你是谁),
|
|
390
402
|
// 403 是授权(身份已确认但不能做:IAM 权限不足、套餐未开通、欠费),两者对用户的行动项不同。
|
|
391
403
|
function volcFailure(action, error) {
|
|
@@ -399,7 +411,7 @@ function volcFailure(action, error) {
|
|
|
399
411
|
}
|
|
400
412
|
async function fetchJson(fetcher, url, headers, lifetimeSignal, init = {}) {
|
|
401
413
|
let response;
|
|
402
|
-
try { response = await fetcher(url, { method: init.method ?? "GET", ...(init.body !== undefined ? { body: init.body } : {}), headers: { accept: "application/json", "user-agent": "dsh-subusage/0.10.
|
|
414
|
+
try { response = await fetcher(url, { method: init.method ?? "GET", ...(init.body !== undefined ? { body: init.body } : {}), headers: { accept: "application/json", "user-agent": "dsh-subusage/0.10.9", ...headers }, signal: lifetimeSignal ? AbortSignal.any([AbortSignal.timeout(10000), lifetimeSignal]) : AbortSignal.timeout(10000), redirect: "manual" }); }
|
|
403
415
|
catch { throw failure("subusage/network", "网络请求失败或超时", true); }
|
|
404
416
|
if (response.status >= 300 && response.status < 400 || response.status === 401 || response.status === 403) throw failure("subusage/auth", "登录或凭据已失效");
|
|
405
417
|
if (!response.ok) {
|
|
@@ -875,6 +887,9 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
875
887
|
this.io = options.io ?? { readFileSync, mkdirSync, writeFileSync, renameSync, chmodSync };
|
|
876
888
|
this.path = options.configPath ?? CONFIG_PATH; this.fetcher = options.fetch ?? globalThis.fetch;
|
|
877
889
|
this.now = options.now ?? Date.now;
|
|
890
|
+
// identity 解析(凭据服务/登录文件读取)与 HTTP 同量级的超时,默认 10s(对齐 fetchJson 的
|
|
891
|
+
// AbortSignal.timeout);经 options 注入以便测试用极短值,不必真等 10 秒。
|
|
892
|
+
this.identityTimeoutMs = options.identityTimeoutMs ?? 10000;
|
|
878
893
|
this.credentials = options.resolveCredentials ?? (async id => await ctx.get("credentials")?.resolve(credentialRef(PROVIDERS[id].envName)));
|
|
879
894
|
this.environment = options.resolveEnvironment ?? (id => launchEnvironmentOf(ctx).get(PROVIDERS[id].envName));
|
|
880
895
|
// 火山方舟的 AK/SK 不是路由的推理 Key,按固定变量名各自解析,两端都命中才算配对。
|
|
@@ -1124,8 +1139,17 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1124
1139
|
async provider(id, settings, force, account = "") {
|
|
1125
1140
|
const startedGeneration = this.generations.get(id) ?? 0;
|
|
1126
1141
|
let identity;
|
|
1127
|
-
|
|
1128
|
-
|
|
1142
|
+
// identity 解析依赖凭据服务/登录文件读取,可能像 HTTP 一样永久挂起:套上与 fetchJson 同量级
|
|
1143
|
+
// 的超时。超时不等于凭据失效——不清 Cookie、不用 subusage/credentials(client 的
|
|
1144
|
+
// /auth|credential/ 判定会把它导去重新登录/凭据编辑入口),而是独立 errorCode 的可读条目。
|
|
1145
|
+
// 只让这一家及时返回,不拖死整批 Promise.all;挂起的原始 promise 由 race 持有,之后
|
|
1146
|
+
// settle 也被静默丢弃(含其 reject,handler 已注册,不会变成 unhandledRejection)。
|
|
1147
|
+
try { identity = await withTimeout(this.identity(id, settings, account), this.identityTimeoutMs, () => failure("subusage/identity-timeout", `凭据解析超时(${Math.round(this.identityTimeoutMs / 1000)} 秒无响应):凭据服务或登录文件读取未返回,请稍后重试`)); }
|
|
1148
|
+
catch (e) {
|
|
1149
|
+
const timedOut = e?.code === "subusage/identity-timeout";
|
|
1150
|
+
this.cache.delete(id);
|
|
1151
|
+
return { ...this.base(id, "none", settings, account), state: "error", errorCode: timedOut ? "subusage/identity-timeout" : "subusage/credentials", error: timedOut ? e.message : "无法解析凭据" };
|
|
1152
|
+
}
|
|
1129
1153
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
1130
1154
|
if (startedGeneration !== (this.generations.get(id) ?? 0)) return this.provider(id, this.load(), force, account);
|
|
1131
1155
|
if (!providerEnabled(settings, id)) {
|
|
@@ -1183,7 +1207,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1183
1207
|
const wanted = matched.length || !unknown.length ? matched : [...IDS];
|
|
1184
1208
|
const settings = this.load();
|
|
1185
1209
|
const force = request?.force === true;
|
|
1186
|
-
const entries = await Promise.all(wanted.map(id => this.provider(id, settings, force).catch(() => ({ providerId: id, label: PROVIDERS[id].label, state: "error", errorCode: "subusage/internal", error: "读取用量失败" }))));
|
|
1210
|
+
const entries = await withTimeout(Promise.all(wanted.map(id => this.provider(id, settings, force).catch(() => ({ providerId: id, label: PROVIDERS[id].label, state: "error", errorCode: "subusage/internal", error: "读取用量失败" })))), 30000, () => failure("subusage/timeout", "额度查询超过 30 秒仍未完成,请稍后重试或缩小 providers"));
|
|
1187
1211
|
// 认不出的名字要回一条**可读**条目而不是静默丢掉:调用方(尤其是模型)无从知道本插件用的是
|
|
1188
1212
|
// 路由 id,丢掉之后它只看到空列表,会当成「本机没配任何额度」——真正的原因只是名字没对上。
|
|
1189
1213
|
// 放在最前面,免得淹没在几十条数据后面。
|
package/package.json
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subusage",
|
|
3
|
+
"version": "0.10.9",
|
|
3
4
|
"displayName": "订阅用量显示",
|
|
4
|
-
"
|
|
5
|
-
"publishConfig": {
|
|
6
|
-
"access": "public",
|
|
7
|
-
"registry": "https://registry.npmjs.org/"
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/KouzakiUmi/dsh-subusage.git"
|
|
12
|
-
},
|
|
13
|
-
"description": "显示 DeepSeek 官方余额、火山方舟(arkcli 官方 CLI 路由与旧插件路由、Agent/Coding Plan 个人版与企业版席位,设置页合并为一组)、Z.ai(中国与国际)、Kimi、MiMo、OpenCode Go、Command Code、SuperGrok、Codex(ChatGPT 订阅,默认关闭)、MiniMax(中国)、Synthetic、NanoGPT、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM 的订阅余量与余额;共享刷新与凭据管理,默认关闭 20 家。",
|
|
5
|
+
"description": "显示 DeepSeek 官方余额、火山方舟(arkcli 官方 CLI 路由:Agent / Coding Plan 个人版与企业版席位,设置页合并为一组)、Z.ai(中国与国际)、Kimi、MiMo、OpenCode Go、Command Code、SuperGrok、Codex(ChatGPT 订阅,默认关闭)、MiniMax(中国)、Synthetic、NanoGPT、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM 的订阅余量与余额;共享刷新与凭据管理,默认关闭 17 家。",
|
|
14
6
|
"keywords": [
|
|
15
7
|
"dsh",
|
|
16
8
|
"dsh-plugin",
|
|
@@ -20,33 +12,60 @@
|
|
|
20
12
|
"quota"
|
|
21
13
|
],
|
|
22
14
|
"license": "MIT",
|
|
23
|
-
"peerDependencies": {
|
|
24
|
-
"@deepseek-ai/dsh": ">=0.2.0-rc.1 <0.3.0-0",
|
|
25
|
-
"@deepseek-ai/dsh-credentials": ">=0.2.0-rc.1 <0.3.0-0",
|
|
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",
|
|
28
|
-
"@deepseek-ai/dsh-typert-protocol": ">=0.2.0-rc.1 <0.3.0-0"
|
|
29
|
-
},
|
|
30
15
|
"type": "module",
|
|
31
16
|
"main": "./lib/index.js",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.19.0"
|
|
19
|
+
},
|
|
32
20
|
"exports": {
|
|
33
21
|
".": "./lib/index.js",
|
|
34
22
|
"./client": "./lib/client.js",
|
|
35
23
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
36
|
-
"./package.json": "./package.json"
|
|
37
|
-
"./locale/*": "./locale/*"
|
|
24
|
+
"./package.json": "./package.json"
|
|
38
25
|
},
|
|
39
26
|
"files": [
|
|
40
27
|
"lib",
|
|
41
|
-
"locale",
|
|
42
28
|
"cordis.patch.yml",
|
|
43
|
-
"README.md"
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
44
31
|
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node tests/run-all.mjs",
|
|
34
|
+
"check": "node tests/run-all.mjs && node scripts/check-manifest.mjs"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"registry": "https://registry.npmjs.org/"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/KouzakiUmi/dsh-subusage.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/KouzakiUmi/dsh-subusage#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/KouzakiUmi/dsh-subusage/issues"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0 <0.3.0-0",
|
|
50
|
+
"@deepseek-ai/dsh-credentials": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0 <0.3.0-0",
|
|
51
|
+
"@deepseek-ai/dsh-launch-environment": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0 <0.3.0-0",
|
|
52
|
+
"@deepseek-ai/dsh-tools": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0 <0.3.0-0",
|
|
53
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.2.0-rc.1 <0.2.1-0 || >=0.2.1-0 <0.3.0-0"
|
|
54
|
+
},
|
|
45
55
|
"dsh": {
|
|
46
56
|
"manifestVersion": 1,
|
|
47
57
|
"bundle": {
|
|
48
58
|
"patch": "./cordis.patch.yml"
|
|
49
59
|
},
|
|
60
|
+
"compatibility": {
|
|
61
|
+
"node": ">=22.19.0",
|
|
62
|
+
"dshReleases": {
|
|
63
|
+
"0.2.0-rc.1": "unknown",
|
|
64
|
+
"0.2.0-rc.2": "unknown",
|
|
65
|
+
"0.2.1-alpha.1": "compatible",
|
|
66
|
+
"0.2.1-alpha.2": "unknown"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
50
69
|
"client": {
|
|
51
70
|
"immediately": true,
|
|
52
71
|
"inject": [
|
package/locale/en.json
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Subscription usage",
|
|
3
|
-
"description": "Shows subscription quota and balance for 30 AI providers (DeepSeek, Volcengine Ark, Z.ai, Kimi, MiMo, Codex, MiniMax, OpenRouter, SiliconFlow, LiteLLM, ...), with one settings page for detection switches and credentials plus a read-only quota API."
|
|
4
|
-
}
|
package/locale/zh.json
DELETED