Files
claw/docs/superpowers/plans/2026-04-03-ws-browser-welcome-frame-plan.md
木炎 bdf8e12246 feat: align browser callback runtime and export flows
Consolidate the browser task runtime around the callback path, add safer artifact opening for Zhihu exports, and cover the new service/browser flows with focused tests and supporting docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 21:44:53 +08:00

363 lines
12 KiB
Markdown

# WS Browser Welcome Frame Compatibility Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the ws service path tolerate the real sgBrowser welcome banner (`Welcome! You are client #...`) without weakening general ws protocol validation or changing pipe behavior.
**Architecture:** Keep the shared `WsBrowserBackend` strict and implement the compatibility shim only in `ServiceBrowserWsClient`, which is already the real-browser adapter for the ws service path. Add one positive red test for the known welcome frame and one negative red test proving non-matching first text frames still fail as protocol errors, then make the minimal stateful change in `src/service/server.rs` and verify ws + pipe regressions.
**Tech Stack:** Rust 2021, tungstenite websocket client/server, existing `WsBrowserBackend`, existing `ServiceBrowserWsClient`, existing Rust unit/integration test suite.
---
## File Structure
### Existing files to modify
- Modify: `src/service/server.rs`
- Add the one-time per-connection welcome-skip state to `ServiceBrowserWsClient`
- Add the minimal helper(s) for detecting and discarding the first known welcome frame
- Add focused service-adapter unit tests in the existing `#[cfg(test)]` module
- Reuse: `src/browser/ws_backend.rs`
- Do not change protocol parsing rules; only verify behavior remains strict for all non-service callers
- Reuse: `tests/service_task_flow_test.rs`
- Re-run to confirm the ws service path still reaches the browser websocket after the service-side shim
- Reuse: `tests/browser_ws_backend_test.rs`
- Re-run to prove the shared backend semantics remain unchanged
### Files deliberately not changed
- `src/browser/ws_backend.rs`
- `src/browser/ws_protocol.rs`
- `src/agent/task_runner.rs`
- `src/compat/runtime.rs`
- `src/compat/orchestration.rs`
- `src/compat/workflow_executor.rs`
- `src/lib.rs`
The design explicitly keeps the welcome-banner workaround out of the shared backend and out of the pipe path.
---
## Task 1: Reproduce the real welcome-frame failure with focused unit tests
**Files:**
- Modify: `src/service/server.rs`
- [ ] **Step 1: Add the positive failing test for the known welcome frame**
In the existing `#[cfg(test)] mod tests` inside `src/service/server.rs`, add one focused test next to the current ws adapter tests.
Test shape:
```rust
#[test]
fn future_server_side_ws_native_adapter_skips_initial_known_welcome_frame() {
// fake server sends:
// 1. "Welcome! You are client #1"
// 2. "0"
// backend.invoke(Action::Navigate, ...) should succeed
}
```
Required assertions:
- the fake websocket server accepts one connection
- it sends the welcome banner first, then the numeric success status
- `WsBrowserBackend.invoke(Action::Navigate, ...)` returns `Ok(CommandOutput { success: true, .. })`
- [ ] **Step 2: Run only the positive new test and watch it fail**
Run:
```bash
cargo test service::server::tests::future_server_side_ws_native_adapter_skips_initial_known_welcome_frame -- --nocapture
```
Expected: FAIL with a protocol error containing `invalid browser status frame: Welcome! You are client #1`.
- [ ] **Step 3: Add the negative failing test for arbitrary first text**
In the same `#[cfg(test)]` module, add one negative test proving we do **not** silently skip arbitrary first text frames.
Test shape:
```rust
#[test]
fn future_server_side_ws_native_adapter_does_not_skip_unknown_first_text_frame() {
// fake server sends:
// 1. "Hello from server"
// assert invoke(...) fails as PipeError::Protocol(...)
}
```
Required assertions:
- the first frame is a non-matching text frame such as `Hello from server`
- `invoke(...)` fails
- the failure remains a protocol error rather than success or timeout
- [ ] **Step 4: Run only the negative new test and verify the current behavior is already strict**
Run:
```bash
cargo test service::server::tests::future_server_side_ws_native_adapter_does_not_skip_unknown_first_text_frame -- --nocapture
```
Expected: PASS, proving the current implementation already treats arbitrary first text as a protocol error. Keep that assertion in place before any production change.
- [ ] **Step 5: Confirm the TDD gate before implementation**
Do not implement production code before both tests exist and the positive test has failed on current behavior.
---
## Task 2: Add the minimal per-connection welcome-skip state in the service adapter
**Files:**
- Modify: `src/service/server.rs`
- [ ] **Step 1: Add one-time per-connection state to `ServiceBrowserWsClient`**
Extend `ServiceBrowserWsClient` with one extra state field that tracks whether the initial welcome candidate has already been consumed for the current websocket connection.
Allowed shape:
```rust
struct ServiceBrowserWsClient {
browser_ws_url: String,
browser_socket: Mutex<Option<WebSocket<MaybeTlsStream<TcpStream>>>>,
initial_text_frame_checked: Mutex<bool>,
}
```
or an equally small equivalent.
Rules:
- state is per connection, not per request
- state must survive multiple `invoke(...)` calls while reusing the same socket
- do not add broader protocol state machines
- [ ] **Step 2: Add a narrow welcome-frame matcher**
In `src/service/server.rs`, add one small helper that recognizes only the known banner prefix:
```rust
fn is_known_welcome_frame(frame: &str) -> bool {
frame.starts_with("Welcome! You are client #")
}
```
Rules:
- no regex needed
- no generic “ignore arbitrary text” logic
- keep the matcher local to `src/service/server.rs`
- [ ] **Step 3: Update `recv_text_timeout(...)` to skip at most one initial known banner**
Modify `impl WsClient for ServiceBrowserWsClient` so that the first text frame received after connection establishment is handled like this:
1. read the next text frame
2. if the initial-frame state is still false:
- mark the first-frame check as consumed
- if the frame matches `is_known_welcome_frame(...)`, read the next frame and return that next frame instead
3. otherwise, return the frame unchanged
Rules:
- skip only once per connection
- do not loop indefinitely over multiple text frames
- do not swallow unknown first text frames
- do not change timeout / close / reset / connect-failure behavior
- [ ] **Step 4: Reset the one-time state when a fresh socket is created**
When `with_socket(...)` establishes a brand-new websocket connection, ensure the one-time banner-check state is reset so a new connection can tolerate its own first welcome frame.
- [ ] **Step 5: Add one reconnect regression in the service adapter tests**
Add one focused test proving the welcome skip resets on a fresh connection after socket close/reset.
Test shape:
```rust
#[test]
fn future_server_side_ws_native_adapter_skips_welcome_again_after_reconnect() {
// first connection closes after use
// second fresh connection sends the same welcome banner again
// both invocations succeed
}
```
Required assertion:
- the one-time skip is per connection, not global for the client instance
- [ ] **Step 6: Run the positive new test**
Run:
```bash
cargo test service::server::tests::future_server_side_ws_native_adapter_skips_initial_known_welcome_frame -- --nocapture
```
Expected: PASS.
- [ ] **Step 7: Run the negative new test**
Run:
```bash
cargo test service::server::tests::future_server_side_ws_native_adapter_does_not_skip_unknown_first_text_frame -- --nocapture
```
Expected: PASS, proving unknown first text is still treated as a protocol error.
- [ ] **Step 8: Run the reconnect regression**
Run:
```bash
cargo test service::server::tests::future_server_side_ws_native_adapter_skips_welcome_again_after_reconnect -- --nocapture
```
Expected: PASS.
- [ ] **Step 9: Run the full service adapter unit group**
Run:
```bash
cargo test service::server::tests -- --nocapture
```
Expected: PASS, including the existing tests for:
- status `0` success
- connect failure => `PipeError::Protocol("browser websocket connect failed: ...")`
- disconnect/reset => `PipeError::PipeClosed`
- callback timeout => `PipeError::Timeout`
- new known-welcome success path
- new unknown-first-frame strictness path
- new reconnect reset behavior
---
## Task 3: Verify the shared backend stayed strict and the ws service path still works
**Files:**
- Reuse: `tests/browser_ws_backend_test.rs`
- Reuse: `tests/service_task_flow_test.rs`
- Reuse: `src/browser/ws_backend.rs`
- [ ] **Step 1: Re-run the shared ws backend tests unchanged**
Run:
```bash
cargo test --test browser_ws_backend_test -- --nocapture
```
Expected: PASS. This proves `WsBrowserBackend` semantics remain unchanged for its existing deterministic callers.
- [ ] **Step 2: Re-run the service task-flow regression**
Run:
```bash
cargo test --test service_task_flow_test -- --nocapture
```
Expected: PASS, including the auth-regression test that proves the ws service path reaches the browser websocket and no longer emits `invalid hmac seed: session key must not be empty`.
- [ ] **Step 3: Re-run the ws-focused mixed verification**
Run:
```bash
cargo test --test browser_ws_backend_test --test browser_ws_protocol_test --test service_ws_session_test --test service_task_flow_test -- --nocapture
```
Expected: PASS.
---
## Task 4: Re-run the real manual smoke that originally failed
**Files:**
- Reuse only: no code changes unless a fresh reproducer proves another bug
- [ ] **Step 1: Confirm real browser websocket reachability**
Run a reachability check for `ws://127.0.0.1:12345` (or the configured `browserWsUrl`) before starting smoke.
Expected: reachable.
- [ ] **Step 2: Start the real ws service**
Run:
```bash
cargo run --bin sg_claw -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json"
```
Expected: the service prints:
- `sg_claw ready: ...`
- the resolved `service_ws_listen_addr`
- the configured `browser_ws_url`
- [ ] **Step 3: Re-run the original failing manual smoke**
Run:
```bash
printf '打开知乎热榜并读取页面主区域文本\n' | cargo run --bin sg_claw_client -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json"
```
Expected:
- no `invalid browser status frame: Welcome! You are client #1`
- browser actions proceed past the first status frame
- if the browser later fails for another reason, capture that new reason exactly
- [ ] **Step 4: Re-run the old Zhihu export task smoke**
Run:
```bash
printf '读取知乎热榜数据,并导出 excel 文件\n' | cargo run --bin sg_claw_client -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json"
```
Expected:
- no `invalid browser status frame: Welcome! You are client #1`
- the task reaches the real browser action path beyond connection banner handling
- [ ] **Step 5: Stop and debug if a new real-browser issue appears**
If smoke now fails for a different reason, do not piggyback a second fix into this slice without:
- capturing the exact new output
- writing a new focused spec/plan if the issue is materially different
---
## Verification Checklist
### Service adapter unit tests
```bash
cargo test service::server::tests -- --nocapture
```
Expected: all service-side ws adapter tests pass, including the new welcome-frame positive/negative cases and reconnect reset case.
### Shared ws backend + ws service regressions
```bash
cargo test --test browser_ws_backend_test --test browser_ws_protocol_test --test service_ws_session_test --test service_task_flow_test -- --nocapture
```
Expected: PASS.
### Real smoke verification
- `browserWsUrl` reachable
- `sg_claw` starts with real config
- `sg_claw_client` no longer fails on `Welcome! You are client #...`
- Zhihu minimal read task gets past the first status frame
- Zhihu export task gets past the first status frame