DSH WorkBuddy Connect
corrinehu/dsh-workbuddy-connect将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness,在 DSH 对话窗口里零配置使用。
注册 Qoder 中国版与国际版的语言模型到 DSH,自动获取每个模型的费率,随错峰时段变化。并提供每天签到、用量胶囊与积分查询功能。需要本机Qoder的登录态,或浏览器登录一次。
把本机已登录的 Qoder 模型(国内版 / 国际版)接入 DeepSeek Harness,零配置用你的账号额度。
把本机已登录的 Qoder 模型接入 DeepSeek Harness, 零配置即可在 DSH 的模型选择器里使用你的 Qoder 账号额度。
国内版 Qoder CN 与国际版 Qoder 是两个并行的 provider(qoder-cn / qoder),
装哪个就出现哪一组模型,两个都装就两组并存,各自使用自己的账号与额度。
输入框旁的用量胶囊:工具栏上的 Qoder CN · 2,222 是区域前缀加主额度剩余 Credits,
点开就是同一份用量面板——套餐内额度与剩余、今日经本客户端的请求数与 token 消耗(含缓存命中)、
7 天用量趋势,以及今日签到状态——与 Plugins 页卡片同款(同一套读数与阈值)。
名册里出现了本机此前没记过的模型时,胶囊会多出第三段 · 3 新模型(没有就不画,宽度与从前
一致),点开面板能看到名字。这个「新」的口径是本机第一次见到,不是上游的上架时间:目录里
没有这个字段,插件能证明的只有「本机上次记下的名册里没有它」。所以首次运行只播种不宣布,
只列最近 7 天内首见的,见 docs/issues/25。

DSH PiAiAdapter(每个区域一套)
-> 安全 loopback shim(随机端口 + 进程内随机 secret)
-> COSY 签名 + 自定义 base64 编码
-> 国内版 https://gateway.qoder.com.cn/
-> 国际版 https://api3.qoder.sh/
-> Qoder 双层包装 SSE
-> OpenAI SSE
-> DSH 本地执行工具并回传结果
Qoder 不是 OpenAI 兼容端点,所以需要三样东西:
Cosy-* 头与 Authorization: Bearer COSY.<payload>.<sig>,
签名是对 base64 负载、RSA 包装的 AES 密钥、时间戳、请求体与签名路径做 MD5。Encode=1 表示 JSON 体要先 base64、再按换过的字母表
逐字符替换、最后按三分之一旋转。data: 帧是一个信封对象,它的 body 字段本身又是 JSON 字符串,
里面才是 OpenAI 风格的 chunk。插件有三个凭据来源,按优先级依次尝试(桌面应用优先):
$DSH_HOME/.qoder-auth.<region>.json。这是没装桌面端(或不想装)的机器零配置登录的路径 ——
详见 凭据安全 与 docs/issues/21-device-login-credential-source.md。QODERCN_PAT 等)。最后的兜底,需要用户自己到后台造一个长期令牌。桌面端那条过期了就不算数(2026-10-20):过期的 access token 什么都干不了——每个带它签名的
请求都会被拒,这个版本本身也不会被注册(region-gate 直接按"登录已过期"跳过发布)。所以它
不再压住后面两条:插件会继续往设备登录、再往 PAT 找一条还能用的凭据。只有三条都拿不出
东西时,那条过期的桌面端登录才作为"本机真实发生过的事"被报出去,而不是被一句含糊的"没登录"
盖过去。
无论走哪一条,都不写入应用的文件:桌面端的凭据文件始终以只读方式打开。
卡片按"真正在服务这个版本的是谁"说话(2026-10-20):宿主把桌面端登录与插件自有登录并列
上报(两条凭据两种寿命,不合并),但账号卡要回答的其实只有一个问题——"这个版本登录了吗"。
所以当插件自有凭据在服务、而桌面端那条不在服务(过期、读不到、或只是环境变量 PAT 在服务)时,
这个版本读作**"已登录"**,并配一句"当前用的是插件自有登录(浏览器登录)——不需要桌面端
客户端";此时那条"本地登录信息读不到"的红字不再出现——它描述的是桌面端,不是这个版本能不能用。
只签浏览器登录的用户也因此第一次有了「在线校验登录」按钮可点(以前按钮只在 source 有值时
出现,而插件自有登录在账号行里没有 source)。
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),src/host/ 里没有任何 macOS / Linux 解包分支——在其它平台上应用目录
找得到(应用数据根目录已按平台解析,见下),但凭据读不出来(loadCredential 返回
undefined)。非 Windows 上的失败说的是真话:runUnwrap 在 spawn 之前就判平台,
记下的是"这个版本只在 Windows 上读得到登录",而不是一条在 Mac 上显示的 Windows 路径;
账号面板的提示也随之改为指向在哪个平台都成立的路子(先「浏览器登录」,再 PAT),
而不是让一个已经装了客户端的用户去确认"客户端在不在"。
但那些平台已经不再只有 PAT 一条路:插件自己的设备登录(上面「凭据来源」的第 2 条)
不读桌面端任何东西——在浏览器里批准一次,凭据就落在本机,平台无关。所以第 1 项 keystore
从"必须补"变成"可选"。诚实的限定:设备登录这条路径只在 Windows 上真机跑过(两区各
一次授权闭环,见 probe/qoder-device-login.mjs),它用到的
都是跨平台 API,但"在 macOS / Linux 上也确实能用"仍属下面第 2 项那类实机验证。
这不是"没有跨平台客户端",而是客户端已跨平台、插件尚未跟上:Qoder 桌面版在
macOS 12+ / Linux (.deb/.rpm) / HarmonyOS 上都有下载(qoder.com.cn/download),
但 src/host/ 只实现了 Windows 这一条解密链。缺口在两处,全部登记在
docs/KNOWN_GAPS.md 第 6 条(跨平台凭据链未实现):
security),
且 Chromium 在 macOS 上对 encrypted_key 还要做 PBKDF2-HMAC-SHA1 派生("peanuts" 常量 +
1003 次迭代)——与 DPAPI 直解是两种算法,不是换个命令;Linux 需 libsecret(secret-tool)
或 Chromium 在 Linux 上的 peanuts 硬编码 key 兜底。secret-tool 的 D-Bus session 都得在实机或 runner 上验。已修的两项(原缺口共四处):应用数据根目录现在按平台解析(Windows %APPDATA、
macOS ~/Library/Application Support、Linux $XDG_CONFIG_HOME/~/.config)——此前直接读
process.env.APPDATA,而该变量在 macOS 上不存在,于是探测得到空串、把"应用装着但读不出密钥"
谎报成"您没登录";MACHINE_OS 也补上了 darwin 档(本机实测过网关对
x86_64_darwin / aarch64_darwin 一律正常应答)。
剩下的是实机工作:service 名、PBKDF2 参数、Linux 的变体都必须实测确认,写出来就是盲代码。 macOS / Linux 用户现在可以走设备登录(零配置),或 PAT 兜底,或自行从本仓库移植解密链。
CI 的 Ubuntu 绿灯说明测试在那边能跑,不等于零配置在 Linux 上存在。
dsh plugin --profile web add @eghrhegpe/dsh-connect-qoder
本仓库是 hdhgsysh/dsh-connect-qoder 的 fork。
可在 DSH 的「插件市场」里粘贴这个源:
github:eghrhegpe/dsh-connect-qoder
github: 源是零配置的:宿主产物 lib/(host + client bundle)随仓库提交,克隆即可加载,
安装过程不需要批准构建脚本、也不需要拉 devDependencies。若 DSH 反而提示该包有待批准的构建
脚本,说明它拿到了一份缺 lib/ 的旧克隆——删掉重装即可(lib/ 自 docs/issues/19 起必须
入库,CI 有新鲜度门禁)。
注意:npm 上不带 scope 的
dsh-connect-qoder由上游发布,装到的是上游那份; 本 fork 自己发的是 scoped 包@eghrhegpe/dsh-connect-qoder(上面那条),二者不是同一个包。 npm 安装每次发布都会现场重建产物(prepack),同版本号下 npm 的产物比仓库提交里更新的 可能性更大——追新走 npm,github:源装的是对应提交时刻的产物。
或本地开发模式:
dsh plugin --profile web add <本仓库路径>
本地开发时 lib/ 对应的是提交时刻的源码:改过 src/ 后先
npm install && npm run build,再让 DSH 指向本目录,否则跑的还是旧产物。
安装后需要重启 DSH 进程:bundle 的 patch 在启动时读取。
来源不同,插件是否在本机留下东西也不同:
auth.v1.dat / 旧版 state.vscdb,不做任何写入),
不存储、不复制。解密主密钥会在 %TEMP% 短暂明文落盘,回收窗口见
docs/issues/02。$DSH_HOME/.qoder-auth.<region>.json,0600 + 临时文件改名写入(不会留下半写文件),
按 region 分文件(两个版本互不影响)。面板上的「退出插件登录」删的就是这个文件;它不影响
桌面端的登录,也不影响环境变量里的 PAT。令牌过期时插件用同一次的刷新令牌换新,换新失败
会在面板上说明原因,而不是装作还登录着。GLM-5.3-Flash、Kimi-K3)声明了推理档位但不允许关闭思考,
对它们发送 enable_thinking: false 会被上游以 provider_error 1210 拒绝。插件从模型目录
识别这类模型并完全省略该字段,让模型使用自己的默认档位。isQuotaExceeded),此时目录请求会返回
403 Login expired,该区域就不会显示模型;国内版不受影响。ok 绿 / expired 红 / needs-app 琥珀 / signed-out 中性)+
区域名 +「模型」开关;点哪个胶囊,账号详情、用量与模型列表都只看那一版,区域名因此
在卡片上只出现一次(模型行的区域 badge 已随之移除)。身份详情只带 name / email /
到期日,不带任何凭据(判定在 src/host/account-state.ts,纯本地证据、可注入可直测)。
两个按钮各管一件事:「重读登录」
让宿主丢弃凭据缓存、重读应用存储,并把启动时未能上线的区域现在就上线(重建并重新
注册 adapter,失败时回滚到原注册,不影响已在服务的区域)——重新登录后不必再重启 DSH;
面板每次渲染只读已缓存的密钥,不会同步起 PowerShell(那会阻塞整个 DSH 进程,最坏 30 s),
所以解不开的机器也能立刻看到"读不到"和原因;即便真的需要解包(点「重读登录」时)也是
异步等待——本机实测调用 29 ms 就返回、等待期间 DSH 照常响应别的请求,而同步版本会冻结
490 ms;「重读登录」是您主动点的,那一次是真读,不受缓存与失败窗口限制。
密钥缓存与 Local State 文件绑定,Qoder 重装换掉主密钥后插件会立刻重新解包,不必重启 DSH;
「在线确认」是账号流程里唯一的联网调用(fetchUserInfo),回答「上游现在还认不认
这个登录」,失败按 classifyUpstreamError 分档(sign-in-expired / 其他)。enabledRegions,版本条上的「模型」开关,对齐
WorkBuddy 的 per-tab 供应商开关):取消勾选的一版向 DSH 提供零个模型,它的
模型组按「空目录即隐藏」的同一条规则从选择器消失,但登录、用量与模型筛选全部
保留,重新勾选即恢复,无需重启 DSH;卡片模型列表与选择器用同一个
regionEnabledFor 谓词过滤,两个界面不会打架。probe/model-shape.mjs,两端真实账号):空分组用 [] 表达,
所以"0 个模型"是可信结果、必须落盘,而"分组键缺失/类型变了"才是格式变化。__save)、「重读登录」、「在线确认」,
以及设备登录那一组的三条写路由:「开授权页」、「取消」、「登出并删掉本插件存的凭据」。
逐条方法、能否改状态、谁在调用见 docs/ROUTES.md
(本仓路由的唯一清单;改路由必须同步那张表)。这些接口此前只按"来源主机名是不是
本机"判断,因此本机任意端口上的任意 HTTP 服务都能让它的网页代为调用(浏览器会挡跨域
读响应,但操作已经发生)。现在要求来源的地址与端口与请求实际拨到的地址一致
(比对请求自己的 Host 头,所以不写死端口、DSH 换端口也不用改),且必须是回环地址。
判定在 src/host/routes.ts 的 loopbackRequest,由 test/route-gates.test.js 覆盖。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 同修):DSH 0.2 代的
客户端设置 scope set() 在原子写重试耗尽后会静默返回成功而不落盘,卡片改为「主机端点
权威写、本地写仅作镜像、写后读回确认」,保存按钮只会显示真实结果。在宿主未为本插件注册设置
命名空间行的环境(如 link: 开发安装)下端点会 503,卡片如实显示失败而不是假"已保存";
正式(registry)安装下该端点可用。CLAIM_BENEFIT 活动,一轮只能领一次,
重复领取是幂等的(replayed,不再发放)。卡片在「我的用量」右侧放一张签到小卡片 + 三态按钮
(立即签到 / 签到中… / 今日已签到)——只有你点才会领,插件不会自动签到。每次点击都先重读一次
活动列表取当轮 id,绝不复用渲染时的旧 id(一天一轮,旧 id 就是上一轮的);领取后用量缓存作废
重读,因为 Credits 落在同一块面板显示的 Add-on 资源包里。两个过滤器是照真实返回加的:同一账号
下的另一个 VIEW_DETAILS 活动同样带着 claimStatus: "CLAIMED" 却没有 benefit,只按状态挑会对
它去领;而列表顶层的 claimable 在「已领过」和「没有活动」时都是 false,不能当判据。
卡片右上角大字是主机解析出的当轮额度(checkin.amount,来自活动自身的 benefit),领过之后
仍然显示——签到卡片因此不是「一行两段短文案占满整行」,而是与用量面板并排的一张卡;窄卡片下
自动换行到面板下方。额度落在 addOnQuota 里,是因为国内版免费层的计划额度为 0,签到攒的资源包
是它唯一的额度。GET /sash/api/v1/me/campaigns 把每日
CLAIM_BENEFIT 轮次按机器身份下发——请求须携带桌面应用 umid 服务的
Cosy-MachineToken/Code/Type 头,否则只回常驻的 VIEW_DETAILS 横幅,签到卡片按
active: false 设计不渲染。插件在 OpenAPI 请求上自动附加这组头(Windows 上定位安装
目录的 resources\umid\runtime-info.exe 读取,失败静默降级为原头组;国内版与 PAT
不受影响;发现与验证记录在 docs/KNOWN_GAPS.md 第 8 条(国际版 campaigns 端点按 umid 机器身份门控每日签到))。npm 包里只有 6 项:lib/、locale/*.json、cordis.patch.yml、icon.svg、README.md、
CHANGELOG.md(即 package.json 的 files;另有 npm 无论如何都会打包的 LICENSE)。
docs/、AGENTS.md、src/、test/、probe/、scripts/、assets/ 都在仓库里、不在包里
——registry 页面上点这些路径会 404,请到 GitHub 仓库看。本文的截图因此一律写成
raw.githubusercontent.com 绝对地址(相对路径在 registry 页面上是裂图)。
阅读顺序按身份分,别从头读到尾:
| 你是 | 读到哪为止 | 之后去哪 |
|---|---|---|
| 装它 / 用它 | 「已知行为」这一节结束 | 没有必须再读的;遇到现象先查 docs/README.md 里的 howto/symptom-triage.md(按屏幕原文排) |
| 改它 / 审它 | 继续往下:「目录」(逐文件职责)、「构建产物」、「测试」 | docs/README.md 的完整地图:缺口 KNOWN_GAPS.md、路由契约 ROUTES.md、平台事实 docs-qoder-cn/ |
| AI 协作会话 | — | AGENTS.md(不随包发布,只在仓库里):验证怎么跑、红线、门禁真源 |
| 文件 | 作用 |
|---|---|
src/host/credentials.ts | 从 Qoder 应用读取登录凭据的存储半边:探测候选应用目录、以只读方式打开 state.vscdb、解码各行(2026-10-20 从中拆出 OSCrypt 链,链在下一行) |
src/host/oscrypt-unwrap.ts | OSCrypt 链:PowerShell/DPAPI 解包主密钥、密钥缓存与 Local State 绑定、失败窗口 60 s、清零与启动清扫(唯一会把密文写到进程外、唯一 spawn 子进程、唯一删东西的一半;两个解包入口都接受注入的 spawner,因此链内环节可无 PowerShell 测) |
src/host/upstream.ts | 上游层的门面(26 行):逐名 re-export src/host/upstream/ 下的 11 个叶模块,让 20 个下游 importer 一行不动(2026-10-17 拆分,docs/issues/22) |
src/host/upstream/crypto.ts | RSA 公钥、置换字母表与 Encode=1 请求体编码(CUSTOM_ALPHABET / encodeBody);协议事实原文注释在此文件头 |
src/host/upstream/client-constants.ts | COSY_VERSION / CLIENT_TYPE / MACHINE_TYPE / DATA_POLICY / LOGIN_VERSION / MACHINE_OS |
src/host/upstream/sign.ts | signaturePath(签名路径,剥掉 gateway 路由掉的 /algo 前缀) |
src/host/upstream/cosy-sign.ts | authHeaders:COSY 签名头组装(AES 加密签名 info 块、RSA 包 AES key、MD5 over payload/key/ts/body/path) |
src/host/upstream/headers.ts | openApiHeaders 与 umid 机器身份头(Windows 定位 runtime-info.exe;读不到时显式降级,__dshQoderUmidState 报原因) |
src/host/upstream/endpoints.ts | 各 API 端点 URL 构造(目录/对话/兑换/账号/用量/签到) |
src/host/upstream/retry.ts | 重试与队列等待预算(QUEUE_WAIT_*、queueWaitFor,含两分钟预算的 rationale) |
src/host/upstream/sse.ts | 双层包裹 SSE 解码与对话流(streamChat,含重试循环与 Retry-After 处理) |
src/host/upstream/fetch-usage.ts | 用量快照(fetchUsage / UsageSnapshot / normalizeQuotaBucket / normalizeDedicatedPackage / normalizePromotion);促销读失败时具名降级而非静默(campaignsError) |
src/host/upstream/api-calls.ts | 各调用的编排与 JSON 读取(readJson / readCampaigns / claimCampaign / fetchModels / readModelCatalogShape / exchangePat / fetchUserInfo) |
src/host/upstream/messages.ts | DSH ↔ Qoder 的消息与工具描述转换(toQoderMessages / toQoderTools) |
src/host/shim.ts | 面向 pi-ai 的 OpenAI 兼容回环端点 |
src/host/adapter.ts | pi-ai provider 与 PiAiAdapter profile(被关的 provider 以零模型组呈现,由 DSH 自行隐藏) |
src/host/route-retry-policy.ts | 本路由交出去给宿主的重试策略:只留 EMPTY_RESPONSE / TIMEOUT / TRANSPORT,让插件的队列阶梯成为唯一重试源——宿主默认策略含 RATE_LIMIT / SERVER,会把已烧完 120 s 预算的队列失败再续 5 次(≈12 min),也会白重试 shim 一律用 502 表达的终态判决;刻意不设 maxRetries / backoff(未知键会让整个 profile 注册失败;docs/issues/24,无 peer 依赖) |
src/host/catalog-entry.ts | 目录条目的归一化、模型过滤(含按区域开关)与卡片行投影(无 peer 依赖) |
src/host/catalog-store.ts | 目录的磁盘缓存与原子落盘(无 peer 依赖) |
src/host/roster-seen-store.ts | 「本机首次见到这个模型」的历史(每区域一份 .qoder-roster-seen.<region>.json,id → 首次见到时间戳):7 天窗口、首次写入只播种不宣布、离开名册即遗忘、不认识的版本不覆盖、超过上限按形状漂移处理(不写不报);它证明的只是「本机上次记下的名册里没有它」,不是上游的上架时间(无 peer 依赖,docs/issues/25) |
src/host/catalog-refresh.ts | 一次目录刷新的结果如何落地:空目录也是结果(照实清空并推进 fetchedAt),只有失败才保留上一份,且失败按 credential / no-credential / fetch / protocol-shape-changed 分档(无 peer 依赖) |
src/host/credential-cache.ts | 凭据缓存与「登录失效后重读」规则,以及 env-PAT 兑换的 429 节流闸门(平台自述窗口优先、否则指数退避;401/403 停泊,仅「重读登录」释放;无 peer 依赖)。三源顺序是「桌面端(没过期才算在场)→ 插件自有设备登录 → 环境变量 PAT」:过期的桌面端 access token 什么都干不了(连 provider 都不会被注册),所以它不再压住后面两条 |
src/host/credential-kind.ts | 「这个版本现在是谁在服务」的答案:凭据来源标签 → desktop / device / env 三档。宿主是唯一知道实际解出哪条凭据的一方,所以由它说;卡片与 status 路由据此把「桌面端登录」与「插件自有登录」两本账并成一条,不再靠面板猜(无 peer 依赖) |
src/host/throttle-store.ts | 兑换节流闸门的磁盘持久化(只存未过期的 rate-limit 窗口、parked 停泊与令牌一律不落盘,原子写,无 peer 依赖) |
src/host/request-counter.ts | 本地「今日经本客户端请求次数」计数:上游只报「用完了」不给每日上限,故只能本地自计(每次对话轮 +1,重试不重复计)。按 region + 上海本地日分文件、零点翻页、原子写(temp/rename)、越界 region 分量拒绝写盘、gc 只删本插件命名且早于保留期的旧日文件;撞 daily_limit_exceeded 时置 dailyLimitHit 不丢计数(无 peer 依赖) |
src/host/token-counter.ts | 本地「今日经本客户端消耗的 token 数」累加器:shim 每抓到一个上游 usage 帧(total_tokens / cached_tokens,上游每轮明确下发)就累加进本插件文件(.qoder-token-count.<region>.<day>.json)。与请求计数器同源同套路(按 region + 上海本地日分文件、零点翻页、原子写、越界防护、gc 只删本插件旧文件),但数据来自上游而非本地估算——聚合的是上游已经公开的真实 token 数,不画比例条(无每日上限口径);tokensFromUsage 对坏帧返回 null 宁可不加也不加错 |
src/host/credential-vault.ts | 插件自有凭据文件的读写(设备登录换来的可刷新令牌):0600 + temp/rename 原子写、损坏文件隔离留存而非被后来者覆盖、读-改-写整体串行(并发的刷新不会把刚登出的凭据写回来,无 peer 依赖) |
src/host/device-login.ts | 设备登录的协议层(两个 region 各自的端点、client_id、PKCE 与轮询/刷新请求):404 与「还没审批」的空信封都读作未授权而非失败;刷新应答不是凭据——实测只有六个字段、把访问令牌叫 device_token 且不带 user_id,必须叠加回被刷新的那一份,否则签名带上空 uid 会被网关回 403 Signature invalid(无 peer 依赖) |
src/host/device-credential.ts | 把 vault 里那份令牌变成可签名的凭据、并在过期时轮换:五档状态(none / ok / expired / refresh-failed / unreadable)而非两档——「本机没登录过」与「文件读不出来」指向不同下一步;刷新被拒后有 60s 窗口(否则每轮对话都会重试一条已被吊销的刷新令牌);刷新成功但落盘失败仍照常出借凭据、只 warn;从不抛(抛出去就等于把登录问题变成 provider 坏掉)(无 peer 依赖) |
src/host/device-login-flow.ts | 设备登录的会话状态机(按 region 一次一个尝试,5 分钟 TTL):把探针那种「一个进程等着」拆成卡片能驱动的 start / poll / cancel,否则一个路由要挂五分钟且面板一关就丢;PKCE verifier 只留在宿主内存、绝不出现在任何返回值里;poll 每次只发一次请求;兑换码只能用一次(尝试先删再写凭据,否则之后的 poll 会给第二个令人困惑的答案);「批准了但没存下」报 failed 而非成功(假确认是红线 1 禁止的)(无 peer 依赖) |
src/host/account-payload.ts | 账号面板三条路由共用的那份应答:逐区域状态 + 开关映射,以及「渲染读缓存 / 重读登录读真」这一个开关(从 index.ts 抽出以便直测,无 peer 依赖) |
src/host/account-state.ts | 每区域账号状态四档判定(ok / expired / needs-app / signed-out;纯本地证据、不含凭据,无 peer 依赖);三种读取模式(默认 / cachedOnly 不解包 / force 忽略失败窗口) |
src/host/settings-save.ts | 设置命名空间的解析(0.2 代由宿主推导,插件不能自选)、设置写入、按区域合并与落盘读回校验(无 peer 依赖) |
src/host/pi-model.ts | pi-ai 模型描述符的构造(纯函数,无 peer 依赖) |
src/host/adapter-models.ts | 单个区域向 DSH 提供的模型列表:区域开关(只认显式 true)、勾选过滤、最大上下文开关、逐模型图像模式(从 adapter.ts 抽出以便直测,无 peer 依赖) |
src/host/region-gate.ts | 一个区域能否作为 provider 上线:三档拒绝(无登录 / 已过期 / 读不到)各自的判定与日志级别(从 index.ts 抽出以便直测,无 peer 依赖) |
src/host/lifecycle.ts | 插件 fiber 退出时要撤销的东西:路由注册的注销句柄收集与释放(宿主是否随 fiber 回收无法从插件侧确认,故两种语义都正确;无 peer 依赖) |
src/host/preferences.ts | 四个设置项的读取与 volatile 解包(enabledRegions 区域开关:缺失/非对象一律读作开启,只有显式 false 才关) |
src/host/offpeak.ts | 错峰窗口与费率算术(无 peer 依赖) |
src/host/single-flight.ts | 同类异步任务的并发合并:刷新在途时,后来的调用并入同一次请求(目录/用量刷新用,无 peer 依赖) |
src/host/claim.ts | 每日签到:当轮活动的挑选、可领状态判定与领取结果的归一化(纯函数,无 peer 依赖) |
src/host/errors.ts | 上游错误帧的判定:105(登录没了)与 10605(在排队)的分诊,队列提示的提取,以及协议形状变化这一档(ProtocolShapeChangedError,retryable: false 所以它不会被当成"排队"慢慢等)(无 peer 依赖) |
src/host/time.ts | 上游时间戳的单一换算(秒 / 毫秒 / RFC 3339 → epoch 毫秒),曾经的两份副本行为不一致(无 peer 依赖) |
src/host/volatile.ts | volatile 活引用 { get() } 的解包,此前散在三处(无 peer 依赖) |
src/host/http-utils.ts | 回环路由共用的 JSON / OpenAI 形状错误响应(no-store,卡片轮询读不到陈旧数据;无 peer 依赖) |
src/host/domain.ts | 各宿主模块标注时共用的领域词表(Region / CatalogEntry / Promotion / CatalogOutcome 等)。只有类型、没有运行时值,所以不进产物、不改变任何 bundle 字节;刻意不描述任何 peer 模块的形状——那些包由宿主运行时提供、此处只有 declare module 空壳,凭空造一个"看起来对"的接口比 any 更危险,因为它不会承认自己不知道(无 peer 依赖) |
src/host/routes.ts | 每条卡片路由共用的两道闸:方法检查(405 带 Allow、HEAD 交给 GET)与同源来源检查(403;比对 Host 头,POST 路由靠它挡住本机其它端口的网页),以及带 64 KiB 上限的 JSON body 读取器(无 peer 依赖) |
src/host/handlers.ts | 十二条卡片路由的 handler,(req, res, deps) => Promise<void> 形态:方法/来源两道闸 + 业务判定 + 应答全在这(含签到 claimTodayFor 与「重读登录」的失败保护),index.ts 只保留 webServer.register 与 deps 装配(从 index.ts 抽出以便直测,无 peer 依赖)。「在线校验登录」的兜底路径经 deps.readCredential 取凭据,宿主把它接到按 region 共享的凭据缓存上,所以没有 runtime 的 region 也能校验插件自有登录(无 peer 依赖) |
src/host/publish-regions.ts | 把当前区域集发布给宿主(registerAdapter + registerConfigurableProviders)以及失败时的三层回滚:先释放旧的一对,新注册失败则释放半注册的 adapter 并把旧的一对装回去,装不回去则如实留空而不谎称已注册(从 index.ts 抽出以便直测,宿主两处注册接缝靠注入,无 peer 依赖) |
src/host/route-mount.ts | 一组路由的挂载判定:要么全挂上、要么全撤下。三个账号路由是一个功能,宿主若只收下 /account 而拒了 /account/confirm,旧写法会留下一个"能读状态、两个按钮却不动"的半截面板,且日志写着"account routes unavailable"——那句话是假的,比没有日志更坏,因为它让人不再往下查。组内任一注册被拒即回滚已拿到的 release 并如实报失败(无 peer 依赖) |
src/host/dispose-order.ts | fiber 注销时的次序(次序本身就是内容):provider 注册先撤、路由先于 runtime 拆(否则 issue 13 那条泄漏)、disposed 必须在 clearInterval 之前置位——否则一次还在途的 refreshCatalog().then 会把一个全新的 interval 装到已经没人管的 runtime 上,成为活过插件的僵尸凭据解析循环。此前这条只由注释担保(无 peer 依赖) |
src/host/injected.ts | requireInjected:把 "ctx.inject(['webServer'], cb) 保证 cb 跑时该服务已在" 这个只活在注释里的前提,转成一次类型收窄——服务缺失就抛错点名该服务,而不是让后续注册把路由挂到 undefined 上、再于更晚静默失败。此前它是 activate() 里的内联闭包(七处调用共用),此前这条也只由注释担保;抽出以便直测,只认 undefined(其它 falsy 值是宿主自己的契约违反,原样交回、不改写)(无 peer 依赖) |
src/host/index.ts | 按区域注册 provider 的插件入口,与十二条路由的注册接线(handler 本体在 handlers.ts;发布与回滚的判定在 publish-regions.ts;挂载分组在 route-mount.ts;注销次序在 dispose-order.ts;注入服务缺失的判定在 injected.ts) |
docs/docs-qoder-cn/ 是从 Qoder CN 官方文档抓回并清洗的资料(纯 Markdown,源 .txt 已删),
由根目录的 convert-qoder-docs.ps1 一次性转换而成——它只处理这一批抓取件,不是常规构建步骤;
产物由 test/docs-markdown.test.js 守着,改动转换规则后重跑该测试即可。文档与代码的对应关系
(哪条系数/活动/签到事实落在哪个模块)记录在该目录的 Qoder CN 文档与插件设计佐证.md。
卡片侧的源码在 src/client/(产物是 lib/client.js,react 由宿主提供,产物不打包它):
| src/client/paths.ts | 卡片用到的十条路由常量(QODER_*_PATH):四条读、六条写。__save 不在其中——它由 settings-write.ts 以字面量直接 POST;status 只供宿主诊断,卡片不读 |
| src/client/styles.ts | 卡片样式与 installStyles(dsm-* 一套与 dsh-connect-workbuddy 逐字一致,原因见文件头) |
| src/client/settings-write.ts | 「写入后读回校验」的浏览器半边 |
| src/client/controller.ts | 卡片可编辑状态机(staged/saved 三字段、dirty、save、discard 与派生的模型视图)——无 React、无 DOM,可被 node:test 直接测 |
| src/client/copy.ts | 卡片文案聚合入口(中/英),index.ts 与 card.tsx 只从这里取 |
| src/client/copy-row.ts copy-usage.ts copy-account.ts | 按面板拆分的三段文案:模型行与错峰、用量与签到、账号与区域标签条 |
| src/client/card-model.ts | 卡片的浏览器无关决策层:视图模型类型词汇 + 纯规则(offPeakState / rateAt / windowLabelOf / refreshNoticeKey / 格式化 / describeThrown——卡片所有 catch 块给用户看的失败文案都过它)。无 React、无 DOM、无 fetch,因此 Node 测试可直接 import 真规则,而非镜像副本(这是向 sensenova snapshot.ts 对齐的那一刀;describeThrown 也从 card.tsx 挪进来,拆掉装配层与两个容器之间的循环 import) |
| src/client/http.ts | 卡片唯一的 fetch 收口:getJson / postJson,模型名册读与两台容器(用量 / 账号)共六道宿主路由 fetch 都只通过这里(向 sensenova http.ts 看齐);GET / POST 失败都先取宿主的 error 字段(如 loopback origin 检查的 origin-not-trusted),无才回落 HTTP {status};宿主的 writeError 写的是 {error:{message,type,code}},故对象形状也取它的 message(否则每道 writeError 拒绝都会退化成一句没有下文的 HTTP 500);GET 200 但 body 解析失败按 unparseable JSON body 抛出而非当空数据,避免「假 undefined 失败」 |
| src/client/usage-panel.tsx | 用量容器 QoderUsagePanel:自有额度读 + 每日签到领取两条 fetch,渲染 RegionUsage / CheckinCard(二者留在 card.tsx 作为无 hook 纯展示件) |
| src/client/account-panel.tsx | 账号容器 QoderAccountPanel:区域条 + 选中区域登录卡,四道 fetch(读 / 重读 / 确认 / 开关写),开关写仍带完整 enabledRegions map。按"在服务的是谁"显示:effectiveStateOf() 判出这个版本真正在服务的是桌面端还是插件自有凭据,只有后者在服务时才读作"已登录"、才给「在线校验登录」按钮(桌面端那条在服务时按钮同样在) |
| src/client/device-login-panel.tsx | 插件自有登录那一块(第三个凭据来源的卡片半边):与账号行的 state 并列上报、绝不合并——桌面端登录与插件令牌是两份凭据两种寿命。两层结构(外层无 hook 做门控,device 缺席 = 老宿主 → 什么都不渲染、绝不报「未登录」;内层持全部 hook)。start → window.open 授权页 + 保留可点回退链接 → 每 2s 轮询一次(链式而非 interval,慢响应只会推迟下一次而不会叠请求);authorized 停并回读,expired/idle/failed 各给不同文案;卸载时若仍在等待就 best-effort cancel(宿主 5 分钟 TTL 是兜底)。每个失败都说出是哪个失败,t/onChanged 由 ref 持有并刻意排除在轮询 effect 依赖外(否则父组件每次渲染都