Repository navigation
Releases: Aa728848/dsh-chatgpt-subscription
Releases · Aa728848/dsh-chatgpt-subscription
Release list
v0.38.1
- [kimi-code] 请求体上限改成按模型分档:1M 档 8 MiB、256K 档 2 MiB(
DSH_KIMI_CODE_MAX_BODY_BYTES仍是全局强制覆盖)- 现象:1M 上下文档位(
k3/k3.1-pro/k3.1-pro-highspeed/kimi-for-coding)的会话把中文正文写到 3–4 MB 就被本线路的本地字节守卫拒绝,请求根本没发出去;宿主压缩也救不回来——压缩请求自己同样超过那个上限,于是长会话表现为「压缩一次后仍然失败」,而服务端从未拒绝过这个体积。 - 根因:这条线路对所有模型套用同一个
MAX_MESSAGE_BODY_BYTES(2,097,152)。该数字来自官方错误参考里的通用示例(total message size N exceeds limit 2097152),从未针对 1M 上下文档位实测;而 1M 窗口的正文按英文约 4 MB、按密集中文约 4.5 MB,2 MB 必然先到。官方kimi-cli全仓库没有字节守卫,说明这个上限至少不是客户端必需的。 - 改法(分档):
model-catalog.ts的KimiCodeCatalogModel新增必填字段maxRequestBodyBytes:k3/k3.1-pro/k3.1-pro-highspeed/kimi-for-coding= 8 MiB(8,388,608),k3-256k/kimi-for-coding-highspeed= 2 MiB(2,097,152,保持原值)。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,压缩与重试照旧生效。8 MiB 仍是待实测的上界,不是文档写明的网关上限。 - 改法(代码):
types.ts新增LONG_CONTEXT_MESSAGE_BODY_BYTES(8 × 1024 × 1024,常量定义在model-catalog.ts、由types.ts重新导出,避免 ESM 循环)与maxRequestBodyBytesFor(model, env),优先级为 env →model.maxRequestBodyBytes→DEFAULT_MAX_MESSAGE_BODY_BYTES(注册表不认识的 id 走最保守的 2 MiB);maxMessageBodyBytes(env)与其语义原样保留(无模型上下文的调用方仍拿 2 MiB)。mapper.ts的assertRequestBodyFits(body, carriesVideo = false, context?, limit?)新增可选limit,拒绝文案引用生效上限,不传时逐字节等于旧行为。adapter.ts的bodyLimit改为carriesVideo ? MAX_VIDEO_MESSAGE_BODY_BYTES : maxRequestBodyBytesFor(<该模型的注册表行>, process.env),并把这个bodyLimit显式传给两处assertRequestBodyFits——此前这两处各自又算一遍上限,模型档位根本到不了守卫。注册表行按 id 从model-catalog.ts取,而不是直接用线上/v1/models的条目:线上目录只报能力、从不报字节额度。 - 改法(env):
DSH_KIMI_CODE_MAX_BODY_BYTES仍是全局强制覆盖、优先级最高;空串、非有限、0、负数一律回落到该模型的默认值而不是关闭守卫(合法值会 floor,前后空格容忍)。视频档MAX_VIDEO_MESSAGE_BODY_BYTES(64 MiB)不受影响。 - 测试:新增
test/kimi-code-body-limit-per-model.test.ts(16 例)——六个条目的maxRequestBodyBytes逐一切值(8 MiB ×4 / 2 MiB ×2)、8 MiB 对两种 1M 正文密度都留有余量、resolver 的三级优先级(env 双向覆盖、模型行、未知 id 回落 2 MiB、小数 floor、空白容忍)、非法值(''、纯空白、abc、0、-1、-2097152、NaN、huge)回落到模型默认值而不关守卫、端到端 3.5 MB 在k3.1-pro上能发出(fetch 调用 1 次)而在k3-256k上被拒(fetch 0 次、文案含2097152、码为CONTEXT_WINDOW_EXCEEDED)、4.5 MB 能发、8.5 MB 被拒且文案含8388608、env 强制把 1M 档按回 2 MiB、env 拼错仍拒、13 MB 视频照旧走 64 MiB 档、以及assertRequestBodyFits(body)无参调用仍按 2 MiB 抛错。另修正两处依赖旧的全局 2 MB的既有用例(没有删任何断言):kimi-code-capacity.test.ts的「2.1 MB 请求本地即拒」与kimi-code-adapter.test.ts的「不做没做过的省略」改跑k3-256k——那两处的体积仍在该档 2 MB 之上,测的行为(本地拒绝、码、文案、fetch 零调用)一条未减。 - 验证:
npm run typecheck三工程 0 错误(退出码 0);npx vitest run test/kimi-code**34 文件 / 512 例全绿;全量npm test282 文件通过 / 2 skipped、4649 passed / 8 skipped / 0 failed;npm run build通过。 - 文档:README 的 Kimi Code 小节把「请求体超 2 MB 本地即拒绝」改写成「按模型分档:1M 档 8 MiB / 256K 档 2 MiB」,写明 8 MiB 是待实测上界与该口径的推导、env 仍为全局覆盖且非法值回落,「上下文窗口」段落与错误处理总表里的 2 MB 措辞同步更新,并注明
DSH_KIMI_CODE_BYTES_PER_TOKEN的折算基准刻意仍是不随模型变动的 2 MiB。 - 已知边界:
DSH_KIMI_CODE_BYTES_PER_TOKEN的窗口折算仍以那个与模型无关的 2 MiB(设了 env 则取 env)为基准,不跟随按模型档位——它是可选的提前压缩开关而不是守卫,本次刻意不动;不想提前压缩时不设该变量即可(默认就是不设)。
- 现象:1M 上下文档位(
Full Changelog: v0.38.0...v0.38.1
v0.38.0
- [kimi-code] 新增 K3.1 Pro 系列(与其它模型同一 OpenAI 兼容 chat completions)、只支持思考的档位、以及缓存写入这个计费项
- 现象:
k3.1-pro/k3.1-pro-highspeed不在模型目录、设置卡与能力表里;「默认思考档位」永远列着low/high/max与「关闭思考」,而 K3.1 Pro 根本关不掉(服务端 400);缓存区块只有命中率、命中 / 新处理 / 输出 tokens 与请求数,写入这个独立计费项一个数字都没有,看不出 5m / 1h 两档各写了多少。 - 模型目录:两条新条目按官方型号表写入(
k3.1-pro默认 256K、可覆盖到 1M;k3.1-pro-highspeed1M),含thinkingType: 'only'、defaultReasoningEffort: 'max'与quotaMultiplier: null(官方未公布相对倍率,卡片显示「未公布」而不是猜成 1×)。协议不新增分支:本线路对全部模型只走 OpenAI 兼容 chat completions(与官方 agent 的type = "kimi"一致),因此 K3.1 Pro 的请求照常发往/v1/chat/completions,视频输入按型号表声明可用。 - thinking-only:
types.ts增thinkingTypeFor/defaultReasoningEffortFor/coerceThinkingEffort,reasoningEffortsFor对only行剔除none;adapter.ts在构建 body 之前加一道闸门(THE THINKING GATE):被禁的none抬到模型默认档并通过recordThinkingCoercion记录(KIMI_CODE_THINKING_COERCED警告),而不是让整轮失败。 - 缓存写入:总量读
usage.prompt_tokens_details.cache_write_tokens(chat)与cache_creation_input_tokens(Anthropic),5m / 1h 分档读Msh-Usage-Cache-Write-Tokens-5m/-1h响应头;KimiCodeCacheStatsDto的cacheWriteTokens/cacheWrite5mTokens/cacheWrite1hTokens由routes.ts填真值(原cacheWriteTokensNote: 'always-zero-on-this-route'占位删除)。chat wire 的新处理量改为total − cached − written(三者互斥),Anthropic 的input_tokens仍不减——它本身不含缓存。 - 改法(客户端):新增
src/client/kimi-code/labels.ts——wireLabel、effortLadder(读模型声明的档位,thinkingType === 'only'时剔除none)、canDisableThinking、effortOptions(取已勾选模型的并集;「关闭思考」是开关不是档位,任一勾选模型允许它就保留),两张卡片都从这里取标签。档位下拉改由effortOptions驱动;存着的档位若所有勾选模型都不接受,仍保留该选项并标注「当前勾选的模型不接受」。只支持思考的模型在档位旁给一句说明(含模型名与 400),能力表新增「只支持思考」标记与脚注。缓存区块新增一行缓存写入 = 总量 · 5 分钟档 · 1 小时档,0 也照实显示(空白会被读成「没有测量」)。 - 改法(三语):
kimi-code/locales.ts(zh + en)与locales-ru/kimi-code.ts同步新增capThinkingOnly/capEfforts/capThinkingOnlyFootnote/effortThinkingOnlyNote/effortNotOffered/quotaUnknown/cacheWrite/cacheWrite5m/cacheWrite1h/cacheWriteHint/unknownMetric,并按官方口径重写cacheTtlHint、cacheTtlDefault、defaultReasoningEffortHint;俄语包键集由Record<KimiCodeLocaleKey, string>在编译期强制,缺一个键tsc就红。 - 测试:新增
test/kimi-code-k31.test.ts(17 例:两条条目的全部字段、thinking-only 语义、双值 wire 与视频按声明保留、卡片契约字段)、test/kimi-code-k31-card-ui.test.tsx(8 例)、test/kimi-code-effort-ladder.test.ts(6 例)与test/kimi-code-cache-write.test.ts(9 例:chat wire 的 usage 三分解、Anthropic 不减、响应头分档、以及 thinking 闸门在 chat / Anthropic 两条 wire 上的行为);test/kimi-code-capability-ui.test.tsx补「未公布倍率」与「只支持思考 + 400 脚注」两例。 - 验证:
npm run typecheck三工程 0 错误;全量npm test281 文件通过 / 2 skipped,4633 passed / 8 skipped / 0 failed(删掉的用例全部只属于已撤除的 Responses 分支)。 - 文档:README 的线路总览把 Kimi 的协议列改为「OpenAI 兼容 Chat Completions(可选 Anthropic Messages)」,模型目录由四款改为六款,补缓存写入那一行、倍率表里的两个「未公布」,并明确「本线路对全部模型使用 OpenAI 兼容 chat completions(与官方 agent 的
type = "kimi"一致)」、视频清单加入k3.1-pro,以及设置步骤里「默认思考档位跟随模型声明」。
- 现象:
Full Changelog: v0.37.0...v0.38.0
v0.37.0
- [progress-updates] 进展更新补上「开局」与「收尾」两条硬要求
- 现象:任务开始和结束都没有进展条目,整场只在被催到的时候冒出两三条;面板看着像「只有出结论才有动静」。
- 根因:整条链路只折叠模型显式调用
progress_update的事件(projection.apply对其它名字的tool/call、tool/ptc-dispatch原样返回),没有任何从工具活动自动推导的路径;而模型侧唯一的正向纠偏是「8 轮催促」,它在tools/post-execute上从 0 数到 8 才响,结构上不可能在 t=0 触发;收尾时模型输出的是最终答复正文而不是工具调用,post-execute不触发,于是也没有收口那一条。实测本会话日志:三条进展分别紧跟三次催促(seq 86→94、160→168、252→260),首条出现在第 8 轮之后,turn/end(seq 347)之前的 87 个事件里一条都没有。 - 改法:
prompt.ts的引导词把「开局(第一批工具调用之前)」与「收尾(最终答复之前)」提成两条硬要求,原来的「任务早期」条目降级为「计划真的变了才补」;tool.ts的工具描述在首句后同步一句书挡要求——描述是模型每次调用都会读到、离调用最近的一段文本。催促阈值不动默认值(仍是 8,NUDGE_THRESHOLD是测试守着的冻结契约),本机在 profile 配置里设成 4。 - 验证:
npm run typecheck三工程 0 错误;全量npm test277 文件 / 4586 passed / 8 skipped / 0 failed(工具描述的子串断言一条没改);npm run build通过,lib/index.js里能搜到新文案。 - 留档:结构层面的锚点(
agent/pre-step做开局、agent/turn-stopping+steer做收尾)这次刻意没做,纯文本仍是软约束,模型可以不照做。
- [progress-updates] 提示词整段转英文,并新增「输出语言」设置(auto / 中文 / English)
- 现象:进展更新的系统提示词是中文的,而工具描述本来就是英文——同一件事两套语言;用户也无法指定进展本身用哪种语言写。
- 改法(提示词):
prompt.ts的PROGRESS_GUIDANCE整段换成英文,与工具描述统一措辞。新增ProgressUpdateLanguage = 'auto' | 'zh' | 'en'与PROGRESS_LANGUAGE_DIRECTIVES:把「标题与正文用哪种语言写」做成一句追加在引导词末尾的指令——auto跟随对话语言(日、俄等其它语言自动跟随),zh/en明确压过对话语言。「指令用英文」与「输出用中文」是两件互不影响的事,这正是把语言单独拆成一个开关的原因。 - 改法(设置):宿主
Config新增progressUpdatesLanguage(z.union([z.const('auto'), z.const('zh'), z.const('en')])+default('auto')+volatile(),与另两个字段同一套:保存即生效、不重挂插件行);resolveProgressUpdatesSettings用白名单校验,配置里写了个不认识的字符串等于没写(退回 auto),而不是把它原样拼进提示词。设置页从三行变四行,第三行是原生<select>(data-dsh-part="settings-language"),三个选项就是 schema 的三个取值;中/英/俄三份字典同时补齐(俄语里语言名用 Китайский,因为russian-locale.test.tsx明令俄语包里不许出现 CJK)。 - 测试:
progress-updates-apply新增「语言指令追加在引导词末尾」(含活引用就地换值,单实例守卫下读一次卸一次)与「未知取值退回 auto」;live-ref-and-route补 schema 默认值;progress-updates-client新增「下拉写的是 progressUpdatesLanguage,选项就是三个取值」,行数断言 3→4,控件禁用态补一条;中文断言同步换成英文。9 个进展更新测试文件 150 例全绿。 - 验证:
npm run typecheck三工程 0 错误;全量npm test277 文件 / 4590 passed / 8 skipped / 0 failed;npm run build通过。
- [opendesign] 额度卡终于有进度条了:吃的是控制台同源的 Design Plan 窗口(5 小时 / 7 天),不是钱包余额
- 现象:OpenDesign 线路的卡与设置页只有金额(「可用余额 $1.50」),没有任何百分比进度条;而 OpenDesign 控制台的计费卡明明画着「Design Plan · 5 小时额度 剩余 90%」「7 天额度 剩余 81%」两条带重置倒计时的进度条。
- 根因(两层):一是读错了面。插件一直只读
/billing/summary与/wallet/balance——前者给套餐档位与「还剩多少 credits」,后者给钱包余额,两者都没有「这一期一共给了多少」,所以「已用/总量」这个分母根本不存在,代码里那句「本卡不发明一个总数去除」是对的。二是真源在另一个端点:从控制台 SPA 的 bundle 反查出GET {amr}/workspaces/{workspaceId}/billing/coding-plan-usage(带x-vela-workspace-id),返回{ tier, eligible, windows: [{ policyId, durationSeconds, remainingCredits, limitCredits, resetsAt }] };控制台自己的过滤逻辑是durationSeconds === 604800(7 天)加 go 档的18000(5 小时),百分比就是remainingCredits / limitCredits。该路径实测存在(未鉴权 401;此前猜的/billing/windows、/billing/coding-plan、/billing/preflight全是 404),工作区 id 来自GET {amr}/workspaces。 - 改法(宿主):新增
fetchWorkspaceId()与fetchCodingPlanUsage()(绝不 throw,非 200 把HTTP <status>: <code|message>作为数据返回,coding_plan_configuration_unavailable原样透传;窗口解析严格——policyId非空、durationSeconds正整数、limitCredits > 0、两个金额都是整数字符串、resetsAt为 null 或可解析 ISO,不合格的窗口丢弃而不是补)。/usage变成三路并行(钱包 / 账单 / 额度窗口),工作区 id 每个请求只解析一次并复用(失败也复用、不重试,两个账号实测只发 1 次/workspaces,避免每个账号各撞一次同一个 401)。DTO 新增OpenDesignCodingPlanDto与OpenDesignUsageAccountDto.codingPlan(失败也带error,因为「空列表」与「这条线路没有窗口」是两句话)。看板缩放器把这批窗口变成line-card的普通quota.windows:usedPercent = (limit − remaining)/limit、windowMinutes = durationSeconds/60、label留空让卡片自己按分钟说「5 小时 / 7 天」,credits继续只放钱包余额。 - 改法(界面):设置页每个窗口一行——名称 + 「已用 x% · 剩余 y%」+
role="progressbar"进度条 + 「{时间} 重置」;读不到或该工作区未配置时只写一句弱化说明,绝不画 0% 的假条。输入框徽标在有窗口时显示最窄窗口的「剩余 90%」并带一条细进度条,tooltip 列出所有窗口;没有窗口则逐字退回原来的$余额(其他线路的徽标 DOM 有专门用例守着,一个节点都没变)。看板卡复用既有窗口行,自动得到「5 小时 / 7 天」两行、两条 meter、百分比与倒计时。 - 一次被推翻的口径(留档):中途曾按「累计消耗占比 = 已消耗 ÷(已消耗 + 剩余)」做过一版,用户指出套餐本身有独立额度后废弃——那条式的分母会随充值漂移,且与「本期用了多少」不是同一件事。全部改动已回退,最终口径只有一套:窗口的
limit − remaining。 - 测试:
test/opendesign-routes.test.ts31 例(workspace 解析的数组/包装/无 id/401、窗口读取的两窗口与坏窗口丢弃、拒绝码作为数据、每请求只解析一次工作区、钱包在额度读取失败时不丢)、test/widget-board-lines.test.ts57 例(两窗口 → 300/10080 分钟、已用 10/19%、resetsAt毫秒、label为空、钱包仍在;读不到时 windows 为空且原因进 error)、test/opendesign-section.test.tsx17 例与test/composer-quota-badge.test.tsx10 例(每窗口一条 bar 与 aria 值、失败/未配置不画条、徽标有窗口显示剩余%、无窗口退回余额且零新节点)。 - 验证(含实机结论):
npm run typecheck三工程 0 错误;全量npm test277 文件通过 / 2 skipped、4578 passed / 8 skipped / 0 failed;npm run build通过。
实机 2026-10-22(真实 Go 账号,三次重启逐步定位):① 首版拿/workspaces列表找不到 id → 改成「先/workspaces/current再列表兜底」,实机确认activeWorkspaceId能解析;② 解析通了以后 coding-plan 端点回HTTP 401: unauthenticated;③ 按序轮试三种凭据形状(Bearer vela_ctrl_…/Bearer sk-amr-…/x-api-key: sk-amr-…)全部 401。
结论:该端点只认控制台浏览器会话,插件持有的 key 一律不放行;官方 CLI 能读到同样窗口是用它自己的vela login会话。因此设置页把这种情况与「工作区未配置」分开,显示「额度窗口仅在 OpenDesign 控制台可见」;看板卡只写一句原因、不画 0% 假条。读取路径保留(上游哪天放开 key 或换到有会话能力的调用方即自动生效),失败时的 error 会带尝试记录,便于日后一眼判断。
- [proxy] 代理不再劫持回环与本机网关,Linux 也能自动发现系统代理;代理卡文案改成“全部供应线路”
- 现象:把某条线路的 baseURL 指向本机网关(例如本地转发在
http://127.0.0.1:9090)时,请求会被塞进系统代理——ProxyAgent只认一个dispatcher,既不看NO_PROXY也不对回环地址做豁免,而本机代理工具(Clash/Mihomo)通常用 fake-ip DNS 拒掉或转回自己,于是出现“直接 curl 通、插件一路 400 / ECONNREFUSED”。同一处还有第二个洞:auto模式在 Linux 上完全不探测,detectSystemProxy只有 win32(注册表)与 darwin(scutil)两个分支,Linux 用户桌面配了系统代理也永远读到null,只能靠环境变量。 - 改法(旁路):
createFetch解析出活动代理后先过一遍旁路判定——isLoopbackHostname(整个127.0.0.0/8、::1、localhost、*.localhost)无条件直连;再按标准语义匹配NO_PROXY/no_proxy(*、精确主机、域名后缀、host:port,不支持 CIDR)。列表与代理同源读取:进程环境变量优先、其次$DSH_HOME/.env,ProxyManager.getNoProxyList()与系统代理共用 5 秒 TTL,读取抛错一律当作“没有列表”——不会反向把代理从所有线路上摘掉。旁路命中走baseFetch,与“没有代理”是同一条路径,流式、超时与诊断包裹都不受影响。 - 改法(Linux):
detectSystemProxy新增platform === 'linux'分支,parseLinuxSystemProxy读 GNOMEgsettings的org.gnome.system.proxy mode|https|http|socks,优先 https → http → socks,输出http://或socks5://;PAC(auto)与关闭(none)返回null并继续回落环境变量,对既有平台零变化。第三个参数是读取器测试缝,缺省才是真正的gsettings。 - 改法(文案):
proxyModeHint与proxyDirectHint三份语言包(中/英/俄)改成“本插件的全部供应线路”,并写明登录、令牌续期、模型目录与配额读取同样经过这条代理;README 的代理段落同步补上按平台的探测方式、旁路规则与NO_PROXY语义。旧文案只说“GPT 与 Antigravity(Gemini)共用”,在 12 条线路的时代早已不成立。 - 测试:
test/proxy-manager.test.ts新增 22 例(42 例全绿)——回环无论列表怎么写都直连;*/后缀/精确主机/host:port/带 scheme 与路径的条目;无法解析的目标不旁路;ProxyManager在代理生效时把回环与NO_PROXY主机交给baseFetch(含Request入参);未列名主机仍走代理;TTL 缓存与抛错读取器;GNOME 解析(https 优先、socks 回落、auto/none/缺端口/IPv6 括号);detectSystemProxy的 linux 分支与环境变量回落。 - 验证:
npm run typecheck三工程 0 错误;npx vitest run test/proxy-manager.test.ts42/42;全量npm test277 文件通过 / 2 skipped,4557 passed / 8 skipped / 0 failed;npm run build通过。
- 现象:把某条线路的 baseURL 指向本机网关(例如本地转发在
- [board] 组件看板上一张卡看一条线路:
line-flip,一条线路一张会翻面的卡- 形态:看板插件在宿主上发布 cordis 服务
widgetBoard(apiVersion 3)。本插件在apply顶部无条件接线,经ctx.inject(['widgetBoard'])注册一个组件目录(widgets/,随包发布)、一个数据源(line-card)与三个能力(line-refresh/line-account-switch/line-account-strategy)。看板缺席时整段是 no-op;注册失败只 warn,绝不影响插件装载;卸载时一次handle.dispose()全撤。 - 一张卡一条线路:
widgets/line-flip在props.line里写线路 id(codex、claude、kimi-code、minimax-code、antigravity、command-code、glm-coding-plan、step-code、qoder、cline-pass、opendesign),line-card就照着它读那一条线路:正面是额度窗口的已用百分比、进度条与重置倒计时,背面是这条线路的号池、当前账号与三档轮换 chip。刷新、切号、改策略都走能力(widget.action拿不到返回值),并用能力返回值重画。同一份组件摆多张就是多条线路,加一条线路只需在src/index.ts的lines.pools里加一行。 - 单位:各线路在 wire 上的时间表示并不统一(codex 是 Unix 秒,antigravity / cline-pass 是 ISO 字符串,其余是毫秒),换算在
src/host/widget-board/line-quota.ts的每线 normalizer 里各做一次,卡片数据一律毫秒,且不按数量级猜单位。 - 命名:看板 manifest 的
source/capabilities只接受[a-z][a-z0-9.-]{1,39}(?::…)?,带 scope 的包名进不去,所以注册用的 provider 取dsh-chatgpt-subscription,与 manifest 逐字相同才能解析;test/widget-board.test.ts现在照着真实注册结果,逐条比对widgets/*/widget.json的source与capabilities。 - 数据源与能力都不声明 permission:它们只读本机线路自己的
GET /status、只写本插件持有的号池文档,不跑命令、不访问组件指定的主机,没有需要人批准的东西。 - 测试:
test/widget-board.test.ts(11 例)只测仍然存在的边界——组件目录注册(provider 名、apiVersion 透传、id 语法)、看板缺席是 no-op、注册失败只 warn、卸载时 dispose(含迟到的注册被退回)、无 capabilities 的看板只注册数据源、没有线路注册表时不注册任何东西,以及 manifest 与注册 id 的逐字对齐。 - 验证:
npm run typecheck三个工程 0 错误;npx vitest run test/widget-board.test.ts test/widget-board-lines.test.ts42 例全绿(11 + 31);全量npx vitest run277 文件通过 / 2 skipped,4519 passed / 8 skipped / 0 failed。
- 形态:看板插件在宿主上发布 cordis 服务
Full Changelog: v0.36.2...v0.37.0
v0.36.2
- [claude] 设置卡不再拿一枚已死的 access token 请求上游:status / 额度 / 测试连接 / 目录刷新统一走号池续期,托管账号闲置一夜后不再诱导用户重新登录(#66)
- 现象:Claude 订阅的 access token 只有约 8 小时寿命。卡片上的「令牌到期」、额度读取与「测试连接」在托管(managed)账号闲置一夜后全部出错——到期时间显示为过去时间、额度区报错、连接测试失败,看起来像「登录每天失效」,用户于是每天重新登录一次;而重登只是把到期时间刷成新的 8 小时,第二天照旧。
- 根因:续期只发生在模型请求路径(
adapter→pool.getEffectiveAccount()→freshAccount(),由claudeNeedsRefresh()在到期前 5 分钟内触发)。/claude/api的status、quota、connection/test、catalog/refresh则直接用activeAccount()从号池读出的存储值请求上游,从不续期:卡片永远只看到「最后一枚被存下来的 access token」,8 小时后必然过期。相邻的 Kimi Code 线路早有effectiveCredentials()解决此事,Claude 线路缺这一段。 - 改法:
registerClaudeRoutes()新增与 Kimi 同形的effectiveCredentials(target)—— 无 pool 或无target.id时返回存储凭据,否则经pool.getFreshCredential(target.id, fetchFn)续期,续期失败时回落存储凭据(卡片仍能渲染账号列表,真实原因由那次请求报告)。四条卡片路径统一改用它;status先续期、再读额度、再构建响应,所以accounts[].expiresAt(读自号池行,续期会写回)就是续期后的新值。readStatus()增加可选第二参,让额度读取、catalog 加载与accounts[]描述同一枚已消费的凭据。 - 不变量:adopted(Claude Code 快照)仍然永不刷新——号池对 adopted 恒不续期、并在发网络请求前就拒绝,正好落进上面的回落分支,卡片只展示快照自身的
authStatus;只有续期被上游拒绝(refreshFailureStatus仅对ClaudeUnauthorizedError写入)时,账号行才显示「需重新登录」,临时网络失败不会把账号标死。单飞由号池内核freshAccount()负责,同一枚 refresh token 不会被花两次,路由层没有另做互斥。 - 测试:
test/claude-routes.test.ts新增describe('credential renewal on the card paths')(6 例,注入假 refresh seam 与记录Authorization的 fetch double,全程无网络)——GET /status恰好续期 1 次且池内expiresAt变未来、不写authStatus、usage 请求带新 bearer、accounts[].expiresAt与池内一致、quotaError: null;POST /quota同理;connection/test与catalog/refresh分别以新 bearer 请求/v1/messages与/v1/models;过期 adopted 快照刷新 0 次且仍渲染authStatus='expired';卡片与模型请求并发、两次卡片轮询并发都只续期一次。反向验证:把routes.ts退回基线后 6 条中 5 条失败(adopted 那条按设计通过)。 - 验证:
npm run typecheck三个工程 0 错误;全量npm test275 文件通过 / 1 跳过,4477 passed / 7 skipped / 0 failed;npm run build通过。 - 已知边界:卡片路径的续期与模型请求共用同一续期入口,因此一次续期失败(例如离线)会让下一次状态轮询再次尝试续期;这与模型请求路径的既有行为一致,未在路由层另加冷却。
What's Changed
Full Changelog: v0.36.1...v0.36.2
v0.36.1
- [cline-pass] 额度不再只在手动刷新时动:状态读取按 TTL 在答背后刷新,设置卡与输入框徽标 60 秒自动更新
- 现象:Cline Pass 设置页打开后额度数字就定住了——同一次会话里消耗掉的部分,要重新打开页面或点一次「刷新额度」才会反映;输入框旁的额度徽标同样停在首次读到的数字。
- 根因:两层各缺一半。宿主侧
GET /cline-pass/api/status只答store.usage()的内存快照,只有显式POST /usage才真正向上游读(模块注释还写着「读取不触发上游调用」);客户端侧设置页只在挂载时读一次 status,而共享徽标虽然自带 60 秒轮询,cline 这一行的readStatus却把refresh参数丢掉了,点击于是退化成又一次廉价 GET。 - 改法(宿主):新增
CLINE_PASS_QUOTA_TTL_MS(5 分钟)与usageStale()(fetchedAt <= 0同样算过期);GET /status先答缓存,快照缺失或过期时用QuotaRefresh在答背后起一轮刷新,并把quotaRefreshing放进状态 DTO,让客户端知道该立刻补读;扇出逻辑抽成refreshUsage(),显式POST /usage与后台刷新共用同一份实现,同源校验、400 文案、凭据零外泄全部保持不变。单飞由start()的pending保证:N 次轮询只扇出一轮。另给路由选项加了quotaTtlMs测试缝。 - 改法(客户端):
ClinePassApi.status(refresh)分流——false走GET /status,true走POST /usage并只取其中的 status 半边;设置页补上 60 秒可见轮询 +visibilitychange+createQuotaFollowUp(读到quotaRefreshing: true就 1.5 秒后补读,最多 3 次);轮询是静默的——失败既不刷红字也不清掉用户正在看的红字,半输入的草稿由既有两道闸保护;徽标把refresh转发出去,点击才真正强制刷新。 - 测试:新增
test/cline-pass-quota-poll.test.ts(9 例)与test/cline-pass-quota-poll.test.tsx(10 例)——冷缓存即排刷新并回quotaRefreshing、新鲜快照 0 次上游调用、假 Date 拨过 TTL 后缓存照答而背后替换、2 账号 3 次轮询只扇出 2 次、显式动作立即刷新且 400/403 原样、徽标点击走 POST /usage、隐藏标签页不读、轮询失败静默、卸载清理定时器与监听。 - 验证:
npm run typecheck三工程 0 错误;npm test274 文件 / 4470 例通过(1 条minimax-code-video-history的 Windowsrename EPERM抖动,单跑 22/22 通过,与本改动无关);npm run build通过。 - 已知边界:
QuotaRefresh.run()不与正在跑的start()合流,所以「手动刷新正好撞上后台刷新」这一步仍会多扇出一轮;两轮写入形状相同、后写的setUsage生效,代价是一次多余上游请求。真正合流要给共享 helper 加wait,会牵动另外六条线路,本次不做,代码里按此写明。
Full Changelog: v0.36.0...v0.36.1
v0.36.0
- [client] 修复俄语语言包与
@linxin666/dsh-i18n等已安装语言包冲突导致插件客户端激活失败崩溃的问题- 现象:Web 界面启动失败,提示
web boot: 1 entry did not activate,@eddyskywalker/dsh-chatgpt-subscription: failed。 - 根因:环境中其它插件若先于本插件注册了
'ru'语言,ctx.locale.addLanguage会直接抛出Error: locale "ru" is already registered;未被捕获的错误导致 Cordis 插件生命周期被标记为 FAILED,从而阻断了整个客户端加载。 - 改法:在
installRussianLocale中使用try-catch保护addLanguage与register调用,发生冲突时平滑捕获并返回 no-op disposer,继续注册本插件的各个 provider 命名空间字典;并在installLineLocale增加对locale对象的空值安全防御。 - 验证:新增冲突用例(
addLanguage抛错且某个命名空间register抛错时,安装不抛错、其余命名空间仍注册);三工程tsc0 错误;全量 vitest 4452 passed / 7 skipped / 0 failed;npm run build通过。
- 现象:Web 界面启动失败,提示
- [opendesign] 套餐档位与额度改读
/billing/summary——订阅后不再显示free,额度打开设置页即自动读取- 现象:已订阅 Go 的账号,账号行仍显示
free;「钱包余额」区打开时永远停在「尚未读取余额」,必须手动点刷新才出现一个美元数字,看不到套餐、周期与 credits。 - 根因:档位抄的是设备授权应答里的
user.plan,登录后永不更新,而上游对已订阅账号也返回"free"(本机实测);额度只读GET /wallet/balance,且只在显式刷新时读、只存在进程内存。真正的档位与额度在控制 Key 可读的GET /billing/summary里:membershipTier(go/plus/pro/max)、订阅状态、本周期、订阅/充值/合计 credits 及美元、今日与累计消耗。 - 改法:
/usage并行读钱包与账单摘要,钱包被拒时用账单总额兜底,两个面各自失败不互相掩盖;账单档位覆盖登录快照,读一次就纠正账号行。设置页打开时自动读一次(仅当号池里有浏览器授权账号;粘贴的 API Key 没有控制 Key,不发请求)。「套餐与额度」区块移到账号卡下方,展示档位、状态、本周期、三类 credits 与美元、今日/累计消耗、计费请求数。仍然不画百分比条:摘要只说剩余,不说配额上限,卡片不臆造分母。 - 验证:三工程
tsc0 错误;全量 vitest 4451 passed / 7 skipped / 0 failed;npm run build通过;用真实控制 Key 请求/billing/summary返回go/active/ 15000 credits($1.50),经/usage后 status 的plan由free变为go。
- 现象:已订阅 Go 的账号,账号行仍显示
- [client] 新增俄语语言包(
ru):插件自己的设置界面可以选择俄语,未翻译的键回落到英文- 背景:
@deepseek-ai/dsh-client-locale的字典注册有两种形态——整本双语register(ns, { zh, en })(内置两种语言都必须给全),以及给语言包用的单语言register(ns, locale, dict);语言本身由addLanguage({ id, label, fallback })加进目录。俄语走第二条路,不是给每个命名空间加第三列。 - 改法:新增
src/client/locales-ru/——13 份字典(每条线路一份,共 1440 个键,键集与zh/en逐一对应)+account-pool.ts(12 条线路共用的账号卡片文案)+index.ts的installRussianLocale(ctx):先addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' }),再按命名空间逐个register(ns, 'ru', dict),每个注册各持一个 disposer。客户端入口apply调用一次。 - 回落语义:声明的
fallback是en,所以本包漏掉的键读作英文而不是键名;宿主自身的界面只有zh/en,因此「插件俄语 + 宿主英文」是预期形态,DSH 本体要俄语是另一个语言包的事。 - 旧宿主兼容:
addLanguage是语言包那半边契约才有的接口。没有它的宿主/测试替身直接跳过整个包(此时语言根本不可选,字典也就无从取用),而不是让客户端插件在缺一个 seam 的服务上崩掉。 - 测试:新增
test/russian-locale.test.tsx(41 例)——13 个命名空间 × 三条断言(键集与zh/en完全一致、每个{placeholder}逐字保留、值里没有中日韩字符且不是整份照抄英文),另外钉住installRussianLocale注册一次语言 + 每条线路一次字典(disposer 数量 = 13 + 1),以及同一张 Claude 卡在ru下渲染俄语且不回落到zh。 - 验证:
npm run typecheck0 错误;npm run build通过;全量npx vitest run4436 passed / 4 failed——同样是与本改动无关的 4 条Intl用例(俄语默认 locale 下1,200→1 200、日期变俄文),在改动前就红。
- 背景:
- [client] 除 ChatGPT 外,所有供应商详情页在英文界面下仍是中文——卡片直接渲染自己的
zh字典,从不问本地化服务- 现象:设置 › 订阅服务里点开 Claude(以及 Antigravity / Command Code / Kimi Code / MiniMax Code / GLM / Ollama / Step Code / Cline Pass / Qoder / OpenDesign),整页文案都是中文,即使界面语言是 English;只有 ChatGPT 那张卡是英文。每个命名空间的
dictionaries里en早就齐全,且键集与zh完全一致,缺的从来不是翻译。 - 根因:12 处
const t = zh(claude/ClaudeSection.tsx、claude/useClaudeSection.ts、antigravity、command-code、glm-coding-plan、kimi-code两处、ollama、cline-pass、step-code、qoder、opendesign)把字典对象在渲染时直接捕获;ctx.locale一次都没被读过。ChatGPT 卡片能用英文,只因为它渲染的是 hub 注入的t座位。 - 改法:新增
src/client/common/line-locale.ts——installLineLocale(ctx.locale)在客户端入口调用一次;useLineDict(ns, zh)返回该命名空间的字典形状视图:属性访问不变(t.signIn)、键集与回退字典一致,但每个值在渲染时经ctx.locale.bind(ns)读取,并订阅 locale revision,所以切换语言会重渲染当前卡片而无需重挂载。没有 locale 服务时(单测裸挂卡片、只提供register/bind的旧宿主)退回回退字典,即改动前行为。 - 顺带修掉的冻结:模块级捕获
zh的四处表(Kimi 的思考档位、cline-pass 与 opendesign 的服务端错误前缀、GLM 的区域名、qoderModelRowHint)改为使用渲染期解析出的字典,否则这几行会一直说中文。 - 验证:新增
test/line-locale.test.tsx(4 例)——同一张 Claude 卡在en下渲染英文、在zh下渲染中文、切换语言不重挂载即可换文案,并钉住 zh/en 键集一致这条前提;npm run typecheck0 错误;npm run build通过;全量npx vitest run4395 passed / 4 failed,与改动前(git stash对照)完全相同的 4 条——它们断言英文Intl格式(1,200、12:03),在俄语默认 locale 的机器上必然失败,与本改动无关。
- 现象:设置 › 订阅服务里点开 Claude(以及 Antigravity / Command Code / Kimi Code / MiniMax Code / GLM / Ollama / Step Code / Cline Pass / Qoder / OpenDesign),整页文案都是中文,即使界面语言是 English;只有 ChatGPT 那张卡是英文。每个命名空间的
What's Changed
- fix(client): resolve provider card copy through the active locale, not the shipped zh dictionary by @unera in #64
- feat(client): Russian language pack (addLanguage + per-namespace ru dictionaries) by @unera in #65
New Contributors
Full Changelog: v0.35.0...v0.36.0
v0.35.0
- 【BREAKING】移除 WorkBuddy(CodeBuddy 订阅)线路及其全部残留
- 该线路的上游已经失效,因此整条移除,而不是藏在开关后面:
src/host/workbuddy/(15 文件)、src/client/workbuddy/(5 文件)、src/shared/workbuddy-contracts.ts、21 个专属测试与夹具、7 个一次性实况探针、其专属文档,以及它在共享词汇表里的那几项(CONTROLLED_PROVIDERS、能力表、请求诊断的 provider/route/wire 三处、hub 卡片与品牌图标、composer 配额槽位)一并删除;src/index.ts里 85 处注册全部摘除。 - 用户可见的影响:provider id
workbuddy与路由 idworkbuddy-subscription不再存在——设置页那张卡片、模型选择器里它的模型、/workbuddy/api/*路由都随之消失。其余 12 条线路的行为没有任何变化;~/.dsh/storages下 7 个workbuddy-*.json(.dpapi)已成惰性文件,可以自行删除,引用了workbuddy-subscription的外部配置(例如 LLM Verifier 的 provider/model)需要改指向其它线路。 - 顺带移除的死管道:账号摘要 DTO 的
modelCooldowns及其客户端渲染、5 条只服务它的 CSS 与modelCooldown标签键——它唯一的生产者就是被删的号池,删完只剩读端,那段 UI 永远不会显示。 - 没有跟着一起死的覆盖:11 处「借这条线路当载体测共享实现」的用例被移植到幸存线路(缓存记账与上下文超限 → step-code,输出预留与图片预算与偏好 seam 与请求诊断 → glm-coding-plan,旧快照缺字段兼容 → kimi-code),只有真正随线路消失的机制(双区目录、
code 6004按账号×模型停用、签到、atrest 密钥、CodeBuddy 账号采纳)才整条删除。 - 验证:三工程
tsc0 错误;全量测试 4352 用例通过、0 失败;npm run build通过且lib/index.js与lib/client.js内 0 处命中;重启宿主后GET /workbuddy/api/status返回 404 而/claude/api/status与/glm-coding-plan/api/status仍是 200,hub overview 只剩 12 条线路。
- 该线路的上游已经失效,因此整条移除,而不是藏在开关后面:
- 【hub】订阅服务标题行右侧新增一枚「检测更新 / 更新插件」图标按钮——安静、四种结局各自说得清,本地链接安装与没有 pluginManager 的宿主降级为「只给命令」
- 现象:这个插件的用户在设置页里看不到自己装的是哪个版本,也不知道有没有新版;升级只能靠自己去 npm 搜一遍再敲命令。手动更新又有一个坑:从源码 checkout 用
link:/file:装上的人,一旦让工具「一键更新」,pnpm 会把链接覆盖成一份 registry 副本,作者的编辑目录当场变成一份旧包。 - 改法(位置与形态:标题行右侧的一枚图标按钮):控件挂在 ProviderHubSection.tsx 的 overview 分支里——
<header class="dsh-hub-head">用space-between把<h2>放左、UpdateCheck.tsx 放右,HubOverview只剩它原本那行<p class="dsh-hub-hint">。按钮本体是一枚 28×28 的环形刷新图标(16×16 内联 SVG,stroke="currentColor"、1.5 描边):透明底、无描边、圆角 8px,常态只是三级色图标,hover 才出现--dsw-alias-interactive-bg-hover并转主文字色,:focus-visible是 2px 描边焦点环;检测/安装中图标按@keyframes dsh-hub-update-spin旋转并置 disabled;「有新版本且可一键装」时在图标右上角点一个 6px 的--dsw-alias-button-info-fill圆点,而不是把按钮撑长。状态文字 11px、右对齐,排在图标之前(DOM 顺序),命令(本地链接等只能用命令的情形)落在 header 行下方单独一行。样式全部追加进既有的 hub-styles.ts 那一个 CSS 字符串——test/client-style-ownership.test.ts枚举的是安装器而不是表,新增一张表会牵动它;颜色一律var(--dsw-alias-*, 兜底),深浅色不分支。 - 改法(只在点击时取数):组件挂载时不发任何请求。设置页打开照旧只花那次 overview 请求,registry 只在对应用户动作时被问一次;
test/client-style-ownership与client-registration里的假 fetch 因此不需要多一条分支。 - 改法(四种结局由 DTO 唯一决定):update-contracts.ts 的
UpdateCheckDto用{ current, latest, updateAvailable, installKind, canApply, reason?, command, checkedAt }把四种结局压成可判定的事实:latest有值且updateAvailable:false= 已最新;updateAvailable:true且canApply:true= 一键可装(按钮变「更新到 vX」);updateAvailable:true且canApply:false+reason= 只能手动(附可选中命令);latest:null+reason:'registry-unreachable'= 检测失败。current:null(读不到自己的 manifest)单独成一种显示,绝不写「已最新」——那是对一个没读到的版本下结论。 - 改法(当前版本:有界向上查找):update-check.ts 的
readOwnManifest()从fileURLToPath(import.meta.url)起点逐级向上,最多 6 层,只接受name等于本包名的package.json。不用固定的../package.json:bundle 后这个模块在<root>/lib/index.js、源码运行时在<root>/src/host/update-check.ts,同一个相对路径指向两个不同文件。 - 改法(安装形态:先问插件管理器,再退回路径):
pluginManager.listBundles()的source是 pnpm 真正会接受的那个 spec(dependencySpec()会把link:/file:转成绝对路径但保留协议前缀),所以它是权威答案;取不到时才退回路径启发式——rootrealpath后按整段切分看有没有node_modules段(子串匹配会让node_modules-backup蒙混过关)。与任务书的一处偏差:任务书写的是「用listPlugins()按 spec 前缀判定」,但宿主PluginInventoryEntry只有entryId/moduleName/enabled/fiberPhase/meta,没有 spec 字段;带 spec 的是listBundles()的BundleInfo.source,故按后者实现。 - 改法(最新版本与超时):向
https://registry.npmjs.org/@eddyskywalker/dsh-chatgpt-subscription/latest取version,必须走注入的 proxyFetch(本部署开 127.0.0.1:7890,裸fetch连不上),超时 8s(AbortSignal.timeout);拒绝连接、代理不答、超时、HTML 错误页、缺version一律映射成reason:'registry-unreachable'的 200,不是 500——一次离线不该把整个插件报成坏的。比较用自写的compareVersions()(不引依赖):主/次/修订三元组,prerelease 小于同号正式版,数字标识符小于字母标识符,解析不了返回null并因此不报有新版本。 - 改法(一键更新:先同源、再两道守卫、spec 服务端写死):
POST /update/apply先isSameOriginMutation(不过就 403csrf-rejected),然后按与检测同一顺序守卫:本地链接 → 409development-install;无pluginManager→ 409plugin-manager-missing;都过才installBundle('@eddyskywalker/dsh-chatgpt-subscription@latest', { enabled: true, requestId })。这个 spec 是模块常量UPDATE_INSTALL_SPEC,请求体一个字都不读,所以任何同源 POST 都无法选择要装进 profile 的包或版本。安装失败(application: 'failed' | 'cancelled')把 pnpm 自己的 diagnostic 原样带回成 502install-failed,不吞。成功返回{ installed, restartRequired, removedFirst }:restartRequired由安装结果自己的application推导(applied→ false,其余 → true),不再写死——HMR profile 下宿主会当场重载被替换的 bundle,那时没有任何东西需要重启;removedFirst说清这次是否走了下面那条「先移除再安装」的路。 - 改法(HMR 宿主上的一键更新:先 remove 再 install,失败回滚):宿主的
installBundle开头有一道硬拒——if (this.ownerContext.get('hmr') !== undefined && known !== undefined && Object.hasOwn(before, known)) throw new ManagementFailure('already-installed'),也就是 HMR profile 里不允许覆盖安装。于是「一键更新」在开着 HMR 的宿主上原本只有一个结局:502 + 让用户自己去设置页。DSH 官方插件管理界面处理「更新」走的正是先removeBundle(packageName)再安装(packages/client/ui-plugin-manager/src/client/manager-store.ts的update:动作),本插件的 apply 按同一条路实现并加失败回滚,状态机四步:① 先直接installBundle(UPDATE_INSTALL_SPEC)——非 HMR 宿主在这一步就成功,不发生任何移除,工作副本连一瞬都不会消失;② 只有它抛错或返回error.code === 'already-installed'时才进入官方路径removeBundle('@eddyskywalker/dsh-chatgpt-subscription')→ 再次安装;③removeBundle抛错、返回stop-profile(宿主要求先停 profile)或changed:false(报了移除但 profile 文件没变)时不再安装——此时盘上还留着可用的旧版,再装一次只会被同样拒绝——返回可辨的失败(stop-profile单独成码,文案让人先停宿主、再dsh plugin add …@latest,或去「设置 › 插件」);④ 移除后的安装失败时立刻回滚installBundle('@eddyskywalker/dsh-chatgpt-subscription@<当前版本>'),版本来自服务端自己readOwnManifest()读到的 manifest(读不到就跳过回滚而不是猜一个),回滚结果restored/failed/skipped随失败一起回到error.rollback,restored还带restoredVersion;回滚失败时 message 写明插件可能已不可用并给出钉在版本上的手动恢复命令。两条安装 spec 与回滚 spec 全部由服务端写死或由服务端读到的版本拼出,请求体仍然一个字都不读;POST 仍然先过isSameOriginMutation。 - 改法(四种结局的文案各有各的说法):
UpdateCheck.tsx的成功文案按两个事实分支——是否还需要重启、是否走了移除路径(已安装 v0.35.0,当前已生效,无需重启/已移除旧版并安装 v0.35.0,重启宿主后生效等四种,zh/en 两本字典各加 4 个键);失败文案把服务端给的原因原样接在后面(另有 3 个新键:已恢复 / 回滚失败 / 回滚跳过),并在前面说清回滚结果:更新失败,已恢复 v0.34.0,或更新失败,且旧版本也没能重新装回来——插件可能已不可用(此时同一行里带着dsh plugin add …@0.34.0这条手动恢复命令,回滚被跳过时另有一套「无法自动恢复」的措辞)。仍然是同一行 11px 红字,仍然不用弹窗。 - 一处有意的省略:
waitForInstall(requestId)没有在installBundle之后再调一次。它的用途是「远端响应丢了再取一次结果」,而这条路由就在宿主进程内调用,installBundle本身 resolve 的就是结算后的结果;宿主的installsmap 在调用 settle 前就把条目删了,那一次调用必然返回null,只会在测试里多需要一个桩。 - 验证:
npx tsc -b --pretty falseexit 0;npx tsc -p test/tsconfig.json --pretty falseexit 0;npx vitest run test/update-check.test.ts test/hub-update-ui.test.tsx test/hub-overview-ui.test.tsx test/hub-overview-routing.test.ts test/client-registration.test.ts test/client-style-ownership.test.ts→ 6 文件 / 75 条全过(update-check28 条,其中新增 12 条状态机用例;hub-update-ui22 条,含图标形态的 aria-label/旋转类/强调圆点/状态与命令位置);全量npx vitest run→ 271 文件通过 / 1 文件整文件跳过(token-store-linux,平台跳过)/ 4402 passed / 7 skipped / 0 失败(4379 → 4395 → 4402:先是机制八条加两套新增,图标形态又补了 7 条 UI 用例);npm run build通过。新增的宿主用例全部用注入的假 fetch 与假 pluginManager,不打真网络:无 pluginManager 时 check 仍报两版、apply 409;link:时 check 带canApply:false且 apply 409development-install、安装零次;registry 抛错 →registry-unreachable而非 500;正常配对(0.34.0 vs 0.99.0)updateAvailable:true且确实装的是常量 spec;跨域 POST 403 且安装零次。另有一条把真实的registerRoutes前缀表与这两条 exact 路由一起挂上去,钉住「exact 先于 prefix」这条依赖——前缀处理器对非 POST 一律 405,若匹配顺序反了,这两条路由在生产里就是死的而所有单测仍会绿。UI 用例覆盖四种结局的文案、图标的aria-label随状态变化、忙碌时 disabled 且带旋转类、可一键装时出现强调圆点、状态文字与命令各自的位置、错误原地红字,以及「挂载时零请求」。布局另有三条结构断言:图标永远是.dsh-hub-update-row的最后一个子元素、命令永远在.dsh-hub-update里位于该行之下、.dsh-hub-head的最后一个子元素是控件。本轮新增的宿主用例把四条状态机结局逐条钉住:直接安装成功(removedFirst:false、removeBundle零调用);首次already-installed→ 移除 → 再装(removedFirst:true,且restartRequired随application在applied/restart-required之间变);移除后的安装失败 → 回滚 spec 逐字是@eddyskywalker/dsh-chatgpt-subscription@0.34.0,带回rollback:'restored'、restoredVersion与 pnpm 原文;回滚也失败 →rollback:'failed'、两条 diagnostic 都在、message 里含dsh plugin add …@0.34.0;stop-profile(返回与抛出各一例)→ 409 且第二次安装零调用;旧宿主没有removeBundle→ 行为与本轮之前逐字一致;另有一条把开发链接放到「有 removeBundle 的 manager」上,证明安装与移除都是零次。UI 侧新增 4 条:live 成功(无需重启)、移除路径成功(写明移除过)、回滚成功(红字更新失败,已恢复 v0.34.0+ 原因)、回滚失败(红字写明可能已不可用 + 手动恢复命令全文)。
- 现象:这个插件的用户在设置页里看不到自己装的是哪个版本,也不知道有没有新版;升级只能靠自己去 npm 搜一遍再敲命令。手动更新又有一个坑:从源码 checkout 用
Full Changelog: v0.34.0...v0.35.0
v0.34.0
-
[Kimi Code] 长会话撞 2 MB 请求体上限整轮失败——现在撞墙交给压缩恢复,窗口折算降为可选
- 现象:中文长会话在 K3 线上整轮失败,报
Kimi Code rejected the request before sending: the serialized body is 2107355 bytes, above the 2097152-byte limit this route enforces. Composition: images 0 bytes, tool schemas 2531 bytes, conversation text and system prompt 2104824 bytes。图片 0 字节,也就是说这不是读图问题,上一版的「丢最旧的图」回收路径在这里丢无可丢,只能报错并建议压缩——而用户真正要做的是让这一轮活下来。 - 根因一(错误码不对,宿主接不上自愈):mapper.ts 的
assertRequestBodyFits抛的是PROVIDER_ERROR,而 DSH 的溢出恢复只对CONTEXT_WINDOW_EXCEEDED生效(compaction-basic的agent/request-error监听器按该码强制压缩本轮历史、再重试同一步,重试上限默认 1 次)。于是唯一还能靠压缩救回来的场景——正文太大、图已丢完——恰好是唯一不会被救的那个。本线路服务端真返回 400 溢出的路径(classifyKimiFailure)早就用对了这个码,只有本地守卫没接上。 - 根因二(两个上限量不同的东西):DSH 用 adapter 申报的
context.contextWindow算压缩阈值(resolveCompactSpec:thresholdRatio默认 0.8、headroomTokens默认 65536,本线路不申报defaultMaxTokens故无输出预留,即min(floor(0.8W), W − 65536)),这是 token 口径;网关的 2 MB 是 字节 口径。而 DSH 的 token 计量是固定密度(@deepseek-ai/dsh-token-meter的CHARS_PER_TOKEN = 4,按text.length / 4计价,看不见中日韩字符占 3 字节)。所以「一个计量 token 占多少字节」= 4 × 每字符字节数:纯 ASCII 约 4、中英混合约 8、密集中文约 12。kimi-for-coding申报 1M、阈值 85 万计量 token,而 2 MB 中文正文只有约 17.5 万计量 token——压缩永远不会先触发,字节墙先到。 - 根因三(网关原文没被认出来):
total message size 5943865 exceeds limit 2097152匹配不上common/context-overflow.ts里任何一条超限措辞,所以请求即便漏过本地守卫、由服务端拒绝,同样落进PROVIDER_ERROR。 - 改法(兜底,本次的主修复):
assertRequestBodyFits的错误码从PROVIDER_ERROR改为CONTEXT_OVERFLOW_CODE,文案与BodyFitContext一字未动;classifyKimiFailure增加isKimiBodySizeRefusal(认total message size … exceed(s) … limit)归入同一码,message 明说是字节上限而不是 token 窗口。判定函数刻意写在 Kimi 自己的分类器里——共享的isContextOverflow被其它八条线路共用,把一句话加进去会连带改写别人的失败语义。 - 改法(窗口折算,默认关):types.ts 新增
wireBytesPerToken(env)、byteDerivedTokenCap(env)与effectiveContextWindow(tokenWindow, env);adapter.ts 的contextWindowFor仍是「申报值与内部预算的唯一入口」,但默认原样发布目录值或用户覆盖值:2 MB 墙已经由恢复码接管,用户显式覆盖(例如把k3覆盖成 1,048,576)不该被插件悄悄压小——这条是本轮返工的核心,最初把 clamp 设成默认是错的。设DSH_KIMI_CODE_BYTES_PER_TOKEN才启用折算,申报值 =min(声明值, floor(2MB / d)):d=4→ 524288、5→ 419430、8→ 262144、12→ 174762。 - 为什么是这组数:DSH 的 token 计量是固定密度(
@deepseek-ai/dsh-token-meter的CHARS_PER_TOKEN = 4,按text.length / 4计价,看不见中文占 3 字节),所以「字节 / 计量 token」= 4 × 每字符字节数:纯 ASCII 约 4、中英混合约 8、密集中文约 12;2 MB 墙换算成计量 token 分别是 524288 / 262144 / 174763。压力阈值是min(floor(0.8 × 窗口), 窗口 − 65536)(compaction-basic/src/config.ts的resolveCompactSpec,thresholdRatio默认 0.8、headroomTokens默认 65536,本线路不申报defaultMaxTokens故无输出预留)。选哪个d就是选「让哪一类会话在墙前压缩」。 - 旋钮语义(与守卫不同):该变量是可选的提前压缩开关,不是安全守卫——不设、空串、非正、非法一律等于不启用(真正的安全网是恢复码,它永远在);
DSH_KIMI_CODE_MAX_BODY_BYTES仍按旧约定(typo 回落默认值而不是关掉守卫)。 - 默认行为的代价(写成事实):不折算时,
k3@1m 的压力阈值min(838860, 983040)= 838860 计量 token,而字节墙只有 174763(中文)/ 262144(混合)/ 524288(ASCII)——阈值永远够不到,所以长会话走的是「撞墙 → 自动压缩 → 重试同一步」。想让压缩发生在墙前就开折算。设置卡片仍按用户配置值显示窗口(客户端展示层,未改动)。 - 佐证:官方
kimi-cli走的是同一条「预算前置」路线——src/kimi_cli/soul/compaction.py::should_auto_compact只在「达窗口 85%」或「剩余不足 50K token」时压缩,从不特殊处理 2097152;它的 CHANGELOG 还记过一次同类缺陷(工具结果 token 没算进压缩触发 → 压缩不触发 → 直接 400),与本次的「计量口径没算字节」是同一类错误。 - 测试:新增
test/kimi-code-capacity.test.ts(29 例),分三段钉住:window folding is opt-in(默认无 env 时k3的 1M 覆盖与kimi-for-coding的 1M 都原样发布、4/5/8/12 各自的 min 语义、非法值当未设、min不会把 256K 放大);what each density setting protects, when folding is on(把三种密度的撞墙点 524288 / 262144 / 174763、压力阈值floor(min(0.8W, W − 65536))与各档的护住范围写成断言,并明写默认不折算时三种密度都先撞墙、这不是缺陷而是恢复码接管);以及「2.1 MB 正文在发请求前就以CONTEXT_WINDOW_EXCEEDED被拒」的端到端用例(fetch零调用);另有「2.1 MB 正文在发请求前就以CONTEXT_WINDOW_EXCEEDED被拒」的端到端用例(fetch零调用)、k3/k3-256kclamp 前后相等、以及网关原文归类与「无关 400 仍是PROVIDER_ERROR」的用例。test/kimi-code-adapter.test.ts、test/kimi-code-k3.test.ts、test/kimi-code-body-fit.test.ts、test/dynamic-output-reservation.test.ts按新语义更新。 - 验证:
npm run typecheck0 错误;npx vitest run test/kimi-code test/dynamic-output-reservation30 文件 / 475 条全过;全量npm test290 文件通过 / 4751 条通过 / 0 失败(7 skipped 为仓库既有平台跳过:token-store-linux等)。
- 现象:中文长会话在 K3 线上整轮失败,报
-
[qoder] 模型选择里 Qoder 整条线路消失、「全不选」等于「全选」、重启后先出现一大堆用不上的模型——三件事都出在「哪些模型算可用」这一条链上
- 现象一(最严重):重启宿主后,模型快捷选择栏里根本没有 Qoder 这个供应商;要打开一次设置页才会出现。
- 现象二:设置卡片上的「全不选」点下去与「全选」效果完全相同;计数行还字面显示「已选 {selected} / 2」(占位符没被替换)。
- 现象三:进设置页先看到 15 个模型,其中大部分是当前套餐根本没有的,必须点一次「刷新目录」才缩到自己账号真实的 2 个。
- 真因一(白名单词汇表错配):adapter.ts 的
listModels拿用户白名单去收敛随包兜底表,而两者不是同一套 id——白名单是实时目录的display_name去空白(DeepSeek-Flash),兜底表照录 pi-provider 的静态表(DeepSeek-V4-Flash)。冷启动时进程里没有实时目录,收敛结果必然是空数组,listModels返回 0 个模型,DSH 于是把整个 provider 分组过滤掉。实测:白名单['DeepSeek-Flash']+ 兜底表 → 0 个模型。 - 真因二(三态缺失):
enabledModelIds只有「缺省或空数组 = 不过滤」一种读法,而旧版defaultQoderModelSettings()的出厂值恰好就是[]。于是「全不选」提交的[]被读成「不过滤」,同时也意味着每一个老用户的落盘文件里都写着[]——直接改语义会让他们升级后一个模型都不剩。 - 真因三(快照读写不互逆):目录快照的写入侧存上游原始条目(
{ key, display_name, context_config }),读取侧却用只认{ chat: [...] }的parseQoderModelListing去解析它,任何快照都解析成空数组并被当作「没有可用快照」丢掉。那份qoder-global-catalog.json一直都在、内容也是对的,却从来没有被成功读回来过一次——所谓「打开设置页就恢复」,真相是「等了一次真实的上游刷新」。 - 真因四(刷新不发事件):目录刷新成功后没有发
llm/adapters-updated,所以即使真实目录已经到手,模型选择器的快照也不会重读,停在空分组上。 - 改法(三态):
enabledModelIds改成string[] | null——null= 不过滤(出厂)、[]= 用户明确全关、非空 = 白名单。存储、适配器过滤、状态下发、hub 概览、客户端五处同步;ModelChecklist的「全不选」不再在自己已是 0 时禁用(全关是合法终态)。 - 改法(迁移):新增
selectionVersion迁移标记。无标记 +[]→ 按旧版出厂值迁移成null并落盘;有标记 +[]→ 尊重用户真实的全关。幂等,且 register 后端的 schema 缺省刻意写成0(旧update()写的是?? [],那个后端同样会留下无标记的[])。 - 改法(收敛口径):新增
convergeQoderSelection(stored, live, catalog)——收敛只在实时目录上做,兜底表阶段不按白名单筛(它是能力占位,且与白名单不是同一套词汇);[]在两种阶段下都生效。listModels、状态面勾选标记、hub 概览三处读同一个函数。 - 改法(预热与事件):新增
persistedQoderModelRecord/parsePersistedQoderModelEntries这一对互为反函数的序列化器(顺带修掉is_reasoning按reasoningEfforts.length > 0写、会把 toggle 形态推理模型抹成不推理的问题);启动时void warmQoderCatalogOnStartup(...)非阻塞预热,先读磁盘快照回填、再让上游刷新跑在背后;refreshCatalog改为返回「目录有没有真的换掉」,只有变了才ctx.emit('llm/adapters-updated')。 - 改法(自动取目录):
GET /status在应答背后自动触发一次目录刷新(复用QuotaRefresh,用force: false让 30 分钟 TTL 与 in-flight 去重生效);客户端首屏拿到catalogRefreshing就静默补拉一次,最多一次,不设 busy、不弹错。「刷新目录」按钮保留为强制出口。 - 验证:
npm run typecheck0 错误;全量npx vitest run282 文件 / 4626 passed / 7 skipped;npm run build通过。冷启动回归用例验证过先红后绿(把规则改回旧实现时它报expected 0 to be greater than 0);快照读写不对称用真实文件对拍过(旧解析器 0 条 / 新解析器 2 条);三态迁移用真实落盘文件跑过完整往返(迁移 → 幂等 → 全关不被翻回 → 全选回到null)。
-
[qoder] 模型行只有名字:窗口、是否收图、思考档位、输出预算现在逐行写在勾选行下面
- 现象:Qoder 卡的模型清单每一行只有一个模型名,用户勾选时看不出这个模型能收多长的上下文、收不收图、有几档思考——与同一屏里的 OpenDesign 卡形成反差(那边一行写全)。
- 真因:不是 host 没数据。useQoderSection 拿到的
QoderModelOption里contextWindow/supportsImages/reasoningEfforts/defaultMaxTokens都在(能力字段一律来自目录端点,随包兜底表也有逐模型事实),而是 QoderSection.tsx 当时刻意不把 hint 传给 ModelChecklist——那次的需求是「模型行下面不需要介绍文字」,被误读成了「不要任何能力行」。 - 改法:新增
qoderModelRowHint(),由 DTO 字段生成一行数据(1M · text+image · 档位 low/high · 输出 131k)并传给共享 ModelChecklist;散文式介绍仍然不进这一行。目录不给档位的模型写「无档位」而不是留空或套默认三档。共用新增的formatTokenCount()(百万位保留两位、千位取整),二进制窗口不再渲染成1048576或131.1k。 - 顺带修掉一个真 bug:落盘快照的写入侧只写了
is_reasoning,没写thinking_config,而回读侧的档位只从thinking_config.enabled.efforts取——于是重启后所有推理模型都变成「无档位」(模型是真推理模型,档位在磁盘上被丢了;重启前一切正常,所以只在冷启动那一眼看得出来)。现在写入侧按上游 effort 形态把梯子写回去(toggle 形态写enabled: {},不可关闭的写不带disabled),读写仍是同一个readQoderThinkingConfig,互为反函数。 - 验证:
test/qoder-ui.test.tsx新增一例,钉住三行不同能力的摘要(单模态两档 / 多模态两档 / 无档位)与用户覆盖窗口按覆盖值显示;test/qoder-model-catalog.test.ts新增两例,用生产写入侧 → 生产读取侧往返,断言梯子、可关闭性、图片与窗口都不丢(老快照缺字段时仍是原来的兜底,不炸);全量 typecheck 与 vitest 通过。
-
[opendesign] 新增第十三条订阅线路:OpenDesign(open-design.ai 订阅,浏览器设备授权 + API Key)
- 协议面:OpenAI Chat Completions 流式对话,网关
https://amr-link.open-design.ai/v1,Authorization: Bearer鉴权。网关主机与/v1拼写转录自官方 Cloud 控制台的 quick-setup 包(它给 DeepSeek Harness 推荐的正是 Chat Completions),四个协议端点均经未认证探测确认(401 包络符合各自协议)。 - 登录一:浏览器设备授权(主路径)。按官方
vela login的公开 HTTP 契约重写:POST /api/v1/cli/device-authorizations({host, profile, cliVersion},实测 201 返回 deviceId / userCode / deviceSecret / activationUrl / pollIntervalSeconds),用户在浏览器确认后POST …/{deviceId}/token轮询取回vela_ctrl_…控制 Key +sk-amr-…运行时 Key + 账号身份(字段名据上游 issue #8870;pending 帧的 null 形状也是实测的)。判定凭证「到位」看字段在不在,不看 status 字符串;取消过的流程绝不因迟到的确认而落号;宿主自身不打开浏览器(test/login-opens-browser-once.test.ts钉住)。 - 登录二:粘贴 API Key(回退)。控制台(工作区 → API Keys)签发,仅用于对话。
OPENDESIGN_API_KEY环境变量可投射首个账号;Key 测试读免费的目录端点,不花额度。 - 模型目录:随包 37 个聊天模型(id、显示名、美元单价逐字转录自官方公开价目接口
/api/v1/public/model-prices),登录成功或添加 Key 后自动同步一次实时目录(GET /v1/models),同步条目持久化进设置文档,重启无需网络;目录真的变了才发llm/adapters-updated。官方不公布各模型上下文窗口,默认 128k,卡片可按模型覆盖。 - 每模型能力(窗口 / 输出上限 / 是否收图 / 推理档位):原先一律 128k + 固定三档,现在随包表按模型带上真实取值——转录自本仓库已核对的同族模型表(command-code 官方注册表、cline-pass、Claude、Antigravity;冲突以 cline-pass 为准,注册表的
off译成 DSH 的none)。浏览器授权账号还会读 AMR 控制面目录(GET /api/v1/models,metadata.contextWindowTokens,public_model_*id 按字母数字归一),把上游真实窗口写回;用户覆盖值 > 实时目录 > 随包表 > 默认。minimax-m3.1-flash、muse-wonder无同族表可核对,保留默认并在计划文档列名,不猜。 - 额度(钱包余额):只有浏览器授权账号带控制 Key,故只有它们能读
GET /api/v1/wallet/balance(balanceUsd字符串);余额显示在账号行与输入框胶囊(胶囊只显示主账号,$0 才转红)。上游只给当前余额、没有可对照的总额,所以不画百分比额度条;钱包 401/403 按上游语义(OPEND-3553)只作「读不到」显示,绝不标记账号、绝不影响对话。粘贴 Key 的账号没有控制 Key,卡片会说明并指向控制台。 - 验证:
npm run typecheck0 错误;新增 7 个 opendesign 套件(设备流 / 路由 / 适配器 / 映射 / 目录 / 契约 / UI)并给login-opens-browser-once与既有计数断言(TAB_IDS、卡片名、开关数、槽位 order 46)各追加一项;全量npx vitest run与npm run build通过。未用真实订阅联调:设备授权的创建/轮询响应是实测的,批准后的帧未观察到(据 #8870 推断且以字段存在为准),对话链路未与真实 Key 联调——清单见docs/opendesign-line-plan.md§5。
- 协议面:OpenAI Chat Completions 流式对话,网关
Full Changelog: v0.33.0...v0.34.0
v0.33.0
- [qoder] 看板上的 Qoder 卡片用的是字母 Q,不是官方标志——现在换成官方那张 412×412 应用图标
- 现象:设置页订阅列表里其余线路都是自家标志,只有 Qoder 是一个灰底字母方块。
- 真因:brand-icons.tsx 里那条注释给出的理由是「官方 favicon 是一枚 73 KB 的复合字形,体量不适合内联」。那个 73 KB 确实存在——但
https://qoder.com/favIcon.svg现在返回的是整站 HTML(73,379 字节,HTML 长度不是图标大小),页面真正挂的图标是<link rel="icon" type="image/png">指向的一张 alicdn PNG。于是「太大所以不做」这个判断建立在一次看错的抓取上。 - 改法:
ProviderBrand新增image形态(data URI +fit),BrandTile在它有值时画<img>并铺满底砖;Qoder 用官方 412×412 PNG(4,990 字节,sha2562276f0f8…26f3d,2026-10-21 抓取 HTTP 200),base64 后 6.6 KB。旧注释连同 monogram 一起删掉,换成带 URL / 抓取时间 / 字节数 / 校验和的出处。 - 验证:
npm run typecheck0 错误;test/hub-overview-ui.test.tsx新增一例断言 Qoder 卡渲染 data URI 且不再出现dsh-hub-brand-mono;全量npx vitest run280 文件 / 4572 passed / 7 skipped。
- [workbuddy] 会话一长就整轮失败、红条只有一句「必须以 system 提示词开头」——真因是上游 WAF 在按竞品客户端身份做指纹拦截
- 现象:截图那次是国际区账号第 2 轮起每轮必挂,报错前缀写着
rejected the message history: the conversation must open with a system prompt,与正文{"code":11128,"msg":"Illegal API invocation from an unapproved channel"}自相矛盾(历史首条本来就是 system)。同一账号发一句ping却可能正常,像是「聊着聊着就坏」。 - 真因:
11128被上游复用给了至少两种拒绝,而unapproved channel这一种是内容侧的 WAF——网关对system、assistant.content以及assistant.tool_calls[].function.arguments里出现的第三方 Agent 身份声明做精确短语匹配,命中即整单拒绝,与鉴权、额度、模型、区域都无关;又因为每轮都全量重放历史,一条 assistant 消息或一次工具参数中毒,此后每轮必挂。本机对失败会话的 1,552 行系统提示词做行级二分,定位出唯一触发行是首行You are an AI agent powered by DeepSeek Harness.(国内区在system/assistant中原样出现即 400,把AI agent改成AI assistant后同一份提示词 200);Claude Code、Codex CLI、OhMyOpenCode、x-anthropic-billing-header则两区都会触发。 - 为什么还牵扯到别的区域:号池里两个
intl账号被上游瞬时429 (code 14003)打回后按既有逻辑冷却并在同一次请求内轮换到cn账号,而cn网关恰好拦首行那句话——于是「国际区账号」的会话撞上的是国内区的规则。 - 改法(mapper.ts):
sanitizeText()增加出站指纹改写(覆盖options.system、role: "system"消息、assistant文本、以及tool_calls[].function.arguments的序列化参数),每条只改实测确认会触发的那一句、不动其余指令语义;同时把「剥\0」拆成stripNulls(),入站 SSE 仍只做\0清理——模型自己吐出的字不该被改写。 - 改法(adapter.ts):
code === 11128的判定从只认conversation id扩到conversation id|unapproved channel,并把原始 detail 一并带出;此前除conversation id外的所有 11128 都被翻译成「必须以 system 提示词开头」,把读者指向一段本来正确无误的历史。 - 验证:两区对拍——同一份含 5 类指纹的请求,脱敏前国内区与国际区都
400 / 11128,脱敏后都200;新增test/workbuddy-message-shape.test.ts的跨system/assistant/tool_calls用例与test/workbuddy-adapter.test.ts的两种 11128 文案用例;npm run typecheck0 错误,全量npx vitest run280 文件 / 4571 passed / 7 skipped,npm run build通过;用完整 1,552 行系统提示词带上下文对号池 4 个账号(2intl+ 2cn)端到端实测,四个账号全部 200。
- 现象:截图那次是国际区账号第 2 轮起每轮必挂,报错前缀写着
- [qoder] 新增第十二条订阅线路:Qoder(全球区 qoder.com / 中国站 qoder.com.cn)
- 协议面:Qoder 自有的 COSY 签名对话协议(
POST {base}/algo/api/v2/service/pro/sse/agent_chat_generation)——AES-128-CBC 加密的 UserInfo + RSA-1024 包裹的会话密钥 + canonical MD5 签名、自定义 Base64 alphabet + 三段交换的 body codec、外层信封包 inner JSON 的明文 SSE。全部算法按 Liki4/qodercli2api 的全合成 frozen fixtures(qodercli v1.1.34 互操作基线)逐字节锁定(test/fixtures/qoder/vendored,test/qoder-cosy.test.ts8 例),并与 simonsmh/pi-provider-qoder 的 live 验证实现互相印证。 - 登录三路径:浏览器设备流(仅全球区,challenge/verifier 轮询,不开本地回调端口,host 不自行开浏览器——
test/login-opens-browser-once.test.ts钉住)、手动粘贴 PAT(jobToken/exchange兑换,PAT 本体留在 host 加密存储里供过期重兑换)、adopt 只读导入本机 qodercli 登录态(~/.qoder/.auth/user,AES 解密由 fixture 锁定;区域不猜,是对两区 userinfo 各探一次探出来的)。 - 账号模型:多账号号池,凭据会过期、可刷新(提前 1 小时,逐字上游文档;PAT 来源重兑换、其余走 refresh_token 轮换),两区账号可同池共存(区域是凭据的属性)。
- 模型与额度:目录实时发现(COSY 签名的
/algo/api/v2/model/list)+ 随包能力兜底表;COSY 版本每日跟随官方 CLI release feed(落后会拿到缩减目录);能力一律来自目录字段(max_input_tokens那个陈旧地板不信)。额度卡片读/api/v2/quota/usage(非公开个人端点,失败绝不影响模型请求),三只桶(个人/加购/组织包)分开画绝对量;订阅无公开单价,整页只有 token 与 Credits 数字,没有金额。 - 验证:
npm run typecheck0 错误;新增 9 个 qoder 套件;全量测试与 build 通过。未用真实账号联调(mock + frozen fixtures 验收),联调清单见docs/qoder-line-plan.md§9。
- 协议面:Qoder 自有的 COSY 签名对话协议(
- [kimi-code] K3 跑到一半突然整轮失败,红条上只有一个
terminated配UNKNOWN——现在它是可重试的TRANSPORT- 现象:
k3的长回答(截图那次已经执行 27 次思考、调用 30 次工具)偶发中断,界面是「本轮运行失败 terminated」,右侧徽标UNKNOWN;手动重发一次立刻恢复。 - 真因:连接在响应体中途被掐断时 undici 抛的是裸
TypeError: terminated——它自身不带任何错误码,真原因(ECONNRESET、对端关闭、body timeout)只挂在.cause上。adapter.ts 的读流循环只改写 mapper 已经定性的 in-band 错误,其余原样throw error,于是这个裸值一路漏到 harness:normalizeLlmFailure对它只能归一成UNKNOWN,而UNKNOWN不在本线路的retryableCodes(RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT)里——一次断连就把整轮判死,连一次自动重试都没有,同时还把.cause丢掉,报告里连发生过什么都说不出来。 - 对照:同仓库 codex 线路早为同一形状写过
streamFailure()(responses-client.ts),claude 线路有outputStarted ? PROVIDER_ERROR : TRANSPORT的分支;kimi 是唯一照旧写法留下的那条。 - 改法:新增
streamFailure()(adapter.ts),把逃出读循环的非LlmError值改判成TRANSPORT,并把.cause链拼进消息(Kimi Code stream failed: terminated: read ECONNRESET);调用方取消仍是ABORTED,本线路自己定性的LlmError原样通过。assertStreamComplete那条同类形状——连接干净结束、但始终没等到终止事件——同步从PROVIDER_ERROR改成TRANSPORT(mapper.ts),两种「说到一半没了」都交给 harness 的退避重试(最多 3 次,1.5s–15s)。 - 为什么已经出字也重试:失败的那次 attempt 由 DSH 记成
assistant/attempt——不进行模型历史、也不作为正文渲染(宿主turn-process.ts的visibleAssistantEvent对 attempt 恒为不可见),工具调用要等流结束才执行,所以重试既不会让模型看到重复内容,也不会重跑工具;用户看到的只是已流出的那段字被重试的那一次顶替,比整轮作废划算。 - 验证:
npx vitest run test/kimi-code-adapter.test.ts test/kimi-code-stream-idle.test.ts test/kimi-code-mapper.test.ts90 passed(新增 3 条断流用例;把throw streamFailure(...)临时改回throw error时,断流用例当场复现裸TypeError: terminated,证明用例咬得住);npm run typecheck0 错误;全量npx vitest run270 文件 / 4491 passed / 7 skipped。
- 现象:
- [minimax-code]
/video不再是「先选档位、再往输入框塞一段模板」:命令行有了图标,档位搬进面板,面板也终于不再把整页压暗- 现象:斜杠菜单里
/video是唯一一行没有图标的,混在「压缩 / 权限 / 模型 / 导出」里明显没对齐;点它先弹一个只有「常用档位 + 自定义…」的小列表,选中任何一档都会把一整段用 minimax_generate_video 生成视频:prompt=(在这里写画面描述)…灌进输入框,用户还得回到那段话里找该改哪一处;而「自定义…」打开的面板盖着一层半透明遮罩,整页对话被压暗,参数又是 9 个同权重字段竖着排下来。 - 改法(video-command.ts):
popupSelect换成action——命令只做一件事:打开面板。图标走宿主的CommandContribution.icon(video-command-icon.tsx,按IconProps的{ size, className }同形自绘,因为dsh-client-ui-primitives的lib是宿主装配产物、插件装不到)。宿主侧限制:ui-commands的matchEnter只在裸命令时把一行交给客户端贡献(if (!bare) return undefined),所以「/video后面直接写描述」做不到;改为打开时把输入框里已有的文字当成画面描述带进来(composerDraftOf读InputState.draft,并剥掉可能残留的前导命令 token)。 - 改法(VideoFormDialog.tsx / styles.ts):去掉遮罩,面板变成阴影浮起的居中浮层(
margin:auto保住「内容太高仍可滚动」),点浮层外仍然关闭;版面按「先说画面、再说参数」重排——描述占最大位,下面一排档位胶囊,再一行模型/时长/分辨率,比例按联动规则锁死或可选,水印与参考媒体收进「更多参数」。 - 参考媒体(新增 video-media-input.ts 与 video-media-dropzone.tsx):四排纯文本框换成四个媒体块,浏览 / 拖拽 / 粘贴三入口合一,路径显示成可移除的标签。桌面端靠
__DSH_HOST_PATHS__.pathFor把File还原成真实绝对路径;拿不到路径时如实提示而不是编一条。file://地址还原成本机路径;http(s)链接里只有cdn.hailuoai.com被接受(宿主assertMinimaxCdnUrl实测只读这一个主机),其余公网地址当场拒绝——否则会变成计费之后才被服务端打回。CDN 参考视频读不到时长,所以每出现一条就长出一行「它的时长」,填了才进请求(宿主MINIMAX_VIDEO_REFERENCE_DURATION_REQUIRED)。 - 两处回调:① 档位胶囊原本会折成两行(三个全名并排塞不进 600px),改成
grid-auto-flow:column等宽一行,胶囊上只留H3 · 4秒这类短标签,分辨率与比例挂在title/aria-label上仍然查得到;② 每个媒体块补了一行手填入口——路径与@工作区文件都收(stripReferencePrefix去掉那个前缀,因为工具参数要的是普通路径),回车即添加。 - 版面再调整:面板从
600px加宽到880px并改成两栏(左栏只放画面描述,右栏放档位、参数与参考媒体,≤720px 时折回一栏);拖拽区与输入行合并为同一个框——已收下的标签在上、输入行贴底,整块都是放置目标;输入框里的@现在会弹出真实工作区候选(新增 video-workspace.ts 包住宿主remote.workspaceFiles.list:@后的目录片段决定列哪一层,选中文件即入列、选中目录则继续下钻,Tab取第一项、Esc先收候选再关面板);宿主没有这条远端时自动退化为纯手填,不报错。 - 两栏落地后的三处修正:① 描述框的
width:100%加上左右内边距把它顶出了栏宽,文字一路压到右栏的「档位」标签上——补box-sizing:border-box,两栏间距同时从 16px 放到 28px;② 展开「更多参数」后左栏只剩一小块输入框、下半截全空——描述框改为跟随右栏一起长(flex:1一路传到 textarea,上限 520px);③ 左栏标题行的「已带入输入框里的文字」提示与右栏列头贴在一起,允许换行并在挤不下时省略。 - 验证:
npm run typecheck0 错误;全量npx vitest run270 文件 / 4488 passed / 7 skipped(新增test/minimax-code-video-media-input.test.ts,扩写命令与面板两个套件);npm run build通过。
- 现象:斜杠菜单里
Full Changelog: v0.32.0...v0.33.0
v0.32.0
- [cline-pass] 凭据后端读不出来时不再吞掉
CLINE_PASS_API_KEY:环境变量与密钥环是两条独立来源- 现象:在没有 Secret Service 的 Linux 环境(裸 CI runner 最典型)里,
cline-pass的账号列表是空的,即使显式导出了CLINE_PASS_API_KEY——于是「随包内置的密钥改用环境变量注入」这条文档化的用法在这些环境里静默失效。 - 真因:
account-pool.ts的legacyAccount投影第一行就是await store.read()。读取平台加密后端在 Linux 上要有个解锁的 keyring;没有就抛错,而这一抛把后面那句ambientCredentials()(读环境变量)一起带走了——两个本来互不相干的来源被串成了一条链。Windows 有 DPAPI、macOS 有 Keychain,所以本机从未暴露,只有 CI 红。 - 改法:
store.read().catch(() => null)——密钥环答不上来就当作「没有已存凭据」,环境变量照常投影。不是妥协:环境变量本来就不需要密钥环可读,它从来没有依赖过存储。 - 验证:
test/cline-pass-plugin.test.ts新增一例把store.read打桩成 reject 并断言环境变量投影仍然生效(5 passed);同线路另外 2 个套件(account-refresh/routes)一并回归通过。
- 现象:在没有 Secret Service 的 Linux 环境(裸 CI runner 最典型)里,
- [cline-pass] 高级设置与整站版式对齐:输入框终于有主题样式,六行数值与三行开关各归其位
- 现象:设置页「高级设置」里的六个输入框是浏览器原生外观(白色实心框 + 粗边框),与旁边
.dsha-select的深色圆角控件格格不入;原因是 styles.ts 用了.dsha-input这个类名,而全站样式表从未定义过它——其他线路的输入框都自带各自的-control input规则,本线路抄了类名没抄规则。 - 错位:原布局用一张双列 Grid(
.dshcp-field)把所有控件塞在右半屏,而<select>被.dsha-select的max-width:220px卡住,于是输入框顶到最右、下拉框缩在中间;「在模型选择器中列出网关目录的额外模型」的复选框被 Grid 推到距左 240px 处,与下方「注册cline_pass_*管理工具」那个贴右的复选框不在同一条竖线上。 - 说明串了行:
t.pinSpellingHint(「网关可能在两次请求间切换管线…」)被写在整张 Grid 下方,视觉上成了「列出网关目录额外模型」的副标题,而它真正的t.exposeCatalogHint根本没渲染。 - 改法:① 补上
.dsha-input的主题样式(--dsw-alias-bg-layer-2底、0.5px边框、7px圆角、focus-visible描边),与.dsha-select同一套 token;② 六个数值行改用共享.dsha-row(左标签右控件),右侧统一240px定宽(.dshcp-adv-control),数值与地址分别用等宽字体(.dshcp-adv-num右对齐 /.dshcp-adv-mono);③ 三行开关改用共享.dsha-pref-row(左侧粗体标题 + 灰字说明、右侧.dshcp-check复选框),三枚复选框在右侧对齐;④ 每句提示归位到它自己那一行的标题下,并补回漏掉的t.exposeCatalogHint。 - 验证:
npm run typecheck0 错误;test/cline-pass-section.test.tsx12 passed(新增一例断言 6 行.dsha-row+ 3 行.dsha-pref-row且每行副标题与自己的标题配对);npm run build通过。
- 现象:设置页「高级设置」里的六个输入框是浏览器原生外观(白色实心框 + 粗边框),与旁边
- [进展更新] 面板正文终于能画代码块、公式与 mermaid:不再把源码原样摊在读者面前
- 现象:进展正文走的是本插件自己的
MarkdownLite渲染器(平台那个MarkdownText的 lib 是浏览器构建,顶层import css from X.module.css会让任何 Node/vitest 导入方直接失败),而它只认段落、列表、GFM 表格、行内代码、粗体与标题——于是正文里只要出现围栏代码块或公式,源码就逐字显示出来。 - 改法(markdown-lite.tsx):加围栏代码块(按 CommonMark 的闭合规则,未闭合的围栏吞掉剩余全文,所以流式输出永远不会漏出反引号)与 KaTeX 公式(行内
$..$/\(..\)、块级$$..$$/\[..\])。KaTeX 的 HTML 经DOMParser映射成 React 元素,不走dangerouslySetInnerHTML,渲染器「无注入」的性质得以保持;美元符号的判定照抄平台语法守卫,因此「prices $5 to $10」仍然是散文。 - 改法(renderer.ts):语法关键字表从 10 种扩到 mermaid 11 能识别的 47 种(journey、quadrantChart、C4Context、sankey-beta 等此前根本看不见);并且旧代码只匹配行首关键字,于是非常常见的
%%{init}%%、注释与 frontmatter 前言会把一张本来合法的图判死——新增stripMermaidPreamble在匹配前剥掉它们。横幅语言标签改读文档化的[data-code-block-banner]钩子,而不是firstElementChild(后者的textContent是语言名加复制按钮文案)。 - 一个容易踩空的点:平台对 mermaid 围栏不会打印 "mermaid"(高亮器没有 mermaid 语法,
CodeToolbar回落到codeBlock.title占位符)。所以那个标签对特定语言是否决票、对 mermaid 是显式放行,而占位符的含义是「没有标签」,此时由关键字表说了算。 - 依赖:
katex进dependencies(^0.18.7,平台基线钉的同一个版本);它的 CSS 与字体仍来自宿主页面,所以 bundle 只是变大,不会多打包一份样式表。 - 验证:
npm run typecheck0 错误;新增test/markdown-lite.test.tsx与扩写的test/mermaid-renderer.test.ts;全量npm test268 文件 / 4452 passed / 7 skipped。
- 现象:进展正文走的是本插件自己的
- [antigravity] 上游 5xx 与「根本没连上」不再判本轮死刑:按可重试码上报,交给宿主的退避重试
- 现象与真因:个别轮次直接失败并显示
Antigravity API error (500): No response(徽标PROVIDER_ERROR),手动重发立刻能过。上游那次 500 确实是瞬时抖动,但这条线路把它归成了终局错误码:adapter.ts里非 429 的非 2xx 一律抛PROVIDER_ERROR,而这条线路的providerRetryPolicy()返回undefined,宿主因此套用默认策略,默认可重试码只有EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT——PROVIDER_ERROR不在其中,llm-retry一次都不会退避重试,直接把这一轮判死。 - 还有一个会撒谎的状态码:
fetch在每个端点上都抛错(DNS / TLS / 连接被重置)时根本没有response对象,而旧代码用response?.status ?? 500补了一个 500,于是"根本没连上"和"上游真的回了 500"在界面上长得一模一样。 - 改法:① 所有端点都没拿到 HTTP 响应 →
TRANSPORT(带cause),不再伪造 500;②status >= 500→SERVER;③ 400 / 401 / 403 / 404 仍是PROVIDER_ERROR终局,429 的两条既有判决不变。瞬时故障从此由宿主的 5 次退避重试(500ms 起、上限 10s)接管,真正被拒的请求仍然当场失败。 - 验证:
npm run typecheck0 错误;npm test268 文件 / 4429 passed / 7 skipped。test/antigravity-adapter.test.ts新增 1 例(全端点fetch拒绝 →TRANSPORT),503 那条断言的 code 由PROVIDER_ERROR改为SERVER,其"端点优先级"的本意不变。
- 现象与真因:个别轮次直接失败并显示
- [cline-pass] 新增第 10 条订阅线路:Cline Pass(WorkOS OAuth 登录 + 上游渠道钉住)
- 范围:把 MIT 许可的
dsh-cline-pass的功能按本仓库的线路规范兼容移植为独立线路cline-pass,复用共享内核(AccountPoolCore、并发闸、空闲看门狗、请求图片预算、claimProviderRoute、账号卡片 / 模型清单 / 上下文编辑器 / composer 胶囊),不改动任何既有线路的行为。 - 登录:Cline 账号走 WorkOS 设备码 OAuth(与官方 CLI 的
cline/cline-pass同一条链),浏览器确认后账号自动入池;access token 以Bearer workos:<token>发送,到期前自动 refresh,轮换后的 refresh token 先落盘再使用;invalid_grant标记账号失效并要求重新登录,网络/5xx 不误登出。静态 API Key 保留为兼容回退。端点、client id 与workos:前缀转录自 Cline 官方开源仓库(sdk/packages/core/src/auth/cline.ts、auth/provider-auth-registry.ts、sdk/packages/shared/src/runtime/cline-environment.ts,commitca7402c,2026-10-20)。 - 能力:OpenAI Chat Completions 流式(文本 / reasoning / 工具调用 / 图片);多账号(OAuth)或 API Key 顺序 / 轮询 / 粘性调度、429 冷却、401/403 失效标记与换号;上游渠道探测 / 逐渠道校验 / 钉住 / 排除,
strict与preferred两种模式,首个 token 前故障转移;两种管线写法默认都写(provider.*与providerOptions.gateway.*),并以响应回读的实际渠道判定钉住是否被采纳;官方目录(recommended-models+ models.dev + 上游网关)刷新;按账号读取 5 小时 / 周 / 月额度;进程内请求历史(含失败尝试链与 token 用量);8 个cline_pass_*管理工具默认不注册,卡片内可开启(重启生效)。 - 注册点:
src/index.ts(线路工厂 / 路由 / hub 摘要 / dispose / 导出尾部)、CONTROLLED_PROVIDERS、DIAGNOSTIC_PROVIDERS、hub/providers.tsx、hub/brand-icons.tsx、client/index.tsx(locale / 样式 / 胶囊 order 44),以及既有计数断言各追加一项。 - 传输:对"请求尚未发出"的 TLS/connect 失败自动重试 2 次,并在最终错误里说明"未到达网关、钉住未生效";实测本机到
api.cline.bot的 Node TLS 握手会间歇失败(同一时刻 curl 正常、IPv6 不可达),这与上游钉住无关。流中段断流不重试,避免重复计费。 - 安全:OAuth access / refresh token 与 API Key 只进平台加密凭据库(DPAPI / Keychain / Secret Service),响应与日志均不出现原文;登录流程 DTO 逐字段投影;所有写路由同源校验;错误信息最多回显上游一行原因。
- 文档与品牌:新增
docs/cline-pass-line-plan.md(每个常量的出处、决策、未验证清单);README 增加线路章节与路由表;协议证据矩阵增加一行;hub 卡片改用 Cline 官方标记(assets/branding/logos/cline-icon-white.svg+ 官方 app icon 的#343B42底砖),不再用字母占位。 - 验证:
npm run typecheck0 错误;全量npx vitest run267 文件 / 4390 passed(在 upstream 0.31.0 之上新增 8 个测试文件、68 例);npm run build与npm pack --dry-run通过,lib/types/host/cline-pass/*已进包。
- 范围:把 MIT 许可的
What's Changed
- feat(cline-pass): add the Cline Pass subscription line by @rinDBeans in #63
New Contributors
- @rinDBeans made their first contribution in #63
Full Changelog: v0.31.0...v0.32.0