# deepseek-harness-codearts **Repository Path**: iJetLi/deepseek-harness-codearts ## Basic Information - **Project Name**: deepseek-harness-codearts - **Description**: deepseek-harness的插件,支持codearts登录和模型调用 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 147 - **Forks**: 60 - **Created**: 2026-08-14 - **Last Updated**: 2026-10-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # dsh-codearts-auth > **使用声明**:本项目仅供学习研究使用,请遵守各服务提供方的服务条款。 deepseek-harness 插件:统一管理多个 AI 编程服务的账号,并把它们的模型接进 DSH 的模型选择器。支持**多账号池**(同一服务登录多个账号,按可用性自动选号)与 **凭据静默续期**(到期前自动刷新,无需再次打开浏览器)。 - **14 个 provider 路由**,外加 1 个**聚合**路由(见下) - 登录、状态、续期、积分领取**全部在 Jet Hub 设置页完成**,不注册任何斜杠命令 - 附带一个**本机 OpenAI 兼容网关**,供其它客户端复用已登录账号 > 📌 **本 README 只写「怎么用」。** 各 provider 的协议、端点、登录流程、请求头 > 构造、实测数据与事故史等**实现细节**,统一记录在不入库的内部文档 > `docs/implementation-notes.md` 中。需要改动实现或排查协议问题时读它。 --- ## 目录 - [安装](#安装) - [用法](#用法) - [支持的渠道](#支持的渠道) - [聚合路由](#聚合路由aggregate) - [本机 OpenAI 网关](#本机-openai-网关) - [用量徽标与 Token 用量](#用量徽标与-token-用量) - [配置项](#配置项) - [开发](#开发) --- ## 安装 该包尚未发布到 npm registry。提供两种安装方式。 ### 方式一:从 git 仓库安装(推荐) `add` 以 `git+https` 方式安装,pnpm 会运行本包的 `prepare` 脚本自动构建 `lib/`, 无需手动 `pnpm build`。 ⚠️ pnpm 10 起会拦截依赖的构建脚本,必须先在 profile 的 `pnpm-workspace.yaml` (路径形如 `~/.dsh/profiles//pnpm-workspace.yaml`)里放行;而**放行键的写法 在 pnpm 10 与 pnpm 11 之间互不兼容**,写错就装不上。 **1. 先跑一次安装**(这一次必然失败,为的是让 pnpm 打印它期望的键): ```sh dsh plugin --profile add "https://gitee.com/iJetLi/deepseek-harness-codearts.git" ``` **2. 按第 1 步的报错选一种写法,写进 profile 的 `pnpm-workspace.yaml`。** 报 **`ERR_PNPM_INVALID_VERSION_UNION`** 的(**pnpm 10.x**)—— 只认**纯包名**键: ```yaml allowBuilds: dsh-codearts-auth: true ``` 报 **`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`** 的(**pnpm 11.x**)—— git 托管包 **不认**纯包名键,必须用第 1 步 pnpm 打印的**完整键(含 `#`)**: ```yaml allowBuilds: dsh-codearts-auth@git+https://gitee.com/iJetLi/deepseek-harness-codearts.git#<第 1 步打印的 commit>: true ``` **两代通用(省事,但放宽了权限)**: ```yaml dangerouslyAllowAllBuilds: true ``` 它会放行该 profile 里**所有**依赖的构建脚本(不止本插件)。 **3. 重跑第 1 步的命令**:这次会拉取、构建并安装成功。之后每次升级重新 `add` 即可。 ⚠️ **不要**写 `dsh-codearts-auth@git+https://gitee.com/…`(不带 `#`)—— 这个"看起来最自然"的键在两代 pnpm 上都不工作。 ⚠️ pnpm 11.x 的精确键里带 commit,因此**升级(重新 `add` 拉到新 commit)后该键 失效**,按新报错里的键替换即可;用 `dangerouslyAllowAllBuilds` 则不必改。 ### 方式二:从源码目录安装(本地开发) 先在本仓库中构建 `lib/`,再安装为 pnpm `link:` 依赖: ```sh pnpm build:all dsh plugin --profile install ``` > `dsh plugin install` 以 `link:` 方式安装,pnpm 不会为 `link:` 依赖运行 > `prepare` 脚本,因此必须先手动执行 `pnpm build:all` 生成 `lib/`。 > 注意必须用 `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 设置页的对应 provider 面板。** ⚠️ **不注册任何斜杠命令** —— 登录、状态查看、续期与积分领取统一在 Jet Hub 完成。 基本流程: 1. 打开 Jet Hub 设置页(侧边 rail 选择要用的渠道); 2. 点「**+ 新建账号**」→ 浏览器打开授权页 → 完成登录; 3. 需要多个账号时重复第 2 步(账号池会自动选号); 4. 回到 dsh 的模型选择器,选中该渠道的模型即可开始对话。 「+ 新建账号」是**两步式**的:先返回 `loginUrl` 由前端弹窗打开,后台再轮询换取 令牌 —— 这样不会阻塞浏览器手势窗口,弹窗不会被拦截。 账号卡片支持:**测试 / 重测 / 重置 / 停用 / 代理 / 指纹 / 删除**,以及拖拽排序 (**顺序即选号优先级**)。停用只影响自动选号,**不影响凭据续期与积分领取**。 --- ## 支持的渠道 14 个 provider 路由,互不覆盖,可同时使用。 | id | 渠道 | 登录方式 | 积分能力 | |---|---|---|---| | `codearts` | CodeArts(华为云) | 浏览器回调 | 余额 + 每日签到 | | `buddy` | CodeBuddy(腾讯) | 浏览器授权 + 轮询 | 余额 + 每日签到 + 成长任务 + 锁定永久积分 | | `workbuddy` | WorkBuddy(国际版) | 同上 | 余额 + 锁定永久积分(**无签到**) | | `lobsterai` | LobsterAI(有道) | 浏览器回调 | 余额 + 每日签到 | | `qoder` | Qoder(阿里系) | 设备码轮询 | 余额 + 每日领取 | | `qodercn` | Qoder 中国版 | 同上(共用实现) | 余额 + 每日领取 | | `trae` | TRAE(字节跳动) | 浏览器回调 | 余额 + 签到 | | `cline` | Cline | WorkOS 设备码轮询 | 余额 | | `loomy` | Loomy(讯飞) | 短信验证码 | 余额(两个池)+ 每日额度 + 新手任务 | | `raccoon` | Raccoon Work(商汤) | 微信扫码 / 短信 | 余额 + 一次性登录奖励 | | `minimax` | MiniMax Code(中国版) | OAuth 设备码 + PKCE | 余额 + 每日签到 | | `zcode` | ZCode(智谱 z.ai) | OAuth 设备码 | 余额 + 每日领取 | | `opencode` | OpenCode | — | 余额 | | `gemini` | Gemini Code Assist | 浏览器回调 | 余额 | 各渠道的能力与限制要点: - **图片输入**:`buddy` / `workbuddy` / `trae`(逐模型)/ `minimax`(部分模型)支持; `lobsterai`、`zcode` **不支持**;其余以模型声明为准。 - **思考档位**:多数渠道支持,档位名按各渠道自己的定义下发;未声明的渠道不发送该字段。 - **`loomy`** 是唯一用**短信验证码**登录、且**不能自动续期**的渠道。 - **`zcode`** 的领取路径需要一次性验证参数,由本机浏览器环境产出(web 版需要 chromium,桌面版用自带内核)。 ⚠ **推理(对话)路径是否也要它,由服务端开关决定**:官方 3.14.5 起 `GET /api/v1/client/configs` 会下发 `captcha.skip_model_request` —— 当前实测是 `true`(**不校验**,故按最省的方式「先探后取」: 不带验证头先发一发,被 `3007` 拒了才产);一旦服务端翻成 `false`, 插件会**自动改为首发直接带上**,不再白吃那发注定失败的往返 (判据是三态:缺席 = 不知道 = 保持先探,见 `requiresCaptchaUpfront`)。 - **`qoder` / `qodercn`** 共用同一套实现,差异仅在产品配置。 - **`minimax`** 是唯一的 Anthropic Messages 协议族渠道。 ### 一键领取积分 在对应面板标题栏点「**一键领取积分**」,插件会**顺序**处理该面板下的所有账号 (顺序执行以避免并发请求),并**逐账号显示结果与原因**。 ⚠️ 汇总**按单位分列**,不跨量纲求和 —— `ZCode` 的额度单位是 **token**、其余渠道是 **积分**,两者不可折算,因此显示成 `+100.00MToken, +100积分` 而不是加成一个数。 ### 模型列表开关 每个 provider 的面板都有「**显示列表**」按钮,可逐个开关模型,控制它是否出现在 对话框的模型选择器里(**黑名单制**,默认全部显示)。开关按 provider 隔离。 ### 锁定永久积分 `buddy` / `workbuddy` / `loomy` 提供「**锁定永久积分**」按钮,用来保住不会马上 作废的积分。判定规则:距扣费截止不足 15 天的算**临时**(优先消耗),其余算永久。 锁定后只消耗临时积分,用完则明确报错而不是偷偷烧掉永久积分。 --- ## 聚合路由(`aggregate`) 同一个真实模型往往在**多个渠道**都能用(例如 DeepSeek V4.1 Flash 在 buddy、 codearts、cline 等都有)。聚合路由把「同一个真实模型」在各渠道的条目**归到一个 规范名下**,请求时自动挑一个渠道发出去。 **解决什么**:各家渠道送的免费额度**有到期时间**,用不完就作废。聚合层按 「**最快作废的额度优先用**」自动排序,把临期积分先烧掉。 **怎么用**: 1. 在模型选择器里选 **`aggregate`** 分组; 2. 想**不挑模型**就选该分组下的 **`auto`** —— 它用全部渠道的全部模型当候选池; 3. 想**指定模型**就选该分组下的**具体规范名**(如 `Deepseek-V4.1-Flash`)。 选中后模型名旁会附一个 **`· <渠道>`** 后缀,那是**首选渠道**(临期最早的那个)。 ⚠️ 它**不是「本次实际发往的渠道」** —— 真实历史由**用量徽标**显示。 **自动切换**:某个渠道失败时会换下一个候选重试,但只在**还没吐出任何可见内容** 时才切换(已输出的内容收不回来,拼接会产生畸形回答)。 **参与轮换的渠道**:`buddy` / `workbuddy` / `loomy` / `codearts` / `zcode` / `lobsterai` / `trae` 七家。其余渠道**暂不参与**(插件对它们的额度到期时刻没有 可靠信息,宁可少一个候选,不可把未知当可用)—— 它们仍可**直连**正常使用。 ⚠️ **`zcode` 的额度是「按模型」分桶的**,故它的可用性是**逐模型**判定的: 赠送的 8M 实测分成 `GLM-5.3` 3M 与 `GLM-5.3-Flash` 5M 两个**专属桶** (上游在余额接口里下发 `capabilities: ["model:<模型>"]`)。某个模型烧光而另一个 还有额度时,只会把**烧光的那个模型**在该账号上标记为当日用尽 —— 另一个模型照常可用,不会因为「账号整体还有额度」而误派发到已尽的模型上。 若账号另有**不限定模型**的通用赠送额度(如 1 亿 token),且它还有余额, 则两个模型都仍可用(通用额度能承接它们)。 **上下文窗口**:聚合模型声明的是**候选渠道里的最大值**。同一个模型在 buddy / codearts 上都是 1M,那么 `aggregate` 下选它也声明 1M(与直连一致)。⚠️ 若某次 请求恰好被路由到窗口更小的候选,由**失败切换**兜住(上游报超长 ⇒ 换下一个候选)。 **思考档位**:声明各渠道档位的**并集**(官方中文名),默认 `high` —— 与你选的 档位会**逐候选翻译**到目标渠道(精确命中就原样下发,写法不同就就近落到同族, 目标没有这一族就**不下发**该字段让它走自己的默认)。 聚合有**自己的**设置面板(Jet Hub 左侧 rail 的「聚合」项),可逐条开关 「参与轮换」。它没有账号与凭据(复用各渠道的账号)。 --- ## 本机 OpenAI 网关 插件启动后会在 `127.0.0.1:8326` 提供**两套**标准 OpenAI 接口,供 Pi、Continue、 Cline、OpenCode、Codex 或其他兼容客户端使用。网关复用 Jet Hub 已登录账号和现有 provider 适配器,不把上游凭据复制到客户端。 ``` GET http://127.0.0.1:8326/v1/models GET http://127.0.0.1:8326/v1/reasoning-efforts ← 思考档位对照表 POST http://127.0.0.1:8326/v1/chat/completions ← OpenAI Chat Completions POST http://127.0.0.1:8326/v1/responses ← OpenAI Responses API ``` ⚠️ **两个端点同时可用,没有「格式开关」**:客户端用哪一套协议,由它自己请求的 路径决定。 ⚠️ 上面那两个地址里的 **host 与端口都可被环境变量改掉** (`DSH_OPENAI_GATEWAY_HOST` / `DSH_OPENAI_GATEWAY_PORT`);设置页里的 「API 请求地址」显示的是**实际监听地址**,照它抄即可。 ### 容器化部署(Docker / Compose) 默认只绑 `127.0.0.1`,因此**端口映射对网关无效**:容器里 `curl 127.0.0.1:8326` 能通只是假象,docker-proxy 与同网络的其它容器走的是容器桥接 IP(`eth0`), 那个地址上没有监听,于是宿主侧看到 `Connection reset by peer` (对照:Web 端口绑的是容器 IP,所以映射可用)。 修法是让网关绑**容器自身的接口地址**: ```yaml environment: - DSH_OPENAI_GATEWAY_ENABLED=1 - DSH_OPENAI_GATEWAY_HOST=172.21.0.8 # 容器自身 IP:hostname -i 或 ip -4 addr show eth0 - DSH_OPENAI_GATEWAY_PORT=8326 ``` ```console $ docker exec dsh ss -tlnp | grep 8326 LISTEN 172.21.0.8:8326 # 绑上桥接 IP 后,端口映射才可达 ``` ⚠️ **不许填 `0.0.0.0` / `::` 这类通配地址**:会被显式拒绝并在启动前报错 (dsh 本体的 `--host` 也是同一姿态)。绑通配等于把网关暴露给整个网络, 真正的隔离只剩 API Key —— 而本网关承担的是「用你已登录的账号发请求」。 容器场景填容器自身 IP 即可。 ⚠️ **暴露面不止「同一容器网络内的其它容器」,还包括宿主**:经端口映射(`-p`) 或 bridge 网关,宿主本身就能连到该端口。跨 bridge 网络的容器才被隔离。 设置页里绑了非回环地址时会显示这条提示;不打算对外提供时请保持默认 `127.0.0.1`(或删除该环境变量)。 ### 拿到地址与密钥 鉴权使用 `Authorization: Bearer <网关 API Key>`。 ⚠️ 因此**在浏览器地址栏直接打开这些地址会返回 401**(没有带 Bearer 头)—— 这是预期行为,不是网关没启动。请用客户端或 `curl` 带上传头访问。 优先从 `DSH_OPENAI_GATEWAY_API_KEY` 读取;未设置时,插件首次启动会在 DSH home 的 `openai-gateway/api-key` 生成并持久化随机密钥,重启后保持不变。 设置页里的开关状态存在 `$DSH_HOME/jet-hub/state.json` 的 `gatewayEnabled` 字段 (缺省为启用)。它**不进**账号备份/恢复。 ### 关闭网关 网关是**旁路功能**:它启动失败或被关闭,都不影响插件其余功能(登录、积分、模型 目录照常)。三种关闭方式: | 方式 | 做法 | |---|---| | 环境变量 | `DSH_OPENAI_GATEWAY_ENABLED=0`(**完全不启动**,连 API Key 文件都不生成)| | 设置页开关 | 关掉后不再监听 | | 改端口 | `DSH_OPENAI_GATEWAY_PORT` 填非法值会记录错误并跳过启动,**不会**静默改用其它端口 | | 改地址 | `DSH_OPENAI_GATEWAY_HOST` 填通配地址(`0.0.0.0` / `::`)同样记录错误并跳过启动 —— **不会**静默绑到全部网卡 | ### 请求体上限与 keep-alive | 项 | 值 | 超限时的行为 | |---|---|---| | 请求体 | **300 MiB** | `HTTP 413` + `invalid_request_error` / `request_too_large`,响应体里带上限值 | | 空闲连接(keep-alive) | **120 秒** | 服务端发 FIN;此后复用该连接会得到 `Connection reset` | ⚠️ 每轮把全量历史重新回传的客户端(Codex 等),历史里并入大截图后可能撞上 413: 新建会话、或删掉那条带大图的回合即可恢复。 ### 安全边界 - **默认**只绑定 `127.0.0.1`,且**拒绝**通配地址(`0.0.0.0` / `::`)。 只有容器/端口映射这类「客户端不在同一网络命名空间」的场景才需要改 `DSH_OPENAI_GATEWAY_HOST`,且应填**具体接口地址**(如容器自身 IP)而不是通配。 ⚠️ 改了之后暴露面**包含宿主**(端口映射/bridge 网关都能连到), 同机与同容器网络的其它用户或进程也能连到该端口 —— 真正的隔离靠 API Key, 不要把它配置进任何浏览器端工具或扩展。 - 密钥以明文写在 `$DSH_HOME/openai-gateway/api-key`。POSIX 下的 `0600` 权限在 **Windows 上不生效**,多用户机器请改用 `DSH_OPENAI_GATEWAY_API_KEY` 环境变量。 --- ## 用量徽标与 Token 用量 ### 用量徽标 会话输入区右侧、模型选择器左边有一枚薄胶囊,显示**当前渠道一共有多少可用额度**; 点开是完整浮层(逐账号明细 / 订阅窗口 / 刷新 / 签到 / 显示偏好)。 只在选中本插件渠道的模型时渲染,非本插件的模型**不渲染、不发请求**。 折叠态按 **`渠道 • 读数`** 两段显示,例如: ``` CodeBuddy (腾讯) • 2434.96积分 CodeArts (华为云) • 9499.84积分 ZCode (智谱) • 94.54MToken ``` - **默认显示余额**(一共能用的额度);可在浮层里切换成「优先订阅」或「只看积分」。 - **多账号只显示一个数字**(所有启用账号的余额合计),不会变成一长串。 - 账号读不到时合计会**静默少报**,故胶囊会挂一个**警示角标**(`441.78积分 ⚠`)。 - 浮层里逐账号列表**按余额降序**,超过 5 个折叠成「展开其余 N 个」。 ### Token 用量 点 Jet Hub 页头的「**Token 用量**」按钮查看。数据分两份: | 数据 | 位置 | 回答的问题 | 重启后 | |---|---|---|---| | 实时明细 + 汇总树 | 进程内存(上限 500 条) | 每一笔长什么样、这次会话花了多少 | 清空 | | 历史日聚合 | `~/.dsh/jet-hub/token-ledger.json` | 本周 / 本月用了多少 | **保留**(按 UTC+8 日界,90 天)| 在历史图表中点选某一天并展开,可删除该日的历史累计。删除前会二次确认,只移除该日聚合,不影响实时明细、其他日期或账号设置;之后该日的新请求会重新累计。 - **计数是 wire 口径**:上游 usage 报什么记什么,不做本地估算。 - **总处理量** = 未命中缓存输入 + 缓存命中输入 + 输出;输出已含思考 token,缓存写入另列且不重复计入。不同渠道价格不同,因此这里不把 token 数冒充账单金额。 - 没收到 usage(失败 / 中断)显示 `—`,与真实的 `0` **严格区分**。 - 输出速率(tok/s)的分子含思考 token;汇总显示算术均值及完整样本的 P50/P90 估值。 - 日聚合文件升级到 schema v2 增加 TPS 对数直方图;旧 v1 数据照常读取(计数全保留)。⚠️ **升级前已累计过「输出速率样本」的日期不会显示 P50/P90** —— 判据要求「直方图样本数 = 累计样本数」,而旧桶的 `tpsCount` 在升级前就已累加、直方图为空,两者之差恒定 ⇒ 该日期在 90 天保留期内一直无分位数(`avgTps` 均值仍照常显示)。完全在 v2 之后累计满样本的日期正常显示;升级前存在但**从未有过速率样本**的日期(如当天全是失败请求)也会正常显示。 --- ## 配置项 | 项 | 位置 | 说明 | |---|---|---| | 账号池 | `$DSH_HOME/jet-hub/state.json` | 账号、有效期、限流标记、模型黑名单、provider 顺序 | | 网关开关 | 同上,`gatewayEnabled` | 缺省启用 | | 供应商显示顺序 | 同上,`providerOrder` | 纯展示偏好,拖动行调整 | | 永久积分锁定 | `$DSH_HOME/jet-hub/*.json`(独立文档)| **不与账号池同放** —— 避免多 profile 互相覆盖 | | Token 账本 | `~/.dsh/jet-hub/token-ledger.json` | 历史日聚合 | ### 常见环境变量 | 变量 | 默认 | 说明 | |---|---|---| | `DSH_HIDE_MODELS_WITHOUT_ACCOUNT` | 关闭 | 设为真值时,**没有已登录账号的渠道**在模型选择器里整体隐藏 | | `DSH_OPENAI_GATEWAY_ENABLED` | `1` | 设为 `0` / `false` / `off` / `no` 时完全不启动网关 | | `DSH_OPENAI_GATEWAY_HOST` | `127.0.0.1` | 网关监听地址。容器/端口映射场景填**容器自身 IP**(`hostname -i`);填 `0.0.0.0` / `::` 等通配地址会被拒绝并跳过启动 | | `DSH_OPENAI_GATEWAY_PORT` | `8326` | 网关监听端口 | | `DSH_OPENAI_GATEWAY_API_KEY` | 自动生成 | 网关 Bearer 密钥 | | `DSH_REASONING_LOOP_GUARD` | 开启 | 思考死循环守卫总开关 | | `DSH_REASONING_LOOP_AUTO_RESUME` | 开启 | 中止后是否自动补一句「继续」并重跑 | | `DSH_REASONING_LOOP_AUTO_RESUME_MAX` | `2` | 连续自动续跑上限(最大 `10`)| | `DSH_TRAE_CHANNELS` | `solo_work_lite,solo_agent_remote` | TRAE 要拉取的通道,**顺序即优先级** | | `DSH_TRAE_MAX_COMPLETION_TOKENS` | `64000` | TRAE 单次输出上限(设 `0` 关闭收敛)| 更多 provider 专属变量(如 `DSH_ZCODE_INTERNAL_CARRIER`、 `DSH_TRAE_ROTATE_MACHINE_ID` 等)见内部文档。 --- ## 账号备份与迁移 Jet Hub 设置页底部的**备份**区提供导出 / 导入,用于两种情况:**换 DSH 版本时整体迁移**, 以及**从别处单独搬一个账号进来**。 备份文件是**自包含**的 JSON(账号索引 + 凭据原文 + 模型黑名单 + 永久积分锁定表), 与 DSH 版本无关 —— 导入时按**当前版本**的存储契约重建,因此天然跨版本。 - **加密在浏览器侧完成**(PBKDF2 + AES-GCM),RPC 只收发明文载荷,明文与口令 都不经过宿主持久化与日志。可选加密,不勾选即导出明文。 - 账号凭据、`refresh_token` 都在文件里,**等同于明文密钥**,请自行妥善保管。 - ⚠️ **不含的就是本机偏好**:备份只搬账号与凭据,**不搬**本机 OpenAI 网关开关、 用量徽标偏好、自动签到记账等本机状态(导入时它们保持当前值,不会被备份覆盖)。 账号条目上的字段(含 OpenCode 的 `opencodeProxy` / 指纹代次)则原样随快照往返。 ### 导出:可以只导出选中的账号 导出弹窗列出当前账号池并可**逐个勾选**(`backup.export` 的 `accountIds`), 默认「全部账号」。只导一个账号时文件名会带上该账号 id(`dsh-codearts-backup-buddy-xxxx-日期.json`), 避免被误当成整份备份收着。 ⚠️ **过滤只作用于 `accounts` 与 `credentials` 两个字段**:模型黑名单与永久积分锁定表 仍按**全量**写出。这样「只导一个账号」的产物依旧是**一份可整体还原的自包含快照** —— 让过滤只承担一种语义,两条路径就不会互相污染。 ### 导入:两种模式(`replace` / `merge`) | 模式 | 语义 | 用于 | |---|---|---| | `replace`(默认) | **整体替换**账号池与模型黑名单,还原快照 | 「恢复」按钮、换 DSH 版本后整体迁移 | | `merge` | **只追加**:已存在的账号一律跳过,**不碰**模型黑名单与锁定表 | 「单独导入一个账号」、从别人的备份里挑账号 | 导入弹窗可勾选要落地哪些账号,并选择模式。 ⚠️ **`merge` 下裁剪发生在发请求之前**(`plugin-src/client/backup-payload.js`)。 判重虽然在后端,但后端只知道「这份载荷里有哪些账号」,不知道用户勾了哪几个; 不先裁剪的话,从整份备份里只勾一个账号,其余账号仍会被追加,更糟的是备份里那些 **不在勾选范围内**的 `credentialRef` 可能已被本机现有账号占用,写进去等于 **覆盖现有账号的凭据**,与「只增加不覆盖」的承诺相反。故裁剪必须同时裁掉 `accounts`、`credentials` 与 `disabledModels` 三处。 ### ⚠️ 合并导入判重的三层口径(缺一层就会堆出重复账号) `src/backup.ts` 的 `importBackup({ mode: 'merge' })` 依次判: 1. **池里已有的 `id`** —— 账号的唯一键; 2. **池里已有的身份**(`provider:` + 凭据里的身份字段,见下表); 3. **备份文件内部重复的 `id`** —— 同一份文件手工编辑后可能出现两条同 id。 ⚠️ **只按 `id` 判重是不够的,第 2 层才是关键**:插件自己登录时 `id` 由 `shortId()` **随机**生成(`buddy-a1b2c3d4`),而从第三方文件转换来的 id 是**确定性哈希** (见下节)。同一个人因此有两套 id,只比 id 会把他重复加进池里 —— 而用户在 「从别人的备份里挑账号」时,最常见的情形恰恰是「这个号我已经手登过了」。 ⚠️ **身份字段逐 provider 对齐**(漏一个字段,那个 provider 的「同一个号我已手登过」 场景就**必然**多一条重复账号): | 字段 | 哪些 provider 的凭据里有 | |---|---| | `user_id` | CodeBuddy / WorkBuddy / LobsterAI / Raccoon / ZCode | | `uid` | Qoder(含中国版)/ TRAE | | `userid` | Loomy | | `account_id` | Cline / MiniMax | | `sub` | Gemini(OIDC `sub`;**不取 `email`** —— 它会随改邮箱而变) | | `api_key` | OpenCode(**仅账号槽**) | ⚠️ **Gemini 只认 `sub`,不认 `email`**:两者都在它的凭据里,但 `sub` 是 OIDC 的 **稳定主体标识**(换邮箱、改昵称都不变,上游就拿它当账号 id),而 `email` 会随用户 改邮箱而变 —— 拿它判重会让同一个账号在改邮箱后多出一条重复账号。 ⚠️ **OpenCode 的匿名通道要排除**:它的凭据只有 `api_key`,而匿名通道的 `api_key` 恒为字面量 `public` —— 全部匿名槽共用一个值。若拿它当身份,用户刻意添加的 **第二条匿名通道会被判成「已存在」而拒绝导入**(「多条匿名通道各走各的出口」 是上游明确提供的能力)。故匿名槽不产生身份,退回按 `id` 判重。 ⚠️ **轮换令牌(`access_token` / `refresh_token`)不能当身份**:它们每次续期都会变, 拿来判重会让续期后的同一账号被判成新账号。 ⚠️ **`id` 与身份都可能判不出来**:凭据缺失/损坏时按「无法判断」跳过该条, 不让一次判重失败阻断整体导入。 ### ⚠️ 第三方账号列表可以直接导入(自动转换) 很多用户手上的是**别的工具**(账号池 / 卡号池脚本、IDE 本地缓存导出)产生的 CodeBuddy 账号列表,而不是本插件备份。两者结构完全不同: | | 第三方账号列表 | 本插件备份 | |---|---|---| | 顶层 | **裸数组** `[ {...} ]`(或 `{ accounts / data / items / list: [...] }`) | 对象 `{ format, version, ... }` | | 格式标记 | 无 | `format: 'dsh-codearts-auth/backup'` | | 凭据 | 账号对象里**直接带** `access_token` / `refresh_token` | 独立 `credentials` 字典(ref → 凭据 JSON **原文字符串**) | | 账号索引 | `id` 是 uid 或工具自己的编号 | `accounts[]`,`id` 形如 `buddy-a1b2c3d4` | 直接把它丢给「恢复」只会得到「该备份文件里没有账号」—— 因为取不到 `accounts`。 故选中这类文件时会**自动识别并转换**(`plugin-src/client/backup-convert.js`), 转换结果显示在弹窗里供核对,并**默认落到 `merge` 模式**(别人的新号,避免误覆盖本机账号)。 ⚠️ **转换只改结构,不改写令牌**:`access_token` / `refresh_token` 原样搬运。 其余字段以「本机一个真实可用的 CodeBuddy 凭据」为基准对齐: | 字段 | 来源 | |---|---| | `expires_at` | 原值(**毫秒**时间戳),统一成字符串毫秒 | | `refresh_expires_at` | 优先 `auth_raw` 的 `refreshExpiresIn - expiresIn` 差值补算,退回 refresh_token 的 JWT `exp` | | `domain` | **access_token 的 JWT `iss` 主机名**(它会被当作 `X-Domain` 头发出去) | | `user_id` | access_token 的 JWT `sub` | | `token_type` / `account_type` | 原值,缺省 `Bearer` / `personal` | | `enterprise_id` | 缺省 `''`(个人版为空串,与真实凭据一致) | ⚠️ **provider 按 JWT `iss` 的主机名分派**:含 `workbuddy` → 国际版,否则 → `buddy`。 两套 provider 是**不同路由**,认错会让凭据发到错误的端点。 ⚠️ **账号 id 由 uid 经 FNV-1a 哈希确定性生成**(`stableShortId`),与插件自己登录 生成的形式一致。这样**同一份文件导入两次,第二次会全部被判为「已存在」而跳过**, 而不是堆出两份账号。但 id 判重挡不住「同一个人的账号已用随机 id 登录在本机」—— 那正是靠上文第 2 层的身份字段判重。 ⚠️ **不写 `scope`**:真实凭据里有它,但插件**从不读取** `scope` 发请求 (只在续期时原样带回),而第三方文件里也没有 —— 与其猜一个可能不准的值,不如省略。 ### ⚠️ 合并导入只写「真正要新增的账号」的凭据 `merge` 模式下**绝不回写备份里其余凭据**(含各 provider 的默认单凭据 ref)。 `replace` 模式才按字典逐条写入 —— 备份文件里可能含账号池之外的独立凭据 ref, 导出时一并纳入,导入时同样还原。 ⚠️ 写入顺序是**先凭据、后账号池**:账号池写入成功后门控(`hasLoggedInAccount`) 与黑名单立即反映新状态;凭据写入中途失败(非法 ref 等)只跳过该条、不中断整体。 ⚠️ **模式必须取白名单**(`req?.mode === 'merge' ? 'merge' : 'replace'`), 不能把请求里的任意字符串透传下去:未知值会被 `?? 'replace'` 静默当成整体替换 —— 那时用户以为只是「加一个账号」,实际账号池被清空,属于**不可逆的数据损失**。 ### 测试 - `tests/unit/backup.spec.ts`(44 条)—— 选择性导出、合并导入三层判重、 `merge` 不回写其余凭据、`replace` 兼容旧行为、各 provider 的身份字段覆盖度; - `tests/unit/backup-payload.spec.ts`(13 条)—— 载荷裁剪与账号摘要; - `tests/unit/backup-convert.spec.ts`(28 条)—— 第三方列表识别与转换; - `tests/unit/account-pool.spec.ts` 的 `mergeAccounts` 段(6 条)—— 只追加、 不改黑名单与锁定、坏条目归一化、文件内重复 id。 ### ⚠️ 与上游同步(2026-10-02) 本章功能已随上游 62 个提交(新增 OpenCode provider、本机 OpenAI 网关、用量徽标、 自动签到、供应商开关)重新落地:只有 `plugin-src/client/jet-hub.js` 的 import 块 需要手工合并,`BackupPanel` 与 `src/backup.ts` 上游未改动。唯一的适配点是 **OpenCode 的身份字段**(见上表与匿名槽说明)。细节与反向验证记录见 `AGENTS.md` 的「上游同步记录」一节。 ⚠️ 已做**反向验证**(2026-09-30):禁用身份判重 → 1 条变红;让 `merge` 回写全部 凭据 → 3 条变红;`mergeAccounts` 去掉文件内去重 → 1 条变红。 ## 开发 ```sh pnpm test # 单元测试(快速,无网络,全部 mock) pnpm typecheck # 只做类型检查 pnpm build:all # 完整构建(宿主 tsc + 资源拷贝 + 客户端 esbuild) ``` ### 构建 - `pnpm build` — 用 tsc 将 `src/` 编译到 `lib/`。插件**宿主侧**入口是 `lib/index.js`。 - `pnpm build:client` — 用 esbuild 将 `plugin-src/client/` 打包为 `lib/client/jet-hub.js`(Jet Hub 设置页的客户端 bundle)。 - `pnpm build:all` — 依次执行上述步骤,是完整的构建。 - `pnpm typecheck` — 只做类型检查,不产出文件。 `lib/` 已被 gitignore,因此构建是安装或运行前的必需步骤。 ### E2E 测试 E2E 探针**按 provider 分列**(如 `pnpm test:e2e:codearts`、`pnpm test:e2e:buddy`、 `pnpm test:e2e:loomy`),**均有闸门、默认全部跳过** —— 需要显式设置对应环境变量 (部分还需 `..._CONFIRM=yes`)才会真正发出请求。详见 `tests/e2e/README.md`。 ⚠️ 带 `-claim` / `-chat` 后缀的探针会**真实消耗额度或领取机会**,运行前请确认。 ### 项目结构 | 路径 | 说明 | |---|---| | `src/` | TypeScript 源码(宿主侧) | | `plugin-src/client/` | Jet Hub 客户端源码(esbuild 打包) | | `lib/` | 编译产物(已 gitignore) | | `tests/unit/` | 单元测试 | | `tests/e2e/` | 端到端测试(默认跳过) | | `docs/agent-notes/` | 逐 provider 的事故史与约定(开发向) | | `docs/implementation-notes.md` | **全部实现细节与协议笔记(不入库)** | | `cordis.patch.yml` | DSH bundle 补丁 |