Skip to content

feat(providers): LLM seam 改造 Stack 合并版——统一流式入口/重试策略/拦截器注册化/轨迹账本 + 生产构建修复 - #605

Open
AlphaCatMeow wants to merge 8 commits into
Stack-Cairn:mainfrom
AlphaCatMeow:feat-llm-full-stack
Open

feat(providers): LLM seam 改造 Stack 合并版——统一流式入口/重试策略/拦截器注册化/轨迹账本 + 生产构建修复#605
AlphaCatMeow wants to merge 8 commits into
Stack-Cairn:mainfrom
AlphaCatMeow:feat-llm-full-stack

Conversation

@AlphaCatMeow

Copy link
Copy Markdown
Contributor

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 各层职责(自底向上,每层在上一层的行为等价基线上推进):

  1. PR-0 golden 基线(原 test(providers): 建立五协议 wire payload 与传输装配 golden 快照基线 #590):五协议 wire payload 与传输装配的快照回归防线,零生产代码改动
  2. PR-1 seam 骨架(原 feat(providers): 引入 LLM seam 骨架——统一入口 llm.stream() 与协议适配器注册表 #594):引入统一入口 llm.stream() 与协议适配器注册表,streamByApi.ts 收缩为兼容壳
  3. PR-2 重试策略(原 feat(providers): 供应商级流式重试策略——default/off/custom 三态反转到设置层 #597):流内重试从全局常量反转为供应商级配置(default/off/custom 三态)
  4. PR-3 拦截器注册化(原 feat(providers): payload 拦截器注册化——finalize 管线反转到 seam 注册表 #599):payload 中间件组织权反转到 seam 注册表,支持具名拦截器动态插拔
  5. PR-4 轨迹账本(原 feat(trajectory): 重试/切换/传输事件写入轨迹账本——LLM 可观测性补齐 #601):重试/failover/传输三类运行时事实写入轨迹账本,落盘可回放,脱敏后不泄漏凭据

每层改动前均以上一层 golden 快照为基线验证行为严格等价,改动仅发生在各自声明的范围内。

追加修复:生产构建安装包界面空白

Stack 完成后,本地重新走生产构建(tauri build)+ NSIS 打包 + 安装验证时发现:dev 模式运行正常,但打包安装后界面完全空白(AppErrorBoundary 捕获渲染错误)。通过 WebView2 原生 CDP 远程调试连接安装后的实例,定位到两处独立问题,均已修复:

  1. style-to-js CJS/ESM interop 不兼容:该依赖是纯 CJS 包(无 exports 字段),与 rolldown 生产构建的 interop 判断存在冲突,通过 pnpm.overrides 固定到 2.0.2 规避
  2. 跨 chunk 模块顶层求值时序问题SkillCategoryControls.tsxSTORE_CATEGORY_OPTIONS 在模块顶层对跨 rolldown chunk 导入的 CLAWHUB_CATEGORY_SLUGS 做 spread 展开,在项目的激进代码分割配置(vite.config.ts maxSize: 450_000)下,读取时机偶发早于目标 chunk 完成初始化,触发 TypeError: xt is not iterable。改为惰性求值 + memoization 规避

两处问题都只在生产打包后复现,dev server 不做同等代码分割因而无法触发;本仓库 CI/既有测试套件也未覆盖"安装后启动"这一路径,因而 5 层 PR 各自的验证均未发现。

验证结果

继承自各层 PR 的自动化验证(详见各原 PR 描述,此处不重复罗列用例明细):

  • PR-0 golden 两套件 13/13
  • PR-1 seam 套件 8/8
  • PR-2 retry-policy 套件 13/13
  • PR-3 interceptors 套件 8/8(含与旧数组组合的逐字段行为等价断言)
  • PR-4 scrub-transport 套件 14/14 + recorder/event-log/stream-retry 增补 12 用例
  • 全链路累计 127/127 回归通过(golden + seam + retry-policy + interceptors + failover/text-only-failover)
  • pnpm --dir crates/agent-gui build ✅ / test:frontend
  • pnpm --dir crates/agent-gateway/web build ✅ / test 631/631 ✅
  • cargo check --tests ✅;全 diff 不含非预期的 src-tauri/** 改动;git diff --check

本次追加验证(生产构建 bugfix):

  • 重新完整执行 tauri build(release profile)+ NSIS 打包 + 静默安装
  • WebView2 CDP 远程调试连接安装后实例:修复前 TypeError 两枚(style-to-js interop 报错 → 修复后消失;xt is not iterable → 定位并修复后消失)
  • 最终诊断:EXCEPTIONS_COUNT: 0,console 日志干净(仅 4 条正常 MCP bridge 初始化日志)
  • Page.captureScreenshot 截图确认界面完整渲染(侧边栏、工作空间列表、欢迎语、快捷操作卡片、消息输入框),与 dev 模式视觉一致(见下方截图)

人工验收结果

继承自各层 PR 在 Windows Tauri 调试客户端的人工验收(详见各原 PR):

  • 带工具多轮对话、会话标题生成、手动压缩、断网重试提示、failover 切换 ✅
  • 流式重试三态控件行为与默认态兼容性 ✅
  • 附件发送、联网搜索、thinking 档位、拦截器链尾观测不变量 ✅
  • agent/text 两种模式的 failover 落账、传输快照逐候选独立性、重启持久化回放、脱敏(错误文本无真实 key 片段)✅

补充:本次生产安装包验收,确认修复后界面正常渲染,无 JS 异常。

Screenshots / preview

生产安装包修复后界面(WebView2 CDP Page.captureScreenshot 截图,修复前此处为空白):

production-build-fixed

LLM seam 骨架(原 PR-1,#594):

seam-core

Golden 基线(原 PR-0,#590):

golden-baseline

流式重试三态控件(原 PR-2,#597):

retry-policy-1 retry-policy-2 retry-policy-3

拦截器注册化(原 PR-3,#599):

interceptors-1 interceptors-2

轨迹账本(原 PR-4,#601):

trajectory-failover

trajectory-retries

关联 PR

原 stacked PR:#590#594#597#599#601(本 PR 合入后关闭,改动历史保留在各自分支)

为 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 渲染三类新行,中英文案;旧读端对未知事件种类
  按既有收敛路径静默忽略,线格式只增不改
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,截图确认界面完整渲染。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment