2026-06-17 | blade-agent | 经 Codex 审核
Vibe Coding v2 会话拥有端口分配能力(test_port + deploy_port),让智能体可以在沙盒内启动 Web 服务并从外部预览。现在需要将这个能力下放给所有用户的持久化个人电脑模式。
未来计划废弃临时模式,所有用户统一使用持久化容器。这意味着端口预分配不存在浪费问题。
核心技术约束
Docker 容器端口映射只能在 docker create 时指定,创建后无法追加。因此不支持"先建容器再申请端口"。
| 系统 | 端口范围 | 分配粒度 | 存储 |
|---|---|---|---|
| Vibe Coding 专属 | 21000-23000 (test) / 23001-25000 (deploy) |
per-session | vibe_coding_config SQLite 表 |
| 通用 SandboxPortPool | 30000-30499 |
per-session | sandbox_port_pool SQLite 表 + sandbox-ports.json |
| 持久化个人电脑(待实现) | 复用 30000-30499 |
per-user | 升级后的 sandbox_port_pool |
1. 分配粒度:per-user
持久化容器是用户级的(blade-user-{user_id}),多个 session 共享同一个容器。端口必须跟着用户走,不是跟着 session。
2. 创建容器时预分配 5 个端口
Docker 端口只能 create 时绑定,所以在 _ensure_persistent_container() 创建新容器前分配。容器内端口 5000-5004,宿主机端口由池分配。
3. 端口池升级为 owner-scope 模型
不是简单把 session_id 换成 persistent:{user_id}。而是给 sandbox_port_pool 表新增 owner_type 列(session / user),区分两种生命周期。
-- sandbox_port_pool 表新增列 ALTER TABLE sandbox_port_pool ADD COLUMN owner_type TEXT DEFAULT 'session'; -- owner_type: 'session' (临时/vibe-coding) | 'user' (持久化个人电脑) -- 新增索引 CREATE INDEX idx_sandbox_port_owner ON sandbox_port_pool(owner_type, session_id);
流程 A:持久容器创建时分配端口
_ensure_persistent_container() 发现容器不存在(NotFound)port_pool.allocate_for_user(user_id, count=5) 分配端口port_bindings 传入 _create_container()sandbox-ports.json(user 维度),供 prompt 注入读取流程 B:已有容器的端口检测
_ensure_persistent_container() 发现容器已存在流程 C:重置电脑时释放端口
reset_user_computer() 停止并删除容器port_pool.release_for_user(user_id)顺序很重要:必须先删容器再释放端口。如果先释放端口再删容器失败,新分配可能抢到仍被旧容器占用的端口。
流程 D:启动对账(reconcile)
当前 reconcile() 会把不在 alive_session_ids 中的端口释放。必须排除 owner_type='user' 的条目,否则重启后用户端口会被误释放。
-- reconcile 只清理 session 类型的端口 UPDATE sandbox_port_pool SET session_id=NULL, ... WHERE session_id IN (stale_ids) AND owner_type='session' -- 新增条件
class SandboxPortPool:
# 现有 API 保持不变(session scope)
def allocate(self, session_id, count=5) -> list[PortBinding]: ...
def release(self, session_id) -> int: ...
def list_by_session(self, session_id) -> list[PortBinding]: ...
def reconcile(self, alive_session_ids) -> int: ... # 跳过 owner_type='user'
# 新增 user scope API
def allocate_for_user(self, user_id, count=5) -> list[PortBinding]: ...
def release_for_user(self, user_id) -> int: ...
def list_by_user(self, user_id) -> list[PortBinding]: ...
封装 user scope 方法,业务层不需要拼 pseudo key。内部实现可以用 owner_type='user' 区分。
现有 build_sandbox_port_mappings() 按 session_id 读取 sandbox-ports.json。持久化 session 需要 fallback 到用户级端口。
两种方案(选其一):
sandbox-ports.json。简单,不改读取路径。read_sandbox_ports(session_id) 如果当前 session 没有端口且是持久 session,则 fallback 到 port_pool.list_by_user(user_id)。推荐方案 1,因为和现有流程兼容性最好,不需要改 host 层读取逻辑。
| 文件 | 改动内容 |
|---|---|
| server/services/sandbox_port_pool.py | 新增 owner_type 列 + allocate_for_user / release_for_user / list_by_user 方法 + reconcile 排除 user 类型 |
| host/sandbox/docker.py | DockerSandboxProvider.__init__ 接受 user_port_resolver 回调;_ensure_persistent_container 创建前获取/分配端口 |
| host/_engine_runtime.py | 持久 session 的 port_bindings 从 user 维度读取;创建时写 sandbox-ports.json |
| host/engine.py | reset_user_computer 成功后调用 port_pool.release_for_user() |
| server/routes/sessions.py | 持久 session 创建时,写端口快照到 session 元数据 |
| server/app.py | 启动对账逻辑适配 owner_type |
| tests/test_sandbox_port_pool.py | 新增 user scope 分配/释放/对账测试 |
per-user 正确,不用 pseudo key
升级为 owner-scope 模型(owner_type 列),不是把 user_id 伪装成 session_id
reconcile 必须排除 user 端口
否则重启后用户端口被当作 stale 误释放
已有容器无法补端口
检测端口缺失/不匹配,提示用户 reset 重建
reset 释放时机和容器删除绑定
先删容器,成功后才释放端口,避免端口被抢占
容器内端口 5000-5004 语义
约定为"任意服务可监听",智能体 prompt 中说明用法
端口范围共池问题
30000-30499 与现有 vibe-coding-v2 共池消费;迁移期结束后临时模式废弃,池容量自然释放。_detect_unavailable_host_ports() 已有实际可用性检测兜底
docker_container_create.py)— 已通用化build_sandbox_port_mappings() — 只要 sandbox-ports.json 存在就能自动注入 prompt| 问题 | 建议 | 备注 |
|---|---|---|
| 宿主机端口绑定地址 | 0.0.0.0(和 vibe coding 一致) |
盒子是局域网设备,需要从其他机器访问预览 |
| 端口数量 | 5 个 | 和现有 vibe coding 一致,够覆盖前端 dev server + 后端 + 数据库等场景 |
| 迁移期老容器处理 | 软提示 | 检测到无端口映射时,在 UI 提示"重置电脑以启用端口预览",不强制 |