WechatOnCloud/doc/优化方案/01-多应用桥接框架设计.md

940 lines
41 KiB
Markdown
Raw Normal View History

# 多应用桥接框架设计
> 目标:将 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 必须串行;当前尚未实现 |
| 应用间通信 | 显式编排(无直接调用) | 流程可观测、可调试 |
| 部署模型 | 单容器多应用 + 多容器单应用并存 | 兼顾资源效率与隔离性;当前仅多容器单应用可用 |