@goodandready/dsh-clinebot 0.3.11 โ 0.3.12
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/lib/access.js +10 -0
- package/lib/account-pool.js +131 -0
- package/lib/client.js +1113 -1141
- package/lib/cline-client.js +1 -128
- package/lib/config.js +84 -0
- package/lib/index.js +36 -864
- package/lib/provider-sync.js +216 -0
- package/lib/routes/accounts.js +79 -0
- package/lib/routes/auth.js +102 -0
- package/lib/routes/models.js +126 -0
- package/lib/routes/settings.js +102 -0
- package/lib/slash-command.js +198 -0
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -1,285 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
3
|
-
import { writeJson, readBody, isTrustedSettingsRequest } from './http.js'
|
|
1
|
+
import { Config, publicConfig, NS, LLM_PI_AI_NS } from './config.js'
|
|
4
2
|
import { registerPluginUpdater } from './updater.js'
|
|
5
3
|
import {
|
|
6
|
-
CLINE_MODELS,
|
|
7
|
-
DEFAULT_MODEL_ID,
|
|
8
|
-
PROVIDER_ID,
|
|
9
|
-
PROVIDER_DISPLAY_NAME,
|
|
10
|
-
getAllModels,
|
|
11
|
-
getDefaultModelIds,
|
|
12
|
-
getActiveModelIds,
|
|
13
|
-
isSupportedModel,
|
|
14
|
-
parsePlanIncludedModels,
|
|
15
|
-
saveModelsDiskCache,
|
|
16
|
-
loadModelsDiskCache,
|
|
17
|
-
isVisionModel,
|
|
18
|
-
} from './models.js'
|
|
19
|
-
import {
|
|
20
|
-
DEFAULT_BASE_URL,
|
|
21
|
-
DEFAULT_API_KEY_ENV,
|
|
22
|
-
DEFAULT_TIMEOUT_MS,
|
|
23
|
-
DEFAULT_SMOKE_TIMEOUT_MS,
|
|
24
|
-
normalizeBaseUrl,
|
|
25
|
-
resolveApiKey,
|
|
26
|
-
resolveKeyValue,
|
|
27
|
-
resolveAccountPool,
|
|
28
|
-
rotateToNextAccount,
|
|
29
|
-
saveCredentialKey,
|
|
30
|
-
fetchUsageLimits,
|
|
31
|
-
probeHealth,
|
|
32
|
-
smokeChat,
|
|
33
|
-
buildPiAiProvider,
|
|
34
4
|
sessionStats,
|
|
35
5
|
recordSessionRequest,
|
|
36
6
|
resetSessionStats,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
7
|
+
rotateToNextAccount,
|
|
8
|
+
resolveKeyValue,
|
|
9
|
+
smokeChat,
|
|
10
|
+
fetchUsageLimits,
|
|
40
11
|
} from './cline-client.js'
|
|
12
|
+
import {
|
|
13
|
+
checkRegisteredInPiAi,
|
|
14
|
+
upsertPiAiProvider,
|
|
15
|
+
removePiAiProvider,
|
|
16
|
+
buildStatus,
|
|
17
|
+
autoDiscoverPlanModels,
|
|
18
|
+
resolveActiveAccountKey,
|
|
19
|
+
} from './provider-sync.js'
|
|
20
|
+
import { registerSettingsRoutes } from './routes/settings.js'
|
|
21
|
+
import { registerAccountsRoutes } from './routes/accounts.js'
|
|
22
|
+
import { registerModelsRoutes } from './routes/models.js'
|
|
23
|
+
import { registerAuthRoutes } from './routes/auth.js'
|
|
24
|
+
import { registerSlashCommand } from './slash-command.js'
|
|
41
25
|
|
|
42
26
|
export const name = '@goodandready/dsh-clinebot'
|
|
43
27
|
export const inject = ['settings', 'webServer', 'credentials']
|
|
44
28
|
|
|
45
|
-
export
|
|
46
|
-
export const LLM_PI_AI_NS = 'llm-pi-ai'
|
|
47
|
-
|
|
48
|
-
export { sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
|
|
49
|
-
|
|
50
|
-
export const Config = z.object({
|
|
51
|
-
enabled: z.boolean().default(true)
|
|
52
|
-
.description('When true, ClineBot is registered as a model provider in DSH.'),
|
|
53
|
-
baseUrl: z.string().default(DEFAULT_BASE_URL)
|
|
54
|
-
.description('Base API URL (default: https://api.cline.bot/api/v1).'),
|
|
55
|
-
apiKeyEnv: z.string().default(DEFAULT_API_KEY_ENV)
|
|
56
|
-
.description('Credential / env name containing the ClinePass API key (never store key directly here).'),
|
|
57
|
-
defaultModel: z.string().default(DEFAULT_MODEL_ID)
|
|
58
|
-
.description('Default model ID for chat and smoke tests.'),
|
|
59
|
-
disabledModels: z.array(z.string()).default([])
|
|
60
|
-
.description('List of model IDs explicitly disabled by the user (new models are enabled automatically).'),
|
|
61
|
-
enabledModels: z.array(z.string()).default([])
|
|
62
|
-
.description('Deprecated: preserved for backwards compatibility with earlier versions.'),
|
|
63
|
-
dynamicModels: z.array(z.object({
|
|
64
|
-
id: z.string(),
|
|
65
|
-
name: z.string(),
|
|
66
|
-
description: z.string().default(''),
|
|
67
|
-
contextLength: z.number().default(200000),
|
|
68
|
-
maxTokens: z.number().default(8192),
|
|
69
|
-
input: z.array(z.string()).default(['text']),
|
|
70
|
-
category: z.string().default('general'),
|
|
71
|
-
isCustom: z.boolean().default(false),
|
|
72
|
-
})).default([])
|
|
73
|
-
.description('Models automatically discovered from the official ClinePass subscription plan.'),
|
|
74
|
-
timeoutMs: z.number().default(DEFAULT_TIMEOUT_MS)
|
|
75
|
-
.description('HTTP probe timeout in milliseconds.'),
|
|
76
|
-
smokeTimeoutMs: z.number().default(DEFAULT_SMOKE_TIMEOUT_MS)
|
|
77
|
-
.description('Timeout for smoke chat completions in milliseconds.'),
|
|
78
|
-
modelsCachePath: z.string().default('~/.dsh/clinebot-models-cache.json')
|
|
79
|
-
.description('Local on-disk cache path for models snapshot.'),
|
|
80
|
-
accounts: z.array(z.object({
|
|
81
|
-
label: z.string().default(''),
|
|
82
|
-
apiKeyEnv: z.string(),
|
|
83
|
-
})).default([])
|
|
84
|
-
.description('Additional accounts for multi-account failover and rate limit rotation.'),
|
|
85
|
-
activeAccount: z.string().default('')
|
|
86
|
-
.description('Manually pinned active account envName or empty for auto/default.'),
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
function publicConfig(cfg) {
|
|
90
|
-
const dynamic = Array.isArray(cfg?.dynamicModels) ? cfg.dynamicModels : []
|
|
91
|
-
const allModels = getAllModels(dynamic)
|
|
92
|
-
const allDefaultIds = allModels.map((m) => m.id)
|
|
93
|
-
|
|
94
|
-
// Migration / compatibility: if disabledModels was not yet set, but enabledModels was provided
|
|
95
|
-
let disabledList = Array.isArray(cfg?.disabledModels) ? cfg.disabledModels : []
|
|
96
|
-
if (!Array.isArray(cfg?.disabledModels) || (cfg.disabledModels.length === 0 && Array.isArray(cfg?.enabledModels) && cfg.enabledModels.length > 0)) {
|
|
97
|
-
const enabledSet = new Set(cfg.enabledModels)
|
|
98
|
-
disabledList = allDefaultIds.filter((id) => !enabledSet.has(id))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const activeIds = getActiveModelIds(allDefaultIds, disabledList)
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
enabled: !!cfg?.enabled,
|
|
105
|
-
baseUrl: normalizeBaseUrl(cfg?.baseUrl),
|
|
106
|
-
apiKeyEnv: cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV,
|
|
107
|
-
defaultModel: cfg?.defaultModel || DEFAULT_MODEL_ID,
|
|
108
|
-
dynamicModels: dynamic,
|
|
109
|
-
disabledModels: disabledList,
|
|
110
|
-
enabledModels: activeIds,
|
|
111
|
-
timeoutMs: Number(cfg?.timeoutMs) || DEFAULT_TIMEOUT_MS,
|
|
112
|
-
smokeTimeoutMs: Number(cfg?.smokeTimeoutMs) || DEFAULT_SMOKE_TIMEOUT_MS,
|
|
113
|
-
modelsCachePath: String(cfg?.modelsCachePath || '~/.dsh/clinebot-models-cache.json'),
|
|
114
|
-
accounts: Array.isArray(cfg?.accounts) ? cfg.accounts : [],
|
|
115
|
-
activeAccount: String(cfg?.activeAccount || ''),
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
import os from 'node:os'
|
|
120
|
-
import path from 'node:path'
|
|
121
|
-
|
|
122
|
-
function resolvePathWithHome(p) {
|
|
123
|
-
if (!p || typeof p !== 'string') return ''
|
|
124
|
-
if (p === '~') return os.homedir()
|
|
125
|
-
if (p.startsWith('~/') || p.startsWith('~\\')) {
|
|
126
|
-
return path.join(os.homedir(), p.slice(2))
|
|
127
|
-
}
|
|
128
|
-
return p
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function checkRegisteredInPiAi(ctx) {
|
|
132
|
-
const settings = ctx?.get?.('settings')
|
|
133
|
-
if (!settings?.get) return false
|
|
134
|
-
try {
|
|
135
|
-
const piAi = settings.get(LLM_PI_AI_NS)
|
|
136
|
-
return !!piAi?.providers?.[PROVIDER_ID]
|
|
137
|
-
} catch {
|
|
138
|
-
return false
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function resolveActiveAccountKey(ctx, cfg) {
|
|
143
|
-
const pool = await resolveAccountPool(ctx, cfg)
|
|
144
|
-
const configured = pool.filter((acc) => acc.present && acc.value)
|
|
145
|
-
if (!configured.length) {
|
|
146
|
-
return { envName: publicConfig(cfg).apiKeyEnv, value: '', source: 'none', id: 'default' }
|
|
147
|
-
}
|
|
148
|
-
const pub = publicConfig(cfg)
|
|
149
|
-
if (pub.activeAccount) {
|
|
150
|
-
const pinned = configured.find((acc) => acc.apiKeyEnv === pub.activeAccount || acc.id === pub.activeAccount)
|
|
151
|
-
if (pinned) return pinned
|
|
152
|
-
}
|
|
153
|
-
return configured[0]
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function buildStatus(ctx, cfg) {
|
|
157
|
-
const pub = publicConfig(cfg)
|
|
158
|
-
const probeTimeout = Math.min(2500, pub.timeoutMs || 2500)
|
|
159
|
-
|
|
160
|
-
// Concurrent resolution of keys, health probe (SWR cached), accounts pool and DSH registration
|
|
161
|
-
const [key, pool, isRegistered, health] = await Promise.all([
|
|
162
|
-
resolveKeyValue(ctx, pub.apiKeyEnv),
|
|
163
|
-
resolveAccountPool(ctx, cfg),
|
|
164
|
-
checkRegisteredInPiAi(ctx),
|
|
165
|
-
probeHealth(pub.baseUrl, { timeoutMs: probeTimeout }),
|
|
166
|
-
])
|
|
167
|
-
|
|
168
|
-
const activeAcc = await resolveActiveAccountKey(ctx, cfg)
|
|
169
|
-
const allModels = getAllModels(pub.dynamicModels)
|
|
170
|
-
|
|
171
|
-
let usage = null
|
|
172
|
-
const keyToUse = activeAcc.value || key.value
|
|
173
|
-
if (keyToUse) {
|
|
174
|
-
// Uses 60s cache; if cache miss, times out quickly
|
|
175
|
-
usage = await fetchUsageLimits(pub.baseUrl, keyToUse, { timeoutMs: probeTimeout }).catch(() => null)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Evaluate warning state
|
|
179
|
-
let quotaWarning = null
|
|
180
|
-
if (usage?.windows?.fiveHour) {
|
|
181
|
-
const pct = usage.windows.fiveHour.percentUsed
|
|
182
|
-
if (pct >= 95) {
|
|
183
|
-
quotaWarning = {
|
|
184
|
-
level: 'exhausted',
|
|
185
|
-
message: `5-hour rolling limit is almost exhausted (${pct}%). New requests may be rejected until quota reset.`,
|
|
186
|
-
resetsAt: usage.windows.fiveHour.resetsAt,
|
|
187
|
-
}
|
|
188
|
-
} else if (pct >= 80) {
|
|
189
|
-
quotaWarning = {
|
|
190
|
-
level: 'warning',
|
|
191
|
-
message: `Notice: ${pct}% of the 5-hour rolling limit has been consumed.`,
|
|
192
|
-
resetsAt: usage.windows.fiveHour.resetsAt,
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
ok: true,
|
|
199
|
-
providerId: PROVIDER_ID,
|
|
200
|
-
displayName: PROVIDER_DISPLAY_NAME,
|
|
201
|
-
config: pub,
|
|
202
|
-
key: {
|
|
203
|
-
envName: activeAcc.apiKeyEnv || key.envName,
|
|
204
|
-
present: !!(activeAcc.value || key.value),
|
|
205
|
-
source: activeAcc.source || key.source,
|
|
206
|
-
},
|
|
207
|
-
accounts: pool.map((acc) => ({
|
|
208
|
-
id: acc.id,
|
|
209
|
-
label: acc.label,
|
|
210
|
-
apiKeyEnv: acc.apiKeyEnv,
|
|
211
|
-
present: acc.present,
|
|
212
|
-
source: acc.source,
|
|
213
|
-
isPinned: acc.isPinned,
|
|
214
|
-
})),
|
|
215
|
-
activeAccount: activeAcc.apiKeyEnv,
|
|
216
|
-
health,
|
|
217
|
-
usage,
|
|
218
|
-
quotaWarning,
|
|
219
|
-
sessionStats: { ...sessionStats },
|
|
220
|
-
isRegistered,
|
|
221
|
-
availableModels: allModels,
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async function upsertPiAiProvider(ctx, cfg, activeModelIds) {
|
|
226
|
-
const settings = ctx?.get?.('settings')
|
|
227
|
-
if (!settings?.mutate) {
|
|
228
|
-
throw new Error('DSH settings service unavailable')
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const pub = publicConfig(cfg)
|
|
232
|
-
const allModels = getAllModels(pub.dynamicModels)
|
|
233
|
-
const allowedSet = new Set(activeModelIds || pub.enabledModels)
|
|
234
|
-
const modelsToRegister = allModels.filter((m) => allowedSet.has(m.id))
|
|
235
|
-
|
|
236
|
-
const providerObj = buildPiAiProvider({
|
|
237
|
-
baseUrl: pub.baseUrl,
|
|
238
|
-
apiKeyEnv: pub.apiKeyEnv,
|
|
239
|
-
models: modelsToRegister.length ? modelsToRegister : allModels,
|
|
240
|
-
customModels: pub.dynamicModels,
|
|
241
|
-
displayName: PROVIDER_DISPLAY_NAME,
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
await settings.mutate(LLM_PI_AI_NS, [
|
|
245
|
-
{
|
|
246
|
-
op: 'set',
|
|
247
|
-
path: ['providers', PROVIDER_ID],
|
|
248
|
-
value: providerObj,
|
|
249
|
-
},
|
|
250
|
-
])
|
|
251
|
-
|
|
252
|
-
return providerObj
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async function removePiAiProvider(ctx) {
|
|
256
|
-
const settings = ctx?.get?.('settings')
|
|
257
|
-
if (!settings?.mutate) {
|
|
258
|
-
throw new Error('DSH settings service unavailable')
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
await settings.mutate(LLM_PI_AI_NS, [
|
|
262
|
-
{
|
|
263
|
-
op: 'remove',
|
|
264
|
-
path: ['providers', PROVIDER_ID],
|
|
265
|
-
},
|
|
266
|
-
])
|
|
267
|
-
return { ok: true }
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function formatProgressBar(pct, totalWidth = 10) {
|
|
271
|
-
const clamped = Math.max(0, Math.min(100, pct || 0))
|
|
272
|
-
const filled = Math.round((clamped / 100) * totalWidth)
|
|
273
|
-
const empty = Math.max(0, totalWidth - filled)
|
|
274
|
-
return `[${'โ'.repeat(filled)}${'โ'.repeat(empty)}] ${clamped}%`
|
|
275
|
-
}
|
|
29
|
+
export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
|
|
276
30
|
|
|
277
31
|
export function apply(ctx, config) {
|
|
278
32
|
let getConfig = () => config
|
|
279
33
|
const live = () => (getConfig() ? Config(structuredClone(getConfig())) : config)
|
|
280
34
|
let settingsApi
|
|
281
35
|
|
|
282
|
-
// Declarative sync helper: auto-registers or unregisters provider based on config & key availability
|
|
283
36
|
const syncProviderState = async (cfg) => {
|
|
284
37
|
try {
|
|
285
38
|
const pub = publicConfig(cfg)
|
|
@@ -298,50 +51,8 @@ export function apply(ctx, config) {
|
|
|
298
51
|
}
|
|
299
52
|
}
|
|
300
53
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
const pub = publicConfig(cfg)
|
|
305
|
-
const cacheFile = resolvePathWithHome(pub.modelsCachePath)
|
|
306
|
-
|
|
307
|
-
// 1. If dynamicModels is empty, attempt to load from disk cache first
|
|
308
|
-
if ((!pub.dynamicModels || !pub.dynamicModels.length) && cacheFile) {
|
|
309
|
-
const fromDisk = await loadModelsDiskCache(cacheFile)
|
|
310
|
-
if (Array.isArray(fromDisk) && fromDisk.length && settingsApi?.replace) {
|
|
311
|
-
const next = Config({ ...live(), dynamicModels: fromDisk })
|
|
312
|
-
await settingsApi.replace(next)
|
|
313
|
-
await syncProviderState(next)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const activeAcc = await resolveActiveAccountKey(ctx, cfg)
|
|
318
|
-
if (!activeAcc.value) return
|
|
319
|
-
|
|
320
|
-
const usageData = await fetchUsageLimits(pub.baseUrl, activeAcc.value, {
|
|
321
|
-
timeoutMs: Math.min(pub.timeoutMs, 5000),
|
|
322
|
-
bypassCache: true,
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
if (usageData?.ok && Array.isArray(usageData.dynamicModels) && usageData.dynamicModels.length > 0) {
|
|
326
|
-
const existingDynamic = pub.dynamicModels || []
|
|
327
|
-
const existingIds = new Set(existingDynamic.map((m) => m.id))
|
|
328
|
-
const hasNew = usageData.dynamicModels.some((m) => !existingIds.has(m.id))
|
|
329
|
-
|
|
330
|
-
if (hasNew && settingsApi?.replace) {
|
|
331
|
-
const next = Config({
|
|
332
|
-
...live(),
|
|
333
|
-
dynamicModels: usageData.dynamicModels,
|
|
334
|
-
})
|
|
335
|
-
await settingsApi.replace(next)
|
|
336
|
-
await syncProviderState(next)
|
|
337
|
-
if (cacheFile) {
|
|
338
|
-
await saveModelsDiskCache(cacheFile, usageData.dynamicModels)
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
} catch {
|
|
343
|
-
/* best-effort discovery */
|
|
344
|
-
}
|
|
54
|
+
const triggerAutoDiscover = () => {
|
|
55
|
+
autoDiscoverPlanModels(ctx, { live, getSettingsApi: () => settingsApi, syncProviderState })
|
|
345
56
|
}
|
|
346
57
|
|
|
347
58
|
if (typeof ctx.inject === 'function') {
|
|
@@ -371,14 +82,10 @@ export function apply(ctx, config) {
|
|
|
371
82
|
}
|
|
372
83
|
}
|
|
373
84
|
|
|
374
|
-
// On startup: ensure provider is synced to llm-pi-ai if key is present
|
|
375
85
|
syncProviderState(live())
|
|
376
|
-
|
|
377
|
-
setTimeout(() => autoDiscoverPlanModels(live()), 500)
|
|
86
|
+
setTimeout(triggerAutoDiscover, 500)
|
|
378
87
|
|
|
379
|
-
// Web server HTTP route handlers
|
|
380
88
|
if (ctx.webServer?.register) {
|
|
381
|
-
// 0. GET & POST /dsh-clinebot/update โ host one-click updater
|
|
382
89
|
const unregisterUpdater = registerPluginUpdater(ctx, {
|
|
383
90
|
endpoint: '/dsh-clinebot/update',
|
|
384
91
|
packageName: name,
|
|
@@ -388,558 +95,23 @@ export function apply(ctx, config) {
|
|
|
388
95
|
ctx.effect(() => () => unregisterUpdater?.(), 'dsh-clinebot: updater')
|
|
389
96
|
}
|
|
390
97
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
const st = await buildStatus(ctx, live())
|
|
399
|
-
writeJson(res, 200, st)
|
|
400
|
-
} catch (err) {
|
|
401
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
402
|
-
}
|
|
403
|
-
},
|
|
404
|
-
}), 'dsh-clinebot: /status')
|
|
405
|
-
|
|
406
|
-
// 2. GET & PUT /dsh-clinebot/config
|
|
407
|
-
ctx.effect(() => ctx.webServer.register({
|
|
408
|
-
kind: 'exact',
|
|
409
|
-
path: '/dsh-clinebot/config',
|
|
410
|
-
handler: async (req, res) => {
|
|
411
|
-
if (req.method === 'GET') {
|
|
412
|
-
return writeJson(res, 200, { ok: true, config: publicConfig(live()) })
|
|
413
|
-
}
|
|
414
|
-
if (req.method !== 'PUT') {
|
|
415
|
-
return writeJson(res, 405, { ok: false, error: 'GET or PUT' })
|
|
416
|
-
}
|
|
417
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
418
|
-
return writeJson(res, 403, { ok: false, error: 'same-origin only' })
|
|
419
|
-
}
|
|
420
|
-
if (!settingsApi) {
|
|
421
|
-
return writeJson(res, 503, { ok: false, error: 'settings not ready' })
|
|
422
|
-
}
|
|
423
|
-
let payload
|
|
424
|
-
try {
|
|
425
|
-
payload = JSON.parse((await readBody(req)).toString('utf8') || '{}')
|
|
426
|
-
} catch {
|
|
427
|
-
return writeJson(res, 400, { ok: false, error: 'invalid json' })
|
|
428
|
-
}
|
|
429
|
-
if (payload && typeof payload.config === 'object') payload = payload.config
|
|
430
|
-
try {
|
|
431
|
-
const parsed = Config({ ...publicConfig(live()), ...payload })
|
|
432
|
-
await settingsApi.replace(parsed)
|
|
433
|
-
await syncProviderState(parsed)
|
|
434
|
-
writeJson(res, 200, { ok: true, config: publicConfig(live()) })
|
|
435
|
-
} catch (e) {
|
|
436
|
-
writeJson(res, 400, { ok: false, error: String(e?.message || e) })
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
}), 'dsh-clinebot: /config')
|
|
440
|
-
|
|
441
|
-
// 3. POST /dsh-clinebot/save-key โ direct saving into DSH credentials service
|
|
442
|
-
ctx.effect(() => ctx.webServer.register({
|
|
443
|
-
kind: 'exact',
|
|
444
|
-
path: '/dsh-clinebot/save-key',
|
|
445
|
-
handler: async (req, res) => {
|
|
446
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
447
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
448
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
449
|
-
}
|
|
450
|
-
try {
|
|
451
|
-
const bodyBuf = await readBody(req)
|
|
452
|
-
let body = {}
|
|
453
|
-
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
|
|
454
|
-
|
|
455
|
-
const apiKey = String(body.apiKey || '').trim()
|
|
456
|
-
if (!apiKey) {
|
|
457
|
-
return writeJson(res, 400, { ok: false, error: 'API key cannot be empty' })
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const pub = publicConfig(live())
|
|
461
|
-
const targetEnvName = String(body.apiKeyEnv || pub.apiKeyEnv || DEFAULT_API_KEY_ENV).trim()
|
|
462
|
-
await saveCredentialKey(ctx, targetEnvName, apiKey)
|
|
463
|
-
|
|
464
|
-
// Auto-sync provider to DSH Models and discover plan models
|
|
465
|
-
await syncProviderState(live())
|
|
466
|
-
autoDiscoverPlanModels(live())
|
|
467
|
-
|
|
468
|
-
// Run validation probe with the newly saved key
|
|
469
|
-
const validation = await smokeChat(pub.baseUrl, apiKey, {
|
|
470
|
-
model: pub.defaultModel,
|
|
471
|
-
timeoutMs: 15000,
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
writeJson(res, 200, {
|
|
475
|
-
ok: true,
|
|
476
|
-
envName: targetEnvName,
|
|
477
|
-
validated: validation.ok,
|
|
478
|
-
latencyMs: validation.latencyMs,
|
|
479
|
-
validationError: validation.ok ? null : validation.error,
|
|
480
|
-
})
|
|
481
|
-
} catch (err) {
|
|
482
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
483
|
-
}
|
|
484
|
-
},
|
|
485
|
-
}), 'dsh-clinebot: /save-key')
|
|
486
|
-
|
|
487
|
-
// 4. GET /dsh-clinebot/usage โ direct fresh usage limit query
|
|
488
|
-
ctx.effect(() => ctx.webServer.register({
|
|
489
|
-
kind: 'exact',
|
|
490
|
-
path: '/dsh-clinebot/usage',
|
|
491
|
-
handler: async (req, res) => {
|
|
492
|
-
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
493
|
-
try {
|
|
494
|
-
const pub = publicConfig(live())
|
|
495
|
-
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
496
|
-
if (!activeKey.value) {
|
|
497
|
-
return writeJson(res, 400, { ok: false, error: 'API key not found' })
|
|
498
|
-
}
|
|
499
|
-
const usageData = await fetchUsageLimits(pub.baseUrl, activeKey.value, {
|
|
500
|
-
timeoutMs: pub.timeoutMs,
|
|
501
|
-
bypassCache: true,
|
|
502
|
-
})
|
|
503
|
-
writeJson(res, usageData.ok ? 200 : 502, usageData)
|
|
504
|
-
} catch (err) {
|
|
505
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
506
|
-
}
|
|
507
|
-
},
|
|
508
|
-
}), 'dsh-clinebot: /usage')
|
|
509
|
-
|
|
510
|
-
// 5. POST /dsh-clinebot/register โ upsert into DSH llm-pi-ai
|
|
511
|
-
ctx.effect(() => ctx.webServer.register({
|
|
512
|
-
kind: 'exact',
|
|
513
|
-
path: '/dsh-clinebot/register',
|
|
514
|
-
handler: async (req, res) => {
|
|
515
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
516
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
517
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
518
|
-
}
|
|
519
|
-
try {
|
|
520
|
-
const bodyBuf = await readBody(req)
|
|
521
|
-
let body = {}
|
|
522
|
-
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
|
|
523
|
-
const activeModels = body.models || publicConfig(live()).enabledModels
|
|
524
|
-
const result = await upsertPiAiProvider(ctx, live(), activeModels)
|
|
525
|
-
writeJson(res, 200, { ok: true, provider: result })
|
|
526
|
-
} catch (err) {
|
|
527
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
528
|
-
}
|
|
529
|
-
},
|
|
530
|
-
}), 'dsh-clinebot: /register')
|
|
531
|
-
|
|
532
|
-
// 6. POST /dsh-clinebot/unregister โ remove from DSH
|
|
533
|
-
ctx.effect(() => ctx.webServer.register({
|
|
534
|
-
kind: 'exact',
|
|
535
|
-
path: '/dsh-clinebot/unregister',
|
|
536
|
-
handler: async (req, res) => {
|
|
537
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
538
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
539
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
540
|
-
}
|
|
541
|
-
try {
|
|
542
|
-
await removePiAiProvider(ctx)
|
|
543
|
-
writeJson(res, 200, { ok: true })
|
|
544
|
-
} catch (err) {
|
|
545
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
546
|
-
}
|
|
547
|
-
},
|
|
548
|
-
}), 'dsh-clinebot: /unregister')
|
|
549
|
-
|
|
550
|
-
// 7. POST /dsh-clinebot/smoke โ live ping test
|
|
551
|
-
ctx.effect(() => ctx.webServer.register({
|
|
552
|
-
kind: 'exact',
|
|
553
|
-
path: '/dsh-clinebot/smoke',
|
|
554
|
-
handler: async (req, res) => {
|
|
555
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
556
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
557
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
558
|
-
}
|
|
559
|
-
try {
|
|
560
|
-
const bodyBuf = await readBody(req)
|
|
561
|
-
let body = {}
|
|
562
|
-
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
|
|
563
|
-
|
|
564
|
-
const pub = publicConfig(live())
|
|
565
|
-
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
566
|
-
if (!activeKey.value) {
|
|
567
|
-
return writeJson(res, 400, {
|
|
568
|
-
ok: false,
|
|
569
|
-
error: `API key not found. Ensure ${activeKey.envName} is added to DSH credentials or environment.`,
|
|
570
|
-
})
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const modelToTest = body.model || pub.defaultModel || DEFAULT_MODEL_ID
|
|
574
|
-
const outcome = await smokeChat(pub.baseUrl, activeKey.value, {
|
|
575
|
-
model: modelToTest,
|
|
576
|
-
timeoutMs: pub.smokeTimeoutMs,
|
|
577
|
-
})
|
|
578
|
-
recordSessionRequest({
|
|
579
|
-
latencyMs: outcome.latencyMs,
|
|
580
|
-
ok: outcome.ok,
|
|
581
|
-
error: outcome.error,
|
|
582
|
-
promptTokens: outcome.promptTokens || 5,
|
|
583
|
-
completionTokens: outcome.completionTokens || 10,
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
let failover = null
|
|
587
|
-
if (outcome.status === 429) {
|
|
588
|
-
failover = await rotateToNextAccount(ctx, live(), 'smoke_429', settingsApi)
|
|
589
|
-
if (failover.rotated) {
|
|
590
|
-
await syncProviderState(live())
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
writeJson(res, outcome.ok ? 200 : 502, { ...outcome, failover })
|
|
595
|
-
} catch (err) {
|
|
596
|
-
recordSessionRequest({ ok: false, error: String(err?.message || err) })
|
|
597
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
598
|
-
}
|
|
599
|
-
},
|
|
600
|
-
}), 'dsh-clinebot: /smoke')
|
|
601
|
-
|
|
602
|
-
// 8. POST /dsh-clinebot/models/sync โ sync real models from official ClinePass subscription plan
|
|
603
|
-
ctx.effect(() => ctx.webServer.register({
|
|
604
|
-
kind: 'exact',
|
|
605
|
-
path: '/dsh-clinebot/models/sync',
|
|
606
|
-
handler: async (req, res) => {
|
|
607
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
608
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
609
|
-
}
|
|
610
|
-
if (req.method !== 'POST') {
|
|
611
|
-
return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
612
|
-
}
|
|
613
|
-
try {
|
|
614
|
-
const pub = publicConfig(live())
|
|
615
|
-
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
616
|
-
if (!activeKey.value) {
|
|
617
|
-
return writeJson(res, 400, { ok: false, error: 'API key not configured' })
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const usageData = await fetchUsageLimits(pub.baseUrl, activeKey.value, {
|
|
621
|
-
timeoutMs: pub.timeoutMs,
|
|
622
|
-
bypassCache: true,
|
|
623
|
-
})
|
|
624
|
-
|
|
625
|
-
if (!usageData.ok) {
|
|
626
|
-
return writeJson(res, 502, { ok: false, error: usageData.error || 'Failed to fetch plan models' })
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const dynamicModels = Array.isArray(usageData.dynamicModels) ? usageData.dynamicModels : []
|
|
630
|
-
const allModels = getAllModels(dynamicModels)
|
|
631
|
-
|
|
632
|
-
if (settingsApi?.replace) {
|
|
633
|
-
const next = Config({
|
|
634
|
-
...live(),
|
|
635
|
-
dynamicModels,
|
|
636
|
-
})
|
|
637
|
-
await settingsApi.replace(next)
|
|
638
|
-
await syncProviderState(next)
|
|
639
|
-
const cacheFile = resolvePathWithHome(pub.modelsCachePath)
|
|
640
|
-
if (cacheFile) {
|
|
641
|
-
await saveModelsDiskCache(cacheFile, dynamicModels)
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
return writeJson(res, 200, {
|
|
646
|
-
ok: true,
|
|
647
|
-
plan: usageData.plan,
|
|
648
|
-
discoveredCount: dynamicModels.length,
|
|
649
|
-
totalModelsCount: allModels.length,
|
|
650
|
-
models: allModels,
|
|
651
|
-
})
|
|
652
|
-
} catch (err) {
|
|
653
|
-
return writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
654
|
-
}
|
|
655
|
-
},
|
|
656
|
-
}), 'dsh-clinebot: /models/sync')
|
|
657
|
-
|
|
658
|
-
// 9. POST /dsh-clinebot/models/toggle โ toggle disabled/enabled status in picker
|
|
659
|
-
ctx.effect(() => ctx.webServer.register({
|
|
660
|
-
kind: 'exact',
|
|
661
|
-
path: '/dsh-clinebot/models/toggle',
|
|
662
|
-
handler: async (req, res) => {
|
|
663
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
664
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
665
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
666
|
-
}
|
|
667
|
-
try {
|
|
668
|
-
const bodyBuf = await readBody(req)
|
|
669
|
-
let body = {}
|
|
670
|
-
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
|
|
671
|
-
|
|
672
|
-
if (settingsApi?.replace) {
|
|
673
|
-
const patch = {}
|
|
674
|
-
if (Array.isArray(body.disabledModels)) {
|
|
675
|
-
patch.disabledModels = body.disabledModels
|
|
676
|
-
} else if (Array.isArray(body.enabledModels)) {
|
|
677
|
-
// Convert legacy enabledModels toggle to disabledModels
|
|
678
|
-
const allModels = getAllModels(live().dynamicModels)
|
|
679
|
-
const enabledSet = new Set(body.enabledModels)
|
|
680
|
-
patch.disabledModels = allModels.map((m) => m.id).filter((id) => !enabledSet.has(id))
|
|
681
|
-
}
|
|
682
|
-
if (body.defaultModel) patch.defaultModel = body.defaultModel
|
|
683
|
-
const next = Config({ ...live(), ...patch })
|
|
684
|
-
await settingsApi.replace(next)
|
|
685
|
-
await syncProviderState(next)
|
|
686
|
-
writeJson(res, 200, { ok: true, disabledModels: next.disabledModels, enabledModels: publicConfig(next).enabledModels })
|
|
687
|
-
} else {
|
|
688
|
-
writeJson(res, 200, { ok: true })
|
|
689
|
-
}
|
|
690
|
-
} catch (err) {
|
|
691
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
692
|
-
}
|
|
693
|
-
},
|
|
694
|
-
}), 'dsh-clinebot: /models/toggle')
|
|
695
|
-
|
|
696
|
-
// 10. POST /dsh-clinebot/accounts/active โ switch or pin active account
|
|
697
|
-
ctx.effect(() => ctx.webServer.register({
|
|
698
|
-
kind: 'exact',
|
|
699
|
-
path: '/dsh-clinebot/accounts/active',
|
|
700
|
-
handler: async (req, res) => {
|
|
701
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
702
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
703
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
704
|
-
}
|
|
705
|
-
try {
|
|
706
|
-
const bodyBuf = await readBody(req)
|
|
707
|
-
let body = {}
|
|
708
|
-
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch {}
|
|
709
|
-
const account = String(body.account || '').trim()
|
|
710
|
-
|
|
711
|
-
clearUsageCache()
|
|
712
|
-
clearProbeCache()
|
|
713
|
-
if (settingsApi?.replace) {
|
|
714
|
-
const next = Config({ ...live(), activeAccount: account })
|
|
715
|
-
await settingsApi.replace(next)
|
|
716
|
-
await syncProviderState(next)
|
|
717
|
-
writeJson(res, 200, { ok: true, activeAccount: next.activeAccount })
|
|
718
|
-
} else {
|
|
719
|
-
writeJson(res, 200, { ok: true, activeAccount: account })
|
|
720
|
-
}
|
|
721
|
-
} catch (err) {
|
|
722
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
723
|
-
}
|
|
724
|
-
},
|
|
725
|
-
}), 'dsh-clinebot: /accounts/active')
|
|
726
|
-
|
|
727
|
-
// 11. POST /dsh-clinebot/auth/begin โ start loopback auth listener
|
|
728
|
-
let authSession = null
|
|
729
|
-
ctx.effect(() => ctx.webServer.register({
|
|
730
|
-
kind: 'exact',
|
|
731
|
-
path: '/dsh-clinebot/auth/begin',
|
|
732
|
-
handler: async (req, res) => {
|
|
733
|
-
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
734
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
735
|
-
return writeJson(res, 403, { ok: false, error: 'Forbidden' })
|
|
736
|
-
}
|
|
737
|
-
try {
|
|
738
|
-
const authUrl = 'https://app.cline.bot'
|
|
739
|
-
authSession = {
|
|
740
|
-
state: 'waiting',
|
|
741
|
-
startedAt: Date.now(),
|
|
742
|
-
authUrl,
|
|
743
|
-
}
|
|
744
|
-
writeJson(res, 200, {
|
|
745
|
-
ok: true,
|
|
746
|
-
status: authSession.state,
|
|
747
|
-
authUrl,
|
|
748
|
-
})
|
|
749
|
-
} catch (err) {
|
|
750
|
-
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
751
|
-
}
|
|
752
|
-
},
|
|
753
|
-
}), 'dsh-clinebot: /auth/begin')
|
|
98
|
+
const routeEnv = {
|
|
99
|
+
live,
|
|
100
|
+
getSettingsApi: () => settingsApi,
|
|
101
|
+
syncProviderState,
|
|
102
|
+
triggerAutoDiscover,
|
|
103
|
+
}
|
|
754
104
|
|
|
755
|
-
|
|
756
|
-
ctx
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
handler: async (req, res) => {
|
|
760
|
-
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
761
|
-
writeJson(res, 200, {
|
|
762
|
-
ok: true,
|
|
763
|
-
status: authSession?.state || 'idle',
|
|
764
|
-
authUrl: authSession?.authUrl || 'https://app.cline.bot',
|
|
765
|
-
})
|
|
766
|
-
},
|
|
767
|
-
}), 'dsh-clinebot: /auth/status')
|
|
105
|
+
registerSettingsRoutes(ctx, routeEnv)
|
|
106
|
+
registerAccountsRoutes(ctx, routeEnv)
|
|
107
|
+
registerModelsRoutes(ctx, routeEnv)
|
|
108
|
+
registerAuthRoutes(ctx, routeEnv)
|
|
768
109
|
}
|
|
769
110
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const unregister = commands.register({
|
|
776
|
-
name: 'cline',
|
|
777
|
-
description: 'Check ClinePass subscription quota, models, accounts and test connectivity (/cline [quota|models|accounts|switch <name>|test [model]|ping|rotate])',
|
|
778
|
-
execute: async (rawArgs) => {
|
|
779
|
-
const pub = publicConfig(live())
|
|
780
|
-
const subcmd = String(rawArgs || '').trim().toLowerCase().split(/\s+/)[0] || 'quota'
|
|
781
|
-
const param = String(rawArgs || '').trim().split(/\s+/)[1] || ''
|
|
782
|
-
|
|
783
|
-
// 1. Subcommand /cline models
|
|
784
|
-
if (subcmd === 'models') {
|
|
785
|
-
const allModels = getAllModels(pub.dynamicModels)
|
|
786
|
-
const disabledSet = new Set(pub.disabledModels || [])
|
|
787
|
-
const lines = [
|
|
788
|
-
'### ๐ฏ ClinePass Models Catalog',
|
|
789
|
-
`* **ะัะตะณะพ ะผะพะดะตะปะตะน**: ${allModels.length} (${allModels.filter((m) => !disabledSet.has(m.id)).length} ะฐะบัะธะฒะฝะพ)`,
|
|
790
|
-
'',
|
|
791
|
-
]
|
|
792
|
-
for (const m of allModels) {
|
|
793
|
-
const active = !disabledSet.has(m.id) ? 'โ
' : 'โ'
|
|
794
|
-
const isVis = isVisionModel(m.id, pub.dynamicModels) ? '๐ท Vision' : '๐ Text'
|
|
795
|
-
const efforts = Array.isArray(m.reasoningEfforts) ? `๐ง [${m.reasoningEfforts.join(', ')}]` : ''
|
|
796
|
-
lines.push(`* ${active} **${m.name}** (\`${m.id}\`) โ ${isVis} ยท ${formatModelContext(m.contextLength)} ${efforts}`)
|
|
797
|
-
}
|
|
798
|
-
return lines.join('\n')
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// 2. Subcommand /cline accounts
|
|
802
|
-
if (subcmd === 'accounts') {
|
|
803
|
-
const pool = await resolveAccountPool(ctx, live())
|
|
804
|
-
const lines = [
|
|
805
|
-
'### ๐ ClinePass Accounts Pool',
|
|
806
|
-
`* **Active Account**: \`${pub.activeAccount || pub.apiKeyEnv}\``,
|
|
807
|
-
'',
|
|
808
|
-
]
|
|
809
|
-
for (const acc of pool) {
|
|
810
|
-
const pinBadge = acc.isPinned ? '๐ [Pinned]' : ''
|
|
811
|
-
const statusBadge = acc.present ? 'โ
Configured' : 'โ ๏ธ Missing Key'
|
|
812
|
-
const cacheKey = `cline:usage:${(acc.value || '').slice(-8)}`
|
|
813
|
-
const cached = usageCache.get(cacheKey)?.data
|
|
814
|
-
const usageInfo = cached?.windows?.fiveHour ? ` ยท โฑ ${cached.windows.fiveHour.percentUsed}% used` : ''
|
|
815
|
-
lines.push(`* **${acc.label}** (\`${acc.apiKeyEnv}\`): ${statusBadge} ${pinBadge}${usageInfo}`)
|
|
816
|
-
}
|
|
817
|
-
lines.push('', 'Switch active account: `/cline switch <env_variable_name>`')
|
|
818
|
-
return lines.join('\n')
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// 3. Subcommand /cline switch <account>
|
|
822
|
-
if (subcmd === 'switch') {
|
|
823
|
-
if (!param) {
|
|
824
|
-
return 'โ ๏ธ Please specify account: `/cline switch <CLINEBOT_API_KEY_2>`'
|
|
825
|
-
}
|
|
826
|
-
clearUsageCache()
|
|
827
|
-
clearProbeCache()
|
|
828
|
-
if (settingsApi?.replace) {
|
|
829
|
-
const next = Config({ ...live(), activeAccount: param })
|
|
830
|
-
await settingsApi.replace(next)
|
|
831
|
-
await syncProviderState(next)
|
|
832
|
-
return `โ
Active account switched to \`${param}\``
|
|
833
|
-
}
|
|
834
|
-
return `โ ๏ธ Could not apply setting (settings service unavailable).`
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// 4. Subcommand /cline rotate (smart failover next)
|
|
838
|
-
if (subcmd === 'rotate') {
|
|
839
|
-
const res = await rotateToNextAccount(ctx, live(), 'slash_command', settingsApi)
|
|
840
|
-
if (res.rotated) {
|
|
841
|
-
await syncProviderState(live())
|
|
842
|
-
return `๐ **Account Rotated**: switched from \`${res.previousAccount}\` to \`${res.activeAccount}\`. DSH provider updated!`
|
|
843
|
-
}
|
|
844
|
-
return `โ ๏ธ Rotation skipped: ${res.message || 'no alternative configured accounts in pool'}.`
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// 5. Subcommand /cline ping (fresh host reachability probe)
|
|
848
|
-
if (subcmd === 'ping') {
|
|
849
|
-
const health = await probeHealth(pub.baseUrl, { bypassCache: true, timeoutMs: 5000 })
|
|
850
|
-
if (health.ok) {
|
|
851
|
-
return `๐ **Cline API Pong**: \`${pub.baseUrl}\` is reachable (latency: **${health.latencyMs} ms**, HTTP ${health.status})`
|
|
852
|
-
}
|
|
853
|
-
return `โ **Cline API Ping Failed**: ${health.error || 'Host unreachable'}`
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// 6. Subcommand /cline test [model] / smoke
|
|
857
|
-
if (subcmd === 'test' || subcmd === 'smoke') {
|
|
858
|
-
if (param && !isSupportedModel(param, pub.dynamicModels)) {
|
|
859
|
-
return `โ ๏ธ **ClineBot**: Model \`${param}\` is not recognized. Use \`/cline models\` to see available models.`
|
|
860
|
-
}
|
|
861
|
-
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
862
|
-
if (!activeKey.value) {
|
|
863
|
-
return 'โ ๏ธ **ClineBot**: API key is not configured. Open **Settings โ Plugins โ ClineBot**.'
|
|
864
|
-
}
|
|
865
|
-
const modelToTest = param || pub.defaultModel || DEFAULT_MODEL_ID
|
|
866
|
-
const outcome = await smokeChat(pub.baseUrl, activeKey.value, {
|
|
867
|
-
model: modelToTest,
|
|
868
|
-
timeoutMs: pub.smokeTimeoutMs,
|
|
869
|
-
})
|
|
870
|
-
recordSessionRequest({
|
|
871
|
-
latencyMs: outcome.latencyMs,
|
|
872
|
-
ok: outcome.ok,
|
|
873
|
-
error: outcome.error,
|
|
874
|
-
promptTokens: outcome.promptTokens || 5,
|
|
875
|
-
completionTokens: outcome.completionTokens || 10,
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
let failoverNotice = ''
|
|
879
|
-
if (outcome.status === 429) {
|
|
880
|
-
const failover = await rotateToNextAccount(ctx, live(), 'smoke_429', settingsApi)
|
|
881
|
-
if (failover.rotated) {
|
|
882
|
-
await syncProviderState(live())
|
|
883
|
-
failoverNotice = `\n๐ **Auto-failover**: HTTP 429 detected! Active account automatically rotated to \`${failover.activeAccount}\`.`
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
if (outcome.ok) {
|
|
888
|
-
return [
|
|
889
|
-
`### ๐ข Smoke Test Passed: \`${outcome.model}\``,
|
|
890
|
-
`* **Latency**: ${outcome.latencyMs} ms`,
|
|
891
|
-
`* **Tokens**: prompt: ${outcome.promptTokens}, completion: ${outcome.completionTokens} (total: ${outcome.totalTokens})`,
|
|
892
|
-
`* **Preview**: _"${outcome.preview}"_`,
|
|
893
|
-
].join('\n')
|
|
894
|
-
}
|
|
895
|
-
return `โ **Smoke Test Failed**: ${outcome.error} (HTTP ${outcome.status || 'timeout'})${failoverNotice}`
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// 7. Subcommand /cline quota or /cline balance
|
|
899
|
-
const activeKey = await resolveActiveAccountKey(ctx, live())
|
|
900
|
-
if (!activeKey.value) {
|
|
901
|
-
return 'โ ๏ธ **ClineBot**: API key is not configured. Open **Settings โ Plugins โ ClineBot**.'
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const [health, usage] = await Promise.all([
|
|
905
|
-
probeHealth(pub.baseUrl, { timeoutMs: 5000 }),
|
|
906
|
-
fetchUsageLimits(pub.baseUrl, activeKey.value, { timeoutMs: 8000 }),
|
|
907
|
-
])
|
|
908
|
-
|
|
909
|
-
const fiveHour = usage?.windows?.fiveHour
|
|
910
|
-
const weekly = usage?.windows?.weekly
|
|
911
|
-
const reset5h = fiveHour?.resetsAt ? new Date(fiveHour.resetsAt).toLocaleTimeString() : 'N/A'
|
|
912
|
-
const resetWk = weekly?.resetsAt ? new Date(weekly.resetsAt).toLocaleDateString() : 'N/A'
|
|
913
|
-
|
|
914
|
-
const lines = [
|
|
915
|
-
`### ๐ค ClinePass Status (${usage?.plan || 'ClinePass'})`,
|
|
916
|
-
`* **Host Ping**: ${health.ok ? `โ
${health.latencyMs} ms` : 'โ Unreachable'}`,
|
|
917
|
-
`* **Active Key**: \`${activeKey.envName}\` (${activeKey.source})`,
|
|
918
|
-
`* **Default Model**: \`${pub.defaultModel}\``,
|
|
919
|
-
'',
|
|
920
|
-
`**โฑ 5-Hour Rolling Limit**: ${formatProgressBar(fiveHour?.percentUsed)} (reset: ${reset5h})`,
|
|
921
|
-
`**๐
Weekly Window**: ${formatProgressBar(weekly?.percentUsed)} (reset: ${resetWk})`,
|
|
922
|
-
]
|
|
923
|
-
|
|
924
|
-
if (fiveHour?.percentUsed >= 95) {
|
|
925
|
-
lines.push('', '๐จ **CRITICAL LIMIT**: 5-hour quota is 95%+ consumed! New requests may be throttled until reset.')
|
|
926
|
-
} else if (fiveHour?.percentUsed >= 80) {
|
|
927
|
-
lines.push('', `โ ๏ธ **Warning**: 5-hour quota is ${fiveHour.percentUsed}% consumed.`)
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
if (sessionStats.totalRequests > 0) {
|
|
931
|
-
lines.push('', `**๐ Session Telemetry**: ${sessionStats.successfulRequests}/${sessionStats.totalRequests} successful requests, ~${sessionStats.totalTokensEst} total tokens`)
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
if (usage?.user?.email) {
|
|
935
|
-
lines.push(`* **Account**: \`${usage.user.email}\``)
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
return lines.join('\n')
|
|
939
|
-
},
|
|
940
|
-
})
|
|
941
|
-
|
|
942
|
-
ctx.effect(() => () => unregister?.(), 'dsh-clinebot: slash-command')
|
|
111
|
+
registerSlashCommand(ctx, {
|
|
112
|
+
live,
|
|
113
|
+
getSettingsApi: () => settingsApi,
|
|
114
|
+
syncProviderState,
|
|
943
115
|
})
|
|
944
116
|
|
|
945
117
|
return {
|