面向二次开发者。每个模块的导出面与行为均对照
lib/实际代码核实;带「VERIFIED/实测」字样的结论出自docs/QODER-PROTOCOL.md(机器校验的逆向规格)与docs/QODER-PROTOCOL-FINDINGS.md(实测台账)。
lib/
├── index.js Cordis 插件入口(宿主侧装配)
├── client.js 浏览器端设置卡片
└── qoder/
├── constants.js 端点/路由/命名空间常量(全部带 VERIFIED 注释)
├── models.js 模型目录三层链 + degraded 标记
├── catalog-reader.js 本地目录解密(Qoder 官方 WASM 进程内调用)
├── quota.js quota/usage API 客户端
├── gate-cache.js 每账号 gate 探测缓存(24h TTL)
├── credential-failover.js 凭据多源遍历 + probe + memo
├── credentials.js IDE 桌面端凭据解析(safeStorage/DPAPI/AES-GCM)
├── wasm-credential-reader.js CLI 凭据 WASM 解密(库化封装)
├── device-login.js 设备码登录(PKCE 全流程)
├── store.js 凭据存储(插件自有副本 vs 发现凭据,取新者)
├── wire.js 请求线格式(URL/头/ body 规范化、SSE 帧解析)
├── relay.js 上游客户端(QoderUpstreamClient,错误分类)
├── shim.js OpenAI 兼容回环 shim
├── web.js HTTP 路由(状态/开关/模型选项/登录/分享)
└── catalog-snapshot.json 随包目录快照(三层链第 2 层)
| 模块 | 职责 | 关键导出 |
|---|---|---|
index.js |
Cordis 装配:settings schema、shim 启动、provider 注册(ctx.llm.registerAdapter + registerConfigurableProviders)、web 路由挂载、catalog 引导 |
apply(ctx)(插件工厂) |
client.js |
浏览器端卡片:账号卡片(头像/剩余 Credits/资源明细展开)、模型开关行、用量统计;通过 6 条 *_PATH 与宿主通信 |
ModuleLoader 工厂(id: 'dsh-qoder-cli') |
models.js |
三层目录链 + degraded 修正 + allowlist 过滤 | buildCatalog(options) / FALLBACK_QODER_MODELS(10 键) / REJECTED_QODER_KEYS / filterEnabledModels / toPiModel |
catalog-reader.js |
从 Qoder 官方 qoder_auth_wasm_bg.wasm 进程内解密本地目录 ~/.qoder/.models/<uid>/catalog-v6(AES-GCM,密钥=uid),零网络零签名 |
readLocalCatalog() / decryptCatalog(b64, uid) / rawModelToCatalogEntry(raw) / initAuthWasm() |
quota.js |
GET /api/v2/quota/usage(VERIFIED §5.1);任何失败静默返回 null(display-only 数据不许弄断 status 路由) |
fetchQuotaUsage(token, {region, fetchImpl}) / QODER_QUOTA_USAGE_PATH |
gate-cache.js |
每账号 chat 网关可达性:1 token 探测(间隔 300ms),结果缓存 $DSH_HOME/.qoder-cli-gate-cache.json,TTL 24h,原子写(temp+rename) |
loadGateCache / saveGateCache / probeCatalog / resolveProbeToken / GATE_CACHE_TTL_MS |
credential-failover.js |
请求遇 401/403/invalid_model_error 时遍历备用凭据;probe 通过者 memo 化进程生命周期 | findWorkingCredential / isCredentialFailure / FAILOVER_KINDS / resetFailoverMemo(测试钩子) |
credentials.js |
IDE 桌面端凭据:auth.v1.dat = v10+nonce+ciphertext+tag,AES-256-GCM;Local State DPAPI 解 32 字节密钥(koffi 进程内,不走子进程管道——DSH 运行时禁止) |
discoverCredential / decryptSafeStorage / extractSafeStorageKey / parseQoderCredential |
device-login.js |
PKCE 设备码流:verifier(64 字符 RFC 7636)→challenge S256→qoder.com/device/selectAccounts→轮询 deviceToken/poll(1s,5 分钟超时)→userinfo 组装凭据 |
createDeviceLoginAttempt / waitForDeviceToken / buildCredentialFromDeviceLogin |
store.js |
插件自有凭据副本($DSH_HOME/.qoder-cli-auth.json)与发现凭据取 mtime 新者;刷新降级(refresh 抛错→用缓存 token) |
QoderCredentialStore / QoderNotSignedInError / isStale |
wire.js |
线格式:chat URL(QODER_MODEL_SERVER_HOST 可覆盖,兼容 scheme/尾斜杠)、Cosy-* 头族、body 规范化(坏 JSON 抛错不发)、SSE 帧判定 |
qoderChatUrl / qoderHeaders / prepareChatBody / parseSseDataFrame |
relay.js |
上游客户端:非 2xx 分类(401/403→auth_error、408/429→rate_limit、5xx→server_error)+ in-band 错误帧提取 | QoderUpstreamClient(chatStream)/ classifyUpstreamStatus / KIND_STATUS |
shim.js |
本机回环 HTTP 服务:只绑 127.0.0.1 随机端口;进程内随机 bearer(常数时间比较);Host/Origin 双回环校验防 DNS rebinding;补 Cosy-* 头转发;凭据 failover 钩子 | createQoderShim(ready/baseUrl/token/chat)/ hostIsLoopback / originIsLoopback |
web.js |
设置卡片的数据面:status 聚合(account+models+quota 并发拉取,quota 失败静默)、allowlist/模型选项写路由(loopback Host+Origin+JSON content-type 三重校验)、设备码登录 registry、目录分享 dump(脱敏) | buildStatus / registerQoderRoutes / 各 handler |
| 路由(constants.js) | 方法 | 用途 |
|---|---|---|
/plugins/dsh-qoder-cli/status |
GET | 卡片聚合状态(含 quota,t15 起并发拉取) |
/plugins/dsh-qoder-cli/enabled-models |
POST | allowlist 写入(id 与 live catalog 求交) |
/plugins/dsh-qoder-cli/model-options |
POST | 每模型 contextLabel / thinkingEffort |
/plugins/dsh-qoder-cli/auth/start auth/poll |
POST / GET | 设备码登录起停与轮询 |
/plugins/dsh-qoder-cli/catalog-dump |
GET | 脱敏目录分享(无凭据/uid/机器信息) |
DSH 模型选择器
→ pi-ai provider(index.js 注册,id=qoder-cli)
→ shim(127.0.0.1 随机端口,OpenAI Chat Completions 形态)
├─ bearer 校验(进程内随机密钥,常数时间比较)
├─ Host/Origin 回环校验
├─ prepareChatBody(model/messages 保真,坏 JSON 400)
├─ store.resolve():插件副本 vs 发现凭据取新者
│ └─ 失败且 isCredentialFailure → findWorkingCredential(failover walk)
├─ relay.chatStream → wire.qoderChatUrl + qoderHeaders(Cosy-* 头族)
└─ api2-v2.qoder.sh(SSE 原样透传;in-band 错误帧→对应 HTTP 状态)
为什么必须有 shim:Qoder 网关要求每请求带 Cosy-ClientType / Cosy-Version / Cosy-MachineId 动态头,pi-ai provider 描述符只能设静态头。shim 在回环层补齐。
浏览器卡片(client.js)
→ GET /status
→ web.buildStatus:
store.resolve() → account(user_id/name/email/avatar_url/expires_at)
└─ 同时并发 fetchQuotaUsage(token) → quota(失败→null,字段整体缺省)
→ 渲染:账号卡片(头像/剩余 Credits/资源明细展开)+ 模型行(开关/下拉/徽章)
开关/下拉变更
→ POST /enabled-models 或 /model-options(loopback 三重校验)
→ settings.update(qoder-cli 命名空间)
index.js refreshCatalog()(启动时):
buildCatalog({dshHome, gateCache}) → catalog.set(快路径,选择器永不空)
└─ 无有效 gateCache 且未在探测中 → 后台 probeCatalog → saveGateCache
→ buildCatalog(带探测结果) → catalog.set(二次刷新,degraded 按本账号修正)
| 层 | 来源 | 条件 | 规模 |
|---|---|---|---|
| 1 local-cache | Qoder 客户端写的 catalog-v6(WASM 进程内解密) |
本机装过 Qoder 且登录过 | 最新鲜(实测 17) |
| 2 bundled-snapshot | lib/qoder/catalog-snapshot.json(随包分发) |
无 Qoder 环境(裸机默认) | 17 |
| 3 fallback | FALLBACK_QODER_MODELS 硬编码 |
上两层损坏/缺失(防御层) | 10(逐个实测被网关接受的键) |
每层包 try/catch,任何失败降级到下一层——目录出任何问题都不允许 provider 失效。快照时代无法逐账号核实的键带 degraded 备注(REJECTED_QODER_KEYS 是实测被拒的记录,非猜测缺失)。
- 动机:服务器目录是全账号视角,chat 网关按账号灰度放行(如
efficient长期 429 NO_BACKEND)。 - 做法:后台对每个 key 发 1 token 探测(
PROBE_INTERVAL_MS=300温和间隔),结果三态accepted / rejected / unknown。 - 缓存:
$DSH_HOME/.qoder-cli-gate-cache.json,TTL 24h(GATE_CACHE_TTL_MS),原子写。 - 修正语义(models.js):本账号实测
accepted覆盖快照时代的 degraded 备注(别的账号放开的模型自动"变可用"),反向亦然;探测失败静默,快照标注兜底。 - 并发安全:
gateProbing闭包标志在 spawn 前同步置位(无 await 间隙,同 tick 不可能双开);stopped守卫覆盖整条异步链,finally必然复位(t7 审查实测确认)。
- 触发:上游 401/403 或 body 含
invalid_model_error(isCredentialFailure;注意 200 body 引用该词同样算失败——status-blind 设计)。 - 遍历序:IDE 桌面 store(DPAPI)→ CLI 设备流 store(WASM AES)→
QODER_CLI_AUTH_FILE显式文件(全部只读)。 - probe:1 token 真实 chat 请求,HTTP 200 才算通过。
- memo:胜者存进程级变量,后续请求跳过遍历;
resetFailoverMemo()供测试复位。 - 降级:全部失败时调用方的原始错误原样上抛(不吞)。
- shim:随机端口 + 进程内随机 bearer(常数时间比较)+ Host/Origin 双回环(防 DNS rebinding)。
- web 写路由:loopback Host + loopback Origin + JSON content-type 三重校验;body 上限 64KiB。
- catalog-dump:产品数据脱敏(仅 id/name/窗口/档位/degraded/source,无凭据/uid/机器信息),用户显式点击才提交。
模型目录不硬编码在 UI——改目录有三个入口(按优先级):
- 服务端新增(无需改码):本机装 Qoder 登录一次,
catalog-v6自动含新键;或跑node tools/snapshot-refresh.mjs刷新随包快照后发版。 - 快照更新:
lib/qoder/catalog-snapshot.json直接覆盖(scene 取 assistant > chat > app > qwake 首个非空)。 - fallback 层:仅当新键在无快照环境也要可用时,往
models.js FALLBACK_QODER_MODELS加条目——必须先实测网关接受该键;被拒键加入REJECTED_QODER_KEYS留档。
不需要动 client.js:模型行由 status.models 驱动,字段(reasoningEfforts/contextOptions/priceFactor/promotion/degraded)由 rawModelToCatalogEntry 从服务器原始条目自动映射。
t15 的五步是模板:
- 协议核实:确认端点在
docs/QODER-PROTOCOL.md有 VERIFIED 记录(如 §5.1@18993360)——不猜协议。 - 新建客户端模块(
lib/qoder/quota.js样板):导出fetchXxx(token, {region, fetchImpl});失败静默返回 null(display-only 数据不许弄断 status);camelCase/snake_case 双兼容解析(pickQuotaBlock样板)。 - web.js buildStatus 并发接入:
quotaPromise = fetchXxx(token).catch(() => null),与凭据解析并行;结果...(quota === null ? {} : { quota })条件展开——字段整体缺省而非填假值。 - 前端降级:client.js 读
status.quota时逐字段判空(num()+?? '—'),缺失显示 '—' 而非 0。 - 离线单测:fetchImpl 注入 mock(web.test.mjs / t15 quota 契约用例样板)。
- 设计参照:
preview/qoder-card-interactive.html(完整可交互四 tab 预览,样式值与 client.js 同步)。 - 实现位置:全部在
lib/client.js(浏览器端,ReactcreateElement——无 JSX/TS 转换)。 - 主题适配守则:中性色一律
var(--dsw-alias-*, 降级种子)形态;token 名必须先在主题体核实(D:\DSH Desktop\resources\app\node_modules\@deepseek-ai\dsh-client-ui-theme\lib\client.js)——已实测发现--dsw-alias-label-error/--dsw-alias-fill-tertiary不存在(正确名:--dsw-alias-state-error-primary;fill 家族整体缺失)。语义色(促销绿/警示琥珀/错误红/头像六色)不跟随主题,保持硬编码。 - React 内联样式限制:不支持
:hover伪类(用 state 驱动,参照 hoverId);伪元素/伪类一律不可用。 - 离线验证:client.js 不能在 node import,用
test/client-layout.test.mjs的源码级断言 +vm.Script解析模式。
constants.js 定义路径常量 → web.js 写 handler(写路由复用 checkLoopbackPost)→ registerQoderRoutes 注册(effect 内,随插件卸载自动清理)→ client.js 加对应 *_PATH。
| 限制 | 现状 | 影响 |
|---|---|---|
| model/list 在线拉取不通 | /api/v2/model/list 全 host 实测 403 Signature invalid;签名已产出(WASM glue 闭环)但 umid 设备指纹(阿里聚安全 sgsdk.dll)疑未参与签名 info 加密,调查进行中(docs/MODEL-LIST-SIGNATURE-LOG.md) |
目录依赖三层链(本地缓存/快照/fallback);服务端新增模型需快照刷新才能触达无 Qoder 环境用户 |
| 签到未接入 | 卡片签到按钮 disabled + title 说明;三态分支(ok/already/fail)已按 CodeBuddy 预留 | 用户无法在卡片内签到领积分 |
| 多账号存储未实现 | UI 已按多账号设计(卡片数组形态、「设为当前」按钮位、active 置顶排序点),但后端仅单账号(store.resolve 单凭据) | 当前恒显示一张卡;多账号需要 store 层扩展(多凭据持久化 + switch 路由 + 「写宿主成功才激活」语义,参照 docs/ACCOUNT-UI-REFERENCE.md 参照 B) |
| 令牌刷新未实现 | refreshCredential() 原样返回;刷新端点请求体已挖出(协议 §2.5)但未实测,不猜协议 |
token 过期表现为一次正常 401,重开 Qoder 端或卡片重新登录即可 |
| 主测环境 Windows + Web profile | macOS/Linux 的候选目录写了但未实测 | 其他平台凭据发现路径未验证 |
| 未测图片输入 | 所有模型只声明 text |
声明 image 会让 DSH 把图片发给可能拒收的模型 |
node test/run-all.mjs # 全量(离线套件必须全绿;live 需本机 Qoder 登录态)
node test/client-layout.test.mjs # 卡片布局源码级断言(含负向仪器自检)
node tools/snapshot-refresh.mjs # 刷新随包目录快照
node tools/gate-catalog.mjs # live gate 探测(无凭据显式 SKIP)
离线 12 套件约 197 用例;live 套件(live-credential / e2e-live)需真实 Qoder 登录态,缺失时显式 SKIP。