dsh-opencode-routing 2.2.1 → 2.4.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/CONTRIBUTING.md +46 -0
- package/README.ja.md +111 -0
- package/README.ko.md +154 -0
- package/README.md +102 -73
- package/README.ru.md +166 -0
- package/README.zh-CN.md +141 -0
- package/README.zh-TW.md +111 -0
- package/client.js +1519 -1137
- package/package.json +11 -1
- package/test.mjs +36 -1
- package/usage.js +87 -17
package/client.js
CHANGED
|
@@ -1,342 +1,411 @@
|
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
kindCacheRead: '缓存命中输入',
|
|
107
|
+
kindInput: '缓存未命中输入',
|
|
108
|
+
kindOutput: '输出',
|
|
109
|
+
peakHoliday: '——今天是中国法定节假日,官方整天算谷,OpenCode 照算峰',
|
|
110
|
+
colKind: '计费项',
|
|
111
|
+
peakExhausted: '但本月额度已经用尽,只能走官方。',
|
|
112
|
+
peakGoPay: 'OpenCode',
|
|
113
|
+
peakOfficialPay: 'DeepSeek 官方',
|
|
114
|
+
colOpenCode: 'OpenCode',
|
|
115
|
+
colOfficial: 'DeepSeek 官方',
|
|
116
|
+
peakVerdict: '结论',
|
|
117
|
+
clockTitle: '此刻的峰谷',
|
|
118
|
+
clockPeak: '高峰期',
|
|
119
|
+
clockOff: '低谷期',
|
|
120
|
+
clockFlip: '还剩 {hours} 小时到{next}',
|
|
121
|
+
clockHoliday: '今天是中国法定节假日,官方 API 全天按低谷计',
|
|
122
|
+
colRollingQuota: '5 小时额度',
|
|
123
|
+
capHintText: '纯文本:只吃文字、只出文字。',
|
|
124
|
+
capHintImage: '图像输入:能看截图、图表和 UI 稿(JPEG/PNG/GIF/WebP)。',
|
|
125
|
+
capHintVideo: '视频输入:能直接读 mp4 等视频文件并描述画面内容。',
|
|
126
|
+
capHintAudio: '音频输入:能直接读音频文件。',
|
|
127
|
+
capHintPdf: 'PDF 输入:能直接读 PDF 文档,不用先转成文本。',
|
|
128
|
+
capHintTools: '工具调用:模型可以调用 harness 提供的函数——读文件、跑命令、搜网页、写代码——再按返回结果继续干活。',
|
|
129
|
+
capHintReasoning: '推理:回答前先自己思考一段,难题更稳,但更慢、更费 token。',
|
|
130
|
+
backToTop: '回到顶部',
|
|
131
|
+
subscribe: '订阅 OpenCode',
|
|
132
|
+
copyKey: '复制',
|
|
133
|
+
copied: '已复制',
|
|
134
|
+
peakGoCheaper: '走 OpenCode 便宜 {percent}%',
|
|
135
|
+
verdictGo: '走 OpenCode 便宜 {percent}%',
|
|
136
|
+
verdictOfficial: '走官方直连便宜 {percent}%',
|
|
137
|
+
verdictExhausted: '本月额度已用尽,只能走官方直连',
|
|
138
|
+
demoOn: '看官方更便宜的样子',
|
|
139
|
+
demoOff: '回到真实数据',
|
|
140
|
+
demoNote: '示例数据:假设某个模型走 OpenCode 反而更贵(现实中只有额度用尽才会发生)。',
|
|
141
|
+
peakFootnote: '按每 100 万输出 tokens 折算(编码账单主要来自输出),汇率 ¥{fx}/$。峰谷只影响额度烧多快,不改变结论:额度内走 OpenCode 始终比官方直连便宜;例外是额度用尽,或某个模型的额度先见底。',
|
|
142
|
+
footerOfficial: '上面「官方 5 小时 / 本周 / 本月」百分比来自 OpenCode 接口,是账户真实用量,别的客户端或别的机器用了也会算进去。',
|
|
143
|
+
footerLocal: '按模型的金额是本机估算:只统计这个 harness 的会话日志,用来拆开看模型分布;其它工具的调用没有本地记录。',
|
|
144
|
+
footerPeakRule: '金额按离峰价估算,DeepSeek 模型峰时按 2 倍;免费模型不计费。',
|
|
145
|
+
},
|
|
146
|
+
en: {
|
|
147
|
+
title: 'OpenCode accounts & usage',
|
|
148
|
+
refresh: 'Refresh',
|
|
149
|
+
refreshing: 'Refreshing…',
|
|
150
|
+
updatedAt: 'Updated {time}',
|
|
151
|
+
loadError: 'Could not load: {message}',
|
|
152
|
+
routerTitle: 'Quota routing',
|
|
153
|
+
modePrimary: 'Routing normally',
|
|
154
|
+
modeFallback: 'Fell back to the official subscription',
|
|
155
|
+
modeReason: 'Reason: {reason}',
|
|
156
|
+
activeAccount: 'Active account',
|
|
157
|
+
nextProbe: 'Next probe',
|
|
158
|
+
probeAll: 'Probe now',
|
|
159
|
+
resetMarks: 'Clear blocked marks',
|
|
160
|
+
resetHint: 'Clears every "out of allowance / auth failed" mark and sends routing straight back to the OpenCode pool. Accounts and keys are untouched.',
|
|
161
|
+
routerMissing: 'No quota router detected — the account pool is read-only.',
|
|
162
|
+
accountsTitle: 'Account pool',
|
|
163
|
+
accountsOrder: 'Used in order: the next account only takes over once the one above runs dry.',
|
|
164
|
+
addAccount: 'Add an account',
|
|
165
|
+
labelPlaceholder: 'Label (optional, e.g. alt A)',
|
|
166
|
+
keyPlaceholder: 'oc_sk_…',
|
|
167
|
+
addAndProbe: 'Add and probe',
|
|
168
|
+
save: 'Save',
|
|
169
|
+
cancel: 'Cancel',
|
|
170
|
+
setActive: 'Make active',
|
|
171
|
+
disable: 'Disable',
|
|
172
|
+
enable: 'Enable',
|
|
173
|
+
editKey: 'Edit key',
|
|
174
|
+
remove: 'Delete',
|
|
175
|
+
viewUsage: 'View usage',
|
|
176
|
+
keyUpdated: 'Key updated',
|
|
177
|
+
accountAdded: 'Account added and probed',
|
|
178
|
+
shortKey: 'Key looks incomplete: at least 8 characters',
|
|
179
|
+
keepOne: 'Keep at least one account',
|
|
180
|
+
notConfigured: 'Not set',
|
|
181
|
+
readonly: 'read-only',
|
|
182
|
+
newKeyFor: 'New key is stored in {ref}',
|
|
183
|
+
noAccounts: 'No accounts yet. Add a key to begin.',
|
|
184
|
+
statusReady: 'Ready',
|
|
185
|
+
statusExhausted: 'Out of allowance',
|
|
186
|
+
statusAuth: 'Auth failed',
|
|
187
|
+
statusDisabled: 'Disabled',
|
|
188
|
+
statusMissing: 'No key',
|
|
189
|
+
statusError: 'Probe failed',
|
|
190
|
+
statusActive: 'In use',
|
|
191
|
+
statusSpare: 'Spare',
|
|
192
|
+
statusCurrent: 'current',
|
|
193
|
+
colIndex: '#',
|
|
194
|
+
colAccount: 'Account',
|
|
195
|
+
colKey: 'Key',
|
|
196
|
+
colRolling: '5 hours',
|
|
197
|
+
colWeekly: 'This week',
|
|
198
|
+
colMonthly: 'This month',
|
|
199
|
+
colStatus: 'Status',
|
|
200
|
+
colActions: 'Actions',
|
|
201
|
+
colModel: 'Model',
|
|
202
|
+
colCaps: 'Modality',
|
|
203
|
+
colRequests: 'Requests',
|
|
204
|
+
colTokens: 'Input / output tokens',
|
|
205
|
+
colRemaining: 'Left this month',
|
|
206
|
+
colMonthlyUsed: 'Used this month',
|
|
207
|
+
colWeeklyUsed: 'Used this week',
|
|
208
|
+
modelsTitle: 'Per model (local estimate)',
|
|
209
|
+
capsText: 'text',
|
|
210
|
+
capsImage: 'image',
|
|
211
|
+
capsVideo: 'video',
|
|
212
|
+
capsAudio: 'audio',
|
|
213
|
+
capsPdf: 'PDF',
|
|
214
|
+
capsTools: 'tools',
|
|
215
|
+
capsReasoning: 'reasoning',
|
|
216
|
+
capsUnknown: 'undeclared',
|
|
217
|
+
free: 'free',
|
|
218
|
+
unlimited: 'unlimited',
|
|
219
|
+
none: '—',
|
|
220
|
+
dialogTitle: 'Usage for {label}',
|
|
221
|
+
dialogNote: 'The official endpoint only reports these three windows; it has no per-model breakdown. Per-model money can only be a local estimate.',
|
|
222
|
+
dialogProbe: 'Probe again',
|
|
223
|
+
close: 'Close',
|
|
224
|
+
probedAt: 'Probed {time}',
|
|
225
|
+
resetsAt: 'Resets {time}',
|
|
226
|
+
usageError: 'Error: {message}',
|
|
227
|
+
peakTitle: 'Which route is cheaper right now',
|
|
228
|
+
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.',
|
|
229
|
+
peakNow: 'The official route bills list price directly. Right now OpenCode is {goPeak} and the official API is {officialPeak}{holiday}.',
|
|
230
|
+
peakGoPeak: 'at peak (allowance burns twice as fast)',
|
|
231
|
+
peakGoOff: 'off-peak',
|
|
232
|
+
peakOfficialPeak: 'at peak (price doubles)',
|
|
233
|
+
peakOfficialOff: 'off-peak',
|
|
234
|
+
peakAtPeak: 'peak',
|
|
235
|
+
peakOffPeak: 'off-peak',
|
|
236
|
+
kindCacheRead: 'cached input',
|
|
237
|
+
colKind: 'billing line',
|
|
238
|
+
kindInput: 'uncached input',
|
|
239
|
+
kindOutput: 'output',
|
|
240
|
+
peakHoliday: ' — today is a Chinese public holiday, so the official API charges off-peak all day while OpenCode still charges peak',
|
|
241
|
+
peakExhausted: ' But this month’s allowance is already spent, so only the official route is left.',
|
|
242
|
+
peakGoPay: 'OpenCode',
|
|
243
|
+
peakOfficialPay: 'DeepSeek official',
|
|
244
|
+
colOpenCode: 'OpenCode',
|
|
245
|
+
colOfficial: 'DeepSeek official',
|
|
246
|
+
peakVerdict: 'Verdict',
|
|
247
|
+
clockTitle: 'Peak / off-peak right now',
|
|
248
|
+
clockPeak: 'peak',
|
|
249
|
+
clockOff: 'off-peak',
|
|
250
|
+
clockFlip: '{hours} h until {next}',
|
|
251
|
+
clockHoliday: 'Chinese public holiday today, so the official API bills off-peak all day',
|
|
252
|
+
colRollingQuota: '5-hour allowance',
|
|
253
|
+
capHintText: 'Text only: plain text in, plain text out.',
|
|
254
|
+
capHintImage: 'Image input: understands screenshots, charts and UI mockups (JPEG/PNG/GIF/WebP).',
|
|
255
|
+
capHintVideo: 'Video input: reads video files such as mp4 and can describe what happens in them.',
|
|
256
|
+
capHintAudio: 'Audio input: reads audio files directly.',
|
|
257
|
+
capHintPdf: 'PDF input: reads PDF documents without converting them to text first.',
|
|
258
|
+
capHintTools: 'Tool calling: the model can call the functions the harness exposes (read files, run commands, search the web, write code) and act on the results.',
|
|
259
|
+
capHintReasoning: 'Reasoning: the model thinks before answering; better on hard tasks, but slower and it burns more tokens.',
|
|
260
|
+
backToTop: 'Back to top',
|
|
261
|
+
subscribe: 'Subscribe to OpenCode',
|
|
262
|
+
copyKey: 'Copy',
|
|
263
|
+
copied: 'Copied',
|
|
264
|
+
peakGoCheaper: 'OpenCode is {percent}% cheaper',
|
|
265
|
+
verdictGo: 'OpenCode is {percent}% cheaper',
|
|
266
|
+
verdictOfficial: 'Official API is {percent}% cheaper',
|
|
267
|
+
verdictExhausted: 'This month’s allowance is used up, so only the official API is left',
|
|
268
|
+
demoOn: 'Show the official-is-cheaper case',
|
|
269
|
+
demoOff: 'Back to real data',
|
|
270
|
+
demoNote: 'Sample data: pretend one model is more expensive on OpenCode — in practice that only happens once the allowance runs out.',
|
|
271
|
+
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.',
|
|
272
|
+
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.',
|
|
273
|
+
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.',
|
|
274
|
+
footerPeakRule: 'Money is estimated at off-peak prices, doubled during DeepSeek peak hours; free models are not billed.',
|
|
275
|
+
},
|
|
276
|
+
'zh-tw': {
|
|
277
|
+
title: 'OpenCode 帳號與用量',
|
|
278
|
+
refresh: '重新整理',
|
|
279
|
+
refreshing: '重新整理中…',
|
|
280
|
+
updatedAt: '更新於 {time}',
|
|
281
|
+
loadError: '讀取失敗:{message}',
|
|
282
|
+
routerTitle: '額度路由',
|
|
283
|
+
modePrimary: '正常路由',
|
|
284
|
+
modeFallback: '已回退到官方訂閱',
|
|
285
|
+
modeReason: '原因:{reason}',
|
|
286
|
+
activeAccount: '目前帳號',
|
|
287
|
+
nextProbe: '下次探測',
|
|
288
|
+
probeAll: '立即探測',
|
|
289
|
+
resetMarks: '重設封鎖標記',
|
|
290
|
+
resetHint: '清空所有「額度用盡/驗證失敗」標記,並讓路由立刻回到 OpenCode 帳號池;不會改動帳號與金鑰。',
|
|
291
|
+
routerMissing: '未偵測到額度路由,帳號池為唯讀。',
|
|
292
|
+
accountsTitle: '帳號池',
|
|
293
|
+
accountsOrder: '依序使用:前面的帳號用完才會用後面的。',
|
|
294
|
+
addAccount: '新增帳號',
|
|
295
|
+
labelPlaceholder: '備註(可選,例如 小號 A)',
|
|
296
|
+
keyPlaceholder: 'oc_sk_…',
|
|
297
|
+
addAndProbe: '新增並探測',
|
|
298
|
+
save: '儲存',
|
|
299
|
+
cancel: '取消',
|
|
300
|
+
setActive: '設為目前',
|
|
301
|
+
disable: '停用',
|
|
302
|
+
enable: '啟用',
|
|
303
|
+
editKey: '編輯金鑰',
|
|
304
|
+
remove: '刪除',
|
|
305
|
+
viewUsage: '查看用量',
|
|
306
|
+
keyUpdated: '金鑰已更新',
|
|
307
|
+
accountAdded: '帳號已新增並探測',
|
|
308
|
+
shortKey: '金鑰不完整:至少 8 個字元',
|
|
309
|
+
keepOne: '至少保留一個帳號',
|
|
310
|
+
notConfigured: '未設定',
|
|
311
|
+
readonly: '唯讀',
|
|
312
|
+
newKeyFor: '新金鑰寫入 {ref}',
|
|
313
|
+
noAccounts: '還沒有帳號。填入金鑰即可開始。',
|
|
314
|
+
statusReady: '可用',
|
|
315
|
+
statusExhausted: '額度用盡',
|
|
316
|
+
statusAuth: '驗證失敗',
|
|
317
|
+
statusDisabled: '已停用',
|
|
318
|
+
statusMissing: '未設定金鑰',
|
|
319
|
+
statusError: '讀取失敗',
|
|
320
|
+
statusActive: '目前使用',
|
|
321
|
+
statusSpare: '備用',
|
|
322
|
+
statusCurrent: '目前',
|
|
323
|
+
colIndex: '#',
|
|
324
|
+
colAccount: '帳號',
|
|
325
|
+
colKey: '金鑰',
|
|
326
|
+
colRolling: '5 小時',
|
|
327
|
+
colWeekly: '本週',
|
|
328
|
+
colMonthly: '本月',
|
|
329
|
+
colStatus: '狀態',
|
|
330
|
+
colActions: '操作',
|
|
331
|
+
colModel: '模型',
|
|
332
|
+
colCaps: '能力',
|
|
333
|
+
colRequests: '請求',
|
|
334
|
+
colTokens: '輸入 / 輸出 tokens',
|
|
335
|
+
colRemaining: '本月剩餘',
|
|
336
|
+
colMonthlyUsed: '本月用量',
|
|
337
|
+
colWeeklyUsed: '本週用量',
|
|
338
|
+
modelsTitle: '依模型(本機估算)',
|
|
339
|
+
capsText: '純文字',
|
|
340
|
+
capsImage: '圖像',
|
|
341
|
+
capsVideo: '影片',
|
|
342
|
+
capsAudio: '音訊',
|
|
343
|
+
capsPdf: 'PDF',
|
|
344
|
+
capsTools: '工具',
|
|
345
|
+
capsReasoning: '推理',
|
|
346
|
+
capsUnknown: '未宣告',
|
|
347
|
+
free: '免費',
|
|
348
|
+
unlimited: '無限',
|
|
349
|
+
none: '—',
|
|
350
|
+
dialogTitle: '{label} 的用量',
|
|
351
|
+
dialogNote: '官方介面只提供這三個窗口的百分比,沒有依模型拆分的明細;依模型的金額只能是本機估算。',
|
|
352
|
+
dialogProbe: '重新探測',
|
|
353
|
+
close: '關閉',
|
|
354
|
+
probedAt: '探測於 {time}',
|
|
355
|
+
resetsAt: '重置於 {time}',
|
|
356
|
+
usageError: '錯誤:{message}',
|
|
357
|
+
peakTitle: '此刻用哪個更划算',
|
|
358
|
+
peakPlan: '你為 OpenCode 的 {plan} 方案付 ¥{price}/月(${usd}),額度給到 ¥{allowance}(${allowanceUsd})的官方價用量。額度依官方價扣,等於打 {discount} 折——同一筆請求走 OpenCode 你只付官方價的 {percent}%。',
|
|
359
|
+
peakNow: '走官方直連照官方價付現。此刻 OpenCode 是{goPeak},官方是{officialPeak}{holiday}。',
|
|
360
|
+
peakGoPeak: '峰段(額度消耗 2 倍)',
|
|
361
|
+
peakGoOff: '谷段',
|
|
362
|
+
peakOfficialPeak: '峰段(價格 2 倍)',
|
|
363
|
+
peakOfficialOff: '谷段',
|
|
364
|
+
peakAtPeak: '高峰期',
|
|
365
|
+
peakOffPeak: '低谷期',
|
|
366
|
+
kindCacheRead: '緩存命中輸入',
|
|
367
|
+
colKind: '計費項',
|
|
368
|
+
kindInput: '緩存未命中輸入',
|
|
369
|
+
kindOutput: '輸出',
|
|
370
|
+
peakHoliday: '——今天是中國法定假日,官方整天算谷,OpenCode 照算峰',
|
|
371
|
+
peakExhausted: '但本月額度已用盡,只能走官方。',
|
|
372
|
+
peakGoPay: 'OpenCode',
|
|
373
|
+
peakOfficialPay: 'DeepSeek 官方',
|
|
374
|
+
colOpenCode: 'OpenCode',
|
|
375
|
+
colOfficial: 'DeepSeek 官方',
|
|
376
|
+
peakVerdict: '結論',
|
|
377
|
+
clockTitle: '此刻的峰谷',
|
|
378
|
+
clockPeak: '高峰期',
|
|
379
|
+
clockOff: '低谷期',
|
|
380
|
+
clockFlip: '還剩 {hours} 小時到{next}',
|
|
381
|
+
clockHoliday: '今天是中國法定節假日,官方 API 全天按低谷計',
|
|
382
|
+
colRollingQuota: '5 小時額度',
|
|
383
|
+
capHintText: '純文字:只吃文字、只出文字。',
|
|
384
|
+
capHintImage: '圖像輸入:能看截圖、圖表和 UI 稿(JPEG/PNG/GIF/WebP)。',
|
|
385
|
+
capHintVideo: '影片輸入:能直接讀 mp4 等影片檔並描述畫面內容。',
|
|
386
|
+
capHintAudio: '音訊輸入:能直接讀音訊檔。',
|
|
387
|
+
capHintPdf: 'PDF 輸入:能直接讀 PDF 文件,不用先轉成文字。',
|
|
388
|
+
capHintTools: '工具調用:模型可以調用 harness 提供的函式——讀檔、跑指令、搜網頁、寫程式——再依回傳結果繼續做事。',
|
|
389
|
+
capHintReasoning: '推理:回答前先自己想一段,難題更穩,但更慢、更耗 token。',
|
|
390
|
+
backToTop: '回到頂端',
|
|
391
|
+
subscribe: '訂閱 OpenCode',
|
|
392
|
+
copyKey: '複製',
|
|
393
|
+
copied: '已複製',
|
|
394
|
+
peakGoCheaper: '走 OpenCode 便宜 {percent}%',
|
|
395
|
+
verdictGo: '走 OpenCode 便宜 {percent}%',
|
|
396
|
+
verdictOfficial: '走官方直連便宜 {percent}%',
|
|
397
|
+
verdictExhausted: '本月額度已用盡,只能走官方直連',
|
|
398
|
+
demoOn: '看官方更便宜的樣子',
|
|
399
|
+
demoOff: '回到真實資料',
|
|
400
|
+
demoNote: '範例資料:假設某個模型走 OpenCode 反而更貴(現實中只有額度用盡才會發生)。',
|
|
401
|
+
peakFootnote: '依每 100 萬輸出 tokens 折算(編碼帳單主要來自輸出),匯率 ¥{fx}/$。峰谷只影響額度消耗速度,不改變結論:額度內走 OpenCode 始終比官方直連便宜;例外是額度用盡,或某個模型的額度先用完。',
|
|
402
|
+
footerOfficial: '上面「官方 5 小時/本週/本月」百分比來自 OpenCode 介面,是帳號真實用量,其他用戶端或機器用掉的也會算進去。',
|
|
403
|
+
footerLocal: '依模型的金額是本機估算:只統計這個 harness 的工作階段日誌,用來拆開看模型分布;其他工具的呼叫沒有本機記錄。',
|
|
404
|
+
footerPeakRule: '金額依離峰價估算,DeepSeek 模型峰時按 2 倍;免費模型不計費。',
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
|
|
340
409
|
/* @@EXTRA_LOCALES_BEGIN@@ */
|
|
341
410
|
Object.assign(DICT, {
|
|
342
411
|
"de": {
|
|
@@ -431,15 +500,43 @@ window.__ModuleLoader__.load({
|
|
|
431
500
|
"peakOffPeak": "Nebenzeit",
|
|
432
501
|
"peakHoliday": " – heute ist ein chinesischer gesetzlicher Feiertag: Die offizielle API rechnet den ganzen Tag Nebenzeit ab, OpenCode weiter Hauptzeit",
|
|
433
502
|
"peakExhausted": " Das Kontingent für diesen Monat ist aber schon aufgebraucht, es bleibt nur der offizielle Weg.",
|
|
434
|
-
"peakGoPay": "
|
|
435
|
-
"peakOfficialPay": "
|
|
503
|
+
"peakGoPay": "OpenCode",
|
|
504
|
+
"peakOfficialPay": "DeepSeek offiziell",
|
|
436
505
|
"peakVerdict": "Fazit",
|
|
437
506
|
"peakGoCheaper": "OpenCode ist {percent}% günstiger",
|
|
438
|
-
"peakOfficialCheaper": "Die offizielle API ist {percent}% günstiger",
|
|
439
507
|
"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.",
|
|
440
508
|
"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.",
|
|
441
509
|
"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.",
|
|
442
510
|
"footerPeakRule": "Beträge sind zum Nebenzeit-Preis geschätzt, bei DeepSeek-Modellen zur Hauptzeit doppelt; kostenlose Modelle werden nicht berechnet.",
|
|
511
|
+
"kindCacheRead": "zwischengespeicherte Eingabe",
|
|
512
|
+
"kindInput": "nicht zwischengespeicherte Eingabe",
|
|
513
|
+
"kindOutput": "Ausgabe",
|
|
514
|
+
"colKind": "Posten",
|
|
515
|
+
"verdictGo": "Über OpenCode {percent}% günstiger",
|
|
516
|
+
"verdictOfficial": "Über die offizielle API {percent}% günstiger",
|
|
517
|
+
"verdictExhausted": "Monatskontingent aufgebraucht – nur die offizielle API bleibt",
|
|
518
|
+
"demoOn": "Fall „offiziell günstiger\" zeigen",
|
|
519
|
+
"demoOff": "Zurück zu echten Daten",
|
|
520
|
+
"demoNote": "Beispieldaten: ein Modell, das über OpenCode teurer wäre (real nur bei aufgebrauchtem Kontingent).",
|
|
521
|
+
"colOpenCode": "OpenCode",
|
|
522
|
+
"colOfficial": "DeepSeek offiziell",
|
|
523
|
+
"clockTitle": "Haupt- und Nebenzeit gerade jetzt",
|
|
524
|
+
"clockPeak": "Hauptzeit",
|
|
525
|
+
"clockOff": "Nebenzeit",
|
|
526
|
+
"clockFlip": "noch {hours} Std. bis {next}",
|
|
527
|
+
"clockHoliday": "Heute ist chinesischer gesetzlicher Feiertag, deshalb rechnet die offizielle API den ganzen Tag Nebenzeit ab",
|
|
528
|
+
"colRollingQuota": "5-Stunden-Kontingent",
|
|
529
|
+
"capHintText": "Nur Text: reiner Text rein, reiner Text raus.",
|
|
530
|
+
"capHintImage": "Bildeingabe: versteht Screenshots, Diagramme und UI-Mockups (JPEG/PNG/GIF/WebP).",
|
|
531
|
+
"capHintVideo": "Videoeingabe: liest Videodateien wie mp4 und beschreibt, was darin passiert.",
|
|
532
|
+
"capHintAudio": "Audioeingabe: liest Audiodateien direkt.",
|
|
533
|
+
"capHintPdf": "PDF-Eingabe: liest PDF-Dokumente, ohne sie vorher in Text umzuwandeln.",
|
|
534
|
+
"capHintTools": "Tool-Aufruf: Das Modell kann die Funktionen aufrufen, die das Harness bereitstellt (Dateien lesen, Befehle ausführen, im Web suchen, Code schreiben), und mit den Ergebnissen arbeiten.",
|
|
535
|
+
"capHintReasoning": "Reasoning: Das Modell denkt vor der Antwort nach; besser bei schwierigen Aufgaben, dafür langsamer und mit mehr Tokenverbrauch.",
|
|
536
|
+
"backToTop": "Nach oben",
|
|
537
|
+
"subscribe": "OpenCode abonnieren",
|
|
538
|
+
"copyKey": "Kopieren",
|
|
539
|
+
"copied": "Kopiert",
|
|
443
540
|
},
|
|
444
541
|
"es": {
|
|
445
542
|
"title": "Cuentas y uso de OpenCode",
|
|
@@ -533,15 +630,43 @@ window.__ModuleLoader__.load({
|
|
|
533
630
|
"peakOffPeak": "hora valle",
|
|
534
631
|
"peakHoliday": " — hoy es festivo oficial en China: la API oficial cobra tarifa valle todo el día, mientras que OpenCode sigue cobrando pico",
|
|
535
632
|
"peakExhausted": " Pero la cuota de este mes ya está agotada, así que solo queda la ruta oficial.",
|
|
536
|
-
"peakGoPay": "
|
|
537
|
-
"peakOfficialPay": "
|
|
633
|
+
"peakGoPay": "OpenCode",
|
|
634
|
+
"peakOfficialPay": "DeepSeek oficial",
|
|
538
635
|
"peakVerdict": "Conclusión",
|
|
539
636
|
"peakGoCheaper": "OpenCode sale {percent}% más barato",
|
|
540
|
-
"peakOfficialCheaper": "La API oficial sale {percent}% más barata",
|
|
541
637
|
"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.",
|
|
542
638
|
"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.",
|
|
543
639
|
"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.",
|
|
544
640
|
"footerPeakRule": "El importe se estima a tarifa valle, duplicada en hora pico para los modelos DeepSeek; los modelos gratuitos no se facturan.",
|
|
641
|
+
"kindCacheRead": "entrada en caché",
|
|
642
|
+
"kindInput": "entrada sin caché",
|
|
643
|
+
"kindOutput": "salida",
|
|
644
|
+
"colKind": "Concepto",
|
|
645
|
+
"verdictGo": "Con OpenCode un {percent}% más barato",
|
|
646
|
+
"verdictOfficial": "Con la API oficial un {percent}% más barato",
|
|
647
|
+
"verdictExhausted": "Cuota mensual agotada: solo queda la API oficial",
|
|
648
|
+
"demoOn": "Ver el caso de que lo oficial sea más barato",
|
|
649
|
+
"demoOff": "Volver a los datos reales",
|
|
650
|
+
"demoNote": "Datos de ejemplo: un modelo que con OpenCode sería más caro (en la realidad solo pasa al agotar la cuota).",
|
|
651
|
+
"colOpenCode": "OpenCode",
|
|
652
|
+
"colOfficial": "DeepSeek oficial",
|
|
653
|
+
"clockTitle": "Hora pico / valle ahora mismo",
|
|
654
|
+
"clockPeak": "pico",
|
|
655
|
+
"clockOff": "valle",
|
|
656
|
+
"clockFlip": "{next} en {hours} h",
|
|
657
|
+
"clockHoliday": "Hoy es festivo oficial en China, así que la API oficial cobra tarifa valle todo el día",
|
|
658
|
+
"colRollingQuota": "Cuota de 5 horas",
|
|
659
|
+
"capHintText": "Solo texto: entra texto plano, sale texto plano.",
|
|
660
|
+
"capHintImage": "Entrada de imagen: entiende capturas, gráficos y maquetas de interfaz (JPEG/PNG/GIF/WebP).",
|
|
661
|
+
"capHintVideo": "Entrada de vídeo: lee archivos de vídeo como mp4 y describe lo que ocurre en ellos.",
|
|
662
|
+
"capHintAudio": "Entrada de audio: lee archivos de audio directamente.",
|
|
663
|
+
"capHintPdf": "Entrada de PDF: lee documentos PDF sin convertirlos antes a texto.",
|
|
664
|
+
"capHintTools": "Llamada a herramientas: el modelo puede llamar a las funciones que expone el harness (leer archivos, ejecutar comandos, buscar en la web, escribir código) y actuar con el resultado.",
|
|
665
|
+
"capHintReasoning": "Razonamiento: el modelo piensa antes de responder; va mejor en tareas difíciles, pero es más lento y gasta más tokens.",
|
|
666
|
+
"backToTop": "Volver arriba",
|
|
667
|
+
"subscribe": "Suscribirse a OpenCode",
|
|
668
|
+
"copyKey": "Copiar",
|
|
669
|
+
"copied": "Copiado",
|
|
545
670
|
},
|
|
546
671
|
"fr": {
|
|
547
672
|
"title": "Comptes et usage OpenCode",
|
|
@@ -635,15 +760,43 @@ window.__ModuleLoader__.load({
|
|
|
635
760
|
"peakOffPeak": "heures creuses",
|
|
636
761
|
"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",
|
|
637
762
|
"peakExhausted": " Mais le quota de ce mois est déjà épuisé : il ne reste que la route officielle.",
|
|
638
|
-
"peakGoPay": "
|
|
639
|
-
"peakOfficialPay": "
|
|
763
|
+
"peakGoPay": "OpenCode",
|
|
764
|
+
"peakOfficialPay": "DeepSeek officiel",
|
|
640
765
|
"peakVerdict": "Verdict",
|
|
641
766
|
"peakGoCheaper": "OpenCode coûte {percent}% de moins",
|
|
642
|
-
"peakOfficialCheaper": "L’API officielle coûte {percent}% de moins",
|
|
643
767
|
"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.",
|
|
644
768
|
"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.",
|
|
645
769
|
"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.",
|
|
646
770
|
"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.",
|
|
771
|
+
"kindCacheRead": "entrée en cache",
|
|
772
|
+
"kindInput": "entrée hors cache",
|
|
773
|
+
"kindOutput": "sortie",
|
|
774
|
+
"colKind": "Poste",
|
|
775
|
+
"verdictGo": "{percent}% moins cher via OpenCode",
|
|
776
|
+
"verdictOfficial": "{percent}% moins cher via l’API officielle",
|
|
777
|
+
"verdictExhausted": "Quota mensuel épuisé — il ne reste que l’API officielle",
|
|
778
|
+
"demoOn": "Voir le cas où l’officiel est moins cher",
|
|
779
|
+
"demoOff": "Revenir aux données réelles",
|
|
780
|
+
"demoNote": "Données d’exemple : un modèle plus cher via OpenCode (en pratique, seulement quota épuisé).",
|
|
781
|
+
"colOpenCode": "OpenCode",
|
|
782
|
+
"colOfficial": "DeepSeek officiel",
|
|
783
|
+
"clockTitle": "Heures pleines / creuses en ce moment",
|
|
784
|
+
"clockPeak": "heures pleines",
|
|
785
|
+
"clockOff": "heures creuses",
|
|
786
|
+
"clockFlip": "{next} dans {hours} h",
|
|
787
|
+
"clockHoliday": "Jour férié officiel en Chine aujourd’hui : l’API officielle facture en heures creuses toute la journée",
|
|
788
|
+
"colRollingQuota": "Quota de 5 heures",
|
|
789
|
+
"capHintText": "Texte seul : du texte simple en entrée, du texte simple en sortie.",
|
|
790
|
+
"capHintImage": "Entrée image : comprend les captures d’écran, les graphiques et les maquettes d’interface (JPEG/PNG/GIF/WebP).",
|
|
791
|
+
"capHintVideo": "Entrée vidéo : lit les fichiers vidéo comme mp4 et décrit ce qui s’y passe.",
|
|
792
|
+
"capHintAudio": "Entrée audio : lit directement les fichiers audio.",
|
|
793
|
+
"capHintPdf": "Entrée PDF : lit les documents PDF sans les convertir d’abord en texte.",
|
|
794
|
+
"capHintTools": "Appel d’outils : le modèle peut appeler les fonctions exposées par le harness (lire des fichiers, lancer des commandes, chercher sur le web, écrire du code) et exploiter les résultats.",
|
|
795
|
+
"capHintReasoning": "Raisonnement : le modèle réfléchit avant de répondre ; plus fort sur les tâches difficiles, mais plus lent et plus gourmand en tokens.",
|
|
796
|
+
"backToTop": "Retour en haut",
|
|
797
|
+
"subscribe": "S’abonner à OpenCode",
|
|
798
|
+
"copyKey": "Copier",
|
|
799
|
+
"copied": "Copié",
|
|
647
800
|
},
|
|
648
801
|
"ja": {
|
|
649
802
|
"title": "OpenCode アカウントと使用量",
|
|
@@ -737,15 +890,43 @@ window.__ModuleLoader__.load({
|
|
|
737
890
|
"peakOffPeak": "オフピーク",
|
|
738
891
|
"peakHoliday": " — 今日は中国の祝日なので、一日中オフピーク、OpenCode はピークのままです",
|
|
739
892
|
"peakExhausted": "ただし今月のクォータはすでに使い切っているため、公式を使うしかありません。",
|
|
740
|
-
"peakGoPay": "OpenCode
|
|
741
|
-
"peakOfficialPay": "
|
|
893
|
+
"peakGoPay": "OpenCode",
|
|
894
|
+
"peakOfficialPay": "DeepSeek 公式",
|
|
742
895
|
"peakVerdict": "結論",
|
|
743
896
|
"peakGoCheaper": "OpenCode のほうが {percent}% 安い",
|
|
744
|
-
"peakOfficialCheaper": "公式直結のほうが {percent}% 安い",
|
|
745
897
|
"peakFootnote": "出力 100 万トークンあたりに換算(コーディングの請求はほとんどが出力由来)、レートは ¥{fx}/$。ピークとオフピークはクォータの減る速さに影響するだけで、結論は変わりません。クォータの範囲内なら OpenCode 経由のほうが公式直結より常に安く、例外はクォータを使い切った場合と、特定モデルの上限に先に達した場合です。",
|
|
746
898
|
"footerOfficial": "上の「公式 5 時間 / 今週 / 今月」の割合は OpenCode の API が返す実際のアカウント使用量で、他のクライアントや別のマシンからの利用も含まれます。",
|
|
747
899
|
"footerLocal": "モデル別の金額はローカル推定です。この harness のセッションログだけを集計してモデル別の内訳を出すもので、他のツールからの呼び出しはローカルに記録されません。",
|
|
748
900
|
"footerPeakRule": "金額はオフピーク価格で見積もり、DeepSeek モデルはピーク時に 2 倍で計算します。無料モデルは課金されません。",
|
|
901
|
+
"kindCacheRead": "キャッシュ済み入力",
|
|
902
|
+
"kindInput": "未キャッシュ入力",
|
|
903
|
+
"kindOutput": "出力",
|
|
904
|
+
"colKind": "課金項目",
|
|
905
|
+
"verdictGo": "OpenCode 経由が {percent}% 安い",
|
|
906
|
+
"verdictOfficial": "公式 API 経由が {percent}% 安い",
|
|
907
|
+
"verdictExhausted": "今月の枠を使い切ったため公式 API のみ",
|
|
908
|
+
"demoOn": "公式が安い場合を表示",
|
|
909
|
+
"demoOff": "実際のデータに戻す",
|
|
910
|
+
"demoNote": "サンプル表示:OpenCode 経由のほうが高くなる例(実際は枠を使い切ったときのみ)。",
|
|
911
|
+
"colOpenCode": "OpenCode",
|
|
912
|
+
"colOfficial": "DeepSeek 公式",
|
|
913
|
+
"clockTitle": "現在のピーク / オフピーク",
|
|
914
|
+
"clockPeak": "ピーク",
|
|
915
|
+
"clockOff": "オフピーク",
|
|
916
|
+
"clockFlip": "{next}まであと {hours} 時間",
|
|
917
|
+
"clockHoliday": "今日は中国の祝日なので、公式 API は一日中オフピークの料金になります",
|
|
918
|
+
"colRollingQuota": "5 時間の枠",
|
|
919
|
+
"capHintText": "テキストのみ: プレーンテキストを入力し、プレーンテキストを返します。",
|
|
920
|
+
"capHintImage": "画像入力: スクリーンショット、グラフ、UI モックアップを理解します(JPEG/PNG/GIF/WebP)。",
|
|
921
|
+
"capHintVideo": "動画入力: mp4 などの動画ファイルを読み、中で何が起きているかを説明できます。",
|
|
922
|
+
"capHintAudio": "音声入力: 音声ファイルをそのまま読み取ります。",
|
|
923
|
+
"capHintPdf": "PDF 入力: PDF 文書をテキストに変換せずにそのまま読み取ります。",
|
|
924
|
+
"capHintTools": "ツール呼び出し: harness が公開している関数(ファイル読み取り、コマンド実行、Web 検索、コード記述)を呼び出し、その結果を使って動けます。",
|
|
925
|
+
"capHintReasoning": "推論: 答える前に考えるため難しいタスクに強い反面、遅く、トークンも多く消費します。",
|
|
926
|
+
"backToTop": "トップへ戻る",
|
|
927
|
+
"subscribe": "OpenCode を購読する",
|
|
928
|
+
"copyKey": "コピー",
|
|
929
|
+
"copied": "コピーしました",
|
|
749
930
|
},
|
|
750
931
|
"ko": {
|
|
751
932
|
"title": "OpenCode 계정 및 사용량",
|
|
@@ -839,15 +1020,43 @@ window.__ModuleLoader__.load({
|
|
|
839
1020
|
"peakOffPeak": "비피크",
|
|
840
1021
|
"peakHoliday": " — 오늘은 중국 공휴일이라 하루 종일 비피크, OpenCode는 피크 그대로입니다",
|
|
841
1022
|
"peakExhausted": "다만 이번 달 할당량을 이미 다 썼으므로 공식만 쓸 수 있습니다.",
|
|
842
|
-
"peakGoPay": "OpenCode
|
|
843
|
-
"peakOfficialPay": "공식
|
|
1023
|
+
"peakGoPay": "OpenCode",
|
|
1024
|
+
"peakOfficialPay": "DeepSeek 공식",
|
|
844
1025
|
"peakVerdict": "결론",
|
|
845
1026
|
"peakGoCheaper": "OpenCode가 {percent}% 더 저렴",
|
|
846
|
-
"peakOfficialCheaper": "공식 직결이 {percent}% 더 저렴",
|
|
847
1027
|
"peakFootnote": "출력 100만 토큰 기준 환산(코딩 요금은 대부분 출력에서 발생), 환율 ¥{fx}/$. 피크·비피크는 할당량이 줄어드는 속도에만 영향을 주고 결론은 바뀌지 않습니다. 할당량 안에서는 OpenCode 경유가 공식 직결보다 항상 저렴하며, 예외는 할당량을 다 쓴 경우와 특정 모델 한도가 먼저 바닥나는 경우입니다.",
|
|
848
1028
|
"footerOfficial": "위의 \"공식 5시간 / 이번 주 / 이번 달\" 비율은 OpenCode API가 주는 실제 계정 사용량이며, 다른 클라이언트나 다른 기기에서 쓴 양도 포함됩니다.",
|
|
849
1029
|
"footerLocal": "모델별 금액은 로컬 추정치입니다. 이 harness의 세션 로그만 집계해 모델별 분포를 보여 주는 것이며, 다른 도구의 호출은 로컬에 기록되지 않습니다.",
|
|
850
1030
|
"footerPeakRule": "금액은 비피크 가격으로 추정하고, DeepSeek 모델은 피크 시간에 2배로 계산합니다. 무료 모델은 과금되지 않습니다.",
|
|
1031
|
+
"kindCacheRead": "캐시된 입력",
|
|
1032
|
+
"kindInput": "캐시되지 않은 입력",
|
|
1033
|
+
"kindOutput": "출력",
|
|
1034
|
+
"colKind": "과금 항목",
|
|
1035
|
+
"verdictGo": "OpenCode 경유가 {percent}% 저렴",
|
|
1036
|
+
"verdictOfficial": "공식 API 경유가 {percent}% 저렴",
|
|
1037
|
+
"verdictExhausted": "이번 달 한도를 모두 사용해 공식 API만 가능",
|
|
1038
|
+
"demoOn": "공식이 더 저렴한 경우 보기",
|
|
1039
|
+
"demoOff": "실제 데이터로 돌아가기",
|
|
1040
|
+
"demoNote": "예시 데이터: OpenCode 경유가 더 비싼 경우(실제로는 한도를 다 썼을 때만).",
|
|
1041
|
+
"colOpenCode": "OpenCode",
|
|
1042
|
+
"colOfficial": "DeepSeek 공식",
|
|
1043
|
+
"clockTitle": "지금 피크 / 비피크",
|
|
1044
|
+
"clockPeak": "피크",
|
|
1045
|
+
"clockOff": "비피크",
|
|
1046
|
+
"clockFlip": "{next}까지 {hours}시간 남음",
|
|
1047
|
+
"clockHoliday": "오늘은 중국 공휴일이라 공식 API는 하루 종일 비피크 요금으로 계산됩니다",
|
|
1048
|
+
"colRollingQuota": "5시간 할당량",
|
|
1049
|
+
"capHintText": "텍스트 전용: 일반 텍스트를 넣으면 일반 텍스트로 답합니다.",
|
|
1050
|
+
"capHintImage": "이미지 입력: 스크린샷, 차트, UI 목업을 이해합니다(JPEG/PNG/GIF/WebP).",
|
|
1051
|
+
"capHintVideo": "영상 입력: mp4 같은 동영상 파일을 읽고 그 안에서 무슨 일이 일어나는지 설명합니다.",
|
|
1052
|
+
"capHintAudio": "오디오 입력: 오디오 파일을 그대로 읽습니다.",
|
|
1053
|
+
"capHintPdf": "PDF 입력: PDF 문서를 텍스트로 변환하지 않고 바로 읽습니다.",
|
|
1054
|
+
"capHintTools": "도구 호출: 모델이 harness가 노출하는 함수(파일 읽기, 명령 실행, 웹 검색, 코드 작성)를 호출하고 그 결과로 작업할 수 있습니다.",
|
|
1055
|
+
"capHintReasoning": "추론: 답하기 전에 생각하므로 어려운 작업에 강하지만 느리고 토큰을 더 씁니다.",
|
|
1056
|
+
"backToTop": "맨 위로",
|
|
1057
|
+
"subscribe": "OpenCode 구독하기",
|
|
1058
|
+
"copyKey": "복사",
|
|
1059
|
+
"copied": "복사됨",
|
|
851
1060
|
},
|
|
852
1061
|
"pt": {
|
|
853
1062
|
"title": "Contas e uso do OpenCode",
|
|
@@ -941,15 +1150,43 @@ window.__ModuleLoader__.load({
|
|
|
941
1150
|
"peakOffPeak": "fora do pico",
|
|
942
1151
|
"peakHoliday": " — hoje é feriado nacional na China: a API oficial cobra fora do pico o dia inteiro, enquanto o OpenCode continua cobrando pico",
|
|
943
1152
|
"peakExhausted": " Mas a cota deste mês já acabou, então só resta a rota oficial.",
|
|
944
|
-
"peakGoPay": "
|
|
945
|
-
"peakOfficialPay": "
|
|
1153
|
+
"peakGoPay": "OpenCode",
|
|
1154
|
+
"peakOfficialPay": "DeepSeek oficial",
|
|
946
1155
|
"peakVerdict": "Veredito",
|
|
947
1156
|
"peakGoCheaper": "O OpenCode sai {percent}% mais barato",
|
|
948
|
-
"peakOfficialCheaper": "A API oficial sai {percent}% mais barata",
|
|
949
1157
|
"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.",
|
|
950
1158
|
"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.",
|
|
951
1159
|
"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.",
|
|
952
1160
|
"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.",
|
|
1161
|
+
"kindCacheRead": "entrada em cache",
|
|
1162
|
+
"kindInput": "entrada sem cache",
|
|
1163
|
+
"kindOutput": "saída",
|
|
1164
|
+
"colKind": "Item",
|
|
1165
|
+
"verdictGo": "{percent}% mais barato via OpenCode",
|
|
1166
|
+
"verdictOfficial": "{percent}% mais barato via API oficial",
|
|
1167
|
+
"verdictExhausted": "Cota mensal esgotada — resta apenas a API oficial",
|
|
1168
|
+
"demoOn": "Ver o caso em que o oficial é mais barato",
|
|
1169
|
+
"demoOff": "Voltar aos dados reais",
|
|
1170
|
+
"demoNote": "Dados de exemplo: um modelo mais caro via OpenCode (na prática só com a cota esgotada).",
|
|
1171
|
+
"colOpenCode": "OpenCode",
|
|
1172
|
+
"colOfficial": "DeepSeek oficial",
|
|
1173
|
+
"clockTitle": "Pico / fora do pico agora",
|
|
1174
|
+
"clockPeak": "pico",
|
|
1175
|
+
"clockOff": "fora do pico",
|
|
1176
|
+
"clockFlip": "{next} em {hours} h",
|
|
1177
|
+
"clockHoliday": "Hoje é feriado nacional na China, então a API oficial cobra fora do pico o dia inteiro",
|
|
1178
|
+
"colRollingQuota": "Cota de 5 horas",
|
|
1179
|
+
"capHintText": "Só texto: entra texto puro, sai texto puro.",
|
|
1180
|
+
"capHintImage": "Entrada de imagem: entende capturas de tela, gráficos e mockups de interface (JPEG/PNG/GIF/WebP).",
|
|
1181
|
+
"capHintVideo": "Entrada de vídeo: lê arquivos de vídeo como mp4 e descreve o que acontece neles.",
|
|
1182
|
+
"capHintAudio": "Entrada de áudio: lê arquivos de áudio diretamente.",
|
|
1183
|
+
"capHintPdf": "Entrada de PDF: lê documentos PDF sem convertê-los antes para texto.",
|
|
1184
|
+
"capHintTools": "Chamada de ferramentas: o modelo pode chamar as funções que o harness expõe (ler arquivos, rodar comandos, buscar na web, escrever código) e agir com o resultado.",
|
|
1185
|
+
"capHintReasoning": "Raciocínio: o modelo pensa antes de responder; vai melhor em tarefas difíceis, mas é mais lento e gasta mais tokens.",
|
|
1186
|
+
"backToTop": "Voltar ao topo",
|
|
1187
|
+
"subscribe": "Assinar o OpenCode",
|
|
1188
|
+
"copyKey": "Copiar",
|
|
1189
|
+
"copied": "Copiado",
|
|
953
1190
|
},
|
|
954
1191
|
"ru": {
|
|
955
1192
|
"title": "Аккаунты и использование OpenCode",
|
|
@@ -1043,791 +1280,936 @@ window.__ModuleLoader__.load({
|
|
|
1043
1280
|
"peakOffPeak": "не пик",
|
|
1044
1281
|
"peakHoliday": "; из-за государственного праздника в Китае он весь день считается вне пика, а OpenCode — по-прежнему в пике",
|
|
1045
1282
|
"peakExhausted": "Но квота за этот месяц уже израсходована, так что остаётся только официальный API.",
|
|
1046
|
-
"peakGoPay": "
|
|
1047
|
-
"peakOfficialPay": "
|
|
1283
|
+
"peakGoPay": "OpenCode",
|
|
1284
|
+
"peakOfficialPay": "Официальный DeepSeek",
|
|
1048
1285
|
"peakVerdict": "Итог",
|
|
1049
1286
|
"peakGoCheaper": "OpenCode дешевле на {percent}%",
|
|
1050
|
-
"peakOfficialCheaper": "Официальный API дешевле на {percent}%",
|
|
1051
1287
|
"peakFootnote": "В расчёте на 1 млн выходных токенов (в счёте за код преобладает выход), курс ¥{fx}/$. Пик влияет только на скорость расхода квоты и не меняет вывод: в пределах квоты OpenCode всегда дешевле прямого официального API; исключения — исчерпанная квота или лимит отдельной модели, который закончится раньше.",
|
|
1052
1288
|
"footerOfficial": "Проценты «официально 5 часов / неделя / месяц» выше приходят из API OpenCode и отражают реальный расход аккаунта — включая другие клиенты и другие машины.",
|
|
1053
1289
|
"footerLocal": "Суммы по моделям — локальная оценка: учитывается только журнал сессий этого harness, поэтому разбивка по моделям и видна; вызовы из других инструментов локально не записываются.",
|
|
1054
1290
|
"footerPeakRule": "Суммы считаются по ценам вне пика, для моделей DeepSeek в часы пика применяется коэффициент 2. Бесплатные модели не тарифицируются.",
|
|
1291
|
+
"kindCacheRead": "вход из кеша",
|
|
1292
|
+
"kindInput": "вход без кеша",
|
|
1293
|
+
"kindOutput": "выход",
|
|
1294
|
+
"colKind": "Статья",
|
|
1295
|
+
"verdictGo": "Через OpenCode дешевле на {percent}%",
|
|
1296
|
+
"verdictOfficial": "Через официальный API дешевле на {percent}%",
|
|
1297
|
+
"verdictExhausted": "Месячная квота исчерпана — остаётся только официальный API",
|
|
1298
|
+
"demoOn": "Показать случай, когда официальный дешевле",
|
|
1299
|
+
"demoOff": "Вернуться к реальным данным",
|
|
1300
|
+
"demoNote": "Демо-данные: модель, которая через OpenCode дороже (в реальности — только при исчерпанной квоте).",
|
|
1301
|
+
"colOpenCode": "OpenCode",
|
|
1302
|
+
"colOfficial": "Официальный DeepSeek",
|
|
1303
|
+
"clockTitle": "Пик или не пик прямо сейчас",
|
|
1304
|
+
"clockPeak": "пик",
|
|
1305
|
+
"clockOff": "не пик",
|
|
1306
|
+
"clockFlip": "{next} через {hours} ч",
|
|
1307
|
+
"clockHoliday": "Сегодня государственный праздник в Китае, поэтому официальный API весь день считает по цене вне пика",
|
|
1308
|
+
"colRollingQuota": "Квота на 5 часов",
|
|
1309
|
+
"capHintText": "Только текст: на входе обычный текст, на выходе обычный текст.",
|
|
1310
|
+
"capHintImage": "Ввод изображений: понимает скриншоты, графики и макеты интерфейса (JPEG/PNG/GIF/WebP).",
|
|
1311
|
+
"capHintVideo": "Ввод видео: читает видеофайлы вроде mp4 и описывает, что в них происходит.",
|
|
1312
|
+
"capHintAudio": "Ввод аудио: читает аудиофайлы напрямую.",
|
|
1313
|
+
"capHintPdf": "Ввод PDF: читает PDF-документы, не переводя их сначала в текст.",
|
|
1314
|
+
"capHintTools": "Вызов инструментов: модель может вызывать функции, которые предоставляет harness (читать файлы, выполнять команды, искать в интернете, писать код), и действовать по результатам.",
|
|
1315
|
+
"capHintReasoning": "Рассуждение: модель думает перед ответом; лучше справляется со сложными задачами, но отвечает медленнее и тратит больше токенов.",
|
|
1316
|
+
"backToTop": "Наверх",
|
|
1317
|
+
"subscribe": "Оформить подписку на OpenCode",
|
|
1318
|
+
"copyKey": "Копировать",
|
|
1319
|
+
"copied": "Скопировано",
|
|
1055
1320
|
},
|
|
1056
1321
|
});
|
|
1057
|
-
/* @@EXTRA_LOCALES_END@@ */
|
|
1058
|
-
|
|
1059
|
-
const localeOf = (tag) => {
|
|
1060
|
-
const value = String(tag || '').toLowerCase();
|
|
1061
|
-
if (value.startsWith('zh')) return /tw|hk|mo|hant/.test(value) ? 'zh-tw' : 'zh';
|
|
1062
|
-
for (const key of Object.keys(DICT)) if (key !== 'zh' && key !== 'zh-tw' && value.startsWith(key.split('-')[0])) return key;
|
|
1063
|
-
return 'en';
|
|
1064
|
-
};
|
|
1065
|
-
const LANG = localeOf(typeof navigator === 'undefined' ? '' : navigator.language);
|
|
1066
|
-
const dict = DICT[LANG] ?? DICT.en;
|
|
1067
|
-
const t = (key, vars) => {
|
|
1068
|
-
const template = dict[key] ?? DICT.en[key] ?? key;
|
|
1069
|
-
if (vars === undefined) return template;
|
|
1070
|
-
return template.replace(/\{(\w+)\}/g, (match, name) => (vars[name] === undefined ? match : String(vars[name])));
|
|
1071
|
-
};
|
|
1072
|
-
|
|
1073
|
-
const money = (value) => {
|
|
1074
|
-
if (value === undefined || value === null) return t('none');
|
|
1075
|
-
if (value === 'free') return t('free');
|
|
1076
|
-
if (value === Infinity) return t('unlimited');
|
|
1077
|
-
return value < 1 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
|
|
1078
|
-
};
|
|
1079
|
-
const tokens = (n) => (n >= 1000000 ? `${(n / 1000000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
|
|
1080
|
-
const yuan = (value) => {
|
|
1081
|
-
if (value === undefined || value === null) return t('none');
|
|
1082
|
-
return value >= 1 ? `¥${value.toFixed(2)}` : `¥${value.toFixed(3)}`;
|
|
1083
|
-
};
|
|
1084
|
-
const percentText = (used, limit, free) => {
|
|
1085
|
-
if (free) return t('unlimited');
|
|
1086
|
-
if (limit === undefined || limit === null) return t('none');
|
|
1087
|
-
if (limit === Infinity || limit === 'free') return t('unlimited');
|
|
1088
|
-
if (!limit) return t('none');
|
|
1089
|
-
const ratio = (used / limit) * 100;
|
|
1090
|
-
return `${ratio < 10 ? ratio.toFixed(1) : ratio.toFixed(0)}%`;
|
|
1091
|
-
};
|
|
1092
|
-
const official = (window) => {
|
|
1093
|
-
const value = window && typeof window.percent === 'number' ? window.percent : null;
|
|
1094
|
-
if (value === null) return t('none');
|
|
1095
|
-
const color = value >= 100 ? '#d9534f' : value >= 80 ? '#e0a030' : 'inherit';
|
|
1096
|
-
return h('span', { style: { color } }, `${value < 10 ? value.toFixed(1) : Math.round(value)}%`);
|
|
1097
|
-
};
|
|
1098
|
-
const clock = (ms) => (ms ? new Date(ms).toLocaleString() : t('none'));
|
|
1099
|
-
const when = (ms) => {
|
|
1100
|
-
if (!ms) return t('none');
|
|
1101
|
-
const delta = ms - Date.now();
|
|
1102
|
-
if (delta <= 0) return LANG === 'zh' || LANG === 'zh-tw' ? '已到期' : 'expired';
|
|
1103
|
-
const minutes = Math.floor(delta / 60000);
|
|
1104
|
-
const seconds = Math.floor((delta % 60000) / 1000);
|
|
1105
|
-
const relative = LANG === 'zh' || LANG === 'zh-tw'
|
|
1106
|
-
? minutes < 60
|
|
1107
|
-
? `${minutes} 分 ${String(seconds).padStart(2, '0')} 秒后`
|
|
1108
|
-
: `${Math.round(minutes / 60)} 小时后`
|
|
1109
|
-
: new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1110
|
-
return relative;
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
// The dialog floats over the page, so it cannot inherit a translucent surface:
|
|
1114
|
-
// it needs a solid fill and an explicit text colour
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
}
|
|
1221
|
-
if (row.tools === true)
|
|
1222
|
-
if (row.reasoning === true)
|
|
1223
|
-
return
|
|
1224
|
-
};
|
|
1225
|
-
|
|
1226
|
-
function WindowBar({ window, label }) {
|
|
1227
|
-
const value = typeof window?.percent === 'number' ? window.percent : null;
|
|
1228
|
-
const width = value === null ? 0 : Math.max(2, Math.min(100, value));
|
|
1229
|
-
const color = value === null ? 'rgba(127,127,127,.4)' : value >= 100 ? '#d9534f' : value >= 80 ? '#e0a030' : '#3c9d5d';
|
|
1230
|
-
return h('div', { style: { marginBottom: 10 } },
|
|
1231
|
-
h('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 } },
|
|
1232
|
-
h('span', null, label),
|
|
1233
|
-
h('span', { style: { fontWeight: 600, color: value !== null && value >= 80 ? color : 'inherit' } },
|
|
1234
|
-
value === null ? t('none') : `${value < 10 ? value.toFixed(1) : Math.round(value)}%`),
|
|
1235
|
-
),
|
|
1236
|
-
h('div', { style: { height: 6, borderRadius: 999, background: 'rgba(127,127,127,.2)', overflow: 'hidden' } },
|
|
1237
|
-
h('div', { style: { width: `${width}%`, height: '100%', background: color, borderRadius: 999 } }),
|
|
1238
|
-
),
|
|
1239
|
-
window?.resetsAt
|
|
1240
|
-
? h('div', { style: { fontSize: 11, opacity: .55, marginTop: 3 } }, t('resetsAt', { time: new Date(window.resetsAt).toLocaleString() }))
|
|
1241
|
-
: null,
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
function UsageDialog({ account, busy, onProbe, onClose }) {
|
|
1246
|
-
if (!account) return null;
|
|
1247
|
-
const skin = theme();
|
|
1248
|
-
return h('div', {
|
|
1249
|
-
style: {
|
|
1250
|
-
position: 'fixed', inset: 0, background: skin.scrim, zIndex: 1200,
|
|
1251
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
|
1252
|
-
},
|
|
1253
|
-
onMouseDown: (event) => { if (event.target === event.currentTarget) onClose(); },
|
|
1254
|
-
},
|
|
1255
|
-
h('div', {
|
|
1256
|
-
style: {
|
|
1257
|
-
minWidth: 380, maxWidth: 520, width: '100%', padding: '16px 18px', borderRadius: 10,
|
|
1258
|
-
background: skin.panel, color: skin.text, opacity: 1,
|
|
1259
|
-
border: `1px solid ${skin.line}`, boxShadow: '0 18px 48px rgba(0,0,0,.45)',
|
|
1260
|
-
},
|
|
1261
|
-
},
|
|
1262
|
-
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 } },
|
|
1263
|
-
h('div', { style: { fontWeight: 600, fontSize: 14 } }, t('dialogTitle', { label: account.label })),
|
|
1264
|
-
account.active ? h('span', {
|
|
1265
|
-
style: { fontSize: 11, padding: '1px 6px', borderRadius: 999, border: '1px solid rgba(60,157,93,.6)', color: '#3c9d5d' },
|
|
1266
|
-
}, t('statusCurrent')) : null,
|
|
1267
|
-
h('span', { style: { flex: 1 } }),
|
|
1268
|
-
h('button', { type: 'button', className: 'ocr-btn', style: btn, onClick: onClose }, t('close')),
|
|
1269
|
-
),
|
|
1270
|
-
h('div', { style: { fontSize: 12, opacity: .6, marginBottom: 12 } },
|
|
1271
|
-
account.hint ? h('span', { style: { fontFamily: 'ui-monospace, Consolas, monospace' } }, account.hint) : t('notConfigured')),
|
|
1272
|
-
h(WindowBar, { window: account.usage?.rolling, label: t('colRolling') }),
|
|
1273
|
-
h(WindowBar, { window: account.usage?.weekly, label: t('colWeekly') }),
|
|
1274
|
-
h(WindowBar, { window: account.usage?.monthly, label: t('colMonthly') }),
|
|
1275
|
-
account.usageError ? h('div', { style: { color: '#d9534f', fontSize: 12, marginBottom: 8 } }, t('usageError', { message: account.usageError })) : null,
|
|
1276
|
-
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 } },
|
|
1277
|
-
account.checkedAt ? h('span', { style: { fontSize: 11, opacity: .55 } }, t('probedAt', { time: clock(account.checkedAt) })) : null,
|
|
1278
|
-
h('span', { style: { flex: 1 } }),
|
|
1279
|
-
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy, onClick: onProbe }, t('dialogProbe')),
|
|
1280
|
-
),
|
|
1281
|
-
h('div', { style: { fontSize: 11, opacity: .55, marginTop: 12, lineHeight: 1.6 } }, t('dialogNote')),
|
|
1282
|
-
),
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
function UsageSection() {
|
|
1287
|
-
const [state, setState] = React.useState({ phase: 'loading', usage: null, router: null, error: null });
|
|
1288
|
-
const [notice, setNotice] = React.useState(null);
|
|
1289
|
-
const [actionError, setActionError] = React.useState(null);
|
|
1290
|
-
const [draft, setDraft] = React.useState({ label: '', key: '' });
|
|
1291
|
-
const [editing, setEditing] = React.useState(null);
|
|
1292
|
-
const [keyDraft, setKeyDraft] = React.useState('');
|
|
1293
|
-
const [inspecting, setInspecting] = React.useState(null);
|
|
1294
|
-
const [busy, setBusy] = React.useState(false);
|
|
1295
|
-
const [demo, setDemo] = React.useState(false);
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
if (!
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
}
|
|
1368
|
-
}, [load]);
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
:
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
},
|
|
1501
|
-
},
|
|
1502
|
-
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}, t('
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
if (
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
}
|
|
1586
|
-
},
|
|
1587
|
-
}, t('
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
:
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
?
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
:
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1322
|
+
/* @@EXTRA_LOCALES_END@@ */
|
|
1323
|
+
|
|
1324
|
+
const localeOf = (tag) => {
|
|
1325
|
+
const value = String(tag || '').toLowerCase();
|
|
1326
|
+
if (value.startsWith('zh')) return /tw|hk|mo|hant/.test(value) ? 'zh-tw' : 'zh';
|
|
1327
|
+
for (const key of Object.keys(DICT)) if (key !== 'zh' && key !== 'zh-tw' && value.startsWith(key.split('-')[0])) return key;
|
|
1328
|
+
return 'en';
|
|
1329
|
+
};
|
|
1330
|
+
const LANG = localeOf(typeof navigator === 'undefined' ? '' : navigator.language);
|
|
1331
|
+
const dict = DICT[LANG] ?? DICT.en;
|
|
1332
|
+
const t = (key, vars) => {
|
|
1333
|
+
const template = dict[key] ?? DICT.en[key] ?? key;
|
|
1334
|
+
if (vars === undefined) return template;
|
|
1335
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => (vars[name] === undefined ? match : String(vars[name])));
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
const money = (value) => {
|
|
1339
|
+
if (value === undefined || value === null) return t('none');
|
|
1340
|
+
if (value === 'free') return t('free');
|
|
1341
|
+
if (value === Infinity) return t('unlimited');
|
|
1342
|
+
return value < 1 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
|
|
1343
|
+
};
|
|
1344
|
+
const tokens = (n) => (n >= 1000000 ? `${(n / 1000000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
|
|
1345
|
+
const yuan = (value) => {
|
|
1346
|
+
if (value === undefined || value === null) return t('none');
|
|
1347
|
+
return value >= 1 ? `¥${value.toFixed(2)}` : `¥${value.toFixed(3)}`;
|
|
1348
|
+
};
|
|
1349
|
+
const percentText = (used, limit, free) => {
|
|
1350
|
+
if (free) return t('unlimited');
|
|
1351
|
+
if (limit === undefined || limit === null) return t('none');
|
|
1352
|
+
if (limit === Infinity || limit === 'free') return t('unlimited');
|
|
1353
|
+
if (!limit) return t('none');
|
|
1354
|
+
const ratio = (used / limit) * 100;
|
|
1355
|
+
return `${ratio < 10 ? ratio.toFixed(1) : ratio.toFixed(0)}%`;
|
|
1356
|
+
};
|
|
1357
|
+
const official = (window) => {
|
|
1358
|
+
const value = window && typeof window.percent === 'number' ? window.percent : null;
|
|
1359
|
+
if (value === null) return t('none');
|
|
1360
|
+
const color = value >= 100 ? '#d9534f' : value >= 80 ? '#e0a030' : 'inherit';
|
|
1361
|
+
return h('span', { style: { color } }, `${value < 10 ? value.toFixed(1) : Math.round(value)}%`);
|
|
1362
|
+
};
|
|
1363
|
+
const clock = (ms) => (ms ? new Date(ms).toLocaleString() : t('none'));
|
|
1364
|
+
const when = (ms) => {
|
|
1365
|
+
if (!ms) return t('none');
|
|
1366
|
+
const delta = ms - Date.now();
|
|
1367
|
+
if (delta <= 0) return LANG === 'zh' || LANG === 'zh-tw' ? '已到期' : 'expired';
|
|
1368
|
+
const minutes = Math.floor(delta / 60000);
|
|
1369
|
+
const seconds = Math.floor((delta % 60000) / 1000);
|
|
1370
|
+
const relative = LANG === 'zh' || LANG === 'zh-tw'
|
|
1371
|
+
? minutes < 60
|
|
1372
|
+
? `${minutes} 分 ${String(seconds).padStart(2, '0')} 秒后`
|
|
1373
|
+
: `${Math.round(minutes / 60)} 小时后`
|
|
1374
|
+
: new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1375
|
+
return relative;
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
// The dialog floats over the page, so it cannot inherit a translucent surface:
|
|
1379
|
+
// it needs a solid fill and an explicit text colour. It stays light even in a
|
|
1380
|
+
// dark harness theme, matching OpenCode's own white/grey dialogs.
|
|
1381
|
+
const theme = () => ({
|
|
1382
|
+
panel: '#f7f7f8',
|
|
1383
|
+
text: '#1a1a1a',
|
|
1384
|
+
line: 'rgba(0,0,0,.12)',
|
|
1385
|
+
scrim: 'rgba(0,0,0,.38)',
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
// Model ids read badly in a table ("deepseek-v4.1-flash"); the page shows the
|
|
1389
|
+
// vendor casing plus spaces instead.
|
|
1390
|
+
const MODEL_CASE = [
|
|
1391
|
+
[/^deepseek/i, 'DeepSeek'], [/^glm/i, 'GLM'], [/^gpt/i, 'GPT'], [/^claude/i, 'Claude'],
|
|
1392
|
+
[/^grok/i, 'Grok'], [/^qwen/i, 'Qwen'], [/^kimi/i, 'Kimi'], [/^minimax/i, 'MiniMax'],
|
|
1393
|
+
[/^mimo/i, 'MiMo'], [/^longcat/i, 'LongCat'], [/^hunyuan/i, 'Hunyuan'], [/^hy\d/i, 'Hunyuan'],
|
|
1394
|
+
[/^muse/i, 'Muse'],
|
|
1395
|
+
];
|
|
1396
|
+
const prettyModel = (id) => {
|
|
1397
|
+
const text = String(id ?? '');
|
|
1398
|
+
let head = '';
|
|
1399
|
+
let rest = text;
|
|
1400
|
+
for (const [pattern, name] of MODEL_CASE) {
|
|
1401
|
+
if (!pattern.test(text)) continue;
|
|
1402
|
+
head = name;
|
|
1403
|
+
rest = text.replace(pattern, '');
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1406
|
+
const tail = rest
|
|
1407
|
+
.replace(/[-_]+/g, ' ')
|
|
1408
|
+
.split(' ')
|
|
1409
|
+
.filter(Boolean)
|
|
1410
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1411
|
+
.join(' ');
|
|
1412
|
+
return [head, tail].filter(Boolean).join(' ');
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
const btn = {
|
|
1416
|
+
padding: '5px 12px', borderRadius: 6, border: '1px solid rgba(127,127,127,.45)',
|
|
1417
|
+
background: 'transparent', color: 'inherit', cursor: 'pointer', font: 'inherit', fontSize: 12,
|
|
1418
|
+
lineHeight: 1.6, whiteSpace: 'nowrap',
|
|
1419
|
+
};
|
|
1420
|
+
const input = {
|
|
1421
|
+
padding: '4px 8px', borderRadius: 6, border: '1px solid rgba(127,127,127,.45)',
|
|
1422
|
+
background: 'transparent', color: 'inherit', font: 'inherit', fontSize: 13,
|
|
1423
|
+
};
|
|
1424
|
+
const cell = (content, extra) =>
|
|
1425
|
+
h('td', { style: { padding: '5px 10px', borderBottom: '1px solid rgba(127,127,127,.22)', whiteSpace: 'nowrap', ...extra } }, content);
|
|
1426
|
+
const th = (title) =>
|
|
1427
|
+
h('th', {
|
|
1428
|
+
key: title,
|
|
1429
|
+
style: {
|
|
1430
|
+
textAlign: 'left', padding: '6px 10px', fontWeight: 600, whiteSpace: 'nowrap',
|
|
1431
|
+
borderBottom: '1px solid rgba(127,127,127,.3)', position: 'sticky', top: 0,
|
|
1432
|
+
background: 'var(--dsh-surface, rgba(127,127,127,.08))', backdropFilter: 'blur(4px)',
|
|
1433
|
+
},
|
|
1434
|
+
}, title);
|
|
1435
|
+
|
|
1436
|
+
// Horizontal drag-to-scroll for wide tables: press anywhere in the pane and
|
|
1437
|
+
// drag. A press that starts on a control is left alone, so buttons and inputs
|
|
1438
|
+
// keep working inside the scroller.
|
|
1439
|
+
const draggable = () => ({
|
|
1440
|
+
onMouseDown: (event) => {
|
|
1441
|
+
if (event.button !== 0) return;
|
|
1442
|
+
const tag = event.target && event.target.tagName;
|
|
1443
|
+
if (tag === 'BUTTON' || tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || tag === 'A') return;
|
|
1444
|
+
const pane = event.currentTarget;
|
|
1445
|
+
if (pane.scrollWidth <= pane.clientWidth + 1) return;
|
|
1446
|
+
const startX = event.clientX;
|
|
1447
|
+
const startLeft = pane.scrollLeft;
|
|
1448
|
+
const move = (moveEvent) => {
|
|
1449
|
+
pane.scrollLeft = startLeft - (moveEvent.clientX - startX);
|
|
1450
|
+
};
|
|
1451
|
+
const stop = () => {
|
|
1452
|
+
pane.style.cursor = 'grab';
|
|
1453
|
+
pane.style.userSelect = '';
|
|
1454
|
+
window.removeEventListener('mousemove', move);
|
|
1455
|
+
window.removeEventListener('mouseup', stop);
|
|
1456
|
+
};
|
|
1457
|
+
pane.style.cursor = 'grabbing';
|
|
1458
|
+
pane.style.userSelect = 'none';
|
|
1459
|
+
window.addEventListener('mousemove', move);
|
|
1460
|
+
window.addEventListener('mouseup', stop);
|
|
1461
|
+
},
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
const STATUS = () => ({
|
|
1465
|
+
ready: [t('statusReady'), '#3c9d5d'],
|
|
1466
|
+
exhausted: [t('statusExhausted'), '#e0a030'],
|
|
1467
|
+
auth: [t('statusAuth'), '#d9534f'],
|
|
1468
|
+
disabled: [t('statusDisabled'), 'rgba(127,127,127,.8)'],
|
|
1469
|
+
missing: [t('statusMissing'), '#d9534f'],
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
const capabilityTags = (row) => {
|
|
1473
|
+
const inputs = Array.isArray(row.inputs) && row.inputs.length > 0 ? row.inputs : ['text'];
|
|
1474
|
+
const hintOf = {
|
|
1475
|
+
text: 'capHintText', image: 'capHintImage', video: 'capHintVideo',
|
|
1476
|
+
audio: 'capHintAudio', pdf: 'capHintPdf',
|
|
1477
|
+
};
|
|
1478
|
+
const tags = inputs.map((kind) => ({
|
|
1479
|
+
key: kind,
|
|
1480
|
+
label: {
|
|
1481
|
+
text: t('capsText'), image: t('capsImage'), video: t('capsVideo'),
|
|
1482
|
+
audio: t('capsAudio'), pdf: t('capsPdf'),
|
|
1483
|
+
}[kind] ?? kind,
|
|
1484
|
+
hint: t(hintOf[kind] ?? 'capHintText'),
|
|
1485
|
+
}));
|
|
1486
|
+
if (row.tools === true) tags.push({ key: 'tools', label: t('capsTools'), hint: t('capHintTools') });
|
|
1487
|
+
if (row.reasoning === true) tags.push({ key: 'reasoning', label: t('capsReasoning'), hint: t('capHintReasoning') });
|
|
1488
|
+
return tags;
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
function WindowBar({ window, label }) {
|
|
1492
|
+
const value = typeof window?.percent === 'number' ? window.percent : null;
|
|
1493
|
+
const width = value === null ? 0 : Math.max(2, Math.min(100, value));
|
|
1494
|
+
const color = value === null ? 'rgba(127,127,127,.4)' : value >= 100 ? '#d9534f' : value >= 80 ? '#e0a030' : '#3c9d5d';
|
|
1495
|
+
return h('div', { style: { marginBottom: 10 } },
|
|
1496
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 } },
|
|
1497
|
+
h('span', null, label),
|
|
1498
|
+
h('span', { style: { fontWeight: 600, color: value !== null && value >= 80 ? color : 'inherit' } },
|
|
1499
|
+
value === null ? t('none') : `${value < 10 ? value.toFixed(1) : Math.round(value)}%`),
|
|
1500
|
+
),
|
|
1501
|
+
h('div', { style: { height: 6, borderRadius: 999, background: 'rgba(127,127,127,.2)', overflow: 'hidden' } },
|
|
1502
|
+
h('div', { style: { width: `${width}%`, height: '100%', background: color, borderRadius: 999 } }),
|
|
1503
|
+
),
|
|
1504
|
+
window?.resetsAt
|
|
1505
|
+
? h('div', { style: { fontSize: 11, opacity: .55, marginTop: 3 } }, t('resetsAt', { time: new Date(window.resetsAt).toLocaleString() }))
|
|
1506
|
+
: null,
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function UsageDialog({ account, busy, onProbe, onClose }) {
|
|
1511
|
+
if (!account) return null;
|
|
1512
|
+
const skin = theme();
|
|
1513
|
+
return h('div', {
|
|
1514
|
+
style: {
|
|
1515
|
+
position: 'fixed', inset: 0, background: skin.scrim, zIndex: 1200,
|
|
1516
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
|
1517
|
+
},
|
|
1518
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) onClose(); },
|
|
1519
|
+
},
|
|
1520
|
+
h('div', {
|
|
1521
|
+
style: {
|
|
1522
|
+
minWidth: 380, maxWidth: 520, width: '100%', padding: '16px 18px', borderRadius: 10,
|
|
1523
|
+
background: skin.panel, color: skin.text, opacity: 1,
|
|
1524
|
+
border: `1px solid ${skin.line}`, boxShadow: '0 18px 48px rgba(0,0,0,.45)',
|
|
1525
|
+
},
|
|
1526
|
+
},
|
|
1527
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 } },
|
|
1528
|
+
h('div', { style: { fontWeight: 600, fontSize: 14 } }, t('dialogTitle', { label: account.label })),
|
|
1529
|
+
account.active ? h('span', {
|
|
1530
|
+
style: { fontSize: 11, padding: '1px 6px', borderRadius: 999, border: '1px solid rgba(60,157,93,.6)', color: '#3c9d5d' },
|
|
1531
|
+
}, t('statusCurrent')) : null,
|
|
1532
|
+
h('span', { style: { flex: 1 } }),
|
|
1533
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, onClick: onClose }, t('close')),
|
|
1534
|
+
),
|
|
1535
|
+
h('div', { style: { fontSize: 12, opacity: .6, marginBottom: 12 } },
|
|
1536
|
+
account.hint ? h('span', { style: { fontFamily: 'ui-monospace, Consolas, monospace' } }, account.hint) : t('notConfigured')),
|
|
1537
|
+
h(WindowBar, { window: account.usage?.rolling, label: t('colRolling') }),
|
|
1538
|
+
h(WindowBar, { window: account.usage?.weekly, label: t('colWeekly') }),
|
|
1539
|
+
h(WindowBar, { window: account.usage?.monthly, label: t('colMonthly') }),
|
|
1540
|
+
account.usageError ? h('div', { style: { color: '#d9534f', fontSize: 12, marginBottom: 8 } }, t('usageError', { message: account.usageError })) : null,
|
|
1541
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 } },
|
|
1542
|
+
account.checkedAt ? h('span', { style: { fontSize: 11, opacity: .55 } }, t('probedAt', { time: clock(account.checkedAt) })) : null,
|
|
1543
|
+
h('span', { style: { flex: 1 } }),
|
|
1544
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy, onClick: onProbe }, t('dialogProbe')),
|
|
1545
|
+
),
|
|
1546
|
+
h('div', { style: { fontSize: 11, opacity: .55, marginTop: 12, lineHeight: 1.6 } }, t('dialogNote')),
|
|
1547
|
+
),
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function UsageSection() {
|
|
1552
|
+
const [state, setState] = React.useState({ phase: 'loading', usage: null, router: null, error: null });
|
|
1553
|
+
const [notice, setNotice] = React.useState(null);
|
|
1554
|
+
const [actionError, setActionError] = React.useState(null);
|
|
1555
|
+
const [draft, setDraft] = React.useState({ label: '', key: '' });
|
|
1556
|
+
const [editing, setEditing] = React.useState(null);
|
|
1557
|
+
const [keyDraft, setKeyDraft] = React.useState('');
|
|
1558
|
+
const [inspecting, setInspecting] = React.useState(null);
|
|
1559
|
+
const [busy, setBusy] = React.useState(false);
|
|
1560
|
+
const [demo, setDemo] = React.useState(false);
|
|
1561
|
+
const [showTop, setShowTop] = React.useState(false);
|
|
1562
|
+
const scroller = React.useRef(null);
|
|
1563
|
+
// The settings pane scrolls, not the window, so the listener is attached in
|
|
1564
|
+
// the capture phase and remembers whichever element actually scrolled.
|
|
1565
|
+
React.useEffect(() => {
|
|
1566
|
+
const onScroll = (event) => {
|
|
1567
|
+
const target = event.target;
|
|
1568
|
+
if (target && target !== document && typeof target.scrollTop === 'number') {
|
|
1569
|
+
scroller.current = target;
|
|
1570
|
+
setShowTop(target.scrollTop > 320);
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
setShowTop((window.scrollY ?? document.documentElement.scrollTop ?? 0) > 320);
|
|
1574
|
+
};
|
|
1575
|
+
document.addEventListener('scroll', onScroll, true);
|
|
1576
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
1577
|
+
return () => {
|
|
1578
|
+
document.removeEventListener('scroll', onScroll, true);
|
|
1579
|
+
window.removeEventListener('scroll', onScroll);
|
|
1580
|
+
};
|
|
1581
|
+
}, []);
|
|
1582
|
+
const toTop = () => {
|
|
1583
|
+
const pane = scroller.current;
|
|
1584
|
+
if (pane && typeof pane.scrollTo === 'function') pane.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1585
|
+
else window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1586
|
+
};
|
|
1587
|
+
// Re-render every second so the "next probe" line counts down instead of
|
|
1588
|
+
// freezing at whatever it said when the page opened.
|
|
1589
|
+
const [, setTick] = React.useState(0);
|
|
1590
|
+
React.useEffect(() => {
|
|
1591
|
+
const timer = setInterval(() => setTick((value) => value + 1), 1000);
|
|
1592
|
+
return () => clearInterval(timer);
|
|
1593
|
+
}, []);
|
|
1594
|
+
|
|
1595
|
+
const load = React.useCallback(async (force) => {
|
|
1596
|
+
try {
|
|
1597
|
+
const [usageResponse, routerResponse] = await Promise.all([
|
|
1598
|
+
fetch(`${USAGE_API}${force ? '?refresh=1' : ''}`, { headers: { accept: 'application/json' } }),
|
|
1599
|
+
fetch(ROUTER_API, { headers: { accept: 'application/json' } }).catch(() => null),
|
|
1600
|
+
]);
|
|
1601
|
+
if (!usageResponse.ok) throw new Error(`HTTP ${usageResponse.status}`);
|
|
1602
|
+
const usage = await usageResponse.json();
|
|
1603
|
+
let router = null;
|
|
1604
|
+
if (routerResponse && routerResponse.ok) {
|
|
1605
|
+
try { router = await routerResponse.json(); } catch { router = null; }
|
|
1606
|
+
}
|
|
1607
|
+
setState({ phase: 'ready', usage, router, error: null });
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
setState((previous) => ({ ...previous, phase: 'error', error: String(error?.message ?? error) }));
|
|
1610
|
+
}
|
|
1611
|
+
}, []);
|
|
1612
|
+
|
|
1613
|
+
const act = React.useCallback(async (payload) => {
|
|
1614
|
+
setBusy(true);
|
|
1615
|
+
setActionError(null);
|
|
1616
|
+
setNotice(null);
|
|
1617
|
+
try {
|
|
1618
|
+
const response = await fetch(ROUTER_API, {
|
|
1619
|
+
method: 'POST',
|
|
1620
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
1621
|
+
body: JSON.stringify(payload),
|
|
1622
|
+
});
|
|
1623
|
+
const body = await response.json().catch(() => ({}));
|
|
1624
|
+
if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
|
|
1625
|
+
await load(true);
|
|
1626
|
+
return true;
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
setActionError(String(error?.message ?? error));
|
|
1629
|
+
return false;
|
|
1630
|
+
} finally {
|
|
1631
|
+
setBusy(false);
|
|
1632
|
+
}
|
|
1633
|
+
}, [load]);
|
|
1634
|
+
|
|
1635
|
+
// Editing a key starts from the stored value, so the input is pre-filled.
|
|
1636
|
+
const revealKey = React.useCallback(async (id) => {
|
|
1637
|
+
try {
|
|
1638
|
+
const response = await fetch(`${ROUTER_API}?key=${encodeURIComponent(id)}`, { headers: { accept: 'application/json' } });
|
|
1639
|
+
const body = await response.json().catch(() => ({}));
|
|
1640
|
+
if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
|
|
1641
|
+
if (typeof body?.key === 'string') setKeyDraft(body.key);
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
setActionError(String(error?.message ?? error));
|
|
1644
|
+
}
|
|
1645
|
+
}, []);
|
|
1646
|
+
|
|
1647
|
+
React.useEffect(() => {
|
|
1648
|
+
let alive = true;
|
|
1649
|
+
const run = (force) => {
|
|
1650
|
+
if (!alive) return;
|
|
1651
|
+
void load(force);
|
|
1652
|
+
};
|
|
1653
|
+
run(false);
|
|
1654
|
+
const timer = setInterval(() => run(false), 60000);
|
|
1655
|
+
return () => {
|
|
1656
|
+
alive = false;
|
|
1657
|
+
clearInterval(timer);
|
|
1658
|
+
};
|
|
1659
|
+
}, [load]);
|
|
1660
|
+
|
|
1661
|
+
const data = state.usage;
|
|
1662
|
+
const router = state.router;
|
|
1663
|
+
const statusText = STATUS();
|
|
1664
|
+
const chinese = LANG === 'zh' || LANG === 'zh-tw';
|
|
1665
|
+
|
|
1666
|
+
// The router stores machine reasons ("allowance recovered"); the page says
|
|
1667
|
+
// what actually happened instead of echoing the enum.
|
|
1668
|
+
const reasonText = (reason) => {
|
|
1669
|
+
const raw = String(reason ?? '');
|
|
1670
|
+
const pick = (zh, en) => (chinese ? zh : en);
|
|
1671
|
+
const rules = [
|
|
1672
|
+
[/^allowance recovered/i, pick('额度已恢复,已切回 OpenCode 账户池', 'allowance recovered, back on the OpenCode pool')],
|
|
1673
|
+
[/^reset in Settings/i, pick('在设置里重置了封禁标记', 'marks were reset in Settings')],
|
|
1674
|
+
[/^selected in Settings/i, pick('在设置里手动设为当前账户', 'selected by hand in Settings')],
|
|
1675
|
+
[/^first account added/i, pick('首个账户已启用', 'the first account was enabled')],
|
|
1676
|
+
[/^initial$/i, pick('初始状态', 'initial')],
|
|
1677
|
+
[/^loaded$/i, pick('从状态文件恢复', 'restored from the state file')],
|
|
1678
|
+
[/^failover: (\S+) unavailable/i, pick('上一个账户不可用,已自动换下一个', 'the previous account went unavailable, switched to the next one')],
|
|
1679
|
+
[/^account failover after (\w+)/i, pick('鉴权失败($1),已换下一个账户', 'authentication failed ($1), switched to the next account')],
|
|
1680
|
+
[/^quota failover from (\S+)/i, pick('$1 额度用尽,已换下一个账户', '$1 ran out of allowance, switched to the next account')],
|
|
1681
|
+
[/^every OpenCode account is out of allowance/i, pick('所有账户额度都用完了,暂时走官方直连', 'every account is out of allowance, using the official API for now')],
|
|
1682
|
+
[/^OpenCode authentication failed \((\w+)\)/i, pick('OpenCode 鉴权失败($1),暂时走官方直连', 'OpenCode authentication failed ($1), using the official API for now')],
|
|
1683
|
+
[/^OpenCode quota window exhausted/i, pick('本窗口额度用尽,暂时走官方直连', 'this allowance window is used up, using the official API for now')],
|
|
1684
|
+
[/^OpenCode quota exhausted/i, pick('额度用尽,暂时走官方直连', 'out of allowance, using the official API for now')],
|
|
1685
|
+
[/^active account (\S+) removed/i, pick('当前账户被删除,已自动换下一个', 'the active account was deleted, switched to the next one')],
|
|
1686
|
+
[/^auth \((.+)\)/i, pick('鉴权失败:$1', 'authentication failed: $1')],
|
|
1687
|
+
];
|
|
1688
|
+
for (const [pattern, text] of rules) {
|
|
1689
|
+
if (pattern.test(raw)) return raw.replace(pattern, text);
|
|
1690
|
+
}
|
|
1691
|
+
return raw;
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
// A vendor badge per model family: the mark and the tint both come from the
|
|
1695
|
+
// family, so a card's gradient matches its avatar.
|
|
1696
|
+
const LOGOS = {
|
|
1697
|
+
deepseek: 'M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45',
|
|
1698
|
+
claude: 'm4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z',
|
|
1699
|
+
grok: 'M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z',
|
|
1700
|
+
qwen: 'M23.919 14.545 20.817 9.17l1.47-2.544a.56.56 0 0 0 0-.566l-1.633-2.83a.57.57 0 0 0-.49-.283h-6.207L12.487.402a.57.57 0 0 0-.49-.284H8.732a.56.56 0 0 0-.49.284L5.139 5.775h-2.94a.56.56 0 0 0-.49.284L.077 8.887a.56.56 0 0 0 0 .567L3.18 14.83l-1.47 2.545a.56.56 0 0 0 0 .566l1.634 2.83a.57.57 0 0 0 .49.283h6.205l1.47 2.545a.57.57 0 0 0 .49.284h3.266a.57.57 0 0 0 .49-.284l3.104-5.375h2.94a.57.57 0 0 0 .49-.283l1.634-2.828a.55.55 0 0 0-.004-.568M8.733.686l1.634 2.828-1.634 2.828H21.8L20.164 9.17H7.425L5.63 6.06Zm1.306 19.801-6.205-.002 1.634-2.83h3.265L2.201 6.344h3.267q3.182 5.517 6.367 11.032zm10.124-5.66L18.53 12l-6.532 11.315-1.634-2.83c2.129-3.673 4.25-7.351 6.373-11.028h3.592l3.102 5.374z',
|
|
1701
|
+
kimi: 'M21.765.351C22.998.351 24 1.353 24 2.586S22.998 4.82 21.765 4.82h-1.974c-.15 0-.26-.12-.26-.26V2.586A2.237 2.237 0 0 1 21.765.35M9.41 13.388l8.447-8.377c.16-.16.07-.471-.14-.471h-4.55s-.1.02-.14.06l-9.099 9.029c-.14.14-.35.02-.35-.21V4.81c0-.15-.1-.27-.221-.27H.22c-.12 0-.22.12-.22.27v18.57c0 .15.1.27.22.27h3.137c.12 0 .22-.12.22-.27v-3.79c0-.08.03-.16.08-.21l2.826-2.796c.07-.07.16-.08.241-.03l7.546 5.551a8.9 8.9 0 0 0 4.018 1.493c.12.01.23-.11.23-.27V19.76c0-.14-.08-.25-.19-.26a5.8 5.8 0 0 1-2.355-.942l-6.533-4.73c-.14-.09-.15-.32-.03-.441',
|
|
1702
|
+
minimax: 'M11.43 3.92a.86.86 0 1 0-1.718 0v14.236a1.999 1.999 0 0 1-3.997 0V9.022a.86.86 0 1 0-1.718 0v3.87a1.999 1.999 0 0 1-3.997 0V11.49a.57.57 0 0 1 1.139 0v1.404a.86.86 0 0 0 1.719 0V9.022a1.999 1.999 0 0 1 3.997 0v9.134a.86.86 0 0 0 1.719 0V3.92a1.998 1.998 0 1 1 3.996 0v11.788a.57.57 0 1 1-1.139 0zm10.572 3.105a2 2 0 0 0-1.999 1.997v7.63a.86.86 0 0 1-1.718 0V3.923a1.999 1.999 0 0 0-3.997 0v16.16a.86.86 0 0 1-1.719 0V18.08a.57.57 0 1 0-1.138 0v2a1.998 1.998 0 0 0 3.996 0V3.92a.86.86 0 0 1 1.719 0v12.73a1.999 1.999 0 0 0 3.996 0V9.023a.86.86 0 1 1 1.72 0v6.686a.57.57 0 0 0 1.138 0V9.022a2 2 0 0 0-1.998-1.997',
|
|
1703
|
+
mimo: 'M12 0C8.016 0 4.756.255 2.493 2.516.23 4.776 0 8.033 0 12.012c0 3.98.23 7.235 2.494 9.497C4.757 23.77 8.017 24 12 24c3.983 0 7.243-.23 9.506-2.491C23.77 19.247 24 15.99 24 12.012c0-3.984-.233-7.243-2.502-9.504C19.234.252 15.978 0 12 0zM4.906 7.405h5.624c1.47 0 3.007.068 3.764.827.746.746.827 2.233.83 3.676v4.54a.15.15 0 0 1-.152.147h-1.947a.15.15 0 0 1-.152-.148V11.83c-.002-.806-.048-1.634-.464-2.051-.358-.36-1.026-.441-1.72-.458H7.158a.15.15 0 0 0-.151.147v6.98a.15.15 0 0 1-.152.148H4.906a.15.15 0 0 1-.15-.148V7.554a.15.15 0 0 1 .15-.149zm12.131 0h1.949a.15.15 0 0 1 .15.15v8.892a.15.15 0 0 1-.15.148h-1.949a.15.15 0 0 1-.151-.148V7.554a.15.15 0 0 1 .151-.149zM8.92 10.948h2.046c.083 0 .15.066.15.147v5.352a.15.15 0 0 1-.15.148H8.92a.15.15 0 0 1-.152-.148v-5.352a.15.15 0 0 1 .152-.147Z',
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
const VENDORS = [
|
|
1707
|
+
[/^deepseek/i, { mark: 'DS', name: 'DeepSeek', color: '#4d6bfe', logo: 'deepseek' }],
|
|
1708
|
+
[/^glm/i, { mark: 'GLM', name: 'Zhipu', color: '#3b6cf6' }],
|
|
1709
|
+
[/^gpt/i, { mark: 'GPT', name: 'OpenAI', color: '#10a37f' }],
|
|
1710
|
+
[/^claude/i, { mark: 'C', name: 'Anthropic', color: '#d97757', logo: 'claude' }],
|
|
1711
|
+
[/^grok/i, { mark: 'X', name: 'xAI', color: '#64748b', logo: 'grok' }],
|
|
1712
|
+
[/^qwen/i, { mark: 'Q', name: 'Qwen', color: '#615ced', logo: 'qwen' }],
|
|
1713
|
+
[/^kimi/i, { mark: 'K', name: 'Moonshot', color: '#8b5cf6', logo: 'kimi' }],
|
|
1714
|
+
[/^minimax/i, { mark: 'MM', name: 'MiniMax', color: '#ff6b00', logo: 'minimax' }],
|
|
1715
|
+
[/^mimo/i, { mark: 'Mi', name: 'MiMo', color: '#ff6900', logo: 'mimo' }],
|
|
1716
|
+
[/^longcat/i, { mark: 'LC', name: 'LongCat', color: '#f59e0b' }],
|
|
1717
|
+
[/^hy\d/i, { mark: 'H', name: 'Hunyuan', color: '#0057ff' }],
|
|
1718
|
+
[/^muse/i, { mark: 'M', name: 'Muse', color: '#a855f7' }],
|
|
1719
|
+
[/free$/i, { mark: '∞', name: 'Free', color: '#3c9d5d' }],
|
|
1720
|
+
];
|
|
1721
|
+
const vendorOf = (model) => {
|
|
1722
|
+
const hit = VENDORS.find(([pattern]) => pattern.test(model));
|
|
1723
|
+
const vendor = hit ? { ...hit[1] } : { mark: String(model).slice(0, 2).toUpperCase(), name: model, color: '#6b7280' };
|
|
1724
|
+
if (vendor.logo && LOGOS[vendor.logo]) vendor.path = LOGOS[vendor.logo];
|
|
1725
|
+
return vendor;
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
const CAP_ICON = {
|
|
1729
|
+
text: '📝', image: '🖼️', video: '🎬', audio: '🎵', pdf: '📄',
|
|
1730
|
+
tools: '🛠️', reasoning: '🧠',
|
|
1731
|
+
};
|
|
1732
|
+
const capIcon = (key) => CAP_ICON[key] ?? '•';
|
|
1733
|
+
|
|
1734
|
+
const routerAccounts = new Map((router?.accounts ?? []).map((account) => [account.id, account]));
|
|
1735
|
+
const usageAccounts = data?.accounts ?? [];
|
|
1736
|
+
const accountIds = router?.accounts?.length
|
|
1737
|
+
? router.accounts.map((account) => account.id)
|
|
1738
|
+
: usageAccounts.map((account) => account.id);
|
|
1739
|
+
const accountList = accountIds.map((id) => {
|
|
1740
|
+
const fromRouter = routerAccounts.get(id);
|
|
1741
|
+
const fromUsage = usageAccounts.find((account) => account.id === id);
|
|
1742
|
+
return {
|
|
1743
|
+
id,
|
|
1744
|
+
label: fromRouter?.label ?? fromUsage?.label ?? id,
|
|
1745
|
+
hint: fromRouter?.hint ?? fromUsage?.hint ?? null,
|
|
1746
|
+
keyRef: fromRouter?.keyRef,
|
|
1747
|
+
active: fromRouter?.active ?? fromUsage?.active ?? false,
|
|
1748
|
+
enabled: fromRouter?.enabled ?? fromUsage?.enabled ?? true,
|
|
1749
|
+
status: fromRouter?.status ?? null,
|
|
1750
|
+
exhaustedUntil: fromRouter?.exhaustedUntil ?? null,
|
|
1751
|
+
usage: fromRouter?.usage ?? fromUsage?.usage ?? null,
|
|
1752
|
+
usageError: fromUsage?.usageError ?? null,
|
|
1753
|
+
checkedAt: fromRouter?.checkedAt ?? fromUsage?.checkedAt ?? null,
|
|
1754
|
+
configured: fromRouter?.configured ?? fromUsage?.configured ?? false,
|
|
1755
|
+
};
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
// Layout primitives: every block is a card with its own padding and gap, so
|
|
1759
|
+
// buttons never sit flush against each other or against the table edges.
|
|
1760
|
+
const card = (...children) => h('div', {
|
|
1761
|
+
style: {
|
|
1762
|
+
padding: '14px 16px', border: '1px solid rgba(127,127,127,.25)', borderRadius: 10,
|
|
1763
|
+
background: 'var(--dsh-surface, rgba(127,127,127,.04))',
|
|
1764
|
+
display: 'flex', flexDirection: 'column', gap: 12,
|
|
1765
|
+
},
|
|
1766
|
+
}, ...children);
|
|
1767
|
+
|
|
1768
|
+
const cardHead = (text, extra) => h('div', {
|
|
1769
|
+
style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' },
|
|
1770
|
+
},
|
|
1771
|
+
h('span', { style: { fontWeight: 600, fontSize: 13 } }, text),
|
|
1772
|
+
extra ?? null,
|
|
1773
|
+
);
|
|
1774
|
+
|
|
1775
|
+
const stat = (label, value) => h('div', {
|
|
1776
|
+
style: { display: 'flex', flexDirection: 'column', gap: 3, minWidth: 96 },
|
|
1777
|
+
},
|
|
1778
|
+
h('span', { style: { fontSize: 11, opacity: .55 } }, label),
|
|
1779
|
+
h('span', { style: { fontSize: 13, fontWeight: 500 } }, value),
|
|
1780
|
+
);
|
|
1781
|
+
|
|
1782
|
+
const tools = (...children) => h('div', {
|
|
1783
|
+
style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' },
|
|
1784
|
+
}, ...children);
|
|
1785
|
+
|
|
1786
|
+
const pill = (text, color) => h('span', {
|
|
1787
|
+
style: {
|
|
1788
|
+
fontSize: 11, padding: '1px 8px', borderRadius: 999,
|
|
1789
|
+
border: `1px solid ${color}99`, color,
|
|
1790
|
+
},
|
|
1791
|
+
}, text);
|
|
1792
|
+
|
|
1793
|
+
const accountCards = accountList.map((account, index) => {
|
|
1794
|
+
const [text, color] = statusText[account.status] ?? [
|
|
1795
|
+
account.usageError ? t('statusError') : account.enabled === false ? t('statusDisabled') : account.active ? t('statusActive') : t('statusSpare'),
|
|
1796
|
+
account.usageError ? '#d9534f' : 'inherit',
|
|
1797
|
+
];
|
|
1798
|
+
return h('div', {
|
|
1799
|
+
key: account.id,
|
|
1800
|
+
style: {
|
|
1801
|
+
display: 'flex', flexDirection: 'column', gap: 12, padding: '12px 14px', borderRadius: 10,
|
|
1802
|
+
border: `1px solid ${account.active ? 'rgba(60,157,93,.45)' : 'rgba(127,127,127,.22)'}`,
|
|
1803
|
+
background: account.active ? 'rgba(60,157,93,.06)' : 'rgba(127,127,127,.03)',
|
|
1804
|
+
},
|
|
1805
|
+
},
|
|
1806
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } },
|
|
1807
|
+
h('span', { style: { opacity: .45, fontSize: 11 } }, `#${index + 1}`),
|
|
1808
|
+
h('span', { style: { fontWeight: 600, fontSize: 13 } }, account.label),
|
|
1809
|
+
account.active ? pill(t('statusCurrent'), '#3c9d5d') : null,
|
|
1810
|
+
h('span', {
|
|
1811
|
+
style: { fontFamily: 'ui-monospace, Consolas, monospace', fontSize: 12, opacity: account.hint ? .65 : .45 },
|
|
1812
|
+
}, account.hint ?? t('notConfigured')),
|
|
1813
|
+
h('span', { style: { flex: 1 } }),
|
|
1814
|
+
h('span', { style: { color, fontSize: 12 } },
|
|
1815
|
+
text,
|
|
1816
|
+
account.exhaustedUntil ? ` · ${clock(account.exhaustedUntil)}` : ''),
|
|
1817
|
+
),
|
|
1818
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(110px, 1fr))', gap: 12 } },
|
|
1819
|
+
stat(t('colRolling'), official(account.usage?.rolling)),
|
|
1820
|
+
stat(t('colWeekly'), official(account.usage?.weekly)),
|
|
1821
|
+
stat(t('colMonthly'), official(account.usage?.monthly)),
|
|
1822
|
+
),
|
|
1823
|
+
router
|
|
1824
|
+
? h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', paddingTop: 2 } },
|
|
1825
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, onClick: () => setInspecting(account.id) }, t('viewUsage')),
|
|
1826
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy || account.active, onClick: () => void act({ action: 'activate', id: account.id }) }, t('setActive')),
|
|
1827
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy, onClick: () => void act({ action: 'update', id: account.id, enabled: !account.enabled }) }, account.enabled ? t('disable') : t('enable')),
|
|
1828
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy, onClick: () => { void act({ action: 'move', id: account.id, direction: 'up' }); } }, '↑'),
|
|
1829
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy, onClick: () => { void act({ action: 'move', id: account.id, direction: 'down' }); } }, '↓'),
|
|
1830
|
+
h('button', {
|
|
1831
|
+
type: 'button', className: 'ocr-btn', style: btn, disabled: busy,
|
|
1832
|
+
onClick: () => {
|
|
1833
|
+
if (editing === account.id) {
|
|
1834
|
+
setEditing(null);
|
|
1835
|
+
setKeyDraft('');
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
setEditing(account.id);
|
|
1839
|
+
setKeyDraft('');
|
|
1840
|
+
void revealKey(account.id);
|
|
1841
|
+
},
|
|
1842
|
+
}, t('editKey')),
|
|
1843
|
+
h('button', {
|
|
1844
|
+
type: 'button', className: 'ocr-btn', style: { ...btn, color: '#d9534f' }, disabled: busy,
|
|
1845
|
+
onClick: () => {
|
|
1846
|
+
if (accountList.length <= 1) {
|
|
1847
|
+
setActionError(t('keepOne'));
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
void act({ action: 'remove', id: account.id });
|
|
1851
|
+
},
|
|
1852
|
+
}, t('remove')),
|
|
1853
|
+
)
|
|
1854
|
+
: h('span', { style: { opacity: .5, fontSize: 12 } }, t('readonly')),
|
|
1855
|
+
editing === account.id
|
|
1856
|
+
? h('div', {
|
|
1857
|
+
style: {
|
|
1858
|
+
display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap',
|
|
1859
|
+
paddingTop: 10, borderTop: '1px solid rgba(127,127,127,.18)',
|
|
1860
|
+
},
|
|
1861
|
+
},
|
|
1862
|
+
h('input', {
|
|
1863
|
+
style: { ...input, minWidth: 340, fontFamily: 'ui-monospace, Consolas, monospace' },
|
|
1864
|
+
type: 'text', spellCheck: false, placeholder: 'oc_sk_…', value: keyDraft,
|
|
1865
|
+
onChange: (event) => setKeyDraft(event.target.value),
|
|
1866
|
+
}),
|
|
1867
|
+
h('button', {
|
|
1868
|
+
type: 'button', className: 'ocr-btn', style: btn, disabled: keyDraft.length === 0,
|
|
1869
|
+
onClick: async () => {
|
|
1870
|
+
try {
|
|
1871
|
+
await navigator.clipboard.writeText(keyDraft);
|
|
1872
|
+
setNotice(t('copied'));
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
setActionError(String(error?.message ?? error));
|
|
1875
|
+
}
|
|
1876
|
+
},
|
|
1877
|
+
}, t('copyKey')),
|
|
1878
|
+
h('button', {
|
|
1879
|
+
type: 'button', className: 'ocr-btn', style: btn, disabled: busy || keyDraft.trim().length < 8,
|
|
1880
|
+
onClick: async () => {
|
|
1881
|
+
const ok = await act({ action: 'update', id: account.id, key: keyDraft.trim() });
|
|
1882
|
+
if (ok) {
|
|
1883
|
+
setEditing(null);
|
|
1884
|
+
setKeyDraft('');
|
|
1885
|
+
setNotice(t('keyUpdated'));
|
|
1886
|
+
}
|
|
1887
|
+
},
|
|
1888
|
+
}, t('save')),
|
|
1889
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, onClick: () => { setEditing(null); setKeyDraft(''); } }, t('cancel')),
|
|
1890
|
+
)
|
|
1891
|
+
: null,
|
|
1892
|
+
);
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
const addAccount = async () => {
|
|
1896
|
+
if (draft.key.trim().length < 8) {
|
|
1897
|
+
setActionError(t('shortKey'));
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
const ok = await act({ action: 'add', label: draft.label.trim(), key: draft.key.trim() });
|
|
1901
|
+
if (ok) {
|
|
1902
|
+
setDraft({ label: '', key: '' });
|
|
1903
|
+
setNotice(t('accountAdded'));
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
const modelCards = (data?.models ?? []).map((row) => {
|
|
1908
|
+
const vendor = vendorOf(row.model);
|
|
1909
|
+
const chips = capabilityTags(row);
|
|
1910
|
+
return h('div', {
|
|
1911
|
+
key: row.model,
|
|
1912
|
+
style: {
|
|
1913
|
+
display: 'flex', flexDirection: 'column', gap: 12, padding: '14px 16px', borderRadius: 10,
|
|
1914
|
+
border: `1px solid ${vendor.color}3d`,
|
|
1915
|
+
background: `linear-gradient(135deg, ${vendor.color}2e, ${vendor.color}0f 55%, rgba(127,127,127,.03))`,
|
|
1916
|
+
},
|
|
1917
|
+
},
|
|
1918
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } },
|
|
1919
|
+
h('span', {
|
|
1920
|
+
style: {
|
|
1921
|
+
width: 30, height: 30, borderRadius: '50%', flex: 'none',
|
|
1922
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1923
|
+
background: `${vendor.color}26`, border: `1px solid ${vendor.color}80`,
|
|
1924
|
+
color: vendor.color, fontSize: 11, fontWeight: 700,
|
|
1925
|
+
},
|
|
1926
|
+
},
|
|
1927
|
+
vendor.path
|
|
1928
|
+
? h('svg', { viewBox: '0 0 24 24', width: 17, height: 17, role: 'img', 'aria-label': vendor.name },
|
|
1929
|
+
h('path', { d: vendor.path, fill: vendor.color }),
|
|
1930
|
+
)
|
|
1931
|
+
: vendor.mark,
|
|
1932
|
+
),
|
|
1933
|
+
h('span', { style: { fontWeight: 600, fontSize: 13 } }, prettyModel(row.model)),
|
|
1934
|
+
row.free ? pill(t('unlimited'), '#3c9d5d') : null,
|
|
1935
|
+
),
|
|
1936
|
+
chips.length > 0
|
|
1937
|
+
? h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1938
|
+
chips.map((chip) => h('span', {
|
|
1939
|
+
key: chip.key,
|
|
1940
|
+
title: chip.hint,
|
|
1941
|
+
style: {
|
|
1942
|
+
display: 'inline-flex', alignItems: 'center', gap: 4,
|
|
1943
|
+
padding: '2px 8px', borderRadius: 999, fontSize: 11, cursor: 'help',
|
|
1944
|
+
border: `1px solid ${vendor.color}59`, background: `${vendor.color}14`, color: 'inherit',
|
|
1945
|
+
},
|
|
1946
|
+
}, h('span', null, capIcon(chip.key)), h('span', null, chip.label))),
|
|
1947
|
+
)
|
|
1948
|
+
: null,
|
|
1949
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(118px, 1fr))', gap: 12 } },
|
|
1950
|
+
stat(t('colRemaining'), row.free ? t('unlimited') : money(row.remaining)),
|
|
1951
|
+
stat(t('colMonthlyUsed'), row.free ? t('unlimited') : `${money(row.monthly)} / ${money(row.limit)}`),
|
|
1952
|
+
stat(t('colWeeklyUsed'), row.free ? t('unlimited') : money(row.weekly)),
|
|
1953
|
+
stat(t('colRollingQuota'), percentText(row.rolling, row.rollingLimit, row.free)),
|
|
1954
|
+
stat(t('colRequests'), String(row.requests)),
|
|
1955
|
+
),
|
|
1956
|
+
);
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
const peak = data?.peak;
|
|
1960
|
+
const quotaSpent = typeof data?.usage?.monthly?.percent === 'number' ? data.usage.monthly.percent : null;
|
|
1961
|
+
const exhausted = quotaSpent !== null && quotaSpent >= 100;
|
|
1962
|
+
// The real numbers can never favour the official API (a $10 plan carries
|
|
1963
|
+
// $100 of list price), so the page can also show what that case looks like.
|
|
1964
|
+
const demoPeak = {
|
|
1965
|
+
plan: peak?.plan ?? { leverage: 10 },
|
|
1966
|
+
models: [{
|
|
1967
|
+
model: 'deepseek-v4-pro',
|
|
1968
|
+
rows: [
|
|
1969
|
+
{ kind: 'cacheRead', go: { pay: 0.015, factor: 1 }, official: { pay: 0.15, factor: 1 }, cheaper: 'go', ratio: 0.1 },
|
|
1970
|
+
{ kind: 'input', go: { pay: 0.45, factor: 1 }, official: { pay: 4.5, factor: 1 }, cheaper: 'go', ratio: 0.1 },
|
|
1971
|
+
{ kind: 'output', go: { pay: 1.35, factor: 1 }, official: { pay: 13.5, factor: 1 }, cheaper: 'go', ratio: 0.1 },
|
|
1972
|
+
],
|
|
1973
|
+
go: { pay: 13.5, factor: 1 },
|
|
1974
|
+
official: { pay: 9, factor: 1 },
|
|
1975
|
+
cheaper: 'official',
|
|
1976
|
+
ratio: 1.5,
|
|
1977
|
+
}],
|
|
1978
|
+
};
|
|
1979
|
+
const shown = demo ? demoPeak : peak;
|
|
1980
|
+
const allGo = shown ? shown.models.every((row) => row.cheaper === 'go') : false;
|
|
1981
|
+
const saving = shown && shown.models.length > 0
|
|
1982
|
+
? Math.round(Math.max(...shown.models.map((row) => (row.cheaper === 'go' ? (1 - row.ratio) * 100 : (row.ratio - 1) * 100))))
|
|
1983
|
+
: 0;
|
|
1984
|
+
const verdict = exhausted && !demo
|
|
1985
|
+
? t('verdictExhausted')
|
|
1986
|
+
: allGo ? t('verdictGo', { percent: saving }) : t('verdictOfficial', { percent: saving });
|
|
1987
|
+
const verdictColor = exhausted && !demo ? '#e0a030' : allGo ? '#3c9d5d' : '#e0a030';
|
|
1988
|
+
const priceCell = (price, peakFlag, winner) => h('span', {
|
|
1989
|
+
style: winner
|
|
1990
|
+
? { fontWeight: 600, color: '#3c9d5d', background: 'rgba(60,157,93,.12)', padding: '1px 6px', borderRadius: 4 }
|
|
1991
|
+
: undefined,
|
|
1992
|
+
}, yuan(price));
|
|
1993
|
+
|
|
1994
|
+
// Both sides run the same wall-clock windows but keep different holiday
|
|
1995
|
+
// calendars, so the page reports them separately with a countdown.
|
|
1996
|
+
const clockInfo = data?.clock ?? null;
|
|
1997
|
+
const periodOf = (side) => (side?.peak ? t('clockPeak') : t('clockOff'));
|
|
1998
|
+
const withPeriod = (label, side) => {
|
|
1999
|
+
if (!side) return label;
|
|
2000
|
+
const period = periodOf(side);
|
|
2001
|
+
// Full-width brackets read correctly in Chinese, but look odd in Latin text.
|
|
2002
|
+
return /^zh/i.test(LANG) ? `${label}(${period})` : `${label} (${period})`;
|
|
2003
|
+
};
|
|
2004
|
+
const clockLine = (label, side) => h('div', {
|
|
2005
|
+
style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, flexWrap: 'wrap' },
|
|
2006
|
+
},
|
|
2007
|
+
h('span', { style: { minWidth: 96, opacity: .75 } }, label),
|
|
2008
|
+
side ? pill(periodOf(side), side.peak ? '#e0a030' : '#3c9d5d') : h('span', { style: { opacity: .5 } }, t('none')),
|
|
2009
|
+
side && typeof side.hours === 'number'
|
|
2010
|
+
? h('span', { style: { opacity: .65 } }, t('clockFlip', {
|
|
2011
|
+
hours: side.hours.toFixed(1),
|
|
2012
|
+
next: side.peak ? t('clockOff') : t('clockPeak'),
|
|
2013
|
+
}))
|
|
2014
|
+
: null,
|
|
2015
|
+
);
|
|
2016
|
+
|
|
2017
|
+
const inspectingAccount = accountList.find((account) => account.id === inspecting) ?? null;
|
|
2018
|
+
|
|
2019
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 16 } },
|
|
2020
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } },
|
|
2021
|
+
h('div', { style: { fontSize: 16, fontWeight: 600 } }, t('title')),
|
|
2022
|
+
state.phase === 'ready' ? h('span', { style: { opacity: .55, fontSize: 12 } }, t('updatedAt', { time: clock(data?.checkedAt) })) : null,
|
|
2023
|
+
h('span', { style: { flex: 1 } }),
|
|
2024
|
+
h('a', {
|
|
2025
|
+
href: 'https://opencode.ai/console',
|
|
2026
|
+
target: '_blank',
|
|
2027
|
+
rel: 'noreferrer',
|
|
2028
|
+
className: 'ocr-btn',
|
|
2029
|
+
style: { ...btn, textDecoration: 'none', display: 'inline-block' },
|
|
2030
|
+
}, t('subscribe')),
|
|
2031
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy, onClick: () => { void load(true); } }, busy ? t('refreshing') : t('refresh')),
|
|
2032
|
+
),
|
|
2033
|
+
state.phase === 'error' ? h('div', { style: { color: '#d9534f', fontSize: 13 } }, t('loadError', { message: state.error })) : null,
|
|
2034
|
+
actionError ? h('div', { style: { color: '#d9534f', fontSize: 13 } }, actionError) : null,
|
|
2035
|
+
notice ? h('div', { style: { color: '#3c9d5d', fontSize: 13 } }, notice) : null,
|
|
2036
|
+
|
|
2037
|
+
card(
|
|
2038
|
+
cardHead(t('routerTitle'), router
|
|
2039
|
+
? pill(router.mode === 'fallback' ? t('modeFallback') : t('modePrimary'), router.mode === 'fallback' ? '#e0a030' : '#3c9d5d')
|
|
2040
|
+
: h('span', { style: { opacity: .7, fontSize: 12 } }, t('routerMissing'))),
|
|
2041
|
+
router?.reason ? h('div', { style: { fontSize: 12, opacity: .65 } }, t('modeReason', { reason: reasonText(router.reason) })) : null,
|
|
2042
|
+
router
|
|
2043
|
+
? h('div', { style: { display: 'flex', gap: 32, flexWrap: 'wrap' } },
|
|
2044
|
+
stat(t('activeAccount'), router.activeLabel ?? t('none')),
|
|
2045
|
+
stat(t('nextProbe'), router.nextProbeAt ? when(router.nextProbeAt) : when(Date.now() + (router.probeIntervalMs ?? 0))),
|
|
2046
|
+
stat(t('colStatus'), router.mode === 'fallback' ? t('modeFallback') : t('modePrimary')),
|
|
2047
|
+
)
|
|
2048
|
+
: null,
|
|
2049
|
+
router
|
|
2050
|
+
? tools(
|
|
2051
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy, onClick: () => void act({ action: 'probe' }) }, t('probeAll')),
|
|
2052
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy, onClick: () => void act({ action: 'reset' }) }, t('resetMarks')),
|
|
2053
|
+
h('span', { style: { flexBasis: '100%', fontSize: 11, opacity: .55, lineHeight: 1.6 } }, t('resetHint')),
|
|
2054
|
+
)
|
|
2055
|
+
: null,
|
|
2056
|
+
),
|
|
2057
|
+
|
|
2058
|
+
card(
|
|
2059
|
+
cardHead(t('accountsTitle'), h('span', { style: { opacity: .55, fontSize: 12 } }, t('accountsOrder'))),
|
|
2060
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
2061
|
+
accountCards.length > 0
|
|
2062
|
+
? accountCards
|
|
2063
|
+
: h('div', { style: { opacity: .6, fontSize: 12 } }, t('noAccounts')),
|
|
2064
|
+
),
|
|
2065
|
+
),
|
|
2066
|
+
|
|
2067
|
+
router
|
|
2068
|
+
? card(
|
|
2069
|
+
cardHead(t('addAccount')),
|
|
2070
|
+
h('div', { style: { display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap' } },
|
|
2071
|
+
h('label', { style: { display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11, opacity: .8 } },
|
|
2072
|
+
t('colAccount'),
|
|
2073
|
+
h('input', {
|
|
2074
|
+
style: { ...input, minWidth: 200 }, placeholder: t('labelPlaceholder'), value: draft.label,
|
|
2075
|
+
onChange: (event) => setDraft({ ...draft, label: event.target.value }),
|
|
2076
|
+
}),
|
|
2077
|
+
),
|
|
2078
|
+
h('label', { style: { display: 'flex', flexDirection: 'column', gap: 5, fontSize: 11, opacity: .8 } },
|
|
2079
|
+
t('colKey'),
|
|
2080
|
+
h('input', {
|
|
2081
|
+
style: { ...input, minWidth: 320 }, type: 'password', placeholder: t('keyPlaceholder'), value: draft.key,
|
|
2082
|
+
onChange: (event) => setDraft({ ...draft, key: event.target.value }),
|
|
2083
|
+
}),
|
|
2084
|
+
),
|
|
2085
|
+
h('button', { type: 'button', className: 'ocr-btn', style: btn, disabled: busy || draft.key.trim().length < 8, onClick: () => void addAccount() }, t('addAndProbe')),
|
|
2086
|
+
),
|
|
2087
|
+
)
|
|
2088
|
+
: null,
|
|
2089
|
+
|
|
2090
|
+
shown
|
|
2091
|
+
? card(
|
|
2092
|
+
h('div', {
|
|
2093
|
+
style: {
|
|
2094
|
+
padding: '16px 18px', borderRadius: 10, fontSize: 18, fontWeight: 700, lineHeight: 1.5,
|
|
2095
|
+
color: verdictColor, background: `${verdictColor}22`, border: `1px solid ${verdictColor}80`,
|
|
2096
|
+
},
|
|
2097
|
+
}, verdict),
|
|
2098
|
+
clockInfo
|
|
2099
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
2100
|
+
h('div', { style: { fontSize: 12, opacity: .6 } }, t('clockTitle')),
|
|
2101
|
+
clockLine(t('colOfficial'), clockInfo.official),
|
|
2102
|
+
clockLine(t('colOpenCode'), clockInfo.opencode),
|
|
2103
|
+
clockInfo.holiday
|
|
2104
|
+
? h('div', { style: { fontSize: 11, opacity: .55 } }, t('clockHoliday'))
|
|
2105
|
+
: null,
|
|
2106
|
+
)
|
|
2107
|
+
: null,
|
|
2108
|
+
demo ? h('div', { style: { fontSize: 12, opacity: .7 } }, t('demoNote')) : null,
|
|
2109
|
+
h('div', { style: { overflowX: 'auto', cursor: 'grab' }, ...draggable() },
|
|
2110
|
+
h('table', { style: { borderCollapse: 'collapse', width: '100%', fontSize: 12 } },
|
|
2111
|
+
h('thead', null,
|
|
2112
|
+
h('tr', null, [
|
|
2113
|
+
t('colModel'), t('colKind'),
|
|
2114
|
+
withPeriod(t('colOpenCode'), clockInfo?.opencode),
|
|
2115
|
+
withPeriod(t('colOfficial'), clockInfo?.official),
|
|
2116
|
+
].map((title) => th(title))),
|
|
2117
|
+
),
|
|
2118
|
+
h('tbody', null, shown.models.flatMap((row) =>
|
|
2119
|
+
(row.rows ?? [{ kind: 'output', go: row.go, official: row.official, cheaper: row.cheaper, ratio: row.ratio }])
|
|
2120
|
+
.map((line, index) =>
|
|
2121
|
+
h('tr', {
|
|
2122
|
+
key: `${row.model}-${line.kind}`,
|
|
2123
|
+
// The cached-input line is the one that actually lands on
|
|
2124
|
+
// most requests, so it is highlighted instead of buried.
|
|
2125
|
+
style: line.kind === 'cacheRead'
|
|
2126
|
+
? { background: 'rgba(60,157,93,.12)' }
|
|
2127
|
+
: index === 0 ? { borderTop: '1px solid rgba(127,127,127,.22)' } : undefined,
|
|
2128
|
+
},
|
|
2129
|
+
cell(index === 0 ? prettyModel(row.model) : '', { fontWeight: index === 0 ? 600 : 400 }),
|
|
2130
|
+
cell(t(`kind${line.kind[0].toUpperCase()}${line.kind.slice(1)}`),
|
|
2131
|
+
line.kind === 'cacheRead' ? { fontWeight: 600, color: '#3c9d5d' } : { opacity: .75 }),
|
|
2132
|
+
cell(priceCell(line.go.pay, line.go.factor > 1, line.cheaper === 'go')),
|
|
2133
|
+
cell(priceCell(line.official.pay, line.official.factor > 1, line.cheaper === 'official')),
|
|
2134
|
+
),
|
|
2135
|
+
),
|
|
2136
|
+
)),
|
|
2137
|
+
),
|
|
2138
|
+
),
|
|
2139
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end' } },
|
|
2140
|
+
h('button', {
|
|
2141
|
+
type: 'button', className: 'ocr-btn', style: btn,
|
|
2142
|
+
onClick: () => setDemo(!demo),
|
|
2143
|
+
}, demo ? t('demoOff') : t('demoOn')),
|
|
2144
|
+
),
|
|
2145
|
+
)
|
|
2146
|
+
: null,
|
|
2147
|
+
|
|
2148
|
+
card(
|
|
2149
|
+
cardHead(t('modelsTitle')),
|
|
2150
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } }, modelCards),
|
|
2151
|
+
),
|
|
2152
|
+
|
|
2153
|
+
h('div', { style: { opacity: .55, fontSize: 12, lineHeight: 1.8 } },
|
|
2154
|
+
t('footerOfficial'),
|
|
2155
|
+
h('br'),
|
|
2156
|
+
t('footerLocal'),
|
|
2157
|
+
),
|
|
2158
|
+
|
|
2159
|
+
h(UsageDialog, {
|
|
2160
|
+
account: inspectingAccount,
|
|
2161
|
+
busy,
|
|
2162
|
+
onClose: () => setInspecting(null),
|
|
2163
|
+
onProbe: async () => {
|
|
2164
|
+
if (!inspectingAccount) return;
|
|
2165
|
+
const ok = await act({ action: 'probe', id: inspectingAccount.id });
|
|
2166
|
+
if (ok) setNotice(t('probedAt', { time: clock(Date.now()) }));
|
|
2167
|
+
},
|
|
2168
|
+
}),
|
|
2169
|
+
|
|
2170
|
+
showTop
|
|
2171
|
+
? h('button', {
|
|
2172
|
+
type: 'button', className: 'ocr-btn', title: t('backToTop'),
|
|
2173
|
+
onClick: toTop,
|
|
2174
|
+
style: {
|
|
2175
|
+
position: 'fixed', right: 28, bottom: 28, zIndex: 900,
|
|
2176
|
+
width: 38, height: 38, padding: 0, borderRadius: 999, fontSize: 16, lineHeight: 1,
|
|
2177
|
+
background: 'var(--dsh-surface, rgba(127,127,127,.16))', backdropFilter: 'blur(6px)',
|
|
2178
|
+
boxShadow: '0 6px 18px rgba(0,0,0,.28)',
|
|
2179
|
+
},
|
|
2180
|
+
}, '↑')
|
|
2181
|
+
: null,
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// Hover and press feedback cannot live in an inline style object, so the
|
|
2186
|
+
// bundle ships one small stylesheet for its buttons.
|
|
2187
|
+
const CSS = [
|
|
2188
|
+
'.ocr-btn { transition: background .12s ease, border-color .12s ease, transform .06s ease; }',
|
|
2189
|
+
'.ocr-btn:hover:not(:disabled) { background: rgba(127,127,127,.16); border-color: rgba(127,127,127,.75); }',
|
|
2190
|
+
'.ocr-btn:active:not(:disabled) { transform: translateY(1px); background: rgba(127,127,127,.26); }',
|
|
2191
|
+
'.ocr-btn:disabled { opacity: .45; cursor: default; }',
|
|
2192
|
+
].join('\n');
|
|
2193
|
+
const ensureStyles = () => {
|
|
2194
|
+
if (typeof document === 'undefined') return;
|
|
2195
|
+
if (document.getElementById('ocr-styles') !== null) return;
|
|
2196
|
+
const style = document.createElement('style');
|
|
2197
|
+
style.id = 'ocr-styles';
|
|
2198
|
+
style.textContent = CSS;
|
|
2199
|
+
document.head.appendChild(style);
|
|
2200
|
+
};
|
|
2201
|
+
|
|
2202
|
+
return {
|
|
2203
|
+
inject: ['slots'],
|
|
2204
|
+
apply(ctx) {
|
|
2205
|
+
ensureStyles();
|
|
2206
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
2207
|
+
name: 'settings.section',
|
|
2208
|
+
id: 'opencode-router',
|
|
2209
|
+
order: 18,
|
|
2210
|
+
label: 'OpenCode 路由与用量',
|
|
2211
|
+
}, UsageSection));
|
|
2212
|
+
},
|
|
2213
|
+
};
|
|
2214
|
+
},
|
|
2215
|
+
});
|