Sessions & Messages

dsh-chatgpt-subscription

Aa728848/dsh-chatgpt-subscription · ★ 98

Aa728848

A plugin that lets DSH (DeepSeek Harness) use GPT-series models through a ChatGPT subscription.

README body is shown in Chinese.

License
MIT
Added
2026-08-17
Maintenance
active
Install kind
github
npm
@eddyskywalker/dsh-chatgpt-subscription
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-08-19
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add github:Aa728848/dsh-chatgpt-subscription

GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: zh · Read Chinese README

Table of contents
  1. 目录
  2. 线路总览
  3. 功能特性
  4. 登录与会话
  5. 模型接入
  6. WorkBuddy(CodeBuddy 订阅)线路
  7. Command Code 线路
  8. Kimi Code 线路
  9. MiniMax Code(编程订阅)线路
  10. Claude(订阅)线路
  11. 设置页
  12. 模型目录
  13. 环境要求
  14. 号池存储后端(poolStorage)
  15. 安装
  16. 方式 1:通过 DSH CLI 安装(推荐)
  17. 方式 2:在 DSH 界面里安装
  18. 方式 3:本地开发调试(源码软链接)
  19. 使用
  20. Command Code
  21. 随包分发的 Agent Preset
  22. 与 Agent Teams 不兼容(刻意互斥)
  23. 回合边界纪律(R-W)
  24. 为什么不降级:teammate 不能选模型
  25. 配置项
  26. 手动安装(可选)
  27. 计划模式管线(R9)
  28. 子代理模型授权(0.2.15 起,0.3.2 起强制指定模型)

让 DSH(DeepSeek Harness)通过 ChatGPT 订阅使用 Gpt 系列模型的插件。

插件注册 codex-chatgpt Provider(显示名 “Codex(ChatGPT 订阅)”),以当前 Host 用户的 ChatGPT OAuth 登录态访问模型,并在设置页展示账号信息、连接状态与订阅额度。支持 Windows 与 Linux。

目录

线路总览

本插件同时接入 13 条订阅线路,每条各自独立注册 Provider、独立登录、独立额度卡片。

Provider ID 订阅 协议 登录方式 模型目录来源
codex-chatgpt ChatGPT(Plus / Pro / Business…) Responses 浏览器 OAuth(localhost:1455 回调),或导入本机 Codex CLI 登录 实时 /backend-api/codex/models
kimi-code Kimi 会员 OpenAI 兼容 Chat Completions(可选 Anthropic Messages) 设备码(RFC 8628) 实时 /v1/models + 本地兜底
command-code Command Code Anthropic / OpenAI 双轨 浏览器 OAuth 或粘贴 API Key 实时 /provider/v1/models
claude-subscription Claude Pro / Max Anthropic Messages 手动粘贴 / loopback 回调 实时 /v1/models + 本地兜底
minimax-code MiniMax Code 编程订阅 Anthropic Messages 复用桌面端登录态 或设备码 硬编码(见下)
antigravity Google Antigravity Gemini Google OAuth 内置表
glm-coding-plan GLM Coding Plan(ZCode 账号,国区 BigModel / 国际区 Z.ai) Anthropic Messages + OpenAI Chat Completions(按模型声明) ZCode 账号 OAuth(浏览器中转轮询)+ 可选导入本机 ZCode 登录态 随包目录(源自官方 zcode-builtin.json)
step-code StepFun Step Plan 订阅(国区 api.stepfun.com / 国际区 api.stepfun.ai)或开放平台 API Key OpenAI Chat Completions 云端轮询浏览器授权(不开本地回调端口)/ 手工粘贴 Platform API Key / 只读导入本机 ~/.stepcode/auth.json 实时运行时发现(随包表刻意为空)
cline-pass Cline Pass OpenAI Chat Completions 浏览器设备码 OAuth(WorkOS,账号入池、自动续期);API Key 仅作兼容回退 随包表兜底 + 官方 recommended-models / models.dev / 上游网关目录补全
qoder Qoder 订阅(全球区 qoder.com / 中国站 qoder.com.cn) Qoder 自有(COSY 签名 + 信封 SSE) 浏览器设备流(仅全球区)/ 手工粘贴 PAT(两区)/ 只读导入本机 ~/.qoder/.auth/user 实时运行时发现(COSY 签名 GET)+ 随包能力兜底表
opendesign OpenDesign 订阅(Go / Plus / Pro / Max) OpenAI Chat Completions 浏览器设备授权(AMR 设备流,签发控制 Key + 运行时 Key);粘贴 Cloud 控制台的 API Key 作回退 随包表兜底(源自官方公开价目)+ 实时 GET /v1/models
workbuddy-subscription WorkBuddy(CodeBuddy 订阅,国区 / 国际区) OpenAI Chat Completions;国区 Claude 走 Anthropic Messages 浏览器轮询授权,或只读采纳 CodeBuddy 桌面登录态 实时 /v3/config + 本地快照与随包兜底

只有 minimax-code 是硬编码目录,而且是有意的:该端点的 GET /v1/models 未对订阅流量开放(返回 503 direct_route_not_configured),任何「实时目录」都只会是一个必然失败的请求。其取值转录自本机官方客户端的 config.yaml。

线路之间互不影响:某条线路的登录失败、额度耗尽或上游故障都不会波及其它线路;它们共用同一套号池内核与账号卡片,但不是同一个池。

⚠️ 合规提示:claude-subscription 使用的订阅凭据转发方式与 Anthropic 现行条款存在冲突(详见该章节开头的「风险须知」),且插件未获官方授权;antigravity 线路的用户亦有账号被限制的公开报告。请自行评估风险。

功能特性

登录与会话

  • Authorization Code + PKCE(S256)登录,一次性 localhost 回调;
  • 支持 token 刷新、登录取消与账号注销;
  • Windows 使用 CurrentUser DPAPI 加密存储 token;Linux 使用当前用户独占的 0600 文件存储;明文不发送给 Client;
  • 设置页会明确显示当前存储类型,并在 Linux 上提示文件存储未额外加密。
  • 本机已经登录过 Codex CLI 的用户可以不再走一次 OAuth(详见「本机登录」):ChatGPT 卡片的账号池里,「登录」按钮旁边还有一个「导入本机 Codex CLI 登录」按钮——两者是同一件事的两种做法,布局与 Claude 页面上那个收编按钮一致。它只看凭据文件是否存在、从不读取令牌,且要你显式点那个导入按钮之后才会读取。导入得到的是一份快照,永不由本插件刷新——ChatGPT 的 refresh token 会轮换,而 CLI 会在原地刷新自己的文件,两个进程各自刷新会互相作废,最后是你被本插件的好意踢出 Codex CLI,而进程内的单飞解决不了这个跨进程竞态。快照过期后请回 Codex CLI 重新登录后再导入;本插件绝不写入或删除 Codex CLI 的任何文件。

模型接入

  • 固定 Codex Responses 地址,支持流式文本、reasoning summary、图片输入与工具调用/结果;
  • 模型目录来自订阅后端(GET /backend-api/codex/models):
    • 按账号缓存并持久化到本地快照,重启后无需等待网络即可渲染选择器;
    • 随包发布的模型表是兜底而非上限:取不到目录时选择器变宽,而不是消失;
    • 目录里本表没听过的模型照常出现,但未声明的能力按保守值回落,不会凭空多出图片或思考档位;
    • 只有目录确实列出的模型会进入选择器——它是「这个账号能调什么」的权威;
  • 请求带 openai-beta: responses=experimental:
    • 该订阅后端在 beta 标志下提供,官方 Codex CLI 一直发送这个头;
    • 缺它时请求面不同:当前后端宽容,但这正是收紧后会变成 400/403 的那一行;
  • 读取参数默认值与官方 Codex 一致(依据订阅目录 GET /backend-api/codex/models,与官方随包的 codex-rs/models-manager/models.json 同源):
    • 推理摘要默认不发:目录里每个模型都声明 default_reasoning_summary: none,官方以此为准。此前本插件在未配置时发 summary: auto,而摘要属于计费生成,等于每一轮都多花一块官方从不花的额度;现在默认省略该字段,用户显式选择 auto / concise / detailed 时照旧发送,选「无」也改为省略字段而不是发 summary: none(后者是让后端先生成再丢弃)。
    • 输出详细程度默认发目录值 low:官方客户端每轮都发送模型目录里的 default_verbosity(当前全部为 low)。此前本插件在未配置时整个不发 text 字段,于是服务端套用隐含的 medium——一个从未打开过该设置的用户,会拿到比官方客户端更啰嗦、也更贵的回答。现在未配置时按目录发 low,显式选择仍以用户为准。
    • 目录里没有记录的模型不猜:仅对该表声明了 supportsOutputVerbosity 的模型发送 text,避免把一个模型可能拒收的字段强加给它;
  • 多轮续传,避免每轮重发整个历史:
    • 每个会话发送稳定的 prompt_cache_key,让后端复用提示前缀;
    • 回传后端在响应头给出的 x-codex-turn-state,让它续接该轮;
    • 后端不再下发该头时立即停止回送——不重放过期值;
  • 对话报文绝不发送 max_output_tokens:订阅版 Responses 端点在部分账号/模型上直接以 400 Unsupported parameter: max_output_tokens 拒收该字段(#29,实测 gpt-6-sol / gpt-6.1-sol),官方 CLI 的请求结构里也没有这个字段;输出长度由服务端默认值决定,撞上限仍以 max-tokens 结束原因上报(详见「模型目录」);
  • 单次请求的图片负载有上限(2 MiB,可用 DSH_CODEX_MAX_IMAGE_BYTES 覆盖):历史里的每张图片每轮都会被重新内联,此前这条线路没有任何总量约束,长会话的请求体随读图数量单调增长,超过传输层能接受的体积后整轮失败,而报错只是一句没有信息量的「Codex could not be reached.」(#50)。现在按最旧优先把超出预算的图片替换为一条明确的占位文本,请求体因此有界,且模型知道那张图已经不在了;
    • 预算是在构造好的请求体上执行的,因为图片有两种到达方式:粘贴的附件是 { type: 'image', attachment } 块,而工具读取的本地图片是 ![](/describe-image/raw/sha256:…) 链接、由映射器在构造时才取回并内联。只扫消息块的方案看不到第二种;
    • 数字取自 #50 的实测(≤3 MB 全过、≥4 MB 全挂),而不是照搬别的线路:Kimi 的 1.5 MB 是按它自己的 2 MB 请求上限定的,用在这里会静默删掉本端点能接受的图片;MiniMax 的 16 MB 是按 64 MB 请求体定的,这里没有那个上限;
    • 只约束图片这部分。纯文本本身就超出传输层体积的请求仍然会被拒绝,任何图片预算都救不了它;
  • 请求路径的传输层失败会带上原因链:此前 request() 把任何失败都报成同一句 Codex could not be reached.,而 DSH 只持久化 {message, code},于是 NGHTTP2_ENHANCE_YOUR_CALM、ECONNRESET、UND_ERR_SOCKET 在界面和日志里长得一模一样——上面那条体积问题正是因此无法自助定位。现在与流中段断流(streamFailure)一致,把 cause 链拼进 message;
  • Antigravity(Gemini / Claude)线路同样接受图片输入:DSH 以 { type: 'image', attachment } 下发的粘贴图片会经附件服务读出字节并按 Gemini inlineData 发出,读不出的图片降级为一条可见的说明文本而不是被静默丢弃。单次请求的图片 base64 负载超过 12 MiB 时,最旧的图片按上游同款占位文案替换为文本,避免整条请求被体积上限拒绝;
  • 原样转发 DSH 暴露的工具 schema;命令工具兼容 pwsh / powershell、bash、sh 与 shell,并按 PowerShell、Bash 或 POSIX sh 注入对应说明;
  • 429/5xx 由 DSH retry policy 接管;401 只强制刷新并重试一次,支持 AbortSignal;
  • Codex、Code review 及上游返回的额外窗口额度,支持 Credits、月度消费控制与 reset credits 展示;60 秒缓存、15 秒上游节流并遵守 Retry-After;
  • 提供 ChatGPT 订阅侧 Codex 搜索 provider,可在设置页切换 DSH 默认搜索或 Codex 订阅搜索;
  • 新增 codex_image_generate 工具,生成图片后通过 DSH 附件系统保存并在会话中渲染;
  • 可选 composer 快捷用量徽标,按当前 codex-chatgpt 模型显示最紧张窗口的剩余额度。

WorkBuddy(CodeBuddy 订阅)线路

  • 注册 workbuddy-subscription Provider,设置卡片与 /workbuddy/api/* 路由同时恢复;支持国区 copilot.tencent.com 与国际区 www.workbuddy.ai,区域由凭据的 domain 决定,不能靠换端点跨区调用。
  • 登录沿用浏览器 state/token 轮询,也可只读采纳桌面端现有登录态;桌面账号只能隐藏,插件不会删除它们。自管账号进入同一账号池,支持轮换、配额、credits 倍率、上下文覆盖、每日签到(仅国区)与本地用量护栏。
  • 普通模型走 /v2/chat/completions,国区 Claude 走 /v1/messages 并保留 signed thinking 与 output_config.effort;国际区 Claude 仍走 Chat Completions。请求使用当前桌面客户端身份、每账号会话头与 gzip,目录来自 /v3/config 而非不存在的 /v1/models。
  • 对上游 11128 渠道校验,先仅改写已核实的窄指纹;仍有拒绝时只允许一次把头部 system 搬进首条 user 的 <instructions>,并记住该提示文本,后续会话直接走搬移路径。其它 11128(缺会话头、首消息形状)不会误触发这条重试。
  • 401 只按当前账号续期并重放一次;11140 立即停用该账号并持久化,30 分钟内第二个不同账号也受限时整池暂停 30 分钟,暂停到期后受限账号仍需重新登录。不新增静态 API Key,不伪造 X-Device-Token。
  • 风险须知:该线路依赖非官方、逆向得出的上游行为,WorkBuddy 仍可能继续收紧渠道校验和账号风控;实时可用性以本机最小 live probe 为准,离线测试不承诺上游永久兼容。实施证据与未验证边界见 docs/workbuddy-line-plan.md 和 docs/workbuddy-upstream-hardening.md。

Command Code 线路

  • 注册 command-code Provider,使用 Command Code 的 Provider API 与账户 API;模型 id 决定线路:claude-* 走 Anthropic Messages(/provider/v1/messages),其余模型走 OpenAI Chat Completions(/provider/v1/chat/completions),因为该 API 会拒绝把模型发到格式不符的端点;
  • 浏览器登录复刻官方 CLI 的回环回调契约(127.0.0.1:5959 起顺延,/callback 接受 Studio 页面的跨域 POST),也可在设置页手动粘贴 API Key;两条路径都先用 /alpha/whoami 验证再加密保存;
  • 模型目录取自公开的 /provider/v1/models,每个模型的 context_length 作为默认上下文窗口,可逐模型覆盖;
  • 模型能力逐模型查表(src/host/command-code/model-catalog.ts,转录自官方 CLI 的模型注册表):是否接受图片输入、支持哪些思考档位由该表决定,未知模型回落纯文本;
    • 图片能力不能靠厂商/模型名前缀推断——同厂商内部就会自相矛盾:deepseek/deepseek-v4.1-flash 与 deepseek/deepseek-v4-flash-vision-exp 支持图片,而 deepseek/deepseek-v4-flash、deepseek/deepseek-v4-pro 不支持;z-ai/glm-5.3-flash 支持而 zai-org/GLM-5.3 不支持;
  • 额度与用量来自账户 API 的账单/用量线路,任一条失败不影响其余;
  • 瞬时失败按 DSH retry policy 有界重试:command-code 路由显式声明 normal 策略(最多 3 次,1.5s 起指数退避、15s 上限、0.2 抖动),覆盖 RATE_LIMIT、SERVER、TIMEOUT、TRANSPORT;
    • 上游模型供应商临时不可用(502/503/504/500,典型响应体是 "{\"error\":{\"type\":\"server_error\"}}")被归类为 SERVER 并自动重试;
    • 429 会带上上游的 Retry-After 让退避按对方的节奏走;401/403 与 ABORTED 明确不重试;
  • 模型勾选(含线路标签)、思考深度、上下文窗口覆盖与额度在「设置 → 订阅服务 → Command Code」标签页中配置,输入框右侧另有额度胶囊。

Kimi Code 线路

  • 注册 kimi-code Provider,接入 Moonshot 的 Kimi Code 订阅(https://www.kimi.com/code)。它与 Moonshot 开放平台(pay-as-you-go)是两套互不通用的系统:订阅的模型接口是 https://api.kimi.com/coding/v1,凭据只来自订阅 OAuth;把开放平台的 key 或 base URL 用在这里会被判为 401 Invalid Authentication;
  • 登录用 RFC 8628 设备码流程(auth.kimi.com):设置页点「设备码登录」后直接展示用户码与一次性链接(浏览器会自动打开),装好后无需回调端口、无浏览器环境也能手工完成;slow_down 会按 RFC 调宽轮询间隔,设备码过期会自动重新申请而不是直接失败;
  • 访问令牌到期前按 max(300s, expires_in×0.5) 自动续期,同进程并发调用共用一次刷新;被拒的 refresh token 进入冷却并提示重新登录;
  • 瞬时失败按错误类别重试(src/host/kimi-code/adapter.ts)。会重试(有界退避,最多 3 次,1.5s 起步、15s 上限、0.2 抖动,并遵守上游 Retry-After):
    • 上游模型供应商临时不可用(典型是 502 "{\"error\":{\"message\":\"Upstream model provider is temporarily unavailable. Please try again in a moment.\",\"type\":\"server_error\"}}");
    • 真正的 429 背压(too many requests / engine is currently overloaded)、连接失败与流停滞;
  • 不重试,直接给出可操作提示(换模型 / 降上下文 / 等窗口重置 / 重新登录):配额耗尽型的 429、403 的账号额度上限、401 里的套餐权限不足、400 请求格式错误;
    • 服务把这些含义压进同一个状态码,因此分类读响应正文而不只看状态码;
  • 模型目录为订阅侧的六款模型:k3(1M 上下文,需 Allegretto+;Moderato 上限 256K,故默认按 256K 计算,可用上下文覆盖升到 1M)、k3-256k、kimi-for-coding(K2.8 Preview)、kimi-for-coding-highspeed(约 6× 速度、3× 额度)、k3.1-pro(K3.1 Pro,默认 256K、可覆盖到 1M,需 Moderato 及以上,1M 需 Allegretto 及以上)与 k3.1-pro-highspeed(K3.1 Pro Highspeed,1M 上下文、约 2 倍配额,需 Pro / Allegretto 及以上),运行时以 GET /v1/models 为准;
  • K3 行为按其官方文档实现:
    • 思考档位只发 low / high / max(其余写法收敛映射,未知档位不发送);关闭思考发 thinking:{type:"disabled"},开启时发 thinking:{type,effort,keep:"all"};
    • 开启思考时每条 assistant 消息都回传 reasoning_content(无推理则回传空串——服务要求的是空值而非省略,否则 400);
    • 不发送 temperature(采样参数按模型固定,显式值会报错);
    • 工具调用 id 截断到 64 字符;
    • stop 按上限裁剪为最多 5 条、每条 ≤32 字节,超长整条丢弃(截断的停止串会在错误位置终止生成);
    • K3.1 Pro 只支持思考模式:关闭思考(none)会被服务端以 400 拒绝,所以它的档位只有 low / high / max、默认 max;「默认思考档位」下拉只列出勾选模型真正声明的档位,任一勾选模型允许关闭思考时它才出现,被拒时卡片上直接写明原因;
  • 本线路对全部模型使用 OpenAI 兼容 chat completions(与官方 agent 的 type = "kimi" 一致):K3.1 Pro 系列与其它模型同一协议、同一端点(/v1/chat/completions),Anthropic Messages 只在实时目录为某个 id 声明时启用;视频输入按官方型号表声明走,K3.1 Pro 系列可用;
  • K3 的长思考不会被截断:输出上限跟随上下文窗口(保留 4096 余量),因为 reasoning_content 计入输出,固定 32K 会把 max 档的长推理中途截断并返回 length;调用方已知 prompt 规模时上限会被下调到放得下,未知时不做猜测;
  • 请求体上限按模型分档:1M 档 8 MiB / 256K 档 2 MiB,本地即拒绝:该端点最常见的 400 是 total message size N exceeds limit 2097152,但 2,097,152 来自官方文档的通用错误示例,从未针对 1M 上下文档位实测——1M 档的真实会话(中文正文 3–4 MB)会被本地守卫提前拦下,而宿主压缩也救不回来(压缩请求自己同样超限);官方 kimi-cli 全仓库没有字节守卫,说明这个上限至少不是客户端必需的。现在按模型档位:k3 / k3.1-pro / k3.1-pro-highspeed / kimi-for-coding = 8 MiB(8,388,608,待实测的上界),k3-256k / kimi-for-coding-highspeed = 2 MiB(保持原值,窗口与上限本就对齐)。8 MiB 的依据是「1M 窗口的正文大小」:一个 1M token 的请求体按英文约 4 MB、按密集中文约 4.5 MB,8 MiB 留出近一倍余量,所以字节上限不会成为 1M 会话的实际瓶颈;真正先到的是模型自身的 token 窗口——服务端回 Your request exceeded model token limit,该措辞已被 common/context-overflow.ts 归一到 CONTEXT_WINDOW_EXCEEDED,压缩与重试照旧生效。拦截仍按真实序列化体积,并按「循环实际做了什么」给出处方(有图可丢但丢了也补不上缺口 / 图已丢完仍超限 / 本来无图,三种情况文案不同,不再声称发生了没发生的省略);DSH_KIMI_CODE_MAX_BODY_BYTES 仍是全局强制覆盖、优先级最高,确有实测上限的部署(前置代理/网关)用它指定该档上限,取值非正整数时回落到该模型的默认值而不是关闭守卫;带视频的请求走 64 MiB 档,不受该变量影响;超限时报的是可恢复的溢出码(CONTEXT_WINDOW_EXCEEDED):DSH 会对该码强制压缩本轮历史并重试同一步,所以长会话的表现是「压缩一次后继续」,而不是整轮失败——官方 kimi-cli 从不特殊处理 2097152,它靠 85% 提前压缩避开,详见下文「上下文窗口」那条;
  • 缓存是自动的,且无法手动干预:
    • Kimi 按请求内容哈希命中前缀缓存。实测 prompt_cache_key 与 Anthropic cache_control 标记均被忽略(设与不设、同 key 与异 key 命中的是同一缓存),设备 id 与协议切换也不影响;
    • TTL 实测约 25–35 分钟,按 256 token 对齐;/messages 与 /chat/completions 共享同一缓存。两份独立实测互相印证:一份给出 [300s, 1800s)(300s 命中、1800s 未命中),另一份按每个年龄 3 个副本、每条前缀只探一次的方法给出存活曲线 —— 5–25 分钟 3/3 命中、30 分钟 1/3、35 分钟 0/3。所以「30 分钟」是首个多数副本已失效的年龄;
    • 真正决定命中率的是内容稳定性:同一会话内 system 或工具列表一旦变化会使整个前缀缓存失效(实测归零)。因此应保持工具集合稳定、把新增内容追加在末尾;
    • 卡片会显示滚动命中率,方便验证效果;
    • 本线路不发送任何缓存时长字段,缓存写入与存活完全由服务端决定。这不是省略,而是唯一有依据的选择:5m / 1h 两档只写在 Moonshot 开放平台文档里,Kimi Code 订阅端点没有任何缓存 TTL 的文档,官方 CLI 2.0.0 的代码路径里也不存在 prompt_cache_options(只有 prompt_cache_key);MoonshotAI/kimi-code#3942 专门请求暴露该档位并附带问了一句「如果 coding 端点不认这个字段,请明确写出来」,至今 open、零回应。此前设置页提供的 5m / 1h 选择因此已撤除;
    • 缓存过期提示按 30 分钟判定(cache-hint.ts 的 CACHE_TTL_MS)。此前该阈值取的是 5m 档位的名字(5 分钟),在实测能热 25 分钟的端点上,从第 6 分钟起就开始误报、误报窗口长达 19 分钟 —— 恰好违反本模块自己写下的「宁漏报不误报,误报会训练用户忽略提示」。现在取首个多数副本已失效的年龄,代价是真正过期后最长晚 5 分钟才提示;
    • 设置卡的缓存区块把写入单独列成一行(总量 + 5 分钟档 + 1 小时档):写入是独立计费项,折进「新处理 tokens」会让命中率说错话;某个分档是 0 时照实显示 0——响应头缺失只表示服务端没回报该档,不等于那里确实没写。注意这两个分档响应头同样只有开放平台文档背书,订阅端点尚无实测样本,恒为 0 时不要读成「确实没写入」;
  • 视频输入可用(k3、kimi-for-coding、k3.1-pro):
    • DSH 的模态词表只有 text/image,但它是可合并扩展的接口,本插件用 TypeScript 模块增强把它扩到 video(未改动 DSH 任何代码),因此视频走 DSH 真实的能力通道,而不是只能显示在提示里;
    • 适配器把视频映射为服务文档的 {type:'video_url',video_url:{url:'data:…'}}。视频与图片各有独立预算(图片 1.5 MB、视频 48 MiB base64,最旧优先省略),请求体校验只在确实带视频时才放宽到 64 MiB;
    • 图片预算可用 DSH_KIMI_CODE_MAX_IMAGE_BYTES 覆盖(取值非正整数时回落到默认值)。视频预算没有环境变量出口,与 MiniMax 线路一致;
    • 以下情况会把视频降级为明确的文字说明而不是猜字段发出去:k3-256k 只接受图片、文档白名单外的容器、以及未文档化视频内容块的 Anthropic 线路;
  • dynamically_loaded_tools(仅 K3)已实现:K3 接受消息级工具声明(messages[].tools),可在会话中途用「无 content 字段的 system 消息」注入完整工具定义。官方把「保持顶层 tools 字节稳定」列为该特性的目的之一——中途修改/删除已发出的声明会使缓存从该点起失效,而在末尾追加不影响已缓存前缀,所以这是提升缓存命中的正道。声明按请求重发(服务端不保留),且仅在模型声明该能力时发送;
  • 额度卡片区分 5 小时 / 7 天 / 月度(会员共享池)/ 月度(Kimi Code 池) 四个窗口并显示重置时间,另可显示加油包余额;设置页为「设置 → 订阅服务 → Kimi Code」标签页,对话输入框右侧有该线路的额度胶囊。

MiniMax Code(编程订阅)线路

  • 注册 minimax-code Provider,接入 MiniMax Code 编程订阅。它与 MiniMax 开放平台(按量计费的 API Key)是两套互不通用的系统:
    • 订阅的模型接口是 Anthropic Messages 协议(https://agent.minimax.cn/mavis/api/v1/llm/v1/messages);
    • 只用 authorization: Bearer <accessToken> 认证——实测 x-api-key 一律返回 401 "{\"code\":401,\"message\":\"token is required\"}",因此本线路没有 x-api-key 回退分支(回退只会在每次请求上白花一个往返);
  • 复用桌面端登录态,而不是让你再登录一次:凭据就是 MiniMax Code 自己写的 ~/.minimax/auth/<buildEnv>/<region>/mcode-public/auth.json。
    • 只读优先:仍在有效期内(且距离到期还有 5 分钟以上)的令牌原样使用;
    • 进入续期窗口才轮换,并走原子替换(临时文件 + rename),失败或中断都让原文件保持逐字节不变;
    • 不创建、不删除、不等待 auth.lock(那是桌面端自己的刷新锁,第二方碰它就可能打断官方客户端的刷新);
  • 两种登录来源互不覆盖:
    • 桌面端的 auth.json 归桌面端所有,本插件只读;若本机没有它,才在设置页用 RFC 8628 设备码流程(PKCE S256)登录,凭据存到本插件自己的 $DSH_HOME/storages/minimax-code-credentials.json;
    • 登出只作用于本插件自己那份凭据:桌面端的登录态会被续期写回(只读优先),但绝不撤销、绝不删除,登出请求对它会被拒绝并说明原因(撤销它等于把你从正在跑的 MiniMax Code 里踢下线);
  • 区域是凭据属性:cn 用 account.minimax.cn / agent.minimax.cn,global 用对应 .io 域名。两个区域的目录都会被探测,因此国际区账号不会因为没有国区文件而「未登录」;读回凭据时以记录里存的区域为准,而不是拿默认区域覆盖它;
  • 模型目录是硬编码的,而且必须硬编码(src/host/minimax-code/model-catalog.ts):
    • 该端点的 GET /v1/models 未对订阅流量开放(503 "{\"errorCode\":50115,\"errorReason\":\"direct_route_not_configured\"}"),所以任何「实时目录」都只会是一个必然失败的请求;
    • 四款模型:M2.7 / M2.7 HighSpeed / M3 取自本机 ~/.minimax/config.yaml 的模型表;M3.1 Flash 是 2026-10-19 换上的正式版 id(MiniMax-M3.1-Flash,去掉了 -Preview 后缀),取自 MiniMax 自己的 Model Invocation 页 Supported Models 表并在该订阅端点上实测(正式 id、output_config.effort 的 low/max、内联图片)。无论哪一条,都不从不存在的接口推断;
  • 思考形态按模型表三态实现,且一律走 output_config,不发 thinking 对象(该字段从未被文档确认,发出去只是一种形状的猜测):M2.7 系列恒定开启且没有档位(不发送任何字段);M3 是开关二态(关闭时 output_config.effort: 'none');M3.1 Flash 强制开启并可指定档位(output_config.effort 取 low/medium/high/xhigh/max,省略即 max)。未知或越档的取值一律回落到该模型目录声明的默认档,不会把模型没有声明的档位发出去;
  • 视频已在能力声明里,并且真的能发出去:M3 与 M3.1 Flash 的 inputModalities 都是 ['text', 'image', 'video'](model-catalog.ts),线上形状取自实测——{ type: 'video', source: { type: 'base64', … } } 被该端点接受并解码(同一段实测也否掉了 data-URL 字符串那种封装,它会在 : 处按裸 base64 解码失败)。能不能读到字节是宿主注入的接缝:AttachmentVideoReader(adapter.ts:372,类型在 src/host/common/video-request.ts)由宿主提供,DSH 自己的附件服务只存图片,所以视频只能来自本部署里某个真实的生产者;宿主不提供读取器、或某一条读不出来时,resolveRequestVideos 把它解析成 unavailable,映射器(mapper.ts:416)才退化成一段说明文本——modalities.ts 的 videoOmissionText 按 4 种 reason 给出各自不同的措辞(unsupported-model / unsupported-container / unreadable / unsupported-wire),让模型知道「有这段视频、为什么没带上」,而不是对着空白回答。降级发生在单个片段上,不发生在声明上:声明的依据是接缝存在且线上形状实测可用,不是「任何情况下都一定能读到」;
  • 失败按 8 级判决分类,判词决定重试与号池动作(classifyMinimaxFailure,adapter.ts:388-470;读的是原始正文而不是 summarizeFailureBody 抽出的那一个字段,因为同一个判词在两种信封里放的位置不同):
    • 上下文超限——isHttpContextOverflow 先判,CONTEXT_OVERFLOW_CODE,终局、不归账号;
    • 账号级额度耗尽——403 或 429 都可能落到这一类,判据是正文点名了一个用尽的窗口,与状态码无关:isSpentWindow403(adapter.ts:355)管 403,429 分支(adapter.ts:425-431)用同一个谓词问同一个问题,两条都返回同一个 quotaExhaustedFailure(adapter.ts:368:PROVIDER_ERROR / retryable: false / accountScoped: true)。判据本身是两个臂(先问上游用量限额的数字码、再问措辞表兜底;这个先后只是求值顺序,不代表这条路由上真正会开火的是前者):namesUsageLimitStatusCode(adapter.ts:311)读两种信封里的上游码(平台信封 base_resp.status_code、客户端信封 errorCode,并额外往流内错误体 { error: … } 里多读一层),命中 USAGE_LIMIT_UPSTREAM_STATUS_CODES = {2056, 2067} 即算;非 JSON 或没有该字段的正文回落到措辞表 QUOTA_EXHAUSTED_PATTERNS。两个臂汇进同一个判决 namesSpentWindow = namesUsageLimitStatusCode || 措辞表(adapter.ts:334)——它们不是两套规则,而是同一个问题的两种承载方式:一句中文没有码,一个裸码没有句子。但要把定位说准:数字码臂在这条 Messages 路由上目前不会开火,它是跨信封的前向兼容臂(哪天服务真的在 Messages 路由上带出 2056/2067/errorCode,它就会生效),不是这条路由当前的主要判据。2026-10-20 的活体探针(scripts/probe-minimax-capabilities.probe.ts,原始证据 docs/minimax-code-probe-capabilities.md)把这条路由的错误信封量了个遍:端点上只有两种信封,没有一种带 base_resp——① 请求被拒 400 是 {"type":"error","error":{"type":"invalid_request_error","message":"…"}}(参数类错误上多一个 request_id),没有任何数字字段;② 认证被拒 401 是 {"code":401,"message":"auth failed"},顶层 code 是这条路由唯一的数字字段。base_resp.status_code 那个信封属于这条线路的别的路由(配额 coding_plan/remains、Files API、reset-card),不属于 Messages。连探针抓到的 2013 也不是字段:它只以文本形式嵌在 error.message 的括号里(… does not support max tokens > 524288 (2013)),而且是通用「参数非法」码——超限的 max_tokens、空 body、畸形 JSON 全都归它;本仓库在 reset-card 与 Files API 上另有语义,绝不拿它当额度判决。所以在 Messages 路由上当前真正 operative 的那一条是措辞表:数字码臂留在这里,是为了不让一个将来可能带上数字码的信封被当成措辞去猜,而不是因为这条路由现在会给码。它必须排在下面的凭据分支之前,因为 401/403 的正文在这条线上是同一句 token is required,光看状态码分不出「窗口用尽」与「凭据被拒」。号池据此冷却到窗口重置(未命中周窗口词冷却 5 小时,命中 WEEKLY_QUOTA_WINDOW_PATTERN 冷却 7 天,见 adapter.ts:172-184),且刻意不采信 Retry-After——等 N 秒不会让一个用尽的窗口重新可用;
    • 凭据失效——401/403 里不属于上面的,INVALID_CREDENTIAL,终局且归账号,但走 markAuthFailed 而不是 markCooldown;这条线的 401 正文在「没发凭据」和「凭据被拒」两种情况下是同一句 token is required,所以提示直接指向重新登录;
    • 纯背压 429——没有点名用尽窗口的 429(数字码与措辞两个臂都没命中),RATE_LIMIT,可重试且不归账号(同一套餐后面的每个账号都会被同样拒绝,冷却它只会把号池抽干);
    • 5xx——SERVER,可重试、不归账号;
    • 400——PROVIDER_ERROR,终局:请求本身不被接受,重发一次也不会被接受;
    • 其他状态码——PROVIDER_ERROR,终局,正文为空时提示「无响应」;
    • 401 另有一次性补救(合并下方一条):第一次 401 会强制续期一次并原样重试同一个请求体,重试再被拒才是终局;
    • 可重试的四类(RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT)走 DSH retry policy 的有界退避:最多 3 次,1.5s 起步、15s 上限、0.2 抖动;终局类不重试——重试只会推迟用户真正需要看到的那句提示;
  • 401 会强制续期一次再重试:本地时钟看着还有效、服务端却拒收的令牌(在桌面端被撤销、时钟偏移、桌面端已轮换)与「真的需要重新登录」在状态码上无法区分,所以第一次 401 会强制刷新一次并原样重试同一个请求体;重试再被拒才是终局;
  • 刷新令牌是单次使用的,因此续期提前 5 分钟、并全进程共用一次轮换:
    • 这是「每小时自己掉线、然后要求重新登录」的根因。access token 实测只有 1 小时,而原先的续期阈值是「到期前 60 秒」——等于每次都在令牌已经会被拒收的那一瞬间才去轮换;
    • 并发请求(状态卡轮询、签到、用量读取、模型请求)会各自拿同一枚刷新令牌去换,除第一个之外全部拿到 invalid_grant,而这条终局判定会被记成「该账号需要重新登录」并写进号池(重启也在);
    • 现在:① 续期窗口 PRE_EXPIRY_REFRESH_MS 为 5 分钟,轮换发生在服务端还没有拒收之前;② 同一份凭证的轮换按身份(recordKey / loginEpoch)单飞,后到的调用者要么加入在途轮换、要么采纳结果,绝不二次花费同一枚令牌;
    • ③ 轮换先看 storage 再相信终局判定——磁盘上的凭证若已前进(别人刚成功轮换,或桌面端自己刷新过),直接采纳,不记 tombstone、不报「重新登录」;
    • ④ 号池行上遗留的过期标记会在下次读取或请求时自动核对并清除;⑤ 卡片补上了「重新登录」按钮(onRelogin 此前未接线);⑥ 响应未返回新 refresh token 时沿用旧值,不再当作失败;
  • 请求体上限是本线路自己的 64 MB,不是 Kimi 线路的 2 MB(本条为修复项,详见 CHANGELOG):
    • 原缺陷:守卫直接复用了 Kimi 线路的 MAX_MESSAGE_BODY_BYTES(2,097,152),理由写着「两条线路上游都是同一族端点」。2,097,152 与 total message size N exceeds limit 2097152 是 Kimi Code 自己文档里的网关上限(见其错误参考),MiniMax 没有任何文档这样规定;
    • 代价:真实会话在 2,098,045 字节时被本地拒绝——请求根本没发出去,这一轮就失败了。该数字离 2,097,152 只差 893 字节,而超过 2 MB 本身不代表这个路由发不出去;
    • 更自相矛盾的地方:本线路的模型目录给 M3 标了 512K 上下文(可选 1M)、单张图片 10 MB——一个 2 MB 的整包上限与「单图 10 MB」根本不能共存,一张合法图片就能单独触发它;
    • 现在:上限改为本线路自己的 DEFAULT_MAX_MESSAGE_BODY_BYTES(64 MB,MiniMax 对带媒体请求自己公布的请求体量级),普通会话永远不会被本地拒绝,守卫只拦真正的失控请求(如每轮追加数 MB 工具结果的死循环),且仍在花掉连接之前拦下并给出同样的可操作提示;
    • 确有实测上限的部署(前置代理/网关)可设 DSH_MINIMAX_CODE_MAX_BODY_BYTES 指定;取值非正整数时回落到默认值而不是关闭守卫;
    • 同一个错误在图片预算上又犯了一次,而且更隐蔽(已一并修复):本线路调用的是 Kimi 的 offloadOldestRequestImages,其预算是 Kimi 的 1,500,000——那是为塞进 Kimi 自己的 2 MB 请求体上限而定的数字。MiniMax 的模型表允许单图 10 MB 原始字节(base64 约 13.3 MB),超出近 9 倍,于是一张普通截图就被换成占位文本;
      • 比拒绝更糟:omit 是静默替换——没有报错、没有计数,模型是在一个「图根本不存在」的对话上作答的,而现场没有任何线索指向原因。现在预算是本线路自己的 DEFAULT_MAX_REQUEST_IMAGE_BYTES(16 MB,装得下模型 10 MB 单图上限并留出余量,且远在 64 MB 请求体上限之内),可用 DSH_MINIMAX_CODE_MAX_IMAGE_BYTES 覆盖;
      • 改法:共享的是机制(度量、按最旧优先丢弃、不动持久历史),不是数字。offloadOldestRequestImages(options, maxBytes) 现把预算变成参数,缺省仍是 Kimi 的值,其余线路行为逐字节不变;
    • 守卫的形状仍与 Kimi 共用,数字不共用——字节上限是上游网关的属性,不是什么通用常量;
  • 额度面板有两套用量端点,第一轮打错了那一套:
    • 平台端点 /v1/token_plan/remains(api.minimax.cn / api.minimaxi.com / api.minimax.io)只接受平台 API 密钥:mcode-public 登录态在四种认证写法(Bearer / 原始 / x-api-key / 两者都带)下全部被拒,且是 HTTP 200 + base_resp.status_code: 1004——不是 401,很容易被误读为成功;
    • 官方 mmx CLI 能用该端点,是因为它的 OAuth 是平台登录,与本线路的 mcode 登录是两套身份;
    • mcode 自己的端点在官方客户端源码里(packages/tui/src/account/matrix-account-client.ts):GET https://agent.minimaxi.com/v1/api/openplatform/coding_plan/remains(国际 agent.minimax.io),路径与平台端点完全不同。本插件用的就是这条(本线路的 agent.minimax.cn 优先、官方主机兜底);
    • ⚠️ 但它带 yy / x-timestamp / x-signature / User-Agent: MiniMaxCode 四个「官方客户端标识头」——官方注释自己写明这些是「把请求标记为来自 MiniMax 第一方客户端」的字面量;
    • 本插件 types.ts 已明确决定不伪造官方客户端身份(伪造既失信,也可能是封号理由),因此默认不发,宁可拿到一个拒绝让卡片如实说明;
    • 确实要数字的用户可设 DSH_MINIMAX_CODE_QUOTA_ATTRIBUTION=1 显式选择该取舍;
  • 用量端点不在 MiniMax 的 API 文档里(文档只说「用量显示在控制台的用量条上」,这大概就是最初判定「没有端点」的原因),它来自官方 CLI——mmx quota show。返回的每个 model_remains 行带两个窗口(5 小时滚动 + 每周),卡片因此有两根条,各自显示重置时刻与「已用/总量」;
  • 三个必须照抄官方实现的解析细节:
    • ① *_usage_count 的语义是模糊的(老响应是「剩余」、新响应是「已用」),官方用显式百分比消歧——照抄,否则进度条会反过来;
    • ② 周窗口带显示倍率(返回的是百分比 × weekly_boost_permille,可超过 100%);
    • ③ status: 3 通常表示「不限量」,但两个总量都为 0 时它表示「当前套餐不含该模型」——渲染成不限量会凭空许诺额度;
  • 三处降噪防护:① base_resp 非 0 视为凭据被拒,只试一台就停并记住 30 分钟;② 其它失败按 unreachable 区分、记住 10 分钟;③ /status 只读缓存、绝不在轮询里发网络请求;
  • 读用量永远不影响对话:独立只读路径,失败只让卡片换一句话;
  • 设置面与其余各线路逐项对齐(该 PR 最初缺失这一整块,用户明确指出):
    • ① 「启用此供应商」总开关——关闭后适配器不暴露任何模型(包括已勾选的),因为「关掉」如果还能用就等于没关;
    • ② 模型勾选——只有勾选的模型进入对话页的模型选择列表,另有「全选 / 全不选」;
    • ③ 模型上下文窗口覆盖——每个已勾选模型可填自定义容量(接受 1M / 512K / 200000),「恢复默认」以 null 语义在 host 侧删除该键而不是写入 0;
    • ④ 默认思考深度——全局档位只在模型确实列了该档时生效,否则回落到该模型自己的默认档(广告一个模型会拒绝的档位只会让服务端拒掉请求),且「关闭思考」只对可关闭的模型生效;
    • ⑤ 号池管理(见下条);
  • 目录仍然硬编码,但「这一安装当前怎么用它」是用户设置:两者分开存储(目录随代码发布、设置随用户数据保存),因此一份用户数据不会看起来像一条已发布的目录项;
  • 号池与多账号登录:复用与其它线路同一个号池内核(src/host/common/account-pool.ts)与同一张共享账号卡片,具备顺序耗尽 / 轮询 / 粘性调度、429 冷却换号、设为主账号与备注。身份键不是令牌:桌面端凭据用它自己的 recordKey(同一个记录槽每次轮换都是同一账号,因此在池里原地更新),插件自持凭据用 loginEpoch(每次设备码登录就是一次独立会话)——用刷新令牌做键在每次轮换后都会把同一账号看成新账号;
  • 桌面端登录态可被显式「导入」为号池账号(POST /accounts {action:'adopt'}):只读取、只新增一行,不改动官方客户端的任何文件;重复导入按记录槽身份原地更新,不会产生重复账号。在设置页新发起的设备码登录会自动加入号池(pollWebLogin 的 onSave 钩子)——没有这个钩子,新登录只会写进镜像文件而号池看不见,这正是「多账号登录没配好」的根因;该钩子的失败不会让登录失败,因为凭据此时已经落盘。

Claude(订阅)线路

  • 注册 claude-subscription Provider,以 Claude Pro / Max 订阅的 OAuth 登录态访问 Claude 模型(Anthropic Messages 接口),不使用 API Key、也不按量计费;
  • ⚠️ 风险须知(插件不设确认步骤,但事实不变):Anthropic 现行条款明确写明不允许第三方应用提供 Claude.ai 登录、也不允许代用户经 Free / Pro / Max 凭据转发请求,并保留不经预告的执法权;已有开源项目被下架、有账号因此被限制。本插件未获 Anthropic 任何授权或认可,使用风险由使用者自行承担。此前版本在卡片上设有确认步骤与配套主机门禁,两者均已移除;移除的只是那一步交互,不改变上述事实;
  • 这不是抄一个 token 就能用:订阅令牌要求请求完整模仿 Claude Code 的身份,否则会被服务端拒绝或分类判别。这些都在代码里显式实现并有测试锁定:
    • Authorization: Bearer 且 x-api-key 必须缺省;
    • user-agent: claude-cli/<版本>、x-app: cli;
    • anthropic-beta 至少含 oauth-2025-04-20 与 claude-code-20250219;
    • system 的首个块必须是 Claude Code 身份声明;
  • OAuth 用 PKCE,且 state 与 verifier 独立生成。这一点特意不照抄参照实现:本机参照实现把 PKCE verifier 直接当作 OAuth state(state: verifier),那会把本应保密的 verifier 写进授权 URL、地址栏、浏览器历史乃至剪贴板。本线路是两次独立随机抽样,并有测试断言授权 URL 中不出现原始 verifier;
  • 两条登录路径,模式在流程开始时确定且不可中途切换:默认是手动粘贴(授权页把码显示在屏幕上,粘贴 <code>#<state>;也容错接受整条重定向 URL)。可选 loopback 回调,绑定 127.0.0.1 并顺序探测可用端口(Windows 的保留端口段会让固定端口绑定失败)。端口探测失败降级为手动而不是报错。切换模式等于作废当前流程并重新发起——授权码与签发它的那次请求的 redirect_uri 绑定,这是该流程最常见的失败;
  • 一次授权只兑换一次:浏览器回调与手动粘贴可能同时到达,用 compare-and-set 保证只有一方发起兑换(另一方得到「正在处理中」,已结算的流程得到 410 且不做任何兑换)。错误的 state 只拒绝那一个请求,不会终止正在进行的合法登录。回调服务器只绑 loopback、只应答 /callback、只接受本机来源;
  • 刷新是单飞的:并发请求共享同一次刷新。否则到期瞬间的一批请求会各自轮换刷新令牌,除第一个之外全部作废(上游会给出终局判定)。刷新令牌轮换后先读回校验再落盘;
  • 模型能力逐模型查表(src/host/claude/model-catalog.ts),不从模型名推断。该表转录自本机随 harness 安装的参照目录,只证明抄录忠实,不证明服务端提供这些模型——服务端自己的 GET /v1/models 才是权威,且它只覆盖上下文窗口,能力字段不被改写;
  • 思考形态有四类,顺序决定成败(thinkingMode):
    • mid-convo → {type:'adaptive', block_binding:{prefix_mismatch_behavior:'drop_block'}} 外加 output_config.effort;
    • adaptive → {type:'adaptive'};
    • budget → {type:'enabled', budget_tokens}(预算算术按参照实现转录;思考预算计入 max_tokens,必须为回答留出至少 1024 token);
    • none → 不发思考字段;
    • mid-convo 排在最前且无条件:claude-fable-5-1 与 claude-opus-5 同时带 forceAdaptiveThinking,把它们当成普通 adaptive 会静默丢掉 block_binding 与 output_config,而参照实现自己的注释写明该缺失会导致持续 400;
    • 反过来,block_binding 本身必须由 anthropic-beta: thinking-binding-controls-2026-08-01 授权(官方文档:缺该 beta 时返回 400 block_binding: Extra inputs are not permitted)。因此请求头从已构建的请求体读出是否带 block_binding,带则追加该 beta;
  • 思考块带签名则原样回放(这是思考模式下多轮工具调用的前提;redacted_thinking 同样回放),无签名则丢弃。工具名在出站时按 Claude Code 规范大小写归一化、入站时按大小写无关匹配回用户工具名;若两个工具归一化后碰撞,则放弃归一化原样发送,避免把结果投给错误的工具;
  • 多账号号池,与其它线路同源:复用与其它线路同一个号池内核(src/host/common/account-pool.ts)与同一张共享账号卡片,具备顺序耗尽 / 轮询 / 粘性调度、429 冷却换号、设为主账号与备注;
    • 身份键不是令牌:桌面端凭据用它自己的 recordKey(同一个记录槽每次轮换都是同一账号,因此在池里原地更新),插件自持凭据用 loginEpoch(每次设备码登录就是一次独立会话)——用刷新令牌做键在每次轮换后都会把同一账号看成新账号;
  • 可选择性收编本机已有的 Claude Code 登录(默认关闭):开启前只做一次「文件是否存在」的探测,不读取内容;只有你显式开启后才读取。收编得到的是一份快照,永不由本插件刷新——Claude Code 刷新的是同一枚轮换令牌,两个进程各自刷新会互相作废,而进程内的单飞解决不了跨进程竞态。快照过期后卡片会提示你回 Claude Code 重新登录后再收编,并且绝不会写入或删除 Claude Code 的任何文件;
  • 额度面有两个来源、两套单位,换算后有交叉测试证明它们描述同一状态:
    • 主来源 GET /api/oauth/usage 的 utilization 是已用百分比 0–100;
    • 而 /v1/messages 响应头 anthropic-ratelimit-unified-5h-utilization 是分数 0–1,重置时间是 epoch 秒;
    • 有真实流量时以响应头为准以降低查询频率,但卡片仍会按 QUOTA_FULL_REFRESH_MS 做一次完整读取;
  • 提示缓存时长可选择(设置页):本线路用订阅凭据,而 Claude Code 官方文档 写明订阅用户在套餐额度内对主对话使用 1 小时 TTL(超出额度改按用量计费后官方会降回 5 分钟)。此前固定使用 5 分钟默认值,与官方客户端行为不一致——同样的用量,官方用户享受 4 倍缓存窗口而本插件没有;
    • 1 小时是需要许可的能力:body 里写 ttl: '1h' 必须同时带 anthropic-beta: extended-cache-ttl-2025-04-11,否则被拒(与 block_binding 同理)。插件从已构建的请求体读出所选档位并据此发出该 beta,两者不可能不一致;
    • 写入 1 小时档价格更高,短会话不划算,因此设置项可覆盖为「跟随官方 / 1 小时 / 5 分钟」;
  • 换号的两条硬约束:只在凭据失败或账号级限流时换号——全局限流、过载与 5xx 绝不换号(其它账号共享同一全局限制)。一旦已有输出产出就绝不换号(否则会重复文本或重复工具调用),改为直接报错。换号最多 3 次;
  • 模型勾选、思考深度、上下文窗口覆盖与额度在「设置 → 订阅服务 → Claude」标签页中配置,输入框右侧另有额度胶囊(取剩余最紧的那个窗口)。

设置页

  • 展示账号(脱敏 email、套餐、账号 ID 后四位)、连接状态、额度与订阅增强功能开关;「输出详细程度」「推理摘要」两项的默认值即官方 Codex 客户端的取值;
  • 偏好落盘位置随 harness 生成:
    • 有 settings.register 的一代(≤0.1.6)仍写进 harness 的设置文档;
    • 0.1.7 起该 API 被 SettingsForms 取代,偏好改由插件自己持久化到 <dshHome>/storages/dsh-chatgpt-subscription-preferences.json(0600、原子写;读取失败或校验不过就回落默认值);
    • 首次运行会从旧设置文档里本插件的段一次性迁移;五条线路的模型开关同样从各自既有的 storages/*-models.json 水合,因此重启后不会像被重置;
  • 子代理的模型与思考深度沿用 DSH 自身设置:设置 → Subagent 卡片授权 Agent 可以为子代理挑选的模型(来自 DSH 已接入的全部 Provider,包含本插件的 Codex / Antigravity),新 Agent 的默认路由由 DSH 的 agent-default-model 设置提供;
  • 最大嵌套深度不在本插件设置内,由 DSH 侧决定:0.1.5 及以前是 preset 中 tool-subagent 行的 maxDepth(默认 3),0.1.6 起改由 subagent 服务的设置项提供(默认 1);provider-managed 表示把预算交给进程外提供方;
  • GPT-6 系列(6 Astra / 6 Sol / 6 Luna)默认使用 384K 有效上下文,可配置最高 872K;5.6 Sol / Terra / Luna 保持 272K,最高 1M,用于 DSH 压缩与溢出判断;其他模型保持目录声明值;
  • 单次输出上限按模型区分:GPT-6 系列为 128K(官方对 6 Astra / 6 Sol / 6 Luna 均标 128K),更早的模型保持 32768。这个数字只在本插件进程内使用,不会写进请求:订阅版 Responses 端点会以 400 Unsupported parameter: max_output_tokens 拒收该参数(#29),官方 Codex CLI 的 ResponsesApiRequest 里同样没有这个字段。它的实际用途是作为 DSH 侧的输出预留量(defaultMaxTokens),供压缩判定把完成部分计入上下文窗口;请求本身交给服务端决定长度。撞上服务端上限时该轮仍以 max-tokens 结束原因呈现(后端以 response.incomplete 收尾)。
  • 可访问的进度条、窄窗口/200% 缩放布局、深浅主题与 reduced-motion。

模型目录

显示名 模型 slug
6.1 Sol gpt-6.1-sol
6 Astra gpt-6-astra
6 Sol gpt-6-sol
6 Luna gpt-6-luna
5.6 Sol gpt-5.6-sol
5.6 Terra gpt-5.6-terra
5.6 Luna gpt-5.6-luna
5.5 gpt-5.5
5.4 gpt-5.4
5.4 Mini gpt-5.4-mini
5.3 Codex Spark gpt-5.3-codex-spark

目录只用于展示;账号实际可用的模型由 ChatGPT 套餐、workspace 策略与上游兼容状态决定。

GPT-6 系列(6.1 Sol / 6 Astra / 6 Sol / 6 Luna)支持文本、图片输入和工具调用,默认思考档位为 medium,可选 low、medium、high、xhigh、max。从旧会话带入的 none / minimal 会按 OpenAI 官方迁移说明 转为 low。三个模型的默认 384K 与上限 872K 均取自 2026-09-23 的 Codex 模型目录(gpt-6-sol / gpt-6-luna 于 2026-09-22 发布,能力与 gpt-6-astra 一致;目录里的 context_window 是 272K,本插件把默认有效上下文提高到 384K,仍低于 872K 上限);Codex Ultra 涉及客户端的子代理编排,本插件不将它作为 Responses 思考参数暴露。

新配置默认显示 GPT-6 系列与 GPT-5.6 系列;已有配置保留原来的模型勾选,可在 设置 → Codex 订阅 → 可用模型 中勾选 6 Sol / 6 Luna。

环境要求

  • Windows 或 Linux;
    • Windows:系统需提供 Windows PowerShell,以使用 CurrentUser DPAPI;
    • Linux:Host 用户必须拥有可写的 ~/.dsh(或 $DSH_HOME),凭据文件会强制使用 0600、目录使用 0700;
  • 已安装 DSH:peer 范围覆盖 0.1.2-alpha.5 及以后的每一代,一直到 0.2.1-rc.3。构建与测试以 0.2.1-rc.3 为基线,旧版行为由版本兼容层保留,因此同一份代码可装在 0.1.2-alpha.5 以来的所有代上(用户分散在 npm 的 latest / next / alpha 三个标签上,多数人跑的是比 alpha 落后几个版本的 latest)。需要桥接的破坏性变更:
    • 0.1.7 重写了会话消息模型(工具结果由 tool-result 内容块改为 role: "tool" 消息)、删除了 settings.register(偏好改由插件自有存储落盘)、并让 agent preset 不再从 ~/.dsh/.agent-presets 读取。插件在请求边界、设置服务与 preset 注册三处同时适配。
    • 0.1.6 把 workflow 引擎改了包名,插件在 preset 同步时按当前安装自动适配(见「随包分发的 Agent Preset」)。
    • 0.1.1-rc.2 不再声明支持——它既没有 preset 用到的 present 工具,mode 枚举那时也还写作 code。
  • Node.js 与 npm。

号池存储后端(poolStorage)

号池(每个供应线路的账号文档)存在哪里是一项配置,不再由平台写死。原先 Linux 上恒为 Secret Service(libsecret / org.freedesktop.secrets):桌面机没问题,服务器上通常根本没有 secret service,令牌换成功了、凭据却存不下来,登录报「ChatGPT credentials could not be saved securely」。唯一的老办法是装 gnome-keyring 并用空密码解锁去跑——那除了文件权限之外没有任何加密,还把一整套桌面栈拖上了服务器。

值 存储位置 静态加密
keyring(默认) 桌面密钥环:Windows 上是 DPAPI,macOS 上是钥匙串,Linux 上是 Secret Service 是
credentials DSH 自己的凭据库:$DSH_HOME/.credentials.yaml(由 DSH 的 dsh-credentials-local 提供;POSIX 上强制 0600,文件对他人可读时该 provider 拒绝启动;Windows 上没有权限位可强制,只有 profile ACL) 否(权限保护)

没有自动模式:一个会按「守护进程是否应答」自己挑存储的 auto,等于让同一个号池在两个存储之间漂移,而漂移看起来和「账号全没了」一模一样。选错了就如实报错——卡片写明当前实际使用的存储、为什么选中的那个不可用,并把切换开关放在同一处;留在另一个存储里的账号仍然在(切回去就能看到),不做迁移。

  • 默认 keyring:现有安装零变化,桌面凭据路径完全没动。Linux 上如果密钥环不可达(没有 secret-tool、没有会话总线、或密钥环锁着),保存会失败,但报的是一句明确的、告诉你把「账号存储位置」改成 DSH 凭据库的话,而不是静默换地方、也不是一句没有上下文的错误。
  • 服务器安装推荐 poolStorage: "credentials":唯一让本插件的账号和 DeepSeek、Telegram 等其它密钥躺在同一个 DSH 凭据文档里的模式,不需要任何外部守护进程。
  • 两个平台都可用:credentials 不绑定平台,Windows / macOS 服务器也可以选;选它时 DPAPI 与钥匙串完全不参与。
  • 该键只管号池文档;单凭据镜像(登录态预览用的那份)仍走平台存储,Linux 上本来就是 0600 文件。
  • 改 profile 里的 poolStorage 要重新挂载插件才生效(约等于重启):这个值在 apply 时读一次,池子只在插件重新应用时按它解析。设置页卡片上的选择是另一条路——它写进插件自己的存储文档、并立刻让所有池重新解析,所以两个入口的生效时机不同,别把它们当成一回事。
  • 号池在 DSH 凭据库里的地址是 dsh-chatgpt-subscription/pool-<路径的 sha256>。pool- 前缀是必须的:seam 要求每段匹配 /^[a-z][a-z0-9-]*$/,而裸摘要里有 62.5% 以数字开头,那样的 key 会在下一次启动解析整份凭据文档时被判非法,把宿主上所有凭据一起带走。
  • 如果曾经用本 PR 的未发布构建选过 credentials,旧的无前缀 key 要手工改一次,自动迁移做不到:key 非法时 DSH 的凭据 provider 自己就起不来,插件手里根本没有可以拿来做迁移的 seam,而背着 provider 去改它的 $DSH_HOME/.credentials.yaml 比这个 bug 更危险。修法是改名,改完重启即可(账号一条不丢):
    sed -i -E 's#(dsh-chatgpt-subscription)/([0-9a-f]{64})#\1/pool-\2#g' "$DSH_HOME/.credentials.yaml"
    
    没有任何发布版本写过无前缀形态,所以这条只影响跑过本分支本地构建的机器。
  • 设置页的账号卡片里就是这条设置的界面:Account storage 选择器(带每个选项的说明)、一行「当前实际使用:…」、以及一句「另一个存储里的账号留在那里,切回去之前不显示,不做迁移」。选完立刻写进插件自己的存储文档(<dshHome>/storages/dsh-chatgpt-subscription-pool-storage.json),并以这份文档为准——profile 里的 poolStorage 只作为没有在卡片里选过时的默认值。
# 服务器(主机专用于 DSH、没有桌面密钥环)
poolStorage: credentials

# 带桌面会话、密钥环已解锁的机器:保持默认即可
poolStorage: keyring

安装

方式 1:通过 DSH CLI 安装(推荐)

直接从 npm 安装已发布的插件包:

# Windows PowerShell、Bash 和 POSIX sh 均可执行
# 如果全局安装了 dsh
dsh plugin --profile web add @eddyskywalker/dsh-chatgpt-subscription

# 或使用 npx 直接运行
npx @deepseek-ai/dsh plugin --profile web add @eddyskywalker/dsh-chatgpt-subscription

版本阶段:上面两条命令装到的是 npm latest 标签指向的版本(撰写时为 0.10.12)。包不预置 publishConfig.tag,因此稳定版一发布就落在 latest——也就是 npm install 与 dsh plugin add 解析到的那个标签;预发布版只进 alpha,需要显式带上标签或版本号:

dsh plugin --profile web add @eddyskywalker/dsh-chatgpt-subscription@alpha
npm install @eddyskywalker/dsh-chatgpt-subscription@alpha

方式 2:在 DSH 界面里安装

DSH 没有插件市场;Web 界面的 Plugins 页提供按包名安装的入口(底层与 dsh plugin add 相同):

  1. 打开侧栏的 Plugins 页;
  2. 在添加插件的输入框里填 @eddyskywalker/dsh-chatgpt-subscription 并安装;
  3. 重启 dsh web。

方式 3:本地开发调试(源码软链接)

如需进行二次开发或本地源码调试:

git clone https://github.com/Aa728848/dsh-chatgpt-subscription.git
cd dsh-chatgpt-subscription
npm install
npm run build

# Linux
npx @deepseek-ai/dsh plugin --profile web add "link:/absolute/path/to/dsh-chatgpt-subscription"

# Windows PowerShell
npx @deepseek-ai/dsh plugin --profile web add "link:C:\absolute\path\to\dsh-chatgpt-subscription"

使用

  1. 重启 dsh web;
  2. 打开 设置 → Codex 订阅;
  3. 完成 ChatGPT 登录;
  4. 执行 测试连接。

DSH 模型选择器应显示 “Codex(ChatGPT 订阅)”。GPT-6 系列与 GPT-5.6 系列的有效上下文窗口在“Codex 订阅 → 增强功能”中配置。子代理的模型与思考深度由 DSH 自身的设置决定(Subagent 卡片授权的模型清单,以及 agent-default-model 的默认路由;该卡片 0.1.5 及以前在「设置」页,0.1.6 起在 Plugins 页);最大嵌套深度由 DSH 侧决定(0.1.5 及以前取 preset 中 tool-subagent 的 maxDepth,默认 3;0.1.6 起取 subagent 服务的设置,默认 1)。

设置 → Codex 订阅 → 网络代理 是本插件全部供应线路共用的一份代理设置(ChatGPT、Claude、Antigravity、Kimi Code、MiniMax Code、Command Code、Ollama、GLM Coding Plan、Step、Cline Pass、Qoder、OpenDesign),可选择系统代理(自动检测)、自定义代理或直连。各线路的模型生成、登录与令牌交换/续期、账号信息、项目发现、配额与模型目录查询,以及插件自带的工具与网页抓取,都使用此设置;修改后对后续请求生效,无需重启 DSH。

系统代理自动检测按平台分流:Windows 读注册表(同一份输出里的 ProxyEnable/ProxyServer)、macOS 读 scutil --proxy、Linux 读 GNOME 的 gsettings(org.gnome.system.proxy 的 mode/host/port;PAC 模式不支持,因为代理只能经代理端口转发,无法施用自动代理脚本);都拿不到时回落到 HTTPS_PROXY/HTTP_PROXY/ALL_PROXY 环境变量与 $DSH_HOME/.env。

代理生效时,回环地址(整个 127.0.0.0/8、::1、localhost)与 NO_PROXY / no_proxy 列表里的主机仍然直连——把某条线路的 baseURL 指向本机网关(例如 http://127.0.0.1:9090)时不会被塞进代理。NO_PROXY 与代理同源读取(进程环境变量优先,其次 $DSH_HOME/.env),支持 *、域名后缀(example.com 与 .example.com 都匹配子域)以及 host:port;不支持 CIDR。浏览器中的授权页面使用浏览器自己的网络设置。

已经用官方 Codex CLI 登录过的用户可以跳过第 3 步的 OAuth:设置 → 订阅服务 → ChatGPT,在账号池卡片里点「登录」旁边的「导入本机 Codex CLI 登录」即可(见「本机登录」)。

Command Code

  1. 重启 dsh web;
  2. 打开 设置 → Command Code;
  3. 点击 浏览器登录(或把 Command Code Studio 里创建的 API Key 粘到「手动填写 API Key」里保存);
  4. 登录完成后按需勾选模型、设置上下文窗口与默认思考深度。

command-code 会像其他 Provider 一样出现在 DSH 模型选择器中。线路按模型 id 自动选择:claude-* 走 Anthropic Messages,其余走 OpenAI Chat Completions;设置卡片的模型标签会显示每个模型对应的线路。

路由归属:DSH 的 registerAdapter 对重复 Provider 是 all-or-nothing 并抛 DUPLICATE_ADAPTER,因此若 command-code 已被别的适配器占用(典型情况是内置 llm-pi-ai 用同一端点声明过同名 Provider),本插件不会加载失败,而是在卡片上显示“模型路由已被其他 Provider 占用”;从占用方的配置里移除该 Provider 后,插件会在下一次路由变更事件时自动接管,无需重启 DSH。

上下文窗口默认取 Command Code 模型目录的 context_length,可在卡片中逐模型覆盖(用于 DSH 的压缩与溢出判断,支持 1M / 512K / 200000 等写法)。默认思考深度逐模型生效:下拉里列出的档位可按模型能力选用(minimal / low / medium / high / xhigh / max),实际发给上游前会按当前模型声明的档位取值——模型不声明该档位时不发送。对 Anthropic 线路映射为 thinking 预算(minimal 1K / low 2K / medium 8K / high 16K / xhigh 24K / max 32K),预算放不下时该请求不启用 thinking;对 OpenAI 线路映射为 reasoning_effort。

额度来自 /alpha/billing/credits、/alpha/billing/subscriptions 与 /alpha/usage/summary,三条线路相互独立容错,页面可见时最多每 60 秒刷新一次;解析不出有界额度时显示空态而不是 0%。

额度卡片展示:套餐名(由订阅返回的机器 id 查表得出,表在 src/host/command-code/plans.ts,按最长前缀匹配)、订阅状态与续费日期、滚动窗口用量(windowLimits 的 fiveHour / weekly,按官方 CLI 同款标签 5-hour / Weekly 显示,并各自带重置倒计时),以及余额明细(月度额度 / 已购额度 / 赠送额度,另附合计)。服务只回报数字不回报名称的字段一律补上可读标签——窗口按 key 命名,余额按池命名,实在没有名字的兜底为 Extra allowance 并注明来源,不会渲染成 meter-1 这类无意义编号。

搜索来源 切换 DSH 的搜索后端;网页抓取来源由下面的抓取模式决定。模式为 auto(默认)时跟随此处的搜索选择:选择 ChatGPT 来源后,网页也改由本插件在 Host 抓取,并使用上述网络代理设置;模式为 dsh 时即使选了 ChatGPT 搜索,抓取也仍由 DSH 原有来源完成。纯 TUN 模式可使用直连,流量由虚拟网卡接管。来源切换即时生效,DSH web 服务重载后会重新注册插件后端,并保留切回 DSH 默认来源所需的配置。设置页在该选择器下方只读展示 Host 上报的抓取来源、切换器状态、抓取模式与插件上限;Host 未提供这些字段(较旧的 Host)时整个区块不显示。

网页抓取后端(web_fetch) DSH 内置抓取 provider 会先解析域名、校验并固定解析结果,而且只在进程环境变量里读到代理时才走代理——系统代理对它不可见。代理工具(Clash/Mihomo 等)常把域名解析成自己的 fake-ip 地址(默认 198.18.0.0/15),于是内置 provider 直接以 WEB_BLOCKED_URL(resolves to a non-public IP address)拒绝,代理根本没被用上。因此在 auto 模式下,只要插件配置了可用代理(网络代理 选系统代理且检测到,或填写自定义代理),web_fetch 就改用本插件的 provider:由代理解析源站,与 DSH 对“走代理的请求”采用的语义一致;未配置代理时仍由 DSH 内置 provider 抓取,保留其解析与固定策略。若在纯 TUN 模式下把代理设为直连,内置 provider 会重新接管(auto 模式下),此时可改回系统代理让插件接管抓取。代理如果在 DSH 启动之后才可用(代理工具后启动,或首次探测失败),插件会在下一次探测到代理时重新选择抓取后端,不必重启或改设置。

抓取模式与上限(插件 Config)

抓取模式与上限属于插件部署配置,不在设置页的偏好里:设置页只读展示当前值,要改写在 profile 的 cordis.patch.yml 里,重载插件或重启 DSH 后生效。

写在安装时已有的那一条插件行上(id: dsh-chatgpt-subscription,包名 @eddyskywalker/dsh-chatgpt-subscription)的 config 片段:

# $DSH_HOME/profiles/<profile>/cordis.patch.yml
- id: dsh-chatgpt-subscription
  config:
    fetchProvider: auto              # auto | plugin | dsh
    fetchMaxBodyChars: 100000        # 解码后正文保留的字符数
    fetchMaxResponseBytes: 2097152   # 响应体保留的字节数(2 MiB)

patch 按 id 定位条目并整体替换它的 config,所以在这一行上追加字段时请保留原有的其它字段。

字段 默认 含义
fetchProvider auto auto 跟随搜索来源(有可用代理或选了 Codex 搜索时用插件抓取);plugin 始终用本插件抓取;dsh 强制保留 DSH 原有的抓取来源,即使有代理或选了 Codex 搜索也不接管
fetchMaxBodyChars 100000 解码后交给工具的正文上限(字符)
fetchMaxResponseBytes 2097152 进入解码前的响应体上限(字节)

两者都必须是正安全整数,否则该配置不被接受(DSH 不会把非法值当成 0 使用)。

必须说清的四条边界:

  • fetchMaxResponseBytes 是“读进来之后再截断”,不是下载量或内存的硬上限。 实现仍是 response.arrayBuffer() 先把整个响应体读进内存,超出部分才被切掉;调小它省不下下载流量和峰值内存,只能限制进入解码与工具结果的内容量。
  • 工具层另有上限。 这两个数字只管本插件 provider 交给 web_fetch 的内容,DSH 工具结果本身还有自己的裁剪预算,因此调大它们并不保证取回完整页面。
  • 两个上限只在实际使用插件抓取时生效;走 DSH 原有抓取来源(dsh 模式,或 auto 模式下本次抓取没落到插件)时不适用。设置页会直接写明这一点。
  • plugin 抓取不做 DNS 固定(pinning)。 内置 provider 会解析并固定解析结果,而代理的 fake-ip DNS 让这一步无法照搬;本插件沿用既有安全模型——只做 http/https 与目标地址不得落在私网/回环的校验,域名交给解析它的代理解析,解析失败也放行。抬高上限不改变这条边界。

卸载前建议先在设置页点击 “注销”,它会删除当前平台的凭据和 Host 内存中的额度缓存。

若 DSH 已异常退出,可在确认路径后手动处理凭据文件:

  • Windows:%DSH_HOME%\storages\dsh-chatgpt-subscription\oauth.dpapi,未设置 DSH_HOME 时为 %USERPROFILE%\.dsh\storages\dsh-chatgpt-subscription\oauth.dpapi;
  • macOS:凭据位于登录钥匙串,可执行 security delete-generic-password -s dsh-chatgpt-subscription -a oauth 删除;
  • Linux:$DSH_HOME/storages/dsh-chatgpt-subscription/oauth.json,未设置 DSH_HOME 时为 ~/.dsh/storages/dsh-chatgpt-subscription/oauth.json。

Windows 文件只能由创建它的用户通过 DPAPI 解密。macOS 凭据由登录钥匙串在本机加密保存。Linux 文件是未额外加密的 JSON,依赖目录 0700 和文件 0600 隔离;不要复制、打印或提交该文件。跨平台迁移需要重新登录。

随包分发的 Agent Preset

插件自带一个 调度模式 agent preset(id dispatch),随 npm 安装一起分发:0.1.7 起 harness 不再从发现根目录读取 preset,插件改为运行时注册(src/host/agent-preset.ts:探测 @deepseek-ai/dsh-agent-preset 能否解析、agentPresets 服务是否在场,然后在 presets/dispatch/preset.yml 与 agent.cordis.yml 转录出的定义上调用 register(),注册失败只记 warn);0.1.6 及以前仍按老办法把包内 presets/dispatch/ 同步到 <dshHome>/.agent-presets/。两条路都让装了本插件的机器在新建会话时直接选到它,不需要手工拷贝文件。

为什么不用静态声明行:0.1.7 的 preset 声明要写进 bundle patch,而 assertEntriesLoaded 会把「无 fiber 且未 disabled」的条目判为启动失败——@deepseek-ai/dsh-agent-preset 在 ≤0.1.6 上并不存在,静态声明会让那些机器直接开不了机。声明行也不能按运行环境条件化,所以选择运行时注册 + 能力探测。

同步在每个 profile 启动时执行一次(幂等):

  • 目标树与包内副本逐字节相同时跳过,有差异时整体重写,并把包内已删除的多余文件清理掉;
  • 行里的包名按运行环境改写:preset 要挂载 @deepseek-ai/dsh-workflow-worker-thread,而 harness 0.1.6 把它改名成了 @deepseek-ai/dsh-workflow-ptc,指向不存在的包会让整个 preset 被判为 broken、既不可选也不可复制。同步时用 import.meta.resolve 探测当前安装能解析哪个名字(旧名仍在就保留,否则改写成新名;两个都不可用时不改写,因为改名只会掩盖试过哪个),所以同一份 preset 在 0.1.5 与 0.1.6 上都能挂载;
  • 只处理本包自己的 preset id(BUNDLED_PRESET_IDS),绝不改动用户手写的 preset 或其它插件的 preset;
  • 包内已不再随附的旧 id 会从目标根目录移除(retire);
  • 同步失败(例如 home 只读)只记一条 warn,不会导致插件加载失败——preset 是便利项,不是本插件提供的核心能力。

路径解析不写死相对路径:src/host/preset-sync.ts 从模块位置向上查找最近的 package.json 作为包根,因此 src/ 布局、打包后的 lib/ 布局,以及通过 pnpm symlink / Windows junction 安装都能正确解析。注意 fs.cpSync({ recursive: true }) 在 Node 22 + Windows 上遇到含非 ASCII 的源路径会直接崩进程(nodejs/node#54476),所以复制是逐条目实现的。

与 Agent Teams 不兼容(刻意互斥)

DSH 的 Agent Teams(bundle @deepseek-ai/dsh-experimental-agent-team-profile)会在每个会话自己的作用域里注册一套与内置同名的协调工具。工具注册表按作用域链解析、近的层遮蔽远的层,所以一旦该 bundle 被组合进来,会话里的 send_message、list_agents、interrupt_agent 就都换成了 teammate 版:

  • list_agents() 只列 Team 成员(Lead 自己显示为 lead),看不到 subagent 派出去的子代理;
  • send_message 的 target 只接受成员名,把子代理的 session id 传进去会抛 active teammate "…" not found(实测);
  • 于是「派得出去、回访不了」——subagent 的孩子只能等完成通知,无法追问。

坏消息在时机上:Agent Teams 的 agent-team/src/roster.ts 里 tryMembership 对任何「无 parentSession、且不是 subagent descriptor」的 Agent 直接判为 lead,不需要先有 teammate。于是 team 工具从会话第一轮起就装在 Lead 上,调度模式还没派发第一个子代理,复审与返工所依赖的三个工具已经没了。

上游对此的立场是明确的:tool-agent-team 的文档写着「当同名的旧全局 subagent 工具必须继续可用时,请不要选择……想同时使用两者的组合必须禁用旧定义」。而 dispatch preset 恰恰是那组旧定义的持有者——0.1.7+ 的运行时注册里由本插件自己挂了 tool-subagent-control / tool-subagent-list-agents / tool-subagent / tool-subagent-fork 四行(preset 每一代都是独立 createScope,与宿主组合里被 profile patch 置 disabled 的那四行不是同一批实例)。所以这不是「可以调好的配置」,而是一个上游明确不承诺的组合。

dispatch 的处理是停,不降级(persona 的 R0.5 兼容性闸门,每个新会话判一次,早于 R0 分诊):

判据 行为
spawn_teammate 与 wait_agent 都不存在 编排可用,写一行「编排可用:subagent」后按 R0 → R-G → R5 → R6 正常走
任一存在 本会话不可编排:一句话说明 → 一次 ask_user_question 给出三条出路 → 用户答复前不分诊、不规划、不派发、不动文件

三条出路是:① 关掉 Agent Teams bundle、重启 Host 后新建会话;② 换用别的 preset,本会话不编排;③ 本次直接做,不进编排流程。都要新建会话——工具面在会话启动时就定死了,改完配置在旧会话里也不会回来。

刻意没有保留「切到 teammate 词汇继续编排」这条分支(原 R-T)。三条理由:

  1. 遮蔽由 scope 链决定,persona 改不回工具注册表——「适配」只能改行为,改不了「哪些工具被顶替」这个事实;
  2. 那条分支把「回访不了」从明摆着的失败变成看不见的失败:roster 里查不到孩子 ≠ 孩子没了,模型会误判并重复派发,白烧一轮预算;
  3. teammate 结构上拿不到 per-task 路由(见下节),而 R2 显式路由正是这套设计里最值钱的部分——组队等于自愿放弃它。

想把误用也用机制挡掉:把 spawn_teammate 从 inherit 清单去掉即可,配置 subagentModelInheritTools: ['subagent_fork'],该工具在守卫里就变成硬拒(拒绝理由指向 subagent),而不是让它悄悄按 Lead 的路由跑完整轮。插件默认值不动——那是全局设置,改它会波及不使用调度模式的会话。

回合边界纪律(R-W)

调度模式的 persona 里有一条专门管「回合边界」的规则 R-W,约束三件最容易把编排拖慢、拖贵的事:

场景 规则 为什么
等子代理返回 默认结束本轮回复、一个工具都不调,由完成通知唤醒主代理;禁止 sleep、轮询循环和任何只为占位存在的长命令。确实被阻塞时才用 job_output({ wait: true }) 有界地等一次 子代理完成通知是自带的;占位长命令不产出信息,只烧上下文和预算,还把用户晾在一段沉默里
任务开始建目标 除非用户明确要求,否则不调用 create_goal / /goal,任务清单一律走 todo_write 机制性的:目标一旦激活,DSH 会自动续跑唤醒主代理,而子代理何时返回不由它控制——等待被从中间劈开,模型重新分诊、重新检查「还差什么」,每次续跑还要重载整段历史
需要用户拍板 一切「我停在这里是因为缺一个决定」的停点(收到子代理结果要选下一步、验收不过要定返工范围、前提被推翻要改计划)都用 ask_user_question 递出去,并给出选项与推荐答案 提问是代理的工具,不是用户的义务:否则用户得先替模型把那段话翻译成一次输入。例外是 plan mode 的计划审批(走 exit_plan_mode)与任务完成后的收尾

三条各自对应 persona 反模式列表里的第 13–15 条。此外,本插件在运行时通过 system-prompt/assemble 原位重写 list_subagent_models 与 subagent 工具描述中的 "Catalog membership is advisory" / "Child LLM selection is optional..." 误导措辞,并通过 tools/post-execute 在每次 list_subagent_models 返回末尾直接附上本会话全部已授权的 { provider, model } 拆解对;R2 与 plan-mode 提示词也同步写明 list_subagent_models 是本会话允许清单的唯一真源,直接采信其输出即可,无需仅为了确认哪些子代理模型可用去翻找本地配置文件或工作区代码(对应 Anti-Pattern 第 16 条)。

为什么不降级:teammate 不能选模型

这是上面第三条理由的技术底座,也是上游修好之后才谈得上重新评估兼容性的前提。

spawn_teammate 目前无法指定 provider/model,这不是插件的问题,而是 Agent Teams 有意不做 per-teammate 路由。三层都已核对:

层 能否带路由 证据
工具入参 ❌ spawn_teammate 的 schema 只有 name/description/prompt/context
Team 服务 ❌ SpawnTeammateRequest.provider 是 subagent 后端名(spawn/fork),不是 LLM provider;且无 model 字段
底层 API ✅ ContinuableStartSpec.request 可带 agentOptions(含 provider/model/reasoningEffort),但 Team 服务构造 request 时只放 prompt 与 parent,整个省略

因此成员必然跑在 Lead 自己的路由上——这正是守卫把它归 inherit 模式的原因,也是不应把它加进 explicit 的 subagentModelTools 的原因。

插件侧无法绕过,四条路都已验证死:pre-execute 明确排除输入改写(参数已被记录与呈现,PreToolDecision 只有 allow/deny/cancel/ask);agentTeams 服务在内部丢弃后包装不到;注册自定义 subagent provider 也拿不到路由(agentOptions 在 continuation manager 就已解析完,provider 只返回 seed);唯一能改子会话路由的 selectForNextRequest 只作用于「下一个请求」,而 teammate 创建后立刻开跑,抢不进去。

上游正路:在 packages/experimental/agent-team/src/roster.ts 构造 startContinuable 的 request 处补上 agentOptions,并给 SpawnTeammateRequest 加字段、给工具 schema 加参数、创建前接 assertAllowedModelSelection。该改动落地后,spawn_teammate 就应从 subagentModelInheritTools 移到 subagentModelTools(explicit)——否则它会变成「能选模型却没人管」。只补这一条还不足以解除互斥:team 工具的遮蔽发生在会话第一轮(见上节),要真正共存还需要把安装条件从「是 team 成员」收紧成「有 teammate」,让没有队友的会话保留内置的 subagent 协调工具。

实践提醒:inherit 模式校验的是当前会话路由。若当前会话跑在允许清单之外的路由上(例如默认模型与勾选清单不是同一条),teammate 与 subagent_fork 都会被拒绝——拒绝文案会指出恢复路径(改用显式路由的 subagent,或先把会话切到清单内的模型)。subagent 的显式路由不受影响。

配置项

字段 默认 含义
syncAgentPresets true 启动时是否把包内 preset 同步到 <dshHome>/.agent-presets;设为 false 则完全不写用户目录

手动安装(可选)

不想让插件写 home 目录时,可把 syncAgentPresets 设为 false,再自行拷贝包内的 presets/dispatch/ 到 <dshHome>/.agent-presets/dispatch/。

计划模式管线(R9)

调度模式多给主代理两个工具,把「进入计划模式」和「出计划」拆成两步:

  1. 澄清收敛才进入 —— 只有判定 L2 且 R-G 访谈收敛(前沿为空 + 用户明确确认)时才调 enter_plan_mode。进入这一步只完成进入:不做只读探索、不写计划、不派发。
  2. 下一步出计划 —— 下一步在计划模式内只读探索并写出 decision-complete 计划,然后以 exit_plan_mode 作为该轮唯一且最后的工具调用提交审批。leave_plan_mode 是放弃计划的出口,不是审批出口。
  3. 被打回就留在计划模式内按反馈修订,再走 exit_plan_mode;放弃计划必须由用户明确表示。

两个工具由 preset 的 planning 组内 plan-mode-pipeline 行注册(与 plan-mode 同一个 realm、排在其后),只属于挂载了本 preset 的会话。开关 planModePipeline 默认开启;关闭后它们不再出现在模型可见的工具列表里(新代按逐步准入隐藏;老代的注册面不接受准入参数,此时调用直接返回 active: false / transition: 'disabled'),调度模式退化为用一次 ask_user_question 请用户手动输入 /plan,然后按已配置的 plan-mode section 继续——不得把计划贴在普通回复里代替审批。设置页「进展更新」卡片里有同名开关,保存即生效。

子代理模型授权(0.2.15 起,0.3.2 起强制指定模型)

DSH 设置页的「Subagent」卡片会把勾选的模型写成会话级的允许列表(会话日志事件 subagent/model-selection-policy)。DSH 内置委派工具只拒绝模型显式填写且不在列表内的路由;调用里不写 provider/model 时,子代理会继承父级模型,于是白名单之外的主模型(例如 deepseek-official/deepseek-flash)仍会被子代理使用。

插件在 Host 工具注册表上补一个单调守卫(ctx.tools.guard),在委派执行前要求每次委派都必须写明路由:

  • 调用会话(或最近的、记录了策略的祖先会话)带有允许列表时,provider 与 model 必须成对给出,且该路由必须落在列表内——缺省不再回退到配置默认值或父级模型;
  • 只写一半(例如只给 model)同样被拒绝,拒绝理由会指出缺的是哪一个字段;
  • 子代理永远和主代理同一个模型的历史行为由此消失:模型必须先用 list_subagent_models 查出已授权路由,再按拒绝理由里列出的路由(例如 antigravity/gemini-3.8-flash)重试;该工具也只展示已授权路由;
  • 未记录允许列表的会话(例如恢复的旧会话、未启用该设置的会话)保持 DSH 原有行为(继承父级模型)。

inherit 模式(subagentModelInheritTools,默认 ['subagent_fork', 'spawn_teammate']):有些委派工具按设计就没有路由参数,子代理必然跑在调用者自己的路由上——subagent_fork(复用对话与 KV Cache)与 Agent Teams 的 spawn_teammate(成员一律继承 Lead 的路由)都是这一类。「工具不能选」不等于「这个选择被授权」,所以守卫改为校验它实际继承到的路由:允许清单内有该路由就放行,没有就拒绝,并说明该工具无法改路由、要么改用能显式指定路由的 subagent、要么先把本会话切到清单内的模型。设为 [] 可关闭 inherit 模式的全部校验。

fork 的准入与发现工具无关,这是它最重要的性质:subagent_fork 不需要 provider/model,也不需要 list_subagent_models,判据只有「调用者当前实际在跑的路由是否在允许清单内」(取会话最新一次请求头,中途换模型判定随之更新)。

会话状态 subagent_fork 说明
没有记录允许清单 可用,且不校验 allowed.length === 0 时守卫直接放行
有清单,父路由在清单内 可用 主代理本身就是被勾选的模型即为这一类
有清单,父路由不在清单内 拒绝,且不可自救 换 subagent 显式指定清单内路由,或先把会话切到清单内模型

因此「发现工具缺失」或「委派工具不暴露路由参数」都不构成派不出子代理的理由:先试一次 fork,拒绝理由会直接写出父路由与授权清单。反过来,fork 也不该被当成通用更优解——它不能换模型,且继承整段已完成的轮次,只适合需要主代理上下文的复审与接续。

自适应:工具到底有没有路由参数,是运行时读出来的,不是配置里假设的。 上面两种模式是配置,而工具的参数是事实——harness 改键、或 preset 写错一个键,都会让一个被当作 explicit 的工具根本没有 provider/model(典型是 0.2.1-rc.1 把 modelSelectionSettings 拆成 modelSelection + modelDiscovery 之后,仍带旧键的 preset)。此时若继续要求显式路由,模型拿到的理由无法照做,而工具自己还会抛 child model selection is disabled for this tool instance。守卫因此会在拒绝前通过宿主工具注册表的 definitions(agent) 读一次该工具的参数表:没有路由参数就按 inherit 模式校验它实际继承到的路由(该拒绝照样拒绝,只是理由变成可执行的),有就维持 explicit 模式;注册表读不到定义时退回配置模式,不退化成放行。

守卫装在 DSH 工

…