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/dist/tui.js
CHANGED
|
@@ -13,7 +13,109 @@ import { createElement as _$createElement } from "@opentui/solid";
|
|
|
13
13
|
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show, untrack } from "solid-js";
|
|
14
14
|
|
|
15
15
|
// src/_version.ts
|
|
16
|
-
var PLUGIN_VERSION = "1.
|
|
16
|
+
var PLUGIN_VERSION = "1.5.0";
|
|
17
|
+
|
|
18
|
+
// src/balance-providers.ts
|
|
19
|
+
var BalanceError = class extends Error {
|
|
20
|
+
};
|
|
21
|
+
var siliconflowProvider = {
|
|
22
|
+
id: "siliconflow",
|
|
23
|
+
name: "SiliconFlow",
|
|
24
|
+
keyPlaceholder: "sk-...",
|
|
25
|
+
async fetchBalance(apiKey, signal) {
|
|
26
|
+
const res = await fetch("https://api.siliconflow.cn/v1/user/info", {
|
|
27
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
28
|
+
signal
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
if (res.status === 401) throw new BalanceError("401");
|
|
32
|
+
if (res.status === 403) throw new BalanceError("403");
|
|
33
|
+
throw new BalanceError(String(res.status));
|
|
34
|
+
}
|
|
35
|
+
const json = await res.json();
|
|
36
|
+
const total = json.data?.totalBalance ?? json.data?.balance;
|
|
37
|
+
if (typeof total === "undefined" || total === null) throw new BalanceError("EMPTY");
|
|
38
|
+
return [{ currency: "CNY", total: String(total) }];
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var deepseekProvider = {
|
|
42
|
+
id: "deepseek",
|
|
43
|
+
name: "DeepSeek",
|
|
44
|
+
keyPlaceholder: "sk-...",
|
|
45
|
+
async fetchBalance(apiKey, signal) {
|
|
46
|
+
const res = await fetch("https://api.deepseek.com/user/balance", {
|
|
47
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
48
|
+
signal
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
if (res.status === 401) throw new BalanceError("401");
|
|
52
|
+
if (res.status === 402 || res.status === 403) throw new BalanceError("403");
|
|
53
|
+
throw new BalanceError(String(res.status));
|
|
54
|
+
}
|
|
55
|
+
const json = await res.json();
|
|
56
|
+
const infos = json.balance_infos ?? [];
|
|
57
|
+
if (infos.length === 0) throw new BalanceError("EMPTY");
|
|
58
|
+
return infos.map((info) => ({
|
|
59
|
+
currency: info.currency ?? "CNY",
|
|
60
|
+
total: info.total_balance ?? "0"
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var openrouterProvider = {
|
|
65
|
+
id: "openrouter",
|
|
66
|
+
name: "OpenRouter",
|
|
67
|
+
keyPlaceholder: "sk-or-...",
|
|
68
|
+
async fetchBalance(apiKey, signal) {
|
|
69
|
+
const res = await fetch("https://openrouter.ai/api/v1/credits", {
|
|
70
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
71
|
+
signal
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
if (res.status === 401 || res.status === 403) throw new BalanceError("403");
|
|
75
|
+
throw new BalanceError(String(res.status));
|
|
76
|
+
}
|
|
77
|
+
const json = await res.json();
|
|
78
|
+
const credits = json.data?.total_credits;
|
|
79
|
+
const usage = json.data?.total_usage;
|
|
80
|
+
if (typeof credits !== "number" || typeof usage !== "number") throw new BalanceError("EMPTY");
|
|
81
|
+
return [{ currency: "USD", total: (credits - usage).toFixed(2) }];
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var moonshotProvider = {
|
|
85
|
+
id: "moonshot",
|
|
86
|
+
name: "Moonshot",
|
|
87
|
+
keyPlaceholder: "sk-...",
|
|
88
|
+
async fetchBalance(apiKey, signal) {
|
|
89
|
+
const res = await fetch("https://api.moonshot.cn/v1/users/me/balance", {
|
|
90
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
91
|
+
signal
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
if (res.status === 401) throw new BalanceError("401");
|
|
95
|
+
if (res.status === 403) throw new BalanceError("403");
|
|
96
|
+
throw new BalanceError(String(res.status));
|
|
97
|
+
}
|
|
98
|
+
const json = await res.json();
|
|
99
|
+
const balance = json.data?.available_balance;
|
|
100
|
+
if (typeof balance === "undefined" || balance === null) throw new BalanceError("EMPTY");
|
|
101
|
+
return [{ currency: "CNY", total: String(balance) }];
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var balanceProviders = [deepseekProvider, siliconflowProvider, openrouterProvider, moonshotProvider];
|
|
105
|
+
function getBalanceProvider(id) {
|
|
106
|
+
return balanceProviders.find((p) => p.id === id) ?? balanceProviders[0] ?? deepseekProvider;
|
|
107
|
+
}
|
|
108
|
+
function matchBalanceProvider(providerId) {
|
|
109
|
+
const id = providerId.toLowerCase();
|
|
110
|
+
const exact = balanceProviders.find((p) => p.id.toLowerCase() === id);
|
|
111
|
+
if (exact) return exact;
|
|
112
|
+
return balanceProviders.find((p) => id.startsWith(p.id.toLowerCase()));
|
|
113
|
+
}
|
|
114
|
+
function maskKey(k) {
|
|
115
|
+
if (!k) return "";
|
|
116
|
+
if (k.length <= 10) return k.slice(0, 5) + "*".repeat(Math.max(3, k.length - 5));
|
|
117
|
+
return k.slice(0, 5) + "*".repeat(Math.max(3, k.length - 10)) + k.slice(-5);
|
|
118
|
+
}
|
|
17
119
|
|
|
18
120
|
// src/index.tsx
|
|
19
121
|
function charColumns(c) {
|
|
@@ -91,7 +193,7 @@ var ZH_T = {
|
|
|
91
193
|
secModel: "\u6A21\u578B",
|
|
92
194
|
secSkills: "\u5DF2\u52A0\u8F7D\u6280\u80FD",
|
|
93
195
|
balTotal: "\u603B\u4F59\u989D:",
|
|
94
|
-
balNoKey: "\u672A\u914D\u7F6E API Key",
|
|
196
|
+
balNoKey: "\u672A\u914D\u7F6E {p} API Key",
|
|
95
197
|
balLoading: "\u67E5\u8BE2\u4E2D...",
|
|
96
198
|
balError: "\u67E5\u8BE2\u5931\u8D25",
|
|
97
199
|
balErr401: "API Key \u65E0\u6548",
|
|
@@ -130,7 +232,7 @@ var EN_T = {
|
|
|
130
232
|
secModel: "Model",
|
|
131
233
|
secSkills: "Loaded Skills",
|
|
132
234
|
balTotal: "Total:",
|
|
133
|
-
balNoKey: "
|
|
235
|
+
balNoKey: "{p} API Key not set",
|
|
134
236
|
balLoading: "Fetching...",
|
|
135
237
|
balError: "Fetch failed",
|
|
136
238
|
balErr401: "Invalid API Key",
|
|
@@ -249,33 +351,25 @@ function estimateTokens(text) {
|
|
|
249
351
|
return Math.max(1, Math.ceil(ascii / asciiPerToken + cjk / 1));
|
|
250
352
|
}
|
|
251
353
|
var BALANCE_POLL_MS = 5 * 60 * 1e3;
|
|
252
|
-
async function fetchDeepSeekBalance(apiKey, signal) {
|
|
253
|
-
const res = await fetch("https://api.deepseek.com/user/balance", {
|
|
254
|
-
headers: {
|
|
255
|
-
Authorization: `Bearer ${apiKey}`,
|
|
256
|
-
Accept: "application/json"
|
|
257
|
-
},
|
|
258
|
-
signal
|
|
259
|
-
});
|
|
260
|
-
if (!res.ok) {
|
|
261
|
-
if (res.status === 401) throw new Error("401");
|
|
262
|
-
if (res.status === 402 || res.status === 403) throw new Error("403");
|
|
263
|
-
throw new Error(String(res.status));
|
|
264
|
-
}
|
|
265
|
-
const json = await res.json();
|
|
266
|
-
const infos = json.balance_infos ?? [];
|
|
267
|
-
if (infos.length === 0) throw new Error("EMPTY");
|
|
268
|
-
return infos.map((info) => ({
|
|
269
|
-
currency: info.currency ?? "CNY",
|
|
270
|
-
total: info.total_balance ?? "0"
|
|
271
|
-
}));
|
|
272
|
-
}
|
|
273
354
|
function convertBalance(target, targetRate, amount, from) {
|
|
274
355
|
if (from === target) return amount;
|
|
275
356
|
const fromRate = DEFAULT_RATES[from] ?? 1;
|
|
276
357
|
const usd = from === "USD" ? amount : amount / fromRate;
|
|
277
358
|
return target === "USD" ? usd : usd * targetRate;
|
|
278
359
|
}
|
|
360
|
+
function findOpencodeKey(api, provider) {
|
|
361
|
+
try {
|
|
362
|
+
const provs = api.state.provider;
|
|
363
|
+
const id = provider.id.toLowerCase();
|
|
364
|
+
const hit = provs.find((p) => p.id.toLowerCase() === id) ?? provs.find((p) => p.id.toLowerCase().startsWith(id));
|
|
365
|
+
if (!hit) return "";
|
|
366
|
+
const k = typeof hit.key === "string" ? hit.key : "";
|
|
367
|
+
if (k) return k;
|
|
368
|
+
return typeof hit.options?.apiKey === "string" ? hit.options.apiKey : "";
|
|
369
|
+
} catch {
|
|
370
|
+
return "";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
279
373
|
function balanceSymbol(currency) {
|
|
280
374
|
const sym = CURRENCIES[currency];
|
|
281
375
|
return sym ?? currency + " ";
|
|
@@ -330,6 +424,10 @@ function TokenCachePanel(props) {
|
|
|
330
424
|
sectionBalance,
|
|
331
425
|
setSectionBalance,
|
|
332
426
|
balanceRefresh,
|
|
427
|
+
balanceProviderId,
|
|
428
|
+
setBalanceProviderId,
|
|
429
|
+
autoBalance,
|
|
430
|
+
setAutoBalance,
|
|
333
431
|
balanceCurrency,
|
|
334
432
|
setBalanceCurrency,
|
|
335
433
|
borderVisible,
|
|
@@ -388,8 +486,10 @@ function TokenCachePanel(props) {
|
|
|
388
486
|
lastFetch: 0
|
|
389
487
|
});
|
|
390
488
|
let balanceSeq = 0;
|
|
489
|
+
const providerName = createMemo(() => getBalanceProvider(balanceProviderId()).name);
|
|
391
490
|
const pollBalance = async () => {
|
|
392
|
-
const
|
|
491
|
+
const provider = getBalanceProvider(balanceProviderId());
|
|
492
|
+
const key = props.api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "") || findOpencodeKey(props.api, provider);
|
|
393
493
|
if (!key) {
|
|
394
494
|
setBalanceState({
|
|
395
495
|
status: "idle",
|
|
@@ -417,7 +517,7 @@ function TokenCachePanel(props) {
|
|
|
417
517
|
controller.abort();
|
|
418
518
|
}, 1e4);
|
|
419
519
|
try {
|
|
420
|
-
const data2 = await
|
|
520
|
+
const data2 = await provider.fetchBalance(key, controller.signal);
|
|
421
521
|
clearTimeout(timer);
|
|
422
522
|
if (seq !== balanceSeq) return;
|
|
423
523
|
setBalanceState({
|
|
@@ -446,6 +546,32 @@ function TokenCachePanel(props) {
|
|
|
446
546
|
void pollBalance();
|
|
447
547
|
});
|
|
448
548
|
});
|
|
549
|
+
createEffect(() => {
|
|
550
|
+
if (!autoBalance()) return;
|
|
551
|
+
const sid = props.signals.overrideSessionId() ?? props.sessionId;
|
|
552
|
+
const msgs = props.api.state.session.messages(sid);
|
|
553
|
+
let pid = "";
|
|
554
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
555
|
+
const m = msgs[i];
|
|
556
|
+
if (m.role === "assistant" && m.providerID) {
|
|
557
|
+
pid = m.providerID;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (!pid) {
|
|
562
|
+
try {
|
|
563
|
+
const session = props.api.state.session.get(sid);
|
|
564
|
+
pid = session?.model?.providerID ?? "";
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!pid) return;
|
|
569
|
+
const hit = matchBalanceProvider(pid);
|
|
570
|
+
if (hit && hit.id !== balanceProviderId()) {
|
|
571
|
+
setBalanceProviderId(hit.id);
|
|
572
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
449
575
|
let lastMainSid = props.sessionId;
|
|
450
576
|
createEffect(() => {
|
|
451
577
|
const sid = props.sessionId;
|
|
@@ -512,7 +638,7 @@ function TokenCachePanel(props) {
|
|
|
512
638
|
const freshTotal = input + read, sessionHitRate = freshTotal > 0 ? read / freshTotal * 100 : 0;
|
|
513
639
|
const model = mid.split("/").pop() ?? mid, hasPricing = inputRate > 0 || cacheReadRate > 0 || cacheWriteRate > 0;
|
|
514
640
|
const hasTrendData = prevMsgHitRate >= 0 && lastMsgHitRate >= 0;
|
|
515
|
-
const trend = hasTrendData ? lastMsgHitRate - prevMsgHitRate : 0,
|
|
641
|
+
const trend = hasTrendData ? lastMsgHitRate - prevMsgHitRate : 0, providerName2 = pid || "";
|
|
516
642
|
const distData = untrack(() => {
|
|
517
643
|
let dist = {
|
|
518
644
|
system: 0,
|
|
@@ -637,7 +763,7 @@ function TokenCachePanel(props) {
|
|
|
637
763
|
hasData: read > 0 || write > 0 || input > 0 || output > 0 || cost > 0,
|
|
638
764
|
trend,
|
|
639
765
|
hasTrendData,
|
|
640
|
-
providerName,
|
|
766
|
+
providerName: providerName2,
|
|
641
767
|
sessionHitRate,
|
|
642
768
|
dist: distData.finalDist,
|
|
643
769
|
hasDistData: distData.finalHasDist,
|
|
@@ -689,6 +815,19 @@ function TokenCachePanel(props) {
|
|
|
689
815
|
if (typeof rate === "number" && rate > 0) setExchangeRate(rate);
|
|
690
816
|
const balCur = props.api.kv.get(`${KV_PREFIX}.balance_currency`);
|
|
691
817
|
if (typeof balCur === "string") setBalanceCurrency(balCur);
|
|
818
|
+
const savedProvider = props.api.kv.get(`${KV_PREFIX}.balance.provider`);
|
|
819
|
+
if (typeof savedProvider === "string" && balanceProviders.some((p) => p.id === savedProvider)) {
|
|
820
|
+
setBalanceProviderId(savedProvider);
|
|
821
|
+
}
|
|
822
|
+
const savedAuto = props.api.kv.get(`${KV_PREFIX}.balance.auto`);
|
|
823
|
+
if (typeof savedAuto === "boolean") setAutoBalance(savedAuto);
|
|
824
|
+
const legacyKey = props.api.kv.get(`${KV_PREFIX}.ds_key`, "");
|
|
825
|
+
if (legacyKey) {
|
|
826
|
+
const dsKey = props.api.kv.get(`${KV_PREFIX}.balance.deepseek.key`, "");
|
|
827
|
+
if (!dsKey) props.api.kv.set(`${KV_PREFIX}.balance.deepseek.key`, legacyKey);
|
|
828
|
+
props.api.kv.set(`${KV_PREFIX}.ds_key`, "");
|
|
829
|
+
}
|
|
830
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1);
|
|
692
831
|
setSectionDetail(Boolean(props.api.kv.get(`${KV_PREFIX}.section.detail`, true)));
|
|
693
832
|
setSectionModel(Boolean(props.api.kv.get(`${KV_PREFIX}.section.model`, true)));
|
|
694
833
|
setSectionDist(Boolean(props.api.kv.get(`${KV_PREFIX}.section.dist`, true)));
|
|
@@ -1428,7 +1567,7 @@ function TokenCachePanel(props) {
|
|
|
1428
1567
|
_$insertNode(_el$52, _el$53);
|
|
1429
1568
|
_$insertNode(_el$52, _el$55);
|
|
1430
1569
|
_$insertNode(_el$53, _$createTextNode(`> `));
|
|
1431
|
-
_$insert(_el$55, () => t().balNoKey);
|
|
1570
|
+
_$insert(_el$55, () => t().balNoKey.replace("{p}", providerName()));
|
|
1432
1571
|
_$effect((_p$) => {
|
|
1433
1572
|
var _v$9 = pal().muted, _v$0 = {
|
|
1434
1573
|
fg: pal().muted
|
|
@@ -1585,6 +1724,8 @@ var tui = async (api) => {
|
|
|
1585
1724
|
const [sectionSkills, setSectionSkills] = createSignal(true);
|
|
1586
1725
|
const [sectionBalance, setSectionBalance] = createSignal(true);
|
|
1587
1726
|
const [balanceRefresh, setBalanceRefresh] = createSignal(0);
|
|
1727
|
+
const [balanceProviderId, setBalanceProviderId] = createSignal("deepseek");
|
|
1728
|
+
const [autoBalance, setAutoBalance] = createSignal(true);
|
|
1588
1729
|
const [balanceCurrency, setBalanceCurrency] = createSignal("");
|
|
1589
1730
|
const [borderVisible, setBorderVisible] = createSignal(true);
|
|
1590
1731
|
const [langZH, setLangZH] = createSignal(LANG_ZH);
|
|
@@ -1608,6 +1749,10 @@ var tui = async (api) => {
|
|
|
1608
1749
|
setSectionBalance,
|
|
1609
1750
|
balanceRefresh,
|
|
1610
1751
|
setBalanceRefresh,
|
|
1752
|
+
balanceProviderId,
|
|
1753
|
+
setBalanceProviderId,
|
|
1754
|
+
autoBalance,
|
|
1755
|
+
setAutoBalance,
|
|
1611
1756
|
balanceCurrency,
|
|
1612
1757
|
setBalanceCurrency,
|
|
1613
1758
|
borderVisible,
|
|
@@ -1617,6 +1762,56 @@ var tui = async (api) => {
|
|
|
1617
1762
|
};
|
|
1618
1763
|
api.slots.register(createSidebarSlot(api, signals));
|
|
1619
1764
|
const KV_PREFIX = "cache_panel";
|
|
1765
|
+
const providerOptionTitle = (p, current) => {
|
|
1766
|
+
const zh = langZH();
|
|
1767
|
+
const hasManual = !!api.kv.get(`${KV_PREFIX}.balance.${p.id}.key`, "");
|
|
1768
|
+
const hasAuto = !hasManual && !!findOpencodeKey(api, p);
|
|
1769
|
+
const mark = hasManual ? zh ? "\uFF08\u7528\u6237 key\uFF09" : " (user key)" : hasAuto ? zh ? "\uFF08OpenCode\uFF09" : " (OpenCode)" : zh ? "\uFF08\u672A\u914D\u7F6E\uFF09" : " (not set)";
|
|
1770
|
+
return p.name + mark + (current && p.id === current ? " *" : "");
|
|
1771
|
+
};
|
|
1772
|
+
const promptBalanceKey = (dialog, provider) => {
|
|
1773
|
+
const zh = langZH();
|
|
1774
|
+
const current = api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "");
|
|
1775
|
+
const masked = maskKey(current);
|
|
1776
|
+
dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
|
|
1777
|
+
get title() {
|
|
1778
|
+
return provider.name;
|
|
1779
|
+
},
|
|
1780
|
+
description: () => (() => {
|
|
1781
|
+
var _el$93 = _$createElement("text");
|
|
1782
|
+
_$insert(_el$93, () => zh ? `\u8F93\u5165 ${provider.name} API Key \u4EE5\u663E\u793A\u8D26\u6237\u4F59\u989D\uFF08\u7559\u7A7A\u6E05\u9664\uFF09` : `Enter your ${provider.name} API key to show account balance (leave empty to clear)`);
|
|
1783
|
+
return _el$93;
|
|
1784
|
+
})(),
|
|
1785
|
+
get placeholder() {
|
|
1786
|
+
return provider.keyPlaceholder ?? "sk-...";
|
|
1787
|
+
},
|
|
1788
|
+
value: masked,
|
|
1789
|
+
onConfirm: (val) => {
|
|
1790
|
+
const input = val.trim();
|
|
1791
|
+
let key;
|
|
1792
|
+
if (input === "") {
|
|
1793
|
+
key = "";
|
|
1794
|
+
} else if (input.includes("*")) {
|
|
1795
|
+
key = current;
|
|
1796
|
+
} else {
|
|
1797
|
+
key = input;
|
|
1798
|
+
}
|
|
1799
|
+
api.kv.set(`${KV_PREFIX}.balance.${provider.id}.key`, key);
|
|
1800
|
+
setBalanceRefresh((v) => v + 1);
|
|
1801
|
+
if (key) {
|
|
1802
|
+
api.ui.toast({
|
|
1803
|
+
message: zh ? "API Key \u5DF2\u4FDD\u5B58\uFF0C\u6B63\u5728\u67E5\u8BE2\u4F59\u989D..." : "API Key saved, fetching balance..."
|
|
1804
|
+
});
|
|
1805
|
+
} else {
|
|
1806
|
+
api.ui.toast({
|
|
1807
|
+
message: zh ? "API Key \u5DF2\u6E05\u9664" : "API Key cleared"
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
dialog?.clear();
|
|
1811
|
+
},
|
|
1812
|
+
onCancel: () => dialog?.clear()
|
|
1813
|
+
}));
|
|
1814
|
+
};
|
|
1620
1815
|
api.command?.register(() => [{
|
|
1621
1816
|
title: "Cache: Set Currency",
|
|
1622
1817
|
value: "cache.currency",
|
|
@@ -1660,9 +1855,9 @@ var tui = async (api) => {
|
|
|
1660
1855
|
dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
|
|
1661
1856
|
title: "Exchange Rate",
|
|
1662
1857
|
description: () => (() => {
|
|
1663
|
-
var _el$
|
|
1664
|
-
_$insertNode(_el$
|
|
1665
|
-
return _el$
|
|
1858
|
+
var _el$94 = _$createElement("text");
|
|
1859
|
+
_$insertNode(_el$94, _$createTextNode(`Enter the exchange rate from USD to your currency (e.g. 7.2 for CNY)`));
|
|
1860
|
+
return _el$94;
|
|
1666
1861
|
})(),
|
|
1667
1862
|
placeholder: "1.0",
|
|
1668
1863
|
get value() {
|
|
@@ -1710,7 +1905,7 @@ var tui = async (api) => {
|
|
|
1710
1905
|
title: `Loaded Skills [${skillsOn ? "ON" : "OFF"}]`,
|
|
1711
1906
|
value: "skills"
|
|
1712
1907
|
}, {
|
|
1713
|
-
title: `
|
|
1908
|
+
title: `Balance [${balanceOn ? "ON" : "OFF"}]`,
|
|
1714
1909
|
value: "balance"
|
|
1715
1910
|
}, {
|
|
1716
1911
|
title: `Panel Border [${borderOn ? "ON" : "OFF"}]`,
|
|
@@ -1793,56 +1988,83 @@ var tui = async (api) => {
|
|
|
1793
1988
|
}));
|
|
1794
1989
|
}
|
|
1795
1990
|
}, {
|
|
1796
|
-
title: "Cache:
|
|
1797
|
-
value: "cache.balance
|
|
1798
|
-
description: "
|
|
1991
|
+
title: "Cache: Switch Balance Provider",
|
|
1992
|
+
value: "cache.balance",
|
|
1993
|
+
description: "\u5207\u6362\u4F59\u989D\u63D0\u4F9B\u5546 / \u81EA\u52A8\u5207\u6362\u5F53\u524D\u4F1A\u8BDD\u63D0\u4F9B\u5546 | Switch balance provider / auto-switch session provider",
|
|
1799
1994
|
slash: {
|
|
1800
|
-
name: "cache-balance
|
|
1995
|
+
name: "cache-balance"
|
|
1801
1996
|
},
|
|
1802
1997
|
onSelect: (dialog) => {
|
|
1803
1998
|
const zh = langZH();
|
|
1804
|
-
const current =
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
const input = val.trim();
|
|
1824
|
-
let key;
|
|
1825
|
-
if (input === "") {
|
|
1826
|
-
key = "";
|
|
1827
|
-
} else if (input.includes("*")) {
|
|
1828
|
-
key = current;
|
|
1829
|
-
} else {
|
|
1830
|
-
key = input;
|
|
1831
|
-
}
|
|
1832
|
-
api.kv.set(`${KV_PREFIX}.ds_key`, key);
|
|
1833
|
-
setBalanceRefresh((v) => v + 1);
|
|
1834
|
-
if (key) {
|
|
1999
|
+
const current = signals.balanceProviderId();
|
|
2000
|
+
const auto = signals.autoBalance();
|
|
2001
|
+
const autoLabel = auto ? zh ? "\u81EA\u52A8\u5207\u6362\u63D0\u4F9B\u5546 [\u5F00]" : "Auto-switch provider [ON]" : zh ? "\u81EA\u52A8\u5207\u6362\u63D0\u4F9B\u5546 [\u5173]" : "Auto-switch provider [OFF]";
|
|
2002
|
+
dialog?.replace(() => _$createComponent(api.ui.DialogSelect, {
|
|
2003
|
+
title: zh ? "\u4F59\u989D\u63D0\u4F9B\u5546 / \u81EA\u52A8\u5207\u6362" : "Balance Provider / Auto-switch",
|
|
2004
|
+
get options() {
|
|
2005
|
+
return [{
|
|
2006
|
+
title: autoLabel,
|
|
2007
|
+
value: "__auto__"
|
|
2008
|
+
}, ...balanceProviders.map((p) => ({
|
|
2009
|
+
title: providerOptionTitle(p, current),
|
|
2010
|
+
value: p.id
|
|
2011
|
+
}))];
|
|
2012
|
+
},
|
|
2013
|
+
onSelect: (opt) => {
|
|
2014
|
+
if (opt.value === "__auto__") {
|
|
2015
|
+
const next = !auto;
|
|
2016
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, next);
|
|
2017
|
+
signals.setAutoBalance(next);
|
|
1835
2018
|
api.ui.toast({
|
|
1836
|
-
message: zh ?
|
|
2019
|
+
message: zh ? `\u81EA\u52A8\u5207\u6362\u4F59\u989D\u63D0\u4F9B\u5546: ${next ? "\u5F00" : "\u5173"}` : `Auto-switch balance provider: ${next ? "ON" : "OFF"}`
|
|
1837
2020
|
});
|
|
2021
|
+
dialog?.clear();
|
|
1838
2022
|
} else {
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
});
|
|
2023
|
+
const provider = getBalanceProvider(opt.value);
|
|
2024
|
+
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id);
|
|
2025
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, false);
|
|
2026
|
+
signals.setBalanceProviderId(provider.id);
|
|
2027
|
+
signals.setAutoBalance(false);
|
|
2028
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1);
|
|
2029
|
+
const hasKey = !!api.kv.get(`${KV_PREFIX}.balance.${provider.id}.key`, "");
|
|
2030
|
+
if (!hasKey) {
|
|
2031
|
+
promptBalanceKey(dialog, provider);
|
|
2032
|
+
} else {
|
|
2033
|
+
api.ui.toast({
|
|
2034
|
+
message: zh ? `\u4F59\u989D\u63D0\u4F9B\u5546: ${provider.name}\uFF08\u81EA\u52A8\u5207\u6362\u5DF2\u5173\u95ED\uFF09` : `Balance provider: ${provider.name} (auto-switch off)`
|
|
2035
|
+
});
|
|
2036
|
+
dialog?.clear();
|
|
2037
|
+
}
|
|
1842
2038
|
}
|
|
1843
|
-
|
|
2039
|
+
}
|
|
2040
|
+
}));
|
|
2041
|
+
}
|
|
2042
|
+
}, {
|
|
2043
|
+
title: "Cache: Set Balance API Key",
|
|
2044
|
+
value: "cache.balance.key",
|
|
2045
|
+
description: "Select a provider and set its API key for balance display",
|
|
2046
|
+
slash: {
|
|
2047
|
+
name: "cache-balance-key"
|
|
2048
|
+
},
|
|
2049
|
+
onSelect: (dialog) => {
|
|
2050
|
+
const zh = langZH();
|
|
2051
|
+
dialog?.replace(() => _$createComponent(api.ui.DialogSelect, {
|
|
2052
|
+
title: zh ? "\u9009\u62E9\u4F59\u989D\u63D0\u4F9B\u5546" : "Select Balance Provider",
|
|
2053
|
+
get options() {
|
|
2054
|
+
return balanceProviders.map((p) => ({
|
|
2055
|
+
title: providerOptionTitle(p),
|
|
2056
|
+
value: p.id
|
|
2057
|
+
}));
|
|
1844
2058
|
},
|
|
1845
|
-
|
|
2059
|
+
onSelect: (opt) => {
|
|
2060
|
+
const provider = getBalanceProvider(opt.value);
|
|
2061
|
+
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id);
|
|
2062
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, false);
|
|
2063
|
+
signals.setBalanceProviderId(provider.id);
|
|
2064
|
+
signals.setAutoBalance(false);
|
|
2065
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1);
|
|
2066
|
+
promptBalanceKey(dialog, provider);
|
|
2067
|
+
}
|
|
1846
2068
|
}));
|
|
1847
2069
|
}
|
|
1848
2070
|
}, {
|
package/package.json
CHANGED
package/src/_version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// auto-generated
|
|
2
|
-
export const PLUGIN_VERSION="1.
|
|
2
|
+
export const PLUGIN_VERSION="1.5.0";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Balance providers — pluggable account-balance query adapters.
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
/** 归一化后的余额条目——显示层与具体 provider 解耦。 */
|
|
6
|
+
export interface BalanceEntry {
|
|
7
|
+
currency: string // 原生币种(CNY/USD…),复用现有汇率换算
|
|
8
|
+
total: string // 余额字符串
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** provider 统一错误:message 即错误码(401/403/EMPTY/…),显示层直接展示。 */
|
|
12
|
+
export class BalanceError extends Error {}
|
|
13
|
+
|
|
14
|
+
/** 可插拔的余额 provider 适配器。 */
|
|
15
|
+
export interface BalanceProvider {
|
|
16
|
+
id: string // 唯一标识,同时用作 KV key 命名空间
|
|
17
|
+
name: string // 显示名(专有名词,无需 i18n)
|
|
18
|
+
keyPlaceholder?: string // key 输入框占位(如 "sk-...")
|
|
19
|
+
fetchBalance(apiKey: string, signal?: AbortSignal): Promise<BalanceEntry[]>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const siliconflowProvider: BalanceProvider = {
|
|
23
|
+
id: "siliconflow",
|
|
24
|
+
name: "SiliconFlow",
|
|
25
|
+
keyPlaceholder: "sk-...",
|
|
26
|
+
async fetchBalance(apiKey, signal) {
|
|
27
|
+
// 国内站 api.siliconflow.cn(CNY);国际站为 api.siliconflow.com(USD)
|
|
28
|
+
const res = await fetch("https://api.siliconflow.cn/v1/user/info", {
|
|
29
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
30
|
+
signal,
|
|
31
|
+
})
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
if (res.status === 401) throw new BalanceError("401")
|
|
34
|
+
if (res.status === 403) throw new BalanceError("403")
|
|
35
|
+
throw new BalanceError(String(res.status))
|
|
36
|
+
}
|
|
37
|
+
const json = await res.json() as {
|
|
38
|
+
status?: boolean
|
|
39
|
+
data?: {
|
|
40
|
+
balance?: string | number
|
|
41
|
+
chargeBalance?: string | number
|
|
42
|
+
totalBalance?: string | number
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// totalBalance 为总余额(含充值+赠送),缺失时回退 balance
|
|
46
|
+
const total = json.data?.totalBalance ?? json.data?.balance
|
|
47
|
+
if (typeof total === "undefined" || total === null) throw new BalanceError("EMPTY")
|
|
48
|
+
return [{ currency: "CNY", total: String(total) }]
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const deepseekProvider: BalanceProvider = {
|
|
53
|
+
id: "deepseek",
|
|
54
|
+
name: "DeepSeek",
|
|
55
|
+
keyPlaceholder: "sk-...",
|
|
56
|
+
async fetchBalance(apiKey, signal) {
|
|
57
|
+
const res = await fetch("https://api.deepseek.com/user/balance", {
|
|
58
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
59
|
+
signal,
|
|
60
|
+
})
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
if (res.status === 401) throw new BalanceError("401")
|
|
63
|
+
if (res.status === 402 || res.status === 403) throw new BalanceError("403")
|
|
64
|
+
throw new BalanceError(String(res.status))
|
|
65
|
+
}
|
|
66
|
+
const json = await res.json() as {
|
|
67
|
+
is_available?: boolean
|
|
68
|
+
balance_infos?: { currency: string; total_balance: string; granted_balance: string; topped_up_balance: string }[]
|
|
69
|
+
}
|
|
70
|
+
const infos = json.balance_infos ?? []
|
|
71
|
+
if (infos.length === 0) throw new BalanceError("EMPTY")
|
|
72
|
+
return infos.map((info) => ({
|
|
73
|
+
currency: info.currency ?? "CNY",
|
|
74
|
+
total: info.total_balance ?? "0",
|
|
75
|
+
}))
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const openrouterProvider: BalanceProvider = {
|
|
80
|
+
id: "openrouter",
|
|
81
|
+
name: "OpenRouter",
|
|
82
|
+
keyPlaceholder: "sk-or-...",
|
|
83
|
+
async fetchBalance(apiKey, signal) {
|
|
84
|
+
// 官方文档标注需 Management key,实测普通 API key 亦可查询账户余额
|
|
85
|
+
const res = await fetch("https://openrouter.ai/api/v1/credits", {
|
|
86
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
87
|
+
signal,
|
|
88
|
+
})
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
if (res.status === 401 || res.status === 403) throw new BalanceError("403")
|
|
91
|
+
throw new BalanceError(String(res.status))
|
|
92
|
+
}
|
|
93
|
+
const json = await res.json() as {
|
|
94
|
+
data?: { total_credits?: number; total_usage?: number }
|
|
95
|
+
}
|
|
96
|
+
const credits = json.data?.total_credits
|
|
97
|
+
const usage = json.data?.total_usage
|
|
98
|
+
if (typeof credits !== "number" || typeof usage !== "number") throw new BalanceError("EMPTY")
|
|
99
|
+
// 剩余额度 = 充值总额 - 已用
|
|
100
|
+
return [{ currency: "USD", total: (credits - usage).toFixed(2) }]
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const moonshotProvider: BalanceProvider = {
|
|
105
|
+
id: "moonshot",
|
|
106
|
+
name: "Moonshot",
|
|
107
|
+
keyPlaceholder: "sk-...",
|
|
108
|
+
async fetchBalance(apiKey, signal) {
|
|
109
|
+
// 国内站 api.moonshot.cn(CNY);国际站 api.moonshot.ai(USD)
|
|
110
|
+
const res = await fetch("https://api.moonshot.cn/v1/users/me/balance", {
|
|
111
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
112
|
+
signal,
|
|
113
|
+
})
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
if (res.status === 401) throw new BalanceError("401")
|
|
116
|
+
if (res.status === 403) throw new BalanceError("403")
|
|
117
|
+
throw new BalanceError(String(res.status))
|
|
118
|
+
}
|
|
119
|
+
const json = await res.json() as {
|
|
120
|
+
data?: { available_balance?: string | number }
|
|
121
|
+
}
|
|
122
|
+
const balance = json.data?.available_balance
|
|
123
|
+
if (typeof balance === "undefined" || balance === null) throw new BalanceError("EMPTY")
|
|
124
|
+
return [{ currency: "CNY", total: String(balance) }]
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 已注册的 provider 列表(按需追加新适配器)。 */
|
|
129
|
+
export const balanceProviders: BalanceProvider[] = [deepseekProvider, siliconflowProvider, openrouterProvider, moonshotProvider]
|
|
130
|
+
|
|
131
|
+
/** 按 id 取 provider;未知 id 回退到第一个。 */
|
|
132
|
+
export function getBalanceProvider(id: string): BalanceProvider {
|
|
133
|
+
return balanceProviders.find((p) => p.id === id) ?? balanceProviders[0] ?? deepseekProvider
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 按 OpenCode providerID 匹配余额 provider。
|
|
138
|
+
* 先精确匹配,再按前缀匹配(如 moonshotai-cn → moonshot);未命中返回 undefined。
|
|
139
|
+
* 比较不区分大小写,容忍 providerID 的大小写变体。
|
|
140
|
+
*/
|
|
141
|
+
export function matchBalanceProvider(providerId: string): BalanceProvider | undefined {
|
|
142
|
+
const id = providerId.toLowerCase()
|
|
143
|
+
const exact = balanceProviders.find((p) => p.id.toLowerCase() === id)
|
|
144
|
+
if (exact) return exact
|
|
145
|
+
return balanceProviders.find((p) => id.startsWith(p.id.toLowerCase()))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** key 脱敏:保留头 5 尾 5 字符,中间用 * 填充。 */
|
|
149
|
+
export function maskKey(k: string): string {
|
|
150
|
+
if (!k) return ""
|
|
151
|
+
if (k.length <= 10) return k.slice(0, 5) + "*".repeat(Math.max(3, k.length - 5))
|
|
152
|
+
return k.slice(0, 5) + "*".repeat(Math.max(3, k.length - 10)) + k.slice(-5)
|
|
153
|
+
}
|