dsh-opencode-routing 0.0.0-stage → 2.1.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/LICENSE +21 -0
- package/README.md +122 -2
- package/client.js +1640 -0
- package/cordis.patch.yml +93 -0
- package/icon.svg +8 -0
- package/index.js +33 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -4
- package/router.js +1027 -0
- package/test.mjs +370 -0
- package/usage.js +749 -0
package/client.js
ADDED
|
@@ -0,0 +1,1640 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-opencode-routing',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react');
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
|
|
7
|
+
const USAGE_API = '/opencode-usage.json';
|
|
8
|
+
const ROUTER_API = '/opencode-quota-router.json';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* UI strings. `zh` is the source; every other locale is a translation of the
|
|
12
|
+
* same keys. Missing keys fall back to `en`, then to the key itself, so a
|
|
13
|
+
* half-finished translation never breaks the page.
|
|
14
|
+
*/
|
|
15
|
+
const DICT = {
|
|
16
|
+
zh: {
|
|
17
|
+
title: 'OpenCode 账户与用量',
|
|
18
|
+
refresh: '刷新',
|
|
19
|
+
refreshing: '刷新中…',
|
|
20
|
+
updatedAt: '更新于 {time}',
|
|
21
|
+
loadError: '读取失败:{message}',
|
|
22
|
+
routerTitle: '额度路由',
|
|
23
|
+
modePrimary: '正常路由',
|
|
24
|
+
modeFallback: '已回退到官方订阅',
|
|
25
|
+
modeReason: '原因:{reason}',
|
|
26
|
+
activeAccount: '当前账户',
|
|
27
|
+
nextProbe: '下次探测',
|
|
28
|
+
probeAll: '立即探测',
|
|
29
|
+
resetMarks: '重置封禁标记',
|
|
30
|
+
resetHint: '清空所有“额度用尽 / 鉴权失败”的标记,并让路由立刻回到 OpenCode 账户池;不改账户,也不改密钥。',
|
|
31
|
+
routerMissing: '未检测到额度路由,账户池只读。',
|
|
32
|
+
accountsTitle: '账户池',
|
|
33
|
+
accountsOrder: '按顺序使用:前面的账户用完才用后面的。',
|
|
34
|
+
addAccount: '添加账户',
|
|
35
|
+
labelPlaceholder: '备注(可选,例如 小号 A)',
|
|
36
|
+
keyPlaceholder: 'oc_sk_…',
|
|
37
|
+
addAndProbe: '添加并探测',
|
|
38
|
+
save: '保存',
|
|
39
|
+
cancel: '取消',
|
|
40
|
+
setActive: '设为当前',
|
|
41
|
+
disable: '停用',
|
|
42
|
+
enable: '启用',
|
|
43
|
+
editKey: '改密钥',
|
|
44
|
+
remove: '删除',
|
|
45
|
+
viewUsage: '查看用量',
|
|
46
|
+
keyUpdated: '密钥已更新',
|
|
47
|
+
accountAdded: '账户已添加并探测',
|
|
48
|
+
shortKey: '密钥不完整:至少 8 个字符',
|
|
49
|
+
keepOne: '至少保留一个账户',
|
|
50
|
+
notConfigured: '未配置',
|
|
51
|
+
readonly: '只读',
|
|
52
|
+
newKeyFor: '新密钥写入 {ref}',
|
|
53
|
+
noAccounts: '还没有账户。填一个密钥就能开始。',
|
|
54
|
+
statusReady: '可用',
|
|
55
|
+
statusExhausted: '额度用尽',
|
|
56
|
+
statusAuth: '鉴权失败',
|
|
57
|
+
statusDisabled: '已停用',
|
|
58
|
+
statusMissing: '未配置密钥',
|
|
59
|
+
statusError: '读取失败',
|
|
60
|
+
statusActive: '当前使用',
|
|
61
|
+
statusSpare: '备用',
|
|
62
|
+
statusCurrent: '当前',
|
|
63
|
+
colIndex: '#',
|
|
64
|
+
colAccount: '账户',
|
|
65
|
+
colKey: '密钥',
|
|
66
|
+
colRolling: '5 小时',
|
|
67
|
+
colWeekly: '本周',
|
|
68
|
+
colMonthly: '本月',
|
|
69
|
+
colStatus: '状态',
|
|
70
|
+
colActions: '操作',
|
|
71
|
+
colModel: '模型',
|
|
72
|
+
colCaps: '能力',
|
|
73
|
+
colRequests: '请求',
|
|
74
|
+
colTokens: '输入 / 输出 tokens',
|
|
75
|
+
colRemaining: '本月剩余',
|
|
76
|
+
colMonthlyUsed: '本月用量',
|
|
77
|
+
colWeeklyUsed: '本周用量',
|
|
78
|
+
modelsTitle: '按模型(本机估算)',
|
|
79
|
+
capsText: '纯文本',
|
|
80
|
+
capsImage: '图像',
|
|
81
|
+
capsVideo: '视频',
|
|
82
|
+
capsAudio: '音频',
|
|
83
|
+
capsPdf: 'PDF',
|
|
84
|
+
capsTools: '工具',
|
|
85
|
+
capsReasoning: '推理',
|
|
86
|
+
capsUnknown: '未声明',
|
|
87
|
+
free: '免费',
|
|
88
|
+
unlimited: '无限',
|
|
89
|
+
none: '—',
|
|
90
|
+
dialogTitle: '{label} 的用量',
|
|
91
|
+
dialogNote: '官方接口只给这三个窗口的百分比,没有按模型的明细;按模型的金额只能是本机估算。',
|
|
92
|
+
dialogProbe: '重新探测',
|
|
93
|
+
close: '关闭',
|
|
94
|
+
probedAt: '探测于 {time}',
|
|
95
|
+
resetsAt: '重置于 {time}',
|
|
96
|
+
usageError: '错误:{message}',
|
|
97
|
+
peakTitle: '此刻用哪个更划算',
|
|
98
|
+
peakPlan: '你为 OpenCode 的 {plan} 套餐付 ¥{price} /月(${usd}),额度给到 ¥{allowance}(${allowanceUsd})的官方价用量。额度按官方价扣,等于打 {discount} 折——同一笔请求走 OpenCode 你只付官方价的 {percent}%。',
|
|
99
|
+
peakNow: '走官方直连照官方价付现。此刻 OpenCode 是{goPeak},官方是{officialPeak}{holiday}。',
|
|
100
|
+
peakGoPeak: '峰段(额度烧 2 倍)',
|
|
101
|
+
peakGoOff: '谷段',
|
|
102
|
+
peakOfficialPeak: '峰段(价格 2 倍)',
|
|
103
|
+
peakOfficialOff: '谷段',
|
|
104
|
+
peakAtPeak: '峰段',
|
|
105
|
+
peakOffPeak: '谷段',
|
|
106
|
+
peakHoliday: '——今天是中国法定节假日,官方整天算谷,OpenCode 照算峰',
|
|
107
|
+
peakExhausted: '但本月额度已经用尽,只能走官方。',
|
|
108
|
+
peakGoPay: '走 OpenCode 你实付',
|
|
109
|
+
peakOfficialPay: '走官方直连你实付',
|
|
110
|
+
peakVerdict: '结论',
|
|
111
|
+
peakGoCheaper: '走 OpenCode 便宜 {percent}%',
|
|
112
|
+
peakOfficialCheaper: '走官方直连便宜 {percent}%',
|
|
113
|
+
peakFootnote: '按每 100 万输出 tokens 折算(编码账单主要来自输出),汇率 ¥{fx}/$。峰谷只影响额度烧多快,不改变结论:额度内走 OpenCode 始终比官方直连便宜;例外是额度用尽,或某个模型的额度先见底。',
|
|
114
|
+
footerOfficial: '上面「官方 5 小时 / 本周 / 本月」百分比来自 OpenCode 接口,是账户真实用量,别的客户端或别的机器用了也会算进去。',
|
|
115
|
+
footerLocal: '按模型的金额是本机估算:只统计这个 harness 的会话日志,用来拆开看模型分布;其它工具的调用没有本地记录。',
|
|
116
|
+
footerPeakRule: '金额按离峰价估算,DeepSeek 模型峰时按 2 倍;免费模型不计费。',
|
|
117
|
+
},
|
|
118
|
+
en: {
|
|
119
|
+
title: 'OpenCode accounts & usage',
|
|
120
|
+
refresh: 'Refresh',
|
|
121
|
+
refreshing: 'Refreshing…',
|
|
122
|
+
updatedAt: 'Updated {time}',
|
|
123
|
+
loadError: 'Could not load: {message}',
|
|
124
|
+
routerTitle: 'Quota routing',
|
|
125
|
+
modePrimary: 'Routing normally',
|
|
126
|
+
modeFallback: 'Fell back to the official subscription',
|
|
127
|
+
modeReason: 'Reason: {reason}',
|
|
128
|
+
activeAccount: 'Active account',
|
|
129
|
+
nextProbe: 'Next probe',
|
|
130
|
+
probeAll: 'Probe now',
|
|
131
|
+
resetMarks: 'Clear blocked marks',
|
|
132
|
+
resetHint: 'Clears every "out of allowance / auth failed" mark and sends routing straight back to the OpenCode pool. Accounts and keys are untouched.',
|
|
133
|
+
routerMissing: 'No quota router detected — the account pool is read-only.',
|
|
134
|
+
accountsTitle: 'Account pool',
|
|
135
|
+
accountsOrder: 'Used in order: the next account only takes over once the one above runs dry.',
|
|
136
|
+
addAccount: 'Add an account',
|
|
137
|
+
labelPlaceholder: 'Label (optional, e.g. alt A)',
|
|
138
|
+
keyPlaceholder: 'oc_sk_…',
|
|
139
|
+
addAndProbe: 'Add and probe',
|
|
140
|
+
save: 'Save',
|
|
141
|
+
cancel: 'Cancel',
|
|
142
|
+
setActive: 'Make active',
|
|
143
|
+
disable: 'Disable',
|
|
144
|
+
enable: 'Enable',
|
|
145
|
+
editKey: 'Change key',
|
|
146
|
+
remove: 'Delete',
|
|
147
|
+
viewUsage: 'View usage',
|
|
148
|
+
keyUpdated: 'Key updated',
|
|
149
|
+
accountAdded: 'Account added and probed',
|
|
150
|
+
shortKey: 'Key looks incomplete: at least 8 characters',
|
|
151
|
+
keepOne: 'Keep at least one account',
|
|
152
|
+
notConfigured: 'Not set',
|
|
153
|
+
readonly: 'read-only',
|
|
154
|
+
newKeyFor: 'New key is stored in {ref}',
|
|
155
|
+
noAccounts: 'No accounts yet. Add a key to begin.',
|
|
156
|
+
statusReady: 'Ready',
|
|
157
|
+
statusExhausted: 'Out of allowance',
|
|
158
|
+
statusAuth: 'Auth failed',
|
|
159
|
+
statusDisabled: 'Disabled',
|
|
160
|
+
statusMissing: 'No key',
|
|
161
|
+
statusError: 'Probe failed',
|
|
162
|
+
statusActive: 'In use',
|
|
163
|
+
statusSpare: 'Spare',
|
|
164
|
+
statusCurrent: 'current',
|
|
165
|
+
colIndex: '#',
|
|
166
|
+
colAccount: 'Account',
|
|
167
|
+
colKey: 'Key',
|
|
168
|
+
colRolling: '5 hours',
|
|
169
|
+
colWeekly: 'This week',
|
|
170
|
+
colMonthly: 'This month',
|
|
171
|
+
colStatus: 'Status',
|
|
172
|
+
colActions: 'Actions',
|
|
173
|
+
colModel: 'Model',
|
|
174
|
+
colCaps: 'Modality',
|
|
175
|
+
colRequests: 'Requests',
|
|
176
|
+
colTokens: 'Input / output tokens',
|
|
177
|
+
colRemaining: 'Left this month',
|
|
178
|
+
colMonthlyUsed: 'Used this month',
|
|
179
|
+
colWeeklyUsed: 'Used this week',
|
|
180
|
+
modelsTitle: 'Per model (local estimate)',
|
|
181
|
+
capsText: 'text',
|
|
182
|
+
capsImage: 'image',
|
|
183
|
+
capsVideo: 'video',
|
|
184
|
+
capsAudio: 'audio',
|
|
185
|
+
capsPdf: 'PDF',
|
|
186
|
+
capsTools: 'tools',
|
|
187
|
+
capsReasoning: 'reasoning',
|
|
188
|
+
capsUnknown: 'undeclared',
|
|
189
|
+
free: 'free',
|
|
190
|
+
unlimited: 'unlimited',
|
|
191
|
+
none: '—',
|
|
192
|
+
dialogTitle: 'Usage for {label}',
|
|
193
|
+
dialogNote: 'The official endpoint only reports these three windows; it has no per-model breakdown. Per-model money can only be a local estimate.',
|
|
194
|
+
dialogProbe: 'Probe again',
|
|
195
|
+
close: 'Close',
|
|
196
|
+
probedAt: 'Probed {time}',
|
|
197
|
+
resetsAt: 'Resets {time}',
|
|
198
|
+
usageError: 'Error: {message}',
|
|
199
|
+
peakTitle: 'Which route is cheaper right now',
|
|
200
|
+
peakPlan: 'You pay ¥{price}/month (${usd}) for OpenCode {plan} and the plan meters ¥{allowance} (${allowanceUsd}) of usage at official prices — a {discount}× discount, so the same request costs you {percent}% of the official price when it goes through OpenCode.',
|
|
201
|
+
peakNow: 'The official route bills list price directly. Right now OpenCode is {goPeak} and the official API is {officialPeak}{holiday}.',
|
|
202
|
+
peakGoPeak: 'at peak (allowance burns twice as fast)',
|
|
203
|
+
peakGoOff: 'off-peak',
|
|
204
|
+
peakOfficialPeak: 'at peak (price doubles)',
|
|
205
|
+
peakOfficialOff: 'off-peak',
|
|
206
|
+
peakAtPeak: 'peak',
|
|
207
|
+
peakOffPeak: 'off-peak',
|
|
208
|
+
peakHoliday: ' — today is a Chinese public holiday, so the official API charges off-peak all day while OpenCode still charges peak',
|
|
209
|
+
peakExhausted: ' But this month’s allowance is already spent, so only the official route is left.',
|
|
210
|
+
peakGoPay: 'You pay via OpenCode',
|
|
211
|
+
peakOfficialPay: 'You pay via official API',
|
|
212
|
+
peakVerdict: 'Verdict',
|
|
213
|
+
peakGoCheaper: 'OpenCode is {percent}% cheaper',
|
|
214
|
+
peakOfficialCheaper: 'Official API is {percent}% cheaper',
|
|
215
|
+
peakFootnote: 'Per 1M output tokens (output dominates a coding bill), at ¥{fx}/$. Peak hours only change how fast the allowance burns, not the verdict: on allowance, OpenCode stays cheaper than the official API; the exceptions are a spent allowance or a model whose individual cap runs out first.',
|
|
216
|
+
footerOfficial: 'The official 5-hour / weekly / monthly percentages come from OpenCode’s API and describe the whole account — usage from other clients or machines counts too.',
|
|
217
|
+
footerLocal: 'Per-model money is a local estimate: it only counts this harness’s session log, which is what makes the model split visible; calls from other tools leave no local record.',
|
|
218
|
+
footerPeakRule: 'Money is estimated at off-peak prices, doubled during DeepSeek peak hours; free models are not billed.',
|
|
219
|
+
},
|
|
220
|
+
'zh-tw': {
|
|
221
|
+
title: 'OpenCode 帳號與用量',
|
|
222
|
+
refresh: '重新整理',
|
|
223
|
+
refreshing: '重新整理中…',
|
|
224
|
+
updatedAt: '更新於 {time}',
|
|
225
|
+
loadError: '讀取失敗:{message}',
|
|
226
|
+
routerTitle: '額度路由',
|
|
227
|
+
modePrimary: '正常路由',
|
|
228
|
+
modeFallback: '已回退到官方訂閱',
|
|
229
|
+
modeReason: '原因:{reason}',
|
|
230
|
+
activeAccount: '目前帳號',
|
|
231
|
+
nextProbe: '下次探測',
|
|
232
|
+
probeAll: '立即探測',
|
|
233
|
+
resetMarks: '重設封鎖標記',
|
|
234
|
+
resetHint: '清空所有「額度用盡/驗證失敗」標記,並讓路由立刻回到 OpenCode 帳號池;不會改動帳號與金鑰。',
|
|
235
|
+
routerMissing: '未偵測到額度路由,帳號池為唯讀。',
|
|
236
|
+
accountsTitle: '帳號池',
|
|
237
|
+
accountsOrder: '依序使用:前面的帳號用完才會用後面的。',
|
|
238
|
+
addAccount: '新增帳號',
|
|
239
|
+
labelPlaceholder: '備註(可選,例如 小號 A)',
|
|
240
|
+
keyPlaceholder: 'oc_sk_…',
|
|
241
|
+
addAndProbe: '新增並探測',
|
|
242
|
+
save: '儲存',
|
|
243
|
+
cancel: '取消',
|
|
244
|
+
setActive: '設為目前',
|
|
245
|
+
disable: '停用',
|
|
246
|
+
enable: '啟用',
|
|
247
|
+
editKey: '更換金鑰',
|
|
248
|
+
remove: '刪除',
|
|
249
|
+
viewUsage: '查看用量',
|
|
250
|
+
keyUpdated: '金鑰已更新',
|
|
251
|
+
accountAdded: '帳號已新增並探測',
|
|
252
|
+
shortKey: '金鑰不完整:至少 8 個字元',
|
|
253
|
+
keepOne: '至少保留一個帳號',
|
|
254
|
+
notConfigured: '未設定',
|
|
255
|
+
readonly: '唯讀',
|
|
256
|
+
newKeyFor: '新金鑰寫入 {ref}',
|
|
257
|
+
noAccounts: '還沒有帳號。填入金鑰即可開始。',
|
|
258
|
+
statusReady: '可用',
|
|
259
|
+
statusExhausted: '額度用盡',
|
|
260
|
+
statusAuth: '驗證失敗',
|
|
261
|
+
statusDisabled: '已停用',
|
|
262
|
+
statusMissing: '未設定金鑰',
|
|
263
|
+
statusError: '讀取失敗',
|
|
264
|
+
statusActive: '目前使用',
|
|
265
|
+
statusSpare: '備用',
|
|
266
|
+
statusCurrent: '目前',
|
|
267
|
+
colIndex: '#',
|
|
268
|
+
colAccount: '帳號',
|
|
269
|
+
colKey: '金鑰',
|
|
270
|
+
colRolling: '5 小時',
|
|
271
|
+
colWeekly: '本週',
|
|
272
|
+
colMonthly: '本月',
|
|
273
|
+
colStatus: '狀態',
|
|
274
|
+
colActions: '操作',
|
|
275
|
+
colModel: '模型',
|
|
276
|
+
colCaps: '能力',
|
|
277
|
+
colRequests: '請求',
|
|
278
|
+
colTokens: '輸入 / 輸出 tokens',
|
|
279
|
+
colRemaining: '本月剩餘',
|
|
280
|
+
colMonthlyUsed: '本月用量',
|
|
281
|
+
colWeeklyUsed: '本週用量',
|
|
282
|
+
modelsTitle: '依模型(本機估算)',
|
|
283
|
+
capsText: '純文字',
|
|
284
|
+
capsImage: '圖像',
|
|
285
|
+
capsVideo: '影片',
|
|
286
|
+
capsAudio: '音訊',
|
|
287
|
+
capsPdf: 'PDF',
|
|
288
|
+
capsTools: '工具',
|
|
289
|
+
capsReasoning: '推理',
|
|
290
|
+
capsUnknown: '未宣告',
|
|
291
|
+
free: '免費',
|
|
292
|
+
unlimited: '無限',
|
|
293
|
+
none: '—',
|
|
294
|
+
dialogTitle: '{label} 的用量',
|
|
295
|
+
dialogNote: '官方介面只提供這三個窗口的百分比,沒有依模型拆分的明細;依模型的金額只能是本機估算。',
|
|
296
|
+
dialogProbe: '重新探測',
|
|
297
|
+
close: '關閉',
|
|
298
|
+
probedAt: '探測於 {time}',
|
|
299
|
+
resetsAt: '重置於 {time}',
|
|
300
|
+
usageError: '錯誤:{message}',
|
|
301
|
+
peakTitle: '此刻用哪個更划算',
|
|
302
|
+
peakPlan: '你為 OpenCode 的 {plan} 方案付 ¥{price}/月(${usd}),額度給到 ¥{allowance}(${allowanceUsd})的官方價用量。額度依官方價扣,等於打 {discount} 折——同一筆請求走 OpenCode 你只付官方價的 {percent}%。',
|
|
303
|
+
peakNow: '走官方直連照官方價付現。此刻 OpenCode 是{goPeak},官方是{officialPeak}{holiday}。',
|
|
304
|
+
peakGoPeak: '峰段(額度消耗 2 倍)',
|
|
305
|
+
peakGoOff: '谷段',
|
|
306
|
+
peakOfficialPeak: '峰段(價格 2 倍)',
|
|
307
|
+
peakOfficialOff: '谷段',
|
|
308
|
+
peakAtPeak: '峰段',
|
|
309
|
+
peakOffPeak: '谷段',
|
|
310
|
+
peakHoliday: '——今天是中國法定假日,官方整天算谷,OpenCode 照算峰',
|
|
311
|
+
peakExhausted: '但本月額度已用盡,只能走官方。',
|
|
312
|
+
peakGoPay: '走 OpenCode 你實付',
|
|
313
|
+
peakOfficialPay: '走官方直連你實付',
|
|
314
|
+
peakVerdict: '結論',
|
|
315
|
+
peakGoCheaper: '走 OpenCode 便宜 {percent}%',
|
|
316
|
+
peakOfficialCheaper: '走官方直連便宜 {percent}%',
|
|
317
|
+
peakFootnote: '依每 100 萬輸出 tokens 折算(編碼帳單主要來自輸出),匯率 ¥{fx}/$。峰谷只影響額度消耗速度,不改變結論:額度內走 OpenCode 始終比官方直連便宜;例外是額度用盡,或某個模型的額度先用完。',
|
|
318
|
+
footerOfficial: '上面「官方 5 小時/本週/本月」百分比來自 OpenCode 介面,是帳號真實用量,其他用戶端或機器用掉的也會算進去。',
|
|
319
|
+
footerLocal: '依模型的金額是本機估算:只統計這個 harness 的工作階段日誌,用來拆開看模型分布;其他工具的呼叫沒有本機記錄。',
|
|
320
|
+
footerPeakRule: '金額依離峰價估算,DeepSeek 模型峰時按 2 倍;免費模型不計費。',
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
/* @@EXTRA_LOCALES_BEGIN@@ */
|
|
326
|
+
Object.assign(DICT, {
|
|
327
|
+
"de": {
|
|
328
|
+
"title": "OpenCode: Konten und Nutzung",
|
|
329
|
+
"refresh": "Aktualisieren",
|
|
330
|
+
"refreshing": "Wird aktualisiert…",
|
|
331
|
+
"updatedAt": "Aktualisiert {time}",
|
|
332
|
+
"loadError": "Laden fehlgeschlagen: {message}",
|
|
333
|
+
"routerTitle": "Kontingent-Routing",
|
|
334
|
+
"modePrimary": "Normales Routing",
|
|
335
|
+
"modeFallback": "Auf offizielles Abo zurückgefallen",
|
|
336
|
+
"modeReason": "Grund: {reason}",
|
|
337
|
+
"activeAccount": "Aktives Konto",
|
|
338
|
+
"nextProbe": "Nächste Prüfung",
|
|
339
|
+
"probeAll": "Jetzt prüfen",
|
|
340
|
+
"resetMarks": "Sperrmarken löschen",
|
|
341
|
+
"resetHint": "Löscht alle Marken „Kontingent aufgebraucht / Anmeldung fehlgeschlagen“ und schickt das Routing sofort in den OpenCode-Kontenpool zurück. Konten und Schlüssel bleiben unverändert.",
|
|
342
|
+
"routerMissing": "Kein Kontingent-Router gefunden – der Kontenpool ist schreibgeschützt.",
|
|
343
|
+
"accountsTitle": "Kontenpool",
|
|
344
|
+
"accountsOrder": "Wird der Reihe nach genutzt: Das nächste Konto übernimmt erst, wenn das davor leer ist.",
|
|
345
|
+
"addAccount": "Konto hinzufügen",
|
|
346
|
+
"labelPlaceholder": "Bezeichnung (optional, z. B. Zweitkonto A)",
|
|
347
|
+
"keyPlaceholder": "oc_sk_…",
|
|
348
|
+
"addAndProbe": "Hinzufügen und prüfen",
|
|
349
|
+
"save": "Speichern",
|
|
350
|
+
"cancel": "Abbrechen",
|
|
351
|
+
"setActive": "Als aktiv setzen",
|
|
352
|
+
"disable": "Deaktivieren",
|
|
353
|
+
"enable": "Aktivieren",
|
|
354
|
+
"editKey": "Schlüssel ändern",
|
|
355
|
+
"remove": "Löschen",
|
|
356
|
+
"viewUsage": "Nutzung ansehen",
|
|
357
|
+
"keyUpdated": "Schlüssel aktualisiert",
|
|
358
|
+
"accountAdded": "Konto hinzugefügt und geprüft",
|
|
359
|
+
"shortKey": "Schlüssel unvollständig: mindestens 8 Zeichen",
|
|
360
|
+
"keepOne": "Mindestens ein Konto behalten",
|
|
361
|
+
"notConfigured": "Nicht gesetzt",
|
|
362
|
+
"readonly": "schreibgeschützt",
|
|
363
|
+
"newKeyFor": "Neuer Schlüssel wird in {ref} gespeichert",
|
|
364
|
+
"noAccounts": "Noch keine Konten. Schlüssel eintragen und loslegen.",
|
|
365
|
+
"statusReady": "Bereit",
|
|
366
|
+
"statusExhausted": "Kontingent aufgebraucht",
|
|
367
|
+
"statusAuth": "Anmeldung fehlgeschlagen",
|
|
368
|
+
"statusDisabled": "Deaktiviert",
|
|
369
|
+
"statusMissing": "Kein Schlüssel",
|
|
370
|
+
"statusError": "Prüfung fehlgeschlagen",
|
|
371
|
+
"statusActive": "In Nutzung",
|
|
372
|
+
"statusSpare": "Reserve",
|
|
373
|
+
"statusCurrent": "aktuell",
|
|
374
|
+
"colIndex": "#",
|
|
375
|
+
"colAccount": "Konto",
|
|
376
|
+
"colKey": "Schlüssel",
|
|
377
|
+
"colRolling": "5 Stunden",
|
|
378
|
+
"colWeekly": "Diese Woche",
|
|
379
|
+
"colMonthly": "Dieser Monat",
|
|
380
|
+
"colStatus": "Status",
|
|
381
|
+
"colActions": "Aktionen",
|
|
382
|
+
"colModel": "Modell",
|
|
383
|
+
"colCaps": "Modalität",
|
|
384
|
+
"colRequests": "Anfragen",
|
|
385
|
+
"colTokens": "Tokens Eingabe / Ausgabe",
|
|
386
|
+
"colRemaining": "Rest diesen Monat",
|
|
387
|
+
"colMonthlyUsed": "Verbrauch im Monat",
|
|
388
|
+
"colWeeklyUsed": "Verbrauch in der Woche",
|
|
389
|
+
"modelsTitle": "Pro Modell (lokale Schätzung)",
|
|
390
|
+
"capsText": "Text",
|
|
391
|
+
"capsImage": "Bild",
|
|
392
|
+
"capsVideo": "Video",
|
|
393
|
+
"capsAudio": "Audio",
|
|
394
|
+
"capsPdf": "PDF",
|
|
395
|
+
"capsTools": "Tools",
|
|
396
|
+
"capsReasoning": "Reasoning",
|
|
397
|
+
"capsUnknown": "nicht angegeben",
|
|
398
|
+
"free": "kostenlos",
|
|
399
|
+
"unlimited": "unbegrenzt",
|
|
400
|
+
"none": "—",
|
|
401
|
+
"dialogTitle": "Nutzung von {label}",
|
|
402
|
+
"dialogNote": "Die offizielle Schnittstelle meldet nur diese drei Zeitfenster, ohne Aufschlüsselung pro Modell; Beträge pro Modell können nur eine lokale Schätzung sein.",
|
|
403
|
+
"dialogProbe": "Erneut prüfen",
|
|
404
|
+
"close": "Schließen",
|
|
405
|
+
"probedAt": "Geprüft {time}",
|
|
406
|
+
"resetsAt": "Zurückgesetzt {time}",
|
|
407
|
+
"usageError": "Fehler: {message}",
|
|
408
|
+
"peakTitle": "Welcher Weg gerade günstiger ist",
|
|
409
|
+
"peakPlan": "Du zahlst ¥{price}/Monat (${usd}) für OpenCode {plan}, und der Tarif gibt ¥{allowance} (${allowanceUsd}) Nutzung zu offiziellen Preisen frei – ein Rabatt von {discount}×, dieselbe Anfrage kostet über OpenCode also nur {percent}% des offiziellen Preises.",
|
|
410
|
+
"peakNow": "Der offizielle Weg rechnet direkt zum Listenpreis ab. Aktuell ist OpenCode {goPeak} und die offizielle API {officialPeak}{holiday}.",
|
|
411
|
+
"peakGoPeak": "in der Hauptzeit (Kontingent verbraucht sich doppelt so schnell)",
|
|
412
|
+
"peakGoOff": "in der Nebenzeit",
|
|
413
|
+
"peakOfficialPeak": "in der Hauptzeit (Preis verdoppelt)",
|
|
414
|
+
"peakOfficialOff": "in der Nebenzeit",
|
|
415
|
+
"peakAtPeak": "Hauptzeit",
|
|
416
|
+
"peakOffPeak": "Nebenzeit",
|
|
417
|
+
"peakHoliday": " – heute ist ein chinesischer gesetzlicher Feiertag: Die offizielle API rechnet den ganzen Tag Nebenzeit ab, OpenCode weiter Hauptzeit",
|
|
418
|
+
"peakExhausted": " Das Kontingent für diesen Monat ist aber schon aufgebraucht, es bleibt nur der offizielle Weg.",
|
|
419
|
+
"peakGoPay": "Du zahlst über OpenCode",
|
|
420
|
+
"peakOfficialPay": "Du zahlst über die offizielle API",
|
|
421
|
+
"peakVerdict": "Fazit",
|
|
422
|
+
"peakGoCheaper": "OpenCode ist {percent}% günstiger",
|
|
423
|
+
"peakOfficialCheaper": "Die offizielle API ist {percent}% günstiger",
|
|
424
|
+
"peakFootnote": "Gerechnet pro 1 Mio. Ausgabe-Tokens (bei einer Coding-Rechnung dominiert die Ausgabe), bei ¥{fx} je $. Haupt- und Nebenzeit ändern nur, wie schnell das Kontingent verbraucht wird, nicht das Fazit: Innerhalb des Kontingents bleibt OpenCode günstiger als die offizielle API; Ausnahmen sind ein aufgebrauchtes Kontingent oder ein Modell, dessen eigenes Limit zuerst endet.",
|
|
425
|
+
"footerOfficial": "Die offiziellen Prozente für 5 Stunden / Woche / Monat kommen von der OpenCode-API und beschreiben das ganze Konto – Nutzung über andere Clients oder Rechner zählt mit.",
|
|
426
|
+
"footerLocal": "Beträge pro Modell sind eine lokale Schätzung: Sie zählen nur das Sitzungsprotokoll dieses Harness, und genau das macht die Verteilung pro Modell sichtbar; Aufrufe anderer Tools hinterlassen lokal keine Spur.",
|
|
427
|
+
"footerPeakRule": "Beträge sind zum Nebenzeit-Preis geschätzt, bei DeepSeek-Modellen zur Hauptzeit doppelt; kostenlose Modelle werden nicht berechnet.",
|
|
428
|
+
},
|
|
429
|
+
"es": {
|
|
430
|
+
"title": "Cuentas y uso de OpenCode",
|
|
431
|
+
"refresh": "Actualizar",
|
|
432
|
+
"refreshing": "Actualizando…",
|
|
433
|
+
"updatedAt": "Actualizado {time}",
|
|
434
|
+
"loadError": "Error al leer: {message}",
|
|
435
|
+
"routerTitle": "Enrutado por cuota",
|
|
436
|
+
"modePrimary": "Enrutado normal",
|
|
437
|
+
"modeFallback": "Se recurrió a la suscripción oficial",
|
|
438
|
+
"modeReason": "Motivo: {reason}",
|
|
439
|
+
"activeAccount": "Cuenta activa",
|
|
440
|
+
"nextProbe": "Próximo sondeo",
|
|
441
|
+
"probeAll": "Sondear ahora",
|
|
442
|
+
"resetMarks": "Borrar marcas de bloqueo",
|
|
443
|
+
"resetHint": "Borra todas las marcas de «cuota agotada / fallo de autenticación» y devuelve el enrutado al grupo de cuentas de OpenCode al instante. No cambia cuentas ni claves.",
|
|
444
|
+
"routerMissing": "No se detectó el enrutador de cuota; el grupo de cuentas es de solo lectura.",
|
|
445
|
+
"accountsTitle": "Grupo de cuentas",
|
|
446
|
+
"accountsOrder": "Se usan en orden: la siguiente cuenta entra solo cuando la anterior se queda sin cuota.",
|
|
447
|
+
"addAccount": "Añadir cuenta",
|
|
448
|
+
"labelPlaceholder": "Etiqueta (opcional, p. ej. alterna A)",
|
|
449
|
+
"keyPlaceholder": "oc_sk_…",
|
|
450
|
+
"addAndProbe": "Añadir y sondear",
|
|
451
|
+
"save": "Guardar",
|
|
452
|
+
"cancel": "Cancelar",
|
|
453
|
+
"setActive": "Usar esta",
|
|
454
|
+
"disable": "Desactivar",
|
|
455
|
+
"enable": "Activar",
|
|
456
|
+
"editKey": "Cambiar clave",
|
|
457
|
+
"remove": "Eliminar",
|
|
458
|
+
"viewUsage": "Ver uso",
|
|
459
|
+
"keyUpdated": "Clave actualizada",
|
|
460
|
+
"accountAdded": "Cuenta añadida y sondeada",
|
|
461
|
+
"shortKey": "La clave parece incompleta: mínimo 8 caracteres",
|
|
462
|
+
"keepOne": "Conserva al menos una cuenta",
|
|
463
|
+
"notConfigured": "Sin configurar",
|
|
464
|
+
"readonly": "solo lectura",
|
|
465
|
+
"newKeyFor": "La clave nueva se guarda en {ref}",
|
|
466
|
+
"noAccounts": "Aún no hay cuentas. Añade una clave para empezar.",
|
|
467
|
+
"statusReady": "Disponible",
|
|
468
|
+
"statusExhausted": "Cuota agotada",
|
|
469
|
+
"statusAuth": "Fallo de autenticación",
|
|
470
|
+
"statusDisabled": "Desactivada",
|
|
471
|
+
"statusMissing": "Sin clave",
|
|
472
|
+
"statusError": "Fallo de sondeo",
|
|
473
|
+
"statusActive": "En uso",
|
|
474
|
+
"statusSpare": "Reserva",
|
|
475
|
+
"statusCurrent": "actual",
|
|
476
|
+
"colIndex": "#",
|
|
477
|
+
"colAccount": "Cuenta",
|
|
478
|
+
"colKey": "Clave",
|
|
479
|
+
"colRolling": "5 horas",
|
|
480
|
+
"colWeekly": "Esta semana",
|
|
481
|
+
"colMonthly": "Este mes",
|
|
482
|
+
"colStatus": "Estado",
|
|
483
|
+
"colActions": "Acciones",
|
|
484
|
+
"colModel": "Modelo",
|
|
485
|
+
"colCaps": "Modalidad",
|
|
486
|
+
"colRequests": "Peticiones",
|
|
487
|
+
"colTokens": "Tokens de entrada / salida",
|
|
488
|
+
"colRemaining": "Restante del mes",
|
|
489
|
+
"colMonthlyUsed": "Uso del mes",
|
|
490
|
+
"colWeeklyUsed": "Uso de la semana",
|
|
491
|
+
"modelsTitle": "Por modelo (estimación local)",
|
|
492
|
+
"capsText": "texto",
|
|
493
|
+
"capsImage": "imagen",
|
|
494
|
+
"capsVideo": "video",
|
|
495
|
+
"capsAudio": "audio",
|
|
496
|
+
"capsPdf": "PDF",
|
|
497
|
+
"capsTools": "herramientas",
|
|
498
|
+
"capsReasoning": "razonamiento",
|
|
499
|
+
"capsUnknown": "sin declarar",
|
|
500
|
+
"free": "gratis",
|
|
501
|
+
"unlimited": "ilimitado",
|
|
502
|
+
"none": "—",
|
|
503
|
+
"dialogTitle": "Uso de {label}",
|
|
504
|
+
"dialogNote": "La API oficial solo informa estos tres periodos y no desglosa por modelo; el importe por modelo solo puede ser una estimación local.",
|
|
505
|
+
"dialogProbe": "Sondear de nuevo",
|
|
506
|
+
"close": "Cerrar",
|
|
507
|
+
"probedAt": "Sondeado {time}",
|
|
508
|
+
"resetsAt": "Se reinicia {time}",
|
|
509
|
+
"usageError": "Error: {message}",
|
|
510
|
+
"peakTitle": "Qué conviene más ahora mismo",
|
|
511
|
+
"peakPlan": "Pagas ¥{price} al mes (${usd}) por el plan {plan} de OpenCode, y el plan te da ¥{allowance} (${allowanceUsd}) de consumo a precio oficial: un descuento de {discount}×, así que la misma petición te cuesta el {percent}% del precio oficial si va por OpenCode.",
|
|
512
|
+
"peakNow": "La ruta oficial cobra el precio de lista directo. Ahora mismo OpenCode está {goPeak} y la API oficial está {officialPeak}{holiday}.",
|
|
513
|
+
"peakGoPeak": "en hora pico (la cuota se consume al doble)",
|
|
514
|
+
"peakGoOff": "fuera de hora pico",
|
|
515
|
+
"peakOfficialPeak": "en hora pico (el precio se duplica)",
|
|
516
|
+
"peakOfficialOff": "fuera de hora pico",
|
|
517
|
+
"peakAtPeak": "hora pico",
|
|
518
|
+
"peakOffPeak": "hora valle",
|
|
519
|
+
"peakHoliday": " — hoy es festivo oficial en China: la API oficial cobra tarifa valle todo el día, mientras que OpenCode sigue cobrando pico",
|
|
520
|
+
"peakExhausted": " Pero la cuota de este mes ya está agotada, así que solo queda la ruta oficial.",
|
|
521
|
+
"peakGoPay": "Pagas por OpenCode",
|
|
522
|
+
"peakOfficialPay": "Pagas por la API oficial",
|
|
523
|
+
"peakVerdict": "Conclusión",
|
|
524
|
+
"peakGoCheaper": "OpenCode sale {percent}% más barato",
|
|
525
|
+
"peakOfficialCheaper": "La API oficial sale {percent}% más barata",
|
|
526
|
+
"peakFootnote": "Calculado por millón de tokens de salida (en una factura de código manda la salida), a ¥{fx} por dólar. Las horas pico solo cambian la rapidez con que se consume la cuota, no la conclusión: dentro de la cuota, OpenCode siempre sale más barato que la API oficial; las excepciones son que la cuota se agote o que el límite de un modelo concreto se acabe antes.",
|
|
527
|
+
"footerOfficial": "Los porcentajes oficiales de 5 horas / semana / mes vienen de la API de OpenCode y describen la cuenta completa: el uso desde otros clientes u otras máquinas también cuenta.",
|
|
528
|
+
"footerLocal": "El importe por modelo es una estimación local: solo cuenta el registro de sesiones de este harness, que es lo que permite ver el reparto por modelo; las llamadas de otras herramientas no dejan rastro local.",
|
|
529
|
+
"footerPeakRule": "El importe se estima a tarifa valle, duplicada en hora pico para los modelos DeepSeek; los modelos gratuitos no se facturan.",
|
|
530
|
+
},
|
|
531
|
+
"fr": {
|
|
532
|
+
"title": "Comptes et usage OpenCode",
|
|
533
|
+
"refresh": "Actualiser",
|
|
534
|
+
"refreshing": "Actualisation…",
|
|
535
|
+
"updatedAt": "Mis à jour {time}",
|
|
536
|
+
"loadError": "Échec de lecture : {message}",
|
|
537
|
+
"routerTitle": "Routage par quota",
|
|
538
|
+
"modePrimary": "Routage normal",
|
|
539
|
+
"modeFallback": "Repli sur l’abonnement officiel",
|
|
540
|
+
"modeReason": "Motif : {reason}",
|
|
541
|
+
"activeAccount": "Compte actif",
|
|
542
|
+
"nextProbe": "Prochaine sonde",
|
|
543
|
+
"probeAll": "Sonder maintenant",
|
|
544
|
+
"resetMarks": "Effacer les marques de blocage",
|
|
545
|
+
"resetHint": "Efface toutes les marques « quota épuisé / échec d’authentification » et renvoie aussitôt le routage vers le pool de comptes OpenCode. Les comptes et les clés ne sont pas modifiés.",
|
|
546
|
+
"routerMissing": "Aucun routeur de quota détecté : le pool de comptes est en lecture seule.",
|
|
547
|
+
"accountsTitle": "Pool de comptes",
|
|
548
|
+
"accountsOrder": "Utilisés dans l’ordre : le compte suivant ne prend le relais qu’une fois le précédent à sec.",
|
|
549
|
+
"addAccount": "Ajouter un compte",
|
|
550
|
+
"labelPlaceholder": "Libellé (facultatif, ex. alt A)",
|
|
551
|
+
"keyPlaceholder": "oc_sk_…",
|
|
552
|
+
"addAndProbe": "Ajouter et sonder",
|
|
553
|
+
"save": "Enregistrer",
|
|
554
|
+
"cancel": "Annuler",
|
|
555
|
+
"setActive": "Définir comme actif",
|
|
556
|
+
"disable": "Désactiver",
|
|
557
|
+
"enable": "Activer",
|
|
558
|
+
"editKey": "Changer la clé",
|
|
559
|
+
"remove": "Supprimer",
|
|
560
|
+
"viewUsage": "Voir l’usage",
|
|
561
|
+
"keyUpdated": "Clé mise à jour",
|
|
562
|
+
"accountAdded": "Compte ajouté et sondé",
|
|
563
|
+
"shortKey": "Clé incomplète : 8 caractères minimum",
|
|
564
|
+
"keepOne": "Gardez au moins un compte",
|
|
565
|
+
"notConfigured": "Non défini",
|
|
566
|
+
"readonly": "lecture seule",
|
|
567
|
+
"newKeyFor": "La nouvelle clé est écrite dans {ref}",
|
|
568
|
+
"noAccounts": "Aucun compte pour l’instant. Ajoutez une clé pour commencer.",
|
|
569
|
+
"statusReady": "Disponible",
|
|
570
|
+
"statusExhausted": "Quota épuisé",
|
|
571
|
+
"statusAuth": "Échec d’authentification",
|
|
572
|
+
"statusDisabled": "Désactivé",
|
|
573
|
+
"statusMissing": "Sans clé",
|
|
574
|
+
"statusError": "Échec de la sonde",
|
|
575
|
+
"statusActive": "Utilisé",
|
|
576
|
+
"statusSpare": "Secours",
|
|
577
|
+
"statusCurrent": "actuel",
|
|
578
|
+
"colIndex": "#",
|
|
579
|
+
"colAccount": "Compte",
|
|
580
|
+
"colKey": "Clé",
|
|
581
|
+
"colRolling": "5 heures",
|
|
582
|
+
"colWeekly": "Cette semaine",
|
|
583
|
+
"colMonthly": "Ce mois",
|
|
584
|
+
"colStatus": "État",
|
|
585
|
+
"colActions": "Actions",
|
|
586
|
+
"colModel": "Modèle",
|
|
587
|
+
"colCaps": "Modalité",
|
|
588
|
+
"colRequests": "Requêtes",
|
|
589
|
+
"colTokens": "Tokens entrée / sortie",
|
|
590
|
+
"colRemaining": "Reste ce mois",
|
|
591
|
+
"colMonthlyUsed": "Usage du mois",
|
|
592
|
+
"colWeeklyUsed": "Usage de la semaine",
|
|
593
|
+
"modelsTitle": "Par modèle (estimation locale)",
|
|
594
|
+
"capsText": "texte",
|
|
595
|
+
"capsImage": "image",
|
|
596
|
+
"capsVideo": "vidéo",
|
|
597
|
+
"capsAudio": "audio",
|
|
598
|
+
"capsPdf": "PDF",
|
|
599
|
+
"capsTools": "outils",
|
|
600
|
+
"capsReasoning": "raisonnement",
|
|
601
|
+
"capsUnknown": "non déclaré",
|
|
602
|
+
"free": "gratuit",
|
|
603
|
+
"unlimited": "illimité",
|
|
604
|
+
"none": "—",
|
|
605
|
+
"dialogTitle": "Usage de {label}",
|
|
606
|
+
"dialogNote": "L’API officielle ne renvoie que ces trois fenêtres, sans détail par modèle ; le montant par modèle ne peut être qu’une estimation locale.",
|
|
607
|
+
"dialogProbe": "Sonder à nouveau",
|
|
608
|
+
"close": "Fermer",
|
|
609
|
+
"probedAt": "Sondé {time}",
|
|
610
|
+
"resetsAt": "Réinitialisation {time}",
|
|
611
|
+
"usageError": "Erreur : {message}",
|
|
612
|
+
"peakTitle": "La route la moins chère maintenant",
|
|
613
|
+
"peakPlan": "Vous payez ¥{price}/mois (${usd}) pour le forfait OpenCode {plan}, et le forfait vous ouvre ¥{allowance} (${allowanceUsd}) d’usage au prix officiel — soit un rabais de {discount}× : la même requête ne vous coûte que {percent}% du prix officiel en passant par OpenCode.",
|
|
614
|
+
"peakNow": "La route officielle facture le prix catalogue. En ce moment OpenCode est {goPeak} et l’API officielle est {officialPeak}{holiday}.",
|
|
615
|
+
"peakGoPeak": "en heures pleines (le quota se consomme deux fois plus vite)",
|
|
616
|
+
"peakGoOff": "en heures creuses",
|
|
617
|
+
"peakOfficialPeak": "en heures pleines (le prix double)",
|
|
618
|
+
"peakOfficialOff": "en heures creuses",
|
|
619
|
+
"peakAtPeak": "heures pleines",
|
|
620
|
+
"peakOffPeak": "heures creuses",
|
|
621
|
+
"peakHoliday": " — aujourd’hui est un jour férié officiel en Chine : l’API officielle facture en heures creuses toute la journée, alors qu’OpenCode reste en heures pleines",
|
|
622
|
+
"peakExhausted": " Mais le quota de ce mois est déjà épuisé : il ne reste que la route officielle.",
|
|
623
|
+
"peakGoPay": "Vous payez via OpenCode",
|
|
624
|
+
"peakOfficialPay": "Vous payez via l’API officielle",
|
|
625
|
+
"peakVerdict": "Verdict",
|
|
626
|
+
"peakGoCheaper": "OpenCode coûte {percent}% de moins",
|
|
627
|
+
"peakOfficialCheaper": "L’API officielle coûte {percent}% de moins",
|
|
628
|
+
"peakFootnote": "Pour 1 M de tokens en sortie (la sortie domine une facture de code), au taux de ¥{fx} pour 1 $. Les heures pleines ne changent que la vitesse à laquelle le quota se consomme, pas la conclusion : tant qu’il reste du quota, OpenCode reste moins cher que l’API officielle ; les exceptions sont un quota épuisé ou un modèle dont le plafond tombe en premier.",
|
|
629
|
+
"footerOfficial": "Les pourcentages officiels 5 heures / semaine / mois viennent de l’API OpenCode et décrivent le compte entier : l’usage d’autres clients ou d’autres machines compte aussi.",
|
|
630
|
+
"footerLocal": "Le montant par modèle est une estimation locale : seuls les journaux de session de ce harness sont comptés, ce qui permet de voir la répartition par modèle ; les appels des autres outils ne laissent aucune trace locale.",
|
|
631
|
+
"footerPeakRule": "Le montant est estimé au tarif heures creuses, doublé en heures pleines pour les modèles DeepSeek ; les modèles gratuits ne sont pas facturés.",
|
|
632
|
+
},
|
|
633
|
+
"ja": {
|
|
634
|
+
"title": "OpenCode アカウントと使用量",
|
|
635
|
+
"refresh": "更新",
|
|
636
|
+
"refreshing": "更新中…",
|
|
637
|
+
"updatedAt": "{time} に更新",
|
|
638
|
+
"loadError": "読み込みに失敗しました: {message}",
|
|
639
|
+
"routerTitle": "クォータルーティング",
|
|
640
|
+
"modePrimary": "通常ルーティング",
|
|
641
|
+
"modeFallback": "公式サブスクへフォールバック済み",
|
|
642
|
+
"modeReason": "理由: {reason}",
|
|
643
|
+
"activeAccount": "現在のアカウント",
|
|
644
|
+
"nextProbe": "次回の確認",
|
|
645
|
+
"probeAll": "今すぐ確認",
|
|
646
|
+
"resetMarks": "ブロック記録をリセット",
|
|
647
|
+
"resetHint": "「クォータ切れ / 認証失敗」の記録をすべて消し、ルーティングをすぐに OpenCode のアカウントプールへ戻します。アカウントもキーも変更しません。",
|
|
648
|
+
"routerMissing": "クォータルーティングが見つかりません。アカウントプールは読み取り専用です。",
|
|
649
|
+
"accountsTitle": "アカウントプール",
|
|
650
|
+
"accountsOrder": "上から順に使用します。前のアカウントを使い切ってから次に切り替わります。",
|
|
651
|
+
"addAccount": "アカウントを追加",
|
|
652
|
+
"labelPlaceholder": "ラベル(任意、例: サブ A)",
|
|
653
|
+
"keyPlaceholder": "oc_sk_…",
|
|
654
|
+
"addAndProbe": "追加して確認",
|
|
655
|
+
"save": "保存",
|
|
656
|
+
"cancel": "キャンセル",
|
|
657
|
+
"setActive": "現在のアカウントにする",
|
|
658
|
+
"disable": "無効化",
|
|
659
|
+
"enable": "有効化",
|
|
660
|
+
"editKey": "キーを変更",
|
|
661
|
+
"remove": "削除",
|
|
662
|
+
"viewUsage": "使用量を見る",
|
|
663
|
+
"keyUpdated": "キーを更新しました",
|
|
664
|
+
"accountAdded": "アカウントを追加して確認しました",
|
|
665
|
+
"shortKey": "キーが不完全です: 8 文字以上必要です",
|
|
666
|
+
"keepOne": "アカウントは 1 つ以上必要です",
|
|
667
|
+
"notConfigured": "未設定",
|
|
668
|
+
"readonly": "読み取り専用",
|
|
669
|
+
"newKeyFor": "新しいキーの保存先: {ref}",
|
|
670
|
+
"noAccounts": "アカウントがまだありません。キーを 1 つ入れれば始められます。",
|
|
671
|
+
"statusReady": "利用可能",
|
|
672
|
+
"statusExhausted": "クォータ切れ",
|
|
673
|
+
"statusAuth": "認証失敗",
|
|
674
|
+
"statusDisabled": "無効",
|
|
675
|
+
"statusMissing": "キー未設定",
|
|
676
|
+
"statusError": "読み込み失敗",
|
|
677
|
+
"statusActive": "使用中",
|
|
678
|
+
"statusSpare": "予備",
|
|
679
|
+
"statusCurrent": "現在",
|
|
680
|
+
"colIndex": "#",
|
|
681
|
+
"colAccount": "アカウント",
|
|
682
|
+
"colKey": "キー",
|
|
683
|
+
"colRolling": "5 時間",
|
|
684
|
+
"colWeekly": "今週",
|
|
685
|
+
"colMonthly": "今月",
|
|
686
|
+
"colStatus": "状態",
|
|
687
|
+
"colActions": "操作",
|
|
688
|
+
"colModel": "モデル",
|
|
689
|
+
"colCaps": "モダリティ",
|
|
690
|
+
"colRequests": "リクエスト",
|
|
691
|
+
"colTokens": "入力 / 出力トークン",
|
|
692
|
+
"colRemaining": "今月の残り",
|
|
693
|
+
"colMonthlyUsed": "今月の使用量",
|
|
694
|
+
"colWeeklyUsed": "今週の使用量",
|
|
695
|
+
"modelsTitle": "モデル別(ローカル推定)",
|
|
696
|
+
"capsText": "テキスト",
|
|
697
|
+
"capsImage": "画像",
|
|
698
|
+
"capsVideo": "動画",
|
|
699
|
+
"capsAudio": "音声",
|
|
700
|
+
"capsPdf": "PDF",
|
|
701
|
+
"capsTools": "ツール",
|
|
702
|
+
"capsReasoning": "推論",
|
|
703
|
+
"capsUnknown": "未申告",
|
|
704
|
+
"free": "無料",
|
|
705
|
+
"unlimited": "無制限",
|
|
706
|
+
"none": "—",
|
|
707
|
+
"dialogTitle": "{label} の使用量",
|
|
708
|
+
"dialogNote": "公式 API が返すのはこの 3 つの期間の割合だけで、モデル別の内訳はありません。モデル別の金額はローカル推定にすぎません。",
|
|
709
|
+
"dialogProbe": "再確認",
|
|
710
|
+
"close": "閉じる",
|
|
711
|
+
"probedAt": "{time} に確認",
|
|
712
|
+
"resetsAt": "{time} にリセット",
|
|
713
|
+
"usageError": "エラー: {message}",
|
|
714
|
+
"peakTitle": "いまはどちらが得か",
|
|
715
|
+
"peakPlan": "OpenCode の {plan} プランに月 ¥{price}(${usd})を支払うと、公式価格換算で ¥{allowance}(${allowanceUsd})分のクォータが付きます。クォータは公式価格で差し引かれるので実質 {discount} 割の支払い、つまり同じリクエストを OpenCode 経由にすると公式価格の {percent}% で済みます。",
|
|
716
|
+
"peakNow": "公式に直結すると公式価格での支払いになります。いま OpenCode は{goPeak}、公式は{officialPeak}{holiday}。",
|
|
717
|
+
"peakGoPeak": "ピーク時間(クォータ消費 2 倍)",
|
|
718
|
+
"peakGoOff": "オフピーク",
|
|
719
|
+
"peakOfficialPeak": "ピーク時間(価格 2 倍)",
|
|
720
|
+
"peakOfficialOff": "オフピーク",
|
|
721
|
+
"peakAtPeak": "ピーク",
|
|
722
|
+
"peakOffPeak": "オフピーク",
|
|
723
|
+
"peakHoliday": " — 今日は中国の祝日なので、一日中オフピーク、OpenCode はピークのままです",
|
|
724
|
+
"peakExhausted": "ただし今月のクォータはすでに使い切っているため、公式を使うしかありません。",
|
|
725
|
+
"peakGoPay": "OpenCode 経由の支払い",
|
|
726
|
+
"peakOfficialPay": "公式直結の支払い",
|
|
727
|
+
"peakVerdict": "結論",
|
|
728
|
+
"peakGoCheaper": "OpenCode のほうが {percent}% 安い",
|
|
729
|
+
"peakOfficialCheaper": "公式直結のほうが {percent}% 安い",
|
|
730
|
+
"peakFootnote": "出力 100 万トークンあたりに換算(コーディングの請求はほとんどが出力由来)、レートは ¥{fx}/$。ピークとオフピークはクォータの減る速さに影響するだけで、結論は変わりません。クォータの範囲内なら OpenCode 経由のほうが公式直結より常に安く、例外はクォータを使い切った場合と、特定モデルの上限に先に達した場合です。",
|
|
731
|
+
"footerOfficial": "上の「公式 5 時間 / 今週 / 今月」の割合は OpenCode の API が返す実際のアカウント使用量で、他のクライアントや別のマシンからの利用も含まれます。",
|
|
732
|
+
"footerLocal": "モデル別の金額はローカル推定です。この harness のセッションログだけを集計してモデル別の内訳を出すもので、他のツールからの呼び出しはローカルに記録されません。",
|
|
733
|
+
"footerPeakRule": "金額はオフピーク価格で見積もり、DeepSeek モデルはピーク時に 2 倍で計算します。無料モデルは課金されません。",
|
|
734
|
+
},
|
|
735
|
+
"ko": {
|
|
736
|
+
"title": "OpenCode 계정 및 사용량",
|
|
737
|
+
"refresh": "새로고침",
|
|
738
|
+
"refreshing": "새로고침 중…",
|
|
739
|
+
"updatedAt": "{time}에 업데이트",
|
|
740
|
+
"loadError": "불러오지 못했습니다: {message}",
|
|
741
|
+
"routerTitle": "할당량 라우팅",
|
|
742
|
+
"modePrimary": "정상 라우팅",
|
|
743
|
+
"modeFallback": "공식 구독으로 폴백됨",
|
|
744
|
+
"modeReason": "이유: {reason}",
|
|
745
|
+
"activeAccount": "현재 계정",
|
|
746
|
+
"nextProbe": "다음 점검",
|
|
747
|
+
"probeAll": "지금 점검",
|
|
748
|
+
"resetMarks": "차단 표시 초기화",
|
|
749
|
+
"resetHint": "\"할당량 소진 / 인증 실패\" 표시를 모두 지우고 라우팅을 즉시 OpenCode 계정 풀로 되돌립니다. 계정과 키는 그대로 둡니다.",
|
|
750
|
+
"routerMissing": "할당량 라우팅을 찾지 못했습니다. 계정 풀은 읽기 전용입니다.",
|
|
751
|
+
"accountsTitle": "계정 풀",
|
|
752
|
+
"accountsOrder": "순서대로 사용합니다. 앞 계정을 다 쓴 뒤에야 다음 계정으로 넘어갑니다.",
|
|
753
|
+
"addAccount": "계정 추가",
|
|
754
|
+
"labelPlaceholder": "라벨(선택, 예: 부계정 A)",
|
|
755
|
+
"keyPlaceholder": "oc_sk_…",
|
|
756
|
+
"addAndProbe": "추가 후 점검",
|
|
757
|
+
"save": "저장",
|
|
758
|
+
"cancel": "취소",
|
|
759
|
+
"setActive": "현재 계정으로 설정",
|
|
760
|
+
"disable": "사용 안 함",
|
|
761
|
+
"enable": "사용",
|
|
762
|
+
"editKey": "키 변경",
|
|
763
|
+
"remove": "삭제",
|
|
764
|
+
"viewUsage": "사용량 보기",
|
|
765
|
+
"keyUpdated": "키를 업데이트했습니다",
|
|
766
|
+
"accountAdded": "계정을 추가하고 점검했습니다",
|
|
767
|
+
"shortKey": "키가 불완전합니다: 8자 이상 필요",
|
|
768
|
+
"keepOne": "계정을 하나 이상 남겨야 합니다",
|
|
769
|
+
"notConfigured": "설정 안 됨",
|
|
770
|
+
"readonly": "읽기 전용",
|
|
771
|
+
"newKeyFor": "새 키 저장 위치: {ref}",
|
|
772
|
+
"noAccounts": "아직 계정이 없습니다. 키를 하나 넣으면 시작할 수 있습니다.",
|
|
773
|
+
"statusReady": "사용 가능",
|
|
774
|
+
"statusExhausted": "할당량 소진",
|
|
775
|
+
"statusAuth": "인증 실패",
|
|
776
|
+
"statusDisabled": "사용 안 함",
|
|
777
|
+
"statusMissing": "키 없음",
|
|
778
|
+
"statusError": "읽기 실패",
|
|
779
|
+
"statusActive": "사용 중",
|
|
780
|
+
"statusSpare": "예비",
|
|
781
|
+
"statusCurrent": "현재",
|
|
782
|
+
"colIndex": "#",
|
|
783
|
+
"colAccount": "계정",
|
|
784
|
+
"colKey": "키",
|
|
785
|
+
"colRolling": "5시간",
|
|
786
|
+
"colWeekly": "이번 주",
|
|
787
|
+
"colMonthly": "이번 달",
|
|
788
|
+
"colStatus": "상태",
|
|
789
|
+
"colActions": "작업",
|
|
790
|
+
"colModel": "모델",
|
|
791
|
+
"colCaps": "모달리티",
|
|
792
|
+
"colRequests": "요청",
|
|
793
|
+
"colTokens": "입력 / 출력 토큰",
|
|
794
|
+
"colRemaining": "이번 달 잔여",
|
|
795
|
+
"colMonthlyUsed": "이번 달 사용량",
|
|
796
|
+
"colWeeklyUsed": "이번 주 사용량",
|
|
797
|
+
"modelsTitle": "모델별(로컬 추정)",
|
|
798
|
+
"capsText": "텍스트",
|
|
799
|
+
"capsImage": "이미지",
|
|
800
|
+
"capsVideo": "영상",
|
|
801
|
+
"capsAudio": "오디오",
|
|
802
|
+
"capsPdf": "PDF",
|
|
803
|
+
"capsTools": "도구",
|
|
804
|
+
"capsReasoning": "추론",
|
|
805
|
+
"capsUnknown": "미선언",
|
|
806
|
+
"free": "무료",
|
|
807
|
+
"unlimited": "무제한",
|
|
808
|
+
"none": "—",
|
|
809
|
+
"dialogTitle": "{label} 사용량",
|
|
810
|
+
"dialogNote": "공식 API는 이 세 구간의 비율만 주고 모델별 내역은 없습니다. 모델별 금액은 로컬 추정치일 뿐입니다.",
|
|
811
|
+
"dialogProbe": "다시 점검",
|
|
812
|
+
"close": "닫기",
|
|
813
|
+
"probedAt": "{time}에 점검",
|
|
814
|
+
"resetsAt": "{time}에 초기화",
|
|
815
|
+
"usageError": "오류: {message}",
|
|
816
|
+
"peakTitle": "지금 어느 쪽이 더 저렴한가",
|
|
817
|
+
"peakPlan": "OpenCode {plan} 플랜에 월 ¥{price}(${usd})를 내면 공식 가격 기준 ¥{allowance}(${allowanceUsd})만큼의 할당량이 주어집니다. 할당량은 공식 가격으로 차감되므로 실질 부담은 {discount}할, 즉 같은 요청을 OpenCode로 보내면 공식 가격의 {percent}%만 내면 됩니다.",
|
|
818
|
+
"peakNow": "공식에 직결하면 공식 가격으로 결제됩니다. 지금 OpenCode는 {goPeak}, 공식은 {officialPeak}{holiday}.",
|
|
819
|
+
"peakGoPeak": "피크 시간(할당량 2배 소모)",
|
|
820
|
+
"peakGoOff": "비피크",
|
|
821
|
+
"peakOfficialPeak": "피크 시간(가격 2배)",
|
|
822
|
+
"peakOfficialOff": "비피크",
|
|
823
|
+
"peakAtPeak": "피크",
|
|
824
|
+
"peakOffPeak": "비피크",
|
|
825
|
+
"peakHoliday": " — 오늘은 중국 공휴일이라 하루 종일 비피크, OpenCode는 피크 그대로입니다",
|
|
826
|
+
"peakExhausted": "다만 이번 달 할당량을 이미 다 썼으므로 공식만 쓸 수 있습니다.",
|
|
827
|
+
"peakGoPay": "OpenCode 경유 결제액",
|
|
828
|
+
"peakOfficialPay": "공식 직결 결제액",
|
|
829
|
+
"peakVerdict": "결론",
|
|
830
|
+
"peakGoCheaper": "OpenCode가 {percent}% 더 저렴",
|
|
831
|
+
"peakOfficialCheaper": "공식 직결이 {percent}% 더 저렴",
|
|
832
|
+
"peakFootnote": "출력 100만 토큰 기준 환산(코딩 요금은 대부분 출력에서 발생), 환율 ¥{fx}/$. 피크·비피크는 할당량이 줄어드는 속도에만 영향을 주고 결론은 바뀌지 않습니다. 할당량 안에서는 OpenCode 경유가 공식 직결보다 항상 저렴하며, 예외는 할당량을 다 쓴 경우와 특정 모델 한도가 먼저 바닥나는 경우입니다.",
|
|
833
|
+
"footerOfficial": "위의 \"공식 5시간 / 이번 주 / 이번 달\" 비율은 OpenCode API가 주는 실제 계정 사용량이며, 다른 클라이언트나 다른 기기에서 쓴 양도 포함됩니다.",
|
|
834
|
+
"footerLocal": "모델별 금액은 로컬 추정치입니다. 이 harness의 세션 로그만 집계해 모델별 분포를 보여 주는 것이며, 다른 도구의 호출은 로컬에 기록되지 않습니다.",
|
|
835
|
+
"footerPeakRule": "금액은 비피크 가격으로 추정하고, DeepSeek 모델은 피크 시간에 2배로 계산합니다. 무료 모델은 과금되지 않습니다.",
|
|
836
|
+
},
|
|
837
|
+
"pt": {
|
|
838
|
+
"title": "Contas e uso do OpenCode",
|
|
839
|
+
"refresh": "Atualizar",
|
|
840
|
+
"refreshing": "Atualizando…",
|
|
841
|
+
"updatedAt": "Atualizado {time}",
|
|
842
|
+
"loadError": "Falha ao ler: {message}",
|
|
843
|
+
"routerTitle": "Roteamento por cota",
|
|
844
|
+
"modePrimary": "Roteamento normal",
|
|
845
|
+
"modeFallback": "Voltou para a assinatura oficial",
|
|
846
|
+
"modeReason": "Motivo: {reason}",
|
|
847
|
+
"activeAccount": "Conta ativa",
|
|
848
|
+
"nextProbe": "Próxima sondagem",
|
|
849
|
+
"probeAll": "Sondar agora",
|
|
850
|
+
"resetMarks": "Limpar marcas de bloqueio",
|
|
851
|
+
"resetHint": "Apaga todas as marcas de “cota esgotada / falha de autenticação” e devolve o roteamento na hora para o pool de contas do OpenCode. Não mexe em contas nem em chaves.",
|
|
852
|
+
"routerMissing": "Nenhum roteador de cota encontrado — o pool de contas está em somente leitura.",
|
|
853
|
+
"accountsTitle": "Pool de contas",
|
|
854
|
+
"accountsOrder": "Usadas em ordem: a próxima conta só entra quando a de cima seca.",
|
|
855
|
+
"addAccount": "Adicionar conta",
|
|
856
|
+
"labelPlaceholder": "Rótulo (opcional, ex. alternativa A)",
|
|
857
|
+
"keyPlaceholder": "oc_sk_…",
|
|
858
|
+
"addAndProbe": "Adicionar e sondar",
|
|
859
|
+
"save": "Salvar",
|
|
860
|
+
"cancel": "Cancelar",
|
|
861
|
+
"setActive": "Tornar ativa",
|
|
862
|
+
"disable": "Desativar",
|
|
863
|
+
"enable": "Ativar",
|
|
864
|
+
"editKey": "Trocar chave",
|
|
865
|
+
"remove": "Excluir",
|
|
866
|
+
"viewUsage": "Ver uso",
|
|
867
|
+
"keyUpdated": "Chave atualizada",
|
|
868
|
+
"accountAdded": "Conta adicionada e sondada",
|
|
869
|
+
"shortKey": "Chave incompleta: mínimo de 8 caracteres",
|
|
870
|
+
"keepOne": "Mantenha pelo menos uma conta",
|
|
871
|
+
"notConfigured": "Não definido",
|
|
872
|
+
"readonly": "somente leitura",
|
|
873
|
+
"newKeyFor": "A nova chave é gravada em {ref}",
|
|
874
|
+
"noAccounts": "Nenhuma conta ainda. Adicione uma chave para começar.",
|
|
875
|
+
"statusReady": "Disponível",
|
|
876
|
+
"statusExhausted": "Cota esgotada",
|
|
877
|
+
"statusAuth": "Falha de autenticação",
|
|
878
|
+
"statusDisabled": "Desativada",
|
|
879
|
+
"statusMissing": "Sem chave",
|
|
880
|
+
"statusError": "Falha na sondagem",
|
|
881
|
+
"statusActive": "Em uso",
|
|
882
|
+
"statusSpare": "Reserva",
|
|
883
|
+
"statusCurrent": "atual",
|
|
884
|
+
"colIndex": "#",
|
|
885
|
+
"colAccount": "Conta",
|
|
886
|
+
"colKey": "Chave",
|
|
887
|
+
"colRolling": "5 horas",
|
|
888
|
+
"colWeekly": "Esta semana",
|
|
889
|
+
"colMonthly": "Este mês",
|
|
890
|
+
"colStatus": "Status",
|
|
891
|
+
"colActions": "Ações",
|
|
892
|
+
"colModel": "Modelo",
|
|
893
|
+
"colCaps": "Modalidade",
|
|
894
|
+
"colRequests": "Requisições",
|
|
895
|
+
"colTokens": "Tokens de entrada / saída",
|
|
896
|
+
"colRemaining": "Restante no mês",
|
|
897
|
+
"colMonthlyUsed": "Uso no mês",
|
|
898
|
+
"colWeeklyUsed": "Uso na semana",
|
|
899
|
+
"modelsTitle": "Por modelo (estimativa local)",
|
|
900
|
+
"capsText": "texto",
|
|
901
|
+
"capsImage": "imagem",
|
|
902
|
+
"capsVideo": "vídeo",
|
|
903
|
+
"capsAudio": "áudio",
|
|
904
|
+
"capsPdf": "PDF",
|
|
905
|
+
"capsTools": "ferramentas",
|
|
906
|
+
"capsReasoning": "raciocínio",
|
|
907
|
+
"capsUnknown": "não declarado",
|
|
908
|
+
"free": "grátis",
|
|
909
|
+
"unlimited": "ilimitado",
|
|
910
|
+
"none": "—",
|
|
911
|
+
"dialogTitle": "Uso de {label}",
|
|
912
|
+
"dialogNote": "A API oficial só informa estas três janelas, sem detalhe por modelo; o valor por modelo só pode ser uma estimativa local.",
|
|
913
|
+
"dialogProbe": "Sondar de novo",
|
|
914
|
+
"close": "Fechar",
|
|
915
|
+
"probedAt": "Sondado {time}",
|
|
916
|
+
"resetsAt": "Reinicia {time}",
|
|
917
|
+
"usageError": "Erro: {message}",
|
|
918
|
+
"peakTitle": "Qual rota compensa agora",
|
|
919
|
+
"peakPlan": "Você paga ¥{price}/mês (${usd}) pelo plano {plan} do OpenCode, e o plano libera ¥{allowance} (${allowanceUsd}) de uso a preço oficial — um desconto de {discount}×, então a mesma requisição custa {percent}% do preço oficial passando pelo OpenCode.",
|
|
920
|
+
"peakNow": "A rota oficial cobra o preço de tabela direto. Agora o OpenCode está {goPeak} e a API oficial está {officialPeak}{holiday}.",
|
|
921
|
+
"peakGoPeak": "no horário de pico (a cota queima 2× mais rápido)",
|
|
922
|
+
"peakGoOff": "fora do pico",
|
|
923
|
+
"peakOfficialPeak": "no horário de pico (o preço dobra)",
|
|
924
|
+
"peakOfficialOff": "fora do pico",
|
|
925
|
+
"peakAtPeak": "pico",
|
|
926
|
+
"peakOffPeak": "fora do pico",
|
|
927
|
+
"peakHoliday": " — hoje é feriado nacional na China: a API oficial cobra fora do pico o dia inteiro, enquanto o OpenCode continua cobrando pico",
|
|
928
|
+
"peakExhausted": " Mas a cota deste mês já acabou, então só resta a rota oficial.",
|
|
929
|
+
"peakGoPay": "Você paga pelo OpenCode",
|
|
930
|
+
"peakOfficialPay": "Você paga pela API oficial",
|
|
931
|
+
"peakVerdict": "Veredito",
|
|
932
|
+
"peakGoCheaper": "O OpenCode sai {percent}% mais barato",
|
|
933
|
+
"peakOfficialCheaper": "A API oficial sai {percent}% mais barata",
|
|
934
|
+
"peakFootnote": "Cálculo por 1 M de tokens de saída (numa fatura de código a saída domina), a ¥{fx} por dólar. O pico só muda a velocidade com que a cota queima, não a conclusão: dentro da cota, o OpenCode continua mais barato que a API oficial; as exceções são a cota esgotada ou um modelo cujo limite próprio acaba antes.",
|
|
935
|
+
"footerOfficial": "As porcentagens oficiais de 5 horas / semana / mês vêm da API do OpenCode e descrevem a conta inteira: uso de outros clientes ou de outras máquinas também entra na conta.",
|
|
936
|
+
"footerLocal": "O valor por modelo é uma estimativa local: conta só o log de sessões deste harness, que é o que permite ver a divisão por modelo; chamadas de outras ferramentas não deixam registro local.",
|
|
937
|
+
"footerPeakRule": "O valor é estimado a preço fora do pico, dobrado no horário de pico para os modelos DeepSeek; modelos gratuitos não são cobrados.",
|
|
938
|
+
},
|
|
939
|
+
"ru": {
|
|
940
|
+
"title": "Аккаунты и использование OpenCode",
|
|
941
|
+
"refresh": "Обновить",
|
|
942
|
+
"refreshing": "Обновление…",
|
|
943
|
+
"updatedAt": "Обновлено {time}",
|
|
944
|
+
"loadError": "Не удалось загрузить: {message}",
|
|
945
|
+
"routerTitle": "Маршрутизация квот",
|
|
946
|
+
"modePrimary": "Обычная маршрутизация",
|
|
947
|
+
"modeFallback": "Переключено на официальную подписку",
|
|
948
|
+
"modeReason": "Причина: {reason}",
|
|
949
|
+
"activeAccount": "Активный аккаунт",
|
|
950
|
+
"nextProbe": "Следующая проверка",
|
|
951
|
+
"probeAll": "Проверить сейчас",
|
|
952
|
+
"resetMarks": "Сбросить отметки блокировки",
|
|
953
|
+
"resetHint": "Снимает все отметки «квота исчерпана / ошибка авторизации» и сразу возвращает маршрутизацию в пул аккаунтов OpenCode. Аккаунты и ключи не меняются.",
|
|
954
|
+
"routerMissing": "Маршрутизация квот не обнаружена — пул аккаунтов доступен только для чтения.",
|
|
955
|
+
"accountsTitle": "Пул аккаунтов",
|
|
956
|
+
"accountsOrder": "Используются по порядку: следующий аккаунт подключается, только когда исчерпан предыдущий.",
|
|
957
|
+
"addAccount": "Добавить аккаунт",
|
|
958
|
+
"labelPlaceholder": "Метка (необязательно, напр. доп. A)",
|
|
959
|
+
"keyPlaceholder": "oc_sk_…",
|
|
960
|
+
"addAndProbe": "Добавить и проверить",
|
|
961
|
+
"save": "Сохранить",
|
|
962
|
+
"cancel": "Отмена",
|
|
963
|
+
"setActive": "Сделать активным",
|
|
964
|
+
"disable": "Отключить",
|
|
965
|
+
"enable": "Включить",
|
|
966
|
+
"editKey": "Сменить ключ",
|
|
967
|
+
"remove": "Удалить",
|
|
968
|
+
"viewUsage": "Показать расход",
|
|
969
|
+
"keyUpdated": "Ключ обновлён",
|
|
970
|
+
"accountAdded": "Аккаунт добавлен и проверен",
|
|
971
|
+
"shortKey": "Ключ неполный: нужно минимум 8 символов",
|
|
972
|
+
"keepOne": "Нужен хотя бы один аккаунт",
|
|
973
|
+
"notConfigured": "Не задан",
|
|
974
|
+
"readonly": "только чтение",
|
|
975
|
+
"newKeyFor": "Новый ключ сохранён в {ref}",
|
|
976
|
+
"noAccounts": "Аккаунтов пока нет. Введите ключ, чтобы начать.",
|
|
977
|
+
"statusReady": "Готов",
|
|
978
|
+
"statusExhausted": "Квота исчерпана",
|
|
979
|
+
"statusAuth": "Ошибка авторизации",
|
|
980
|
+
"statusDisabled": "Отключён",
|
|
981
|
+
"statusMissing": "Нет ключа",
|
|
982
|
+
"statusError": "Ошибка чтения",
|
|
983
|
+
"statusActive": "Используется",
|
|
984
|
+
"statusSpare": "Резервный",
|
|
985
|
+
"statusCurrent": "текущий",
|
|
986
|
+
"colIndex": "#",
|
|
987
|
+
"colAccount": "Аккаунт",
|
|
988
|
+
"colKey": "Ключ",
|
|
989
|
+
"colRolling": "5 часов",
|
|
990
|
+
"colWeekly": "Неделя",
|
|
991
|
+
"colMonthly": "Месяц",
|
|
992
|
+
"colStatus": "Статус",
|
|
993
|
+
"colActions": "Действия",
|
|
994
|
+
"colModel": "Модель",
|
|
995
|
+
"colCaps": "Модальность",
|
|
996
|
+
"colRequests": "Запросы",
|
|
997
|
+
"colTokens": "Токены: вход / выход",
|
|
998
|
+
"colRemaining": "Остаток за месяц",
|
|
999
|
+
"colMonthlyUsed": "Расход за месяц",
|
|
1000
|
+
"colWeeklyUsed": "Расход за неделю",
|
|
1001
|
+
"modelsTitle": "По моделям (локальная оценка)",
|
|
1002
|
+
"capsText": "текст",
|
|
1003
|
+
"capsImage": "изображения",
|
|
1004
|
+
"capsVideo": "видео",
|
|
1005
|
+
"capsAudio": "аудио",
|
|
1006
|
+
"capsPdf": "PDF",
|
|
1007
|
+
"capsTools": "инструменты",
|
|
1008
|
+
"capsReasoning": "рассуждения",
|
|
1009
|
+
"capsUnknown": "не указано",
|
|
1010
|
+
"free": "бесплатно",
|
|
1011
|
+
"unlimited": "без лимита",
|
|
1012
|
+
"none": "—",
|
|
1013
|
+
"dialogTitle": "Расход: {label}",
|
|
1014
|
+
"dialogNote": "Официальный API отдаёт только проценты по этим трём окнам — разбивки по моделям у него нет. Суммы по моделям могут быть только локальной оценкой.",
|
|
1015
|
+
"dialogProbe": "Проверить снова",
|
|
1016
|
+
"close": "Закрыть",
|
|
1017
|
+
"probedAt": "Проверено {time}",
|
|
1018
|
+
"resetsAt": "Сброс {time}",
|
|
1019
|
+
"usageError": "Ошибка: {message}",
|
|
1020
|
+
"peakTitle": "Что сейчас выгоднее",
|
|
1021
|
+
"peakPlan": "Вы платите ¥{price} в месяц (${usd}) за тариф {plan} в OpenCode, а квота покрывает ¥{allowance} (${allowanceUsd}) по официальным ценам. Списание идёт по прайсу, то есть {discount} из 10 от цены — тот же запрос через OpenCode обходится вам в {percent}% официальной цены.",
|
|
1022
|
+
"peakNow": "Прямой доступ к официальному API тарифицируется по прайсу. Сейчас OpenCode — {goPeak}, официальный API — {officialPeak}{holiday}.",
|
|
1023
|
+
"peakGoPeak": "пик (квота расходуется вдвое быстрее)",
|
|
1024
|
+
"peakGoOff": "не пик",
|
|
1025
|
+
"peakOfficialPeak": "пик (цена вдвое выше)",
|
|
1026
|
+
"peakOfficialOff": "не пик",
|
|
1027
|
+
"peakAtPeak": "пик",
|
|
1028
|
+
"peakOffPeak": "не пик",
|
|
1029
|
+
"peakHoliday": "; из-за государственного праздника в Китае он весь день считается вне пика, а OpenCode — по-прежнему в пике",
|
|
1030
|
+
"peakExhausted": "Но квота за этот месяц уже израсходована, так что остаётся только официальный API.",
|
|
1031
|
+
"peakGoPay": "Вы платите через OpenCode",
|
|
1032
|
+
"peakOfficialPay": "Вы платите через официальный API",
|
|
1033
|
+
"peakVerdict": "Итог",
|
|
1034
|
+
"peakGoCheaper": "OpenCode дешевле на {percent}%",
|
|
1035
|
+
"peakOfficialCheaper": "Официальный API дешевле на {percent}%",
|
|
1036
|
+
"peakFootnote": "В расчёте на 1 млн выходных токенов (в счёте за код преобладает выход), курс ¥{fx}/$. Пик влияет только на скорость расхода квоты и не меняет вывод: в пределах квоты OpenCode всегда дешевле прямого официального API; исключения — исчерпанная квота или лимит отдельной модели, который закончится раньше.",
|
|
1037
|
+
"footerOfficial": "Проценты «официально 5 часов / неделя / месяц» выше приходят из API OpenCode и отражают реальный расход аккаунта — включая другие клиенты и другие машины.",
|
|
1038
|
+
"footerLocal": "Суммы по моделям — локальная оценка: учитывается только журнал сессий этого harness, поэтому разбивка по моделям и видна; вызовы из других инструментов локально не записываются.",
|
|
1039
|
+
"footerPeakRule": "Суммы считаются по ценам вне пика, для моделей DeepSeek в часы пика применяется коэффициент 2. Бесплатные модели не тарифицируются.",
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
1042
|
+
/* @@EXTRA_LOCALES_END@@ */
|
|
1043
|
+
|
|
1044
|
+
const localeOf = (tag) => {
|
|
1045
|
+
const value = String(tag || '').toLowerCase();
|
|
1046
|
+
if (value.startsWith('zh')) return /tw|hk|mo|hant/.test(value) ? 'zh-tw' : 'zh';
|
|
1047
|
+
for (const key of Object.keys(DICT)) if (key !== 'zh' && key !== 'zh-tw' && value.startsWith(key.split('-')[0])) return key;
|
|
1048
|
+
return 'en';
|
|
1049
|
+
};
|
|
1050
|
+
const LANG = localeOf(typeof navigator === 'undefined' ? '' : navigator.language);
|
|
1051
|
+
const dict = DICT[LANG] ?? DICT.en;
|
|
1052
|
+
const t = (key, vars) => {
|
|
1053
|
+
const template = dict[key] ?? DICT.en[key] ?? key;
|
|
1054
|
+
if (vars === undefined) return template;
|
|
1055
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => (vars[name] === undefined ? match : String(vars[name])));
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
const money = (value) => {
|
|
1059
|
+
if (value === undefined || value === null) return t('none');
|
|
1060
|
+
if (value === 'free') return t('free');
|
|
1061
|
+
if (value === Infinity) return t('unlimited');
|
|
1062
|
+
return value < 1 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
|
|
1063
|
+
};
|
|
1064
|
+
const tokens = (n) => (n >= 1000000 ? `${(n / 1000000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
|
|
1065
|
+
const yuan = (value) => {
|
|
1066
|
+
if (value === undefined || value === null) return t('none');
|
|
1067
|
+
return value >= 1 ? `¥${value.toFixed(2)}` : `¥${value.toFixed(3)}`;
|
|
1068
|
+
};
|
|
1069
|
+
const percentText = (used, limit, free) => {
|
|
1070
|
+
if (free) return t('unlimited');
|
|
1071
|
+
if (limit === undefined || limit === null) return t('none');
|
|
1072
|
+
if (limit === Infinity || limit === 'free') return t('unlimited');
|
|
1073
|
+
if (!limit) return t('none');
|
|
1074
|
+
const ratio = (used / limit) * 100;
|
|
1075
|
+
return `${ratio < 10 ? ratio.toFixed(1) : ratio.toFixed(0)}%`;
|
|
1076
|
+
};
|
|
1077
|
+
const official = (window) => {
|
|
1078
|
+
const value = window && typeof window.percent === 'number' ? window.percent : null;
|
|
1079
|
+
if (value === null) return t('none');
|
|
1080
|
+
const color = value >= 100 ? '#d9534f' : value >= 80 ? '#e0a030' : 'inherit';
|
|
1081
|
+
return h('span', { style: { color } }, `${value < 10 ? value.toFixed(1) : Math.round(value)}%`);
|
|
1082
|
+
};
|
|
1083
|
+
const clock = (ms) => (ms ? new Date(ms).toLocaleString() : t('none'));
|
|
1084
|
+
const when = (ms) => {
|
|
1085
|
+
if (!ms) return t('none');
|
|
1086
|
+
const delta = ms - Date.now();
|
|
1087
|
+
if (delta <= 0) return localeOf(navigator.language) === 'zh' || LANG === 'zh-tw' ? '已到期' : 'expired';
|
|
1088
|
+
const minutes = Math.round(delta / 60000);
|
|
1089
|
+
const relative = LANG === 'zh' || LANG === 'zh-tw'
|
|
1090
|
+
? `${minutes < 60 ? `${minutes} 分钟` : `${Math.round(minutes / 60)} 小时`}后`
|
|
1091
|
+
: new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1092
|
+
return relative;
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const btn = {
|
|
1096
|
+
padding: '5px 12px', borderRadius: 6, border: '1px solid rgba(127,127,127,.45)',
|
|
1097
|
+
background: 'transparent', color: 'inherit', cursor: 'pointer', font: 'inherit', fontSize: 12,
|
|
1098
|
+
lineHeight: 1.6, whiteSpace: 'nowrap',
|
|
1099
|
+
};
|
|
1100
|
+
const input = {
|
|
1101
|
+
padding: '4px 8px', borderRadius: 6, border: '1px solid rgba(127,127,127,.45)',
|
|
1102
|
+
background: 'transparent', color: 'inherit', font: 'inherit', fontSize: 13,
|
|
1103
|
+
};
|
|
1104
|
+
const cell = (content, extra) =>
|
|
1105
|
+
h('td', { style: { padding: '5px 10px', borderBottom: '1px solid rgba(127,127,127,.22)', whiteSpace: 'nowrap', ...extra } }, content);
|
|
1106
|
+
const th = (title) =>
|
|
1107
|
+
h('th', {
|
|
1108
|
+
key: title,
|
|
1109
|
+
style: {
|
|
1110
|
+
textAlign: 'left', padding: '6px 10px', fontWeight: 600, whiteSpace: 'nowrap',
|
|
1111
|
+
borderBottom: '1px solid rgba(127,127,127,.3)', position: 'sticky', top: 0,
|
|
1112
|
+
background: 'var(--dsh-surface, rgba(127,127,127,.08))', backdropFilter: 'blur(4px)',
|
|
1113
|
+
},
|
|
1114
|
+
}, title);
|
|
1115
|
+
|
|
1116
|
+
// Horizontal drag-to-scroll for wide tables: press anywhere in the pane and
|
|
1117
|
+
// drag. A press that starts on a control is left alone, so buttons and inputs
|
|
1118
|
+
// keep working inside the scroller.
|
|
1119
|
+
const draggable = () => ({
|
|
1120
|
+
onMouseDown: (event) => {
|
|
1121
|
+
if (event.button !== 0) return;
|
|
1122
|
+
const tag = event.target && event.target.tagName;
|
|
1123
|
+
if (tag === 'BUTTON' || tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || tag === 'A') return;
|
|
1124
|
+
const pane = event.currentTarget;
|
|
1125
|
+
if (pane.scrollWidth <= pane.clientWidth + 1) return;
|
|
1126
|
+
const startX = event.clientX;
|
|
1127
|
+
const startLeft = pane.scrollLeft;
|
|
1128
|
+
const move = (moveEvent) => {
|
|
1129
|
+
pane.scrollLeft = startLeft - (moveEvent.clientX - startX);
|
|
1130
|
+
};
|
|
1131
|
+
const stop = () => {
|
|
1132
|
+
pane.style.cursor = 'grab';
|
|
1133
|
+
pane.style.userSelect = '';
|
|
1134
|
+
window.removeEventListener('mousemove', move);
|
|
1135
|
+
window.removeEventListener('mouseup', stop);
|
|
1136
|
+
};
|
|
1137
|
+
pane.style.cursor = 'grabbing';
|
|
1138
|
+
pane.style.userSelect = 'none';
|
|
1139
|
+
window.addEventListener('mousemove', move);
|
|
1140
|
+
window.addEventListener('mouseup', stop);
|
|
1141
|
+
},
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
const STATUS = () => ({
|
|
1145
|
+
ready: [t('statusReady'), '#3c9d5d'],
|
|
1146
|
+
exhausted: [t('statusExhausted'), '#e0a030'],
|
|
1147
|
+
auth: [t('statusAuth'), '#d9534f'],
|
|
1148
|
+
disabled: [t('statusDisabled'), 'rgba(127,127,127,.8)'],
|
|
1149
|
+
missing: [t('statusMissing'), '#d9534f'],
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
const capabilityTags = (row) => {
|
|
1153
|
+
const inputs = Array.isArray(row.inputs) && row.inputs.length > 0 ? row.inputs : ['text'];
|
|
1154
|
+
const labels = inputs.map((kind) => ({
|
|
1155
|
+
text: t('capsText'), image: t('capsImage'), video: t('capsVideo'),
|
|
1156
|
+
audio: t('capsAudio'), pdf: t('capsPdf'),
|
|
1157
|
+
}[kind] ?? kind));
|
|
1158
|
+
if (row.tools === true) labels.push(t('capsTools'));
|
|
1159
|
+
if (row.reasoning === true) labels.push(t('capsReasoning'));
|
|
1160
|
+
return labels;
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
function WindowBar({ window, label }) {
|
|
1164
|
+
const value = typeof window?.percent === 'number' ? window.percent : null;
|
|
1165
|
+
const width = value === null ? 0 : Math.max(2, Math.min(100, value));
|
|
1166
|
+
const color = value === null ? 'rgba(127,127,127,.4)' : value >= 100 ? '#d9534f' : value >= 80 ? '#e0a030' : '#3c9d5d';
|
|
1167
|
+
return h('div', { style: { marginBottom: 10 } },
|
|
1168
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 } },
|
|
1169
|
+
h('span', null, label),
|
|
1170
|
+
h('span', { style: { fontWeight: 600, color: value !== null && value >= 80 ? color : 'inherit' } },
|
|
1171
|
+
value === null ? t('none') : `${value < 10 ? value.toFixed(1) : Math.round(value)}%`),
|
|
1172
|
+
),
|
|
1173
|
+
h('div', { style: { height: 6, borderRadius: 999, background: 'rgba(127,127,127,.2)', overflow: 'hidden' } },
|
|
1174
|
+
h('div', { style: { width: `${width}%`, height: '100%', background: color, borderRadius: 999 } }),
|
|
1175
|
+
),
|
|
1176
|
+
window?.resetsAt
|
|
1177
|
+
? h('div', { style: { fontSize: 11, opacity: .55, marginTop: 3 } }, t('resetsAt', { time: new Date(window.resetsAt).toLocaleString() }))
|
|
1178
|
+
: null,
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function UsageDialog({ account, busy, onProbe, onClose }) {
|
|
1183
|
+
if (!account) return null;
|
|
1184
|
+
return h('div', {
|
|
1185
|
+
style: {
|
|
1186
|
+
position: 'fixed', inset: 0, background: 'rgba(0,0,0,.45)', zIndex: 1200,
|
|
1187
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
|
1188
|
+
},
|
|
1189
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) onClose(); },
|
|
1190
|
+
},
|
|
1191
|
+
h('div', {
|
|
1192
|
+
style: {
|
|
1193
|
+
minWidth: 380, maxWidth: 520, width: '100%', padding: '16px 18px', borderRadius: 10,
|
|
1194
|
+
background: 'var(--dsh-surface, #1b1b1b)', color: 'inherit',
|
|
1195
|
+
border: '1px solid rgba(127,127,127,.35)', boxShadow: '0 18px 48px rgba(0,0,0,.45)',
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 } },
|
|
1199
|
+
h('div', { style: { fontWeight: 600, fontSize: 14 } }, t('dialogTitle', { label: account.label })),
|
|
1200
|
+
account.active ? h('span', {
|
|
1201
|
+
style: { fontSize: 11, padding: '1px 6px', borderRadius: 999, border: '1px solid rgba(60,157,93,.6)', color: '#3c9d5d' },
|
|
1202
|
+
}, t('statusCurrent')) : null,
|
|
1203
|
+
h('span', { style: { flex: 1 } }),
|
|
1204
|
+
h('button', { type: 'button', style: btn, onClick: onClose }, t('close')),
|
|
1205
|
+
),
|
|
1206
|
+
h('div', { style: { fontSize: 12, opacity: .6, marginBottom: 12 } },
|
|
1207
|
+
account.hint ? h('span', { style: { fontFamily: 'ui-monospace, Consolas, monospace' } }, account.hint) : t('notConfigured')),
|
|
1208
|
+
h(WindowBar, { window: account.usage?.rolling, label: t('colRolling') }),
|
|
1209
|
+
h(WindowBar, { window: account.usage?.weekly, label: t('colWeekly') }),
|
|
1210
|
+
h(WindowBar, { window: account.usage?.monthly, label: t('colMonthly') }),
|
|
1211
|
+
account.usageError ? h('div', { style: { color: '#d9534f', fontSize: 12, marginBottom: 8 } }, t('usageError', { message: account.usageError })) : null,
|
|
1212
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 } },
|
|
1213
|
+
account.checkedAt ? h('span', { style: { fontSize: 11, opacity: .55 } }, t('probedAt', { time: clock(account.checkedAt) })) : null,
|
|
1214
|
+
h('span', { style: { flex: 1 } }),
|
|
1215
|
+
h('button', { type: 'button', style: btn, disabled: busy, onClick: onProbe }, t('dialogProbe')),
|
|
1216
|
+
),
|
|
1217
|
+
h('div', { style: { fontSize: 11, opacity: .55, marginTop: 12, lineHeight: 1.6 } }, t('dialogNote')),
|
|
1218
|
+
),
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function UsageSection() {
|
|
1223
|
+
const [state, setState] = React.useState({ phase: 'loading', usage: null, router: null, error: null });
|
|
1224
|
+
const [notice, setNotice] = React.useState(null);
|
|
1225
|
+
const [actionError, setActionError] = React.useState(null);
|
|
1226
|
+
const [draft, setDraft] = React.useState({ label: '', key: '' });
|
|
1227
|
+
const [editing, setEditing] = React.useState(null);
|
|
1228
|
+
const [keyDraft, setKeyDraft] = React.useState('');
|
|
1229
|
+
const [inspecting, setInspecting] = React.useState(null);
|
|
1230
|
+
const [busy, setBusy] = React.useState(false);
|
|
1231
|
+
|
|
1232
|
+
const load = React.useCallback(async (force) => {
|
|
1233
|
+
try {
|
|
1234
|
+
const [usageResponse, routerResponse] = await Promise.all([
|
|
1235
|
+
fetch(`${USAGE_API}${force ? '?refresh=1' : ''}`, { headers: { accept: 'application/json' } }),
|
|
1236
|
+
fetch(ROUTER_API, { headers: { accept: 'application/json' } }).catch(() => null),
|
|
1237
|
+
]);
|
|
1238
|
+
if (!usageResponse.ok) throw new Error(`HTTP ${usageResponse.status}`);
|
|
1239
|
+
const usage = await usageResponse.json();
|
|
1240
|
+
let router = null;
|
|
1241
|
+
if (routerResponse && routerResponse.ok) {
|
|
1242
|
+
try { router = await routerResponse.json(); } catch { router = null; }
|
|
1243
|
+
}
|
|
1244
|
+
setState({ phase: 'ready', usage, router, error: null });
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
setState((previous) => ({ ...previous, phase: 'error', error: String(error?.message ?? error) }));
|
|
1247
|
+
}
|
|
1248
|
+
}, []);
|
|
1249
|
+
|
|
1250
|
+
const act = React.useCallback(async (payload) => {
|
|
1251
|
+
setBusy(true);
|
|
1252
|
+
setActionError(null);
|
|
1253
|
+
setNotice(null);
|
|
1254
|
+
try {
|
|
1255
|
+
const response = await fetch(ROUTER_API, {
|
|
1256
|
+
method: 'POST',
|
|
1257
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
1258
|
+
body: JSON.stringify(payload),
|
|
1259
|
+
});
|
|
1260
|
+
const body = await response.json().catch(() => ({}));
|
|
1261
|
+
if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
|
|
1262
|
+
await load(true);
|
|
1263
|
+
return true;
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
setActionError(String(error?.message ?? error));
|
|
1266
|
+
return false;
|
|
1267
|
+
} finally {
|
|
1268
|
+
setBusy(false);
|
|
1269
|
+
}
|
|
1270
|
+
}, [load]);
|
|
1271
|
+
|
|
1272
|
+
React.useEffect(() => {
|
|
1273
|
+
let alive = true;
|
|
1274
|
+
const run = (force) => {
|
|
1275
|
+
if (!alive) return;
|
|
1276
|
+
void load(force);
|
|
1277
|
+
};
|
|
1278
|
+
run(false);
|
|
1279
|
+
const timer = setInterval(() => run(false), 60000);
|
|
1280
|
+
return () => {
|
|
1281
|
+
alive = false;
|
|
1282
|
+
clearInterval(timer);
|
|
1283
|
+
};
|
|
1284
|
+
}, [load]);
|
|
1285
|
+
|
|
1286
|
+
const data = state.usage;
|
|
1287
|
+
const router = state.router;
|
|
1288
|
+
const statusText = STATUS();
|
|
1289
|
+
|
|
1290
|
+
const routerAccounts = new Map((router?.accounts ?? []).map((account) => [account.id, account]));
|
|
1291
|
+
const usageAccounts = data?.accounts ?? [];
|
|
1292
|
+
const accountIds = router?.accounts?.length
|
|
1293
|
+
? router.accounts.map((account) => account.id)
|
|
1294
|
+
: usageAccounts.map((account) => account.id);
|
|
1295
|
+
const accountList = accountIds.map((id) => {
|
|
1296
|
+
const fromRouter = routerAccounts.get(id);
|
|
1297
|
+
const fromUsage = usageAccounts.find((account) => account.id === id);
|
|
1298
|
+
return {
|
|
1299
|
+
id,
|
|
1300
|
+
label: fromRouter?.label ?? fromUsage?.label ?? id,
|
|
1301
|
+
hint: fromRouter?.hint ?? fromUsage?.hint ?? null,
|
|
1302
|
+
keyRef: fromRouter?.keyRef,
|
|
1303
|
+
active: fromRouter?.active ?? fromUsage?.active ?? false,
|
|
1304
|
+
enabled: fromRouter?.enabled ?? fromUsage?.enabled ?? true,
|
|
1305
|
+
status: fromRouter?.status ?? null,
|
|
1306
|
+
exhaustedUntil: fromRouter?.exhaustedUntil ?? null,
|
|
1307
|
+
usage: fromRouter?.usage ?? fromUsage?.usage ?? null,
|
|
1308
|
+
usageError: fromUsage?.usageError ?? null,
|
|
1309
|
+
checkedAt: fromRouter?.checkedAt ?? fromUsage?.checkedAt ?? null,
|
|
1310
|
+
configured: fromRouter?.configured ?? fromUsage?.configured ?? false,
|
|
1311
|
+
};
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
const accountRows = accountList.map((account, index) => {
|
|
1315
|
+
const [text, color] = statusText[account.status] ?? [
|
|
1316
|
+
account.usageError ? t('statusError') : account.enabled === false ? t('statusDisabled') : account.active ? t('statusActive') : t('statusSpare'),
|
|
1317
|
+
account.usageError ? '#d9534f' : 'inherit',
|
|
1318
|
+
];
|
|
1319
|
+
return h(React.Fragment, { key: account.id },
|
|
1320
|
+
h('tr', null,
|
|
1321
|
+
cell(String(index + 1), { opacity: .6 }),
|
|
1322
|
+
cell(h('span', null,
|
|
1323
|
+
account.label,
|
|
1324
|
+
account.active ? h('span', {
|
|
1325
|
+
style: { marginLeft: 8, fontSize: 11, padding: '1px 6px', borderRadius: 999, border: '1px solid rgba(60,157,93,.6)', color: '#3c9d5d' },
|
|
1326
|
+
}, t('statusCurrent')) : null,
|
|
1327
|
+
)),
|
|
1328
|
+
cell(account.hint
|
|
1329
|
+
? h('span', { style: { fontFamily: 'ui-monospace, Consolas, monospace', fontSize: 12 } }, account.hint)
|
|
1330
|
+
: h('span', { style: { opacity: .55 } }, t('notConfigured'))),
|
|
1331
|
+
cell(official(account.usage?.rolling)),
|
|
1332
|
+
cell(official(account.usage?.weekly)),
|
|
1333
|
+
cell(official(account.usage?.monthly)),
|
|
1334
|
+
cell(h('span', { style: { color } },
|
|
1335
|
+
text,
|
|
1336
|
+
account.exhaustedUntil ? ` · ${clock(account.exhaustedUntil)}` : '',
|
|
1337
|
+
)),
|
|
1338
|
+
cell(router && h('div', {
|
|
1339
|
+
style: {
|
|
1340
|
+
display: 'grid', gridTemplateColumns: 'repeat(2, minmax(78px, max-content))',
|
|
1341
|
+
gap: 6, padding: '2px 0',
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
h('button', { type: 'button', style: btn, onClick: () => setInspecting(account.id) }, t('viewUsage')),
|
|
1345
|
+
h('button', { type: 'button', style: btn, disabled: busy || account.active, onClick: () => void act({ action: 'activate', id: account.id }) }, t('setActive')),
|
|
1346
|
+
h('button', { type: 'button', style: btn, disabled: busy, onClick: () => void act({ action: 'update', id: account.id, enabled: !account.enabled }) }, account.enabled ? t('disable') : t('enable')),
|
|
1347
|
+
h('button', {
|
|
1348
|
+
type: 'button', style: btn, disabled: busy,
|
|
1349
|
+
onClick: () => { void act({ action: 'move', id: account.id, direction: 'up' }); },
|
|
1350
|
+
}, '↑'),
|
|
1351
|
+
h('button', {
|
|
1352
|
+
type: 'button', style: btn, disabled: busy,
|
|
1353
|
+
onClick: () => { void act({ action: 'move', id: account.id, direction: 'down' }); },
|
|
1354
|
+
}, '↓'),
|
|
1355
|
+
h('button', {
|
|
1356
|
+
type: 'button', style: btn, disabled: busy,
|
|
1357
|
+
onClick: () => { setEditing(editing === account.id ? null : account.id); setKeyDraft(''); },
|
|
1358
|
+
}, t('editKey')),
|
|
1359
|
+
h('button', {
|
|
1360
|
+
type: 'button', style: { ...btn, color: '#d9534f' }, disabled: busy,
|
|
1361
|
+
onClick: () => {
|
|
1362
|
+
if (accountList.length <= 1) {
|
|
1363
|
+
setActionError(t('keepOne'));
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
void act({ action: 'remove', id: account.id });
|
|
1367
|
+
},
|
|
1368
|
+
}, t('remove')),
|
|
1369
|
+
) || h('span', { style: { opacity: .5, fontSize: 12 } }, t('readonly'))),
|
|
1370
|
+
),
|
|
1371
|
+
editing === account.id
|
|
1372
|
+
? h('tr', { key: `${account.id}-edit` },
|
|
1373
|
+
h('td', { colSpan: 8, style: { padding: '6px 10px', borderBottom: '1px solid rgba(127,127,127,.22)' } },
|
|
1374
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
1375
|
+
h('span', { style: { opacity: .7, fontSize: 12 } }, t('newKeyFor', { ref: account.keyRef ?? '' })),
|
|
1376
|
+
h('input', {
|
|
1377
|
+
style: { ...input, minWidth: 320 }, type: 'password', placeholder: 'oc_sk_…', value: keyDraft,
|
|
1378
|
+
onChange: (event) => setKeyDraft(event.target.value),
|
|
1379
|
+
}),
|
|
1380
|
+
h('button', {
|
|
1381
|
+
type: 'button', style: btn, disabled: busy || keyDraft.trim().length < 8,
|
|
1382
|
+
onClick: async () => {
|
|
1383
|
+
const ok = await act({ action: 'update', id: account.id, key: keyDraft.trim() });
|
|
1384
|
+
if (ok) {
|
|
1385
|
+
setEditing(null);
|
|
1386
|
+
setKeyDraft('');
|
|
1387
|
+
setNotice(t('keyUpdated'));
|
|
1388
|
+
}
|
|
1389
|
+
},
|
|
1390
|
+
}, t('save')),
|
|
1391
|
+
h('button', { type: 'button', style: btn, onClick: () => { setEditing(null); setKeyDraft(''); } }, t('cancel')),
|
|
1392
|
+
),
|
|
1393
|
+
),
|
|
1394
|
+
)
|
|
1395
|
+
: null,
|
|
1396
|
+
);
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
const addAccount = async () => {
|
|
1400
|
+
if (draft.key.trim().length < 8) {
|
|
1401
|
+
setActionError(t('shortKey'));
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const ok = await act({ action: 'add', label: draft.label.trim(), key: draft.key.trim() });
|
|
1405
|
+
if (ok) {
|
|
1406
|
+
setDraft({ label: '', key: '' });
|
|
1407
|
+
setNotice(t('accountAdded'));
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
const rows = (data?.models ?? []).map((row) =>
|
|
1412
|
+
h('tr', { key: row.model },
|
|
1413
|
+
cell(money(row.remaining), { fontWeight: 600 }),
|
|
1414
|
+
cell(row.free ? t('unlimited') : `${yuan(row.monthly)} / ${money(row.limit)}`),
|
|
1415
|
+
cell(yuan(row.weekly)),
|
|
1416
|
+
cell(percentText(row.rolling, row.rollingLimit, row.free)),
|
|
1417
|
+
cell(h('div', { style: { display: 'flex', flexDirection: 'column' } },
|
|
1418
|
+
h('span', { style: { fontFamily: 'ui-monospace, Consolas, monospace' } }, row.model),
|
|
1419
|
+
row.name && row.name !== row.model ? h('span', { style: { fontSize: 11, opacity: .55 } }, row.name) : null,
|
|
1420
|
+
)),
|
|
1421
|
+
cell(h('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap', maxWidth: 260 } },
|
|
1422
|
+
capabilityTags(row).map((label) => h('span', {
|
|
1423
|
+
key: label,
|
|
1424
|
+
style: {
|
|
1425
|
+
padding: '0 6px', borderRadius: 4, fontSize: 11,
|
|
1426
|
+
border: '1px solid rgba(127,127,127,.35)', opacity: .85,
|
|
1427
|
+
},
|
|
1428
|
+
}, label)),
|
|
1429
|
+
)),
|
|
1430
|
+
cell(String(row.requests)),
|
|
1431
|
+
cell(`${tokens(row.input)} / ${tokens(row.output)}`),
|
|
1432
|
+
),
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
const peak = data?.peak;
|
|
1436
|
+
const quotaSpent = typeof data?.usage?.monthly?.percent === 'number' ? data.usage.monthly.percent : null;
|
|
1437
|
+
const priceCell = (price, peakFlag, winner) => h('span', {
|
|
1438
|
+
style: winner
|
|
1439
|
+
? { fontWeight: 600, color: '#3c9d5d', background: 'rgba(60,157,93,.12)', padding: '1px 6px', borderRadius: 4 }
|
|
1440
|
+
: undefined,
|
|
1441
|
+
}, `${yuan(price)}(${peakFlag ? t('peakAtPeak') : t('peakOffPeak')})`);
|
|
1442
|
+
|
|
1443
|
+
const inspectingAccount = accountList.find((account) => account.id === inspecting) ?? null;
|
|
1444
|
+
|
|
1445
|
+
// Layout primitives: every block is a card with its own padding and gap, so
|
|
1446
|
+
// buttons never sit flush against each other or against the table edges.
|
|
1447
|
+
const card = (...children) => h('div', {
|
|
1448
|
+
style: {
|
|
1449
|
+
padding: '14px 16px', border: '1px solid rgba(127,127,127,.25)', borderRadius: 10,
|
|
1450
|
+
background: 'var(--dsh-surface, rgba(127,127,127,.04))',
|
|
1451
|
+
display: 'flex', flexDirection: 'column', gap: 12,
|
|
1452
|
+
},
|
|
1453
|
+
}, ...children);
|
|
1454
|
+
|
|
1455
|
+
const cardHead = (text, extra) => h('div', {
|
|
1456
|
+
style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' },
|
|
1457
|
+
},
|
|
1458
|
+
h('span', { style: { fontWeight: 600, fontSize: 13 } }, text),
|
|
1459
|
+
extra ?? null,
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
const stat = (label, value) => h('div', {
|
|
1463
|
+
style: { display: 'flex', flexDirection: 'column', gap: 3, minWidth: 110 },
|
|
1464
|
+
},
|
|
1465
|
+
h('span', { style: { fontSize: 11, opacity: .55 } }, label),
|
|
1466
|
+
h('span', { style: { fontSize: 13, fontWeight: 500 } }, value),
|
|
1467
|
+
);
|
|
1468
|
+
|
|
1469
|
+
const tools = (...children) => h('div', {
|
|
1470
|
+
style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' },
|
|
1471
|
+
}, ...children);
|
|
1472
|
+
|
|
1473
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 16 } },
|
|
1474
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } },
|
|
1475
|
+
h('div', { style: { fontSize: 16, fontWeight: 600 } }, t('title')),
|
|
1476
|
+
state.phase === 'ready' ? h('span', { style: { opacity: .55, fontSize: 12 } }, t('updatedAt', { time: clock(data?.checkedAt) })) : null,
|
|
1477
|
+
h('span', { style: { flex: 1 } }),
|
|
1478
|
+
h('button', { type: 'button', style: btn, disabled: busy, onClick: () => { void load(true); } }, busy ? t('refreshing') : t('refresh')),
|
|
1479
|
+
),
|
|
1480
|
+
state.phase === 'error' ? h('div', { style: { color: '#d9534f', fontSize: 13 } }, t('loadError', { message: state.error })) : null,
|
|
1481
|
+
actionError ? h('div', { style: { color: '#d9534f', fontSize: 13 } }, actionError) : null,
|
|
1482
|
+
notice ? h('div', { style: { color: '#3c9d5d', fontSize: 13 } }, notice) : null,
|
|
1483
|
+
|
|
1484
|
+
card(
|
|
1485
|
+
cardHead(t('routerTitle'), router
|
|
1486
|
+
? h('span', {
|
|
1487
|
+
style: {
|
|
1488
|
+
padding: '1px 8px', borderRadius: 999, fontSize: 12,
|
|
1489
|
+
border: `1px solid ${router.mode === 'fallback' ? 'rgba(224,160,48,.7)' : 'rgba(60,157,93,.6)'}`,
|
|
1490
|
+
color: router.mode === 'fallback' ? '#e0a030' : '#3c9d5d',
|
|
1491
|
+
},
|
|
1492
|
+
}, router.mode === 'fallback' ? t('modeFallback') : t('modePrimary'))
|
|
1493
|
+
: h('span', { style: { opacity: .7, fontSize: 12 } }, t('routerMissing'))),
|
|
1494
|
+
router?.reason ? h('div', { style: { fontSize: 12, opacity: .6 } }, t('modeReason', { reason: router.reason })) : null,
|
|
1495
|
+
router
|
|
1496
|
+
? h('div', { style: { display: 'flex', gap: 32, flexWrap: 'wrap' } },
|
|
1497
|
+
stat(t('activeAccount'), router.activeLabel ?? t('none')),
|
|
1498
|
+
stat(t('nextProbe'), router.nextProbeAt ? when(router.nextProbeAt) : t('none')),
|
|
1499
|
+
stat(t('colStatus'), router.mode === 'fallback' ? t('modeFallback') : t('modePrimary')),
|
|
1500
|
+
)
|
|
1501
|
+
: null,
|
|
1502
|
+
router
|
|
1503
|
+
? tools(
|
|
1504
|
+
h('button', { type: 'button', style: btn, disabled: busy, onClick: () => void act({ action: 'probe' }) }, t('probeAll')),
|
|
1505
|
+
h('button', { type: 'button', style: btn, disabled: busy, onClick: () => void act({ action: 'reset' }) }, t('resetMarks')),
|
|
1506
|
+
h('span', { style: { flexBasis: '100%', fontSize: 11, opacity: .55, lineHeight: 1.6 } }, t('resetHint')),
|
|
1507
|
+
)
|
|
1508
|
+
: null,
|
|
1509
|
+
),
|
|
1510
|
+
|
|
1511
|
+
card(
|
|
1512
|
+
cardHead(t('accountsTitle'), h('span', { style: { opacity: .55, fontSize: 12 } }, t('accountsOrder'))),
|
|
1513
|
+
h('div', {
|
|
1514
|
+
style: {
|
|
1515
|
+
overflowX: 'auto', cursor: 'grab', borderRadius: 8,
|
|
1516
|
+
border: '1px solid rgba(127,127,127,.18)',
|
|
1517
|
+
},
|
|
1518
|
+
...draggable(),
|
|
1519
|
+
},
|
|
1520
|
+
h('table', { style: { borderCollapse: 'collapse', width: '100%', fontSize: 12 } },
|
|
1521
|
+
h('thead', null,
|
|
1522
|
+
h('tr', null, [t('colIndex'), t('colAccount'), t('colKey'), t('colRolling'), t('colWeekly'), t('colMonthly'), t('colStatus'), t('colActions')].map((title) => th(title))),
|
|
1523
|
+
),
|
|
1524
|
+
h('tbody', null, accountRows.length > 0
|
|
1525
|
+
? accountRows
|
|
1526
|
+
: h('tr', null, h('td', { colSpan: 8, style: { padding: 14, opacity: .6 } }, t('noAccounts')))),
|
|
1527
|
+
),
|
|
1528
|
+
),
|
|
1529
|
+
router ? h('div', {
|
|
1530
|
+
style: {
|
|
1531
|
+
display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap',
|
|
1532
|
+
paddingTop: 12, borderTop: '1px solid rgba(127,127,127,.18)',
|
|
1533
|
+
},
|
|
1534
|
+
},
|
|
1535
|
+
h('label', { style: { display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11, opacity: .8 } },
|
|
1536
|
+
t('colAccount'),
|
|
1537
|
+
h('input', {
|
|
1538
|
+
style: { ...input, minWidth: 200 }, placeholder: t('labelPlaceholder'), value: draft.label,
|
|
1539
|
+
onChange: (event) => setDraft({ ...draft, label: event.target.value }),
|
|
1540
|
+
}),
|
|
1541
|
+
),
|
|
1542
|
+
h('label', { style: { display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11, opacity: .8 } },
|
|
1543
|
+
t('colKey'),
|
|
1544
|
+
h('input', {
|
|
1545
|
+
style: { ...input, minWidth: 320 }, type: 'password', placeholder: t('keyPlaceholder'), value: draft.key,
|
|
1546
|
+
onChange: (event) => setDraft({ ...draft, key: event.target.value }),
|
|
1547
|
+
}),
|
|
1548
|
+
),
|
|
1549
|
+
h('button', { type: 'button', style: btn, disabled: busy || draft.key.trim().length < 8, onClick: () => void addAccount() }, t('addAndProbe')),
|
|
1550
|
+
) : null,
|
|
1551
|
+
),
|
|
1552
|
+
|
|
1553
|
+
peak && card(
|
|
1554
|
+
cardHead(t('peakTitle')),
|
|
1555
|
+
h('div', { style: { fontSize: 12, opacity: .78, lineHeight: 1.8, marginBottom: 2 } },
|
|
1556
|
+
t('peakPlan', {
|
|
1557
|
+
plan: peak.plan.id,
|
|
1558
|
+
price: peak.plan.priceCny.toFixed(0),
|
|
1559
|
+
usd: peak.plan.priceUsd,
|
|
1560
|
+
allowance: peak.plan.allowanceCny.toFixed(0),
|
|
1561
|
+
allowanceUsd: peak.plan.allowanceUsd,
|
|
1562
|
+
discount: (10 / peak.plan.leverage).toFixed(1),
|
|
1563
|
+
percent: (100 / peak.plan.leverage).toFixed(0),
|
|
1564
|
+
})),
|
|
1565
|
+
h('div', { style: { fontSize: 12, opacity: .78, lineHeight: 1.8, marginBottom: 8 } },
|
|
1566
|
+
t('peakNow', {
|
|
1567
|
+
goPeak: peak.go.peak ? t('peakGoPeak') : t('peakGoOff'),
|
|
1568
|
+
officialPeak: peak.official.peak ? t('peakOfficialPeak') : t('peakOfficialOff'),
|
|
1569
|
+
holiday: peak.holiday && peak.go.peak ? t('peakHoliday') : '',
|
|
1570
|
+
}),
|
|
1571
|
+
quotaSpent !== null && quotaSpent >= 100 ? t('peakExhausted') : ''),
|
|
1572
|
+
h('table', { style: { borderCollapse: 'collapse', width: '100%', fontSize: 12 } },
|
|
1573
|
+
h('thead', null,
|
|
1574
|
+
h('tr', null, [t('colModel'), t('peakGoPay'), t('peakOfficialPay'), t('peakVerdict')].map((title) => th(title))),
|
|
1575
|
+
),
|
|
1576
|
+
h('tbody', null, peak.models.map((row) =>
|
|
1577
|
+
h('tr', { key: row.model },
|
|
1578
|
+
cell(h('span', { style: { fontFamily: 'ui-monospace, Consolas, monospace' } }, row.model)),
|
|
1579
|
+
cell(priceCell(row.go.pay, row.go.factor > 1, row.cheaper === 'go')),
|
|
1580
|
+
cell(priceCell(row.official.pay, row.official.factor > 1, row.cheaper === 'official')),
|
|
1581
|
+
cell(row.cheaper === 'go'
|
|
1582
|
+
? t('peakGoCheaper', { percent: Math.round((1 - row.ratio) * 100) })
|
|
1583
|
+
: t('peakOfficialCheaper', { percent: Math.round((row.ratio - 1) * 100) }), { fontWeight: 600 }),
|
|
1584
|
+
),
|
|
1585
|
+
)),
|
|
1586
|
+
),
|
|
1587
|
+
h('div', { style: { opacity: .55, fontSize: 11, marginTop: 6, lineHeight: 1.6 } },
|
|
1588
|
+
t('peakFootnote', { fx: peak.cnyPerUsd })),
|
|
1589
|
+
),
|
|
1590
|
+
|
|
1591
|
+
card(
|
|
1592
|
+
cardHead(t('modelsTitle')),
|
|
1593
|
+
h('div', {
|
|
1594
|
+
style: {
|
|
1595
|
+
overflowX: 'auto', maxHeight: '56vh', overflowY: 'auto', cursor: 'grab',
|
|
1596
|
+
borderRadius: 8, border: '1px solid rgba(127,127,127,.18)',
|
|
1597
|
+
},
|
|
1598
|
+
...draggable(),
|
|
1599
|
+
},
|
|
1600
|
+
h('table', { style: { borderCollapse: 'collapse', width: '100%', fontSize: 12 } },
|
|
1601
|
+
h('thead', null,
|
|
1602
|
+
h('tr', null, [t('colRemaining'), t('colMonthlyUsed'), t('colWeeklyUsed'), t('colRolling'), t('colModel'), t('colCaps'), t('colRequests'), t('colTokens')].map((title) => th(title))),
|
|
1603
|
+
),
|
|
1604
|
+
h('tbody', null, rows),
|
|
1605
|
+
),
|
|
1606
|
+
),
|
|
1607
|
+
),
|
|
1608
|
+
|
|
1609
|
+
h('div', { style: { opacity: .55, fontSize: 12, marginTop: 4, lineHeight: 1.8 } },
|
|
1610
|
+
t('footerOfficial'),
|
|
1611
|
+
h('br'),
|
|
1612
|
+
t('footerLocal'),
|
|
1613
|
+
),
|
|
1614
|
+
|
|
1615
|
+
h(UsageDialog, {
|
|
1616
|
+
account: inspectingAccount,
|
|
1617
|
+
busy,
|
|
1618
|
+
onClose: () => setInspecting(null),
|
|
1619
|
+
onProbe: async () => {
|
|
1620
|
+
if (!inspectingAccount) return;
|
|
1621
|
+
const ok = await act({ action: 'probe', id: inspectingAccount.id });
|
|
1622
|
+
if (ok) setNotice(t('probedAt', { time: clock(Date.now()) }));
|
|
1623
|
+
},
|
|
1624
|
+
}),
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return {
|
|
1629
|
+
inject: ['slots'],
|
|
1630
|
+
apply(ctx) {
|
|
1631
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
1632
|
+
name: 'settings.section',
|
|
1633
|
+
id: 'opencode-router',
|
|
1634
|
+
order: 18,
|
|
1635
|
+
label: 'OpenCode 路由与用量',
|
|
1636
|
+
}, UsageSection));
|
|
1637
|
+
},
|
|
1638
|
+
};
|
|
1639
|
+
},
|
|
1640
|
+
});
|