feat(providers): LLM seam 改造 Stack 合并版——统一流式入口/重试策略/拦截器注册化/轨迹账本 + 生产构建修复 - #605
feat(providers): LLM seam 改造 Stack 合并版——统一流式入口/重试策略/拦截器注册化/轨迹账本 + 生产构建修复#605AlphaCatMeow wants to merge 8 commits into
Conversation
为 LLM seam 改造提供行为等价判定基准: - wire-payload-golden:anthropic-messages / openai-completions / openai-responses / google-generative-ai / deepseek-responses 五协议 固定输入下的完整请求体逐字段锁定(thinking 档位、工具、缓存断点、 text-only 双形态),走真实 pi-ai stream() 与全部 payload 中间件, onPayload 链尾截获后中断,零网络。 - transport-golden:prepareProviderRequest 完整输出快照(反代 URL、 全量头集、base64 覆盖包解码断言、鉴权头排除、full URL 模式、 useSystemProxy 开关),并锁定 failover 逐候选传输配置独立性 (主选走代理+备选直连互不泄漏,双向拓扑)。 零生产代码改动。
将 streamByApi.ts 的五协议 switch 原样搬移为 service/ 下的双适配器 (piAiAdapter 承接 4 条 pi-ai 协议,deepSeekAdapter 承接 deepseek 原生), 经 api→adapter 注册表分发;新增统一流式入口 llm.stream(),携带仅 dev 构建生效的请求信封冻结与一次性分发不变量。streamByApi.ts 收缩为保留 原签名的兼容壳(分发针孔),agentRunner 与 textOnlyRuntime 共 5 处调用 换用统一入口。行为严格等价:PR-0 两个 golden 套件(13 用例)零修改 通过;新增 seam 单测 8 用例覆盖注册表分发、错误文案逐字等价、dev 冻结开关与双入口 wire payload 等价。零 Rust 改动、零镜像文件改动。
将流内重试策略的归属权从全局常量反转到供应商配置(PR-2,stack 第 3 层): - 共享真源 agent-ui settings 新增 CustomProvider.retryPolicy?(off | custom+maxRetries),maxRetries 为首次失败后的重试次数(不含首次请求, 钳位 1..10)。normalizeProviderRetryPolicy 保证非法/缺省一律落 default 且不落字段,旧配置零迁移;default 态在持久层不存在。 - agent-gui 运行时经 ProviderRuntimeConfig 唯一构造点透传策略,新增 resolveStreamRetryConfig 把策略解析为 withStreamRetry 选项(off → disabled,custom → maxAttempts=maxRetries+1,缺省 → 空对象落全局默认)。 agentRunner 与 textOnlyRuntime 两个 streamRetry 注入点展开合并,回调 语义与 buffer-until-commit 不变;failover 逐候选使用各自 runtime 的 策略。streamRetry.ts 与协议适配器零改动, DEFAULT_STREAM_RETRY_MAX_ATTEMPTS 降级为未配置时的默认值。 - 共享 settings UI(ProviderModal/ProviderModalView)请求面板新增 流式重试三态控件(默认/关闭/自定义次数),GUI 与 WebUI 镜像同源; UI 展示镜像常量 PROVIDER_RETRY_DEFAULT_MAX_RETRIES 与运行时真源的 一致性由单测锁定。 - 新增 provider-retry-policy.test.mjs(13 用例):归一化矩阵、构造点 透传、消费方合并语义三种 mode、failover 候选策略独立、口径换算。 PR-0 golden 两套件与既有 stream-retry 套件零修改通过。
把 payload 中间件的组织权反转到 LlmService seam(PR-3,stack 第 4 层)。 未注册任何自定义拦截器时行为与 PR-2 严格等价: - 新增 service/interceptors.ts 注册表:具名 PayloadInterceptor (name + intercept),usePayloadInterceptor / llm.use() 返回幂等 dispose,同名重复注册抛错;组合链带失效缓存,注册/移除时重建, finalize 热路径(agentRunner 每轮、textOnly 每次调用)零重组开销。 - payloadPipeline.ts 的 10 个中间件原样包装为具名默认拦截器,模块 初始化时一次性安装,顺序与注册化前数组逐项一致(顺序即协议正确性 的一部分,由顺序快照测试锁定)。payload-debug-logging 钉住链尾: 自定义拦截器插入默认之后、链尾之前,自定义改动仍被调试日志观测。 - finalizeProviderStreamOptions 改为从注册表组合执行;agentRunner 与 textOnlyRuntime 两处调用零改动,composePayloadMiddlewares 等 既有导出保留,10 个中间件实现文件零改动。 - 新增 llm-interceptors.test.mjs(8 用例):默认顺序快照(10 个 名字)、llm.use 同源、params 可见与 options 变换、插入位置、链尾 观测不变量、dispose 幂等、同名抛错、与旧数组组合逐字段等价(多 形态参数矩阵)。golden 两套件、seam、retry-policy、stream-retry 及中间件相关回归零修改通过。
seam 改造第五层(PR-4):流内重试与跨供应商 failover 此前只喂 UI 临时状态, 审计线索随进程消失。本层把三类事实落入轨迹账本,重启后仍可回放: - 线格式新增 failover(from/to/ti/err)与 transport(p/o/sp/fu/hn)事件; retry 增补 p(候选标签)与真实退避时长——failover 下各候选的重试可区分 - transport 快照只采头名与路由标记,逐候选独立记录,审计"主选带 use-system-proxy、备选不带"的传输装配独立性;头值一律不采集 - recorder 全部 err 出口接入密钥洗涤(URL query key/Bearer/已知 key 形状), 供应商报错回显的凭据不落盘;测试含反向断言 - withStreamRetry 退避提前计算并经 onRetry 上报整毫秒值——上报的就是实际 要睡的值;取整同时关闭浮点往返身份漂移(serde_json ULP 误差会让同一条 重试在收敛账本里出现两份) - agent 模式 failover 首次落账(此前仅 onToolStatus);text 模式 failover 从误记 noteRetry 改为独立 failover 事件,fromLabel/toLabel/targetIndex 不再丢失 - OptionsTab/OverviewTab 渲染三类新行,中英文案;旧读端对未知事件种类 按既有收敛路径静默忽略,线格式只增不改
…tors, trajectory-events)
1. style-to-js 是纯 CJS 包(无 exports 字段),与 rolldown 生产构建的 interop 判断不兼容,通过 pnpm.overrides 固定到 2.0.2 规避。 2. SkillCategoryControls.tsx 中 STORE_CATEGORY_OPTIONS 在模块顶层对 跨 chunk 导入的 CLAWHUB_CATEGORY_SLUGS 做 spread 展开,在激进代码 分割(vite.config.ts maxSize: 450_000)下偶发早于目标 chunk 初始化 完成执行,读到 undefined 抛出 TypeError。改为惰性求值+memoization 规避。 两处均只在生产打包后复现,dev 模式因不做代码分割而无法触发,通过 WebView2 CDP 远程调试(--remote-debugging-port)连接安装后的实例, 逐步定位 console 异常与 DOM 渲染状态确认根因。修复后重新构建安装包, CDP 诊断 EXCEPTIONS_COUNT: 0,截图确认界面完整渲染。
|
审核意见(只列最主要的一点): 本 PR 追加的"生产构建修复"与 main 上已合入的 #613 修的是同一个黑屏 bug,方案互相冲突,其中
建议:rebase 到最新 main,移除 其余部分没有发现需要改代码的问题。已独立验证:五协议 switch 与 payloadPipeline 10 个中间件为逐行/逐项等价搬移;线格式"只增不改"对旧读端安全;failover 落账的 targetIndex 映射与逐候选重试策略正确。并用 |
审核意见:前三项问题1. [需要合并前处理] 生产构建修复已被当前
|
Closes #591, Closes #593, Closes #596, Closes #598, Closes #600
概述
本 PR 是 LLM seam 改造 Stack 的合并版本,把原本 5 层 stacked PR(#590 → #594 → #597 → #599 → #601)整合为一个 PR 提交,额外包含一处生产构建下发现的界面渲染 bugfix。原 5 层 PR 保持 Open 状态作为改动记录,本 PR 合并后会一并关闭。
Stack 各层职责(自底向上,每层在上一层的行为等价基线上推进):
llm.stream()与协议适配器注册表,streamByApi.ts收缩为兼容壳每层改动前均以上一层 golden 快照为基线验证行为严格等价,改动仅发生在各自声明的范围内。
追加修复:生产构建安装包界面空白
Stack 完成后,本地重新走生产构建(
tauri build)+ NSIS 打包 + 安装验证时发现:dev 模式运行正常,但打包安装后界面完全空白(AppErrorBoundary捕获渲染错误)。通过 WebView2 原生 CDP 远程调试连接安装后的实例,定位到两处独立问题,均已修复:style-to-jsCJS/ESM interop 不兼容:该依赖是纯 CJS 包(无exports字段),与 rolldown 生产构建的 interop 判断存在冲突,通过pnpm.overrides固定到2.0.2规避SkillCategoryControls.tsx中STORE_CATEGORY_OPTIONS在模块顶层对跨 rolldown chunk 导入的CLAWHUB_CATEGORY_SLUGS做 spread 展开,在项目的激进代码分割配置(vite.config.tsmaxSize: 450_000)下,读取时机偶发早于目标 chunk 完成初始化,触发TypeError: xt is not iterable。改为惰性求值 + memoization 规避两处问题都只在生产打包后复现,dev server 不做同等代码分割因而无法触发;本仓库 CI/既有测试套件也未覆盖"安装后启动"这一路径,因而 5 层 PR 各自的验证均未发现。
验证结果
继承自各层 PR 的自动化验证(详见各原 PR 描述,此处不重复罗列用例明细):
pnpm --dir crates/agent-gui build✅ /test:frontend✅pnpm --dir crates/agent-gateway/web build✅ /test631/631 ✅cargo check --tests✅;全 diff 不含非预期的src-tauri/**改动;git diff --check✅本次追加验证(生产构建 bugfix):
tauri build(release profile)+ NSIS 打包 + 静默安装TypeError两枚(style-to-js interop 报错 → 修复后消失;xt is not iterable→ 定位并修复后消失)EXCEPTIONS_COUNT: 0,console 日志干净(仅 4 条正常 MCP bridge 初始化日志)Page.captureScreenshot截图确认界面完整渲染(侧边栏、工作空间列表、欢迎语、快捷操作卡片、消息输入框),与 dev 模式视觉一致(见下方截图)人工验收结果
继承自各层 PR 在 Windows Tauri 调试客户端的人工验收(详见各原 PR):
补充:本次生产安装包验收,确认修复后界面正常渲染,无 JS 异常。
Screenshots / preview
生产安装包修复后界面(WebView2 CDP
Page.captureScreenshot截图,修复前此处为空白):LLM seam 骨架(原 PR-1,#594):
Golden 基线(原 PR-0,#590):
流式重试三态控件(原 PR-2,#597):
拦截器注册化(原 PR-3,#599):
轨迹账本(原 PR-4,#601):
关联 PR
原 stacked PR:#590、#594、#597、#599、#601(本 PR 合入后关闭,改动历史保留在各自分支)