dsh-llm-codebuddy 1.3.4 → 1.3.5
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 +73 -29
- package/cli.js +59 -2
- package/client.js +152 -0
- package/codebuddy-auth.js +186 -0
- package/codebuddy-web.js +98 -0
- package/index.js +96 -15
- package/package.json +13 -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/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# DSH CodeBuddy Provider
|
|
2
2
|
|
|
3
|
-
为 DeepSeek Harness(DSH)增加 `CodeBuddy 中国区` Provider
|
|
4
|
-
|
|
3
|
+
为 DeepSeek Harness(DSH)增加 `CodeBuddy 中国区` Provider。支持 WorkBuddy
|
|
4
|
+
API Key 和 CodeBuddy 中国站账号令牌两种认证方式,并可在 DSH WebUI 中获取、
|
|
5
|
+
选择和调整模型。
|
|
5
6
|
|
|
6
7
|
> [!IMPORTANT]
|
|
7
8
|
> API Key 由 **WorkBuddy** 提供,本插件使用该 Key 调用供 CodeBuddy 使用的模型服务。
|
|
@@ -10,8 +11,10 @@ DSH WebUI 中填写 API Key、获取模型、调整模型参数并使用 CodeBud
|
|
|
10
11
|
|
|
11
12
|
## 功能
|
|
12
13
|
|
|
13
|
-
- WebUI 中直接添加 `CodeBuddy 中国区`;
|
|
14
|
-
-
|
|
14
|
+
- WebUI 中直接添加 `CodeBuddy 中国区`;
|
|
15
|
+
- 支持从 WorkBuddy 获取的 API Key;
|
|
16
|
+
- API Key 输入框旁可显式切换 `API Key` / `令牌登录`;
|
|
17
|
+
- 支持从 WebUI 拉起浏览器登录 CodeBuddy 中国站,并自动复用、刷新登录令牌;
|
|
15
18
|
- 自动获取 CodeBuddy 当前可用模型;
|
|
16
19
|
- 支持编辑模型 ID、名称、上下文窗口和最大输出 Token;
|
|
17
20
|
- 支持添加、删除模型以及重新同步模型目录;
|
|
@@ -24,10 +27,10 @@ DSH WebUI 中填写 API Key、获取模型、调整模型参数并使用 CodeBud
|
|
|
24
27
|
|
|
25
28
|
- Windows、Linux 或 macOS;
|
|
26
29
|
- Node.js `>= 22.19.0`;
|
|
27
|
-
- 已安装 DSH;
|
|
28
|
-
- 已验证 DSH `0.1.0-rc.6`。
|
|
29
|
-
|
|
30
|
-
安装器已自带 DSH 所需的 `pnpm
|
|
30
|
+
- 已安装 DSH;
|
|
31
|
+
- 已验证 DSH `0.1.0-rc.6`。
|
|
32
|
+
|
|
33
|
+
安装器已自带 DSH 所需的 `pnpm`,无需全局安装;令牌登录也不需要安装 CodeBuddy CLI。
|
|
31
34
|
|
|
32
35
|
> DSH 仍处于预发布阶段。未来版本如果调整插件接口,本插件可能需要同步升级;
|
|
33
36
|
> DSH 普通更新不会覆盖本插件。
|
|
@@ -51,9 +54,35 @@ dsh plugin --profile headless add dsh-llm-codebuddy@latest
|
|
|
51
54
|
|
|
52
55
|
只使用 WebUI 时,仅执行第一条即可。
|
|
53
56
|
|
|
54
|
-
## WebUI 配置
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
## WebUI 配置
|
|
58
|
+
|
|
59
|
+
### CodeBuddy 账号令牌登录(新用户推荐)
|
|
60
|
+
|
|
61
|
+
1. 打开“设置 → 模型”。
|
|
62
|
+
2. 添加或编辑 `CodeBuddy 中国区`。
|
|
63
|
+
3. 点击 API Key 输入框右侧的“令牌登录”。
|
|
64
|
+
4. 在自动打开的浏览器中完成 CodeBuddy 中国站登录。
|
|
65
|
+
5. WebUI 显示“令牌已登录”后即可获取模型并保存自定义目录。
|
|
66
|
+
|
|
67
|
+
如果 WebUI 无法打开浏览器,也可以使用命令行入口:
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
npx --yes dsh-llm-codebuddy@latest login
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
插件会调用 CodeBuddy 官方认证流程。完成登录后,插件会自动把
|
|
74
|
+
`CodeBuddy 中国区` Provider 切换为令牌模式,无需 API Key。点击同一行的
|
|
75
|
+
“API Key”可以切回 API Key 模式;已经保存的 API Key 不会被删除。
|
|
76
|
+
两种凭据彼此独立保留,之后再次点击“令牌登录”会优先复用已保存令牌;仅在没有
|
|
77
|
+
令牌或主动重新登录时才需要再次打开浏览器。
|
|
78
|
+
|
|
79
|
+
插件直接调用 CodeBuddy 中国站官方网页登录和令牌刷新接口,不依赖本机
|
|
80
|
+
`codebuddy` 命令。访问令牌和刷新令牌保存在 DSH 凭据服务中,不会写入
|
|
81
|
+
`settings.yaml` 或模型目录。
|
|
82
|
+
|
|
83
|
+
### WorkBuddy API Key
|
|
84
|
+
|
|
85
|
+
1. 打开“设置 → 模型”。
|
|
57
86
|
2. 点击“添加提供方”。
|
|
58
87
|
3. 选择 `CodeBuddy 中国区`。
|
|
59
88
|
4. 输入从 WorkBuddy 获取的 API Key 并保存。
|
|
@@ -116,7 +145,7 @@ npx --yes dsh-llm-codebuddy@latest uninstall
|
|
|
116
145
|
2. 只删除 `llm-pi-ai.providers.codebuddy-cn` 配置;
|
|
117
146
|
3. 保留其他 Provider 和 DSH 设置;
|
|
118
147
|
4. 从 `web`、`headless` Profile 移除插件;
|
|
119
|
-
5. 保留 API Key
|
|
148
|
+
5. 保留 API Key 和登录令牌凭据,方便以后重新安装。
|
|
120
149
|
|
|
121
150
|
完成后重启 DSH。备份文件名类似:
|
|
122
151
|
|
|
@@ -141,28 +170,43 @@ dsh plugin --profile web list --depth 0
|
|
|
141
170
|
确认 API Key 来自 WorkBuddy 且仍然有效,然后重新输入 Key 并点击“获取可用模型”。
|
|
142
171
|
接口临时不可用时,插件仍会提供内置模型目录。
|
|
143
172
|
|
|
144
|
-
### 为什么别人能看到某个模型,我这里看不到
|
|
173
|
+
### 为什么别人能看到某个模型,我这里看不到
|
|
145
174
|
|
|
146
175
|
模型权限与 API Key 绑定。插件只导入 `/v3/config` 中 `agents[name=cli].models` 为
|
|
147
176
|
当前 Key 返回的模型;更换 Key 后请重新点击“获取可用模型”。插件不会强行显示
|
|
148
|
-
当前 Key 未授权的模型。
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
177
|
+
当前 Key 未授权的模型。
|
|
178
|
+
|
|
179
|
+
### 调用时报 `500 status code (no body)`
|
|
180
|
+
|
|
181
|
+
先更新到最新版并重启 DSH:
|
|
182
|
+
|
|
183
|
+
```powershell
|
|
184
|
+
npx --yes dsh-llm-codebuddy@latest install
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
旧版本可能被 DSH 覆盖 CodeBuddy 请求标识,导致服务端拒绝请求;最新版已在插件请求层
|
|
188
|
+
恢复 CodeBuddy 官方标识,同时适用于 API Key 和令牌模式。
|
|
189
|
+
|
|
190
|
+
### 卸载后仍显示旧页面
|
|
191
|
+
|
|
192
|
+
关闭正在运行的 DSH,再重新启动。已经运行的进程不会自动卸载内存中的插件。
|
|
193
|
+
|
|
194
|
+
## 开发文档
|
|
195
|
+
|
|
196
|
+
需要开发其他 Agent 或 Provider 时,请阅读
|
|
197
|
+
[反向代理调用 WorkBuddy API 开发文档](./docs/反向代理调用WorkBuddy-API开发文档.md)。
|
|
198
|
+
|
|
199
|
+
## 工作原理
|
|
160
200
|
|
|
161
201
|
```text
|
|
162
|
-
WorkBuddy 提供 API Key
|
|
163
|
-
↓
|
|
164
|
-
DSH Agent → 本插件 → CodeBuddy /v2/chat/completions
|
|
165
|
-
↘ CodeBuddy /v3/config(获取模型)
|
|
202
|
+
WorkBuddy 提供 API Key
|
|
203
|
+
↓
|
|
204
|
+
DSH Agent → 本插件 → CodeBuddy /v2/chat/completions
|
|
205
|
+
↘ CodeBuddy /v3/config(获取模型)
|
|
206
|
+
|
|
207
|
+
CodeBuddy 中国站网页登录 → 本插件保存并刷新 DSH 登录凭据
|
|
208
|
+
↓ 当前 access token
|
|
209
|
+
DSH Agent → 本插件 → CodeBuddy API(Authorization: Bearer)
|
|
166
210
|
```
|
|
167
211
|
|
|
168
212
|
- DSH:负责 Agent 循环、上下文、工具调用和权限;
|
package/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createRequire } from "node:module";
|
|
|
6
6
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { parseDocument } from "yaml";
|
|
9
|
+
import { CODEBUDDY_SESSION_REF, loginCodeBuddy, parseCodeBuddySession, serializeCodeBuddySession } from "./codebuddy-auth.js";
|
|
9
10
|
|
|
10
11
|
const PACKAGE = "dsh-llm-codebuddy";
|
|
11
12
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
|
|
@@ -110,6 +111,43 @@ function cleanSettings(file) {
|
|
|
110
111
|
return backup;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
function enableTokenLogin(home = dshHome()) {
|
|
115
|
+
const file = join(home, "settings.yaml");
|
|
116
|
+
const source = existsSync(file) ? readFileSync(file, "utf8") : "{}\n";
|
|
117
|
+
const document = parseDocument(source);
|
|
118
|
+
if (document.errors.length) throw new Error(`无法解析 ${file}:${document.errors[0].message}`);
|
|
119
|
+
if (document.hasIn(PROVIDER_PATH)) document.deleteIn([...PROVIDER_PATH, "apiKeyEnv"]);
|
|
120
|
+
else document.setIn(PROVIDER_PATH, {});
|
|
121
|
+
if (existsSync(file)) {
|
|
122
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
123
|
+
copyFileSync(file, `${file}.codebuddy-backup-${stamp}`);
|
|
124
|
+
}
|
|
125
|
+
writeYamlDocument(file, document);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function storeLoginSession(session, home = dshHome()) {
|
|
129
|
+
const file = join(home, ".credentials.yaml");
|
|
130
|
+
const document = parseDocument(existsSync(file) ? readFileSync(file, "utf8") : "{}\n");
|
|
131
|
+
if (document.errors.length) throw new Error(`无法解析 ${file}:${document.errors[0].message}`);
|
|
132
|
+
document.set(CODEBUDDY_SESSION_REF, serializeCodeBuddySession(session));
|
|
133
|
+
if (existsSync(file)) {
|
|
134
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
135
|
+
copyFileSync(file, `${file}.codebuddy-backup-${stamp}`);
|
|
136
|
+
}
|
|
137
|
+
writeYamlDocument(file, document);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function login() {
|
|
141
|
+
console.log("正在打开 CodeBuddy 中国站网页登录(无需安装 CodeBuddy CLI)……");
|
|
142
|
+
const session = await loginCodeBuddy((url, opened) => {
|
|
143
|
+
if (opened) console.log("浏览器登录页已打开,请在浏览器中完成登录。");
|
|
144
|
+
else console.log(`无法自动打开浏览器,请手动访问:${url}`);
|
|
145
|
+
});
|
|
146
|
+
storeLoginSession(session);
|
|
147
|
+
enableTokenLogin();
|
|
148
|
+
console.log("CodeBuddy 令牌登录成功,Provider 已切换为令牌模式。请重启 DSH。");
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
function install() {
|
|
114
152
|
for (const profile of ["web", "headless"]) {
|
|
115
153
|
runDsh(["plugin", "--profile", profile, "list", "--depth", "0"]);
|
|
@@ -129,7 +167,7 @@ function uninstall(home = dshHome()) {
|
|
|
129
167
|
cleanPnpmWorkspace(workspace);
|
|
130
168
|
}
|
|
131
169
|
console.log(backup ? `CodeBuddy 配置已清理,备份:${backup}` : "未发现 CodeBuddy Provider 配置。");
|
|
132
|
-
console.log("插件已卸载,API Key
|
|
170
|
+
console.log("插件已卸载,API Key 和登录令牌凭据保持不变。请重启 DSH。");
|
|
133
171
|
}
|
|
134
172
|
|
|
135
173
|
function selfTest() {
|
|
@@ -137,11 +175,29 @@ function selfTest() {
|
|
|
137
175
|
try {
|
|
138
176
|
const file = join(root, "settings.yaml");
|
|
139
177
|
writeFileSync(file, "llm-pi-ai:\n providers:\n opencode-go:\n apiKeyEnv: OPENCODE_GO_API_KEY\n codebuddy-cn:\n apiKeyEnv: CODEBUDDY_CN_API_KEY\n", "utf8");
|
|
178
|
+
enableTokenLogin(root);
|
|
179
|
+
const tokenMode = parseDocument(readFileSync(file, "utf8"));
|
|
180
|
+
if (tokenMode.hasIn([...PROVIDER_PATH, "apiKeyEnv"]) || !tokenMode.hasIn(PROVIDER_PATH)) {
|
|
181
|
+
throw new Error("token login settings self-test failed");
|
|
182
|
+
}
|
|
140
183
|
const backup = cleanSettings(file);
|
|
141
184
|
const result = parseDocument(readFileSync(file, "utf8"));
|
|
142
185
|
if (!backup || !existsSync(backup) || result.hasIn(PROVIDER_PATH) || !result.hasIn(["llm-pi-ai", "providers", "opencode-go"])) {
|
|
143
186
|
throw new Error("uninstall settings cleanup self-test failed");
|
|
144
187
|
}
|
|
188
|
+
enableTokenLogin(root);
|
|
189
|
+
if (!parseDocument(readFileSync(file, "utf8")).hasIn(PROVIDER_PATH)) {
|
|
190
|
+
throw new Error("token login provider creation self-test failed");
|
|
191
|
+
}
|
|
192
|
+
const sampleSession = {
|
|
193
|
+
auth: { accessToken: "test-access-token", refreshToken: "test-refresh-token", expiresAt: Date.now() + 60_000 },
|
|
194
|
+
account: { userId: "test-user" },
|
|
195
|
+
};
|
|
196
|
+
storeLoginSession(sampleSession, root);
|
|
197
|
+
const storedSession = parseDocument(readFileSync(join(root, ".credentials.yaml"), "utf8")).get(CODEBUDDY_SESSION_REF);
|
|
198
|
+
if (parseCodeBuddySession(storedSession).account.userId !== "test-user") {
|
|
199
|
+
throw new Error("token credential storage self-test failed");
|
|
200
|
+
}
|
|
145
201
|
const workspace = join(root, "pnpm-workspace.yaml");
|
|
146
202
|
writeFileSync(workspace, "packages:\n - .\nallowBuilds:\n '@google/genai': true\n protobufjs: pending\n", "utf8");
|
|
147
203
|
withPnpmBuildPolicy(workspace, () => {
|
|
@@ -172,8 +228,9 @@ function selfTest() {
|
|
|
172
228
|
const command = process.argv[2];
|
|
173
229
|
if (command === "install") install();
|
|
174
230
|
else if (command === "uninstall") uninstall();
|
|
231
|
+
else if (command === "login") await login();
|
|
175
232
|
else if (command === "--self-test") selfTest();
|
|
176
233
|
else {
|
|
177
|
-
console.log("用法:dsh-llm-codebuddy <install|uninstall>");
|
|
234
|
+
console.log("用法:dsh-llm-codebuddy <install|login|uninstall>");
|
|
178
235
|
process.exitCode = 1;
|
|
179
236
|
}
|
package/client.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-llm-codebuddy",
|
|
3
|
+
factory: () => {
|
|
4
|
+
const ROUTE = "/dsh-llm-codebuddy/auth";
|
|
5
|
+
const MARKER = "data-codebuddy-auth-switch";
|
|
6
|
+
|
|
7
|
+
function button(text) {
|
|
8
|
+
const element = document.createElement("button");
|
|
9
|
+
element.type = "button";
|
|
10
|
+
element.textContent = text;
|
|
11
|
+
Object.assign(element.style, {
|
|
12
|
+
minHeight: "44px",
|
|
13
|
+
padding: "0 12px",
|
|
14
|
+
border: "1px solid var(--dsw-border-subtle, #d0d5dd)",
|
|
15
|
+
borderRadius: "8px",
|
|
16
|
+
background: "var(--dsw-surface-subtle, transparent)",
|
|
17
|
+
color: "inherit",
|
|
18
|
+
cursor: "pointer",
|
|
19
|
+
whiteSpace: "nowrap",
|
|
20
|
+
});
|
|
21
|
+
return element;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isCodeBuddy(input) {
|
|
25
|
+
const editor = input.parentElement?.parentElement;
|
|
26
|
+
if (!editor) return false;
|
|
27
|
+
if (editor.textContent?.includes("codebuddy-cn")) return true;
|
|
28
|
+
return editor.parentElement?.querySelector('select[aria-label="提供方"]')?.value === "codebuddy-cn";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function applyMode(input, keyButton, tokenButton, message, status) {
|
|
32
|
+
const token = status.mode === "token";
|
|
33
|
+
input.disabled = token;
|
|
34
|
+
input.placeholder = token ? "当前使用 CodeBuddy 账号令牌" : input.dataset.codebuddyPlaceholder || "输入 API 密钥";
|
|
35
|
+
keyButton.setAttribute("aria-pressed", String(!token));
|
|
36
|
+
tokenButton.setAttribute("aria-pressed", String(token));
|
|
37
|
+
keyButton.style.background = !token ? "var(--dsw-accent-subtle, #eef4ff)" : "var(--dsw-surface-subtle, transparent)";
|
|
38
|
+
tokenButton.style.background = token ? "var(--dsw-accent-subtle, #eef4ff)" : "var(--dsw-surface-subtle, transparent)";
|
|
39
|
+
message.textContent = token ? status.authenticated ? "令牌已登录" : "令牌缺失,请重新登录" : "";
|
|
40
|
+
message.style.color = token && !status.authenticated ? "var(--dsw-text-danger, #c62828)" : "var(--dsw-text-success, #2e7d32)";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function request(path) {
|
|
44
|
+
const response = await fetch(`${ROUTE}/${path}`, { method: "POST" });
|
|
45
|
+
const body = await response.json();
|
|
46
|
+
if (!response.ok || !body.ok) {
|
|
47
|
+
const error = new Error(body.message || `请求失败(${response.status})`);
|
|
48
|
+
error.status = response.status;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return body;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mount(input) {
|
|
55
|
+
const field = input.parentElement;
|
|
56
|
+
if (!field || field.querySelector(`[${MARKER}]`)) return;
|
|
57
|
+
field.setAttribute("data-codebuddy-auth-field", "");
|
|
58
|
+
input.dataset.codebuddyPlaceholder = input.placeholder;
|
|
59
|
+
const controls = document.createElement("div");
|
|
60
|
+
controls.setAttribute(MARKER, "");
|
|
61
|
+
controls.setAttribute("role", "group");
|
|
62
|
+
controls.setAttribute("aria-label", "CodeBuddy 认证方式");
|
|
63
|
+
Object.assign(controls.style, { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" });
|
|
64
|
+
const keyButton = button("API Key");
|
|
65
|
+
const tokenButton = button("令牌登录");
|
|
66
|
+
const message = document.createElement("span");
|
|
67
|
+
message.setAttribute("role", "status");
|
|
68
|
+
message.setAttribute("aria-live", "polite");
|
|
69
|
+
Object.assign(message.style, { fontSize: "12px", minHeight: "18px" });
|
|
70
|
+
controls.append(keyButton, tokenButton, message);
|
|
71
|
+
field.append(controls);
|
|
72
|
+
let current = { mode: "api-key", authenticated: false };
|
|
73
|
+
const render = (status) => {
|
|
74
|
+
current = { ...current, ...status };
|
|
75
|
+
applyMode(input, keyButton, tokenButton, message, current);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const setBusy = (busy) => {
|
|
79
|
+
keyButton.disabled = busy;
|
|
80
|
+
tokenButton.disabled = busy;
|
|
81
|
+
keyButton.style.cursor = busy ? "progress" : "pointer";
|
|
82
|
+
tokenButton.style.cursor = busy ? "progress" : "pointer";
|
|
83
|
+
};
|
|
84
|
+
keyButton.addEventListener("click", async () => {
|
|
85
|
+
setBusy(true);
|
|
86
|
+
message.textContent = "正在切换…";
|
|
87
|
+
try {
|
|
88
|
+
render(await request("api-key"));
|
|
89
|
+
input.focus();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
message.textContent = error instanceof Error ? error.message : "切换失败";
|
|
92
|
+
message.style.color = "var(--dsw-text-danger, #c62828)";
|
|
93
|
+
} finally {
|
|
94
|
+
setBusy(false);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
tokenButton.addEventListener("click", async () => {
|
|
98
|
+
setBusy(true);
|
|
99
|
+
tokenButton.textContent = "等待浏览器登录…";
|
|
100
|
+
message.textContent = "请在浏览器中完成 CodeBuddy 中国站登录";
|
|
101
|
+
try {
|
|
102
|
+
const path = current.mode === "api-key" && current.authenticated ? "token" : "login";
|
|
103
|
+
render(await request(path));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
message.textContent = error instanceof Error ? error.message : "登录失败";
|
|
106
|
+
message.style.color = "var(--dsw-text-danger, #c62828)";
|
|
107
|
+
} finally {
|
|
108
|
+
tokenButton.textContent = "令牌登录";
|
|
109
|
+
setBusy(false);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
fetch(`${ROUTE}/status`, { cache: "no-store" })
|
|
113
|
+
.then((response) => response.json())
|
|
114
|
+
.then(render)
|
|
115
|
+
.catch(() => {
|
|
116
|
+
message.textContent = "认证状态读取失败";
|
|
117
|
+
message.style.color = "var(--dsw-text-danger, #c62828)";
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function enhance() {
|
|
122
|
+
for (const input of document.querySelectorAll('input[aria-label="API 密钥"]')) {
|
|
123
|
+
if (isCodeBuddy(input)) mount(input);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function apply(ctx) {
|
|
128
|
+
ctx.effect(() => {
|
|
129
|
+
const style = document.createElement("style");
|
|
130
|
+
style.textContent = `
|
|
131
|
+
[data-codebuddy-auth-field] { display: grid !important; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
|
|
132
|
+
[data-codebuddy-auth-field] > span:first-child { grid-column: 1 / -1; }
|
|
133
|
+
[data-codebuddy-auth-field] > input[aria-label="API 密钥"] { grid-column: 1; min-width: 0; }
|
|
134
|
+
[data-codebuddy-auth-field] > [data-codebuddy-auth-switch] { grid-column: 2; }
|
|
135
|
+
@media (max-width: 640px) { [data-codebuddy-auth-field] > [data-codebuddy-auth-switch] { grid-column: 1 / -1; } }
|
|
136
|
+
`;
|
|
137
|
+
document.head.append(style);
|
|
138
|
+
const observer = new MutationObserver(enhance);
|
|
139
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
140
|
+
document.addEventListener("change", enhance, true);
|
|
141
|
+
enhance();
|
|
142
|
+
return () => {
|
|
143
|
+
style.remove();
|
|
144
|
+
observer.disconnect();
|
|
145
|
+
document.removeEventListener("change", enhance, true);
|
|
146
|
+
};
|
|
147
|
+
}, "llm-codebuddy: auth switch");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { name: "dsh-llm-codebuddy-client", inject: [], apply };
|
|
151
|
+
},
|
|
152
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
|
|
4
|
+
export const CODEBUDDY_SESSION_REF = "CODEBUDDY_LOGIN_SESSION";
|
|
5
|
+
|
|
6
|
+
const BASE_URL = "https://copilot.tencent.com/v2/plugin";
|
|
7
|
+
const USER_AGENT = "CLI/unknown CodeBuddy/2.137.1";
|
|
8
|
+
const REQUEST_HEADERS = {
|
|
9
|
+
accept: "application/json",
|
|
10
|
+
"content-type": "application/json",
|
|
11
|
+
"user-agent": USER_AGENT,
|
|
12
|
+
"x-product": "SaaS",
|
|
13
|
+
};
|
|
14
|
+
const NO_ACCOUNT_HEADERS = {
|
|
15
|
+
"X-No-Authorization": "true",
|
|
16
|
+
"X-No-User-Id": "true",
|
|
17
|
+
"X-No-Enterprise-Id": "true",
|
|
18
|
+
"X-No-Department-Info": "true",
|
|
19
|
+
};
|
|
20
|
+
const NO_ID_HEADERS = {
|
|
21
|
+
"X-No-User-Id": "true",
|
|
22
|
+
"X-No-Enterprise-Id": "true",
|
|
23
|
+
"X-No-Department-Info": "true",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function calculateExpiresAt(auth, now = Date.now()) {
|
|
27
|
+
const result = { ...auth };
|
|
28
|
+
if (!result.expiresAt && Number.isFinite(result.expiresIn)) result.expiresAt = now + result.expiresIn * 1000;
|
|
29
|
+
if (!result.refreshExpiresAt && Number.isFinite(result.refreshExpiresIn)) result.refreshExpiresAt = now + result.refreshExpiresIn * 1000;
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function responseBody(response, action) {
|
|
34
|
+
try {
|
|
35
|
+
return await response.json();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new Error(`${action}返回了无法解析的数据`, { cause: error });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function request(path, options, action) {
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetch(`${BASE_URL}${path}`, options);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (options.signal?.aborted) throw new Error(`${action}已取消`, { cause: error });
|
|
47
|
+
throw new Error(`${action}无法连接 CodeBuddy 中国站`, { cause: error });
|
|
48
|
+
}
|
|
49
|
+
const body = await responseBody(response, action);
|
|
50
|
+
if (!response.ok || body?.code !== 0) throw new Error(`${action}失败(${body?.message ?? body?.msg ?? response.status})`);
|
|
51
|
+
return body.data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function enterpriseHeaders(session) {
|
|
55
|
+
const enterpriseId = session.account?.enterpriseId;
|
|
56
|
+
return {
|
|
57
|
+
...(enterpriseId ? { "X-Enterprise-Id": enterpriseId, "X-Tenant-Id": enterpriseId } : {}),
|
|
58
|
+
...(session.auth?.domain ? { "X-Domain": session.auth.domain } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function poll(path, headers, action, timeoutMs, signal) {
|
|
63
|
+
const deadline = Date.now() + timeoutMs;
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
await delay(1000, undefined, { signal });
|
|
66
|
+
let response;
|
|
67
|
+
try {
|
|
68
|
+
response = await fetch(`${BASE_URL}${path}`, { headers: { ...REQUEST_HEADERS, ...headers }, signal });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (signal?.aborted) throw new Error(`${action}已取消`, { cause: error });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const body = await responseBody(response, action);
|
|
74
|
+
if (response.ok && body?.code === 0 && body.data) return body.data;
|
|
75
|
+
if (response.status === 401 || response.status === 403) throw new Error(`${action}失败(${body?.message ?? body?.msg ?? response.status})`);
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`${action}超时`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function openBrowser(url) {
|
|
81
|
+
const [command, args] = process.platform === "win32"
|
|
82
|
+
? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]]
|
|
83
|
+
: process.platform === "darwin"
|
|
84
|
+
? ["open", [url]]
|
|
85
|
+
: ["xdg-open", [url]];
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
88
|
+
child.once("spawn", () => {
|
|
89
|
+
child.unref();
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
child.once("error", reject);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeAccount(account) {
|
|
97
|
+
return {
|
|
98
|
+
...(account?.userId || account?.uid ? { userId: account.userId ?? account.uid } : {}),
|
|
99
|
+
...(account?.enterpriseId || account?.tenantId ? { enterpriseId: account.enterpriseId ?? account.tenantId } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function loginCodeBuddy(onAuthUrl, signal) {
|
|
104
|
+
const state = await request("/auth/state?platform=CLI", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: { ...REQUEST_HEADERS, ...NO_ACCOUNT_HEADERS },
|
|
107
|
+
body: "{}",
|
|
108
|
+
signal,
|
|
109
|
+
}, "创建 CodeBuddy 登录会话");
|
|
110
|
+
if (!state?.state || !state?.authUrl) throw new Error("CodeBuddy 登录接口没有返回登录地址");
|
|
111
|
+
try {
|
|
112
|
+
await openBrowser(state.authUrl);
|
|
113
|
+
onAuthUrl?.(state.authUrl, true);
|
|
114
|
+
} catch {
|
|
115
|
+
onAuthUrl?.(state.authUrl, false);
|
|
116
|
+
}
|
|
117
|
+
const auth = calculateExpiresAt(await poll(
|
|
118
|
+
`/auth/token?state=${encodeURIComponent(state.state)}`,
|
|
119
|
+
NO_ACCOUNT_HEADERS,
|
|
120
|
+
"等待 CodeBuddy 登录",
|
|
121
|
+
10 * 60_000,
|
|
122
|
+
signal,
|
|
123
|
+
));
|
|
124
|
+
if (!auth.accessToken || !auth.refreshToken) throw new Error("CodeBuddy 登录接口没有返回完整令牌");
|
|
125
|
+
const account = await poll(
|
|
126
|
+
`/login/account?state=${encodeURIComponent(state.state)}`,
|
|
127
|
+
{ ...enterpriseHeaders({ auth }), authorization: `Bearer ${auth.accessToken}`, ...NO_ID_HEADERS },
|
|
128
|
+
"获取 CodeBuddy 账号",
|
|
129
|
+
60_000,
|
|
130
|
+
signal,
|
|
131
|
+
);
|
|
132
|
+
return { auth, account: normalizeAccount(account) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function refreshCodeBuddySession(session, signal) {
|
|
136
|
+
if (!session?.auth?.refreshToken) throw new Error("CodeBuddy 登录会话缺少刷新令牌,请重新登录");
|
|
137
|
+
const auth = await request("/auth/token/refresh", {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
...REQUEST_HEADERS,
|
|
141
|
+
...enterpriseHeaders(session),
|
|
142
|
+
"X-Refresh-Token": session.auth.refreshToken,
|
|
143
|
+
"X-Auth-Refresh-Source": "plugin",
|
|
144
|
+
},
|
|
145
|
+
body: "{}",
|
|
146
|
+
signal,
|
|
147
|
+
}, "刷新 CodeBuddy 登录令牌");
|
|
148
|
+
const fresh = calculateExpiresAt(auth);
|
|
149
|
+
const merged = { ...session.auth, ...fresh, refreshToken: fresh?.refreshToken ?? session.auth.refreshToken };
|
|
150
|
+
if (!merged.accessToken) throw new Error("CodeBuddy 刷新接口没有返回访问令牌");
|
|
151
|
+
return { auth: merged, account: normalizeAccount(session.account) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function serializeCodeBuddySession(session) {
|
|
155
|
+
if (!session?.auth?.accessToken || !session?.auth?.refreshToken) throw new Error("CodeBuddy 登录会话无效");
|
|
156
|
+
return JSON.stringify({ auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account) });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function parseCodeBuddySession(value) {
|
|
160
|
+
let session;
|
|
161
|
+
try {
|
|
162
|
+
session = JSON.parse(value);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new Error("CodeBuddy 登录凭据已损坏,请重新登录", { cause: error });
|
|
165
|
+
}
|
|
166
|
+
if (!session?.auth?.accessToken || !session?.auth?.refreshToken) throw new Error("CodeBuddy 登录凭据不完整,请重新登录");
|
|
167
|
+
return { auth: calculateExpiresAt(session.auth), account: normalizeAccount(session.account) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function sessionNeedsRefresh(session, now = Date.now()) {
|
|
171
|
+
const expiresAt = Number(session?.auth?.expiresAt);
|
|
172
|
+
if (Number.isFinite(expiresAt)) return expiresAt <= now + 2 * 60_000;
|
|
173
|
+
try {
|
|
174
|
+
const payload = JSON.parse(Buffer.from(session.auth.accessToken.split(".")[1], "base64url").toString("utf8"));
|
|
175
|
+
return Number.isFinite(payload.exp) ? payload.exp * 1000 <= now + 2 * 60_000 : true;
|
|
176
|
+
} catch {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function sessionCacheDeadline(session, now = Date.now()) {
|
|
182
|
+
const expiresAt = Number(session?.auth?.expiresAt);
|
|
183
|
+
return Number.isFinite(expiresAt)
|
|
184
|
+
? Math.max(now, Math.min(expiresAt - 2 * 60_000, now + 30 * 60_000))
|
|
185
|
+
: now + 5 * 60_000;
|
|
186
|
+
}
|
package/codebuddy-web.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
2
|
+
import { CODEBUDDY_SESSION_REF, loginCodeBuddy, serializeCodeBuddySession } from "./codebuddy-auth.js";
|
|
3
|
+
|
|
4
|
+
const PROVIDER = "codebuddy-cn";
|
|
5
|
+
const API_KEY_ENV = "CODEBUDDY_API_KEY";
|
|
6
|
+
const ROUTE = "/dsh-llm-codebuddy/auth";
|
|
7
|
+
|
|
8
|
+
export function authenticationMode(config) {
|
|
9
|
+
const profile = config?.providers?.[PROVIDER];
|
|
10
|
+
return profile && profile.apiKeyEnv === undefined ? "token" : "api-key";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function json(res, status, body) {
|
|
14
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
15
|
+
res.end(JSON.stringify(body));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function localPost(req) {
|
|
19
|
+
const address = req.socket.remoteAddress;
|
|
20
|
+
const loopback = address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
21
|
+
if (!loopback) return false;
|
|
22
|
+
const origin = req.headers.origin;
|
|
23
|
+
if (!origin) return req.headers["sec-fetch-site"] === "same-origin";
|
|
24
|
+
try {
|
|
25
|
+
return ["127.0.0.1", "localhost", "[::1]"].includes(new URL(origin).hostname);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function setMode(settings, mode) {
|
|
32
|
+
const config = settings.get("llm-pi-ai");
|
|
33
|
+
const exists = Object.hasOwn(config?.providers ?? {}, PROVIDER);
|
|
34
|
+
const path = ["providers", PROVIDER];
|
|
35
|
+
if (!exists) {
|
|
36
|
+
await settings.mutate("llm-pi-ai", [{ op: "set", path, value: mode === "token" ? {} : { apiKeyEnv: API_KEY_ENV } }]);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await settings.mutate("llm-pi-ai", [{
|
|
40
|
+
op: mode === "token" ? "unset" : "set",
|
|
41
|
+
path: [...path, "apiKeyEnv"],
|
|
42
|
+
...(mode === "api-key" ? { value: API_KEY_ENV } : {}),
|
|
43
|
+
}]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function installCodeBuddyWeb(ctx) {
|
|
47
|
+
ctx.inject(["webServer", "settings", "credentials"], (webCtx) => {
|
|
48
|
+
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
|
+
});
|
|
54
|
+
const status = async (_req, res) => {
|
|
55
|
+
json(res, 200, await currentState());
|
|
56
|
+
};
|
|
57
|
+
const apiKey = async (req, res) => {
|
|
58
|
+
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
59
|
+
if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面切换认证方式" });
|
|
60
|
+
await setMode(webCtx.settings, "api-key");
|
|
61
|
+
json(res, 200, await currentState());
|
|
62
|
+
};
|
|
63
|
+
const token = async (req, res) => {
|
|
64
|
+
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
65
|
+
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());
|
|
70
|
+
};
|
|
71
|
+
const login = async (req, res) => {
|
|
72
|
+
if (req.method !== "POST") return json(res, 405, { ok: false, message: "Method not allowed" });
|
|
73
|
+
if (!localPost(req)) return json(res, 403, { ok: false, message: "只允许从本机 DSH 页面登录" });
|
|
74
|
+
try {
|
|
75
|
+
loginPromise ??= (async () => {
|
|
76
|
+
const session = await loginCodeBuddy();
|
|
77
|
+
await webCtx.credentials.set(credentialRef(CODEBUDDY_SESSION_REF), serializeCodeBuddySession(session));
|
|
78
|
+
await setMode(webCtx.settings, "token");
|
|
79
|
+
})().finally(() => {
|
|
80
|
+
loginPromise = undefined;
|
|
81
|
+
});
|
|
82
|
+
await loginPromise;
|
|
83
|
+
json(res, 200, { ok: true, mode: "token", authenticated: true });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
json(res, 500, { ok: false, message: error instanceof Error ? error.message : "CodeBuddy 登录失败" });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
webCtx.effect(() => {
|
|
89
|
+
const dispose = [
|
|
90
|
+
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/status`, handler: status }),
|
|
91
|
+
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/api-key`, handler: apiKey }),
|
|
92
|
+
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/token`, handler: token }),
|
|
93
|
+
webCtx.webServer.register({ kind: "exact", path: `${ROUTE}/login`, handler: login }),
|
|
94
|
+
];
|
|
95
|
+
return () => dispose.forEach((fn) => fn());
|
|
96
|
+
}, "llm-codebuddy: web login routes");
|
|
97
|
+
});
|
|
98
|
+
}
|
package/index.js
CHANGED
|
@@ -6,6 +6,15 @@ 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
|
+
parseCodeBuddySession,
|
|
12
|
+
refreshCodeBuddySession,
|
|
13
|
+
serializeCodeBuddySession,
|
|
14
|
+
sessionCacheDeadline,
|
|
15
|
+
sessionNeedsRefresh,
|
|
16
|
+
} from "./codebuddy-auth.js";
|
|
17
|
+
import { installCodeBuddyWeb } from "./codebuddy-web.js";
|
|
9
18
|
|
|
10
19
|
export { Config };
|
|
11
20
|
|
|
@@ -18,7 +27,7 @@ const DISPLAY_NAME = "CodeBuddy 中国区";
|
|
|
18
27
|
const API_KEY_ENV = "CODEBUDDY_API_KEY";
|
|
19
28
|
const BASE_URL = "https://copilot.tencent.com/v2";
|
|
20
29
|
const CONFIG_URL = "https://copilot.tencent.com/v3/config";
|
|
21
|
-
const USER_AGENT = "CLI/unknown CodeBuddy/2.
|
|
30
|
+
const USER_AGENT = "CLI/unknown CodeBuddy/2.137.1";
|
|
22
31
|
const STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
23
32
|
const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
24
33
|
const EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
@@ -31,6 +40,16 @@ const COMPAT = {
|
|
|
31
40
|
thinkingFormat: "openai",
|
|
32
41
|
};
|
|
33
42
|
|
|
43
|
+
function codeBuddyRequestOptions(options) {
|
|
44
|
+
return { ...options, headers: { ...(options?.headers ?? {}), "user-agent": USER_AGENT } };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const codeBuddyApi = {
|
|
48
|
+
...openAICompletionsApi,
|
|
49
|
+
stream: (model, context, options) => openAICompletionsApi.stream(model, context, codeBuddyRequestOptions(options)),
|
|
50
|
+
streamSimple: (model, context, options) => openAICompletionsApi.streamSimple(model, context, codeBuddyRequestOptions(options)),
|
|
51
|
+
};
|
|
52
|
+
|
|
34
53
|
const FALLBACK_MODELS = [
|
|
35
54
|
["hy3", "Hy3", 192000, 64000, true],
|
|
36
55
|
["glm-5.2", "GLM-5.2", 1000000, 48000, false],
|
|
@@ -133,13 +152,18 @@ function modelsFromConfig(data) {
|
|
|
133
152
|
});
|
|
134
153
|
}
|
|
135
154
|
|
|
136
|
-
|
|
155
|
+
function authenticationHeaders(credential) {
|
|
156
|
+
const value = assertUsableApiKey(credential.value, name, credential.ref ?? API_KEY_ENV);
|
|
157
|
+
return credential.kind === "bearer" ? { authorization: `Bearer ${value}` } : { "x-api-key": value };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function fetchCodeBuddyModels(credential, signal) {
|
|
137
161
|
let response;
|
|
138
162
|
try {
|
|
139
163
|
response = await fetch(CONFIG_URL, {
|
|
140
164
|
headers: {
|
|
141
165
|
accept: "application/json",
|
|
142
|
-
|
|
166
|
+
...authenticationHeaders(credential),
|
|
143
167
|
"user-agent": USER_AGENT,
|
|
144
168
|
"x-product": "SaaS",
|
|
145
169
|
},
|
|
@@ -164,7 +188,7 @@ function codeBuddyProvider(models, auth) {
|
|
|
164
188
|
baseUrl: BASE_URL,
|
|
165
189
|
auth,
|
|
166
190
|
models,
|
|
167
|
-
api:
|
|
191
|
+
api: codeBuddyApi,
|
|
168
192
|
});
|
|
169
193
|
}
|
|
170
194
|
|
|
@@ -172,6 +196,7 @@ function resolvedProfile(provider, source, piProvider, configuredMaxTokens = new
|
|
|
172
196
|
const apiKeyEnv = source.apiKeyEnv === undefined ? undefined : credentialRef(source.apiKeyEnv);
|
|
173
197
|
return {
|
|
174
198
|
...source,
|
|
199
|
+
headers: runtimeHeaders(source.headers),
|
|
175
200
|
provider,
|
|
176
201
|
displayName: source.displayName ?? piProvider.name ?? provider,
|
|
177
202
|
...(apiKeyEnv === undefined ? {} : { apiKeyEnv }),
|
|
@@ -216,15 +241,30 @@ function selectCodeBuddyModels(base, entries) {
|
|
|
216
241
|
});
|
|
217
242
|
}
|
|
218
243
|
|
|
219
|
-
|
|
244
|
+
function ownsProvider(provider, builtins) {
|
|
245
|
+
return provider === PROVIDER || builtins.has(provider);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runtimeHeaders(headers) {
|
|
249
|
+
return { ...(headers ?? {}) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function codeBuddySource(config, source) {
|
|
253
|
+
return Object.hasOwn(config?.providers ?? {}, PROVIDER) ? source : { ...source, apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export const __testing = Object.freeze({ authenticationHeaders, codeBuddyRequestOptions, codeBuddySource, modelsFromConfig, ownsProvider, runtimeHeaders, selectCodeBuddyModels });
|
|
220
257
|
|
|
221
258
|
export function apply(ctx, config) {
|
|
259
|
+
installCodeBuddyWeb(ctx);
|
|
222
260
|
let current = () => config;
|
|
223
261
|
let remoteModels;
|
|
224
262
|
let generation = 0;
|
|
225
263
|
let memoRaw;
|
|
226
264
|
let memoGeneration = -1;
|
|
227
265
|
let memoized;
|
|
266
|
+
let loginSession;
|
|
267
|
+
let loginSessionPromise;
|
|
228
268
|
const builtins = new Map(builtinProviders().map((provider) => [provider.id, provider]));
|
|
229
269
|
const apiKeyAuth = builtins.get("deepseek")?.auth;
|
|
230
270
|
if (!apiKeyAuth) throw new Error(`${name}: pi-ai DeepSeek auth helper is unavailable`);
|
|
@@ -245,20 +285,20 @@ export function apply(ctx, config) {
|
|
|
245
285
|
if (memoRaw === current() && memoGeneration === generation && memoized) return memoized;
|
|
246
286
|
const result = new Map();
|
|
247
287
|
for (const [provider, source] of Object.entries(raw.providers)) {
|
|
288
|
+
if (!ownsProvider(provider, builtins)) continue;
|
|
248
289
|
if (provider === PROVIDER) {
|
|
290
|
+
const sourceWithAuth = codeBuddySource(current(), source);
|
|
249
291
|
const models = selectCodeBuddyModels(remoteModels ?? FALLBACK_MODELS, source.models);
|
|
250
292
|
const configured = new Map((source.models ?? []).flatMap((model) =>
|
|
251
293
|
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
|
|
252
294
|
));
|
|
253
295
|
result.set(provider, resolvedProfile(provider, {
|
|
254
|
-
...
|
|
255
|
-
apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV,
|
|
296
|
+
...sourceWithAuth,
|
|
256
297
|
displayName: DISPLAY_NAME,
|
|
257
298
|
}, codeBuddyProvider(models, apiKeyAuth), configured));
|
|
258
299
|
continue;
|
|
259
300
|
}
|
|
260
301
|
const base = builtins.get(provider);
|
|
261
|
-
if (!base) throw new Error(`${name}: 不支持非内置 Provider "${provider}"`);
|
|
262
302
|
const selected = selectBuiltinModels(base, source.models);
|
|
263
303
|
const configured = new Map((source.models ?? []).flatMap((model) =>
|
|
264
304
|
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
|
|
@@ -271,15 +311,54 @@ export function apply(ctx, config) {
|
|
|
271
311
|
return result;
|
|
272
312
|
};
|
|
273
313
|
|
|
274
|
-
const
|
|
314
|
+
const resolveLoginSession = async () => {
|
|
315
|
+
if (loginSession?.expiresAt > Date.now()) return loginSession;
|
|
316
|
+
loginSessionPromise ??= (async () => {
|
|
317
|
+
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
|
+
if (sessionNeedsRefresh(session)) {
|
|
324
|
+
session = await refreshCodeBuddySession(session);
|
|
325
|
+
await credentials?.set(ref, serializeCodeBuddySession(session));
|
|
326
|
+
}
|
|
327
|
+
return { ...session, expiresAt: sessionCacheDeadline(session) };
|
|
328
|
+
})().finally(() => {
|
|
329
|
+
loginSessionPromise = undefined;
|
|
330
|
+
});
|
|
331
|
+
loginSession = await loginSessionPromise;
|
|
332
|
+
return loginSession;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const resolveCredential = async (provider, profile) => {
|
|
275
336
|
const ref = profile.apiKeyEnv;
|
|
276
|
-
if (!ref)
|
|
337
|
+
if (!ref && provider === PROVIDER) {
|
|
338
|
+
let session;
|
|
339
|
+
try {
|
|
340
|
+
session = await resolveLoginSession();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
throw new LlmError(`${name}: 未找到可用的 CodeBuddy 登录令牌,请运行 dsh-llm-codebuddy login`, "MISSING_CREDENTIAL", { cause: error });
|
|
343
|
+
}
|
|
344
|
+
profile.headers ??= {};
|
|
345
|
+
if (session.account.userId) profile.headers["X-User-Id"] = session.account.userId;
|
|
346
|
+
if (session.account.enterpriseId) {
|
|
347
|
+
profile.headers["X-Enterprise-Id"] = session.account.enterpriseId;
|
|
348
|
+
profile.headers["X-Tenant-Id"] = session.account.enterpriseId;
|
|
349
|
+
}
|
|
350
|
+
if (session.auth.domain) profile.headers["X-Domain"] = session.auth.domain;
|
|
351
|
+
return { value: assertUsableApiKey(session.auth.accessToken, name, "CodeBuddy login session"), kind: "bearer" };
|
|
352
|
+
}
|
|
353
|
+
if (!ref) return { value: undefined, kind: "none" };
|
|
277
354
|
const stored = await ctx.get("credentials")?.resolve(ref);
|
|
278
355
|
const value = stored?.value ?? launchEnvironmentOf(ctx).get(ref)?.value;
|
|
279
|
-
if (value) return assertUsableApiKey(value, name, ref);
|
|
356
|
+
if (value) return { value: assertUsableApiKey(value, name, ref), kind: "api-key", ref };
|
|
280
357
|
throw new LlmError(`${name}: Provider "${provider}" 缺少 API Key,请在 WebUI 的模型设置中填写`, "MISSING_CREDENTIAL");
|
|
281
358
|
};
|
|
282
359
|
|
|
360
|
+
const resolveApiKey = async (provider, profile) => (await resolveCredential(provider, profile)).value;
|
|
361
|
+
|
|
283
362
|
const adapter = new PiAiAdapter({
|
|
284
363
|
profiles,
|
|
285
364
|
resolveApiKey,
|
|
@@ -301,8 +380,8 @@ export function apply(ctx, config) {
|
|
|
301
380
|
refreshPromise ??= (async () => {
|
|
302
381
|
try {
|
|
303
382
|
const profile = profiles().get(PROVIDER);
|
|
304
|
-
const
|
|
305
|
-
remoteModels = await fetchCodeBuddyModels(
|
|
383
|
+
const credential = await resolveCredential(PROVIDER, profile);
|
|
384
|
+
remoteModels = await fetchCodeBuddyModels(credential);
|
|
306
385
|
generation += 1;
|
|
307
386
|
} catch {
|
|
308
387
|
// Keep the built-in catalog available while the key or network is absent.
|
|
@@ -333,8 +412,10 @@ export function apply(ctx, config) {
|
|
|
333
412
|
ctx.llm.registerModelDiscovery(NS, async (request) => {
|
|
334
413
|
if (request.provider === PROVIDER) {
|
|
335
414
|
const profile = profiles().get(PROVIDER);
|
|
336
|
-
const
|
|
337
|
-
|
|
415
|
+
const credential = request.apiKey
|
|
416
|
+
? { value: request.apiKey, kind: "api-key", ref: API_KEY_ENV }
|
|
417
|
+
: await resolveCredential(PROVIDER, profile);
|
|
418
|
+
remoteModels = await fetchCodeBuddyModels(credential, request.signal);
|
|
338
419
|
generation += 1;
|
|
339
420
|
return remoteModels.map((model) => ({
|
|
340
421
|
id: model.id,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-llm-codebuddy",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.5",
|
|
4
|
+
"description": "通过 WorkBuddy API Key 或 CodeBuddy 登录令牌为 DeepSeek Harness 接入 CodeBuddy 模型",
|
|
5
5
|
"author": "Axiaohungry",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek-harness",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "index.js",
|
|
16
16
|
"exports": {
|
|
17
|
-
".": "./index.js"
|
|
17
|
+
".": "./index.js",
|
|
18
|
+
"./client": "./client.js",
|
|
19
|
+
"./package.json": "./package.json"
|
|
18
20
|
},
|
|
19
21
|
"bin": {
|
|
20
22
|
"dsh-llm-codebuddy": "cli.js"
|
|
@@ -29,6 +31,9 @@
|
|
|
29
31
|
},
|
|
30
32
|
"files": [
|
|
31
33
|
"index.js",
|
|
34
|
+
"codebuddy-auth.js",
|
|
35
|
+
"codebuddy-web.js",
|
|
36
|
+
"client.js",
|
|
32
37
|
"cli.js",
|
|
33
38
|
"cordis.patch.yml",
|
|
34
39
|
"docs",
|
|
@@ -36,9 +41,13 @@
|
|
|
36
41
|
"LICENSE"
|
|
37
42
|
],
|
|
38
43
|
"scripts": {
|
|
39
|
-
"check": "node --check index.js && node --check cli.js && node --test test.js && node cli.js --self-test"
|
|
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"
|
|
40
45
|
},
|
|
41
46
|
"dsh": {
|
|
47
|
+
"client": {
|
|
48
|
+
"inject": [],
|
|
49
|
+
"platform": "web"
|
|
50
|
+
},
|
|
42
51
|
"bundle": {
|
|
43
52
|
"patch": "./cordis.patch.yml"
|
|
44
53
|
}
|