opencode-visual-cache 1.6.4 → 1.6.5
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/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/balance-providers.d.ts +9 -0
- package/dist/balance-providers.js +219 -1
- package/dist/i18n.d.ts +13 -0
- package/dist/i18n.js +52 -0
- package/dist/index.js +106 -4
- package/dist/tui.js +554 -161
- package/package.json +2 -1
- package/src/_version.ts +1 -1
- package/src/balance-providers.ts +248 -1
- package/src/i18n.ts +52 -0
- package/src/index.tsx +124 -9
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "1.6.
|
|
1
|
+
export declare const PLUGIN_VERSION = "1.6.5";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION = "1.6.
|
|
2
|
+
export const PLUGIN_VERSION = "1.6.5";
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
export interface BalanceEntry {
|
|
3
3
|
currency: string;
|
|
4
4
|
total: string;
|
|
5
|
+
display?: string;
|
|
6
|
+
details?: BalanceDetail[];
|
|
7
|
+
}
|
|
8
|
+
export type BalanceDetailKey = "plan" | "used" | "remaining" | "window" | "reset" | "codeReview" | "credits" | "resetCredits";
|
|
9
|
+
export interface BalanceDetail {
|
|
10
|
+
key: BalanceDetailKey;
|
|
11
|
+
value: string;
|
|
12
|
+
windowSeconds?: number;
|
|
5
13
|
}
|
|
6
14
|
/** provider 统一错误:message 即错误码(401/403/EMPTY/…),显示层直接展示。 */
|
|
7
15
|
export declare class BalanceError extends Error {
|
|
@@ -13,6 +21,7 @@ export interface BalanceProvider {
|
|
|
13
21
|
keyPlaceholder?: string;
|
|
14
22
|
fetchBalance(apiKey: string, signal?: AbortSignal): Promise<BalanceEntry[]>;
|
|
15
23
|
}
|
|
24
|
+
export declare function parseOpenAIUsage(raw: unknown, nowMs?: number): BalanceEntry[];
|
|
16
25
|
/** 已注册的 provider 列表(按需追加新适配器)。 */
|
|
17
26
|
export declare const balanceProviders: BalanceProvider[];
|
|
18
27
|
/** 按 id 取 provider;未知 id 回退到第一个。 */
|
|
@@ -129,8 +129,226 @@ const hyperProvider = {
|
|
|
129
129
|
return [{ currency: "USD", total: usd }];
|
|
130
130
|
},
|
|
131
131
|
};
|
|
132
|
+
function decodeJwtPayload(token) {
|
|
133
|
+
try {
|
|
134
|
+
const encoded = token.split(".")[1];
|
|
135
|
+
if (!encoded || typeof atob !== "function")
|
|
136
|
+
return undefined;
|
|
137
|
+
const binary = atob(encoded.replace(/-/g, "+").replace(/_/g, "/"));
|
|
138
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
139
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function getChatGPTAccountId(token) {
|
|
146
|
+
const payload = decodeJwtPayload(token);
|
|
147
|
+
const auth = payload?.["https://api.openai.com/auth"];
|
|
148
|
+
if (auth && typeof auth === "object") {
|
|
149
|
+
const accountId = auth.chatgpt_account_id;
|
|
150
|
+
if (typeof accountId === "string" && accountId)
|
|
151
|
+
return accountId;
|
|
152
|
+
}
|
|
153
|
+
const accountId = payload?.chatgpt_account_id;
|
|
154
|
+
return typeof accountId === "string" && accountId ? accountId : undefined;
|
|
155
|
+
}
|
|
156
|
+
function asRecord(value) {
|
|
157
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
158
|
+
}
|
|
159
|
+
function asFiniteNumber(value) {
|
|
160
|
+
const number = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : NaN;
|
|
161
|
+
return Number.isFinite(number) ? number : undefined;
|
|
162
|
+
}
|
|
163
|
+
function clampPercent(value) {
|
|
164
|
+
return Math.max(0, Math.min(100, value));
|
|
165
|
+
}
|
|
166
|
+
function formatPercent(value) {
|
|
167
|
+
return Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1);
|
|
168
|
+
}
|
|
169
|
+
function formatCreditAmount(value) {
|
|
170
|
+
if (Number.isInteger(value))
|
|
171
|
+
return String(value);
|
|
172
|
+
return value.toFixed(2).replace(/\.?0+$/, "");
|
|
173
|
+
}
|
|
174
|
+
function getPercentages(snapshot) {
|
|
175
|
+
const explicitUsed = asFiniteNumber(snapshot.used_percent);
|
|
176
|
+
const explicitRemaining = asFiniteNumber(snapshot.remaining_percent);
|
|
177
|
+
if (explicitUsed !== undefined || explicitRemaining !== undefined) {
|
|
178
|
+
const used = clampPercent(explicitUsed ?? 100 - explicitRemaining);
|
|
179
|
+
const remaining = clampPercent(explicitRemaining ?? 100 - used);
|
|
180
|
+
return { used, remaining };
|
|
181
|
+
}
|
|
182
|
+
const limit = asFiniteNumber(snapshot.limit);
|
|
183
|
+
const usedAmount = asFiniteNumber(snapshot.used);
|
|
184
|
+
const remainingAmount = asFiniteNumber(snapshot.remaining);
|
|
185
|
+
if (limit !== undefined && limit > 0 && (usedAmount !== undefined || remainingAmount !== undefined)) {
|
|
186
|
+
const used = usedAmount !== undefined ? (usedAmount / limit) * 100 : 100 - (remainingAmount / limit) * 100;
|
|
187
|
+
const remaining = remainingAmount !== undefined ? (remainingAmount / limit) * 100 : 100 - used;
|
|
188
|
+
return { used: clampPercent(used), remaining: clampPercent(remaining) };
|
|
189
|
+
}
|
|
190
|
+
const amountTotal = (usedAmount ?? 0) + (remainingAmount ?? 0);
|
|
191
|
+
if (amountTotal > 0 && (usedAmount !== undefined || remainingAmount !== undefined)) {
|
|
192
|
+
const used = usedAmount !== undefined ? (usedAmount / amountTotal) * 100 : 0;
|
|
193
|
+
return { used: clampPercent(used), remaining: clampPercent(100 - used) };
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
function getRateWindows(rateLimit) {
|
|
198
|
+
const record = asRecord(rateLimit);
|
|
199
|
+
if (!record)
|
|
200
|
+
return [];
|
|
201
|
+
return Object.entries(record)
|
|
202
|
+
.map(([name, value], order) => {
|
|
203
|
+
const data = asRecord(value);
|
|
204
|
+
if (!data)
|
|
205
|
+
return undefined;
|
|
206
|
+
const normalizedName = name.toLowerCase();
|
|
207
|
+
if (normalizedName.includes("individual"))
|
|
208
|
+
return undefined;
|
|
209
|
+
const windowSeconds = asFiniteNumber(data.limit_window_seconds);
|
|
210
|
+
const looksLikeWindow = normalizedName.includes("window") ||
|
|
211
|
+
windowSeconds !== undefined ||
|
|
212
|
+
"used_percent" in data ||
|
|
213
|
+
"remaining_percent" in data;
|
|
214
|
+
if (!looksLikeWindow)
|
|
215
|
+
return undefined;
|
|
216
|
+
return { data, windowSeconds, order };
|
|
217
|
+
})
|
|
218
|
+
.filter((window) => window !== undefined)
|
|
219
|
+
.sort((a, b) => (a.windowSeconds ?? Number.MAX_SAFE_INTEGER) - (b.windowSeconds ?? Number.MAX_SAFE_INTEGER) || a.order - b.order);
|
|
220
|
+
}
|
|
221
|
+
function getResetAfterSeconds(snapshot, nowMs) {
|
|
222
|
+
const relative = asFiniteNumber(snapshot.reset_after_seconds);
|
|
223
|
+
if (relative !== undefined)
|
|
224
|
+
return Math.max(0, Math.round(relative));
|
|
225
|
+
for (const key of ["reset_at", "resets_at", "resetAt", "resetsAt"]) {
|
|
226
|
+
const timestamp = asFiniteNumber(snapshot[key]);
|
|
227
|
+
if (timestamp === undefined)
|
|
228
|
+
continue;
|
|
229
|
+
const timestampSeconds = timestamp > 1e12 ? timestamp / 1000 : timestamp;
|
|
230
|
+
return Math.max(0, Math.round(timestampSeconds - nowMs / 1000));
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
function appendQuotaDetails(details, percentages, windowSeconds) {
|
|
235
|
+
const scope = windowSeconds === undefined ? {} : { windowSeconds };
|
|
236
|
+
details.push({ key: "used", value: `${formatPercent(percentages.used)}%`, ...scope });
|
|
237
|
+
details.push({ key: "remaining", value: `${formatPercent(percentages.remaining)}%`, ...scope });
|
|
238
|
+
}
|
|
239
|
+
export function parseOpenAIUsage(raw, nowMs = Date.now()) {
|
|
240
|
+
const json = asRecord(raw);
|
|
241
|
+
if (!json)
|
|
242
|
+
throw new BalanceError("EMPTY");
|
|
243
|
+
const details = [];
|
|
244
|
+
if (typeof json.plan_type === "string" && json.plan_type) {
|
|
245
|
+
details.push({ key: "plan", value: json.plan_type.toUpperCase() });
|
|
246
|
+
}
|
|
247
|
+
const rateLimit = asRecord(json.rate_limit);
|
|
248
|
+
const remainingValues = [];
|
|
249
|
+
let hasRateQuota = false;
|
|
250
|
+
for (const window of getRateWindows(rateLimit)) {
|
|
251
|
+
const percentages = getPercentages(window.data);
|
|
252
|
+
if (percentages) {
|
|
253
|
+
appendQuotaDetails(details, percentages, window.windowSeconds);
|
|
254
|
+
remainingValues.push(percentages.remaining);
|
|
255
|
+
hasRateQuota = true;
|
|
256
|
+
}
|
|
257
|
+
const resetAfter = getResetAfterSeconds(window.data, nowMs);
|
|
258
|
+
if (resetAfter !== undefined) {
|
|
259
|
+
details.push({
|
|
260
|
+
key: "reset",
|
|
261
|
+
value: String(resetAfter),
|
|
262
|
+
...(window.windowSeconds === undefined ? {} : { windowSeconds: window.windowSeconds }),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const spendControl = asRecord(asRecord(json.spend_control)?.individual_limit);
|
|
267
|
+
const individualLimit = asRecord(json.individual_limit) ?? asRecord(rateLimit?.individual_limit) ?? spendControl;
|
|
268
|
+
const individualPercentages = individualLimit ? getPercentages(individualLimit) : undefined;
|
|
269
|
+
if (individualPercentages) {
|
|
270
|
+
remainingValues.push(individualPercentages.remaining);
|
|
271
|
+
if (!hasRateQuota)
|
|
272
|
+
appendQuotaDetails(details, individualPercentages);
|
|
273
|
+
}
|
|
274
|
+
if (individualLimit) {
|
|
275
|
+
const resetAfter = getResetAfterSeconds(individualLimit, nowMs);
|
|
276
|
+
if (resetAfter !== undefined)
|
|
277
|
+
details.push({ key: "reset", value: String(resetAfter) });
|
|
278
|
+
}
|
|
279
|
+
const codeReviewWindow = asRecord(asRecord(json.code_review_rate_limit)?.primary_window);
|
|
280
|
+
const codeReviewUsed = asFiniteNumber(codeReviewWindow?.used_percent);
|
|
281
|
+
if (codeReviewUsed !== undefined) {
|
|
282
|
+
details.push({ key: "codeReview", value: `${formatPercent(clampPercent(100 - codeReviewUsed))}%` });
|
|
283
|
+
}
|
|
284
|
+
const credits = asRecord(json.credits);
|
|
285
|
+
let hasCreditDetail = false;
|
|
286
|
+
if (credits?.unlimited === true) {
|
|
287
|
+
details.push({ key: "credits", value: "unlimited" });
|
|
288
|
+
hasCreditDetail = true;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const creditBalance = asFiniteNumber(credits?.balance);
|
|
292
|
+
if (creditBalance !== undefined) {
|
|
293
|
+
details.push({ key: "credits", value: `$${creditBalance.toFixed(2)}` });
|
|
294
|
+
hasCreditDetail = true;
|
|
295
|
+
}
|
|
296
|
+
else if (individualLimit) {
|
|
297
|
+
const remaining = asFiniteNumber(individualLimit.remaining);
|
|
298
|
+
const limit = asFiniteNumber(individualLimit.limit);
|
|
299
|
+
const amounts = [remaining, limit].filter((value) => value !== undefined);
|
|
300
|
+
if (amounts.length > 0) {
|
|
301
|
+
details.push({ key: "credits", value: amounts.map(formatCreditAmount).join(" / ") });
|
|
302
|
+
hasCreditDetail = true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const resetCredits = asFiniteNumber(asRecord(json.rate_limit_reset_credits)?.available_count);
|
|
307
|
+
if (resetCredits !== undefined)
|
|
308
|
+
details.push({ key: "resetCredits", value: String(resetCredits) });
|
|
309
|
+
if (details.length === 0 || (!hasRateQuota && !individualPercentages && !hasCreditDetail && resetCredits === undefined)) {
|
|
310
|
+
throw new BalanceError("EMPTY");
|
|
311
|
+
}
|
|
312
|
+
const summaryRemaining = remainingValues.length > 0 ? Math.min(...remainingValues) : undefined;
|
|
313
|
+
const summary = summaryRemaining === undefined ? undefined : formatPercent(summaryRemaining);
|
|
314
|
+
return [{
|
|
315
|
+
currency: "CODEX",
|
|
316
|
+
total: summary === undefined ? "0" : `${summary}%`,
|
|
317
|
+
display: summary === undefined ? "Codex" : `Codex ${summary}%`,
|
|
318
|
+
details,
|
|
319
|
+
}];
|
|
320
|
+
}
|
|
321
|
+
const openaiProvider = {
|
|
322
|
+
id: "openai",
|
|
323
|
+
name: "OpenAI Codex",
|
|
324
|
+
keyPlaceholder: "OAuth access token (eyJ...)",
|
|
325
|
+
async fetchBalance(accessToken, signal) {
|
|
326
|
+
const headers = {
|
|
327
|
+
Authorization: `Bearer ${accessToken}`,
|
|
328
|
+
Accept: "application/json",
|
|
329
|
+
Referer: "https://chatgpt.com/",
|
|
330
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/147.0.0.0 Safari/537.36",
|
|
331
|
+
"OpenAI-Beta": "codex-1",
|
|
332
|
+
"oai-language": "zh-CN",
|
|
333
|
+
originator: "Codex Desktop",
|
|
334
|
+
};
|
|
335
|
+
const accountId = getChatGPTAccountId(accessToken);
|
|
336
|
+
if (accountId)
|
|
337
|
+
headers["ChatGPT-Account-Id"] = accountId;
|
|
338
|
+
const res = await fetch("https://chatgpt.com/backend-api/wham/usage", { headers, signal });
|
|
339
|
+
if (!res.ok) {
|
|
340
|
+
if (res.status === 401)
|
|
341
|
+
throw new BalanceError("401");
|
|
342
|
+
if (res.status === 403)
|
|
343
|
+
throw new BalanceError("403");
|
|
344
|
+
throw new BalanceError(String(res.status));
|
|
345
|
+
}
|
|
346
|
+
const json = await res.json();
|
|
347
|
+
return parseOpenAIUsage(json);
|
|
348
|
+
},
|
|
349
|
+
};
|
|
132
350
|
/** 已注册的 provider 列表(按需追加新适配器)。 */
|
|
133
|
-
export const balanceProviders = [deepseekProvider, siliconflowProvider, openrouterProvider, moonshotProvider, hyperProvider];
|
|
351
|
+
export const balanceProviders = [deepseekProvider, siliconflowProvider, openrouterProvider, moonshotProvider, hyperProvider, openaiProvider];
|
|
134
352
|
/** 按 id 取 provider;未知 id 回退到第一个。 */
|
|
135
353
|
export function getBalanceProvider(id) {
|
|
136
354
|
return balanceProviders.find((p) => p.id === id) ?? balanceProviders[0] ?? deepseekProvider;
|
package/dist/i18n.d.ts
CHANGED
|
@@ -38,6 +38,19 @@ declare const ZH_T: {
|
|
|
38
38
|
readonly balErrEmpty: "未获取到余额数据";
|
|
39
39
|
readonly balErrTimeout: "查询超时";
|
|
40
40
|
readonly balUnsupported: "当前提供商不支持余额查询";
|
|
41
|
+
readonly balDetailPlan: "套餐";
|
|
42
|
+
readonly balDetailUsed: "已用";
|
|
43
|
+
readonly balDetailRemaining: "剩余";
|
|
44
|
+
readonly balDetailWindow: "周期";
|
|
45
|
+
readonly balDetailReset: "重置";
|
|
46
|
+
readonly balDetailCodeReview: "Code Review";
|
|
47
|
+
readonly balDetailCredits: "Credits";
|
|
48
|
+
readonly balDetailResetCredits: "重置次数";
|
|
49
|
+
readonly balUnlimited: "无限";
|
|
50
|
+
readonly balDay: "天";
|
|
51
|
+
readonly balHour: "小时";
|
|
52
|
+
readonly balMinute: "分钟";
|
|
53
|
+
readonly balResetSoon: "即将重置";
|
|
41
54
|
readonly barHit: "命中率";
|
|
42
55
|
readonly barBal: "余额";
|
|
43
56
|
readonly barTok: "Tokens";
|
package/dist/i18n.js
CHANGED
|
@@ -41,6 +41,19 @@ const ZH_T = {
|
|
|
41
41
|
balErrEmpty: "未获取到余额数据",
|
|
42
42
|
balErrTimeout: "查询超时",
|
|
43
43
|
balUnsupported: "当前提供商不支持余额查询",
|
|
44
|
+
balDetailPlan: "套餐",
|
|
45
|
+
balDetailUsed: "已用",
|
|
46
|
+
balDetailRemaining: "剩余",
|
|
47
|
+
balDetailWindow: "周期",
|
|
48
|
+
balDetailReset: "重置",
|
|
49
|
+
balDetailCodeReview: "Code Review",
|
|
50
|
+
balDetailCredits: "Credits",
|
|
51
|
+
balDetailResetCredits: "重置次数",
|
|
52
|
+
balUnlimited: "无限",
|
|
53
|
+
balDay: "天",
|
|
54
|
+
balHour: "小时",
|
|
55
|
+
balMinute: "分钟",
|
|
56
|
+
balResetSoon: "即将重置",
|
|
44
57
|
barHit: "命中率",
|
|
45
58
|
barBal: "余额",
|
|
46
59
|
barTok: "Tokens",
|
|
@@ -122,6 +135,19 @@ const EN_T = {
|
|
|
122
135
|
balErrEmpty: "No balance data",
|
|
123
136
|
balErrTimeout: "Request timed out",
|
|
124
137
|
balUnsupported: "Balance query unsupported",
|
|
138
|
+
balDetailPlan: "Plan",
|
|
139
|
+
balDetailUsed: "Used",
|
|
140
|
+
balDetailRemaining: "Remaining",
|
|
141
|
+
balDetailWindow: "Window",
|
|
142
|
+
balDetailReset: "Reset",
|
|
143
|
+
balDetailCodeReview: "Code Review",
|
|
144
|
+
balDetailCredits: "Credits",
|
|
145
|
+
balDetailResetCredits: "Reset credits",
|
|
146
|
+
balUnlimited: "Unlimited",
|
|
147
|
+
balDay: "d",
|
|
148
|
+
balHour: "h",
|
|
149
|
+
balMinute: "m",
|
|
150
|
+
balResetSoon: "soon",
|
|
125
151
|
barHit: "Hit",
|
|
126
152
|
barBal: "Balance",
|
|
127
153
|
barTok: "Tokens",
|
|
@@ -203,6 +229,19 @@ const JA_T = {
|
|
|
203
229
|
balErrEmpty: "残高データなし",
|
|
204
230
|
balErrTimeout: "タイムアウト",
|
|
205
231
|
balUnsupported: "このプロバイダは残高照会非対応",
|
|
232
|
+
balDetailPlan: "プラン",
|
|
233
|
+
balDetailUsed: "使用済み",
|
|
234
|
+
balDetailRemaining: "残り",
|
|
235
|
+
balDetailWindow: "期間",
|
|
236
|
+
balDetailReset: "リセット",
|
|
237
|
+
balDetailCodeReview: "Code Review",
|
|
238
|
+
balDetailCredits: "Credits",
|
|
239
|
+
balDetailResetCredits: "リセット回数",
|
|
240
|
+
balUnlimited: "無制限",
|
|
241
|
+
balDay: "日",
|
|
242
|
+
balHour: "時間",
|
|
243
|
+
balMinute: "分",
|
|
244
|
+
balResetSoon: "まもなく",
|
|
206
245
|
barHit: "ヒット率",
|
|
207
246
|
barBal: "残高",
|
|
208
247
|
barTok: "Tokens",
|
|
@@ -284,6 +323,19 @@ const KO_T = {
|
|
|
284
323
|
balErrEmpty: "잔액 데이터 없음",
|
|
285
324
|
balErrTimeout: "시간 초과",
|
|
286
325
|
balUnsupported: "이 프로바이더는 잔액 조회 미지원",
|
|
326
|
+
balDetailPlan: "플랜",
|
|
327
|
+
balDetailUsed: "사용",
|
|
328
|
+
balDetailRemaining: "잔여",
|
|
329
|
+
balDetailWindow: "주기",
|
|
330
|
+
balDetailReset: "재설정",
|
|
331
|
+
balDetailCodeReview: "Code Review",
|
|
332
|
+
balDetailCredits: "Credits",
|
|
333
|
+
balDetailResetCredits: "재설정 횟수",
|
|
334
|
+
balUnlimited: "무제한",
|
|
335
|
+
balDay: "일",
|
|
336
|
+
balHour: "시간",
|
|
337
|
+
balMinute: "분",
|
|
338
|
+
balResetSoon: "곧 재설정",
|
|
287
339
|
barHit: "히트율",
|
|
288
340
|
barBal: "잔액",
|
|
289
341
|
barTok: "Tokens",
|
package/dist/index.js
CHANGED
|
@@ -248,15 +248,57 @@ function convertBalance(target, targetRate, amount, from) {
|
|
|
248
248
|
/**
|
|
249
249
|
* 从 OpenCode 已认证的 provider 读取 API key 作为余额查询的自动兜底。
|
|
250
250
|
* 匹配复用前缀逻辑:先精确匹配 id,再前缀匹配(如 moonshotai-cn → moonshot)。
|
|
251
|
-
*
|
|
252
|
-
*
|
|
251
|
+
* OpenAI 优先读取 auth.json OAuth;其他 provider 读取 provider.key / provider.options.apiKey。
|
|
252
|
+
* 读取失败或未匹配返回空串。
|
|
253
253
|
*/
|
|
254
|
+
function readOpenAIOAuthToken(api) {
|
|
255
|
+
try {
|
|
256
|
+
// OpenAI OAuth credentials are stored separately from provider.key.
|
|
257
|
+
const loader = typeof process !== "undefined" ? process?.getBuiltinModule : undefined;
|
|
258
|
+
const fs = loader?.("node:fs");
|
|
259
|
+
if (!fs)
|
|
260
|
+
return "";
|
|
261
|
+
const stateDir = api.state.path.state.replace(/[\\/]+$/, "");
|
|
262
|
+
const home = typeof process !== "undefined" ? (process?.env.HOME || process?.env.USERPROFILE || "") : "";
|
|
263
|
+
const dataHome = typeof process !== "undefined" ? process?.env.XDG_DATA_HOME : undefined;
|
|
264
|
+
const paths = [
|
|
265
|
+
stateDir ? `${stateDir}/auth.json` : "",
|
|
266
|
+
dataHome ? `${dataHome}/opencode/auth.json` : "",
|
|
267
|
+
home ? `${home}/.local/share/opencode/auth.json` : "",
|
|
268
|
+
];
|
|
269
|
+
for (const path of paths) {
|
|
270
|
+
if (!path)
|
|
271
|
+
continue;
|
|
272
|
+
try {
|
|
273
|
+
const auth = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
274
|
+
const openai = auth.openai;
|
|
275
|
+
if (openai && typeof openai === "object") {
|
|
276
|
+
const record = openai;
|
|
277
|
+
if (record.type === "oauth" && typeof record.access === "string")
|
|
278
|
+
return record.access;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch { /* try the next known auth path */ }
|
|
282
|
+
}
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
254
289
|
function findOpencodeKey(api, provider) {
|
|
255
290
|
try {
|
|
256
291
|
const provs = api.state.provider;
|
|
257
292
|
// 大小写不敏感:精确匹配 id,否则前缀匹配(如 moonshotai-cn → moonshot)
|
|
258
293
|
const id = provider.id.toLowerCase();
|
|
259
294
|
const hit = provs.find((p) => p.id.toLowerCase() === id) ?? provs.find((p) => p.id.toLowerCase().startsWith(id));
|
|
295
|
+
const isOpenAI = id === "openai";
|
|
296
|
+
// OAuth token 优先于 provider.key,避免把配置中的占位值当成 access token。
|
|
297
|
+
if (isOpenAI) {
|
|
298
|
+
const oauth = readOpenAIOAuthToken(api);
|
|
299
|
+
if (oauth)
|
|
300
|
+
return oauth;
|
|
301
|
+
}
|
|
260
302
|
if (!hit)
|
|
261
303
|
return "";
|
|
262
304
|
const k = typeof hit.key === "string" ? hit.key : "";
|
|
@@ -295,6 +337,9 @@ function formatBalanceAmount(total) {
|
|
|
295
337
|
* 优先直接显示偏好币种(CNY/USD…);偏好币种为换算币种时按汇率折算第一条余额。
|
|
296
338
|
*/
|
|
297
339
|
function formatBalanceText(list, pref, rate) {
|
|
340
|
+
const custom = list.find((x) => x.display);
|
|
341
|
+
if (custom?.display)
|
|
342
|
+
return custom.display;
|
|
298
343
|
const native = pref ? list.find((x) => x.currency === pref) : undefined;
|
|
299
344
|
if (native)
|
|
300
345
|
return balanceSymbol(native.currency) + formatBalanceAmount(native.total);
|
|
@@ -308,6 +353,16 @@ function formatBalanceText(list, pref, rate) {
|
|
|
308
353
|
: formatBalanceAmount(base.total);
|
|
309
354
|
return balanceSymbol(pref || base.currency) + shown;
|
|
310
355
|
}
|
|
356
|
+
const BALANCE_DETAIL_LABELS = {
|
|
357
|
+
plan: "balDetailPlan",
|
|
358
|
+
used: "balDetailUsed",
|
|
359
|
+
remaining: "balDetailRemaining",
|
|
360
|
+
window: "balDetailWindow",
|
|
361
|
+
reset: "balDetailReset",
|
|
362
|
+
codeReview: "balDetailCodeReview",
|
|
363
|
+
credits: "balDetailCredits",
|
|
364
|
+
resetCredits: "balDetailResetCredits",
|
|
365
|
+
};
|
|
311
366
|
const CURRENCIES = {
|
|
312
367
|
USD: "$", CNY: "¥", EUR: "€", JPY: "JP¥", GBP: "£", KRW: "₩",
|
|
313
368
|
};
|
|
@@ -332,6 +387,7 @@ function TokenCachePanel(props) {
|
|
|
332
387
|
const [modelOpen, setModelOpen] = createSignal(true);
|
|
333
388
|
const [distOpen, setDistOpen] = createSignal(false);
|
|
334
389
|
const [skillsOpen, setSkillsOpen] = createSignal(true);
|
|
390
|
+
const [balanceOpen, setBalanceOpen] = createSignal(false);
|
|
335
391
|
let boxEl;
|
|
336
392
|
// 侧边栏可见性通知:本面板挂载 ⇒ 宿主侧边栏可见(固定占用 42 列输入框宽度)
|
|
337
393
|
createEffect(() => {
|
|
@@ -342,6 +398,38 @@ function TokenCachePanel(props) {
|
|
|
342
398
|
const { currencySymbol, setCurrencySymbol, exchangeRate, setExchangeRate, langCode, sectionDetail, setSectionDetail, sectionModel, setSectionModel, sectionDist, setSectionDist, sectionSkills, setSectionSkills, sectionBalance, setSectionBalance, balanceRefresh, balanceProviderId, setBalanceProviderId, autoBalance, setAutoBalance, balanceUnsupported, setBalanceUnsupported, balanceState, balanceCurrency, setBalanceCurrency, borderVisible, setBorderVisible, } = props.signals;
|
|
343
399
|
// ── reactive translation (follows langCode signal) ──
|
|
344
400
|
const t = createT(() => langCode());
|
|
401
|
+
const formatBalanceDuration = (seconds, fallback = "") => {
|
|
402
|
+
if (!Number.isFinite(seconds))
|
|
403
|
+
return "";
|
|
404
|
+
let remaining = Math.max(0, Math.round(seconds));
|
|
405
|
+
const days = Math.floor(remaining / 86400);
|
|
406
|
+
remaining %= 86400;
|
|
407
|
+
const hours = Math.floor(remaining / 3600);
|
|
408
|
+
remaining %= 3600;
|
|
409
|
+
const minutes = Math.floor(remaining / 60);
|
|
410
|
+
const parts = [];
|
|
411
|
+
if (days > 0)
|
|
412
|
+
parts.push(`${days}${t("balDay")}`);
|
|
413
|
+
if (hours > 0 && parts.length < 2)
|
|
414
|
+
parts.push(`${hours}${t("balHour")}`);
|
|
415
|
+
if (minutes > 0 && parts.length < 2)
|
|
416
|
+
parts.push(`${minutes}${t("balMinute")}`);
|
|
417
|
+
return parts.join(langCode() === "en" ? " " : "") || fallback;
|
|
418
|
+
};
|
|
419
|
+
const formatBalanceDetailValue = (detail) => {
|
|
420
|
+
if (detail.value === "unlimited")
|
|
421
|
+
return t("balUnlimited");
|
|
422
|
+
if (detail.key !== "reset")
|
|
423
|
+
return detail.value;
|
|
424
|
+
return formatBalanceDuration(Number(detail.value), t("balResetSoon")) || detail.value;
|
|
425
|
+
};
|
|
426
|
+
const formatBalanceDetailLabel = (detail) => {
|
|
427
|
+
const label = t(BALANCE_DETAIL_LABELS[detail.key]);
|
|
428
|
+
if (detail.windowSeconds === undefined)
|
|
429
|
+
return label;
|
|
430
|
+
const window = formatBalanceDuration(detail.windowSeconds);
|
|
431
|
+
return window ? `${label} (${window})` : label;
|
|
432
|
+
};
|
|
345
433
|
// ── scan session messages reactively ──
|
|
346
434
|
// SolidJS createMemo re-evaluates whenever the underlying
|
|
347
435
|
// api.state.session state changes — no event listener needed.
|
|
@@ -646,6 +734,7 @@ function TokenCachePanel(props) {
|
|
|
646
734
|
const data = createMemo(() => {
|
|
647
735
|
return dataSignal();
|
|
648
736
|
});
|
|
737
|
+
const balanceDetails = createMemo(() => balanceState().data?.find((entry) => entry.details)?.details ?? []);
|
|
649
738
|
// Persist the last valid distribution so that data() can fall back
|
|
650
739
|
// to it while api.state.part() is re-hydrating after a view switch.
|
|
651
740
|
createEffect(() => {
|
|
@@ -681,6 +770,7 @@ function TokenCachePanel(props) {
|
|
|
681
770
|
setModelOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.model`, true)));
|
|
682
771
|
setDistOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.dist`, false)));
|
|
683
772
|
setSkillsOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.skills`, true)));
|
|
773
|
+
setBalanceOpen(Boolean(props.api.kv.get(`${KV_PREFIX}.balance.open`, false)));
|
|
684
774
|
}
|
|
685
775
|
catch { }
|
|
686
776
|
// Restore user config (currency, rate, section visibility).
|
|
@@ -833,6 +923,14 @@ function TokenCachePanel(props) {
|
|
|
833
923
|
const gap = Math.max(1, gauge - used);
|
|
834
924
|
return label + " ".repeat(gap) + value + (unit ? " " + unit : "");
|
|
835
925
|
};
|
|
926
|
+
const balanceHeader = () => {
|
|
927
|
+
const arrow = balanceDetails().length > 0 ? (balanceOpen() ? "\u25bc " : "\u25b6 ") : "";
|
|
928
|
+
const title = t("secBalance");
|
|
929
|
+
const summary = balanceState().data ? formatBalanceText(balanceState().data, balanceCurrency(), exchangeRate()) : "";
|
|
930
|
+
const gauge = panelWidth() - gutter();
|
|
931
|
+
const dividerLength = Math.max(1, gauge - visualWidth(arrow + title) - visualWidth(summary) - 1);
|
|
932
|
+
return { arrow, title, summary, divider: sep().slice(0, dividerLength) };
|
|
933
|
+
};
|
|
836
934
|
return (_jsxs("box", { border: borderVisible(), ...(borderVisible() ? { borderColor: pal().border } : {}), paddingTop: 0, paddingBottom: 0, paddingLeft: borderVisible() ? 2 : 0, paddingRight: borderVisible() ? 2 : 0, flexDirection: "column", gap: 0, ref: boxEl, onSizeChange: () => {
|
|
837
935
|
// boxEl.width may be undefined before the first measurement — guard with 0
|
|
838
936
|
const w = boxEl ? Math.max(MIN_PANEL_WIDTH, boxEl.width ?? 0) : DEFAULT_PANEL_WIDTH;
|
|
@@ -846,7 +944,7 @@ function TokenCachePanel(props) {
|
|
|
846
944
|
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1);
|
|
847
945
|
const label = truncateVisual(sk.name, maxLabel);
|
|
848
946
|
return (_jsx("text", { fg: pal().muted, children: justify(label, fmt(sk.tokens), t("tok")) }));
|
|
849
|
-
}) })] }) }), _jsxs(Show, { when: sectionBalance(), children: [_jsx(
|
|
947
|
+
}) })] }) }), _jsxs(Show, { when: sectionBalance(), children: [_jsx(Show, { when: balanceUnsupported(), children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t("balUnsupported") })] }) }), _jsxs(Show, { when: !balanceUnsupported(), children: [_jsx(Show, { when: balanceState().status === "idle", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t("balNoKey", { p: providerName() }) })] }) }), _jsx(Show, { when: balanceState().status === "loading", children: _jsxs("text", { fg: pal().muted, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: t("balLoading") })] }) }), _jsx(Show, { when: balanceState().status === "error", children: _jsxs("text", { fg: pal().error, children: [_jsx("span", { style: { fg: pal().muted }, children: "> " }), _jsx("span", { children: (() => {
|
|
850
948
|
const code = balanceState().error;
|
|
851
949
|
if (code === "401")
|
|
852
950
|
return t("balErr401");
|
|
@@ -857,7 +955,11 @@ function TokenCachePanel(props) {
|
|
|
857
955
|
if (code === "TIMEOUT")
|
|
858
956
|
return t("balErrTimeout");
|
|
859
957
|
return t("balError") + (code ? ` (${code})` : "");
|
|
860
|
-
})() })] }) }),
|
|
958
|
+
})() })] }) }), _jsxs(Show, { when: balanceState().status === "ok" && balanceState().data, children: [_jsxs(Show, { when: balanceDetails().length > 0, children: [_jsxs("text", { fg: pal().text, onMouseUp: () => {
|
|
959
|
+
const next = !balanceOpen();
|
|
960
|
+
setBalanceOpen(next);
|
|
961
|
+
persistFold("balance.open", next);
|
|
962
|
+
}, children: [_jsx("span", { style: { fg: pal().muted }, children: balanceHeader().arrow }), _jsx("span", { style: { fg: pal().primary }, children: _jsx("b", { children: balanceHeader().title }) }), _jsx("span", { style: { fg: pal().muted }, children: balanceHeader().divider }), _jsx("span", { children: " " + balanceHeader().summary })] }), _jsx(Show, { when: balanceOpen(), children: balanceDetails().map((detail) => (_jsx("text", { fg: pal().muted, children: justify(formatBalanceDetailLabel(detail) + ":", formatBalanceDetailValue(detail)) }))) })] }), _jsxs(Show, { when: balanceDetails().length === 0, children: [_jsx("text", { fg: pal().muted, children: sep() }), _jsx("text", { fg: pal().text, children: justify(t("balTotal"), formatBalanceText(balanceState().data, balanceCurrency(), exchangeRate())) })] })] })] })] })] })] })] }));
|
|
861
963
|
}
|
|
862
964
|
// ---------------------------------------------------------------------------
|
|
863
965
|
// Plugin entry
|