dsh-llm-codebuddy 1.3.3 → 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 CHANGED
@@ -1,163 +1,220 @@
1
- # DSH CodeBuddy Provider
2
-
3
- 为 DeepSeek Harness(DSH)增加 `CodeBuddy 中国区` Provider。安装后可直接在
4
- DSH WebUI 中填写 API Key、获取模型、调整模型参数并使用 CodeBuddy 模型。
5
-
6
- > [!IMPORTANT]
7
- > API Key 由 **WorkBuddy** 提供,本插件使用该 Key 调用供 CodeBuddy 使用的模型服务。
8
- > DSH 中的 Provider 名称仍为 `CodeBuddy 中国区`。本项目是第三方适配器,并非
9
- > WorkBuddy、CodeBuddy DSH 官方插件。
10
-
11
- ## 功能
12
-
1
+ # DSH CodeBuddy Provider
2
+
3
+ 为 DeepSeek Harness(DSH)增加 `CodeBuddy 中国区` Provider。支持 WorkBuddy
4
+ API Key CodeBuddy 中国站账号令牌两种认证方式,并可在 DSH WebUI 中获取、
5
+ 选择和调整模型。
6
+
7
+ > [!IMPORTANT]
8
+ > API Key **WorkBuddy** 提供,本插件使用该 Key 调用供 CodeBuddy 使用的模型服务。
9
+ > DSH 中的 Provider 名称仍为 `CodeBuddy 中国区`。本项目是第三方适配器,并非
10
+ > WorkBuddy、CodeBuddy 或 DSH 官方插件。
11
+
12
+ ## 功能
13
+
13
14
  - WebUI 中直接添加 `CodeBuddy 中国区`;
14
- - 只需填写从 WorkBuddy 获取的 API Key;
15
- - 自动获取 CodeBuddy 当前可用模型;
16
- - 支持编辑模型 ID、名称、上下文窗口和最大输出 Token;
17
- - 支持添加、删除模型以及重新同步模型目录;
18
- - 支持 `minimal / low / medium / high / xhigh / max` 思考程度;
19
- - 输入新 API Key 可替换旧值,留空保存则保留原值;
20
- - 模型接口暂时不可用时使用内置目录兜底;
21
- - 独立安装,不修改 DSH 全局安装目录。
22
-
23
- ## 环境要求
24
-
25
- - Windows、Linux 或 macOS;
26
- - Node.js `>= 22.19.0`;
15
+ - 支持从 WorkBuddy 获取的 API Key;
16
+ - API Key 输入框旁可显式切换 `API Key` / `令牌登录`;
17
+ - 支持从 WebUI 拉起浏览器登录 CodeBuddy 中国站,并自动复用、刷新登录令牌;
18
+ - 自动获取 CodeBuddy 当前可用模型;
19
+ - 支持编辑模型 ID、名称、上下文窗口和最大输出 Token;
20
+ - 支持添加、删除模型以及重新同步模型目录;
21
+ - 按模型目录声明各模型自己的思考能力、可选档位和默认档位;
22
+ - 输入新 API Key 可替换旧值,留空保存则保留原值;
23
+ - 模型接口暂时不可用时使用内置目录兜底;
24
+ - 独立安装,不修改 DSH 全局安装目录。
25
+
26
+ ## 环境要求
27
+
28
+ - Windows、Linux 或 macOS;
29
+ - Node.js `>= 22.19.0`;
27
30
  - 已安装 DSH;
28
31
  - 已验证 DSH `0.1.0-rc.6`。
29
32
 
30
- 安装器已自带 DSH 所需的 `pnpm`,无需全局安装。
33
+ 安装器已自带 DSH 所需的 `pnpm`,无需全局安装;令牌登录也不需要安装 CodeBuddy CLI。
34
+
35
+ > DSH 仍处于预发布阶段。未来版本如果调整插件接口,本插件可能需要同步升级;
36
+ > DSH 普通更新不会覆盖本插件。
37
+
38
+ ## 安装
39
+
40
+ 推荐使用一键安装命令:
41
+
42
+ ```powershell
43
+ npx --yes dsh-llm-codebuddy@latest install
44
+ ```
45
+
46
+ 该命令会为 DSH 的 `web` 和 `headless` Profile 安装插件。完成后重启 DSH。
47
+
48
+ 也可以分别安装:
49
+
50
+ ```powershell
51
+ dsh plugin --profile web add dsh-llm-codebuddy@latest
52
+ dsh plugin --profile headless add dsh-llm-codebuddy@latest
53
+ ```
54
+
55
+ 只使用 WebUI 时,仅执行第一条即可。
56
+
57
+ ## WebUI 配置
31
58
 
32
- > DSH 仍处于预发布阶段。未来版本如果调整插件接口,本插件可能需要同步升级;
33
- > DSH 普通更新不会覆盖本插件。
59
+ ### CodeBuddy 账号令牌登录(新用户推荐)
34
60
 
35
- ## 安装
61
+ 1. 打开“设置 → 模型”。
62
+ 2. 添加或编辑 `CodeBuddy 中国区`。
63
+ 3. 点击 API Key 输入框右侧的“令牌登录”。
64
+ 4. 在自动打开的浏览器中完成 CodeBuddy 中国站登录。
65
+ 5. WebUI 显示“令牌已登录”后即可获取模型并保存自定义目录。
36
66
 
37
- 推荐使用一键安装命令:
67
+ 如果 WebUI 无法打开浏览器,也可以使用命令行入口:
38
68
 
39
69
  ```powershell
40
- npx --yes dsh-llm-codebuddy@latest install
70
+ npx --yes dsh-llm-codebuddy@latest login
41
71
  ```
42
72
 
43
- 该命令会为 DSH 的 `web` 和 `headless` Profile 安装插件。完成后重启 DSH。
73
+ 插件会调用 CodeBuddy 官方认证流程。完成登录后,插件会自动把
74
+ `CodeBuddy 中国区` Provider 切换为令牌模式,无需 API Key。点击同一行的
75
+ “API Key”可以切回 API Key 模式;已经保存的 API Key 不会被删除。
76
+ 两种凭据彼此独立保留,之后再次点击“令牌登录”会优先复用已保存令牌;仅在没有
77
+ 令牌或主动重新登录时才需要再次打开浏览器。
44
78
 
45
- 也可以分别安装:
79
+ 插件直接调用 CodeBuddy 中国站官方网页登录和令牌刷新接口,不依赖本机
80
+ `codebuddy` 命令。访问令牌和刷新令牌保存在 DSH 凭据服务中,不会写入
81
+ `settings.yaml` 或模型目录。
46
82
 
47
- ```powershell
48
- dsh plugin --profile web add dsh-llm-codebuddy@latest
49
- dsh plugin --profile headless add dsh-llm-codebuddy@latest
50
- ```
51
-
52
- 只使用 WebUI 时,仅执行第一条即可。
53
-
54
- ## WebUI 配置
83
+ ### WorkBuddy API Key
55
84
 
56
85
  1. 打开“设置 → 模型”。
57
- 2. 点击“添加提供方”。
58
- 3. 选择 `CodeBuddy 中国区`。
59
- 4. 输入从 WorkBuddy 获取的 API Key 并保存。
60
- 5. 点击该 Provider 的“编辑”,展开“自定义设置”。
61
- 6. 点击“获取可用模型”,选择需要的模型并导入。
62
- 7. 按需修改模型参数,然后保存。
63
-
64
- 再次编辑已配置的 Provider 时,会直接显示上次保存的模型目录。
65
-
66
- ## API Key 替换
67
-
68
- - 输入新的 API Key 并保存:替换原 Key;
69
- - API Key 输入框留空并保存:保留原 Key;
70
- - 更换 Key 后建议重新点击“获取可用模型”,同步新账号的模型权限。
71
-
72
- API Key 由 DSH 凭据服务保存,不会写入模型目录或插件源码。
73
-
74
- ## 模型配置
75
-
76
- - 没有自定义目录:使用 CodeBuddy 在线目录,失败时使用内置目录;
77
- - 保存自定义目录:仅向 DSH 提供目录中保留的模型;
78
- - 已知模型字段留空:继承在线目录或内置目录中的值;
79
- - 新模型缺少容量:上下文窗口默认 `262144`,最大输出默认 `32768`;
80
- - 点击“恢复默认模型”:删除自定义目录并恢复适配器目录。
81
-
82
- 配置值超过服务端真实限制时,CodeBuddy 仍可能拒绝请求。
83
-
84
- ## 思考程度
85
-
86
- DSH 显示思考程度选项,插件把选中的档位转换为 `reasoning_effort` 并发送给
87
- CodeBuddy。模型推理由 CodeBuddy 云端执行。
88
-
89
- ```text
90
- minimal / low / medium / high / xhigh / max
91
- ```
92
-
93
- 当前不提供 `off`,因为 CodeBuddy 模型目录中的模型属于推理模型。未选择档位时,
94
- 使用 CodeBuddy 对应模型的默认值。
95
-
96
- ## 更新
97
-
98
- 重新运行安装命令即可更新到最新版:
86
+ 2. 点击“添加提供方”。
87
+ 3. 选择 `CodeBuddy 中国区`。
88
+ 4. 输入从 WorkBuddy 获取的 API Key 并保存。
89
+ 5. 点击该 Provider 的“编辑”,展开“自定义设置”。
90
+ 6. 点击“获取可用模型”,选择需要的模型并导入。
91
+ 7. 按需修改模型参数,然后保存。
92
+
93
+ 再次编辑已配置的 Provider 时,会直接显示上次保存的模型目录。
94
+
95
+ ## API Key 替换
96
+
97
+ - 输入新的 API Key 并保存:替换原 Key;
98
+ - API Key 输入框留空并保存:保留原 Key;
99
+ - 更换 Key 后建议重新点击“获取可用模型”,同步新账号的模型权限。
100
+
101
+ API Key 由 DSH 凭据服务保存,不会写入模型目录或插件源码。
102
+
103
+ ## 模型配置
104
+
105
+ - 没有自定义目录:使用 CodeBuddy 在线目录,失败时使用内置目录;
106
+ - 保存自定义目录:仅向 DSH 提供目录中保留的模型;
107
+ - 已知模型字段留空:继承在线目录或内置目录中的值;
108
+ - 新模型缺少容量:上下文窗口默认 `262144`,最大输出默认 `32768`;
109
+ - 点击“恢复默认模型”:删除自定义目录并恢复适配器目录。
110
+
111
+ 配置值超过服务端真实限制时,CodeBuddy 仍可能拒绝请求。
112
+
113
+ ## 思考程度
114
+
115
+ DSH 显示的思考程度来自当前模型自身的能力声明,插件把选中的档位转换为
116
+ `reasoning_effort` 并发送给 CodeBuddy。模型推理由 CodeBuddy 云端执行。
117
+
118
+ ```text
119
+ off / minimal / low / medium / high / xhigh / max
120
+ ```
121
+
122
+ 实际显示哪些档位由 `/v3/config` 中该模型的 `supportsReasoning`、`onlyReasoning`、
123
+ `thinkingLevelMap` `reasoning.effort` 决定,不能跨模型共用一套固定档位。未手动
124
+ 选择时,使用 CodeBuddy 为该模型返回的默认档位;服务端没有声明时则不强行指定。
125
+
126
+ ## 更新
127
+
128
+ 重新运行安装命令即可更新到最新版:
129
+
130
+ ```powershell
131
+ npx --yes dsh-llm-codebuddy@latest install
132
+ ```
133
+
134
+ 更新完成后重启 DSH。模型配置和 API Key 不会被覆盖。
135
+
136
+ ## 卸载
137
+
138
+ ```powershell
139
+ npx --yes dsh-llm-codebuddy@latest uninstall
140
+ ```
141
+
142
+ 卸载命令会:
143
+
144
+ 1. 备份 `~/.dsh/settings.yaml`;
145
+ 2. 只删除 `llm-pi-ai.providers.codebuddy-cn` 配置;
146
+ 3. 保留其他 Provider 和 DSH 设置;
147
+ 4. 从 `web`、`headless` Profile 移除插件;
148
+ 5. 保留 API Key 和登录令牌凭据,方便以后重新安装。
149
+
150
+ 完成后重启 DSH。备份文件名类似:
151
+
152
+ ```text
153
+ settings.yaml.codebuddy-backup-2026-08-14T12-00-00-000Z
154
+ ```
155
+
156
+ 源码仓库、本地安装包和 API Key 不会被删除。
157
+
158
+ ## 常见问题
159
+
160
+ ### 安装后看不到 CodeBuddy
161
+
162
+ 确认已经重启 DSH,并检查 Web Profile:
163
+
164
+ ```powershell
165
+ dsh plugin --profile web list --depth 0
166
+ ```
167
+
168
+ ### 获取模型失败
169
+
170
+ 确认 API Key 来自 WorkBuddy 且仍然有效,然后重新输入 Key 并点击“获取可用模型”。
171
+ 接口临时不可用时,插件仍会提供内置模型目录。
172
+
173
+ ### 为什么别人能看到某个模型,我这里看不到
174
+
175
+ 模型权限与 API Key 绑定。插件只导入 `/v3/config` 中 `agents[name=cli].models` 为
176
+ 当前 Key 返回的模型;更换 Key 后请重新点击“获取可用模型”。插件不会强行显示
177
+ 当前 Key 未授权的模型。
178
+
179
+ ### 调用时报 `500 status code (no body)`
180
+
181
+ 先更新到最新版并重启 DSH:
99
182
 
100
183
  ```powershell
101
184
  npx --yes dsh-llm-codebuddy@latest install
102
185
  ```
103
186
 
104
- 更新完成后重启 DSH。模型配置和 API Key 不会被覆盖。
105
-
106
- ## 卸载
107
-
108
- ```powershell
109
- npx --yes dsh-llm-codebuddy@latest uninstall
110
- ```
111
-
112
- 卸载命令会:
113
-
114
- 1. 备份 `~/.dsh/settings.yaml`;
115
- 2. 只删除 `llm-pi-ai.providers.codebuddy-cn` 配置;
116
- 3. 保留其他 Provider 和 DSH 设置;
117
- 4. 从 `web`、`headless` Profile 移除插件;
118
- 5. 保留 API Key 凭据,方便以后重新安装。
119
-
120
- 完成后重启 DSH。备份文件名类似:
121
-
122
- ```text
123
- settings.yaml.codebuddy-backup-2026-08-14T12-00-00-000Z
124
- ```
125
-
126
- 源码仓库、本地安装包和 API Key 不会被删除。
127
-
128
- ## 常见问题
129
-
130
- ### 安装后看不到 CodeBuddy
131
-
132
- 确认已经重启 DSH,并检查 Web Profile:
133
-
134
- ```powershell
135
- dsh plugin --profile web list --depth 0
136
- ```
137
-
138
- ### 获取模型失败
139
-
140
- 确认 API Key 来自 WorkBuddy 且仍然有效,然后重新输入 Key 并点击“获取可用模型”。
141
- 接口临时不可用时,插件仍会提供内置模型目录。
187
+ 旧版本可能被 DSH 覆盖 CodeBuddy 请求标识,导致服务端拒绝请求;最新版已在插件请求层
188
+ 恢复 CodeBuddy 官方标识,同时适用于 API Key 和令牌模式。
142
189
 
143
190
  ### 卸载后仍显示旧页面
144
191
 
145
192
  关闭正在运行的 DSH,再重新启动。已经运行的进程不会自动卸载内存中的插件。
146
193
 
147
- ## 工作原理
194
+ ## 开发文档
148
195
 
149
- ```text
196
+ 需要开发其他 Agent 或 Provider 时,请阅读
197
+ [反向代理调用 WorkBuddy API 开发文档](./docs/反向代理调用WorkBuddy-API开发文档.md)。
198
+
199
+ ## 工作原理
200
+
201
+ ```text
150
202
  WorkBuddy 提供 API Key
151
203
 
152
204
  DSH Agent → 本插件 → CodeBuddy /v2/chat/completions
153
205
  ↘ CodeBuddy /v3/config(获取模型)
154
- ```
155
-
156
- - DSH:负责 Agent 循环、上下文、工具调用和权限;
157
- - WorkBuddy:提供 API Key;
158
- - 插件:负责 Provider 注册、模型目录转换和请求兼容;
159
- - CodeBuddy:负责模型推理并返回结果。
160
-
161
- ## License
162
206
 
163
- [MIT](./LICENSE)
207
+ CodeBuddy 中国站网页登录 → 本插件保存并刷新 DSH 登录凭据
208
+ ↓ 当前 access token
209
+ DSH Agent → 本插件 → CodeBuddy API(Authorization: Bearer)
210
+ ```
211
+
212
+ - DSH:负责 Agent 循环、上下文、工具调用和权限;
213
+ - WorkBuddy:提供 API Key;
214
+ - 插件:负责 Provider 注册、模型目录转换和请求兼容;
215
+ - CodeBuddy:负责模型推理并返回结果。
216
+ <img width="1885" height="853" alt="image" src="https://github.com/user-attachments/assets/eda31b48-8412-414d-b552-1b7ce0a7c3a0" />
217
+
218
+ ## License
219
+
220
+ [MIT](./LICENSE)
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 凭据保持不变。请重启 DSH。");
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
+ });