anye-99/dsh-codearts-auth

dsh-codearts-auth

华为云 CodeArts 浏览器登录,并把 CodeBuddy、WorkBuddy、LobsterAI、Qoder(国际版与中国版)、TRAE、Cline、Loomy、Raccoon Work、MiniMax Code、TeleAgent、ZCode、Step Code、Antigravity 注册为 LLM 提供商;各 provider 独立账号池,其中多家在同一面板内提供额度余额与每日签到。

미분류

dsh-codearts-auth

deepseek-harness 插件:执行 CodeArts(华为云)登录流程,默认走新式 IAM OAuth (portal /authorize 授权 → 本地 /oauth/callback 回调 → STS token 端点换取含 refresh_token 的凭据),到期前静默续期,无需再次打开浏览器;旧 ticket 流程保留 为显式回退(flow: 'ticket')。插件还注册一个 codearts LLM provider 路由,使该 凭证可直接用于 CodeArts 后端模型调用。

此外插件内置另外十三个 provider 路由(完整清单见 LLM provider); 其中带积分能力的几个:

  • buddy(腾讯 CodeBuddy) — 见 buddy provider; 另支持「一键领取积分」(每日签到)与「锁定永久积分」。
  • workbuddy(腾讯 WorkBuddy 国际版) — 见 WorkBuddy provider; 支持「锁定永久积分」。
  • lobsterai(有道 LobsterAI / 龙虾) — 见 LobsterAI provider; 另支持「一键领取积分」(每日签到)。
  • qoder(阿里系 Qoder) — 见 Qoder provider; 支持积分余额与每日领取(每日 100 Credits,10:00(UTC+8)刷新); 走加密推理端点,模型池与客户端一致(含 Qwen3.8 系列)。
  • loomy(讯飞 Loomy 办公助手) — 见 Loomy provider; 唯一用短信验证码登录、唯一不能自动续期的 provider; 支持积分余额(两个池)、每日额度签到、新手任务一键领取 10000 积分, 以及「锁定永久积分」(CodeBuddy / WorkBuddy 也有同名按钮,但判据不同)。
  • minimax(MiniMax Code 中国版) — 见 MiniMax Code provider; 首个 Anthropic Messages 协议族的 provider; 支持积分余额与每日签到,推理已启用(Anthropic Messages, 4 个模型实测通过;⚠️ 图片输入未实现)。
  • teleagent(中国电信天翼 AI 星辰超级智能体) — 见 TeleAgent provider; 唯一一个登录产物需要用户手动粘贴回调地址的面板(它的回调 URL 是服务端常量、 且自定义协议插件注册不了,后端抓不到 token); OpenAI 兼容且无加密(推理直接复用 openai-compat.ts); 无续期端点(失效只能重新登录);支持积分余额与每日签到。
  • antigravity(Google Antigravity / 官方 agy CLI) — 见 Antigravity provider; 唯一一个不由本插件持有凭据的面板(凭据归官方 agy 二进制、 一个账号 = 一个隔离 HOME 目录);支持多账号池、family 配额双窗 (5 小时滚动 + 本周)与限流自动切号。

codearts 面板同样支持积分账户检测、积分余额与「一键领取积分」 (华为云「每日签到得积分」活动,走 SDK-HMAC-SHA256 签名)—— 见 CodeArts 积分。

十四个 provider 面板(含 antigravity)都提供「显示列表」按钮,可逐个开关模型以控制其 是否出现在对话框的模型选择里(黑名单制,默认全部显示)—— 见 模型列表开关。

安装

该包尚未发布到 npm registry。提供两种安装方式:git 仓库安装(推荐,自动拉取 并构建)和源码目录安装(本地开发联调)。

方式一:从 git 仓库安装(推荐)

add 以 git+https 方式安装,pnpm 会运行本包的 prepare 脚本自动构建 lib/, 无需手动 pnpm build。

⚠️ pnpm 10 起会拦截依赖的构建脚本,必须先在 profile 的 pnpm-workspace.yaml (路径形如 ~/.dsh/profiles/<name>/pnpm-workspace.yaml)里放行;而放行键的写法 在 pnpm 10 与 pnpm 11 之间互不兼容,写错就装不上(两种报错见本节末)。

1. 先跑一次安装(这一次必然失败,为的是让 pnpm 打印它期望的键):

dsh plugin --profile <name> add "https://gitee.com/iJetLi/deepseek-harness-codearts.git"

2. 按第 1 步的报错选一种写法,写进 profile 的 pnpm-workspace.yaml。

报 ERR_PNPM_INVALID_VERSION_UNION 的(pnpm 10.x)—— 只认纯包名键:

allowBuilds:
  dsh-codearts-auth: true

报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 的(pnpm 11.x,如本机实测的 一版 DSH Desktop 内置 pnpm 11.7.0)—— git 托管包不认纯包名键,必须用第 1 步 pnpm 打印的完整键(含 #<commit>):

allowBuilds:
  dsh-codearts-auth@git+https://gitee.com/iJetLi/deepseek-harness-codearts.git#<第 1 步打印的 commit>: true

两代通用(省事,但放宽了权限):

dangerouslyAllowAllBuilds: true

它会放行该 profile 里所有依赖的构建脚本(不止本插件)。

3. 重跑第 1 步的命令:这次会拉取、构建并安装成功。之后每次升级重新 add 即可。

⚠️ 不要写 dsh-codearts-auth@git+https://gitee.com/…(不带 #<commit>)—— 这个"看起来最自然"的键在两代 pnpm 上都不工作:

  • pnpm 10.x:解析该键即抛 ERR_PNPM_INVALID_VERSION_UNION("Use exact versions only."),安装当场失败 —— issue IKJCOC 报的就是它;
  • pnpm 11.x:不报错,但该键缺 commit,永远匹配不上真实依赖,表现为反复 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED。

⚠️ pnpm 11.x 的精确键里带 commit,因此升级(重新 add 拉到新 commit)后该键 失效,按新报错里的键替换即可;用 dangerouslyAllowAllBuilds 则不必改。

实测矩阵、判据与本仓库自身 pnpm-workspace.yaml 为什么不能出现同类键, 见 AGENTS.md 的「安装(git 插件)的 allowBuilds 键在 pnpm 10 / 11 语义互不兼容」。

方式二:从源码目录安装(本地开发)

先在本仓库中构建 lib/,再用 dsh plugin install 将本地检出安装为 pnpm link: 依赖(指向本目录):

pnpm build:all
dsh plugin --profile <name> install <path-to-this-repo>

dsh plugin install 以 link: 方式安装,pnpm 不会为 link: 依赖运行 prepare 脚本,因此必须先手动执行 pnpm build:all 生成 lib/,否则 dsh 启动时 报 ERR_MODULE_NOT_FOUND: ... dsh-codearts-auth/lib/index.js。 注意必须用 build:all 而非 build:后者只编译宿主侧,不产出 lib/client/jet-hub.js。

每次修改 src/ 或 plugin-src/ 后都需要重新执行 pnpm build:all——dsh 启动时 不会自动重建。

通用说明

该包声明了 dsh.bundle 补丁(cordis.patch.yml),因此 profile 的 layer 栈会 自动拾取 codearts-auth 行。插件注入由 dsh base 提供的 credentials、 commands 和 llm 服务。

用法

登录入口:Jet Hub 设置页的 CodeArts 面板(与其余五个 provider 一致)。

⚠️ 不注册任何斜杠命令。早期的 /codearts-login、/codearts-status、 /codearts-refresh 三个命令已移除 —— 登录、状态查看与续期统一在 Jet Hub 完成。 编程式调用仍可用:ctx.codeartsAuth.login() / startLogin() / refreshAccountCredential(refName) / refreshAll(pool)。

LLM provider

插件在 ctx.llm 上注册了一个 codearts provider 路由(OpenAI 兼容端点 https://snap-access.cn-north-4.myhuaweicloud.com/api/v2)。每个模型请求都使用 存储的 AK/SK/SecurityToken 按华为 SDK-HMAC-SHA256 方案签名,并附带 Chat-Id/Session-Id 请求头。

模型列表以远端两个端点的合并结果为准(opengw gateway/config 的 result.models ∪ snap-access /v1/model/builtin 的 builtinModels)。 2026-10-09 实测远端共下发 7 条:deepseek-v4-flash / deepseek-v4-pro / deepseek-v4.1-flash(每日 1000 万免费 Tokens 福利)、GLM-5.2、 glm-5.2-sft-harmony(展示名 GLM-5.2-ArkTS-SPARK)、 openpangu-2.0-flash (92B) / openpangu-2.0-pro (505B)。 登录后在 dsh Models 页面选择该 provider 即可。

注 1:CodeArts Agent IDE 模型列表显示的 flash ID 为 deepseek-v4-flash-0731 (带日期后缀),但后端实际注册的可用 ID 是 deepseek-v4-flash(无后缀)。 用 deepseek-v4-flash-0731 调用会返回 InferHub.002002009.404 The model is not registered,因此本插件只注册无后缀的 deepseek-v4-flash。

注 2:maas_type: benefit 的判据已改为自适应(2026-10-09 第二次推翻)。 早期结论是「benefit 模型必须带该头,否则 404 not registered」,该结论 已失效:实测当前服务端对所有模型带上该头都回 InferHub.4004.200 benefit not found,而默认不带时 GLM-5.2 / deepseek-v4-flash / kimi-k2.6-vl 等全部可用。故适配器现在默认不带, 只有当服务端以 not registered 明确拒绝时才带上重试一次,并把结果学习进缓存 —— 不再依赖任何静态或远端清单。详见 src/models.ts 的 isCodeArtsBenefitModel(含三套判据的历史与失效方式)。

注 3:glm-5.3-flash 已退役(服务端不再注册,带/不带该头都失败), 故 src/models.ts 的 RETIRED_MODEL_IDS 会把它从远端目录里过滤掉。 GLM-5.1 与 GLM-5 在 src/llm-adapter.ts 的 DEFAULT_MODELS 里保留, 但远端两个端点都不下发它们 —— 它们只在远端拉取失败时出现在列表里; 按「某次实测没看到 ≠ 不存在」的红线未删除(详见该常量注释)。

注 4:视觉(VL)多模态模型不参与对话模型列表(它们上下文小、不支持工具 调用,只通过 analyzeImage 等工具间接调用)。⚠️ 该过滤必须大小写不敏感 —— 真实缺陷(2026-10-09 实测):原判据是 id.includes('-VL-') || id.endsWith('-VL'),而远端下发的 id 大小写并不统一: Qwen3-VL-235B 是大写(会被过滤),而 kimi-k2.6-vl 是全小写, 原判据不命中,于是它混进了模型选择器。 远端自己对两者的标注完全同类(都是 display_enabled: false + model_category: "图片理解"),可见「不该展示」是服务端的明确意图。 复核脚本 scripts/probe-codearts-vl.mjs。

凭据来自默认的新式 IAM OAuth 流程(含 refresh_token)。请求发起时会解析最新 凭据,若已过期则先静默续期,再用新 AK/SK/SecurityToken 签名,无需重新打开浏览器。

除 codearts 外,插件另注册十四个独立的 provider 路由:buddy(见 buddy provider)、workbuddy(见 WorkBuddy provider)、lobsterai(见 LobsterAI provider)、qoder(见 Qoder provider)、qodercn(见 Qoder CN provider)、trae(见 TRAE provider)、cline(见 Cline provider)、loomy(见 Loomy provider)、raccoon(见 Raccoon Work provider)、minimax(见 MiniMax Code provider)、zcode(见 ZCode provider)、stepcode(见 Step Code provider)、antigravity(见 Antigravity provider)。十四者互不覆盖,可同时使用。

历史:2026-10-01 曾接入 vibex(RunningHub 的 vibe-coding 平台)作为一个 非 LLM 面板(OAuth 账号 + Remote MCP 工具挂载,无补全端点);2026-10-02 应要求整块移除。相关设计要点保留在 AGENTS.md 的「VibeX 维护要点」里, 仅作将来重新评估时的参考,代码与面板均已不存在。 2026-10-04 曾接入 doubao(字节豆包桌面客户端的 CDP 调试通道桥接,非凭据型 LLM 路由);2026-10-05 应要求整块移除 —— 实现文件、Jet Hub 面板、RPC 分支 与本文档章节均已不存在。

凭证

  • ⚠️ 只支持账号池:每个账号的凭据存储在 CODEARTS_ACCOUNT_XXX(Ref 为 POSIX 标识符格式的凭证 ref),由 Jet Hub 设置页管理。
  • 单凭据模式已移除:早期那条「登录写固定 ref CODEARTS_ACCESS_TOKEN、 适配器在账号池取不到时回退读它」的路径已删除。固定的 CODEARTS_ACCESS_TOKEN 不再被写入或读取 —— 若你此前只用它登录过, 模型列表会变空,请在 Jet Hub 的 CodeArts 面板重新登录一次。
  • 值:JSON 字符串 { access_key_id, secret_access_key, security_token, expires_at, domain_id?, user_id?, user_name? } — AK/SK 对用于给每个 CodeArts 后端 API 请求签名。

续期(refresh)

  • 默认登录流程为新式 IAM OAuth(PKCE + DPoP):portal /authorize 授权 → 本地 /oauth/callback 回调收取 code → sts.cn-north-4.myhuaweicloud.com/v1/oauth2/tokens 换取含 refresh_token 的凭据。
  • 凭据在过期前 1 小时静默续期(getFirstRefreshTime 语义:距过期 ≤1h 立即刷, 否则 now+1h 叠加随机秒偏移),全程无浏览器、无人工操作。
  • 刷新失败后 10 分钟重试(异常网络 1 分钟);refresh_token 失效后停止续期并提示 重新登录。
  • 旧 ticket 流程保留为显式回退:编程式调用 ctx.codeartsAuth.login({ flow: 'ticket' })。ticket 凭据没有 refresh_token, 其续期仍意味着重新运行浏览器登录流程。
  • 续期按账号:refreshAccountCredential(refName)(账号卡片的「刷新」按钮) 与 refreshAll(pool)(定时调度器)。早期的 refresh() / logout() / status() / scheduleRefresh() / scheduleModelRefresh() 只服务于单凭据路径, 已随该模式一并移除。
  • 运行时依赖新增 jose(用于 DPoP JWS 签发,与 CodeArts Agent 插件实现一致)。

⚠️ 停用的账号同样会被续期

refreshAll() 与续期调度器只按 refreshable 过滤,不看 enabled。

停用只应影响「账号池的自动选号」,与「凭据是否需要保持新鲜」无关 —— 停用账号仍然出现在 Jet Hub 里,也仍然参与积分领取。

真实缺陷:两个曾停用的 CodeBuddy 账号显示「凭证过期」,点「一键领取 积分」报 Unexpected token '<', "<html> <h"... is not valid JSON。 根因是两处都按 enabled 过滤:

  • refreshAll() 里的 if (!entry.enabled || !entry.refreshable) continue → 停用期间 refresh_token 一路放到失效;
  • src/index.ts 的 accounts.some(a => a.refreshable && a.enabled) → 所有账号都停用时,续期定时器根本不启动。

用户重新启用后拿到的是死凭据,只能重新登录。四个 provider 的 refreshAll 与调度器都必须保持只看 refreshable。

凭据失效时不再抛 Unexpected token '<'

积分请求原本直接 await response.json()。凭据失效时腾讯网关返回的是 HTML 错误页,于是抛出 Unexpected token '<', "<html> <h"... is not valid JSON —— 用户既不知道发生了什么,也看不出该重新登录。

现在先取文本再解析,非 JSON 时给出可读原因:

  • HTTP 401/403 → 「凭据已失效(HTTP 401),请重新登录该账号」
  • 其他状态 → 「服务端返回了非 JSON 响应(HTTP 502):<片段>」

src/credits.ts(CodeBuddy 系)与 src/lobsterai-credits.ts 都已按此处理。

开发

  • pnpm test — 单元测试(快速,无网络)。
  • pnpm test:e2e — 针对华为线上端点的真实登录流程;需要在打开的浏览器中由人工 点击授权按钮(续期为静默刷新,无需再次点击)。
  • pnpm typecheck、pnpm build:all。

构建

  • pnpm build — 用 tsc 将 src/ 编译到 lib/(生成 .js、.d.ts 和 source map)。插件宿主侧入口是 lib/index.js。
  • pnpm build:client — 用 esbuild 将 plugin-src/client/ 打包为 lib/client/jet-hub.js(Jet Hub 设置页的客户端 bundle,由 exports["./client"] 引用)。它不在 tsc 的编译范围内,必须单独构建。
  • pnpm build:all — 依次执行上面两步(build + build:client),是完整的构建。
  • pnpm typecheck — 只做类型检查(tsc --noEmit),不产出文件,可在构建前快速 验证。

lib/ 已被 gitignore,因此构建是安装或运行前的必需步骤。只执行 pnpm build 会漏掉客户端 bundle,dsh 启动时会因 exports["./client"] 指向的文件不存在而 加载失败(Jet Hub 设置页不显示),请改用 pnpm build:all。

每次修改 src/ 或 plugin-src/ 后都需要重新执行 pnpm build:all——dsh 启动时 不会自动重建。

安装到 profile 之前先构建

详见「安装」小节。dsh plugin install 以 link: 方式安装,pnpm 不会为 link: 依赖运行 prepare 脚本,因此必须先 pnpm build:all 生成 lib/(含客户端 bundle)。

工作原理

默认登录流程(新式 IAM OAuth,PKCE + DPoP):

  1. 生成 PKCE 配对与 DPoP ES256 密钥对,并启动本地 127.0.0.1 回调服务器。
  2. 构造 portal /authorize URL 并打开华为云授权页面。
  3. 授权后浏览器回调本地 /oauth/callback,携带授权码 code。
  4. 向 STS token 端点(sts.cn-north-4.myhuaweicloud.com/v1/oauth2/tokens)用 code 换取含 refresh_token 的凭据 JSON,并存储到 CODEARTS_ACCESS_TOKEN 下。
  5. 凭据到期前静默续期(见「续期(refresh)」),无需再次打开浏览器。

旧 ticket 流程保留为显式回退(编程式调用 ctx.codeartsAuth.login({ flow: 'ticket' })): 生成 ticket_id,打开 devcloud.cn-north-4.huaweicloud.com/doer/redirect 认证页, 回调后轮询 snap-manager ticket 端点(120 × 1 秒)获取临时凭证;此类凭据没有 refresh_token,其续期仍意味着重新运行浏览器登录流程。

buddy provider

独立路由 buddy(腾讯 CodeBuddy,OpenAI 兼容端点 https://copilot.tencent.com/v2/chat/completions),Bearer access_token 鉴权。

登录采用 external-link-v2 轮询式(与 CodeArts 的本地回调服务器不同,CodeBuddy 不起本地端口,而是轮询后端 API):

  1. POST /v2/plugin/auth/state?platform=ide → 取得 state 与 authUrl。
  2. 打开浏览器到 https://www.codebuddy.cn/login/?platform=ide&state=...。
  3. 轮询 GET /v2/plugin/auth/token?state=...(1 秒间隔、5 分钟超时)→ 令牌; 错误码 11217 表示 token 未就绪,继续轮询。
  4. 轮询 GET /v2/plugin/login/account?state=... → 账户信息;错误码 12151 表示账户信息未就绪,继续轮询。
  5. 续期:POST /v2/plugin/auth/token/refresh,通过 X-Refresh-Token 头提交 refresh_token。
  • 登录入口:Jet Hub 设置页的 CodeBuddy 面板(支持多账号与账号池自动切换)。 已不再注册斜杠命令 —— 设置面板已覆盖登录、状态查看与续期,命令式入口冗余。
  • 编程式调用:ctx.buddyAuth.login() / status() / refresh() / logout() / fetchModels()。
  • 模型列表:以内置的产品目录为准(src/product.ts 的 fallbackModels), 远端 GET /v3/config 可用时优先采用其元数据。
  • 请求头:除 Authorization: Bearer 外,还需 X-Domain、X-Product、 X-Product-Code 以及伪装为 CodeBuddyIDE/1.106.1 的 User-Agent。
  • 凭据 ref:BUDDY_ACCESS_TOKEN,值为含 access_token / refresh_token / expires_at 的 JSON 字符串。

流式工具调用 id 稳定性:CodeBuddy 仅首个工具调用分片携带真实 id (chatcmpl-tool-xxx),后续参数分片只有 index。适配器按 index 缓存并沿用 真实 id(缺失时回退 call_{index}),保证同一工具的所有分片 id 一致——否则 跨轮次(每轮都从 call_0 重新编号)会把 tool/result 配对到错误的历史条目。

安全策略拦截(11140):会自动换号,被拦账号冷却 30 分钟

腾讯侧业务码 11140(request illegal)的文案写的是「内容未通过安全审核,请调整后重试」, 但实测是按账号生效:同一份请求体(哪怕只有「你好」两个字)发往池里 7 个账号, 结果是「2 个正常、4 个被拦、1 个限流」,换一个会话照样被拦。所以它的出路是换号, 不是让用户改内容。

适配器对它的处理(CodeBuddy 与 WorkBuddy 共用同一实现):

  • 三条通道都会继续换号:首发 401/403 的认证换号、首发限流的换号、以及 HTTP 200 + SSE 流内的错误帧 —— 坏账号排在池里任何位置都不会中断这一轮。

  • 被拦的账号会在「该模型」上冷却 30 分钟,避免每一轮都重撞它 (池的候选顺序由你在 Jet Hub 拖拽决定,排在前面的坏账号否则每次都会先吃一发)。 ⚠️ 账号卡片因此会亮「限额重置 · <模型> · 30 分钟后」徽章 —— 这个徽章现在同时承载两种原因(服务端限流 / 策略拦截冷却), 因为账号池里只有「模型 → 解禁时刻」这一个「暂时不可用」的载体,没有单独的字段。 若确认该账号是好的,点「重测」(它会真发一条最小消息,正常返回就清掉标记) 或「重置」即可立即解禁。

  • 报错不会显示「API 密钥无效」:这类错误归 PERMISSION_DENIED 而不是 AUTH。 DSH 客户端对 AUTH 一律替换成「API 密钥无效」并丢掉原文,会把用户引向重新登录 —— 而实测这些账号的 token 全部有效。现在看到的是原文,例如:

    workbuddy: 全部账号均被服务端安全策略拦截(HTTP 403,已逐个换号重试)。
    该拦截按账号生效(同一请求在其他账号可正常返回),请在 Jet Hub 停用或更换被拦账号:
    内容未通过安全审核,请调整后重试。 request illegal
    

单次输出上限(max_tokens)

远端下发 maxOutputTokens,适配器必须消费它并写进请求体。 这是「回答被 截断、UI 报『已达到输出 token 上限』」的唯一根因修复。

  • 请求体 max_tokens 取值优先级: options.maxTokens(DSH 注入)→ 远端 data.models[].maxOutputTokens → 产品兜底表。 三者皆无则不发该字段,交回网关默认值(不编造数值)。
  • resolveModel() 同时把该值声明为 defaultMaxTokens。DSH 只在调用方未显式 给值时用声明的默认值兜底;适配器不声明就等于把上限永久交给网关默认。
  • ⚠️ 远端非法值必须过滤:DSH 对 defaultMaxTokens 有硬校验(非安全整数 或 ≤0 直接抛 INVALID_MODEL_MAX_TOKENS,整轮对话起不来),故远端是外部 输入,0 / 负数 / NaN 一律视为未声明(见 positiveMaxTokens)。

实测(2026-09-19,node scripts/dump-max-output.mjs):

模型中国版 scoped 端点中国版 /v3/config国际版 /v3/config
deepseek-v4.1-flash128000131072128000
deepseek-v4-pro128000131072—
deepseek-v4-flash5000050000—
hy4-preview640006400064000

⚠️ 网关默认值恰好是 32000(远端 auto / glm-4.6 / kimi-k2.6 等模型 声明的就是 32000),这正是未下发 max_tokens 时 deepseek-v4.1-flash 在 32000 处被截断的原因 —— 不是「网关固定上限」,而是上游声明的额度被适配器丢了。

网关确实接受 max_tokens 且精确生效(node scripts/verify-max-tokens.mjs, 国际版 deepseek-v4.1-flash,当时处于官方限免期 credit: 0):

请求结果
不带 max_tokensHTTP 200,finish_reason=stop
max_tokens: 128000HTTP 200,接受
max_tokens: 64HTTP 200,finish_reason=length、completion_tokens=64

第三行是关键证据:输出被精确截断在 64,证明该字段被服务端真实消费,而不是 被静默忽略。

单次请求 ≠ 单轮:每个 step 都是独立请求、各有各的预算。因此 「拆多步写文件」仍是超出单次额度时最有效的手段;max_tokens 只是把单次 额度提升到上游声明的真实值。

思考档位与输出预算共享同一额度:reasoning_tokens 计入 completion_tokens(实测 thinking 内容与正文同池),故思考开到 max 时正文更早撞上上限。

模型计费倍率与同名模型

模型切换列表里每个模型后面会显示它的计费倍率:

Deepseek-V4.1-Flash · x0.03
GLM-5.3 · x0.79→x0.50          ← 有促销活动时显示 原价→促销价

倍率拼在模型名(name)后面,而不是说明(description)里 —— composer 的模型切换菜单只渲染 name,description 仅用于 /model 弹窗。 name 纯属展示,DSH 的选择与持久化只用 id,所以附加价格不会影响会话。

各 provider 的倍率字段形态互不相同,实现里是分开解析的:

provider远端字段真实形态
buddy / workbuddydata.models[].credits字符串 "x0.29"(可为空串)
buddy / workbuddymodelPromotions[].discount.discountedCredits字符串 "0.50x"(x 在后);factor: 0 = 免费。⚠️ 只由 /v3/config 下发,企业模型端点没有;且须按 schedule 判时段
lobsteraidata[].costMultiplier裸数字 0.05
qoder目录 chat[].price_factor裸数字,0 = 免费
traedisplay_contact_config.consumption_rate.data.rate裸数字 0.08;0 = 免费(⚠️ display_contact_config 本身是 JSON 字符串,须二次解析)
codearts无两个目录端点都不含计费字段

腾讯系的促销值带 schedule(每日时段 + 有效期 + 时区),必须按当前时间本地 推算此刻是否生效,不能只看 enabled;factor: 0 表示免费(如 hy4-preview 的夜间免费),只有没有时间窗口的 "0x" 才当作「已结束」占位。

TRAE 的活动折扣只用当前确实生效的那一档:activity_discount.enable 为 true 也可能是「无折扣」(discount_type: "none"、原价==折后价,实测 off_peak 型即如此),照显会得到 x0.13→x0.13;已过 end_at 的活动同样不展示。

Qoder 的免费模型(price_factor: 0)显示为「免费」而不是 x0;有错峰折扣的 模型在折扣时段内显示「原价→折后价」,时段外只显示原价:

Qwen3.8-Flash · 免费
Qwen3.8-Max · x0.5→x0.2          ← 22:00–08:00(UTC+8)内
Qwen3.8-Max · x0.5               ← 时段外
DeepSeek-V4-Pro · x0.5

⚠️ 折扣形态三个 provider 统一为「原价→折后价」(TRAE x0.4→x0.2、 buddy x0.79→x0.50、Qoder x0.5→x0.2)。Qoder 早期是「只有折后价 + 中文角标」(x0.2 错峰 4 折),问题有二:① 看不出原价与折扣幅度; ② 角标与数字冗余(0.2/0.5 本就是 4 折)。

⚠️ credits 与 discountedCredits 的 x 位置相反("x0.29" vs "0.50x")。早期版本只认前缀写法,导致促销价被静默丢弃。

⚠️ 腾讯系两个端点下发的模型 id 集合不同,必须取并集 —— 促销可能只挂在 其中一个端点独有的 id 上。实测 hy4-preview-f(新用户限时免费)只由 /v3/config 下发且被 agent 引用,而 scoped 端点给的是 hy4-preview (无促销)。只采信 scoped 就会显示 x0.29 而 IDE 显示免费(用户报障 「hy4 preview 现在 ide 是免费我们还是 0.29」)。不同账号下发的变体 id 也不同,排查时须多账号对照。

⚠️ 腾讯系的促销只在 /v3/config 下发,企业模型端点没有 —— 而后者被优先 返回,所以早期实现永远不显示促销(用户报障「codebuddy 的倍率显示也是 没折扣的,GLM-5.2 是 0.5,现在显示 0.79」)。现补取促销表并合并。 且必须按 schedule 判此刻是否生效(glm-5.2 夜间/白天是两条互补活动, 不看时段会全天显示折扣价);factor: 0 是「免费」而非「已结束」 (hy4-preview 夜间免费,用户报障「夜间 0,现在显示 0.29」)。 ⚠️ /v3/config 有 UA 校验,UA 不对返回 HTTP 200 + code:12403, 极易误判为「该端点没有促销数据」。

⚠️ 产品兜底表是白名单(不在表里的 id 会被丢弃),但被 agent 引用的 模型例外保留(服务端自己的「可选」信号)—— 否则 hy4-preview-f 会被丢掉。 判据不是猜 id 后缀(-f/-x/-sg 含义各异,猜错会放进不可用的模型)。

⚠️ Qoder 的字段是 price_factor,不是 cost_multiplier(后者是 LobsterAI 的)。而 price_factor: 0 是合法的免费值,不能用 > 0 过滤掉。

⚠️ 倍率不能放进 description:那在切换模型列表里根本不可见 (用户报障「消耗倍率没有显示在切换模型列表的后面」)。

⚠️ Qoder 的错峰时段按 windowStart/windowEnd 本地推算,不采信目录的 promotion.active —— 后者是目录下发那一刻的快照,客户端长时间不重启 就会与真实时段脱节(用户在时段外看到折后价、或时段内看不到折扣)。 生效价由 beforePromotionPriceFactor × discountFactor 推出(实测三条全部吻合)。 真实缺陷:早期表里存的是「采集时刻的生效价」却当成恒定值展示, 且 14 个模型的数值本身也是过期估值(smodel 写 3.2 实际 8), 用户报障「qwen3.8-max 是 0.5 打折到 0.2,界面显示的是 0.5」。

同名模型会自动区分

服务端会给不同 id 配同一个展示名,实测三组:

模型 id远端展示名实际差异
deepseek-v4.1-flash / deepseek-v4.1-flash-sg都是 Deepseek-V4.1-Flash新加坡区,倍率 x0.00 vs x0.03
hy3 / hy3-x都是 Hy3—
hy4-preview-f / hy4-preview都是 Hy4 preview—

由于选择器按展示名渲染,这些会变成无法区分的重复条目(用户报障: 「workbuddy 国际版同时显示 2 个 ds v4.1 flash,IDE 只有一个」——IDE 按展示名 归并,本插件按 id 列出)。二者是不同区域的独立计费实体,不能简单丢弃其一, 故对撞车的 id 求公共前缀、把剩余段追加到名字后:

Deepseek-V4.1-Flash · x0.00        (deepseek-v4.1-flash)
Deepseek-V4.1-Flash · x0.03 SG     (deepseek-v4.1-flash-sg)
Hy3 · x0.00                        (hy3)
Hy3 · x0.05 X                      (hy3-x)

用公共前缀而不是硬编码 -sg,是因为撞车组会随服务端上新变化(本次实测三组 里只有一组带 -sg)。LobsterAI 实测无同名(28 个模型 0 组重名),故不做消歧。

WorkBuddy provider(国际版)

独立路由 workbuddy(腾讯 WorkBuddy 国际版 / WorkBuddy AI),与 buddy provider 同源:共用同一 CLI 内核与同一认证协议 (cli-external-link 轮询式),Bearer access_token 鉴权。差异收敛在 src/product.ts 的产品配置里:

项CodeBuddy(中国)WorkBuddy(国际版)
endpointhttps://copilot.tencent.comhttps://www.workbuddy.ai
platformideworkbuddy-ai
登录 URL 附加参数无version / loginSessionId
pluginVersion—5.5.2

模型列表不能与中国版共用:两者的路径与响应解析完全相同 (GET /v3/config → data.data.models / data.data.agents),差异只来自 endpoint —— 不同区域的后端返回不同模型池(中国版含 glm / hy / deepseek 系, 国际版含 claude / gpt / gemini / kimi 系)。因此 endpoint 必须随产品切换, 不能被当成全局常量。

登录流程与 CodeBuddy 一致(auth/state → 浏览器授权 → 轮询 auth/token → 轮询 login/account),仅身份标识与端点按上表区分。X-Product-Code 为 workbuddy,X-Domain 随 apiDomain 切换为 www.workbuddy.ai。

没有每日签到积分:国际版后端不提供签到接口(内核中只有 /v2/billing/meter/get-dosage-notify 用量通知),因此 Jet Hub 的 WorkBuddy 面板不显示「一键领取积分」按钮;签到领取在 CodeBuddy 面板完成。

但积分余额(Credits Balance)可以查。 签到与余额是两项独立能力:国际版 确实没有签到,但有积分余额查询接口,见下节。不要因为"没有签到"就推断 也查不到余额。

  • 登录入口:Jet Hub 设置页的 WorkBuddy 面板(支持多账号与账号池自动切换)。 同样不注册斜杠命令。
  • 编程式调用:ctx.workbuddyAuth.login() / status() / refresh() / logout() / fetchModels()。
  • 凭据 ref:
    • 单账号:WORKBUDDY_ACCESS_TOKEN,值为含 access_token / refresh_token / expires_at 的 JSON 字符串(与 BUDDY_ACCESS_TOKEN 同构)。
    • 多账号:WORKBUDDY_ACCOUNT_<UUID_SHORT>,由 Jet Hub 设置页「+ 新建账号」 登录时自动生成并登记到账号池;每条账号记录带 provider: 'workbuddy', 与 CodeBuddy 的 BUDDY_ACCOUNT_* 相互隔离,不会串用凭据或限流标记。
  • 从中国版升级:本插件早期版本把 workbuddy 指向中国版 (copilot.tencent.com)。启动时会自动清理凭据 domain 与当前 apiDomain 不符的旧账号(这类凭据在新端点必然失败),清理结果记入日志, 请在 Jet Hub 重新登录。
  • 续期:与 CodeBuddy 共用同一套机制,插件启动后每 30 分钟对可续期账号静默刷新 (refresh_token 经 X-Refresh-Token 头提交),无需重新打开浏览器。
  • 请求头、模型列表拉取与流式工具调用 id 处理均与 CodeBuddy 一致,详见上一节。

与 Jet Hub 设置页的关系

Jet Hub(设置页)的账号面板按 provider 分组展示,WorkBuddy 是其中一栏:

  • 面板提供账号列表、新建账号(浏览器登录入池)、启用/停用、删除、拖拽排序, 以及「重测 / 重测所有 / 重置 / 重置所有」限流标记操作,行为与 CodeBuddy 面板一致,但只操作 provider: 'workbuddy' 的账号。
  • 账号卡片展示 credentialRef、有效期(含「自动续期」标记)、限流状态与积分 余额(见下节)。「一键领取积分」按钮仅 CodeBuddy 面板提供,结果来自 RPC 端点 credits.claimAll(实现见 src/jet-hub-rpc.ts,签到客户端见 src/credits.ts)。
  • 后端另实现了 credits.status(查询某 provider 下全部启用账号的签到状态), 但前端尚无消费者:plugin-src/client/jet-hub.js 只调用 credits.claimAll, credits.status 目前仅供外部脚本或直接 RPC 调用使用。
  • 对应 LLM provider 的设置命名空间为 llm-workbuddy。

「+ 新建账号」必须走两步式登录(四个 provider 一致)

account.create 对全部四个 provider 都遵守同一契约:在用户完成授权之前 就返回 loginUrl,由前端立即 window.open,后台再异步等回调。

这不是风格偏好,而是浏览器安全模型的硬约束:window.open 只在用户点击后的 transient activation 窗口(约 5 秒)内被允许。若 account.create 阻塞到 用户授权完成才返回(数十秒),拿到 URL 时手势早已过期,弹窗必被拦截并返回 null,前端若兜底执行 window.location.href = loginUrl,就会把整个设置页 导航到外部登录页 —— 用户报障「codearts 新建账号应该弹出新的页面,现在主页面 直接跳转过去了」正是此因。

provider后端实现前端交互
buddy / workbuddyrunBuddyLoginFlow 不 await,立即返回 URL弹小窗 + 轮询 login.poll
codeartsCodeArtsAuth.startLogin()同上
lobsteraiLobsteraiAuth.startLogin()同上
  • 两步式的入口在 src/login.ts 的 startOAuthFlow 与 src/lobsterai-oauth.ts 的 startLobsteraiLoginFlow;原阻塞式 runOAuthFlow / runLobsteraiLoginFlow 保留(CLI、e2e 仍用),现由前者实现。
  • 两步式路径没有外层 try/finally 兜底,故超时与「结果落定即关闭回调 服务器」都收在 start* 内部,避免泄漏监听端口。
  • 前端不再保留 window.location.href 兜底:弹窗被拦截时改为展示一个可点击 的登录链接(loginUrlForManual),轮询照常进行,用户手动打开也能完成登录。
  • 回归测试见 tests/unit/jet-hub-rpc.spec.ts 的 「account.create 必须立即返回 loginUrl」:用「授权永不完成」的替身模拟用户 尚未操作 —— 旧实现会超时失败,新实现立即返回。

账号拖拽排序(顺序 = 选号优先级)

Jet Hub 各 provider 面板的账号卡片可拖动调整顺序,卡片左上角显示序号。

这不是纯 UI 装饰:账号列表的数组顺序就是 getAvailableAccount 的候选 优先级 —— 自动选号、以及限流后换号重试,都按这个顺序取「第一个可用账号」。 把常用账号拖到前面,它就会优先被使用。

语义是「手动顺序优先,限流豁免」:

  • 顺序完全由用户决定(不按任何服务端字段重排);
  • 但当前正处于限流期的账号会被跳过,不会选到 —— 即使它排在第一位。

⚠️ 早期实现有一个 candidates.sort((a,b) => resetAtA - resetAtB)(按限流 重置时间最早到期优先)。它会让拖拽形同虚设:用户把某账号拖到首位,只要 另一个账号的重置时间更早,实际选中的仍是后者。该排序已移除, tests/unit/account-pool.spec.ts 有回归用例锁死。

交互细节:

  • 拖动整张卡片,或抓住左侧的 ⠿ 手柄;
  • 插入位置按指针落在目标卡片的上半 / 下半决定(前插 / 后插), 并以卡片上方或下方的蓝线指示。只支持「前插」时把卡片往下拖一格会变成 空操作,因此必须区分方向;
  • 顺序乐观更新(本地先变、再提交),失败则回滚并提示;
  • 提交期间禁用拖拽,避免并发提交互相覆盖;
  • 只有 1 个账号时不启用拖拽(排序无意义)。

实现:RPC account.reorder → AccountPool.reorderAccounts() (只动本 provider 占用的下标,其他 provider 账号位置不变); 前端拖拽逻辑在 plugin-src/client/account-order.js(纯函数,可单测)。

积分余额(Credits Balance)

账号卡片上的「积分」一行显示该账号的可用积分,与 IDE 顶部显示的 Credits Balance 是同一个数值。鼠标悬停可看到各资源包的明细与到期时间。

两个 buddy 的卡片还会额外显示「临时 X · 永久 Y」分桶(与下方 锁定永久积分同一判据),tooltip 里每个包 标出「距到期 N 天」。这条也直接解释了"锁上为什么没号可用"——临时桶是 0。

⚠️ 分类在渲染的那一刻现算,不缓存:它是「距扣费截止是否满 15 天」的判断, 而宿主长期开着、时间只向前流——一笔距到期 15 天 30 秒的余额,用户什么都不做, 半分钟后就越过了线。所以选号侧缓存的也只是 get-user-resource 的原始包列表 (TTL 60 秒只用于抑制网络请求),命中缓存同样重新分桶;面板侧每次渲染传当下的