dsh-client-ui-cpa-quota 0.4.2 → 0.6.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 +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +204 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,6 +180,7 @@ Point the plugin's manual instance input at the mock to see a full five-account
|
|
|
180
180
|
- **Stale numbers?** Numbers refresh on the configured interval; a failed refresh keeps the last good snapshot and says so on the card.
|
|
181
181
|
- **Where are my keys stored?** In this browser's `localStorage` only. Quota requests go straight from the browser to each instance; the DSH host never sees your keys.
|
|
182
182
|
- **Switching browsers?** Use **Export config / Import config** on the card to move instances and keys between browsers. The exported JSON is plaintext — store it carefully.
|
|
183
|
+
- **Tooltip shows every account?** The tooltip filters accounts by what each one actually serves, using CPA's own per-auth model registry first, then the model name and the DSH provider id as fallbacks. Only when a model matches no registry entry and no family signal can it be pinned down does the tooltip list the remaining usable accounts — and it says so when it does.
|
|
183
184
|
|
|
184
185
|
## Community
|
|
185
186
|
|
package/README.zh.md
CHANGED
|
@@ -180,6 +180,7 @@ node tests/mock-cpa.mjs # 可选:在 http://127.0.0.1:8317 起一个假 CP
|
|
|
180
180
|
- **数字不新鲜?** 按设定间隔自动刷新;刷新失败会保留上次成功数据并在卡片上说明。
|
|
181
181
|
- **密钥存在哪?** 只存当前浏览器的 `localStorage`。额度请求由浏览器直达各实例,DSH 宿主完全接触不到你的密钥。
|
|
182
182
|
- **换浏览器?** 用卡片里的「导出配置 / 导入配置」在浏览器之间迁移实例与密钥。导出的 JSON 是明文,注意保管。
|
|
183
|
+
- **悬浮卡片显示了全部账号?** 卡片按每个账号「实际服务什么模型」过滤,优先读 CPA 的每账号模型注册表(auth-files/models),模型名和 DSH 提供方 id 只作兜底。仅当某个模型在注册表里无记录、且无法从名称判断归属时,才会列出剩余可用账号,并在卡片里注明。
|
|
183
184
|
|
|
184
185
|
## 社区
|
|
185
186
|
|
package/lib/client.js
CHANGED
|
@@ -39,6 +39,9 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
const EV_UPDATED = "dsh-cpa-quota:updated";
|
|
40
40
|
const EV_REFRESH = "dsh-cpa-quota:refresh-requested";
|
|
41
41
|
const PROBE_TTL = 60 * 60 * 1000;
|
|
42
|
+
/** Unreachable/unknown verdicts retry on this shorter clock so one
|
|
43
|
+
* transient network failure never hides a ring for an hour. */
|
|
44
|
+
const PROBE_RETRY_TTL = 2 * 60 * 1000;
|
|
42
45
|
const W5H = 5 * 60 * 60;
|
|
43
46
|
const W7D = 7 * 24 * 60 * 60;
|
|
44
47
|
const W28D = 28 * 24 * 60 * 60;
|
|
@@ -80,10 +83,13 @@ window.__ModuleLoader__.load({
|
|
|
80
83
|
anthropic: "Claude",
|
|
81
84
|
kimi: "Kimi",
|
|
82
85
|
qwen: "Qwen",
|
|
86
|
+
glm: "GLM",
|
|
87
|
+
deepseek: "DeepSeek",
|
|
83
88
|
xai: "xAI / Grok",
|
|
84
89
|
grok: "xAI / Grok",
|
|
85
90
|
iflow: "iFlow",
|
|
86
91
|
vertex: "Vertex",
|
|
92
|
+
interactions: "Interactions API",
|
|
87
93
|
aistudio: "AI Studio",
|
|
88
94
|
"openai-compatibility": "OpenAI-compatible",
|
|
89
95
|
"openai-compatible": "OpenAI-compatible",
|
|
@@ -170,6 +176,7 @@ window.__ModuleLoader__.load({
|
|
|
170
176
|
if (provider === "anthropic" || provider === "claude-code") return "claude";
|
|
171
177
|
if (provider === "grok" || provider === "x-ai" || provider === "x.ai") return "xai";
|
|
172
178
|
if (provider === "gemini-cli" || provider === "gemini") return provider;
|
|
179
|
+
if (provider === "zai" || provider === "zhipu" || provider === "chatglm") return "glm";
|
|
173
180
|
return provider;
|
|
174
181
|
};
|
|
175
182
|
|
|
@@ -457,13 +464,34 @@ window.__ModuleLoader__.load({
|
|
|
457
464
|
}
|
|
458
465
|
}
|
|
459
466
|
|
|
467
|
+
/**
|
|
468
|
+
* The ChatGPT account id for a Codex auth file. The management API
|
|
469
|
+
* exposes `id_token` as an ALREADY-PARSED claims object (not a JWT
|
|
470
|
+
* string), and CPA also accepts `tokens.account_id` in newer builds —
|
|
471
|
+
* all shapes are handled here.
|
|
472
|
+
*/
|
|
473
|
+
function claimsOf(value) {
|
|
474
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
475
|
+
if (typeof value === "string") return jwtClaims(value);
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
460
479
|
function codexAccountId(entry) {
|
|
461
|
-
const
|
|
480
|
+
const direct = firstText(
|
|
481
|
+
entry.chatgpt_account_id,
|
|
482
|
+
entry.account_id,
|
|
483
|
+
getPath(entry, ["tokens", "account_id"]),
|
|
484
|
+
getPath(entry, ["tokens", "chatgpt_account_id"]),
|
|
485
|
+
getPath(entry, ["metadata", "account_id"]),
|
|
486
|
+
getPath(entry, ["attributes", "account_id"]),
|
|
487
|
+
);
|
|
488
|
+
if (direct !== "") return direct;
|
|
489
|
+
const candidates = [entry.id_token, getPath(entry, ["tokens", "id_token"]), getPath(entry, ["metadata", "id_token"]), getPath(entry, ["attributes", "id_token"])];
|
|
462
490
|
for (const candidate of candidates) {
|
|
463
|
-
const claims =
|
|
491
|
+
const claims = claimsOf(candidate);
|
|
464
492
|
if (claims === null) continue;
|
|
465
|
-
const
|
|
466
|
-
if (
|
|
493
|
+
const directClaim = text(claims.chatgpt_account_id);
|
|
494
|
+
if (directClaim !== "") return directClaim;
|
|
467
495
|
const auth = claims["https://api.openai.com/auth"];
|
|
468
496
|
if (auth !== null && typeof auth === "object") {
|
|
469
497
|
const nestedId = text(auth.chatgpt_account_id);
|
|
@@ -474,8 +502,8 @@ window.__ModuleLoader__.load({
|
|
|
474
502
|
}
|
|
475
503
|
|
|
476
504
|
function codexPlanFromEntry(entry) {
|
|
477
|
-
const candidates = [entry.id_token, getPath(entry, ["metadata", "id_token"]), getPath(entry, ["attributes", "id_token"])]
|
|
478
|
-
.map(
|
|
505
|
+
const candidates = [entry.id_token, getPath(entry, ["tokens", "id_token"]), getPath(entry, ["metadata", "id_token"]), getPath(entry, ["attributes", "id_token"])]
|
|
506
|
+
.map(claimsOf)
|
|
479
507
|
.filter((claims) => claims !== null);
|
|
480
508
|
return extractPlan(...candidates);
|
|
481
509
|
}
|
|
@@ -620,8 +648,12 @@ window.__ModuleLoader__.load({
|
|
|
620
648
|
* @returns {'cpa'|'no'|'unknown'|'unreachable'}
|
|
621
649
|
*/
|
|
622
650
|
async function probeCpa(base) {
|
|
651
|
+
// A hung probe never yields a verdict and the ring stays missing, so
|
|
652
|
+
// bound the request like every other network call.
|
|
653
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
654
|
+
const timer = controller === null ? 0 : window.setTimeout(() => controller.abort(), 10_000);
|
|
623
655
|
try {
|
|
624
|
-
const response = await fetch(`${base}/v0/management/usage-statistics-enabled`, { method: "GET" });
|
|
656
|
+
const response = await fetch(`${base}/v0/management/usage-statistics-enabled`, { method: "GET", ...(controller === null ? {} : { signal: controller.signal }) });
|
|
625
657
|
const body = await response.text();
|
|
626
658
|
if (response.status === 401) return /management|unauthor/i.test(body) ? "cpa" : "unknown";
|
|
627
659
|
if (response.status === 404) return "no";
|
|
@@ -631,6 +663,8 @@ window.__ModuleLoader__.load({
|
|
|
631
663
|
return "unknown";
|
|
632
664
|
} catch {
|
|
633
665
|
return "unreachable";
|
|
666
|
+
} finally {
|
|
667
|
+
if (timer !== 0) window.clearTimeout(timer);
|
|
634
668
|
}
|
|
635
669
|
}
|
|
636
670
|
|
|
@@ -639,9 +673,16 @@ window.__ModuleLoader__.load({
|
|
|
639
673
|
const targets = [];
|
|
640
674
|
for (const [key, info] of providerBases) {
|
|
641
675
|
const hit = cache[key];
|
|
642
|
-
if (!force && hit !== undefined
|
|
643
|
-
|
|
644
|
-
|
|
676
|
+
if (!force && hit !== undefined) {
|
|
677
|
+
// Confirmed verdicts (cpa / a plain 404) keep the long cache; a
|
|
678
|
+
// transient network failure or ambiguous answer must retry
|
|
679
|
+
// quickly — caching it for an hour left rings missing after a
|
|
680
|
+
// single hiccup.
|
|
681
|
+
const ttl = hit.cpa === true || hit.verdict === "no" ? PROBE_TTL : PROBE_RETRY_TTL;
|
|
682
|
+
if (Date.now() - hit.at < ttl) {
|
|
683
|
+
discoveredByKey.set(key, { ...hit, baseURL: info.baseURL });
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
645
686
|
}
|
|
646
687
|
targets.push([key, info]);
|
|
647
688
|
}
|
|
@@ -1337,6 +1378,49 @@ window.__ModuleLoader__.load({
|
|
|
1337
1378
|
* trying it first spares the other accounts a 5-endpoint walk. */
|
|
1338
1379
|
const agEndpointByBase = new Map();
|
|
1339
1380
|
|
|
1381
|
+
/**
|
|
1382
|
+
* Per-account served-model registry: CPA exposes the models each auth
|
|
1383
|
+
* file routes (`GET /v0/management/auth-files/models?name=…`) — the same
|
|
1384
|
+
* table its own router uses. Cached per account so refreshes stay cheap;
|
|
1385
|
+
* a failed lookup stays uncached and retries next refresh.
|
|
1386
|
+
*/
|
|
1387
|
+
const ACCOUNT_MODELS_TTL = 10 * 60 * 1000;
|
|
1388
|
+
const accountModelsByKey = new Map();
|
|
1389
|
+
|
|
1390
|
+
async function loadAccountModelList(instance, entry) {
|
|
1391
|
+
// Key by the auth FILE NAME — it is stable across CPA's index
|
|
1392
|
+
// reshuffles; auth_index moves when accounts are added or removed.
|
|
1393
|
+
const name = firstText(entry.name, entry.id);
|
|
1394
|
+
if (name === "") return null;
|
|
1395
|
+
const key = `${baseKey(instance.baseURL)}\u0000${name}`;
|
|
1396
|
+
const hit = accountModelsByKey.get(key);
|
|
1397
|
+
if (hit !== undefined && Date.now() - hit.at < ACCOUNT_MODELS_TTL) return hit.ids;
|
|
1398
|
+
try {
|
|
1399
|
+
const payload = await fetchJSON(`${instance.base}/v0/management/auth-files/models?name=${encodeURIComponent(name)}`, {
|
|
1400
|
+
headers: { Authorization: `Bearer ${instance.managementKey}` },
|
|
1401
|
+
});
|
|
1402
|
+
const rows = Array.isArray(payload?.models) ? payload.models : [];
|
|
1403
|
+
const ids = rows
|
|
1404
|
+
.map((m) => (typeof m === "string" ? text(m) : firstText(m?.id, m?.name, m?.display_name)))
|
|
1405
|
+
.filter((id) => id !== "");
|
|
1406
|
+
const out = ids.length > 0 ? ids : null;
|
|
1407
|
+
if (out !== null) accountModelsByKey.set(key, { at: Date.now(), ids: out });
|
|
1408
|
+
return out;
|
|
1409
|
+
} catch {
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/** Loose match between a declared model id and the selected one: exact,
|
|
1415
|
+
* same series (effort suffixes stripped), or a pure extension either way. */
|
|
1416
|
+
function modelListMatches(listedId, modelId) {
|
|
1417
|
+
const a = text(listedId).toLowerCase();
|
|
1418
|
+
const b = text(modelId).toLowerCase();
|
|
1419
|
+
if (a === "" || b === "") return false;
|
|
1420
|
+
if (a === b || seriesKey(a) === seriesKey(b)) return true;
|
|
1421
|
+
return b.startsWith(`${a}-`) || a.startsWith(`${b}-`);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1340
1424
|
async function queryAccount(instance, entry) {
|
|
1341
1425
|
const provider = normalizeProvider(firstText(entry.provider, entry.type));
|
|
1342
1426
|
const authIndex = firstText(entry.auth_index, entry.authIndex);
|
|
@@ -1345,6 +1429,7 @@ window.__ModuleLoader__.load({
|
|
|
1345
1429
|
name: firstText(entry.label, entry.email, entry.account_email, entry.name, entry.id, authIndex, "unknown"),
|
|
1346
1430
|
provider,
|
|
1347
1431
|
authIndex,
|
|
1432
|
+
modelList: null,
|
|
1348
1433
|
status: firstText(entry.status),
|
|
1349
1434
|
statusMessage: firstText(entry.status_message, entry.statusMessage),
|
|
1350
1435
|
disabled: boolOf(entry.disabled),
|
|
@@ -1354,6 +1439,10 @@ window.__ModuleLoader__.load({
|
|
|
1354
1439
|
windows: [],
|
|
1355
1440
|
error: "",
|
|
1356
1441
|
};
|
|
1442
|
+
// Which models CPA routes to this account — the authoritative filter
|
|
1443
|
+
// signal. Fetched with the account's own management key; a failure
|
|
1444
|
+
// just means the account falls back to family-name filtering.
|
|
1445
|
+
account.modelList = await loadAccountModelList(instance, entry);
|
|
1357
1446
|
const rememberPlan = () => {
|
|
1358
1447
|
if (planKey !== "" && account.plan !== "") planByAccountKey.set(planKey, account.plan);
|
|
1359
1448
|
};
|
|
@@ -1374,21 +1463,21 @@ window.__ModuleLoader__.load({
|
|
|
1374
1463
|
}
|
|
1375
1464
|
try {
|
|
1376
1465
|
if (provider === "codex") {
|
|
1466
|
+
// No id found is not fatal: some CPA setups bind the account
|
|
1467
|
+
// server-side, so try without the Chatgpt-Account-Id header
|
|
1468
|
+
// and let an upstream rejection surface as the account error.
|
|
1377
1469
|
const accountID = codexAccountId(entry);
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1470
|
+
const usageHeaders = {
|
|
1471
|
+
Authorization: "Bearer $TOKEN$",
|
|
1472
|
+
"Content-Type": "application/json",
|
|
1473
|
+
"User-Agent": "codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal",
|
|
1474
|
+
};
|
|
1475
|
+
if (accountID !== "") usageHeaders["Chatgpt-Account-Id"] = accountID;
|
|
1382
1476
|
const body = unwrapApiCall(await apiCall(instance, {
|
|
1383
1477
|
auth_index: authIndex,
|
|
1384
1478
|
method: "GET",
|
|
1385
1479
|
url: UPSTREAM.codexUsage,
|
|
1386
|
-
header:
|
|
1387
|
-
Authorization: "Bearer $TOKEN$",
|
|
1388
|
-
"Content-Type": "application/json",
|
|
1389
|
-
"User-Agent": "codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal",
|
|
1390
|
-
"Chatgpt-Account-Id": accountID,
|
|
1391
|
-
},
|
|
1480
|
+
header: usageHeaders,
|
|
1392
1481
|
}));
|
|
1393
1482
|
account.windows = parseCodexWindows(body);
|
|
1394
1483
|
account.plan = extractPlan(body) || account.plan;
|
|
@@ -1613,12 +1702,35 @@ window.__ModuleLoader__.load({
|
|
|
1613
1702
|
if (lower === "") return "";
|
|
1614
1703
|
if (/(?:grok|xai)/.test(lower)) return "xai";
|
|
1615
1704
|
if (/(?:kimi|moonshot)/.test(lower)) return "kimi";
|
|
1705
|
+
if (/(?:qwen|qwq)/.test(lower)) return "qwen";
|
|
1706
|
+
if (/(?:iflow)/.test(lower)) return "iflow";
|
|
1707
|
+
if (/(?:glm|chatglm|zhipu)/.test(lower)) return "glm";
|
|
1708
|
+
if (/(?:deepseek)/.test(lower)) return "deepseek";
|
|
1616
1709
|
if (/(?:gemini|gemma)/.test(lower)) return "gemini";
|
|
1617
1710
|
if (/(?:claude|sonnet|opus|haiku|fable)/.test(lower)) return "claude";
|
|
1618
1711
|
if (/(?:gpt|openai|codex)/.test(lower) || /(?:^|[-_:/.])o[1-4](?:$|[-_:/.])/.test(lower)) return "gpt";
|
|
1619
1712
|
return "";
|
|
1620
1713
|
}
|
|
1621
1714
|
|
|
1715
|
+
/**
|
|
1716
|
+
* Second signal for the account filter: the DSH provider id that serves
|
|
1717
|
+
* the model. Custom route names ("team-claude", "fast-tier") reveal
|
|
1718
|
+
* nothing on their own, but the provider usually does.
|
|
1719
|
+
*/
|
|
1720
|
+
function providerFamilyFromId(providerId) {
|
|
1721
|
+
const p = text(providerId).toLowerCase();
|
|
1722
|
+
if (/(?:anthropic|claude)/.test(p)) return "claude";
|
|
1723
|
+
if (/(?:gemini)/.test(p)) return "gemini";
|
|
1724
|
+
if (/(?:codex|openai|gpt)/.test(p)) return "gpt";
|
|
1725
|
+
if (/(?:kimi|moonshot)/.test(p)) return "kimi";
|
|
1726
|
+
if (/(?:grok|xai)/.test(p)) return "xai";
|
|
1727
|
+
if (/(?:qwen)/.test(p)) return "qwen";
|
|
1728
|
+
if (/(?:iflow)/.test(p)) return "iflow";
|
|
1729
|
+
if (/(?:deepseek)/.test(p)) return "deepseek";
|
|
1730
|
+
if (/(?:glm|zai|zhipu)/.test(p)) return "glm";
|
|
1731
|
+
return "";
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1622
1734
|
function modelSpecificWindows(account, modelId) {
|
|
1623
1735
|
const lower = text(modelId).toLowerCase();
|
|
1624
1736
|
if (lower === "") return [];
|
|
@@ -1648,25 +1760,58 @@ window.__ModuleLoader__.load({
|
|
|
1648
1760
|
case "openai": return family === "gpt";
|
|
1649
1761
|
case "kimi": return family === "kimi";
|
|
1650
1762
|
case "xai": return family === "xai";
|
|
1763
|
+
case "qwen": return family === "qwen";
|
|
1764
|
+
case "iflow": return family === "iflow";
|
|
1765
|
+
case "glm": return family === "glm";
|
|
1766
|
+
case "deepseek": return family === "deepseek";
|
|
1767
|
+
case "vertex": return family === "gemini" || family === "claude";
|
|
1768
|
+
case "interactions": return ["gemini", "claude", "gpt"].includes(family);
|
|
1651
1769
|
default: return false;
|
|
1652
1770
|
}
|
|
1653
1771
|
}
|
|
1654
1772
|
|
|
1655
|
-
/**
|
|
1656
|
-
|
|
1773
|
+
/**
|
|
1774
|
+
* Accounts whose served models match the selected one, in three tiers:
|
|
1775
|
+
* 1. accounts that DECLARE their model list (CPA's per-auth routing
|
|
1776
|
+
* registry) and list this model — authoritative, wins outright, and
|
|
1777
|
+
* a declared list without the model excludes that account from every
|
|
1778
|
+
* fallback below;
|
|
1779
|
+
* 2. family heuristics over the remaining accounts — the model name
|
|
1780
|
+
* first, then the DSH provider id that serves it;
|
|
1781
|
+
* 3. when no family can be determined, CPA's routing decision is
|
|
1782
|
+
* unknowable from the client — every usable account is listed and
|
|
1783
|
+
* the tooltip says why.
|
|
1784
|
+
*/
|
|
1785
|
+
function accountsForModel(accounts, modelId, providerHint = "") {
|
|
1657
1786
|
if (!Array.isArray(accounts)) return [];
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
return
|
|
1669
|
-
|
|
1787
|
+
if (text(modelId) === "") return accounts;
|
|
1788
|
+
let family = modelFamily(modelId);
|
|
1789
|
+
if (family === "") family = providerFamilyFromId(providerHint);
|
|
1790
|
+
|
|
1791
|
+
const listed = accounts.filter((account) => Array.isArray(account.modelList) && account.modelList.length > 0);
|
|
1792
|
+
const exact = listed.filter((account) => account.modelList.some((id) => modelListMatches(id, modelId)));
|
|
1793
|
+
if (exact.length > 0) return exact;
|
|
1794
|
+
const unlisted = accounts.filter((account) => !(Array.isArray(account.modelList) && account.modelList.length > 0));
|
|
1795
|
+
const usable = (list) => list.filter((account) => !account.disabled && !account.unavailable);
|
|
1796
|
+
|
|
1797
|
+
if (family === "") return usable(unlisted);
|
|
1798
|
+
// Cross-channel models (Claude served by both Antigravity and Claude
|
|
1799
|
+
// OAuth, for example) are load-balanced by CPA between the channels,
|
|
1800
|
+
// so specific-window accounts and generic family matches union — the
|
|
1801
|
+
// provider badge tells the user which channel each account is on.
|
|
1802
|
+
const specific = usable(unlisted).filter((account) => modelSpecificWindows(account, modelId).length > 0);
|
|
1803
|
+
const generic = usable(unlisted).filter((account) => {
|
|
1804
|
+
if (!providerMatchesFamily(account.provider, family)) return false;
|
|
1805
|
+
const rows = Array.isArray(account.windows) ? account.windows : [];
|
|
1806
|
+
// An account with model/prefix-tagged rows for another family must not
|
|
1807
|
+
// fall through as a generic provider account for this model.
|
|
1808
|
+
const hasTaggedRows = rows.some((row) => row.modelId !== null || Array.isArray(row.modelIds) || Array.isArray(row.prefixes));
|
|
1809
|
+
return !hasTaggedRows;
|
|
1810
|
+
});
|
|
1811
|
+
if (specific.length > 0 || generic.length > 0) return [...specific, ...generic];
|
|
1812
|
+
// Family known but nothing serves it: the honest answer is "no
|
|
1813
|
+
// matching account", not a list of guesses.
|
|
1814
|
+
return [];
|
|
1670
1815
|
}
|
|
1671
1816
|
|
|
1672
1817
|
function modelRowsForAccount(account, modelId) {
|
|
@@ -1728,12 +1873,12 @@ window.__ModuleLoader__.load({
|
|
|
1728
1873
|
});
|
|
1729
1874
|
}
|
|
1730
1875
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1876
|
+
/** Ring level for one instance given the current model. */
|
|
1877
|
+
function levelFor(state, modelId, providerHint = "") {
|
|
1878
|
+
if (state === undefined) return { level: "pending", remaining: null };
|
|
1879
|
+
if (state.state === "nokey") return { level: "pending", remaining: null };
|
|
1880
|
+
if (state.state === "error") return { level: "error", remaining: null };
|
|
1881
|
+
const matchingAccounts = accountsForModel(state.accounts, modelId, providerHint);
|
|
1737
1882
|
const rows = [];
|
|
1738
1883
|
for (const account of matchingAccounts) {
|
|
1739
1884
|
if (account.disabled || account.unavailable) continue;
|
|
@@ -2464,6 +2609,7 @@ body[data-ds-dark-theme] .cpa-q-select{color-scheme:dark}
|
|
|
2464
2609
|
function showTooltip(dot) {
|
|
2465
2610
|
const key = dot.getAttribute("data-cpa-base") ?? "";
|
|
2466
2611
|
const modelId = dot.getAttribute("data-cpa-model") ?? "";
|
|
2612
|
+
const providerHint = dot.getAttribute("data-cpa-provider") ?? "";
|
|
2467
2613
|
const instance = instanceByKey.get(key);
|
|
2468
2614
|
const state = quotaByKey.get(key);
|
|
2469
2615
|
if (instance === undefined) return;
|
|
@@ -2515,12 +2661,17 @@ body[data-ds-dark-theme] .cpa-q-select{color-scheme:dark}
|
|
|
2515
2661
|
} else if (state.accounts.length === 0) {
|
|
2516
2662
|
tip.appendChild(el("div", "cpa-q-row", ZH ? "该实例没有账号(auth-files 为空)" : "no auth files on this instance"));
|
|
2517
2663
|
} else {
|
|
2518
|
-
const matchingAccounts = accountsForModel(state.accounts, modelId);
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2664
|
+
const matchingAccounts = accountsForModel(state.accounts, modelId, providerHint);
|
|
2665
|
+
if (matchingAccounts.length === 0) {
|
|
2666
|
+
tip.appendChild(el("div", "cpa-q-row", ZH ? "该模型没有匹配的厂商额度" : "no matching provider quota for this model"));
|
|
2667
|
+
} else {
|
|
2668
|
+
if (modelFamily(modelId) === "" && providerFamilyFromId(providerHint) === "") {
|
|
2669
|
+
tip.appendChild(el("div", "cpa-q-hint", ZH
|
|
2670
|
+
? "无法从模型名识别账号归属,以下为该实例全部可用账号"
|
|
2671
|
+
: "model family unknown — listing every usable account on this instance"));
|
|
2672
|
+
}
|
|
2673
|
+
for (const account of matchingAccounts) tip.appendChild(accountSection(account, modelId));
|
|
2674
|
+
}
|
|
2524
2675
|
if (state.stale && state.lastError) {
|
|
2525
2676
|
const hint = el("div", "cpa-q-hint");
|
|
2526
2677
|
hint.textContent = ZH ? "暂时无法刷新,显示上次成功数据" : "refresh temporarily unavailable; showing the last good snapshot";
|
|
@@ -2612,7 +2763,7 @@ body[data-ds-dark-theme] .cpa-q-select{color-scheme:dark}
|
|
|
2612
2763
|
function applyDotState(ring) {
|
|
2613
2764
|
const key = ring.getAttribute("data-cpa-base") ?? "";
|
|
2614
2765
|
const modelId = ring.getAttribute("data-cpa-model") ?? "";
|
|
2615
|
-
const agg = levelFor(quotaByKey.get(key), modelId);
|
|
2766
|
+
const agg = levelFor(quotaByKey.get(key), modelId, ring.getAttribute("data-cpa-provider") ?? "");
|
|
2616
2767
|
ring.setAttribute("data-cpa-level", agg.level);
|
|
2617
2768
|
const arc = ring.querySelector(".cpa-q-arc");
|
|
2618
2769
|
if (arc !== null) {
|
|
@@ -2805,9 +2956,11 @@ body[data-ds-dark-theme] .cpa-q-select{color-scheme:dark}
|
|
|
2805
2956
|
if (ring.getAttribute("data-cpa-model") !== modelId) {
|
|
2806
2957
|
ring.setAttribute("data-cpa-model", modelId);
|
|
2807
2958
|
ring.setAttribute("data-cpa-base", key);
|
|
2959
|
+
ring.setAttribute("data-cpa-provider", model.providerId ?? "");
|
|
2808
2960
|
if (tipAnchor === ring) showTooltip(ring);
|
|
2809
2961
|
}
|
|
2810
2962
|
ring.setAttribute("data-cpa-base", key);
|
|
2963
|
+
ring.setAttribute("data-cpa-provider", model.providerId ?? "");
|
|
2811
2964
|
placeRing(ring, button);
|
|
2812
2965
|
applyDotState(ring);
|
|
2813
2966
|
}
|
|
@@ -2890,8 +3043,10 @@ body[data-ds-dark-theme] .cpa-q-select{color-scheme:dark}
|
|
|
2890
3043
|
styleTag.remove();
|
|
2891
3044
|
|
|
2892
3045
|
quotaByKey.clear();
|
|
2893
|
-
|
|
2894
|
-
|
|
3046
|
+
planByAccountKey.clear();
|
|
3047
|
+
discoveredByKey.clear();
|
|
3048
|
+
accountModelsByKey.clear();
|
|
3049
|
+
agEndpointByBase.clear();
|
|
2895
3050
|
}, "ui-cpa-quota: dot, tooltip, card, observer, timers");
|
|
2896
3051
|
}
|
|
2897
3052
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-client-ui-cpa-quota",
|
|
3
3
|
"description": "CliProxyAPI quota indicator: a native-styled ring left of the composer model name; hover shows per-account provider quotas, subscription plans, and status for Codex, Claude, Antigravity, Gemini CLI, Kimi, xAI/Grok, and other CPA auth providers. Targets the current dsh plugin contract (settings-namespace-keyed plugin cards).",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"author": "wkscc310",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|