Vai al contenuto principale
E

dsh-connect-qoder

eghrhegpe/dsh-connect-qoder

将本机已登录的 Qoder(国内版 Qoder CN / 国际版 Qoder)模型接入 DeepSeek Harness —— bring locally signed-in Qoder models into DeepSeek Harness with zero configuration.

Installazione

dsh plugin --profile web add github:eghrhegpe/dsh-connect-qoder

README

DSH Connect Qoder

把本机已登录的 Qoder 模型接入 DeepSeek Harness, 零配置即可在 DSH 的模型选择器里使用你的 Qoder 账号额度。

国内版 Qoder CN 与国际版 Qoder 是两个并行的 provider(qoder-cn / qoder), 装哪个就出现哪一组模型,两个都装就两组并存,各自使用自己的账号与额度。

工作原理

DSH PiAiAdapter(每个区域一套)
  -> 安全 loopback shim(随机端口 + 进程内随机 secret)
  -> COSY 签名 + 自定义 base64 编码
  -> 国内版 https://gateway.qoder.com.cn/
  -> 国际版 https://api3.qoder.sh/
  -> Qoder 双层包装 SSE
  -> OpenAI SSE
  -> DSH 本地执行工具并回传结果

Qoder 不是 OpenAI 兼容端点,所以需要三样东西:

  1. COSY 签名头 —— 每个网关请求都带 Cosy-* 头与 Authorization: Bearer COSY.<payload>.<sig>, 签名是对 base64 负载、RSA 包装的 AES 密钥、时间戳、请求体与签名路径做 MD5。
  2. 置换 base64 请求体 —— 查询串里的 Encode=1 表示 JSON 体要先 base64、再按换过的字母表 逐字符替换、最后按三分之一旋转。
  3. 双层 SSE —— 每个 data: 帧是一个信封对象,它的 body 字段本身又是 JSON 字符串, 里面才是 OpenAI 风格的 chunk。

凭据来源

插件复用 Qoder 桌面应用自己的登录状态,不启动额外的 OAuth 流程,也不写入应用的文件 (凭据文件以只读方式打开)。

Qoder 把登录信息放在 Chromium OSCrypt 格式的凭据文件里(v10 + nonce + 密文 + tag,AES-256-GCM)。 新版(0.3.x)的凭据直接保存在 <userData>/auth.v1.dat,旧版(0.2.x 及更早)则放在 VS Code 风格的 state.vscdb SQLite 数据库里。两种布局都支持,新版优先尝试。 密文用的密钥保存在应用的 Local State 中,由操作系统 keystore 包裹 —— Windows 上是当前用户 作用域的 DPAPI,因此同一用户下的进程都能解开它。 Node 没有内置 DPAPI 绑定,这一步交给 PowerShell,并通过临时文件交换结果(不使用管道), 这样在禁止管道 stdio 的沙箱里同样可用。

没有桌面应用登录时,可以用官方文档的 PAT 兜底:设置 QODERCN_PAT(国内版)或 QODER_PAT(国际版),插件会用它换取 job token。

平台边界:零配置路径只在 Windows 成立。 解密链路是 PowerShell + DPAPI(Crypt32.dll), lib/ 里没有任何 macOS / Linux 分支——在其它平台上应用凭据永远探测不到(loadCredential 返回 undefined,该区域不注册),只剩上面的 PAT 兜底。CI 的 Ubuntu 绿灯说明测试在那边 能跑,不等于零配置在 Linux 上存在。

安装

从 npm 安装(推荐)

dsh plugin --profile web add @eghrhegpe/dsh-connect-qoder

从 DSH 市场安装(DSH 桌面用户)

本仓库是 hdhgsysh/dsh-connect-qoder 的 fork。

可在 DSH 的「插件市场」里粘贴这个源:

github:eghrhegpe/dsh-connect-qoder

注意:npm 上不带 scope 的 dsh-connect-qoder 由上游发布,装到的是上游那份; 本 fork 自己发的是 scoped 包 @eghrhegpe/dsh-connect-qoder(上面那条),二者不是同一个包。

或本地开发模式:

dsh plugin --profile web add <本仓库路径>

安装后需要重启 DSH 进程:bundle 的 patch 在启动时读取。

凭据安全

插件不存储任何凭据:

  • 复用本机已登录的 Qoder 桌面应用的凭据文件(新版 auth.v1.dat / 旧版 state.vscdb, 均以只读方式打开,不做任何写入)。
  • 进程内随机 bearer token 绑定 loopback 端口;Qoder 真实的 RSA 包装 key 与 session key 不会离开本插件的沙箱。

已知行为

  • 始终思考的模型:部分模型(如 GLM-5.3-Flash、Kimi-K3)声明了推理档位但不允许关闭思考, 对它们发送 enable_thinking: false 会被上游以 provider_error 1210 拒绝。插件从模型目录 识别这类模型并完全省略该字段,让模型使用自己的默认档位。
  • 上游错误可见:上游失败时返回的是普通 200 帧里的错误对象,而不是 chunk。插件把它翻译成 一条可读的错误,而不是让用户看到一个空的助手回合。
  • 国际版额度:国际版的试用额度可能已用尽(isQuotaExceeded),此时目录请求会返回 403 Login expired,该区域就不会显示模型;国内版不受影响。
  • 账号状态四档与卡片重读:插件入口读不出登录凭据的区域不注册 provider(启动日志会说 原因)。卡片顶部的「当前账号」面板收敛成版本条(对齐 WorkBuddy 的 tab 条):每个 区域一个胶囊——状态点(ok 绿 / expired 红 / needs-app 琥珀 / signed-out 中性)+ 区域名 +「模型」开关;点哪个胶囊,账号详情、用量与模型列表都只看那一版,区域名因此 在卡片上只出现一次(模型行的区域 badge 已随之移除)。身份详情只带 name / email / 到期日,不带任何凭据(判定在 lib/account-state.js,纯本地证据、可注入可直测)。 两个按钮各管一件事:「重读登录」 让宿主丢弃凭据缓存、重读应用存储,并把启动时未能上线的区域现在就上线(重建并重新 注册 adapter,失败时回滚到原注册,不影响已在服务的区域)——重新登录后不必再重启 DSH; 「在线确认」是账号流程里唯一的联网调用(fetchUserInfo),回答「上游现在还认不认 这个登录」,失败按 classifyUpstreamError 分档(sign-in-expired / 其他)。
  • 每一版的模型可以单独关掉(enabledRegions,版本条上的「模型」开关,对齐 WorkBuddy 的 per-tab 供应商开关):取消勾选的一版向 DSH 提供零个模型,它的 模型组按「空目录即隐藏」的同一条规则从选择器消失,但登录、用量与模型筛选全部 保留,重新勾选即恢复,无需重启 DSH;卡片模型列表与选择器用同一个 regionEnabledFor 谓词过滤,两个界面不会打架。
  • 依赖 Qoder 客户端接口(非官方开放 API),Qoder 更新后插件可能需要随之调整。
  • 设置命名空间由宿主决定,不能自选(0.1.7 起):describe() 用 Loader 条目 id 作为 ns(本 bundle 是 llm-qoder,不是 dsh-connect-qoder),而「设置 → 模型」 页按精确匹配查 namespaces.get(entry.settingsNs)。若插件宣告的命名空间与宿主实际 服务的不一致,该 provider 会被判为「未配置」,整行从页面上消失——不报错、不灰显, 而插件本身仍在正常注册和应答,非常难查。因此 settingsNs 一律经 settingsNamespaceOf(ctx) 从 ctx.fiber.entry.options.id 推导,常量只作为宿主不暴露 条目 id 时的回落值(对齐 WorkBuddy 2.1.0 的同款修复)。
  • 设置保存走插件的 __save 主机端点 + 读回校验(对齐 WorkBuddy 0.1.7 修复):DSH 0.1.7 的 客户端 settingsScope.set() 在原子写重试耗尽后会静默返回成功而不落盘,卡片改为「主机端点 权威写、本地写仅作镜像、写后读回确认」,保存按钮只会显示真实结果。在宿主未为本插件注册设置 命名空间行的环境(如 link: 开发安装)下端点会 503,卡片如实显示失败而不是假"已保存"; 正式(registry)安装下该端点可用。
  • 每日签到:Qoder 每天 10:00(UTC+8)给每个账号发一轮 CLAIM_BENEFIT 活动,一轮只能领一次, 重复领取是幂等的(replayed,不再发放)。卡片在「我的用量」里显示一行签到 + 三态按钮 (立即签到 / 签到中… / 今日已签到)——只有你点才会领,插件不会自动签到。每次点击都先重读一次 活动列表取当轮 id,绝不复用渲染时的旧 id(一天一轮,旧 id 就是上一轮的);领取后用量缓存作废 重读,因为 Credits 落在同一块面板显示的 Add-on 资源包里。两个过滤器是照真实返回加的:同一账号 下的另一个 VIEW_DETAILS 活动同样带着 claimStatus: "CLAIMED" 却没有 benefit,只按状态挑会对 它去领;而列表顶层的 claimable 在「已领过」和「没有活动」时都是 false,不能当判据。 按钮文案落到 addOnQuota 旁边,是因为国内版免费层的计划额度为 0,签到攒的资源包是它唯一的额度。

目录

文件作用
lib/credentials.js从 Qoder 应用读取并解密登录凭据
lib/upstream.jsCOSY 签名、请求体编码、目录与对话流
lib/shim.js面向 pi-ai 的 OpenAI 兼容回环端点
lib/adapter.jspi-ai provider 与 PiAiAdapter profile(被关的 provider 以零模型组呈现,由 DSH 自行隐藏)
lib/catalog-entry.js目录条目的归一化、模型过滤(含按区域开关)与卡片行投影(无 peer 依赖)
lib/catalog-store.js目录的磁盘缓存与原子落盘(无 peer 依赖)
lib/credential-cache.js凭据缓存与「登录失效后重读」规则(无 peer 依赖)
lib/account-state.js每区域账号状态四档判定(ok / expired / needs-app / signed-out;纯本地证据、不含凭据,无 peer 依赖)
lib/settings-save.js设置命名空间的解析(0.1.7 由宿主推导,插件不能自选)、设置写入、按区域合并与落盘读回校验(无 peer 依赖)
lib/pi-model.jspi-ai 模型描述符的构造(纯函数,无 peer 依赖)
lib/preferences.js四个设置项的读取与 volatile 解包(enabledRegions 区域开关:缺失/非对象一律读作开启,只有显式 false 才关)
lib/offpeak.js错峰窗口与费率算术(无 peer 依赖)
lib/single-flight.js同类异步任务的并发合并:刷新在途时,后来的调用并入同一次请求(目录/用量刷新用,无 peer 依赖)
lib/claim.js每日签到:当轮活动的挑选、可领状态判定与领取结果的归一化(纯函数,无 peer 依赖)
lib/index.js按区域注册 provider 的插件入口,与模型/用量/保存/账号状态路由(账号路由含「重读登录」的上线与回滚)

lib/client.js 是注入到宿主设置页的那张卡片的构建产物(react 由宿主提供,产物不打包它)。 它的源码在 src/client/,npm run build 从源码重建它:

| src/client/paths.ts | 卡片用到的五条插件路由 | | src/client/styles.ts | 卡片样式与 installStyles(dsm-* 一套与 dsh-connect-workbuddy 逐字一致,原因见文件头) | | src/client/settings-write.ts | 「写入后读回校验」的浏览器半边 | | src/client/copy.ts | 卡片文案(中/英) | | src/client/card.ts | 卡片的纯函数与五个组件(QoderPluginCard / QoderUsagePanel / QoderAccountPanel / RegionUsage / QuotaBlock) | | src/client/index.ts | 注册入口(apply / inject / name) |

这些源码不是原始手稿,是还原出来的:2026-09 用 docs/history/restore-client-src.mjs 把当时的 产物按 //#region 标记机械切分而成,模块边界来自产物,card.ts 那一段在产物里没有标记、 是按引用关系推断的。还原后做过一次对拍——13 个纯函数 × 420 组输入共 5460 次调用,新旧产物的 返回值与抛错逐条一致(方法记录在 docs/issues/17-client-source-restore.md)。

产物是构建输出:改源码重建,不要手改产物。test/client-bundle.test.js 从产物里提取并 执行卡片的纯函数,所以产物一改那里的断言就得跟着看一眼。

测试

npm run verify          # 下面三条串起来,全过才算过
npm test                # node --test "test/*.test.js"
npm run test:coverage   # 同上 + 覆盖率门槛(行 68 / 分支 85 / 函数 66,跌破即失败)
npm run verify:deploy   # 比对已部署副本与本仓库,报告漂移
npm run build           # 从 src/client 重建 lib/client.js

npm run verify 跑三件事,每件回答一个不同的问题:

步骤回答什么全过时的输出
npm test卡片逻辑与宿主半边没被改坏# pass 269 / # fail 0
build --tsdown(不写)lib/client.js 确实由 src/client/ 生成MATCH: … byte-for-byte identical
verify:bundle重建产物与 HEAD 的行为一致behaviour: IDENTICAL

中间那条是最要紧的:它不写产物,只比较。它过了,就说明产物不是手抄进来的副本——改 src/ 而产物不变的情况会在这里红。

绿灯不等于门禁有效。 这三道门禁每条都用故意的破坏验过:build 缺关键串时会拒绝写入 (第一次构建摇掉全部模块、只剩 84 行,bundler 仍然退出 0);verify:bundle 把探针下界从 Math.max(0, …) 改成 1,就会在 formatCountdown|undefined|0 上报出 00:00:00 → 00:00:01 并以非 0 退出。怀疑门禁时照这个法子再破一次,比看它绿不绿有用。

npm run build 需要构建器(tsdown),它是 devDependency,跑之前先 npm install。 测试仍然不需要安装任何东西(裸 node --test),两者是 CI 里分开的两个 job:一个证明 测试无依赖,一个证明产物可重建。

对拍覆盖不到渲染:JSX 被 stub 成 null,所以改了 UI 仍然要在浏览器里看一眼(展开卡片 → 切区域 → 改图像档位 → 保存 → 看错峰倒计时)。

.npmrc 里的 legacy-peer-deps=true 是必需的、不是随手加的:本包的 peer 依赖是 @deepseek-ai/*,由宿主在运行时提供,不在公共 registry 上,npm 自动安装 peer 会在装到 devDependencies 之前就失败。

verify:deploy 存在的理由和上面那些测试一样:一台机器上可以同时装着好几个版本的本插件。 link: 安装是指向本仓库的符号链接、永远最新;市场安装是复制,停在安装那一刻, 而且两边 package.json 的 version 一样——任何按版本判断新旧的升级路径都会认为「已是最新」。 该脚本比对 lib/** 的内容哈希、文件清单,以及三个由真实缺陷换来的标记 (错峰 active 门、账号状态模块、账号路由),并把「版本号相同但内容不同」单独标出来。 test/deploy-drift.test.js 用假目录树钉住这套判定。

CI 在 Node 22.19 / 24 × Ubuntu / Windows 上跑(.github/workflows/test.yml)—— Windows 不是冗余:shim 绑定回环监听、目录缓存依赖 rename 覆盖、凭据读取要调 PowerShell,这些在别的平台上行为不同。

测试只用 Node 内置的 node:test,不需要安装任何依赖——也不需要安装 peer 依赖, 这是刻意的:lib/ 中凡是纯逻辑的部分都放在无 peer 依赖的模块里(见上表), 这样它们才能被直接 import 并断言真实的实现,而不是在测试里手抄一份。

几个文件存在的理由,都是因为曾经出过问题:

  • test/catalog-fields.test.js 与 test/model-row.test.js —— 错峰机制曾因 promotion.active / promotion.timezone 在 Host 投影时被丢掉而全程哑火, 而当时的测试是绿的,因为它测的是自己手抄的副本。同一个毛病在客户端又犯过一次: 卡片自己算窗口时漏看 promotion.active,把拿不到的折扣价显示给用户, 而模型选择器按 before 价计费,两个界面自相矛盾。 model-row.test.js 现在逐状态对拍两个门控;client-bundle.test.js 更进一步, 直接从产物里提取卡片的 offPeakState 并执行——删掉那行门控会让它变红, 而只会让 model-row.test.js 保持绿色。副本不是防线。
  • test/credential-cache.test.js —— 「重新登录无需重启」这条卖点的完整链路: 网关拒绝 → 谓词判定 → 置失效标志 → 下次请求重读。此前只有两端被测。
  • test/credential-invalidation.test.js —— 上面那条链路上的两个纯谓词。
  • test/account-state.test.js —— 账号状态四档判定(lib/account-state.js): 全注入的存储读器 + 真实临时目录跑目录存在性检查,钉住「判定只信本地证据」 与「状态记录不含任何凭据材料」两条不变量。
  • test/errors-classify.test.js —— 105 与 10605 的优先级决定了「提示用户重新登录」 还是「排队等待」,两者弄反的代价完全不同。
  • test/settings-save.test.js —— DSH 0.1.7 上 set() 会静默成功而不落盘; 这段代码用「写入→读回→深比较」把假成功变成显式失败,测试里直接模拟 「mutate 成功但文档没变」的那个场景。也钉住了两件靠肉眼会漏的事: 命名空间只做全等匹配(llm-qoder-extra 不算我们的),字段白名单用 Object.hasOwn(constructor 不是一个字段)。
  • test/pi-model.test.js —— toPiModel 抽离成纯函数后的直接覆盖: compat.supportsDeveloperRole: false 与「不声明 maxTokens」这两处, 失效时每个请求都会 403,或长回复被截成 finish: max-tokens。
  • test/catalog-store.test.js —— 目录缓存的原子落盘,用真实临时目录跑, 在 CI 所在的平台上实测 rename 覆盖行为,而不是在注释里假设。
  • test/shim.test.js —— 回环端点的鉴权与 /v1/models 过滤;对着真实 HTTP 服务器说话,Host 头用裸 socket 发送(fetch 禁止设置该头)。
  • test/oscrypt.test.js —— 凭据解密往返;夹具用真实 AES-256-GCM 构造, key 是固定哈希,失败可复现。
  • test/upstream-protocol.test.js —— 编码与签名。两个盲区是原理上不可测的, 已在文件头写明。
  • test/upstream-messages.test.js —— 消息与工具调用翻译,注释里自称 「最重要的一件事」,此前零覆盖。

尚未覆盖的部分集中登记在 docs/KNOWN_GAPS.md,不在各文件里重复叙述—— 重复三处正是「手抄副本」那类问题的文档版。

免责声明

仅供个人学习研究使用,仅驱动使用者自己的 Qoder 账号在本机调用。使用者需遵守 Qoder 的服务条款, 因使用本项目产生的后果由使用者自行承担。本项目与 Qoder、DeepSeek 均无关联。

插件除「每日签到」外的所有请求都是只读的;签到是它唯一会对 Qoder 账号产生变更的操作(领取 Qoder 正在向你自己的账号发放的额度),它不会自动执行,每次都要手动点击。是否使用它、以及它是否符合 Qoder 当前的服务条款,请自行判断。

许可证

MIT

Plugin correlati