@goodandready/dsh-clinebot 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +61 -23
- package/docs/README.ru.md +186 -0
- package/docs/README.zh.md +188 -0
- package/docs/design/DESIGN.md +63 -0
- package/lib/client.js +181 -198
- package/lib/cline-client.js +70 -5
- package/lib/index.js +113 -81
- package/lib/models.js +58 -48
- package/package.json +3 -4
- package/README.ru.md +0 -131
- package/README.zh.md +0 -68
package/lib/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
PROVIDER_DISPLAY_NAME,
|
|
9
9
|
getAllModels,
|
|
10
10
|
getDefaultModelIds,
|
|
11
|
-
|
|
11
|
+
parsePlanIncludedModels,
|
|
12
12
|
} from './models.js'
|
|
13
13
|
import {
|
|
14
14
|
DEFAULT_BASE_URL,
|
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
probeHealth,
|
|
23
23
|
smokeChat,
|
|
24
24
|
buildPiAiProvider,
|
|
25
|
+
sessionStats,
|
|
26
|
+
recordSessionRequest,
|
|
27
|
+
resetSessionStats,
|
|
25
28
|
} from './cline-client.js'
|
|
26
29
|
|
|
27
30
|
export const name = '@goodandready/dsh-clinebot'
|
|
@@ -30,6 +33,8 @@ export const inject = ['settings', 'webServer', 'credentials']
|
|
|
30
33
|
export const NS = 'dsh-clinebot'
|
|
31
34
|
export const LLM_PI_AI_NS = 'llm-pi-ai'
|
|
32
35
|
|
|
36
|
+
export { sessionStats, recordSessionRequest, resetSessionStats }
|
|
37
|
+
|
|
33
38
|
export const Config = z.object({
|
|
34
39
|
enabled: z.boolean().default(true)
|
|
35
40
|
.description('When true, ClineBot is registered as a model provider in DSH.'),
|
|
@@ -41,7 +46,7 @@ export const Config = z.object({
|
|
|
41
46
|
.description('Default model ID for chat and smoke tests.'),
|
|
42
47
|
enabledModels: z.array(z.string()).default(getDefaultModelIds())
|
|
43
48
|
.description('List of model IDs enabled for selection in DSH.'),
|
|
44
|
-
|
|
49
|
+
dynamicModels: z.array(z.object({
|
|
45
50
|
id: z.string(),
|
|
46
51
|
name: z.string(),
|
|
47
52
|
description: z.string().default(''),
|
|
@@ -49,9 +54,9 @@ export const Config = z.object({
|
|
|
49
54
|
maxTokens: z.number().default(8192),
|
|
50
55
|
input: z.array(z.string()).default(['text']),
|
|
51
56
|
category: z.string().default('general'),
|
|
52
|
-
isCustom: z.boolean().default(
|
|
57
|
+
isCustom: z.boolean().default(false),
|
|
53
58
|
})).default([])
|
|
54
|
-
.description('
|
|
59
|
+
.description('Models automatically discovered from the official ClinePass subscription plan.'),
|
|
55
60
|
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
|
|
56
61
|
.description('HTTP probe timeout in milliseconds.'),
|
|
57
62
|
smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
|
|
@@ -59,14 +64,14 @@ export const Config = z.object({
|
|
|
59
64
|
})
|
|
60
65
|
|
|
61
66
|
function publicConfig(cfg) {
|
|
62
|
-
const
|
|
63
|
-
const allDefaultIds = getDefaultModelIds(
|
|
67
|
+
const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
|
|
68
|
+
const allDefaultIds = getDefaultModelIds(dynamic)
|
|
64
69
|
return {
|
|
65
70
|
enabled: !!cfg?.enabled,
|
|
66
71
|
baseUrl: normalizeBaseUrl(cfg?.baseUrl),
|
|
67
72
|
apiKeyEnv: cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
68
73
|
defaultModel: cfg?.defaultModel || DEFAULT_MODEL_ID,
|
|
69
|
-
|
|
74
|
+
dynamicModels: dynamic,
|
|
70
75
|
enabledModels: Array.isArray(cfg?.enabledModels) && cfg.enabledModels.length
|
|
71
76
|
? cfg.enabledModels
|
|
72
77
|
: allDefaultIds,
|
|
@@ -112,13 +117,32 @@ async function buildStatus(ctx, cfg) {
|
|
|
112
117
|
const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
|
|
113
118
|
const health = await probeHealth(pub.baseUrl, { timeoutMs: pub.timeoutMs })
|
|
114
119
|
const isRegistered = await checkRegisteredInPiAi(ctx)
|
|
115
|
-
const allModels = getAllModels(pub.
|
|
120
|
+
const allModels = getAllModels(pub.dynamicModels)
|
|
116
121
|
|
|
117
122
|
let usage = null
|
|
118
123
|
if (key.value) {
|
|
119
124
|
usage = await fetchUsageLimits(pub.baseUrl, key.value, { timeoutMs: pub.timeoutMs }).catch(() => null)
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
// Evaluate warning state
|
|
128
|
+
let quotaWarning = null
|
|
129
|
+
if (usage?.windows?.fiveHour) {
|
|
130
|
+
const pct = usage.windows.fiveHour.percentUsed
|
|
131
|
+
if (pct >= 95) {
|
|
132
|
+
quotaWarning = {
|
|
133
|
+
level: 'exhausted',
|
|
134
|
+
message: `5-часовой лимит почти полностью исчерпан (${pct}%). Новые запросы могут отклоняться провайдером до сброса.`,
|
|
135
|
+
resetsAt: usage.windows.fiveHour.resetsAt,
|
|
136
|
+
}
|
|
137
|
+
} else if (pct >= 80) {
|
|
138
|
+
quotaWarning = {
|
|
139
|
+
level: 'warning',
|
|
140
|
+
message: `Внимание: израсходовано ${pct}% 5-часового скользящего лимита.`,
|
|
141
|
+
resetsAt: usage.windows.fiveHour.resetsAt,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
122
146
|
return {
|
|
123
147
|
ok: true,
|
|
124
148
|
providerId: PROVIDER_ID,
|
|
@@ -131,27 +155,29 @@ async function buildStatus(ctx, cfg) {
|
|
|
131
155
|
},
|
|
132
156
|
health,
|
|
133
157
|
usage,
|
|
158
|
+
quotaWarning,
|
|
159
|
+
sessionStats: { ...sessionStats },
|
|
134
160
|
isRegistered,
|
|
135
161
|
availableModels: allModels,
|
|
136
162
|
}
|
|
137
163
|
}
|
|
138
164
|
|
|
139
|
-
async function upsertPiAiProvider(ctx, cfg,
|
|
165
|
+
async function upsertPiAiProvider(ctx, cfg, dynamicModelIds) {
|
|
140
166
|
const settings = ctx?.get?.('settings')
|
|
141
167
|
if (!settings?.mutate) {
|
|
142
168
|
throw new Error('DSH settings service unavailable')
|
|
143
169
|
}
|
|
144
170
|
|
|
145
171
|
const pub = publicConfig(cfg)
|
|
146
|
-
const allModels = getAllModels(pub.
|
|
147
|
-
const selectedIds = new Set(
|
|
172
|
+
const allModels = getAllModels(pub.dynamicModels)
|
|
173
|
+
const selectedIds = new Set(dynamicModelIds || pub.enabledModels)
|
|
148
174
|
const modelsToRegister = allModels.filter((m) => selectedIds.has(m.id))
|
|
149
175
|
|
|
150
176
|
const providerObj = buildPiAiProvider({
|
|
151
177
|
baseUrl: pub.baseUrl,
|
|
152
178
|
apiKeyEnv: pub.apiKeyEnv,
|
|
153
179
|
models: modelsToRegister.length ? modelsToRegister : allModels,
|
|
154
|
-
customModels: pub.
|
|
180
|
+
customModels: pub.dynamicModels,
|
|
155
181
|
displayName: PROVIDER_DISPLAY_NAME,
|
|
156
182
|
})
|
|
157
183
|
|
|
@@ -387,96 +413,82 @@ export function apply(ctx, config) {
|
|
|
387
413
|
model: modelToTest,
|
|
388
414
|
timeoutMs: pub.smokeTimeoutMs,
|
|
389
415
|
})
|
|
416
|
+
recordSessionRequest({
|
|
417
|
+
latencyMs: outcome.latencyMs,
|
|
418
|
+
ok: outcome.ok,
|
|
419
|
+
error: outcome.error,
|
|
420
|
+
promptTokens: 5,
|
|
421
|
+
completionTokens: 10,
|
|
422
|
+
})
|
|
390
423
|
writeJson(res, outcome.ok ? 200 : 502, outcome)
|
|
391
424
|
} catch (err) {
|
|
425
|
+
recordSessionRequest({ ok: false, error: String(err?.message || err) })
|
|
392
426
|
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
393
427
|
}
|
|
394
428
|
},
|
|
395
429
|
}), 'dsh-clinebot: /smoke')
|
|
396
430
|
|
|
397
|
-
// 8. POST
|
|
431
|
+
// 8. POST /dsh-clinebot/models/sync — sync real models from official ClinePass subscription plan
|
|
398
432
|
ctx.effect(() => ctx.webServer.register({
|
|
399
433
|
kind: 'exact',
|
|
400
|
-
path: '/dsh-clinebot/models/
|
|
434
|
+
path: '/dsh-clinebot/models/sync',
|
|
401
435
|
handler: async (req, res) => {
|
|
402
436
|
if (!isTrustedSettingsRequest(req)) {
|
|
403
437
|
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
404
438
|
}
|
|
405
|
-
if (req.method
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const currentCustom = Array.isArray(live().customModels) ? [...live().customModels] : []
|
|
417
|
-
const existingIdx = currentCustom.findIndex((m) => m.id === validated.model.id)
|
|
418
|
-
if (existingIdx >= 0) {
|
|
419
|
-
currentCustom[existingIdx] = validated.model
|
|
420
|
-
} else {
|
|
421
|
-
currentCustom.push(validated.model)
|
|
422
|
-
}
|
|
439
|
+
if (req.method !== 'POST') {
|
|
440
|
+
return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const pub = publicConfig(live())
|
|
444
|
+
const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
|
|
445
|
+
if (!key.value) {
|
|
446
|
+
return writeJson(res, 400, { ok: false, error: 'API key not configured' })
|
|
447
|
+
}
|
|
423
448
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const next = Config({
|
|
429
|
-
...live(),
|
|
430
|
-
customModels: currentCustom,
|
|
431
|
-
enabledModels: Array.from(currentEnabled),
|
|
432
|
-
})
|
|
433
|
-
await settingsApi.replace(next)
|
|
434
|
-
if (await checkRegisteredInPiAi(ctx)) {
|
|
435
|
-
await upsertPiAiProvider(ctx, next, Array.from(currentEnabled))
|
|
436
|
-
}
|
|
437
|
-
}
|
|
449
|
+
const usageData = await fetchUsageLimits(pub.baseUrl, key.value, {
|
|
450
|
+
timeoutMs: pub.timeoutMs,
|
|
451
|
+
bypassCache: true,
|
|
452
|
+
})
|
|
438
453
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
454
|
+
if (!usageData.ok) {
|
|
455
|
+
return writeJson(res, 502, { ok: false, error: usageData.error || 'Failed to fetch plan models' })
|
|
442
456
|
}
|
|
443
|
-
}
|
|
444
457
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
let body = {}
|
|
449
|
-
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
|
|
458
|
+
const dynamicModels = Array.isArray(usageData.dynamicModels) ? usageData.dynamicModels : []
|
|
459
|
+
const allModels = getAllModels(dynamicModels)
|
|
460
|
+
const allIds = allModels.map((m) => m.id)
|
|
450
461
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
462
|
+
// Preserve currently enabled models, plus add any newly discovered ones
|
|
463
|
+
const existingEnabled = new Set(live().enabledModels || getDefaultModelIds())
|
|
464
|
+
for (const m of allModels) {
|
|
465
|
+
existingEnabled.add(m.id)
|
|
466
|
+
}
|
|
455
467
|
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
await settingsApi.replace(next)
|
|
466
|
-
if (await checkRegisteredInPiAi(ctx)) {
|
|
467
|
-
await upsertPiAiProvider(ctx, next, currentEnabled)
|
|
468
|
-
}
|
|
468
|
+
if (settingsApi?.replace) {
|
|
469
|
+
const next = Config({
|
|
470
|
+
...live(),
|
|
471
|
+
dynamicModels,
|
|
472
|
+
enabledModels: Array.from(existingEnabled),
|
|
473
|
+
})
|
|
474
|
+
await settingsApi.replace(next)
|
|
475
|
+
if (await checkRegisteredInPiAi(ctx)) {
|
|
476
|
+
await upsertPiAiProvider(ctx, next, Array.from(existingEnabled))
|
|
469
477
|
}
|
|
470
|
-
|
|
471
|
-
return writeJson(res, 200, { ok: true, removed: modelId, customModels: currentCustom })
|
|
472
|
-
} catch (err) {
|
|
473
|
-
return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
474
478
|
}
|
|
475
|
-
}
|
|
476
479
|
|
|
477
|
-
|
|
480
|
+
return writeJson(res, 200, {
|
|
481
|
+
ok: true,
|
|
482
|
+
plan: usageData.plan,
|
|
483
|
+
discoveredCount: dynamicModels.length,
|
|
484
|
+
totalModelsCount: allModels.length,
|
|
485
|
+
models: allModels,
|
|
486
|
+
})
|
|
487
|
+
} catch (err) {
|
|
488
|
+
return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
489
|
+
}
|
|
478
490
|
},
|
|
479
|
-
}), 'dsh-clinebot: /models/
|
|
491
|
+
}), 'dsh-clinebot: /models/sync')
|
|
480
492
|
|
|
481
493
|
// 9. POST /dsh-clinebot/models/toggle — toggle enabled status in picker
|
|
482
494
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -516,7 +528,7 @@ export function apply(ctx, config) {
|
|
|
516
528
|
|
|
517
529
|
const unregister = commands.register({
|
|
518
530
|
name: 'cline',
|
|
519
|
-
description: 'Check ClinePass subscription quota, rate limits and latency',
|
|
531
|
+
description: 'Check ClinePass subscription quota, rate limits, session stats and latency',
|
|
520
532
|
execute: async () => {
|
|
521
533
|
const pub = publicConfig(live())
|
|
522
534
|
const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
|
|
@@ -544,6 +556,16 @@ export function apply(ctx, config) {
|
|
|
544
556
|
`**📅 Недельное окно**: ${formatProgressBar(weekly?.percentUsed)} (сброс: ${resetWk})`,
|
|
545
557
|
]
|
|
546
558
|
|
|
559
|
+
if (fiveHour?.percentUsed >= 95) {
|
|
560
|
+
lines.push('', '🚨 **КРИТИЧЕСКИЙ ЛИМИТ**: 5-часовая квота израсходована на 95%+. Запросы могут быть заблокированы до сброса!')
|
|
561
|
+
} else if (fiveHour?.percentUsed >= 80) {
|
|
562
|
+
lines.push('', '⚠️ **Внимание**: 5-часовая квота израсходована на ' + fiveHour.percentUsed + '%.')
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (sessionStats.totalRequests > 0) {
|
|
566
|
+
lines.push('', `**📊 Сессия DSH**: ${sessionStats.successfulRequests}/${sessionStats.totalRequests} успешных запросов, ~${sessionStats.totalTokensEst} токенов`)
|
|
567
|
+
}
|
|
568
|
+
|
|
547
569
|
if (usage?.user?.email) {
|
|
548
570
|
lines.push(`* **Аккаунт**: \`${usage.user.email}\``)
|
|
549
571
|
}
|
|
@@ -559,6 +581,8 @@ export function apply(ctx, config) {
|
|
|
559
581
|
getStatus: () => buildStatus(ctx, live()),
|
|
560
582
|
registerProvider: (models) => upsertPiAiProvider(ctx, live(), models),
|
|
561
583
|
unregisterProvider: () => removePiAiProvider(ctx),
|
|
584
|
+
recordRequestMetrics: (metrics) => recordSessionRequest(metrics),
|
|
585
|
+
getSessionStats: () => ({ ...sessionStats }),
|
|
562
586
|
getUsageLimits: async () => {
|
|
563
587
|
const pub = publicConfig(live())
|
|
564
588
|
const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
|
|
@@ -567,7 +591,15 @@ export function apply(ctx, config) {
|
|
|
567
591
|
runSmokeTest: async (model) => {
|
|
568
592
|
const pub = publicConfig(live())
|
|
569
593
|
const key = await resolveKeyValue(ctx, pub.apiKeyEnv)
|
|
570
|
-
|
|
594
|
+
const res = await smokeChat(pub.baseUrl, key.value, { model: model || pub.defaultModel })
|
|
595
|
+
recordSessionRequest({
|
|
596
|
+
latencyMs: res.latencyMs,
|
|
597
|
+
ok: res.ok,
|
|
598
|
+
error: res.error,
|
|
599
|
+
promptTokens: 5,
|
|
600
|
+
completionTokens: 10,
|
|
601
|
+
})
|
|
602
|
+
return res
|
|
571
603
|
},
|
|
572
604
|
}
|
|
573
605
|
}
|
package/lib/models.js
CHANGED
|
@@ -135,60 +135,69 @@ export const CLINE_MODELS = Object.freeze([
|
|
|
135
135
|
])
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
138
|
+
* Parse human-readable included models string from ClinePass plan features.
|
|
139
|
+
* Example: "Includes Kimi K3, GLM 5.2, Kimi K2.6, Kimi K2.7 Code, Mimo v2.5, Mimo v2.5 Pro, Minimax M3, Qwen3.7 Plus, Qwen3.7 Max, DeepSeek V4 Pro, and DeepSeek V4 Flash"
|
|
139
140
|
*/
|
|
140
|
-
export function
|
|
141
|
-
if (!
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
export function parsePlanIncludedModels(includedText) {
|
|
142
|
+
if (!includedText || typeof includedText !== 'string') return []
|
|
143
|
+
const clean = includedText
|
|
144
|
+
.replace(/^includes\s+/i, '')
|
|
145
|
+
.replace(/\band\b/gi, ',')
|
|
146
|
+
.replace(/\./g, '')
|
|
147
|
+
.trim()
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// If user didn't prefix with cline-pass/, allow both or auto-prefix if helpful
|
|
150
|
-
if (!id.startsWith('cline-pass/') && !id.includes('/')) {
|
|
151
|
-
id = `cline-pass/${id}`
|
|
152
|
-
}
|
|
149
|
+
const parts = clean
|
|
150
|
+
.split(',')
|
|
151
|
+
.map((p) => p.trim())
|
|
152
|
+
.filter(Boolean)
|
|
153
153
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const input = hasVision ? ['text', 'image'] : ['text']
|
|
159
|
-
const category = ['coding', 'reasoning', 'multimodal', 'general'].includes(raw.category)
|
|
160
|
-
? raw.category
|
|
161
|
-
: hasVision ? 'multimodal' : 'general'
|
|
154
|
+
const normalize = (s) =>
|
|
155
|
+
String(s || '')
|
|
156
|
+
.toLowerCase()
|
|
157
|
+
.replace(/[\s._-]+/g, '')
|
|
162
158
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
159
|
+
const matched = []
|
|
160
|
+
for (const name of parts) {
|
|
161
|
+
const targetNorm = normalize(name)
|
|
162
|
+
const found = CLINE_MODELS.find(
|
|
163
|
+
(m) =>
|
|
164
|
+
normalize(m.name) === targetNorm ||
|
|
165
|
+
normalize(m.id.replace(/^cline-pass\//, '')) === targetNorm
|
|
166
|
+
)
|
|
167
|
+
if (found) {
|
|
168
|
+
matched.push(found)
|
|
169
|
+
} else {
|
|
170
|
+
// Dynamic fallback for newly introduced models mentioned in plan
|
|
171
|
+
const idPart = name.toLowerCase().replace(/[\s_]+/g, '-')
|
|
172
|
+
matched.push({
|
|
173
|
+
id: `cline-pass/${idPart}`,
|
|
174
|
+
name,
|
|
175
|
+
description: `Official ClinePass subscription model: ${name}`,
|
|
176
|
+
contextLength: 200000,
|
|
177
|
+
maxTokens: 8192,
|
|
178
|
+
input: ['text'],
|
|
179
|
+
category: 'general',
|
|
180
|
+
recommended: false,
|
|
181
|
+
isCustom: false,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
176
184
|
}
|
|
185
|
+
|
|
186
|
+
return matched
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
/**
|
|
180
|
-
* Get all models
|
|
190
|
+
* Get all models from known catalog.
|
|
181
191
|
*/
|
|
182
|
-
export function getAllModels(
|
|
192
|
+
export function getAllModels(dynamicModels = []) {
|
|
183
193
|
const result = [...CLINE_MODELS]
|
|
184
194
|
const seenIds = new Set(result.map((m) => m.id))
|
|
185
195
|
|
|
186
|
-
if (Array.isArray(
|
|
187
|
-
for (const item of
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
seenIds.add(valid.model.id)
|
|
196
|
+
if (Array.isArray(dynamicModels)) {
|
|
197
|
+
for (const item of dynamicModels) {
|
|
198
|
+
if (item && item.id && !seenIds.has(item.id)) {
|
|
199
|
+
result.push(item)
|
|
200
|
+
seenIds.add(item.id)
|
|
192
201
|
}
|
|
193
202
|
}
|
|
194
203
|
}
|
|
@@ -196,15 +205,16 @@ export function getAllModels(customModels = []) {
|
|
|
196
205
|
return result
|
|
197
206
|
}
|
|
198
207
|
|
|
199
|
-
export function findModel(id,
|
|
200
|
-
const all = getAllModels(
|
|
208
|
+
export function findModel(id, dynamicModels = []) {
|
|
209
|
+
const all = getAllModels(dynamicModels)
|
|
201
210
|
return all.find((m) => m.id === id) || null
|
|
202
211
|
}
|
|
203
212
|
|
|
204
|
-
export function isSupportedModel(id,
|
|
205
|
-
return findModel(id,
|
|
213
|
+
export function isSupportedModel(id, dynamicModels = []) {
|
|
214
|
+
return findModel(id, dynamicModels) !== null
|
|
206
215
|
}
|
|
207
216
|
|
|
208
|
-
export function getDefaultModelIds(
|
|
209
|
-
return getAllModels(
|
|
217
|
+
export function getDefaultModelIds(dynamicModels = []) {
|
|
218
|
+
return getAllModels(dynamicModels).map((m) => m.id)
|
|
210
219
|
}
|
|
220
|
+
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness companion for ClineBot / ClinePass:
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"main": "./lib/index.js",
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
"lib/",
|
|
16
16
|
"cordis.patch.yml",
|
|
17
17
|
"README.md",
|
|
18
|
-
"
|
|
19
|
-
"README.zh.md",
|
|
18
|
+
"docs/",
|
|
20
19
|
"CHANGELOG.md",
|
|
21
20
|
"LICENSE"
|
|
22
21
|
],
|
package/README.ru.md
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# 📦 @goodandready/dsh-clinebot
|
|
2
|
-
|
|
3
|
-
<div align="center">
|
|
4
|
-
|
|
5
|
-
<h3>Нативное подключение провайдера ClineBot / ClinePass для DeepSeek Harness</h3>
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<a href="https://www.npmjs.com/package/@goodandready/dsh-clinebot"><img src="https://img.shields.io/npm/v/@goodandready/dsh-clinebot.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
-
<a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-clinebot.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
-
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
-
</p>
|
|
13
|
-
|
|
14
|
-
<!-- Обязательная кнопка перехода на витрину всех проектов -->
|
|
15
|
-
<p align="center">
|
|
16
|
-
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/Все_проекты_автора-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="Все проекты автора"></a>
|
|
17
|
-
</p>
|
|
18
|
-
|
|
19
|
-
<p align="center">
|
|
20
|
-
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
21
|
-
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
22
|
-
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
|
-
</p>
|
|
24
|
-
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## ⚡ Обзор и решаемая проблема
|
|
30
|
-
|
|
31
|
-
**ClinePass** (`https://cline.bot`) — сервис единой фиксированной подписки (\$9.99/мес), предоставляющий разработчикам повышенные лимиты (в 2–5 раз выше стандартных) на передовые open-weights модели программирования и рассуждений через единый OpenAI-совместимый интерфейс (`https://api.cline.bot/api/v1`).
|
|
32
|
-
|
|
33
|
-
Плагин **`@goodandready/dsh-clinebot`** обеспечивает полноценную интеграцию подписки в DeepSeek Harness:
|
|
34
|
-
* 🖥️ **Отдельная страница в Настройках**: собственная полноэкранная страница в меню Настроек DSH (`Настройки → ClineBot`).
|
|
35
|
-
* 📊 **Дашборд лимитов подписки (Usage)**: наглядные прогресс-бары расхода 5-часового и недельного скользящего окна из официального API `GET /users/me/plan/usage-limits` с таймером сброса.
|
|
36
|
-
* 🔑 **Сохранение ключа прямо из UI**: поле ввода ключа с маскировкой; сохранение напрямую в системный сервис `credentials` (`~/.dsh/.credentials.yaml`) без ручной правки файлов на сервере.
|
|
37
|
-
* 🎯 **Управление моделями в пикере**: включение/выключение отображения конкретных моделей в диалогах чата.
|
|
38
|
-
* ➕ **Добавление кастомных моделей**: форма добавления новых моделей подписки (ID, имя, контекст, Vision) без необходимости ждать обновления плагина.
|
|
39
|
-
* 💬 **Слэш-команда `/cline` в чате**: просмотр остатка квот, задержки и активной модели прямо из чата.
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## 🏛️ Архитектура
|
|
44
|
-
|
|
45
|
-
```mermaid
|
|
46
|
-
graph LR
|
|
47
|
-
subgraph UI [Интерфейс DSH]
|
|
48
|
-
Page["Отдельная страница (Настройки -> ClineBot)"]
|
|
49
|
-
QuotaBar["Прогресс-бары 5h и недельного лимита"]
|
|
50
|
-
KeyInput["Ввод и сохранение API-ключа"]
|
|
51
|
-
ModelPick["Управление пикером и новые модели"]
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
subgraph PluginHost [Хост-часть dsh-clinebot]
|
|
55
|
-
HttpEndpoints["API: /api/plugins/dsh-clinebot/*"]
|
|
56
|
-
ClientCore["lib/cline-client.js"]
|
|
57
|
-
ModelCatalog["lib/models.js (Встроенные + Кастомные)"]
|
|
58
|
-
SlashCmd["Слэш-команда: /cline"]
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
subgraph DSHCore [Сервисы DSH]
|
|
62
|
-
Credentials["Сервис credentials (~/.dsh/.credentials.yaml)"]
|
|
63
|
-
PiAi["Настройки: llm-pi-ai.providers.clinebot"]
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
subgraph Upstream [Сервер Cline]
|
|
67
|
-
ClinePass["api.cline.bot/api/v1/chat/completions"]
|
|
68
|
-
ClineQuota["api.cline.bot/api/v1/users/me/plan/usage-limits"]
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
Page -->|GET /status & /usage| HttpEndpoints
|
|
72
|
-
KeyInput -->|POST /save-key| HttpEndpoints
|
|
73
|
-
ModelPick -->|POST /register & /models| HttpEndpoints
|
|
74
|
-
HttpEndpoints --> Credentials
|
|
75
|
-
HttpEndpoints --> ClientCore
|
|
76
|
-
ClientCore --> ModelCatalog
|
|
77
|
-
HttpEndpoints -->|Атомарная мутация| PiAi
|
|
78
|
-
ClientCore -->|Чат| ClinePass
|
|
79
|
-
ClientCore -->|Квоты| ClineQuota
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## 📦 Быстрая установка
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
dsh plugin --profile web add @goodandready/dsh-clinebot
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## 💬 Слэш-команда `/cline` в чате
|
|
93
|
-
|
|
94
|
-
В любой сессии чата введите команду `/cline` для проверки остатка лимитов:
|
|
95
|
-
|
|
96
|
-
```text
|
|
97
|
-
### 🤖 ClinePass Status (ClinePass ($9.99/mo))
|
|
98
|
-
* Пинг хоста: ✅ 210 мс
|
|
99
|
-
* Активный ключ: CLINEBOT_API_KEY (credentials)
|
|
100
|
-
* Модель по умолчанию: `cline-pass/deepseek-v4-flash`
|
|
101
|
-
|
|
102
|
-
⏱ 5-часовое окно: [████░░░░░░] 42% (сброс: 18:00)
|
|
103
|
-
📅 Недельное окно: [██████░░░░] 60% (сброс: 08.09)
|
|
104
|
-
* Аккаунт: `developer@example.com`
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
## ⚙️ Таблица конфигурации (`settings.yaml`)
|
|
110
|
-
|
|
111
|
-
```yaml
|
|
112
|
-
dsh-clinebot:
|
|
113
|
-
enabled: true
|
|
114
|
-
baseUrl: https://api.cline.bot/api/v1
|
|
115
|
-
apiKeyEnv: CLINEBOT_API_KEY
|
|
116
|
-
defaultModel: cline-pass/deepseek-v4-flash
|
|
117
|
-
timeoutMs: 15000
|
|
118
|
-
smokeTimeoutMs: 25000
|
|
119
|
-
enabledModels:
|
|
120
|
-
- cline-pass/deepseek-v4-flash
|
|
121
|
-
- cline-pass/deepseek-v4-pro
|
|
122
|
-
- cline-pass/kimi-k3
|
|
123
|
-
- cline-pass/qwen3.7-max
|
|
124
|
-
customModels: []
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## 📄 Лицензия
|
|
130
|
-
|
|
131
|
-
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|