feat: 个人定制版 —— API Key 模式显示剩余积分 + 悬浮卡片

基于 @axiaohungry/dsh-llm-workbuddy 1.3.21 (MIT)。
详见 README.CUSTOM.md。
This commit is contained in:
dsh-custom committed 2026-10-10 13:59:09 +08:00
commit a44227b69a
14 files changed
+5517

No files matched your search

@@ -0,0 +1,579 @@
# WorkBuddy API 调用开发文档
> 适用对象:需要在其他 Agent、CLI、IDE 插件或本地代理中接入 CodeBuddy 模型的开发者
> 验证日期:2026-08-14
> 验证客户端:`@tencent-ai/codebuddy-code 2.136.0`
> 文档性质:基于已安装客户端、实际接口响应和本项目适配代码整理的第三方开发说明,不是腾讯官方 API 承诺。
## 1. 先说明名称关系
日常说的“调用 WorkBuddy API”实际包含两个不同角色:
| 名称 | 实际作用 |
|---|---|
| WorkBuddy | 提供或签发 API Key |
| CodeBuddy | 使用该 Key 的 Agent/CLI 产品 |
| `copilot.tencent.com` | 当前实际提供模型目录和推理接口的服务端 |
| 本项目 | 把上述服务适配为 DSH Provider |
因此,当前观察到的调用链是:
```mermaid
flowchart LR
A["WorkBuddy 提供 API Key"] --> B["CodeBuddy 或第三方 Agent"]
B --> C["GET /v3/config\n获取当前 Key 的模型权限"]
B --> D["POST /v2/chat/completions\n发起模型推理"]
C --> E["copilot.tencent.com"]
D --> E
```
不存在一个需要额外调用的“WorkBuddy 换取 CodeBuddy Token”步骤。开发者拿到有效 Key
后,直接把它用于模型目录和聊天请求。
## 2. 重要边界
1. 接口目前不是公开、稳定承诺的开发者 API,路径、Header 和字段可能随 CodeBuddy 更新。
2. 只能使用本人或组织授权的 API Key,不要绕过账号、额度、模型权限或企业策略。
3. 模型列表与 API Key 绑定,不同 Key 返回的模型 ID 可能不同。
4. 不要把模型目录写死在代码中;每次添加或替换 Key 后应重新获取 `/v3/config`。
5. API Key 只能放在环境变量、凭据服务或 Secret Manager 中,不能提交到 Git。
## 3. 接口总览
| 用途 | 方法 | 地址 | 协议 |
|---|---|---|---|
| 获取模型与产品配置 | `GET` | `https://copilot.tencent.com/v3/config` | JSON |
| 模型推理 | `POST` | `https://copilot.tencent.com/v2/chat/completions` | OpenAI Chat Completions 兼容,推荐 SSE 流式 |
当前不是 OpenAI Responses API,也不是 Anthropic Messages API。第三方 Agent 应优先实现
`openai-completions` / Chat Completions 适配。
## 4. API Key 管理
推荐环境变量:
```text
WORKBUDDY_API_KEY=<从 WorkBuddy 获取的 Key>
```
Windows PowerShell 设置用户环境变量:
```powershell
[Environment]::SetEnvironmentVariable(
"WORKBUDDY_API_KEY",
"<在本机填写,不要提交到仓库>",
"User"
)
```
新进程才能读取新设置。不要在日志中输出 Key;诊断两个 Key 是否相同时,可以比较
SHA-256 指纹,而不是打印明文。
本插件新版本使用 `WORKBUDDY_API_KEY`。为兼容旧版本,仍可读取 `CODEBUDDY_API_KEY`,但新
项目建议统一改用前者。
## 5. 获取当前 Key 可用的模型
### 5.1 请求
```http
GET /v3/config HTTP/1.1
Host: copilot.tencent.com
Accept: application/json
X-API-Key: <API_KEY>
User-Agent: CLI/unknown CodeBuddy/2.136.0
X-Product: SaaS
```
其中:
- `X-API-Key`:模型目录接口的关键鉴权 Header;
- `User-Agent`:按当前 CodeBuddy CLI 行为填写,建议版本升级时同步验证;
- `X-Product: SaaS`:标识当前产品部署类型;
- Header 名大小写不敏感。
### 5.2 响应骨架
成功时 HTTP 通常为 `200`,同时还要检查业务字段 `code`:
```json
{
"code": 0,
"msg": "OK",
"data": {
"agents": [
{
"name": "cli",
"models": ["hy3", "glm-5.3", "deepseek-v4-flash"]
}
],
"models": [
{
"id": "deepseek-v4-flash",
"name": "Deepseek-V4-Flash",
"maxInputTokens": 1000000,
"maxOutputTokens": 50000,
"maxAllowedSize": 1000000,
"supportsImages": true,
"supportsReasoning": true,
"onlyReasoning": true,
"reasoning": {
"effort": "high",
"summary": "auto"
}
}
]
}
}
```
示例只展示与适配有关的字段;实际响应还可能包含企业信息和产品功能开关。
### 5.3 正确的模型筛选算法
不能直接把 `data.models` 全部暴露给用户。正确流程是:
1. 在 `data.agents` 中找到 `name === "cli"` 的 Agent;
2. 读取该 Agent 的 `models`,这是当前 Key 对 CLI 开放的模型 ID;
3. 用这些 ID 与 `data.models[].id` 关联;
4. 丢弃没有容量信息或没有对应配置的异常条目;
5. 保留服务端顺序,避免客户端自行重排造成默认模型变化。
兼容旧响应时,可以同时检查:
```javascript
const agents = Array.isArray(data.agents)
? data.agents
: data.agent?.agents;
```
### 5.4 Node.js 获取模型示例
Node.js 22 以上可直接使用内置 `fetch`,不需要 SDK:
```javascript
const apiKey = process.env.WORKBUDDY_API_KEY;
if (!apiKey) throw new Error("缺少 WORKBUDDY_API_KEY");
const response = await fetch("https://copilot.tencent.com/v3/config", {
headers: {
accept: "application/json",
"x-api-key": apiKey,
"user-agent": "CLI/unknown CodeBuddy/2.136.0",
"x-product": "SaaS",
},
signal: AbortSignal.timeout(20_000),
});
if (!response.ok) {
throw new Error(`模型目录 HTTP ${response.status}`);
}
const body = await response.json();
if (body?.code !== 0) {
throw new Error(`模型目录业务错误:${body?.msg ?? body?.code}`);
}
const agents = Array.isArray(body.data?.agents)
? body.data.agents
: body.data?.agent?.agents;
const allowedIds = agents?.find((agent) => agent?.name === "cli")?.models ?? [];
const byId = new Map((body.data?.models ?? []).map((model) => [model.id, model]));
const models = allowedIds.flatMap((id) => {
const model = byId.get(id);
if (!model) return [];
const contextWindow = model.maxInputTokens ?? model.maxAllowedSize;
const maxTokens = model.maxOutputTokens;
if (!Number.isSafeInteger(contextWindow) || !Number.isSafeInteger(maxTokens)) return [];
return [{
id,
name: model.name ?? id,
contextWindow,
maxTokens,
supportsImages: model.supportsImages === true,
supportsReasoning: model.supportsReasoning === true,
onlyReasoning: model.onlyReasoning === true,
defaultReasoningEffort: model.reasoning?.effort,
thinkingLevelMap: model.thinkingLevelMap,
thinkingFormat: model.thinkingFormat,
}];
});
console.table(models);
```
## 6. 为什么不同账号看到的模型不同
`agents[name=cli].models` 是 Key 级别的授权结果,不是全平台公共目录。
2026-08-14 实测,同一台电脑上的两把有效 Key 返回了不同结果:
| 现象 | Key A | Key B |
|---|---|---|
| GLM 新版本 | 包含 `glm-5.3` | 最高为 `glm-5.2` |
| MiniMax ID | `minimax-m3-pay` | `minimax-m3` |
| Kimi ID | `kimi-k3-2` | `kimi-k3-1` |
因此,出现“CodeBuddy 能看到 `glm-5.3`,另一个 Agent 看不到”时,先确认两个程序实际使用
的是不是同一把 Key。不要通过硬编码 `glm-5.3` 解决,否则请求阶段仍会被服务端拒绝。
## 7. 发起 Chat Completions 请求
### 7.1 请求地址
```text
POST https://copilot.tencent.com/v2/chat/completions
```
### 7.2 鉴权 Header
当前 CodeBuddy CLI 会为模型请求同时准备:
```http
Authorization: Bearer <API_KEY>
X-API-Key: <API_KEY>
Content-Type: application/json
Accept: text/event-stream
```
本项目基于 OpenAI SDK 的适配路径使用 `Authorization: Bearer` 即可完成请求;为了更贴近
CodeBuddy CLI 并兼容服务端策略变化,独立开发的新客户端建议同时发送 `Authorization`
和 `X-API-Key`。不要把 Key 放进 URL Query。
CodeBuddy CLI 还会添加请求 ID、会话 ID、IDE 名称、产品类型等内部 Header。第三方开发的
最小客户端不应伪造这些字段;只有在服务端明确要求时再增加。
### 7.3 最小请求体
```json
{
"model": "deepseek-v4-flash",
"messages": [
{ "role": "user", "content": "请只回复:连接成功" }
],
"stream": true,
"stream_options": { "include_usage": true },
"max_tokens": 1024
}
```
注意:
- 使用 `max_tokens`,不是 `max_completion_tokens`;
- `model` 必须来自当前 Key 的 CLI 模型列表;
- 推荐 `stream: true`,这是 CodeBuddy Agent 的主要工作模式;
- `max_tokens` 不应超过目录中的 `maxOutputTokens`;
- 输入和预期输出总量不能超过模型上下文限制。
## 8. 思考能力必须逐模型处理
不能为所有模型统一写死 `high`,也不能假设所有模型都支持 `xhigh` 或 `max`。至少读取:
| 字段 | 含义 | 处理方式 |
|---|---|---|
| `supportsReasoning` | 模型是否支持推理 | `false` 时不显示思考档位,不发送推理参数 |
| `onlyReasoning` | 是否只能以推理模式工作 | `true` 时不提供 `off` |
| `reasoning.effort` | 该模型的默认思考档位 | 用户未选择时使用;不要拿一个模型的默认值套给其他模型 |
| `thinkingLevelMap` | UI 档位到线上参数的逐模型映射 | 存在时严格按映射提供选项和转换 |
| `thinkingFormat` | 推理参数协议 | 存在时按该格式转换;缺失时才使用当前端点的兼容默认 |
当前实测就存在不同默认值:部分模型为 `high`,部分模型为 `medium`。
### 8.1 选择规则
1. 用户显式选择档位:发送该模型声明支持的档位;
2. 用户选择“默认”或未选择:使用该模型自己的 `reasoning.effort`;
3. 服务端没有声明默认值:省略思考参数,让服务端决定;
4. `supportsReasoning === false`:删除所有推理参数;
5. `onlyReasoning === true`:不要提供 `off`;
6. `thinkingLevelMap` 存在:未出现在 Map 中的档位视为不支持;
7. 不要自动把不支持的档位静默替换成另一个档位,最好在请求前报错。
### 8.2 OpenAI 风格
当前适配使用的默认格式是:
```json
{
"reasoning_effort": "medium"
}
```
常见候选值为:
```text
minimal / low / medium / high / xhigh / max
```
候选值不等于所有模型均支持。以当前模型的能力声明为准。
### 8.3 其他思考格式
如果未来目录返回不同 `thinkingFormat`,适配器可能需要转换,例如:
```json
{ "reasoning": { "effort": "high" } }
```
或:
```json
{ "thinking": { "type": "enabled" }, "reasoning_effort": "high" }
```
不要同时无条件发送所有格式。应为每个模型只生成一种服务端声明的格式。
## 9. 工具调用
请求中的工具遵循 OpenAI Chat Completions 格式:
```json
{
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询天气",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"],
"additionalProperties": false
}
}
}
],
"tool_choice": "auto"
}
```
流式响应中的工具参数会分段出现在:
```text
choices[0].delta.tool_calls[].function.arguments
```
客户端必须按 `tool_calls[].index` 累积字符串,结束后再解析 JSON。执行工具后,把结果作为
`role: "tool"`、带相同 `tool_call_id` 的消息加入下一轮请求。
## 10. 图片输入
只有 `supportsImages === true` 的模型才能接收图片。OpenAI 兼容格式示例:
```json
{
"role": "user",
"content": [
{ "type": "text", "text": "描述这张图片" },
{
"type": "image_url",
"image_url": {
"url": "data:image/png;base64,<BASE64>"
}
}
]
}
```
发送前应限制文件大小和 MIME 类型,不要仅根据扩展名判断图片。
## 11. SSE 流式响应解析
服务端返回 `text/event-stream`。每个事件通常形如:
```text
data: {"id":"...","choices":[{"delta":{"content":"你"}}]}
data: {"id":"...","choices":[{"delta":{"content":"好"},"finish_reason":"stop"}]}
data: [DONE]
```
需要处理的字段:
| 字段 | 用途 |
|---|---|
| `choices[0].delta.content` | 最终回答文本增量 |
| `choices[0].delta.reasoning_content` | 思考内容增量之一 |
| `choices[0].delta.reasoning` | 部分模型使用的思考内容字段 |
| `choices[0].delta.reasoning_text` | 另一种兼容思考字段 |
| `choices[0].delta.tool_calls` | 工具调用增量 |
| `choices[0].finish_reason` | `stop`、`length`、`tool_calls` 等结束原因 |
| `usage` | Token 用量,通常在开启 `include_usage` 后的尾部 Chunk 返回 |
同一个 Chunk 可能没有 `choices`,只有 `usage`,不能因此判定响应异常。
## 12. 完整 Node.js 流式示例
```javascript
const apiKey = process.env.WORKBUDDY_API_KEY;
if (!apiKey) throw new Error("缺少 WORKBUDDY_API_KEY");
const response = await fetch("https://copilot.tencent.com/v2/chat/completions", {
method: "POST",
headers: {
authorization: `Bearer ${apiKey}`,
"x-api-key": apiKey,
"content-type": "application/json",
accept: "text/event-stream",
},
body: JSON.stringify({
model: "deepseek-v4-flash",
messages: [{ role: "user", content: "请只回复:连接成功" }],
stream: true,
stream_options: { include_usage: true },
max_tokens: 1024,
// reasoning_effort: "high", // 只在当前模型明确支持且用户选择时添加
}),
signal: AbortSignal.timeout(300_000),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`推理接口 HTTP ${response.status}: ${detail.slice(0, 500)}`);
}
const decoder = new TextDecoder();
let buffer = "";
let answer = "";
let reasoning = "";
for await (const chunk of response.body) {
buffer += decoder.decode(chunk, { stream: true });
let boundary;
while ((boundary = buffer.indexOf("\n\n")) !== -1) {
const event = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
for (const line of event.split(/\r?\n/)) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (!data || data === "[DONE]") continue;
const payload = JSON.parse(data);
const delta = payload.choices?.[0]?.delta;
if (typeof delta?.content === "string") {
answer += delta.content;
process.stdout.write(delta.content);
}
const thought = delta?.reasoning_content ?? delta?.reasoning ?? delta?.reasoning_text;
if (typeof thought === "string") reasoning += thought;
if (payload.usage) console.error("\nusage:", payload.usage);
}
}
}
console.log("\n\nanswer:", answer);
console.log("reasoning length:", reasoning.length);
```
生产代码还需要按 `tool_calls[].index` 累积工具参数,并处理单个 SSE 事件跨网络 Chunk 的情况。
## 13. 错误处理与重试
### 13.1 模型目录
同时检查 HTTP 状态和 JSON 业务状态:
```javascript
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (body.code !== 0) throw new Error(body.msg ?? String(body.code));
```
### 13.2 常见分类
| 情况 | 建议处理 |
|---|---|
| `401` / `403` | Key 无效、过期或无权限;停止重试并要求重新填写 |
| `408` / 网络断开 | 指数退避重试 |
| `429` | 遵守 `Retry-After`;提示额度或频率限制 |
| `5xx` | 短暂退避后有限重试 |
| 模型不在 CLI 列表 | 刷新目录,不要强制调用 |
| 流结束但没有结束原因 | 视为不完整响应,不要保存为成功回答 |
流式请求只应在“尚未收到任何有效增量”时自动重试。收到文本或工具调用后再次自动重试,
可能造成重复输出或重复执行工具。
### 13.3 推荐超时
- 模型目录:20 秒总超时;
- 建立推理连接:30–60 秒;
- 流空闲超时:300 秒;
- 用户取消:通过 `AbortController` 立即向上游传播。
## 14. 缓存策略
模型目录可以按 Key 指纹缓存,但不能跨 Key 共用:
```text
cache key = SHA-256(API Key) + 客户端产品类型
```
建议:
- 缓存 5–15 分钟;
- 用户点击“刷新模型”时绕过缓存;
- 更换 Key 后立即清除旧缓存;
- 缓存失败时可以使用最近一次成功目录,但 UI 必须标注“可能过期”;
- 不要把完整 Key 写进缓存键或日志。
## 15. 适配其他 Agent 的最小接口
一个可维护的 Provider 只需要四个职责:
```text
resolveCredential() -> 安全读取 Key
discoverModels() -> 获取并按 CLI 权限过滤模型
describeModel(id) -> 返回该模型容量、模态、思考档位和默认值
stream(request) -> 转换消息并解析 SSE
```
DSH、OpenCode 或其他 Agent 自己负责:
- Agent 循环;
- 上下文裁剪与压缩;
- 工具实际执行;
- 权限确认;
- 会话持久化;
- 重试策略和用户取消。
CodeBuddy 服务端负责模型推理,不会替第三方 Agent 自动执行本地工具。
## 16. 验收清单
开发完成后至少验证:
- [ ] Key 不出现在源码、Git、日志和异常堆栈中;
- [ ] 两把不同 Key 的模型目录不会串用;
- [ ] 只显示 `agents[name=cli].models` 授权的模型;
- [ ] 新模型无需发布新代码即可出现;
- [ ] 旧模型下线后不会继续从缓存永久显示;
- [ ] 上下文窗口和最大输出来自逐模型字段;
- [ ] 非图片模型会在请求前拒绝图片;
- [ ] 非推理模型不显示思考控件;
- [ ] `onlyReasoning` 模型不显示 `off`;
- [ ] 每个模型使用自己的默认思考档位;
- [ ] 用户显式档位会转换为该模型自己的线上值;
- [ ] 能累计文本、思考和工具调用 SSE 增量;
- [ ] 用户取消能中止网络请求;
- [ ] 429 和 5xx 有限重试,不会无限循环;
- [ ] 工具调用不会因自动重试而重复执行。
## 17. 版本漂移检查
升级 CodeBuddy CLI 后,应重新核对:
1. `product.json` 和 CLI 版本;
2. 配置接口是否仍为 `/v3/config`;
3. 推理接口是否仍为 `/v2/chat/completions`;
4. 鉴权是否仍接受 `Authorization` / `X-API-Key`;
5. 模型能力字段是否新增 `thinkingLevelMap`、`thinkingFormat` 等;
6. SSE 推理和工具字段是否变化;
7. 当前 Key 实际返回的 CLI 模型目录。
本项目的可执行实现位于 [`index.js`](../index.js),安装和使用说明见
[`README.md`](../README.md)。