Skip to content

Commit 68ccfde

Browse files
committed
refactor(box): use single port with path-based routing for Box WS
Update connector to use ws://host:5410/rpc/ws instead of ws://host:5411. Update review docs to reflect the single-port architecture.
1 parent ed878c5 commit 68ccfde

3 files changed

Lines changed: 36 additions & 33 deletions

File tree

docs/review/box-architecture.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ BoxService
9999

100100
管理与 Box Runtime 的通信连接:
101101

102-
- **本地 stdio**: 无 `runtime_url` 时,fork `python -m langbot_plugin.box.server --port {port}` 子进程
103-
- **远程 WebSocket**: 有 `runtime_url` 时,连接 `ws://{host}:{port+1}`(+1 偏移,5410 是 relay,5411 是 RPC)
102+
- **本地 stdio**: Unix/macOS 无特殊配置时,fork `python -m langbot_plugin.box.server --port {port}` 子进程
103+
- **远程 WebSocket**: Docker / `--standalone-box` / 显式 `runtime_url` 时,连接 `ws://{host}:{port}/rpc/ws`(同一端口,路径区分)
104+
- **Windows**: subprocess + WebSocket(Windows 不支持 async stdio pipe)
104105
- **同步等待**: 使用 `asyncio.Event` + `wait_for(timeout=30s)` 模式确认连接
105106

106107
### 2.3 BoxWorkspaceSession (`pkg/box/workspace.py`, 404 行)
@@ -200,14 +201,13 @@ start_managed_process(session, spec):
200201

201202
### 3.3 Server (`box/server.py`, 268 行)
202203

203-
两个服务共存
204+
单端口 aiohttp 服务(默认 5410),通过路径区分
204205

205-
1. **Action RPC**: `BoxServerHandler` 处理 11 种 action(HEALTH/STATUS/EXEC/CREATE_SESSION/...),通过 stdio 或 WS 传输
206-
2. **WS Relay** (aiohttp, port 5410): `GET /v1/sessions/{id}/managed-process/ws`双向桥接 WebSocket ↔ managed process stdin/stdout
206+
1. **Action RPC** (`/rpc/ws`): `BoxServerHandler` 处理 11 种 action(HEALTH/STATUS/EXEC/CREATE_SESSION/...),通过 stdio 或 WS 传输。WS 模式使用 `AiohttpWSConnection` 适配层。
207+
2. **WS Relay** (`/v1/sessions/{id}/managed-process/ws`): 双向桥接 WebSocket ↔ managed process stdin/stdout
207208

208209
端口分配:
209-
- Port N (默认 5410): WS relay(managed process I/O)
210-
- Port N+1 (5411): Action RPC WebSocket(仅远程模式使用)
210+
- Port N (默认 5410): 所有 WebSocket 端点(Action RPC + managed process relay)
211211

212212
### 3.4 Client (`box/client.py`, 177 行)
213213

docs/review/box-vs-plugin-runtime.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,29 @@ else:
3535
# Unix/Mac → StdioClientController(python -m langbot_plugin.cli rt -s)
3636
```
3737

38-
### Box: 2-路决策
38+
### Box: 3-路决策
3939

4040
```python
41-
# pkg/box/connector.py:56-60
42-
if self.manages_local_runtime: # = not configured_runtime_url
43-
await self._start_local_stdio() # StdioClientController
41+
# pkg/box/connector.py
42+
if self._uses_websocket():
43+
if platform.get_platform() == 'win32' and not self.configured_runtime_url:
44+
await self._start_subprocess_then_ws() # subprocess + ws://localhost:5410/rpc/ws
45+
else:
46+
await self._connect_remote_ws() # ws://{host}:5410/rpc/ws
4447
else:
45-
await self._connect_remote_ws() # ws://{host}:{port+1}
48+
await self._start_local_stdio() # StdioClientController
4649
```
4750

4851
### 决策矩阵
4952

5053
| 环境 | Plugin | Box |
5154
|------|--------|-----|
52-
| Docker | WS → `:5400` | WS → `:{port+1}` (5411) |
53-
| Windows 非 Docker | subprocess + WS (`:5400`) | **stdio (可能失败!)** |
55+
| Docker | WS → `:5400` | WS → `:5410/rpc/ws` |
56+
| `--standalone-box` | N/A | WS → `localhost:5410/rpc/ws` |
57+
| Windows 非 Docker | subprocess + WS (`:5400`) | subprocess + WS (`localhost:5410/rpc/ws`) |
5458
| Unix/Mac 非 Docker | stdio | stdio |
5559
| 手动配置 URL | 通过配置项 | WS → 用户配置的 URL |
5660

57-
**Box 的 Windows 问题**: 无 Win32 分支,asyncio ProactorEventLoop 不支持 subprocess stdio pipe。Plugin 为此专门做了处理。
58-
5961
---
6062

6163
## 3. 连接建立
@@ -168,10 +170,10 @@ Controller ← ABC
168170
| 服务 | Plugin | Box |
169171
|------|--------|-----|
170172
| Action RPC (stdio) | stdin/stdout | stdin/stdout |
171-
| Action RPC (WS) | `:5400` | `:{port+1}` (默认 5411) |
172-
| 辅助服务 | debug WS `:5401` | managed process WS relay `:5410` |
173+
| Action RPC (WS) | `:5400` | `:5410/rpc/ws` |
174+
| 辅助服务 | debug WS `:5401` | managed process WS relay `:5410/v1/sessions/{id}/managed-process/ws` |
173175

174-
**Box 特点**: 即使在 stdio 模式,也额外在 `:5410` 启动 aiohttp WS 服务用于 managed process attach。Plugin 在 stdio 模式不开额外端口。
176+
**Box 特点**: 单端口 aiohttp 服务(默认 5410),通过路径区分 Action RPC 和 managed process relay。即使在 stdio 模式,也在 `:5410` 启动 aiohttp 用于 managed process attach。Plugin 在 stdio 模式不开额外端口。
175177

176178
---
177179

src/langbot/pkg/box/connector.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222

2323
# Default Docker Compose service name for the standalone Box container.
2424
_DOCKER_BOX_HOST = 'langbot_box'
25-
_DEFAULT_RELAY_PORT = 5410
26-
_DEFAULT_RPC_PORT = 5411 # relay_port + 1
25+
_DEFAULT_PORT = 5410
2726

2827

2928
def _get_box_config(ap) -> dict:
@@ -48,9 +47,9 @@ def resolve_box_ws_relay_url(ap: core_app.Application) -> str:
4847

4948
# In Docker, relay lives on the box runtime container.
5049
if platform.get_platform() == 'docker':
51-
return f'http://{_DOCKER_BOX_HOST}:{_DEFAULT_RELAY_PORT}'
50+
return f'http://{_DOCKER_BOX_HOST}:{_DEFAULT_PORT}'
5251

53-
return f'http://127.0.0.1:{_DEFAULT_RELAY_PORT}'
52+
return f'http://127.0.0.1:{_DEFAULT_PORT}'
5453

5554

5655
class BoxRuntimeConnector(ManagedRuntimeConnector):
@@ -72,10 +71,10 @@ def __init__(self, ap: core_app.Application):
7271
self._handler_task: asyncio.Task | None = None
7372
self._ctrl_task: asyncio.Task | None = None
7473

75-
# Parse the relay URL once for reuse (relay port, not RPC port).
74+
# Parse the relay URL once for reuse.
7675
parsed = urlparse(self.ws_relay_base_url)
7776
self._relay_host = parsed.hostname or '127.0.0.1'
78-
self._relay_port = parsed.port or _DEFAULT_RELAY_PORT
77+
self._relay_port = parsed.port or _DEFAULT_PORT
7978

8079
def _uses_websocket(self) -> bool:
8180
"""Whether the connector should use WebSocket to reach the Box runtime.
@@ -142,18 +141,18 @@ async def _start_subprocess_then_ws(self) -> None:
142141
"""Launch box server as detached subprocess, then connect via WS (Windows)."""
143142
self.ap.logger.info('(windows) Use cmd to launch box runtime and communicate via ws')
144143

145-
# Launch the box server subprocess (no stdio pipe).
146-
# The server will listen on _relay_port for the WS relay and
147-
# _relay_port+1 for action-RPC WebSocket.
144+
# Launch the box server subprocess in ws mode (no stdio pipe).
148145
await self._start_runtime_subprocess(
149146
'-m',
150147
'langbot_plugin.box.server',
148+
'--mode',
149+
'ws',
151150
'--port',
152151
str(self._relay_port),
153152
)
154153

155154
# Wait for the WS endpoint to become reachable, then connect.
156-
ws_url = f'ws://localhost:{self._relay_port + 1}'
155+
ws_url = f'ws://localhost:{self._relay_port}/rpc/ws'
157156
await self._connect_ws(ws_url, '(windows) WebSocket')
158157

159158
async def _connect_remote_ws(self) -> None:
@@ -167,18 +166,20 @@ async def _connect_remote_ws(self) -> None:
167166
def _resolve_rpc_ws_url(self) -> str:
168167
"""Determine the action-RPC WebSocket URL.
169168
169+
All endpoints share a single port; action RPC is at ``/rpc/ws``.
170+
170171
Priority:
171172
1. Explicit ``box.runtime_url`` from config (user-supplied, used as-is)
172-
2. Docker environment -> ``ws://langbot_box_runtime:5411``
173-
3. --standalone-box / Windows fallback -> ``ws://localhost:5411``
173+
2. Docker environment -> ``ws://langbot_box:5410/rpc/ws``
174+
3. --standalone-box / Windows fallback -> ``ws://localhost:5410/rpc/ws``
174175
"""
175176
if self.configured_runtime_url:
176177
return self.configured_runtime_url
177178

178179
if platform.get_platform() == 'docker':
179-
return f'ws://{_DOCKER_BOX_HOST}:{_DEFAULT_RPC_PORT}'
180+
return f'ws://{_DOCKER_BOX_HOST}:{_DEFAULT_PORT}/rpc/ws'
180181

181-
return f'ws://localhost:{self._relay_port + 1}'
182+
return f'ws://localhost:{self._relay_port}/rpc/ws'
182183

183184
async def _connect_ws(self, ws_url: str, transport_name: str) -> None:
184185
"""Shared WebSocket connection procedure."""

0 commit comments

Comments
 (0)