Skip to content

fix(webui): 批次三 — MCP 行「测试连接」显示真实原因 + 事件通道 unknown 状态设 deadline - #151

Open
stevenjj33 wants to merge 4 commits into
webuifrom
fix/webui-mcp-test-connection
Open

stevenjj33 wants to merge 4 commits into
webuifrom
fix/webui-mcp-test-connection

Conversation

@stevenjj33

@stevenjj33 stevenjj33 commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

基线

webui @ 6a455a2(动手前 git ls-remote 自查),独立分支、独立 PR。第一个提交是 cherry-pick 的 composer 草稿持久化修复(与 #149/#150 同一提交)——webui HEAD 上 composer.spec.mjs drafts 稳定红(e40a63f 回归),不带它浏览器门对任何 PR 都是红的。

本 PR 覆盖批次三两项:BUG-5(D-3 的下一步动作)与 BUG-4b(Q 区连接存活残留之一)。BUG-4a 的评估见文末。

BUG-5:MCP 连不上时界面上没有下一步

插件面板从不调用 testMcpServer——而那是唯一返回真实连接结果的动作(McpConnectionTestResult:{ success, toolCount?, errorCode?, errorMessage? })。listing 行本身也不带任何状态(上游 listConfiguredServers → configuredSummary 只给 name/enabled/transport/description/endpoint/configJson,configJson 写死 "{}"),所以服务端日志里明明有完整原因(例:baidu-map 实为 bmap-cli 未登录、spawnSync ENOENT),用户界面上看不到、也没有任何可操作的补救动作。

修法(只做客户端,不碰上游):每个 MCP 行加「测试连接」,直接驱动 testMcpServer(mutate 会吞结果,所以走 runAction 拿返回),把结果原样显示在行上——失败逐字展示运行时的 errorMessage(只有 errorCode 时退而显示它,两者皆无只显示「连接失败」不编造);成功显示 已连接 · N 个工具;请求抛出显示「测试请求未能发送」,点过之后行永不沉默。纯投影 describeWebuiMcpTestResult 与既有 describeWebuiMcpServerStatus 同住 projection/plugins/mcp-status.ts。configuredSummary 属上游,未动;errorMessage 逐字而非翻译——它本身就是人话。

证伪:摘掉行内测试按钮 → 源码钉桩用例转红(1 failed / 11 passed)、浏览器用例在点击处失败;恢复后全绿。浏览器用例走真实路径(插件 → 管理 → MCP → 测试连接),listing 行用无 status 字段的真实形状——只在带状态的行上能用的修复是演示不是修复。

BUG-4b:事件通道的 unknown 状态没有 deadline

watcher 从未被 accept 时永远停在 unknown,所以页面加载时后端就已不可达仍然什么都不显示:banner 读的是 session stream phase(空闲 = connected),transport 的 250ms 静默重连谁也不告诉。原规则保护的是启动形态(socket 还在打开时不闪「重连中」),代价是把「从未连上」变成了永久状态。

修法(只动 liveness 源,phase 投影未碰):unknown 状态设 10s 宽限期——超时仍未 accept 的 watcher 降级该通道,因为那时诚实的表述是「这条链路起不来」而不是「还在启动」。宽限期后到达的 accept 仍会清除标记;健康 watcher 的计时器被取消;unregister 也取消——计时器永不比它盯的 watcher 活得久。 Node 侧 unref 经窄转型读取(DOM lib 下 setTimeout 返回 number)。

证伪:把聚合规则改回旧的 everHealthy-only 形态 → 三条宽限期用例转红(3 failed / 9 passed),九条既有用例全绿(它们是行为保持的诚实证据);恢复后 12/12。

BUG-4a:不做,附评估(需要协议级改动)

客户端在常驻 socket(watchEvents)上不发应用层心跳,半开连接(无 FIN)发现不了——属实,但不能安全地只在客户端修:服务端心跳是协议级 WebSocket ping()(websocket-server.ts 的 heartbeatTimer),浏览器 WebSocket API 不向 JS 暴露 ping/pong 事件;而 watchEvents 在空闲时没有任何应用层流量(只在有事件时推送)。所以「读超时」方案会在每个空闲会话上误报(把健康当降级),「应用层 ping/pong」需要服务端与客户端一起改 watchEvents 协议——那是协议变更,超出「liveness 源」的修复边界,需要设计签字。建议单独立项,不在本 PR 用会喊狼来了的读超时糊过去。

边界遵守

  • 没动 roadmap 满足度/统计表;没碰设置页;没认领新区;没动 desktop/browser-core/agent-tools provider。
  • BUG-4b 明确走 liveness 源(connection-health.ts),phase 投影函数一字节未动。
  • 没有为 CI 改断言/删用例/skip。
  • 上游 configuredSummary 缺口按任务书写清边界,未越界。

Validation(本地全过)

  • pnpm build:webui
  • 全量单测:369 files / 8094 passed
  • pnpm typecheck:webui + typecheck:webui-full:0 错误
  • pnpm verify:22 gates PASS on darwin
  • pnpm test:webui-browser:98 passed
  • 无新文件,清单幂等

CI 状态

仓库级 Actions 当前被关闭(gh workflow run → HTTP 422: Actions has been disabled for this repository;全仓最后一次成功运行 10-07),本 PR 收不到 check,需管理员恢复后补跑。本地全量证据如上。

e40a63f moved composer-store ownership into the application and made the
shell adopt the composition root's application (A4-s2). The root built
its application without a `composer` dep, so `create-application` fell
back to `createWebuiComposerStore()` — storage-less by design, because
the application layer may not import infrastructure. The shell's own
storage-backed store (`providedApplication?.composer ?? create(...)`)
became dead code on the live path, and with it every draft: typing
still worked and switching sessions still restored the in-memory draft,
but nothing ever reached localStorage, so a reload lost them all.

Verified on webui @ 6a455a2 before this change: the drafts spec failed
deterministically (3 runs, same failure — after reload the composer is
empty and `webui.composer.state.v1` is null even before the reload), and
a probe showed the key was never written. With the injection: 7/7 in
composer.spec.mjs, 97/97 in the full browser suite.

The composition root now injects the same storage adapter the shell
would have built, exactly as the shell already does for `unreadStorage`
when it builds its own application. The unread controller has the same
gap at the same call site (no failing gate covers it); left for a
follow-up rather than widening this fix.
…'s reason

The plugin panel never called testMcpServer — the one action whose answer
is a real connection result — so a server that could not connect had no
way to say why from the UI: the listing rows carry no status at all
(upstream's configuredSummary sends only name/enabled/transport/
description/endpoint/configJson, which is a separate fix), and the reason
sat in the server log.

Each MCP row now drives the probe and shows what came back: the runtime's
own errorMessage verbatim on failure (falling back to the errorCode when
that is all it said, and claiming nothing when it said nothing), and the
tool count on success. The projection lives beside the existing status
derivation so the row stays a renderer.

Falsified: removing the row's test button turns the new source pin red
(1 failed / 11 passed) and the new browser case fails at the click; both
green again with the wiring restored. The browser case drives the real
path (插件 → 管理 → MCP → 测试连接) with the runtime's own result shape.
Q-4b: a watcher that was never accepted stayed unknown forever, so a page
whose backend was already unreachable when it loaded showed nothing at
all — the banner reads the session stream's phase, which stays idle
("connected") between turns, and the transport's 250ms reconnect loop
told nobody. The original rule protected the boot shape (no reconnecting
flash while the socket is still opening) by making "never accepted"
permanent; the fix keeps that protection and adds its deadline: a watcher
still unaccepted after a 10s grace degrades the channel, because at that
point the honest statement is "this link is not coming up".

An acceptance after the grace still clears the flag, a healthy watcher's
timer is cancelled, and an unregister cancels it too — the timer never
outlives the watcher it watches. The phase projection is untouched: this
is the liveness source the boundary names.

Falsified: reverting the aggregation rule to the old everHealthy-only
form turns the three grace cases red (3 failed / 9 passed) while every
pre-existing case stays green.
@stevenjj33 stevenjj33 changed the title fix(webui): MCP 行增加「测试连接」,把运行时的真实失败原因显示出来(批次三 BUG-5) fix(webui): 批次三 — MCP 行「测试连接」显示真实原因 + 事件通道 unknown 状态设 deadline Oct 13, 2026
@stevenjj33 stevenjj33 closed this Oct 14, 2026
@stevenjj33 stevenjj33 reopened this Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant