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>
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
# Service Chat Web Console 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:** Add a standalone local HTML console that connects to the existing service websocket, submits natural-language tasks with the current `submit_task` payload, and leaves the browser-helper/runtime path untouched.
|
||||
|
||||
**Architecture:** The change stays fully at the presentation edge. A new self-contained HTML file under `frontend/service-console/` reuses the current websocket protocol from `src/service/protocol.rs`, while one narrow Rust integration test guards the page's protocol shape and forbids any reference to `browser-helper.html`, callback-host endpoints, or the browser websocket. No Rust runtime logic changes are part of this slice.
|
||||
|
||||
**Tech Stack:** HTML, CSS, vanilla JavaScript, Rust integration tests, std::fs, Cargo test
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
- Create: `frontend/service-console/sg_claw_service_console.html`
|
||||
- Standalone local page with inline CSS and JavaScript
|
||||
- Connects to the existing service websocket at `ws://127.0.0.1:42321` by default
|
||||
- Sends existing `ClientMessage::SubmitTask` JSON
|
||||
- Renders inbound `ServiceMessage` rows only
|
||||
- Create: `tests/service_console_html_test.rs`
|
||||
- Source guard for the standalone page
|
||||
- Verifies file location, allowed protocol usage, and forbidden helper/callback references
|
||||
- Reference: `src/service/protocol.rs`
|
||||
- Existing websocket message shape to mirror exactly
|
||||
- Reference: `src/bin/sg_claw_client.rs`
|
||||
- Existing terminal client behavior to mirror for `submit_task`
|
||||
- Reference: `docs/superpowers/specs/2026-04-06-service-chat-web-console-design.md`
|
||||
|
||||
## Scope Guardrails
|
||||
|
||||
- Do not modify `src/service/server.rs`.
|
||||
- Do not modify `src/browser/callback_host.rs`.
|
||||
- Do not modify `src/browser/callback_backend.rs`.
|
||||
- Do not modify `src/bin/sg_claw_client.rs`.
|
||||
- Do not add an HTTP server.
|
||||
- Do not connect the new page to `ws://127.0.0.1:12345`.
|
||||
- Do not reference `/sgclaw/browser-helper.html` or `/sgclaw/callback/*` anywhere in the new page.
|
||||
|
||||
### Task 1: Add a failing source-guard test for the standalone page
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/service_console_html_test.rs`
|
||||
- Reference: `docs/superpowers/specs/2026-04-06-service-chat-web-console-design.md`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create a focused integration test that resolves the HTML path from `CARGO_MANIFEST_DIR` and asserts the file contract.
|
||||
|
||||
```rust
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn service_console_html_stays_on_service_ws_boundary() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let html_path = manifest_dir
|
||||
.join("frontend")
|
||||
.join("service-console")
|
||||
.join("sg_claw_service_console.html");
|
||||
let source = fs::read_to_string(&html_path)
|
||||
.expect("service console html should exist");
|
||||
|
||||
assert!(source.contains("ws://127.0.0.1:42321"));
|
||||
assert!(source.contains("submit_task"));
|
||||
assert!(!source.contains("/sgclaw/browser-helper.html"));
|
||||
assert!(!source.contains("/sgclaw/callback/ready"));
|
||||
assert!(!source.contains("/sgclaw/callback/events"));
|
||||
assert!(!source.contains("/sgclaw/callback/commands/next"));
|
||||
assert!(!source.contains("/sgclaw/callback/commands/ack"));
|
||||
assert!(!source.contains("ws://127.0.0.1:12345"));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" service_console_html_stays_on_service_ws_boundary --test service_console_html_test -- --exact
|
||||
```
|
||||
|
||||
Expected: FAIL because the HTML file does not exist yet.
|
||||
|
||||
- [ ] **Step 3: Keep the test narrow**
|
||||
|
||||
Before writing production code, confirm the test guards only the approved boundary:
|
||||
|
||||
```text
|
||||
- file exists at frontend/service-console/sg_claw_service_console.html
|
||||
- service websocket default is present
|
||||
- submit_task payload marker is present
|
||||
- no helper-page path
|
||||
- no callback-host endpoints
|
||||
- no browser websocket URL
|
||||
```
|
||||
|
||||
Do not turn this into an end-to-end browser test.
|
||||
|
||||
- [ ] **Step 4: Commit the red test**
|
||||
|
||||
```bash
|
||||
git add tests/service_console_html_test.rs
|
||||
git commit -m "test: add service console html boundary guard"
|
||||
```
|
||||
|
||||
### Task 2: Implement the standalone HTML console with the approved boundary
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/service-console/sg_claw_service_console.html`
|
||||
- Reference: `src/service/protocol.rs:6`
|
||||
- Reference: `src/bin/sg_claw_client.rs:16`
|
||||
- Test: `tests/service_console_html_test.rs`
|
||||
|
||||
- [ ] **Step 1: Create the HTML file with the minimal structure**
|
||||
|
||||
Write one self-contained page with:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>sgClaw Service Console</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<input id="wsUrl" value="ws://127.0.0.1:42321" />
|
||||
<button id="connectBtn">连接</button>
|
||||
<div id="connectionState">未连接</div>
|
||||
<div id="messageStream"></div>
|
||||
<textarea id="instructionInput"></textarea>
|
||||
<div id="validationText"></div>
|
||||
<button id="sendBtn" disabled>发送任务</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Keep all CSS and JavaScript inline. Do not add external assets or a build step.
|
||||
|
||||
- [ ] **Step 2: Implement websocket connect/disconnect behavior**
|
||||
|
||||
Add the smallest possible JS behavior, including explicit disconnect on the same button so the UI
|
||||
matches the approved connect/disconnect contract:
|
||||
|
||||
```javascript
|
||||
let socket = null;
|
||||
|
||||
function appendRow(kind, text) {
|
||||
// append a visible row to #messageStream
|
||||
}
|
||||
|
||||
function updateUiState() {
|
||||
const connected = socket && socket.readyState === WebSocket.OPEN;
|
||||
document.getElementById('connectBtn').textContent = connected ? '断开' : '连接';
|
||||
document.getElementById('sendBtn').disabled = !connected;
|
||||
document.getElementById('connectionState').textContent = connected ? '已连接' : '未连接';
|
||||
}
|
||||
|
||||
function connectOrDisconnectService() {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = document.getElementById('wsUrl').value.trim() || 'ws://127.0.0.1:42321';
|
||||
socket = new WebSocket(url);
|
||||
updateUiState();
|
||||
socket.addEventListener('open', () => {
|
||||
appendRow('status', 'service websocket connected');
|
||||
updateUiState();
|
||||
});
|
||||
socket.addEventListener('close', () => {
|
||||
appendRow('status', 'service websocket disconnected');
|
||||
updateUiState();
|
||||
});
|
||||
socket.addEventListener('error', () => appendRow('error', 'service websocket error'));
|
||||
socket.addEventListener('message', handleMessage);
|
||||
}
|
||||
```
|
||||
|
||||
Do not add retry loops or background reconnect logic.
|
||||
|
||||
- [ ] **Step 3: Implement submit_task sending with the current message shape**
|
||||
|
||||
Mirror the terminal client payload shape exactly and show inline validation for empty input:
|
||||
|
||||
```javascript
|
||||
function setValidation(message) {
|
||||
document.getElementById('validationText').textContent = message;
|
||||
}
|
||||
|
||||
function sendTask() {
|
||||
const instruction = document.getElementById('instructionInput').value.trim();
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
if (!instruction) {
|
||||
setValidation('请输入任务内容。');
|
||||
return;
|
||||
}
|
||||
|
||||
setValidation('');
|
||||
socket.send(JSON.stringify({
|
||||
type: 'submit_task',
|
||||
instruction,
|
||||
conversation_id: '',
|
||||
messages: [],
|
||||
page_url: '',
|
||||
page_title: ''
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
Do not add new fields. Do not add conversation replay logic in this slice.
|
||||
|
||||
- [ ] **Step 4: Render existing inbound service messages only**
|
||||
|
||||
Handle the current `ServiceMessage` variants with a minimal dispatcher:
|
||||
|
||||
```javascript
|
||||
function handleMessage(event) {
|
||||
const message = JSON.parse(event.data);
|
||||
switch (message.type) {
|
||||
case 'status_changed':
|
||||
appendRow('status', message.state);
|
||||
break;
|
||||
case 'log_entry':
|
||||
appendRow('log', message.message);
|
||||
break;
|
||||
case 'task_complete':
|
||||
appendRow(message.success ? 'complete' : 'error', message.summary);
|
||||
break;
|
||||
case 'busy':
|
||||
appendRow('error', message.message);
|
||||
break;
|
||||
default:
|
||||
appendRow('error', 'unknown service message: ' + event.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep the composer enabled during in-flight work so repeated submits surface the existing `busy` response instead of inventing a frontend queue.
|
||||
|
||||
- [ ] **Step 5: Keep the helper boundary explicit in the source**
|
||||
|
||||
Before running tests, inspect the HTML source and confirm:
|
||||
|
||||
```text
|
||||
- no /sgclaw/browser-helper.html
|
||||
- no /sgclaw/callback/*
|
||||
- no ws://127.0.0.1:12345
|
||||
- no browser websocket register frame logic
|
||||
```
|
||||
|
||||
If any such string appears, remove it before testing.
|
||||
|
||||
- [ ] **Step 6: Run the source-guard test to verify green**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" service_console_html_stays_on_service_ws_boundary --test service_console_html_test -- --exact
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 7: Commit the standalone page**
|
||||
|
||||
```bash
|
||||
git add frontend/service-console/sg_claw_service_console.html tests/service_console_html_test.rs
|
||||
git commit -m "feat: add standalone service chat console"
|
||||
```
|
||||
|
||||
### Task 3: Run the focused verification sweep
|
||||
|
||||
**Files:**
|
||||
- Verify: `tests/service_console_html_test.rs`
|
||||
- Reference: `src/service/protocol.rs`
|
||||
- Reference: `src/bin/sg_claw_client.rs`
|
||||
|
||||
- [ ] **Step 1: Re-run the source-guard test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" service_console_html_stays_on_service_ws_boundary --test service_console_html_test -- --exact
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 2: Manually inspect disconnected-send and validation markers in the HTML source**
|
||||
|
||||
Before broader verification, confirm the page source clearly contains all three UI-local rules:
|
||||
|
||||
```text
|
||||
- connect button can disconnect an open websocket
|
||||
- send button starts disabled while disconnected
|
||||
- empty instruction shows inline validation text
|
||||
```
|
||||
|
||||
This inspection stays source-level; do not add extra backend tests for it in this slice.
|
||||
|
||||
- [ ] **Step 3: Run an existing service protocol regression for safety**
|
||||
|
||||
Run the narrow existing protocol coverage to prove the page did not require backend changes:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" submit_task_client_message_converts_into_shared_runner_request --test service_ws_session_test -- --exact
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 4: Run an existing terminal-client regression for safety**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" client_submits_first_user_line_to_service --test service_task_flow_test -- --exact
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit only if verification required any code change**
|
||||
|
||||
```bash
|
||||
git add frontend/service-console/sg_claw_service_console.html tests/service_console_html_test.rs
|
||||
git commit -m "test: tighten service console verification"
|
||||
```
|
||||
|
||||
If verification required no code changes, do not create an extra commit.
|
||||
|
||||
### Task 4: Perform the manual smoke check
|
||||
|
||||
**Files:**
|
||||
- Verify live behavior only; no new code required
|
||||
|
||||
- [ ] **Step 1: Start the existing service binary**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cargo run --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" --bin sg_claw -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json"
|
||||
```
|
||||
|
||||
Expected: service starts and prints its ready line with the service websocket listen address.
|
||||
|
||||
- [ ] **Step 2: Open the standalone page directly**
|
||||
|
||||
Open:
|
||||
|
||||
```text
|
||||
D:/data/ideaSpace/rust/sgClaw/claw-new/frontend/service-console/sg_claw_service_console.html
|
||||
```
|
||||
|
||||
Expected: the page loads through the browser as a local file and shows the default websocket URL `ws://127.0.0.1:42321`.
|
||||
|
||||
- [ ] **Step 3: Connect, disconnect, and reconnect once**
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
- message stream shows websocket connected
|
||||
- clicking the same button disconnects the websocket cleanly
|
||||
- message stream shows websocket disconnected
|
||||
- send button is disabled again while disconnected
|
||||
- reconnect succeeds without reloading the page
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Submit one natural-language task**
|
||||
|
||||
Use a small harmless instruction such as:
|
||||
|
||||
```text
|
||||
打开百度
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
- empty textarea send attempt first shows inline validation without sending a websocket frame
|
||||
- page sends one submit_task payload after valid input
|
||||
- page receives and renders status/log/task_complete or busy rows
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Confirm the helper boundary stayed untouched**
|
||||
|
||||
Verify from the page source and observed behavior:
|
||||
|
||||
```text
|
||||
- the page never loads /sgclaw/browser-helper.html
|
||||
- the page never calls /sgclaw/callback/*
|
||||
- the page never connects to ws://127.0.0.1:12345
|
||||
```
|
||||
|
||||
If the task itself triggers browser automation, that remains owned by the existing Rust runtime rather than by the page.
|
||||
|
||||
- [ ] **Step 6: Commit only if the manual pass required code changes**
|
||||
|
||||
```bash
|
||||
git add frontend/service-console/sg_claw_service_console.html tests/service_console_html_test.rs
|
||||
git commit -m "fix: tighten standalone service console smoke flow"
|
||||
```
|
||||
|
||||
If the manual pass required no code changes, do not create an extra commit.
|
||||
Reference in New Issue
Block a user