dsh-llm-codebuddy 1.3.4 → 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.
@@ -0,0 +1,420 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
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";
25
+
26
+ const PROVIDER = "codebuddy-cn";
27
+ const API_KEY_ENV = "CODEBUDDY_API_KEY";
28
+ const ROUTE = "/dsh-llm-codebuddy/auth";
29
+ const ENV_SOURCES = new Set(["env", "user-env", "project-env"]);
30
+
31
+ export function authenticationMode(config) {
32
+ const profile = config?.providers?.[PROVIDER];
33
+ return profile && profile.apiKeyEnv === undefined ? "token" : "api-key";
34
+ }
35
+
36
+ function json(res, status, body) {
37
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
38
+ res.end(JSON.stringify(body));
39
+ }
40
+
41
+ function localPost(req) {
42
+ const address = req.socket.remoteAddress;
43
+ const loopback = address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
44
+ if (!loopback) return false;
45
+ const origin = req.headers.origin;
46
+ if (!origin) return req.headers["sec-fetch-site"] === "same-origin";
47
+ try {
48
+ return ["127.0.0.1", "localhost", "[::1]"].includes(new URL(origin).hostname);
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
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) {
70
+ const config = settings.get("llm-pi-ai");
71
+ const exists = Object.hasOwn(config?.providers ?? {}, PROVIDER);
72
+ const path = ["providers", PROVIDER];
73
+ if (!exists) {
74
+ await settings.mutate("llm-pi-ai", [{ op: "set", path, value: mode === "token" ? {} : { apiKeyEnv: apiKeyRef } }]);
75
+ return;
76
+ }
77
+ await settings.mutate("llm-pi-ai", [{
78
+ op: mode === "token" ? "unset" : "set",
79
+ path: [...path, "apiKeyEnv"],
80
+ ...(mode === "api-key" ? { value: apiKeyRef } : {}),
81
+ }]);
82
+ }
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
+
204
+ export function installCodeBuddyWeb(ctx) {
205
+ ctx.inject(["webServer", "settings", "credentials"], (webCtx) => {
206
+ let loginPromise;
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
+ };
220
+ const status = async (_req, res) => {
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
+ }
226
+ };
227
+ const apiKey = async (req, res) => {
228
+ if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
229
+ if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面切换认证方式" });
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
+ }
307
+ };
308
+ const token = async (req, res) => {
309
+ if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
310
+ if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面切换认证方式" });
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
+ }
371
+ };
372
+ const login = async (req, res) => {
373
+ if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
374
+ if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面登录" });
375
+ try {
376
+ loginPromise ??= (async () => {
377
+ const session = await loginCodeBuddy();
378
+ const store = await readSessionStore(webCtx.credentials);
379
+ await writeSessionStore(webCtx.credentials, upsertCodeBuddySession(store, session));
380
+ await setMode(webCtx.settings, "token");
381
+ })().finally(() => {
382
+ loginPromise = undefined;
383
+ });
384
+ await loginPromise;
385
+ json(res, 200, await currentState());
386
+ } catch (error) {
387
+ json(res, 500, { ok: false, message: error instanceof Error ? error.message : "CodeBuddy 登录失败" });
388
+ }
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
+ };
406
+ webCtx.effect(() => {
407
+ const dispose = [
408
+ webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/status`, handler: status }),
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 }),
412
+ webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/token`, handler: token }),
413
+ webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/credits`, handler: credits }),
414
+ webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/login`, handler: login }),
415
+ webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/remove`, handler: remove }),
416
+ ];
417
+ return () => dispose.forEach((fn) => fn());
418
+ }, "llm-codebuddy: web login routes");
419
+ });
420
+ }
package/index.js CHANGED
@@ -6,6 +6,21 @@ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-sett
6
6
  import { createProvider } from "@earendil-works/pi-ai";
7
7
  import * as openAICompletionsApi from "@earendil-works/pi-ai/api/openai-completions";
8
8
  import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
9
+ import {
10
+ CODEBUDDY_SESSION_REF,
11
+ CODEBUDDY_SESSIONS_REF,
12
+ activeCodeBuddySession,
13
+ createCodeBuddySessionStore,
14
+ parseCodeBuddySession,
15
+ parseCodeBuddySessions,
16
+ refreshCodeBuddySession,
17
+ serializeCodeBuddySession,
18
+ serializeCodeBuddySessions,
19
+ sessionCacheDeadline,
20
+ sessionNeedsRefresh,
21
+ upsertCodeBuddySession,
22
+ } from "./codebuddy-auth.js";
23
+ import { installCodeBuddyWeb } from "./codebuddy-web.js";
9
24
 
10
25
  export { Config };
11
26
 
@@ -18,7 +33,7 @@ const DISPLAY_NAME = "CodeBuddy 中国区";
18
33
  const API_KEY_ENV = "CODEBUDDY_API_KEY";
19
34
  const BASE_URL = "https://copilot.tencent.com/v2";
20
35
  const CONFIG_URL = "https://copilot.tencent.com/v3/config";
21
- const USER_AGENT = "CLI/unknown CodeBuddy/2.136.0";
36
+ const USER_AGENT = "CLI/unknown CodeBuddy/2.137.1";
22
37
  const STREAM_IDLE_TIMEOUT_MS = 300_000;
23
38
  const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
24
39
  const EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"];
@@ -31,6 +46,16 @@ const COMPAT = {
31
46
  thinkingFormat: "openai",
32
47
  };
33
48
 
49
+ function codeBuddyRequestOptions(options) {
50
+ return { ...options, headers: { ...(options?.headers ?? {}), "user-agent": USER_AGENT } };
51
+ }
52
+
53
+ const codeBuddyApi = {
54
+ ...openAICompletionsApi,
55
+ stream: (model, context, options) => openAICompletionsApi.stream(model, context, codeBuddyRequestOptions(options)),
56
+ streamSimple: (model, context, options) => openAICompletionsApi.streamSimple(model, context, codeBuddyRequestOptions(options)),
57
+ };
58
+
34
59
  const FALLBACK_MODELS = [
35
60
  ["hy3", "Hy3", 192000, 64000, true],
36
61
  ["glm-5.2", "GLM-5.2", 1000000, 48000, false],
@@ -133,13 +158,18 @@ function modelsFromConfig(data) {
133
158
  });
134
159
  }
135
160
 
136
- async function fetchCodeBuddyModels(apiKey, signal) {
161
+ function authenticationHeaders(credential) {
162
+ const value = assertUsableApiKey(credential.value, name, credential.ref ?? API_KEY_ENV);
163
+ return credential.kind === "bearer" ? { authorization: `Bearer ${value}` } : { "x-api-key": value };
164
+ }
165
+
166
+ async function fetchCodeBuddyModels(credential, signal) {
137
167
  let response;
138
168
  try {
139
169
  response = await fetch(CONFIG_URL, {
140
170
  headers: {
141
171
  accept: "application/json",
142
- "x-api-key": assertUsableApiKey(apiKey, name, API_KEY_ENV),
172
+ ...authenticationHeaders(credential),
143
173
  "user-agent": USER_AGENT,
144
174
  "x-product": "SaaS",
145
175
  },
@@ -164,7 +194,7 @@ function codeBuddyProvider(models, auth) {
164
194
  baseUrl: BASE_URL,
165
195
  auth,
166
196
  models,
167
- api: openAICompletionsApi,
197
+ api: codeBuddyApi,
168
198
  });
169
199
  }
170
200
 
@@ -172,6 +202,7 @@ function resolvedProfile(provider, source, piProvider, configuredMaxTokens = new
172
202
  const apiKeyEnv = source.apiKeyEnv === undefined ? undefined : credentialRef(source.apiKeyEnv);
173
203
  return {
174
204
  ...source,
205
+ headers: runtimeHeaders(source.headers),
175
206
  provider,
176
207
  displayName: source.displayName ?? piProvider.name ?? provider,
177
208
  ...(apiKeyEnv === undefined ? {} : { apiKeyEnv }),
@@ -216,15 +247,30 @@ function selectCodeBuddyModels(base, entries) {
216
247
  });
217
248
  }
218
249
 
219
- export const __testing = Object.freeze({ modelsFromConfig, selectCodeBuddyModels });
250
+ function ownsProvider(provider, builtins) {
251
+ return provider === PROVIDER || builtins.has(provider);
252
+ }
253
+
254
+ function runtimeHeaders(headers) {
255
+ return { ...(headers ?? {}) };
256
+ }
257
+
258
+ function codeBuddySource(config, source) {
259
+ return Object.hasOwn(config?.providers ?? {}, PROVIDER) ? source : { ...source, apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV };
260
+ }
261
+
262
+ export const __testing = Object.freeze({ authenticationHeaders, codeBuddyRequestOptions, codeBuddySource, modelsFromConfig, ownsProvider, runtimeHeaders, selectCodeBuddyModels });
220
263
 
221
264
  export function apply(ctx, config) {
265
+ installCodeBuddyWeb(ctx);
222
266
  let current = () => config;
223
267
  let remoteModels;
224
268
  let generation = 0;
225
269
  let memoRaw;
226
270
  let memoGeneration = -1;
227
271
  let memoized;
272
+ let loginSessionPromise;
273
+ let remoteModelsKey;
228
274
  const builtins = new Map(builtinProviders().map((provider) => [provider.id, provider]));
229
275
  const apiKeyAuth = builtins.get("deepseek")?.auth;
230
276
  if (!apiKeyAuth) throw new Error(`${name}: pi-ai DeepSeek auth helper is unavailable`);
@@ -245,20 +291,20 @@ export function apply(ctx, config) {
245
291
  if (memoRaw === current() && memoGeneration === generation && memoized) return memoized;
246
292
  const result = new Map();
247
293
  for (const [provider, source] of Object.entries(raw.providers)) {
294
+ if (!ownsProvider(provider, builtins)) continue;
248
295
  if (provider === PROVIDER) {
296
+ const sourceWithAuth = codeBuddySource(current(), source);
249
297
  const models = selectCodeBuddyModels(remoteModels ?? FALLBACK_MODELS, source.models);
250
298
  const configured = new Map((source.models ?? []).flatMap((model) =>
251
299
  Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
252
300
  ));
253
301
  result.set(provider, resolvedProfile(provider, {
254
- ...source,
255
- apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV,
302
+ ...sourceWithAuth,
256
303
  displayName: DISPLAY_NAME,
257
304
  }, codeBuddyProvider(models, apiKeyAuth), configured));
258
305
  continue;
259
306
  }
260
307
  const base = builtins.get(provider);
261
- if (!base) throw new Error(`${name}: 不支持非内置 Provider "${provider}"`);
262
308
  const selected = selectBuiltinModels(base, source.models);
263
309
  const configured = new Map((source.models ?? []).flatMap((model) =>
264
310
  Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
@@ -271,15 +317,67 @@ export function apply(ctx, config) {
271
317
  return result;
272
318
  };
273
319
 
274
- const resolveApiKey = async (provider, profile) => {
320
+ const resolveLoginSession = async () => {
321
+ loginSessionPromise ??= (async () => {
322
+ const credentials = ctx.get("credentials");
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;
341
+ if (sessionNeedsRefresh(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));
346
+ }
347
+ return { ...session, sessionId: active.id, expiresAt: sessionCacheDeadline(session) };
348
+ })().finally(() => {
349
+ loginSessionPromise = undefined;
350
+ });
351
+ return loginSessionPromise;
352
+ };
353
+
354
+ const resolveCredential = async (provider, profile) => {
275
355
  const ref = profile.apiKeyEnv;
276
- if (!ref) return undefined;
356
+ if (!ref && provider === PROVIDER) {
357
+ let session;
358
+ try {
359
+ session = await resolveLoginSession();
360
+ } catch (error) {
361
+ throw new LlmError(`${name}: 未找到可用的 CodeBuddy 登录令牌,请运行 dsh-llm-codebuddy login`, "MISSING_CREDENTIAL", { cause: error });
362
+ }
363
+ profile.headers ??= {};
364
+ if (session.account.userId) profile.headers["X-User-Id"] = session.account.userId;
365
+ if (session.account.enterpriseId) {
366
+ profile.headers["X-Enterprise-Id"] = session.account.enterpriseId;
367
+ profile.headers["X-Tenant-Id"] = session.account.enterpriseId;
368
+ }
369
+ if (session.auth.domain) profile.headers["X-Domain"] = session.auth.domain;
370
+ return { value: assertUsableApiKey(session.auth.accessToken, name, "CodeBuddy login session"), kind: "bearer", sessionId: session.sessionId };
371
+ }
372
+ if (!ref) return { value: undefined, kind: "none" };
277
373
  const stored = await ctx.get("credentials")?.resolve(ref);
278
374
  const value = stored?.value ?? launchEnvironmentOf(ctx).get(ref)?.value;
279
- if (value) return assertUsableApiKey(value, name, ref);
375
+ if (value) return { value: assertUsableApiKey(value, name, ref), kind: "api-key", ref };
280
376
  throw new LlmError(`${name}: Provider "${provider}" 缺少 API Key,请在 WebUI 的模型设置中填写`, "MISSING_CREDENTIAL");
281
377
  };
282
378
 
379
+ const resolveApiKey = async (provider, profile) => (await resolveCredential(provider, profile)).value;
380
+
283
381
  const adapter = new PiAiAdapter({
284
382
  profiles,
285
383
  resolveApiKey,
@@ -297,17 +395,22 @@ export function apply(ctx, config) {
297
395
  const listModels = adapter.listModels.bind(adapter);
298
396
  let refreshPromise;
299
397
  adapter.listModels = async (provider) => {
300
- if (provider === PROVIDER && !remoteModels) {
398
+ if (provider === PROVIDER) {
301
399
  refreshPromise ??= (async () => {
302
400
  try {
303
401
  const profile = profiles().get(PROVIDER);
304
- const apiKey = await resolveApiKey(PROVIDER, profile);
305
- remoteModels = await fetchCodeBuddyModels(apiKey);
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;
405
+ remoteModels = await fetchCodeBuddyModels(credential);
406
+ remoteModelsKey = cacheKey;
306
407
  generation += 1;
307
408
  } catch {
308
409
  // Keep the built-in catalog available while the key or network is absent.
309
410
  }
310
- })();
411
+ })().finally(() => {
412
+ refreshPromise = undefined;
413
+ });
311
414
  await refreshPromise;
312
415
  }
313
416
  return listModels(provider);
@@ -333,8 +436,11 @@ export function apply(ctx, config) {
333
436
  ctx.llm.registerModelDiscovery(NS, async (request) => {
334
437
  if (request.provider === PROVIDER) {
335
438
  const profile = profiles().get(PROVIDER);
336
- const apiKey = request.apiKey ?? await resolveApiKey(PROVIDER, profile);
337
- remoteModels = await fetchCodeBuddyModels(apiKey, request.signal);
439
+ const credential = request.apiKey
440
+ ? { value: request.apiKey, kind: "api-key", ref: API_KEY_ENV }
441
+ : await resolveCredential(PROVIDER, profile);
442
+ remoteModels = await fetchCodeBuddyModels(credential, request.signal);
443
+ remoteModelsKey = credential.kind === "bearer" ? `token:${credential.sessionId ?? "active"}` : `api:${credential.ref ?? API_KEY_ENV}`;
338
444
  generation += 1;
339
445
  return remoteModels.map((model) => ({
340
446
  id: model.id,