持久化个人电脑模式 — 端口分配方案

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:持久容器创建时分配端口

  1. _ensure_persistent_container() 发现容器不存在(NotFound)
  2. 调用 port_pool.allocate_for_user(user_id, count=5) 分配端口
  3. 把分配到的端口作为 port_bindings 传入 _create_container()
  4. 容器创建成功,端口绑定写入 Docker
  5. 同时写入 sandbox-ports.json(user 维度),供 prompt 注入读取

流程 B:已有容器的端口检测

  1. _ensure_persistent_container() 发现容器已存在
  2. 读取 Docker 容器实际端口映射,和 DB 记录对比
  3. 如果一致:直接复用容器,不做任何操作
  4. 如果不一致(旧容器没端口 / 端口对不上):提示用户"重置电脑"以获得端口能力
  5. 不自动重建——避免静默中断用户正在使用的环境

流程 C:重置电脑时释放端口

  1. reset_user_computer() 停止并删除容器
  2. 容器删除成功后才释放端口:port_pool.release_for_user(user_id)
  3. 下次创建新容器时重新分配端口(流程 A)

顺序很重要:必须先删容器再释放端口。如果先释放端口再删容器失败,新分配可能抢到仍被旧容器占用的端口。

流程 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'  -- 新增条件

SandboxPortPool API 扩展

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' 区分。

Prompt 端口注入适配

现有 build_sandbox_port_mappings() 按 session_id 读取 sandbox-ports.json。持久化 session 需要 fallback 到用户级端口。

两种方案(选其一):

  1. 每个持久 session 写一份端口快照:创建持久 session 时,把 user ports 复制到该 session 的 sandbox-ports.json。简单,不改读取路径。
  2. 读取时 fallbackread_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 分配/释放/对账测试

Codex 审核要点(已纳入方案)

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() 已有实际可用性检测兜底

不需要改动

待决策项

问题 建议 备注
宿主机端口绑定地址 0.0.0.0(和 vibe coding 一致) 盒子是局域网设备,需要从其他机器访问预览
端口数量 5 个 和现有 vibe coding 一致,够覆盖前端 dev server + 后端 + 数据库等场景
迁移期老容器处理 软提示 检测到无端口映射时,在 UI 提示"重置电脑以启用端口预览",不强制