@goodandready/dsh-clinebot 0.3.24 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -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 +182 -71
- package/lib/cline-client.js +25 -44
- package/lib/config.js +39 -6
- package/lib/http.js +15 -2
- package/lib/index.js +172 -53
- package/lib/models.js +47 -22
- package/lib/provider-sync.js +15 -5
- package/lib/routes/accounts.js +8 -8
- package/lib/routes/auth.js +65 -37
- package/lib/routes/models.js +31 -28
- package/lib/routes/settings.js +51 -11
- package/lib/slash-command.js +51 -9
- package/lib/telemetry.js +78 -0
- package/package.json +4 -1
package/lib/client.js
CHANGED
|
@@ -23,12 +23,12 @@ const SNAPSHOT_READY = Object.freeze({ status: 'ready', view: null })
|
|
|
23
23
|
// --- src/client/locales.js ---
|
|
24
24
|
const en = {
|
|
25
25
|
title: 'ClineBot',
|
|
26
|
-
subtitle: 'ClinePass subscription
|
|
26
|
+
subtitle: 'ClinePass subscription with dynamic plan models.',
|
|
27
27
|
'settings.loading': 'Loading ClineBot settings…',
|
|
28
28
|
'settings.retry': 'Retry',
|
|
29
29
|
'settings.unavailable': 'ClineBot settings unavailable (host namespace is not ready).',
|
|
30
30
|
'header.title': 'ClineBot (ClinePass) Provider',
|
|
31
|
-
'header.sub': 'Connect models under ClinePass subscription
|
|
31
|
+
'header.sub': 'Connect models under your active ClinePass subscription. OpenAI API compatible, automated rolling limit tracking, and instant auto-registration in DSH.',
|
|
32
32
|
'badge.online': 'Host online ({latency} ms)',
|
|
33
33
|
'badge.offline': 'Host unreachable',
|
|
34
34
|
'badge.key_ok': 'Key ✓ ({source})',
|
|
@@ -43,8 +43,11 @@ const en = {
|
|
|
43
43
|
'key.hide': 'Hide',
|
|
44
44
|
'key.save': 'Save Key',
|
|
45
45
|
'key.saving': 'Saving…',
|
|
46
|
-
'key.login_fast': '
|
|
47
|
-
'key.login_waiting': '
|
|
46
|
+
'key.login_fast': '🔑 Open API Keys ↗',
|
|
47
|
+
'key.login_waiting': 'Paste key below for instant verification.',
|
|
48
|
+
'key.verifying': 'Validating key with Cline…',
|
|
49
|
+
'key.verified': '✓ Valid key for {email} ({plan})',
|
|
50
|
+
'key.invalid': '✗ Key validation failed: {error}',
|
|
48
51
|
'key.env_label': 'Credential environment name: ',
|
|
49
52
|
'key.get_key': 'Get key in app.cline.bot console ↗',
|
|
50
53
|
'key.saved_msg': 'Key saved to DSH Credentials. Validation: {status}',
|
|
@@ -74,6 +77,9 @@ const en = {
|
|
|
74
77
|
'models.sync': '🔄 Check Plan Models',
|
|
75
78
|
'models.syncing': 'Checking…',
|
|
76
79
|
'models.synced_msg': 'Synchronized subscription models: {total} ({discovered} discovered from ClinePass)',
|
|
80
|
+
'models.verified': 'Verified with plan',
|
|
81
|
+
'models.unverified': 'Not synced (fallback catalog)',
|
|
82
|
+
'models.synced_at': 'Synced: {date}',
|
|
77
83
|
'models.all': 'All',
|
|
78
84
|
'models.vision': 'Vision Only',
|
|
79
85
|
'models.coding': 'Coding Only',
|
|
@@ -113,12 +119,12 @@ const en = {
|
|
|
113
119
|
|
|
114
120
|
const zh = {
|
|
115
121
|
title: 'ClineBot',
|
|
116
|
-
subtitle: 'ClinePass
|
|
122
|
+
subtitle: 'ClinePass 订阅服务,包含动态套餐模型。',
|
|
117
123
|
'settings.loading': '正在加载 ClineBot 设置…',
|
|
118
124
|
'settings.retry': '重试',
|
|
119
125
|
'settings.unavailable': 'ClineBot 设置不可用(宿主命名空间尚未就绪)。',
|
|
120
126
|
'header.title': 'ClineBot (ClinePass) 模型服务商',
|
|
121
|
-
'header.sub': '通过 ClinePass
|
|
127
|
+
'header.sub': '通过 ClinePass 订阅连接主流推理与编程模型。完全兼容 OpenAI 接口,自动追踪滚动限额并在 DSH 中即时生效。',
|
|
122
128
|
'badge.online': '服务在线 ({latency} ms)',
|
|
123
129
|
'badge.offline': '主机无法连接',
|
|
124
130
|
'badge.key_ok': '密钥正常 ({source})',
|
|
@@ -133,8 +139,11 @@ const en = {
|
|
|
133
139
|
'key.hide': '隐藏',
|
|
134
140
|
'key.save': '保存密钥',
|
|
135
141
|
'key.saving': '正在保存…',
|
|
136
|
-
'key.login_fast': '
|
|
137
|
-
'key.login_waiting': '
|
|
142
|
+
'key.login_fast': '🔑 打开 API 密钥页 ↗',
|
|
143
|
+
'key.login_waiting': '请在下方粘贴密钥,系统将自动进行有效性校验。',
|
|
144
|
+
'key.verifying': '正在连接 Cline 验证密钥…',
|
|
145
|
+
'key.verified': '✓ 密钥有效:{email} ({plan})',
|
|
146
|
+
'key.invalid': '✗ 密钥验证未通过:{error}',
|
|
138
147
|
'key.env_label': '凭据环境变量名称:',
|
|
139
148
|
'key.get_key': '前往 app.cline.bot 控制台获取密钥 ↗',
|
|
140
149
|
'key.saved_msg': '密钥已成功保存至 DSH 凭据服务。验证状态:{status}',
|
|
@@ -164,6 +173,9 @@ const en = {
|
|
|
164
173
|
'models.sync': '🔄 同步订阅模型',
|
|
165
174
|
'models.syncing': '正在同步…',
|
|
166
175
|
'models.synced_msg': '已成功同步订阅模型:共 {total} 个(发现 {discovered} 个新模型)',
|
|
176
|
+
'models.verified': '已与套餐同步',
|
|
177
|
+
'models.unverified': '未同步(备用目录)',
|
|
178
|
+
'models.synced_at': '同步时间:{date}',
|
|
167
179
|
'models.all': '全部模型',
|
|
168
180
|
'models.vision': '仅视觉 (Vision)',
|
|
169
181
|
'models.coding': '仅编程 (Coding)',
|
|
@@ -275,11 +287,14 @@ function ensureCss() {
|
|
|
275
287
|
.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
288
|
.cb-btn-disabled{opacity:0.5;cursor:not-allowed}
|
|
277
289
|
|
|
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-
|
|
290
|
+
.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)}
|
|
291
|
+
.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%}
|
|
292
|
+
.cb-progress-label{font-weight:500;white-space:nowrap}
|
|
293
|
+
.cb-progress-meta, .cb-bar-meta{font-size:12px;color:var(--dsw-alias-label-secondary);white-space:nowrap;margin-left:auto}
|
|
294
|
+
.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)}
|
|
295
|
+
.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))}
|
|
296
|
+
.cb-progress-bar-warn{height:100%;border-radius:999px;transition:width .3s;background:var(--dsw-alias-state-warning-primary)}
|
|
297
|
+
.cb-progress-bar-bad{height:100%;border-radius:999px;transition:width .3s;background:var(--dsw-alias-state-error-primary)}
|
|
283
298
|
|
|
284
299
|
.cb-table{width:100%;border-collapse:collapse;margin-top:8px}
|
|
285
300
|
.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 +380,62 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
|
365
380
|
|
|
366
381
|
return React.createElement(
|
|
367
382
|
'div',
|
|
368
|
-
{
|
|
383
|
+
{
|
|
384
|
+
className: 'cb-progress-card cb-bar-container',
|
|
385
|
+
style: {
|
|
386
|
+
display: 'flex',
|
|
387
|
+
flexDirection: 'column',
|
|
388
|
+
gap: '8px',
|
|
389
|
+
padding: '12px 14px',
|
|
390
|
+
borderRadius: '8px',
|
|
391
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
392
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
393
|
+
},
|
|
394
|
+
},
|
|
369
395
|
React.createElement(
|
|
370
396
|
'div',
|
|
371
|
-
{
|
|
372
|
-
|
|
397
|
+
{
|
|
398
|
+
className: 'cb-progress-head cb-bar-head',
|
|
399
|
+
style: {
|
|
400
|
+
display: 'flex',
|
|
401
|
+
justifyContent: 'space-between',
|
|
402
|
+
alignItems: 'baseline',
|
|
403
|
+
gap: '16px',
|
|
404
|
+
width: '100%',
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
React.createElement('span', { className: 'cb-progress-label', style: { fontWeight: 500, whiteSpace: 'nowrap' } }, label),
|
|
373
408
|
React.createElement(
|
|
374
409
|
'span',
|
|
375
|
-
{
|
|
410
|
+
{
|
|
411
|
+
className: 'cb-progress-meta cb-bar-meta',
|
|
412
|
+
style: {
|
|
413
|
+
fontSize: '12px',
|
|
414
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
415
|
+
whiteSpace: 'nowrap',
|
|
416
|
+
marginLeft: 'auto',
|
|
417
|
+
},
|
|
418
|
+
},
|
|
376
419
|
t('quota.used', { pct }),
|
|
377
420
|
resetLabel ? ` · ${t('quota.reset_at', { time: resetLabel })}` : ''
|
|
378
421
|
)
|
|
379
422
|
),
|
|
380
423
|
React.createElement(
|
|
381
424
|
'div',
|
|
382
|
-
{
|
|
425
|
+
{
|
|
426
|
+
className: 'cb-progress-track cb-bar-track',
|
|
427
|
+
style: {
|
|
428
|
+
width: '100%',
|
|
429
|
+
height: '8px',
|
|
430
|
+
borderRadius: '999px',
|
|
431
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
432
|
+
overflow: 'hidden',
|
|
433
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
434
|
+
},
|
|
435
|
+
},
|
|
383
436
|
React.createElement('div', {
|
|
384
437
|
className: barClass,
|
|
385
|
-
style: { width: `${pct}
|
|
438
|
+
style: { width: `${pct}%`, height: '100%', borderRadius: '999px' },
|
|
386
439
|
})
|
|
387
440
|
)
|
|
388
441
|
)
|
|
@@ -445,7 +498,7 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
|
445
498
|
}
|
|
446
499
|
|
|
447
500
|
// --- src/client/components/key-section.js ---
|
|
448
|
-
function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowKey, busy, draft, handleSaveKey, handleFastLogin, t }) {
|
|
501
|
+
function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowKey, busy, draft, handleSaveKey, handleFastLogin, verifyStatus, t }) {
|
|
449
502
|
return React.createElement(
|
|
450
503
|
'div',
|
|
451
504
|
{ className: 'cb-section-card' },
|
|
@@ -488,19 +541,28 @@ function KeySection({ keyPresent, apiKeyInput, setApiKeyInput, showKey, setShowK
|
|
|
488
541
|
disabled: !!busy,
|
|
489
542
|
onClick: handleFastLogin,
|
|
490
543
|
},
|
|
491
|
-
|
|
544
|
+
t('key.login_fast')
|
|
492
545
|
)
|
|
493
546
|
),
|
|
547
|
+
verifyStatus && verifyStatus.state === 'verifying'
|
|
548
|
+
? React.createElement('div', { style: { marginTop: '8px', fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('key.verifying'))
|
|
549
|
+
: null,
|
|
550
|
+
verifyStatus && verifyStatus.state === 'valid'
|
|
551
|
+
? React.createElement('div', { className: 'cb-alert-ok', style: { marginTop: '8px', fontSize: '13px' } }, t('key.verified', { email: verifyStatus.email, plan: verifyStatus.plan }))
|
|
552
|
+
: null,
|
|
553
|
+
verifyStatus && verifyStatus.state === 'invalid'
|
|
554
|
+
? React.createElement('div', { className: 'cb-alert-bad', style: { marginTop: '8px', fontSize: '13px' } }, t('key.invalid', { error: verifyStatus.error }))
|
|
555
|
+
: null,
|
|
494
556
|
React.createElement(
|
|
495
557
|
'div',
|
|
496
|
-
{ className: 'cb-row', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
558
|
+
{ className: 'cb-row', style: { fontSize: '13px', marginTop: '12px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
497
559
|
React.createElement('span', null, t('key.env_label')),
|
|
498
560
|
React.createElement('code', null, draft.apiKeyEnv || 'CLINEBOT_API_KEY'),
|
|
499
561
|
React.createElement('span', null, ' · '),
|
|
500
562
|
React.createElement(
|
|
501
563
|
'a',
|
|
502
564
|
{
|
|
503
|
-
href: 'https://app.cline.bot',
|
|
565
|
+
href: 'https://app.cline.bot/settings/api-keys',
|
|
504
566
|
target: '_blank',
|
|
505
567
|
rel: 'noreferrer',
|
|
506
568
|
style: { color: 'var(--dsw-alias-state-brand-primary)' },
|
|
@@ -576,7 +638,14 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
|
|
|
576
638
|
)
|
|
577
639
|
})
|
|
578
640
|
)
|
|
579
|
-
)
|
|
641
|
+
),
|
|
642
|
+
status.lastRotation
|
|
643
|
+
? React.createElement(
|
|
644
|
+
'div',
|
|
645
|
+
{ className: 'cb-rotation-info', style: { marginTop: '10px', fontSize: '12px', opacity: 0.85 } },
|
|
646
|
+
`Last failover: ${status.lastRotation.from} → ${status.lastRotation.to} (${status.lastRotation.reason})`
|
|
647
|
+
)
|
|
648
|
+
: null
|
|
580
649
|
)
|
|
581
650
|
}
|
|
582
651
|
|
|
@@ -627,7 +696,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
|
|
|
627
696
|
'div',
|
|
628
697
|
{ className: 'cb-section-desc' },
|
|
629
698
|
usage?.user?.email
|
|
630
|
-
? t('quota.account', { email: usage.user.email, plan: usage.plan || 'ClinePass
|
|
699
|
+
? t('quota.account', { email: usage.user.email, plan: usage.plan || 'ClinePass' })
|
|
631
700
|
: t('quota.desc')
|
|
632
701
|
),
|
|
633
702
|
React.createElement(
|
|
@@ -654,14 +723,40 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
|
|
|
654
723
|
}
|
|
655
724
|
|
|
656
725
|
// --- src/client/components/models-section.js ---
|
|
657
|
-
function ModelsSection({
|
|
726
|
+
function ModelsSection({
|
|
727
|
+
modelsList,
|
|
728
|
+
disabledSet,
|
|
729
|
+
enabledCount,
|
|
730
|
+
keyPresent,
|
|
731
|
+
busy,
|
|
732
|
+
handleSyncPlanModels,
|
|
733
|
+
handleSetModelsFilter,
|
|
734
|
+
handleToggleModel,
|
|
735
|
+
planSynced,
|
|
736
|
+
planSyncedAt,
|
|
737
|
+
defaultModelWarning,
|
|
738
|
+
t,
|
|
739
|
+
}) {
|
|
740
|
+
const syncDateStr = planSyncedAt ? new Date(planSyncedAt).toLocaleDateString() : ''
|
|
741
|
+
|
|
658
742
|
return React.createElement(
|
|
659
743
|
'div',
|
|
660
744
|
{ className: 'cb-section-card' },
|
|
745
|
+
defaultModelWarning
|
|
746
|
+
? React.createElement('div', { className: 'cb-alert-bad', style: { marginBottom: '12px' } }, `⚠️ ${defaultModelWarning}`)
|
|
747
|
+
: null,
|
|
661
748
|
React.createElement(
|
|
662
749
|
'div',
|
|
663
750
|
{ className: 'cb-section-title' },
|
|
664
|
-
|
|
751
|
+
React.createElement(
|
|
752
|
+
'span',
|
|
753
|
+
null,
|
|
754
|
+
t('models.title', { enabled: enabledCount, total: modelsList.length }),
|
|
755
|
+
planSynced
|
|
756
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '8px', fontSize: '11px' } }, t('models.verified'))
|
|
757
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-warn', style: { marginLeft: '8px', fontSize: '11px' } }, t('models.unverified')),
|
|
758
|
+
syncDateStr ? React.createElement('span', { style: { marginLeft: '8px', fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('models.synced_at', { date: syncDateStr })) : null
|
|
759
|
+
),
|
|
665
760
|
React.createElement(
|
|
666
761
|
'div',
|
|
667
762
|
{ className: 'cb-row' },
|
|
@@ -727,17 +822,31 @@ function ModelsSection({ modelsList, disabledSet, enabledCount, keyPresent, busy
|
|
|
727
822
|
: null
|
|
728
823
|
),
|
|
729
824
|
React.createElement('td', null, React.createElement('code', null, m.id)),
|
|
730
|
-
React.createElement('td', null, `${Math.round((m.contextLength ||
|
|
825
|
+
React.createElement('td', null, `${Math.round((m.contextLength || 128000) / 1000)}k`),
|
|
731
826
|
React.createElement(
|
|
732
827
|
'td',
|
|
733
828
|
null,
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
829
|
+
(() => {
|
|
830
|
+
const hasReasoning = Boolean(
|
|
831
|
+
(Array.isArray(m.reasoningEfforts) && m.reasoningEfforts.length > 0) ||
|
|
832
|
+
(m.reasoningEfforts && typeof m.reasoningEfforts === 'object' && Object.keys(m.reasoningEfforts).length > 0) ||
|
|
833
|
+
m.reasoning ||
|
|
834
|
+
m.category === 'reasoning'
|
|
835
|
+
)
|
|
836
|
+
const category = m.category && m.category !== 'reasoning' ? m.category : (hasReasoning ? null : 'general')
|
|
837
|
+
const hasVision = Boolean(m.input?.includes('image') || m.input?.includes('vision'))
|
|
838
|
+
return React.createElement(
|
|
839
|
+
React.Fragment,
|
|
840
|
+
null,
|
|
841
|
+
category ? React.createElement('span', { className: 'cb-badge' }, category) : null,
|
|
842
|
+
hasVision
|
|
843
|
+
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, 'Vision')
|
|
844
|
+
: null,
|
|
845
|
+
hasReasoning
|
|
846
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: (category || hasVision) ? '4px' : '0' }, title: t('models.reasoning_tooltip') }, '🧠 Reasoning')
|
|
847
|
+
: null
|
|
848
|
+
)
|
|
849
|
+
})()
|
|
741
850
|
)
|
|
742
851
|
)
|
|
743
852
|
})
|
|
@@ -916,21 +1025,6 @@ function SettingsPage(props) {
|
|
|
916
1025
|
const [apiKeyInput, setApiKeyInput] = React.useState('')
|
|
917
1026
|
const [showKey, setShowKey] = React.useState(false)
|
|
918
1027
|
|
|
919
|
-
// Accounts state
|
|
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
1028
|
React.useEffect(() => {
|
|
935
1029
|
ensureCss()
|
|
936
1030
|
}, [])
|
|
@@ -1117,21 +1211,41 @@ function SettingsPage(props) {
|
|
|
1117
1211
|
}
|
|
1118
1212
|
}
|
|
1119
1213
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1214
|
+
const [verifyStatus, setVerifyStatus] = React.useState({ state: 'idle', email: '', plan: '', error: '' })
|
|
1215
|
+
const verifyTimerRef = typeof React.useRef === 'function' ? React.useRef(null) : { current: null }
|
|
1216
|
+
|
|
1217
|
+
React.useEffect(() => {
|
|
1218
|
+
const raw = String(apiKeyInput || '').trim()
|
|
1219
|
+
if (!raw || raw.length < 10) {
|
|
1220
|
+
setVerifyStatus({ state: 'idle', email: '', plan: '', error: '' })
|
|
1221
|
+
return
|
|
1222
|
+
}
|
|
1223
|
+
if (verifyTimerRef.current) clearTimeout(verifyTimerRef.current)
|
|
1224
|
+
verifyTimerRef.current = setTimeout(async () => {
|
|
1225
|
+
setVerifyStatus({ state: 'verifying', email: '', plan: '', error: '' })
|
|
1226
|
+
try {
|
|
1227
|
+
const res = await fetch(`${ROUTE_PREFIX}/key/verify`, {
|
|
1228
|
+
method: 'POST',
|
|
1229
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1230
|
+
body: JSON.stringify({ key: raw }),
|
|
1231
|
+
})
|
|
1232
|
+
const data = await res.json().catch(() => ({}))
|
|
1233
|
+
if (data.ok && data.valid) {
|
|
1234
|
+
setVerifyStatus({ state: 'valid', email: data.email, plan: data.plan, error: '' })
|
|
1235
|
+
} else {
|
|
1236
|
+
setVerifyStatus({ state: 'invalid', email: '', plan: '', error: data.error || 'Verification failed' })
|
|
1237
|
+
}
|
|
1238
|
+
} catch (err) {
|
|
1239
|
+
setVerifyStatus({ state: 'invalid', email: '', plan: '', error: String(err?.message || err) })
|
|
1129
1240
|
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
setBusy('')
|
|
1241
|
+
}, 500)
|
|
1242
|
+
return () => {
|
|
1243
|
+
if (verifyTimerRef.current) clearTimeout(verifyTimerRef.current)
|
|
1134
1244
|
}
|
|
1245
|
+
}, [apiKeyInput])
|
|
1246
|
+
|
|
1247
|
+
function handleFastLogin() {
|
|
1248
|
+
window.open('https://app.cline.bot/settings/api-keys', '_blank')
|
|
1135
1249
|
}
|
|
1136
1250
|
|
|
1137
1251
|
async function handlePinAccount(accountEnv) {
|
|
@@ -1199,13 +1313,6 @@ function SettingsPage(props) {
|
|
|
1199
1313
|
}
|
|
1200
1314
|
|
|
1201
1315
|
if (!status || !draft) {
|
|
1202
|
-
if (snapshotStatus === 'unavailable') {
|
|
1203
|
-
return React.createElement(
|
|
1204
|
-
'div',
|
|
1205
|
-
{ className: 'cb-page' },
|
|
1206
|
-
React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
|
|
1207
|
-
)
|
|
1208
|
-
}
|
|
1209
1316
|
if (err) {
|
|
1210
1317
|
return React.createElement(
|
|
1211
1318
|
'div',
|
|
@@ -1286,6 +1393,7 @@ function SettingsPage(props) {
|
|
|
1286
1393
|
draft,
|
|
1287
1394
|
handleSaveKey,
|
|
1288
1395
|
handleFastLogin,
|
|
1396
|
+
verifyStatus,
|
|
1289
1397
|
t,
|
|
1290
1398
|
}),
|
|
1291
1399
|
|
|
@@ -1305,6 +1413,9 @@ function SettingsPage(props) {
|
|
|
1305
1413
|
handleSyncPlanModels,
|
|
1306
1414
|
handleSetModelsFilter,
|
|
1307
1415
|
handleToggleModel,
|
|
1416
|
+
planSynced: !!(status?.config?.planSynced || (status?.config?.dynamicModels && status.config.dynamicModels.length > 0)),
|
|
1417
|
+
planSyncedAt: status?.config?.planSyncedAt || 0,
|
|
1418
|
+
defaultModelWarning: status?.config?.defaultModelWarning || '',
|
|
1308
1419
|
t,
|
|
1309
1420
|
}),
|
|
1310
1421
|
|
|
@@ -1377,7 +1488,7 @@ function PluginCard(props) {
|
|
|
1377
1488
|
React.createElement(
|
|
1378
1489
|
ErrorBoundary,
|
|
1379
1490
|
null,
|
|
1380
|
-
React.createElement(SettingsPage, { ...props, ctx:
|
|
1491
|
+
React.createElement(SettingsPage, { ...props, ctx: props && props.ctx })
|
|
1381
1492
|
)
|
|
1382
1493
|
)
|
|
1383
1494
|
: null
|
package/lib/cline-client.js
CHANGED
|
@@ -51,46 +51,12 @@ export function normalizeBaseUrl(raw) {
|
|
|
51
51
|
return s
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
completionTokensEst: 0,
|
|
61
|
-
totalTokensEst: 0,
|
|
62
|
-
lastLatencyMs: null,
|
|
63
|
-
lastRequestAt: null,
|
|
64
|
-
lastError: null,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function recordSessionRequest({ latencyMs, ok, error, promptTokens = 0, completionTokens = 0 }) {
|
|
68
|
-
sessionStats.totalRequests += 1
|
|
69
|
-
if (ok) {
|
|
70
|
-
sessionStats.successfulRequests += 1
|
|
71
|
-
sessionStats.lastLatencyMs = typeof latencyMs === 'number' ? latencyMs : null
|
|
72
|
-
sessionStats.lastError = null
|
|
73
|
-
} else {
|
|
74
|
-
sessionStats.failedRequests += 1
|
|
75
|
-
sessionStats.lastError = error || 'Request failed'
|
|
76
|
-
}
|
|
77
|
-
sessionStats.lastRequestAt = Date.now()
|
|
78
|
-
sessionStats.promptTokensEst += Number(promptTokens) || 0
|
|
79
|
-
sessionStats.completionTokensEst += Number(completionTokens) || 0
|
|
80
|
-
sessionStats.totalTokensEst += (Number(promptTokens) || 0) + (Number(completionTokens) || 0)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function resetSessionStats() {
|
|
84
|
-
sessionStats.totalRequests = 0
|
|
85
|
-
sessionStats.successfulRequests = 0
|
|
86
|
-
sessionStats.failedRequests = 0
|
|
87
|
-
sessionStats.promptTokensEst = 0
|
|
88
|
-
sessionStats.completionTokensEst = 0
|
|
89
|
-
sessionStats.totalTokensEst = 0
|
|
90
|
-
sessionStats.lastLatencyMs = null
|
|
91
|
-
sessionStats.lastRequestAt = null
|
|
92
|
-
sessionStats.lastError = null
|
|
93
|
-
}
|
|
54
|
+
export {
|
|
55
|
+
sessionStats,
|
|
56
|
+
recordSessionRequest,
|
|
57
|
+
recordSmokeTest,
|
|
58
|
+
resetSessionStats,
|
|
59
|
+
} from './telemetry.js'
|
|
94
60
|
|
|
95
61
|
/**
|
|
96
62
|
* Resolve API key from environment variables.
|
|
@@ -279,7 +245,7 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
|
|
|
279
245
|
|
|
280
246
|
let userEmail = null
|
|
281
247
|
let createdAt = null
|
|
282
|
-
let planDisplayName = 'ClinePass
|
|
248
|
+
let planDisplayName = 'ClinePass'
|
|
283
249
|
let dynamicModels = []
|
|
284
250
|
|
|
285
251
|
try {
|
|
@@ -297,8 +263,14 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
|
|
|
297
263
|
if (planRes?.ok) {
|
|
298
264
|
const planData = await planRes.json().catch(() => ({}))
|
|
299
265
|
const plan = planData?.data?.plan || planData?.data || planData?.plan || planData
|
|
300
|
-
|
|
301
|
-
|
|
266
|
+
const name = plan?.displayName || plan?.title || plan?.name || 'ClinePass'
|
|
267
|
+
const priceCents = typeof plan?.pricePerSeatCents === 'number'
|
|
268
|
+
? plan.pricePerSeatCents
|
|
269
|
+
: (typeof plan?.priceInCents === 'number' ? plan.priceInCents : null)
|
|
270
|
+
if (priceCents !== null) {
|
|
271
|
+
planDisplayName = `${name} ($${(priceCents / 100).toFixed(2)}/mo)`
|
|
272
|
+
} else {
|
|
273
|
+
planDisplayName = name
|
|
302
274
|
}
|
|
303
275
|
const featuresIncluded = plan?.features?.included || plan?.includedModels
|
|
304
276
|
if (featuresIncluded) {
|
|
@@ -434,6 +406,15 @@ export async function smokeChat(baseUrl, apiKey, {
|
|
|
434
406
|
fetchImpl = fetch,
|
|
435
407
|
} = {}) {
|
|
436
408
|
const base = normalizeBaseUrl(baseUrl)
|
|
409
|
+
try {
|
|
410
|
+
const parsed = new URL(base)
|
|
411
|
+
if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
|
|
412
|
+
return { ok: false, error: 'Insecure protocol: smokeChat requires https: for remote endpoints' }
|
|
413
|
+
}
|
|
414
|
+
} catch (err) {
|
|
415
|
+
return { ok: false, error: `Invalid baseUrl: ${err?.message || err}` }
|
|
416
|
+
}
|
|
417
|
+
|
|
437
418
|
if (!apiKey) {
|
|
438
419
|
return { ok: false, error: 'Missing API key. Set credential or environment variable.' }
|
|
439
420
|
}
|
|
@@ -587,4 +568,4 @@ export async function resolveKeyValue(ctx, apiKeyEnv) {
|
|
|
587
568
|
}
|
|
588
569
|
|
|
589
570
|
|
|
590
|
-
export { resolveAccountPool, isAccountQuotaExhausted, rotateToNextAccount } from './account-pool.js'
|
|
571
|
+
export { resolveAccountPool, isAccountQuotaExhausted, rotateToNextAccount, getLastRotation, setLastRotation, isQuotaExceededError } from './account-pool.js'
|
package/lib/config.js
CHANGED
|
@@ -38,7 +38,7 @@ export const Config = z.object({
|
|
|
38
38
|
disabledModels: z.array(z.string()).default([])
|
|
39
39
|
.description('List of model IDs explicitly disabled by the user (new models are enabled automatically).').volatile(),
|
|
40
40
|
enabledModels: z.array(z.string()).default([])
|
|
41
|
-
.description('Deprecated: preserved for backwards compatibility with earlier versions.')
|
|
41
|
+
.description('Deprecated: preserved for backwards compatibility with earlier versions.'),
|
|
42
42
|
dynamicModels: z.array(z.object({
|
|
43
43
|
id: z.string(),
|
|
44
44
|
name: z.string(),
|
|
@@ -50,7 +50,9 @@ export const Config = z.object({
|
|
|
50
50
|
isCustom: z.boolean().default(false),
|
|
51
51
|
reasoningEfforts: z.any().default(undefined),
|
|
52
52
|
})).default([])
|
|
53
|
-
.description('Models automatically discovered from the official ClinePass subscription plan.')
|
|
53
|
+
.description('Models automatically discovered from the official ClinePass subscription plan.'),
|
|
54
|
+
planSyncedAt: z.number().default(0)
|
|
55
|
+
.description('Timestamp when models were last synchronized with active subscription plan.').volatile(),
|
|
54
56
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
|
|
55
57
|
.description('HTTP probe timeout in milliseconds.').volatile(),
|
|
56
58
|
smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
|
|
@@ -84,25 +86,56 @@ export function plainConfig(cfg) {
|
|
|
84
86
|
return out
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
export function volatileConfig(cfg) {
|
|
90
|
+
const plain = plainConfig(cfg)
|
|
91
|
+
if (!plain || typeof plain !== 'object') return plain
|
|
92
|
+
const out = {}
|
|
93
|
+
for (const [key, field] of Object.entries(Config.dict || {})) {
|
|
94
|
+
if (field?.meta?.volatile && Object.hasOwn(plain, key)) {
|
|
95
|
+
out[key] = plain[key]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
|
|
87
101
|
export function publicConfig(cfg) {
|
|
88
102
|
cfg = plainConfig(cfg)
|
|
89
103
|
const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
|
|
90
|
-
const
|
|
104
|
+
const isSynced = dynamic.length > 0
|
|
105
|
+
const allModels = getAllModels(dynamic)
|
|
106
|
+
const allDefaultIds = allModels.map((m) => m.id)
|
|
91
107
|
|
|
92
108
|
let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels : []
|
|
93
|
-
if (
|
|
109
|
+
if (cfg?.disabledModels === undefined && Array.isArray(cfg?.enabledModels) && cfg.enabledModels.length > 0) {
|
|
94
110
|
const enabledSet = new Set(cfg.enabledModels)
|
|
95
111
|
disabledList = allDefaultIds.filter((id) => !enabledSet.has(id))
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
|
|
114
|
+
let activeIds = getActiveModelIds(allDefaultIds, disabledList)
|
|
115
|
+
if (!activeIds.length && allDefaultIds.length) {
|
|
116
|
+
activeIds = [allDefaultIds[0]]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let defaultModel = cfg?.defaultModel || DEFAULT_MODEL_ID
|
|
120
|
+
let defaultModelWarning = null
|
|
121
|
+
|
|
122
|
+
if (allDefaultIds.length > 0 && !allDefaultIds.includes(defaultModel)) {
|
|
123
|
+
const fallbackModel = activeIds[0] || allDefaultIds[0]
|
|
124
|
+
defaultModelWarning = `Configured default model "${defaultModel}" is not available in your active plan. Using "${fallbackModel}" instead.`
|
|
125
|
+
defaultModel = fallbackModel
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const planSyncedAt = Number(cfg?.planSyncedAt) || (isSynced ? (dynamic.planSyncedAt || 0) : 0)
|
|
99
129
|
|
|
100
130
|
return {
|
|
101
131
|
enabled: !!cfg?.enabled,
|
|
102
132
|
baseUrl: normalizeBaseUrl(cfg?.baseUrl),
|
|
103
133
|
apiKeyEnv: cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
104
|
-
defaultModel
|
|
134
|
+
defaultModel,
|
|
135
|
+
defaultModelWarning,
|
|
105
136
|
dynamicModels: dynamic,
|
|
137
|
+
planSynced: isSynced,
|
|
138
|
+
planSyncedAt,
|
|
106
139
|
disabledModels: disabledList,
|
|
107
140
|
enabledModels: activeIds,
|
|
108
141
|
timeoutMs: Number(cfg?.timeoutMs) || DEFAULT_TIMEOUT_MS,
|
package/lib/http.js
CHANGED
|
@@ -10,7 +10,20 @@ export function writeJson(res, code, body) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function readBody(req, maxBytes = 256 * 1024) {
|
|
13
|
+
export async function readBody(req, maxBytes = 256 * 1024) {
|
|
14
|
+
if (typeof req?.[Symbol.asyncIterator] === 'function') {
|
|
15
|
+
const chunks = []
|
|
16
|
+
let size = 0
|
|
17
|
+
for await (const chunk of req) {
|
|
18
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
19
|
+
size += buf.length
|
|
20
|
+
if (size > maxBytes) {
|
|
21
|
+
throw new Error('body too large')
|
|
22
|
+
}
|
|
23
|
+
chunks.push(buf)
|
|
24
|
+
}
|
|
25
|
+
return Buffer.concat(chunks)
|
|
26
|
+
}
|
|
14
27
|
return new Promise((resolve, reject) => {
|
|
15
28
|
const chunks = []
|
|
16
29
|
let size = 0
|
|
@@ -18,7 +31,7 @@ export function readBody(req, maxBytes = 256 * 1024) {
|
|
|
18
31
|
size += c.length
|
|
19
32
|
if (size > maxBytes) {
|
|
20
33
|
reject(new Error('body too large'))
|
|
21
|
-
req.destroy()
|
|
34
|
+
req.destroy?.()
|
|
22
35
|
return
|
|
23
36
|
}
|
|
24
37
|
chunks.push(c)
|