@goodandready/dsh-clinebot 0.5.7 → 0.5.9
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 +23 -0
- package/README.md +29 -16
- package/README.ru.md +29 -16
- package/README.zh.md +29 -16
- package/lib/account-pool.js +7 -5
- package/lib/client.js +189 -78
- package/lib/cline-client.js +46 -6
- package/lib/index.js +43 -32
- package/lib/models.js +35 -9
- package/lib/provider-sync.js +21 -8
- package/lib/routes/models.js +4 -4
- package/lib/routes/proxy.js +51 -12
- package/lib/slash-command.js +4 -3
- package/lib/updater.js +83 -30
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -24,6 +24,19 @@ function readConfigForms(ctx) {
|
|
|
24
24
|
try { return ctx.get('configForms') || undefined } catch { return undefined }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function useConfigFormsSnapshot(ctx) {
|
|
28
|
+
const scope = React.useMemo(() => {
|
|
29
|
+
try { return readConfigForms(ctx)?.get?.(NS) || undefined } catch { return undefined }
|
|
30
|
+
}, [ctx])
|
|
31
|
+
const subscribe = React.useMemo(() => (cb) => {
|
|
32
|
+
try { return scope?.subscribe ? (scope.subscribe(cb) || (() => {})) : () => {} } catch { return () => {} }
|
|
33
|
+
}, [scope])
|
|
34
|
+
const getSnapshot = React.useCallback(() => {
|
|
35
|
+
try { return scope?.getSnapshot?.() || SNAPSHOT_READY } catch { return SNAPSHOT_READY }
|
|
36
|
+
}, [scope])
|
|
37
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, () => SNAPSHOT_READY)?.status || 'loading'
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
// --- src/client/locales.js ---
|
|
28
41
|
const en = {
|
|
29
42
|
title: 'ClineBot',
|
|
@@ -79,6 +92,27 @@ const en = {
|
|
|
79
92
|
'quota.window_weekly': '📅 Weekly Window',
|
|
80
93
|
'quota.used': '{pct}% used',
|
|
81
94
|
'quota.reset_at': 'Reset: {time}',
|
|
95
|
+
'quota.window_monthly': '📅 Monthly Window',
|
|
96
|
+
'quota.countdown': 'Resets in {time}',
|
|
97
|
+
'quota.warning_low': 'Quota is below 10%. Requests may be throttled soon.',
|
|
98
|
+
'quota.exhausted_5h': '5-hour rolling limit is almost exhausted ({percentUsed}%). New requests may be rejected until quota reset.',
|
|
99
|
+
'quota.warning_5h': 'Low quota warning: {percentUsed}% of 5-hour limit consumed (<10% remaining).',
|
|
100
|
+
'quota.warning_weekly': 'Low weekly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
|
|
101
|
+
'quota.warning_monthly': 'Low monthly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
|
|
102
|
+
'models.reasoning_tag': '🧠 Reasoning',
|
|
103
|
+
'models.effort_low': 'Low',
|
|
104
|
+
'models.effort_medium': 'Medium',
|
|
105
|
+
'models.effort_high': 'High',
|
|
106
|
+
'models.effort_max': 'Max',
|
|
107
|
+
'models.caps_vision': 'Vision',
|
|
108
|
+
'models.caps_general': 'General',
|
|
109
|
+
'models.caps_coding': 'Coding',
|
|
110
|
+
'diag.smoke_result': '✅ Latency: {latency} ms | Model: {model}\nResponse: {preview}',
|
|
111
|
+
'diag.empty': '(empty)',
|
|
112
|
+
'ui.error_title': '⚠️ ClineBot UI Error: ',
|
|
113
|
+
'ui.retry': 'Retry',
|
|
114
|
+
'update.check_failed': 'Failed to check for updates',
|
|
115
|
+
'update.retry': 'Retry check',
|
|
82
116
|
'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
|
|
83
117
|
'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker. Thinking effort is selectable in the composer model menu.',
|
|
84
118
|
'models.reasoning_tooltip': 'Supports thinking effort selection in composer model menu (Off / Low / Medium / High / Max)',
|
|
@@ -206,6 +240,27 @@ const en = {
|
|
|
206
240
|
'quota.window_weekly': '📅 每周滚动窗口',
|
|
207
241
|
'quota.used': '已使用 {pct}%',
|
|
208
242
|
'quota.reset_at': '重置时间:{time}',
|
|
243
|
+
'quota.window_monthly': '📅 每月滚动窗口',
|
|
244
|
+
'quota.countdown': '{time}后重置',
|
|
245
|
+
'quota.warning_low': '额度低于 10%,请求可能很快会受到限制。',
|
|
246
|
+
'quota.exhausted_5h': '5 小时滚动限额已接近耗尽(已用 {percentUsed}%)。在额度重置前新请求可能会被拒绝。',
|
|
247
|
+
'quota.warning_5h': '低额度警告:5 小时限额已消耗 {percentUsed}%(剩余不足 10%)。',
|
|
248
|
+
'quota.warning_weekly': '周限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
249
|
+
'quota.warning_monthly': '月限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
250
|
+
'models.reasoning_tag': '🧠 深度思考',
|
|
251
|
+
'models.effort_low': '低',
|
|
252
|
+
'models.effort_medium': '中',
|
|
253
|
+
'models.effort_high': '高',
|
|
254
|
+
'models.effort_max': '最高',
|
|
255
|
+
'models.caps_vision': '视觉',
|
|
256
|
+
'models.caps_general': '通用',
|
|
257
|
+
'models.caps_coding': '编程',
|
|
258
|
+
'diag.smoke_result': '✅ 延迟:{latency} ms | 模型:{model}\n响应:{preview}',
|
|
259
|
+
'diag.empty': '(空)',
|
|
260
|
+
'ui.error_title': '⚠️ ClineBot 界面错误:',
|
|
261
|
+
'ui.retry': '重试',
|
|
262
|
+
'update.check_failed': '检查更新失败',
|
|
263
|
+
'update.retry': '重试检查',
|
|
209
264
|
'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
|
|
210
265
|
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
|
|
211
266
|
'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
|
|
@@ -399,10 +454,13 @@ function createErrorBoundary() {
|
|
|
399
454
|
}
|
|
400
455
|
render() {
|
|
401
456
|
if (this.state.hasError) {
|
|
457
|
+
const t = this.props && typeof this.props.t === 'function' ? this.props.t : null
|
|
458
|
+
const errorTitle = t ? t('ui.error_title') : '⚠️ ClineBot UI Error: '
|
|
459
|
+
const retryLabel = t ? t('ui.retry') : 'Retry'
|
|
402
460
|
return React.createElement(
|
|
403
461
|
'div',
|
|
404
462
|
{ className: 'cb-alert-err' },
|
|
405
|
-
React.createElement('strong', null,
|
|
463
|
+
React.createElement('strong', null, errorTitle),
|
|
406
464
|
String(this.state.error?.message || this.state.error || 'Unknown rendering error'),
|
|
407
465
|
React.createElement(
|
|
408
466
|
'button',
|
|
@@ -412,7 +470,7 @@ function createErrorBoundary() {
|
|
|
412
470
|
style: { marginLeft: '12px', padding: '2px 8px', fontSize: '11px' },
|
|
413
471
|
onClick: () => this.setState({ hasError: false, error: null }),
|
|
414
472
|
},
|
|
415
|
-
|
|
473
|
+
retryLabel
|
|
416
474
|
)
|
|
417
475
|
)
|
|
418
476
|
}
|
|
@@ -537,7 +595,22 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
|
537
595
|
}
|
|
538
596
|
|
|
539
597
|
// --- src/client/components/update-banner.js ---
|
|
540
|
-
function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
598
|
+
function UpdateBanner({ updateState, handleTriggerUpdate, handleCheckUpdate, t }) {
|
|
599
|
+
const status = updateState.status || (
|
|
600
|
+
updateState.checking
|
|
601
|
+
? 'checking'
|
|
602
|
+
: (updateState.latestCheckFailed
|
|
603
|
+
? 'error'
|
|
604
|
+
: (updateState.updateAvailable
|
|
605
|
+
? 'available'
|
|
606
|
+
: (updateState.lastCheckedAt ? 'current' : 'idle')))
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
const isChecking = status === 'checking' || updateState.checking
|
|
610
|
+
const isAvailable = status === 'available' || updateState.updateAvailable
|
|
611
|
+
const isCurrent = status === 'current'
|
|
612
|
+
const isError = status === 'error' || updateState.latestCheckFailed
|
|
613
|
+
|
|
541
614
|
return React.createElement(
|
|
542
615
|
React.Fragment,
|
|
543
616
|
null,
|
|
@@ -561,19 +634,25 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
|
561
634
|
`v${updateState.currentVersion}`
|
|
562
635
|
)
|
|
563
636
|
: null,
|
|
564
|
-
|
|
637
|
+
isChecking
|
|
565
638
|
? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px' } },
|
|
566
639
|
t('update.checking')
|
|
567
640
|
)
|
|
568
|
-
:
|
|
641
|
+
: isAvailable
|
|
569
642
|
? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
570
643
|
t('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
|
|
571
644
|
)
|
|
572
|
-
:
|
|
573
|
-
|
|
574
|
-
|
|
645
|
+
: isCurrent
|
|
646
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
647
|
+
'✓ ' + t('update.up_to_date')
|
|
648
|
+
)
|
|
649
|
+
: isError
|
|
650
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-bad' },
|
|
651
|
+
t('update.check_failed')
|
|
652
|
+
)
|
|
653
|
+
: null
|
|
575
654
|
),
|
|
576
|
-
|
|
655
|
+
isAvailable
|
|
577
656
|
? React.createElement(
|
|
578
657
|
'button',
|
|
579
658
|
{
|
|
@@ -585,7 +664,19 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
|
585
664
|
},
|
|
586
665
|
updateState.updating ? t('update.updating') : t('update.btn')
|
|
587
666
|
)
|
|
588
|
-
:
|
|
667
|
+
: isError && handleCheckUpdate
|
|
668
|
+
? React.createElement(
|
|
669
|
+
'button',
|
|
670
|
+
{
|
|
671
|
+
type: 'button',
|
|
672
|
+
className: 'cb-btn',
|
|
673
|
+
disabled: isChecking,
|
|
674
|
+
onClick: handleCheckUpdate,
|
|
675
|
+
style: { padding: '5px 12px', fontSize: '12px' }
|
|
676
|
+
},
|
|
677
|
+
t('update.retry')
|
|
678
|
+
)
|
|
679
|
+
: null
|
|
589
680
|
),
|
|
590
681
|
updateState.notice ? React.createElement('div', { className: 'cb-alert-ok' }, '✓ ' + updateState.notice) : null,
|
|
591
682
|
updateState.error ? React.createElement('div', { className: 'cb-alert-err' }, updateState.error) : null
|
|
@@ -936,7 +1027,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
|
|
|
936
1027
|
React.createElement(
|
|
937
1028
|
'div',
|
|
938
1029
|
{ style: { flex: 1 } },
|
|
939
|
-
React.createElement('div', null, status.quotaWarning.message),
|
|
1030
|
+
React.createElement('div', null, (status.quotaWarning.code && t(status.quotaWarning.code, status.quotaWarning.params)) || status.quotaWarning.message),
|
|
940
1031
|
status.quotaWarning.resetsAt
|
|
941
1032
|
? React.createElement(
|
|
942
1033
|
'div',
|
|
@@ -1552,19 +1643,20 @@ function ModelsSection({
|
|
|
1552
1643
|
m.category === 'reasoning'
|
|
1553
1644
|
)
|
|
1554
1645
|
const category = m.category && m.category !== 'reasoning' ? m.category : (hasReasoning ? null : 'general')
|
|
1646
|
+
const categoryLabel = category === 'general' ? t('models.caps_general') : (category === 'coding' ? t('models.caps_coding') : category)
|
|
1555
1647
|
const hasVision = Boolean(m.input?.includes('image') || m.input?.includes('vision'))
|
|
1556
1648
|
return React.createElement(
|
|
1557
1649
|
React.Fragment,
|
|
1558
1650
|
null,
|
|
1559
|
-
category ? React.createElement('span', { className: 'cb-badge' },
|
|
1651
|
+
category ? React.createElement('span', { className: 'cb-badge' }, categoryLabel) : null,
|
|
1560
1652
|
hasVision
|
|
1561
|
-
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, '
|
|
1653
|
+
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, t('models.caps_vision'))
|
|
1562
1654
|
: null,
|
|
1563
1655
|
hasReasoning
|
|
1564
1656
|
? React.createElement(
|
|
1565
1657
|
React.Fragment,
|
|
1566
1658
|
null,
|
|
1567
|
-
React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: (category || hasVision) ? '4px' : '0' }, title: t('models.reasoning_tooltip') }, '
|
|
1659
|
+
React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: (category || hasVision) ? '4px' : '0' }, title: t('models.reasoning_tooltip') }, t('models.reasoning_tag')),
|
|
1568
1660
|
React.createElement(
|
|
1569
1661
|
'select',
|
|
1570
1662
|
{
|
|
@@ -1579,10 +1671,10 @@ function ModelsSection({
|
|
|
1579
1671
|
},
|
|
1580
1672
|
},
|
|
1581
1673
|
React.createElement('option', { value: '' }, t('models.effort_auto')),
|
|
1582
|
-
React.createElement('option', { value: 'low' }, '
|
|
1583
|
-
React.createElement('option', { value: 'medium' }, '
|
|
1584
|
-
React.createElement('option', { value: 'high' }, '
|
|
1585
|
-
React.createElement('option', { value: 'max' }, '
|
|
1674
|
+
React.createElement('option', { value: 'low' }, t('models.effort_low')),
|
|
1675
|
+
React.createElement('option', { value: 'medium' }, t('models.effort_medium')),
|
|
1676
|
+
React.createElement('option', { value: 'high' }, t('models.effort_high')),
|
|
1677
|
+
React.createElement('option', { value: 'max' }, t('models.effort_max'))
|
|
1586
1678
|
)
|
|
1587
1679
|
)
|
|
1588
1680
|
: null
|
|
@@ -1609,10 +1701,19 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
1609
1701
|
const [confirmReset, setConfirmReset] = React.useState(false)
|
|
1610
1702
|
|
|
1611
1703
|
const s = status?.statsSummary || {}
|
|
1704
|
+
const totals = s.totals || {}
|
|
1612
1705
|
const sess = status?.sessionStats || {}
|
|
1613
1706
|
|
|
1707
|
+
const requests = typeof totals.requests === 'number' ? totals.requests : (typeof s.requests === 'number' ? s.requests : 0)
|
|
1708
|
+
const totalTokens = typeof totals.totalTokens === 'number' ? totals.totalTokens : (typeof s.totalTokens === 'number' ? s.totalTokens : 0)
|
|
1709
|
+
const errors429 = typeof totals.rateLimited429 === 'number' ? totals.rateLimited429 : (typeof s.errors429 === 'number' ? s.errors429 : (typeof totals.failed === 'number' ? totals.failed : 0))
|
|
1710
|
+
|
|
1614
1711
|
const topModels = Object.entries(s.byModel || {})
|
|
1615
|
-
.sort((a, b) =>
|
|
1712
|
+
.sort((a, b) => {
|
|
1713
|
+
const bTokens = Number(b[1]?.totalTokens ?? b[1]?.tokens ?? 0)
|
|
1714
|
+
const aTokens = Number(a[1]?.totalTokens ?? a[1]?.tokens ?? 0)
|
|
1715
|
+
return bTokens - aTokens
|
|
1716
|
+
})
|
|
1616
1717
|
.slice(0, 5)
|
|
1617
1718
|
|
|
1618
1719
|
return React.createElement(
|
|
@@ -1674,19 +1775,19 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
1674
1775
|
React.createElement(
|
|
1675
1776
|
'div',
|
|
1676
1777
|
{ className: 'cb-stat-box' },
|
|
1677
|
-
React.createElement('div', { className: 'cb-stat-val' }, `${
|
|
1778
|
+
React.createElement('div', { className: 'cb-stat-val' }, `${requests} (${sess.successfulRequests || 0} sess)`),
|
|
1678
1779
|
React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.requests'))
|
|
1679
1780
|
),
|
|
1680
1781
|
React.createElement(
|
|
1681
1782
|
'div',
|
|
1682
1783
|
{ className: 'cb-stat-box' },
|
|
1683
|
-
React.createElement('div', { className: 'cb-stat-val' }, `${Number(
|
|
1784
|
+
React.createElement('div', { className: 'cb-stat-val' }, `${Number(totalTokens).toLocaleString()}`),
|
|
1684
1785
|
React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.tokens'))
|
|
1685
1786
|
),
|
|
1686
1787
|
React.createElement(
|
|
1687
1788
|
'div',
|
|
1688
1789
|
{ className: 'cb-stat-box' },
|
|
1689
|
-
React.createElement('div', { className: 'cb-stat-val' }, `${
|
|
1790
|
+
React.createElement('div', { className: 'cb-stat-val' }, `${errors429}`),
|
|
1690
1791
|
React.createElement('div', { className: 'cb-stat-lbl' }, t('stats.errors429'))
|
|
1691
1792
|
),
|
|
1692
1793
|
React.createElement(
|
|
@@ -1719,7 +1820,7 @@ function StatsSection({ status, onResetStats, busy, t }) {
|
|
|
1719
1820
|
},
|
|
1720
1821
|
},
|
|
1721
1822
|
React.createElement('span', { style: { fontFamily: 'monospace' } }, modId),
|
|
1722
|
-
React.createElement('span', null, `${data.requests || 0} reqs · ${Number(data.tokens
|
|
1823
|
+
React.createElement('span', null, `${data.requests || 0} reqs · ${Number(data.totalTokens ?? data.tokens ?? 0).toLocaleString()} tok`)
|
|
1723
1824
|
)
|
|
1724
1825
|
)
|
|
1725
1826
|
)
|
|
@@ -1776,27 +1877,17 @@ function DiagSection({ keyPresent, isRegistered, busy, smokeResult, handleSmoke,
|
|
|
1776
1877
|
? React.createElement(
|
|
1777
1878
|
'div',
|
|
1778
1879
|
{ className: 'cb-preview' },
|
|
1779
|
-
|
|
1780
|
-
|
|
1880
|
+
t('diag.smoke_result', {
|
|
1881
|
+
latency: smokeResult.latencyMs,
|
|
1882
|
+
model: smokeResult.model,
|
|
1883
|
+
preview: smokeResult.preview || t('diag.empty'),
|
|
1884
|
+
})
|
|
1781
1885
|
)
|
|
1782
1886
|
: null
|
|
1783
1887
|
)
|
|
1784
1888
|
}
|
|
1785
1889
|
|
|
1786
1890
|
// --- src/client/settings-page.js ---
|
|
1787
|
-
function useConfigFormsSnapshot(ctx) {
|
|
1788
|
-
const scope = React.useMemo(() => {
|
|
1789
|
-
try { return readConfigForms(ctx)?.get?.(NS) || undefined } catch { return undefined }
|
|
1790
|
-
}, [ctx])
|
|
1791
|
-
const subscribe = React.useMemo(() => (cb) => {
|
|
1792
|
-
try { return scope?.subscribe ? (scope.subscribe(cb) || (() => {})) : () => {} } catch { return () => {} }
|
|
1793
|
-
}, [scope])
|
|
1794
|
-
const getSnapshot = React.useCallback(() => {
|
|
1795
|
-
try { return scope?.getSnapshot?.() || SNAPSHOT_READY } catch { return SNAPSHOT_READY }
|
|
1796
|
-
}, [scope])
|
|
1797
|
-
return React.useSyncExternalStore(subscribe, getSnapshot, () => SNAPSHOT_READY)?.status || 'loading'
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
1891
|
function SettingsPage(props) {
|
|
1801
1892
|
const ctx = props?.ctx
|
|
1802
1893
|
const t = props?.t || makeT(en, en)
|
|
@@ -1810,8 +1901,9 @@ function SettingsPage(props) {
|
|
|
1810
1901
|
const [smokeResult, setSmokeResult] = React.useState(null)
|
|
1811
1902
|
|
|
1812
1903
|
const [updateState, setUpdateState] = React.useState({
|
|
1813
|
-
checking: false, updating: false, currentVersion: '', latestVersion: '',
|
|
1814
|
-
updateAvailable: false, canAutoUpdate: true,
|
|
1904
|
+
status: 'idle', checking: false, updating: false, currentVersion: '', latestVersion: '',
|
|
1905
|
+
updateAvailable: false, canAutoUpdate: true, latestCheckFailed: false, lastCheckedAt: null,
|
|
1906
|
+
error: '', notice: '',
|
|
1815
1907
|
})
|
|
1816
1908
|
|
|
1817
1909
|
const [apiKeyInput, setApiKeyInput] = React.useState('')
|
|
@@ -1827,8 +1919,9 @@ function SettingsPage(props) {
|
|
|
1827
1919
|
])
|
|
1828
1920
|
const data = await res.json().catch(() => ({}))
|
|
1829
1921
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1922
|
+
const rawStats = statsRes && statsRes.ok ? await statsRes.json().catch(() => null) : null
|
|
1923
|
+
const statsObj = (rawStats && typeof rawStats === 'object' && rawStats.stats) ? rawStats.stats : (rawStats || data.statsSummary || {})
|
|
1924
|
+
setStatus({ ...data, statsSummary: statsObj })
|
|
1832
1925
|
setDraft(data.config || {})
|
|
1833
1926
|
}, [])
|
|
1834
1927
|
|
|
@@ -1837,23 +1930,45 @@ function SettingsPage(props) {
|
|
|
1837
1930
|
}, [load])
|
|
1838
1931
|
|
|
1839
1932
|
const checkUpdate = React.useCallback(async () => {
|
|
1840
|
-
setUpdateState((s) => ({ ...s, checking: true, error: '' }))
|
|
1933
|
+
setUpdateState((s) => ({ ...s, checking: true, status: 'checking', error: '' }))
|
|
1841
1934
|
try {
|
|
1842
1935
|
const res = await fetch(`${ROUTE_PREFIX}/update`)
|
|
1843
1936
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
1844
1937
|
const data = await res.json().catch(() => ({}))
|
|
1938
|
+
if (data.latestCheckFailed) {
|
|
1939
|
+
setUpdateState((s) => ({
|
|
1940
|
+
...s,
|
|
1941
|
+
checking: false,
|
|
1942
|
+
status: 'error',
|
|
1943
|
+
latestCheckFailed: true,
|
|
1944
|
+
currentVersion: data.currentVersion || s.currentVersion,
|
|
1945
|
+
error: t('update.check_failed'),
|
|
1946
|
+
}))
|
|
1947
|
+
return
|
|
1948
|
+
}
|
|
1949
|
+
const isAvailable = Boolean(data.updateAvailable)
|
|
1845
1950
|
setUpdateState((s) => ({
|
|
1846
1951
|
...s,
|
|
1847
1952
|
checking: false,
|
|
1953
|
+
status: isAvailable ? 'available' : 'current',
|
|
1848
1954
|
currentVersion: data.currentVersion || s.currentVersion,
|
|
1849
1955
|
latestVersion: data.latestVersion || '',
|
|
1850
|
-
updateAvailable:
|
|
1956
|
+
updateAvailable: isAvailable,
|
|
1957
|
+
latestCheckFailed: false,
|
|
1958
|
+
lastCheckedAt: Date.now(),
|
|
1851
1959
|
canAutoUpdate: data.canAutoUpdate !== false,
|
|
1960
|
+
error: '',
|
|
1961
|
+
}))
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
setUpdateState((s) => ({
|
|
1964
|
+
...s,
|
|
1965
|
+
checking: false,
|
|
1966
|
+
status: 'error',
|
|
1967
|
+
latestCheckFailed: true,
|
|
1968
|
+
error: String(err?.message || err || t('update.check_failed')),
|
|
1852
1969
|
}))
|
|
1853
|
-
} catch (_) {
|
|
1854
|
-
setUpdateState((s) => ({ ...s, checking: false }))
|
|
1855
1970
|
}
|
|
1856
|
-
}, [])
|
|
1971
|
+
}, [t])
|
|
1857
1972
|
|
|
1858
1973
|
React.useEffect(() => { checkUpdate() }, [checkUpdate])
|
|
1859
1974
|
|
|
@@ -1871,13 +1986,20 @@ function SettingsPage(props) {
|
|
|
1871
1986
|
setUpdateState((s) => ({
|
|
1872
1987
|
...s,
|
|
1873
1988
|
updating: false,
|
|
1989
|
+
status: 'current',
|
|
1874
1990
|
updateAvailable: false,
|
|
1875
1991
|
currentVersion: newVer,
|
|
1992
|
+
lastCheckedAt: Date.now(),
|
|
1876
1993
|
notice: t('update.done', { version: newVer }),
|
|
1877
1994
|
}))
|
|
1878
1995
|
setTimeout(() => checkUpdate(), 2000)
|
|
1879
1996
|
} catch (err) {
|
|
1880
|
-
setUpdateState((s) => ({
|
|
1997
|
+
setUpdateState((s) => ({
|
|
1998
|
+
...s,
|
|
1999
|
+
updating: false,
|
|
2000
|
+
status: s.updateAvailable ? 'available' : 'error',
|
|
2001
|
+
error: t('update.failed', { error: String(err?.message || err) }),
|
|
2002
|
+
}))
|
|
1881
2003
|
}
|
|
1882
2004
|
}
|
|
1883
2005
|
|
|
@@ -1899,8 +2021,7 @@ function SettingsPage(props) {
|
|
|
1899
2021
|
if (!keyVal) { setErr(t('key.empty_err')); return }
|
|
1900
2022
|
await performAction('save-key', async () => {
|
|
1901
2023
|
const res = await fetch(`${ROUTE_PREFIX}/save-key`, {
|
|
1902
|
-
method: 'POST',
|
|
1903
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2024
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1904
2025
|
body: JSON.stringify({ apiKey: keyVal, apiKeyEnv: draft?.apiKeyEnv }),
|
|
1905
2026
|
})
|
|
1906
2027
|
const data = await res.json().catch(() => ({}))
|
|
@@ -1924,8 +2045,7 @@ function SettingsPage(props) {
|
|
|
1924
2045
|
async function handleUpdateModelContext(payload) {
|
|
1925
2046
|
await performAction('update-context', async () => {
|
|
1926
2047
|
const res = await fetch(`${ROUTE_PREFIX}/models/context`, {
|
|
1927
|
-
method: 'POST',
|
|
1928
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2048
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1929
2049
|
body: JSON.stringify(payload),
|
|
1930
2050
|
})
|
|
1931
2051
|
const data = await res.json().catch(() => ({}))
|
|
@@ -1994,8 +2114,7 @@ function SettingsPage(props) {
|
|
|
1994
2114
|
setVerifyStatus({ state: 'verifying', email: '', plan: '', error: '' })
|
|
1995
2115
|
try {
|
|
1996
2116
|
const res = await fetch(`${ROUTE_PREFIX}/key/verify`, {
|
|
1997
|
-
method: 'POST',
|
|
1998
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2117
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1999
2118
|
body: JSON.stringify({ key: raw }),
|
|
2000
2119
|
})
|
|
2001
2120
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2020,8 +2139,7 @@ function SettingsPage(props) {
|
|
|
2020
2139
|
async function handlePinAccount(accountEnv) {
|
|
2021
2140
|
await performAction('pin-account', async () => {
|
|
2022
2141
|
const res = await fetch(`${ROUTE_PREFIX}/accounts/active`, {
|
|
2023
|
-
method: 'POST',
|
|
2024
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2142
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2025
2143
|
body: JSON.stringify({ account: accountEnv }),
|
|
2026
2144
|
})
|
|
2027
2145
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2033,8 +2151,7 @@ function SettingsPage(props) {
|
|
|
2033
2151
|
async function handleAddAccount({ label, apiKeyEnv, apiKey }) {
|
|
2034
2152
|
await performAction('add-account', async () => {
|
|
2035
2153
|
const res = await fetch(`${ROUTE_PREFIX}/accounts`, {
|
|
2036
|
-
method: 'POST',
|
|
2037
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2154
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2038
2155
|
body: JSON.stringify({ label, apiKeyEnv, apiKey }),
|
|
2039
2156
|
})
|
|
2040
2157
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2047,8 +2164,7 @@ function SettingsPage(props) {
|
|
|
2047
2164
|
async function handleDeleteAccount(accountEnv, deleteSecret = true) {
|
|
2048
2165
|
await performAction('delete-account', async () => {
|
|
2049
2166
|
const res = await fetch(`${ROUTE_PREFIX}/accounts/delete`, {
|
|
2050
|
-
method: 'POST',
|
|
2051
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2167
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2052
2168
|
body: JSON.stringify({ apiKeyEnv: accountEnv, deleteSecret }),
|
|
2053
2169
|
})
|
|
2054
2170
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2058,14 +2174,14 @@ function SettingsPage(props) {
|
|
|
2058
2174
|
}
|
|
2059
2175
|
|
|
2060
2176
|
async function handleConfigPatch(patch) {
|
|
2177
|
+
setDraft((d) => ({ ...(d || {}), ...(patch || {}) }))
|
|
2061
2178
|
await performAction('patch-config', async () => {
|
|
2062
2179
|
const res = await fetch(`${ROUTE_PREFIX}/config`, {
|
|
2063
|
-
method: '
|
|
2064
|
-
|
|
2065
|
-
body: JSON.stringify(patch),
|
|
2180
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
2181
|
+
body: JSON.stringify({ config: patch }),
|
|
2066
2182
|
})
|
|
2067
2183
|
const data = await res.json().catch(() => ({}))
|
|
2068
|
-
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
2184
|
+
if (!res.ok || data.ok === false) throw new Error(data.error || `HTTP ${res.status}`)
|
|
2069
2185
|
await load()
|
|
2070
2186
|
})
|
|
2071
2187
|
}
|
|
@@ -2125,15 +2241,10 @@ function SettingsPage(props) {
|
|
|
2125
2241
|
function handleSetModelsFilter(type) {
|
|
2126
2242
|
const all = status?.availableModels || []
|
|
2127
2243
|
let allowed = new Set()
|
|
2128
|
-
if (type === 'all')
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
} else if (type === 'coding') {
|
|
2133
|
-
allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
|
|
2134
|
-
} else if (type === 'recommended') {
|
|
2135
|
-
allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
|
|
2136
|
-
}
|
|
2244
|
+
if (type === 'all') allowed = new Set(all.map((m) => m.id))
|
|
2245
|
+
else if (type === 'vision') allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
|
|
2246
|
+
else if (type === 'coding') allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
|
|
2247
|
+
else if (type === 'recommended') allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
|
|
2137
2248
|
|
|
2138
2249
|
const curr = draft?.disabledModels || []
|
|
2139
2250
|
const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
|
|
@@ -2194,7 +2305,7 @@ function SettingsPage(props) {
|
|
|
2194
2305
|
),
|
|
2195
2306
|
|
|
2196
2307
|
// In-app Update Bar
|
|
2197
|
-
React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, t }),
|
|
2308
|
+
React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, handleCheckUpdate: checkUpdate, t }),
|
|
2198
2309
|
|
|
2199
2310
|
// Settings host status warning
|
|
2200
2311
|
snapshotStatus === 'unavailable'
|
|
@@ -2327,7 +2438,7 @@ function PluginCard(props) {
|
|
|
2327
2438
|
{ style: { marginTop: '16px' } },
|
|
2328
2439
|
React.createElement(
|
|
2329
2440
|
ErrorBoundary,
|
|
2330
|
-
|
|
2441
|
+
{ t },
|
|
2331
2442
|
React.createElement(SettingsPage, { ...props, ctx: props && props.ctx })
|
|
2332
2443
|
)
|
|
2333
2444
|
)
|
|
@@ -2412,7 +2523,7 @@ function apply(ctx) {
|
|
|
2412
2523
|
locale: NS,
|
|
2413
2524
|
inject: () => ({ ctx }),
|
|
2414
2525
|
},
|
|
2415
|
-
(props) => React.createElement(ErrorBoundary,
|
|
2526
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2416
2527
|
)
|
|
2417
2528
|
)
|
|
2418
2529
|
|
|
@@ -2429,7 +2540,7 @@ function apply(ctx) {
|
|
|
2429
2540
|
locale: NS,
|
|
2430
2541
|
inject: () => ({ ctx }),
|
|
2431
2542
|
},
|
|
2432
|
-
(props) => React.createElement(ErrorBoundary,
|
|
2543
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2433
2544
|
)
|
|
2434
2545
|
)
|
|
2435
2546
|
}
|
|
@@ -2442,7 +2553,7 @@ function apply(ctx) {
|
|
|
2442
2553
|
locale: NS,
|
|
2443
2554
|
inject: () => ({ ctx }),
|
|
2444
2555
|
},
|
|
2445
|
-
(props) => React.createElement(ErrorBoundary,
|
|
2556
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2446
2557
|
)
|
|
2447
2558
|
)
|
|
2448
2559
|
}
|
package/lib/cline-client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
1
2
|
export {
|
|
2
3
|
toCredentialRef,
|
|
3
4
|
resolveApiKey,
|
|
@@ -87,8 +88,32 @@ export function clearProbeCache() {
|
|
|
87
88
|
probeCache.clear()
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Build an opaque, collision-free cache key from normalized baseUrl and full apiKey.
|
|
93
|
+
* Never leaks the raw apiKey or fingerprint into UI/logs.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} baseUrl
|
|
96
|
+
* @param {string} apiKey
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function buildUsageCacheKey(baseUrl = '', apiKey = '') {
|
|
100
|
+
const normBase = normalizeBaseUrl(baseUrl)
|
|
101
|
+
const normKey = String(apiKey || '').trim()
|
|
102
|
+
if (!normKey) return ''
|
|
103
|
+
const hash = createHash('sha256')
|
|
104
|
+
.update(`${normBase}\0${normKey}`)
|
|
105
|
+
.digest('hex')
|
|
106
|
+
.slice(0, 32)
|
|
107
|
+
return `cline:usage:${hash}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function clearUsageCache(baseUrl, apiKey) {
|
|
111
|
+
if (baseUrl && apiKey) {
|
|
112
|
+
const key = buildUsageCacheKey(baseUrl, apiKey)
|
|
113
|
+
usageCache.delete(key)
|
|
114
|
+
} else {
|
|
115
|
+
usageCache.clear()
|
|
116
|
+
}
|
|
92
117
|
}
|
|
93
118
|
|
|
94
119
|
/**
|
|
@@ -106,6 +131,17 @@ export async function retryWithBackoff(fn, {
|
|
|
106
131
|
const res = await fn()
|
|
107
132
|
if (res && typeof res.status === 'number' && [429, 502, 503, 504].includes(res.status) && attempt < maxRetries) {
|
|
108
133
|
attempt++
|
|
134
|
+
if (res.body) {
|
|
135
|
+
try {
|
|
136
|
+
if (typeof res.body.cancel === 'function') {
|
|
137
|
+
res.body.cancel().catch(() => {})
|
|
138
|
+
} else if (typeof res.body.destroy === 'function') {
|
|
139
|
+
res.body.destroy()
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
/* ignore cancel error */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
109
145
|
const retryAfter = res.headers?.get ? Number(res.headers.get('retry-after')) * 1000 : 0
|
|
110
146
|
const waitTime = retryAfter > 0 ? Math.min(retryAfter, maxDelayMs) : delay
|
|
111
147
|
await new Promise((r) => setTimeout(r, waitTime))
|
|
@@ -140,7 +176,7 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
|
|
|
140
176
|
return { ok: false, error: 'API key is missing' }
|
|
141
177
|
}
|
|
142
178
|
|
|
143
|
-
const cacheKey =
|
|
179
|
+
const cacheKey = buildUsageCacheKey(base, apiKey)
|
|
144
180
|
const now = Date.now()
|
|
145
181
|
|
|
146
182
|
const doFetch = async () => {
|
|
@@ -450,15 +486,20 @@ export function buildPiAiProvider({
|
|
|
450
486
|
baseUrl = DEFAULT_BASE_URL,
|
|
451
487
|
apiKeyEnv = DEFAULT_API_KEY_ENV,
|
|
452
488
|
apiKey,
|
|
489
|
+
dynamicModels = [],
|
|
490
|
+
modelContextOverrides = [],
|
|
453
491
|
models = [],
|
|
454
492
|
customModels = [],
|
|
455
493
|
displayName = PROVIDER_DISPLAY_NAME,
|
|
456
494
|
} = {}) {
|
|
457
|
-
const allAvailable = getAllModels(customModels)
|
|
495
|
+
const allAvailable = getAllModels(dynamicModels, modelContextOverrides, customModels)
|
|
496
|
+
const customModelsMap = new Map((Array.isArray(customModels) ? customModels : []).filter(Boolean).map(cm => [cm.id, cm]))
|
|
458
497
|
const modelList = (Array.isArray(models) && models.length ? models : allAvailable).map((m) => {
|
|
459
498
|
let item
|
|
460
499
|
if (typeof m === 'string') {
|
|
461
|
-
|
|
500
|
+
const customMatch = customModelsMap.get(m)
|
|
501
|
+
const baseMatch = allAvailable.find((x) => x.id === m)
|
|
502
|
+
item = customMatch ? { ...baseMatch, ...customMatch } : (baseMatch || { id: m, name: m })
|
|
462
503
|
} else {
|
|
463
504
|
item = m
|
|
464
505
|
}
|
|
@@ -507,7 +548,6 @@ export function buildPiAiProvider({
|
|
|
507
548
|
api: 'openai-completions',
|
|
508
549
|
baseURL: normalizeBaseUrl(baseUrl),
|
|
509
550
|
apiKeyEnv: apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
510
|
-
...(apiKey ? { apiKey } : {}),
|
|
511
551
|
compat: {
|
|
512
552
|
supportsReasoningEffort: true,
|
|
513
553
|
sendSessionAffinityHeaders: true,
|