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