dsh-subusage 0.10.10 → 0.11.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 +91 -39
- package/lib/client.js +309 -149
- package/lib/index.js +185 -39
- package/package.json +3 -3
package/lib/client.js
CHANGED
|
@@ -31,7 +31,7 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
lightOk: "全部拉取成功",
|
|
32
32
|
lightFail: "存在拉取失败",
|
|
33
33
|
statusOk: "数据获取成功",
|
|
34
|
-
statusPartial: "
|
|
34
|
+
statusPartial: "数据不完整",
|
|
35
35
|
statusCache: "缓存数据",
|
|
36
36
|
cacheAge: "缓存 · {n} 分钟前",
|
|
37
37
|
cacheUnknown: "缓存时间未知",
|
|
@@ -58,7 +58,6 @@ window.__ModuleLoader__.load({
|
|
|
58
58
|
resets: "重置于",
|
|
59
59
|
limited: "已达限额",
|
|
60
60
|
cascadeLimited: "(受更长周期限额连累,暂不可用)",
|
|
61
|
-
retry: "立即重试",
|
|
62
61
|
loading: "正在读取用量…",
|
|
63
62
|
unavailable: "暂不可用",
|
|
64
63
|
planLabel: "套餐",
|
|
@@ -91,16 +90,27 @@ window.__ModuleLoader__.load({
|
|
|
91
90
|
restDays: "{n} 天",
|
|
92
91
|
restHours: "{n} 小时",
|
|
93
92
|
restMins: "{n} 分钟",
|
|
94
|
-
|
|
95
|
-
badgeHigh
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
// 档位词只描述「还剩多少」;旧的「偏高」没写清主语(用量高还是余量高),读起来自相矛盾。
|
|
94
|
+
// 键名 badgeHigh 保留(它对应已用 71–90% 的档位),文案改成「偏少」。
|
|
95
|
+
badgeOk: "充足",
|
|
96
|
+
badgeHigh: "偏少",
|
|
97
|
+
badgeTight: "即将用尽",
|
|
98
|
+
badgeLimited: "已用尽",
|
|
98
99
|
badgeBlocked: "受连累",
|
|
100
|
+
statusPlenty: "额度充足",
|
|
101
|
+
statusLow: "余量偏低",
|
|
102
|
+
statusTight: "即将用尽",
|
|
103
|
+
statusExhausted: "额度已用尽",
|
|
104
|
+
statusBlocked: "暂不可用",
|
|
105
|
+
statusWorst: "最紧张:{w} · 剩 {p}%",
|
|
106
|
+
statusOnly: "{w} · 剩 {p}%",
|
|
107
|
+
statusResetIn: "{rest}后重置",
|
|
108
|
+
remainWord: "剩余",
|
|
99
109
|
numUsed: "已用 {p}%",
|
|
100
110
|
numLimited: "⚠ 已达限额",
|
|
101
111
|
numBlocked: "⚠ 暂不可用",
|
|
102
112
|
pillRemaining: "余 {p}%",
|
|
103
|
-
pillLimited: "
|
|
113
|
+
pillLimited: "已用尽",
|
|
104
114
|
pillBlocked: "受连累",
|
|
105
115
|
needConfigShort: "需配置",
|
|
106
116
|
pillNeedKey: "未配置 API Key —— 请打开 设置 → 订阅用量 填写,或导出同名环境变量后重启。",
|
|
@@ -117,14 +127,14 @@ window.__ModuleLoader__.load({
|
|
|
117
127
|
pillNeedGrok: "需登录 —— 可在 设置 → Grok Kit(xAI OAuth)登录或运行 grok login(共享登录文件 ~/.grok/auth.json)。",
|
|
118
128
|
accountLabel: "账户",
|
|
119
129
|
commandcodeManaged: "凭据由 Command Code 提供方插件(设置 → Command Code)管理;本页只读、不保存凭据。用量由本插件直接拉取其计费接口,取用同一凭据来源(凭据服务 COMMANDCODE_API_KEY → 启动环境 → ~/.commandcode/auth.json)。",
|
|
120
|
-
commandcodeHelp: "并行读取 /alpha/billing/credits 和 /alpha/billing/subscriptions(默认 API 地址 https://api.commandcode.ai
|
|
130
|
+
commandcodeHelp: "并行读取 /alpha/billing/credits 和 /alpha/billing/subscriptions(默认 API 地址 https://api.commandcode.ai)。月总额按已知套餐快照推算,未知套餐只显示月剩余;月池耗尽不代表已购/赠送池不可用。显示选中账户的额度(未选择时为默认账户/顶层 Key),可能与实际服务账户不同;插件里自定义 apiBase 不会被跟随。账户切换就在下方「Command Code 账户」区,与药丸弹层同一开关;模型查询额度工具时会同时给出全部账户的余量与当前激活项。",
|
|
121
131
|
grokManaged: "凭据由 dsh-grok-kit / Grok CLI 的 OAuth 登录文件(~/.grok/auth.json)管理;本页只读、不保存凭据、不刷新 token。用量由本插件取同一凭据直连 Grok CLI 计费代理(cli-chat-proxy.grok.com)拉取。",
|
|
122
132
|
grokHelp: "并行读取 /v1/billing?format=credits 与 /v1/settings。creditUsagePercent 是统一用量池的已用百分比(通常为周池,重置于 currentPeriod.end,剩余 = 100 − 已用);套餐名取 subscription_tier_display;已购加量余额 prepaidBalance 以美元显示。API Key(XAI_API_KEY)路线取不到订阅周池,必须 OAuth 登录。",
|
|
123
133
|
ccAccountsTitle: "Command Code 账户",
|
|
124
134
|
ccAccountAuto: "自动轮换",
|
|
125
135
|
ccAccountDefault: "默认账户",
|
|
126
136
|
ccAccountActive: "使用中",
|
|
127
|
-
ccAccountsHint: "切换对 Command Code
|
|
137
|
+
ccAccountsHint: "切换对 Command Code 提供方插件全局生效(与其设置页同一开关);上方用量与药丸同步切换为所选账户的额度。设置页与药丸弹层用的是同一个开关。",
|
|
128
138
|
ccAccountsUnavailable: "账户列表不可用(需要 Command Code 提供方插件在运行)",
|
|
129
139
|
ccUsageAccount: "上方用量:{account}",
|
|
130
140
|
ccUsageAutoNote: "自动轮换时上方用量仍是默认账户(顶层 Key),不跟随实际服务账户",
|
|
@@ -144,7 +154,7 @@ window.__ModuleLoader__.load({
|
|
|
144
154
|
lightOk: "All fetches succeeded",
|
|
145
155
|
lightFail: "Some fetches failed",
|
|
146
156
|
statusOk: "Fetch OK",
|
|
147
|
-
statusPartial: "Partial data
|
|
157
|
+
statusPartial: "Partial data",
|
|
148
158
|
statusCache: "Cached data",
|
|
149
159
|
cacheAge: "Cached · {n} min ago",
|
|
150
160
|
cacheUnknown: "Cache age unknown",
|
|
@@ -171,7 +181,6 @@ window.__ModuleLoader__.load({
|
|
|
171
181
|
resets: "Resets",
|
|
172
182
|
limited: "Limit reached",
|
|
173
183
|
cascadeLimited: " (blocked by a larger window's limit)",
|
|
174
|
-
retry: "Retry now",
|
|
175
184
|
loading: "Loading usage…",
|
|
176
185
|
unavailable: "Unavailable",
|
|
177
186
|
planLabel: "Plan",
|
|
@@ -204,16 +213,26 @@ window.__ModuleLoader__.load({
|
|
|
204
213
|
restDays: "{n} d",
|
|
205
214
|
restHours: "{n} h",
|
|
206
215
|
restMins: "{n} min",
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
// Tier words describe what is LEFT; the old "High" never said high what.
|
|
217
|
+
badgeOk: "Plenty",
|
|
218
|
+
badgeHigh: "Low",
|
|
219
|
+
badgeTight: "Almost out",
|
|
220
|
+
badgeLimited: "Used up",
|
|
211
221
|
badgeBlocked: "Blocked",
|
|
222
|
+
statusPlenty: "Plenty left",
|
|
223
|
+
statusLow: "Running low",
|
|
224
|
+
statusTight: "Almost out",
|
|
225
|
+
statusExhausted: "Quota used up",
|
|
226
|
+
statusBlocked: "Temporarily blocked",
|
|
227
|
+
statusWorst: "Tightest: {w} · {p}% left",
|
|
228
|
+
statusOnly: "{w} · {p}% left",
|
|
229
|
+
statusResetIn: "resets in {rest}",
|
|
230
|
+
remainWord: "left",
|
|
212
231
|
numUsed: "Used {p}%",
|
|
213
232
|
numLimited: "⚠ Limit reached",
|
|
214
233
|
numBlocked: "⚠ Unavailable",
|
|
215
234
|
pillRemaining: "{p}% left",
|
|
216
|
-
pillLimited: "
|
|
235
|
+
pillLimited: "Used up",
|
|
217
236
|
pillBlocked: "Blocked",
|
|
218
237
|
needConfigShort: "Setup",
|
|
219
238
|
pillNeedKey: "No API key — open Settings → Subscription usage to fill one in, or export the same-named env variable and restart.",
|
|
@@ -230,14 +249,14 @@ window.__ModuleLoader__.load({
|
|
|
230
249
|
pillNeedGrok: "Login required — sign in under Settings → Grok Kit (xAI OAuth) or run grok login (shared login file ~/.grok/auth.json).",
|
|
231
250
|
accountLabel: "Account",
|
|
232
251
|
commandcodeManaged: "Credentials are managed by the Command Code provider plugin (Settings → Command Code); this page is read-only and stores nothing. Usage is fetched directly by this plugin from its billing endpoint, using the same credential sources (credential service COMMANDCODE_API_KEY → launch environment → ~/.commandcode/auth.json).",
|
|
233
|
-
commandcodeHelp: "Parallel requests to /alpha/billing/credits and /alpha/billing/subscriptions (default API base https://api.commandcode.ai). Monthly totals use a known-plan snapshot; unknown plans show remaining credits only. Monthly-pool depletion does not imply purchased/free credits are unusable. Shows the
|
|
252
|
+
commandcodeHelp: "Parallel requests to /alpha/billing/credits and /alpha/billing/subscriptions (default API base https://api.commandcode.ai). Monthly totals use a known-plan snapshot; unknown plans show remaining credits only. Monthly-pool depletion does not imply purchased/free credits are unusable. Shows the selected account's quota (the default/top-level key account when none is selected), which may differ from the serving account; a custom apiBase in the provider plugin is not followed. Account switching lives in the “Command Code accounts” block below — the same switch as the pill popover; the quota tool reports every account plus which one is active.",
|
|
234
253
|
grokManaged: "Credentials are the OAuth login file shared by dsh-grok-kit / Grok CLI (~/.grok/auth.json); this page is read-only — it stores nothing and never refreshes the token. Usage is fetched directly by this plugin from the Grok CLI billing proxy (cli-chat-proxy.grok.com) with the same credential.",
|
|
235
254
|
grokHelp: "Parallel requests to /v1/billing?format=credits and /v1/settings. creditUsagePercent is the USED percentage of the unified usage pool (usually weekly, resetting at currentPeriod.end; remaining = 100 − used). The plan name comes from subscription_tier_display; the purchased top-up balance prepaidBalance is shown in USD. An API key (XAI_API_KEY) cannot read the subscription pool — OAuth sign-in is required.",
|
|
236
255
|
ccAccountsTitle: "Command Code accounts",
|
|
237
256
|
ccAccountAuto: "Automatic (rotation order)",
|
|
238
257
|
ccAccountDefault: "Default account",
|
|
239
258
|
ccAccountActive: "in use",
|
|
240
|
-
ccAccountsHint: "Switching applies globally to the Command Code provider plugin (same switch as its settings page); the usage above
|
|
259
|
+
ccAccountsHint: "Switching applies globally to the Command Code provider plugin (same switch as its settings page); the usage above and the pill follow the selected account. This page and the pill popover share one switch.",
|
|
241
260
|
ccAccountsUnavailable: "Account list unavailable (requires the Command Code provider plugin to be running)",
|
|
242
261
|
ccUsageAccount: "Usage above: {account}",
|
|
243
262
|
ccUsageAutoNote: "with automatic rotation the usage above stays on the default (top-level key) account, not the serving one",
|
|
@@ -356,24 +375,6 @@ window.__ModuleLoader__.load({
|
|
|
356
375
|
if (w.percent >= 71) return { icon: "⚠", badge: "badgeHigh", color: "#eab308", num: null, rank: 2 };
|
|
357
376
|
return { icon: "✓", badge: "badgeOk", color: "#22c55e", num: null, rank: 1 };
|
|
358
377
|
}
|
|
359
|
-
/** tag 圆点色:最差窗口档位色(绿/黄/橙/红);拉取失败红。 */
|
|
360
|
-
function tagColor(entry) {
|
|
361
|
-
if (!entry || entry.state === "loading") return "#94a3b8";
|
|
362
|
-
if (entry.state !== "ok") return "#ef4444";
|
|
363
|
-
if ((entry.windows || []).some((w) => w.status === "rate-limited")) return "#ef4444";
|
|
364
|
-
if (entry.coverage === "partial" || entry.freshness === "stale") return "#eab308";
|
|
365
|
-
const rank = { "#22c55e": 0, "#eab308": 1, "#f97316": 2, "#ef4444": 3 };
|
|
366
|
-
let worst = "#22c55e";
|
|
367
|
-
for (const w of entry.windows || []) {
|
|
368
|
-
const c = usageTier(w).color;
|
|
369
|
-
if (rank[c] > rank[worst]) worst = c;
|
|
370
|
-
}
|
|
371
|
-
return worst;
|
|
372
|
-
}
|
|
373
|
-
/** 数字色:0–70 白(前景),其余随档位。 */
|
|
374
|
-
function numColor(w) {
|
|
375
|
-
return usageTier(w).rank === 1 ? "inherit" : usageTier(w).color;
|
|
376
|
-
}
|
|
377
378
|
/** 最差窗:档位最高者优先;同档时根因(非连坐)优先,再取已用更多者。 */
|
|
378
379
|
function worstWindow(windows) {
|
|
379
380
|
let worst = null;
|
|
@@ -651,14 +652,6 @@ window.__ModuleLoader__.load({
|
|
|
651
652
|
const worst = worstWindow(windows);
|
|
652
653
|
return `${statusText(entry, t)}${worst ? " · " + pillText(worst, windows.length > 1, t) : " · " + t("noUsageData")}`;
|
|
653
654
|
}
|
|
654
|
-
function providerIcon(entry) {
|
|
655
|
-
if (!entry || entry.state === "loading") return "…";
|
|
656
|
-
if ((entry.windows || []).some((w) => w.status === "rate-limited")) return "✕";
|
|
657
|
-
if (entry.state !== "ok") return "!";
|
|
658
|
-
if (entry.coverage === "partial" || entry.freshness === "stale") return "?";
|
|
659
|
-
const worst = worstWindow(entry.windows || []);
|
|
660
|
-
return worst ? usageTier(worst).icon : "?";
|
|
661
|
-
}
|
|
662
655
|
function cacheAge(entry, t) {
|
|
663
656
|
if (!entry || entry.freshness !== "stale" || !canShowUsage(entry)) return null;
|
|
664
657
|
const at = Date.parse(entry.lastSuccessAt);
|
|
@@ -746,7 +739,10 @@ window.__ModuleLoader__.load({
|
|
|
746
739
|
return value;
|
|
747
740
|
};
|
|
748
741
|
const setCommandCodeAccount = (id) => {
|
|
749
|
-
|
|
742
|
+
// "default" 与空串在 Host 端是同一个账户(同一缓存键、同一指纹,见 providerCacheKey):
|
|
743
|
+
// 归一后就不必为「固定到默认账户」多打一次强制刷新。
|
|
744
|
+
const raw = typeof id === "string" ? id.trim() : "";
|
|
745
|
+
const next = raw === "default" ? "" : raw;
|
|
750
746
|
if (disposed || ccAccount === next) return Promise.resolve(snapshot);
|
|
751
747
|
ccAccount = next;
|
|
752
748
|
// 账户变了:旧账户的在途/缓存数据立即失效,强制按新账户重拉。
|
|
@@ -806,11 +802,15 @@ window.__ModuleLoader__.load({
|
|
|
806
802
|
const list = [{ id: "default", label: "default" }];
|
|
807
803
|
const accounts = Array.isArray(value?.accounts) ? value.accounts : [];
|
|
808
804
|
const tasks = Array.isArray(value?.accountEnrollmentTasks) ? value.accountEnrollmentTasks : [];
|
|
805
|
+
const seen = new Set(["default"]);
|
|
809
806
|
accounts.forEach((account, index) => {
|
|
810
807
|
const refName = typeof account?.apiKeyEnv === "string" && account.apiKeyEnv.trim() !== "" ? account.apiKeyEnv.trim() : void 0;
|
|
811
808
|
// 与 slots() 一致:无凭据引用的槽位不显示;登记任务未完成的账户暂不可选。
|
|
812
809
|
if (refName === void 0) return;
|
|
810
|
+
// 配置被手改出重复引用名时只列一次:否则同一账户出现两个选项,切换后读的还是同一份额度。
|
|
811
|
+
if (seen.has(refName)) return;
|
|
813
812
|
if (tasks.some((task) => task?.ref === refName && task.phase !== "naming")) return;
|
|
813
|
+
seen.add(refName);
|
|
814
814
|
list.push({ id: refName, label: typeof account.label === "string" && account.label.trim() !== "" ? account.label.trim() : `Account ${index + 2}` });
|
|
815
815
|
});
|
|
816
816
|
return list;
|
|
@@ -830,6 +830,10 @@ window.__ModuleLoader__.load({
|
|
|
830
830
|
let disposed = false;
|
|
831
831
|
const listeners = new Set();
|
|
832
832
|
let state = { status: "idle", accounts: [], activeId: "", revision: null, writable: false, baseActive: "", switching: false, error: null };
|
|
833
|
+
// 设置页与药丸弹层共用同一个控制器,可能同时驱动它:`loadSequence` 标记「哪一次读取才是当前的」,
|
|
834
|
+
// `inflightLoad` 把同一代的并发 load 合并成一次 describe。没有这两样时,切换账户(mutate)在途
|
|
835
|
+
// 期间返回的旧 describe 会把 activeId/revision 覆盖回切换前的快照,两处界面一起显示旧账户。
|
|
836
|
+
let loadSequence = 0, inflightLoad = null;
|
|
833
837
|
const emit = () => { for (const fn of [...listeners]) fn(state); };
|
|
834
838
|
const parseRow = (row) => {
|
|
835
839
|
const accounts = commandCodeAccountList(row?.value);
|
|
@@ -837,18 +841,29 @@ window.__ModuleLoader__.load({
|
|
|
837
841
|
};
|
|
838
842
|
const load = async (keepError = false) => {
|
|
839
843
|
if (disposed) return state;
|
|
844
|
+
// 同一代的并发 load 合并:两处界面同时挂载时只发一次 describe。
|
|
845
|
+
if (inflightLoad?.sequence === loadSequence) return inflightLoad.promise;
|
|
846
|
+
const sequence = ++loadSequence;
|
|
840
847
|
state = { ...state, status: "loading", error: keepError ? state.error : null }; emit();
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
848
|
+
const task = (async () => {
|
|
849
|
+
try {
|
|
850
|
+
const response = await describe();
|
|
851
|
+
if (!response?.ok) throw new Error(response?.error?.message || "settings describe failed");
|
|
852
|
+
// 切换账户(mutate)或更晚的 load 已经给出更新的状态:这次的结果一律作废,
|
|
853
|
+
// 既不用旧快照覆盖,也不把状态留在 loading(mutate 那边已写回 ready)。
|
|
854
|
+
if (disposed || sequence !== loadSequence) return state;
|
|
855
|
+
const row = response.value?.namespaces?.find((candidate) => candidate?.ns === COMMANDCODE_SETTINGS_NS);
|
|
856
|
+
if (!row) throw new Error("Command Code settings namespace not found");
|
|
857
|
+
state = { ...state, status: "ready", writable: response.value.writable === true, ...parseRow(row) };
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (disposed || sequence !== loadSequence) return state;
|
|
860
|
+
state = { ...state, status: "unavailable", error: String(error?.message || error) };
|
|
861
|
+
}
|
|
862
|
+
emit();
|
|
863
|
+
return state;
|
|
864
|
+
})();
|
|
865
|
+
inflightLoad = { sequence, promise: task };
|
|
866
|
+
try { return await task; } finally { if (inflightLoad?.promise === task) inflightLoad = null; }
|
|
852
867
|
};
|
|
853
868
|
const switchTo = async (id) => {
|
|
854
869
|
if (disposed || state.status !== "ready" || state.switching) return state;
|
|
@@ -861,7 +876,10 @@ window.__ModuleLoader__.load({
|
|
|
861
876
|
try {
|
|
862
877
|
const response = await mutate(ops, state.revision);
|
|
863
878
|
if (!response?.ok) throw new Error(response?.error?.message || "settings mutate failed");
|
|
864
|
-
|
|
879
|
+
// 写入成功就是权威状态:作废在途的旧 describe,并把 status 明确拉回 ready
|
|
880
|
+
// (load 开始时置的 loading 不该留在这里)。
|
|
881
|
+
loadSequence++;
|
|
882
|
+
state = { ...state, status: "ready", switching: false, ...parseRow(response.value) };
|
|
865
883
|
} catch (error) {
|
|
866
884
|
const message = String(error?.message || error);
|
|
867
885
|
state = { ...state, switching: false, error: message };
|
|
@@ -878,13 +896,20 @@ window.__ModuleLoader__.load({
|
|
|
878
896
|
};
|
|
879
897
|
}
|
|
880
898
|
|
|
881
|
-
|
|
899
|
+
// 未注入 cc(提供方插件未运行)时给 useSyncExternalStore 一对稳定引用:每次都新建函数
|
|
900
|
+
// 会让 React 反复重新订阅。
|
|
901
|
+
const NO_CC_STATE = () => null;
|
|
902
|
+
const NO_CC_SUBSCRIBE = () => () => {};
|
|
903
|
+
/** 弹层与设置页共用的 Command Code 账户区:自动轮换 + 默认账户 + 各额外账户。 */
|
|
882
904
|
function CommandCodeAccountSwitch({ cc, ccState, t }) {
|
|
883
905
|
if (!cc || !ccState) return null;
|
|
906
|
+
// 状态没 ready(正在读账户配置 / 不可用)时按钮不可点:switchTo 在这种状态下会直接返回,
|
|
907
|
+
// 可点的按钮换来的只是「点了没反应」。
|
|
908
|
+
const locked = ccState.switching || ccState.status !== "ready" || !ccState.writable;
|
|
884
909
|
const optionButton = (id, labelText) => h("button", {
|
|
885
910
|
type: "button",
|
|
886
911
|
onClick: () => void cc.switchTo(id),
|
|
887
|
-
disabled:
|
|
912
|
+
disabled: locked,
|
|
888
913
|
"aria-pressed": ccState.activeId === id,
|
|
889
914
|
style: {
|
|
890
915
|
display: "flex", alignItems: "center", gap: 6, width: "100%", textAlign: "left",
|
|
@@ -892,7 +917,7 @@ window.__ModuleLoader__.load({
|
|
|
892
917
|
border: `1px solid ${ccState.activeId === id ? "rgba(59,130,246,.55)" : "rgba(128,128,128,.3)"}`,
|
|
893
918
|
background: ccState.activeId === id ? "rgba(59,130,246,.10)" : "transparent",
|
|
894
919
|
color: "inherit", font: "inherit",
|
|
895
|
-
cursor:
|
|
920
|
+
cursor: locked ? "default" : "pointer"
|
|
896
921
|
}
|
|
897
922
|
},
|
|
898
923
|
h("span", { "aria-hidden": "true" }, ccState.activeId === id ? "●" : "○"),
|
|
@@ -901,10 +926,13 @@ window.__ModuleLoader__.load({
|
|
|
901
926
|
);
|
|
902
927
|
// 账户多时这一坨会把上方的额度顶出视野:折进 details,summary 只留「当前账户 + 数量」,
|
|
903
928
|
// 展开后列表自身限高滚动,弹层高度不随账户数增长。
|
|
904
|
-
const
|
|
905
|
-
: ccState.activeId === "" ? t("ccAccountAuto")
|
|
929
|
+
const activeName = ccState.status !== "ready" ? null
|
|
906
930
|
: ccState.activeId === "default" ? t("ccAccountDefault")
|
|
907
931
|
: (ccState.accounts.find(account => account.id === ccState.activeId)?.label ?? ccState.activeId);
|
|
932
|
+
// summary 直接回答「现在是哪一个账户」:固定账户写账户名 + 使用中,自动轮换写模式名。
|
|
933
|
+
const activeLabel = ccState.status !== "ready" ? null
|
|
934
|
+
: ccState.activeId === "" ? t("ccAccountAuto")
|
|
935
|
+
: `${activeName} · ${t("ccAccountActive")}`;
|
|
908
936
|
const optionCount = ccState.status === "ready" ? ccState.accounts.length + 1 : 0;
|
|
909
937
|
return h("details", { style: { marginTop: 10, paddingTop: 8, borderTop: "1px solid rgba(128,128,128,.25)" } },
|
|
910
938
|
h("summary", { style: { cursor: "pointer", fontSize: 11, opacity: 0.8 } },
|
|
@@ -920,13 +948,126 @@ window.__ModuleLoader__.load({
|
|
|
920
948
|
);
|
|
921
949
|
}
|
|
922
950
|
|
|
951
|
+
// ── 视觉原语:扁平 + 状态色(药丸 / 弹层 / 设置页共用同一套)────────────────
|
|
952
|
+
// 注意:下面这些都是**普通函数调用**而不是 React 组件元素——它们不用 Hook,直接内联展开,
|
|
953
|
+
// 测试里的 React 桩也能看到它们渲染出的文字。
|
|
954
|
+
const TRACK = "rgba(128,128,128,.22)";
|
|
955
|
+
const NEUTRAL = "#94a3b8";
|
|
956
|
+
const INFO = "#3b82f6";
|
|
957
|
+
/** #rrggbb → rgba(r,g,b,a):底色与描边都由状态色派生,保证同一状态到处同一个颜色。 */
|
|
958
|
+
function alpha(color, a) {
|
|
959
|
+
const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color || "");
|
|
960
|
+
return m ? `rgba(${parseInt(m[1], 16)},${parseInt(m[2], 16)},${parseInt(m[3], 16)},${a})` : `rgba(128,128,128,${a})`;
|
|
961
|
+
}
|
|
962
|
+
/** 状态色实心圆上的字形色:黄色配深字,其余白字。 */
|
|
963
|
+
function onColor(color) { return color === "#eab308" ? "#1f1300" : "#fff"; }
|
|
964
|
+
/** 状态色用作文字时的版本:纯黄在浅色主题上看不清,加深一档。 */
|
|
965
|
+
function textTone(color) { return color === "#eab308" ? "#ca8a04" : color; }
|
|
966
|
+
function windowLeft(w) { return w.status === "rate-limited" ? 0 : remainingPercent(w.percent); }
|
|
967
|
+
/** 状态色实心圆 + 字形(✓ ⚠ ✕ ? ! ⚙ ℹ …):颜色之外还有形状,不靠颜色单独传达。 */
|
|
968
|
+
function statusGlyph(color, icon, size) {
|
|
969
|
+
return h("span", { "aria-hidden": "true", style: { flex: "none", width: size, height: size, borderRadius: "50%", background: color, color: onColor(color), display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: Math.round(size * 0.62), fontWeight: 700, lineHeight: 1 } }, icon);
|
|
970
|
+
}
|
|
971
|
+
/** 小标签:状态色圆点 + 继承前景色的文字(文字不着色,深浅主题都清晰)。 */
|
|
972
|
+
function statusChip(text, color) {
|
|
973
|
+
return h("span", { style: { display: "inline-flex", alignItems: "center", gap: 5, fontSize: 11, lineHeight: 1.5, padding: "1px 8px", borderRadius: 999, background: alpha(color, ".14"), whiteSpace: "nowrap" } },
|
|
974
|
+
h("span", { "aria-hidden": "true", style: { width: 6, height: 6, borderRadius: "50%", background: color } }), text);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* 药丸里的迷你余量条:一个窗口一根横条,满 = 100% 剩余,颜色是该窗口自己的档位色。
|
|
978
|
+
* 条的顺序与弹层里窗口行的顺序一致,所以不用文字也能一眼对上;窗口越多条越细(最多 5 根)。
|
|
979
|
+
*/
|
|
980
|
+
function miniBars(windows, muted) {
|
|
981
|
+
const list = windows.slice(0, 5), n = list.length;
|
|
982
|
+
const barH = n <= 1 ? 6 : n === 2 ? 5 : n === 3 ? 4 : n === 4 ? 3 : 2;
|
|
983
|
+
return h("span", { "aria-hidden": "true", style: { display: "inline-flex", flexDirection: "column", justifyContent: "center", gap: n <= 2 ? 3 : 2, width: 34, flex: "none", opacity: muted ? 0.5 : 1 } },
|
|
984
|
+
list.map((w, i) => {
|
|
985
|
+
const left = windowLeft(w);
|
|
986
|
+
return h("span", { key: `${w.kind}:${i}`, style: { display: "block", height: barH, borderRadius: 99, background: TRACK, overflow: "hidden" } },
|
|
987
|
+
h("span", { style: { display: "block", width: `${left}%`, minWidth: left > 0 ? 3 : 0, height: "100%", borderRadius: 99, background: usageTier(w).color } }));
|
|
988
|
+
}));
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* 订阅整体状态:一个对象回答「现在订阅怎么样」,药丸、弹层横幅、设置页横幅共用,
|
|
992
|
+
* 所以三处永远同一个结论、同一个颜色。优先级:额度用尽 > Cookie 到期 > 需配置 > 读取失败 > 缓存/不完整 > 额度档位。
|
|
993
|
+
* 返回 { key, color, icon, title, sub, short, tinted, ... }:
|
|
994
|
+
* title/sub 给弹层横幅;short 是药丸上的**一个词**(正常时为空——绿色余量条本身就是答案);
|
|
995
|
+
* tinted 为真时药丸整体染色,提醒「这里需要你看一眼」。
|
|
996
|
+
*/
|
|
997
|
+
function computeStatus(rawEntry, { failed = null, failState, providerId, t, getLocale }) {
|
|
998
|
+
const entry = canShowUsage(rawEntry) ? rawEntry : null;
|
|
999
|
+
const windows = entry ? entry.windows || [] : [];
|
|
1000
|
+
const worst = windows.length ? worstWindow(windows) : null;
|
|
1001
|
+
const worstTier = worst ? usageTier(worst) : null;
|
|
1002
|
+
const multi = windows.length > 1;
|
|
1003
|
+
const degraded = !!(rawEntry && (rawEntry.state !== "ok" || rawEntry.freshness === "stale" || rawEntry.coverage === "partial"));
|
|
1004
|
+
const state = rawEntry ? rawEntry.state : failState;
|
|
1005
|
+
const expiry = cookieExpiry(rawEntry, t);
|
|
1006
|
+
const expiring = !!(expiry && expiry.rank >= 2);
|
|
1007
|
+
// 倒计时只用于展示,永远不能盖过官方接口的判定:凭据已被拒绝(state=error)时主文案仍是失败原因;
|
|
1008
|
+
// 例外是已过期——它本身就是「请重新登录」的行动项。
|
|
1009
|
+
const expiryActionable = expiring && (!(rawEntry && rawEntry.state === "error") || expiry.expired);
|
|
1010
|
+
const windowName = (w) => t(KIND_LABEL[w.kind] ?? "wall");
|
|
1011
|
+
const windowShort = (w) => multi ? `${t(KIND_SHORT[w.kind] ?? "srolling")} ` : "";
|
|
1012
|
+
const windowSub = (w) => {
|
|
1013
|
+
const head = t(multi ? "statusWorst" : "statusOnly").replace("{w}", windowName(w)).replace("{p}", String(windowLeft(w)));
|
|
1014
|
+
const ms = w.resetsAt ? Date.parse(w.resetsAt) - Date.now() : NaN;
|
|
1015
|
+
return ms > 0 ? `${head} · ${t("statusResetIn").replace("{rest}", durationText(ms, t))}` : head;
|
|
1016
|
+
};
|
|
1017
|
+
const needConfig = state === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
|
|
1018
|
+
: state === "no-cookie" ? t("pillNeedCookie") : null;
|
|
1019
|
+
const base = { entry, windows, worst, worstTier, multi, degraded, expiry, expiring, expiryActionable, needConfig };
|
|
1020
|
+
if ((!rawEntry && !failed && !failState) || state === "loading") return { ...base, key: "loading", color: NEUTRAL, icon: "…", title: t("loading"), sub: "", short: "", tinted: false };
|
|
1021
|
+
if (state === "disabled") return { ...base, key: "disabled", color: NEUTRAL, icon: "–", title: t("statusDisabled"), sub: "", short: t("statusDisabled"), tinted: false };
|
|
1022
|
+
if (worstTier && worstTier.rank >= 4) {
|
|
1023
|
+
const title = t(worst.cascade ? "statusBlocked" : "statusExhausted");
|
|
1024
|
+
return { ...base, key: "blocked", color: worstTier.color, icon: "✕", title, sub: windowSub(worst), short: windowShort(worst) + t(worst.cascade ? "pillBlocked" : "pillLimited"), tinted: true };
|
|
1025
|
+
}
|
|
1026
|
+
if (expiryActionable) {
|
|
1027
|
+
return { ...base, key: "cookie", color: expiry.color, icon: expiry.icon, title: expiry.label, sub: t("mimoCookieExpiresAt").replace("{time}", new Date(expiry.expiresAt).toLocaleString(getLocale())), short: expiry.label, tinted: true };
|
|
1028
|
+
}
|
|
1029
|
+
if (needConfig) {
|
|
1030
|
+
return { ...base, key: "config", color: INFO, icon: "⚙", title: t(state === "no-key" ? "statusNoKey" : "statusNoCookie"), sub: needConfig, short: t(state === "no-key" ? "needConfigShort" : "statusNoCookie"), tinted: true };
|
|
1031
|
+
}
|
|
1032
|
+
if (!entry) {
|
|
1033
|
+
return { ...base, key: "error", color: "#ef4444", icon: "!", title: t("statusError"), sub: failed || (rawEntry && rawEntry.error) || "", short: t("statusError"), tinted: true };
|
|
1034
|
+
}
|
|
1035
|
+
if (degraded) {
|
|
1036
|
+
// 过期缓存即使 state=error 也要叫「缓存数据」:读取失败是原因,用户看到的是上次的数字。
|
|
1037
|
+
const label = rawEntry.freshness === "stale" ? t("statusCache") : rawEntry.coverage === "partial" ? t("statusPartial") : statusText(rawEntry, t);
|
|
1038
|
+
const worse = worstTier && worstTier.rank >= 3;
|
|
1039
|
+
return { ...base, key: "degraded", color: worse ? worstTier.color : "#eab308", icon: "?", title: label, sub: [cacheAge(rawEntry, t), worst ? windowSub(worst) : null].filter(Boolean).join(" · "), short: label, tinted: true };
|
|
1040
|
+
}
|
|
1041
|
+
if (worstTier) {
|
|
1042
|
+
const sub = windowSub(worst);
|
|
1043
|
+
if (worstTier.rank >= 3) return { ...base, key: "tight", color: worstTier.color, icon: "⚠", title: t("statusTight"), sub, short: windowShort(worst) + t(worstTier.badge), tinted: true };
|
|
1044
|
+
if (worstTier.rank === 2) return { ...base, key: "low", color: worstTier.color, icon: "⚠", title: t("statusLow"), sub, short: windowShort(worst) + t(worstTier.badge), tinted: true };
|
|
1045
|
+
return { ...base, key: "ok", color: worstTier.color, icon: "✓", title: t("statusPlenty"), sub, short: "", tinted: false };
|
|
1046
|
+
}
|
|
1047
|
+
// 成功但没有额度窗口:只有余额(或什么都没有)。余额就是唯一信息,不画成「充足」。
|
|
1048
|
+
const balance = balanceText(entry, t);
|
|
1049
|
+
return { ...base, key: "balance", color: NEUTRAL, icon: "ℹ", title: balance ?? t("noUsageData"), sub: "", short: balance ?? t("noUsageData"), tinted: false };
|
|
1050
|
+
}
|
|
1051
|
+
/** 弹层 / 设置页顶部的状态横幅:左侧实心状态圆,右侧一句结论 + 一行依据。 */
|
|
1052
|
+
function statusBanner(status) {
|
|
1053
|
+
return h("div", { role: "status", style: { display: "flex", alignItems: "center", gap: 12, marginTop: 10, padding: "12px 14px", borderRadius: 12, background: alpha(status.color, ".10"), border: `1px solid ${alpha(status.color, ".28")}` } },
|
|
1054
|
+
statusGlyph(status.color, status.icon, 30),
|
|
1055
|
+
h("div", { style: { minWidth: 0, flex: 1 } },
|
|
1056
|
+
h("div", { style: { fontSize: 15, fontWeight: 700, lineHeight: 1.3, overflowWrap: "anywhere" } }, status.title),
|
|
1057
|
+
status.sub ? h("div", { style: { fontSize: 11.5, opacity: 0.75, marginTop: 2, overflowWrap: "anywhere", whiteSpace: "pre-wrap" } }, status.sub) : null));
|
|
1058
|
+
}
|
|
1059
|
+
/** 套餐名(extras 里 kind=plan):放在标题行右侧做成小标签,不再混在余额明细里。 */
|
|
1060
|
+
function planName(entry) {
|
|
1061
|
+
const row = entry && Array.isArray(entry.extras) ? entry.extras.find((x) => x.kind === "plan") : undefined;
|
|
1062
|
+
return row ? row.value : null;
|
|
1063
|
+
}
|
|
923
1064
|
function RemainingBar({ value, color, label }) {
|
|
924
1065
|
const pct = Math.max(0, Math.min(100, Number(value) || 0));
|
|
925
1066
|
return h("div", {
|
|
926
1067
|
role: "progressbar",
|
|
927
1068
|
"aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct, "aria-label": label,
|
|
928
|
-
style: { width: "100%", height:
|
|
929
|
-
}, h("div", { style: { width: `${pct}%`, height: "100%", borderRadius:
|
|
1069
|
+
style: { width: "100%", height: 8, marginTop: 8, borderRadius: 99, background: TRACK, overflow: "hidden" }
|
|
1070
|
+
}, h("div", { style: { width: `${pct}%`, minWidth: pct > 0 ? 4 : 0, height: "100%", borderRadius: 99, background: color } }));
|
|
930
1071
|
}
|
|
931
1072
|
/**
|
|
932
1073
|
* 单窗口行(药丸弹层与设置页共用):徽标 + 余量% 主数字 + 进度条(满格=100% 剩余,随已用扣减)+ 明细行。
|
|
@@ -950,28 +1091,22 @@ window.__ModuleLoader__.load({
|
|
|
950
1091
|
resetLine(w, t, getLocale),
|
|
951
1092
|
w.cascade ? t("cascadeLimited") + (w.detail && w.detail.blockedBy ? ` [${w.detail.blockedBy.join(", ")}]` : "") : null
|
|
952
1093
|
].filter(Boolean).join(" · ");
|
|
1094
|
+
// 档位词只在「需要留意」时出现:充足时绿色长条与大数字已经说明一切,再贴一个「充足」只是噪音。
|
|
1095
|
+
const alert = tier.rank >= 2;
|
|
1096
|
+
const label = blocked ? t(tier.num) : t("pillRemaining").replace("{p}", String(left));
|
|
953
1097
|
return h("div", {
|
|
954
|
-
style: {
|
|
955
|
-
marginTop: 8, padding: "8px 10px", borderRadius: 8,
|
|
956
|
-
background: blocked ? "rgba(239,68,68,.08)" : "transparent",
|
|
957
|
-
border: blocked ? "1px solid rgba(239,68,68,.35)" : "1px solid transparent"
|
|
958
|
-
}
|
|
1098
|
+
style: { marginTop: 8, padding: "10px 12px", borderRadius: 10, background: blocked ? alpha(tier.color, ".09") : "rgba(128,128,128,.07)" }
|
|
959
1099
|
},
|
|
960
1100
|
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
961
|
-
h("span", { style: { fontSize: 12
|
|
962
|
-
|
|
963
|
-
style: {
|
|
964
|
-
fontSize: 11, padding: "1px 8px", borderRadius: 999, whiteSpace: "nowrap",
|
|
965
|
-
border: `1px solid ${tier.color}`, color: tier.color
|
|
966
|
-
}
|
|
967
|
-
}, `${tier.icon} ${t(tier.badge)}`),
|
|
1101
|
+
h("span", { style: { fontSize: 12.5, fontWeight: 600 } }, t(KIND_LABEL[w.kind] ?? "wall")),
|
|
1102
|
+
alert ? statusChip(t(tier.badge), tier.color) : null,
|
|
968
1103
|
h("span", { style: { flex: 1 } }),
|
|
969
|
-
h("strong", { style: { fontSize:
|
|
970
|
-
|
|
1104
|
+
h("strong", { style: { fontSize: 18, lineHeight: 1.1, color: alert ? textTone(tier.color) : "inherit", whiteSpace: "nowrap" } }, `${left}%`),
|
|
1105
|
+
h("span", { style: { fontSize: 11, opacity: 0.65, whiteSpace: "nowrap" } }, t("remainWord"))
|
|
971
1106
|
),
|
|
972
|
-
RemainingBar({ value: left, color: tier.color, label
|
|
1107
|
+
RemainingBar({ value: left, color: tier.color, label }),
|
|
973
1108
|
usageLine || resetParts
|
|
974
|
-
? h("div", { style: { fontSize: 11, opacity: 0.7, marginTop:
|
|
1109
|
+
? h("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 6 } },
|
|
975
1110
|
usageLine ? h("div", { style: { overflowWrap: "anywhere" } }, usageLine) : null,
|
|
976
1111
|
resetParts ? h("div", { style: { marginTop: usageLine ? 2 : 0, overflowWrap: "anywhere" } }, resetParts) : null)
|
|
977
1112
|
: null
|
|
@@ -1000,7 +1135,8 @@ window.__ModuleLoader__.load({
|
|
|
1000
1135
|
const root = react.useRef(null);
|
|
1001
1136
|
const retry = react.useRef(() => {});
|
|
1002
1137
|
// Command Code 账户区:cc 由装配层注入(提供方插件未运行时为 null,区块整体隐藏)。
|
|
1003
|
-
|
|
1138
|
+
// 兜底用模块级稳定引用(NO_CC_*):内联箭头函数每次渲染都是新引用,会让 React 反复重新订阅。
|
|
1139
|
+
const ccState = react.useSyncExternalStore(cc ? cc.subscribe : NO_CC_SUBSCRIBE, cc ? cc.getSnapshot : NO_CC_STATE, cc ? cc.getSnapshot : NO_CC_STATE);
|
|
1004
1140
|
react.useEffect(() => { if (open && cc) void cc.load(); }, [open, cc]);
|
|
1005
1141
|
// 挂载即读一次账户配置:提供方插件固定了账户时,药丸本体不必等弹层打开就跟随。
|
|
1006
1142
|
react.useEffect(() => { if (providerId === "commandcode" && cc) void cc.load(); }, [providerId, cc]);
|
|
@@ -1070,72 +1206,55 @@ window.__ModuleLoader__.load({
|
|
|
1070
1206
|
}, [open]);
|
|
1071
1207
|
const rawEntry = snapshot && snapshot.entry;
|
|
1072
1208
|
if (rawEntry?.visible === false || rawEntry?.state === "disabled") return null;
|
|
1073
|
-
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
const windows = entry ? entry.windows || [] : [];
|
|
1077
|
-
const worst = windows.length > 0 ? worstWindow(windows) : null;
|
|
1078
|
-
const worstTier = worst ? usageTier(worst) : null;
|
|
1079
|
-
const degraded = !!(rawEntry && (rawEntry.state !== "ok" || rawEntry.freshness === "stale" || rawEntry.coverage === "partial"));
|
|
1209
|
+
// 整体状态由 computeStatus 一处判定:药丸、弹层横幅、设置页横幅读的是同一个结论。
|
|
1210
|
+
const st = computeStatus(rawEntry, { failed, failState, providerId, t, getLocale });
|
|
1211
|
+
const { entry, windows, worst, degraded, expiry, expiring } = st;
|
|
1080
1212
|
const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const isConfig = stateText === "no-key" || stateText === "no-cookie";
|
|
1088
|
-
// 余额请求成功不代表有额度窗口(例如小米接口暂时只返回余额)。
|
|
1089
|
-
// Cookie 临近到期是需要重新登录的行动项:除额度已用尽外,优先于普通余量与降级提示。
|
|
1090
|
-
const expiry = cookieExpiry(rawEntry, t);
|
|
1091
|
-
const quotaBlocked = !!(worstTier && worstTier.rank >= 4);
|
|
1092
|
-
const expiring = !!(expiry && expiry.rank >= 2);
|
|
1093
|
-
// 倒计时只用于展示,永远不能盖过官方接口的判定:凭据已被拒绝(state=error)时
|
|
1094
|
-
// 主文案必须仍是失败原因,否则「还有 2 小时到期」会把重新登录推迟掉。
|
|
1095
|
-
// 例外是已过期:它本身就是「请重新登录」的行动项,比通用错误文案更具体。
|
|
1096
|
-
const expiryActionable = expiring && (!(rawEntry && rawEntry.state === "error") || (expiry && expiry.expired));
|
|
1097
|
-
// 底色与文字沿用额度告警语汇:已过期与额度用尽同为红,30 分钟内为橙,2 小时内为黄。
|
|
1098
|
-
const alertStyle = quotaBlocked || (expiry && expiry.expired) ? { background: "rgba(239,68,68,.10)", borderColor: "rgba(239,68,68,.45)" } : expiryActionable && expiry.rank >= 3 ? { background: "rgba(249,115,22,.10)", borderColor: "rgba(249,115,22,.45)" } : expiryActionable ? { background: "rgba(234,179,8,.10)", borderColor: "rgba(234,179,8,.45)" } : {};
|
|
1099
|
-
const triggerColor = quotaBlocked ? worstTier.color : expiryActionable ? expiry.color : worstTier && worstTier.rank > 1 ? worstTier.color : null;
|
|
1100
|
-
const triggerIcon = quotaBlocked ? worstTier.icon : expiryActionable ? expiry.icon : degraded ? "⚠" : worstTier ? worstTier.icon : ok ? "ℹ" : isConfig ? "⚙" : light;
|
|
1101
|
-
const triggerText = quotaBlocked ? (worst ? pillText(worst, windows.length > 1, t) : entry ? t("noUsageData") : stateLabel) : expiryActionable ? expiry.label : worst ? `${degraded ? statusText(rawEntry, t) + " · " : ""}${pillText(worst, windows.length > 1, t)}` : entry ? `${degraded ? stateLabel + " · " : ""}${(balanceText(entry, t) ?? t("noUsageData"))}` : isConfig ? t("needConfigShort") : stateLabel;
|
|
1213
|
+
// 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
|
|
1214
|
+
// 厂商名就是旁边的模型选择器,具体百分比点开再看;文字与数字都收进 title / aria-label。
|
|
1215
|
+
const tip = `${label}: ${st.title}${st.key === "ok" || st.key === "low" || st.key === "tight" || st.key === "blocked" || st.key === "degraded" ? " · " + summary : ""}${expiring && st.key !== "cookie" && st.key !== "blocked" ? " · " + expiry.label : ""}`;
|
|
1216
|
+
// 有窗口就画迷你余量条:Cookie 到期等行动项通过 short 文案表达,不牺牲余量信息;
|
|
1217
|
+
// 只有没有任何额度窗口时(需配置 / 失败 / 仅余额)才退回状态字形。
|
|
1218
|
+
const glyph = windows.length ? miniBars(windows, degraded) : statusGlyph(st.color, st.icon, 16);
|
|
1102
1219
|
return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
|
|
1103
1220
|
h("button", {
|
|
1104
1221
|
type: "button",
|
|
1105
1222
|
"aria-expanded": open,
|
|
1106
1223
|
"aria-haspopup": "dialog",
|
|
1107
|
-
"aria-label":
|
|
1108
|
-
title:
|
|
1224
|
+
"aria-label": tip,
|
|
1225
|
+
title: tip,
|
|
1109
1226
|
onClick: () => setOpen(!open),
|
|
1110
1227
|
style: {
|
|
1111
|
-
display: "inline-flex", alignItems: "center", gap:
|
|
1112
|
-
borderRadius: 999, border:
|
|
1113
|
-
|
|
1114
|
-
|
|
1228
|
+
display: "inline-flex", alignItems: "center", gap: 8, padding: st.short ? "4px 11px 4px 9px" : "5px 10px", minHeight: 26, boxSizing: "border-box",
|
|
1229
|
+
borderRadius: 999, border: `1px solid ${st.tinted ? alpha(st.color, ".45") : "rgba(128,128,128,.3)"}`,
|
|
1230
|
+
background: st.tinted ? alpha(st.color, ".10") : "transparent",
|
|
1231
|
+
color: "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
|
|
1115
1232
|
}
|
|
1116
1233
|
},
|
|
1117
|
-
|
|
1118
|
-
h("span",
|
|
1119
|
-
failed && ok ? h("span", { style: { opacity: 0.7, fontSize: 11 } }, `(${t("unavailable")})`) : null
|
|
1234
|
+
glyph,
|
|
1235
|
+
st.short ? h("span", null, st.short) : null
|
|
1120
1236
|
),
|
|
1121
1237
|
open && h("div", {
|
|
1122
1238
|
role: "dialog",
|
|
1123
1239
|
"aria-label": `${label} ${t("nav")}`,
|
|
1124
1240
|
style: {
|
|
1125
1241
|
position: "absolute", bottom: "calc(100% + 6px)", right: 0, zIndex: 50,
|
|
1126
|
-
width: "min(340px, calc(100vw - 24px))", boxSizing: "border-box", overflowWrap: "anywhere", padding:
|
|
1242
|
+
width: "min(340px, calc(100vw - 24px))", boxSizing: "border-box", overflowWrap: "anywhere", padding: 14, borderRadius: 14,
|
|
1127
1243
|
background: "var(--dsh-surface, Canvas)", color: "var(--dsh-text, CanvasText)",
|
|
1128
|
-
border: "1px solid rgba(128,128,128,.
|
|
1129
|
-
boxShadow: "0
|
|
1244
|
+
border: "1px solid rgba(128,128,128,.28)",
|
|
1245
|
+
boxShadow: "0 12px 32px rgba(0,0,0,.28)", font: "12px/1.5 sans-serif"
|
|
1130
1246
|
}
|
|
1131
1247
|
},
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1248
|
+
// 标题行:厂商名 + 套餐名小标签。
|
|
1249
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
1250
|
+
h("strong", { style: { fontSize: 13 } }, label),
|
|
1251
|
+
h("span", { style: { flex: 1 } }),
|
|
1252
|
+
planName(entry) ? h("span", { style: { fontSize: 11, fontWeight: 600, padding: "1px 8px", borderRadius: 999, background: "rgba(128,128,128,.16)", whiteSpace: "nowrap" } }, planName(entry)) : null),
|
|
1253
|
+
// 状态横幅:先给结论(充足 / 偏少 / 即将用尽 / 已用尽 / 需配置 / 失败…)与一行依据。
|
|
1254
|
+
statusBanner(st),
|
|
1255
|
+
failed && st.key !== "error" && !st.needConfig ? h("p", { style: { color: "#f87171", margin: "8px 0 0", whiteSpace: "pre-wrap" } }, failed) : null,
|
|
1256
|
+
// Cookie 到期常驻弹层:即使用量正常,也让用户看见 Cookie 还剩多久(已在横幅里说明时不重复)。
|
|
1257
|
+
expiry && st.key !== "cookie" ? h("p", { role: "status", style: { margin: "8px 0 0", fontSize: 11, color: expiring ? textTone(expiry.color) : "inherit", opacity: expiring ? 1 : 0.75 } }, `${expiry.label} · ${t("mimoCookieExpiresAt").replace("{time}", new Date(expiry.expiresAt).toLocaleString(getLocale()))}`) : null,
|
|
1139
1258
|
// 用量账户标注:账户区 ready 时说明上方数字属于哪个账户;自动轮换时明确不跟随实际服务账户。
|
|
1140
1259
|
providerId === "commandcode" && ccState && ccState.status === "ready"
|
|
1141
1260
|
? h("p", { style: { margin: "6px 0 0", fontSize: 11, opacity: 0.75 } },
|
|
@@ -1145,18 +1264,24 @@ window.__ModuleLoader__.load({
|
|
|
1145
1264
|
// 额度线切换时插一条分组标题:AFP 的日限额只覆盖视觉 / 语音模型与 Harness,与文本模型的
|
|
1146
1265
|
// 5 小时 / 周 / 月不是同一条线,混排会让人把两条线的配额当成一组来互相比较。
|
|
1147
1266
|
UsageWindowList({ windows, t, getLocale, uncertain: degraded }),
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1267
|
+
// 余额等补充项:套餐名已在标题行,这里只剩「标签 … 数值」两列。
|
|
1268
|
+
entry && entry.extras && entry.extras.some((x) => x.kind !== "plan")
|
|
1269
|
+
? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
|
|
1270
|
+
entry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
|
|
1271
|
+
h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
|
|
1272
|
+
h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
|
|
1151
1273
|
: null,
|
|
1152
1274
|
providerId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
|
|
1153
|
-
|
|
1154
|
-
h("
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1275
|
+
// 页脚:更新时间 + 刷新。
|
|
1276
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 12 } },
|
|
1277
|
+
h("span", { style: { flex: 1, minWidth: 0, fontSize: 11, opacity: 0.6 } }, snapshot ? `${t("lastUpdated")} ${new Date(snapshot.updatedAt).toLocaleString(getLocale())}` : ""),
|
|
1278
|
+
h("button", {
|
|
1279
|
+
type: "button",
|
|
1280
|
+
onClick: () => retry.current(),
|
|
1281
|
+
disabled: refreshing,
|
|
1282
|
+
style: { padding: "4px 12px", borderRadius: 8, border: "none", background: alpha(INFO, ".14"), color: "inherit", font: "inherit", fontWeight: 600, cursor: refreshing ? "default" : "pointer", whiteSpace: "nowrap" }
|
|
1283
|
+
}, t(refreshing ? "refreshing" : "refresh"))
|
|
1284
|
+
)
|
|
1160
1285
|
)
|
|
1161
1286
|
);
|
|
1162
1287
|
}
|
|
@@ -1212,8 +1337,11 @@ window.__ModuleLoader__.load({
|
|
|
1212
1337
|
}
|
|
1213
1338
|
return patch;
|
|
1214
1339
|
}
|
|
1215
|
-
function SubusageSection({ usageStore, t, getLocale }) {
|
|
1340
|
+
function SubusageSection({ usageStore, t, getLocale, cc }) {
|
|
1216
1341
|
const result = react.useSyncExternalStore(usageStore.subscribe, usageStore.getSnapshot, usageStore.getSnapshot);
|
|
1342
|
+
// Command Code 账户区:与弹层共用同一个控制器与组件;cc 由装配层注入,
|
|
1343
|
+
// 提供方插件未运行时为 undefined,区块整体隐藏。
|
|
1344
|
+
const ccState = react.useSyncExternalStore(cc ? cc.subscribe : NO_CC_SUBSCRIBE, cc ? cc.getSnapshot : NO_CC_STATE, cc ? cc.getSnapshot : NO_CC_STATE);
|
|
1217
1345
|
const [tab, setTab] = react.useState(initialTab);
|
|
1218
1346
|
const [draft, setDraft] = react.useState(() => draftFor(result.settings, tab));
|
|
1219
1347
|
const [dirty, setDirty] = react.useState(false);
|
|
@@ -1371,6 +1499,7 @@ window.__ModuleLoader__.load({
|
|
|
1371
1499
|
const usageMeta = PROVIDER_META[usageId];
|
|
1372
1500
|
// 需要行动时(缺凭据/凭据被拒/Cookie 即将到期)在用量面板里内联修复入口。
|
|
1373
1501
|
const usageAction = usageActionKind(usageEntry, usageId, t);
|
|
1502
|
+
const usageStatus = computeStatus(usageEntry, { failed: usageEntry && usageEntry.state !== "ok" ? usageEntry.error || usageEntry.state : null, failState: usageEntry && usageEntry.state, providerId: usageId, t, getLocale });
|
|
1374
1503
|
// 行动按钮把人送到对应厂商的凭据编辑器:展开管理区、打开该行、滚动过去。
|
|
1375
1504
|
const revealEditor = (target) => {
|
|
1376
1505
|
openEditor(target);
|
|
@@ -1384,6 +1513,21 @@ window.__ModuleLoader__.load({
|
|
|
1384
1513
|
void usageStore.refresh([usageId], false).catch(() => {});
|
|
1385
1514
|
return deactivate;
|
|
1386
1515
|
}, [usageStore, usageId]);
|
|
1516
|
+
// 设置页里的账户区与弹层共用同一个控制器:挂载即读一次账户配置,切到 Command Code
|
|
1517
|
+
// 那一屏就有数据可显示(读取只是一次 settings describe,不发任何计费请求)。
|
|
1518
|
+
react.useEffect(() => {
|
|
1519
|
+
if (!cc) return;
|
|
1520
|
+
void cc.load();
|
|
1521
|
+
}, [cc]);
|
|
1522
|
+
// 账户区 ready 后把当前固定账户同步为显示账户:设置页里切换账户后,上方面板与药丸
|
|
1523
|
+
// 一起改看所选账户的额度(与弹层的同步行为一致)。自动轮换(activeId 为空)同步为
|
|
1524
|
+
// 默认账户,store 内部对相同账户去重,不产生多余请求。
|
|
1525
|
+
react.useEffect(() => {
|
|
1526
|
+
if (usageId !== "commandcode" || !ccState || ccState.status !== "ready") return;
|
|
1527
|
+
// 旧 store stub / 更早版本的客户端装配没有这个方法:缺了就不同步,不影响其他功能。
|
|
1528
|
+
if (typeof usageStore.setCommandCodeAccount !== "function") return;
|
|
1529
|
+
void usageStore.setCommandCodeAccount(ccState.activeId).catch(() => {});
|
|
1530
|
+
}, [usageStore, usageId, ccState && ccState.status, ccState && ccState.activeId]);
|
|
1387
1531
|
const meta = PROVIDER_META[tab];
|
|
1388
1532
|
const source = entry && entry.keySource || "none";
|
|
1389
1533
|
// auth-file 有三家(Grok / Codex / Command Code),凭据来源文件各不相同,不能只特判一家。
|
|
@@ -1503,6 +1647,9 @@ window.__ModuleLoader__.load({
|
|
|
1503
1647
|
h("div", { className: "subusage-provider-tabs", role: "tablist", "aria-label": text("模型商", "Providers"), style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 14 } },
|
|
1504
1648
|
visibleIds.map((id, index) => {
|
|
1505
1649
|
const e = entries.find((item) => item.providerId === id), active = usageId === id;
|
|
1650
|
+
// 标签圆标与面板/药丸读同一个 computeStatus:缺凭据是蓝 ⚙ 而不是红 !,
|
|
1651
|
+
// 无窗口成功是中性 ℹ 而不是绿 ?(没有额度窗口不能默认标绿)。
|
|
1652
|
+
const tabStatus = computeStatus(e, { failed: e && e.state !== "ok" ? e.error || e.state : null, failState: e && e.state, providerId: id, t, getLocale });
|
|
1506
1653
|
return h("button", { key: id, id: `subusage-tab-${id}`, role: "tab", type: "button", tabIndex: active || (!visibleIds.includes(usageId) && index === 0) ? 0 : -1, "aria-selected": active,
|
|
1507
1654
|
"aria-controls": "subusage-provider-panel", title: `${PROVIDER_META[id].label}: ${providerDescription(e, t)}`, "aria-label": `${PROVIDER_META[id].short}: ${providerDescription(e, t)}`,
|
|
1508
1655
|
onClick: () => chooseUsage(id), onKeyDown: (event) => {
|
|
@@ -1512,20 +1659,33 @@ window.__ModuleLoader__.load({
|
|
|
1512
1659
|
else if (event.key === "Home") next = 0; else if (event.key === "End") next = visibleIds.length - 1; else return;
|
|
1513
1660
|
event.preventDefault(); chooseUsage(visibleIds[next]);
|
|
1514
1661
|
const button = event.currentTarget.parentElement.querySelectorAll('[role="tab"]')[next]; if (button) button.focus();
|
|
1515
|
-
}, style: { ...btnStyle,
|
|
1516
|
-
},
|
|
1662
|
+
}, style: { ...btnStyle, display: "inline-flex", alignItems: "center", gap: 6, minWidth: 0, padding: "5px 12px 5px 8px", whiteSpace: "nowrap", fontSize: 12, fontWeight: active ? 600 : 400, borderRadius: 999, border: "1px solid transparent", background: active ? alpha(INFO, ".16") : "transparent" }
|
|
1663
|
+
}, statusGlyph(tabStatus.color, tabStatus.icon, 14), PROVIDER_META[id].short);
|
|
1517
1664
|
})),
|
|
1518
|
-
!!usageId ? h("section", { role: "tabpanel", id: "subusage-provider-panel", "aria-label": usageMeta.label, style: { marginTop: 12, border: "1px solid rgba(128,128,128,.25)", borderRadius:
|
|
1519
|
-
h("
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1665
|
+
!!usageId ? h("section", { role: "tabpanel", id: "subusage-provider-panel", "aria-label": usageMeta.label, style: { marginTop: 12, border: "1px solid rgba(128,128,128,.25)", borderRadius: 14, padding: 16 } },
|
|
1666
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
1667
|
+
h("strong", { style: { fontSize: 14 } }, usageMeta.label),
|
|
1668
|
+
h("span", { style: { flex: 1 } }),
|
|
1669
|
+
planName(usageEntry) ? h("span", { style: { fontSize: 11, fontWeight: 600, padding: "1px 8px", borderRadius: 999, background: "rgba(128,128,128,.16)", whiteSpace: "nowrap" } }, planName(usageEntry)) : null),
|
|
1670
|
+
// 一眼结论:与药丸、弹层同一个状态判定。读取成功只说明接口健康,额度档位另有结论。
|
|
1671
|
+
statusBanner(usageStatus),
|
|
1672
|
+
usageEntry && usageEntry.error && usageStatus.key !== "error" ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, usageEntry.error) : null,
|
|
1523
1673
|
usageAction ? h("div", { role: "group", "aria-label": text("建议操作", "Suggested action"), style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", margin: "10px 0", padding: "8px 10px", border: "1px solid rgba(234,179,8,.45)", borderRadius: 8, background: "rgba(234,179,8,.08)" } },
|
|
1524
1674
|
h("span", { style: { flex: 1, minWidth: 150, fontSize: 12 } }, usageAction === "mimo-relogin" ? text("Cookie 已过期,重新登录即可恢复。", "Cookie expired — sign in again to restore.") : usageAction === "mimo-login" ? text("需要登录 MiMo(或 Cookie 即将到期)。", "MiMo sign-in needed (or the cookie is about to expire).") : text("这家还没有可用的凭据。", "No usable credentials for this provider.")),
|
|
1525
1675
|
usageAction !== "credentials" ? h("button", { type: "button", style: primary, disabled: busy || loginActive, onClick: startLogin }, usageAction === "mimo-relogin" ? text("重新登录", "Sign in again") : text("登录并自动导入", "Sign in & import")) : null,
|
|
1526
1676
|
h("button", { type: "button", style: usageAction === "credentials" ? primary : btnStyle, disabled: busy, onClick: () => revealEditor(usageId) }, usageAction === "credentials" ? text("配置凭据", "Configure credentials") : text("手动填写", "Enter manually"))) : null,
|
|
1527
|
-
|
|
1528
|
-
canShowUsage(usageEntry) && usageEntry.
|
|
1677
|
+
// 无窗口时的占位只给「成功但既无窗口也无余额」:余额/加载/需配置/失败等状态横幅已经说明,不再叠加「暂无额度数据」。
|
|
1678
|
+
canShowUsage(usageEntry) && (usageEntry.windows || []).length ? UsageWindowList({ windows: usageEntry.windows, t, getLocale, uncertain: usageEntry.coverage === "partial" || usageEntry.freshness === "stale" || usageEntry.state !== "ok" })
|
|
1679
|
+
: usageStatus.key === "balance" && !(usageStatus.entry && balanceText(usageStatus.entry, t)) ? h("p", { style: { opacity: .75 } }, t("noUsageData")) : null,
|
|
1680
|
+
canShowUsage(usageEntry) && usageEntry.extras && usageEntry.extras.some((x) => x.kind !== "plan")
|
|
1681
|
+
? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
|
|
1682
|
+
usageEntry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
|
|
1683
|
+
h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
|
|
1684
|
+
h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
|
|
1685
|
+
: null,
|
|
1686
|
+
// 账户区与药丸弹层同一个组件、同一个控制器:设置页里也能切换 activeAccount,
|
|
1687
|
+
// 不必再跳去 Command Code 提供方插件的设置页。提供方插件未运行时整区降级。
|
|
1688
|
+
usageId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
|
|
1529
1689
|
usageEntry?.lastAttemptAt ? h("p", { style: { opacity: .65 } }, `${t("lastUpdated")} ${new Date(usageEntry.lastAttemptAt).toLocaleString(getLocale())}`) : null
|
|
1530
1690
|
) : null,
|
|
1531
1691
|
providerManagement
|
|
@@ -1578,7 +1738,7 @@ window.__ModuleLoader__.load({
|
|
|
1578
1738
|
ctx.inject(["remote.settings"], (settingsCtx) => { ccSettingsNamespace = settingsCtx.remote.settings; });
|
|
1579
1739
|
ctx.effect(() => () => cc.dispose(), "dsh-subusage: commandcode account switch");
|
|
1580
1740
|
try {
|
|
1581
|
-
ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "subusage", order: 22, label: () => t("nav"), inject: () => ({ usageStore, t, getLocale }) }, SubusageSection));
|
|
1741
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "subusage", order: 22, label: () => t("nav"), inject: () => ({ usageStore, t, getLocale, cc }) }, SubusageSection));
|
|
1582
1742
|
} catch (error) { console.error("[dsh-subusage] 设置页注册失败:", error); }
|
|
1583
1743
|
ctx.inject(["modelDirectories"], (scope) => {
|
|
1584
1744
|
try {
|