Files
deepseek-harness/missions/tasks/20260720-0300-web-dev-2-onboarding/audit.md
2026-07-22 22:18:13 +08:00

28 KiB
Raw Blame History

GUI 代码审计(web-dev-2,2026-07-20)

五维度:①严谨 ②面向对象 ③可扩展可维护 ④资源生命周期 ⑤一致性。只审不改;file:line 均已盘上核实(comment-sweep 在途,行号以本次审计时点为准)。与设计文档冲突的标 doc-mismatch。severity:must-fix > should-fix > nice。

批 1:packages/host/apiproxy(api/ + fetch/)

# 问题 所在 为什么是问题 建议改法 severity
A1 stream/error 帧全仓零生产者,但载体注释声称靠它收敛 契约声明 api/events.ts:34,42;handler.ts:69-70 的 catch 注释「Mid-stream failures converge via the stream/error frame」;消费侧 connection.ts:99、session.ts:214 均防御它 grep 全仓(runtime impl、fixture)无任何 type:'stream/error' 构造点:impl 流中途 throw 时 sseResponse 的空 catch 静默吞掉并正常 close 流——client 只见「流正常结束」,触发重连循环但永远不知道 host 侧出错。注释描述了一个不存在的机制,属「空 catch 未如实说明吞掉什么」 sseResponse 的 catch 里真发一帧 stream/error(错误折成 RpcError internal)再 close;或改注释如实声明「v1 静默断流=重连语义」并在 impl TODO 登记 must-fix
A2 S→C 方向 zod 校验缺位:帧与 Value schema 全部零消费者(doc-mismatch:契约 v2.0 §0-5「zod 双向校验,C→S 命令、S→C 事件都 parse」) client.ts:159 JSON.parse(data) as ServerRequest(帧无任何 schema parse);client.ts:128 full.result as ...(result.value 无 Value schema parse);muxFrameSchema/hostFrameSchema(events.schema.ts:22,32)与 6 个 *ValueSchema 在 src 内零 import 契约总则第 5 条只兑现了一半:C→S 两级 parse 齐全,S→C 只 parse 了 ServerResponse 信封层。坏帧(host bug/版本漂移)会以任意形状直插 fold/store,错误暴露点远离源头;同时 6+2 个 schema 成了「写了没人用」的死代码,维护者会误以为有校验 client readSse 对 payload 过 muxFrame/hostFrame schema(可 dev-only 开关,契约 §0-5 已预留「开关是实现细节」);callUnary 按 method 分派 ValueSchema parse result.value;或明确拍板砍掉 S→C 校验并删 8 个死 schema + 改契约文 must-fix
A3 UNARY_ROUTES 类型面失锁:Record<string, UnaryRoute> 擦掉了 key 与 schema/invoke 的关联 handler.ts:25-37(interface UnaryRoute + Record<string,...>),:139 payload.data as never ①漏行不报错:RpcMethodMap 加了 key 而 UNARY_ROUTES 忘加,编译期静默、运行时 404(违反「两处必须同步改要有编译期锁」);②schema 与 invoke 的 payload 类型互不约束——放错 schema(如 prompt 行贴 cancel 的 schema)照样编译过,as never 把最后一道检查也关了 改成 mapped type:const UNARY_ROUTES: { [K in keyof RpcMethodMap]: { schema: z.ZodType<RequestPayload<K>>; invoke(api, r: RpcRequest<RequestPayload<K>>): ... } }——key 覆盖性与 per-key payload 类型双锁,as never 可删 should-fix
A4 RpcId('') 违反本包自己的 min(1) 校验 handler.ts:128(信封 parse 失败时回 errorResponse(RpcId(''), ...));rpc.schema.ts:26 rpcIdSchema = z.string().min(1) 这条 wire 上的 ServerResponse 过不了本包 serverResponseSchema——本客户端 callUnary:125 会 parse throw,把「服务端明明白白告诉你 bad-request」变成 client 侧 ZodError 异常(错误通道降级);任何按契约实现的第三方 client 同样拒收。自家 wire 形自相矛盾 定一个哨兵形并让 schema 接受(如 rpcId 允许空串仅限 error response——不佳),更干净的是:信封 parse 失败时若 body 里能捞到 string rpcId 就回填原值,捞不到用固定哨兵 RpcId('invalid-request') 且 schema 不设 min(1)(min(1) 挡不住真攻击,只造成自伤) should-fix
A5 askUserQuestionItemSchema 手抄 core 形状且无 satisfies 锚定(一致性:全包唯一没锚的业务 schema) events.schema.ts:14-20 同文件其他 schema 或 satisfies z.ZodType<Wire<T>> 或显式 cast+注释;此处是 plain z.object,core AskUserQuestionItem 加字段时静默漂移(编译不报),恰恰是锚定纪律要防的 补 satisfies z.ZodType<Wire<AskUserQuestionItem>>(type-only import 已有先例);对不上就说明手抄已漂移 should-fix
A6 流方法的 request payload 上不了 wire:since 签名承诺无载体通道(doc-mismatch,笔记 §7-4 并入) 契约 events.ts:20(mux(request: RpcRequest<{since?}>, …));client.ts:132,137(_payload 直接丢弃);handler.ts:98,101(GET 无 body,服务端自 mint rpcId+空 payload 调 impl) 「签名留座 v1 不实现」是拍板,但载体层连承载方式都不存在:将来实现续传时不是填 impl 就完事,要先动载体协议(query 编码或升 POST)。此外 client 侧 mux 调用在 wire 上没有 client-request 象限痕迹——四象限模型在流打开这一步是残缺的,RPC 面板台账里看不到「谁发起了流」 在契约 design §6 不做清单里补一行「since 载体通道未定(query vs POST)」;或现在就定 query 编码(?since=...)让签名与载体对齐,实现仍可后置 should-fix
A7 双 POST 路径重复:callUnary 与 respond 各写一遍「POST+headers+timeout+!ok throw」 client.ts:118-124 与 190-196 同构代码两份,改超时/加 header(将来鉴权)要记得改两处——无编译期锁的隐式耦合 抽 protected postJson(path, body): Promise<Response>(协议不变量仍在基类,子类切面不受影响) nice
A8 serverResponseSchema.parse(...) as ServerResponse 的 as 冗余 client.ts:125 schema 已是 z.ZodType<ServerResponse>,parse 返回类型就是 ServerResponse;多余 cast 弱化「本仓 as 必须有理由」的信号密度 删 as(typecheck 可证) nice
A9 SSE 解析对坏帧零容忍:单帧 JSON.parse throw 杀整条流 client.ts:159(在 for-await 体内,throw 逃逸出 generator) 一帧损坏(代理截断/编码问题)→ 整条流断 → 全量重连重拉(成本被放大 N 倍)。与 fold 侧「未知事件 documented-default 跳过」的宽容哲学不同构 try/catch 单帧:坏帧 console.error+跳过(配 A2 的 schema parse 一起做) nice
A10 unary 调用无外部取消通道:AbortSignal 只有内部 timeout client.ts:122,194(AbortSignal.timeout(...) 独占 signal 位);IApiClient 各 unary 签名无 signal 参数 组件卸载/切换 session 后在途 history/prompt 无法取消,只能等 30s 超时或响应到达(Session 层靠状态机挡住了 UI 影响,但请求本身白跑)。契约 §0 有「AbortSignal 不进 input、独立第二参」的先例,流方法有 unary 没有——不对称 IApiClient unary 加可选第二参 signal?: AbortSignal,与 timeout 用 AbortSignal.any 合并;非急(v1 请求都幂等且轻) nice

批 1 小结:契约类型体系(四象限/brand/错误 map/派生泛型)质量高,问题集中在载体层兑现度——S→C 校验缺位(A2)与 stream/error 空头支票(A1)是同一主题:「wire 进来的东西被无条件信任,出错路径没人真走过」。fixture 全绿掩盖了这类问题(假载体不产坏帧)。

批 2:packages/host/runtime + webserver + apps/dsc

# 问题 所在 为什么是问题 建议改法 severity
R1 webserver 请求回调是无兜底 async——一个畸形 URL 就能打崩整个 dsc web 进程 webserver/src/index.ts:45(createServer(async (req,res)=>{...} 无 try);三条可达 throw 路径::56 decodeURIComponent(rawPath)(curl 'http://host/%' → URIError)、:87 for await (const chunk of req)(client 半途断 body → 迭代 throw)、static.ts:44-46(catch 分支里 readFile(distIndex)——dist 运行期被删则二次 throw) async 回调 reject = unhandledRejection,Node ≥15 默认进程退出。任何外部请求可 5 字节触发(无需恶意构造);SSE 全断、host 陪葬。step1 验收测过「403 编码变体」但都是合法编码,畸形序列没人走过 createServer 回调包顶层 try/catch:catch 里 res.writeHead(400).end()(headersSent 则 destroy socket)+ console.error。一处兜底覆盖三条路径 must-fix
R2 SSE 链路两跳皆无背压、无上限缓冲 ① FrameQueue.buffer 无界(runtime/src/api-proxy.ts:59-90,push 只进不看长度)② sseResponse enqueue 不看 controller.desiredSize(apiproxy fetch/handler.ts:57-77)③ bridge res.write(chunk) 忽略返回 false 不等 drain(webserver/src/index.ts:100) 慢客户端/挂起的 tab 收不动时,事件继续全速生产:内存增长 ∝ 事件速率 × 挂起时长(chunk 风暴下很快)。三层各自「转发就完事」,没有任何一层拥有「消费者跟不上」的决策 GUI 期单机可先记台账;正式修法:FrameQueue 设上限(超限断流→client 走「重连+重拉」既有恢复路径,语义已免费);bridge 尊重 write 返回值 await drain should-fix
R3 agentFor 把一切 resume 失败抹成 undefined → 误报 session-not-found runtime/src/api-proxy.ts:128,138(resume.catch(() => undefined))→ :167,:176 统一回 session-not-found 持久化文件损坏、boot 配置错、并发 dispose——全部伪装成「session 不存在」,诊断被引向完全错误的方向。空 catch 家规要求「名其所吞」,这里吞了整个错误族且换了罪名 agentFor 返回 Agent | RpcError(或 throw 分型):真 not-found(store 无此 id)与 resume-failed(internal + reason 透传)分开回 should-fix
R4 impl stub 现状台账(非漂移,TODO 已标注;列入供 TOP 排期权衡) runtime/src/api-proxy.ts:256-259 respond 恒 not-pending、审批/问答帧零发射(pending registry 未建);:144 list 不 merge 持久化目录(冷 session 列表不可见——真 host 首屏空列表的直接原因);:203 describe.version 占位 '0.0.1' web 侧 PendingCard/pendingBuffers/subscribed 重放消费端全部就位空等;列表缺冷 session 让「打开历史会话」这个主场景在真 host 上走不通(只能靠新建) 按 TODO(step2) 排期:冷 session merge(readdir+stat)优先级最高(解锁主场景),pending registry 次之 should-fix
R5 dsc web 打印 URL 与实际监听面不符 webserver listen 0.0.0.0(index.ts:67);web.ts:67 打印 http://127.0.0.1:${port} 本项目明确场景是远程容器+局域网浏览器访问(0.0.0.0 就是为此拍板的),打印 127.0.0.1 误导使用者复制即用 打印行补一句实际绑定面(或列出首个非环回地址);纯壳层文案改动 nice
R6 err() 帮助函数条件类型是无效噪音 runtime/src/api-proxy.ts:54(RpcResult<T> & {ok:false} extends never ? never : Extract<...> ——前半永假,等价于直接 Extract<RpcResult<T>,{ok:false}>['error']) 读者要花两遍才确认它不做任何事;违背「每个 as/复杂类型要有理由」的信号密度 简化为 error: RpcError 或 Extract 直写 nice
R7 createApiProxy 是 260 行闭包工厂,状态(resumes 表、将来的 pending 表)散在闭包变量 runtime/src/api-proxy.ts:120-261 现在可接受;但 pending registry(respond 实装)落地时闭包会再膨胀一截,私有状态无 class 字段可见性管束。仓库同层对象(Session/SessionManager/AbstractApiClient)均已 class 化——OO 一致性 respond 实装时顺势升 class(HostApiProxy implements ApiProxy),resumes/pending 成 private 字段;本轮不动 nice
R8 FrameQueue 将是 pending registry 的公共依赖但目前 module-private runtime/src/api-proxy.ts:59-90 respond 实装要在 waterfall answerer 里等 client-response,大概率复用同款队列/信号原语;到时复制一份就是克隆债 实装 pending registry 时抽到独立文件导出;本轮记录即可 nice

批 2 小结:拆包结构(apiproxy 前置/runtime 装配/webserver 承载/dsc 拼装)边界干净、依赖方向纪律执行到位;风险集中在进程级健壮性(R1 一击致崩)与错误通道保真度(R3 与批 1 A1 同主题:错误在传播链上被静默降级)。R4 的冷 session merge 是功能面最痛的一条。

批 3:packages/client/web-runtime — session/ 对象层

# 问题 所在 为什么是问题 建议改法 severity
S1 liveBuffer 缝合是死循环:open 期间到达的 live 事件被永久卡在缓冲里(doc-mismatch:§D.3-3「历史就绪后按 seq 合并」实际未发生) session.ts:282-291(installWindow 经 acceptLiveEvent 回放 buffered)× :294-297(acceptLiveEvent 首行 openState==='loading' 时又推回 liveBuffer);而 doOpen 里 installWindow(:264,:269)先于 openState='open'(:271)执行 回放循环把每条 buffered 事件原样塞回新 liveBuffer,之后无任何代码再排空它(唯一另一处 drain 是 resync 直接丢弃 :175)→ open 窗口期的 live 事件丢失到下次重连;且后续 live append 因中间缺段产生 seq 洞 → SurfaceManager 连续性断言 throw → fold 静默降级(degraded 线性扫描 + padded[seq] 错位跳节点)。fixture 的 history 同步返回、窗口≈0,验收测不到;真 host 慢 history + 正在流式的 session 必现 installWindow 在 stitch 前先置 openState='open'(或绕开 acceptLiveEvent 用带 seq 过滤的直接 append 路径);补一个「open 期间来帧」的 fixture 时序用例 must-fix
S2 cancel/abort 后 partial 与 runningCalls 变僵尸:无 turn/end 清理 session.ts:310-337(applyEventSideEffects 只在 assistant/message 清 partial、tool/result 清 openCalls,无 turn/end case——grep 全文件无 turn/end);core 已核实:abort 路径不补发 assistant/message(agent-loop/loop.ts:630-636 流循环 signal.aborted 即 throw;:420-435 error 分支 closeStep 后直接 break,recordAssistantMessage 不执行),但 turn/end{aborted} 一定会发(loop.ts:221) 真 host 上每按一次「停止」:脉冲 partial 永驻、执行中工具卡永转,直到下次重连才被 resync 冲掉。fixture 掩蔽(fixture 的 finish(aborted) 会补「(已中断)」assistant/message 清掉 partial);verify-session-real 没测 stop——两级验收都测不到 applyEventSideEffects 加 turn/end case:清同 turn 的 partial 与 openCalls(清理钩子 core 已白送);verify-session-real 补 stop 用例 must-fix
S3 live append 不校验 seq 连续性,洞靠 fold throw 兜底而非主动补拉 session.ts:298-307(acceptLiveEvent 只做 seq<=tail 丢重叠,seq>tail+1 的洞照 push);重连后 resync 前的窗口(connection 泵开流即下发帧,manager.handleConnected 的 resync 在 describe 成功后才跑)必然产生洞 洞进 padded 数组后 SurfaceManager throw→degraded=true,直到下次 reset 才恢复——用「异常+降级视图」处理一个可预期的时序,而非用既有的缝检测语义(subscribedLastSeq 已在手 :200)主动修复 acceptLiveEvent 检 seq !== tail+1 时不 push:进 liveBuffer 并触发一次 resync-lite(重拉尾页缝合)——与 S1 修法同一套路径 should-fix
S4 resync 复用在途 openPromise:重连后可能定格在断连前的失败结果 session.ts:167-178(resync 置 cold 后调 open())× :120-127(open() 见 openPromise !== null 直接返回旧 promise)——断连时在途的 doOpen 其 history 请求已死,将以 transport error 收场并把 openState 写成 'error';resync 等到的就是这个旧结果,不再重拉 重连本该「重建」,却可能以 error 态收场且无自动重试(UI 无 retry 按钮,只能重新点选 session)。触发窗口=初次 open 与断线重叠,越慢的网越容易 resync 引入 generation/取消语义:作废在途 openPromise(记 generation,doOpen 收尾时 generation 不符则丢弃写入),或 resync 处 openPromise=null 强起新一轮 should-fix
S5 快照子结构引用稳定性承诺整体未兑现,memo 全灭(doc-mismatch:§A.9.4「pending/runningCalls 未变沿用旧引用」、§C.2「未变条目引用稳定」) session.ts:354-372(buildSnapshot 每次 [...openCalls.values()]、[...pending.values()] 新数组新对象);manager.ts:191-193 + lineage.ts:44(每次 rebuild 全量 {...s, depth} 新条目);连带 ConversationView 内联 call={{...}}/result={{...}} 新对象 chunk 风暴下每微任务批一次 rebuild:所有 SessionListItem、所有 ToolCallCard、PendingCard 的 React.memo 因 props 引用恒变而 100% miss——设计立了 memo 边界(§C.2/§C.4 明文),实现把前提拆了。列表小时无感,长会话+流式期是主要重渲成本 按设计的 revision 计数器方案:各子结构维护版本号,rebuild 时版本未变即复用旧数组/旧条目引用;View 层内联对象改由快照直供稳定引用 should-fix
S6 padding 哨兵盗用真实事件类型 todo/write(doc-mismatch:设计写 noop/padding,笔记 §7-6) fold-adapter.ts:22-25({type:'todo/write', data:{todos:[]}} as SessionEvent) 与设计字面不符事小;用真类型+伪数据事大:将来任何人给 fold/调试面加 todo/write 处理,万级哨兵就会现形为垃圾节点。反正非 surface-eligible 的任意字符串都被同样跳过,没有理由选真类型 换 'noop/padding'(效果等价、语义自明),cast 保留一处并注释 nice
S7 pendingBuffers 无清理路径:session-removed 不删、未实例化 session 的缓冲无上限 manager.ts:24,130-141(只增);:163-167(removed 分支不触 pendingBuffers) 长跑页面的慢泄漏;且 removed session 的僵尸审批帧会在其将来意外实例化时回放出无意义卡片 session-removed 时 pendingBuffers.delete(id);缓冲加上限(每 session 几十条足够,审批帧低频) nice
S8 F.6 预埋要求未落:Session 无 dispose() no-op 预留 session.ts 全文(grep dispose 无果);design §F.6 预埋列明「新增 dispose() 预留为 no-op 方法」 台账逐条预埋要求里唯一没兑现的一条(其余 F.2/F.7/F.10/F.11 抽查均落实);将来上逐出策略时无收口点 补 no-op dispose() + 注释指 F.6 nice

批 3 小结:对象层骨架(Notifier 合批/懒 build、FoldAdapter 降级收口、draft 在途锁+乐观清空)质量高于平均;但 S1/S2 是两条实锤运行期 bug,共性是「fixture 时序太理想 + 真 host 验收没覆盖 stop/慢 history」——修复时应连带补验收用例,否则会复发。S3/S4/S5 都是「设计承诺了、实现只落一半」:缝检测、重连重建、引用稳定,建议作为一个「对齐 §D.3/§A.9 设计语义」的批次一起修。

批 4:web-runtime 其余(connection/boot/store/rpc-log/intents/fixture)+ web-ui hooks/容器

# 问题 所在 为什么是问题 建议改法 severity
C1 连接状态对 UI 不可见:断线/重连中用户零感知 connection.ts 全文(generation/attempt 实例私有,无任何对外读口);store.ts(无 connection 切片);conversation.ts ConversationSnapshot(无连接位);唯一出口是 console.warn(connection.ts:86) 断线期间界面完全正常——列表还在、输入框可打字,prompt 发出后要么 30s 超时要么 transport error,用户以为是「卡了」。「连接状态」正是 zustand 应承载的跨视图全局展示态(store 红线管的是业务对象,不是它);契约 design §5 图里也画了「连接状态」进 store ConnectionController 加 onStateChange?(state: 'connected'|'reconnecting') sink,boot 接入 store 新 connection 切片,UI 顶部细条即可。severity 按用户体验定 should should-fix
C2 describe 与流打开之间无就绪握手,onConnected 可能早于 subscribed 帧 connection.ts:74-77(describe 是独立 unary,与两条 SSE 并发;成功即 onConnected)→ manager.ts:186-189(立刻 refreshList+resync)→ session resync 的 doOpen 里 subscribedLastSeq 大概率仍是 null(session.ts:265 缝检测直接跳过) describe 只证明「unary 通」,不证明流已建立(GET SSE 握手更慢是常态);resync 抢跑 → 缝检测这一层保护在每次重连后的关键窗口恰好失效,回到纯 liveBuffer 去重路径(S1 修好后此路径才成立,但基线语义仍缺)。设计 §A.1「两条流开启且 describe 成功之后」的"两条流开启"没有实现对应物 pumpStream 收到首帧/首字节时 resolve 一个 opened promise,await Promise.all([mux开启, host开启, describe]) 再 callSink(onConnected);SSE 开流即有 : connected 注释行(handler 已发),client 侧可感知 should-fix
C3 useConversation/useSessionList 的操作句柄引用每快照重建,破坏纯 props 组件的 memo 潜力 useConversation.ts:26(外层 useMemo(..., [snapshot, ops])——handle 对象每 snapshot 换新,虽然 ops 稳定,但 InputBar 等收到的 onSend/onStop 经容器再包一层后引用仍随渲染变);SessionListContainer.tsx:13-16(useCallback(..., [h.create, onSelect]) 依赖 h.create——恰好稳定,但 onCreate 又内联 () => void create() 每渲染新引用) 与 S5 同主题的 UI 侧一半:设计承诺「操作句柄引用稳定」(§B.1),实现里句柄本体稳定但沿途每层都再包新箭头,到叶子组件时引用已不稳。memo 生效前提被层层削弱 容器直传稳定引用(onCreate={create} 不再包箭头);handle 拆成 {snapshot} 与恒定 ops 两个返回位或直接返回稳定 ops 对象 nice
C4 rpc-log 的 inflightMethods 表只删成功对,孤儿条目永不清 rpc-log.ts:19,32-36(client-request set;仅 server-response get+delete) 超时/传输失败的请求没有 server-response(transport error 是 throw 不是消息)→ 表条目永驻。量级小(每失败请求一条 string 对),但它是模块级 Map,与「批量泵收进实例」纠偏走反方向——AbstractApiClient 实例可多个(测试/多 boot),模块表跨实例串味 表挂进 ingest 闭包或加容量上限(LRU 几百条);顺手把 nextId 一起收进去(display-local 的辩解成立,但同一动作可一起收) nice
C5 fixture 的 host 流泵每轮循环叠加一个 abort listener fixture.ts:288-291(while 循环体内每次 new Promise 都 signal.addEventListener('abort', ..., {once:true})——once 只保证 fire 一次,不 fire 就常驻;每个帧批一个新 listener)同型 :259-261 长开页面的 fixture 会话数小时后 listener 数千计(MaxListenersExceeded 警告级,非泄漏实害);真实包 FrameQueue(api-proxy.ts:77)就做对了:一次 addEventListener+finally remove——同类问题两包处理不同构(维度⑤) 照 FrameQueue 模式:循环外挂一次 abort listener,wake 机制复用 nice
C6 React 侧无错误边界:单组件 render throw 白屏整个应用 web-ui/src 与 apps/web/src 全 grep 无 ErrorBoundary/onCaughtError;App.tsx 直渲两大区块 对话流渲染的是透传的任意事件数据(unknown 节点/loose ContentBlock),一条意外形状(如 text 块 text 非 string)在叶子组件 throw 即整页白屏——与 runtime 层「sink 隔离/降级视图」的防御纵深不匹配,最后一层没兜 SessionsScreen 与 RpcLog 各包一层 ErrorBoundary(React 18 class 版或 react-error-boundary),fallback 显示错误细条 should-fix
C7 intents 的 api === null 静默 return 与 getSessionManager 的 fail-loud 不同构 intents.ts:38(pingHost 里 if (api === null) return)vs manager.ts:207-209(未 init throw) 同一「boot 前被调」错误,两个出口一个吞一个炸;吞的那个在 bindIntents 漏接线时永远查不到(按钮就是没反应) pingHost 也走 fail-loud(或两者统一经一个 requireApi());一致性小修 nice
C8 boot 可重入但产物互相踩:二次 bootWebRuntime 覆盖单例却不 stop 旧 controller boot.ts:18-30(每次 new 全套;initSessionManager 覆盖单例、旧 ConnectionController 无人 stop,旧泵继续跑且 sink 还指向旧 manager) 现产品单次 boot 无害;但 HMR/测试重复 boot 时旧泵+新泵并行双倍请求,且旧 manager 还在吃帧——排查成本高的隐性坑 boot 记模块级 prev handle,重入先 prev.stop()(或文档明示「仅可调一次」+ dev 断言) nice

批 4 小结:连接层的代际管理(generation+同代收敛 abort+退避)本身写得干净;缺的是两端的可观测性——对上(C1 用户看不见)与对下(C2 流就绪不可感)。web-ui 的 hooks/容器分界执行得好(组件零 store/manager import 抽查属实),遗留问题集中在引用稳定性(C3,与 S5 合修)与最后一层防御(C6)。


改进点 TOP 清单(severity × 影响面排序,供裁决)

排名 条目 一句话 修复归组建议
1 R1 webserver async 回调无兜底 一个 % 畸形 URL 即 unhandledRejection 崩掉整个 dsc web 进程 独立小修(~10 行),立即
2 S1 liveBuffer 缝合死循环 open 期间 live 事件永久滞留缓冲:丢事件+seq 洞+fold 降级,设计 §D.3 缝合从未真正生效 与 S3/S4 组成「打开/重连时序」批
3 S2 无 turn/end 清理 每按一次停止,partial 脉冲与执行中工具卡永驻到下次重连(core 已核实 abort 不补 assistant/message) 独立小修(一个 case),立即;连带 verify-session-real 补 stop 用例
4 A1 stream/error 零生产者 host 侧流中错误被空 catch 吞成「正常断流」,注释声称的收敛机制不存在 与 A2 组成「载体错误通道」批
5 A2 S→C zod 校验缺位 契约「双向校验」只兑现一半,帧/Value schema 共 8 个是死代码;坏帧直插 fold 同上批;或拍板砍单向+删死码
6 R4 冷 session 不进 list 真 host 首屏空列表,「打开历史会话」主场景走不通(TODO 已标注,纯排期问题) impl step2 批之首
7 C2 onConnected 不等流就绪 重连后 resync 抢跑,subscribed 缝检测在最需要它的窗口失效 「打开/重连时序」批(与 S1/S3/S4 同修同验)
8 S5+C3 引用稳定性承诺未兑现 快照子结构+操作句柄层层新引用,全部 React.memo 形同虚设;流式期重渲成本 独立「性能对齐设计」批,可后置到样式/组件重做轮一起
9 C6 无 React 错误边界 透传任意数据的渲染树没有最后一层,单点 throw 白屏 独立小修;组件重做轮也可
10 C1 连接状态不可见 断线用户零感知;connection 切片本就是 store 的正当职责 与 C2 同批或独立小功能
11 A3 UNARY_ROUTES 失锁 map 加方法忘加路由静默 404;mapped type 可双锁 独立小修(类型改写零行为变化)
12 R3 resume 失败伪装 not-found 诊断误导型错误降级;配合 A4(RpcId('') 自相矛盾)一起理错误通道 「载体错误通道」批
13 R2 SSE 无背压 慢消费者内存增长;GUI 期可缓,正式化前必须 转正前批
14 A5/A6/S6/S7/S8/C4/C5/C7/C8/A7-A10/R5-R8 一致性与预埋小项 顺手修/组件重做轮
— doc-mismatch 汇总 A2(双向校验)、A6(since 载体)、S1(§D.3 缝合)、S5(§A.9 引用稳定)、S6(哨兵类型)+ 笔记 §7 的 8 条 转 RFC/文档线(rfc-consolidation)

总评:架构分层(契约/载体/装配/对象层/hook/组件六层)与依赖纪律执行得好,OO 归属经两次纠偏后基本正位(批 4 抽查组件零越界属实);系统性弱点集中在两条线——错误与异常路径的兑现度(A1/A2/R1/R3/S2:正常路径精心设计,出错路径要么吞要么没人走过)和时序竞态(S1/S3/S4/C2:fixture 同步理想时序掩盖了慢网/重连窗口)。建议修复顺序:R1+S2 两个立即小修 → 「打开/重连时序」批(S1/S3/S4/C2)→「载体错误通道」批(A1/A2/A4/R3)→ impl step2(R4 优先)→ 性能与杂项。每批修完跑 verify-session + verify-session-real 并按发现补用例。