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/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 { CODEBUDDY_SESSION_REF, loginCodeBuddy, serializeCodeBuddySession } from "./codebuddy-auth.js";
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 setMode(settings, mode) {
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: API_KEY_ENV } }]);
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: API_KEY_ENV } : {}),
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
- ok: true,
51
- mode: authenticationMode(webCtx.settings.get("llm-pi-ai")),
52
- authenticated: (await webCtx.credentials.describe(credentialRef(CODEBUDDY_SESSION_REF))).configured,
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
- json(res, 200, await currentState());
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
- await setMode(webCtx.settings, "api-key");
61
- json(res, 200, await currentState());
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
- const state = await currentState();
67
- if (!state.authenticated) return json(res, 409, { ok: false, message: "尚未保存 CodeBuddy 登录令牌" });
68
- await setMode(webCtx.settings, "token");
69
- json(res, 200, await currentState());
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.set(credentialRef(CODEBUDDY_SESSION_REF), serializeCodeBuddySession(session));
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, { ok: true, mode: "token", authenticated: true });
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 ref = credentialRef(CODEBUDDY_SESSION_REF);
319
- const stored = await credentials?.resolve(ref);
320
- const value = stored?.value ?? launchEnvironmentOf(ctx).get(ref)?.value;
321
- if (!value) throw new Error("未找到 CodeBuddy 登录凭据");
322
- let session = parseCodeBuddySession(value);
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
- await credentials?.set(ref, serializeCodeBuddySession(session));
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
- loginSession = await loginSessionPromise;
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 && !remoteModels) {
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.5",
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": {