opencode-visual-cache 1.3.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 +32 -2
- package/README_EN.md +32 -2
- 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 +332 -4
- package/dist/tui.js +631 -98
- package/package.json +1 -1
- package/src/_version.ts +1 -1
- package/src/balance-providers.ts +153 -0
- package/src/index.tsx +402 -2
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
|
+
}
|