opencode-visual-cache 1.4.0 → 1.5.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 +28 -1
- package/README_EN.md +28 -1
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/balance-providers.d.ts +27 -0
- package/dist/balance-providers.js +131 -0
- package/dist/index.js +200 -64
- package/dist/tui.js +296 -74
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/balance-providers.ts +153 -0
- package/src/index.tsx +222 -73
package/src/index.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
TuiSlotPlugin,
|
|
9
9
|
TuiPluginModule,
|
|
10
10
|
TuiThemeCurrent,
|
|
11
|
+
TuiDialogStack,
|
|
11
12
|
} from "@opencode-ai/plugin/tui"
|
|
12
13
|
import type { UserMessage, AssistantMessage, Message } from "@opencode-ai/sdk"
|
|
13
14
|
import type {
|
|
@@ -19,6 +20,7 @@ import type {
|
|
|
19
20
|
} from "@opencode-ai/sdk/v2"
|
|
20
21
|
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show, untrack } from "solid-js"
|
|
21
22
|
import { PLUGIN_VERSION } from "./_version"
|
|
23
|
+
import { balanceProviders, getBalanceProvider, maskKey, matchBalanceProvider, type BalanceEntry, type BalanceProvider } from "./balance-providers"
|
|
22
24
|
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
24
26
|
// Helpers
|
|
@@ -114,7 +116,7 @@ const ZH_T = {
|
|
|
114
116
|
secModel: "模型",
|
|
115
117
|
secSkills: "已加载技能",
|
|
116
118
|
balTotal: "总余额:",
|
|
117
|
-
balNoKey: "未配置 API Key",
|
|
119
|
+
balNoKey: "未配置 {p} API Key",
|
|
118
120
|
balLoading: "查询中...",
|
|
119
121
|
balError: "查询失败",
|
|
120
122
|
balErr401: "API Key 无效",
|
|
@@ -154,7 +156,7 @@ const EN_T = {
|
|
|
154
156
|
secModel: "Model",
|
|
155
157
|
secSkills: "Loaded Skills",
|
|
156
158
|
balTotal: "Total:",
|
|
157
|
-
balNoKey: "
|
|
159
|
+
balNoKey: "{p} API Key not set",
|
|
158
160
|
balLoading: "Fetching...",
|
|
159
161
|
balError: "Fetch failed",
|
|
160
162
|
balErr401: "Invalid API Key",
|
|
@@ -348,17 +350,12 @@ interface TokenDist {
|
|
|
348
350
|
}
|
|
349
351
|
|
|
350
352
|
// ---------------------------------------------------------------------------
|
|
351
|
-
//
|
|
353
|
+
// Balance state
|
|
352
354
|
// ---------------------------------------------------------------------------
|
|
353
355
|
|
|
354
|
-
interface DeepSeekBalance {
|
|
355
|
-
currency: string
|
|
356
|
-
total: string
|
|
357
|
-
}
|
|
358
|
-
|
|
359
356
|
interface BalanceState {
|
|
360
357
|
status: "idle" | "loading" | "ok" | "error"
|
|
361
|
-
data:
|
|
358
|
+
data: BalanceEntry[] | null
|
|
362
359
|
lastFetch: number
|
|
363
360
|
error?: string
|
|
364
361
|
key?: string // 上次成功/尝试查询所用的 key,用于检测 key 是否更换
|
|
@@ -366,28 +363,6 @@ interface BalanceState {
|
|
|
366
363
|
|
|
367
364
|
const BALANCE_POLL_MS = 5 * 60 * 1000 // 5 minutes
|
|
368
365
|
|
|
369
|
-
async function fetchDeepSeekBalance(apiKey: string, signal?: AbortSignal): Promise<DeepSeekBalance[]> {
|
|
370
|
-
const res = await fetch("https://api.deepseek.com/user/balance", {
|
|
371
|
-
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
372
|
-
signal,
|
|
373
|
-
})
|
|
374
|
-
if (!res.ok) {
|
|
375
|
-
if (res.status === 401) throw new Error("401")
|
|
376
|
-
if (res.status === 402 || res.status === 403) throw new Error("403")
|
|
377
|
-
throw new Error(String(res.status))
|
|
378
|
-
}
|
|
379
|
-
const json = await res.json() as {
|
|
380
|
-
is_available?: boolean
|
|
381
|
-
balance_infos?: { currency: string; total_balance: string; granted_balance: string; topped_up_balance: string }[]
|
|
382
|
-
}
|
|
383
|
-
const infos = json.balance_infos ?? []
|
|
384
|
-
if (infos.length === 0) throw new Error("EMPTY")
|
|
385
|
-
return infos.map((info) => ({
|
|
386
|
-
currency: info.currency ?? "CNY",
|
|
387
|
-
total: info.total_balance ?? "0",
|
|
388
|
-
}))
|
|
389
|
-
}
|
|
390
|
-
|
|
391
366
|
/**
|
|
392
367
|
* 将余额从来源币种换算为目标币种。
|
|
393
368
|
* DEFAULT_RATES 以 USD=1 为基准:先折算为 USD,再换算到目标币种。
|
|
@@ -399,6 +374,27 @@ function convertBalance(target: string, targetRate: number, amount: number, from
|
|
|
399
374
|
return target === "USD" ? usd : usd * targetRate
|
|
400
375
|
}
|
|
401
376
|
|
|
377
|
+
/**
|
|
378
|
+
* 从 OpenCode 已认证的 provider 读取 API key 作为余额查询的自动兜底。
|
|
379
|
+
* 匹配复用前缀逻辑:先精确匹配 id,再前缀匹配(如 moonshotai-cn → moonshot)。
|
|
380
|
+
* key 来源:auth.json(provider.key)或配置(provider.options.apiKey)。
|
|
381
|
+
* 仅当手动配置的 key 缺失时使用;读取失败或未匹配返回空串。
|
|
382
|
+
*/
|
|
383
|
+
function findOpencodeKey(api: TuiPluginApi, provider: BalanceProvider): string {
|
|
384
|
+
try {
|
|
385
|
+
const provs = api.state.provider as unknown as Array<{ id: string; key?: string; options?: { apiKey?: string } }>
|
|
386
|
+
// 大小写不敏感:精确匹配 id,否则前缀匹配(如 moonshotai-cn → moonshot)
|
|
387
|
+
const id = provider.id.toLowerCase()
|
|
388
|
+
const hit = provs.find((p) => p.id.toLowerCase() === id) ?? provs.find((p) => p.id.toLowerCase().startsWith(id))
|
|
389
|
+
if (!hit) return ""
|
|
390
|
+
const k = typeof hit.key === "string" ? hit.key : ""
|
|
391
|
+
if (k) return k
|
|
392
|
+
return typeof hit.options?.apiKey === "string" ? hit.options.apiKey : ""
|
|
393
|
+
} catch {
|
|
394
|
+
return ""
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
402
398
|
/** 货币符号:优先取 /cache-currency 内置映射,未知币种回退为代码。 */
|
|
403
399
|
function balanceSymbol(currency: string): string {
|
|
404
400
|
const sym = CURRENCIES[currency]
|
|
@@ -429,9 +425,15 @@ interface PanelSignals {
|
|
|
429
425
|
setSectionSkills: (v: boolean) => void
|
|
430
426
|
sectionBalance: () => boolean
|
|
431
427
|
setSectionBalance: (v: boolean) => void
|
|
432
|
-
/** Increment to force a
|
|
428
|
+
/** Increment to force a balance re-fetch. */
|
|
433
429
|
balanceRefresh: () => number
|
|
434
430
|
setBalanceRefresh: (v: number) => void
|
|
431
|
+
/** Currently selected balance provider id (e.g. "deepseek"). */
|
|
432
|
+
balanceProviderId: () => string
|
|
433
|
+
setBalanceProviderId: (v: string) => void
|
|
434
|
+
/** Auto-switch to the session's provider for balance display. Manual switch disables it. */
|
|
435
|
+
autoBalance: () => boolean
|
|
436
|
+
setAutoBalance: (v: boolean) => void
|
|
435
437
|
/** Preferred currency code for balance display (CNY / USD / …). Empty = first entry. */
|
|
436
438
|
balanceCurrency: () => string
|
|
437
439
|
setBalanceCurrency: (v: string) => void
|
|
@@ -488,6 +490,8 @@ function TokenCachePanel(props: {
|
|
|
488
490
|
sectionSkills, setSectionSkills,
|
|
489
491
|
sectionBalance, setSectionBalance,
|
|
490
492
|
balanceRefresh,
|
|
493
|
+
balanceProviderId, setBalanceProviderId,
|
|
494
|
+
autoBalance, setAutoBalance,
|
|
491
495
|
balanceCurrency, setBalanceCurrency,
|
|
492
496
|
borderVisible, setBorderVisible,
|
|
493
497
|
} = props.signals
|
|
@@ -529,8 +533,14 @@ function TokenCachePanel(props: {
|
|
|
529
533
|
// 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
|
|
530
534
|
let balanceSeq = 0
|
|
531
535
|
|
|
536
|
+
// 当前 provider 显示名
|
|
537
|
+
const providerName = createMemo(() => getBalanceProvider(balanceProviderId()).name)
|
|
538
|
+
|
|
532
539
|
const pollBalance = async () => {
|
|
533
|
-
const
|
|
540
|
+
const provider = getBalanceProvider(balanceProviderId())
|
|
541
|
+
// 手动配置的 key 优先;缺失时自动复用 OpenCode 已认证的 key(auth.json / config)
|
|
542
|
+
const key = props.api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
543
|
+
|| findOpencodeKey(props.api, provider)
|
|
534
544
|
if (!key) { setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined }); return }
|
|
535
545
|
const now = Date.now()
|
|
536
546
|
const prev = balanceState()
|
|
@@ -542,7 +552,7 @@ function TokenCachePanel(props: {
|
|
|
542
552
|
let timedOut = false
|
|
543
553
|
const timer = setTimeout(() => { timedOut = true; controller.abort() }, 10_000)
|
|
544
554
|
try {
|
|
545
|
-
const data = await
|
|
555
|
+
const data = await provider.fetchBalance(key, controller.signal)
|
|
546
556
|
clearTimeout(timer)
|
|
547
557
|
if (seq !== balanceSeq) return // 已被更新的请求取代,丢弃过期结果
|
|
548
558
|
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key })
|
|
@@ -564,6 +574,37 @@ function TokenCachePanel(props: {
|
|
|
564
574
|
untrack(() => { void pollBalance() })
|
|
565
575
|
})
|
|
566
576
|
|
|
577
|
+
// 自动切换当前会话的 provider(前缀匹配)。手动切换会关闭此行为。
|
|
578
|
+
// 直接追踪 messages 取最后一条 assistant 消息的 providerID——
|
|
579
|
+
// 不依赖 session.model 的响应式更新(模型切换时该链路可能不触发重算)。
|
|
580
|
+
createEffect(() => {
|
|
581
|
+
if (!autoBalance()) return
|
|
582
|
+
const sid = props.signals.overrideSessionId() ?? props.sessionId
|
|
583
|
+
const msgs = props.api.state.session.messages(sid) as Message[]
|
|
584
|
+
let pid = ""
|
|
585
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
586
|
+
const m = msgs[i]
|
|
587
|
+
if (m.role === "assistant" && (m as AssistantMessage).providerID) {
|
|
588
|
+
pid = (m as AssistantMessage).providerID
|
|
589
|
+
break
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// 会话尚无 assistant 消息(新会话 / 刚切换模型未对话 / 消息未加载)
|
|
593
|
+
// → 回退到会话级模型元数据,反映当前正在使用的 provider
|
|
594
|
+
if (!pid) {
|
|
595
|
+
try {
|
|
596
|
+
const session = props.api.state.session.get(sid)
|
|
597
|
+
pid = session?.model?.providerID ?? ""
|
|
598
|
+
} catch { /* ignore */ }
|
|
599
|
+
}
|
|
600
|
+
if (!pid) return
|
|
601
|
+
const hit = matchBalanceProvider(pid)
|
|
602
|
+
if (hit && hit.id !== balanceProviderId()) {
|
|
603
|
+
setBalanceProviderId(hit.id)
|
|
604
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1)
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
|
|
567
608
|
// ── auto-clear override when the user navigates to a different main session ──
|
|
568
609
|
let lastMainSid = props.sessionId
|
|
569
610
|
createEffect(() => {
|
|
@@ -763,6 +804,23 @@ function TokenCachePanel(props: {
|
|
|
763
804
|
if (typeof rate === "number" && rate > 0) setExchangeRate(rate)
|
|
764
805
|
const balCur = props.api.kv.get<string>(`${KV_PREFIX}.balance_currency`)
|
|
765
806
|
if (typeof balCur === "string") setBalanceCurrency(balCur)
|
|
807
|
+
// Restore balance provider (fall back to default when unknown)
|
|
808
|
+
const savedProvider = props.api.kv.get<string>(`${KV_PREFIX}.balance.provider`)
|
|
809
|
+
if (typeof savedProvider === "string" && balanceProviders.some((p) => p.id === savedProvider)) {
|
|
810
|
+
setBalanceProviderId(savedProvider)
|
|
811
|
+
}
|
|
812
|
+
// Restore auto-switch (default on)
|
|
813
|
+
const savedAuto = props.api.kv.get<boolean>(`${KV_PREFIX}.balance.auto`)
|
|
814
|
+
if (typeof savedAuto === "boolean") setAutoBalance(savedAuto)
|
|
815
|
+
// Migrate legacy DeepSeek key (cache_panel.ds_key → cache_panel.balance.deepseek.key)
|
|
816
|
+
const legacyKey = props.api.kv.get<string>(`${KV_PREFIX}.ds_key`, "")
|
|
817
|
+
if (legacyKey) {
|
|
818
|
+
const dsKey = props.api.kv.get<string>(`${KV_PREFIX}.balance.deepseek.key`, "")
|
|
819
|
+
if (!dsKey) props.api.kv.set(`${KV_PREFIX}.balance.deepseek.key`, legacyKey)
|
|
820
|
+
props.api.kv.set(`${KV_PREFIX}.ds_key`, "")
|
|
821
|
+
}
|
|
822
|
+
// 恢复的 provider 可能与默认值不同,强制重新查询
|
|
823
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1)
|
|
766
824
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)))
|
|
767
825
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)))
|
|
768
826
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)))
|
|
@@ -1115,7 +1173,7 @@ function TokenCachePanel(props: {
|
|
|
1115
1173
|
<Show when={balanceState().status === "idle"}>
|
|
1116
1174
|
<text fg={pal().muted}>
|
|
1117
1175
|
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1118
|
-
<span>{t().balNoKey}</span>
|
|
1176
|
+
<span>{t().balNoKey.replace("{p}", providerName())}</span>
|
|
1119
1177
|
</text>
|
|
1120
1178
|
</Show>
|
|
1121
1179
|
<Show when={balanceState().status === "loading"}>
|
|
@@ -1214,6 +1272,8 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1214
1272
|
const [sectionSkills, setSectionSkills] = createSignal(true)
|
|
1215
1273
|
const [sectionBalance, setSectionBalance] = createSignal(true)
|
|
1216
1274
|
const [balanceRefresh, setBalanceRefresh] = createSignal(0)
|
|
1275
|
+
const [balanceProviderId, setBalanceProviderId] = createSignal("deepseek")
|
|
1276
|
+
const [autoBalance, setAutoBalance] = createSignal(true)
|
|
1217
1277
|
const [balanceCurrency, setBalanceCurrency] = createSignal("")
|
|
1218
1278
|
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
1219
1279
|
const [langZH, setLangZH] = createSignal(LANG_ZH)
|
|
@@ -1229,6 +1289,8 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1229
1289
|
sectionSkills, setSectionSkills,
|
|
1230
1290
|
sectionBalance, setSectionBalance,
|
|
1231
1291
|
balanceRefresh, setBalanceRefresh,
|
|
1292
|
+
balanceProviderId, setBalanceProviderId,
|
|
1293
|
+
autoBalance, setAutoBalance,
|
|
1232
1294
|
balanceCurrency, setBalanceCurrency,
|
|
1233
1295
|
borderVisible, setBorderVisible,
|
|
1234
1296
|
overrideSessionId, setOverrideSessionId,
|
|
@@ -1238,6 +1300,55 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1238
1300
|
|
|
1239
1301
|
// ── slash commands for runtime config ──
|
|
1240
1302
|
const KV_PREFIX = "cache_panel"
|
|
1303
|
+
|
|
1304
|
+
/** 菜单中 provider 选项标题:标注 key 来源(手动配置 / OpenCode 自动复用 / 未配置)。 */
|
|
1305
|
+
const providerOptionTitle = (p: BalanceProvider, current?: string) => {
|
|
1306
|
+
const zh = langZH()
|
|
1307
|
+
const hasManual = !!api.kv.get<string>(`${KV_PREFIX}.balance.${p.id}.key`, "")
|
|
1308
|
+
const hasAuto = !hasManual && !!findOpencodeKey(api, p)
|
|
1309
|
+
const mark = hasManual
|
|
1310
|
+
? (zh ? "(用户 key)" : " (user key)")
|
|
1311
|
+
: hasAuto
|
|
1312
|
+
? (zh ? "(OpenCode)" : " (OpenCode)")
|
|
1313
|
+
: (zh ? "(未配置)" : " (not set)")
|
|
1314
|
+
return p.name + mark + (current && p.id === current ? " *" : "")
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/** 弹出指定 provider 的 API Key 输入框(脱敏预填;空清除 / 含 * 保留原 key / 新 key 实时刷新)。 */
|
|
1318
|
+
const promptBalanceKey = (dialog: TuiDialogStack | undefined, provider: BalanceProvider) => {
|
|
1319
|
+
const zh = langZH()
|
|
1320
|
+
const current = api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
1321
|
+
const masked = maskKey(current)
|
|
1322
|
+
dialog?.replace(() => (
|
|
1323
|
+
<api.ui.DialogPrompt
|
|
1324
|
+
title={provider.name}
|
|
1325
|
+
description={() => <text>{zh ? `输入 ${provider.name} API Key 以显示账户余额(留空清除)` : `Enter your ${provider.name} API key to show account balance (leave empty to clear)`}</text>}
|
|
1326
|
+
placeholder={provider.keyPlaceholder ?? "sk-..."}
|
|
1327
|
+
value={masked}
|
|
1328
|
+
onConfirm={(val) => {
|
|
1329
|
+
const input = val.trim()
|
|
1330
|
+
let key: string
|
|
1331
|
+
if (input === "") {
|
|
1332
|
+
key = ""
|
|
1333
|
+
} else if (input.includes("*")) {
|
|
1334
|
+
key = current
|
|
1335
|
+
} else {
|
|
1336
|
+
key = input
|
|
1337
|
+
}
|
|
1338
|
+
api.kv.set(`${KV_PREFIX}.balance.${provider.id}.key`, key)
|
|
1339
|
+
setBalanceRefresh(v => v + 1)
|
|
1340
|
+
if (key) {
|
|
1341
|
+
api.ui.toast({ message: zh ? "API Key 已保存,正在查询余额..." : "API Key saved, fetching balance..." })
|
|
1342
|
+
} else {
|
|
1343
|
+
api.ui.toast({ message: zh ? "API Key 已清除" : "API Key cleared" })
|
|
1344
|
+
}
|
|
1345
|
+
dialog?.clear()
|
|
1346
|
+
}}
|
|
1347
|
+
onCancel={() => dialog?.clear()}
|
|
1348
|
+
/>
|
|
1349
|
+
))
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1241
1352
|
api.command?.register(() => [
|
|
1242
1353
|
{
|
|
1243
1354
|
title: "Cache: Set Currency",
|
|
@@ -1314,7 +1425,7 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1314
1425
|
{ title: `Model & Pricing [${modelOn ? "ON" : "OFF"}]`, value: "model" },
|
|
1315
1426
|
{ title: `Token Dist. [${distOn ? "ON" : "OFF"}]`, value: "dist" },
|
|
1316
1427
|
{ title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`, value: "skills" },
|
|
1317
|
-
{ title: `
|
|
1428
|
+
{ title: `Balance [${balanceOn ? "ON" : "OFF"}]`, value: "balance" },
|
|
1318
1429
|
{ title: `Panel Border [${borderOn ? "ON" : "OFF"}]`, value: "border" },
|
|
1319
1430
|
]}
|
|
1320
1431
|
onSelect={(opt) => {
|
|
@@ -1387,49 +1498,87 @@ const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
|
1387
1498
|
},
|
|
1388
1499
|
},
|
|
1389
1500
|
{
|
|
1390
|
-
title: "Cache:
|
|
1391
|
-
value: "cache.balance
|
|
1392
|
-
description: "
|
|
1393
|
-
slash: { name: "cache-balance
|
|
1501
|
+
title: "Cache: Switch Balance Provider",
|
|
1502
|
+
value: "cache.balance",
|
|
1503
|
+
description: "切换余额提供商 / 自动切换当前会话提供商 | Switch balance provider / auto-switch session provider",
|
|
1504
|
+
slash: { name: "cache-balance" },
|
|
1394
1505
|
onSelect: (dialog) => {
|
|
1395
1506
|
const zh = langZH()
|
|
1396
|
-
const current =
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
const body = prefix ? k.slice(3) : k
|
|
1402
|
-
if (body.length <= 10) return prefix + body.slice(0, 5) + "*".repeat(Math.max(3, body.length - 5))
|
|
1403
|
-
return prefix + body.slice(0, 5) + "*".repeat(Math.max(3, body.length - 10)) + body.slice(-5)
|
|
1404
|
-
}
|
|
1405
|
-
const masked = maskKey(current)
|
|
1507
|
+
const current = signals.balanceProviderId()
|
|
1508
|
+
const auto = signals.autoBalance()
|
|
1509
|
+
const autoLabel = auto
|
|
1510
|
+
? (zh ? "自动切换提供商 [开]" : "Auto-switch provider [ON]")
|
|
1511
|
+
: (zh ? "自动切换提供商 [关]" : "Auto-switch provider [OFF]")
|
|
1406
1512
|
dialog?.replace(() => (
|
|
1407
|
-
<api.ui.
|
|
1408
|
-
title={zh ? "
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
api.ui.toast({ message: zh ? "API Key 已保存,正在查询余额..." : "API Key saved, fetching balance..." })
|
|
1513
|
+
<api.ui.DialogSelect
|
|
1514
|
+
title={zh ? "余额提供商 / 自动切换" : "Balance Provider / Auto-switch"}
|
|
1515
|
+
options={[
|
|
1516
|
+
{
|
|
1517
|
+
title: autoLabel,
|
|
1518
|
+
value: "__auto__",
|
|
1519
|
+
},
|
|
1520
|
+
...balanceProviders.map((p) => ({
|
|
1521
|
+
title: providerOptionTitle(p, current),
|
|
1522
|
+
value: p.id,
|
|
1523
|
+
})),
|
|
1524
|
+
]}
|
|
1525
|
+
onSelect={(opt) => {
|
|
1526
|
+
if (opt.value === "__auto__") {
|
|
1527
|
+
const next = !auto
|
|
1528
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, next)
|
|
1529
|
+
signals.setAutoBalance(next)
|
|
1530
|
+
api.ui.toast({ message: zh ? `自动切换余额提供商: ${next ? "开" : "关"}` : `Auto-switch balance provider: ${next ? "ON" : "OFF"}` })
|
|
1531
|
+
dialog?.clear()
|
|
1427
1532
|
} else {
|
|
1428
|
-
|
|
1533
|
+
const provider = getBalanceProvider(opt.value)
|
|
1534
|
+
// 手动切换会关闭自动切换
|
|
1535
|
+
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id)
|
|
1536
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, false)
|
|
1537
|
+
signals.setBalanceProviderId(provider.id)
|
|
1538
|
+
signals.setAutoBalance(false)
|
|
1539
|
+
// 切换后立即按新 provider 刷新显示(无 key 时显示 idle,避免残留上一 provider 余额)
|
|
1540
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
1541
|
+
const hasKey = !!api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
1542
|
+
if (!hasKey) {
|
|
1543
|
+
// 未配置 key → 进入设置流程(对话框保持打开等待输入)
|
|
1544
|
+
promptBalanceKey(dialog, provider)
|
|
1545
|
+
} else {
|
|
1546
|
+
api.ui.toast({ message: zh ? `余额提供商: ${provider.name}(自动切换已关闭)` : `Balance provider: ${provider.name} (auto-switch off)` })
|
|
1547
|
+
dialog?.clear()
|
|
1548
|
+
}
|
|
1429
1549
|
}
|
|
1430
|
-
dialog?.clear()
|
|
1431
1550
|
}}
|
|
1432
|
-
|
|
1551
|
+
/>
|
|
1552
|
+
))
|
|
1553
|
+
},
|
|
1554
|
+
},
|
|
1555
|
+
{
|
|
1556
|
+
title: "Cache: Set Balance API Key",
|
|
1557
|
+
value: "cache.balance.key",
|
|
1558
|
+
description: "Select a provider and set its API key for balance display",
|
|
1559
|
+
slash: { name: "cache-balance-key" },
|
|
1560
|
+
onSelect: (dialog) => {
|
|
1561
|
+
const zh = langZH()
|
|
1562
|
+
// 步骤 1:选择 provider
|
|
1563
|
+
dialog?.replace(() => (
|
|
1564
|
+
<api.ui.DialogSelect
|
|
1565
|
+
title={zh ? "选择余额提供商" : "Select Balance Provider"}
|
|
1566
|
+
options={balanceProviders.map((p) => ({
|
|
1567
|
+
title: providerOptionTitle(p),
|
|
1568
|
+
value: p.id,
|
|
1569
|
+
}))}
|
|
1570
|
+
onSelect={(opt) => {
|
|
1571
|
+
const provider = getBalanceProvider(opt.value)
|
|
1572
|
+
// 手动指定 provider 会关闭自动切换
|
|
1573
|
+
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id)
|
|
1574
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, false)
|
|
1575
|
+
signals.setBalanceProviderId(provider.id)
|
|
1576
|
+
signals.setAutoBalance(false)
|
|
1577
|
+
// 切换后立即刷新显示(防止取消输入时残留上一 provider 的余额)
|
|
1578
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
1579
|
+
// 步骤 2:输入 key
|
|
1580
|
+
promptBalanceKey(dialog, provider)
|
|
1581
|
+
}}
|
|
1433
1582
|
/>
|
|
1434
1583
|
))
|
|
1435
1584
|
},
|