@goodandready/dsh-clinebot 0.5.8 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +29 -16
- package/README.ru.md +29 -16
- package/README.zh.md +29 -16
- package/lib/client.js +169 -69
- package/lib/provider-sync.js +12 -2
- package/lib/updater.js +83 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.5.9] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Updater Timeout Exit & SIGKILL Escalation (Issue #172)**: Ensured `installExact` child process cleanup waits for the operating system process to fully terminate via `exit`/`close` events before releasing profile locks or rejecting update promises. On timeout, sends `SIGTERM`, waits for a grace period, and escalates to `SIGKILL` for stubborn subprocesses.
|
|
12
|
+
- **Proxy Client Disconnection SSE Abort Fixture (Issue #173)**: Validated client disconnection handling against a real Node.js loopback HTTP fixture with client TCP stream destruction, asserting upstream `reader.cancel()` and fetch `signal.aborted` without relying exclusively on synthetic mocks.
|
|
13
|
+
- **Dependency Manifest & Lockfile Synchronization (Issue #174)**: Regenerated `package-lock.json` via clean npm resolution for all 21 dependencies to ensure deterministic, clean `npm ci` builds across environments, while maintaining proper exclusion of the lockfile from package archives.
|
|
14
|
+
- **UI Locale Key Completeness & Reasoning/Quota Strings (Issue #175)**: Resolved missing locale keys across `en` and `zh` dictionaries for structured quota countdowns/warnings, reasoning tags and effort selectors, capability badges, smoke test diagnostic outputs, and ErrorBoundary alerts. Added regression test asserting 100% dictionary completeness and symmetry for client `t()` calls.
|
|
15
|
+
- **Update Check State Machine & Error Handling (Issue #176)**: Implemented explicit update state machine (`idle`, `checking`, `current`, `available`, `error`) in `UpdateBanner` and `settings-page.js`. Eliminated false "✓ Up to date" status indicators on failed network checks or when `latestCheckFailed: true`, and added retry action.
|
|
16
|
+
- **Documentation & Design Contract Modernization (Issue #177)**: Synchronized `README.md`, `README.ru.md`, `README.zh.md`, and `docs/design/DESIGN.md` with active runtime contracts: documented dynamic subscription catalog, custom models, active configuration schema (`disabledModels`, `proxyMode`, `accounts`), corrected route prefixes (`/dsh-clinebot/*`), and documented all 4 registered UI seats and release testing workflows.
|
|
17
|
+
|
|
8
18
|
## [0.5.8] - 2026-09-30
|
|
9
19
|
|
|
10
20
|
### Fixed
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ graph LR
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
subgraph PluginHost [dsh-clinebot Host Runtime]
|
|
77
|
-
HttpEndpoints["API: /
|
|
77
|
+
HttpEndpoints["API: /dsh-clinebot/*"]
|
|
78
78
|
ClientCore["lib/cline-client.js"]
|
|
79
79
|
ModelCatalog["lib/models.js (Curated + Dynamic Plan)"]
|
|
80
80
|
SlashCmd["Command: /cline"]
|
|
@@ -108,7 +108,7 @@ graph LR
|
|
|
108
108
|
## ✨ Features & Module Breakdown
|
|
109
109
|
|
|
110
110
|
* **`lib/models.js`**:
|
|
111
|
-
Manages the
|
|
111
|
+
Manages the dynamic subscription catalog and fallback models with full reasoning effort mappings (`off: null`, `low`, `medium`, `high`, `max`), upstream 200k context limit declarations, user-defined custom models, and dynamic subscription plan model parsing (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
|
|
112
112
|
* **`lib/cline-client.js`**:
|
|
113
113
|
* `fetchUsageLimits`: queries `GET /users/me/plan/usage-limits`, `GET /users/me/plan`, and `GET /users/me` with in-memory caching.
|
|
114
114
|
* `sessionStats` / `recordSessionRequest`: in-memory telemetry recording requests count, tokens, latency, and timestamps.
|
|
@@ -163,29 +163,42 @@ dsh-clinebot:
|
|
|
163
163
|
enabled: true
|
|
164
164
|
baseUrl: https://api.cline.bot/api/v1
|
|
165
165
|
apiKeyEnv: CLINEBOT_API_KEY
|
|
166
|
-
defaultModel:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
166
|
+
defaultModel: your-default-model
|
|
167
|
+
proxyMode: true
|
|
168
|
+
disabledModels: []
|
|
169
|
+
customModels: []
|
|
170
|
+
modelReasoningDefaults:
|
|
171
|
+
your-reasoning-model: high
|
|
172
|
+
modelContextOverrides: []
|
|
173
|
+
statsPath: ~/.dsh/clinebot-stats.json
|
|
174
|
+
timeoutMs: 30000
|
|
175
|
+
smokeTimeoutMs: 60000
|
|
176
|
+
streamIdleTimeoutMs: 60000
|
|
177
|
+
accounts:
|
|
178
|
+
- label: Team Key
|
|
179
|
+
apiKeyEnv: CLINEBOT_API_KEY_2
|
|
175
180
|
```
|
|
176
181
|
|
|
177
182
|
### Configuration Parameters
|
|
178
183
|
|
|
179
184
|
| Parameter | Type | Default | Description |
|
|
180
185
|
|:---|:---|:---|:---|
|
|
181
|
-
| `enabled` | `boolean` | `true` | Enable or disable the ClineBot provider bridge |
|
|
186
|
+
| `enabled` | `boolean` | `true` | Enable or disable the ClineBot provider bridge in DSH |
|
|
182
187
|
| `baseUrl` | `string` | `"https://api.cline.bot/api/v1"` | ClinePass OpenAI-compatible base URL |
|
|
183
188
|
| `apiKeyEnv` | `string` | `"CLINEBOT_API_KEY"` | Environment variable / credentials key name |
|
|
184
|
-
| `defaultModel` | `string` | `"
|
|
189
|
+
| `defaultModel` | `string` | `"claude-3-7-sonnet"` | Default selected model ID for chat and smoke tests |
|
|
190
|
+
| `disabledModels` | `array` | `[]` | List of model IDs hidden from the DSH chat picker (new subscription models are auto-enabled) |
|
|
191
|
+
| `customModels` | `array` | `[]` | User-defined gateway models (`[{ id, name, contextLength, maxTokens, category, isReasoning }]`) |
|
|
192
|
+
| `modelReasoningDefaults` | `object` | `{}` | Configured default reasoning effort per model (`low`, `medium`, `high`, `max`) |
|
|
185
193
|
| `modelContextOverrides` | `array` | `[]` | User-defined model context length and max token overrides |
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
194
|
+
| `proxyMode` | `boolean` | `true` | Enable local transparent loopback proxy for sticky sessions and automatic 429 failover |
|
|
195
|
+
| `statsPath` | `string` | `"~/.dsh/clinebot-stats.json"` | Persistent on-disk path for token usage analytics |
|
|
196
|
+
| `accounts` | `array` | `[]` | Additional accounts for multi-account failover and quota rotation (`[{ label, apiKeyEnv }]`) |
|
|
197
|
+
| `activeAccount` | `string` | `""` | Manually pinned active account env name (empty for least-used auto routing) |
|
|
198
|
+
| `timeoutMs` | `number` | `30000` | HTTP probe timeout in milliseconds |
|
|
199
|
+
| `smokeTimeoutMs` | `number` | `60000` | Timeout for smoke test chat completions in milliseconds |
|
|
200
|
+
| `streamIdleTimeoutMs` | `number` | `60000` | Idle timeout between SSE stream chunks in milliseconds |
|
|
201
|
+
| `enabledModels` | `array` | *(deprecated)* | Read-only in public config; rejected on `PUT /config` in favor of `disabledModels` |
|
|
189
202
|
| `dynamicModels` | `array` | `[]` | Dynamic models automatically synced from the official plan |
|
|
190
203
|
|
|
191
204
|
---
|
package/README.ru.md
CHANGED
|
@@ -74,7 +74,7 @@ graph LR
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
subgraph PluginHost [Хост-часть dsh-clinebot]
|
|
77
|
-
HttpEndpoints["API: /
|
|
77
|
+
HttpEndpoints["API: /dsh-clinebot/*"]
|
|
78
78
|
ClientCore["lib/cline-client.js"]
|
|
79
79
|
ModelCatalog["lib/models.js (Встроенные + Динамические из плана)"]
|
|
80
80
|
SlashCmd["Слэш-команда: /cline"]
|
|
@@ -107,7 +107,7 @@ graph LR
|
|
|
107
107
|
|
|
108
108
|
## ✨ Структура модулей и возможности
|
|
109
109
|
|
|
110
|
-
* **`lib/models.js`**: каталог
|
|
110
|
+
* **`lib/models.js`**: динамический каталог моделей подписки и встроенных шаблонов с поддержкой уровней рассуждения (`Off` / `Low` / `Medium` / `High` / `Max`), контекстных лимитов, пользовательских моделей (`customModels`) и парсинга тарифа (`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`).
|
|
111
111
|
* **`lib/cline-client.js`**:
|
|
112
112
|
* `fetchUsageLimits`: параллельный опрос `GET /users/me/plan/usage-limits`, `GET /users/me/plan` и `GET /users/me` с кэшированием в памяти.
|
|
113
113
|
* `sessionStats` / `recordSessionRequest`: счетчики сессии (запросы, токены, задержка, время).
|
|
@@ -158,29 +158,42 @@ dsh-clinebot:
|
|
|
158
158
|
enabled: true
|
|
159
159
|
baseUrl: https://api.cline.bot/api/v1
|
|
160
160
|
apiKeyEnv: CLINEBOT_API_KEY
|
|
161
|
-
defaultModel:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
161
|
+
defaultModel: your-default-model
|
|
162
|
+
proxyMode: true
|
|
163
|
+
disabledModels: []
|
|
164
|
+
customModels: []
|
|
165
|
+
modelReasoningDefaults:
|
|
166
|
+
your-reasoning-model: high
|
|
167
|
+
modelContextOverrides: []
|
|
168
|
+
statsPath: ~/.dsh/clinebot-stats.json
|
|
169
|
+
timeoutMs: 30000
|
|
170
|
+
smokeTimeoutMs: 60000
|
|
171
|
+
streamIdleTimeoutMs: 60000
|
|
172
|
+
accounts:
|
|
173
|
+
- label: Team Key
|
|
174
|
+
apiKeyEnv: CLINEBOT_API_KEY_2
|
|
170
175
|
```
|
|
171
176
|
|
|
172
177
|
### Параметры конфигурации
|
|
173
178
|
|
|
174
179
|
| Параметр | Тип | По умолчанию | Описание |
|
|
175
180
|
|:---|:---|:---|:---|
|
|
176
|
-
| `enabled` | `boolean` | `true` | Включение моста провайдера ClineBot |
|
|
181
|
+
| `enabled` | `boolean` | `true` | Включение моста провайдера ClineBot в DSH |
|
|
177
182
|
| `baseUrl` | `string` | `"https://api.cline.bot/api/v1"` | Базовый URL OpenAI-совместимого API ClinePass |
|
|
178
183
|
| `apiKeyEnv` | `string` | `"CLINEBOT_API_KEY"` | Имя переменной / ключа в хранилище credentials |
|
|
179
|
-
| `defaultModel` | `string` | `"
|
|
184
|
+
| `defaultModel` | `string` | `"claude-3-7-sonnet"` | Модель, выбираемая по умолчанию для чата и smoke-тестов |
|
|
185
|
+
| `disabledModels` | `array` | `[]` | Список отключённых моделей (новые модели подписки включаются автоматически) |
|
|
186
|
+
| `customModels` | `array` | `[]` | Пользовательские модели шлюза (`[{ id, name, contextLength, maxTokens, category, isReasoning }]`) |
|
|
187
|
+
| `modelReasoningDefaults` | `object` | `{}` | Значения thinking effort по умолчанию для моделей (`low`, `medium`, `high`, `max`) |
|
|
180
188
|
| `modelContextOverrides` | `array` | `[]` | Пользовательские переопределения размера контекста и лимита токенов |
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
189
|
+
| `proxyMode` | `boolean` | `true` | Включение локального прозрачного loopback-прокси с сессионным роутингом и failover |
|
|
190
|
+
| `statsPath` | `string` | `"~/.dsh/clinebot-stats.json"` | Путь к файлу персистентной аналитики использования токенов |
|
|
191
|
+
| `accounts` | `array` | `[]` | Дополнительные аккаунты для ротации квоты и failover (`[{ label, apiKeyEnv }]`) |
|
|
192
|
+
| `activeAccount` | `string` | `""` | Принудительно закреплённый аккаунт (пусто для автоматического выбора least-used) |
|
|
193
|
+
| `timeoutMs` | `number` | `30000` | Таймаут HTTP-запросов (мс) |
|
|
194
|
+
| `smokeTimeoutMs` | `number` | `60000` | Таймаут тестового пинга инференса (мс) |
|
|
195
|
+
| `streamIdleTimeoutMs` | `number` | `60000` | Таймаут ожидания между чанками SSE-потока (мс) |
|
|
196
|
+
| `enabledModels` | `array` | *(устарело)* | Только для чтения в public config; отклоняется при `PUT /config` в пользу `disabledModels` |
|
|
184
197
|
| `dynamicModels` | `array` | `[]` | Динамические модели, автоматически синхронизированные из тарифа |
|
|
185
198
|
|
|
186
199
|
---
|
package/README.zh.md
CHANGED
|
@@ -74,7 +74,7 @@ graph LR
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
subgraph PluginHost [dsh-clinebot 宿主运行环境]
|
|
77
|
-
HttpEndpoints["API 路由: /
|
|
77
|
+
HttpEndpoints["API 路由: /dsh-clinebot/*"]
|
|
78
78
|
ClientCore["lib/cline-client.js"]
|
|
79
79
|
ModelCatalog["lib/models.js (内置精选 + 动态订阅解析)"]
|
|
80
80
|
SlashCmd["斜杠指令: /cline"]
|
|
@@ -107,7 +107,7 @@ graph LR
|
|
|
107
107
|
|
|
108
108
|
## ✨ 核心模块与功能
|
|
109
109
|
|
|
110
|
-
* **`lib/models.js
|
|
110
|
+
* **`lib/models.js`**:管理动态订阅模型目录与内置精选模板(提供完整的 `Off` / `Low` / `Medium` / `High` / `Max` 思考强度映射、上游 200k 上下文容量标注、自定义模型支持)以及套餐模型动态解析器(`parsePlanIncludedModels`, `getAllModels`, `getDynamicModels`)。
|
|
111
111
|
* **`lib/cline-client.js`**:
|
|
112
112
|
* `fetchUsageLimits`:高效并发轮询 `GET /users/me/plan/usage-limits`、`GET /users/me/plan` 与 `GET /users/me` 并进行内存缓存。
|
|
113
113
|
* `sessionStats` / `recordSessionRequest`:内存级会话度量记录器(请求次数、Token 估算、延迟、时间戳)。
|
|
@@ -160,29 +160,42 @@ dsh-clinebot:
|
|
|
160
160
|
enabled: true
|
|
161
161
|
baseUrl: https://api.cline.bot/api/v1
|
|
162
162
|
apiKeyEnv: CLINEBOT_API_KEY
|
|
163
|
-
defaultModel:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
163
|
+
defaultModel: your-default-model
|
|
164
|
+
proxyMode: true
|
|
165
|
+
disabledModels: []
|
|
166
|
+
customModels: []
|
|
167
|
+
modelReasoningDefaults:
|
|
168
|
+
your-reasoning-model: high
|
|
169
|
+
modelContextOverrides: []
|
|
170
|
+
statsPath: ~/.dsh/clinebot-stats.json
|
|
171
|
+
timeoutMs: 30000
|
|
172
|
+
smokeTimeoutMs: 60000
|
|
173
|
+
streamIdleTimeoutMs: 60000
|
|
174
|
+
accounts:
|
|
175
|
+
- label: Team Key
|
|
176
|
+
apiKeyEnv: CLINEBOT_API_KEY_2
|
|
172
177
|
```
|
|
173
178
|
|
|
174
179
|
### 配置参数说明
|
|
175
180
|
|
|
176
181
|
| 参数项 | 类型 | 默认值 | 说明 |
|
|
177
182
|
|:---|:---|:---|:---|
|
|
178
|
-
| `enabled` | `boolean` | `true` |
|
|
183
|
+
| `enabled` | `boolean` | `true` | 是否在 DSH 中启用 ClineBot 桥接插件 |
|
|
179
184
|
| `baseUrl` | `string` | `"https://api.cline.bot/api/v1"` | ClinePass OpenAI 兼容接口地址 |
|
|
180
185
|
| `apiKeyEnv` | `string` | `"CLINEBOT_API_KEY"` | 凭据管理系统中的密钥名称 |
|
|
181
|
-
| `defaultModel` | `string` | `"
|
|
186
|
+
| `defaultModel` | `string` | `"claude-3-7-sonnet"` | 默认选中的模型 ID(用于对话及探活) |
|
|
187
|
+
| `disabledModels` | `array` | `[]` | 在选择器中隐藏的模型 ID 列表(新订阅模型默认自动启用) |
|
|
188
|
+
| `customModels` | `array` | `[]` | 用户自定义网关模型 (`[{ id, name, contextLength, maxTokens, category, isReasoning }]`) |
|
|
189
|
+
| `modelReasoningDefaults` | `object` | `{}` | 各模型默认思考强度配置 (`low`, `medium`, `high`, `max`) |
|
|
182
190
|
| `modelContextOverrides` | `array` | `[]` | 用户自定义模型上下文窗口与最大 Token 数量覆盖 |
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
191
|
+
| `proxyMode` | `boolean` | `true` | 启用透明本地环回代理以支持会话粘性路由与 429 自动故障转移 |
|
|
192
|
+
| `statsPath` | `string` | `"~/.dsh/clinebot-stats.json"` | 本地持久化 Token 用量统计文件路径 |
|
|
193
|
+
| `accounts` | `array` | `[]` | 额外轮转账号池 (`[{ label, apiKeyEnv }]`) |
|
|
194
|
+
| `activeAccount` | `string` | `""` | 手动指定的主账号(为空时自动按最低配额消耗优先路由) |
|
|
195
|
+
| `timeoutMs` | `number` | `30000` | HTTP 请求探测超时时间(毫秒) |
|
|
196
|
+
| `smokeTimeoutMs` | `number` | `60000` | 探活测试超时时间(毫秒) |
|
|
197
|
+
| `streamIdleTimeoutMs` | `number` | `60000` | SSE 流式输出块间空闲超时时间(毫秒) |
|
|
198
|
+
| `enabledModels` | `array` | *(已废弃)* | 仅在 public config 中只读提供,`PUT /config` 时禁止写入并请使用 `disabledModels` |
|
|
186
199
|
| `dynamicModels` | `array` | `[]` | 从官方套餐中自动同步的动态模型列表 |
|
|
187
200
|
|
|
188
201
|
---
|
package/lib/client.js
CHANGED
|
@@ -24,6 +24,19 @@ function readConfigForms(ctx) {
|
|
|
24
24
|
try { return ctx.get('configForms') || undefined } catch { return undefined }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function useConfigFormsSnapshot(ctx) {
|
|
28
|
+
const scope = React.useMemo(() => {
|
|
29
|
+
try { return readConfigForms(ctx)?.get?.(NS) || undefined } catch { return undefined }
|
|
30
|
+
}, [ctx])
|
|
31
|
+
const subscribe = React.useMemo(() => (cb) => {
|
|
32
|
+
try { return scope?.subscribe ? (scope.subscribe(cb) || (() => {})) : () => {} } catch { return () => {} }
|
|
33
|
+
}, [scope])
|
|
34
|
+
const getSnapshot = React.useCallback(() => {
|
|
35
|
+
try { return scope?.getSnapshot?.() || SNAPSHOT_READY } catch { return SNAPSHOT_READY }
|
|
36
|
+
}, [scope])
|
|
37
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, () => SNAPSHOT_READY)?.status || 'loading'
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
// --- src/client/locales.js ---
|
|
28
41
|
const en = {
|
|
29
42
|
title: 'ClineBot',
|
|
@@ -79,6 +92,27 @@ const en = {
|
|
|
79
92
|
'quota.window_weekly': '📅 Weekly Window',
|
|
80
93
|
'quota.used': '{pct}% used',
|
|
81
94
|
'quota.reset_at': 'Reset: {time}',
|
|
95
|
+
'quota.window_monthly': '📅 Monthly Window',
|
|
96
|
+
'quota.countdown': 'Resets in {time}',
|
|
97
|
+
'quota.warning_low': 'Quota is below 10%. Requests may be throttled soon.',
|
|
98
|
+
'quota.exhausted_5h': '5-hour rolling limit is almost exhausted ({percentUsed}%). New requests may be rejected until quota reset.',
|
|
99
|
+
'quota.warning_5h': 'Low quota warning: {percentUsed}% of 5-hour limit consumed (<10% remaining).',
|
|
100
|
+
'quota.warning_weekly': 'Low weekly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
|
|
101
|
+
'quota.warning_monthly': 'Low monthly quota warning: {percentUsed}% consumed ({remainingPercent}% remaining).',
|
|
102
|
+
'models.reasoning_tag': '🧠 Reasoning',
|
|
103
|
+
'models.effort_low': 'Low',
|
|
104
|
+
'models.effort_medium': 'Medium',
|
|
105
|
+
'models.effort_high': 'High',
|
|
106
|
+
'models.effort_max': 'Max',
|
|
107
|
+
'models.caps_vision': 'Vision',
|
|
108
|
+
'models.caps_general': 'General',
|
|
109
|
+
'models.caps_coding': 'Coding',
|
|
110
|
+
'diag.smoke_result': '✅ Latency: {latency} ms | Model: {model}\nResponse: {preview}',
|
|
111
|
+
'diag.empty': '(empty)',
|
|
112
|
+
'ui.error_title': '⚠️ ClineBot UI Error: ',
|
|
113
|
+
'ui.retry': 'Retry',
|
|
114
|
+
'update.check_failed': 'Failed to check for updates',
|
|
115
|
+
'update.retry': 'Retry check',
|
|
82
116
|
'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
|
|
83
117
|
'models.desc': 'Catalog synchronizes directly from ClinePass subscription. New plan models are enabled automatically. Uncheck models to hide them from DSH chat picker. Thinking effort is selectable in the composer model menu.',
|
|
84
118
|
'models.reasoning_tooltip': 'Supports thinking effort selection in composer model menu (Off / Low / Medium / High / Max)',
|
|
@@ -206,6 +240,27 @@ const en = {
|
|
|
206
240
|
'quota.window_weekly': '📅 每周滚动窗口',
|
|
207
241
|
'quota.used': '已使用 {pct}%',
|
|
208
242
|
'quota.reset_at': '重置时间:{time}',
|
|
243
|
+
'quota.window_monthly': '📅 每月滚动窗口',
|
|
244
|
+
'quota.countdown': '{time}后重置',
|
|
245
|
+
'quota.warning_low': '额度低于 10%,请求可能很快会受到限制。',
|
|
246
|
+
'quota.exhausted_5h': '5 小时滚动限额已接近耗尽(已用 {percentUsed}%)。在额度重置前新请求可能会被拒绝。',
|
|
247
|
+
'quota.warning_5h': '低额度警告:5 小时限额已消耗 {percentUsed}%(剩余不足 10%)。',
|
|
248
|
+
'quota.warning_weekly': '周限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
249
|
+
'quota.warning_monthly': '月限额偏低警告:已消耗 {percentUsed}%(剩余 {remainingPercent}%)。',
|
|
250
|
+
'models.reasoning_tag': '🧠 深度思考',
|
|
251
|
+
'models.effort_low': '低',
|
|
252
|
+
'models.effort_medium': '中',
|
|
253
|
+
'models.effort_high': '高',
|
|
254
|
+
'models.effort_max': '最高',
|
|
255
|
+
'models.caps_vision': '视觉',
|
|
256
|
+
'models.caps_general': '通用',
|
|
257
|
+
'models.caps_coding': '编程',
|
|
258
|
+
'diag.smoke_result': '✅ 延迟:{latency} ms | 模型:{model}\n响应:{preview}',
|
|
259
|
+
'diag.empty': '(空)',
|
|
260
|
+
'ui.error_title': '⚠️ ClineBot 界面错误:',
|
|
261
|
+
'ui.retry': '重试',
|
|
262
|
+
'update.check_failed': '检查更新失败',
|
|
263
|
+
'update.retry': '重试检查',
|
|
209
264
|
'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
|
|
210
265
|
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
|
|
211
266
|
'models.reasoning_tooltip': '支持在对话框上方模型菜单中选择思考强度(Off / Low / Medium / High / Max)',
|
|
@@ -399,10 +454,13 @@ function createErrorBoundary() {
|
|
|
399
454
|
}
|
|
400
455
|
render() {
|
|
401
456
|
if (this.state.hasError) {
|
|
457
|
+
const t = this.props && typeof this.props.t === 'function' ? this.props.t : null
|
|
458
|
+
const errorTitle = t ? t('ui.error_title') : '⚠️ ClineBot UI Error: '
|
|
459
|
+
const retryLabel = t ? t('ui.retry') : 'Retry'
|
|
402
460
|
return React.createElement(
|
|
403
461
|
'div',
|
|
404
462
|
{ className: 'cb-alert-err' },
|
|
405
|
-
React.createElement('strong', null,
|
|
463
|
+
React.createElement('strong', null, errorTitle),
|
|
406
464
|
String(this.state.error?.message || this.state.error || 'Unknown rendering error'),
|
|
407
465
|
React.createElement(
|
|
408
466
|
'button',
|
|
@@ -412,7 +470,7 @@ function createErrorBoundary() {
|
|
|
412
470
|
style: { marginLeft: '12px', padding: '2px 8px', fontSize: '11px' },
|
|
413
471
|
onClick: () => this.setState({ hasError: false, error: null }),
|
|
414
472
|
},
|
|
415
|
-
|
|
473
|
+
retryLabel
|
|
416
474
|
)
|
|
417
475
|
)
|
|
418
476
|
}
|
|
@@ -537,7 +595,22 @@ function ProgressBar({ label, percentUsed, remainingPercent, resetsAt, t }) {
|
|
|
537
595
|
}
|
|
538
596
|
|
|
539
597
|
// --- src/client/components/update-banner.js ---
|
|
540
|
-
function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
598
|
+
function UpdateBanner({ updateState, handleTriggerUpdate, handleCheckUpdate, t }) {
|
|
599
|
+
const status = updateState.status || (
|
|
600
|
+
updateState.checking
|
|
601
|
+
? 'checking'
|
|
602
|
+
: (updateState.latestCheckFailed
|
|
603
|
+
? 'error'
|
|
604
|
+
: (updateState.updateAvailable
|
|
605
|
+
? 'available'
|
|
606
|
+
: (updateState.lastCheckedAt ? 'current' : 'idle')))
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
const isChecking = status === 'checking' || updateState.checking
|
|
610
|
+
const isAvailable = status === 'available' || updateState.updateAvailable
|
|
611
|
+
const isCurrent = status === 'current'
|
|
612
|
+
const isError = status === 'error' || updateState.latestCheckFailed
|
|
613
|
+
|
|
541
614
|
return React.createElement(
|
|
542
615
|
React.Fragment,
|
|
543
616
|
null,
|
|
@@ -561,19 +634,25 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
|
561
634
|
`v${updateState.currentVersion}`
|
|
562
635
|
)
|
|
563
636
|
: null,
|
|
564
|
-
|
|
637
|
+
isChecking
|
|
565
638
|
? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px' } },
|
|
566
639
|
t('update.checking')
|
|
567
640
|
)
|
|
568
|
-
:
|
|
641
|
+
: isAvailable
|
|
569
642
|
? React.createElement('span', { className: 'cb-badge cb-badge-warn' },
|
|
570
643
|
t('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
|
|
571
644
|
)
|
|
572
|
-
:
|
|
573
|
-
|
|
574
|
-
|
|
645
|
+
: isCurrent
|
|
646
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' },
|
|
647
|
+
'✓ ' + t('update.up_to_date')
|
|
648
|
+
)
|
|
649
|
+
: isError
|
|
650
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-bad' },
|
|
651
|
+
t('update.check_failed')
|
|
652
|
+
)
|
|
653
|
+
: null
|
|
575
654
|
),
|
|
576
|
-
|
|
655
|
+
isAvailable
|
|
577
656
|
? React.createElement(
|
|
578
657
|
'button',
|
|
579
658
|
{
|
|
@@ -585,7 +664,19 @@ function UpdateBanner({ updateState, handleTriggerUpdate, t }) {
|
|
|
585
664
|
},
|
|
586
665
|
updateState.updating ? t('update.updating') : t('update.btn')
|
|
587
666
|
)
|
|
588
|
-
:
|
|
667
|
+
: isError && handleCheckUpdate
|
|
668
|
+
? React.createElement(
|
|
669
|
+
'button',
|
|
670
|
+
{
|
|
671
|
+
type: 'button',
|
|
672
|
+
className: 'cb-btn',
|
|
673
|
+
disabled: isChecking,
|
|
674
|
+
onClick: handleCheckUpdate,
|
|
675
|
+
style: { padding: '5px 12px', fontSize: '12px' }
|
|
676
|
+
},
|
|
677
|
+
t('update.retry')
|
|
678
|
+
)
|
|
679
|
+
: null
|
|
589
680
|
),
|
|
590
681
|
updateState.notice ? React.createElement('div', { className: 'cb-alert-ok' }, '✓ ' + updateState.notice) : null,
|
|
591
682
|
updateState.error ? React.createElement('div', { className: 'cb-alert-err' }, updateState.error) : null
|
|
@@ -936,7 +1027,7 @@ function QuotaSection({ keyPresent, status, usage, busy, handleRefreshQuota, t }
|
|
|
936
1027
|
React.createElement(
|
|
937
1028
|
'div',
|
|
938
1029
|
{ style: { flex: 1 } },
|
|
939
|
-
React.createElement('div', null, status.quotaWarning.message),
|
|
1030
|
+
React.createElement('div', null, (status.quotaWarning.code && t(status.quotaWarning.code, status.quotaWarning.params)) || status.quotaWarning.message),
|
|
940
1031
|
status.quotaWarning.resetsAt
|
|
941
1032
|
? React.createElement(
|
|
942
1033
|
'div',
|
|
@@ -1552,19 +1643,20 @@ function ModelsSection({
|
|
|
1552
1643
|
m.category === 'reasoning'
|
|
1553
1644
|
)
|
|
1554
1645
|
const category = m.category && m.category !== 'reasoning' ? m.category : (hasReasoning ? null : 'general')
|
|
1646
|
+
const categoryLabel = category === 'general' ? t('models.caps_general') : (category === 'coding' ? t('models.caps_coding') : category)
|
|
1555
1647
|
const hasVision = Boolean(m.input?.includes('image') || m.input?.includes('vision'))
|
|
1556
1648
|
return React.createElement(
|
|
1557
1649
|
React.Fragment,
|
|
1558
1650
|
null,
|
|
1559
|
-
category ? React.createElement('span', { className: 'cb-badge' },
|
|
1651
|
+
category ? React.createElement('span', { className: 'cb-badge' }, categoryLabel) : null,
|
|
1560
1652
|
hasVision
|
|
1561
|
-
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, '
|
|
1653
|
+
? React.createElement('span', { className: 'cb-badge', style: { marginLeft: category ? '4px' : '0' } }, t('models.caps_vision'))
|
|
1562
1654
|
: null,
|
|
1563
1655
|
hasReasoning
|
|
1564
1656
|
? React.createElement(
|
|
1565
1657
|
React.Fragment,
|
|
1566
1658
|
null,
|
|
1567
|
-
React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: (category || hasVision) ? '4px' : '0' }, title: t('models.reasoning_tooltip') }, '
|
|
1659
|
+
React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: (category || hasVision) ? '4px' : '0' }, title: t('models.reasoning_tooltip') }, t('models.reasoning_tag')),
|
|
1568
1660
|
React.createElement(
|
|
1569
1661
|
'select',
|
|
1570
1662
|
{
|
|
@@ -1579,10 +1671,10 @@ function ModelsSection({
|
|
|
1579
1671
|
},
|
|
1580
1672
|
},
|
|
1581
1673
|
React.createElement('option', { value: '' }, t('models.effort_auto')),
|
|
1582
|
-
React.createElement('option', { value: 'low' }, '
|
|
1583
|
-
React.createElement('option', { value: 'medium' }, '
|
|
1584
|
-
React.createElement('option', { value: 'high' }, '
|
|
1585
|
-
React.createElement('option', { value: 'max' }, '
|
|
1674
|
+
React.createElement('option', { value: 'low' }, t('models.effort_low')),
|
|
1675
|
+
React.createElement('option', { value: 'medium' }, t('models.effort_medium')),
|
|
1676
|
+
React.createElement('option', { value: 'high' }, t('models.effort_high')),
|
|
1677
|
+
React.createElement('option', { value: 'max' }, t('models.effort_max'))
|
|
1586
1678
|
)
|
|
1587
1679
|
)
|
|
1588
1680
|
: null
|
|
@@ -1785,27 +1877,17 @@ function DiagSection({ keyPresent, isRegistered, busy, smokeResult, handleSmoke,
|
|
|
1785
1877
|
? React.createElement(
|
|
1786
1878
|
'div',
|
|
1787
1879
|
{ className: 'cb-preview' },
|
|
1788
|
-
|
|
1789
|
-
|
|
1880
|
+
t('diag.smoke_result', {
|
|
1881
|
+
latency: smokeResult.latencyMs,
|
|
1882
|
+
model: smokeResult.model,
|
|
1883
|
+
preview: smokeResult.preview || t('diag.empty'),
|
|
1884
|
+
})
|
|
1790
1885
|
)
|
|
1791
1886
|
: null
|
|
1792
1887
|
)
|
|
1793
1888
|
}
|
|
1794
1889
|
|
|
1795
1890
|
// --- 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
1891
|
function SettingsPage(props) {
|
|
1810
1892
|
const ctx = props?.ctx
|
|
1811
1893
|
const t = props?.t || makeT(en, en)
|
|
@@ -1819,8 +1901,9 @@ function SettingsPage(props) {
|
|
|
1819
1901
|
const [smokeResult, setSmokeResult] = React.useState(null)
|
|
1820
1902
|
|
|
1821
1903
|
const [updateState, setUpdateState] = React.useState({
|
|
1822
|
-
checking: false, updating: false, currentVersion: '', latestVersion: '',
|
|
1823
|
-
updateAvailable: false, canAutoUpdate: true,
|
|
1904
|
+
status: 'idle', checking: false, updating: false, currentVersion: '', latestVersion: '',
|
|
1905
|
+
updateAvailable: false, canAutoUpdate: true, latestCheckFailed: false, lastCheckedAt: null,
|
|
1906
|
+
error: '', notice: '',
|
|
1824
1907
|
})
|
|
1825
1908
|
|
|
1826
1909
|
const [apiKeyInput, setApiKeyInput] = React.useState('')
|
|
@@ -1847,23 +1930,45 @@ function SettingsPage(props) {
|
|
|
1847
1930
|
}, [load])
|
|
1848
1931
|
|
|
1849
1932
|
const checkUpdate = React.useCallback(async () => {
|
|
1850
|
-
setUpdateState((s) => ({ ...s, checking: true, error: '' }))
|
|
1933
|
+
setUpdateState((s) => ({ ...s, checking: true, status: 'checking', error: '' }))
|
|
1851
1934
|
try {
|
|
1852
1935
|
const res = await fetch(`${ROUTE_PREFIX}/update`)
|
|
1853
1936
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
1854
1937
|
const data = await res.json().catch(() => ({}))
|
|
1938
|
+
if (data.latestCheckFailed) {
|
|
1939
|
+
setUpdateState((s) => ({
|
|
1940
|
+
...s,
|
|
1941
|
+
checking: false,
|
|
1942
|
+
status: 'error',
|
|
1943
|
+
latestCheckFailed: true,
|
|
1944
|
+
currentVersion: data.currentVersion || s.currentVersion,
|
|
1945
|
+
error: t('update.check_failed'),
|
|
1946
|
+
}))
|
|
1947
|
+
return
|
|
1948
|
+
}
|
|
1949
|
+
const isAvailable = Boolean(data.updateAvailable)
|
|
1855
1950
|
setUpdateState((s) => ({
|
|
1856
1951
|
...s,
|
|
1857
1952
|
checking: false,
|
|
1953
|
+
status: isAvailable ? 'available' : 'current',
|
|
1858
1954
|
currentVersion: data.currentVersion || s.currentVersion,
|
|
1859
1955
|
latestVersion: data.latestVersion || '',
|
|
1860
|
-
updateAvailable:
|
|
1956
|
+
updateAvailable: isAvailable,
|
|
1957
|
+
latestCheckFailed: false,
|
|
1958
|
+
lastCheckedAt: Date.now(),
|
|
1861
1959
|
canAutoUpdate: data.canAutoUpdate !== false,
|
|
1960
|
+
error: '',
|
|
1961
|
+
}))
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
setUpdateState((s) => ({
|
|
1964
|
+
...s,
|
|
1965
|
+
checking: false,
|
|
1966
|
+
status: 'error',
|
|
1967
|
+
latestCheckFailed: true,
|
|
1968
|
+
error: String(err?.message || err || t('update.check_failed')),
|
|
1862
1969
|
}))
|
|
1863
|
-
} catch (_) {
|
|
1864
|
-
setUpdateState((s) => ({ ...s, checking: false }))
|
|
1865
1970
|
}
|
|
1866
|
-
}, [])
|
|
1971
|
+
}, [t])
|
|
1867
1972
|
|
|
1868
1973
|
React.useEffect(() => { checkUpdate() }, [checkUpdate])
|
|
1869
1974
|
|
|
@@ -1881,13 +1986,20 @@ function SettingsPage(props) {
|
|
|
1881
1986
|
setUpdateState((s) => ({
|
|
1882
1987
|
...s,
|
|
1883
1988
|
updating: false,
|
|
1989
|
+
status: 'current',
|
|
1884
1990
|
updateAvailable: false,
|
|
1885
1991
|
currentVersion: newVer,
|
|
1992
|
+
lastCheckedAt: Date.now(),
|
|
1886
1993
|
notice: t('update.done', { version: newVer }),
|
|
1887
1994
|
}))
|
|
1888
1995
|
setTimeout(() => checkUpdate(), 2000)
|
|
1889
1996
|
} catch (err) {
|
|
1890
|
-
setUpdateState((s) => ({
|
|
1997
|
+
setUpdateState((s) => ({
|
|
1998
|
+
...s,
|
|
1999
|
+
updating: false,
|
|
2000
|
+
status: s.updateAvailable ? 'available' : 'error',
|
|
2001
|
+
error: t('update.failed', { error: String(err?.message || err) }),
|
|
2002
|
+
}))
|
|
1891
2003
|
}
|
|
1892
2004
|
}
|
|
1893
2005
|
|
|
@@ -1909,8 +2021,7 @@ function SettingsPage(props) {
|
|
|
1909
2021
|
if (!keyVal) { setErr(t('key.empty_err')); return }
|
|
1910
2022
|
await performAction('save-key', async () => {
|
|
1911
2023
|
const res = await fetch(`${ROUTE_PREFIX}/save-key`, {
|
|
1912
|
-
method: 'POST',
|
|
1913
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2024
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1914
2025
|
body: JSON.stringify({ apiKey: keyVal, apiKeyEnv: draft?.apiKeyEnv }),
|
|
1915
2026
|
})
|
|
1916
2027
|
const data = await res.json().catch(() => ({}))
|
|
@@ -1934,8 +2045,7 @@ function SettingsPage(props) {
|
|
|
1934
2045
|
async function handleUpdateModelContext(payload) {
|
|
1935
2046
|
await performAction('update-context', async () => {
|
|
1936
2047
|
const res = await fetch(`${ROUTE_PREFIX}/models/context`, {
|
|
1937
|
-
method: 'POST',
|
|
1938
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2048
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1939
2049
|
body: JSON.stringify(payload),
|
|
1940
2050
|
})
|
|
1941
2051
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2004,8 +2114,7 @@ function SettingsPage(props) {
|
|
|
2004
2114
|
setVerifyStatus({ state: 'verifying', email: '', plan: '', error: '' })
|
|
2005
2115
|
try {
|
|
2006
2116
|
const res = await fetch(`${ROUTE_PREFIX}/key/verify`, {
|
|
2007
|
-
method: 'POST',
|
|
2008
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2117
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2009
2118
|
body: JSON.stringify({ key: raw }),
|
|
2010
2119
|
})
|
|
2011
2120
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2030,8 +2139,7 @@ function SettingsPage(props) {
|
|
|
2030
2139
|
async function handlePinAccount(accountEnv) {
|
|
2031
2140
|
await performAction('pin-account', async () => {
|
|
2032
2141
|
const res = await fetch(`${ROUTE_PREFIX}/accounts/active`, {
|
|
2033
|
-
method: 'POST',
|
|
2034
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2142
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2035
2143
|
body: JSON.stringify({ account: accountEnv }),
|
|
2036
2144
|
})
|
|
2037
2145
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2043,8 +2151,7 @@ function SettingsPage(props) {
|
|
|
2043
2151
|
async function handleAddAccount({ label, apiKeyEnv, apiKey }) {
|
|
2044
2152
|
await performAction('add-account', async () => {
|
|
2045
2153
|
const res = await fetch(`${ROUTE_PREFIX}/accounts`, {
|
|
2046
|
-
method: 'POST',
|
|
2047
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2154
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2048
2155
|
body: JSON.stringify({ label, apiKeyEnv, apiKey }),
|
|
2049
2156
|
})
|
|
2050
2157
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2057,8 +2164,7 @@ function SettingsPage(props) {
|
|
|
2057
2164
|
async function handleDeleteAccount(accountEnv, deleteSecret = true) {
|
|
2058
2165
|
await performAction('delete-account', async () => {
|
|
2059
2166
|
const res = await fetch(`${ROUTE_PREFIX}/accounts/delete`, {
|
|
2060
|
-
method: 'POST',
|
|
2061
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2167
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2062
2168
|
body: JSON.stringify({ apiKeyEnv: accountEnv, deleteSecret }),
|
|
2063
2169
|
})
|
|
2064
2170
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2071,8 +2177,7 @@ function SettingsPage(props) {
|
|
|
2071
2177
|
setDraft((d) => ({ ...(d || {}), ...(patch || {}) }))
|
|
2072
2178
|
await performAction('patch-config', async () => {
|
|
2073
2179
|
const res = await fetch(`${ROUTE_PREFIX}/config`, {
|
|
2074
|
-
method: 'PUT',
|
|
2075
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2180
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
2076
2181
|
body: JSON.stringify({ config: patch }),
|
|
2077
2182
|
})
|
|
2078
2183
|
const data = await res.json().catch(() => ({}))
|
|
@@ -2136,15 +2241,10 @@ function SettingsPage(props) {
|
|
|
2136
2241
|
function handleSetModelsFilter(type) {
|
|
2137
2242
|
const all = status?.availableModels || []
|
|
2138
2243
|
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
|
-
}
|
|
2244
|
+
if (type === 'all') allowed = new Set(all.map((m) => m.id))
|
|
2245
|
+
else if (type === 'vision') allowed = new Set(all.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id))
|
|
2246
|
+
else if (type === 'coding') allowed = new Set(all.filter((m) => m.category === 'coding').map((m) => m.id))
|
|
2247
|
+
else if (type === 'recommended') allowed = new Set(all.filter((m) => m.recommended).map((m) => m.id))
|
|
2148
2248
|
|
|
2149
2249
|
const curr = draft?.disabledModels || []
|
|
2150
2250
|
const nextDisabled = all.map((m) => m.id).filter((id) => !allowed.has(id))
|
|
@@ -2205,7 +2305,7 @@ function SettingsPage(props) {
|
|
|
2205
2305
|
),
|
|
2206
2306
|
|
|
2207
2307
|
// In-app Update Bar
|
|
2208
|
-
React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, t }),
|
|
2308
|
+
React.createElement(UpdateBanner, { updateState, handleTriggerUpdate, handleCheckUpdate: checkUpdate, t }),
|
|
2209
2309
|
|
|
2210
2310
|
// Settings host status warning
|
|
2211
2311
|
snapshotStatus === 'unavailable'
|
|
@@ -2338,7 +2438,7 @@ function PluginCard(props) {
|
|
|
2338
2438
|
{ style: { marginTop: '16px' } },
|
|
2339
2439
|
React.createElement(
|
|
2340
2440
|
ErrorBoundary,
|
|
2341
|
-
|
|
2441
|
+
{ t },
|
|
2342
2442
|
React.createElement(SettingsPage, { ...props, ctx: props && props.ctx })
|
|
2343
2443
|
)
|
|
2344
2444
|
)
|
|
@@ -2423,7 +2523,7 @@ function apply(ctx) {
|
|
|
2423
2523
|
locale: NS,
|
|
2424
2524
|
inject: () => ({ ctx }),
|
|
2425
2525
|
},
|
|
2426
|
-
(props) => React.createElement(ErrorBoundary,
|
|
2526
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2427
2527
|
)
|
|
2428
2528
|
)
|
|
2429
2529
|
|
|
@@ -2440,7 +2540,7 @@ function apply(ctx) {
|
|
|
2440
2540
|
locale: NS,
|
|
2441
2541
|
inject: () => ({ ctx }),
|
|
2442
2542
|
},
|
|
2443
|
-
(props) => React.createElement(ErrorBoundary,
|
|
2543
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2444
2544
|
)
|
|
2445
2545
|
)
|
|
2446
2546
|
}
|
|
@@ -2453,7 +2553,7 @@ function apply(ctx) {
|
|
|
2453
2553
|
locale: NS,
|
|
2454
2554
|
inject: () => ({ ctx }),
|
|
2455
2555
|
},
|
|
2456
|
-
(props) => React.createElement(ErrorBoundary,
|
|
2556
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
2457
2557
|
)
|
|
2458
2558
|
)
|
|
2459
2559
|
}
|
package/lib/provider-sync.js
CHANGED
|
@@ -123,12 +123,16 @@ export async function buildStatus(ctx, cfg) {
|
|
|
123
123
|
if (fiveHour.percentUsed >= 95) {
|
|
124
124
|
quotaWarning = {
|
|
125
125
|
level: 'exhausted',
|
|
126
|
+
code: 'quota.exhausted_5h',
|
|
127
|
+
params: { percentUsed: fiveHour.percentUsed },
|
|
126
128
|
message: `5-hour rolling limit is almost exhausted (${fiveHour.percentUsed}%). New requests may be rejected until quota reset.`,
|
|
127
129
|
resetsAt: fiveHour.resetsAt,
|
|
128
130
|
}
|
|
129
131
|
} else if (fiveHour.percentUsed >= 90) {
|
|
130
132
|
quotaWarning = {
|
|
131
133
|
level: 'warning',
|
|
134
|
+
code: 'quota.warning_5h',
|
|
135
|
+
params: { percentUsed: fiveHour.percentUsed },
|
|
132
136
|
message: `Low quota warning: ${fiveHour.percentUsed}% of 5-hour limit consumed (<10% remaining).`,
|
|
133
137
|
resetsAt: fiveHour.resetsAt,
|
|
134
138
|
}
|
|
@@ -136,17 +140,23 @@ export async function buildStatus(ctx, cfg) {
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
if (!quotaWarning && weekly && typeof weekly.percentUsed === 'number' && weekly.percentUsed >= 90) {
|
|
143
|
+
const remainingPercent = weekly.remainingPercent ?? (100 - weekly.percentUsed)
|
|
139
144
|
quotaWarning = {
|
|
140
145
|
level: weekly.percentUsed >= 99 ? 'exhausted' : 'warning',
|
|
141
|
-
|
|
146
|
+
code: 'quota.warning_weekly',
|
|
147
|
+
params: { percentUsed: weekly.percentUsed, remainingPercent },
|
|
148
|
+
message: `Low weekly quota warning: ${weekly.percentUsed}% consumed (${remainingPercent}% remaining).`,
|
|
142
149
|
resetsAt: weekly.resetsAt,
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
if (!quotaWarning && monthly && typeof monthly.percentUsed === 'number' && monthly.percentUsed >= 90) {
|
|
154
|
+
const remainingPercent = monthly.remainingPercent ?? (100 - monthly.percentUsed)
|
|
147
155
|
quotaWarning = {
|
|
148
156
|
level: monthly.percentUsed >= 99 ? 'exhausted' : 'warning',
|
|
149
|
-
|
|
157
|
+
code: 'quota.warning_monthly',
|
|
158
|
+
params: { percentUsed: monthly.percentUsed, remainingPercent },
|
|
159
|
+
message: `Low monthly quota warning: ${monthly.percentUsed}% consumed (${remainingPercent}% remaining).`,
|
|
150
160
|
resetsAt: monthly.resetsAt,
|
|
151
161
|
}
|
|
152
162
|
}
|
package/lib/updater.js
CHANGED
|
@@ -219,10 +219,32 @@ export async function checkUpdateStatus(options, target = detectRuntime()) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
export function cleanStaleLock(profileDir) {
|
|
223
|
+
if (!profileDir) return false
|
|
224
|
+
const lockFile = resolve(profileDir, 'package.json.lock')
|
|
225
|
+
if (existsSync(lockFile)) {
|
|
226
|
+
try {
|
|
227
|
+
const pid = parseInt(readFileSync(lockFile, 'utf8').trim(), 10)
|
|
228
|
+
if (!isPidAlive(pid)) {
|
|
229
|
+
unlinkSync(lockFile)
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
} catch (err) {
|
|
233
|
+
/* best-effort stale lock unlink */
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function installExact(target, packageSpec, options = {}) {
|
|
223
240
|
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
|
|
241
|
+
const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : UPDATE_TIMEOUT_MS
|
|
242
|
+
const sigtermGraceMs = typeof options.sigtermGraceMs === 'number' ? options.sigtermGraceMs : 500
|
|
243
|
+
const sigkillGraceMs = typeof options.sigkillGraceMs === 'number' ? options.sigkillGraceMs : 500
|
|
244
|
+
const spawnFn = typeof options.spawn === 'function' ? options.spawn : spawn
|
|
245
|
+
|
|
224
246
|
await new Promise((resolvePromise, reject) => {
|
|
225
|
-
const child =
|
|
247
|
+
const child = spawnFn(process.execPath, [
|
|
226
248
|
target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
|
|
227
249
|
packageSpec,
|
|
228
250
|
`--registry=${options.registry || 'https://registry.npmjs.org/'}`,
|
|
@@ -235,40 +257,71 @@ async function installExact(target, packageSpec, options) {
|
|
|
235
257
|
let detail = ''
|
|
236
258
|
child.stdout?.on('data', (chunk) => { detail = (detail + String(chunk)).slice(-4000) })
|
|
237
259
|
child.stderr?.on('data', (chunk) => { detail = (detail + String(chunk)).slice(-4000) })
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
|
|
261
|
+
let exited = false
|
|
262
|
+
let isTimedOut = false
|
|
263
|
+
let sigtermTimer = null
|
|
264
|
+
let sigkillTimer = null
|
|
265
|
+
|
|
266
|
+
function cleanupTimers() {
|
|
267
|
+
clearTimeout(mainTimer)
|
|
268
|
+
if (sigtermTimer) clearTimeout(sigtermTimer)
|
|
269
|
+
if (sigkillTimer) clearTimeout(sigkillTimer)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function onProcessExit(code, signal) {
|
|
273
|
+
if (exited) return
|
|
274
|
+
exited = true
|
|
275
|
+
cleanupTimers()
|
|
276
|
+
|
|
277
|
+
if (isTimedOut) {
|
|
278
|
+
cleanStaleLock(target.profileDir)
|
|
279
|
+
reject(new Error('Update timed out.'))
|
|
280
|
+
} else if (code === 0) {
|
|
281
|
+
resolvePromise()
|
|
282
|
+
} else {
|
|
283
|
+
reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
child.once('error', (error) => {
|
|
288
|
+
cleanupTimers()
|
|
289
|
+
reject(error)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
child.once('exit', onProcessExit)
|
|
293
|
+
child.once('close', onProcessExit)
|
|
294
|
+
|
|
295
|
+
const mainTimer = setTimeout(() => {
|
|
296
|
+
isTimedOut = true
|
|
297
|
+
// Step 1: Send SIGTERM
|
|
298
|
+
try {
|
|
299
|
+
child.kill('SIGTERM')
|
|
300
|
+
} catch (err) {
|
|
301
|
+
/* ignore if process already terminated */
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Step 2: Wait for grace period, if process has not exited escalate to SIGKILL (#172)
|
|
305
|
+
sigtermTimer = setTimeout(() => {
|
|
306
|
+
const hasExited = exited || (child.exitCode !== null && child.exitCode !== undefined) || (child.signalCode !== null && child.signalCode !== undefined)
|
|
307
|
+
if (!hasExited) {
|
|
242
308
|
try {
|
|
243
309
|
child.kill('SIGKILL')
|
|
244
310
|
} catch (err) {
|
|
245
311
|
/* ignore if process already terminated */
|
|
246
312
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!isPidAlive(pid)) {
|
|
255
|
-
unlinkSync(lockFile)
|
|
256
|
-
}
|
|
257
|
-
} catch (err) {
|
|
258
|
-
/* best-effort stale lock unlink */
|
|
259
|
-
}
|
|
313
|
+
|
|
314
|
+
// Step 3: Hard deadline for exit notification
|
|
315
|
+
sigkillTimer = setTimeout(() => {
|
|
316
|
+
if (!exited) {
|
|
317
|
+
exited = true
|
|
318
|
+
cleanStaleLock(target.profileDir)
|
|
319
|
+
reject(new Error('Update timed out.'))
|
|
260
320
|
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
}, UPDATE_TIMEOUT_MS)
|
|
266
|
-
child.once('error', (error) => { clearTimeout(timer); reject(error) })
|
|
267
|
-
child.once('exit', (code) => {
|
|
268
|
-
clearTimeout(timer)
|
|
269
|
-
if (code === 0) resolvePromise()
|
|
270
|
-
else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
|
|
271
|
-
})
|
|
321
|
+
}, sigkillGraceMs)
|
|
322
|
+
}
|
|
323
|
+
}, sigtermGraceMs)
|
|
324
|
+
}, timeoutMs)
|
|
272
325
|
})
|
|
273
326
|
}
|
|
274
327
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
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",
|