@goodandready/dsh-clinebot 0.2.1 → 0.3.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 +21 -0
- package/README.md +25 -14
- package/docs/README.ru.md +26 -15
- package/docs/README.zh.md +25 -14
- package/docs/design/DESIGN.md +4 -1
- package/lib/client.js +181 -198
- package/lib/cline-client.js +70 -5
- package/lib/index.js +113 -81
- package/lib/models.js +58 -48
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -10,6 +10,32 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
const SUBTITLE = 'ClinePass subscription ($9.99/mo) with curated & custom models.'
|
|
11
11
|
const ROUTE_PREFIX = '/dsh-clinebot'
|
|
12
12
|
|
|
13
|
+
const en = {
|
|
14
|
+
title: 'ClineBot',
|
|
15
|
+
subtitle: 'ClinePass subscription ($9.99/mo) with curated & custom models.',
|
|
16
|
+
'settings.loading': 'Loading ClineBot settings…',
|
|
17
|
+
'settings.unavailable': 'ClineBot settings unavailable (host namespace is not ready).',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ru = {
|
|
21
|
+
title: 'ClineBot',
|
|
22
|
+
subtitle: 'Подписка ClinePass ($9.99/мес) с каталогом проверенных моделей.',
|
|
23
|
+
'settings.loading': 'Загрузка настроек ClineBot…',
|
|
24
|
+
'settings.unavailable': 'Настройки ClineBot недоступны (пространство хоста ещё не готово).',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeT(dict, fallback) {
|
|
28
|
+
return function t(key, vars) {
|
|
29
|
+
let val = (dict && dict[key]) || (fallback && fallback[key]) || key
|
|
30
|
+
if (vars && typeof val === 'string') {
|
|
31
|
+
for (const k of Object.keys(vars)) {
|
|
32
|
+
val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return val
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
function FallbackChevron() {
|
|
14
40
|
return React.createElement(
|
|
15
41
|
'svg',
|
|
@@ -52,12 +78,13 @@ window.__ModuleLoader__.load({
|
|
|
52
78
|
.cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
|
|
53
79
|
.cb-input-group{display:flex;gap:8px;align-items:center}
|
|
54
80
|
|
|
55
|
-
.cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px}
|
|
56
|
-
.cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-
|
|
81
|
+
.cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
|
|
82
|
+
.cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
|
|
57
83
|
.cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
58
|
-
.cb-btn-primary:hover:not(:disabled){opacity:0.
|
|
84
|
+
.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88;visibility:visible !important}
|
|
59
85
|
.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
|
|
60
|
-
.cb-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.
|
|
86
|
+
.cb-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12) !important;border-color:rgba(239,68,68,0.5)}
|
|
87
|
+
.cb-btn:disabled{opacity:0.5;cursor:not-allowed}
|
|
61
88
|
|
|
62
89
|
.cb-bar-container{display:flex;flex-direction:column;gap:6px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
|
|
63
90
|
.cb-bar-head{display:flex;justify-content:space-between;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
|
|
@@ -72,6 +99,12 @@ window.__ModuleLoader__.load({
|
|
|
72
99
|
|
|
73
100
|
.cb-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px}
|
|
74
101
|
.cb-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
102
|
+
.cb-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
103
|
+
.cb-banner-warning{padding:12px 16px;border-radius:8px;background:rgba(245,158,11,0.12);border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
|
|
104
|
+
.cb-banner-exhausted{padding:12px 16px;border-radius:8px;background:rgba(239,68,68,0.12);border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:600}
|
|
105
|
+
.cb-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
|
|
106
|
+
.cb-stat-val{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary)}
|
|
107
|
+
.cb-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
75
108
|
.cb-preview{padding:12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2)}
|
|
76
109
|
`
|
|
77
110
|
document.head.appendChild(style)
|
|
@@ -111,7 +144,22 @@ window.__ModuleLoader__.load({
|
|
|
111
144
|
)
|
|
112
145
|
}
|
|
113
146
|
|
|
114
|
-
function SettingsPage() {
|
|
147
|
+
function SettingsPage(props) {
|
|
148
|
+
const ctx = props?.ctx
|
|
149
|
+
const t = props?.t || makeT(ru, en)
|
|
150
|
+
|
|
151
|
+
const scope = React.useMemo(
|
|
152
|
+
() => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined),
|
|
153
|
+
[ctx]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const snapshot = React.useSyncExternalStore(
|
|
157
|
+
React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
|
|
158
|
+
React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'ready' }), [scope]),
|
|
159
|
+
React.useCallback(() => ({ status: 'loading' }), [])
|
|
160
|
+
)
|
|
161
|
+
const snapshotStatus = snapshot?.status || 'ready'
|
|
162
|
+
|
|
115
163
|
const [status, setStatus] = React.useState(null)
|
|
116
164
|
const [draft, setDraft] = React.useState(null)
|
|
117
165
|
const [busy, setBusy] = React.useState('')
|
|
@@ -123,13 +171,6 @@ window.__ModuleLoader__.load({
|
|
|
123
171
|
const [apiKeyInput, setApiKeyInput] = React.useState('')
|
|
124
172
|
const [showKey, setShowKey] = React.useState(false)
|
|
125
173
|
|
|
126
|
-
// Custom model form state
|
|
127
|
-
const [customId, setCustomId] = React.useState('')
|
|
128
|
-
const [customName, setCustomName] = React.useState('')
|
|
129
|
-
const [customContext, setCustomContext] = React.useState('200000')
|
|
130
|
-
const [customVision, setCustomVision] = React.useState(false)
|
|
131
|
-
const [customCategory, setCustomCategory] = React.useState('coding')
|
|
132
|
-
|
|
133
174
|
React.useEffect(() => {
|
|
134
175
|
ensureCss()
|
|
135
176
|
}, [])
|
|
@@ -195,6 +236,24 @@ window.__ModuleLoader__.load({
|
|
|
195
236
|
}
|
|
196
237
|
}
|
|
197
238
|
|
|
239
|
+
// Sync official models from ClinePass plan
|
|
240
|
+
async function handleSyncPlanModels() {
|
|
241
|
+
setBusy('sync-models')
|
|
242
|
+
setErr('')
|
|
243
|
+
setMsg('')
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
|
|
246
|
+
const data = await res.json().catch(() => ({}))
|
|
247
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
248
|
+
setMsg(`Синхронизировано моделей подписки: ${data.totalModelsCount} (${data.discoveredCount} получено напрямую из плана ClinePass)`)
|
|
249
|
+
await load()
|
|
250
|
+
} catch (e) {
|
|
251
|
+
setErr(String(e.message || e))
|
|
252
|
+
} finally {
|
|
253
|
+
setBusy('')
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
198
257
|
// Register/Sync Provider in DSH
|
|
199
258
|
async function handleRegister() {
|
|
200
259
|
setBusy('register')
|
|
@@ -251,6 +310,7 @@ window.__ModuleLoader__.load({
|
|
|
251
310
|
if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
252
311
|
setSmokeResult(data)
|
|
253
312
|
setMsg(`Smoke тест пройден! Задержка: ${data.latencyMs} мс`)
|
|
313
|
+
await load()
|
|
254
314
|
} catch (e) {
|
|
255
315
|
setErr(String(e.message || e))
|
|
256
316
|
} finally {
|
|
@@ -270,6 +330,9 @@ window.__ModuleLoader__.load({
|
|
|
270
330
|
const nextList = Array.from(current)
|
|
271
331
|
setDraft({ ...draft, enabledModels: nextList })
|
|
272
332
|
|
|
333
|
+
if (scope && snapshotStatus === 'ready') {
|
|
334
|
+
try { await scope.set('enabledModels', nextList) } catch (_) {}
|
|
335
|
+
}
|
|
273
336
|
try {
|
|
274
337
|
await fetch(`${ROUTE_PREFIX}/models/toggle`, {
|
|
275
338
|
method: 'POST',
|
|
@@ -286,7 +349,7 @@ window.__ModuleLoader__.load({
|
|
|
286
349
|
if (type === 'all') {
|
|
287
350
|
nextList = status.availableModels.map((m) => m.id)
|
|
288
351
|
} else if (type === 'vision') {
|
|
289
|
-
nextList = status.availableModels.filter((m) => m.input?.includes('vision')).map((m) => m.id)
|
|
352
|
+
nextList = status.availableModels.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id)
|
|
290
353
|
} else if (type === 'coding') {
|
|
291
354
|
nextList = status.availableModels.filter((m) => m.category === 'coding').map((m) => m.id)
|
|
292
355
|
} else if (type === 'recommended') {
|
|
@@ -294,6 +357,9 @@ window.__ModuleLoader__.load({
|
|
|
294
357
|
}
|
|
295
358
|
|
|
296
359
|
setDraft({ ...draft, enabledModels: nextList })
|
|
360
|
+
if (scope && snapshotStatus === 'ready') {
|
|
361
|
+
try { await scope.set('enabledModels', nextList) } catch (_) {}
|
|
362
|
+
}
|
|
297
363
|
try {
|
|
298
364
|
await fetch(`${ROUTE_PREFIX}/models/toggle`, {
|
|
299
365
|
method: 'POST',
|
|
@@ -303,61 +369,15 @@ window.__ModuleLoader__.load({
|
|
|
303
369
|
} catch {}
|
|
304
370
|
}
|
|
305
371
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
setBusy('add-model')
|
|
313
|
-
setErr('')
|
|
314
|
-
try {
|
|
315
|
-
const res = await fetch(`${ROUTE_PREFIX}/models/custom`, {
|
|
316
|
-
method: 'POST',
|
|
317
|
-
headers: { 'Content-Type': 'application/json' },
|
|
318
|
-
body: JSON.stringify({
|
|
319
|
-
id: customId.trim(),
|
|
320
|
-
name: customName.trim() || customId.trim(),
|
|
321
|
-
contextLength: Number(customContext) || 200000,
|
|
322
|
-
hasVision: customVision,
|
|
323
|
-
category: customCategory,
|
|
324
|
-
}),
|
|
325
|
-
})
|
|
326
|
-
const data = await res.json().catch(() => ({}))
|
|
327
|
-
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
328
|
-
setMsg(`Модель ${data.model?.name} успешно добавлена в каталог`)
|
|
329
|
-
setCustomId('')
|
|
330
|
-
setCustomName('')
|
|
331
|
-
await load()
|
|
332
|
-
} catch (e) {
|
|
333
|
-
setErr(String(e.message || e))
|
|
334
|
-
} finally {
|
|
335
|
-
setBusy('')
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Remove custom model
|
|
340
|
-
async function handleRemoveCustomModel(id) {
|
|
341
|
-
setBusy(`remove-${id}`)
|
|
342
|
-
setErr('')
|
|
343
|
-
try {
|
|
344
|
-
const res = await fetch(`${ROUTE_PREFIX}/models/custom`, {
|
|
345
|
-
method: 'DELETE',
|
|
346
|
-
headers: { 'Content-Type': 'application/json' },
|
|
347
|
-
body: JSON.stringify({ id }),
|
|
348
|
-
})
|
|
349
|
-
const data = await res.json().catch(() => ({}))
|
|
350
|
-
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
|
351
|
-
setMsg(`Модель ${id} удалена`)
|
|
352
|
-
await load()
|
|
353
|
-
} catch (e) {
|
|
354
|
-
setErr(String(e.message || e))
|
|
355
|
-
} finally {
|
|
356
|
-
setBusy('')
|
|
357
|
-
}
|
|
372
|
+
if (snapshotStatus === 'unavailable') {
|
|
373
|
+
return React.createElement(
|
|
374
|
+
'div',
|
|
375
|
+
{ className: 'cb-page' },
|
|
376
|
+
React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
|
|
377
|
+
)
|
|
358
378
|
}
|
|
359
379
|
|
|
360
|
-
if (!status || !draft) {
|
|
380
|
+
if (!status || !draft || snapshotStatus === 'loading') {
|
|
361
381
|
if (err) {
|
|
362
382
|
return React.createElement(
|
|
363
383
|
'div',
|
|
@@ -490,6 +510,27 @@ window.__ModuleLoader__.load({
|
|
|
490
510
|
)
|
|
491
511
|
),
|
|
492
512
|
|
|
513
|
+
// Quota Warning Banner
|
|
514
|
+
status.quotaWarning
|
|
515
|
+
? React.createElement(
|
|
516
|
+
'div',
|
|
517
|
+
{ className: status.quotaWarning.level === 'exhausted' ? 'cb-banner-exhausted' : 'cb-banner-warning' },
|
|
518
|
+
React.createElement('span', { style: { fontSize: '16px' } }, status.quotaWarning.level === 'exhausted' ? '🚨' : '⚠️'),
|
|
519
|
+
React.createElement(
|
|
520
|
+
'div',
|
|
521
|
+
{ style: { flex: 1 } },
|
|
522
|
+
React.createElement('div', null, status.quotaWarning.message),
|
|
523
|
+
status.quotaWarning.resetsAt
|
|
524
|
+
? React.createElement(
|
|
525
|
+
'div',
|
|
526
|
+
{ style: { fontSize: '11px', opacity: 0.9, marginTop: '2px' } },
|
|
527
|
+
`Ожидаемое время сброса лимита: ${new Date(status.quotaWarning.resetsAt).toLocaleTimeString()}`
|
|
528
|
+
)
|
|
529
|
+
: null
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
: null,
|
|
533
|
+
|
|
493
534
|
// Card 2: Subscription Limits & Usage Dashboard
|
|
494
535
|
keyPresent
|
|
495
536
|
? React.createElement(
|
|
@@ -536,7 +577,7 @@ window.__ModuleLoader__.load({
|
|
|
536
577
|
)
|
|
537
578
|
: null,
|
|
538
579
|
|
|
539
|
-
// Card 3: Model Picker Management
|
|
580
|
+
// Card 3: Model Picker Management with Official Plan Sync
|
|
540
581
|
React.createElement(
|
|
541
582
|
'div',
|
|
542
583
|
{ className: 'cb-section-card' },
|
|
@@ -547,6 +588,16 @@ window.__ModuleLoader__.load({
|
|
|
547
588
|
React.createElement(
|
|
548
589
|
'div',
|
|
549
590
|
{ className: 'cb-row' },
|
|
591
|
+
React.createElement(
|
|
592
|
+
'button',
|
|
593
|
+
{
|
|
594
|
+
type: 'button',
|
|
595
|
+
className: 'cb-btn',
|
|
596
|
+
disabled: !!busy || !keyPresent,
|
|
597
|
+
onClick: handleSyncPlanModels,
|
|
598
|
+
},
|
|
599
|
+
busy === 'sync-models' ? 'Синхронизация…' : '🔄 Получить модели из плана'
|
|
600
|
+
),
|
|
550
601
|
React.createElement(
|
|
551
602
|
'button',
|
|
552
603
|
{ type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') },
|
|
@@ -572,7 +623,7 @@ window.__ModuleLoader__.load({
|
|
|
572
623
|
React.createElement(
|
|
573
624
|
'div',
|
|
574
625
|
{ className: 'cb-section-desc' },
|
|
575
|
-
'Отметьте
|
|
626
|
+
'Список формируется напрямую из официальной подписки ClinePass без ручного ввода. Отметьте модели, которые должны отображаться в выпадающем списке выбора моделей в чате DeepSeek Harness.'
|
|
576
627
|
),
|
|
577
628
|
React.createElement(
|
|
578
629
|
'table',
|
|
@@ -612,9 +663,6 @@ window.__ModuleLoader__.load({
|
|
|
612
663
|
React.createElement('strong', null, m.name),
|
|
613
664
|
m.recommended
|
|
614
665
|
? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, 'Star')
|
|
615
|
-
: null,
|
|
616
|
-
m.isCustom
|
|
617
|
-
? React.createElement('span', { className: 'cb-badge cb-badge-warn', style: { marginLeft: '6px' } }, 'Custom')
|
|
618
666
|
: null
|
|
619
667
|
),
|
|
620
668
|
React.createElement('td', null, React.createElement('code', null, m.id)),
|
|
@@ -623,7 +671,7 @@ window.__ModuleLoader__.load({
|
|
|
623
671
|
'td',
|
|
624
672
|
null,
|
|
625
673
|
React.createElement('span', { className: 'cb-badge' }, m.category || 'general'),
|
|
626
|
-
m.input?.includes('vision')
|
|
674
|
+
m.input?.includes('image') || m.input?.includes('vision')
|
|
627
675
|
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, 'Vision')
|
|
628
676
|
: null
|
|
629
677
|
)
|
|
@@ -633,134 +681,48 @@ window.__ModuleLoader__.load({
|
|
|
633
681
|
)
|
|
634
682
|
),
|
|
635
683
|
|
|
636
|
-
// Card 4:
|
|
684
|
+
// Card 4: Session Metrics & Usage Tracking
|
|
637
685
|
React.createElement(
|
|
638
686
|
'div',
|
|
639
687
|
{ className: 'cb-section-card' },
|
|
640
688
|
React.createElement(
|
|
641
689
|
'div',
|
|
642
690
|
{ className: 'cb-section-title' },
|
|
643
|
-
'
|
|
691
|
+
'📈 Статистика текущей сессии DSH'
|
|
644
692
|
),
|
|
645
693
|
React.createElement(
|
|
646
694
|
'div',
|
|
647
695
|
{ className: 'cb-section-desc' },
|
|
648
|
-
'
|
|
696
|
+
'Локальный мониторинг запросов, задержек сети и ориентировочного расхода токенов через ClinePass.'
|
|
649
697
|
),
|
|
650
698
|
React.createElement(
|
|
651
699
|
'div',
|
|
652
700
|
{ className: 'cb-grid-2' },
|
|
653
701
|
React.createElement(
|
|
654
702
|
'div',
|
|
655
|
-
|
|
656
|
-
React.createElement('
|
|
657
|
-
React.createElement('
|
|
658
|
-
className: 'cb-input',
|
|
659
|
-
placeholder: 'cline-pass/...',
|
|
660
|
-
value: customId,
|
|
661
|
-
onChange: (e) => setCustomId(e.target.value),
|
|
662
|
-
})
|
|
703
|
+
{ className: 'cb-stat-box' },
|
|
704
|
+
React.createElement('div', { className: 'cb-stat-val' }, `${status.sessionStats?.successfulRequests || 0} / ${status.sessionStats?.totalRequests || 0}`),
|
|
705
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, 'Успешных запросов / Всего')
|
|
663
706
|
),
|
|
664
707
|
React.createElement(
|
|
665
708
|
'div',
|
|
666
|
-
|
|
667
|
-
React.createElement('
|
|
668
|
-
React.createElement('
|
|
669
|
-
className: 'cb-input',
|
|
670
|
-
placeholder: 'Например: DeepSeek V4 MoE',
|
|
671
|
-
value: customName,
|
|
672
|
-
onChange: (e) => setCustomName(e.target.value),
|
|
673
|
-
})
|
|
709
|
+
{ className: 'cb-stat-box' },
|
|
710
|
+
React.createElement('div', { className: 'cb-stat-val' }, `~${status.sessionStats?.totalTokensEst || 0}`),
|
|
711
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, 'Оценка токенов сессии')
|
|
674
712
|
),
|
|
675
713
|
React.createElement(
|
|
676
714
|
'div',
|
|
677
|
-
|
|
678
|
-
React.createElement('
|
|
679
|
-
React.createElement('
|
|
680
|
-
className: 'cb-input',
|
|
681
|
-
type: 'number',
|
|
682
|
-
value: customContext,
|
|
683
|
-
onChange: (e) => setCustomContext(e.target.value),
|
|
684
|
-
})
|
|
715
|
+
{ className: 'cb-stat-box' },
|
|
716
|
+
React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastLatencyMs ? `${status.sessionStats.lastLatencyMs} мс` : '—'),
|
|
717
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, 'Последняя задержка (Latency)')
|
|
685
718
|
),
|
|
686
719
|
React.createElement(
|
|
687
720
|
'div',
|
|
688
|
-
|
|
689
|
-
React.createElement('
|
|
690
|
-
React.createElement(
|
|
691
|
-
'select',
|
|
692
|
-
{
|
|
693
|
-
className: 'cb-input',
|
|
694
|
-
value: customCategory,
|
|
695
|
-
onChange: (e) => setCustomCategory(e.target.value),
|
|
696
|
-
},
|
|
697
|
-
React.createElement('option', { value: 'coding' }, 'Кодинг (Coding)'),
|
|
698
|
-
React.createElement('option', { value: 'reasoning' }, 'Рассуждения (Reasoning)'),
|
|
699
|
-
React.createElement('option', { value: 'multimodal' }, 'Мультимодальная'),
|
|
700
|
-
React.createElement('option', { value: 'general' }, 'Общая (General)')
|
|
701
|
-
)
|
|
721
|
+
{ className: 'cb-stat-box' },
|
|
722
|
+
React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastRequestAt ? new Date(status.sessionStats.lastRequestAt).toLocaleTimeString() : '—'),
|
|
723
|
+
React.createElement('div', { className: 'cb-stat-lbl' }, 'Время последнего запроса')
|
|
702
724
|
)
|
|
703
|
-
)
|
|
704
|
-
React.createElement(
|
|
705
|
-
'div',
|
|
706
|
-
{ className: 'cb-row', style: { marginTop: '4px' } },
|
|
707
|
-
React.createElement(
|
|
708
|
-
'label',
|
|
709
|
-
{ className: 'cb-row', style: { cursor: 'pointer', fontSize: '13px' } },
|
|
710
|
-
React.createElement('input', {
|
|
711
|
-
type: 'checkbox',
|
|
712
|
-
checked: customVision,
|
|
713
|
-
onChange: (e) => setCustomVision(e.target.checked),
|
|
714
|
-
}),
|
|
715
|
-
' Модель поддерживает изображения (Vision)'
|
|
716
|
-
),
|
|
717
|
-
React.createElement(
|
|
718
|
-
'button',
|
|
719
|
-
{
|
|
720
|
-
type: 'button',
|
|
721
|
-
className: 'cb-btn cb-btn-primary',
|
|
722
|
-
style: { marginLeft: 'auto' },
|
|
723
|
-
disabled: !!busy || !customId.trim(),
|
|
724
|
-
onClick: handleAddCustomModel,
|
|
725
|
-
},
|
|
726
|
-
busy === 'add-model' ? 'Добавление…' : 'Добавить модель'
|
|
727
|
-
)
|
|
728
|
-
),
|
|
729
|
-
// Custom models list with remove button
|
|
730
|
-
draft.customModels?.length > 0
|
|
731
|
-
? React.createElement(
|
|
732
|
-
'div',
|
|
733
|
-
{ style: { marginTop: '10px' } },
|
|
734
|
-
React.createElement('div', { className: 'cb-section-desc' }, 'Добавленные вами модели:'),
|
|
735
|
-
React.createElement(
|
|
736
|
-
'div',
|
|
737
|
-
{ className: 'cb-row', style: { marginTop: '6px' } },
|
|
738
|
-
draft.customModels.map((cm) =>
|
|
739
|
-
React.createElement(
|
|
740
|
-
'span',
|
|
741
|
-
{ key: cm.id, className: 'cb-badge' },
|
|
742
|
-
`${cm.name} (${cm.id})`,
|
|
743
|
-
React.createElement(
|
|
744
|
-
'button',
|
|
745
|
-
{
|
|
746
|
-
type: 'button',
|
|
747
|
-
style: {
|
|
748
|
-
background: 'none',
|
|
749
|
-
border: 'none',
|
|
750
|
-
cursor: 'pointer',
|
|
751
|
-
color: 'var(--dsw-alias-state-error-primary)',
|
|
752
|
-
padding: 0,
|
|
753
|
-
marginLeft: '4px',
|
|
754
|
-
},
|
|
755
|
-
onClick: () => handleRemoveCustomModel(cm.id),
|
|
756
|
-
},
|
|
757
|
-
'×'
|
|
758
|
-
)
|
|
759
|
-
)
|
|
760
|
-
)
|
|
761
|
-
)
|
|
762
|
-
)
|
|
763
|
-
: null
|
|
725
|
+
)
|
|
764
726
|
),
|
|
765
727
|
|
|
766
728
|
// Card 5: Diagnostics & Sync with DSH
|
|
@@ -826,8 +788,9 @@ window.__ModuleLoader__.load({
|
|
|
826
788
|
}
|
|
827
789
|
|
|
828
790
|
// Accordion item for Plugins section
|
|
829
|
-
function PluginCard() {
|
|
791
|
+
function PluginCard(props) {
|
|
830
792
|
const [open, setOpen] = React.useState(false)
|
|
793
|
+
const t = props?.t || makeT(ru, en)
|
|
831
794
|
React.useEffect(() => {
|
|
832
795
|
ensureCss()
|
|
833
796
|
}, [])
|
|
@@ -855,41 +818,61 @@ window.__ModuleLoader__.load({
|
|
|
855
818
|
React.createElement(
|
|
856
819
|
'div',
|
|
857
820
|
{ style: { flex: 1 } },
|
|
858
|
-
React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } },
|
|
859
|
-
React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
821
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
|
|
822
|
+
React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
860
823
|
),
|
|
861
824
|
React.createElement('span', { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
|
|
862
825
|
React.createElement(Chevron)
|
|
863
826
|
)
|
|
864
827
|
),
|
|
865
|
-
open ? React.createElement('div', { style: { marginTop: '16px' } }, React.createElement(SettingsPage)) : null
|
|
828
|
+
open ? React.createElement('div', { style: { marginTop: '16px' } }, React.createElement(SettingsPage, { ...props, ctx: props?.ctx })) : null
|
|
866
829
|
)
|
|
867
830
|
}
|
|
868
831
|
|
|
869
832
|
function apply(ctx) {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
833
|
+
if (ctx.locale && ctx.locale.register) {
|
|
834
|
+
try { ctx.locale.register(NS, { en, ru }) } catch (_) {}
|
|
835
|
+
}
|
|
836
|
+
const t = (ctx.locale && ctx.locale.bind) ? ctx.locale.bind(NS) : makeT(ru, en)
|
|
837
|
+
|
|
838
|
+
let placed = false
|
|
839
|
+
try {
|
|
840
|
+
placed = !!ctx.slots.inject('settings.plugin.item', () => {
|
|
841
|
+
return ctx.slots.register(
|
|
842
|
+
{
|
|
843
|
+
name: 'settings.plugin.item',
|
|
844
|
+
key: NS,
|
|
845
|
+
locale: NS,
|
|
846
|
+
inject: () => ({ ctx }),
|
|
847
|
+
},
|
|
848
|
+
PluginCard
|
|
849
|
+
)
|
|
850
|
+
})
|
|
851
|
+
} catch (_) {
|
|
852
|
+
placed = false
|
|
853
|
+
}
|
|
882
854
|
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
855
|
+
// Fallback to settings.section if settings.plugin.item is not declared
|
|
856
|
+
if (!placed) {
|
|
857
|
+
try {
|
|
858
|
+
ctx.slots.inject('settings.section', () => {
|
|
859
|
+
return ctx.slots.register(
|
|
860
|
+
{
|
|
861
|
+
name: 'settings.section',
|
|
862
|
+
id: '@goodandready/dsh-clinebot',
|
|
863
|
+
order: 28,
|
|
864
|
+
locale: NS,
|
|
865
|
+
label: () => t('title'),
|
|
866
|
+
inject: () => ({ ctx }),
|
|
867
|
+
},
|
|
868
|
+
SettingsPage
|
|
869
|
+
)
|
|
870
|
+
})
|
|
871
|
+
} catch (_) {}
|
|
872
|
+
}
|
|
890
873
|
}
|
|
891
874
|
|
|
892
|
-
module.exports = { apply, inject: ['slots'] }
|
|
875
|
+
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
|
|
893
876
|
return module.exports
|
|
894
877
|
},
|
|
895
878
|
})
|
package/lib/cline-client.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
PROVIDER_ID,
|
|
12
12
|
PROVIDER_DISPLAY_NAME,
|
|
13
13
|
getAllModels,
|
|
14
|
+
parsePlanIncludedModels,
|
|
14
15
|
} from './models.js'
|
|
15
16
|
|
|
16
17
|
export const DEFAULT_BASE_URL = 'https://api.cline.bot/api/v1'
|
|
@@ -48,6 +49,47 @@ export function normalizeBaseUrl(raw) {
|
|
|
48
49
|
return s
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// In-memory runtime session metrics for ClinePass requests
|
|
53
|
+
export const sessionStats = {
|
|
54
|
+
totalRequests: 0,
|
|
55
|
+
successfulRequests: 0,
|
|
56
|
+
failedRequests: 0,
|
|
57
|
+
promptTokensEst: 0,
|
|
58
|
+
completionTokensEst: 0,
|
|
59
|
+
totalTokensEst: 0,
|
|
60
|
+
lastLatencyMs: null,
|
|
61
|
+
lastRequestAt: null,
|
|
62
|
+
lastError: null,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function recordSessionRequest({ latencyMs, ok, error, promptTokens = 0, completionTokens = 0 }) {
|
|
66
|
+
sessionStats.totalRequests += 1
|
|
67
|
+
if (ok) {
|
|
68
|
+
sessionStats.successfulRequests += 1
|
|
69
|
+
sessionStats.lastLatencyMs = typeof latencyMs === 'number' ? latencyMs : null
|
|
70
|
+
sessionStats.lastError = null
|
|
71
|
+
} else {
|
|
72
|
+
sessionStats.failedRequests += 1
|
|
73
|
+
sessionStats.lastError = error || 'Request failed'
|
|
74
|
+
}
|
|
75
|
+
sessionStats.lastRequestAt = Date.now()
|
|
76
|
+
sessionStats.promptTokensEst += Number(promptTokens) || 0
|
|
77
|
+
sessionStats.completionTokensEst += Number(completionTokens) || 0
|
|
78
|
+
sessionStats.totalTokensEst += (Number(promptTokens) || 0) + (Number(completionTokens) || 0)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resetSessionStats() {
|
|
82
|
+
sessionStats.totalRequests = 0
|
|
83
|
+
sessionStats.successfulRequests = 0
|
|
84
|
+
sessionStats.failedRequests = 0
|
|
85
|
+
sessionStats.promptTokensEst = 0
|
|
86
|
+
sessionStats.completionTokensEst = 0
|
|
87
|
+
sessionStats.totalTokensEst = 0
|
|
88
|
+
sessionStats.lastLatencyMs = null
|
|
89
|
+
sessionStats.lastRequestAt = null
|
|
90
|
+
sessionStats.lastError = null
|
|
91
|
+
}
|
|
92
|
+
|
|
51
93
|
/**
|
|
52
94
|
* Resolve API key from environment variables.
|
|
53
95
|
*/
|
|
@@ -164,24 +206,46 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
|
|
|
164
206
|
const weekly = parseWindow('weekly')
|
|
165
207
|
const monthly = parseWindow('monthly')
|
|
166
208
|
|
|
167
|
-
// 2. Fetch user metadata (optional, best-effort)
|
|
209
|
+
// 2. Fetch user metadata and plan details (optional, best-effort)
|
|
168
210
|
let userEmail = null
|
|
169
211
|
let createdAt = null
|
|
212
|
+
let planDisplayName = 'ClinePass ($9.99/mo)'
|
|
213
|
+
let dynamicModels = []
|
|
214
|
+
|
|
170
215
|
try {
|
|
171
|
-
const meRes = await
|
|
172
|
-
|
|
216
|
+
const [meRes, planRes] = await Promise.all([
|
|
217
|
+
fetchImpl(`${base}/users/me`, { method: 'GET', headers, signal }).catch(() => null),
|
|
218
|
+
fetchImpl(`${base}/users/me/plan`, { method: 'GET', headers, signal }).catch(() => null),
|
|
219
|
+
])
|
|
220
|
+
|
|
221
|
+
if (meRes && meRes.ok) {
|
|
173
222
|
const meData = await meRes.json().catch(() => ({}))
|
|
174
223
|
const me = meData?.data || meData?.user || meData
|
|
175
224
|
userEmail = me?.email || null
|
|
176
225
|
createdAt = me?.createdAt || null
|
|
177
226
|
}
|
|
227
|
+
|
|
228
|
+
if (planRes && planRes.ok) {
|
|
229
|
+
const planData = await planRes.json().catch(() => ({}))
|
|
230
|
+
const planObj = planData?.data?.plan || planData?.plan
|
|
231
|
+
if (planObj?.displayName) {
|
|
232
|
+
planDisplayName = planObj.displayName
|
|
233
|
+
}
|
|
234
|
+
const includedArr = planObj?.features?.included
|
|
235
|
+
if (Array.isArray(includedArr)) {
|
|
236
|
+
const incStr = includedArr.find((s) => typeof s === 'string' && s.toLowerCase().includes('includes'))
|
|
237
|
+
if (incStr) {
|
|
238
|
+
dynamicModels = parsePlanIncludedModels(incStr)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
178
242
|
} catch {
|
|
179
|
-
/* ignore user metadata failure */
|
|
243
|
+
/* ignore user metadata / plan failure */
|
|
180
244
|
}
|
|
181
245
|
|
|
182
246
|
const result = {
|
|
183
247
|
ok: true,
|
|
184
|
-
plan:
|
|
248
|
+
plan: planDisplayName,
|
|
185
249
|
user: {
|
|
186
250
|
email: userEmail,
|
|
187
251
|
createdAt,
|
|
@@ -191,6 +255,7 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
|
|
|
191
255
|
weekly: weekly || { type: 'weekly', percentUsed: 0, remainingPercent: 100, resetsAt: null },
|
|
192
256
|
monthly: monthly || { type: 'monthly', percentUsed: 0, remainingPercent: 100, resetsAt: null },
|
|
193
257
|
},
|
|
258
|
+
dynamicModels,
|
|
194
259
|
checkedAt: now,
|
|
195
260
|
}
|
|
196
261
|
|