Skip to content

Releases: Aa728848/dsh-chatgpt-subscription

v0.38.1

Choose a tag to compare

@github-actions github-actions released this 24 Oct 08:04
  • [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 test 282 文件通过 / 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)为基准,不跟随按模型档位——它是可选的提前压缩开关而不是守卫,本次刻意不动;不想提前压缩时不设该变量即可(默认就是不设)。

Full Changelog: v0.38.0...v0.38.1

v0.38.0

Choose a tag to compare

@github-actions github-actions released this 24 Oct 01:20
  • [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-highspeed 1M),含 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 test 281 文件通过 / 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

Choose a tag to compare

@github-actions github-actions released this 23 Oct 07:19
  • [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 test 277 文件 / 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 test 277 文件 / 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.ts 31 例(workspace 解析的数组/包装/无 id/401、窗口读取的两窗口与坏窗口丢弃、拒绝码作为数据、每请求只解析一次工作区、钱包在额度读取失败时不丢)、test/widget-board-lines.test.ts 57 例(两窗口 → 300/10080 分钟、已用 10/19%、resetsAt 毫秒、label 为空、钱包仍在;读不到时 windows 为空且原因进 error)、test/opendesign-section.test.tsx 17 例与 test/composer-quota-badge.test.tsx 10 例(每窗口一条 bar 与 aria 值、失败/未配置不画条、徽标有窗口显示剩余%、无窗口退回余额且零新节点)。
    • 验证(含实机结论):npm run typecheck 三工程 0 错误;全量 npm test 277 文件通过 / 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 读 GNOME gsettings 的 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.ts 42/42;全量 npm test 277 文件通过 / 2 skipped,4557 passed / 8 skipped / 0 failed;npm run build 通过。
  • [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.ts 42 例全绿(11 + 31);全量 npx vitest run 277 文件通过 / 2 skipped,4519 passed / 8 skipped / 0 failed。

Full Changelog: v0.36.2...v0.37.0

v0.36.2

Choose a tag to compare

@github-actions github-actions released this 22 Oct 06:38
  • [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 test 275 文件通过 / 1 跳过,4477 passed / 7 skipped / 0 failed;npm run build 通过。
    • 已知边界:卡片路径的续期与模型请求共用同一续期入口,因此一次续期失败(例如离线)会让下一次状态轮询再次尝试续期;这与模型请求路径的既有行为一致,未在路由层另加冷却。

What's Changed

  • fix(claude): renew the card credential before spending it by @Aa728848 in #67

Full Changelog: v0.36.1...v0.36.2

v0.36.1

Choose a tag to compare

@github-actions github-actions released this 22 Oct 05:25
  • [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 test 274 文件 / 4470 例通过(1 条 minimax-code-video-history 的 Windows rename EPERM 抖动,单跑 22/22 通过,与本改动无关);npm run build 通过。
    • 已知边界:QuotaRefresh.run() 不与正在跑的 start() 合流,所以「手动刷新正好撞上后台刷新」这一步仍会多扇出一轮;两轮写入形状相同、后写的 setUsage 生效,代价是一次多余上游请求。真正合流要给共享 helper 加 wait,会牵动另外六条线路,本次不做,代码里按此写明。

Full Changelog: v0.36.0...v0.36.1

v0.36.0

Choose a tag to compare

@github-actions github-actions released this 22 Oct 01:56
  • [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 抛错时,安装不抛错、其余命名空间仍注册);三工程 tsc 0 错误;全量 vitest 4452 passed / 7 skipped / 0 failed;npm run build 通过。
  • [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 与美元、今日/累计消耗、计费请求数。仍然不画百分比条:摘要只说剩余,不说配额上限,卡片不臆造分母。
    • 验证:三工程 tsc 0 错误;全量 vitest 4451 passed / 7 skipped / 0 failed;npm run build 通过;用真实控制 Key 请求 /billing/summary 返回 go / active / 15000 credits($1.50),经 /usage 后 status 的 plan 由 free 变为 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 typecheck 0 错误;npm run build 通过;全量 npx vitest run 4436 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 typecheck 0 错误;npm run build 通过;全量 npx vitest run 4395 passed / 4 failed,与改动前(git stash 对照)完全相同的 4 条——它们断言英文 Intl 格式(1,200、12:03),在俄语默认 locale 的机器上必然失败,与本改动无关。

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

  • @unera made their first contribution in #64

Full Changelog: v0.35.0...v0.36.0

v0.35.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 13:59
  • 【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 与路由 id workbuddy-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 账号采纳)才整条删除。
    • 验证:三工程 tsc 0 错误;全量测试 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: 转成绝对路径但保留协议前缀),所以它是权威答案;取不到时才退回路径启发式——root realpath 后按整段切分看有没有 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(不过就 403 csrf-rejected),然后按与检测同一顺序守卫:本地链接 → 409 development-install;无 pluginManager → 409 plugin-manager-missing;都过才 installBundle('@eddyskywalker/dsh-chatgpt-subscription@latest', { enabled: true, requestId })。这个 spec 是模块常量 UPDATE_INSTALL_SPEC,请求体一个字都不读,所以任何同源 POST 都无法选择要装进 profile 的包或版本。安装失败(application: 'failed' | 'cancelled')把 pnpm 自己的 diagnostic 原样带回成 502 install-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 的就是结算后的结果;宿主的 installs map 在调用 settle 前就把条目删了,那一次调用必然返回 null,只会在测试里多需要一个桩。
    • 验证:npx tsc -b --pretty false exit 0;npx tsc -p test/tsconfig.json --pretty false exit 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-check 28 条,其中新增 12 条状态机用例;hub-update-ui 22 条,含图标形态的 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 409 development-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 + 原因)、回滚失败(红字写明可能已不可用 + 手动恢复命令全文)。

Full Changelog: v0.34.0...v0.35.0

v0.34.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 06:43
  • [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-256k clamp 前后相等、以及网关原文归类与「无关 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 typecheck 0 错误;npx vitest run test/kimi-code test/dynamic-output-reservation 30 文件 / 475 条全过;全量 npm test 290 文件通过 / 4751 条通过 / 0 失败(7 skipped 为仓库既有平台跳过:token-store-linux 等)。
  • [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 typecheck 0 错误;全量 npx vitest run 282 文件 / 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 typecheck 0 错误;新增 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。

Full Changelog: v0.33.0...v0.34.0

v0.33.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 16:29
  • [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 字节,sha256 2276f0f8…26f3d,2026-10-21 抓取 HTTP 200),base64 后 6.6 KB。旧注释连同 monogram 一起删掉,换成带 URL / 抓取时间 / 字节数 / 校验和的出处。
    • 验证:npm run typecheck 0 错误;test/hub-overview-ui.test.tsx 新增一例断言 Qoder 卡渲染 data URI 且不再出现 dsh-hub-brand-mono;全量 npx vitest run 280 文件 / 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 typecheck 0 错误,全量 npx vitest run 280 文件 / 4571 passed / 7 skipped,npm run build 通过;用完整 1,552 行系统提示词带上下文对号池 4 个账号(2 intl + 2 cn)端到端实测,四个账号全部 200。
  • [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.ts 8 例),并与 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 typecheck 0 错误;新增 9 个 qoder 套件;全量测试与 build 通过。未用真实账号联调(mock + frozen fixtures 验收),联调清单见 docs/qoder-line-plan.md §9。
  • [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.ts 90 passed(新增 3 条断流用例;把 throw streamFailure(...) 临时改回 throw error 时,断流用例当场复现裸 TypeError: terminated,证明用例咬得住);npm run typecheck 0 错误;全量 npx vitest run 270 文件 / 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 typecheck 0 错误;全量 npx vitest run 270 文件 / 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

Choose a tag to compare

@github-actions github-actions released this 20 Oct 12:28
  • [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)一并回归通过。
  • [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 typecheck 0 错误;test/cline-pass-section.test.tsx 12 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 typecheck 0 错误;新增 test/markdown-lite.test.tsx 与扩写的 test/mermaid-renderer.test.ts;全量 npm test 268 文件 / 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 typecheck 0 错误;npm test 268 文件 / 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,commit ca7402c,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 typecheck 0 错误;全量 npx vitest run 267 文件 / 4390 passed(在 upstream 0.31.0 之上新增 8 个测试文件、68 例);npm run build 与 npm pack --dry-run 通过,lib/types/host/cline-pass/* 已进包。

What's Changed

  • feat(cline-pass): add the Cline Pass subscription line by @rinDBeans in #63

New Contributors

Full Changelog: v0.31.0...v0.32.0