# 多应用桥接框架设计 > 目标:将 bridge 从"微信专属 API 服务"演进为"容器内多桌面应用的统一桥接框架"。一个容器内可同时运行多个应用(微信 / 小红书 / Telegram / Chromium / 自定义),每个应用作为独立实例被 bridge 管理,对外暴露结构化接口。 --- > **阅读提示**:本文档是**目标架构设计**,同时用 `[现状]` 标注当前代码的实际状态。当前 bridge 仍是一个约 2400 行的单文件服务([bridge/server.py](file:///D:/WechatOnCloud-main/bridge/server.py)),全局单例、微信强耦合;docker 与 panel 层已支持"一容器一应用"的多应用类型,但尚未支持"单容器多应用"。因此本文先聚焦 bridge 层改造,使其在"多容器单应用"模式下即可工作,单容器多应用作为后续扩展。 --- ## 1. 背景与转变 ### 1.1 现状 bridge 当前是**单应用、单进程、强耦合微信**的 API 服务: - [bridge/server.py](../bridge/server.py) 约 2400 行单文件,全局单例 `_state = AppState()` 持有唯一的 `XdotoolDriver`、`DbReader`、`SendQueue`、`QrCapture`。 - 所有路由均为 `/api/*` 无前缀(如 `/api/send/text`、`/api/messages/since`),直接操作全局 `_state`,没有 `app_id` 概念。 - 响应模型字段名微信中心化:`wechat_running`、`wechat_window_found`、错误码 `WECHAT_NOT_RUNNING` / `WECHAT_NOT_LOGGED_IN`。 - DB 解密、联系人/消息读取、二维码登录全部写死微信表结构与 SQLCipher 参数。 - 一容器只跑一个应用(由 [docker/app-defs.sh](../docker/app-defs.sh) 的 `WOC_APP_TYPE` 决定);想新增小红书/Telegram 自动化,当前只能再起一个容器。 ### 1.2 转变 用户需求:**一个容器内同时跑多个应用,每个应用对外暴露接口**。 这要求 bridge 从"微信适配器"升格为"应用桥接框架": | 维度 | 现状 | 目标 | |------|------|------| | 容器内应用数 | 1(由 `WOC_APP_TYPE` 决定) | N(动态注册) | | bridge 进程 | 跟随单一应用 | 一个 bridge 管理多个应用实例 | | 路由 | 微信专属路由无前缀 | 通用路由无前缀 + 每个应用实例独立命名空间 | | 状态 | 全局单例 `_state` | 按 app_id 隔离的 AppInstance 容器 | | 资源调度 | 不需要 | 必须串行化 X11 / 剪贴板 / 焦点操作 | ### 1.3 已具备的基础(非本阶段改造重点) - docker 层已通过 `WOC_APP_TYPE` 支持多种应用类型:`wechat` / `telegram` / `chromium` / `custom`([docker/app-defs.sh](../docker/app-defs.sh))。 - panel 的 `Instance.appType` 已能标识实例承载的应用类型,并透传给容器环境变量([panel/server/src/store.ts](../panel/server/src/store.ts))。 - bridge 基于 FastAPI + Pydantic,已有统一的 `BridgeError` 异常模型与全局异常处理器,为路由拆分和错误码扩展提供了基础。 > **本文档范围**:仅聚焦 bridge 层改造;panel UI、docker 单容器多应用启动机制不在本阶段文档范围内。 --- ## 2. 核心理念 ### 2.1 三层抽象 ``` 应用类型 (AppKind) — 静态描述:微信 / 小红书 / Telegram / Chromium / Custom ↓ 实例化 应用实例 (AppInstance) — 运行期实体:唯一 app_id、独立状态、独立窗口 ↓ 适配 应用驱动 (AppDriver) — 该实例的能力实现:发消息 / 发笔记 / 读 DB / 截图 ``` - **AppKind** 是注册表项,定义"这类应用能做什么、怎么启动、需要什么依赖" - **AppInstance** 是运行期实体,一个容器内可以同时存在多个同类实例(如两个微信账号、两个小红书账号) - **AppDriver** 是实例的能力实现层,框架通过它操作具体应用 ### 2.2 框架职责边界 **框架负责(应用无关)**: - HTTP 服务、路由分发、鉴权、限流 - 请求/响应日志、异常处理、错误码体系 - X11 串行化调度(避免多应用并发争用同一个 X server) - 截图、剪贴板、窗口管理(通用 X11 能力) - 应用生命周期管理(启动 / 停止 / 重启 / 健康检查) - 诊断框架(通用检查项 + 应用自报检查项) - 配置管理、命令行参数、环境变量 **应用驱动负责(应用相关)**: - 进程启动命令与参数 - 窗口识别(按 title / class / PID) - 登录态检测与登录流程 - 应用专属能力实现(发消息、发笔记、读 DB、搜索等) - 应用专属路由注册 - 应用专属诊断项 > **[现状]** 当前所有职责都混在 [bridge/server.py](../bridge/server.py) 中,没有框架/驱动分层;`XdotoolDriver` 直接实现微信专用方法,`SendQueue` 是全局单例限流器,UIScheduler 尚未引入。 ### 2.3 设计原则 1. **组合优于继承**:driver 持有通用工具实例(`self.xd`),不继承工具类 2. **一应用一命名空间**:每个实例独立 `app_id`,路由与状态都按 `app_id` 隔离 3. **声明式能力**:driver 声明能力集合,框架据此挂载路由与协商 4. **串行化 X11**:所有窗口操作经统一 UIScheduler 排队执行 5. **应用无关的 DB 抽象延迟**:SQLCipher 解密暂留微信 driver 内,待第二个 SQLCipher 应用出现再抽公共层 6. **向后兼容过渡**:旧路径与新路径并存,alias 机制保证现有调用方不破坏 --- ## 3. 总体架构 ``` ┌─────────────────────────────────────────────────────────────────┐ │ Docker 容器 │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ bridge 框架进程 (FastAPI, :8088) │ │ │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ 通用路由层 │ │ UIScheduler │ │ AppManager │ │ │ │ │ │ /api/status │ │ (X11 串行) │ │ (生命周期) │ │ │ │ │ │ /api/apps │ │ │ │ │ │ │ │ │ │ /api/screens │ └──────────────┘ └──────┬───────┘ │ │ │ │ │ /api/diag/* │ │ │ │ │ │ └──────────────┘ │ │ │ │ │ ┌─────────────┼─────────┐ │ │ │ │ │ │ │ │ │ │ │ ┌───────────────────┴───┐ ┌──────┴───────┐ │ │ │ │ │ AppInstance[wx_a1] │ │ AppInstance │ │ │ │ │ │ └ WechatDriver │ │ [xhs_b2] │ │ │ │ │ │ routes: /api/apps/ │ │ └ XhsDriver │ │ │ │ │ │ wx_a1/* │ │ routes: │ │ │ │ │ │ capabilities: │ │ /api/apps/ │ │ │ │ │ │ text_send, │ │ xhs_b2/* │ │ │ │ │ │ db_read, │ │ capabilities│ │ │ │ │ │ login_qr │ │ publish, │ │ │ │ │ └───────────────────────┘ │ search │ │ │ │ │ └──────────────┘ │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ X server (Xvfb :1) ◀── 所有应用共用,UIScheduler 串行化调度 │ │ ├─ 微信窗口 (wxid_abc) │ │ ├─ chromium 窗口 (xiaohongshu) │ │ KasmVNC (:3000/3001) ◀── web 桌面串流 │ │ │ │ 数据卷 /config/ │ │ ├─ wechat// 微信数据 │ │ ├─ xiaohongshu/ 小红书 cookie + 数据 │ │ ├─ telegram/ Telegram 数据 │ │ └─ apps.json 应用实例注册表(持久化) │ └─────────────────────────────────────────────────────────────────┘ ▲ │ HTTP Bearer Token ┌──────┴──────┐ │ panel 面板 │ ◀── 统一入口,调度多容器、多应用 └─────────────┘ ``` > **[现状]** 上图是目标态。当前每个 Docker 容器内只运行**一个**应用实例,bridge 进程也按单实例设计;`AppManager`、`UIScheduler`、`apps.json` 均未实现。本阶段优先让 bridge 在"多容器单应用"模式下具备框架能力(每个容器一个 bridge、一个 driver、一个 AppInstance),为未来的"单容器多应用"保留扩展点。 --- ## 4. 核心概念 ### 4.1 AppKind(应用类型) 静态注册项,描述"这类应用是什么"。一个 AppKind 在 bridge 启动时注册到 `AppKindRegistry`。 | 属性 | 说明 | 示例 | |------|------|------| | `kind_id` | 唯一标识 | `"wechat"` / `"xiaohongshu"` / `"telegram"` / `"chromium"` / `"custom"` | | `name` | 显示名 | `"微信"` / `"小红书"` | | `driver_class` | AppDriver 实现类 | `WechatDriver` / `XiaohongshuDriver` | | `default_capabilities` | 默认能力集合 | `{"text_send","db_read","login_qr"}` | | `binary_path` | 可执行文件路径(用于检测是否已安装) | `/config/wechat/opt/wechat/wechat` | | `launch_args_template` | 启动参数模板(支持变量替换) | `"--user-data-dir={data_dir}"` | | `single_instance` | 是否限制单实例(如系统托盘类应用) | `true` / `false` | > **[现状]** `AppKindRegistry` 尚未实现。当前应用类型由容器环境变量 `WOC_APP_TYPE` 决定,bridge 代码中无对应抽象;但目标类的字段(`kind_id`、`driver_class`、`binary_path` 等)可直接映射到现有 docker/app-defs.sh 的定义。 ### 4.2 AppInstance(应用实例) 运行期实体,代表一个正在运行(或已注册待启动)的应用。每个实例有唯一 `app_id`。 | 属性 | 说明 | 示例 | |------|------|------| | `app_id` | 全局唯一实例标识 | `"wx_a1"` / `"xhs_b2"`(kind 前缀 + 短随机) | | `kind_id` | 所属应用类型 | `"wechat"` | | `display_name` | 用户可读名 | `"工作微信"` / `"小红书主号"` | | `data_dir` | 该实例独立数据目录 | `/config/apps/wx_a1` | | `window_id` | X11 窗口 ID(运行期动态) | `0x380000a` | | `pid` | 进程 PID | `1234` | | `login_state` | 当前登录态 | `"logged_in"` / `"need_login"` / `"not_running"` | | `capabilities` | 实际可用能力(动态计算) | `{"text_send"}`(DB 未解锁时 db_read 不在集合内) | | `status` | 实例状态 | `"registered"` / `"starting"` / `"running"` / `"stopped"` / `"crashed"` | **实例寻址**:所有应用专属路由以 `/api/apps/{app_id}/` 为前缀,框架据此路由到对应实例的 driver。 > **[现状]** `AppInstance` 数据类尚未实现。当前 bridge 的运行期状态是全局单例 `_state`,没有 `app_id`。本阶段每个 bridge 进程仍只管理一个实例,可把该实例的 `app_id` 固定为 `"default"` 或从 `WOC_APP_TYPE` 派生,使新路由层能提前落地。 ### 4.3 AppDriver(应用驱动) 每种 AppKind 对应一个 AppDriver 实现类,负责该类应用的所有操作。driver 实例与 AppInstance 一一绑定。 ```python class AppDriver(ABC): kind_id: ClassVar[str] default_capabilities: ClassVar[set[str]] def __init__(self, instance: AppInstance, ctx: BridgeContext) -> None: self.instance = instance self.ctx = ctx # 框架通用能力(xd / scheduler / screenshot) self.xd = ctx.xd_factory(instance) # 该实例专属的 XdotoolBase # 生命周期 async def install(self) -> None: ... # 首次安装(下载/解压) async def start(self) -> None: ... # 启动进程 async def stop(self) -> None: ... # 停止进程 async def restart(self) -> None: ... async def is_running(self) -> bool: ... async def health_check(self) -> dict: ... # 窗口与登录 async def find_window(self) -> int | None: ... async def activate_window(self) -> None: ... async def detect_login_state(self) -> str: ... async def wait_for_login(self, timeout: int) -> None: ... async def logout(self) -> None: ... # 能力查询 def capabilities(self) -> set[str]: ... # 返回当前实际可用能力(动态) # 路由注册 def register_routes(self, router: APIRouter) -> None: ... # 诊断 def diagnostic_items(self) -> list[dict]: ... async def diagnostic_run(self, check_id: str) -> dict: ... async def diagnostic_autofix(self, check_id: str) -> dict: ... ``` > **[现状]** `AppDriver` 抽象基类尚未实现。当前微信相关逻辑直接写在 [bridge/server.py](../bridge/server.py) 和 [bridge/xdotool_driver.py](../bridge/xdotool_driver.py) 中,二者均强耦合微信。拆分时建议: > 1. 把 `xdotool_driver.py` 中通用 X11 操作(`_run`、`_key`、`_paste_via_xclip`、窗口几何)抽成 `XdotoolBase`; > 2. 把微信专属方法(`find_wechat_window`、`send_text`、`_open_session_by_name`)迁到 `drivers/wechat/driver.py` 的 `WechatDriver`; > 3. 微信 DB 读写、密钥提取、二维码登录迁到 `drivers/wechat/` 子模块。 ### 4.4 BridgeContext(框架上下文) driver 通过它访问框架通用能力。框架保证线程安全与串行化。 | 字段 | 类型 | 说明 | |------|------|------| | `config` | `BridgeConfig` | 全局配置 | | `xd_factory` | `Callable[[AppInstance], XdotoolBase]` | 为每个实例创建 X11 工具(绑定到该实例窗口) | | `ui_scheduler` | `UIScheduler` | X11 操作串行化调度器 | | `screenshot` | `Screenshot` | 通用截图能力 | | `send_queue` | `SendQueue` | UI 操作串行化队列(多应用共享) | | `logger` | `logging.Logger` | 框架 logger | --- ## 5. 模块设计 ### 5.1 目录结构 ``` bridge/ core/ 框架核心(应用无关) __init__.py context.py BridgeContext config.py BridgeConfig(命令行 + env) server.py FastAPI app 工厂 + lifespan + 全局中间件 logging.py logger 配置 + 请求日志中间件 errors.py BridgeError + ErrorResponse + 错误码表 models.py 通用响应模型(StatusResponse / AppInstanceInfo 等) xdotool_base.py X11 通用操作基类 screenshot.py 通用截图 send_queue.py UI 串行化队列 ui_scheduler.py X11 操作调度器(多应用串行) diagnostic.py 通用诊断框架 app_manager.py AppManager(实例生命周期管理) app_kind.py AppKind + AppKindRegistry(类型注册表) app_instance.py AppInstance 数据类 lifecycle.py InitState 后台任务框架 drivers/ 应用适配器 __init__.py base.py AppDriver 抽象基类 wechat/ __init__.py driver.py WechatDriver(AppDriver) db_reader.py 微信 DB 读取 decryptor.py SQLCipher 解密 key_cache.py 密钥缓存 key_extractor.py 内存扫描提 key models.py 微信专属请求/响应模型 routes.py 微信专属路由 diagnostic.py 微信诊断项 xiaohongshu/ __init__.py driver.py XiaohongshuDriver(AppDriver) routes.py 小红书专属路由 models.py telegram/ __init__.py driver.py TelegramDriver(AppDriver) routes.py chromium/ __init__.py driver.py ChromiumDriver(AppDriver) 通用浏览器 custom/ __init__.py driver.py CustomDriver(AppDriver) tools/ 调试脚本 s6/ s6 服务定义 main.py 入口:解析参数 → 注册 kinds → 启动 app ``` > **[现状]** 上述 `core/`、`drivers/`、`main.py` 均为目标结构。当前 bridge 目录扁平:所有代码在 [bridge/server.py](../bridge/server.py)、[bridge/xdotool_driver.py](../bridge/xdotool_driver.py)、[bridge/send_queue.py](../bridge/send_queue.py) 等根级文件中。S1 阶段可按以下顺序迁移,避免一次性大爆炸: > 1. 先把 `server.py` 中通用配置/错误/模型迁到 `core/`; > 2. 创建 `drivers/base.py` 与 `drivers/wechat/`; > 3. 保持 `main.py` 仍只注册一个 `WechatDriver`,行为与旧版完全一致。 ### 5.2 AppManager(应用管理器) 框架核心组件,管理容器内所有 AppInstance 的生命周期。 **职责**: - 维护实例注册表(内存 + 持久化到 `/config/apps.json`) - 提供实例 CRUD:`register` / `unregister` / `list` / `get` - 控制实例生命周期:`start` / `stop` / `restart` - 健康检查:周期性轮询所有 running 实例的 `is_running` - 自动恢复:crashed 实例按策略重启(最多 N 次/小时) **实例注册表结构**(`/config/apps.json`): ```json { "version": 1, "instances": [ { "app_id": "wx_a1", "kind_id": "wechat", "display_name": "工作微信", "data_dir": "/config/apps/wx_a1", "auto_start": true, "created_at": 1735900800 }, { "app_id": "xhs_b2", "kind_id": "xiaohongshu", "display_name": "小红书主号", "data_dir": "/config/apps/xhs_b2", "auto_start": false, "created_at": 1735900900 } ] } ``` > **[现状]** `AppManager` 与 `/config/apps.json` 均未实现。当前实例生命周期(容器启停、应用安装)由 panel 通过 Docker API 管理,bridge 只感知当前容器内已启动的单个应用。本阶段改造不推翻 panel 的生命周期管理,bridge 内只保留单实例的运行期抽象,为后续多实例注册表预留接口。 ### 5.3 UIScheduler(X11 操作调度器) **问题**:多应用共用一个 X server,并发操作会导致: - 窗口焦点争用(A 激活窗口时 B 的操作打到 A) - 剪贴板污染(A 写剪贴板,B 还没读就被覆盖) - 键盘事件错位(A 的 Ctrl+V 发到 B 的窗口) **解决**:所有 X11 操作(activate / key / paste / click / type)必须经 UIScheduler 排队执行。 ```python class UIScheduler: """X11 操作串行化调度器。 所有 driver 的 X11 操作通过 submit() 提交,调度器按 FIFO 顺序 串行执行。同一时间只有一个操作在 X server 上进行。 """ async def submit(self, coro_factory: CoroFactory, *, app_id: str) -> Any: """提交一个 X11 操作。 Args: coro_factory: 返回 coroutine 的工厂(便于重试与超时控制) app_id: 发起方实例 ID(用于日志与死锁检测) Returns: 操作结果 Notes: - 操作间最小间隔 100ms(避免 X server 压力) - 单操作超时 30s(避免死锁) - 操作日志带 app_id 前缀,便于排障 """ ``` **driver 使用方式**: ```python class WechatDriver(AppDriver): async def send_text(self, to: str, content: str) -> str: async def _op(): await self.xd.activate_window(self.instance.window_id) await self.xd._open_session_by_name(to) await self.xd._paste_via_xclip(content) await self.xd._key("Return") return f"local_{int(time.time())}_{random.randint(0,0xFFFFFF):06x}" return await self.ctx.ui_scheduler.submit(_op, app_id=self.instance.app_id) ``` **不进 UIScheduler 的操作**: - 读窗口列表(`xdotool search`,只读) - 读进程状态(`pgrep`,只读) - 读 DB(不涉及 X11) - 截图(`scrot` 本身是只读快照,不争用焦点) ### 5.4 SendQueue 与 UIScheduler 的关系 | 组件 | 作用 | 粒度 | |------|------|------| | `SendQueue` | 发送类操作的限流(防止频率过高触发风控) | 单实例:每个 driver 自己一个队列 | | `UIScheduler` | X11 操作的串行化(防止多应用争用 X server) | 全局:所有 driver 共享 | 两者正交:一个操作可能先经 SendQueue 限流,再经 UIScheduler 串行执行。 > **[现状]** 当前只有一个全局 [SendQueue](../bridge/send_queue.py),用于单应用发送限流;`UIScheduler` 尚未实现,X11 操作在 HTTP 层并发调用时存在焦点/剪贴板冲突风险。S1 阶段可保持 `SendQueue` 不变,S2 引入多实例后再把 `SendQueue` 下放到每个 `AppInstance`,同时新增全局 `UIScheduler`。 --- ## 6. 接口规范 ### 6.1 路由分层 ``` /api/* 通用路由(无 app_id,框架级) /api/status 框架状态(聚合所有应用摘要) /api/apps 应用实例列表 / 注册新实例 /api/apps/{app_id} 单实例状态 / 控制 /api/apps/{app_id}/start 启动实例 /api/apps/{app_id}/stop 停止实例 /api/apps/{app_id}/restart 重启实例 /api/kinds 支持的应用类型列表 /api/screenshot 全屏截图(所有应用窗口) /api/diagnostic/items 通用诊断项 + 所有实例诊断项聚合 /api/diagnostic/run/{id} 执行诊断 /api/diagnostic/autofix/{id} /api/apps/{app_id}/* 应用专属路由(由 driver.register_routes 挂载) /api/apps/{app_id}/messages 微信消息拉取 /api/apps/{app_id}/contacts 微信联系人 /api/apps/{app_id}/send/text 微信发文本 /api/apps/{app_id}/login/qr 微信扫码登录 /api/apps/{app_id}/db/decrypt 微信 DB 解密 /api/apps/{app_id}/xhs/publish 小红书发笔记 /api/apps/{app_id}/xhs/search 小红书搜索 ``` > **[现状]** 当前所有路由都是 `/api/*` 无前缀(如 `/api/send/text`、`/api/messages/since`、`/api/db/decrypt`),且全部写死微信逻辑。本阶段目标: > 1. 新增 `/api/apps/{app_id}/*` 路由层; > 2. 将旧路由作为 alias 内部转发到 `/api/apps/default/*`; > 3. `/api/status` 同时返回旧字段(取 default app)与新 `apps` 数组,保证面板零改动。 ### 6.2 通用路由详述 #### GET /api/status 返回框架整体状态 + 所有实例摘要。 ```json { "bridge_version": "2.0.0", "uptime_seconds": 3600, "display": ":1", "app_count": 2, "apps": [ { "app_id": "wx_a1", "kind_id": "wechat", "display_name": "工作微信", "status": "running", "login_state": "logged_in", "capabilities": ["text_send", "db_read", "login_qr"] }, { "app_id": "xhs_b2", "kind_id": "xiaohongshu", "display_name": "小红书主号", "status": "running", "login_state": "need_login", "capabilities": ["publish", "search"] } ] } ``` #### GET /api/apps 列出所有实例。 #### POST /api/apps 注册新实例。 ```json // 请求 { "kind_id": "xiaohongshu", "display_name": "小红书小号", "auto_start": true } // 响应 { "app_id": "xhs_c3", "kind_id": "xiaohongshu", "display_name": "小红书小号", "data_dir": "/config/apps/xhs_c3", "status": "registered" } ``` #### GET /api/apps/{app_id} 单实例详细状态。 ```json { "app_id": "wx_a1", "kind_id": "wechat", "display_name": "工作微信", "status": "running", "pid": 1234, "window_id": "0x380000a", "login_state": "logged_in", "capabilities": ["text_send", "db_read", "login_qr", "media"], "uptime_seconds": 1800, "db_accessible": true, "current_wxid": "wxid_abc", "current_nickname": "张三" } ``` #### POST /api/apps/{app_id}/start|stop|restart 控制实例生命周期。 #### GET /api/kinds 列出支持的 AppKind。 ```json { "kinds": [ { "kind_id": "wechat", "name": "微信", "default_capabilities": ["text_send", "db_read", "login_qr", "media"], "single_instance": false, "installed": true }, { "kind_id": "xiaohongshu", "name": "小红书", "default_capabilities": ["publish", "search"], "single_instance": false, "installed": true } ] } ``` ### 6.3 应用专属路由规范 由各 driver 通过 `register_routes(router)` 挂载,router 自动绑定 `/api/apps/{app_id}` 前缀。 **driver 实现示例**: ```python # drivers/wechat/routes.py def register_routes(router: APIRouter) -> None: @router.get("/messages/since") async def get_messages(app_id: str, cursor: int = 0, limit: int = 50): driver = app_manager.get_driver(app_id) return await driver.get_messages_since(cursor, limit) @router.post("/send/text") async def send_text(app_id: str, req: SendTextRequest): driver = app_manager.get_driver(app_id) return await driver.send_text(req.to, req.content) ``` ### 6.4 错误码扩展 新增框架级错误码: | 错误码 | HTTP | 说明 | |--------|------|------| | `APP_NOT_FOUND` | 404 | app_id 不存在 | | `APP_NOT_RUNNING` | 503 | 实例未运行 | | `APP_NOT_LOGGED_IN` | 401 | 实例未登录 | | `APP_KIND_UNKNOWN` | 400 | 未知应用类型 | | `APP_KIND_NOT_INSTALLED` | 503 | 应用类型未安装 | | `APP_ALREADY_EXISTS` | 409 | 实例已存在(single_instance 冲突) | | `UI_BUSY` | 503 | UIScheduler 队列拥塞 | | `APP_CAPABILITY_NOT_SUPPORTED` | 501 | 该实例不支持请求的能力 | > **[现状]** 当前 [bridge/models.py](../bridge/models.py) 中只有微信中心化错误码,如 `WECHAT_NOT_RUNNING`、`WECHAT_NOT_LOGGED_IN` 等。拆分阶段应: > 1. 保留旧错误码作为微信 driver 的返回值; > 2. 新增框架级错误码上表; > 3. alias 路由命中时仍可使用旧错误码,避免面板/外部系统改动。 ### 6.5 向后兼容层 现有无前缀路由(`/api/messages`、`/api/send/text` 等)保留为 alias,内部转发到 `/api/apps/{default_app_id}/...`。 **默认实例选择规则**: 1. 若容器内只有一个实例,自动作为 default 2. 若多实例,读取 `/config/.woc-default-app`(由面板或首次注册时写入) 3. 都没有则返回 404 **alias 路由行为**: - 命中 alias 时日志打 `DEPRECATED` warning,提示调用方迁移 - alias 在 major 版本升级时下线(v3.0.0 移除) ### 6.6 与既有系统的兼容性约束 改造必须尊重以下已验证的硬约束,否则现有微信实例会损坏: 1. **鉴权**:`/api/bridge/:id/*` 外部调用依赖 `WOC_BRIDGE_API_TOKEN` 作为 Bearer token;修改 `.env` 后必须重建 panel 容器(而非重启)才能生效。 2. **DB 解密**:微信 DB 仅兼容 Linux WeChat 4.x,使用 SQLCipher 4 参数(AES-256-CBC、PBKDF2-HMAC-SHA512、256000 rounds、mac_salt = salt XOR 0x3A)。解密能力保留在 `drivers/wechat/` 内,不提前抽象。 3. **ptrace**:自动提取 key 需要容器具备 `SYS_PTRACE` capability 或 `--privileged`,且宿主 `ptrace_scope=0`。 4. **DB 路径**:自动检测写死 `/config/xwechat_files//db_storage/message/message_0.db`;改为 driver 内部常量,不作为框架公共假设。 5. **发送定位**:微信 `send_text/send_file` 使用 `display_name`(备注/昵称)而非 wxid 搜索会话,该行为由 `WechatDriver` 封装,不进入框架公共层。 6. **容器启动**:当前 [docker/autostart](../docker/autostart) 只启动一个应用;bridge 框架不假设自己能启动多个应用,生命周期控制仍由 panel/docker 负责。 --- ## 7. 资源调度与并发 ### 7.1 X11 资源争用对策 | 资源 | 争用场景 | 对策 | |------|---------|------| | 窗口焦点 | A 激活窗口时 B 失焦 | 所有 activate/key/click 经 UIScheduler 串行 | | 剪贴板 | A 写入后 B 未读就被 C 覆盖 | paste 操作是"写剪贴板+立即 Ctrl+V"原子组,经 UIScheduler 串行 | | 键盘事件 | A 的 Ctrl+V 发到 B 窗口 | activate 后立即操作,UIScheduler 保证原子 | | 鼠标坐标 | A 的 click 坐标被 B 的 mousemove 改变 | mousemove + click 必须在同一个 UIScheduler 任务内 | ### 7.2 并发模型 ``` HTTP 请求 (asyncio) ↓ driver 方法 (async) ↓ UIScheduler.submit(coro) ◀── 全局串行点(X11 操作) ↓ XdotoolBase._run (asyncio subprocess) ``` - HTTP 层并发:FastAPI 默认并发处理请求 - driver 层并发:每个实例的方法可并发调用 - UIScheduler 层串行:所有 X11 写操作排队执行 - DB 读取层并发:不涉及 X11,独立并发(受限于 SQLite 读写锁) ### 7.3 限流策略 | 层级 | 限流 | 配置 | |------|------|------| | HTTP 全局 | 单 IP 每秒 N 请求 | `WOC_BRIDGE_MAX_CALLS_PER_SEC` | | 单实例发送 | 发送类操作最小间隔 | `WOC_BRIDGE_SEND_DELAY_MS`(每实例独立) | | UIScheduler | 操作间最小间隔 | 固定 100ms(防止 X server 压力) | | UIScheduler 队列长度 | 最多排队 N 个操作 | 50,超限返回 `UI_BUSY` | --- ## 8. 应用生命周期 ### 8.1 实例状态机 ``` register ┌─────────────────────────┐ │ ▼ ┌────────┐ start ┌─────────┐ running ┌────────┐ │registered├────────►│starting├────────────►│running │ └────────┘ └────┬───┘ └───┬────┘ ▲ │ fail │ stop │ ▼ ▼ │ ┌────────┐ ┌────────┐ │ │crashed │ │stopped │ │ └───┬────┘ └───┬────┘ │ │ restart │ start │ ▼ │ └──────────────────────────────────────────┘ ``` ### 8.2 启动流程 1. `POST /api/apps` 注册实例 → 状态 `registered` 2. `POST /api/apps/{app_id}/start` → 状态 `starting` 3. driver.install()(首次启动,下载/解压) 4. driver.start()(启动应用进程) 5. 轮询 driver.find_window() 直到窗口出现(超时 60s) 6. 状态 `running` 7. 启动失败 → 状态 `crashed`,记录错误 ### 8.3 自动启动 容器启动时,AppManager 读取 `/config/apps.json`,对所有 `auto_start=true` 的实例按顺序执行启动流程。 **顺序约束**:实例间启动间隔 5 秒(避免同时启动多个 GUI 应用导致内存峰值)。 ### 8.4 健康检查 AppManager 后台任务(每 30s): 1. 遍历所有 `running` 状态实例 2. 调用 `driver.is_running()` 3. 失败则状态转 `crashed` 4. 记录崩溃次数与时间 5. 若配置了自动恢复且未超限(默认 3 次/小时),触发 restart ### 8.5 数据卷布局 ``` /config/ apps.json 实例注册表 .woc-default-app 默认实例 app_id(兼容层用) apps/ wx_a1/ data/ 应用数据根(driver 自由组织) wechat/ opt/wechat/wechat 微信二进制 xwechat_files/ 微信运行时数据 logs/ 该实例日志 state/ 该实例状态文件 xhs_b2/ data/ chromium/ 小红书专用 chromium user-data-dir Default/ Cookies 小红书登录 cookie logs/ state/ ``` --- ## 9. 应用间通信 ### 9.1 显式通信(推荐) 应用之间通过外部调用方(如面板或用户脚本)显式编排: ``` # 伪代码:从小红书采集内容,发到微信 xhs_note = await bridge.get(f"/api/apps/xhs_b2/xhs/search?keyword=foo") await bridge.post(f"/api/apps/wx_a1/send/text", json={"to":"文件助手","content":xhs_note}) ``` **优点**:流程显式、可观测、可调试。 **不提供应用间直接调用**:driver 之间不互相感知,避免隐式依赖。 ### 9.2 事件总线(未来扩展) 可选的发布订阅机制: ```python # driver 发布事件 await self.ctx.event_bus.publish("message_received", {"app_id": "wx_a1", "msg": {...}}) # 外部订阅 GET /api/events/stream (SSE) ``` P0 不实现,留待有明确需求时再做。 --- ## 10. 部署模型 ### 10.1 单容器多应用 ``` 容器 woc-app-multi ├─ bridge (:8088) 管理 2 个实例 ├─ Xvfb (:1) ├─ KasmVNC (:3000) ├─ 微信进程 (wx_a1) └─ chromium 进程 (xhs_b2) ``` 适用于:单用户、资源受限(NAS)、应用间需要协同(同一桌面)。 ### 10.2 多容器单应用(现有模式保留) ``` 容器 woc-wx- 微信实例 1 容器 woc-wx- 微信实例 2 容器 woc-xhs- 小红书实例 ``` 适用于:多用户隔离、横向扩展、单应用崩溃不影响其他。 ### 10.3 混合模式 面板支持两种模式并存: - 轻量场景用单容器多应用(省资源) - 隔离场景用多容器单应用(强隔离) 面板层提供创建实例时的"部署位置"选项:新建独立容器 or 加入已有容器的 bridge。 --- ## 11. 演进路径 ### S0:当前现状 - [bridge/server.py](../bridge/server.py) 约 2400 行单文件,全局单例 `_state`。 - 所有 `/api/*` 路由直接写死微信逻辑,无 `app_id`、无 driver 抽象。 - [docker/autostart](../docker/autostart) 一容器一应用;panel 通过 Docker API 管理生命周期。 ### S1:抽 core/ 与 driver 抽象(不改行为,本阶段重点) **目标**:把 server.py 拆成 `core/` + `drivers/wechat/`,建立 `AppDriver` 抽象,但保持单应用模式。 **改动**: - 新建 `core/` 目录,迁移 `config` / `context` / `errors` / `logging` / `models` / `send_queue` / `screenshot`。 - 抽 `XdotoolBase`(通用 X11 操作)到 `core/xdotool_base.py`。 - 定义 `drivers/base.py` 的 `AppDriver` 抽象基类。 - 微信逻辑迁移到 `drivers/wechat/`,`WechatDriver(AppDriver)` 用组合方式持有 `XdotoolBase`。 - 微信路由迁移到 `drivers/wechat/routes.py`。 - `main.py` 读 `WOC_APP_TYPE`,只注册一个 driver,创建一个 `AppInstance`(`app_id="default"`)。 **验证**:所有现有 API 行为不变,面板无感知;`pytest` 或现有集成测试全部通过。 ### S2:新路由层 + 向后兼容层 **目标**:引入 `/api/apps/{app_id}/*` 路由,旧路由作为 alias 转发。 **改动**: - 新增 `/api/apps/{app_id}/status`、`/api/apps/{app_id}/send/text` 等路由。 - 旧路径 `/api/messages`、`/api/send/text` 等转为 alias,转发到 `/api/apps/default/...`。 - `/api/status` 同时返回旧字段(取 default app)与新 `apps` 数组。 - 旧字段日志打 `DEPRECATED` warning。 **验证**:面板与外部调用方无需改动即可工作;新路径可通过 `/api/apps/default/*` 访问。 ### S3:UIScheduler 与 SendQueue 正交化 **目标**:解决多实例共享 X server 时的焦点/剪贴板冲突。 **改动**: - 新增 `core/ui_scheduler.py`,所有 X11 写操作经其串行执行。 - 每个 `AppInstance` 持有独立 `SendQueue`;`UIScheduler` 全局唯一。 - 单应用模式下行为不变,多实例模式下才体现串行价值。 **验证**:高并发调用 `/api/apps/default/send/text` 不再出现剪贴板污染。 ### S4:AppManager 与单容器多实例(未来扩展) **目标**:支持容器内注册多个 `AppInstance`。 **改动**: - 实现 `core/app_manager.py`、`core/app_instance.py`、`core/app_kind.py`。 - 新增 `/api/apps` CRUD 路由。 - `/config/apps.json` 持久化。 - 需同步改造 [docker/autostart](../docker/autostart) 以支持启动多个应用。 **验证**:可在同一容器内通过 API 注册并启动微信 + 小红书两个实例。 ### S5:新增 Xiaohongshu/Telegram Driver(框架可扩展性验证) **目标**:落地非微信应用自动化。 **改动**: - 实现 `drivers/xiaohongshu/driver.py` + `routes.py`(P0:publish/text、search)。 - 或实现 `drivers/telegram/driver.py`(优先浏览器/桌面版,若走原生协议则另议)。 **验证**:通过 `/api/apps/{app_id}/xhs/publish` 成功发笔记。 ### S6:面板适配与单容器多应用部署(不在本文档范围) **目标**:面板支持多应用管理 UI 与"单容器多应用"部署选项。 **改动**(panel 层): - 面板新增"应用实例"管理页。 - 桌面入口支持选择"进入哪个应用窗口"。 - 实例创建支持"新建独立容器"或"加入已有容器"。 ### S7:下线兼容层 **目标**:移除 alias,完成迁移。 **前提**:面板与所有已知适配器已切换到新路径。 **改动**: - 删除旧路径 alias。 - `/api/status` 移除旧字段。 - major 版本升级到 v3.0.0。 --- ## 12. 不做的事 ### 12.1 完全不做 1. **不引入应用间隐式依赖**:driver 之间不互相调用,编排由外部完成。 2. **不做多容器 bridge 集群**:一个 bridge 进程只管一个容器内的应用,跨容器调度由面板负责。 3. **不做应用沙箱**:应用间共享 X server 与文件系统(受 Linux 权限控制),不做额外隔离。 4. **不做 RBAC**:`app_id` 级别的权限控制留给面板层,bridge 层只做 Bearer Token 全局鉴权。 5. **不改 KasmVNC 串流层**:桌面串流保持现状(全屏共享),不按应用切分串流。 6. **不做 OCR**:不引入 tesseract,页面识别靠 URL + 坐标启发式。 ### 12.2 本阶段先不做 1. **不抽象 SQLCipher 解密**:解密能力暂留 `drivers/wechat/`,待第二个 SQLCipher 应用出现再抽公共层。 2. **不实现单容器多应用启动**:[docker/autostart](../docker/autostart) 当前只启动一个应用,改造它需要 panel/docker 同步调整,不在本阶段 bridge 文档范围。 3. **不新增非微信 driver**:先完成框架抽象与微信 driver 迁移,再落地 Xiaohongshu/Telegram driver。 4. **不替换全局 `SendQueue`**:S1 保持现有全局队列,S3 再下放到每个 `AppInstance`。 5. **不引入新依赖**:仍只使用 fastapi / uvicorn / pydantic / pillow / cryptography。 --- ## 13. 关键决策记录 | 决策点 | 选择 | 理由 | |--------|------|------| | 文档定位 | 目标架构 + `[现状]` 注释 | 既描述最终形态,又不掩盖当前代码未落地的事实 | | 本阶段重点 | 先让 bridge 在"多容器单应用"模式下可工作 | docker autostart 当前只支持一容器一应用;先完成 bridge 框架抽象,再扩展单容器多应用 | | 文档范围 | 只聚焦 bridge 层 | panel UI、docker 多应用启动机制另行设计 | | 抽象颗粒度 | 一应用一 driver(粗粒度) | 业务流程差异大,能力复用价值低 | | 容器内应用数 | 目标为多实例并存,当前先单实例 | 满足"一容器多应用"长期需求,但本阶段保留单实例运行 | | 路由前缀 | `/api/apps/{app_id}/*` + alias 过渡 | 实例独立寻址 + 向后兼容 | | XdotoolBase 风格 | 组合(driver 持有 `self.xd`) | 避免多继承混乱 | | SQLCipher 解密位置 | 留 drivers/wechat/ | YAGNI,等第二个应用再抽 | | X11 并发控制 | 全局 UIScheduler 串行 | 多应用争用同一 X server 必须串行;当前尚未实现 | | 应用间通信 | 显式编排(无直接调用) | 流程可观测、可调试 | | 部署模型 | 单容器多应用 + 多容器单应用并存 | 兼顾资源效率与隔离性;当前仅多容器单应用可用 |