@goodandready/dsh-clinebot 0.3.25 → 0.4.1
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/CHANGELOG.md +32 -0
- package/README.md +21 -5
- package/README.ru.md +21 -5
- package/README.zh.md +21 -5
- package/lib/account-pool.js +59 -4
- package/lib/client.js +596 -200
- package/lib/cline-client.js +45 -45
- package/lib/config.js +25 -4
- package/lib/http.js +15 -2
- package/lib/index.js +117 -50
- package/lib/models.js +57 -25
- package/lib/provider-sync.js +60 -22
- package/lib/routes/accounts.js +178 -10
- package/lib/routes/auth.js +80 -38
- package/lib/routes/models.js +32 -29
- package/lib/routes/settings.js +83 -11
- package/lib/slash-command.js +91 -14
- package/lib/telemetry.js +78 -0
- package/package.json +4 -1
package/lib/client.js
CHANGED
|
@@ -14,21 +14,25 @@ const NS = 'dsh-clinebot'
|
|
|
14
14
|
const ROW_ID = 'dsh-clinebot'
|
|
15
15
|
const ROW_CONFIG_KEY = '@goodandready/dsh-clinebot#' + ROW_ID
|
|
16
16
|
const ROUTE_PREFIX = '/dsh-clinebot'
|
|
17
|
-
const SNAPSHOT_LOADING = Object.freeze({ status: 'loading', view: null })
|
|
18
17
|
// Stable (frozen) fallback reference: useSyncExternalStore compares snapshots by
|
|
19
18
|
// identity (Object.is), so returning a fresh object literal causes an infinite render loop
|
|
20
19
|
// (React error #185 "Maximum update depth exceeded"). Keep this module-level.
|
|
21
20
|
const SNAPSHOT_READY = Object.freeze({ status: 'ready', view: null })
|
|
22
21
|
|
|
22
|
+
function readConfigForms(ctx) {
|
|
23
|
+
if (!ctx || typeof ctx.get !== 'function') return undefined
|
|
24
|
+
try { return ctx.get('configForms') || undefined } catch { return undefined }
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
// --- src/client/locales.js ---
|
|
24
28
|
const en = {
|
|
25
29
|
title: 'ClineBot',
|
|
26
|
-
subtitle: 'ClinePass subscription
|
|
30
|
+
subtitle: 'ClinePass subscription with dynamic plan models.',
|
|
27
31
|
'settings.loading': 'Loading ClineBot settings…',
|
|
28
32
|
'settings.retry': 'Retry',
|
|
29
33
|
'settings.unavailable': 'ClineBot settings unavailable (host namespace is not ready).',
|
|
30
34
|
'header.title': 'ClineBot (ClinePass) Provider',
|
|
31
|
-
'header.sub': 'Connect models under ClinePass subscription
|
|
35
|
+
'header.sub': 'Connect models under your active ClinePass subscription. OpenAI API compatible, automated rolling limit tracking, and instant auto-registration in DSH.',
|
|
32
36
|
'badge.online': 'Host online ({latency} ms)',
|
|
33
37
|
'badge.offline': 'Host unreachable',
|
|
34
38
|
'badge.key_ok': 'Key ✓ ({source})',
|
|
@@ -43,8 +47,11 @@ const en = {
|
|
|
43
47
|
'key.hide': 'Hide',
|
|
44
48
|
'key.save': 'Save Key',
|
|
45
49
|
'key.saving': 'Saving…',
|
|
46
|
-
'key.login_fast': '
|
|
47
|
-
'key.login_waiting': '
|
|
50
|
+
'key.login_fast': '🔑 Open API Keys ↗',
|
|
51
|
+
'key.login_waiting': 'Paste key below for instant verification.',
|
|
52
|
+
'key.verifying': 'Validating key with Cline…',
|
|
53
|
+
'key.verified': '✓ Valid key for {email} ({plan})',
|
|
54
|
+
'key.invalid': '✗ Key validation failed: {error}',
|
|
48
55
|
'key.env_label': 'Credential environment name: ',
|
|
49
56
|
'key.get_key': 'Get key in app.cline.bot console ↗',
|
|
50
57
|
'key.saved_msg': 'Key saved to DSH Credentials. Validation: {status}',
|
|
@@ -57,6 +64,14 @@ const en = {
|
|
|
57
64
|
'accounts.add_btn': '+ Add Account',
|
|
58
65
|
'accounts.label_placeholder': 'Account Label (e.g. Work)',
|
|
59
66
|
'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
|
|
67
|
+
'accounts.delete_btn': 'Delete',
|
|
68
|
+
'accounts.delete_confirm': 'Are you sure you want to delete account "{label}"?',
|
|
69
|
+
'accounts.delete_secret': 'Delete secret from credentials',
|
|
70
|
+
'accounts.save_btn': 'Save Account',
|
|
71
|
+
'accounts.cancel_btn': 'Cancel',
|
|
72
|
+
'accounts.key_label': 'API Key',
|
|
73
|
+
'accounts.quota_used': '{pct}% used',
|
|
74
|
+
'accounts.last_failover': 'Last failover: {from} → {to} ({reason})',
|
|
60
75
|
'quota.title': '📊 ClinePass Quota & Rate Limits',
|
|
61
76
|
'quota.desc': 'Official rolling window request limits from ClinePass',
|
|
62
77
|
'quota.account': 'Account: {email} · Plan: {plan}',
|
|
@@ -74,6 +89,15 @@ const en = {
|
|
|
74
89
|
'models.sync': '🔄 Check Plan Models',
|
|
75
90
|
'models.syncing': 'Checking…',
|
|
76
91
|
'models.synced_msg': 'Synchronized subscription models: {total} ({discovered} discovered from ClinePass)',
|
|
92
|
+
'models.verified': 'Verified with plan',
|
|
93
|
+
'models.unverified': 'Not synced (fallback catalog)',
|
|
94
|
+
'models.synced_at': 'Synced: {date}',
|
|
95
|
+
'models.search_placeholder': 'Search models by name, ID or description…',
|
|
96
|
+
'models.filter_all': 'All',
|
|
97
|
+
'models.filter_vision': 'Vision',
|
|
98
|
+
'models.filter_coding': 'Coding',
|
|
99
|
+
'models.filter_recommended': 'Recommended',
|
|
100
|
+
'models.filter_disabled': 'Disabled',
|
|
77
101
|
'models.all': 'All',
|
|
78
102
|
'models.vision': 'Vision Only',
|
|
79
103
|
'models.coding': 'Coding Only',
|
|
@@ -113,12 +137,12 @@ const en = {
|
|
|
113
137
|
|
|
114
138
|
const zh = {
|
|
115
139
|
title: 'ClineBot',
|
|
116
|
-
subtitle: 'ClinePass
|
|
140
|
+
subtitle: 'ClinePass 订阅服务,包含动态套餐模型。',
|
|
117
141
|
'settings.loading': '正在加载 ClineBot 设置…',
|
|
118
142
|
'settings.retry': '重试',
|
|
119
143
|
'settings.unavailable': 'ClineBot 设置不可用(宿主命名空间尚未就绪)。',
|
|
120
144
|
'header.title': 'ClineBot (ClinePass) 模型服务商',
|
|
121
|
-
'header.sub': '通过 ClinePass
|
|
145
|
+
'header.sub': '通过 ClinePass 订阅连接主流推理与编程模型。完全兼容 OpenAI 接口,自动追踪滚动限额并在 DSH 中即时生效。',
|
|
122
146
|
'badge.online': '服务在线 ({latency} ms)',
|
|
123
147
|
'badge.offline': '主机无法连接',
|
|
124
148
|
'badge.key_ok': '密钥正常 ({source})',
|
|
@@ -133,8 +157,11 @@ const en = {
|
|
|
133
157
|
'key.hide': '隐藏',
|
|
134
158
|
'key.save': '保存密钥',
|
|
135
159
|
'key.saving': '正在保存…',
|
|
136
|
-
'key.login_fast': '
|
|
137
|
-
'key.login_waiting': '
|
|
160
|
+
'key.login_fast': '🔑 打开 API 密钥页 ↗',
|
|
161
|
+
'key.login_waiting': '请在下方粘贴密钥,系统将自动进行有效性校验。',
|
|
162
|
+
'key.verifying': '正在连接 Cline 验证密钥…',
|
|
163
|
+
'key.verified': '✓ 密钥有效:{email} ({plan})',
|
|
164
|
+
'key.invalid': '✗ 密钥验证未通过:{error}',
|
|
138
165
|
'key.env_label': '凭据环境变量名称:',
|
|
139
166
|
'key.get_key': '前往 app.cline.bot 控制台获取密钥 ↗',
|
|
140
167
|
'key.saved_msg': '密钥已成功保存至 DSH 凭据服务。验证状态:{status}',
|
|
@@ -147,6 +174,14 @@ const en = {
|
|
|
147
174
|
'accounts.add_btn': '+ 添加账号',
|
|
148
175
|
'accounts.label_placeholder': '账号标签(例如:工作账号)',
|
|
149
176
|
'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
|
|
177
|
+
'accounts.delete_btn': '删除',
|
|
178
|
+
'accounts.delete_confirm': '确定要删除账号“{label}”吗?',
|
|
179
|
+
'accounts.delete_secret': '同时删除凭据密钥',
|
|
180
|
+
'accounts.save_btn': '保存账号',
|
|
181
|
+
'accounts.cancel_btn': '取消',
|
|
182
|
+
'accounts.key_label': 'API 密钥',
|
|
183
|
+
'accounts.quota_used': '已用 {pct}%',
|
|
184
|
+
'accounts.last_failover': '最近故障转移:{from} → {to} ({reason})',
|
|
150
185
|
'quota.title': '📊 ClinePass 用量配额与限额监控',
|
|
151
186
|
'quota.desc': '来自 ClinePass 官方的实时滚动窗口用量限额',
|
|
152
187
|
'quota.account': '账号:{email} · 套餐:{plan}',
|
|
@@ -164,6 +199,15 @@ const en = {
|
|
|
164
199
|
'models.sync': '🔄 同步订阅模型',
|
|
165
200
|
'models.syncing': '正在同步…',
|
|
166
201
|
'models.synced_msg': '已成功同步订阅模型:共 {total} 个(发现 {discovered} 个新模型)',
|
|
202
|
+
'models.verified': '已与套餐同步',
|
|
203
|
+
'models.unverified': '未同步(备用目录)',
|
|
204
|
+
'models.synced_at': '同步时间:{date}',
|
|
205
|
+
'models.search_placeholder': '按名称、ID 或描述搜索模型…',
|
|
206
|
+
'models.filter_all': '全部',
|
|
207
|
+
'models.filter_vision': '仅视觉',
|
|
208
|
+
'models.filter_coding': '仅编程',
|
|
209
|
+
'models.filter_recommended': '推荐',
|
|
210
|
+
'models.filter_disabled': '已禁用',
|
|
167
211
|
'models.all': '全部模型',
|
|
168
212
|
'models.vision': '仅视觉 (Vision)',
|
|
169
213
|
'models.coding': '仅编程 (Coding)',
|
|
@@ -275,11 +319,14 @@ function ensureCss() {
|
|
|
275
319
|
.cb-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent) !important;border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent)}
|
|
276
320
|
.cb-btn-disabled{opacity:0.5;cursor:not-allowed}
|
|
277
321
|
|
|
278
|
-
.cb-bar-container{display:flex;flex-direction:column;gap:
|
|
279
|
-
.cb-bar-head{display:flex;justify-content:space-between;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
280
|
-
.cb-
|
|
281
|
-
.cb-bar-
|
|
282
|
-
.cb-bar-
|
|
322
|
+
.cb-progress-card, .cb-bar-container{display:flex;flex-direction:column;gap:8px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
|
|
323
|
+
.cb-progress-head, .cb-bar-head{display:flex;justify-content:space-between;align-items:baseline;gap:16px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);width:100%}
|
|
324
|
+
.cb-progress-label{font-weight:500;white-space:nowrap}
|
|
325
|
+
.cb-progress-meta, .cb-bar-meta{font-size:12px;color:var(--dsw-alias-label-secondary);white-space:nowrap;margin-left:auto}
|
|
326
|
+
.cb-progress-track, .cb-bar-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;border:1px solid var(--dsw-alias-border-l2)}
|
|
327
|
+
.cb-progress-bar-fill, .cb-bar-fill{height:100%;border-radius:999px;transition:width .3s;background:var(--dsw-alias-state-brand-primary, var(--dsw-alias-state-success-primary))}
|
|
328
|
+
.cb-progress-bar-warn{height:100%;border-radius:999px;transition:width .3s;background:var(--dsw-alias-state-warning-primary)}
|
|
329
|
+
.cb-progress-bar-bad{height:100%;border-radius:999px;transition:width .3s;background:var(--dsw-alias-state-error-primary)}
|
|
283
330
|
|
|
284
331
|
.cb-table{width:100%;border-collapse:collapse;margin-top:8px}
|
|
285
332
|
.cb-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
|
|
@@ -365,24 +412,62 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
|
365
412
|
|
|
366
413
|
return React.createElement(
|
|
367
414
|
'div',
|
|
368
|
-
{
|
|
415
|
+
{
|
|
416
|
+
className: 'cb-progress-card cb-bar-container',
|
|
417
|
+
style: {
|
|
418
|
+
display: 'flex',
|
|
419
|
+
flexDirection: 'column',
|
|
420
|
+
gap: '8px',
|
|
421
|
+
padding: '12px 14px',
|
|
422
|
+
borderRadius: '8px',
|
|
423
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
424
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
425
|
+
},
|
|
426
|
+
},
|
|
369
427
|
React.createElement(
|
|
370
428
|
'div',
|
|
371
|
-
{
|
|
372
|
-
|
|
429
|
+
{
|
|
430
|
+
className: 'cb-progress-head cb-bar-head',
|
|
431
|
+
style: {
|
|
432
|
+
display: 'flex',
|
|
433
|
+
justifyContent: 'space-between',
|
|
434
|
+
alignItems: 'baseline',
|
|
435
|
+
gap: '16px',
|
|
436
|
+
width: '100%',
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
React.createElement('span', { className: 'cb-progress-label', style: { fontWeight: 500, whiteSpace: 'nowrap' } }, label),
|
|
373
440
|
React.createElement(
|
|
374
441
|
'span',
|
|
375
|
-
{
|
|
442
|
+
{
|
|
443
|
+
className: 'cb-progress-meta cb-bar-meta',
|
|
444
|
+
style: {
|
|
445
|
+
fontSize: '12px',
|
|
446
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
447
|
+
whiteSpace: 'nowrap',
|
|
448
|
+
marginLeft: 'auto',
|
|
449
|
+
},
|
|
450
|
+
},
|
|
376
451
|
t('quota.used', { pct }),
|
|
377
452
|
resetLabel ? ` · ${t('quota.reset_at', { time: resetLabel })}` : ''
|
|
378
453
|
)
|
|
379
454
|
),
|
|
380
455
|
React.createElement(
|
|
381
456
|
'div',
|
|
382
|
-
{
|
|
457
|
+
{
|
|
458
|
+
className: 'cb-progress-track cb-bar-track',
|
|
459
|
+
style: {
|
|
460
|
+
width: '100%',
|
|
461
|
+
height: '8px',
|
|
462
|
+
borderRadius: '999px',
|
|
463
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
464
|
+
overflow: 'hidden',
|
|
465
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
466
|
+
},
|
|
467
|
+
},
|
|
383
468
|
React.createElement('div', {
|
|
384
469
|
className: barClass,
|
|
385
|
-
style: { width: `${pct}
|
|
470
|
+
style: { width: `${pct}%`, height: '100%', borderRadius: '999px' },
|
|
386
471
|
})
|
|
387
472
|
)
|
|
388
473
|
)
|
|
@@ -445,7 +530,7 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
|
445
530
|
}
|
|
446
531
|
|
|
447
532
|
// --- src/client/components/key-section.js ---
|
|
448
|
-
function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowKey, busy, draft, handleSaveKey, handleFastLogin, t }) {
|
|
533
|
+
function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowKey, busy, draft, handleSaveKey, handleFastLogin, verifyStatus, t }) {
|
|
449
534
|
return React.createElement(
|
|
450
535
|
'div',
|
|
451
536
|
{ className: 'cb-section-card' },
|
|
@@ -488,19 +573,28 @@ function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowK
|
|
|
488
573
|
disabled: !!busy,
|
|
489
574
|
onClick: handleFastLogin,
|
|
490
575
|
},
|
|
491
|
-
|
|
576
|
+
t('key.login_fast')
|
|
492
577
|
)
|
|
493
578
|
),
|
|
579
|
+
verifyStatus && verifyStatus.state === 'verifying'
|
|
580
|
+
? React.createElement('div', { style: { marginTop: '8px', fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('key.verifying'))
|
|
581
|
+
: null,
|
|
582
|
+
verifyStatus && verifyStatus.state === 'valid'
|
|
583
|
+
? React.createElement('div', { className: 'cb-alert-ok', style: { marginTop: '8px', fontSize: '13px' } }, t('key.verified', { email: verifyStatus.email, plan: verifyStatus.plan }))
|
|
584
|
+
: null,
|
|
585
|
+
verifyStatus && verifyStatus.state === 'invalid'
|
|
586
|
+
? React.createElement('div', { className: 'cb-alert-bad', style: { marginTop: '8px', fontSize: '13px' } }, t('key.invalid', { error: verifyStatus.error }))
|
|
587
|
+
: null,
|
|
494
588
|
React.createElement(
|
|
495
589
|
'div',
|
|
496
|
-
{ className: 'cb-row', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
590
|
+
{ className: 'cb-row', style: { fontSize: '13px', marginTop: '12px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
497
591
|
React.createElement('span', null, t('key.env_label')),
|
|
498
592
|
React.createElement('code', null, draft.apiKeyEnv || 'CLINEBOT_API_KEY'),
|
|
499
593
|
React.createElement('span', null, ' · '),
|
|
500
594
|
React.createElement(
|
|
501
595
|
'a',
|
|
502
596
|
{
|
|
503
|
-
href: 'https://app.cline.bot',
|
|
597
|
+
href: 'https://app.cline.bot/settings/api-keys',
|
|
504
598
|
target: '_blank',
|
|
505
599
|
rel: 'noreferrer',
|
|
506
600
|
style: { color: 'var(--dsw-alias-state-brand-primary)' },
|
|
@@ -512,13 +606,162 @@ function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowK
|
|
|
512
606
|
}
|
|
513
607
|
|
|
514
608
|
// --- src/client/components/accounts-section.js ---
|
|
515
|
-
function AccountsSection({ status, busy, handlePinAccount, t }) {
|
|
609
|
+
function AccountsSection({ status, busy, handlePinAccount, handleAddAccount, handleDeleteAccount, t }) {
|
|
516
610
|
if (!status.accounts || !status.accounts.length) return null
|
|
611
|
+
|
|
612
|
+
const [showAdd, setShowAdd] = React.useState(false)
|
|
613
|
+
const [newLabel, setNewLabel] = React.useState('')
|
|
614
|
+
const [newEnv, setNewEnv] = React.useState('')
|
|
615
|
+
const [newKey, setNewKey] = React.useState('')
|
|
616
|
+
const [showKey, setShowKey] = React.useState(false)
|
|
617
|
+
const [deleteSecret, setDeleteSecret] = React.useState(true)
|
|
618
|
+
|
|
619
|
+
function handleOpenAdd() {
|
|
620
|
+
let max = 1
|
|
621
|
+
for (const a of (status.accounts || [])) {
|
|
622
|
+
const m = String(a.apiKeyEnv || '').match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
623
|
+
if (m) {
|
|
624
|
+
const n = parseInt(m[1], 10)
|
|
625
|
+
if (n >= max) max = n + 1
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
setNewEnv(`CLINEBOT_API_KEY_${max}`)
|
|
629
|
+
setShowAdd(true)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function onSubmitAdd(e) {
|
|
633
|
+
if (e && e.preventDefault) e.preventDefault()
|
|
634
|
+
if (!newKey.trim()) return
|
|
635
|
+
if (typeof handleAddAccount === 'function') {
|
|
636
|
+
await handleAddAccount({
|
|
637
|
+
label: newLabel.trim() || newEnv.trim(),
|
|
638
|
+
apiKeyEnv: newEnv.trim(),
|
|
639
|
+
apiKey: newKey.trim(),
|
|
640
|
+
})
|
|
641
|
+
}
|
|
642
|
+
setNewLabel('')
|
|
643
|
+
setNewEnv('')
|
|
644
|
+
setNewKey('')
|
|
645
|
+
setShowAdd(false)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function onDeleteAccount(envName) {
|
|
649
|
+
if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
|
|
650
|
+
if (!window.confirm(t('accounts.delete_confirm', { label: envName }))) return
|
|
651
|
+
}
|
|
652
|
+
if (typeof handleDeleteAccount === 'function') {
|
|
653
|
+
await handleDeleteAccount(envName, deleteSecret)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
517
657
|
return React.createElement(
|
|
518
658
|
'div',
|
|
519
659
|
{ className: 'cb-section-card' },
|
|
520
|
-
React.createElement(
|
|
660
|
+
React.createElement(
|
|
661
|
+
'div',
|
|
662
|
+
{ className: 'cb-section-title', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
663
|
+
React.createElement('span', null, t('accounts.title')),
|
|
664
|
+
!showAdd
|
|
665
|
+
? React.createElement(
|
|
666
|
+
'button',
|
|
667
|
+
{
|
|
668
|
+
type: 'button',
|
|
669
|
+
className: 'cb-btn',
|
|
670
|
+
style: { fontSize: '11px', padding: '4px 10px' },
|
|
671
|
+
onClick: handleOpenAdd,
|
|
672
|
+
disabled: !!busy,
|
|
673
|
+
},
|
|
674
|
+
t('accounts.add_btn')
|
|
675
|
+
)
|
|
676
|
+
: null
|
|
677
|
+
),
|
|
521
678
|
React.createElement('div', { className: 'cb-section-desc' }, t('accounts.desc')),
|
|
679
|
+
|
|
680
|
+
showAdd
|
|
681
|
+
? React.createElement(
|
|
682
|
+
'form',
|
|
683
|
+
{
|
|
684
|
+
onSubmit: onSubmitAdd,
|
|
685
|
+
style: {
|
|
686
|
+
background: 'var(--dsw-alias-bg-hover, rgba(0,0,0,0.04))',
|
|
687
|
+
border: '1px solid var(--dsw-alias-border, rgba(0,0,0,0.12))',
|
|
688
|
+
borderRadius: '8px',
|
|
689
|
+
padding: '12px',
|
|
690
|
+
marginBottom: '14px',
|
|
691
|
+
display: 'flex',
|
|
692
|
+
flexDirection: 'column',
|
|
693
|
+
gap: '8px',
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
React.createElement(
|
|
697
|
+
'div',
|
|
698
|
+
{ style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
|
|
699
|
+
React.createElement('input', {
|
|
700
|
+
type: 'text',
|
|
701
|
+
className: 'cb-input',
|
|
702
|
+
style: { flex: '1 1 180px' },
|
|
703
|
+
placeholder: t('accounts.label_placeholder'),
|
|
704
|
+
value: newLabel,
|
|
705
|
+
onChange: (e) => setNewLabel(e.target.value),
|
|
706
|
+
}),
|
|
707
|
+
React.createElement('input', {
|
|
708
|
+
type: 'text',
|
|
709
|
+
className: 'cb-input',
|
|
710
|
+
style: { flex: '1 1 180px' },
|
|
711
|
+
placeholder: t('accounts.env_placeholder'),
|
|
712
|
+
value: newEnv,
|
|
713
|
+
onChange: (e) => setNewEnv(e.target.value),
|
|
714
|
+
})
|
|
715
|
+
),
|
|
716
|
+
React.createElement(
|
|
717
|
+
'div',
|
|
718
|
+
{ style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
719
|
+
React.createElement('input', {
|
|
720
|
+
type: showKey ? 'text' : 'password',
|
|
721
|
+
className: 'cb-input',
|
|
722
|
+
style: { flex: '1' },
|
|
723
|
+
placeholder: t('key.placeholder_empty'),
|
|
724
|
+
value: newKey,
|
|
725
|
+
onChange: (e) => setNewKey(e.target.value),
|
|
726
|
+
}),
|
|
727
|
+
React.createElement(
|
|
728
|
+
'button',
|
|
729
|
+
{
|
|
730
|
+
type: 'button',
|
|
731
|
+
className: 'cb-btn',
|
|
732
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
733
|
+
onClick: () => setShowKey(!showKey),
|
|
734
|
+
},
|
|
735
|
+
showKey ? t('key.hide') : t('key.show')
|
|
736
|
+
)
|
|
737
|
+
),
|
|
738
|
+
React.createElement(
|
|
739
|
+
'div',
|
|
740
|
+
{ style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '4px' } },
|
|
741
|
+
React.createElement(
|
|
742
|
+
'button',
|
|
743
|
+
{
|
|
744
|
+
type: 'button',
|
|
745
|
+
className: 'cb-btn',
|
|
746
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
747
|
+
onClick: () => setShowAdd(false),
|
|
748
|
+
},
|
|
749
|
+
t('accounts.cancel_btn')
|
|
750
|
+
),
|
|
751
|
+
React.createElement(
|
|
752
|
+
'button',
|
|
753
|
+
{
|
|
754
|
+
type: 'submit',
|
|
755
|
+
className: 'cb-btn cb-btn-primary',
|
|
756
|
+
style: { padding: '4px 12px', fontSize: '12px' },
|
|
757
|
+
disabled: !newKey.trim() || !!busy,
|
|
758
|
+
},
|
|
759
|
+
busy === 'add-account' ? t('key.saving') : t('accounts.save_btn')
|
|
760
|
+
)
|
|
761
|
+
)
|
|
762
|
+
)
|
|
763
|
+
: null,
|
|
764
|
+
|
|
522
765
|
React.createElement(
|
|
523
766
|
'table',
|
|
524
767
|
{ className: 'cb-table' },
|
|
@@ -541,9 +784,10 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
|
|
|
541
784
|
const envName = typeof acc.apiKeyEnv === 'string' ? acc.apiKeyEnv : ''
|
|
542
785
|
const label = typeof acc.label === 'string' ? acc.label : ''
|
|
543
786
|
const isActive = status.activeAccount === envName || (!status.activeAccount && acc.id === 'default')
|
|
787
|
+
const isPrimary = acc.id === 'default' || envName === status.config?.apiKeyEnv
|
|
544
788
|
return React.createElement(
|
|
545
789
|
'tr',
|
|
546
|
-
{ key: acc.id },
|
|
790
|
+
{ key: acc.id || envName },
|
|
547
791
|
React.createElement('td', null, React.createElement('strong', null, label)),
|
|
548
792
|
React.createElement('td', null, React.createElement('code', null, envName)),
|
|
549
793
|
React.createElement(
|
|
@@ -554,6 +798,12 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
|
|
|
554
798
|
: React.createElement('span', { className: 'cb-badge cb-badge-warn' }, 'Missing Key'),
|
|
555
799
|
isActive
|
|
556
800
|
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, t('accounts.active_badge'))
|
|
801
|
+
: null,
|
|
802
|
+
acc.isPinned && !isActive
|
|
803
|
+
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '6px', opacity: 0.85 } }, t('accounts.pinned_badge'))
|
|
804
|
+
: null,
|
|
805
|
+
typeof acc.percentUsed === 'number'
|
|
806
|
+
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '6px' } }, t('accounts.quota_used', { pct: acc.percentUsed }))
|
|
557
807
|
: null
|
|
558
808
|
),
|
|
559
809
|
React.createElement(
|
|
@@ -571,12 +821,36 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
|
|
|
571
821
|
},
|
|
572
822
|
t('accounts.pin_btn')
|
|
573
823
|
)
|
|
824
|
+
: null,
|
|
825
|
+
!isPrimary
|
|
826
|
+
? React.createElement(
|
|
827
|
+
'button',
|
|
828
|
+
{
|
|
829
|
+
type: 'button',
|
|
830
|
+
className: 'cb-btn cb-btn-danger',
|
|
831
|
+
style: { padding: '4px 8px', fontSize: '11px', marginLeft: '6px' },
|
|
832
|
+
disabled: !!busy,
|
|
833
|
+
onClick: () => onDeleteAccount(envName),
|
|
834
|
+
},
|
|
835
|
+
t('accounts.delete_btn')
|
|
836
|
+
)
|
|
574
837
|
: null
|
|
575
838
|
)
|
|
576
839
|
)
|
|
577
840
|
})
|
|
578
841
|
)
|
|
579
|
-
)
|
|
842
|
+
),
|
|
843
|
+
status.lastRotation
|
|
844
|
+
? React.createElement(
|
|
845
|
+
'div',
|
|
846
|
+
{ className: 'cb-rotation-info', style: { marginTop: '10px', fontSize: '12px', opacity: 0.85 } },
|
|
847
|
+
t('accounts.last_failover', {
|
|
848
|
+
from: status.lastRotation.from,
|
|
849
|
+
to: status.lastRotation.to,
|
|
850
|
+
reason: status.lastRotation.reason,
|
|
851
|
+
})
|
|
852
|
+
)
|
|
853
|
+
: null
|
|
580
854
|
)
|
|
581
855
|
}
|
|
582
856
|
|
|
@@ -627,7 +901,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
|
|
|
627
901
|
'div',
|
|
628
902
|
{ className: 'cb-section-desc' },
|
|
629
903
|
usage?.user?.email
|
|
630
|
-
? t('quota.account', { email: usage.user.email, plan: usage.plan || 'ClinePass
|
|
904
|
+
? t('quota.account', { email: usage.user.email, plan: usage.plan || 'ClinePass' })
|
|
631
905
|
: t('quota.desc')
|
|
632
906
|
),
|
|
633
907
|
React.createElement(
|
|
@@ -654,14 +928,63 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
|
|
|
654
928
|
}
|
|
655
929
|
|
|
656
930
|
// --- src/client/components/models-section.js ---
|
|
657
|
-
function ModelsSection({
|
|
931
|
+
function ModelsSection({
|
|
932
|
+
modelsList,
|
|
933
|
+
disabledSet,
|
|
934
|
+
enabledCount,
|
|
935
|
+
keyPresent,
|
|
936
|
+
busy,
|
|
937
|
+
handleSyncPlanModels,
|
|
938
|
+
handleSetModelsFilter,
|
|
939
|
+
handleToggleModel,
|
|
940
|
+
planSynced,
|
|
941
|
+
planSyncedAt,
|
|
942
|
+
defaultModelWarning,
|
|
943
|
+
t,
|
|
944
|
+
}) {
|
|
945
|
+
const syncDateStr = planSyncedAt ? new Date(planSyncedAt).toLocaleDateString() : ''
|
|
946
|
+
const [search, setSearch] = React.useState('')
|
|
947
|
+
const [viewFilter, setViewFilter] = React.useState('all')
|
|
948
|
+
|
|
949
|
+
const filteredModels = modelsList.filter((m) => {
|
|
950
|
+
if (viewFilter === 'vision') {
|
|
951
|
+
const hasVision = Boolean(m.input?.includes('image') || m.input?.includes('vision'))
|
|
952
|
+
if (!hasVision) return false
|
|
953
|
+
} else if (viewFilter === 'coding') {
|
|
954
|
+
if (m.category !== 'coding') return false
|
|
955
|
+
} else if (viewFilter === 'recommended') {
|
|
956
|
+
if (!m.recommended) return false
|
|
957
|
+
} else if (viewFilter === 'disabled') {
|
|
958
|
+
if (!disabledSet.has(m.id)) return false
|
|
959
|
+
}
|
|
960
|
+
if (search.trim()) {
|
|
961
|
+
const q = search.trim().toLowerCase()
|
|
962
|
+
const name = String(m.name || '').toLowerCase()
|
|
963
|
+
const id = String(m.id || '').toLowerCase()
|
|
964
|
+
const desc = String(m.description || '').toLowerCase()
|
|
965
|
+
if (!name.includes(q) && !id.includes(q) && !desc.includes(q)) return false
|
|
966
|
+
}
|
|
967
|
+
return true
|
|
968
|
+
})
|
|
969
|
+
|
|
658
970
|
return React.createElement(
|
|
659
971
|
'div',
|
|
660
972
|
{ className: 'cb-section-card' },
|
|
973
|
+
defaultModelWarning
|
|
974
|
+
? React.createElement('div', { className: 'cb-alert-bad', style: { marginBottom: '12px' } }, `⚠️ ${defaultModelWarning}`)
|
|
975
|
+
: null,
|
|
661
976
|
React.createElement(
|
|
662
977
|
'div',
|
|
663
978
|
{ className: 'cb-section-title' },
|
|
664
|
-
|
|
979
|
+
React.createElement(
|
|
980
|
+
'span',
|
|
981
|
+
null,
|
|
982
|
+
t('models.title', { enabled: enabledCount, total: modelsList.length }),
|
|
983
|
+
planSynced
|
|
984
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '8px', fontSize: '11px' } }, t('models.verified'))
|
|
985
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-warn', style: { marginLeft: '8px', fontSize: '11px' } }, t('models.unverified')),
|
|
986
|
+
syncDateStr ? React.createElement('span', { style: { marginLeft: '8px', fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('models.synced_at', { date: syncDateStr })) : null
|
|
987
|
+
),
|
|
665
988
|
React.createElement(
|
|
666
989
|
'div',
|
|
667
990
|
{ className: 'cb-row' },
|
|
@@ -682,6 +1005,83 @@ function ModelsSection({ modelsList, disabledSet, enabledCount, keyPresent, busy
|
|
|
682
1005
|
)
|
|
683
1006
|
),
|
|
684
1007
|
React.createElement('div', { className: 'cb-section-desc' }, t('models.desc')),
|
|
1008
|
+
|
|
1009
|
+
// Search and display filter bar
|
|
1010
|
+
React.createElement(
|
|
1011
|
+
'div',
|
|
1012
|
+
{
|
|
1013
|
+
style: {
|
|
1014
|
+
display: 'flex',
|
|
1015
|
+
gap: '8px',
|
|
1016
|
+
alignItems: 'center',
|
|
1017
|
+
flexWrap: 'wrap',
|
|
1018
|
+
marginBottom: '12px',
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
React.createElement('input', {
|
|
1022
|
+
type: 'text',
|
|
1023
|
+
className: 'cb-input',
|
|
1024
|
+
style: { flex: '1 1 200px' },
|
|
1025
|
+
placeholder: t('models.search_placeholder'),
|
|
1026
|
+
value: search,
|
|
1027
|
+
onChange: (e) => setSearch(e.target.value),
|
|
1028
|
+
}),
|
|
1029
|
+
React.createElement(
|
|
1030
|
+
'div',
|
|
1031
|
+
{ className: 'cb-row', style: { gap: '4px' } },
|
|
1032
|
+
React.createElement(
|
|
1033
|
+
'button',
|
|
1034
|
+
{
|
|
1035
|
+
type: 'button',
|
|
1036
|
+
className: `cb-btn ${viewFilter === 'all' ? 'cb-btn-active' : ''}`,
|
|
1037
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1038
|
+
onClick: () => setViewFilter('all'),
|
|
1039
|
+
},
|
|
1040
|
+
t('models.filter_all')
|
|
1041
|
+
),
|
|
1042
|
+
React.createElement(
|
|
1043
|
+
'button',
|
|
1044
|
+
{
|
|
1045
|
+
type: 'button',
|
|
1046
|
+
className: `cb-btn ${viewFilter === 'vision' ? 'cb-btn-active' : ''}`,
|
|
1047
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1048
|
+
onClick: () => setViewFilter('vision'),
|
|
1049
|
+
},
|
|
1050
|
+
t('models.filter_vision')
|
|
1051
|
+
),
|
|
1052
|
+
React.createElement(
|
|
1053
|
+
'button',
|
|
1054
|
+
{
|
|
1055
|
+
type: 'button',
|
|
1056
|
+
className: `cb-btn ${viewFilter === 'coding' ? 'cb-btn-active' : ''}`,
|
|
1057
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1058
|
+
onClick: () => setViewFilter('coding'),
|
|
1059
|
+
},
|
|
1060
|
+
t('models.filter_coding')
|
|
1061
|
+
),
|
|
1062
|
+
React.createElement(
|
|
1063
|
+
'button',
|
|
1064
|
+
{
|
|
1065
|
+
type: 'button',
|
|
1066
|
+
className: `cb-btn ${viewFilter === 'recommended' ? 'cb-btn-active' : ''}`,
|
|
1067
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1068
|
+
onClick: () => setViewFilter('recommended'),
|
|
1069
|
+
},
|
|
1070
|
+
t('models.filter_recommended')
|
|
1071
|
+
),
|
|
1072
|
+
React.createElement(
|
|
1073
|
+
'button',
|
|
1074
|
+
{
|
|
1075
|
+
type: 'button',
|
|
1076
|
+
className: `cb-btn ${viewFilter === 'disabled' ? 'cb-btn-active' : ''}`,
|
|
1077
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1078
|
+
onClick: () => setViewFilter('disabled'),
|
|
1079
|
+
},
|
|
1080
|
+
t('models.filter_disabled')
|
|
1081
|
+
)
|
|
1082
|
+
)
|
|
1083
|
+
),
|
|
1084
|
+
|
|
685
1085
|
React.createElement(
|
|
686
1086
|
'table',
|
|
687
1087
|
{ className: 'cb-table' },
|
|
@@ -701,7 +1101,7 @@ function ModelsSection({ modelsList, disabledSet, enabledCount, keyPresent, busy
|
|
|
701
1101
|
React.createElement(
|
|
702
1102
|
'tbody',
|
|
703
1103
|
null,
|
|
704
|
-
|
|
1104
|
+
filteredModels.map((m) => {
|
|
705
1105
|
const isEnabled = !disabledSet.has(m.id)
|
|
706
1106
|
return React.createElement(
|
|
707
1107
|
'tr',
|
|
@@ -727,17 +1127,31 @@ function ModelsSection({ modelsList, disabledSet, enabledCount, keyPresent, busy
|
|
|
727
1127
|
: null
|
|
728
1128
|
),
|
|
729
1129
|
React.createElement('td', null, React.createElement('code', null, m.id)),
|
|
730
|
-
React.createElement('td', null, `${Math.round((m.contextLength ||
|
|
1130
|
+
React.createElement('td', null, `${Math.round((m.contextLength || 128000) / 1000)}k`),
|
|
731
1131
|
React.createElement(
|
|
732
1132
|
'td',
|
|
733
1133
|
null,
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1134
|
+
(() => {
|
|
1135
|
+
const hasReasoning = Boolean(
|
|
1136
|
+
(Array.isArray(m.reasoningEfforts) && m.reasoningEfforts.length > 0) ||
|
|
1137
|
+
(m.reasoningEfforts && typeof m.reasoningEfforts === 'object' && Object.keys(m.reasoningEfforts).length > 0) ||
|
|
1138
|
+
m.reasoning ||
|
|
1139
|
+
m.category === 'reasoning'
|
|
1140
|
+
)
|
|
1141
|
+
const category = m.category && m.category !== 'reasoning' ? m.category : (hasReasoning ? null : 'general')
|
|
1142
|
+
const hasVision = Boolean(m.input?.includes('image') || m.input?.includes('vision'))
|
|
1143
|
+
return React.createElement(
|
|
1144
|
+
React.Fragment,
|
|
1145
|
+
null,
|
|
1146
|
+
category ? React.createElement('span', { className: 'cb-badge' }, category) : null,
|
|
1147
|
+
hasVision
|
|
1148
|
+
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, 'Vision')
|
|
1149
|
+
: null,
|
|
1150
|
+
hasReasoning
|
|
1151
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: (category || hasVision) ? '4px' : '0' }, title: t('models.reasoning_tooltip') }, '🧠 Reasoning')
|
|
1152
|
+
: null
|
|
1153
|
+
)
|
|
1154
|
+
})()
|
|
741
1155
|
)
|
|
742
1156
|
)
|
|
743
1157
|
})
|
|
@@ -840,58 +1254,23 @@ Response: ${smokeResult.preview || '(empty)'}`
|
|
|
840
1254
|
}
|
|
841
1255
|
|
|
842
1256
|
// --- src/client/settings-page.js ---
|
|
843
|
-
function
|
|
844
|
-
if (!ctx || typeof ctx.get !== 'function') return undefined
|
|
845
|
-
try {
|
|
846
|
-
return ctx.get('configForms') || undefined
|
|
847
|
-
} catch (err) {
|
|
848
|
-
console.warn('[dsh-clinebot] configForms is not available:', err)
|
|
849
|
-
return undefined
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function SettingsPage(props) {
|
|
854
|
-
const ctx = props?.ctx
|
|
855
|
-
const t = props?.t || makeT(en, en)
|
|
856
|
-
|
|
1257
|
+
function useConfigFormsSnapshot(ctx) {
|
|
857
1258
|
const scope = React.useMemo(() => {
|
|
858
|
-
|
|
859
|
-
if (!svc || typeof svc.get !== 'function') return undefined
|
|
860
|
-
try {
|
|
861
|
-
return svc.get(NS)
|
|
862
|
-
} catch (err) {
|
|
863
|
-
console.warn('[dsh-clinebot] configForms.get failed:', err)
|
|
864
|
-
return undefined
|
|
865
|
-
}
|
|
1259
|
+
try { return readConfigForms(ctx)?.get?.(NS) || undefined } catch { return undefined }
|
|
866
1260
|
}, [ctx])
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
return (cb) => {
|
|
870
|
-
if (!scope?.subscribe) return () => {}
|
|
871
|
-
try {
|
|
872
|
-
return scope.subscribe(cb) || (() => {})
|
|
873
|
-
} catch (_) {
|
|
874
|
-
return () => {}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
1261
|
+
const subscribe = React.useMemo(() => (cb) => {
|
|
1262
|
+
try { return scope?.subscribe ? (scope.subscribe(cb) || (() => {})) : () => {} } catch { return () => {} }
|
|
877
1263
|
}, [scope])
|
|
878
|
-
|
|
879
1264
|
const getSnapshot = React.useCallback(() => {
|
|
880
|
-
|
|
881
|
-
try {
|
|
882
|
-
return scope.getSnapshot() || SNAPSHOT_READY
|
|
883
|
-
} catch (err) {
|
|
884
|
-
console.warn('[dsh-clinebot] settings snapshot failed:', err)
|
|
885
|
-
return SNAPSHOT_READY
|
|
886
|
-
}
|
|
1265
|
+
try { return scope?.getSnapshot?.() || SNAPSHOT_READY } catch { return SNAPSHOT_READY }
|
|
887
1266
|
}, [scope])
|
|
1267
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, () => SNAPSHOT_READY)?.status || 'loading'
|
|
1268
|
+
}
|
|
888
1269
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
)
|
|
894
|
-
const snapshotStatus = snapshot?.status || 'loading'
|
|
1270
|
+
function SettingsPage(props) {
|
|
1271
|
+
const ctx = props?.ctx
|
|
1272
|
+
const t = props?.t || makeT(en, en)
|
|
1273
|
+
const snapshotStatus = useConfigFormsSnapshot(ctx)
|
|
895
1274
|
|
|
896
1275
|
const [status, setStatus] = React.useState(null)
|
|
897
1276
|
const [draft, setDraft] = React.useState(null)
|
|
@@ -900,7 +1279,6 @@ function SettingsPage(props) {
|
|
|
900
1279
|
const [msg, setMsg] = React.useState('')
|
|
901
1280
|
const [smokeResult, setSmokeResult] = React.useState(null)
|
|
902
1281
|
|
|
903
|
-
// Plugin in-app updater state
|
|
904
1282
|
const [updateState, setUpdateState] = React.useState({
|
|
905
1283
|
checking: false,
|
|
906
1284
|
updating: false,
|
|
@@ -912,28 +1290,10 @@ function SettingsPage(props) {
|
|
|
912
1290
|
notice: '',
|
|
913
1291
|
})
|
|
914
1292
|
|
|
915
|
-
// Key input state
|
|
916
1293
|
const [apiKeyInput, setApiKeyInput] = React.useState('')
|
|
917
1294
|
const [showKey, setShowKey] = React.useState(false)
|
|
918
1295
|
|
|
919
|
-
|
|
920
|
-
const [newAccountLabel, setNewAccountLabel] = React.useState('')
|
|
921
|
-
const [newAccountEnv, setNewAccountEnv] = React.useState('')
|
|
922
|
-
const [showAddAccount, setShowAddAccount] = React.useState(false)
|
|
923
|
-
|
|
924
|
-
// Filter & Search for Models
|
|
925
|
-
const [modelsFilter, setModelsFilter] = React.useState('all')
|
|
926
|
-
const [modelsSearch, setModelsSearch] = React.useState('')
|
|
927
|
-
const [disabledModels, setDisabledModels] = React.useState([])
|
|
928
|
-
const [advancedOpen, setAdvancedOpen] = React.useState(false)
|
|
929
|
-
|
|
930
|
-
// Loopback fast login state
|
|
931
|
-
const [fastLoginActive, setFastLoginActive] = React.useState(false)
|
|
932
|
-
const [fastLoginNotice, setFastLoginNotice] = React.useState('')
|
|
933
|
-
|
|
934
|
-
React.useEffect(() => {
|
|
935
|
-
ensureCss()
|
|
936
|
-
}, [])
|
|
1296
|
+
React.useEffect(() => { ensureCss() }, [])
|
|
937
1297
|
|
|
938
1298
|
const load = React.useCallback(async () => {
|
|
939
1299
|
setErr('')
|
|
@@ -967,9 +1327,7 @@ function SettingsPage(props) {
|
|
|
967
1327
|
}
|
|
968
1328
|
}, [])
|
|
969
1329
|
|
|
970
|
-
React.useEffect(() => {
|
|
971
|
-
checkUpdate()
|
|
972
|
-
}, [checkUpdate])
|
|
1330
|
+
React.useEffect(() => { checkUpdate() }, [checkUpdate])
|
|
973
1331
|
|
|
974
1332
|
async function handleTriggerUpdate() {
|
|
975
1333
|
if (updateState.updating) return
|
|
@@ -980,9 +1338,7 @@ function SettingsPage(props) {
|
|
|
980
1338
|
headers: { 'x-dsh-plugin-update': '1' },
|
|
981
1339
|
})
|
|
982
1340
|
const data = await res.json().catch(() => ({}))
|
|
983
|
-
if (!res.ok || data.ok === false || data.error) {
|
|
984
|
-
throw new Error(data.error || `HTTP ${res.status}`)
|
|
985
|
-
}
|
|
1341
|
+
if (!res.ok || data.ok === false || data.error) throw new Error(data.error || `HTTP ${res.status}`)
|
|
986
1342
|
const newVer = data.updatedVersion || updateState.latestVersion || updateState.currentVersion
|
|
987
1343
|
setUpdateState((s) => ({
|
|
988
1344
|
...s,
|
|
@@ -993,24 +1349,27 @@ function SettingsPage(props) {
|
|
|
993
1349
|
}))
|
|
994
1350
|
setTimeout(() => checkUpdate(), 2000)
|
|
995
1351
|
} catch (err) {
|
|
996
|
-
setUpdateState((s) => ({
|
|
997
|
-
...s,
|
|
998
|
-
updating: false,
|
|
999
|
-
error: t('update.failed', { error: String(err.message || err) }),
|
|
1000
|
-
}))
|
|
1352
|
+
setUpdateState((s) => ({ ...s, updating: false, error: t('update.failed', { error: String(err.message || err) }) }))
|
|
1001
1353
|
}
|
|
1002
1354
|
}
|
|
1003
1355
|
|
|
1004
|
-
async function
|
|
1005
|
-
|
|
1006
|
-
if (!keyVal) {
|
|
1007
|
-
setErr(t('key.empty_err'))
|
|
1008
|
-
return
|
|
1009
|
-
}
|
|
1010
|
-
setBusy('save-key')
|
|
1356
|
+
async function performAction(busyKey, fn) {
|
|
1357
|
+
setBusy(busyKey)
|
|
1011
1358
|
setErr('')
|
|
1012
1359
|
setMsg('')
|
|
1013
1360
|
try {
|
|
1361
|
+
await fn()
|
|
1362
|
+
} catch (e) {
|
|
1363
|
+
setErr(String(e.message || e))
|
|
1364
|
+
} finally {
|
|
1365
|
+
setBusy('')
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function handleSaveKey() {
|
|
1370
|
+
const keyVal = String(apiKeyInput || '').trim()
|
|
1371
|
+
if (!keyVal) { setErr(t('key.empty_err')); return }
|
|
1372
|
+
await performAction('save-key', async () => {
|
|
1014
1373
|
const res = await fetch(`${ROUTE_PREFIX}/save-key`, {
|
|
1015
1374
|
method: 'POST',
|
|
1016
1375
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1021,87 +1380,53 @@ function SettingsPage(props) {
|
|
|
1021
1380
|
setApiKeyInput('')
|
|
1022
1381
|
setMsg(t('key.saved_msg', { status: data.validated ? 'OK' : 'Notice (check console)' }))
|
|
1023
1382
|
await load()
|
|
1024
|
-
}
|
|
1025
|
-
setErr(String(e.message || e))
|
|
1026
|
-
} finally {
|
|
1027
|
-
setBusy('')
|
|
1028
|
-
}
|
|
1383
|
+
})
|
|
1029
1384
|
}
|
|
1030
1385
|
|
|
1031
1386
|
async function handleRefreshQuota() {
|
|
1032
|
-
|
|
1033
|
-
setErr('')
|
|
1034
|
-
setMsg('')
|
|
1035
|
-
try {
|
|
1387
|
+
await performAction('refresh-quota', async () => {
|
|
1036
1388
|
const res = await fetch(`${ROUTE_PREFIX}/usage`)
|
|
1037
1389
|
const data = await res.json().catch(() => ({}))
|
|
1038
1390
|
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1039
1391
|
setStatus((prev) => (prev ? { ...prev, usage: data } : prev))
|
|
1040
1392
|
setMsg(t('quota.refreshed_msg'))
|
|
1041
|
-
}
|
|
1042
|
-
setErr(String(e.message || e))
|
|
1043
|
-
} finally {
|
|
1044
|
-
setBusy('')
|
|
1045
|
-
}
|
|
1393
|
+
})
|
|
1046
1394
|
}
|
|
1047
1395
|
|
|
1048
1396
|
async function handleSyncPlanModels() {
|
|
1049
|
-
|
|
1050
|
-
setErr('')
|
|
1051
|
-
setMsg('')
|
|
1052
|
-
try {
|
|
1397
|
+
await performAction('sync-models', async () => {
|
|
1053
1398
|
const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
|
|
1054
1399
|
const data = await res.json().catch(() => ({}))
|
|
1055
1400
|
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1056
1401
|
setMsg(t('models.synced_msg', { total: data.totalModelsCount, discovered: data.discoveredCount }))
|
|
1057
1402
|
await load()
|
|
1058
|
-
}
|
|
1059
|
-
setErr(String(e.message || e))
|
|
1060
|
-
} finally {
|
|
1061
|
-
setBusy('')
|
|
1062
|
-
}
|
|
1403
|
+
})
|
|
1063
1404
|
}
|
|
1064
1405
|
|
|
1065
1406
|
async function handleRegister() {
|
|
1066
|
-
|
|
1067
|
-
setErr('')
|
|
1068
|
-
setMsg('')
|
|
1069
|
-
try {
|
|
1407
|
+
await performAction('register', async () => {
|
|
1070
1408
|
const res = await fetch(`${ROUTE_PREFIX}/register`, { method: 'POST' })
|
|
1071
1409
|
const data = await res.json().catch(() => ({}))
|
|
1072
1410
|
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1073
1411
|
const count = status?.availableModels?.filter((m) => !(draft?.disabledModels || []).includes(m.id)).length || 0
|
|
1074
1412
|
setMsg(t('diag.resynced_msg', { count }))
|
|
1075
1413
|
await load()
|
|
1076
|
-
}
|
|
1077
|
-
setErr(String(e.message || e))
|
|
1078
|
-
} finally {
|
|
1079
|
-
setBusy('')
|
|
1080
|
-
}
|
|
1414
|
+
})
|
|
1081
1415
|
}
|
|
1082
1416
|
|
|
1083
1417
|
async function handleUnregister() {
|
|
1084
|
-
|
|
1085
|
-
setErr('')
|
|
1086
|
-
setMsg('')
|
|
1087
|
-
try {
|
|
1418
|
+
await performAction('unregister', async () => {
|
|
1088
1419
|
const res = await fetch(`${ROUTE_PREFIX}/unregister`, { method: 'POST' })
|
|
1089
1420
|
const data = await res.json().catch(() => ({}))
|
|
1090
1421
|
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1091
1422
|
setMsg(t('diag.unregistered_msg'))
|
|
1092
1423
|
await load()
|
|
1093
|
-
}
|
|
1094
|
-
setErr(String(e.message || e))
|
|
1095
|
-
} finally {
|
|
1096
|
-
setBusy('')
|
|
1097
|
-
}
|
|
1424
|
+
})
|
|
1098
1425
|
}
|
|
1099
1426
|
|
|
1100
1427
|
async function handleSmoke() {
|
|
1101
|
-
setBusy('smoke')
|
|
1102
|
-
setErr('')
|
|
1103
1428
|
setSmokeResult(null)
|
|
1104
|
-
|
|
1429
|
+
await performAction('smoke', async () => {
|
|
1105
1430
|
const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
|
|
1106
1431
|
method: 'POST',
|
|
1107
1432
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1110,34 +1435,48 @@ function SettingsPage(props) {
|
|
|
1110
1435
|
const data = await res.json().catch(() => ({}))
|
|
1111
1436
|
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1112
1437
|
setSmokeResult(data)
|
|
1113
|
-
}
|
|
1114
|
-
setErr(String(e.message || e))
|
|
1115
|
-
} finally {
|
|
1116
|
-
setBusy('')
|
|
1117
|
-
}
|
|
1438
|
+
})
|
|
1118
1439
|
}
|
|
1119
1440
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1441
|
+
const [verifyStatus, setVerifyStatus] = React.useState({ state: 'idle', email: '', plan: '', error: '' })
|
|
1442
|
+
const verifyTimerRef = typeof React.useRef === 'function' ? React.useRef(null) : { current: null }
|
|
1443
|
+
|
|
1444
|
+
React.useEffect(() => {
|
|
1445
|
+
const raw = String(apiKeyInput || '').trim()
|
|
1446
|
+
if (!raw || raw.length < 10) {
|
|
1447
|
+
setVerifyStatus({ state: 'idle', email: '', plan: '', error: '' })
|
|
1448
|
+
return
|
|
1449
|
+
}
|
|
1450
|
+
if (verifyTimerRef.current) clearTimeout(verifyTimerRef.current)
|
|
1451
|
+
verifyTimerRef.current = setTimeout(async () => {
|
|
1452
|
+
setVerifyStatus({ state: 'verifying', email: '', plan: '', error: '' })
|
|
1453
|
+
try {
|
|
1454
|
+
const res = await fetch(`${ROUTE_PREFIX}/key/verify`, {
|
|
1455
|
+
method: 'POST',
|
|
1456
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1457
|
+
body: JSON.stringify({ key: raw }),
|
|
1458
|
+
})
|
|
1459
|
+
const data = await res.json().catch(() => ({}))
|
|
1460
|
+
if (data.ok && data.valid) {
|
|
1461
|
+
setVerifyStatus({ state: 'valid', email: data.email, plan: data.plan, error: '' })
|
|
1462
|
+
} else {
|
|
1463
|
+
setVerifyStatus({ state: 'invalid', email: '', plan: '', error: data.error || 'Verification failed' })
|
|
1464
|
+
}
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
setVerifyStatus({ state: 'invalid', email: '', plan: '', error: String(err?.message || err) })
|
|
1129
1467
|
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
setBusy('')
|
|
1468
|
+
}, 500)
|
|
1469
|
+
return () => {
|
|
1470
|
+
if (verifyTimerRef.current) clearTimeout(verifyTimerRef.current)
|
|
1134
1471
|
}
|
|
1472
|
+
}, [apiKeyInput])
|
|
1473
|
+
|
|
1474
|
+
function handleFastLogin() {
|
|
1475
|
+
window.open('https://app.cline.bot/settings/api-keys', '_blank')
|
|
1135
1476
|
}
|
|
1136
1477
|
|
|
1137
1478
|
async function handlePinAccount(accountEnv) {
|
|
1138
|
-
|
|
1139
|
-
setErr('')
|
|
1140
|
-
try {
|
|
1479
|
+
await performAction('pin-account', async () => {
|
|
1141
1480
|
const res = await fetch(`${ROUTE_PREFIX}/accounts/active`, {
|
|
1142
1481
|
method: 'POST',
|
|
1143
1482
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1146,25 +1485,65 @@ function SettingsPage(props) {
|
|
|
1146
1485
|
const data = await res.json().catch(() => ({}))
|
|
1147
1486
|
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1148
1487
|
await load()
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1488
|
+
})
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
async function handleAddAccount({ label, apiKeyEnv, apiKey }) {
|
|
1492
|
+
await performAction('add-account', async () => {
|
|
1493
|
+
const res = await fetch(`${ROUTE_PREFIX}/accounts`, {
|
|
1494
|
+
method: 'POST',
|
|
1495
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1496
|
+
body: JSON.stringify({ label, apiKeyEnv, apiKey }),
|
|
1497
|
+
})
|
|
1498
|
+
const data = await res.json().catch(() => ({}))
|
|
1499
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1500
|
+
setMsg(t('key.saved_msg', { status: data.validated ? 'OK' : (data.validationError || 'Notice') }))
|
|
1501
|
+
await load()
|
|
1502
|
+
})
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
async function handleDeleteAccount(accountEnv, deleteSecret = true) {
|
|
1506
|
+
await performAction('delete-account', async () => {
|
|
1507
|
+
const res = await fetch(`${ROUTE_PREFIX}/accounts/delete`, {
|
|
1508
|
+
method: 'POST',
|
|
1509
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1510
|
+
body: JSON.stringify({ apiKeyEnv: accountEnv, deleteSecret }),
|
|
1511
|
+
})
|
|
1512
|
+
const data = await res.json().catch(() => ({}))
|
|
1513
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1514
|
+
await load()
|
|
1515
|
+
})
|
|
1154
1516
|
}
|
|
1155
1517
|
|
|
1156
1518
|
const debounceTimerRef = typeof React.useRef === 'function' ? React.useRef(null) : { current: null }
|
|
1157
|
-
|
|
1519
|
+
|
|
1520
|
+
React.useEffect(() => {
|
|
1521
|
+
return () => {
|
|
1522
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
|
|
1523
|
+
}
|
|
1524
|
+
}, [])
|
|
1525
|
+
|
|
1526
|
+
function debounceSaveDisabledModels(nextDisabled, previousDisabled) {
|
|
1158
1527
|
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
|
|
1159
1528
|
debounceTimerRef.current = setTimeout(async () => {
|
|
1160
1529
|
try {
|
|
1161
|
-
await fetch(`${ROUTE_PREFIX}/models/toggle`, {
|
|
1530
|
+
const res = await fetch(`${ROUTE_PREFIX}/models/toggle`, {
|
|
1162
1531
|
method: 'POST',
|
|
1163
1532
|
headers: { 'Content-Type': 'application/json' },
|
|
1164
1533
|
body: JSON.stringify({ disabledModels: nextDisabled }),
|
|
1165
1534
|
})
|
|
1535
|
+
const data = await res.json().catch(() => ({}))
|
|
1536
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1537
|
+
await load()
|
|
1166
1538
|
} catch (err) {
|
|
1167
1539
|
console.warn('[dsh-clinebot] Failed saving disabled models:', err)
|
|
1540
|
+
setErr(String(err?.message || err))
|
|
1541
|
+
setDraft((prev) => {
|
|
1542
|
+
if (!prev) return prev
|
|
1543
|
+
const all = status?.availableModels || []
|
|
1544
|
+
const prevEnabled = all.map((m) => m.id).filter((mId) => !previousDisabled.includes(mId))
|
|
1545
|
+
return { ...prev, disabledModels: previousDisabled, enabledModels: prevEnabled }
|
|
1546
|
+
})
|
|
1168
1547
|
}
|
|
1169
1548
|
}, 400)
|
|
1170
1549
|
}
|
|
@@ -1176,7 +1555,7 @@ function SettingsPage(props) {
|
|
|
1176
1555
|
const all = status?.availableModels || []
|
|
1177
1556
|
const enabledIds = all.map((m) => m.id).filter((mId) => !next.includes(mId))
|
|
1178
1557
|
setDraft({ ...draft, disabledModels: next, enabledModels: enabledIds })
|
|
1179
|
-
debounceSaveDisabledModels(next)
|
|
1558
|
+
debounceSaveDisabledModels(next, curr)
|
|
1180
1559
|
}
|
|
1181
1560
|
|
|
1182
1561
|
function handleSetModelsFilter(type) {
|
|
@@ -1192,10 +1571,11 @@ function SettingsPage(props) {
|
|
|
1192
1571
|
allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
|
|
1193
1572
|
}
|
|
1194
1573
|
|
|
1574
|
+
const curr = draft?.disabledModels || []
|
|
1195
1575
|
const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
|
|
1196
1576
|
const nextEnabled = Array.from(allowed)
|
|
1197
1577
|
setDraft({ ...draft, disabledModels: nextDisabled, enabledModels: nextEnabled })
|
|
1198
|
-
debounceSaveDisabledModels(nextDisabled)
|
|
1578
|
+
debounceSaveDisabledModels(nextDisabled, curr)
|
|
1199
1579
|
}
|
|
1200
1580
|
|
|
1201
1581
|
if (!status || !draft) {
|
|
@@ -1264,6 +1644,11 @@ function SettingsPage(props) {
|
|
|
1264
1644
|
// In-app Update Bar
|
|
1265
1645
|
React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, t }),
|
|
1266
1646
|
|
|
1647
|
+
// Settings host status warning
|
|
1648
|
+
snapshotStatus === 'unavailable'
|
|
1649
|
+
? React.createElement('div', { className: 'cb-alert-bad', style: { marginBottom: '12px' } }, t('settings.unavailable'))
|
|
1650
|
+
: null,
|
|
1651
|
+
|
|
1267
1652
|
// Notifications
|
|
1268
1653
|
err ? React.createElement('div', { className: 'cb-alert-bad' }, err) : null,
|
|
1269
1654
|
msg ? React.createElement('div', { className: 'cb-alert-ok' }, msg) : null,
|
|
@@ -1279,11 +1664,19 @@ function SettingsPage(props) {
|
|
|
1279
1664
|
draft,
|
|
1280
1665
|
handleSaveKey,
|
|
1281
1666
|
handleFastLogin,
|
|
1667
|
+
verifyStatus,
|
|
1282
1668
|
t,
|
|
1283
1669
|
}),
|
|
1284
1670
|
|
|
1285
1671
|
// Accounts Pool Card
|
|
1286
|
-
React.createElement(AccountsSection, {
|
|
1672
|
+
React.createElement(AccountsSection, {
|
|
1673
|
+
status,
|
|
1674
|
+
busy,
|
|
1675
|
+
handlePinAccount,
|
|
1676
|
+
handleAddAccount,
|
|
1677
|
+
handleDeleteAccount,
|
|
1678
|
+
t,
|
|
1679
|
+
}),
|
|
1287
1680
|
|
|
1288
1681
|
// Quota Warning & Dashboard Card
|
|
1289
1682
|
React.createElement(QuotaSection, { keyPresent, status, usage, busy, handleRefreshQuota, t }),
|
|
@@ -1298,6 +1691,9 @@ function SettingsPage(props) {
|
|
|
1298
1691
|
handleSyncPlanModels,
|
|
1299
1692
|
handleSetModelsFilter,
|
|
1300
1693
|
handleToggleModel,
|
|
1694
|
+
planSynced: !!(status?.config?.planSynced || (status?.config?.dynamicModels && status.config.dynamicModels.length > 0)),
|
|
1695
|
+
planSyncedAt: status?.config?.planSyncedAt || 0,
|
|
1696
|
+
defaultModelWarning: status?.config?.defaultModelWarning || '',
|
|
1301
1697
|
t,
|
|
1302
1698
|
}),
|
|
1303
1699
|
|
|
@@ -1370,7 +1766,7 @@ function PluginCard(props) {
|
|
|
1370
1766
|
React.createElement(
|
|
1371
1767
|
ErrorBoundary,
|
|
1372
1768
|
null,
|
|
1373
|
-
React.createElement(SettingsPage, { ...props, ctx:
|
|
1769
|
+
React.createElement(SettingsPage, { ...props, ctx: props && props.ctx })
|
|
1374
1770
|
)
|
|
1375
1771
|
)
|
|
1376
1772
|
: null
|