@goodandready/dsh-clinebot 0.3.22 → 0.3.24
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 +19 -1
- package/README.md +2 -2
- package/README.ru.md +2 -2
- package/README.zh.md +2 -2
- package/lib/account-pool.js +12 -4
- package/lib/client.js +50 -86
- package/lib/config.js +41 -12
- package/lib/http.js +31 -0
- package/lib/index.js +2 -2
- package/lib/provider-sync.js +2 -1
- package/lib/routes/auth.js +1 -0
- package/lib/routes/models.js +3 -1
- package/lib/routes/settings.js +2 -0
- package/lib/slash-command.js +1 -1
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.24] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- The settings card opens. Host settings are published as a namespace, copied to plain values before use, and account credential names are read as text, so the form renders instead of a React error.
|
|
13
|
+
|
|
14
|
+
## [0.3.23] - 2026-09-24
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **React Error #185 Infinite Loop Fix (#74 / GitHub #6)**: Restored stable reference constant `SNAPSHOT_READY` in `SettingsPage.getSnapshot` for `useSyncExternalStore`. Returning an inline object literal caused `Object.is` mismatch on every render, triggering an infinite update depth loop (`Maximum update depth exceeded`). The snapshot fallback now returns frozen `SNAPSHOT_READY`, guaranteeing reference stability while keeping the page unblocked in standalone mode.
|
|
18
|
+
- The settings schema marks the user-editable fields volatile, so DSH includes the `dsh-clinebot` namespace and the configuration page is no longer stuck on "host namespace is not ready".
|
|
19
|
+
- Nested fields inside an already volatile array stay plain. The plugin copies host volatile references to plain values before `structuredClone`, so startup does not reject the config or fail to clone a getter.
|
|
20
|
+
- The configuration page renders its own status payload when the host form snapshot is still loading. A missing host namespace still shows the unavailable banner until that payload arrives.
|
|
21
|
+
- Account credential names are read as text. A host volatile reference was sent through as an empty object, and React error #31 replaced the settings card.
|
|
22
|
+
- The settings page binds `configForms.get('dsh-clinebot')` on DSH 0.1.7. The removed `settingsScope` and `lanSettings` services are no longer consulted, so the plugin configuration page can render after install.
|
|
23
|
+
- `GET /dsh-clinebot/status`, `/config`, `/usage`, and `/auth/status` use the same trusted-request check as the write routes. Usage responses keep the quota fields the card shows and omit the raw provider payload.
|
|
24
|
+
- `/cline models` prints the model total in English.
|
|
25
|
+
|
|
8
26
|
## [0.3.22] - 2026-09-24
|
|
9
27
|
|
|
10
28
|
### Fixed
|
|
@@ -186,7 +204,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
186
204
|
## [0.3.4] - 2026-09-08
|
|
187
205
|
|
|
188
206
|
### Fixed
|
|
189
|
-
- **Cache Path Resolution**: Corrected POSIX home directory expansion (`~/`) in `resolvePathWithHome` so `~/.dsh/clinebot-models-cache.json` resolves
|
|
207
|
+
- **Cache Path Resolution**: Corrected POSIX home directory expansion (`~/`) in `resolvePathWithHome` so `~/.dsh/clinebot-models-cache.json` resolves under the user home directory instead of a root-level path.
|
|
190
208
|
|
|
191
209
|
## [0.3.3] - 2026-09-08
|
|
192
210
|
|
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Integrating ClinePass into DeepSeek Harness (DSH) natively poses key challenges:
|
|
|
50
50
|
* 🚀 **One-Click In-App Updater**: Upgrade `@goodandready/dsh-clinebot` directly from the DSH UI or trigger secure loopback updates via `/dsh-clinebot/update`.
|
|
51
51
|
* ⚡ **SWR Quota & Health Caching**: Instantaneous response time (<2ms) on status queries with background revalidation.
|
|
52
52
|
* 🔀 **Smart Quota-Aware Failover**: Automatic multi-account rotation skipping exhausted accounts and recovering when `resetsAt` is reached.
|
|
53
|
-
* 🖥️ **
|
|
53
|
+
* 🖥️ **Plugin configuration page**: Open the installed ClineBot plugin and choose configure. The page shows the credential name, models, quota, and accounts. It is not a separate sidebar section.
|
|
54
54
|
* 🔄 **Dynamic Subscription Model Sync**: Automatically pulls real models included in your ClinePass plan directly from `GET /api/v1/users/me/plan` with one-click DSH provider sync.
|
|
55
55
|
* ⚠️ **Quota Exhaustion Alerts**: Real-time visual warning banners when 5-hour rolling limit reaches 80% (warning) and 95% (exhausted), complete with countdown to reset.
|
|
56
56
|
* 📈 **Session Metrics Telemetry**: Live dashboard tracking request counts, token consumption estimates, latency, and last-request timestamp.
|
|
@@ -118,7 +118,7 @@ graph LR
|
|
|
118
118
|
* **`lib/index.js`**:
|
|
119
119
|
Cordis service module managing routes (including `POST /dsh-clinebot/models/sync`), quota warnings threshold evaluation, credentials, and registering the `/cline` slash command.
|
|
120
120
|
* **`lib/client.js`**:
|
|
121
|
-
|
|
121
|
+
Configuration page on the plugin row (`plugins.row.config`) with live quota bars, an exhaustion warning, session metrics, one-click plan sync, and a fallback plugins card (`settings.plugin.item`). There is no `settings.section` sidebar entry.
|
|
122
122
|
|
|
123
123
|
---
|
|
124
124
|
|
package/README.ru.md
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* 🚀 **Обновление в один клик**: Обновление плагина прямо из интерфейса DSH или через защищённый loopback-эндпоинт `/dsh-clinebot/update`.
|
|
51
51
|
* ⚡ **SWR-кэширование квот и здоровья**: Мгновенный ответ (<2 мс) на запросы статуса с фоновым обновлением данных без блокировки UI.
|
|
52
52
|
* 🔀 **Smart Quota-Aware Failover**: Интеллектуальная ротация аккаунтов с обходом исчерпанных лимитов и авто-восстановлением при наступлении `resetsAt`.
|
|
53
|
-
* 🖥️
|
|
53
|
+
* 🖥️ **Страница конфигурации плагина**: откройте установленный ClineBot и перейдите в настройки плагина. Там имя учётных данных, модели, квота и аккаунты. Отдельного пункта бокового меню нет.
|
|
54
54
|
* 🔄 **Динамическая синхронизация моделей подписки**: автоматическое получение реального списка моделей из `GET /api/v1/users/me/plan` и мгновенное обновление провайдера DSH в один клик.
|
|
55
55
|
* ⚠️ **Предупреждения об исчерпании квоты**: баннеры предупреждения при достижении 80% (внимание) и 95% (исчерпано) 5-часового лимита с таймером сброса.
|
|
56
56
|
* 📈 **Метрики сессии**: учет количества запросов, расчетных токенов (Prompt / Completion), задержки и времени последнего вызова.
|
|
@@ -115,7 +115,7 @@ graph LR
|
|
|
115
115
|
* `smokeChat`: замер задержки и тестовый пинг с фиксацией статистики.
|
|
116
116
|
* `buildPiAiProvider`: генерация конфигурации провайдера для `llm-pi-ai` (`api: 'openai-completions'`).
|
|
117
117
|
* **`lib/index.js`**: сервис Cordis, регистрация системных маршрутов (включая `/dsh-clinebot/models/sync`), расчет порогов предупреждения квоты и слэш-команды `/cline`.
|
|
118
|
-
* **`lib/client.js`**:
|
|
118
|
+
* **`lib/client.js`**: страница конфигурации строки плагина (`plugins.row.config`) с предупреждениями, статистикой сессии, синхронизацией моделей и запасной карточкой (`settings.plugin.item`). Пункта `settings.section` в боковом меню нет.
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
package/README.zh.md
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* 🚀 **应用内一键更新**:直接在 DSH 界面检查并升级插件,或通过受保护的 `/dsh-clinebot/update` 进行本地安全更新。
|
|
51
51
|
* ⚡ **SWR 配额与健康状态缓存**:状态查询毫秒级响应(<2ms),并在后台静默更新,不阻塞前端渲染。
|
|
52
52
|
* 🔀 **智能配额故障转移 (Smart Failover)**:多账号池自动轮询,避开耗尽账号并在 `resetsAt` 到达后自动恢复。
|
|
53
|
-
* 🖥️
|
|
53
|
+
* 🖥️ **插件配置页**:打开已安装的 ClineBot,进入配置。页面包含凭据名、模型、配额和账号,不单独占用侧边栏。
|
|
54
54
|
* 🔄 **订阅模型动态同步**:从官方 `GET /api/v1/users/me/plan` 自动提取真实包含模型,一键原子级同步至 DSH 提供商配置,无需等待插件更新。
|
|
55
55
|
* ⚠️ **额度耗尽实时预警**:当 5 小时滑动窗口达到 80%(警告黄色)和 95%(即将耗尽红色)时展示醒目预警横幅与重置倒计时。
|
|
56
56
|
* 📈 **会话统计与指标看板**:实时追踪请求调用次数、预估 Token(Prompt / Completion)、最近延迟及最后调用时间。
|
|
@@ -115,7 +115,7 @@ graph LR
|
|
|
115
115
|
* `smokeChat`:毫秒级网络探活与非流式延迟测试,并记录会话指标。
|
|
116
116
|
* `buildPiAiProvider`:构建 DSH `llm-pi-ai` 兼容的服务商定义 (`api: 'openai-completions'`)。
|
|
117
117
|
* **`lib/index.js`**:Cordis 插件主生命周期服务,注册后端 REST API 路由(含 `/dsh-clinebot/models/sync`)、额度预警计算与 `/cline` 聊天斜杠指令。
|
|
118
|
-
* **`lib/client.js
|
|
118
|
+
* **`lib/client.js`**:插件行配置页(`plugins.row.config`),含预警横幅、会话指标、一键模型同步,以及后备插件卡片(`settings.plugin.item`)。没有 `settings.section` 侧边栏项。
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
package/lib/account-pool.js
CHANGED
|
@@ -3,8 +3,16 @@ import { resolveKeyValue, usageCache, clearUsageCache, clearProbeCache, DEFAULT_
|
|
|
3
3
|
/**
|
|
4
4
|
* Resolve all accounts in pool with their status and keys.
|
|
5
5
|
*/
|
|
6
|
+
function textSetting(value) {
|
|
7
|
+
let current = value
|
|
8
|
+
while (current && typeof current === 'object' && !Array.isArray(current) && typeof current.get === 'function') {
|
|
9
|
+
current = current.get()
|
|
10
|
+
}
|
|
11
|
+
return typeof current === 'string' ? current.trim() : ''
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export async function resolveAccountPool(ctx, cfg) {
|
|
7
|
-
const apiKeyEnv = cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV
|
|
15
|
+
const apiKeyEnv = textSetting(cfg?.apiKeyEnv) || DEFAULT_API_KEY_ENV
|
|
8
16
|
const defaultSlot = {
|
|
9
17
|
id: 'default',
|
|
10
18
|
label: 'Default',
|
|
@@ -12,16 +20,16 @@ export async function resolveAccountPool(ctx, cfg) {
|
|
|
12
20
|
}
|
|
13
21
|
const accounts = Array.isArray(cfg?.accounts) ? cfg.accounts : []
|
|
14
22
|
const allSlots = [defaultSlot, ...accounts]
|
|
15
|
-
const activeAccount =
|
|
23
|
+
const activeAccount = textSetting(cfg?.activeAccount)
|
|
16
24
|
const resolved = []
|
|
17
25
|
|
|
18
26
|
for (let i = 0; i < allSlots.length; i++) {
|
|
19
27
|
const slot = allSlots[i]
|
|
20
|
-
const envName = slot.apiKeyEnv || (i === 0 ? apiKeyEnv : `CLINEBOT_API_KEY_${i + 1}`)
|
|
28
|
+
const envName = textSetting(slot.apiKeyEnv) || (i === 0 ? apiKeyEnv : `CLINEBOT_API_KEY_${i + 1}`)
|
|
21
29
|
const keyInfo = await resolveKeyValue(ctx, envName)
|
|
22
30
|
resolved.push({
|
|
23
31
|
id: slot.id || (i === 0 ? 'default' : `account-${i + 1}`),
|
|
24
|
-
label: slot.label || (i === 0 ? 'Default' : `Account ${i + 1}`),
|
|
32
|
+
label: textSetting(slot.label) || (i === 0 ? 'Default' : `Account ${i + 1}`),
|
|
25
33
|
apiKeyEnv: envName,
|
|
26
34
|
present: Boolean(keyInfo.value),
|
|
27
35
|
source: keyInfo.source,
|
package/lib/client.js
CHANGED
|
@@ -14,7 +14,11 @@ const NS = 'dsh-clinebot'
|
|
|
14
14
|
const ROW_ID = 'dsh-clinebot'
|
|
15
15
|
const ROW_CONFIG_KEY = '@goodandready/dsh-clinebot#' + ROW_ID
|
|
16
16
|
const ROUTE_PREFIX = '/dsh-clinebot'
|
|
17
|
-
const SNAPSHOT_LOADING = { status: 'loading', view: null }
|
|
17
|
+
const SNAPSHOT_LOADING = Object.freeze({ status: 'loading', view: null })
|
|
18
|
+
// Stable (frozen) fallback reference: useSyncExternalStore compares snapshots by
|
|
19
|
+
// identity (Object.is), so returning a fresh object literal causes an infinite render loop
|
|
20
|
+
// (React error #185 "Maximum update depth exceeded"). Keep this module-level.
|
|
21
|
+
const SNAPSHOT_READY = Object.freeze({ status: 'ready', view: null })
|
|
18
22
|
|
|
19
23
|
// --- src/client/locales.js ---
|
|
20
24
|
const en = {
|
|
@@ -534,12 +538,14 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
|
|
|
534
538
|
'tbody',
|
|
535
539
|
null,
|
|
536
540
|
status.accounts.map((acc) => {
|
|
537
|
-
const
|
|
541
|
+
const envName = typeof acc.apiKeyEnv === 'string' ? acc.apiKeyEnv : ''
|
|
542
|
+
const label = typeof acc.label === 'string' ? acc.label : ''
|
|
543
|
+
const isActive = status.activeAccount === envName || (!status.activeAccount && acc.id === 'default')
|
|
538
544
|
return React.createElement(
|
|
539
545
|
'tr',
|
|
540
546
|
{ key: acc.id },
|
|
541
|
-
React.createElement('td', null, React.createElement('strong', null,
|
|
542
|
-
React.createElement('td', null, React.createElement('code', null,
|
|
547
|
+
React.createElement('td', null, React.createElement('strong', null, label)),
|
|
548
|
+
React.createElement('td', null, React.createElement('code', null, envName)),
|
|
543
549
|
React.createElement(
|
|
544
550
|
'td',
|
|
545
551
|
null,
|
|
@@ -561,7 +567,7 @@ function AccountsSection({ status, busy, handlePinAccount, t }) {
|
|
|
561
567
|
className: 'cb-btn',
|
|
562
568
|
style: { padding: '4px 8px', fontSize: '11px' },
|
|
563
569
|
disabled: !!busy,
|
|
564
|
-
onClick: () => handlePinAccount(
|
|
570
|
+
onClick: () => handlePinAccount(envName),
|
|
565
571
|
},
|
|
566
572
|
t('accounts.pin_btn')
|
|
567
573
|
)
|
|
@@ -834,36 +840,29 @@ Response: ${smokeResult.preview || '(empty)'}`
|
|
|
834
840
|
}
|
|
835
841
|
|
|
836
842
|
// --- src/client/settings-page.js ---
|
|
843
|
+
function readConfigForms(ctx) {
|
|
844
|
+
if (!ctx || typeof ctx.get !== 'function') return undefined
|
|
845
|
+
try {
|
|
846
|
+
return ctx.get('configForms') || undefined
|
|
847
|
+
} catch (err) {
|
|
848
|
+
console.warn('[dsh-clinebot] configForms is not available:', err)
|
|
849
|
+
return undefined
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
837
853
|
function SettingsPage(props) {
|
|
838
854
|
const ctx = props?.ctx
|
|
839
855
|
const t = props?.t || makeT(en, en)
|
|
840
856
|
|
|
841
857
|
const scope = React.useMemo(() => {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
if (typeof ctx?.get === 'function') {
|
|
845
|
-
s = ctx.get('lanSettings') || ctx.get('settingsScope')
|
|
846
|
-
}
|
|
847
|
-
} catch (_) {
|
|
848
|
-
s = null
|
|
849
|
-
}
|
|
850
|
-
if (!s) {
|
|
851
|
-
try {
|
|
852
|
-
if (ctx && typeof ctx === 'object') {
|
|
853
|
-
s = ctx.lanSettings || ctx.settingsScope || null
|
|
854
|
-
}
|
|
855
|
-
} catch (_) {
|
|
856
|
-
s = null
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
if (!s) return undefined
|
|
858
|
+
const svc = readConfigForms(ctx)
|
|
859
|
+
if (!svc || typeof svc.get !== 'function') return undefined
|
|
860
860
|
try {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
861
|
+
return svc.get(NS)
|
|
862
|
+
} catch (err) {
|
|
863
|
+
console.warn('[dsh-clinebot] configForms.get failed:', err)
|
|
864
864
|
return undefined
|
|
865
865
|
}
|
|
866
|
-
return undefined
|
|
867
866
|
}, [ctx])
|
|
868
867
|
|
|
869
868
|
const subscribe = React.useMemo(() => {
|
|
@@ -878,18 +877,19 @@ function SettingsPage(props) {
|
|
|
878
877
|
}, [scope])
|
|
879
878
|
|
|
880
879
|
const getSnapshot = React.useCallback(() => {
|
|
881
|
-
if (!scope?.getSnapshot) return
|
|
880
|
+
if (!scope?.getSnapshot) return SNAPSHOT_READY
|
|
882
881
|
try {
|
|
883
|
-
return scope.getSnapshot() ||
|
|
884
|
-
} catch (
|
|
885
|
-
|
|
882
|
+
return scope.getSnapshot() || SNAPSHOT_READY
|
|
883
|
+
} catch (err) {
|
|
884
|
+
console.warn('[dsh-clinebot] settings snapshot failed:', err)
|
|
885
|
+
return SNAPSHOT_READY
|
|
886
886
|
}
|
|
887
887
|
}, [scope])
|
|
888
888
|
|
|
889
889
|
const snapshot = React.useSyncExternalStore(
|
|
890
890
|
subscribe,
|
|
891
891
|
getSnapshot,
|
|
892
|
-
React.useCallback(() =>
|
|
892
|
+
React.useCallback(() => SNAPSHOT_READY, [])
|
|
893
893
|
)
|
|
894
894
|
const snapshotStatus = snapshot?.status || 'loading'
|
|
895
895
|
|
|
@@ -1198,15 +1198,14 @@ function SettingsPage(props) {
|
|
|
1198
1198
|
debounceSaveDisabledModels(nextDisabled)
|
|
1199
1199
|
}
|
|
1200
1200
|
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
if (!status || !draft || snapshotStatus === 'loading') {
|
|
1201
|
+
if (!status || !draft) {
|
|
1202
|
+
if (snapshotStatus === 'unavailable') {
|
|
1203
|
+
return React.createElement(
|
|
1204
|
+
'div',
|
|
1205
|
+
{ className: 'cb-page' },
|
|
1206
|
+
React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
|
|
1207
|
+
)
|
|
1208
|
+
}
|
|
1210
1209
|
if (err) {
|
|
1211
1210
|
return React.createElement(
|
|
1212
1211
|
'div',
|
|
@@ -1386,49 +1385,14 @@ function PluginCard(props) {
|
|
|
1386
1385
|
}
|
|
1387
1386
|
|
|
1388
1387
|
// --- src/client/entry.js ---
|
|
1389
|
-
function
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
} catch (_) {}
|
|
1397
|
-
try {
|
|
1398
|
-
if (ctx && typeof ctx === 'object') {
|
|
1399
|
-
return ctx.lanSettings || ctx.settingsScope || null
|
|
1400
|
-
}
|
|
1401
|
-
} catch (_) {}
|
|
1402
|
-
return null
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
const s = getSettingsService()
|
|
1406
|
-
if (!s) return
|
|
1407
|
-
|
|
1408
|
-
const visible = () => {
|
|
1409
|
-
try {
|
|
1410
|
-
const current = getSettingsService()
|
|
1411
|
-
const view = current?.describe?.()?.getSnapshot?.()?.view
|
|
1412
|
-
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
|
|
1413
|
-
} catch (_) {
|
|
1414
|
-
return false
|
|
1415
|
-
}
|
|
1388
|
+
function ensureSettingsForm(ctx) {
|
|
1389
|
+
const svc = readConfigForms(ctx)
|
|
1390
|
+
if (!svc || typeof svc.get !== 'function') return
|
|
1391
|
+
try {
|
|
1392
|
+
svc.get(NS)
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
console.warn('[dsh-clinebot] configForms.get failed:', err)
|
|
1416
1395
|
}
|
|
1417
|
-
if (visible()) return
|
|
1418
|
-
|
|
1419
|
-
if (typeof setInterval !== 'function') return
|
|
1420
|
-
|
|
1421
|
-
let tries = 0
|
|
1422
|
-
const timer = setInterval(() => {
|
|
1423
|
-
if (visible() || tries >= 15) { clearInterval(timer); return }
|
|
1424
|
-
tries += 1
|
|
1425
|
-
try {
|
|
1426
|
-
const current = getSettingsService()
|
|
1427
|
-
current?.describe?.()?.load?.()
|
|
1428
|
-
} catch (e) {
|
|
1429
|
-
console.debug?.('[dsh-clinebot] Polling settings mirror:', e)
|
|
1430
|
-
}
|
|
1431
|
-
}, 1000)
|
|
1432
1396
|
}
|
|
1433
1397
|
|
|
1434
1398
|
function apply(ctx) {
|
|
@@ -1453,8 +1417,8 @@ function apply(ctx) {
|
|
|
1453
1417
|
|
|
1454
1418
|
if (typeof ctx.effect === 'function') {
|
|
1455
1419
|
ctx.effect(
|
|
1456
|
-
() =>
|
|
1457
|
-
'dsh-clinebot:
|
|
1420
|
+
() => { ensureSettingsForm(ctx) },
|
|
1421
|
+
'dsh-clinebot: open the settings form for this namespace',
|
|
1458
1422
|
)
|
|
1459
1423
|
}
|
|
1460
1424
|
|
|
@@ -1532,7 +1496,7 @@ function apply(ctx) {
|
|
|
1532
1496
|
)
|
|
1533
1497
|
}
|
|
1534
1498
|
|
|
1535
|
-
module.exports = { apply, inject: ['slots', 'locale'] }
|
|
1499
|
+
module.exports = { apply, inject: ['slots', 'locale', 'configForms'] }
|
|
1536
1500
|
|
|
1537
1501
|
return module.exports
|
|
1538
1502
|
},
|
package/lib/config.js
CHANGED
|
@@ -13,22 +13,32 @@ import {
|
|
|
13
13
|
getActiveModelIds,
|
|
14
14
|
} from './models.js'
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
// DSH settings.describe only publishes fields marked volatile. Without them the
|
|
18
|
+
// namespace is omitted and the configuration page stays unavailable.
|
|
19
|
+
if (typeof z.prototype?.volatile !== 'function') {
|
|
20
|
+
z.prototype.volatile = function volatile() {
|
|
21
|
+
if (this.meta && this.meta.volatile) throw new TypeError('volatile schema is already wrapped')
|
|
22
|
+
return typeof this.extra === 'function' ? this.extra('volatile', true) : this
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export const NS = 'dsh-clinebot'
|
|
17
27
|
export const LLM_PI_AI_NS = 'llm-pi-ai'
|
|
18
28
|
|
|
19
29
|
export const Config = z.object({
|
|
20
30
|
enabled: z.boolean().default(true)
|
|
21
|
-
.description('When true, ClineBot is registered as a model provider in DSH.'),
|
|
31
|
+
.description('When true, ClineBot is registered as a model provider in DSH.').volatile(),
|
|
22
32
|
baseUrl: z.string().default(DEFAULT_BASE_URL)
|
|
23
|
-
.description('Base API URL (default: https://api.cline.bot/api/v1).'),
|
|
33
|
+
.description('Base API URL (default: https://api.cline.bot/api/v1).').volatile(),
|
|
24
34
|
apiKeyEnv: z.string().default(DEFAULT_API_KEY_ENV)
|
|
25
|
-
.description('Credential / env name containing the ClinePass API key (never store key directly here).'),
|
|
35
|
+
.description('Credential / env name containing the ClinePass API key (never store key directly here).').volatile(),
|
|
26
36
|
defaultModel: z.string().default(DEFAULT_MODEL_ID)
|
|
27
|
-
.description('Default model ID for chat and smoke tests.'),
|
|
37
|
+
.description('Default model ID for chat and smoke tests.').volatile(),
|
|
28
38
|
disabledModels: z.array(z.string()).default([])
|
|
29
|
-
.description('List of model IDs explicitly disabled by the user (new models are enabled automatically).'),
|
|
39
|
+
.description('List of model IDs explicitly disabled by the user (new models are enabled automatically).').volatile(),
|
|
30
40
|
enabledModels: z.array(z.string()).default([])
|
|
31
|
-
.description('Deprecated: preserved for backwards compatibility with earlier versions.'),
|
|
41
|
+
.description('Deprecated: preserved for backwards compatibility with earlier versions.').volatile(),
|
|
32
42
|
dynamicModels: z.array(z.object({
|
|
33
43
|
id: z.string(),
|
|
34
44
|
name: z.string(),
|
|
@@ -40,23 +50,42 @@ export const Config = z.object({
|
|
|
40
50
|
isCustom: z.boolean().default(false),
|
|
41
51
|
reasoningEfforts: z.any().default(undefined),
|
|
42
52
|
})).default([])
|
|
43
|
-
.description('Models automatically discovered from the official ClinePass subscription plan.'),
|
|
53
|
+
.description('Models automatically discovered from the official ClinePass subscription plan.').volatile(),
|
|
44
54
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
|
|
45
|
-
.description('HTTP probe timeout in milliseconds.'),
|
|
55
|
+
.description('HTTP probe timeout in milliseconds.').volatile(),
|
|
46
56
|
smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
|
|
47
|
-
.description('Timeout for smoke chat completions in milliseconds.'),
|
|
57
|
+
.description('Timeout for smoke chat completions in milliseconds.').volatile(),
|
|
48
58
|
modelsCachePath: z.string().default('~/.dsh/clinebot-models-cache.json')
|
|
49
|
-
.description('Local on-disk cache path for models snapshot.'),
|
|
59
|
+
.description('Local on-disk cache path for models snapshot.').volatile(),
|
|
50
60
|
accounts: z.array(z.object({
|
|
51
61
|
label: z.string().default(''),
|
|
52
62
|
apiKeyEnv: z.string(),
|
|
53
63
|
})).default([])
|
|
54
|
-
.description('Additional accounts for multi-account failover and rate limit rotation.'),
|
|
64
|
+
.description('Additional accounts for multi-account failover and rate limit rotation.').volatile(),
|
|
55
65
|
activeAccount: z.string().default('')
|
|
56
|
-
.description('Manually pinned active account envName or empty for auto/default.'),
|
|
66
|
+
.description('Manually pinned active account envName or empty for auto/default.').volatile(),
|
|
57
67
|
})
|
|
58
68
|
|
|
69
|
+
|
|
70
|
+
function isVolatileRef(value) {
|
|
71
|
+
return !!value && typeof value === 'object' && !Array.isArray(value) && typeof value.get === 'function'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// DSH stores each volatile field as { get }. structuredClone cannot copy that
|
|
75
|
+
// getter, so callers need a plain snapshot before validation.
|
|
76
|
+
export function plainConfig(cfg) {
|
|
77
|
+
if (isVolatileRef(cfg)) return plainConfig(cfg.get())
|
|
78
|
+
if (!cfg || typeof cfg !== 'object') return cfg
|
|
79
|
+
const out = {}
|
|
80
|
+
for (const key of Object.keys(cfg)) {
|
|
81
|
+
const value = cfg[key]
|
|
82
|
+
out[key] = isVolatileRef(value) ? value.get() : value
|
|
83
|
+
}
|
|
84
|
+
return out
|
|
85
|
+
}
|
|
86
|
+
|
|
59
87
|
export function publicConfig(cfg) {
|
|
88
|
+
cfg = plainConfig(cfg)
|
|
60
89
|
const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
|
|
61
90
|
const allDefaultIds = getDefaultModelIds(dynamic)
|
|
62
91
|
|
package/lib/http.js
CHANGED
|
@@ -92,3 +92,34 @@ export function isTrustedSettingsRequest(request) {
|
|
|
92
92
|
|
|
93
93
|
return false
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
export function publicUsage(usage) {
|
|
97
|
+
if (!usage || typeof usage !== 'object') return null
|
|
98
|
+
const windowView = (window) => {
|
|
99
|
+
if (!window || typeof window !== 'object') return null
|
|
100
|
+
return {
|
|
101
|
+
percentUsed: window.percentUsed,
|
|
102
|
+
remainingPercent: window.remainingPercent,
|
|
103
|
+
resetsAt: window.resetsAt || null,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (usage.ok === false) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: typeof usage.error === 'string' ? usage.error : 'usage unavailable',
|
|
110
|
+
checkedAt: usage.checkedAt,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
plan: usage.plan || null,
|
|
116
|
+
user: { email: usage.user?.email || null },
|
|
117
|
+
windows: {
|
|
118
|
+
fiveHour: windowView(usage.windows?.fiveHour),
|
|
119
|
+
weekly: windowView(usage.windows?.weekly),
|
|
120
|
+
monthly: windowView(usage.windows?.monthly),
|
|
121
|
+
},
|
|
122
|
+
checkedAt: usage.checkedAt,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Config, publicConfig, NS, LLM_PI_AI_NS } from './config.js'
|
|
1
|
+
import { Config, publicConfig, plainConfig, NS, LLM_PI_AI_NS } from './config.js'
|
|
2
2
|
import { registerPluginUpdater } from './updater.js'
|
|
3
3
|
import {
|
|
4
4
|
sessionStats,
|
|
@@ -30,7 +30,7 @@ export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSess
|
|
|
30
30
|
|
|
31
31
|
export function apply(ctx, config) {
|
|
32
32
|
let getConfig = () => config
|
|
33
|
-
const live = () => (getConfig() ? Config(structuredClone(getConfig())) : config)
|
|
33
|
+
const live = () => (getConfig() ? Config(structuredClone(plainConfig(getConfig()))) : config)
|
|
34
34
|
let settingsApi
|
|
35
35
|
|
|
36
36
|
const syncProviderState = async (cfg) => {
|
package/lib/provider-sync.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from 'node:os'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { publicConfig, Config, LLM_PI_AI_NS } from './config.js'
|
|
4
|
+
import { publicUsage } from './http.js'
|
|
4
5
|
import {
|
|
5
6
|
PROVIDER_ID,
|
|
6
7
|
PROVIDER_DISPLAY_NAME,
|
|
@@ -109,7 +110,7 @@ export async function buildStatus(ctx, cfg) {
|
|
|
109
110
|
})),
|
|
110
111
|
activeAccount: activeAcc.apiKeyEnv,
|
|
111
112
|
health,
|
|
112
|
-
usage,
|
|
113
|
+
usage: publicUsage(usage),
|
|
113
114
|
quotaWarning,
|
|
114
115
|
sessionStats: { ...sessionStats },
|
|
115
116
|
isRegistered,
|
package/lib/routes/auth.js
CHANGED
|
@@ -38,6 +38,7 @@ export function registerAuthRoutes(ctx, { live, getSettingsApi, syncProviderStat
|
|
|
38
38
|
path: '/dsh-clinebot/auth/status',
|
|
39
39
|
handler: async (req, res) => {
|
|
40
40
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
41
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
41
42
|
writeJson(res, 200, {
|
|
42
43
|
ok: true,
|
|
43
44
|
status: authSession?.state || 'idle',
|
package/lib/routes/models.js
CHANGED
|
@@ -4,6 +4,7 @@ import { publicConfig, Config } from '../config.js'
|
|
|
4
4
|
import { resolveActiveAccountKey, resolvePathWithHome } from '../provider-sync.js'
|
|
5
5
|
import { getAllModels, saveModelsDiskCache } from '../models.js'
|
|
6
6
|
import { fetchUsageLimits } from '../cline-client.js'
|
|
7
|
+
import { publicUsage } from '../http.js'
|
|
7
8
|
|
|
8
9
|
export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderState }) {
|
|
9
10
|
// POST /dsh-clinebot/models/sync — dynamically load models from plan
|
|
@@ -103,6 +104,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
103
104
|
path: '/dsh-clinebot/usage',
|
|
104
105
|
handler: async (req, res) => {
|
|
105
106
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
107
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
106
108
|
try {
|
|
107
109
|
const pub = publicConfig(live())
|
|
108
110
|
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
@@ -113,7 +115,7 @@ export function registerModelsRoutes(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
113
115
|
timeoutMs: pub.timeoutMs,
|
|
114
116
|
bypassCache: true,
|
|
115
117
|
})
|
|
116
|
-
writeJson(res, usageData.ok ? 200 : 502, usageData)
|
|
118
|
+
writeJson(res, usageData.ok ? 200 : 502, publicUsage(usageData))
|
|
117
119
|
} catch (err) {
|
|
118
120
|
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
119
121
|
}
|
package/lib/routes/settings.js
CHANGED
|
@@ -11,6 +11,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
11
11
|
path: '/dsh-clinebot/status',
|
|
12
12
|
handler: async (req, res) => {
|
|
13
13
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
14
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
14
15
|
try {
|
|
15
16
|
const st = await buildStatus(ctx, live())
|
|
16
17
|
writeJson(res, 200, st)
|
|
@@ -26,6 +27,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
26
27
|
path: '/dsh-clinebot/config',
|
|
27
28
|
handler: async (req, res) => {
|
|
28
29
|
if (req.method === 'GET') {
|
|
30
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
29
31
|
return writeJson(res, 200, { ok: true, config: publicConfig(live()) })
|
|
30
32
|
}
|
|
31
33
|
if (req.method !== 'PUT') {
|
package/lib/slash-command.js
CHANGED
|
@@ -39,7 +39,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
39
39
|
const disabledSet = new Set(pub.disabledModels || [])
|
|
40
40
|
const lines = [
|
|
41
41
|
'### 🎯 ClinePass Models Catalog',
|
|
42
|
-
`*
|
|
42
|
+
`* **Total models**: ${allModels.length} (${allModels.filter((m) => !disabledSet.has(m.id)).length} active)`,
|
|
43
43
|
'',
|
|
44
44
|
]
|
|
45
45
|
for (const m of allModels) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,11 @@
|
|
|
49
49
|
},
|
|
50
50
|
"client": {
|
|
51
51
|
"platform": "web",
|
|
52
|
-
"inject": [
|
|
52
|
+
"inject": [
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
54
|
+
"@deepseek-ai/dsh-client-locale",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
56
|
+
]
|
|
53
57
|
}
|
|
54
58
|
},
|
|
55
59
|
"peerDependencies": {
|