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.
- package/README.md +112 -98
- package/cli.js +78 -2
- package/client.js +547 -0
- package/codebuddy-auth.js +383 -0
- package/codebuddy-credits.js +441 -0
- package/codebuddy-web.js +420 -0
- package/index.js +123 -17
- package/package.json +14 -4
- /package/docs/{CodeBuddy → /345/217/215/345/220/221/344/273/243/347/220/206}/350/260/203/347/224/250WorkBuddy-API/345/274/200/345/217/221/346/226/207/346/241/243.md" +0 -0
package/codebuddy-web.js
ADDED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
|
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)
|
|
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
|
|
398
|
+
if (provider === PROVIDER) {
|
|
301
399
|
refreshPromise ??= (async () => {
|
|
302
400
|
try {
|
|
303
401
|
const profile = profiles().get(PROVIDER);
|
|
304
|
-
const
|
|
305
|
-
|
|
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
|
|
337
|
-
|
|
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,
|