dsh-llm-adapters 1.2.0 → 1.2.2
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/DEVELOPMENT.md +92 -24
- package/FAQ.md +47 -10
- package/README.md +39 -25
- package/client.js +1802 -1733
- package/lib/index.js +733 -302
- package/lib/types/account-store.d.ts +17 -8
- package/lib/types/adapter.d.ts +8 -2
- package/lib/types/auth.d.ts +24 -11
- package/lib/types/codearts-oauth.d.ts +19 -0
- package/lib/types/codearts.d.ts +1 -5
- package/lib/types/config.d.ts +0 -5
- package/lib/types/index.d.ts +5 -6
- package/lib/types/raccoon.d.ts +79 -6
- package/lib/types/trae-client.d.ts +52 -0
- package/lib/types/trae-endpoints.d.ts +2 -0
- package/lib/types/trae-login.d.ts +5 -3
- package/lib/types/trae.d.ts +29 -3
- package/lib/types/types.d.ts +19 -1
- package/package.json +3 -2
- package/src/account-store.ts +20 -8
- package/src/adapter.ts +31 -3
- package/src/api.ts +15 -3
- package/src/auth.ts +62 -17
- package/src/codearts-oauth.ts +36 -5
- package/src/codearts.ts +24 -29
- package/src/config.ts +258 -264
- package/src/index.ts +6 -29
- package/src/raccoon.ts +163 -50
- package/src/trae-client.ts +312 -0
- package/src/trae-endpoints.ts +4 -0
- package/src/trae-login.ts +5 -3
- package/src/trae.ts +191 -3
- package/src/types.ts +20 -1
- package/lib/types/codearts-window.d.ts +0 -65
- package/src/codearts-window.ts +0 -136
package/client.js
CHANGED
|
@@ -1,1733 +1,1802 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-llm-adapters — browser half.
|
|
3
|
-
*
|
|
4
|
-
* One settings section per provider. Each follows the same shape as
|
|
5
|
-
* dsh-agent-sdk-bridge: an account card first, then the live catalog with per
|
|
6
|
-
* model on/off and context-window controls, then a collapsed advanced area.
|
|
7
|
-
*
|
|
8
|
-
* The account card is read-only by design. It never asks for a password or a
|
|
9
|
-
* token; sign-in happens on the provider's own page (a WeChat QR code for Loomy
|
|
10
|
-
* and Raccoon, a dedicated browser window for Trae), and the card only reports
|
|
11
|
-
* the resulting account, quota and check-in state.
|
|
12
|
-
*
|
|
13
|
-
* Trae is one card with a region switch: `traeRegion` is a setting, not a
|
|
14
|
-
* second brand, so both regions share this page and keep separate accounts.
|
|
15
|
-
*
|
|
16
|
-
* Hand-written ModuleLoader bundle: no build step, no dependency beyond the
|
|
17
|
-
* `react` the shell provides.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
window.__ModuleLoader__.load({
|
|
21
|
-
id: 'dsh-llm-adapters',
|
|
22
|
-
factory: require => {
|
|
23
|
-
const module = { exports: {} }
|
|
24
|
-
const exports = module.exports
|
|
25
|
-
const React = require('react')
|
|
26
|
-
const { createElement: h, useState, useEffect, useCallback, useMemo, useRef, useSyncExternalStore } = React
|
|
27
|
-
|
|
28
|
-
const NS = 'llm-adapters'
|
|
29
|
-
const inject = ['slots', 'locale', 'configForms']
|
|
30
|
-
|
|
31
|
-
const zh = {
|
|
32
|
-
pluginOptionsTitle: '启用的供应商',
|
|
33
|
-
pluginOptionsHint: '关闭后,对应设置页、模型列表与模型分组都会移除;登录态与已保存的模型选择会保留。',
|
|
34
|
-
pluginOptionsOn: '显示中',
|
|
35
|
-
pluginOptionsOff: '已关闭',
|
|
36
|
-
pluginOptionsReadOnly: '当前配置不可写',
|
|
37
|
-
pluginOptionsRejected: '宿主拒绝了这次改动;若刚更新过插件,请重启 DSH 后再试',
|
|
38
|
-
loadFailed: '读取失败',
|
|
39
|
-
retry: '重试',
|
|
40
|
-
refresh: '刷新状态与模型',
|
|
41
|
-
refreshing: '刷新中…',
|
|
42
|
-
saveFailed: '保存失败',
|
|
43
|
-
notApplied: '服务器未报告这次改动,可能被其他窗口覆盖了,请刷新后重试',
|
|
44
|
-
timeout: '保存超时,请重试',
|
|
45
|
-
saved: '已保存',
|
|
46
|
-
saving: '保存中…',
|
|
47
|
-
enabledCount: '已启用',
|
|
48
|
-
search: '搜索模型…',
|
|
49
|
-
onlyEnabled: '只看已启用',
|
|
50
|
-
selectAll: '全部启用',
|
|
51
|
-
selectNone: '全部关闭',
|
|
52
|
-
colEnabled: '启用',
|
|
53
|
-
windowDefault: '默认',
|
|
54
|
-
windowDefaultHint: '跟随上游声明的窗口',
|
|
55
|
-
vision: '读图',
|
|
56
|
-
reasoning: '思考',
|
|
57
|
-
free: '免费',
|
|
58
|
-
advanced: '高级设置',
|
|
59
|
-
advancedHint: '以下为细节参数,通常无需改动。',
|
|
60
|
-
show: '展开',
|
|
61
|
-
hide: '收起',
|
|
62
|
-
reset: '恢复默认',
|
|
63
|
-
windowWarn: '窗口越大,DSH 的自动压缩触发得越晚。若对话经常超限,请选较小的窗口。',
|
|
64
|
-
accountTitle: '登录与账号',
|
|
65
|
-
account: '账号',
|
|
66
|
-
accountUnknown: '账号信息未提供',
|
|
67
|
-
expires: '预计到期',
|
|
68
|
-
quota: '额度',
|
|
69
|
-
quotaRefreshing: '正在读取额度…',
|
|
70
|
-
quotaUnavailable: '暂时无法读取额度',
|
|
71
|
-
quotaFailed: '额度读取失败',
|
|
72
|
-
points: '积分',
|
|
73
|
-
loomyAvailable: '可用',
|
|
74
|
-
loomyPermanent: '永久积分',
|
|
75
|
-
loomyDaily: '每日积分',
|
|
76
|
-
loomyCycle: '每日周期',
|
|
77
|
-
raccoonOwner: '账号类型',
|
|
78
|
-
ownerPersonal: '个人',
|
|
79
|
-
ownerEnterprise: '企业',
|
|
80
|
-
signedIn: '已登录',
|
|
81
|
-
notLoggedIn: '未登录',
|
|
82
|
-
reread: '重新读取登录态',
|
|
83
|
-
reading: '读取中…',
|
|
84
|
-
loginGuide: '点“打开扫码登录”,用微信扫码后会自动接回。',
|
|
85
|
-
browserTitle: '微信扫码登录',
|
|
86
|
-
browserOpen: '打开扫码登录',
|
|
87
|
-
browserWaiting: '等待扫码…',
|
|
88
|
-
browserCancel: '取消',
|
|
89
|
-
browserReopen: '重新打开登录页',
|
|
90
|
-
browserPolling: '登录页已在浏览器打开:用微信扫码并在手机上确认,成功后会自动接回。若弹窗被拦截,点“重新打开登录页”。',
|
|
91
|
-
browserHintLoomy: '会打开一个本地登录页:用微信扫码登录 Loomy;首次使用的微信账号需要在页面里绑定手机号。',
|
|
92
|
-
browserHintRaccoon: '会打开一个本地登录页:用微信扫码登录小浣熊,扫码确认后插件会自动接回。',
|
|
93
|
-
loomyHint: 'Loomy 会话约 14 天有效,且没有刷新令牌;过期后再点“打开扫码登录”即可。',
|
|
94
|
-
raccoonHint: '令牌约 30
|
|
95
|
-
loginLogout: '退出登录',
|
|
96
|
-
loginConfirmLogout: '确定要退出当前账号吗?',
|
|
97
|
-
loginFailedPrefix: '登录失败',
|
|
98
|
-
loomyLoginMissing: 'Loomy 尚未登录。请点「打开扫码登录」用微信扫码。',
|
|
99
|
-
raccoonLoginMissing: '小浣熊尚未登录。请点「打开扫码登录」用微信扫码。',
|
|
100
|
-
possiblyExpired: '登录态可能已过期',
|
|
101
|
-
modelsEmptyLoomy: 'Loomy 没有返回模型。请确认账号已登录,然后点“重新读取登录态”。',
|
|
102
|
-
modelsEmptyRaccoon: '小浣熊没有返回模型。请确认账号已登录,然后点“重新读取登录态”。',
|
|
103
|
-
modelsHint: '登录后才会读取并显示可用模型。',
|
|
104
|
-
catalogNotice: '模型列表提示',
|
|
105
|
-
advancedFields: {
|
|
106
|
-
displayName: '分组名称',
|
|
107
|
-
visionModelIds: '强制启用读图的模型 ID(JSON 数组)',
|
|
108
|
-
textOnlyModelIds: '强制按纯文本处理的模型 ID(JSON 数组)',
|
|
109
|
-
modelAliases: '模型别名(JSON 对象)',
|
|
110
|
-
defaultMaxTokens: '默认输出上限',
|
|
111
|
-
},
|
|
112
|
-
loomyNav: 'Loomy',
|
|
113
|
-
loomyTitle: 'Loomy',
|
|
114
|
-
loomyIntro: '点“打开扫码登录”,用微信扫码登录 Loomy 即可使用,无需安装客户端。改动即时生效,无需重启。',
|
|
115
|
-
raccoonNav: '商汤小浣熊',
|
|
116
|
-
raccoonTitle: '商汤小浣熊',
|
|
117
|
-
raccoonIntro: '点“打开扫码登录”,用微信扫码登录小浣熊即可使用,无需安装客户端。改动即时生效,无需重启。',
|
|
118
|
-
traeNav: 'Trae',
|
|
119
|
-
traeTitle: 'Trae',
|
|
120
|
-
traeIntro: '
|
|
121
|
-
traeHint: '
|
|
122
|
-
traeLoginMissing: 'Trae
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
enabled: '
|
|
388
|
-
displayName: '
|
|
389
|
-
disabled: '
|
|
390
|
-
windows: '
|
|
391
|
-
aliases: '
|
|
392
|
-
vision: '
|
|
393
|
-
textOnly: '
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
function
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
: h('
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
h('
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
),
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
? t('
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
},
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
?
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
h('
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
style:
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
style: css.
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
if (
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
})
|
|
1350
|
-
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
h('
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* dsh-llm-adapters — browser half.
|
|
3
|
+
*
|
|
4
|
+
* One settings section per provider. Each follows the same shape as
|
|
5
|
+
* dsh-agent-sdk-bridge: an account card first, then the live catalog with per
|
|
6
|
+
* model on/off and context-window controls, then a collapsed advanced area.
|
|
7
|
+
*
|
|
8
|
+
* The account card is read-only by design. It never asks for a password or a
|
|
9
|
+
* token; sign-in happens on the provider's own page (a WeChat QR code for Loomy
|
|
10
|
+
* and Raccoon, a dedicated browser window for Trae), and the card only reports
|
|
11
|
+
* the resulting account, quota and check-in state.
|
|
12
|
+
*
|
|
13
|
+
* Trae is one card with a region switch: `traeRegion` is a setting, not a
|
|
14
|
+
* second brand, so both regions share this page and keep separate accounts.
|
|
15
|
+
*
|
|
16
|
+
* Hand-written ModuleLoader bundle: no build step, no dependency beyond the
|
|
17
|
+
* `react` the shell provides.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
window.__ModuleLoader__.load({
|
|
21
|
+
id: 'dsh-llm-adapters',
|
|
22
|
+
factory: require => {
|
|
23
|
+
const module = { exports: {} }
|
|
24
|
+
const exports = module.exports
|
|
25
|
+
const React = require('react')
|
|
26
|
+
const { createElement: h, useState, useEffect, useCallback, useMemo, useRef, useSyncExternalStore } = React
|
|
27
|
+
|
|
28
|
+
const NS = 'llm-adapters'
|
|
29
|
+
const inject = ['slots', 'locale', 'configForms']
|
|
30
|
+
|
|
31
|
+
const zh = {
|
|
32
|
+
pluginOptionsTitle: '启用的供应商',
|
|
33
|
+
pluginOptionsHint: '关闭后,对应设置页、模型列表与模型分组都会移除;登录态与已保存的模型选择会保留。',
|
|
34
|
+
pluginOptionsOn: '显示中',
|
|
35
|
+
pluginOptionsOff: '已关闭',
|
|
36
|
+
pluginOptionsReadOnly: '当前配置不可写',
|
|
37
|
+
pluginOptionsRejected: '宿主拒绝了这次改动;若刚更新过插件,请重启 DSH 后再试',
|
|
38
|
+
loadFailed: '读取失败',
|
|
39
|
+
retry: '重试',
|
|
40
|
+
refresh: '刷新状态与模型',
|
|
41
|
+
refreshing: '刷新中…',
|
|
42
|
+
saveFailed: '保存失败',
|
|
43
|
+
notApplied: '服务器未报告这次改动,可能被其他窗口覆盖了,请刷新后重试',
|
|
44
|
+
timeout: '保存超时,请重试',
|
|
45
|
+
saved: '已保存',
|
|
46
|
+
saving: '保存中…',
|
|
47
|
+
enabledCount: '已启用',
|
|
48
|
+
search: '搜索模型…',
|
|
49
|
+
onlyEnabled: '只看已启用',
|
|
50
|
+
selectAll: '全部启用',
|
|
51
|
+
selectNone: '全部关闭',
|
|
52
|
+
colEnabled: '启用',
|
|
53
|
+
windowDefault: '默认',
|
|
54
|
+
windowDefaultHint: '跟随上游声明的窗口',
|
|
55
|
+
vision: '读图',
|
|
56
|
+
reasoning: '思考',
|
|
57
|
+
free: '免费',
|
|
58
|
+
advanced: '高级设置',
|
|
59
|
+
advancedHint: '以下为细节参数,通常无需改动。',
|
|
60
|
+
show: '展开',
|
|
61
|
+
hide: '收起',
|
|
62
|
+
reset: '恢复默认',
|
|
63
|
+
windowWarn: '窗口越大,DSH 的自动压缩触发得越晚。若对话经常超限,请选较小的窗口。',
|
|
64
|
+
accountTitle: '登录与账号',
|
|
65
|
+
account: '账号',
|
|
66
|
+
accountUnknown: '账号信息未提供',
|
|
67
|
+
expires: '预计到期',
|
|
68
|
+
quota: '额度',
|
|
69
|
+
quotaRefreshing: '正在读取额度…',
|
|
70
|
+
quotaUnavailable: '暂时无法读取额度',
|
|
71
|
+
quotaFailed: '额度读取失败',
|
|
72
|
+
points: '积分',
|
|
73
|
+
loomyAvailable: '可用',
|
|
74
|
+
loomyPermanent: '永久积分',
|
|
75
|
+
loomyDaily: '每日积分',
|
|
76
|
+
loomyCycle: '每日周期',
|
|
77
|
+
raccoonOwner: '账号类型',
|
|
78
|
+
ownerPersonal: '个人',
|
|
79
|
+
ownerEnterprise: '企业',
|
|
80
|
+
signedIn: '已登录',
|
|
81
|
+
notLoggedIn: '未登录',
|
|
82
|
+
reread: '重新读取登录态',
|
|
83
|
+
reading: '读取中…',
|
|
84
|
+
loginGuide: '点“打开扫码登录”,用微信扫码后会自动接回。',
|
|
85
|
+
browserTitle: '微信扫码登录',
|
|
86
|
+
browserOpen: '打开扫码登录',
|
|
87
|
+
browserWaiting: '等待扫码…',
|
|
88
|
+
browserCancel: '取消',
|
|
89
|
+
browserReopen: '重新打开登录页',
|
|
90
|
+
browserPolling: '登录页已在浏览器打开:用微信扫码并在手机上确认,成功后会自动接回。若弹窗被拦截,点“重新打开登录页”。',
|
|
91
|
+
browserHintLoomy: '会打开一个本地登录页:用微信扫码登录 Loomy;首次使用的微信账号需要在页面里绑定手机号。',
|
|
92
|
+
browserHintRaccoon: '会打开一个本地登录页:用微信扫码登录小浣熊,扫码确认后插件会自动接回。',
|
|
93
|
+
loomyHint: 'Loomy 会话约 14 天有效,且没有刷新令牌;过期后再点“打开扫码登录”即可。',
|
|
94
|
+
raccoonHint: '令牌约 30 天有效。每日积分由服务端在当天首次打开网页时发放;插件只检测到账情况(点「检测今日积分」读今日流水),不会替你触发领取。没到账就点「打开网页签到」到官网领。',
|
|
95
|
+
loginLogout: '退出登录',
|
|
96
|
+
loginConfirmLogout: '确定要退出当前账号吗?',
|
|
97
|
+
loginFailedPrefix: '登录失败',
|
|
98
|
+
loomyLoginMissing: 'Loomy 尚未登录。请点「打开扫码登录」用微信扫码。',
|
|
99
|
+
raccoonLoginMissing: '小浣熊尚未登录。请点「打开扫码登录」用微信扫码。',
|
|
100
|
+
possiblyExpired: '登录态可能已过期',
|
|
101
|
+
modelsEmptyLoomy: 'Loomy 没有返回模型。请确认账号已登录,然后点“重新读取登录态”。',
|
|
102
|
+
modelsEmptyRaccoon: '小浣熊没有返回模型。请确认账号已登录,然后点“重新读取登录态”。',
|
|
103
|
+
modelsHint: '登录后才会读取并显示可用模型。',
|
|
104
|
+
catalogNotice: '模型列表提示',
|
|
105
|
+
advancedFields: {
|
|
106
|
+
displayName: '分组名称',
|
|
107
|
+
visionModelIds: '强制启用读图的模型 ID(JSON 数组)',
|
|
108
|
+
textOnlyModelIds: '强制按纯文本处理的模型 ID(JSON 数组)',
|
|
109
|
+
modelAliases: '模型别名(JSON 对象)',
|
|
110
|
+
defaultMaxTokens: '默认输出上限',
|
|
111
|
+
},
|
|
112
|
+
loomyNav: 'Loomy',
|
|
113
|
+
loomyTitle: 'Loomy',
|
|
114
|
+
loomyIntro: '点“打开扫码登录”,用微信扫码登录 Loomy 即可使用,无需安装客户端。改动即时生效,无需重启。',
|
|
115
|
+
raccoonNav: '商汤小浣熊',
|
|
116
|
+
raccoonTitle: '商汤小浣熊',
|
|
117
|
+
raccoonIntro: '点“打开扫码登录”,用微信扫码登录小浣熊即可使用,无需安装客户端。改动即时生效,无需重启。',
|
|
118
|
+
traeNav: 'Trae',
|
|
119
|
+
traeTitle: 'Trae',
|
|
120
|
+
traeIntro: '先选国内版或国际版,再任选一种登录方式:在 Trae 官网网页登录,或复用本机 Trae 客户端的登录态。网页登录会用网页会话自动续期;只有复用客户端登录态时,国内版才显示签到按钮。改动即时生效,无需重启。',
|
|
121
|
+
traeHint: '网页登录的令牌约 8 小时,插件会用登录时拿到的网页会话自动续期;复用本机客户端登录态则直接使用客户端令牌,客户端退出登录或令牌过期后重新复用即可。签到仅在国内版、且使用客户端登录态时可用。',
|
|
122
|
+
traeLoginMissing: 'Trae 尚未登录。可以点“打开浏览器登录”,或复用本机已登录的 Trae 客户端。',
|
|
123
|
+
traeClientTitle: '复用本机客户端登录',
|
|
124
|
+
traeClientHint: '检测到本机已安装 Trae 客户端。这里只读取客户端当前的登录态,不会修改或退出客户端;复用后会替换插件里的当前登录态。用这种方式登录后才显示签到按钮。',
|
|
125
|
+
traeClientReady: '本机客户端已登录,可直接复用。',
|
|
126
|
+
traeClientNotLoggedIn: '检测到本机 Trae 客户端,但尚未登录。请先打开客户端完成登录,再点“重新读取登录态”。',
|
|
127
|
+
traeClientAccount: '客户端账号',
|
|
128
|
+
traeClientUse: '复用客户端登录态',
|
|
129
|
+
traeClientReuse: '重新读取客户端登录态',
|
|
130
|
+
traeClientBusy: '正在读取…',
|
|
131
|
+
traeClientFailed: '读取客户端登录态失败',
|
|
132
|
+
traeClientSource: '登录方式',
|
|
133
|
+
traeSourceWeb: '网页登录',
|
|
134
|
+
traeSourceClient: '本机客户端',
|
|
135
|
+
traeClientLogoutNote: '退出登录只会清除插件里的登录态,不会退出本机 Trae 客户端。',
|
|
136
|
+
regionLabel: '区域',
|
|
137
|
+
regionCn: '国内版 · trae.cn',
|
|
138
|
+
regionGlobal: '国际版 · trae.ai',
|
|
139
|
+
regionHint: '两个区域各自保存账号:切换区域不会退出另一个区域的登录。',
|
|
140
|
+
regionSwitching: '正在切换区域…',
|
|
141
|
+
checkin: '签到',
|
|
142
|
+
checkinClaim: '签到领积分',
|
|
143
|
+
checkinDetect: '检测今日积分',
|
|
144
|
+
checkinDetecting: '检测中…',
|
|
145
|
+
checkinBusy: '签到中…',
|
|
146
|
+
checkinClaimed: '今日已签到',
|
|
147
|
+
checkinDevice: '本机今日已签到',
|
|
148
|
+
checkinReward: '可得',
|
|
149
|
+
checkinReceived: '已到账',
|
|
150
|
+
checkinGrantedAt: '到账时间',
|
|
151
|
+
checkinOpenWeb: '打开网页签到',
|
|
152
|
+
checkinFailed: '签到失败',
|
|
153
|
+
checkinUnavailable: '当前账号没有可用的签到活动',
|
|
154
|
+
checkinMonitorTitle: '小浣熊每日积分(只检测)',
|
|
155
|
+
checkinMonitorHint: '插件只读今日积分流水做检测,不会替你触发领取;没到账就去官网用「打开网页签到」领。',
|
|
156
|
+
plan: '套餐',
|
|
157
|
+
planFree: '免费版',
|
|
158
|
+
planTrial: '试用中',
|
|
159
|
+
trialEnds: '试用到期',
|
|
160
|
+
packs: '资源包',
|
|
161
|
+
packExpires: '到期',
|
|
162
|
+
packUsage: '已用 / 总额',
|
|
163
|
+
packNearest: '最近到期',
|
|
164
|
+
packDetail: '详细信息',
|
|
165
|
+
packCollapse: '收起',
|
|
166
|
+
usedOf: '已用',
|
|
167
|
+
usageReset: '重置时间',
|
|
168
|
+
browserHintTrae: '点下面的按钮会打开一个独立的浏览器窗口(不会动你自己的浏览器配置):在其中登录 Trae,登录成功后窗口会自动关闭。国际版若提示 account is at risk,请点 Log in 用你平时能登录的同一个账号(Google/GitHub)进入,不要用新邮箱重新注册;独立配置首次登录会被 Trae 当成新设备。',
|
|
169
|
+
browserPollingTrae: '浏览器已打开:请在窗口里登录 Trae,成功后会自动接回,这个窗口随后会关闭。',
|
|
170
|
+
browserTitleTrae: '浏览器登录',
|
|
171
|
+
browserOpenTrae: '打开浏览器登录',
|
|
172
|
+
browserWaitingTrae: '等待登录…',
|
|
173
|
+
loginGuideTrae: '点“打开浏览器登录”,在弹出的窗口里登录 Trae 即可;登录成功后会自动接回。国际版请用 Log in 登录已有账号,不要新建邮箱账号,否则容易触发 account is at risk。',
|
|
174
|
+
modelsEmptyTrae: '账号没有返回任何 Trae 模型。请确认账号已登录,然后点“重新读取登录态”。',
|
|
175
|
+
codeartsNav: '华为云 CodeArts',
|
|
176
|
+
codeartsTitle: '华为云 CodeArts',
|
|
177
|
+
codeartsIntro: '点“打开浏览器登录”在华为云完成授权,插件用授权得到的 AK/SK 签名调用 CodeArts 网关,不读本机 IDE 的任何文件。改动即时生效,无需重启。',
|
|
178
|
+
codeartsHint: 'CodeArts 用签名而不是令牌调用,STS 令牌两小时有效,到期会自动用续期令牌续上;续期令牌失效后重新登录一次即可。积分计费账户每天可以签到领积分。',
|
|
179
|
+
codeartsVisionHint: '「读图」只标记模型原生支持图片。没有这个标记的模型也能收图:发送时会先由 kimi-k2.6-vl 把图片转述成文字,识别结果按图片内容缓存,同一张图只多花一次对话。',
|
|
180
|
+
browserHintCodeArts: '点下面的按钮会在浏览器里打开华为云授权页:登录并同意授权后会自动回到本机,凭据由宿主接回,页面上不需要复制任何东西。',
|
|
181
|
+
browserPollingCodeArts: '已打开华为云授权页:完成授权后会自动接回,这个标签页可以关闭。',
|
|
182
|
+
browserTitleCodeArts: '华为云授权登录',
|
|
183
|
+
browserOpenCodeArts: '打开浏览器登录',
|
|
184
|
+
browserWaitingCodeArts: '等待授权…',
|
|
185
|
+
loginGuideCodeArts: '点“打开浏览器登录”,在弹出的华为云页面里完成授权即可;成功后会自动接回。',
|
|
186
|
+
codeartsLoginMissing: 'CodeArts 尚未登录。请点“打开浏览器登录”,在华为云页面完成授权。',
|
|
187
|
+
modelsEmptyCodeArts: '账号没有返回任何 CodeArts 模型。请确认已登录,然后点“重新读取登录态”。',
|
|
188
|
+
openRouterTitle: 'OpenRouter 模型同步',
|
|
189
|
+
openRouterHint: 'OpenRouter 的实时模型表用来补充思考档位与读图能力,三个供应商共用一份。缓存保存在本机,连不上时会继续使用上次成功同步的数据;刷新模型不会清空缓存。',
|
|
190
|
+
openRouterInterval: '自动同步',
|
|
191
|
+
openRouterOff: '关闭自动同步',
|
|
192
|
+
openRouterEvery15: '每 15 分钟',
|
|
193
|
+
openRouterEvery60: '每小时',
|
|
194
|
+
openRouterEvery360: '每 6 小时',
|
|
195
|
+
openRouterEvery1440: '每 24 小时',
|
|
196
|
+
openRouterSyncNow: '立即同步',
|
|
197
|
+
openRouterSyncing: '同步中…',
|
|
198
|
+
openRouterNever: '尚未同步',
|
|
199
|
+
openRouterModels: '个模型',
|
|
200
|
+
openRouterLastSync: '上次同步',
|
|
201
|
+
openRouterSyncFailed: '同步失败',
|
|
202
|
+
openRouterSyncDone: '已同步',
|
|
203
|
+
}
|
|
204
|
+
const en = {
|
|
205
|
+
pluginOptionsTitle: 'Enabled providers',
|
|
206
|
+
pluginOptionsHint: 'Turning one off removes its settings page, model list and model group. Login state and saved model choices are kept.',
|
|
207
|
+
pluginOptionsOn: 'Shown',
|
|
208
|
+
pluginOptionsOff: 'Off',
|
|
209
|
+
pluginOptionsReadOnly: 'Configuration is read-only',
|
|
210
|
+
pluginOptionsRejected: 'The host rejected this change; restart DSH after a plugin update and retry',
|
|
211
|
+
loadFailed: 'Could not load',
|
|
212
|
+
retry: 'Retry',
|
|
213
|
+
refresh: 'Refresh status and models',
|
|
214
|
+
refreshing: 'Refreshing…',
|
|
215
|
+
saveFailed: 'Save failed',
|
|
216
|
+
notApplied: 'The host did not report this change; another window may have overwritten it. Refresh and retry.',
|
|
217
|
+
timeout: 'Save timed out; please retry',
|
|
218
|
+
saved: 'Saved',
|
|
219
|
+
saving: 'Saving…',
|
|
220
|
+
enabledCount: 'enabled',
|
|
221
|
+
search: 'Search models…',
|
|
222
|
+
onlyEnabled: 'Enabled only',
|
|
223
|
+
selectAll: 'Enable all',
|
|
224
|
+
selectNone: 'Disable all',
|
|
225
|
+
colEnabled: 'Enabled',
|
|
226
|
+
windowDefault: 'Default',
|
|
227
|
+
windowDefaultHint: 'Follow the upstream context window',
|
|
228
|
+
vision: 'vision',
|
|
229
|
+
reasoning: 'reasoning',
|
|
230
|
+
free: 'free',
|
|
231
|
+
advanced: 'Advanced',
|
|
232
|
+
advancedHint: 'Rarely needed; read the help text before changing.',
|
|
233
|
+
show: 'Show',
|
|
234
|
+
hide: 'Hide',
|
|
235
|
+
reset: 'Reset',
|
|
236
|
+
windowWarn: 'A larger window delays automatic compaction. Choose a smaller value if conversations often overflow.',
|
|
237
|
+
accountTitle: 'Sign-in',
|
|
238
|
+
account: 'Account',
|
|
239
|
+
accountUnknown: 'Account details are unavailable',
|
|
240
|
+
expires: 'Expires (estimated)',
|
|
241
|
+
quota: 'Quota',
|
|
242
|
+
quotaRefreshing: 'Reading quota…',
|
|
243
|
+
quotaUnavailable: 'Quota is temporarily unavailable',
|
|
244
|
+
quotaFailed: 'Could not read quota',
|
|
245
|
+
points: 'points',
|
|
246
|
+
loomyAvailable: 'Available',
|
|
247
|
+
loomyPermanent: 'Permanent',
|
|
248
|
+
loomyDaily: 'Daily',
|
|
249
|
+
loomyCycle: 'Daily cycle',
|
|
250
|
+
raccoonOwner: 'Account type',
|
|
251
|
+
ownerPersonal: 'Personal',
|
|
252
|
+
ownerEnterprise: 'Enterprise',
|
|
253
|
+
signedIn: 'Signed in',
|
|
254
|
+
notLoggedIn: 'Not signed in',
|
|
255
|
+
reread: 'Re-read login state',
|
|
256
|
+
reading: 'Reading…',
|
|
257
|
+
loginGuide: 'Use “Open QR sign-in” and scan the code in WeChat; the login is adopted automatically.',
|
|
258
|
+
browserTitle: 'WeChat QR sign-in',
|
|
259
|
+
browserOpen: 'Open QR sign-in',
|
|
260
|
+
browserWaiting: 'Waiting for scan…',
|
|
261
|
+
browserCancel: 'Cancel',
|
|
262
|
+
browserReopen: 'Reopen the sign-in page',
|
|
263
|
+
browserPolling: 'The sign-in page is open in your browser. Scan the code in WeChat and confirm on your phone; the plugin adopts the login automatically. If the popup was blocked, use “Reopen the sign-in page”.',
|
|
264
|
+
browserHintLoomy: 'A local sign-in page opens: scan it in WeChat to sign in to Loomy. A WeChat account using Loomy for the first time must bind a phone number on that page.',
|
|
265
|
+
browserHintRaccoon: 'A local sign-in page opens: scan it in WeChat to sign in to Raccoon, and the plugin adopts the login once you confirm.',
|
|
266
|
+
loomyHint: 'A Loomy session lasts about 14 days and has no refresh token; use “Open QR sign-in” again when it expires.',
|
|
267
|
+
raccoonHint: 'The QR login lasts about 30 days. Daily points are issued by the server on the day’s first website visit; the plugin only detects them (“Check today’s credits” reads today’s ledger) and never claims on your behalf. Use “Open check-in page” to claim on the website.',
|
|
268
|
+
loginLogout: 'Sign out',
|
|
269
|
+
loginConfirmLogout: 'Sign out of this account?',
|
|
270
|
+
loginFailedPrefix: 'Sign-in failed',
|
|
271
|
+
loomyLoginMissing: 'Loomy is not signed in. Use “Open QR sign-in” and scan the code in WeChat.',
|
|
272
|
+
raccoonLoginMissing: 'Raccoon is not signed in. Use “Open QR sign-in” and scan the code in WeChat.',
|
|
273
|
+
possiblyExpired: 'The login may have expired',
|
|
274
|
+
modelsEmptyLoomy: 'Loomy returned no models. Confirm the account is signed in, then re-read the login state.',
|
|
275
|
+
modelsEmptyRaccoon: 'Raccoon returned no models. Confirm the account is signed in, then re-read the login state.',
|
|
276
|
+
modelsHint: 'Models are read only after sign-in.',
|
|
277
|
+
catalogNotice: 'Model list notice',
|
|
278
|
+
advancedFields: {
|
|
279
|
+
displayName: 'Group name',
|
|
280
|
+
visionModelIds: 'Force vision model IDs (JSON array)',
|
|
281
|
+
textOnlyModelIds: 'Force text-only model IDs (JSON array)',
|
|
282
|
+
modelAliases: 'Model aliases (JSON object)',
|
|
283
|
+
defaultMaxTokens: 'Default output limit',
|
|
284
|
+
},
|
|
285
|
+
loomyNav: 'Loomy',
|
|
286
|
+
loomyTitle: 'Loomy',
|
|
287
|
+
loomyIntro: 'Use “Open QR sign-in” and scan the code in WeChat; no client install needed. Changes apply immediately.',
|
|
288
|
+
raccoonNav: 'SenseTime Raccoon',
|
|
289
|
+
raccoonTitle: 'SenseTime Raccoon',
|
|
290
|
+
raccoonIntro: 'Use “Open QR sign-in” and scan the code in WeChat; no client install needed. Changes apply immediately.',
|
|
291
|
+
traeNav: 'Trae',
|
|
292
|
+
traeTitle: 'Trae',
|
|
293
|
+
traeIntro: 'Pick the mainland or international edition, then use either method: sign in on Trae’s website, or reuse the local Trae client’s sign-in state. A browser login renews itself; the mainland check-in button appears only with the client sign-in. Changes apply immediately.',
|
|
294
|
+
traeHint: 'A browser login uses the website session captured at sign-in; a client login uses the desktop client’s token and is imported again after the client refreshes or signs out. Check-in is mainland-only and client-login-only.',
|
|
295
|
+
traeLoginMissing: 'Trae is not signed in. Use browser sign-in, or reuse a signed-in local Trae client.',
|
|
296
|
+
traeClientTitle: 'Reuse the local client sign-in',
|
|
297
|
+
traeClientHint: 'A Trae desktop client was found. This only reads its current sign-in state and never modifies or signs out the client; reusing it replaces the plugin’s current login. The check-in button appears only after signing in this way.',
|
|
298
|
+
traeClientReady: 'The local client is signed in and ready to reuse.',
|
|
299
|
+
traeClientNotLoggedIn: 'A Trae desktop client was found but is not signed in. Sign in there, then re-read the login state.',
|
|
300
|
+
traeClientAccount: 'Client account',
|
|
301
|
+
traeClientUse: 'Reuse client sign-in',
|
|
302
|
+
traeClientReuse: 'Re-read client sign-in',
|
|
303
|
+
traeClientBusy: 'Reading…',
|
|
304
|
+
traeClientFailed: 'Could not read the client sign-in',
|
|
305
|
+
traeClientSource: 'Sign-in method',
|
|
306
|
+
traeSourceWeb: 'Browser',
|
|
307
|
+
traeSourceClient: 'Local client',
|
|
308
|
+
traeClientLogoutNote: 'Signing out only clears the plugin’s login; the local Trae client stays signed in.',
|
|
309
|
+
regionLabel: 'Region',
|
|
310
|
+
regionCn: 'Mainland · trae.cn',
|
|
311
|
+
regionGlobal: 'International · trae.ai',
|
|
312
|
+
regionHint: 'Each region keeps its own account: switching regions does not sign out the other one.',
|
|
313
|
+
regionSwitching: 'Switching region…',
|
|
314
|
+
checkin: 'Check-in',
|
|
315
|
+
checkinClaim: 'Check in for credits',
|
|
316
|
+
checkinDetect: 'Check today’s credits',
|
|
317
|
+
checkinDetecting: 'Checking…',
|
|
318
|
+
checkinBusy: 'Checking in…',
|
|
319
|
+
checkinClaimed: 'Already checked in today',
|
|
320
|
+
checkinDevice: 'This device checked in today',
|
|
321
|
+
checkinReward: 'reward',
|
|
322
|
+
checkinReceived: 'received',
|
|
323
|
+
checkinGrantedAt: 'credited at',
|
|
324
|
+
checkinOpenWeb: 'Open check-in page',
|
|
325
|
+
checkinFailed: 'Check-in failed',
|
|
326
|
+
checkinUnavailable: 'No check-in available for this account',
|
|
327
|
+
checkinMonitorTitle: 'Raccoon daily credits (detect only)',
|
|
328
|
+
checkinMonitorHint: 'The plugin only reads today’s credit ledger to detect a grant; it never claims on your behalf. If nothing arrived, claim it on the website with “Open check-in page”.',
|
|
329
|
+
plan: 'Plan',
|
|
330
|
+
planFree: 'Free',
|
|
331
|
+
planTrial: 'Trial',
|
|
332
|
+
trialEnds: 'Trial ends',
|
|
333
|
+
packs: 'Credit packs',
|
|
334
|
+
packExpires: 'expires',
|
|
335
|
+
packUsage: 'used / total',
|
|
336
|
+
packNearest: 'nearest expiry',
|
|
337
|
+
packDetail: 'Details',
|
|
338
|
+
packCollapse: 'Collapse',
|
|
339
|
+
usedOf: 'used',
|
|
340
|
+
usageReset: 'Resets',
|
|
341
|
+
browserHintTrae: 'The button opens a dedicated browser window (your own browser profile is untouched): sign in to Trae there and the window closes itself. If the international edition says “account is at risk”, use Log in with the same Google/GitHub account that works in your normal browser — do not create another email account; a fresh profile looks like a new device to Trae.',
|
|
342
|
+
browserPollingTrae: 'The browser is open: sign in to Trae in that window; the login is adopted automatically and the window then closes.',
|
|
343
|
+
browserTitleTrae: 'Browser sign-in',
|
|
344
|
+
browserOpenTrae: 'Open browser sign-in',
|
|
345
|
+
browserWaitingTrae: 'Waiting for sign-in…',
|
|
346
|
+
loginGuideTrae: 'Use “Open browser sign-in” and sign in to Trae in the window that opens; the login is adopted automatically. On the international edition, use Log in with your existing account instead of creating a new email account, which often triggers account is at risk.',
|
|
347
|
+
modelsEmptyTrae: 'Trae returned no models. Confirm the account is signed in, then re-read the login state.',
|
|
348
|
+
codeartsNav: 'Huawei CodeArts',
|
|
349
|
+
codeartsTitle: 'Huawei CodeArts',
|
|
350
|
+
codeartsIntro: 'Use “Open browser sign-in” to authorize on Huawei Cloud; the plugin signs CodeArts gateway calls with the resulting AK/SK and never reads the local IDE. Changes apply immediately.',
|
|
351
|
+
codeartsHint: 'CodeArts is called with a signature, not a token. The STS token lasts two hours and is renewed automatically; sign in again once the refresh token expires. Credit-billed accounts can check in once a day for credits.',
|
|
352
|
+
codeartsVisionHint: '“Vision” marks native image support only. Other models still accept images: kimi-k2.6-vl transcribes the picture to text first, and the transcript is cached per image, so the same picture costs one extra turn in total.',
|
|
353
|
+
browserHintCodeArts: 'The button opens Huawei Cloud’s authorization page: sign in and approve, then the browser returns to this machine and the host adopts the credential. Nothing has to be copied.',
|
|
354
|
+
browserPollingCodeArts: 'The Huawei Cloud page is open: finish authorizing and the login is adopted automatically; this tab can be closed.',
|
|
355
|
+
browserTitleCodeArts: 'Huawei Cloud authorization',
|
|
356
|
+
browserOpenCodeArts: 'Open browser sign-in',
|
|
357
|
+
browserWaitingCodeArts: 'Waiting for authorization…',
|
|
358
|
+
loginGuideCodeArts: 'Use “Open browser sign-in” and authorize on the Huawei Cloud page that opens; the login is adopted automatically.',
|
|
359
|
+
codeartsLoginMissing: 'CodeArts is not signed in. Use “Open browser sign-in” and authorize on Huawei Cloud.',
|
|
360
|
+
modelsEmptyCodeArts: 'CodeArts returned no models. Confirm the account is signed in, then re-read the login state.',
|
|
361
|
+
openRouterTitle: 'OpenRouter model sync',
|
|
362
|
+
openRouterHint: 'OpenRouter’s live catalog supplies thinking levels and image support for all three providers. The snapshot is cached on this machine, so a failed sync keeps the last successful data; refreshing a model list never clears it.',
|
|
363
|
+
openRouterInterval: 'Auto sync',
|
|
364
|
+
openRouterOff: 'Off',
|
|
365
|
+
openRouterEvery15: 'Every 15 minutes',
|
|
366
|
+
openRouterEvery60: 'Hourly',
|
|
367
|
+
openRouterEvery360: 'Every 6 hours',
|
|
368
|
+
openRouterEvery1440: 'Every 24 hours',
|
|
369
|
+
openRouterSyncNow: 'Sync now',
|
|
370
|
+
openRouterSyncing: 'Syncing…',
|
|
371
|
+
openRouterNever: 'Not synced yet',
|
|
372
|
+
openRouterModels: 'models',
|
|
373
|
+
openRouterLastSync: 'Last sync',
|
|
374
|
+
openRouterSyncFailed: 'Sync failed',
|
|
375
|
+
openRouterSyncDone: 'Synced',
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const BRANDS = [
|
|
379
|
+
{ id: 'loomy', settingsId: 'loomy', order: 40, titleKey: 'loomyTitle', navKey: 'loomyNav', introKey: 'loomyIntro' },
|
|
380
|
+
{ id: 'raccoon', settingsId: 'raccoon', order: 41, titleKey: 'raccoonTitle', navKey: 'raccoonNav', introKey: 'raccoonIntro' },
|
|
381
|
+
{ id: 'trae', settingsId: 'trae', order: 42, titleKey: 'traeTitle', navKey: 'traeNav', introKey: 'traeIntro' },
|
|
382
|
+
{ id: 'codearts', settingsId: 'codearts', order: 43, titleKey: 'codeartsTitle', navKey: 'codeartsNav', introKey: 'codeartsIntro' },
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
const FIELDS = {
|
|
386
|
+
loomy: {
|
|
387
|
+
enabled: 'loomyEnabled',
|
|
388
|
+
displayName: 'loomyDisplayName',
|
|
389
|
+
disabled: 'loomyDisabledModels',
|
|
390
|
+
windows: 'loomyModelContextWindows',
|
|
391
|
+
aliases: 'loomyModelAliases',
|
|
392
|
+
vision: 'loomyVisionModelIds',
|
|
393
|
+
textOnly: 'loomyTextOnlyModelIds',
|
|
394
|
+
},
|
|
395
|
+
raccoon: {
|
|
396
|
+
enabled: 'raccoonEnabled',
|
|
397
|
+
displayName: 'raccoonDisplayName',
|
|
398
|
+
disabled: 'raccoonDisabledModels',
|
|
399
|
+
windows: 'raccoonModelContextWindows',
|
|
400
|
+
aliases: 'raccoonModelAliases',
|
|
401
|
+
vision: 'raccoonVisionModelIds',
|
|
402
|
+
textOnly: 'raccoonTextOnlyModelIds',
|
|
403
|
+
},
|
|
404
|
+
trae: {
|
|
405
|
+
enabled: 'traeEnabled',
|
|
406
|
+
displayName: 'traeDisplayName',
|
|
407
|
+
disabled: 'traeDisabledModels',
|
|
408
|
+
windows: 'traeModelContextWindows',
|
|
409
|
+
aliases: 'traeModelAliases',
|
|
410
|
+
vision: 'traeVisionModelIds',
|
|
411
|
+
textOnly: 'traeTextOnlyModelIds',
|
|
412
|
+
region: 'traeRegion',
|
|
413
|
+
},
|
|
414
|
+
codearts: {
|
|
415
|
+
enabled: 'codeartsEnabled',
|
|
416
|
+
displayName: 'codeartsDisplayName',
|
|
417
|
+
disabled: 'codeartsDisabledModels',
|
|
418
|
+
windows: 'codeartsModelContextWindows',
|
|
419
|
+
aliases: 'codeartsModelAliases',
|
|
420
|
+
vision: 'codeartsVisionModelIds',
|
|
421
|
+
textOnly: 'codeartsTextOnlyModelIds',
|
|
422
|
+
},
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const REGIONS = [
|
|
426
|
+
{ id: 'cn', labelKey: 'regionCn' },
|
|
427
|
+
{ id: 'global', labelKey: 'regionGlobal' },
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
const OPENROUTER_INTERVALS = [
|
|
431
|
+
{ value: 0, labelKey: 'openRouterOff' },
|
|
432
|
+
{ value: 15, labelKey: 'openRouterEvery15' },
|
|
433
|
+
{ value: 60, labelKey: 'openRouterEvery60' },
|
|
434
|
+
{ value: 360, labelKey: 'openRouterEvery360' },
|
|
435
|
+
{ value: 1440, labelKey: 'openRouterEvery1440' },
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
function regionOf(value) {
|
|
439
|
+
return value === 'global' ? 'global' : 'cn'
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const css = {
|
|
443
|
+
page: { maxWidth: '860px', display: 'flex', flexDirection: 'column', gap: '14px', color: 'var(--dsw-alias-label-primary)' },
|
|
444
|
+
h2: { margin: '0', fontSize: '1.25em' },
|
|
445
|
+
h3: { margin: 0, fontSize: '0.95em' },
|
|
446
|
+
intro: { margin: '0', opacity: 0.72, lineHeight: 1.55, fontSize: '0.92em' },
|
|
447
|
+
bar: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' },
|
|
448
|
+
btn: { padding: '6px 12px', borderRadius: '7px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))', background: 'transparent', color: 'inherit', cursor: 'pointer', font: 'inherit', fontSize: '0.88em' },
|
|
449
|
+
btnPrimary: { border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.45))', background: '#0b0b0b', color: '#fff', fontWeight: 500 },
|
|
450
|
+
btnAlert: { border: '1px solid var(--dsw-alias-label-danger, #e03131)', color: 'var(--dsw-alias-label-danger, #e03131)' },
|
|
451
|
+
loginBox: { display: 'flex', flexDirection: 'column', gap: '10px', padding: '12px 14px', borderRadius: '10px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))' },
|
|
452
|
+
h4: { margin: 0, fontSize: '0.92em' },
|
|
453
|
+
input: { padding: '6px 10px', borderRadius: '7px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))', background: 'var(--dsw-alias-bg-base, transparent)', color: 'inherit', font: 'inherit', fontSize: '0.88em' },
|
|
454
|
+
search: { flex: '1 1 180px', minWidth: '140px' },
|
|
455
|
+
count: { fontSize: '0.85em', opacity: 0.7, marginLeft: 'auto' },
|
|
456
|
+
list: { display: 'flex', flexDirection: 'column', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))', borderRadius: '10px', overflow: 'hidden' },
|
|
457
|
+
row: { display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 14px', borderTop: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))' },
|
|
458
|
+
rowFirst: { borderTop: 'none' },
|
|
459
|
+
rowOff: { opacity: 0.5 },
|
|
460
|
+
name: { flex: '1 1 auto', minWidth: 0, display: 'flex', flexDirection: 'column', gap: '2px' },
|
|
461
|
+
nameText: { fontSize: '0.92em', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
462
|
+
id: { fontSize: '0.76em', opacity: 0.55, fontFamily: 'ui-monospace, monospace' },
|
|
463
|
+
tag: { fontSize: '0.72em', padding: '1px 6px', borderRadius: '5px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))', opacity: 0.75 },
|
|
464
|
+
price: { fontSize: '0.9em', opacity: 0.7, fontWeight: 400 },
|
|
465
|
+
select: { padding: '5px 8px', borderRadius: '7px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))', background: 'var(--dsw-alias-bg-base, transparent)', color: 'inherit', font: 'inherit', fontSize: '0.85em', minWidth: '104px' },
|
|
466
|
+
switch: { width: '17px', height: '17px', cursor: 'pointer', accentColor: 'var(--dsw-alias-brand-primary, #4c6ef5)' },
|
|
467
|
+
note: { fontSize: '0.82em', opacity: 0.62, lineHeight: 1.5 },
|
|
468
|
+
err: { fontSize: '0.86em', color: 'var(--dsw-alias-label-danger, #e03131)' },
|
|
469
|
+
guide: { display: 'flex', flexDirection: 'column', gap: '8px', padding: '12px 14px', borderRadius: '10px', border: '1px solid var(--dsw-alias-label-danger, #e03131)', background: 'rgba(224,49,49,0.07)' },
|
|
470
|
+
guideText: { fontSize: '0.88em', lineHeight: 1.5, color: 'var(--dsw-alias-label-danger, #e03131)' },
|
|
471
|
+
group: { display: 'flex', flexDirection: 'column', gap: '10px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))', borderRadius: '10px', padding: '14px 16px' },
|
|
472
|
+
card: { display: 'flex', flexDirection: 'column', gap: '10px', padding: '14px 16px', borderRadius: '10px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))', background: 'var(--dsw-alias-bg-base, rgba(128,128,128,0.03))' },
|
|
473
|
+
cardHead: { display: 'flex', alignItems: 'baseline', gap: '10px', flexWrap: 'wrap' },
|
|
474
|
+
line: { display: 'flex', alignItems: 'baseline', gap: '8px', flexWrap: 'wrap', fontSize: '0.86em' },
|
|
475
|
+
label: { opacity: 0.62, minWidth: '78px' },
|
|
476
|
+
value: { fontWeight: 500 },
|
|
477
|
+
mono: { fontFamily: 'ui-monospace, monospace', fontSize: '0.8em', opacity: 0.7, wordBreak: 'break-all' },
|
|
478
|
+
link: { color: 'var(--dsw-alias-brand-primary, #4c6ef5)', textDecoration: 'none', fontSize: '0.86em' },
|
|
479
|
+
foot: { display: 'flex', alignItems: 'center', gap: '14px', flexWrap: 'wrap', fontSize: '0.8em', opacity: 0.6 },
|
|
480
|
+
optionRow: { display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 0', borderTop: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))' },
|
|
481
|
+
optionFirst: { borderTop: 'none', paddingTop: 0 },
|
|
482
|
+
optionText: { flex: '1 1 auto', minWidth: 0, display: 'flex', flexDirection: 'column', gap: '2px' },
|
|
483
|
+
optionName: { fontSize: '0.92em', fontWeight: 500 },
|
|
484
|
+
optionStatus: { fontSize: '0.82em', opacity: 0.62 },
|
|
485
|
+
advLabel: { fontSize: '0.88em' },
|
|
486
|
+
advHelp: { fontSize: '0.78em', opacity: 0.55, lineHeight: 1.4 },
|
|
487
|
+
regionBox: { display: 'flex', flexDirection: 'column', gap: '6px', paddingBottom: '10px', borderBottom: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))' },
|
|
488
|
+
seg: { display: 'flex', gap: '6px', flexWrap: 'wrap' },
|
|
489
|
+
segBtn: { padding: '4px 10px', borderRadius: '7px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))', background: 'transparent', color: 'inherit', cursor: 'pointer', font: 'inherit', fontSize: '0.84em' },
|
|
490
|
+
segOn: { border: '1px solid var(--dsw-alias-brand-primary, #4c6ef5)', fontWeight: 600 },
|
|
491
|
+
section: { display: 'flex', flexDirection: 'column', gap: '10px', paddingTop: '14px', borderTop: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))' },
|
|
492
|
+
packBlock: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '2px' },
|
|
493
|
+
packHead: { display: 'flex', alignItems: 'baseline', gap: '8px' },
|
|
494
|
+
packToggle: { marginLeft: 'auto', background: 'none', border: 'none', padding: 0, font: 'inherit', fontSize: '0.8em', color: 'var(--dsw-alias-brand-primary, #4c6ef5)', cursor: 'pointer' },
|
|
495
|
+
packList: { display: 'flex', flexDirection: 'column', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))', overflow: 'hidden' },
|
|
496
|
+
packListScroll: { maxHeight: '190px', overflowY: 'auto' },
|
|
497
|
+
packRow: { display: 'grid', gridTemplateColumns: 'minmax(90px, 1.5fr) auto auto', alignItems: 'baseline', gap: '10px', padding: '6px 10px', borderTop: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))', fontSize: '0.82em' },
|
|
498
|
+
packFirst: { borderTop: 'none' },
|
|
499
|
+
packLabel: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
500
|
+
packAmount: { fontFamily: 'ui-monospace, monospace', textAlign: 'right', whiteSpace: 'nowrap' },
|
|
501
|
+
packExpiry: { opacity: 0.6, whiteSpace: 'nowrap', fontSize: '0.92em' },
|
|
502
|
+
meterBox: { display: 'flex', flexDirection: 'column', gap: '5px', marginTop: '2px' },
|
|
503
|
+
meterText: { display: 'flex', alignItems: 'baseline', gap: '8px', flexWrap: 'wrap', fontSize: '0.86em' },
|
|
504
|
+
meterPercent: { marginLeft: 'auto', opacity: 0.62, fontSize: '0.92em' },
|
|
505
|
+
meter: { height: '6px', borderRadius: '999px', background: 'var(--dsw-alias-border-l2, rgba(128,128,128,0.25))', overflow: 'hidden' },
|
|
506
|
+
meterFill: { height: '100%', borderRadius: '999px', background: 'var(--dsw-alias-brand-primary, #4c6ef5)' },
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const API = (() => {
|
|
510
|
+
try { return new URL('api/llm-adapters', document.baseURI).pathname } catch { return '/api/llm-adapters' }
|
|
511
|
+
})()
|
|
512
|
+
|
|
513
|
+
const CACHE_PREFIX = 'dsh-llm-adapters/cache/v1'
|
|
514
|
+
const cacheMemory = new Map()
|
|
515
|
+
|
|
516
|
+
function cacheKey(brand, kind) {
|
|
517
|
+
return `${CACHE_PREFIX}:${brand}:${kind}`
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function readCache(brand, kind) {
|
|
521
|
+
const key = cacheKey(brand, kind)
|
|
522
|
+
if (cacheMemory.has(key)) return cacheMemory.get(key)
|
|
523
|
+
try {
|
|
524
|
+
const raw = window.localStorage.getItem(key)
|
|
525
|
+
if (raw === null) return undefined
|
|
526
|
+
const parsed = JSON.parse(raw)
|
|
527
|
+
cacheMemory.set(key, parsed)
|
|
528
|
+
return parsed
|
|
529
|
+
} catch {
|
|
530
|
+
return undefined
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function writeCache(brand, kind, value) {
|
|
535
|
+
const key = cacheKey(brand, kind)
|
|
536
|
+
cacheMemory.set(key, value)
|
|
537
|
+
try { window.localStorage.setItem(key, JSON.stringify(value)) } catch { /* private mode */ }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function api(path, options) {
|
|
541
|
+
const { timeout = 30_000, ...rest } = options ?? {}
|
|
542
|
+
const ctrl = new AbortController()
|
|
543
|
+
const timer = setTimeout(() => ctrl.abort(), timeout)
|
|
544
|
+
try {
|
|
545
|
+
const response = await fetch(`${API}${path}`, { ...rest, signal: ctrl.signal })
|
|
546
|
+
const text = await response.text()
|
|
547
|
+
let payload
|
|
548
|
+
try { payload = text === '' ? {} : JSON.parse(text) } catch { payload = { error: text.slice(0, 200) } }
|
|
549
|
+
if (!response.ok) throw new Error(payload?.error ?? `HTTP ${response.status}`)
|
|
550
|
+
return payload
|
|
551
|
+
} finally {
|
|
552
|
+
clearTimeout(timer)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const WRITE_TIMEOUT_MS = 20_000
|
|
557
|
+
|
|
558
|
+
function withDeadline(promise, ms, message) {
|
|
559
|
+
return new Promise((resolve, reject) => {
|
|
560
|
+
const timer = setTimeout(() => reject(new Error(message)), ms)
|
|
561
|
+
promise.then(
|
|
562
|
+
value => { clearTimeout(timer); resolve(value) },
|
|
563
|
+
error => { clearTimeout(timer); reject(error) },
|
|
564
|
+
)
|
|
565
|
+
})
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function windowLabel(n) {
|
|
569
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) return '—'
|
|
570
|
+
if (n >= 1_000_000) return `${Number((n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1))}M`
|
|
571
|
+
if (n >= 1000) return `${Math.round(n / 1000)}K`
|
|
572
|
+
return String(n)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function creditsOf(value) {
|
|
576
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return '—'
|
|
577
|
+
return value.toLocaleString('zh-CN', { maximumFractionDigits: 2 })
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Trae's international plan reports dollars; every other quota is credits. */
|
|
581
|
+
function amountText(quota, value) {
|
|
582
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return '—'
|
|
583
|
+
return quota?.unit === 'usd' ? `$${value.toFixed(2)}` : creditsOf(value)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* A usage bar needs both halves: what was spent and what the account was
|
|
588
|
+
* granted. Loomy and Raccoon only report what is left, so they keep the
|
|
589
|
+
* plain figure rather than a bar with an invented denominator.
|
|
590
|
+
*/
|
|
591
|
+
function meterPercentOf(quota) {
|
|
592
|
+
const spent = typeof quota?.used === 'number' && Number.isFinite(quota.used) ? quota.used : undefined
|
|
593
|
+
const granted = typeof quota?.total === 'number' && Number.isFinite(quota.total) ? quota.total : undefined
|
|
594
|
+
if (spent === undefined || granted === undefined || granted <= 0) return undefined
|
|
595
|
+
return Math.min(100, Math.max(0, (spent / granted) * 100))
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function percentLabel(value) {
|
|
599
|
+
const text = value > 0 && value < 1
|
|
600
|
+
? value.toFixed(2)
|
|
601
|
+
: value < 10 ? value.toFixed(1) : String(Math.round(value))
|
|
602
|
+
return `${text}%`
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function dateLabel(value) {
|
|
606
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return '—'
|
|
607
|
+
if (value > Date.now() + 100 * 365 * 24 * 3600 * 1000) return '—'
|
|
608
|
+
return new Date(value).toLocaleString('zh-CN', {
|
|
609
|
+
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function priceText(t, factor) {
|
|
614
|
+
if (typeof factor !== 'number' || !Number.isFinite(factor) || factor < 0) return ''
|
|
615
|
+
if (factor === 0) return t('free')
|
|
616
|
+
return `x${Number(factor.toFixed(2))}`
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function ownerLabel(t, owner) {
|
|
620
|
+
if (typeof owner !== 'string' || owner === '') return '—'
|
|
621
|
+
if (owner === 'users' || owner === 'user' || owner === 'personal') return t('ownerPersonal')
|
|
622
|
+
if (owner === 'org' || owner === 'enterprise') return t('ownerEnterprise')
|
|
623
|
+
return owner
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function AccountCard({ t, brand, region, regionBusy, onRegionChange, reloadSignal, onStatus, onAfterRefresh }) {
|
|
627
|
+
// Trae keeps one snapshot per region, so switching back and forth never
|
|
628
|
+
// shows the other region's account while a fresh read is in flight.
|
|
629
|
+
const cacheId = brand.id === 'trae' ? `trae:${region}` : brand.id
|
|
630
|
+
const cached = readCache(cacheId, 'status')
|
|
631
|
+
const [status, setStatus] = useState(cached)
|
|
632
|
+
const [busy, setBusy] = useState(false)
|
|
633
|
+
const [error, setError] = useState(undefined)
|
|
634
|
+
const seenReload = useRef(reloadSignal)
|
|
635
|
+
const mounted = useRef(true)
|
|
636
|
+
const [browser, setBrowser] = useState(undefined)
|
|
637
|
+
const [actionBusy, setActionBusy] = useState(false)
|
|
638
|
+
const [actionError, setActionError] = useState(undefined)
|
|
639
|
+
const [checkinBusy, setCheckinBusy] = useState(false)
|
|
640
|
+
const [clientBusy, setClientBusy] = useState(false)
|
|
641
|
+
const [packsExpanded, setPacksExpanded] = useState(false)
|
|
642
|
+
const ticket = useRef(0)
|
|
643
|
+
|
|
644
|
+
const load = useCallback(async (fresh, notify) => {
|
|
645
|
+
const mine = ++ticket.current
|
|
646
|
+
setBusy(true)
|
|
647
|
+
setError(undefined)
|
|
648
|
+
try {
|
|
649
|
+
const payload = await api(`/status?brand=${brand.id}${fresh ? '&fresh=1' : ''}`, { timeout: 35_000 })
|
|
650
|
+
// A region switch starts a second read; the older one must not win.
|
|
651
|
+
if (!mounted.current || mine !== ticket.current) return
|
|
652
|
+
setStatus(payload)
|
|
653
|
+
writeCache(cacheId, 'status', { ...payload, cachedAt: Date.now() })
|
|
654
|
+
onStatus?.(payload)
|
|
655
|
+
if (notify && payload?.signedIn === true) onAfterRefresh?.()
|
|
656
|
+
} catch (err) {
|
|
657
|
+
if (!mounted.current || mine !== ticket.current) return
|
|
658
|
+
setError(String(err?.message ?? err))
|
|
659
|
+
} finally {
|
|
660
|
+
if (mounted.current && mine === ticket.current) setBusy(false)
|
|
661
|
+
}
|
|
662
|
+
}, [brand.id, cacheId, onStatus, onAfterRefresh])
|
|
663
|
+
|
|
664
|
+
useEffect(() => {
|
|
665
|
+
mounted.current = true
|
|
666
|
+
void load(false, false)
|
|
667
|
+
return () => { mounted.current = false }
|
|
668
|
+
}, [load])
|
|
669
|
+
|
|
670
|
+
// A region switch changes which stored account the card describes, so the
|
|
671
|
+
// previous region's snapshot is dropped before the new read lands.
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
setStatus(readCache(cacheId, 'status'))
|
|
674
|
+
setBrowser(undefined)
|
|
675
|
+
setActionError(undefined)
|
|
676
|
+
setPacksExpanded(false)
|
|
677
|
+
}, [cacheId])
|
|
678
|
+
|
|
679
|
+
useEffect(() => {
|
|
680
|
+
if (reloadSignal === seenReload.current) return
|
|
681
|
+
seenReload.current = reloadSignal
|
|
682
|
+
void load(true, true)
|
|
683
|
+
}, [reloadSignal, load])
|
|
684
|
+
|
|
685
|
+
// Follow the server-side browser login. The flow lives in the host, so it
|
|
686
|
+
// keeps going even if this page is closed and reopened mid-login.
|
|
687
|
+
useEffect(() => {
|
|
688
|
+
if (browser?.running !== true) return undefined
|
|
689
|
+
let alive = true
|
|
690
|
+
const tick = async () => {
|
|
691
|
+
try {
|
|
692
|
+
const payload = await api(`/account/browser?brand=${brand.id}`, { timeout: 10_000 })
|
|
693
|
+
if (!alive) return
|
|
694
|
+
setBrowser(payload)
|
|
695
|
+
if (payload?.running === false && payload?.signedIn === true) {
|
|
696
|
+
await load(true, true)
|
|
697
|
+
}
|
|
698
|
+
} catch {
|
|
699
|
+
/* transient; the next tick retries */
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const timer = setInterval(() => { void tick() }, 1500)
|
|
703
|
+
return () => { alive = false; clearInterval(timer) }
|
|
704
|
+
}, [browser?.running, brand.id, load])
|
|
705
|
+
|
|
706
|
+
const signedIn = status?.signedIn === true
|
|
707
|
+
const account = status?.account ?? {}
|
|
708
|
+
const accountText = [account.name, account.phone, account.ownerType === undefined ? undefined : ownerLabel(t, account.ownerType)]
|
|
709
|
+
.filter(part => typeof part === 'string' && part.length > 0)
|
|
710
|
+
.join(' · ') || t('accountUnknown')
|
|
711
|
+
const quota = status?.quota
|
|
712
|
+
|
|
713
|
+
const accountRequest = useCallback(async body => api('/account', {
|
|
714
|
+
method: 'POST',
|
|
715
|
+
headers: { 'content-type': 'application/json' },
|
|
716
|
+
body: JSON.stringify({ brand: brand.id, ...(brand.id === 'trae' ? { region } : {}), ...body }),
|
|
717
|
+
timeout: 45_000,
|
|
718
|
+
}), [brand.id, region])
|
|
719
|
+
|
|
720
|
+
const logout = useCallback(async () => {
|
|
721
|
+
if (typeof window.confirm === 'function' && !window.confirm(t('loginConfirmLogout'))) return
|
|
722
|
+
setActionBusy(true)
|
|
723
|
+
setActionError(undefined)
|
|
724
|
+
try {
|
|
725
|
+
const payload = await accountRequest({ action: 'logout' })
|
|
726
|
+
setStatus(payload)
|
|
727
|
+
writeCache(cacheId, 'status', { ...payload, cachedAt: Date.now() })
|
|
728
|
+
onStatus?.(payload)
|
|
729
|
+
onAfterRefresh?.()
|
|
730
|
+
} catch (err) {
|
|
731
|
+
setActionError(`${t('loginFailedPrefix')}:${String(err?.message ?? err)}`)
|
|
732
|
+
} finally {
|
|
733
|
+
setActionBusy(false)
|
|
734
|
+
}
|
|
735
|
+
}, [accountRequest, cacheId, onStatus, onAfterRefresh, t])
|
|
736
|
+
|
|
737
|
+
const startBrowserLogin = useCallback(async () => {
|
|
738
|
+
setActionError(undefined)
|
|
739
|
+
try {
|
|
740
|
+
const payload = await accountRequest({ action: 'browser' })
|
|
741
|
+
setBrowser(payload)
|
|
742
|
+
if (typeof payload?.url === 'string' && payload.url !== '' && typeof window.open === 'function') {
|
|
743
|
+
window.open(payload.url, '_blank', 'noopener')
|
|
744
|
+
}
|
|
745
|
+
} catch (err) {
|
|
746
|
+
setActionError(`${t('loginFailedPrefix')}:${String(err?.message ?? err)}`)
|
|
747
|
+
}
|
|
748
|
+
}, [accountRequest, t])
|
|
749
|
+
|
|
750
|
+
const cancelBrowserLogin = useCallback(async () => {
|
|
751
|
+
try {
|
|
752
|
+
const payload = await accountRequest({ action: 'browser-cancel' })
|
|
753
|
+
setBrowser(payload)
|
|
754
|
+
} catch (err) {
|
|
755
|
+
setActionError(`${t('loginFailedPrefix')}:${String(err?.message ?? err)}`)
|
|
756
|
+
}
|
|
757
|
+
}, [accountRequest, t])
|
|
758
|
+
|
|
759
|
+
const importClientLogin = useCallback(async () => {
|
|
760
|
+
setClientBusy(true)
|
|
761
|
+
setActionError(undefined)
|
|
762
|
+
try {
|
|
763
|
+
const payload = await accountRequest({ action: 'client-login' })
|
|
764
|
+
setStatus(payload)
|
|
765
|
+
setBrowser({ running: false, phase: 'idle' })
|
|
766
|
+
writeCache(cacheId, 'status', { ...payload, cachedAt: Date.now() })
|
|
767
|
+
onStatus?.(payload)
|
|
768
|
+
onAfterRefresh?.()
|
|
769
|
+
} catch (err) {
|
|
770
|
+
setActionError(`${t('traeClientFailed')}:${String(err?.message ?? err)}`)
|
|
771
|
+
} finally {
|
|
772
|
+
setClientBusy(false)
|
|
773
|
+
}
|
|
774
|
+
}, [accountRequest, cacheId, onStatus, onAfterRefresh, t])
|
|
775
|
+
|
|
776
|
+
// Claiming is the only state-changing account action. The reward changes
|
|
777
|
+
// the quota, so the reading is refreshed afterwards instead of guessed.
|
|
778
|
+
//
|
|
779
|
+
// Raccoon has no claim endpoint: its points appear on the day's first
|
|
780
|
+
// website visit. Pressing this used to replay that visit for the user, which
|
|
781
|
+
// triggered the grant without them asking at that moment — so for Raccoon
|
|
782
|
+
// the button only re-reads today's ledger and the link does the claiming.
|
|
783
|
+
const detectOnly = brand.id === 'raccoon'
|
|
784
|
+
const claimCheckin = useCallback(async () => {
|
|
785
|
+
setCheckinBusy(true)
|
|
786
|
+
setActionError(undefined)
|
|
787
|
+
try {
|
|
788
|
+
if (detectOnly) {
|
|
789
|
+
await load(true, false)
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
const payload = await accountRequest({ action: 'checkin' })
|
|
793
|
+
if (payload?.checkin !== undefined) {
|
|
794
|
+
setStatus(previous => previous === undefined
|
|
795
|
+
? previous
|
|
796
|
+
: { ...previous, checkin: payload.checkin })
|
|
797
|
+
}
|
|
798
|
+
await load(true, false)
|
|
799
|
+
} catch (err) {
|
|
800
|
+
setActionError(`${t('checkinFailed')}:${String(err?.message ?? err)}`)
|
|
801
|
+
} finally {
|
|
802
|
+
setCheckinBusy(false)
|
|
803
|
+
}
|
|
804
|
+
}, [accountRequest, detectOnly, load, t])
|
|
805
|
+
|
|
806
|
+
const quotaContent = (() => {
|
|
807
|
+
if (busy && quota === undefined) return h('div', { style: css.note }, t('quotaRefreshing'))
|
|
808
|
+
if (status?.quotaError !== undefined) return h('div', { style: css.err }, `${t('quotaFailed')}:${status.quotaError}`)
|
|
809
|
+
if (quota === undefined) return h('div', { style: css.note }, signedIn ? t('quotaUnavailable') : t('quotaRefreshing'))
|
|
810
|
+
if (brand.id === 'loomy') {
|
|
811
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '5px' } },
|
|
812
|
+
h('div', { style: css.line },
|
|
813
|
+
h('span', { style: css.label }, t('loomyAvailable')),
|
|
814
|
+
h('span', { style: css.value }, `${creditsOf(quota.available ?? quota.total)} ${t('points')}`),
|
|
815
|
+
),
|
|
816
|
+
h('div', { style: css.line },
|
|
817
|
+
h('span', { style: css.label }, t('loomyPermanent')),
|
|
818
|
+
h('span', { style: css.value }, creditsOf(quota.permanent)),
|
|
819
|
+
quota.daily === undefined ? null : h('span', { style: css.label }, t('loomyDaily')),
|
|
820
|
+
quota.daily === undefined ? null : h('span', { style: css.value }, creditsOf(quota.daily)),
|
|
821
|
+
),
|
|
822
|
+
quota.cycle === undefined ? null : h('div', { style: css.line },
|
|
823
|
+
h('span', { style: css.label }, t('loomyCycle')),
|
|
824
|
+
h('span', { style: css.value }, quota.cycle),
|
|
825
|
+
),
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
if (brand.id === 'trae' || brand.id === 'codearts') {
|
|
829
|
+
const packs = Array.isArray(quota.packs) ? quota.packs : []
|
|
830
|
+
const plan = typeof quota.plan === 'string' && quota.plan !== '' ? quota.plan : undefined
|
|
831
|
+
const spent = typeof quota.used === 'number' && Number.isFinite(quota.used) ? quota.used : undefined
|
|
832
|
+
const granted = typeof quota.total === 'number' && Number.isFinite(quota.total) ? quota.total : undefined
|
|
833
|
+
const percent = meterPercentOf(quota)
|
|
834
|
+
const unitSuffix = quota.unit === 'usd' ? '' : ` ${t('points')}`
|
|
835
|
+
const usedText = spent === undefined
|
|
836
|
+
? undefined
|
|
837
|
+
: `${amountText(quota, spent)}${granted === undefined ? '' : ` / ${amountText(quota, granted)}`}${unitSuffix}`
|
|
838
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '5px' } },
|
|
839
|
+
plan === undefined && granted === undefined && usedText === undefined && packs.length === 0
|
|
840
|
+
? h('div', { style: css.note }, t('quotaUnavailable'))
|
|
841
|
+
: null,
|
|
842
|
+
plan === undefined ? null : h('div', { style: css.line },
|
|
843
|
+
h('span', { style: css.label }, t('plan')),
|
|
844
|
+
h('span', { style: css.value }, plan),
|
|
845
|
+
quota.trialEndsAt === undefined ? null : h('span', { style: css.label }, t('trialEnds')),
|
|
846
|
+
quota.trialEndsAt === undefined ? null : h('span', { style: css.value }, dateLabel(quota.trialEndsAt)),
|
|
847
|
+
),
|
|
848
|
+
// Both halves present → a bar. A visible sliver is kept for a
|
|
849
|
+
// spend that rounds to zero, or the bar reads as "untouched".
|
|
850
|
+
percent === undefined
|
|
851
|
+
? granted === undefined
|
|
852
|
+
? null
|
|
853
|
+
: h('div', { style: css.line },
|
|
854
|
+
h('span', { style: css.label }, t('quota')),
|
|
855
|
+
h('span', { style: css.value }, `${amountText(quota, granted)}${unitSuffix}`),
|
|
856
|
+
)
|
|
857
|
+
: h('div', { style: css.meterBox },
|
|
858
|
+
h('div', { style: css.meterText },
|
|
859
|
+
h('span', { style: css.label }, t('usedOf')),
|
|
860
|
+
h('span', { style: css.value }, usedText),
|
|
861
|
+
h('span', { style: css.meterPercent }, percentLabel(percent)),
|
|
862
|
+
),
|
|
863
|
+
h('div', { style: css.meter },
|
|
864
|
+
h('div', {
|
|
865
|
+
style: { ...css.meterFill, width: `${percent > 0 ? Math.max(percent, 1.5) : 0}%` },
|
|
866
|
+
}),
|
|
867
|
+
),
|
|
868
|
+
),
|
|
869
|
+
percent === undefined && usedText !== undefined
|
|
870
|
+
? h('div', { style: css.line },
|
|
871
|
+
h('span', { style: css.label }, t('usedOf')),
|
|
872
|
+
h('span', { style: css.value }, usedText),
|
|
873
|
+
quota.ratio === undefined ? null : h('span', { style: css.label }, `${Math.round(quota.ratio)}%`),
|
|
874
|
+
)
|
|
875
|
+
: null,
|
|
876
|
+
quota.resetAt === undefined ? null : h('div', { style: css.line },
|
|
877
|
+
h('span', { style: css.label }, t('usageReset')),
|
|
878
|
+
h('span', { style: css.value }, dateLabel(quota.resetAt)),
|
|
879
|
+
),
|
|
880
|
+
packs.length === 0 ? null : (() => {
|
|
881
|
+
// Collapsed (the default) is one summary row: how many packs, the
|
|
882
|
+
// combined total and the nearest expiry. Expanding lists every
|
|
883
|
+
// pack, aligned in a bounded scroll area.
|
|
884
|
+
const total = packs.reduce((sum, pack) => sum + (pack.total ?? pack.used ?? 0), 0)
|
|
885
|
+
const nearest = packs
|
|
886
|
+
.map(pack => pack.expiresAt)
|
|
887
|
+
.filter(value => typeof value === 'number')
|
|
888
|
+
.sort((a, b) => a - b)[0]
|
|
889
|
+
return h('div', { style: css.packBlock },
|
|
890
|
+
h('div', { style: css.packHead },
|
|
891
|
+
h('span', { style: css.label }, `${t('packs')}(${packs.length})`),
|
|
892
|
+
h('span', { style: css.value }, `${creditsOf(total)} ${t('points')}`),
|
|
893
|
+
nearest === undefined ? null : h('span', { style: css.packExpiry }, `${t('packNearest')} ${dateLabel(nearest)}`),
|
|
894
|
+
h('button', {
|
|
895
|
+
style: css.packToggle,
|
|
896
|
+
onClick: () => setPacksExpanded(value => !value),
|
|
897
|
+
}, packsExpanded ? t('packCollapse') : `(${t('packDetail')})`),
|
|
898
|
+
),
|
|
899
|
+
packsExpanded
|
|
900
|
+
? h('div', { style: css.packBlock },
|
|
901
|
+
h('div', { style: css.packHead },
|
|
902
|
+
h('span', { style: css.label }, t('packUsage')),
|
|
903
|
+
),
|
|
904
|
+
h('div', { style: { ...css.packList, ...css.packListScroll } },
|
|
905
|
+
packs.map((pack, index) => h('div', {
|
|
906
|
+
key: `${pack.label}-${index}`,
|
|
907
|
+
style: { ...css.packRow, ...(index === 0 ? css.packFirst : {}) },
|
|
908
|
+
},
|
|
909
|
+
h('span', { style: css.packLabel, title: pack.label }, pack.label),
|
|
910
|
+
h('span', { style: css.packAmount }, pack.total === undefined
|
|
911
|
+
? (pack.used === undefined ? '—' : creditsOf(pack.used))
|
|
912
|
+
: `${creditsOf(pack.used ?? 0)} / ${creditsOf(pack.total)}`),
|
|
913
|
+
h('span', { style: css.packExpiry }, pack.expiresAt === undefined ? '' : dateLabel(pack.expiresAt)),
|
|
914
|
+
)),
|
|
915
|
+
),
|
|
916
|
+
)
|
|
917
|
+
: null,
|
|
918
|
+
)
|
|
919
|
+
})(),
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
return h('div', { style: css.line },
|
|
923
|
+
h('span', { style: css.label }, t('quota')),
|
|
924
|
+
h('span', { style: css.value }, `${creditsOf(quota.total)} ${t('points')}`),
|
|
925
|
+
)
|
|
926
|
+
})()
|
|
927
|
+
|
|
928
|
+
// Trae's check-in needs the device id only a client-login import has, so
|
|
929
|
+
// a web login must not show the panel even though the provider exposes
|
|
930
|
+
// the method. CodeArts grants one reward per account per day, but its
|
|
931
|
+
// device flag can also block the claim; the two reasons are shown apart
|
|
932
|
+
// so "本机已签" never reads as "this account already claimed".
|
|
933
|
+
const checkin = brand.id === 'trae'
|
|
934
|
+
? (status?.loginSource === 'client' ? status?.checkin : undefined)
|
|
935
|
+
: (brand.id === 'codearts' || brand.id === 'raccoon' ? status?.checkin : undefined)
|
|
936
|
+
const checkinBlocked = checkin === undefined
|
|
937
|
+
|| checkin.available === false
|
|
938
|
+
// A detect button stays useful after the points arrived, so only a real
|
|
939
|
+
// claim (or a device-level one) disables the action.
|
|
940
|
+
|| (!detectOnly && (checkin.claimed === true || checkin.deviceClaimed === true))
|
|
941
|
+
const checkinLabel = detectOnly
|
|
942
|
+
? (checkin?.claimed === true ? t('checkinClaimed') : t('checkinDetect'))
|
|
943
|
+
: checkin?.claimed === true
|
|
944
|
+
? t('checkinClaimed')
|
|
945
|
+
: checkin?.deviceClaimed === true ? t('checkinDevice') : t('checkinClaim')
|
|
946
|
+
// A failed activity read is shown rather than hidden: hiding it is what
|
|
947
|
+
// makes "no check-in" and "check-in broken" look the same.
|
|
948
|
+
const checkinNote = checkin === undefined
|
|
949
|
+
? undefined
|
|
950
|
+
: checkin.available === false
|
|
951
|
+
? checkin.message ?? t('checkinUnavailable')
|
|
952
|
+
: brand.id === 'raccoon' && checkin.claimed === false
|
|
953
|
+
? checkin.message
|
|
954
|
+
: undefined
|
|
955
|
+
// The timestamp is part of the claim, not decoration: without it a grant
|
|
956
|
+
// issued just after midnight reads as if it were still yesterday's.
|
|
957
|
+
const grantedAt = typeof checkin?.grantedAt === 'number' ? checkin.grantedAt : undefined
|
|
958
|
+
const rewardNote = checkin === undefined || checkin.reward === undefined
|
|
959
|
+
|| checkin.deviceClaimed === true
|
|
960
|
+
? null
|
|
961
|
+
: h('span', { style: css.note },
|
|
962
|
+
`${checkin.claimed === true ? t('checkinReceived') : t('checkinReward')} +${creditsOf(checkin.reward)} ${t('points')}`
|
|
963
|
+
+ (grantedAt === undefined ? '' : ` · ${t('checkinGrantedAt')} ${dateLabel(grantedAt)}`))
|
|
964
|
+
const checkinPanel = signedIn && checkin !== undefined
|
|
965
|
+
&& (checkin.available !== false || brand.id === 'codearts')
|
|
966
|
+
? h('div', { style: css.bar },
|
|
967
|
+
h('button', {
|
|
968
|
+
style: { ...css.btn, ...css.btnPrimary },
|
|
969
|
+
disabled: checkinBlocked || checkinBusy || clientBusy || actionBusy,
|
|
970
|
+
onClick: () => void claimCheckin(),
|
|
971
|
+
}, checkinBusy ? t(detectOnly ? 'checkinDetecting' : 'checkinBusy') : checkinLabel),
|
|
972
|
+
checkinNote === undefined ? null : h('span', { style: css.note }, checkinNote),
|
|
973
|
+
rewardNote,
|
|
974
|
+
typeof checkin.url === 'string' && checkin.url !== ''
|
|
975
|
+
? h('a', {
|
|
976
|
+
href: checkin.url,
|
|
977
|
+
target: '_blank',
|
|
978
|
+
rel: 'noreferrer',
|
|
979
|
+
style: css.link,
|
|
980
|
+
}, t('checkinOpenWeb'))
|
|
981
|
+
: null,
|
|
982
|
+
)
|
|
983
|
+
: null
|
|
984
|
+
|
|
985
|
+
const browserRunning = browser?.running === true
|
|
986
|
+
const browserPanel = h('div', { style: css.loginBox },
|
|
987
|
+
h('h4', { style: css.h4 }, brand.id === 'trae'
|
|
988
|
+
? t('browserTitleTrae')
|
|
989
|
+
: brand.id === 'codearts' ? t('browserTitleCodeArts') : t('browserTitle')),
|
|
990
|
+
h('div', { style: css.note }, brand.id === 'loomy'
|
|
991
|
+
? t('browserHintLoomy')
|
|
992
|
+
: brand.id === 'raccoon'
|
|
993
|
+
? t('browserHintRaccoon')
|
|
994
|
+
: brand.id === 'codearts' ? t('browserHintCodeArts') : t('browserHintTrae')),
|
|
995
|
+
h('div', { style: css.bar },
|
|
996
|
+
h('button', {
|
|
997
|
+
style: { ...css.btn, ...css.btnPrimary },
|
|
998
|
+
disabled: browserRunning || actionBusy || regionBusy,
|
|
999
|
+
onClick: () => void startBrowserLogin(),
|
|
1000
|
+
}, browserRunning
|
|
1001
|
+
? brand.id === 'trae'
|
|
1002
|
+
? t('browserWaitingTrae')
|
|
1003
|
+
: brand.id === 'codearts' ? t('browserWaitingCodeArts') : t('browserWaiting')
|
|
1004
|
+
: brand.id === 'trae'
|
|
1005
|
+
? t('browserOpenTrae')
|
|
1006
|
+
: brand.id === 'codearts' ? t('browserOpenCodeArts') : t('browserOpen')),
|
|
1007
|
+
browserRunning
|
|
1008
|
+
? h('button', {
|
|
1009
|
+
style: css.btn,
|
|
1010
|
+
onClick: () => void cancelBrowserLogin(),
|
|
1011
|
+
}, t('browserCancel'))
|
|
1012
|
+
: null,
|
|
1013
|
+
browserRunning && typeof browser?.url === 'string'
|
|
1014
|
+
? h('a', {
|
|
1015
|
+
href: browser.url,
|
|
1016
|
+
target: '_blank',
|
|
1017
|
+
rel: 'noreferrer',
|
|
1018
|
+
style: css.link,
|
|
1019
|
+
}, t('browserReopen'))
|
|
1020
|
+
: null,
|
|
1021
|
+
),
|
|
1022
|
+
browserRunning
|
|
1023
|
+
? h('div', { style: css.note }, typeof browser?.message === 'string' && browser.message !== ''
|
|
1024
|
+
? browser.message
|
|
1025
|
+
: brand.id === 'trae'
|
|
1026
|
+
? t('browserPollingTrae')
|
|
1027
|
+
: brand.id === 'codearts' ? t('browserPollingCodeArts') : t('browserPolling'))
|
|
1028
|
+
: null,
|
|
1029
|
+
browser !== undefined && browser.running === false && browser.error !== undefined
|
|
1030
|
+
? h('div', { style: css.err }, browser.error)
|
|
1031
|
+
: null,
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
const clientLogin = brand.id === 'trae' ? status?.clientLogin : undefined
|
|
1035
|
+
const clientAccount = clientLogin?.signedIn === true
|
|
1036
|
+
? ([clientLogin.name, clientLogin.expiresAt === undefined
|
|
1037
|
+
? undefined
|
|
1038
|
+
: `${t('expires')} ${dateLabel(clientLogin.expiresAt)}`]
|
|
1039
|
+
.filter(part => typeof part === 'string' && part.length > 0)
|
|
1040
|
+
.join(' · ') || t('accountUnknown'))
|
|
1041
|
+
: undefined
|
|
1042
|
+
const clientPanel = brand.id === 'trae' && clientLogin?.available === true
|
|
1043
|
+
? h('div', { style: css.loginBox },
|
|
1044
|
+
h('h4', { style: css.h4 }, t('traeClientTitle')),
|
|
1045
|
+
h('div', { style: css.note }, t('traeClientHint')),
|
|
1046
|
+
clientLogin.signedIn === true
|
|
1047
|
+
? h('div', { style: css.line },
|
|
1048
|
+
h('span', { style: css.label }, t('traeClientAccount')),
|
|
1049
|
+
h('span', { style: css.value }, clientAccount),
|
|
1050
|
+
)
|
|
1051
|
+
: h('div', { style: css.note }, clientLogin.error ?? t('traeClientNotLoggedIn')),
|
|
1052
|
+
h('div', { style: css.bar },
|
|
1053
|
+
h('button', {
|
|
1054
|
+
style: { ...css.btn, ...(clientLogin.signedIn === true ? css.btnPrimary : {}) },
|
|
1055
|
+
disabled: clientBusy || checkinBusy || actionBusy || browserRunning || clientLogin.signedIn !== true,
|
|
1056
|
+
onClick: () => void importClientLogin(),
|
|
1057
|
+
}, clientBusy
|
|
1058
|
+
? t('traeClientBusy')
|
|
1059
|
+
: status?.loginSource === 'client' ? t('traeClientReuse') : t('traeClientUse')),
|
|
1060
|
+
clientLogin.signedIn === true
|
|
1061
|
+
? h('span', { style: css.note }, t('traeClientReady'))
|
|
1062
|
+
: null,
|
|
1063
|
+
),
|
|
1064
|
+
h('div', { style: css.note }, t('traeClientLogoutNote')),
|
|
1065
|
+
)
|
|
1066
|
+
: null
|
|
1067
|
+
|
|
1068
|
+
// Trae's region is a saved setting, not a tab: the selector writes the
|
|
1069
|
+
// same `traeRegion` the provider reads, and the page then re-reads both
|
|
1070
|
+
// the account and the catalog for the newly selected region.
|
|
1071
|
+
const regionPicker = brand.id === 'trae'
|
|
1072
|
+
? h('div', { style: css.regionBox },
|
|
1073
|
+
h('div', { style: css.bar },
|
|
1074
|
+
h('span', { style: css.label }, t('regionLabel')),
|
|
1075
|
+
h('div', { style: css.seg },
|
|
1076
|
+
...REGIONS.map(option => h('button', {
|
|
1077
|
+
key: option.id,
|
|
1078
|
+
style: { ...css.segBtn, ...(region === option.id ? css.segOn : {}) },
|
|
1079
|
+
disabled: regionBusy || busy,
|
|
1080
|
+
onClick: () => {
|
|
1081
|
+
if (region === option.id) return
|
|
1082
|
+
onRegionChange?.(option.id)
|
|
1083
|
+
},
|
|
1084
|
+
}, t(option.labelKey))),
|
|
1085
|
+
),
|
|
1086
|
+
regionBusy ? h('span', { style: css.note }, t('regionSwitching')) : null,
|
|
1087
|
+
),
|
|
1088
|
+
h('div', { style: css.note }, t('regionHint')),
|
|
1089
|
+
)
|
|
1090
|
+
: null
|
|
1091
|
+
|
|
1092
|
+
return h('div', { style: css.card },
|
|
1093
|
+
regionPicker,
|
|
1094
|
+
h('div', { style: css.cardHead },
|
|
1095
|
+
h('h3', { style: css.h3 }, t('accountTitle')),
|
|
1096
|
+
h('span', { style: { ...css.note, opacity: 0.75 } }, signedIn ? t('signedIn') : t('notLoggedIn')),
|
|
1097
|
+
h('button', {
|
|
1098
|
+
style: { ...css.btn, marginLeft: 'auto' },
|
|
1099
|
+
disabled: busy,
|
|
1100
|
+
onClick: () => { void load(true, true) },
|
|
1101
|
+
}, busy ? t('reading') : t('reread')),
|
|
1102
|
+
),
|
|
1103
|
+
h('div', { style: css.note }, brand.id === 'loomy'
|
|
1104
|
+
? t('loomyHint')
|
|
1105
|
+
: brand.id === 'raccoon'
|
|
1106
|
+
? t('raccoonHint')
|
|
1107
|
+
: brand.id === 'codearts' ? t('codeartsHint') : t('traeHint')),
|
|
1108
|
+
signedIn
|
|
1109
|
+
? h('div', { style: css.line },
|
|
1110
|
+
h('span', { style: css.label }, t('account')),
|
|
1111
|
+
h('span', { style: css.value }, accountText),
|
|
1112
|
+
)
|
|
1113
|
+
: null,
|
|
1114
|
+
signedIn
|
|
1115
|
+
? h('div', { style: css.line },
|
|
1116
|
+
h('span', { style: css.label }, t('expires')),
|
|
1117
|
+
h('span', { style: css.value }, dateLabel(status?.expiresAt)),
|
|
1118
|
+
)
|
|
1119
|
+
: null,
|
|
1120
|
+
signedIn && brand.id === 'trae'
|
|
1121
|
+
? h('div', { style: css.line },
|
|
1122
|
+
h('span', { style: css.label }, t('traeClientSource')),
|
|
1123
|
+
h('span', { style: css.value }, status?.loginSource === 'client'
|
|
1124
|
+
? t('traeSourceClient')
|
|
1125
|
+
: t('traeSourceWeb')),
|
|
1126
|
+
)
|
|
1127
|
+
: null,
|
|
1128
|
+
signedIn && brand.id === 'raccoon'
|
|
1129
|
+
? h('div', { style: css.line },
|
|
1130
|
+
h('span', { style: css.label }, t('raccoonOwner')),
|
|
1131
|
+
h('span', { style: css.value }, ownerLabel(t, account.ownerType)),
|
|
1132
|
+
)
|
|
1133
|
+
: null,
|
|
1134
|
+
signedIn ? quotaContent : null,
|
|
1135
|
+
checkinPanel,
|
|
1136
|
+
status?.possiblyExpired === true ? h('div', { style: css.err }, t('possiblyExpired')) : null,
|
|
1137
|
+
!signedIn && status !== undefined
|
|
1138
|
+
? h('div', { style: css.guide },
|
|
1139
|
+
h('div', { style: css.guideText }, status?.error ?? signInGuide(t, brand)),
|
|
1140
|
+
h('div', { style: css.note }, brand.id === 'trae'
|
|
1141
|
+
? t('loginGuideTrae')
|
|
1142
|
+
: brand.id === 'codearts' ? t('loginGuideCodeArts') : t('loginGuide')),
|
|
1143
|
+
)
|
|
1144
|
+
: null,
|
|
1145
|
+
signedIn
|
|
1146
|
+
? h('div', { style: css.bar },
|
|
1147
|
+
h('button', {
|
|
1148
|
+
style: { ...css.btn, ...css.btnAlert },
|
|
1149
|
+
disabled: actionBusy,
|
|
1150
|
+
onClick: () => void logout(),
|
|
1151
|
+
}, t('loginLogout')),
|
|
1152
|
+
)
|
|
1153
|
+
: null,
|
|
1154
|
+
!signedIn || status?.possiblyExpired === true ? browserPanel : null,
|
|
1155
|
+
clientPanel,
|
|
1156
|
+
actionError !== undefined ? h('div', { style: css.err }, actionError) : null,
|
|
1157
|
+
error !== undefined ? h('div', { style: css.err }, `${t('loadFailed')}:${error}`) : null,
|
|
1158
|
+
)
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function AdvancedInput({ t, form, fieldKey, label, kind, value, writable }) {
|
|
1162
|
+
const [draft, setDraft] = useState(undefined)
|
|
1163
|
+
const initial = kind === 'number'
|
|
1164
|
+
? value === undefined ? '' : String(value)
|
|
1165
|
+
: kind === 'json'
|
|
1166
|
+
? value === undefined ? (fieldKey.includes('Aliases') ? '{}' : '[]') : JSON.stringify(value, null, 2)
|
|
1167
|
+
: value === undefined ? '' : String(value)
|
|
1168
|
+
const shown = draft === undefined ? initial : draft
|
|
1169
|
+
|
|
1170
|
+
const commit = () => {
|
|
1171
|
+
const raw = shown
|
|
1172
|
+
if (raw.trim() === '') {
|
|
1173
|
+
form.unset(fieldKey).catch(() => undefined)
|
|
1174
|
+
return
|
|
1175
|
+
}
|
|
1176
|
+
if (kind === 'number') {
|
|
1177
|
+
const number = Number(raw)
|
|
1178
|
+
if (!Number.isFinite(number) || number <= 0) { setDraft(initial); return }
|
|
1179
|
+
form.set(fieldKey, Math.floor(number)).catch(() => undefined)
|
|
1180
|
+
return
|
|
1181
|
+
}
|
|
1182
|
+
if (kind === 'json') {
|
|
1183
|
+
try {
|
|
1184
|
+
const parsed = JSON.parse(raw)
|
|
1185
|
+
const expectedArray = fieldKey.includes('Aliases') ? false : true
|
|
1186
|
+
if (expectedArray && !Array.isArray(parsed)) { setDraft(initial); return }
|
|
1187
|
+
if (!expectedArray && (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))) { setDraft(initial); return }
|
|
1188
|
+
form.set(fieldKey, parsed).catch(() => undefined)
|
|
1189
|
+
} catch {
|
|
1190
|
+
setDraft(initial)
|
|
1191
|
+
}
|
|
1192
|
+
return
|
|
1193
|
+
}
|
|
1194
|
+
form.set(fieldKey, raw).catch(() => undefined)
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
1198
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1199
|
+
h('span', { style: css.advLabel }, label),
|
|
1200
|
+
value === undefined || !writable
|
|
1201
|
+
? null
|
|
1202
|
+
: h('button', {
|
|
1203
|
+
style: { fontSize: '0.75em', opacity: 0.55, background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit', color: 'inherit' },
|
|
1204
|
+
onClick: () => { form.unset(fieldKey).catch(() => undefined) },
|
|
1205
|
+
}, t('reset')),
|
|
1206
|
+
),
|
|
1207
|
+
kind === 'json'
|
|
1208
|
+
? h('textarea', {
|
|
1209
|
+
style: { ...css.input, minHeight: '64px', fontFamily: 'ui-monospace, monospace', resize: 'vertical' },
|
|
1210
|
+
value: shown,
|
|
1211
|
+
disabled: !writable,
|
|
1212
|
+
onChange: event => setDraft(event.target.value),
|
|
1213
|
+
onBlur: commit,
|
|
1214
|
+
})
|
|
1215
|
+
: h('input', {
|
|
1216
|
+
style: css.input,
|
|
1217
|
+
type: kind === 'number' ? 'number' : 'text',
|
|
1218
|
+
value: shown,
|
|
1219
|
+
disabled: !writable,
|
|
1220
|
+
onChange: event => setDraft(event.target.value),
|
|
1221
|
+
onBlur: commit,
|
|
1222
|
+
}),
|
|
1223
|
+
h('div', { style: css.advHelp }, t('advancedHint')),
|
|
1224
|
+
)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function Advanced({ ctx, t, brand, show, onToggle }) {
|
|
1228
|
+
const form = ctx.configForms.get(NS)
|
|
1229
|
+
const subscribe = useCallback(listener => form.subscribe(listener), [form])
|
|
1230
|
+
const read = useCallback(() => form.getSnapshot(), [form])
|
|
1231
|
+
const snapshot = useSyncExternalStore(subscribe, read)
|
|
1232
|
+
const value = snapshot?.value ?? {}
|
|
1233
|
+
const writable = snapshot?.writable !== false
|
|
1234
|
+
const fields = FIELDS[brand.id]
|
|
1235
|
+
const labels = t('advancedFields')
|
|
1236
|
+
const items = [
|
|
1237
|
+
[fields.displayName, labels.displayName, 'text', value[fields.displayName]],
|
|
1238
|
+
[fields.vision, labels.visionModelIds, 'json', value[fields.vision]],
|
|
1239
|
+
[fields.textOnly, labels.textOnlyModelIds, 'json', value[fields.textOnly]],
|
|
1240
|
+
[fields.aliases, labels.modelAliases, 'json', value[fields.aliases]],
|
|
1241
|
+
['defaultMaxTokens', labels.defaultMaxTokens, 'number', value.defaultMaxTokens],
|
|
1242
|
+
]
|
|
1243
|
+
return h('div', { style: css.group },
|
|
1244
|
+
h('h3', { style: css.h3 }, t('advanced')),
|
|
1245
|
+
h('div', { style: css.bar },
|
|
1246
|
+
h('button', { style: css.btn, onClick: onToggle }, show ? t('hide') : t('show')),
|
|
1247
|
+
h('span', { style: css.note }, t('advancedHint')),
|
|
1248
|
+
),
|
|
1249
|
+
show
|
|
1250
|
+
? items.map(([key, label, kind, current]) => h(AdvancedInput, {
|
|
1251
|
+
key,
|
|
1252
|
+
t,
|
|
1253
|
+
form,
|
|
1254
|
+
fieldKey: key,
|
|
1255
|
+
label,
|
|
1256
|
+
kind,
|
|
1257
|
+
value: current,
|
|
1258
|
+
writable,
|
|
1259
|
+
}))
|
|
1260
|
+
: null,
|
|
1261
|
+
)
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function SettingsPage({ ctx, t, brand }) {
|
|
1265
|
+
const form = ctx.configForms.get(NS)
|
|
1266
|
+
const subscribe = useCallback(listener => form.subscribe(listener), [form])
|
|
1267
|
+
const read = useCallback(() => form.getSnapshot(), [form])
|
|
1268
|
+
const snapshot = useSyncExternalStore(subscribe, read)
|
|
1269
|
+
const fields = FIELDS[brand.id]
|
|
1270
|
+
// Trae's two regions have independent accounts AND catalogs, so every
|
|
1271
|
+
// snapshot (models and status) is keyed by region as well as by brand.
|
|
1272
|
+
const region = brand.id === 'trae' ? regionOf(snapshot?.value?.[fields.region]) : undefined
|
|
1273
|
+
const cacheId = region === undefined ? brand.id : `${brand.id}:${region}`
|
|
1274
|
+
const cached = readCache(cacheId, 'models')
|
|
1275
|
+
const [models, setModels] = useState(() => Array.isArray(cached?.rows) ? cached.rows : undefined)
|
|
1276
|
+
const [notice, setNotice] = useState(() => typeof cached?.notice === 'string' ? cached.notice : undefined)
|
|
1277
|
+
const [signedIn, setSignedIn] = useState(() => cached?.signedIn === true ? true : undefined)
|
|
1278
|
+
const [error, setError] = useState(undefined)
|
|
1279
|
+
const [saveStatus, setSaveStatus] = useState(undefined)
|
|
1280
|
+
const [busy, setBusy] = useState(false)
|
|
1281
|
+
const [query, setQuery] = useState('')
|
|
1282
|
+
const [onlyEnabled, setOnlyEnabled] = useState(false)
|
|
1283
|
+
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
1284
|
+
const [reloadSignal, setReloadSignal] = useState(0)
|
|
1285
|
+
const [regionBusy, setRegionBusy] = useState(false)
|
|
1286
|
+
const ticket = useRef(0)
|
|
1287
|
+
const writeTicket = useRef(0)
|
|
1288
|
+
|
|
1289
|
+
const fetchModels = useCallback(async (fresh = false) => {
|
|
1290
|
+
const payload = await api(`/models?brand=${brand.id}${fresh ? '&fresh=1' : ''}`)
|
|
1291
|
+
return {
|
|
1292
|
+
rows: Array.isArray(payload?.models) ? payload.models : [],
|
|
1293
|
+
notice: typeof payload?.notice === 'string' ? payload.notice : undefined,
|
|
1294
|
+
signedIn: typeof payload?.signedIn === 'boolean' ? payload.signedIn : undefined,
|
|
1295
|
+
}
|
|
1296
|
+
}, [brand.id])
|
|
1297
|
+
|
|
1298
|
+
const adopt = useCallback((rows, nextNotice, nextSignedIn) => {
|
|
1299
|
+
setModels(rows)
|
|
1300
|
+
setNotice(nextNotice)
|
|
1301
|
+
if (nextSignedIn !== undefined) setSignedIn(nextSignedIn)
|
|
1302
|
+
writeCache(cacheId, 'models', { rows, notice: nextNotice, signedIn: nextSignedIn, at: Date.now() })
|
|
1303
|
+
}, [cacheId])
|
|
1304
|
+
|
|
1305
|
+
const load = useCallback(async (fresh = false) => {
|
|
1306
|
+
const mine = ++ticket.current
|
|
1307
|
+
setError(undefined)
|
|
1308
|
+
try {
|
|
1309
|
+
const payload = await fetchModels(fresh)
|
|
1310
|
+
if (mine !== ticket.current) return
|
|
1311
|
+
adopt(payload.rows, payload.notice, payload.signedIn)
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
if (mine !== ticket.current) return
|
|
1314
|
+
setError(String(err?.message ?? err))
|
|
1315
|
+
setModels(previous => previous ?? [])
|
|
1316
|
+
}
|
|
1317
|
+
}, [adopt, fetchModels])
|
|
1318
|
+
|
|
1319
|
+
useEffect(() => {
|
|
1320
|
+
const saved = readCache(cacheId, 'models')
|
|
1321
|
+
setModels(Array.isArray(saved?.rows) ? saved.rows : undefined)
|
|
1322
|
+
setNotice(typeof saved?.notice === 'string' ? saved.notice : undefined)
|
|
1323
|
+
setError(undefined)
|
|
1324
|
+
void load(false)
|
|
1325
|
+
}, [cacheId, load])
|
|
1326
|
+
|
|
1327
|
+
// Switching region writes the same field the provider reads, so the host
|
|
1328
|
+
// starts answering for the new region immediately; the cacheId change
|
|
1329
|
+
// above then restores that region's own account and catalog.
|
|
1330
|
+
const changeRegion = useCallback(async next => {
|
|
1331
|
+
setRegionBusy(true)
|
|
1332
|
+
setError(undefined)
|
|
1333
|
+
try {
|
|
1334
|
+
const accepted = await withDeadline(form.set(fields.region, next), WRITE_TIMEOUT_MS, t('timeout'))
|
|
1335
|
+
if (accepted === false) throw new Error(t('saveFailed'))
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
setError(`${t('saveFailed')}:${String(err?.message ?? err)}`)
|
|
1338
|
+
} finally {
|
|
1339
|
+
setRegionBusy(false)
|
|
1340
|
+
}
|
|
1341
|
+
}, [fields.region, form, t])
|
|
1342
|
+
|
|
1343
|
+
const handleStatus = useCallback((payload) => {
|
|
1344
|
+
if (typeof payload?.signedIn === 'boolean') setSignedIn(payload.signedIn)
|
|
1345
|
+
}, [])
|
|
1346
|
+
|
|
1347
|
+
const afterRefresh = useCallback(() => {
|
|
1348
|
+
void load(true)
|
|
1349
|
+
}, [load])
|
|
1350
|
+
|
|
1351
|
+
const loadUntil = useCallback(async (verify, attempts = 5) => {
|
|
1352
|
+
let rows
|
|
1353
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1354
|
+
try {
|
|
1355
|
+
const payload = await fetchModels()
|
|
1356
|
+
rows = payload.rows
|
|
1357
|
+
adopt(rows, payload.notice, payload.signedIn)
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
setError(String(err?.message ?? err))
|
|
1360
|
+
return undefined
|
|
1361
|
+
}
|
|
1362
|
+
if (verify === undefined || verify(rows)) return rows
|
|
1363
|
+
await new Promise(resolve => setTimeout(resolve, 120 + attempt * 120))
|
|
1364
|
+
}
|
|
1365
|
+
return rows
|
|
1366
|
+
}, [adopt, fetchModels])
|
|
1367
|
+
|
|
1368
|
+
const write = useCallback(async (key, next, verify) => {
|
|
1369
|
+
const mine = ++writeTicket.current
|
|
1370
|
+
setBusy(true)
|
|
1371
|
+
setSaveStatus(t('saving'))
|
|
1372
|
+
setError(undefined)
|
|
1373
|
+
try {
|
|
1374
|
+
const accepted = await withDeadline(next === undefined ? form.unset(key) : form.set(key, next), WRITE_TIMEOUT_MS, t('timeout'))
|
|
1375
|
+
if (accepted === false) throw new Error(t('saveFailed'))
|
|
1376
|
+
const rows = await loadUntil(verify)
|
|
1377
|
+
if (rows !== undefined && verify !== undefined && !verify(rows)) throw new Error(t('notApplied'))
|
|
1378
|
+
if (mine === writeTicket.current) setSaveStatus(t('saved'))
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
if (mine !== writeTicket.current) return
|
|
1381
|
+
setError(`${t('saveFailed')}:${String(err?.message ?? err)}`)
|
|
1382
|
+
setSaveStatus(undefined)
|
|
1383
|
+
await load()
|
|
1384
|
+
} finally {
|
|
1385
|
+
if (mine === writeTicket.current) setBusy(false)
|
|
1386
|
+
}
|
|
1387
|
+
}, [form, load, loadUntil, t])
|
|
1388
|
+
|
|
1389
|
+
const writeDisabled = useCallback((rows, id, enabled) => {
|
|
1390
|
+
const next = new Set(rows.filter(model => !model.enabled).map(model => model.id))
|
|
1391
|
+
if (enabled) next.delete(id)
|
|
1392
|
+
else next.add(id)
|
|
1393
|
+
setModels(current => current?.map(model => model.id === id ? { ...model, enabled } : model))
|
|
1394
|
+
return write(fields.disabled, [...next], list => list.some(model => model.id === id && model.enabled === enabled))
|
|
1395
|
+
}, [fields.disabled, write])
|
|
1396
|
+
|
|
1397
|
+
const writeWindow = useCallback((rows, id, window) => {
|
|
1398
|
+
const next = {}
|
|
1399
|
+
for (const model of rows) if (model.override !== undefined) next[model.id] = model.override
|
|
1400
|
+
if (window === undefined) delete next[id]
|
|
1401
|
+
else next[id] = window
|
|
1402
|
+
setModels(current => current?.map(model => model.id === id
|
|
1403
|
+
? { ...model, override: window, contextWindow: window ?? model.defaultContextWindow }
|
|
1404
|
+
: model))
|
|
1405
|
+
return write(fields.windows, next, list => list.some(model => model.id === id
|
|
1406
|
+
&& (window === undefined ? model.override === undefined : model.override === window)))
|
|
1407
|
+
}, [fields.windows, write])
|
|
1408
|
+
|
|
1409
|
+
const writeAll = useCallback((enabled) => {
|
|
1410
|
+
const ids = (models ?? []).map(model => model.id)
|
|
1411
|
+
setModels(current => current?.map(model => ({ ...model, enabled })))
|
|
1412
|
+
return write(fields.disabled, enabled ? [] : ids, list => list.every(model => model.enabled === enabled))
|
|
1413
|
+
}, [fields.disabled, models, write])
|
|
1414
|
+
|
|
1415
|
+
const all = models ?? []
|
|
1416
|
+
const enabledTotal = all.filter(model => model.enabled).length
|
|
1417
|
+
const shown = useMemo(() => {
|
|
1418
|
+
const q = query.trim().toLowerCase()
|
|
1419
|
+
return all.filter(model => {
|
|
1420
|
+
if (onlyEnabled && !model.enabled) return false
|
|
1421
|
+
if (q === '') return true
|
|
1422
|
+
return String(model.name).toLowerCase().includes(q) || String(model.id).toLowerCase().includes(q)
|
|
1423
|
+
})
|
|
1424
|
+
}, [all, query, onlyEnabled])
|
|
1425
|
+
|
|
1426
|
+
const accountCard = h(AccountCard, {
|
|
1427
|
+
t,
|
|
1428
|
+
brand,
|
|
1429
|
+
region,
|
|
1430
|
+
regionBusy,
|
|
1431
|
+
onRegionChange: next => { void changeRegion(next) },
|
|
1432
|
+
reloadSignal,
|
|
1433
|
+
onStatus: handleStatus,
|
|
1434
|
+
onAfterRefresh: afterRefresh,
|
|
1435
|
+
})
|
|
1436
|
+
|
|
1437
|
+
const refreshButton = h('button', {
|
|
1438
|
+
style: css.btn,
|
|
1439
|
+
disabled: busy,
|
|
1440
|
+
onClick: () => {
|
|
1441
|
+
setReloadSignal(value => value + 1)
|
|
1442
|
+
void load(true)
|
|
1443
|
+
},
|
|
1444
|
+
}, busy ? t('refreshing') : t('refresh'))
|
|
1445
|
+
|
|
1446
|
+
const advanced = h(Advanced, {
|
|
1447
|
+
ctx, t, brand, show: showAdvanced,
|
|
1448
|
+
onToggle: () => setShowAdvanced(value => !value),
|
|
1449
|
+
})
|
|
1450
|
+
|
|
1451
|
+
if (models === undefined) {
|
|
1452
|
+
return h('div', { style: css.page },
|
|
1453
|
+
h('h2', { style: css.h2 }, t(brand.titleKey)),
|
|
1454
|
+
h('p', { style: css.intro }, t(brand.introKey)),
|
|
1455
|
+
accountCard,
|
|
1456
|
+
error === undefined
|
|
1457
|
+
? h('div', { style: css.note }, t('reading'))
|
|
1458
|
+
: h('div', null,
|
|
1459
|
+
h('div', { style: css.err }, `${t('loadFailed')}:${error}`),
|
|
1460
|
+
h('button', { style: { ...css.btn, marginTop: '8px' }, onClick: () => void load(true) }, t('retry')),
|
|
1461
|
+
),
|
|
1462
|
+
)
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (signedIn !== true) {
|
|
1466
|
+
return h('div', { style: css.page },
|
|
1467
|
+
h('h2', { style: css.h2 }, t(brand.titleKey)),
|
|
1468
|
+
h('p', { style: css.intro }, t(brand.introKey)),
|
|
1469
|
+
accountCard,
|
|
1470
|
+
h('div', { style: css.guide },
|
|
1471
|
+
h('div', { style: css.guideText }, signInGuide(t, brand)),
|
|
1472
|
+
h('div', { style: css.note }, t('modelsHint')),
|
|
1473
|
+
),
|
|
1474
|
+
refreshButton,
|
|
1475
|
+
advanced,
|
|
1476
|
+
error !== undefined ? h('div', { style: css.err }, error) : null,
|
|
1477
|
+
)
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const rows = shown.map((model, index) => {
|
|
1481
|
+
const choices = Array.isArray(model.contextWindows) ? model.contextWindows : []
|
|
1482
|
+
const price = priceText(t, model.priceFactor)
|
|
1483
|
+
return h('div', {
|
|
1484
|
+
key: model.id,
|
|
1485
|
+
style: { ...css.row, ...(index === 0 ? css.rowFirst : {}), ...(model.enabled ? {} : css.rowOff) },
|
|
1486
|
+
},
|
|
1487
|
+
h('input', {
|
|
1488
|
+
type: 'checkbox',
|
|
1489
|
+
style: css.switch,
|
|
1490
|
+
checked: model.enabled === true,
|
|
1491
|
+
disabled: busy,
|
|
1492
|
+
title: t('colEnabled'),
|
|
1493
|
+
onChange: event => { void writeDisabled(all, model.id, event.target.checked) },
|
|
1494
|
+
}),
|
|
1495
|
+
h('div', { style: css.name },
|
|
1496
|
+
h('div', { style: css.nameText },
|
|
1497
|
+
model.name,
|
|
1498
|
+
// Only native image support gets the tag; relayed capability is
|
|
1499
|
+
// explained once below the list instead of on every row.
|
|
1500
|
+
model.vision === true && model.visionRelay !== true
|
|
1501
|
+
? h('span', { key: 'v', style: { ...css.tag, marginLeft: '6px' } }, t('vision'))
|
|
1502
|
+
: null,
|
|
1503
|
+
model.reasoning === true ? h('span', { key: 'r', style: { ...css.tag, marginLeft: '6px' } }, t('reasoning')) : null,
|
|
1504
|
+
...(Array.isArray(model.labels) ? model.labels : []).map((label, labelIndex) =>
|
|
1505
|
+
h('span', { key: `l${labelIndex}`, style: { ...css.tag, marginLeft: '6px' } }, label)),
|
|
1506
|
+
model.badge !== undefined
|
|
1507
|
+
? h('span', { key: 'b', style: { ...css.tag, marginLeft: '6px' } }, model.badge)
|
|
1508
|
+
: null,
|
|
1509
|
+
price !== '' ? h('span', { key: 'p', style: css.price }, ` · ${price}`) : null,
|
|
1510
|
+
),
|
|
1511
|
+
h('div', { style: css.id }, model.id),
|
|
1512
|
+
),
|
|
1513
|
+
h('select', {
|
|
1514
|
+
style: css.select,
|
|
1515
|
+
value: model.override === undefined ? '' : String(model.override),
|
|
1516
|
+
disabled: busy,
|
|
1517
|
+
title: t('windowDefaultHint'),
|
|
1518
|
+
onChange: event => {
|
|
1519
|
+
const value = event.target.value
|
|
1520
|
+
void writeWindow(all, model.id, value === '' ? undefined : Number(value))
|
|
1521
|
+
},
|
|
1522
|
+
},
|
|
1523
|
+
h('option', { key: 'default', value: '' }, `${t('windowDefault')} · ${windowLabel(model.defaultContextWindow)}`),
|
|
1524
|
+
choices.map(value => h('option', { key: String(value), value: String(value) }, windowLabel(value))),
|
|
1525
|
+
),
|
|
1526
|
+
)
|
|
1527
|
+
})
|
|
1528
|
+
|
|
1529
|
+
return h('div', { style: css.page },
|
|
1530
|
+
h('h2', { style: css.h2 }, t(brand.titleKey)),
|
|
1531
|
+
h('p', { style: css.intro }, t(brand.introKey)),
|
|
1532
|
+
accountCard,
|
|
1533
|
+
h('div', { style: css.bar },
|
|
1534
|
+
h('input', {
|
|
1535
|
+
style: { ...css.input, ...css.search },
|
|
1536
|
+
placeholder: t('search'),
|
|
1537
|
+
value: query,
|
|
1538
|
+
onChange: event => setQuery(event.target.value),
|
|
1539
|
+
}),
|
|
1540
|
+
h('label', { style: { ...css.note, display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap' } },
|
|
1541
|
+
h('input', { type: 'checkbox', checked: onlyEnabled, onChange: event => setOnlyEnabled(event.target.checked) }),
|
|
1542
|
+
t('onlyEnabled'),
|
|
1543
|
+
),
|
|
1544
|
+
),
|
|
1545
|
+
h('div', { style: { ...css.bar, flexWrap: 'nowrap' } },
|
|
1546
|
+
refreshButton,
|
|
1547
|
+
h('button', {
|
|
1548
|
+
style: css.btn,
|
|
1549
|
+
disabled: busy || all.every(model => model.enabled),
|
|
1550
|
+
onClick: () => void writeAll(true),
|
|
1551
|
+
}, t('selectAll')),
|
|
1552
|
+
h('button', {
|
|
1553
|
+
style: css.btn,
|
|
1554
|
+
disabled: busy || enabledTotal === 0,
|
|
1555
|
+
onClick: () => void writeAll(false),
|
|
1556
|
+
}, t('selectNone')),
|
|
1557
|
+
h('span', { style: css.count }, `${t('enabledCount')} ${enabledTotal} / ${all.length}`),
|
|
1558
|
+
),
|
|
1559
|
+
all.length === 0
|
|
1560
|
+
? h('div', { style: css.guide },
|
|
1561
|
+
h('div', { style: css.guideText }, brand.id === 'loomy'
|
|
1562
|
+
? t('modelsEmptyLoomy')
|
|
1563
|
+
: brand.id === 'raccoon'
|
|
1564
|
+
? t('modelsEmptyRaccoon')
|
|
1565
|
+
: brand.id === 'codearts' ? t('modelsEmptyCodeArts') : t('modelsEmptyTrae')),
|
|
1566
|
+
)
|
|
1567
|
+
: h('div', { style: css.list }, rows.length > 0
|
|
1568
|
+
? rows
|
|
1569
|
+
: h('div', { style: { ...css.note, padding: '12px 14px' } }, '—')),
|
|
1570
|
+
all.length > 0 && brand.id === 'codearts'
|
|
1571
|
+
? h('div', { style: css.note }, t('codeartsVisionHint'))
|
|
1572
|
+
: null,
|
|
1573
|
+
notice !== undefined ? h('div', { style: css.note }, `${t('catalogNotice')}:${notice}`) : null,
|
|
1574
|
+
all.length > 0 ? h('div', { style: css.note }, t('windowWarn')) : null,
|
|
1575
|
+
error !== undefined ? h('div', { style: css.err }, error) : null,
|
|
1576
|
+
saveStatus !== undefined && error === undefined
|
|
1577
|
+
? h('div', { style: css.note }, saveStatus)
|
|
1578
|
+
: null,
|
|
1579
|
+
advanced,
|
|
1580
|
+
)
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function signInGuide(t, brand) {
|
|
1584
|
+
if (brand.id === 'loomy') return t('loomyLoginMissing')
|
|
1585
|
+
if (brand.id === 'raccoon') return t('raccoonLoginMissing')
|
|
1586
|
+
return brand.id === 'codearts' ? t('codeartsLoginMissing') : t('traeLoginMissing')
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function BundleSettings({ ctx, t, view }) {
|
|
1590
|
+
const form = ctx.configForms.get(NS)
|
|
1591
|
+
const subscribe = useCallback(listener => form.subscribe(listener), [form])
|
|
1592
|
+
const read = useCallback(() => form.getSnapshot(), [form])
|
|
1593
|
+
const snapshot = useSyncExternalStore(subscribe, read)
|
|
1594
|
+
const value = snapshot?.value ?? {}
|
|
1595
|
+
const writable = snapshot?.writable !== false
|
|
1596
|
+
const [busy, setBusy] = useState(false)
|
|
1597
|
+
const [error, setError] = useState(undefined)
|
|
1598
|
+
const [status, setStatus] = useState(undefined)
|
|
1599
|
+
const [openRouter, setOpenRouter] = useState(undefined)
|
|
1600
|
+
const [syncing, setSyncing] = useState(false)
|
|
1601
|
+
const [routerError, setRouterError] = useState(undefined)
|
|
1602
|
+
const [routerNotice, setRouterNotice] = useState(undefined)
|
|
1603
|
+
const ticket = useRef(0)
|
|
1604
|
+
const syncInterval = typeof value.openRouterSyncMinutes === 'number' ? value.openRouterSyncMinutes : 60
|
|
1605
|
+
const loadOpenRouter = useCallback(async () => {
|
|
1606
|
+
try {
|
|
1607
|
+
setOpenRouter(await api('/openrouter', { timeout: 10_000 }))
|
|
1608
|
+
} catch (err) {
|
|
1609
|
+
setRouterError(String(err?.message ?? err))
|
|
1610
|
+
}
|
|
1611
|
+
}, [])
|
|
1612
|
+
|
|
1613
|
+
useEffect(() => { void loadOpenRouter() }, [loadOpenRouter])
|
|
1614
|
+
|
|
1615
|
+
const syncOpenRouter = useCallback(async () => {
|
|
1616
|
+
setSyncing(true)
|
|
1617
|
+
setRouterError(undefined)
|
|
1618
|
+
setRouterNotice(undefined)
|
|
1619
|
+
try {
|
|
1620
|
+
// The host may try twice with its own 30 s window; this deadline has
|
|
1621
|
+
// to outlast that, or the page reports a timeout while a retry is
|
|
1622
|
+
// still running and the user sees a failure that never happened.
|
|
1623
|
+
const payload = await api('/openrouter/sync', { method: 'POST', timeout: 75_000 })
|
|
1624
|
+
setOpenRouter(payload)
|
|
1625
|
+
if (payload?.ok === true) {
|
|
1626
|
+
setRouterNotice(`${t('openRouterSyncDone')} · ${payload.models} ${t('openRouterModels')}`)
|
|
1627
|
+
} else {
|
|
1628
|
+
setRouterError(`${t('openRouterSyncFailed')}:${payload?.error ?? '—'}`)
|
|
1629
|
+
}
|
|
1630
|
+
} catch (err) {
|
|
1631
|
+
setRouterError(`${t('openRouterSyncFailed')}:${String(err?.message ?? err)}`)
|
|
1632
|
+
} finally {
|
|
1633
|
+
setSyncing(false)
|
|
1634
|
+
}
|
|
1635
|
+
}, [t])
|
|
1636
|
+
|
|
1637
|
+
const setMinutes = useCallback(async (key, next) => {
|
|
1638
|
+
setStatus(t('saving'))
|
|
1639
|
+
setError(undefined)
|
|
1640
|
+
try {
|
|
1641
|
+
const accepted = await withDeadline(form.set(key, next), WRITE_TIMEOUT_MS, t('timeout'))
|
|
1642
|
+
if (accepted === false) throw new Error(t('pluginOptionsRejected'))
|
|
1643
|
+
setStatus(t('saved'))
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
setError(`${t('saveFailed')}:${String(err?.message ?? err)}`)
|
|
1646
|
+
setStatus(undefined)
|
|
1647
|
+
}
|
|
1648
|
+
}, [form, t])
|
|
1649
|
+
|
|
1650
|
+
const setSyncInterval = useCallback(next => setMinutes('openRouterSyncMinutes', next), [setMinutes])
|
|
1651
|
+
|
|
1652
|
+
const toggle = useCallback(async (key, next) => {
|
|
1653
|
+
const mine = ++ticket.current
|
|
1654
|
+
setBusy(true)
|
|
1655
|
+
setStatus(t('saving'))
|
|
1656
|
+
setError(undefined)
|
|
1657
|
+
try {
|
|
1658
|
+
const accepted = await withDeadline(form.set(key, next), WRITE_TIMEOUT_MS, t('timeout'))
|
|
1659
|
+
if (accepted === false) throw new Error(t('pluginOptionsRejected'))
|
|
1660
|
+
if (mine === ticket.current) setStatus(t('saved'))
|
|
1661
|
+
} catch (err) {
|
|
1662
|
+
if (mine !== ticket.current) return
|
|
1663
|
+
setError(`${t('saveFailed')}:${String(err?.message ?? err)}`)
|
|
1664
|
+
setStatus(undefined)
|
|
1665
|
+
} finally {
|
|
1666
|
+
if (mine === ticket.current) setBusy(false)
|
|
1667
|
+
}
|
|
1668
|
+
}, [form, t])
|
|
1669
|
+
|
|
1670
|
+
if (view !== undefined && view !== 'page') {
|
|
1671
|
+
const names = BRANDS
|
|
1672
|
+
.filter(brand => value[FIELDS[brand.id].enabled] !== false)
|
|
1673
|
+
.map(brand => {
|
|
1674
|
+
const stored = value[FIELDS[brand.id].displayName]
|
|
1675
|
+
return typeof stored === 'string' && stored !== '' ? stored : t(brand.navKey)
|
|
1676
|
+
})
|
|
1677
|
+
return h('span', null, names.join(' · '))
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
return h('div', { style: css.group },
|
|
1681
|
+
h('h3', { style: css.h3 }, t('pluginOptionsTitle')),
|
|
1682
|
+
h('div', { style: css.note }, t('pluginOptionsHint')),
|
|
1683
|
+
BRANDS.map((brand, index) => {
|
|
1684
|
+
const key = FIELDS[brand.id].enabled
|
|
1685
|
+
const enabled = value[key] !== false
|
|
1686
|
+
const stored = value[FIELDS[brand.id].displayName]
|
|
1687
|
+
const name = typeof stored === 'string' && stored !== '' ? stored : t(brand.navKey)
|
|
1688
|
+
return h('div', { key: brand.id, style: { ...css.optionRow, ...(index === 0 ? css.optionFirst : {}) } },
|
|
1689
|
+
h('input', {
|
|
1690
|
+
type: 'checkbox',
|
|
1691
|
+
style: css.switch,
|
|
1692
|
+
checked: enabled,
|
|
1693
|
+
disabled: busy || !writable,
|
|
1694
|
+
onChange: event => { void toggle(key, event.target.checked) },
|
|
1695
|
+
}),
|
|
1696
|
+
h('div', { style: css.optionText },
|
|
1697
|
+
h('div', { style: css.optionName }, name),
|
|
1698
|
+
h('div', { style: css.optionStatus }, enabled ? t('pluginOptionsOn') : t('pluginOptionsOff')),
|
|
1699
|
+
),
|
|
1700
|
+
)
|
|
1701
|
+
}),
|
|
1702
|
+
!writable ? h('div', { style: css.note }, t('pluginOptionsReadOnly')) : null,
|
|
1703
|
+
h('div', { style: css.section },
|
|
1704
|
+
h('h3', { style: css.h3 }, t('openRouterTitle')),
|
|
1705
|
+
h('div', { style: css.note }, t('openRouterHint')),
|
|
1706
|
+
h('div', { style: css.bar },
|
|
1707
|
+
h('span', { style: css.label }, t('openRouterInterval')),
|
|
1708
|
+
h('select', {
|
|
1709
|
+
style: css.select,
|
|
1710
|
+
value: String(syncInterval),
|
|
1711
|
+
disabled: busy || !writable,
|
|
1712
|
+
onChange: event => { void setSyncInterval(Number(event.target.value)) },
|
|
1713
|
+
},
|
|
1714
|
+
OPENROUTER_INTERVALS.map(option => h('option', {
|
|
1715
|
+
key: String(option.value),
|
|
1716
|
+
value: String(option.value),
|
|
1717
|
+
}, t(option.labelKey))),
|
|
1718
|
+
),
|
|
1719
|
+
h('button', {
|
|
1720
|
+
style: { ...css.btn, ...css.btnPrimary },
|
|
1721
|
+
disabled: syncing || !writable,
|
|
1722
|
+
onClick: () => { void syncOpenRouter() },
|
|
1723
|
+
}, syncing ? t('openRouterSyncing') : t('openRouterSyncNow')),
|
|
1724
|
+
),
|
|
1725
|
+
h('div', { style: css.bar },
|
|
1726
|
+
h('span', { style: css.note }, openRouter === undefined
|
|
1727
|
+
? t('openRouterNever')
|
|
1728
|
+
: `${openRouter.models ?? 0} ${t('openRouterModels')}${typeof openRouter.at === 'number' ? ` · ${t('openRouterLastSync')} ${dateLabel(openRouter.at)}` : ''}`),
|
|
1729
|
+
typeof openRouter?.error === 'string'
|
|
1730
|
+
? h('span', { style: css.err }, `${t('openRouterSyncFailed')}:${openRouter.error}`)
|
|
1731
|
+
: null,
|
|
1732
|
+
),
|
|
1733
|
+
routerNotice !== undefined ? h('div', { style: css.note }, routerNotice) : null,
|
|
1734
|
+
routerError !== undefined ? h('div', { style: css.err }, routerError) : null,
|
|
1735
|
+
),
|
|
1736
|
+
h('div', { style: css.section },
|
|
1737
|
+
h('h3', { style: css.h3 }, t('checkinMonitorTitle')),
|
|
1738
|
+
h('div', { style: css.note }, t('checkinMonitorHint')),
|
|
1739
|
+
),
|
|
1740
|
+
error !== undefined ? h('div', { style: css.err }, error) : null,
|
|
1741
|
+
status !== undefined && error === undefined ? h('div', { style: css.note }, status) : null,
|
|
1742
|
+
)
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function apply(ctx) {
|
|
1746
|
+
const t = ctx.locale.bind(NS)
|
|
1747
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'llm-adapters: dictionaries')
|
|
1748
|
+
|
|
1749
|
+
ctx.effect(() => ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
1750
|
+
name: 'plugins.bundle.config',
|
|
1751
|
+
key: 'dsh-llm-adapters',
|
|
1752
|
+
locale: NS,
|
|
1753
|
+
}, props => h(BundleSettings, { ...props, ctx, t }))), 'llm-adapters: bundle options')
|
|
1754
|
+
|
|
1755
|
+
ctx.effect(() => ctx.configForms.whileServed([NS], () => {
|
|
1756
|
+
const form = ctx.configForms.get(NS)
|
|
1757
|
+
const live = new Map()
|
|
1758
|
+
|
|
1759
|
+
const sync = () => {
|
|
1760
|
+
const snapshot = form.getSnapshot()
|
|
1761
|
+
if (snapshot.status !== 'ready') return
|
|
1762
|
+
const value = snapshot.value ?? {}
|
|
1763
|
+
for (const brand of BRANDS) {
|
|
1764
|
+
const enabled = value[FIELDS[brand.id].enabled] !== false
|
|
1765
|
+
const current = live.get(brand.id)
|
|
1766
|
+
if (enabled && current === undefined) {
|
|
1767
|
+
live.set(brand.id, ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
1768
|
+
name: 'settings.section',
|
|
1769
|
+
id: brand.settingsId,
|
|
1770
|
+
order: brand.order,
|
|
1771
|
+
label: () => t(brand.navKey),
|
|
1772
|
+
locale: NS,
|
|
1773
|
+
}, props => h(SettingsPage, { ...props, ctx, t, brand }))))
|
|
1774
|
+
} else if (!enabled && current !== undefined) {
|
|
1775
|
+
current()
|
|
1776
|
+
live.delete(brand.id)
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const unsubscribe = form.subscribe(sync)
|
|
1782
|
+
sync()
|
|
1783
|
+
return () => {
|
|
1784
|
+
unsubscribe()
|
|
1785
|
+
for (const off of live.values()) off()
|
|
1786
|
+
live.clear()
|
|
1787
|
+
}
|
|
1788
|
+
}), 'llm-adapters: pages')
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
exports.apply = apply
|
|
1792
|
+
exports.inject = inject
|
|
1793
|
+
exports.name = 'dsh-llm-adapters'
|
|
1794
|
+
exports.__test = {
|
|
1795
|
+
BRANDS, FIELDS, REGIONS, OPENROUTER_INTERVALS, regionOf, zh, en, css, windowLabel, creditsOf,
|
|
1796
|
+
amountText,
|
|
1797
|
+
meterPercentOf, percentLabel, ownerLabel, priceText, dateLabel, readCache, writeCache, AccountCard,
|
|
1798
|
+
SettingsPage, BundleSettings,
|
|
1799
|
+
}
|
|
1800
|
+
return module.exports
|
|
1801
|
+
},
|
|
1802
|
+
})
|