Skip to content

v2.0.5

Choose a tag to compare

@github-actions github-actions released this 29 Jul 02:47
· 30 commits to main since this release

F-01 多 Provider Failover 在正常 UI 选模链可达

  • 问题:B2 已修好多 provider 候选链 + failover 核心(router/resolver/circuit breaker 全就绪且有单测),但用户从正常 Cursor UI 选模型时,mocks.go 暴露给 UI 的模型标识是渠道 IDadapter.ID)而非逻辑 modelID。UI 回传渠道 ID 后,ResolveAdapterIndexes 第 1 层精确 ID 匹配命中唯一 adapter,第 3 层 providerModelID fallback 永不触发——同 modelID 多 provider 的候选链恒为 1,主渠道失败不切备用。用户配了候选链却走不进去。
  • 修复mocks.go 三处暴露标识从渠道 ID 改为逻辑 modelID:
    • collectModelAdapterRefsdefaultModel/fallbackModels)返回 modelID,按 Priority 升序、按 modelID 去重——同 modelID 多 provider 在列表里只出现一次(选中任一都激活该 modelID 全部候选链)。
    • buildAvailableModelEntriesname/serverModelName 改 modelID。
    • buildThinkingEffortVariantsvariantStringRepresentation<modelID>:<effort>——UI 按 thinking intensity variant 选中后 splitRuntimeThinkingEffortVariantString 拆出 modelID。
    • UI 回传 modelID → ResolveAdapterIndexes 第 1 层精确 ID(渠道 ID)不命中、第 3 层 providerModelID fallback 命中所有同 modelID 的 enabled adapter → 候选链 >1 → B2 failover 在正常 UI 选模链可达。
    • 默认模型按 Priority 选(新增 orderAdaptersByPriority,与 config/resolver.go 候选链排序同口径),UI 默认选中项即候选链主候选。
  • 同 modelID 多 adapter 的 UI 呈现:采"每 adapter 一条 entry + displayName 区分"方案(不合并)——下拉可能出现同名项(name=modelID),但选任一都回传 modelID 激活整条候选链,这是 failover 冗余配置的预期态;用户给主备 adapter 配不同 displayName 即可在下拉区分。
  • 测试mocks_f01_failover_test.go 3 端到端(暴露 modelID 契约 / 默认模型按 Priority / failover 经 modelID 可达)+ mocks_disabled_filter_test.go 更新断言(name=modelID)+ 加 dedupe/Priority 测试。resolver 层"传 modelID 返回多候选"已由 config.TestResolveModelAdapterChannelsReturnsAllCandidates / modelchannel.TestResolveAdapterIndexesReturnsAll 覆盖。
  • 未做真机验证:Cursor 客户端是否对 name 字段格式有假设(之前是渠道 ID 字符串,现是 modelID)需真机点选验证;纯后端逻辑链已闭合。

L7 stale exec control 区分"已处理"与"从未存在"(可观测性)

  • 问题shouldIgnoreMissingExecControl(stream 存在但 pending exec 找不到时)经 shouldIgnoreStaleExecControl 对 Heartbeat/StreamClose 一律静默吞。重连客户端迟到的控制消息确实是传输级噪声(合理忽略),但若 pending exec 被错误清除也表现为 missing,无条件吞会掩盖真实协议错误。
  • 修复:拆出 isStaleTransportExecControl 复用于两处。shouldIgnoreMissingExecControl 对传输级控制消息先查 recentlyCompletedExecExists——已处理则忽略(合理);从未存在则仍忽略但记 WARN... never existed; ignored, may indicate protocol drift),让真实协议异常可被诊断。
  • 关键取舍:"never existed" 必须返回 true 不杀流——若返回 error 会经 actor.gofailStream 把整个流标失败,重连客户端迟到的 Heartbeat 会误杀整个对话,比静默吞更糟。故选"忽略 + WARN"而非"surface error"。shouldIgnoreStaleExecControl(stream 已不 active 场景)转调 isStaleTransportExecControl 保持原静默吞语义。13 回归场景。

L3 前端 normalizer 回归测试

  • 问题appState.js 的 normalizer(normalizeModelAdapter/normalizeModelAdapters/normalizeConfig)是前端 config 层契约边界——F-02(payload merge 透传)、L5(旧品牌 key 迁移)、v2.0.3(per-namespace 路由)、A6(cursor 配置)等改动都经手这些函数,但前端此前零自动化测试,回归只能靠人工点 UI。
  • 修复:引入 vitest + happy-dom 最小测试环境。frontend/vitest.config.js 故意不复用 vite.config.js(后者挂载 wails 插件,纯 Node 测试环境会拉起 wails 绑定导致加载失败),只设最小 @/@bindings 别名 + happy-dom(提供 localStorage/window,appState.js 模块加载时 migrateLegacyStorageKeys/loadCachedState 依赖之)。appState.test.js 12 场景锁住契约:baseURL 归一(协议/host 小写、去尾斜杠、非 http(s) 置空)、字段别名契约(baseURL||urlapiKey||key、数值字段接受 snake_case)、未知 type 置空、非法 reasoningEffort 回落 medium、enabled 默认 true、costMultiplier F-02 透传、openai 专属字段仅在 openai 类型生效、非数组归一为空数组、空 config 全默认、perNamespace 清洗非法值丢 auto、mode 非法值回落 local、round-trip 幂等。为测 normalizeConfig 导出之(纯函数,导出无副作用,生产代码仅加一个 export 关键字,行为零变化)。package.jsontest/test:watch 脚本。
  • 未做:ESLint(维持 2.0.2 既定暂缓决策——前端无现成 lint 基础设施,投入产出比低)、i18n 快照测试(static-i18n-plugin.js 是构建期转换,快照价值低于 normalizer,留后续)。normalizer 单测是 L3 高价值核心,先行落地。

A6 Cursor 配置接管安全网(备份/还原/崩溃恢复)

  • 问题:cursor-switch 接管时 WriteUserProxySettings 改写用户 Cursor settings.json 注入 5 个代理键(http.proxy 等)。原 ClearUserProxySettings 退出时直接 delete 这些键——若用户接管前有自己的 http.proxy,退出后用户原始代理配置丢失(被覆盖再被抹掉,而非还原)。若崩溃没来得及 Clear,下次启动在已污染 settings 上再注入,备份污染值还会覆盖真实原始值。
  • 修复(对齐 cc-switch proxy_live_backup + live_takeover_active):
    • 接管前备份WriteUserProxySettings 注入前把被覆盖键的原始值(含"接管前是否存在"标记)快照到 ~/.cursor-local-assistant-v2/data/cursor-settings-backup.json(0700 目录 + 0600 文件)。仅当无已有备份时写——防"接管→崩溃→重启→又备份当前注入值"覆盖真实原始值。
    • 退出还原ClearUserProxySettings 不再简单 delete,从备份还原——接管前存在的键写回原始值(用户原始代理回来),接管前不存在的键 delete。无备份退回旧 delete 语义。还原后清备份。
    • 崩溃恢复ApplyCursorSettings 注入前先 RestoreCursorSettingsFromCrash——检测 settings 残留注入键 + 有备份 → 上次非正常退出 → 据备份还原原始配置;残留但无备份 → best-effort 清注入键退回"无代理"。
    • B3 切换锁:已由 F-35 的 lifecycleMu 串行化 StartProxy/StopProxy/SaveUserConfig 覆盖,ApplyCursorSettings/ClearCursorSettings 都在锁内调用不会并发半切换,无需额外锁。
  • 测试settings_backup_test.go 8 场景——备份+还原核心契约(接管前有代理→退出还原原始值而非抹掉)/ 无原始代理退出删键 / 崩溃恢复从备份还原 / 崩溃恢复无备份清残留 / 无残留 no-op / 备份不覆盖防污染 / 逐键还原逻辑 / 无备份退回 delete。

A3 Thinking Signature 整流器(自愈式重试)

  • 问题:走 Anthropic 兼容路由的第三方中转(DeepSeek/Kimi/Qwen/GLM/… 回签 Anthropic 风格响应的)实现 thinking signature 参差,常回签无效签名。cursor-switch 把会话历史里的 assistant 推理内容与签名原样回带上行,provider 校验签名失败直接返回 HTTP 400——而 400 属不可重试错误,Router 不会 failover,签名错误原样透传给用户,对话中断。
  • 修复:adapter 层加 thinking signature 整流器(internal/backend/agent/model/thinking_rectifier.go,对齐 cc-switch thinking_rectifier.rs)。首试若命中签名类错误(且流尚未首字节、本请求未整流过),自动剥离会话历史里 assistant 消息携带的推理内容与签名(ReasoningContent/ReasoningSignature/ReasoningSignatureSource 与 OpenAI Responses 推理字段),让 provider 视作"无 thinking 历史的新请求"重试一次,绕开签名校验。
  • 为何在 adapter 内而非 Router failover:签名错误是 HTTP 400,isRetryableChannelError 视为不可重试,Router 直接透传;根因是会话历史里的无效签名,换候选治标不治本——同一份历史发到下一个候选大概率还是 400。必须整流消息本身。整流重试发生在 sink 首字节之前(provider 在流开始前就拒绝请求),与 Router 的 sinkStarted failover 闸门正交不冲突。
  • 安全闸门:① shouldRectifyThinkingSignature 只匹配 cc-switch 对齐的 7 个签名错误场景(thinking block 签名无效 / Thought signature not valid / must start with a thinking block / expected thinking found tool_use / signature field required / signature extra inputs not permitted / thinking blocks cannot be modified / 非法请求兜底),普通 400 不触发;② 本地 sinkStarted 闸门——首字节已发绝不重试(避免双发);③ rectifiedOnce 闸门(RequestKnobs["thinking_rectified"])——只整流一次,二次失败透传给上层由 Router 决定,避免与真正坏掉的 provider 死循环;④ 客户端已取消不重试。
  • 接入AnthropicAdapter.Stream / OpenAIAdapter.StreamstreamWithThinkingRectifier 包装,原流逻辑下沉为 streamOnce(方法体一字未改)。OpenAI 原生协议不含 thinking signature 概念,但走 Anthropic 兼容路由的中转会把签名错误透传进来;判定只在错误形态匹配时触发,对纯 OpenAI 路径零副作用。
  • 测试thinking_rectifier_test.go——shouldRectifyThinkingSignature 14 场景(7 触发 + 反例)、rectifyMessagesForThinkingSignature(assistant 推理清空 / user·tool 不动 / 深拷贝不改入参 / 无改动返回 nil,false)、streamWithThinkingRectifier 7 端到端(签名错误重试成功 / 非签名不重试 / sinkStarted 不重试 / 已整流不重试 / 无可剥离不重试 / 二次失败透传 / 取消不重试)。