WechatOnCloud/doc/优化方案/01-多应用桥接框架设计.md
Kris 67aab58a00 docs: 新增PRD规范与WechatOnCloud改造、多应用桥接框架文档
新增三份文档:
1. 产品需求文档(PRD)编写规范
2. WechatOnCloud容器化微信改造需求方案
3. 多应用桥接框架整体设计方案
2026-07-18 16:04:25 +08:00

940 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 多应用桥接框架设计
> 目标:将 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/<wxid>/ 微信数据 │
│ ├─ 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 UISchedulerX11 操作调度器)
**问题**:多应用共用一个 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/<wxid>/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-<id1> 微信实例 1
容器 woc-wx-<id2> 微信实例 2
容器 woc-xhs-<id3> 小红书实例
```
适用于:多用户隔离、横向扩展、单应用崩溃不影响其他。
### 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/*` 访问。
### S3UIScheduler 与 SendQueue 正交化
**目标**:解决多实例共享 X server 时的焦点/剪贴板冲突。
**改动**
- 新增 `core/ui_scheduler.py`,所有 X11 写操作经其串行执行。
- 每个 `AppInstance` 持有独立 `SendQueue``UIScheduler` 全局唯一。
- 单应用模式下行为不变,多实例模式下才体现串行价值。
**验证**:高并发调用 `/api/apps/default/send/text` 不再出现剪贴板污染。
### S4AppManager 与单容器多实例(未来扩展)
**目标**:支持容器内注册多个 `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`P0publish/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 必须串行;当前尚未实现 |
| 应用间通信 | 显式编排(无直接调用) | 流程可观测、可调试 |
| 部署模型 | 单容器多应用 + 多容器单应用并存 | 兼顾资源效率与隔离性;当前仅多容器单应用可用 |