@goodandready/dsh-clinebot 0.5.8 → 0.5.10
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 +26 -0
- package/README.md +46 -21
- package/README.ru.md +49 -22
- package/README.zh.md +51 -24
- package/lib/account-pool.js +5 -3
- package/lib/client.js +175 -70
- package/lib/cline-client.js +4 -7
- package/lib/index.js +73 -13
- package/lib/models.js +26 -5
- package/lib/provider-sync.js +45 -24
- package/lib/routes/accounts.js +22 -10
- package/lib/routes/proxy.js +54 -4
- package/lib/session-router.js +5 -5
- package/lib/updater.js +96 -34
- 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,28 @@ 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.resetting': 'Resetting…',
|
|
98
|
+
'quota.warning_low': 'Quota is below 10%. Requests may be throttled soon.',
|
|
99
|
+
'quota.exhausted_5h': '5-hour rolling limit is almost exhausted ({percentUsed}%). New requests may be rejected until quota reset.',
|
|
100
|
+
'quota.warning_5h': 'Low quota warning: {percentUsed}% of 5-hour limit consumed (<10% remaining).',
|
|
101
|
+
'quota.warning_weekly': 'Low weekly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
|
|
102
|
+
'quota.warning_monthly': 'Low monthly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
|
|
103
|
+
'models.reasoning_tag': '🧠 Reasoning',
|
|
104
|
+
'models.effort_low': 'Low',
|
|
105
|
+
'models.effort_medium': 'Medium',
|
|
106
|
+
'models.effort_high': 'High',
|
|
107
|
+
'models.effort_max': 'Max',
|
|
108
|
+
'models.caps_vision': 'Vision',
|
|
109
|
+
'models.caps_general': 'General',
|
|
110
|
+
'models.caps_coding': 'Coding',
|
|
111
|
+
'diag.smoke_result': '✅ Latency: {latency} ms | Model: {model}\nResponse: {preview}',
|
|
112
|
+
'diag.empty': '(empty)',
|
|
113
|
+
'ui.error_title': '⚠️ ClineBot UI Error: ',
|
|
114
|
+
'ui.retry': 'Retry',
|
|
115
|
+
'update.check_failed': 'Failed to check for updates',
|
|
116
|
+
'update.retry': 'Retry check',
|
|
82
117
|
'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
|
|
83
118
|
'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
119
|
'models.reasoning_tooltip': 'Supports thinking effort selection in composer model menu (Off / Low / Medium / High / Max)',
|
|
@@ -206,6 +241,28 @@ const en = {
|
|
|
206
241
|
'quota.window_weekly': '📅 每周滚动窗口',
|
|
207
242
|
'quota.used': '已使用 {pct}%',
|
|
208
243
|
'quota.reset_at': '重置时间:{time}',
|
|
244
|
+
'quota.window_monthly': '📅 每月滚动窗口',
|
|
245
|
+
'quota.countdown': '{time}后重置',
|
|
246
|
+
'quota.resetting': '正在重置',
|
|
247
|
+
'quota.warning_low': '额度低于 10%,请求可能很快会受到限制。',
|
|
248
|
+
'quota.exhausted_5h': '5 小时滚动限额已接近耗尽(已用 {percentUsed}%)。在额度重置前新请求可能会被拒绝。',
|
|
249
|
+
'quota.warning_5h': '低额度警告:5 小时限额已消耗 {percentUsed}%(剩余不足 10%)。',
|
|
250
|
+
'quota.warning_weekly': '周限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
251
|
+
'quota.warning_monthly': '月限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
252
|
+
'models.reasoning_tag': '🧠 深度思考',
|
|
253
|
+
'models.effort_low': '低',
|
|
254
|
+
'models.effort_medium': '中',
|
|
255
|
+
'models.effort_high': '高',
|
|
256
|
+
'models.effort_max': '最高',
|
|
257
|
+
'models.caps_vision': '视觉',
|
|
258
|
+
'models.caps_general': '通用',
|
|
259
|
+
'models.caps_coding': '编程',
|
|
260
|
+
'diag.smoke_result': '✅ 延迟:{latency} ms | 模型:{model}\n响应:{preview}',
|
|
261
|
+
'diag.empty': '(空)',
|
|
262
|
+
'ui.error_title': '⚠️ ClineBot 界面错误:',
|
|
263
|
+
'ui.retry': '重试',
|
|
264
|
+
'update.check_failed': '检查更新失败',
|
|
265
|
+
'update.retry': '重试检查',
|
|
209
266
|
'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
|
|
210
267
|
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
|
|
211
268
|
'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
|
|
@@ -281,6 +338,9 @@ const en = {
|
|
|
281
338
|
|
|
282
339
|
function makeT(dict, fallback) {
|
|
283
340
|
return function t(key, vars) {
|
|
341
|
+
if (key === 'quota.countdown' && vars?.time === 'resetting') {
|
|
342
|
+
return (dict && dict['quota.resetting']) || (fallback && fallback['quota.resetting']) || '正在重置'
|
|
343
|
+
}
|
|
284
344
|
let val = (dict && dict[key]) || (fallback && fallback[key]) || key
|
|
285
345
|
if (vars && typeof val === 'string') {
|
|
286
346
|
for (const k of Object.keys(vars)) {
|
|
@@ -399,10 +459,13 @@ function createErrorBoundary() {
|
|
|
399
459
|
}
|
|
400
460
|
render() {
|
|
401
461
|
if (this.state.hasError) {
|
|
462
|
+
const t = this.props && typeof this.props.t === 'function' ? this.props.t : null
|
|
463
|
+
const errorTitle = t ? t('ui.error_title') : '⚠️ ClineBot UI Error: '
|
|
464
|
+
const retryLabel = t ? t('ui.retry') : 'Retry'
|
|
402
465
|
return React.createElement(
|
|
403
466
|
'div',
|
|
404
467
|
{ className: 'cb-alert-err' },
|
|
405
|
-
React.createElement('strong', null,
|
|
468
|
+
React.createElement('strong', null, errorTitle),
|
|
406
469
|
String(this.state.error?.message || this.state.error || 'Unknown rendering error'),
|
|
407
470
|
React.createElement(
|
|
408
471
|
'button',
|
|
@@ -412,7 +475,7 @@ function createErrorBoundary() {
|
|
|
412
475
|
style: { marginLeft: '12px', padding: '2px 8px', fontSize: '11px' },
|
|
413
476
|
onClick: () => this.setState({ hasError: false, error: null }),
|
|
414
477
|
},
|
|
415
|
-
|
|
478
|
+
retryLabel
|
|
416
479
|
)
|
|
417
480
|
)
|
|
418
481
|
}
|
|
@@ -512,7 +575,7 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
|
512
575
|
},
|
|
513
576
|
},
|
|
514
577
|
t('quota.used', { pct }),
|
|
515
|
-
resetCountdown ? ` · ${t('quota.countdown', { time: resetCountdown })}` : (resetClock ? ` · ${t('quota.reset_at', { time: resetClock })}` : '')
|
|
578
|
+
resetCountdown ? ` · ${resetCountdown === 'resetting' ? (t('quota.resetting') || '正在重置') : t('quota.countdown', { time: resetCountdown })}` : (resetClock ? ` · ${t('quota.reset_at', { time: resetClock })}` : '')
|
|
516
579
|
)
|
|
517
580
|
),
|
|
518
581
|
React.createElement(
|
|
@@ -537,7 +600,22 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
|
537
600
|
}
|
|
538
601
|
|
|
539
602
|
// --- src/client/components/update-banner.js ---
|
|
540
|
-
function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
603
|
+
function UpdateBanner({ updateState, handleTriggerUpdate, handleCheckUpdate, t }) {
|
|
604
|
+
const status = updateState.status || (
|
|
605
|
+
updateState.checking
|
|
606
|
+
? 'checking'
|
|
607
|
+
: (updateState.latestCheckFailed
|
|
608
|
+
? 'error'
|
|
609
|
+
: (updateState.updateAvailable
|
|
610
|
+
? 'available'
|
|
611
|
+
: (updateState.lastCheckedAt ? 'current' : 'idle')))
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
const isChecking = status === 'checking' || updateState.checking
|
|
615
|
+
const isAvailable = status === 'available' || updateState.updateAvailable
|
|
616
|
+
const isCurrent = status === 'current'
|
|
617
|
+
const isError = status === 'error' || updateState.latestCheckFailed
|
|
618
|
+
|
|
541
619
|
return React.createElement(
|
|
542
620
|
React.Fragment,
|
|
543
621
|
null,
|
|
@@ -561,19 +639,25 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
|
561
639
|
`v${updateState.currentVersion}`
|
|
562
640
|
)
|
|
563
641
|
: null,
|
|
564
|
-
|
|
642
|
+
isChecking
|
|
565
643
|
? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px' } },
|
|
566
644
|
t('update.checking')
|
|
567
645
|
)
|
|
568
|
-
:
|
|
646
|
+
: isAvailable
|
|
569
647
|
? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
570
648
|
t('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
|
|
571
649
|
)
|
|
572
|
-
:
|
|
573
|
-
|
|
574
|
-
|
|
650
|
+
: isCurrent
|
|
651
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
652
|
+
'✓ ' + t('update.up_to_date')
|
|
653
|
+
)
|
|
654
|
+
: isError
|
|
655
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-bad' },
|
|
656
|
+
t('update.check_failed')
|
|
657
|
+
)
|
|
658
|
+
: null
|
|
575
659
|
),
|
|
576
|
-
|
|
660
|
+
isAvailable
|
|
577
661
|
? React.createElement(
|
|
578
662
|
'button',
|
|
579
663
|
{
|
|
@@ -585,7 +669,19 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
|
585
669
|
},
|
|
586
670
|
updateState.updating ? t('update.updating') : t('update.btn')
|
|
587
671
|
)
|
|
588
|
-
:
|
|
672
|
+
: isError && handleCheckUpdate
|
|
673
|
+
? React.createElement(
|
|
674
|
+
'button',
|
|
675
|
+
{
|
|
676
|
+
type: 'button',
|
|
677
|
+
className: 'cb-btn',
|
|
678
|
+
disabled: isChecking,
|
|
679
|
+
onClick: handleCheckUpdate,
|
|
680
|
+
style: { padding: '5px 12px', fontSize: '12px' }
|
|
681
|
+
},
|
|
682
|
+
t('update.retry')
|
|
683
|
+
)
|
|
684
|
+
: null
|
|
589
685
|
),
|
|
590
686
|
updateState.notice ? React.createElement('div', { className: 'cb-alert-ok' }, '✓ ' + updateState.notice) : null,
|
|
591
687
|
updateState.error ? React.createElement('div', { className: 'cb-alert-err' }, updateState.error) : null
|
|
@@ -936,7 +1032,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
|
|
|
936
1032
|
React.createElement(
|
|
937
1033
|
'div',
|
|
938
1034
|
{ style: { flex: 1 } },
|
|
939
|
-
React.createElement('div', null, status.quotaWarning.message),
|
|
1035
|
+
React.createElement('div', null, (status.quotaWarning.code && t(status.quotaWarning.code, status.quotaWarning.params)) || status.quotaWarning.message),
|
|
940
1036
|
status.quotaWarning.resetsAt
|
|
941
1037
|
? React.createElement(
|
|
942
1038
|
'div',
|
|
@@ -1552,19 +1648,20 @@ function ModelsSection({
|
|
|
1552
1648
|
m.category === 'reasoning'
|
|
1553
1649
|
)
|
|
1554
1650
|
const category = m.category && m.category !== 'reasoning' ? m.category : (hasReasoning ? null : 'general')
|
|
1651
|
+
const categoryLabel = category === 'general' ? t('models.caps_general') : (category === 'coding' ? t('models.caps_coding') : category)
|
|
1555
1652
|
const hasVision = Boolean(m.input?.includes('image') || m.input?.includes('vision'))
|
|
1556
1653
|
return React.createElement(
|
|
1557
1654
|
React.Fragment,
|
|
1558
1655
|
null,
|
|
1559
|
-
category ? React.createElement('span', { className: 'cb-badge' },
|
|
1656
|
+
category ? React.createElement('span', { className: 'cb-badge' }, categoryLabel) : null,
|
|
1560
1657
|
hasVision
|
|
1561
|
-
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, '
|
|
1658
|
+
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, t('models.caps_vision'))
|
|
1562
1659
|
: null,
|
|
1563
1660
|
hasReasoning
|
|
1564
1661
|
? React.createElement(
|
|
1565
1662
|
React.Fragment,
|
|
1566
1663
|
null,
|
|
1567
|
-
React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: (category || hasVision) ? '4px' : '0' }, title: t('models.reasoning_tooltip') }, '
|
|
1664
|
+
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
1665
|
React.createElement(
|
|
1569
1666
|
'select',
|
|
1570
1667
|
{
|
|
@@ -1579,10 +1676,10 @@ function ModelsSection({
|
|
|
1579
1676
|
},
|
|
1580
1677
|
},
|
|
1581
1678
|
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' }, '
|
|
1679
|
+
React.createElement('option', { value: 'low' }, t('models.effort_low')),
|
|
1680
|
+
React.createElement('option', { value: 'medium' }, t('models.effort_medium')),
|
|
1681
|
+
React.createElement('option', { value: 'high' }, t('models.effort_high')),
|
|
1682
|
+
React.createElement('option', { value: 'max' }, t('models.effort_max'))
|
|
1586
1683
|
)
|
|
1587
1684
|
)
|
|
1588
1685
|
: null
|
|
@@ -1785,27 +1882,17 @@ function DiagSection({ keyPresent, isRegistered, busy, smokeResult, handleSmoke,
|
|
|
1785
1882
|
? React.createElement(
|
|
1786
1883
|
'div',
|
|
1787
1884
|
{ className: 'cb-preview' },
|
|
1788
|
-
|
|
1789
|
-
|
|
1885
|
+
t('diag.smoke_result', {
|
|
1886
|
+
latency: smokeResult.latencyMs,
|
|
1887
|
+
model: smokeResult.model,
|
|
1888
|
+
preview: smokeResult.preview || t('diag.empty'),
|
|
1889
|
+
})
|
|
1790
1890
|
)
|
|
1791
1891
|
: null
|
|
1792
1892
|
)
|
|
1793
1893
|
}
|
|
1794
1894
|
|
|
1795
1895
|
// --- src/client/settings-page.js ---
|
|
1796
|
-
function useConfigFormsSnapshot(ctx) {
|
|
1797
|
-
const scope = React.useMemo(() => {
|
|
1798
|
-
try { return readConfigForms(ctx)?.get?.(NS) || undefined } catch { return undefined }
|
|
1799
|
-
}, [ctx])
|
|
1800
|
-
const subscribe = React.useMemo(() => (cb) => {
|
|
1801
|
-
try { return scope?.subscribe ? (scope.subscribe(cb) || (() => {})) : () => {} } catch { return () => {} }
|
|
1802
|
-
}, [scope])
|
|
1803
|
-
const getSnapshot = React.useCallback(() => {
|
|
1804
|
-
try { return scope?.getSnapshot?.() || SNAPSHOT_READY } catch { return SNAPSHOT_READY }
|
|
1805
|
-
}, [scope])
|
|
1806
|
-
return React.useSyncExternalStore(subscribe, getSnapshot, () => SNAPSHOT_READY)?.status || 'loading'
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
1896
|
function SettingsPage(props) {
|
|
1810
1897
|
const ctx = props?.ctx
|
|
1811
1898
|
const t = props?.t || makeT(en, en)
|
|
@@ -1819,8 +1906,9 @@ function SettingsPage(props) {
|
|
|
1819
1906
|
const [smokeResult, setSmokeResult] = React.useState(null)
|
|
1820
1907
|
|
|
1821
1908
|
const [updateState, setUpdateState] = React.useState({
|
|
1822
|
-
checking: false, updating: false, currentVersion: '', latestVersion: '',
|
|
1823
|
-
updateAvailable: false, canAutoUpdate: true,
|
|
1909
|
+
status: 'idle', checking: false, updating: false, currentVersion: '', latestVersion: '',
|
|
1910
|
+
updateAvailable: false, canAutoUpdate: true, latestCheckFailed: false, lastCheckedAt: null,
|
|
1911
|
+
error: '', notice: '',
|
|
1824
1912
|
})
|
|
1825
1913
|
|
|
1826
1914
|
const [apiKeyInput, setApiKeyInput] = React.useState('')
|
|
@@ -1847,23 +1935,45 @@ function SettingsPage(props) {
|
|
|
1847
1935
|
}, [load])
|
|
1848
1936
|
|
|
1849
1937
|
const checkUpdate = React.useCallback(async () => {
|
|
1850
|
-
setUpdateState((s) => ({ ...s, checking: true, error: '' }))
|
|
1938
|
+
setUpdateState((s) => ({ ...s, checking: true, status: 'checking', error: '' }))
|
|
1851
1939
|
try {
|
|
1852
1940
|
const res = await fetch(`${ROUTE_PREFIX}/update`)
|
|
1853
1941
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
1854
1942
|
const data = await res.json().catch(() => ({}))
|
|
1943
|
+
if (data.latestCheckFailed) {
|
|
1944
|
+
setUpdateState((s) => ({
|
|
1945
|
+
...s,
|
|
1946
|
+
checking: false,
|
|
1947
|
+
status: 'error',
|
|
1948
|
+
latestCheckFailed: true,
|
|
1949
|
+
currentVersion: data.currentVersion || s.currentVersion,
|
|
1950
|
+
error: t('update.check_failed'),
|
|
1951
|
+
}))
|
|
1952
|
+
return
|
|
1953
|
+
}
|
|
1954
|
+
const isAvailable = Boolean(data.updateAvailable)
|
|
1855
1955
|
setUpdateState((s) => ({
|
|
1856
1956
|
...s,
|
|
1857
1957
|
checking: false,
|
|
1958
|
+
status: isAvailable ? 'available' : 'current',
|
|
1858
1959
|
currentVersion: data.currentVersion || s.currentVersion,
|
|
1859
1960
|
latestVersion: data.latestVersion || '',
|
|
1860
|
-
updateAvailable:
|
|
1961
|
+
updateAvailable: isAvailable,
|
|
1962
|
+
latestCheckFailed: false,
|
|
1963
|
+
lastCheckedAt: Date.now(),
|
|
1861
1964
|
canAutoUpdate: data.canAutoUpdate !== false,
|
|
1965
|
+
error: '',
|
|
1966
|
+
}))
|
|
1967
|
+
} catch (err) {
|
|
1968
|
+
setUpdateState((s) => ({
|
|
1969
|
+
...s,
|
|
1970
|
+
checking: false,
|
|
1971
|
+
status: 'error',
|
|
1972
|
+
latestCheckFailed: true,
|
|
1973
|
+
error: String(err?.message || err || t('update.check_failed')),
|
|
1862
1974
|
}))
|
|
1863
|
-
} catch (_) {
|
|
1864
|
-
setUpdateState((s) => ({ ...s, checking: false }))
|
|
1865
1975
|
}
|
|
1866
|
-
}, [])
|
|
1976
|
+
}, [t])
|
|
1867
1977
|
|
|
1868
1978
|
React.useEffect(() => { checkUpdate() }, [checkUpdate])
|
|
1869
1979
|
|
|
@@ -1881,13 +1991,20 @@ function SettingsPage(props) {
|
|
|
1881
1991
|
setUpdateState((s) => ({
|
|
1882
1992
|
...s,
|
|
1883
1993
|
updating: false,
|
|
1994
|
+
status: 'current',
|
|
1884
1995
|
updateAvailable: false,
|
|
1885
1996
|
currentVersion: newVer,
|
|
1997
|
+
lastCheckedAt: Date.now(),
|
|
1886
1998
|
notice: t('update.done', { version: newVer }),
|
|
1887
1999
|
}))
|
|
1888
2000
|
setTimeout(() => checkUpdate(), 2000)
|
|
1889
2001
|
} catch (err) {
|
|
1890
|
-
setUpdateState((s) => ({
|
|
2002
|
+
setUpdateState((s) => ({
|
|
2003
|
+
...s,
|
|
2004
|
+
updating: false,
|
|
2005
|
+
status: s.updateAvailable ? 'available' : 'error',
|
|
2006
|
+
error: t('update.failed', { error: String(err?.message || err) }),
|
|
2007
|
+
}))
|
|
1891
2008
|
}
|
|
1892
2009
|
}
|
|
1893
2010
|
|
|
@@ -1909,8 +2026,7 @@ function SettingsPage(props) {
|
|
|
1909
2026
|
if (!keyVal) { setErr(t('key.empty_err')); return }
|
|
1910
2027
|
await performAction('save-key', async () => {
|
|
1911
2028
|
const res = await fetch(`${ROUTE_PREFIX}/save-key`, {
|
|
1912
|
-
method: 'POST',
|
|
1913
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2029
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1914
2030
|
body: JSON.stringify({ apiKey: keyVal, apiKeyEnv: draft?.apiKeyEnv }),
|
|
1915
2031
|
})
|
|
1916
2032
|
const data = await res.json().catch(() => ({}))
|
|
@@ -1934,8 +2050,7 @@ function SettingsPage(props) {
|
|
|
1934
2050
|
async function handleUpdateModelContext(payload) {
|
|
1935
2051
|
await performAction('update-context', async () => {
|
|
1936
2052
|
const res = await fetch(`${ROUTE_PREFIX}/models/context`, {
|
|
1937
|
-
method: 'POST',
|
|
1938
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2053
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1939
2054
|
body: JSON.stringify(payload),
|
|
1940
2055
|
})
|
|
1941
2056
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2004,8 +2119,7 @@ function SettingsPage(props) {
|
|
|
2004
2119
|
setVerifyStatus({ state: 'verifying', email: '', plan: '', error: '' })
|
|
2005
2120
|
try {
|
|
2006
2121
|
const res = await fetch(`${ROUTE_PREFIX}/key/verify`, {
|
|
2007
|
-
method: 'POST',
|
|
2008
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2122
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2009
2123
|
body: JSON.stringify({ key: raw }),
|
|
2010
2124
|
})
|
|
2011
2125
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2030,8 +2144,7 @@ function SettingsPage(props) {
|
|
|
2030
2144
|
async function handlePinAccount(accountEnv) {
|
|
2031
2145
|
await performAction('pin-account', async () => {
|
|
2032
2146
|
const res = await fetch(`${ROUTE_PREFIX}/accounts/active`, {
|
|
2033
|
-
method: 'POST',
|
|
2034
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2147
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2035
2148
|
body: JSON.stringify({ account: accountEnv }),
|
|
2036
2149
|
})
|
|
2037
2150
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2043,8 +2156,7 @@ function SettingsPage(props) {
|
|
|
2043
2156
|
async function handleAddAccount({ label, apiKeyEnv, apiKey }) {
|
|
2044
2157
|
await performAction('add-account', async () => {
|
|
2045
2158
|
const res = await fetch(`${ROUTE_PREFIX}/accounts`, {
|
|
2046
|
-
method: 'POST',
|
|
2047
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2159
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2048
2160
|
body: JSON.stringify({ label, apiKeyEnv, apiKey }),
|
|
2049
2161
|
})
|
|
2050
2162
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2057,8 +2169,7 @@ function SettingsPage(props) {
|
|
|
2057
2169
|
async function handleDeleteAccount(accountEnv, deleteSecret = true) {
|
|
2058
2170
|
await performAction('delete-account', async () => {
|
|
2059
2171
|
const res = await fetch(`${ROUTE_PREFIX}/accounts/delete`, {
|
|
2060
|
-
method: 'POST',
|
|
2061
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2172
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2062
2173
|
body: JSON.stringify({ apiKeyEnv: accountEnv, deleteSecret }),
|
|
2063
2174
|
})
|
|
2064
2175
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2071,8 +2182,7 @@ function SettingsPage(props) {
|
|
|
2071
2182
|
setDraft((d) => ({ ...(d || {}), ...(patch || {}) }))
|
|
2072
2183
|
await performAction('patch-config', async () => {
|
|
2073
2184
|
const res = await fetch(`${ROUTE_PREFIX}/config`, {
|
|
2074
|
-
method: 'PUT',
|
|
2075
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2185
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
2076
2186
|
body: JSON.stringify({ config: patch }),
|
|
2077
2187
|
})
|
|
2078
2188
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2136,15 +2246,10 @@ function SettingsPage(props) {
|
|
|
2136
2246
|
function handleSetModelsFilter(type) {
|
|
2137
2247
|
const all = status?.availableModels || []
|
|
2138
2248
|
let allowed = new Set()
|
|
2139
|
-
if (type === 'all')
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
} else if (type === 'coding') {
|
|
2144
|
-
allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
|
|
2145
|
-
} else if (type === 'recommended') {
|
|
2146
|
-
allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
|
|
2147
|
-
}
|
|
2249
|
+
if (type === 'all') allowed = new Set(all.map((m) => m.id))
|
|
2250
|
+
else if (type === 'vision') allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
|
|
2251
|
+
else if (type === 'coding') allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
|
|
2252
|
+
else if (type === 'recommended') allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
|
|
2148
2253
|
|
|
2149
2254
|
const curr = draft?.disabledModels || []
|
|
2150
2255
|
const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
|
|
@@ -2205,7 +2310,7 @@ function SettingsPage(props) {
|
|
|
2205
2310
|
),
|
|
2206
2311
|
|
|
2207
2312
|
// In-app Update Bar
|
|
2208
|
-
React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, t }),
|
|
2313
|
+
React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, handleCheckUpdate: checkUpdate, t }),
|
|
2209
2314
|
|
|
2210
2315
|
// Settings host status warning
|
|
2211
2316
|
snapshotStatus === 'unavailable'
|
|
@@ -2338,7 +2443,7 @@ function PluginCard(props) {
|
|
|
2338
2443
|
{ style: { marginTop: '16px' } },
|
|
2339
2444
|
React.createElement(
|
|
2340
2445
|
ErrorBoundary,
|
|
2341
|
-
|
|
2446
|
+
{ t },
|
|
2342
2447
|
React.createElement(SettingsPage, { ...props, ctx: props && props.ctx })
|
|
2343
2448
|
)
|
|
2344
2449
|
)
|
|
@@ -2423,7 +2528,7 @@ function apply(ctx) {
|
|
|
2423
2528
|
locale: NS,
|
|
2424
2529
|
inject: () => ({ ctx }),
|
|
2425
2530
|
},
|
|
2426
|
-
(props) => React.createElement(ErrorBoundary,
|
|
2531
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2427
2532
|
)
|
|
2428
2533
|
)
|
|
2429
2534
|
|
|
@@ -2440,7 +2545,7 @@ function apply(ctx) {
|
|
|
2440
2545
|
locale: NS,
|
|
2441
2546
|
inject: () => ({ ctx }),
|
|
2442
2547
|
},
|
|
2443
|
-
(props) => React.createElement(ErrorBoundary,
|
|
2548
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2444
2549
|
)
|
|
2445
2550
|
)
|
|
2446
2551
|
}
|
|
@@ -2453,7 +2558,7 @@ function apply(ctx) {
|
|
|
2453
2558
|
locale: NS,
|
|
2454
2559
|
inject: () => ({ ctx }),
|
|
2455
2560
|
},
|
|
2456
|
-
(props) => React.createElement(ErrorBoundary,
|
|
2561
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2457
2562
|
)
|
|
2458
2563
|
)
|
|
2459
2564
|
}
|
package/lib/cline-client.js
CHANGED
|
@@ -528,10 +528,7 @@ export function buildPiAiProvider({
|
|
|
528
528
|
if (validEfforts.includes(key)) efforts[key] = key === 'off' ? (v === undefined ? null : v) : (typeof v === 'string' && v.trim() ? v.trim() : key)
|
|
529
529
|
}
|
|
530
530
|
}
|
|
531
|
-
const modelCompat = {
|
|
532
|
-
sendSessionAffinityHeaders: true,
|
|
533
|
-
sessionAffinityFormat: 'openrouter',
|
|
534
|
-
}
|
|
531
|
+
const modelCompat = {}
|
|
535
532
|
if (efforts && Object.keys(efforts).length) {
|
|
536
533
|
if (!('off' in efforts)) efforts.off = null
|
|
537
534
|
res.reasoningEfforts = efforts
|
|
@@ -539,7 +536,9 @@ export function buildPiAiProvider({
|
|
|
539
536
|
} else {
|
|
540
537
|
res.reasoningEfforts = false
|
|
541
538
|
}
|
|
542
|
-
|
|
539
|
+
if (Object.keys(modelCompat).length > 0) {
|
|
540
|
+
res.compat = modelCompat
|
|
541
|
+
}
|
|
543
542
|
return res
|
|
544
543
|
})
|
|
545
544
|
|
|
@@ -550,8 +549,6 @@ export function buildPiAiProvider({
|
|
|
550
549
|
apiKeyEnv: apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
551
550
|
compat: {
|
|
552
551
|
supportsReasoningEffort: true,
|
|
553
|
-
sendSessionAffinityHeaders: true,
|
|
554
|
-
sessionAffinityFormat: 'openrouter',
|
|
555
552
|
},
|
|
556
553
|
models: modelList,
|
|
557
554
|
}
|
package/lib/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { registerModelsRoutes } from './routes/models.js'
|
|
|
22
22
|
import { registerAuthRoutes } from './routes/auth.js'
|
|
23
23
|
import { registerProxyRoutes } from './routes/proxy.js'
|
|
24
24
|
import { registerSlashCommand } from './slash-command.js'
|
|
25
|
-
import { recordUsage, loadStats, flushStats } from './stats-storage.js'
|
|
25
|
+
import { recordUsage, loadStats, flushStats, resolvePath } from './stats-storage.js'
|
|
26
26
|
|
|
27
27
|
export const name = '@goodandready/dsh-clinebot'
|
|
28
28
|
export const inject = ['webServer', 'credentials']
|
|
@@ -35,7 +35,41 @@ export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSess
|
|
|
35
35
|
|
|
36
36
|
export function apply(ctx, config) {
|
|
37
37
|
let currentConfig = config
|
|
38
|
-
|
|
38
|
+
const overrides = {}
|
|
39
|
+
const getConfig = () => {
|
|
40
|
+
if (!currentConfig) return currentConfig
|
|
41
|
+
if (Object.isFrozen(currentConfig)) {
|
|
42
|
+
return new Proxy({}, {
|
|
43
|
+
get(_target, prop) {
|
|
44
|
+
const orig = Reflect.get(currentConfig, prop)
|
|
45
|
+
if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(currentConfig, prop)?.get) {
|
|
46
|
+
return orig
|
|
47
|
+
}
|
|
48
|
+
if (prop in overrides) {
|
|
49
|
+
return overrides[prop]
|
|
50
|
+
}
|
|
51
|
+
return orig
|
|
52
|
+
},
|
|
53
|
+
has(_target, prop) {
|
|
54
|
+
return prop in overrides || Reflect.has(currentConfig, prop)
|
|
55
|
+
},
|
|
56
|
+
ownKeys() {
|
|
57
|
+
return Array.from(new Set([...Reflect.ownKeys(currentConfig), ...Object.keys(overrides)]))
|
|
58
|
+
},
|
|
59
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
60
|
+
if (prop in overrides) {
|
|
61
|
+
return { configurable: true, enumerable: true, value: overrides[prop], writable: true }
|
|
62
|
+
}
|
|
63
|
+
const desc = Object.getOwnPropertyDescriptor(currentConfig, prop)
|
|
64
|
+
if (desc) {
|
|
65
|
+
return { configurable: true, enumerable: desc.enumerable ?? true, value: Reflect.get(currentConfig, prop) }
|
|
66
|
+
}
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
return currentConfig
|
|
72
|
+
}
|
|
39
73
|
const live = () => (getConfig() ? Config(structuredClone(plainConfig(getConfig()))) : config)
|
|
40
74
|
let settingsApi
|
|
41
75
|
|
|
@@ -59,6 +93,18 @@ export function apply(ctx, config) {
|
|
|
59
93
|
autoDiscoverPlanModels(ctx, { live, getSettingsApi: () => settingsApi, syncProviderState })
|
|
60
94
|
}
|
|
61
95
|
|
|
96
|
+
let activeStatsPath = resolvePath(publicConfig(live()).statsPath)
|
|
97
|
+
loadStats(activeStatsPath)
|
|
98
|
+
|
|
99
|
+
const syncStatsStorage = (cfg) => {
|
|
100
|
+
const nextPath = resolvePath(publicConfig(cfg).statsPath)
|
|
101
|
+
if (nextPath !== activeStatsPath) {
|
|
102
|
+
flushStats(activeStatsPath)
|
|
103
|
+
activeStatsPath = nextPath
|
|
104
|
+
loadStats(nextPath)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
62
108
|
const getRevision = (svc) => {
|
|
63
109
|
try {
|
|
64
110
|
return svc?.describe?.().find((row) => row.ns === NS)?.revision
|
|
@@ -69,22 +115,37 @@ export function apply(ctx, config) {
|
|
|
69
115
|
|
|
70
116
|
const applyConfigMutations = (target, source) => {
|
|
71
117
|
const targetConfig = typeof target === 'function' ? target() : target
|
|
72
|
-
if (targetConfig && typeof targetConfig === 'object'
|
|
118
|
+
if (targetConfig && typeof targetConfig === 'object') {
|
|
119
|
+
if (Object.isFrozen(targetConfig)) {
|
|
120
|
+
for (const [key, value] of Object.entries(source)) {
|
|
121
|
+
const orig = targetConfig[key]
|
|
122
|
+
if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(targetConfig, key)?.get) {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
overrides[key] = value
|
|
126
|
+
}
|
|
127
|
+
return
|
|
128
|
+
}
|
|
73
129
|
for (const [key, value] of Object.entries(source)) {
|
|
74
130
|
try {
|
|
75
|
-
if (isVolatileRef(targetConfig[key])
|
|
76
|
-
|
|
77
|
-
|
|
131
|
+
if (isVolatileRef(targetConfig[key])) {
|
|
132
|
+
// Cosmokit native Volatile references are reactive and authoritative;
|
|
133
|
+
// preserve the reference without overwriting with plain value.
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
const desc = Object.getOwnPropertyDescriptor(targetConfig, key)
|
|
137
|
+
if (desc?.get && !desc.set) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
if (desc?.set) {
|
|
78
141
|
targetConfig[key] = value
|
|
79
|
-
} else
|
|
142
|
+
} else {
|
|
80
143
|
targetConfig[key] = value
|
|
81
144
|
}
|
|
82
145
|
} catch {
|
|
83
146
|
/* ignore read-only */
|
|
84
147
|
}
|
|
85
148
|
}
|
|
86
|
-
} else {
|
|
87
|
-
currentConfig = source
|
|
88
149
|
}
|
|
89
150
|
}
|
|
90
151
|
|
|
@@ -104,7 +165,7 @@ export function apply(ctx, config) {
|
|
|
104
165
|
await svc.update(NS, payload, rev)
|
|
105
166
|
}
|
|
106
167
|
applyConfigMutations(currentConfig, parsed)
|
|
107
|
-
|
|
168
|
+
syncStatsStorage(live())
|
|
108
169
|
await syncProviderState(live())
|
|
109
170
|
return parsed
|
|
110
171
|
},
|
|
@@ -122,7 +183,7 @@ export function apply(ctx, config) {
|
|
|
122
183
|
await svc.replace(NS, payload, rev)
|
|
123
184
|
}
|
|
124
185
|
applyConfigMutations(currentConfig, merged)
|
|
125
|
-
|
|
186
|
+
syncStatsStorage(live())
|
|
126
187
|
await syncProviderState(live())
|
|
127
188
|
return merged
|
|
128
189
|
},
|
|
@@ -133,11 +194,10 @@ export function apply(ctx, config) {
|
|
|
133
194
|
},
|
|
134
195
|
}
|
|
135
196
|
|
|
136
|
-
loadStats(publicConfig(live()).statsPath)
|
|
137
197
|
if (typeof ctx.effect === 'function') {
|
|
138
198
|
ctx.effect(() => {
|
|
139
199
|
return () => {
|
|
140
|
-
flushStats(
|
|
200
|
+
flushStats(activeStatsPath)
|
|
141
201
|
}
|
|
142
202
|
}, 'dsh-clinebot: stats-storage')
|
|
143
203
|
}
|