dsh-llm-codebuddy 1.3.5 → 1.3.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/README.md +115 -145
- package/cli.js +21 -2
- package/client.js +427 -32
- package/codebuddy-auth.js +204 -7
- package/codebuddy-credits.js +441 -0
- package/codebuddy-web.js +340 -18
- package/index.js +40 -15
- package/package.json +3 -2
package/codebuddy-web.js
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CODEBUDDY_API_KEYS_REF,
|
|
5
|
+
CODEBUDDY_SESSION_REF,
|
|
6
|
+
CODEBUDDY_SESSIONS_REF,
|
|
7
|
+
activeCodeBuddySession,
|
|
8
|
+
codeBuddyApiKeyEntries,
|
|
9
|
+
codeBuddySessionAccounts,
|
|
10
|
+
createCodeBuddyApiKeyStore,
|
|
11
|
+
createCodeBuddySessionStore,
|
|
12
|
+
parseCodeBuddyApiKeys,
|
|
13
|
+
loginCodeBuddy,
|
|
14
|
+
parseCodeBuddySession,
|
|
15
|
+
parseCodeBuddySessions,
|
|
16
|
+
refreshCodeBuddySession,
|
|
17
|
+
serializeCodeBuddyApiKeys,
|
|
18
|
+
serializeCodeBuddySession,
|
|
19
|
+
serializeCodeBuddySessions,
|
|
20
|
+
sessionNeedsRefresh,
|
|
21
|
+
upsertCodeBuddyApiKey,
|
|
22
|
+
upsertCodeBuddySession,
|
|
23
|
+
} from "./codebuddy-auth.js";
|
|
24
|
+
import { fetchCodeBuddyCredits } from "./codebuddy-credits.js";
|
|
3
25
|
|
|
4
26
|
const PROVIDER = "codebuddy-cn";
|
|
5
27
|
const API_KEY_ENV = "CODEBUDDY_API_KEY";
|
|
6
28
|
const ROUTE = "/dsh-llm-codebuddy/auth";
|
|
29
|
+
const ENV_SOURCES = new Set(["env", "user-env", "project-env"]);
|
|
7
30
|
|
|
8
31
|
export function authenticationMode(config) {
|
|
9
32
|
const profile = config?.providers?.[PROVIDER];
|
|
@@ -28,45 +51,323 @@ function localPost(req) {
|
|
|
28
51
|
}
|
|
29
52
|
}
|
|
30
53
|
|
|
31
|
-
async function
|
|
54
|
+
async function requestBody(req) {
|
|
55
|
+
let raw = "";
|
|
56
|
+
for await (const chunk of req) {
|
|
57
|
+
raw += chunk.toString();
|
|
58
|
+
if (raw.length > 64 * 1024) throw new Error("请求体过大");
|
|
59
|
+
}
|
|
60
|
+
if (!raw.trim()) return {};
|
|
61
|
+
try {
|
|
62
|
+
const value = JSON.parse(raw);
|
|
63
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error("请求参数格式无效");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function setMode(settings, mode, apiKeyRef = API_KEY_ENV) {
|
|
32
70
|
const config = settings.get("llm-pi-ai");
|
|
33
71
|
const exists = Object.hasOwn(config?.providers ?? {}, PROVIDER);
|
|
34
72
|
const path = ["providers", PROVIDER];
|
|
35
73
|
if (!exists) {
|
|
36
|
-
await settings.mutate("llm-pi-ai", [{ op: "set", path, value: mode === "token" ? {} : { apiKeyEnv:
|
|
74
|
+
await settings.mutate("llm-pi-ai", [{ op: "set", path, value: mode === "token" ? {} : { apiKeyEnv: apiKeyRef } }]);
|
|
37
75
|
return;
|
|
38
76
|
}
|
|
39
77
|
await settings.mutate("llm-pi-ai", [{
|
|
40
78
|
op: mode === "token" ? "unset" : "set",
|
|
41
79
|
path: [...path, "apiKeyEnv"],
|
|
42
|
-
...(mode === "api-key" ? { value:
|
|
80
|
+
...(mode === "api-key" ? { value: apiKeyRef } : {}),
|
|
43
81
|
}]);
|
|
44
82
|
}
|
|
45
83
|
|
|
84
|
+
function configuredApiKeyRef(settings) {
|
|
85
|
+
return settings.get("llm-pi-ai")?.providers?.[PROVIDER]?.apiKeyEnv ?? API_KEY_ENV;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function maskApiKey(value) {
|
|
89
|
+
const text = typeof value === "string" ? value : "";
|
|
90
|
+
return text.length > 4 ? `••••${text.slice(-4)}` : text ? "••••" : "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function textLabel(value) {
|
|
94
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function storedApiKeyRef() {
|
|
98
|
+
return `CODEBUDDY_API_KEY_DSH_${Date.now().toString(36).toUpperCase()}_${randomBytes(6).toString("hex").toUpperCase()}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function readApiKeyStore(credentials) {
|
|
102
|
+
const stored = await credentials.resolve(credentialRef(CODEBUDDY_API_KEYS_REF));
|
|
103
|
+
if (!stored?.value) return createCodeBuddyApiKeyStore();
|
|
104
|
+
return parseCodeBuddyApiKeys(stored.value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function writeApiKeyStore(credentials, store) {
|
|
108
|
+
const normalized = createCodeBuddyApiKeyStore(store?.entries ?? [], store?.activeId);
|
|
109
|
+
if (normalized.entries.length === 0) {
|
|
110
|
+
await credentials.unset(credentialRef(CODEBUDDY_API_KEYS_REF));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
await credentials.set(credentialRef(CODEBUDDY_API_KEYS_REF), serializeCodeBuddyApiKeys(normalized));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function currentApiKeyState(webCtx) {
|
|
117
|
+
const store = await readApiKeyStore(webCtx.credentials);
|
|
118
|
+
const apiMode = authenticationMode(webCtx.settings.get("llm-pi-ai")) === "api-key";
|
|
119
|
+
const ref = apiMode ? configuredApiKeyRef(webCtx.settings) : undefined;
|
|
120
|
+
const items = [];
|
|
121
|
+
const seenRefs = new Set();
|
|
122
|
+
const environment = await webCtx.credentials.resolve(credentialRef(API_KEY_ENV));
|
|
123
|
+
if (environment?.value) {
|
|
124
|
+
items.push({
|
|
125
|
+
id: `env:${API_KEY_ENV}`,
|
|
126
|
+
kind: "environment",
|
|
127
|
+
label: `环境变量 ${API_KEY_ENV}`,
|
|
128
|
+
ref: API_KEY_ENV,
|
|
129
|
+
configured: true,
|
|
130
|
+
masked: maskApiKey(environment.value),
|
|
131
|
+
source: environment.source,
|
|
132
|
+
});
|
|
133
|
+
seenRefs.add(API_KEY_ENV);
|
|
134
|
+
}
|
|
135
|
+
for (const entry of codeBuddyApiKeyEntries(store)) {
|
|
136
|
+
if (seenRefs.has(entry.ref)) continue;
|
|
137
|
+
const resolved = await webCtx.credentials.resolve(credentialRef(entry.ref));
|
|
138
|
+
items.push({
|
|
139
|
+
...entry,
|
|
140
|
+
kind: "dsh",
|
|
141
|
+
configured: Boolean(resolved?.value),
|
|
142
|
+
...(resolved?.value ? { masked: maskApiKey(resolved.value), source: resolved.source } : {}),
|
|
143
|
+
});
|
|
144
|
+
seenRefs.add(entry.ref);
|
|
145
|
+
}
|
|
146
|
+
if (ref && !seenRefs.has(ref)) {
|
|
147
|
+
const resolved = await webCtx.credentials.resolve(credentialRef(ref));
|
|
148
|
+
if (resolved?.value) {
|
|
149
|
+
items.push({
|
|
150
|
+
id: `dsh:${ref}`,
|
|
151
|
+
kind: ENV_SOURCES.has(resolved.source) ? "environment" : "dsh",
|
|
152
|
+
label: ENV_SOURCES.has(resolved.source) ? `环境变量 ${ref}` : "DSH 默认 API Key",
|
|
153
|
+
ref,
|
|
154
|
+
configured: true,
|
|
155
|
+
masked: maskApiKey(resolved.value),
|
|
156
|
+
source: resolved.source,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const configured = apiMode ? items.find((item) => item.ref === ref) : undefined;
|
|
161
|
+
const active = configured ?? items.find((item) => item.id === store.activeId) ?? items[0];
|
|
162
|
+
return {
|
|
163
|
+
apiKeys: items,
|
|
164
|
+
activeApiKeyId: active?.id ?? null,
|
|
165
|
+
apiKeyConfigured: Boolean(configured?.configured),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readSessionStore(credentials) {
|
|
170
|
+
const stored = await credentials.resolve(credentialRef(CODEBUDDY_SESSIONS_REF));
|
|
171
|
+
if (stored?.value) return parseCodeBuddySessions(stored.value);
|
|
172
|
+
const legacy = await credentials.resolve(credentialRef(CODEBUDDY_SESSION_REF));
|
|
173
|
+
if (!legacy?.value) return createCodeBuddySessionStore();
|
|
174
|
+
const session = parseCodeBuddySession(legacy.value);
|
|
175
|
+
return createCodeBuddySessionStore([session]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function writeSessionStore(credentials, store) {
|
|
179
|
+
const active = activeCodeBuddySession(store);
|
|
180
|
+
if (!active) {
|
|
181
|
+
await credentials.unset(credentialRef(CODEBUDDY_SESSIONS_REF));
|
|
182
|
+
await credentials.unset(credentialRef(CODEBUDDY_SESSION_REF));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await credentials.set(credentialRef(CODEBUDDY_SESSIONS_REF), serializeCodeBuddySessions(store));
|
|
186
|
+
// Keep the old single-session reference as a compatibility pointer for older plugin versions.
|
|
187
|
+
await credentials.set(credentialRef(CODEBUDDY_SESSION_REF), serializeCodeBuddySession(active));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function resolveSession(webCtx, accountId) {
|
|
191
|
+
const store = await readSessionStore(webCtx.credentials);
|
|
192
|
+
const requestedId = typeof accountId === "string" && accountId ? accountId : store.activeId;
|
|
193
|
+
let session = store.sessions.find((entry) => entry.id === requestedId) ?? activeCodeBuddySession(store);
|
|
194
|
+
if (!session) throw new Error("没有找到该 CodeBuddy 登录账号");
|
|
195
|
+
if (sessionNeedsRefresh(session)) {
|
|
196
|
+
session = { ...session, ...(await refreshCodeBuddySession(session)), updatedAt: Date.now() };
|
|
197
|
+
const nextStore = upsertCodeBuddySession({ ...store, activeId: session.id }, session);
|
|
198
|
+
await writeSessionStore(webCtx.credentials, nextStore);
|
|
199
|
+
session = activeCodeBuddySession(nextStore);
|
|
200
|
+
}
|
|
201
|
+
return session;
|
|
202
|
+
}
|
|
203
|
+
|
|
46
204
|
export function installCodeBuddyWeb(ctx) {
|
|
47
205
|
ctx.inject(["webServer", "settings", "credentials"], (webCtx) => {
|
|
48
206
|
let loginPromise;
|
|
49
|
-
const currentState = async () =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
207
|
+
const currentState = async () => {
|
|
208
|
+
const store = await readSessionStore(webCtx.credentials);
|
|
209
|
+
const active = activeCodeBuddySession(store);
|
|
210
|
+
const apiKeys = await currentApiKeyState(webCtx);
|
|
211
|
+
return {
|
|
212
|
+
ok: true,
|
|
213
|
+
mode: authenticationMode(webCtx.settings.get("llm-pi-ai")),
|
|
214
|
+
authenticated: active !== undefined,
|
|
215
|
+
activeAccountId: active?.id ?? null,
|
|
216
|
+
accounts: codeBuddySessionAccounts(store),
|
|
217
|
+
...apiKeys,
|
|
218
|
+
};
|
|
219
|
+
};
|
|
54
220
|
const status = async (_req, res) => {
|
|
55
|
-
|
|
221
|
+
try {
|
|
222
|
+
json(res, 200, await currentState());
|
|
223
|
+
} catch (error) {
|
|
224
|
+
json(res, 500, { ok: false, message: error instanceof Error ? error.message : "读取 CodeBuddy 认证状态失败" });
|
|
225
|
+
}
|
|
56
226
|
};
|
|
57
227
|
const apiKey = async (req, res) => {
|
|
58
228
|
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
59
229
|
if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面切换认证方式" });
|
|
60
|
-
|
|
61
|
-
|
|
230
|
+
try {
|
|
231
|
+
const body = await requestBody(req);
|
|
232
|
+
let ref = API_KEY_ENV;
|
|
233
|
+
if (typeof body.keyId === "string" && body.keyId) {
|
|
234
|
+
const state = await currentApiKeyState(webCtx);
|
|
235
|
+
const selected = state.apiKeys.find((entry) => entry.id === body.keyId);
|
|
236
|
+
if (!selected) return json(res, 404, { ok: false, message: "没有找到该 CodeBuddy API Key" });
|
|
237
|
+
if (!selected.configured) return json(res, 409, { ok: false, message: "该 API Key 已不可用,请删除后重新添加" });
|
|
238
|
+
ref = selected.ref;
|
|
239
|
+
const store = await readApiKeyStore(webCtx.credentials);
|
|
240
|
+
await writeApiKeyStore(webCtx.credentials, { ...store, activeId: selected.kind === "dsh" ? selected.id : null });
|
|
241
|
+
}
|
|
242
|
+
credentialRef(ref);
|
|
243
|
+
await setMode(webCtx.settings, "api-key", ref);
|
|
244
|
+
json(res, 200, await currentState());
|
|
245
|
+
} catch (error) {
|
|
246
|
+
json(res, 500, { ok: false, message: error instanceof Error ? error.message : "切换 API Key 失败" });
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const addApiKey = async (req, res) => {
|
|
250
|
+
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
251
|
+
if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面保存 API Key" });
|
|
252
|
+
try {
|
|
253
|
+
const body = await requestBody(req);
|
|
254
|
+
const value = typeof body.key === "string" ? body.key.trim() : "";
|
|
255
|
+
if (!value) return json(res, 400, { ok: false, message: "请输入 API Key" });
|
|
256
|
+
if (value.length > 16 * 1024) return json(res, 413, { ok: false, message: "API Key 长度超出限制" });
|
|
257
|
+
const store = await readApiKeyStore(webCtx.credentials);
|
|
258
|
+
const ref = storedApiKeyRef();
|
|
259
|
+
const entry = {
|
|
260
|
+
id: `dsh:${ref}`,
|
|
261
|
+
ref,
|
|
262
|
+
label: textLabel(body.label) ?? `DSH API Key ${store.entries.length + 1}`,
|
|
263
|
+
};
|
|
264
|
+
await webCtx.credentials.set(credentialRef(ref), value);
|
|
265
|
+
try {
|
|
266
|
+
const next = upsertCodeBuddyApiKey(store, entry);
|
|
267
|
+
await writeApiKeyStore(webCtx.credentials, next);
|
|
268
|
+
await setMode(webCtx.settings, "api-key", ref);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
await webCtx.credentials.unset(credentialRef(ref));
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
json(res, 200, await currentState());
|
|
274
|
+
} catch (error) {
|
|
275
|
+
json(res, 500, { ok: false, message: error instanceof Error ? error.message : "保存 API Key 失败" });
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const removeApiKey = async (req, res) => {
|
|
279
|
+
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
280
|
+
if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面删除 API Key" });
|
|
281
|
+
try {
|
|
282
|
+
const body = await requestBody(req);
|
|
283
|
+
const store = await readApiKeyStore(webCtx.credentials);
|
|
284
|
+
const entry = store.entries.find((item) => item.id === body.keyId);
|
|
285
|
+
if (!entry) return json(res, 404, { ok: false, message: "没有找到该 CodeBuddy API Key" });
|
|
286
|
+
const activeRef = configuredApiKeyRef(webCtx.settings);
|
|
287
|
+
await webCtx.credentials.unset(credentialRef(entry.ref));
|
|
288
|
+
const remaining = store.entries.filter((item) => item.id !== entry.id);
|
|
289
|
+
await writeApiKeyStore(webCtx.credentials, { version: 1, activeId: remaining[0]?.id, entries: remaining });
|
|
290
|
+
if (authenticationMode(webCtx.settings.get("llm-pi-ai")) === "api-key" && activeRef === entry.ref) {
|
|
291
|
+
const environment = await webCtx.credentials.resolve(credentialRef(API_KEY_ENV));
|
|
292
|
+
let fallback = API_KEY_ENV;
|
|
293
|
+
if (!environment?.value) {
|
|
294
|
+
for (const candidate of remaining) {
|
|
295
|
+
if ((await webCtx.credentials.resolve(credentialRef(candidate.ref)))?.value) {
|
|
296
|
+
fallback = candidate.ref;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
await setMode(webCtx.settings, "api-key", fallback);
|
|
302
|
+
}
|
|
303
|
+
json(res, 200, await currentState());
|
|
304
|
+
} catch (error) {
|
|
305
|
+
json(res, 500, { ok: false, message: error instanceof Error ? error.message : "删除 API Key 失败" });
|
|
306
|
+
}
|
|
62
307
|
};
|
|
63
308
|
const token = async (req, res) => {
|
|
64
309
|
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
65
310
|
if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面切换认证方式" });
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
311
|
+
try {
|
|
312
|
+
const body = await requestBody(req);
|
|
313
|
+
const store = await readSessionStore(webCtx.credentials);
|
|
314
|
+
const accountId = typeof body.accountId === "string" ? body.accountId : store.activeId;
|
|
315
|
+
const active = store.sessions.find((entry) => entry.id === accountId);
|
|
316
|
+
if (!active) return json(res, 409, { ok: false, message: "没有找到该 CodeBuddy 登录账号" });
|
|
317
|
+
await writeSessionStore(webCtx.credentials, { ...store, activeId: active.id });
|
|
318
|
+
await setMode(webCtx.settings, "token");
|
|
319
|
+
json(res, 200, await currentState());
|
|
320
|
+
} catch (error) {
|
|
321
|
+
json(res, 500, { ok: false, message: error instanceof Error ? error.message : "切换令牌账号失败" });
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
const credits = async (req, res) => {
|
|
325
|
+
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
326
|
+
if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面查询 CodeBuddy 积分" });
|
|
327
|
+
try {
|
|
328
|
+
if (authenticationMode(webCtx.settings.get("llm-pi-ai")) !== "token") {
|
|
329
|
+
return json(res, 200, {
|
|
330
|
+
ok: true,
|
|
331
|
+
accountId: null,
|
|
332
|
+
credits: null,
|
|
333
|
+
totalDosage: null,
|
|
334
|
+
segments: [],
|
|
335
|
+
unlimited: false,
|
|
336
|
+
cycleResetTime: null,
|
|
337
|
+
creditError: "积分查询仅支持 CodeBuddy 令牌登录",
|
|
338
|
+
todayUsage: null,
|
|
339
|
+
todayUsageError: "今日请求量查询仅支持 CodeBuddy 令牌登录",
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const body = await requestBody(req);
|
|
343
|
+
const session = await resolveSession(webCtx, body.accountId);
|
|
344
|
+
const result = await fetchCodeBuddyCredits(session);
|
|
345
|
+
json(res, 200, {
|
|
346
|
+
ok: true,
|
|
347
|
+
accountId: session.id,
|
|
348
|
+
credits: result.credits,
|
|
349
|
+
totalDosage: result.totalDosage,
|
|
350
|
+
segments: result.segments,
|
|
351
|
+
unlimited: !!result.unlimited,
|
|
352
|
+
cycleResetTime: result.cycleResetTime ?? null,
|
|
353
|
+
creditError: result.creditError ?? null,
|
|
354
|
+
todayUsage: result.todayUsage ?? null,
|
|
355
|
+
todayUsageError: result.todayUsageError ?? null,
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
json(res, 200, {
|
|
359
|
+
ok: true,
|
|
360
|
+
accountId: null,
|
|
361
|
+
credits: null,
|
|
362
|
+
totalDosage: null,
|
|
363
|
+
segments: [],
|
|
364
|
+
unlimited: false,
|
|
365
|
+
cycleResetTime: null,
|
|
366
|
+
creditError: error instanceof Error ? error.message : "查询 CodeBuddy 积分失败",
|
|
367
|
+
todayUsage: null,
|
|
368
|
+
todayUsageError: error instanceof Error ? error.message : "查询 CodeBuddy 今日请求量失败",
|
|
369
|
+
});
|
|
370
|
+
}
|
|
70
371
|
};
|
|
71
372
|
const login = async (req, res) => {
|
|
72
373
|
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
@@ -74,23 +375,44 @@ export function installCodeBuddyWeb(ctx) {
|
|
|
74
375
|
try {
|
|
75
376
|
loginPromise ??= (async () => {
|
|
76
377
|
const session = await loginCodeBuddy();
|
|
77
|
-
await webCtx.credentials
|
|
378
|
+
const store = await readSessionStore(webCtx.credentials);
|
|
379
|
+
await writeSessionStore(webCtx.credentials, upsertCodeBuddySession(store, session));
|
|
78
380
|
await setMode(webCtx.settings, "token");
|
|
79
381
|
})().finally(() => {
|
|
80
382
|
loginPromise = undefined;
|
|
81
383
|
});
|
|
82
384
|
await loginPromise;
|
|
83
|
-
json(res, 200,
|
|
385
|
+
json(res, 200, await currentState());
|
|
84
386
|
} catch (error) {
|
|
85
387
|
json(res, 500, { ok: false, message: error instanceof Error ? error.message : "CodeBuddy 登录失败" });
|
|
86
388
|
}
|
|
87
389
|
};
|
|
390
|
+
const remove = async (req, res) => {
|
|
391
|
+
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
392
|
+
if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面管理登录账号" });
|
|
393
|
+
try {
|
|
394
|
+
const body = await requestBody(req);
|
|
395
|
+
const store = await readSessionStore(webCtx.credentials);
|
|
396
|
+
const accountId = typeof body.accountId === "string" ? body.accountId : store.activeId;
|
|
397
|
+
const sessions = store.sessions.filter((entry) => entry.id !== accountId);
|
|
398
|
+
if (sessions.length === store.sessions.length) return json(res, 404, { ok: false, message: "没有找到该 CodeBuddy 登录账号" });
|
|
399
|
+
const activeId = accountId === store.activeId ? sessions[0]?.id : store.activeId;
|
|
400
|
+
await writeSessionStore(webCtx.credentials, { version: 1, activeId, sessions });
|
|
401
|
+
json(res, 200, await currentState());
|
|
402
|
+
} catch (error) {
|
|
403
|
+
json(res, 500, { ok: false, message: error instanceof Error ? error.message : "删除令牌账号失败" });
|
|
404
|
+
}
|
|
405
|
+
};
|
|
88
406
|
webCtx.effect(() => {
|
|
89
407
|
const dispose = [
|
|
90
408
|
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/status`, handler: status }),
|
|
91
409
|
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/api-key`, handler: apiKey }),
|
|
410
|
+
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/api-key/add`, handler: addApiKey }),
|
|
411
|
+
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/api-key/remove`, handler: removeApiKey }),
|
|
92
412
|
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/token`, handler: token }),
|
|
413
|
+
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/credits`, handler: credits }),
|
|
93
414
|
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/login`, handler: login }),
|
|
415
|
+
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/remove`, handler: remove }),
|
|
94
416
|
];
|
|
95
417
|
return () => dispose.forEach((fn) => fn());
|
|
96
418
|
}, "llm-codebuddy: web login routes");
|
package/index.js
CHANGED
|
@@ -8,11 +8,17 @@ import * as openAICompletionsApi from "@earendil-works/pi-ai/api/openai-completi
|
|
|
8
8
|
import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
9
9
|
import {
|
|
10
10
|
CODEBUDDY_SESSION_REF,
|
|
11
|
+
CODEBUDDY_SESSIONS_REF,
|
|
12
|
+
activeCodeBuddySession,
|
|
13
|
+
createCodeBuddySessionStore,
|
|
11
14
|
parseCodeBuddySession,
|
|
15
|
+
parseCodeBuddySessions,
|
|
12
16
|
refreshCodeBuddySession,
|
|
13
17
|
serializeCodeBuddySession,
|
|
18
|
+
serializeCodeBuddySessions,
|
|
14
19
|
sessionCacheDeadline,
|
|
15
20
|
sessionNeedsRefresh,
|
|
21
|
+
upsertCodeBuddySession,
|
|
16
22
|
} from "./codebuddy-auth.js";
|
|
17
23
|
import { installCodeBuddyWeb } from "./codebuddy-web.js";
|
|
18
24
|
|
|
@@ -263,8 +269,8 @@ export function apply(ctx, config) {
|
|
|
263
269
|
let memoRaw;
|
|
264
270
|
let memoGeneration = -1;
|
|
265
271
|
let memoized;
|
|
266
|
-
let loginSession;
|
|
267
272
|
let loginSessionPromise;
|
|
273
|
+
let remoteModelsKey;
|
|
268
274
|
const builtins = new Map(builtinProviders().map((provider) => [provider.id, provider]));
|
|
269
275
|
const apiKeyAuth = builtins.get("deepseek")?.auth;
|
|
270
276
|
if (!apiKeyAuth) throw new Error(`${name}: pi-ai DeepSeek auth helper is unavailable`);
|
|
@@ -312,24 +318,37 @@ export function apply(ctx, config) {
|
|
|
312
318
|
};
|
|
313
319
|
|
|
314
320
|
const resolveLoginSession = async () => {
|
|
315
|
-
if (loginSession?.expiresAt > Date.now()) return loginSession;
|
|
316
321
|
loginSessionPromise ??= (async () => {
|
|
317
322
|
const credentials = ctx.get("credentials");
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
let
|
|
323
|
+
const env = launchEnvironmentOf(ctx);
|
|
324
|
+
const sessionsRef = credentialRef(CODEBUDDY_SESSIONS_REF);
|
|
325
|
+
const storedSessions = await credentials?.resolve(sessionsRef);
|
|
326
|
+
const sessionsValue = storedSessions?.value ?? env.get(sessionsRef)?.value;
|
|
327
|
+
let store;
|
|
328
|
+
if (sessionsValue) {
|
|
329
|
+
store = parseCodeBuddySessions(sessionsValue);
|
|
330
|
+
} else {
|
|
331
|
+
const legacyRef = credentialRef(CODEBUDDY_SESSION_REF);
|
|
332
|
+
const storedLegacy = await credentials?.resolve(legacyRef);
|
|
333
|
+
const legacyValue = storedLegacy?.value ?? env.get(legacyRef)?.value;
|
|
334
|
+
if (!legacyValue) throw new Error("未找到 CodeBuddy 登录凭据");
|
|
335
|
+
const legacy = parseCodeBuddySession(legacyValue);
|
|
336
|
+
store = createCodeBuddySessionStore([legacy]);
|
|
337
|
+
}
|
|
338
|
+
const active = activeCodeBuddySession(store);
|
|
339
|
+
if (!active) throw new Error("未找到 CodeBuddy 登录账号");
|
|
340
|
+
let session = active;
|
|
323
341
|
if (sessionNeedsRefresh(session)) {
|
|
324
|
-
session = await refreshCodeBuddySession(session);
|
|
325
|
-
|
|
342
|
+
session = { ...session, ...(await refreshCodeBuddySession(session)), updatedAt: Date.now() };
|
|
343
|
+
const nextStore = upsertCodeBuddySession({ ...store, activeId: active.id }, session);
|
|
344
|
+
await credentials?.set(sessionsRef, serializeCodeBuddySessions(nextStore));
|
|
345
|
+
await credentials?.set(credentialRef(CODEBUDDY_SESSION_REF), serializeCodeBuddySession(session));
|
|
326
346
|
}
|
|
327
|
-
return { ...session, expiresAt: sessionCacheDeadline(session) };
|
|
347
|
+
return { ...session, sessionId: active.id, expiresAt: sessionCacheDeadline(session) };
|
|
328
348
|
})().finally(() => {
|
|
329
349
|
loginSessionPromise = undefined;
|
|
330
350
|
});
|
|
331
|
-
|
|
332
|
-
return loginSession;
|
|
351
|
+
return loginSessionPromise;
|
|
333
352
|
};
|
|
334
353
|
|
|
335
354
|
const resolveCredential = async (provider, profile) => {
|
|
@@ -348,7 +367,7 @@ export function apply(ctx, config) {
|
|
|
348
367
|
profile.headers["X-Tenant-Id"] = session.account.enterpriseId;
|
|
349
368
|
}
|
|
350
369
|
if (session.auth.domain) profile.headers["X-Domain"] = session.auth.domain;
|
|
351
|
-
return { value: assertUsableApiKey(session.auth.accessToken, name, "CodeBuddy login session"), kind: "bearer" };
|
|
370
|
+
return { value: assertUsableApiKey(session.auth.accessToken, name, "CodeBuddy login session"), kind: "bearer", sessionId: session.sessionId };
|
|
352
371
|
}
|
|
353
372
|
if (!ref) return { value: undefined, kind: "none" };
|
|
354
373
|
const stored = await ctx.get("credentials")?.resolve(ref);
|
|
@@ -376,17 +395,22 @@ export function apply(ctx, config) {
|
|
|
376
395
|
const listModels = adapter.listModels.bind(adapter);
|
|
377
396
|
let refreshPromise;
|
|
378
397
|
adapter.listModels = async (provider) => {
|
|
379
|
-
if (provider === PROVIDER
|
|
398
|
+
if (provider === PROVIDER) {
|
|
380
399
|
refreshPromise ??= (async () => {
|
|
381
400
|
try {
|
|
382
401
|
const profile = profiles().get(PROVIDER);
|
|
383
402
|
const credential = await resolveCredential(PROVIDER, profile);
|
|
403
|
+
const cacheKey = credential.kind === "bearer" ? `token:${credential.sessionId ?? "active"}` : `api:${credential.ref ?? API_KEY_ENV}`;
|
|
404
|
+
if (remoteModels && remoteModelsKey === cacheKey) return;
|
|
384
405
|
remoteModels = await fetchCodeBuddyModels(credential);
|
|
406
|
+
remoteModelsKey = cacheKey;
|
|
385
407
|
generation += 1;
|
|
386
408
|
} catch {
|
|
387
409
|
// Keep the built-in catalog available while the key or network is absent.
|
|
388
410
|
}
|
|
389
|
-
})()
|
|
411
|
+
})().finally(() => {
|
|
412
|
+
refreshPromise = undefined;
|
|
413
|
+
});
|
|
390
414
|
await refreshPromise;
|
|
391
415
|
}
|
|
392
416
|
return listModels(provider);
|
|
@@ -416,6 +440,7 @@ export function apply(ctx, config) {
|
|
|
416
440
|
? { value: request.apiKey, kind: "api-key", ref: API_KEY_ENV }
|
|
417
441
|
: await resolveCredential(PROVIDER, profile);
|
|
418
442
|
remoteModels = await fetchCodeBuddyModels(credential, request.signal);
|
|
443
|
+
remoteModelsKey = credential.kind === "bearer" ? `token:${credential.sessionId ?? "active"}` : `api:${credential.ref ?? API_KEY_ENV}`;
|
|
419
444
|
generation += 1;
|
|
420
445
|
return remoteModels.map((model) => ({
|
|
421
446
|
id: model.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-llm-codebuddy",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "通过 WorkBuddy API Key 或 CodeBuddy 登录令牌为 DeepSeek Harness 接入 CodeBuddy 模型",
|
|
5
5
|
"author": "Axiaohungry",
|
|
6
6
|
"keywords": [
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"files": [
|
|
33
33
|
"index.js",
|
|
34
34
|
"codebuddy-auth.js",
|
|
35
|
+
"codebuddy-credits.js",
|
|
35
36
|
"codebuddy-web.js",
|
|
36
37
|
"client.js",
|
|
37
38
|
"cli.js",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"LICENSE"
|
|
42
43
|
],
|
|
43
44
|
"scripts": {
|
|
44
|
-
"check": "node --check index.js && node --check codebuddy-auth.js && node --check codebuddy-web.js && node --check client.js && node --check cli.js && node --test test.js && node cli.js --self-test"
|
|
45
|
+
"check": "node --check index.js && node --check codebuddy-auth.js && node --check codebuddy-credits.js && node --check codebuddy-web.js && node --check client.js && node --check cli.js && node --test test.js && node cli.js --self-test"
|
|
45
46
|
},
|
|
46
47
|
"dsh": {
|
|
47
48
|
"client": {
|