@goodandready/dsh-clinebot 0.3.25 → 0.4.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 +17 -0
- package/README.md +21 -5
- package/README.ru.md +21 -5
- package/README.zh.md +21 -5
- package/lib/account-pool.js +59 -4
- package/lib/client.js +182 -64
- package/lib/cline-client.js +25 -44
- package/lib/config.js +25 -4
- package/lib/http.js +15 -2
- package/lib/index.js +116 -47
- package/lib/models.js +47 -22
- package/lib/provider-sync.js +15 -5
- package/lib/routes/accounts.js +8 -8
- package/lib/routes/auth.js +65 -37
- package/lib/routes/models.js +31 -28
- package/lib/routes/settings.js +51 -11
- package/lib/slash-command.js +51 -9
- package/lib/telemetry.js +78 -0
- package/package.json +4 -1
package/lib/routes/settings.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
2
|
import { isTrustedSettingsRequest, assertTrustedSettingsRequest } from '../access.js'
|
|
3
|
-
import { publicConfig, Config } from '../config.js'
|
|
3
|
+
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
4
4
|
import { buildStatus } from '../provider-sync.js'
|
|
5
5
|
import { saveCredentialKey, smokeChat, DEFAULT_API_KEY_ENV } from '../cline-client.js'
|
|
6
6
|
|
|
@@ -33,22 +33,42 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
33
33
|
if (req.method !== 'PUT') {
|
|
34
34
|
return writeJson(res, 405, { ok: false, error: 'GET or PUT' })
|
|
35
35
|
}
|
|
36
|
-
if (!
|
|
37
|
-
return writeJson(res, 403, { ok: false, error: 'same-origin only' })
|
|
38
|
-
}
|
|
36
|
+
if (!assertTrustedSettingsRequest(req, res)) return
|
|
39
37
|
const settingsApi = getSettingsApi()
|
|
40
38
|
if (!settingsApi) {
|
|
41
39
|
return writeJson(res, 503, { ok: false, error: 'settings not ready' })
|
|
42
40
|
}
|
|
41
|
+
let bodyBuf
|
|
42
|
+
try {
|
|
43
|
+
bodyBuf = await readBody(req)
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return writeJson(res, 400, { ok: false, error: String(err?.message || err) })
|
|
46
|
+
}
|
|
43
47
|
let payload
|
|
44
48
|
try {
|
|
45
|
-
payload = JSON.parse(
|
|
49
|
+
payload = JSON.parse(bodyBuf.toString('utf8') || '{}')
|
|
46
50
|
} catch {
|
|
47
51
|
return writeJson(res, 400, { ok: false, error: 'invalid json' })
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
const rawPayload = payload && typeof payload.config === 'object' && payload.config !== null ? payload.config : payload
|
|
54
|
+
if (!rawPayload || typeof rawPayload !== 'object' || Array.isArray(rawPayload)) {
|
|
55
|
+
return writeJson(res, 400, { ok: false, error: 'body must be a JSON object' })
|
|
56
|
+
}
|
|
57
|
+
if ('enabledModels' in rawPayload) {
|
|
58
|
+
return writeJson(res, 400, { ok: false, error: 'enabledModels is deprecated and not allowed; configure disabledModels instead' })
|
|
59
|
+
}
|
|
60
|
+
const allowedKeys = new Set(Object.keys(Config.dict || {}))
|
|
61
|
+
allowedKeys.delete('enabledModels')
|
|
62
|
+
for (const k of Object.keys(rawPayload)) {
|
|
63
|
+
if (!allowedKeys.has(k)) {
|
|
64
|
+
return writeJson(res, 400, { ok: false, error: `unknown config field: ${k}` })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
50
67
|
try {
|
|
51
|
-
const
|
|
68
|
+
const base = plainConfig(live()) || {}
|
|
69
|
+
delete base.enabledModels
|
|
70
|
+
const merged = { ...base, ...rawPayload }
|
|
71
|
+
const parsed = Config(merged)
|
|
52
72
|
await settingsApi.replace(parsed)
|
|
53
73
|
await syncProviderState(parsed)
|
|
54
74
|
writeJson(res, 200, { ok: true, config: publicConfig(live()) })
|
|
@@ -77,15 +97,35 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
77
97
|
|
|
78
98
|
const pub = publicConfig(live())
|
|
79
99
|
const targetEnvName = String(body.apiKeyEnv || pub.apiKeyEnv || DEFAULT_API_KEY_ENV).trim()
|
|
100
|
+
|
|
101
|
+
const isDefaultPattern = /^CLINEBOT_API_KEY(_[A-Z0-9]+)?$/.test(targetEnvName)
|
|
102
|
+
const isConfiguredEnv = targetEnvName === pub.apiKeyEnv ||
|
|
103
|
+
(Array.isArray(pub.accounts) && pub.accounts.some(acc => acc && acc.apiKeyEnv === targetEnvName))
|
|
104
|
+
|
|
105
|
+
if (!isDefaultPattern && !isConfiguredEnv) {
|
|
106
|
+
return writeJson(res, 400, {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: `Disallowed apiKeyEnv: "${targetEnvName}". Must match ^CLINEBOT_API_KEY(_[A-Z0-9]+)?$ or be configured in settings.`,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
await saveCredentialKey(ctx, targetEnvName, apiKey)
|
|
81
113
|
|
|
82
114
|
await syncProviderState(live())
|
|
83
115
|
triggerAutoDiscover()
|
|
84
116
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
117
|
+
let validation = { ok: false, error: 'Skipped validation: baseUrl is not https' }
|
|
118
|
+
try {
|
|
119
|
+
const parsedUrl = new URL(pub.baseUrl)
|
|
120
|
+
if (parsedUrl.protocol === 'https:') {
|
|
121
|
+
validation = await smokeChat(pub.baseUrl, apiKey, {
|
|
122
|
+
model: pub.defaultModel,
|
|
123
|
+
timeoutMs: 15000,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
validation = { ok: false, error: 'Invalid baseUrl' }
|
|
128
|
+
}
|
|
89
129
|
|
|
90
130
|
writeJson(res, 200, {
|
|
91
131
|
ok: true,
|
package/lib/slash-command.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { publicConfig, Config } from './config.js'
|
|
1
|
+
import { publicConfig, plainConfig, Config } from './config.js'
|
|
2
2
|
import { resolveActiveAccountKey, formatProgressBar } from './provider-sync.js'
|
|
3
3
|
import {
|
|
4
4
|
getAllModels,
|
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
probeHealth,
|
|
14
14
|
smokeChat,
|
|
15
15
|
fetchUsageLimits,
|
|
16
|
-
|
|
16
|
+
recordSmokeTest,
|
|
17
17
|
clearUsageCache,
|
|
18
18
|
clearProbeCache,
|
|
19
19
|
usageCache,
|
|
20
20
|
sessionStats,
|
|
21
|
+
getLastRotation,
|
|
21
22
|
} from './cline-client.js'
|
|
22
23
|
|
|
23
24
|
export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderState }) {
|
|
@@ -37,11 +38,18 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
37
38
|
if (subcmd === 'models') {
|
|
38
39
|
const allModels = getAllModels(pub.dynamicModels)
|
|
39
40
|
const disabledSet = new Set(pub.disabledModels || [])
|
|
41
|
+
const syncStatus = pub.planSynced
|
|
42
|
+
? `✅ Verified with plan (${pub.planSyncedAt ? new Date(pub.planSyncedAt).toLocaleDateString() : 'synced'})`
|
|
43
|
+
: '⚠️ Not verified with plan (fallback catalog)'
|
|
40
44
|
const lines = [
|
|
41
45
|
'### 🎯 ClinePass Models Catalog',
|
|
46
|
+
`* **Plan Sync**: ${syncStatus}`,
|
|
42
47
|
`* **Total models**: ${allModels.length} (${allModels.filter((m) => !disabledSet.has(m.id)).length} active)`,
|
|
43
|
-
'',
|
|
44
48
|
]
|
|
49
|
+
if (pub.defaultModelWarning) {
|
|
50
|
+
lines.push(`* ⚠️ **Warning**: ${pub.defaultModelWarning}`)
|
|
51
|
+
}
|
|
52
|
+
lines.push('')
|
|
45
53
|
for (const m of allModels) {
|
|
46
54
|
const active = !disabledSet.has(m.id) ? '✅' : '❌'
|
|
47
55
|
const isVis = isVisionModel(m.id, pub.dynamicModels) ? '📷 Vision' : '📝 Text'
|
|
@@ -67,6 +75,10 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
67
75
|
const usageInfo = cached?.windows?.fiveHour ? ` · ⏱ ${cached.windows.fiveHour.percentUsed}% used` : ''
|
|
68
76
|
lines.push(`* **${acc.label}** (\`${acc.apiKeyEnv}\`): ${statusBadge} ${pinBadge}${usageInfo}`)
|
|
69
77
|
}
|
|
78
|
+
const lastRot = getLastRotation()
|
|
79
|
+
if (lastRot) {
|
|
80
|
+
lines.push('', `* **Last Failover**: \`${lastRot.from}\` → \`${lastRot.to}\` (${lastRot.reason})`)
|
|
81
|
+
}
|
|
70
82
|
lines.push('', 'Switch active account: `/cline switch <env_variable_name>`')
|
|
71
83
|
return lines.join('\n')
|
|
72
84
|
}
|
|
@@ -80,7 +92,7 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
80
92
|
clearProbeCache()
|
|
81
93
|
const settingsApi = getSettingsApi()
|
|
82
94
|
if (settingsApi?.replace) {
|
|
83
|
-
const next = Config({ ...live(), activeAccount: param })
|
|
95
|
+
const next = Config({ ...plainConfig(live()), activeAccount: param })
|
|
84
96
|
await settingsApi.replace(next)
|
|
85
97
|
await syncProviderState(next)
|
|
86
98
|
return `✅ Active account switched to \`${param}\``
|
|
@@ -107,7 +119,33 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
107
119
|
return `❌ **Cline API Ping Failed**: ${health.error || 'Host unreachable'}`
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
// 6. Subcommand /cline
|
|
122
|
+
// 6. Subcommand /cline stats (real DSH stream metrics)
|
|
123
|
+
if (subcmd === 'stats' || subcmd === 'telemetry') {
|
|
124
|
+
const lines = [
|
|
125
|
+
'### 📈 DSH ClineBot Stream Telemetry',
|
|
126
|
+
`* **Requests**: ${sessionStats.successfulRequests} successful / ${sessionStats.totalRequests} total${sessionStats.abortedRequests ? ` (${sessionStats.abortedRequests} aborted)` : ''}${sessionStats.failedRequests ? ` (${sessionStats.failedRequests} failed)` : ''}`,
|
|
127
|
+
`* **Estimated Tokens**: ~${sessionStats.totalTokensEst} total (~${sessionStats.promptTokensEst} prompt, ~${sessionStats.completionTokensEst} completion)`,
|
|
128
|
+
`* **Last Stream Latency**: ${sessionStats.lastLatencyMs !== null ? `${sessionStats.lastLatencyMs} ms` : '—'}`,
|
|
129
|
+
`* **Last Request At**: ${sessionStats.lastRequestAt ? new Date(sessionStats.lastRequestAt).toLocaleTimeString() : '—'}`,
|
|
130
|
+
]
|
|
131
|
+
if (sessionStats.lastError) {
|
|
132
|
+
lines.push(`* **Last Stream Error**: ${sessionStats.lastError}`)
|
|
133
|
+
}
|
|
134
|
+
if (sessionStats.byModel && Object.keys(sessionStats.byModel).length) {
|
|
135
|
+
lines.push('', '**By Model:**')
|
|
136
|
+
for (const [mId, mStats] of Object.entries(sessionStats.byModel)) {
|
|
137
|
+
lines.push(`* \`${mId}\`: ${mStats.requests} reqs, ~${mStats.promptTokens + mStats.completionTokens} tokens`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (sessionStats.lastSmoke) {
|
|
141
|
+
const smoke = sessionStats.lastSmoke
|
|
142
|
+
const smokeTime = new Date(smoke.at).toLocaleTimeString()
|
|
143
|
+
lines.push('', `* **Last Smoke Test**: ${smoke.ok ? '✅ Passed' : '❌ Failed'} (${smoke.latencyMs} ms at ${smokeTime}${smoke.model ? `, model: \`${smoke.model}\`` : ''})`)
|
|
144
|
+
}
|
|
145
|
+
return lines.join('\n')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 7. Subcommand /cline test [model] / smoke
|
|
111
149
|
if (subcmd === 'test' || subcmd === 'smoke') {
|
|
112
150
|
if (param && !isSupportedModel(param, pub.dynamicModels)) {
|
|
113
151
|
return `⚠️ **ClineBot**: Model \`${param}\` is not recognized. Use \`/cline models\` to see available models.`
|
|
@@ -121,12 +159,11 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
121
159
|
model: modelToTest,
|
|
122
160
|
timeoutMs: pub.smokeTimeoutMs,
|
|
123
161
|
})
|
|
124
|
-
|
|
162
|
+
recordSmokeTest({
|
|
125
163
|
latencyMs: outcome.latencyMs,
|
|
126
164
|
ok: outcome.ok,
|
|
127
165
|
error: outcome.error,
|
|
128
|
-
|
|
129
|
-
completionTokens: outcome.completionTokens || 10,
|
|
166
|
+
model: modelToTest,
|
|
130
167
|
})
|
|
131
168
|
|
|
132
169
|
let failoverNotice = ''
|
|
@@ -170,10 +207,15 @@ export function registerSlashCommand(ctx, { live, getSettingsApi, syncProviderSt
|
|
|
170
207
|
`* **Host Ping**: ${health.ok ? `✅ ${health.latencyMs} ms` : '❌ Unreachable'}`,
|
|
171
208
|
`* **Active Key**: \`${activeKey.envName}\` (${activeKey.source})`,
|
|
172
209
|
`* **Default Model**: \`${pub.defaultModel}\``,
|
|
210
|
+
]
|
|
211
|
+
if (pub.defaultModelWarning) {
|
|
212
|
+
lines.push(`* ⚠️ **Warning**: ${pub.defaultModelWarning}`)
|
|
213
|
+
}
|
|
214
|
+
lines.push(
|
|
173
215
|
'',
|
|
174
216
|
`**⏱ 5-Hour Rolling Limit**: ${formatProgressBar(fiveHour?.percentUsed)} (reset: ${reset5h})`,
|
|
175
217
|
`**📅 Weekly Window**: ${formatProgressBar(weekly?.percentUsed)} (reset: ${resetWk})`,
|
|
176
|
-
|
|
218
|
+
)
|
|
177
219
|
|
|
178
220
|
if (fiveHour?.percentUsed >= 95) {
|
|
179
221
|
lines.push('', '🚨 **CRITICAL LIMIT**: 5-hour quota is 95%+ consumed! New requests may be throttled until reset.')
|
package/lib/telemetry.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory runtime session metrics for ClinePass requests in DeepSeek Harness.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const sessionStats = {
|
|
6
|
+
totalRequests: 0,
|
|
7
|
+
successfulRequests: 0,
|
|
8
|
+
failedRequests: 0,
|
|
9
|
+
abortedRequests: 0,
|
|
10
|
+
promptTokensEst: 0,
|
|
11
|
+
completionTokensEst: 0,
|
|
12
|
+
totalTokensEst: 0,
|
|
13
|
+
lastLatencyMs: null,
|
|
14
|
+
lastRequestAt: null,
|
|
15
|
+
lastError: null,
|
|
16
|
+
byModel: {},
|
|
17
|
+
lastSmoke: null,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function recordSessionRequest({
|
|
21
|
+
latencyMs,
|
|
22
|
+
ok,
|
|
23
|
+
error,
|
|
24
|
+
aborted = false,
|
|
25
|
+
promptTokens = 0,
|
|
26
|
+
completionTokens = 0,
|
|
27
|
+
model,
|
|
28
|
+
}) {
|
|
29
|
+
sessionStats.totalRequests += 1
|
|
30
|
+
if (aborted) {
|
|
31
|
+
sessionStats.abortedRequests = (sessionStats.abortedRequests || 0) + 1
|
|
32
|
+
} else if (ok) {
|
|
33
|
+
sessionStats.successfulRequests += 1
|
|
34
|
+
sessionStats.lastLatencyMs = typeof latencyMs === 'number' ? latencyMs : null
|
|
35
|
+
sessionStats.lastError = null
|
|
36
|
+
} else {
|
|
37
|
+
sessionStats.failedRequests += 1
|
|
38
|
+
sessionStats.lastError = error || 'Request failed'
|
|
39
|
+
}
|
|
40
|
+
sessionStats.lastRequestAt = Date.now()
|
|
41
|
+
sessionStats.promptTokensEst += Number(promptTokens) || 0
|
|
42
|
+
sessionStats.completionTokensEst += Number(completionTokens) || 0
|
|
43
|
+
sessionStats.totalTokensEst += (Number(promptTokens) || 0) + (Number(completionTokens) || 0)
|
|
44
|
+
|
|
45
|
+
if (model) {
|
|
46
|
+
if (!sessionStats.byModel) sessionStats.byModel = {}
|
|
47
|
+
const m = sessionStats.byModel[model] || { requests: 0, promptTokens: 0, completionTokens: 0 }
|
|
48
|
+
m.requests += 1
|
|
49
|
+
m.promptTokens += Number(promptTokens) || 0
|
|
50
|
+
m.completionTokens += Number(completionTokens) || 0
|
|
51
|
+
sessionStats.byModel[model] = m
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function recordSmokeTest({ latencyMs, ok, error, model }) {
|
|
56
|
+
sessionStats.lastSmoke = {
|
|
57
|
+
at: Date.now(),
|
|
58
|
+
latencyMs: typeof latencyMs === 'number' ? latencyMs : null,
|
|
59
|
+
ok: Boolean(ok),
|
|
60
|
+
error: error || null,
|
|
61
|
+
model: model || null,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resetSessionStats() {
|
|
66
|
+
sessionStats.totalRequests = 0
|
|
67
|
+
sessionStats.successfulRequests = 0
|
|
68
|
+
sessionStats.failedRequests = 0
|
|
69
|
+
sessionStats.abortedRequests = 0
|
|
70
|
+
sessionStats.promptTokensEst = 0
|
|
71
|
+
sessionStats.completionTokensEst = 0
|
|
72
|
+
sessionStats.totalTokensEst = 0
|
|
73
|
+
sessionStats.lastLatencyMs = null
|
|
74
|
+
sessionStats.lastRequestAt = null
|
|
75
|
+
sessionStats.lastError = null
|
|
76
|
+
sessionStats.byModel = {}
|
|
77
|
+
sessionStats.lastSmoke = null
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
@@ -63,6 +63,9 @@
|
|
|
63
63
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
64
64
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
65
65
|
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
68
|
+
},
|
|
66
69
|
"homepage": "https://github.com/GooDAnDReaDY/dsh-clinebot#readme",
|
|
67
70
|
"bugs": {
|
|
68
71
|
"url": "https://github.com/GooDAnDReaDY/dsh-clinebot/issues"
|