@goodandready/dsh-clinebot 0.4.4 → 0.4.6
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 -0
- package/README.md +8 -0
- package/lib/client.js +3 -15
- package/lib/cline-client.js +9 -88
- package/lib/credential-refs.js +84 -0
- package/lib/index.js +51 -58
- package/lib/models.js +44 -5
- package/lib/provider-sync.js +15 -1
- package/lib/slash-command.js +1 -0
- package/media/visual-verification.png +0 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.4.6] - 2026-09-25
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Obsolete Settings Service Dependency Removal (#119)**: Removed ` settings` from `inject` and eradicated legacy `ctx.inject([settings], ...)` and `sctx.settings.register(...)` calls, adapting to DSH 0.1.7 kernel architecture where the Cordis `settings` service was removed.
|
|
12
|
+
- **Config Reader Export (#119)**: Exported `configReader(config)` returning `() => config` to provide standard DSH configuration introspection.
|
|
13
|
+
- **Defensive SettingsForms Adapter (#119)**: Implemented lightweight `settingsApi` adapter providing `get()`, `replace()`, `update()`, and `watch()` without declaring `settings` in `inject`, accessing `ctx?.get?.(settings)` defensively.
|
|
14
|
+
- **Failover & Settings Regression Tests (#119)**: Added 5 strict DoD regression tests verifying `inject`, absence of `settings.register`, proxy property safety, config reader, and strict proxy boot.
|
|
15
|
+
|
|
16
|
+
## [0.4.5] - 2026-09-25
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Obsolete Model ID Migration & Canonical Normalization (#103)**: Automatically migrate historical model identifiers lacking dots (e.g., `deepseek-v41-flash` -> `deepseek-v4.1-flash`, `qwen38-max` -> `qwen3.8-max`, `glm-53` -> `glm-5.3`, `glm-53-flash` -> `glm-5.3-flash`, `muse-spark-13-contributor` -> `muse-spark-1.3-contributor`) across disk cache, DSH settings, and auto-discovery, eliminating stuck legacy IDs.
|
|
20
|
+
- **Config Schema Test Assertion for Schemastery 3.18.4 (#112)**: Aligned `test/config-schema.test.js` with Schemastery 3.18.4 volatile getter behavior using `plainConfig(validated).enabled`.
|
|
21
|
+
- **UI Color Compliance & Token Hardening (#116)**: Replaced hardcoded `rgba()` fallbacks in `accounts-section.js` and `models-section.js` with canonical DSH `color-mix()` and design tokens.
|
|
22
|
+
- **Decomposition Compliance (#117)**: Extracted credentials helpers into `lib/credential-refs.js`, reducing `lib/cline-client.js` from 590 to 511 lines (well below the 600-line ceiling).
|
|
23
|
+
- **Client Localization Pruning (#115)**: Removed 6 obsolete dead locale keys from `src/client/locales.js` in both `en` and `zh`.
|
|
24
|
+
- **Route Policy & Packaging Contract (#113)**: Documented mandatory «Политика маршрутов» (16 registered endpoints) and «Что публикуется» sections in `docs/design/DESIGN.md`.
|
|
25
|
+
- **Visual Verification Evidence (#114)**: Added dual-theme production visual verification artifact `media/visual-verification.png` and embedded under `## Visual verification` in `README.md`.
|
|
26
|
+
|
|
8
27
|
## [0.4.4] - 2026-09-25
|
|
9
28
|
|
|
10
29
|
### Added
|
package/README.md
CHANGED
|
@@ -206,6 +206,14 @@ All endpoints are registered under `/dsh-clinebot/*` and protected against untru
|
|
|
206
206
|
|
|
207
207
|
---
|
|
208
208
|
|
|
209
|
+
## Visual verification
|
|
210
|
+
|
|
211
|
+
Settings card in DeepSeek Harness, Dark and Light themes side by side:
|
|
212
|
+
|
|
213
|
+

|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
209
217
|
## 🧪 Testing
|
|
210
218
|
|
|
211
219
|
Run the automated test suite:
|
package/lib/client.js
CHANGED
|
@@ -48,7 +48,6 @@ const en = {
|
|
|
48
48
|
'key.save': 'Save Key',
|
|
49
49
|
'key.saving': 'Saving…',
|
|
50
50
|
'key.login_fast': '🔑 Open API Keys ↗',
|
|
51
|
-
'key.login_waiting': 'Paste key below for instant verification.',
|
|
52
51
|
'key.verifying': 'Validating key with Cline…',
|
|
53
52
|
'key.verified': '✓ Valid key for {email} ({plan})',
|
|
54
53
|
'key.invalid': '✗ Key validation failed: {error}',
|
|
@@ -66,10 +65,8 @@ const en = {
|
|
|
66
65
|
'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
|
|
67
66
|
'accounts.delete_btn': 'Delete',
|
|
68
67
|
'accounts.delete_confirm': 'Are you sure you want to delete account "{label}"?',
|
|
69
|
-
'accounts.delete_secret': 'Delete secret from credentials',
|
|
70
68
|
'accounts.save_btn': 'Save Account',
|
|
71
69
|
'accounts.cancel_btn': 'Cancel',
|
|
72
|
-
'accounts.key_label': 'API Key',
|
|
73
70
|
'accounts.quota_used': '{pct}% used',
|
|
74
71
|
'accounts.last_failover': 'Last failover: {from} → {to} ({reason})',
|
|
75
72
|
'quota.title': '📊 ClinePass Quota & Rate Limits',
|
|
@@ -81,7 +78,6 @@ const en = {
|
|
|
81
78
|
'quota.window_5h': '⏱ 5-Hour Rolling Limit',
|
|
82
79
|
'quota.window_weekly': '📅 Weekly Window',
|
|
83
80
|
'quota.used': '{pct}% used',
|
|
84
|
-
'quota.remaining': 'Remaining: {pct}%',
|
|
85
81
|
'quota.reset_at': 'Reset: {time}',
|
|
86
82
|
'models.title': '🎯 Model Picker in DSH ({enabled} of {total} active)',
|
|
87
83
|
'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.',
|
|
@@ -114,7 +110,6 @@ const en = {
|
|
|
114
110
|
'models.context_updated_msg': 'Model context limits updated and synchronized to DSH.',
|
|
115
111
|
'models.ctx_default': 'Default ({ctx})',
|
|
116
112
|
'models.ctx_original': 'Original ({ctx} · {provider})',
|
|
117
|
-
'models.ctx_custom': 'Custom…',
|
|
118
113
|
'models.ctx_edit': 'Edit Context Window',
|
|
119
114
|
'models.ctx_custom_placeholder': 'Tokens (e.g. 2000000)',
|
|
120
115
|
'models.ctx_save': 'Save',
|
|
@@ -131,7 +126,6 @@ const en = {
|
|
|
131
126
|
'diag.desc': 'ClineBot registers into DSH Models automatically when key is configured. You can run connection smoke test or force re-sync.',
|
|
132
127
|
'diag.smoke_btn': 'Run Smoke Test (Ping)',
|
|
133
128
|
'diag.smoke_testing': 'Testing…',
|
|
134
|
-
'diag.smoke_ok': 'Smoke test passed! Latency: {latency} ms',
|
|
135
129
|
'diag.resync_btn': 'Force Re-sync to DSH',
|
|
136
130
|
'diag.resyncing': 'Syncing…',
|
|
137
131
|
'diag.resynced_msg': 'Provider re-synced to DSH Models ({count} models)',
|
|
@@ -170,7 +164,6 @@ const en = {
|
|
|
170
164
|
'key.save': '保存密钥',
|
|
171
165
|
'key.saving': '正在保存…',
|
|
172
166
|
'key.login_fast': '🔑 打开 API 密钥页 ↗',
|
|
173
|
-
'key.login_waiting': '请在下方粘贴密钥,系统将自动进行有效性校验。',
|
|
174
167
|
'key.verifying': '正在连接 Cline 验证密钥…',
|
|
175
168
|
'key.verified': '✓ 密钥有效:{email} ({plan})',
|
|
176
169
|
'key.invalid': '✗ 密钥验证未通过:{error}',
|
|
@@ -188,10 +181,8 @@ const en = {
|
|
|
188
181
|
'accounts.env_placeholder': 'CLINEBOT_API_KEY_2',
|
|
189
182
|
'accounts.delete_btn': '删除',
|
|
190
183
|
'accounts.delete_confirm': '确定要删除账号“{label}”吗?',
|
|
191
|
-
'accounts.delete_secret': '同时删除凭据密钥',
|
|
192
184
|
'accounts.save_btn': '保存账号',
|
|
193
185
|
'accounts.cancel_btn': '取消',
|
|
194
|
-
'accounts.key_label': 'API 密钥',
|
|
195
186
|
'accounts.quota_used': '已用 {pct}%',
|
|
196
187
|
'accounts.last_failover': '最近故障转移:{from} → {to} ({reason})',
|
|
197
188
|
'quota.title': '📊 ClinePass 用量配额与限额监控',
|
|
@@ -203,7 +194,6 @@ const en = {
|
|
|
203
194
|
'quota.window_5h': '⏱ 5 小时滚动限额',
|
|
204
195
|
'quota.window_weekly': '📅 每周滚动窗口',
|
|
205
196
|
'quota.used': '已使用 {pct}%',
|
|
206
|
-
'quota.remaining': '剩余可用:{pct}%',
|
|
207
197
|
'quota.reset_at': '重置时间:{time}',
|
|
208
198
|
'models.title': '🎯 DSH 模型选择列表(已启用 {enabled}/{total})',
|
|
209
199
|
'models.desc': '模型目录与 ClinePass 官方订阅实时同步。新订阅模型默认自动启用。取消勾选可在 DSH 对话模型列表中隐藏不需要的模型。支持在对话输入框上方切换推理思考强度(Effort)。',
|
|
@@ -236,7 +226,6 @@ const en = {
|
|
|
236
226
|
'models.context_updated_msg': '模型上下文限制已更新并同步生效至 DSH。',
|
|
237
227
|
'models.ctx_default': '默认 ({ctx})',
|
|
238
228
|
'models.ctx_original': '原厂 ({ctx} · {provider})',
|
|
239
|
-
'models.ctx_custom': '自定义…',
|
|
240
229
|
'models.ctx_edit': '编辑上下文窗口',
|
|
241
230
|
'models.ctx_custom_placeholder': 'Token 数量 (如 2000000)',
|
|
242
231
|
'models.ctx_save': '保存',
|
|
@@ -253,7 +242,6 @@ const en = {
|
|
|
253
242
|
'diag.desc': '配置 API 密钥后,ClineBot 会自动向 DSH 注册模型服务。您可以在此进行连通性测试或强制重新同步。',
|
|
254
243
|
'diag.smoke_btn': '执行连通性冒烟测试 (Ping)',
|
|
255
244
|
'diag.smoke_testing': '正在测试…',
|
|
256
|
-
'diag.smoke_ok': '冒烟测试通过!网络延迟:{latency} ms',
|
|
257
245
|
'diag.resync_btn': '强制重新同步到 DSH',
|
|
258
246
|
'diag.resyncing': '正在同步…',
|
|
259
247
|
'diag.resynced_msg': '模型服务已重新同步到 DSH ({count} 个模型)',
|
|
@@ -707,8 +695,8 @@ function AccountsSection({ status, busy, handlePinAccount, handleAddAccount, han
|
|
|
707
695
|
{
|
|
708
696
|
onSubmit: onSubmitAdd,
|
|
709
697
|
style: {
|
|
710
|
-
background: 'var(--dsw-alias-bg-hover,
|
|
711
|
-
border: '1px solid var(--dsw-alias-border,
|
|
698
|
+
background: 'var(--dsw-alias-bg-hover, color-mix(in srgb, currentColor 4%, transparent))',
|
|
699
|
+
border: '1px solid var(--dsw-alias-border, color-mix(in srgb, currentColor 12%, transparent))',
|
|
712
700
|
borderRadius: '8px',
|
|
713
701
|
padding: '12px',
|
|
714
702
|
marginBottom: '14px',
|
|
@@ -1206,7 +1194,7 @@ function ModelsSection({
|
|
|
1206
1194
|
display: 'flex',
|
|
1207
1195
|
flexDirection: 'column',
|
|
1208
1196
|
gap: '6px',
|
|
1209
|
-
background: 'var(--dsw-alias-bg-hover,
|
|
1197
|
+
background: 'var(--dsw-alias-bg-hover, color-mix(in srgb, currentColor 4%, transparent))',
|
|
1210
1198
|
padding: '6px',
|
|
1211
1199
|
borderRadius: '6px',
|
|
1212
1200
|
border: '1px solid var(--dsw-alias-border-l2)',
|
package/lib/cline-client.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
toCredentialRef,
|
|
3
|
+
resolveApiKey,
|
|
4
|
+
saveCredentialKey,
|
|
5
|
+
deleteCredentialKey,
|
|
6
|
+
resolveKeyValue,
|
|
7
|
+
} from './credential-refs.js'
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* ClineBot / ClinePass client helpers for DeepSeek Harness.
|
|
3
11
|
*
|
|
@@ -22,20 +30,7 @@ export const USAGE_CACHE_TTL_MS = 60000
|
|
|
22
30
|
|
|
23
31
|
export { PROVIDER_ID, PROVIDER_DISPLAY_NAME, DEFAULT_MODEL_ID }
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
* Resolve or fallback credential reference descriptor.
|
|
27
|
-
*/
|
|
28
|
-
export async function toCredentialRef(name) {
|
|
29
|
-
try {
|
|
30
|
-
const mod = await import('@deepseek-ai/dsh-credentials')
|
|
31
|
-
if (typeof mod.credentialRef === 'function') {
|
|
32
|
-
return mod.credentialRef(name)
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
/* fallback when executed in standalone unit tests outside DSH bundle */
|
|
36
|
-
}
|
|
37
|
-
return typeof name === 'object' && name !== null ? name : { type: 'env', name: String(name || '') }
|
|
38
|
-
}
|
|
33
|
+
// Credentials helpers extracted to ./credential-refs.js
|
|
39
34
|
|
|
40
35
|
/**
|
|
41
36
|
* Normalize base URL ensuring clean format without trailing slashes.
|
|
@@ -57,57 +52,7 @@ export {
|
|
|
57
52
|
resetSessionStats,
|
|
58
53
|
} from './telemetry.js'
|
|
59
54
|
|
|
60
|
-
/**
|
|
61
|
-
* Resolve API key from environment variables.
|
|
62
|
-
*/
|
|
63
|
-
export function resolveApiKey(apiKeyEnv, env = process.env) {
|
|
64
|
-
const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
65
|
-
return {
|
|
66
|
-
envName: name,
|
|
67
|
-
value: String(env[name] || ''),
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Save API key directly into DSH credentials service (~/.dsh/.credentials.yaml).
|
|
73
|
-
*/
|
|
74
|
-
export async function saveCredentialKey(ctx, apiKeyEnv, apiKey) {
|
|
75
|
-
const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
76
|
-
const value = String(apiKey || '').trim()
|
|
77
|
-
|
|
78
|
-
if (!value) {
|
|
79
|
-
throw new Error('API key cannot be empty')
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const credentials = ctx?.credentials || ctx?.get?.('credentials')
|
|
83
|
-
if (!credentials || typeof credentials.set !== 'function') {
|
|
84
|
-
throw new Error('DSH credentials service is unavailable in this runtime profile')
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const ref = await toCredentialRef(name)
|
|
88
|
-
await credentials.set(ref, value)
|
|
89
|
-
return { ok: true, envName: name }
|
|
90
|
-
}
|
|
91
55
|
|
|
92
|
-
/**
|
|
93
|
-
* Delete API key from DSH credentials service.
|
|
94
|
-
*/
|
|
95
|
-
export async function deleteCredentialKey(ctx, apiKeyEnv) {
|
|
96
|
-
const name = String(apiKeyEnv || '').trim()
|
|
97
|
-
if (!name) return { ok: false }
|
|
98
|
-
const credentials = ctx?.credentials || ctx?.get?.('credentials')
|
|
99
|
-
if (!credentials) return { ok: false }
|
|
100
|
-
try {
|
|
101
|
-
const ref = await toCredentialRef(name)
|
|
102
|
-
if (typeof credentials.unset === 'function') {
|
|
103
|
-
await credentials.unset(ref)
|
|
104
|
-
return { ok: true, envName: name }
|
|
105
|
-
}
|
|
106
|
-
} catch (err) {
|
|
107
|
-
console.warn('[dsh-clinebot] Failed unsetting credential:', err)
|
|
108
|
-
}
|
|
109
|
-
return { ok: false }
|
|
110
|
-
}
|
|
111
56
|
|
|
112
57
|
function abortAfter(ms) {
|
|
113
58
|
const ac = new AbortController()
|
|
@@ -560,31 +505,7 @@ export function buildPiAiProvider({
|
|
|
560
505
|
}
|
|
561
506
|
}
|
|
562
507
|
|
|
563
|
-
/**
|
|
564
|
-
* Safely resolve key value from DSH credentials service or process.env.
|
|
565
|
-
*/
|
|
566
|
-
export async function resolveKeyValue(ctx, apiKeyEnv) {
|
|
567
|
-
const refName = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
568
|
-
const creds = (ctx?.get && ctx.get('credentials')) || ctx?.credentials
|
|
569
|
-
if (creds && typeof creds.resolve === 'function') {
|
|
570
|
-
try {
|
|
571
|
-
const ref = await toCredentialRef(refName)
|
|
572
|
-
const hit = await creds.resolve(ref)
|
|
573
|
-
if (hit?.value) {
|
|
574
|
-
return { envName: refName, value: hit.value, source: 'credentials' }
|
|
575
|
-
}
|
|
576
|
-
} catch {
|
|
577
|
-
/* miss */
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const fromEnv = resolveApiKey(refName)
|
|
582
|
-
if (fromEnv.value) {
|
|
583
|
-
return { ...fromEnv, source: 'env' }
|
|
584
|
-
}
|
|
585
508
|
|
|
586
|
-
return { envName: refName, value: '', source: 'none' }
|
|
587
|
-
}
|
|
588
509
|
|
|
589
510
|
|
|
590
511
|
export { resolveAccountPool, isAccountQuotaExhausted, rotateToNextAccount, getLastRotation, setLastRotation, isQuotaExceededError } from './account-pool.js'
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials management helper for @goodandready/dsh-clinebot.
|
|
3
|
+
* Isolates DSH Credentials storage and environment resolution.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_API_KEY_ENV = 'CLINEBOT_API_KEY'
|
|
7
|
+
|
|
8
|
+
export async function toCredentialRef(name) {
|
|
9
|
+
try {
|
|
10
|
+
const mod = await import('@deepseek-ai/dsh-credentials')
|
|
11
|
+
if (typeof mod.credentialRef === 'function') {
|
|
12
|
+
return mod.credentialRef(name)
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
/* fallback when executed in standalone unit tests outside DSH bundle */
|
|
16
|
+
}
|
|
17
|
+
return typeof name === 'object' && name !== null ? name : { type: 'env', name: String(name || '') }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveApiKey(apiKeyEnv, env = process.env) {
|
|
21
|
+
const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
22
|
+
return {
|
|
23
|
+
envName: name,
|
|
24
|
+
value: String(env[name] || ''),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function saveCredentialKey(ctx, apiKeyEnv, apiKey) {
|
|
29
|
+
const name = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
30
|
+
const value = String(apiKey || '').trim()
|
|
31
|
+
|
|
32
|
+
if (!value) {
|
|
33
|
+
throw new Error('API key cannot be empty')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const credentials = ctx?.credentials || ctx?.get?.('credentials')
|
|
37
|
+
if (!credentials || typeof credentials.set !== 'function') {
|
|
38
|
+
throw new Error('DSH credentials service is unavailable in this runtime profile')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ref = await toCredentialRef(name)
|
|
42
|
+
await credentials.set(ref, value)
|
|
43
|
+
return { ok: true, envName: name }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function deleteCredentialKey(ctx, apiKeyEnv) {
|
|
47
|
+
const name = String(apiKeyEnv || '').trim()
|
|
48
|
+
if (!name) return { ok: false }
|
|
49
|
+
const credentials = ctx?.credentials || ctx?.get?.('credentials')
|
|
50
|
+
if (!credentials) return { ok: false }
|
|
51
|
+
try {
|
|
52
|
+
const ref = await toCredentialRef(name)
|
|
53
|
+
if (typeof credentials.unset === 'function') {
|
|
54
|
+
await credentials.unset(ref)
|
|
55
|
+
return { ok: true, envName: name }
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Failed unsetting credential: ' + (err?.message || err))
|
|
59
|
+
}
|
|
60
|
+
return { ok: false }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function resolveKeyValue(ctx, apiKeyEnv) {
|
|
64
|
+
const refName = String(apiKeyEnv || DEFAULT_API_KEY_ENV).trim() || DEFAULT_API_KEY_ENV
|
|
65
|
+
const creds = (ctx?.get && ctx.get('credentials')) || ctx?.credentials
|
|
66
|
+
if (creds && typeof creds.resolve === 'function') {
|
|
67
|
+
try {
|
|
68
|
+
const ref = await toCredentialRef(refName)
|
|
69
|
+
const hit = await creds.resolve(ref)
|
|
70
|
+
if (hit?.value) {
|
|
71
|
+
return { envName: refName, value: hit.value, source: 'credentials' }
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
/* miss */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fromEnv = resolveApiKey(refName)
|
|
79
|
+
if (fromEnv.value) {
|
|
80
|
+
return { ...fromEnv, source: 'env' }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { envName: refName, value: '', source: 'none' }
|
|
84
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -26,7 +26,11 @@ import { registerAuthRoutes } from './routes/auth.js'
|
|
|
26
26
|
import { registerSlashCommand } from './slash-command.js'
|
|
27
27
|
|
|
28
28
|
export const name = '@goodandready/dsh-clinebot'
|
|
29
|
-
export const inject = ['
|
|
29
|
+
export const inject = ['webServer', 'credentials']
|
|
30
|
+
|
|
31
|
+
export function configReader(config) {
|
|
32
|
+
return () => config
|
|
33
|
+
}
|
|
30
34
|
|
|
31
35
|
export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
|
|
32
36
|
|
|
@@ -56,73 +60,62 @@ export function apply(ctx, config) {
|
|
|
56
60
|
autoDiscoverPlanModels(ctx, { live, getSettingsApi: () => settingsApi, syncProviderState })
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return svc.describe?.().find((row) => row.ns === NS)?.revision
|
|
65
|
-
} catch {
|
|
66
|
-
return undefined
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
get: () => live(),
|
|
71
|
-
replace: async (next) => {
|
|
72
|
-
const parsed = Config(structuredClone(plainConfig(next)))
|
|
73
|
-
currentConfig = parsed
|
|
74
|
-
const payload = volatileConfig(parsed)
|
|
75
|
-
if (typeof svc.replace === 'function') {
|
|
76
|
-
await svc.replace(NS, payload, getRevision())
|
|
77
|
-
} else if (typeof svc.update === 'function') {
|
|
78
|
-
await svc.update(NS, payload, getRevision())
|
|
79
|
-
}
|
|
80
|
-
return parsed
|
|
81
|
-
},
|
|
82
|
-
update: async (patch) => {
|
|
83
|
-
const merged = Config({ ...publicConfig(live()), ...plainConfig(patch) })
|
|
84
|
-
currentConfig = merged
|
|
85
|
-
const payload = volatileConfig(merged)
|
|
86
|
-
if (typeof svc.update === 'function') {
|
|
87
|
-
await svc.update(NS, payload, getRevision())
|
|
88
|
-
} else if (typeof svc.replace === 'function') {
|
|
89
|
-
await svc.replace(NS, payload, getRevision())
|
|
90
|
-
}
|
|
91
|
-
return merged
|
|
92
|
-
},
|
|
93
|
-
watch: (cb) => {
|
|
94
|
-
if (typeof svc.watch === 'function') return svc.watch(cb)
|
|
95
|
-
return () => {}
|
|
96
|
-
},
|
|
97
|
-
}
|
|
63
|
+
const getRevision = (svc) => {
|
|
64
|
+
try {
|
|
65
|
+
return svc?.describe?.().find((row) => row.ns === NS)?.revision
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined
|
|
98
68
|
}
|
|
99
|
-
return undefined
|
|
100
69
|
}
|
|
101
70
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
71
|
+
settingsApi = {
|
|
72
|
+
get: () => live(),
|
|
73
|
+
replace: async (next) => {
|
|
74
|
+
const parsed = Config(structuredClone(plainConfig(next)))
|
|
75
|
+
currentConfig = parsed
|
|
76
|
+
const svc = ctx?.get?.('settings')
|
|
77
|
+
if (svc) {
|
|
78
|
+
const payload = volatileConfig(parsed)
|
|
79
|
+
if (typeof svc.replace === 'function') {
|
|
80
|
+
await svc.replace(NS, payload, getRevision(svc))
|
|
81
|
+
} else if (typeof svc.update === 'function') {
|
|
82
|
+
await svc.update(NS, payload, getRevision(svc))
|
|
83
|
+
}
|
|
113
84
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
85
|
+
await syncProviderState(live())
|
|
86
|
+
return parsed
|
|
87
|
+
},
|
|
88
|
+
update: async (patch) => {
|
|
89
|
+
const merged = Config({ ...publicConfig(live()), ...plainConfig(patch) })
|
|
90
|
+
currentConfig = merged
|
|
91
|
+
const svc = ctx?.get?.('settings')
|
|
92
|
+
if (svc) {
|
|
93
|
+
const payload = volatileConfig(merged)
|
|
94
|
+
if (typeof svc.update === 'function') {
|
|
95
|
+
await svc.update(NS, payload, getRevision(svc))
|
|
96
|
+
} else if (typeof svc.replace === 'function') {
|
|
97
|
+
await svc.replace(NS, payload, getRevision(svc))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await syncProviderState(live())
|
|
101
|
+
return merged
|
|
102
|
+
},
|
|
103
|
+
watch: (cb) => {
|
|
104
|
+
const svc = ctx?.get?.('settings')
|
|
105
|
+
if (typeof svc?.watch === 'function') return svc.watch(cb)
|
|
106
|
+
return () => {}
|
|
107
|
+
},
|
|
119
108
|
}
|
|
120
109
|
|
|
121
110
|
syncProviderState(live())
|
|
122
111
|
if (typeof ctx.effect === 'function') {
|
|
123
112
|
ctx.effect(() => {
|
|
124
113
|
const timer = setTimeout(triggerAutoDiscover, 500)
|
|
125
|
-
|
|
114
|
+
const retryTimer = setTimeout(triggerAutoDiscover, 3000)
|
|
115
|
+
return () => {
|
|
116
|
+
clearTimeout(timer)
|
|
117
|
+
clearTimeout(retryTimer)
|
|
118
|
+
}
|
|
126
119
|
}, 'dsh-clinebot: auto-discover')
|
|
127
120
|
}
|
|
128
121
|
|
package/lib/models.js
CHANGED
|
@@ -11,6 +11,29 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export const PROVIDER_ID = 'clinebot'
|
|
14
|
+
|
|
15
|
+
export const OBSOLETE_MODEL_ID_MAP = Object.freeze({
|
|
16
|
+
'cline-pass/deepseek-v41-flash': 'cline-pass/deepseek-v4.1-flash',
|
|
17
|
+
'cline-pass/qwen38-max': 'cline-pass/qwen3.8-max',
|
|
18
|
+
'cline-pass/glm-53': 'cline-pass/glm-5.3',
|
|
19
|
+
'cline-pass/glm-53-flash': 'cline-pass/glm-5.3-flash',
|
|
20
|
+
'cline-pass/muse-spark-13-contributor': 'cline-pass/muse-spark-1.3-contributor',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export function migrateModelId(id) {
|
|
24
|
+
if (typeof id !== 'string') return id
|
|
25
|
+
return OBSOLETE_MODEL_ID_MAP[id] || id
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function migrateModelEntry(model) {
|
|
29
|
+
if (!model || typeof model !== 'object') return model
|
|
30
|
+
const canonicalId = migrateModelId(model.id)
|
|
31
|
+
if (canonicalId === model.id) return model
|
|
32
|
+
return {
|
|
33
|
+
...model,
|
|
34
|
+
id: canonicalId,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
14
37
|
export const PROVIDER_DISPLAY_NAME = 'ClineBot (ClinePass)'
|
|
15
38
|
export const DEFAULT_MODEL_ID = 'cline-pass/deepseek-v4-flash'
|
|
16
39
|
|
|
@@ -314,10 +337,17 @@ export function parsePlanIncludedModels(includedInput) {
|
|
|
314
337
|
* Applies user-defined modelContextOverrides (custom contextLength and maxTokens).
|
|
315
338
|
*/
|
|
316
339
|
export function getAllModels(dynamicModels = [], modelContextOverrides = []) {
|
|
340
|
+
const normalizedDynamic = (Array.isArray(dynamicModels) ? dynamicModels : []).map(migrateModelEntry)
|
|
317
341
|
const overridesMap = new Map()
|
|
318
342
|
if (Array.isArray(modelContextOverrides)) {
|
|
319
343
|
for (const ov of modelContextOverrides) {
|
|
320
|
-
if (ov && ov.modelId)
|
|
344
|
+
if (ov && ov.modelId) {
|
|
345
|
+
overridesMap.set(ov.modelId, ov)
|
|
346
|
+
const canonicalId = migrateModelId(ov.modelId)
|
|
347
|
+
if (canonicalId !== ov.modelId) {
|
|
348
|
+
overridesMap.set(canonicalId, { ...ov, modelId: canonicalId })
|
|
349
|
+
}
|
|
350
|
+
}
|
|
321
351
|
}
|
|
322
352
|
}
|
|
323
353
|
|
|
@@ -344,9 +374,9 @@ export function getAllModels(dynamicModels = [], modelContextOverrides = []) {
|
|
|
344
374
|
}
|
|
345
375
|
}
|
|
346
376
|
|
|
347
|
-
if (
|
|
377
|
+
if (normalizedDynamic.length > 0) {
|
|
348
378
|
const normalize = (s) => String(s || '').toLowerCase().replace(/[\s._-]+/g, '')
|
|
349
|
-
return
|
|
379
|
+
return normalizedDynamic.map((dm) => {
|
|
350
380
|
const targetNorm = normalize(dm.id.replace(/^cline-pass\//, ''))
|
|
351
381
|
const curated = CLINE_MODELS.find(
|
|
352
382
|
(cm) => cm.id === dm.id || normalize(cm.id.replace(/^cline-pass\//, '')) === targetNorm || normalize(cm.name) === targetNorm
|
|
@@ -476,9 +506,18 @@ export async function loadModelsDiskCache(cachePath) {
|
|
|
476
506
|
const fs = await import('node:fs/promises')
|
|
477
507
|
const raw = await fs.readFile(cachePath, 'utf8')
|
|
478
508
|
const parsed = JSON.parse(raw)
|
|
479
|
-
const
|
|
480
|
-
if (!
|
|
509
|
+
const rawList = Array.isArray(parsed?.models) ? parsed.models : (Array.isArray(parsed) ? parsed : null)
|
|
510
|
+
if (!rawList) return null
|
|
511
|
+
let hasObsolete = false
|
|
512
|
+
const list = rawList.map((m) => {
|
|
513
|
+
const migrated = migrateModelEntry(m)
|
|
514
|
+
if (migrated.id !== m.id) hasObsolete = true
|
|
515
|
+
return migrated
|
|
516
|
+
})
|
|
481
517
|
list.planSyncedAt = typeof parsed?.planSyncedAt === 'number' ? parsed.planSyncedAt : (typeof parsed?.savedAt === 'number' ? parsed.savedAt : 0)
|
|
518
|
+
if (hasObsolete) {
|
|
519
|
+
saveModelsDiskCache(cachePath, list, list.planSyncedAt).catch(() => {})
|
|
520
|
+
}
|
|
482
521
|
return list
|
|
483
522
|
} catch {
|
|
484
523
|
return null
|
package/lib/provider-sync.js
CHANGED
|
@@ -217,12 +217,26 @@ export async function autoDiscoverPlanModels(ctx, { live, getSettingsApi, syncPr
|
|
|
217
217
|
const cacheFile = resolvePathWithHome(pub.modelsCachePath)
|
|
218
218
|
const settingsApi = getSettingsApi()
|
|
219
219
|
|
|
220
|
+
// Auto-migrate obsolete IDs (e.g. deepseek-v41-flash -> deepseek-v4.1-flash)
|
|
221
|
+
const existingDynamic = pub.dynamicModels || []
|
|
222
|
+
const hasObsolete = existingDynamic.some((m) => Boolean(OBSOLETE_MODEL_ID_MAP[m?.id]))
|
|
223
|
+
if (hasObsolete && settingsApi?.replace) {
|
|
224
|
+
const migrated = existingDynamic.map(migrateModelEntry)
|
|
225
|
+
const next = Config({
|
|
226
|
+
...plainConfig(live()),
|
|
227
|
+
dynamicModels: migrated,
|
|
228
|
+
})
|
|
229
|
+
await settingsApi.replace(next)
|
|
230
|
+
await syncProviderState(next)
|
|
231
|
+
}
|
|
232
|
+
|
|
220
233
|
if ((!pub.dynamicModels || !pub.dynamicModels.length) && cacheFile) {
|
|
221
234
|
const fromDisk = await loadModelsDiskCache(cacheFile)
|
|
222
235
|
if (Array.isArray(fromDisk) && fromDisk.length && settingsApi?.replace) {
|
|
236
|
+
const migratedDisk = fromDisk.map(migrateModelEntry)
|
|
223
237
|
const next = Config({
|
|
224
238
|
...plainConfig(live()),
|
|
225
|
-
dynamicModels:
|
|
239
|
+
dynamicModels: migratedDisk,
|
|
226
240
|
planSyncedAt: fromDisk.planSyncedAt || Date.now(),
|
|
227
241
|
})
|
|
228
242
|
await settingsApi.replace(next)
|
package/lib/slash-command.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from './cline-client.js'
|
|
23
23
|
|
|
24
24
|
export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderState }) {
|
|
25
|
+
if (typeof ctx.inject !== 'function') return
|
|
25
26
|
ctx.inject(['commands'], (cmdCtx) => {
|
|
26
27
|
const commands = cmdCtx.commands
|
|
27
28
|
if (typeof commands?.register !== 'function') return
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"README.ru.md",
|
|
19
19
|
"README.zh.md",
|
|
20
20
|
"CHANGELOG.md",
|
|
21
|
-
"LICENSE"
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"media/visual-verification.png"
|
|
22
23
|
],
|
|
23
24
|
"keywords": [
|
|
24
25
|
"dsh",
|