dsh-subusage 0.7.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.
@@ -0,0 +1,122 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export const MIMO_ORIGIN = "https://platform.xiaomimimo.com";
4
+ export const MIMO_API_URLS = ["balance", "tokenPlan/detail", "tokenPlan/usage"].map(path => `${MIMO_ORIGIN}/api/v1/${path}`);
5
+ const ACTIVE = new Set(["launching", "waiting", "verifying"]);
6
+ const REQUIRED = ["api-platform_serviceToken", "userId"];
7
+ // Playwright 已按 URL 过滤;再防御性校验每一个必需 Cookie 对全部验证 URL 有效。
8
+ export function extractMimoCookie(cookies, now = Date.now()) {
9
+ const selected = REQUIRED.map(name => cookies.filter(c => c.name === name && typeof c.value === "string" && c.value && (c.expires === undefined || c.expires === -1 || c.expires * 1000 > now) && MIMO_API_URLS.every(raw => {
10
+ const u = new URL(raw); const domain = (c.domain ?? "").replace(/^\./, "").toLowerCase();
11
+ const allowed = ["platform.xiaomimimo.com", "xiaomimimo.com"].includes(domain);
12
+ const domainOK = allowed && (c.domain?.startsWith(".") ? u.hostname === domain || u.hostname.endsWith(`.${domain}`) : u.hostname === domain);
13
+ const path = c.path || "/";
14
+ return domainOK && (u.pathname === path || u.pathname.startsWith(path.endsWith("/") ? path : `${path}/`));
15
+ })));
16
+ if (selected.some(rows => rows.length !== 1)) return null;
17
+ return selected.map(rows => `${rows[0].name}=${rows[0].value}`).join("; ");
18
+ }
19
+ const sleep = (ms, signal) => new Promise(resolve => {
20
+ if (signal.aborted) return resolve();
21
+ const finish = () => { clearTimeout(timer); signal.removeEventListener("abort", finish); resolve(); };
22
+ const timer = setTimeout(finish, ms); signal.addEventListener("abort", finish, { once: true });
23
+ });
24
+ export class MimoLogin {
25
+ constructor(callbacks, options = {}) {
26
+ this.callbacks = callbacks; this.options = options; this.now = options.now ?? Date.now;
27
+ this.current = null; this.disposed = false;
28
+ }
29
+ status() {
30
+ const job = this.current;
31
+ if (job?.public.state === "success" && this.callbacks.reconcileResult) {
32
+ // 终态可被新 UI 反复轮询,但不能回放旧身份或旧 settings revision。
33
+ let result;
34
+ try { result = this.callbacks.reconcileResult(job.public.result); } catch { result = null; }
35
+ if (!result) { this.current = null; return { jobId: null, state: "idle" }; }
36
+ job.public.result = result;
37
+ }
38
+ return this.current ? structuredClone(this.current.public) : { jobId: null, state: "idle" };
39
+ }
40
+ start(request) {
41
+ if (this.disposed) throw this.callbacks.error("subusage/disposed", "服务已卸载");
42
+ if (this.current && ACTIVE.has(this.current.public.state)) return this.status();
43
+ const captured = this.callbacks.capture(request.expectedRevision);
44
+ const job = { public: { jobId: randomUUID(), state: "launching", startedAt: new Date(this.now()).toISOString(), message: "正在打开临时 Chrome 登录窗口" }, controller: new AbortController(), captured, browser: null };
45
+ this.current = job;
46
+ job.timer = setTimeout(() => this.finish(job, "error", "subusage/login-timeout", "登录已超过五分钟,请重新开始"), this.options.timeoutMs ?? 300000);
47
+ // 启动只排队,RPC 不等待浏览器、网络或人工登录。
48
+ void Promise.resolve().then(() => this.run(job));
49
+ return this.status();
50
+ }
51
+ alive(job) { return !job.controller.signal.aborted && !this.disposed && this.current === job; }
52
+ async close(browser) { try { await browser?.close(); } catch {} }
53
+ finish(job, state, errorCode, message, result) {
54
+ if (!this.alive(job)) return;
55
+ Object.assign(job.public, { state, finishedAt: new Date(this.now()).toISOString(), ...(errorCode ? { errorCode } : {}), ...(message ? { message } : {}), ...(result ? { result } : {}) });
56
+ clearTimeout(job.timer); job.controller.abort(); void this.close(job.browser);
57
+ }
58
+ cancel(request) {
59
+ const job = this.current;
60
+ if (job?.public.jobId === request.jobId && ACTIVE.has(job.public.state)) this.finish(job, "cancelled", undefined, "登录已取消,未保存 Cookie");
61
+ return this.status();
62
+ }
63
+ dispose() { if (this.current && ACTIVE.has(this.current.public.state)) this.finish(this.current, "cancelled", undefined, "服务已卸载,登录已取消"); this.disposed = true; }
64
+ async run(job) {
65
+ let phase = "launch";
66
+ try {
67
+ if (!this.alive(job)) return;
68
+ const launch = this.options.launch ?? (async () => {
69
+ const { chromium } = await import("playwright-core");
70
+ if (!this.alive(job)) return null;
71
+ return chromium.launch({ channel: "chrome", headless: false });
72
+ });
73
+ const browser = await launch({ channel: "chrome", headless: false });
74
+ if (!browser) return;
75
+ job.browser = browser;
76
+ if (!this.alive(job)) { await this.close(browser); return; }
77
+ browser.on?.("disconnected", () => this.finish(job, "cancelled", undefined, "登录窗口已关闭,未保存 Cookie"));
78
+ const context = await browser.newContext();
79
+ if (!this.alive(job)) return;
80
+ const page = await context.newPage();
81
+ page.on?.("close", () => this.finish(job, "cancelled", undefined, "登录窗口已关闭,未保存 Cookie"));
82
+ if (!this.alive(job)) return;
83
+ phase = "navigate";
84
+ await page.goto(MIMO_ORIGIN, { waitUntil: "domcontentloaded", timeout: 30000 });
85
+ if (!this.alive(job)) return;
86
+ job.public.state = "waiting"; job.public.message = "请在官方窗口自行完成登录;账户验证通过后保存,额度状态另行显示";
87
+ phase = "verify";
88
+ let nextAttempt = 0, failures = 0;
89
+ while (this.alive(job)) {
90
+ const cookie = extractMimoCookie(await context.cookies(MIMO_API_URLS), this.now());
91
+ if (!this.alive(job)) return;
92
+ if (cookie && this.now() >= nextAttempt) {
93
+ job.public.state = "verifying"; job.public.message = "正在验证登录和订阅额度(尚未保存)";
94
+ try {
95
+ const verified = await this.callbacks.verify(cookie, job.controller.signal);
96
+ if (!this.alive(job)) return;
97
+ if (verified.usageError || !verified.balanceOk) {
98
+ job.public.state = "waiting"; job.public.message = "已读取账户,但订阅额度尚未完整验证;尚未保存,请确认套餐或继续登录";
99
+ nextAttempt = this.now() + 15000;
100
+ } else {
101
+ // commit 必须同步检查冲突并写入,不能在取消后写入。
102
+ const result = this.callbacks.commit(cookie, job.captured, verified);
103
+ this.finish(job, "success", undefined, verified.coverage === "complete" && verified.windows?.length ? "登录及订阅额度已验证,Cookie 已保存" : "登录已保存;仅余额可读,订阅额度未知", result); return;
104
+ }
105
+ } catch (e) {
106
+ if (!this.alive(job)) return;
107
+ if (["subusage/revision-conflict", "subusage/invalid-request", "subusage/config"].includes(e?.code)) {
108
+ this.finish(job, "error", e.code, "设置已改变或无法保存,请重新读取后重试"); return;
109
+ }
110
+ failures++;
111
+ const retry = Number(e?.details?.retryAfterMs);
112
+ nextAttempt = this.now() + Math.max(5000, Math.min(60000, 2000 * 2 ** Math.min(failures, 5)), Number.isFinite(retry) ? Math.min(300000, retry) : 0);
113
+ job.public.state = "waiting"; job.public.message = e?.code === "subusage/auth" ? "登录尚未生效,请继续在官方窗口完成登录;未保存" : "额度验证暂未成功,将限频重试;未保存";
114
+ }
115
+ }
116
+ await sleep(this.options.pollMs ?? 1000, job.controller.signal);
117
+ }
118
+ } catch {
119
+ if (this.alive(job)) this.finish(job, "error", phase === "launch" ? "subusage/chrome-unavailable" : "subusage/login-browser", phase === "launch" ? "无法启动 Chrome,请确认已安装 Chrome 和 playwright-core" : "登录窗口无法继续,请重试;未保存 Cookie");
120
+ } finally { if (!this.alive(job)) await this.close(job.browser); }
121
+ }
122
+ }
package/locale/en.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "Subscription usage",
3
+ "description": "Shows subscription quota for Z.ai, Kimi, MiMo, OpenCode Go, Command Code, MiniMax, Synthetic and NanoGPT, with new providers disabled by default, shared refresh and credential management."
4
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "订阅用量显示",
3
+ "description": "显示 Z.ai / Kimi / MiMo / OpenCode Go / Command Code / MiniMax / Synthetic / NanoGPT 订阅余量;新增提供商默认关闭、共享刷新与凭据管理。"
4
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "dsh-subusage",
3
+ "displayName": "订阅用量显示",
4
+ "version": "0.7.0",
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": "显示 Z.ai(中国与国际)/ Kimi / MiMo / OpenCode Go / Command Code / MiniMax / Synthetic / NanoGPT 订阅余量;新增提供商默认关闭,共享刷新与凭据管理。",
14
+ "keywords": [
15
+ "dsh",
16
+ "dsh-plugin",
17
+ "dsh-client-plugin",
18
+ "subscription",
19
+ "usage",
20
+ "quota"
21
+ ],
22
+ "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-typert-protocol": ">=0.2.0-rc.1 <0.3.0-0"
28
+ },
29
+ "type": "module",
30
+ "main": "./lib/index.js",
31
+ "exports": {
32
+ ".": "./lib/index.js",
33
+ "./client": "./lib/client.js",
34
+ "./cordis.patch.yml": "./cordis.patch.yml",
35
+ "./package.json": "./package.json",
36
+ "./locale/*": "./locale/*"
37
+ },
38
+ "files": [
39
+ "lib",
40
+ "locale",
41
+ "cordis.patch.yml",
42
+ "README.md"
43
+ ],
44
+ "dsh": {
45
+ "manifestVersion": 1,
46
+ "bundle": {
47
+ "patch": "./cordis.patch.yml"
48
+ },
49
+ "client": {
50
+ "immediately": true,
51
+ "inject": [
52
+ "@deepseek-ai/dsh-client-locale",
53
+ "@deepseek-ai/dsh-client-ui-settings",
54
+ "@deepseek-ai/dsh-client-ui-slots",
55
+ "@deepseek-ai/dsh-api-gateway",
56
+ "@deepseek-ai/dsh-api-remotes",
57
+ "@deepseek-ai/dsh-api-session-controller",
58
+ "@deepseek-ai/dsh-client-ui-model-selection"
59
+ ],
60
+ "platform": "web"
61
+ }
62
+ },
63
+ "dependencies": {
64
+ "playwright-core": "1.63.0"
65
+ }
66
+ }