fix(multi-server): 服务器作用域请求统一使用当前绑定的 serverId - #169
Conversation
- 修复 usePermissionHandler 的权限与提问回复回调:空依赖数组把 serverId 冻结在首次渲染的值上,切换服务器后回复被发到旧服务器,表现为弹窗消失又复现、对话不前进 - 补齐 useChatSession 中 paneServerId 依赖:SSE 回调、agents 列表、@ 目录与斜杠命令预取、待处理请求刷新、发送消息、fork、中止、命令执行、归档 - 补齐其余服务器作用域单元的 serverId 依赖:useFileExplorer、BottomPanel、RightPanel、SessionChangesPanel、Terminal、ProjectDialog、SessionChildrenSlot - 新增回归测试:断言面板切换服务器后权限与提问回复打到当前服务器;该测试在修复前会失败 - 校验:typecheck、eslint src、vitest --dir src(92 文件 / 596 用例)、build 全部通过
task 工具匹配子/孙 session 的权限与提问卡片时,把消息 metadata 里的原始 session id 交给 splitSessionKey;该函数遇到不带 :: 前缀的 id 会回退到全局 活动服务器。多服务器 / WSL 场景下面板绑定的服务器与活动服务器不是同一台, 孙层请求因此永远匹配不上,卡片点不动。 - InlineToolRequestContext: 新增 TaskChildSessionRef(子 session key + 权威 serverId),context 值补必填 serverId,复合 key 由它合成,不再猜服务器 - ChatPane: 把 paneServerId 发布进 context,并补进 memo 依赖 - ToolPartView / MessageRenderer: 调用点透传 serverId - 新增契约测试:活动服务器为 local、面板绑定 wsl:Ubuntu 时,孙层的权限与 提问必须能命中
该用例挂载完整 ConfigSettings 与配置编辑器弹窗,是 jsdom 下最重的路径之一: 单跑 0.5 秒通过,但全量并发跑会超过 5 秒默认 testTimeout 被误判为失败。 给它单独设 20 秒并写明原因,断言与流程都未改动。
旧实现拿 changedId 与 active 比对,但面板数据主体是 serverId prop(可为 固定绑定):面板绑 WSL 而 active 在 local 时,这台 WSL 换端口重启被误跳过 (终端失联),无关服务器切换反而误触发重恢复。对齐 useVcsInfo 的绑定语义, 统一走共享谓词。依赖数组补 serverId 属 lehhair#169 范围,本提交不动。
lehhair
left a comment
There was a problem hiding this comment.
Review:根因定位准确、修复到位、有红绿验证,Approve
这条修的是 #168(多服务器/WSL 下权限与提问回复打到旧服务器),我按同样流程做了独立复核:拉到本地、把 #164 合并后的 main 与它做实际集成合并、跑全套校验、并对两处核心修复做红绿验证。
结论
可以合并,我没有阻塞意见。 三条提交各司其职:usePermissionHandler 的空依赖(真正的根因)、task 子 session 匹配猜服务器(同根因的另一入口)、以及一条独立的测试稳定性改动。
我独立复验的东西
1. 根因确实成立(红绿验证)
usePermissionHandler.ts 三个回复回调的依赖数组从 [] 改为 [serverId]。我把这三处改回 [] 后重跑,新增的那条契约测试恰好变红,其余用例不受影响;改回即绿:
FAIL usePermissionHandler > routes replies to the server the pane is bound to now,
not the one captured at mount
Tests 1 failed | 2 passed (3)
replyQuestion(..., serverId) / rejectQuestion(..., serverId) 最终都走 getSDKClient(serverId),所以"serverId 冻结 → 请求打到旧服务器"这条因果链成立。
2. 子 session 匹配那条(commit 2)我也做了实证
你的判断(splitSessionKey 遇到不带 :: 的原始 id 会回退到全局活动服务器)我在 src/utils/sessionKey.ts:24-30 核实了,确实如此。为了不只看代码,我写了个临时探针(真实 isChildOf 语义、pane 绑定 wsl:Ubuntu、active 为 local、孙 session 请求):
PROBE correct binding (wsl:Ubuntu): perm-grand ← 能命中
PROBE old behavior (guessed local): NO MATCH ← 旧实现命中不了,这就是 #168 的孙层路径
PROBE cross-server (wsl:Debian): NO MATCH ← 不会跨服务器误命中
PROBE direct callID match: perm-grand ← 直接 callID 匹配路径未被破坏
同样做了红绿:把 makeSessionKey(child.serverId, ...) 换回旧的猜服务器写法,InlineToolRequestContext.test.tsx 两条契约测试双双变红。修复后我按字节还原了文件(git status 干净)。
3. 29 处依赖数组:没有引入新告警,也没有"补错"
这条是我最担心的部分(批量补依赖最容易补出多余重渲染或请求风暴),所以我做了集合对比:把当前 main 与"main + 本 PR"分别在两个 worktree 里跑 eslint -f json,按 相对路径|规则|消息 归一化后取差集:
main(no-169) = 93 warnings
integration(with-169) = 64 warnings
仅出现在 integration、不在 main 的告警 = 空
即没有新增任何告警,减少的正是 29 条 serverId 漏声明。逐点抽看被补的依赖也都是真依赖(如 useFileExplorer 的 getFileContent(path, effectiveDirectory, serverId)、SessionChildrenSlot 的 getSessionChildren(..., serverId)、ProjectDialog 的 initPath),不是机械加参数。
另外我确认了几处故意不补是合理的:resetPendingRequests 不发请求(保持 []);useGlobalEvents 的 serverId 是每订阅一份的入参而非 hook 作用域;被 routeSessionId 守卫的单元里 paneServerId 是其纯函数。这些都和你正文里的说明一致。
4. 集成合并 + 全套校验(main 含 #164 + 本 PR)
| 校验 | 结果 |
|---|---|
git merge |
无冲突(我实际合了,merge-tree 也干净) |
tsc -b |
0 错误 |
eslint . |
0 error(告警见上,且无新增) |
vitest run |
96 文件 / 667 用例全过 |
vite build |
通过 |
顺带更正我之前的说法:我在 #164 的 review 里提醒"#169 直接合会覆盖掉 N2 门控"——那是我没验证就下的结论,实际是错的。我这次真做了合并测试,BottomPanel.tsx 的 affectsBoundServer 门控与 #169 补的 serverId 依赖两者都保留(git 三方合并把两处改动分别应用了)。不需要 rebase。
5. 测试稳定性那条改动(commit 3)
ConfigSettings.search.test.tsx 单独放宽到 20s。我单跑该文件实测 2.47s(含 setup),在当前机器负载下确实容易被 5s 默认值卡住;断言与流程未动,是纯超时参数。这类改动我一般会警惕"用放宽超时掩盖真 bug",但这里的耗时体量(挂载完整 ConfigSettings + 配置编辑器弹窗,jsdom 最重路径)与现象一致,且你把它单独成一个 commit 便于摘除——处理方式合适。
一点非阻塞建议
本 PR 的验证里你提到用 eslint src / vitest --dir src 规避本地 opencode/ 参考目录。这个目录现在只存在于你本地(#164 里那段排除配置已经按评审意见回退掉了),所以不影响 CI;但如果以后还想本地跑全量,建议在自己的全局 eslintignore / 本地未跟踪配置里处理,不要再进仓库。
合并方式
head 84ab01aa 与本 review 一致。我会按仓库规范 merge --no-ff(保留你的原始 commit 让 GitHub 显示 Merged),并按 AGENTS.md 的顺序先合 dev 再快进 main。合完跑一遍合并后的校验并把结果贴出来。
感谢这条独立开 PR 而不是塞进 #164——引入点是 main 里已有的 75a00b64,边界划分是对的。
关于审查方式:本评论由 AI 辅助产出(自动化跑通全套校验、逐行追踪、临时探针实测含红绿验证、worktree 双份 eslint 差集比对),结论经我确认后发布。
起因
我在 Windows 上主力用 WSL 跑 opencode,也在做 WSL 服务器管理那个 PR(#164)。用着用着发现一个很别扭的问题:在 WSL 的对话里,模型一提问(带选项的卡片),卡片能正常显示,但不管我选选项、输入自己的回答还是跳过,都点不动——点完卡片会消失一下(像发送成功了),过一会儿又冒出来,对话也不往前走。权限确认弹窗其实也一样,只是我没第一时间反应过来是同一回事。
排查过程
我让 AI 排查了这个问题,重点是「面板绑定的服务器」这条线:
paneServerId)在面板生命周期里会变——启动时活动服务器是 local,WSL sidecar 就绪后wslStore才把活动服务器切过去;切换会话当然也会变。而面板本身不会重挂(key 只是 paneId)。usePermissionHandler里三个回复回调(权限回复、提问回复、提问跳过)的依赖数组是空的[],serverId被冻在面板首次渲染时的值上。同一个 hook 里的refreshPendingRequests却是[serverId](每次拿新值)——这一新一旧的不对称,正好解释「卡片消失又回来」。renderHook,先local再rerender成wsl:Ubuntu,然后分别调用三个函数,把各自收到的 serverId 打出来):数据出来就对上了:卡片显示走 SSE(按服务器分别订阅,是对的),回复走 HTTP(用的是旧 serverId,发到 local 报错),于是 WSL 那条请求一直 pending,刷新一拉就"复活"。探针脚本用完删掉了。
react-hooks/exhaustive-deps扫全仓:同类漏声明serverId的地方一共 29 条告警(还有 1 处规则看不到但同样会漏:useChatSession的 SSE 回调)。逐个判断了哪些真会把请求打到旧服务器(被routeSessionId守卫的那几处不会,因为那种情况下paneServerId是它的纯函数),然后一起修掉。splitSessionKey,而这个函数碰到不带::前缀的 id 会直接回退到「全局活动服务器」。于是子 agent 再起一层子 session 时,只要活动服务器和面板绑定的服务器不是同一台,那张卡片就永远匹配不上——和前面是同一个根因,只是换了个入口。修法是把权威 serverId 从面板一路传下来,而不是让匹配器去猜。修复内容
usePermissionHandler:三个回复回调的依赖数组[]→[serverId],并在 hook 上写明约定「凡发起请求的回调都必须声明 serverId」useChatSession:补齐paneServerId依赖——SSE 回调、agents 列表、@ 目录与斜杠命令预取、待处理请求刷新、发送消息、fork、中止、执行命令、归档;同时把这条约定写进paneServerId定义处的注释(并标注例外:被routeSessionId守卫的单元)useFileExplorer(4)、BottomPanel(4)、SessionChangesPanel(3)、SessionChildrenSlot(3)、ProjectDialog(2)、RightPanel(1)、Terminal(1)InlineToolRequestContext新增TaskChildSessionRef(子 session key + 面板绑定的权威 serverId),context 值加上必填serverId,复合 key 用它合成,不再走splitSessionKey的「裸 id 回退活动服务器」;ChatPane把paneServerId发布进 context,ToolPartView/MessageRenderer跟着透传local切到wsl:Ubuntu后,回复与跳过必须打到wsl:Ubuntu;活动服务器是local而面板绑定wsl:Ubuntu时,子 agent 下一层会话的权限/提问卡片必须能匹配上。两条都是把修复回退就会红的(前者AssertionError: expected "vi.fn()" to be called with …,后者expected undefined to be { id: 'perm-1', … })验证
src下 0 错误 / 43 警告(与改动前基线一致);该类 serverId 漏声明告警 29 → 0question,点一个选项,看那条 POST 打到哪个端口——修复前是127.0.0.1:4096(local)并报错,修复后是 WSL sidecar 的随机端口且 200。子 agent 那条路径同理:在别的服务器上开会话跑一个会再下一层的 task,看孙层卡片能不能点动ConfigSettings.search.test.tsx挂了完整 ConfigSettings 和配置编辑器弹窗,单跑 0.5 秒,但全量并发跑会超过 5 秒默认testTimeout被误判超时(我这边一加测试文件就复现)。给它单独放宽到 20 秒并写了注释,断言没动。这一步是独立的 commit,跟上面的修复无关,觉得不合适可以只挑第一个小提示:我本机上有个未跟踪的
opencode/参考目录会被eslint .和 vitest 扫到(它只在 WSL 分支的 cfd5ce6 里被排除),所以本地验证用的是eslint src和vitest --dir src;CI 全新 clone 不受影响。说明
这个缺陷的引入点是 main 里已有的多服务器提交(
75a00b64),不是 #164 带来的,WSL 只是最容易触发它的场景(活动服务器是异步切过去的),所以我单独开了这个 PR,而不是塞进 #164。Fixes #168