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

1095 lines
34 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.

# WechatOnCloud 改造需求方案
> 版本v2.0
> 日期2026-07-05
> 状态:方案待评审
> 变更:基于 channels 渠道插件协议深度调研,重新设计 API 契约,新增两条改造路径对比
## 一、背景与目标
### 1.1 现状
[WechatOnCloud](file:///d:/ForcePilot-v1.1/docs/source-code/WechatOnCloud-main) 是一个容器化的「服务端微信」项目,核心能力是:
- 在 Docker 容器里运行官方 Linux 原版微信(`/config/wechat/opt/wechat/wechat`
- 通过 Xvfb 虚拟显示 + KasmVNC 把桌面画面串流到浏览器
- 面板panel作为唯一对外入口管理多实例、子账号权限、反向代理
- **不修改微信客户端**,依靠 xdotool/xclip 已具备 UI 自动化基础设施
但 WechatOnCloud 目前**只暴露了 KasmVNC 的桌面串流3000 端口)**,没有提供任何业务 API。外部系统无法通过 HTTP 调用「发消息 / 读消息 / 查联系人」等能力。
### 1.2 目标
改造 WechatOnCloud使其在保留现有桌面串流能力的基础上**原生提供业务 HTTP API**,供 ForcePilot channels 模块集成,实现个人微信的自动收发消息闭环。
### 1.3 核心诉求
| 能力 | 优先级 | channels 对接的 adapter | 说明 |
|---|---|---|---|
| 发送文本消息 | P0 | OutboundAdapter | 通过 xdotool 操作微信窗口发送文本 |
| 拉取增量消息 | P0 | PullerAdapter | 通过读取微信本地 DB 获取新消息 |
| 查询实例状态 | P0 | ProbeableAdapter / StatusAdapter | 微信窗口状态、登录态、bridge 服务健康 |
| 扫码登录 | P0 | LoginAdapter | 复用 KasmVNC 桌面串流 + 截图二维码 |
| 联系人/群聊查询 | P1 | SessionAdapter / DirectoryAdapter | 通过 DB 查询 |
| 发送图片/文件 | P1 | OutboundAdapter | 通过 xdotool 模拟拖拽 |
| 下载媒体文件 | P1 | InboundAdapter.downloadAttachment | 读取微信媒体文件并返回 |
| 诊断 | P1 | DoctorAdapter | 连通性、登录态、DB 可达性诊断 |
| 多账号管理 | P2 | LifecycleAdapter | 一个面板管理多个实例 |
### 1.4 非目标
- **不修改微信客户端本身**(保持「官方原版」的核心原则)
- **不实现微信私有协议**(不做协议逆向)
- **不替代 KasmVNC 桌面串流**(保留人工查看/扫码登录入口)
- **不直接对接 channels**channels 侧插件开发是独立项目)
---
## 二、channels 渠道插件协议调研结论
### 2.1 channels 插件需要实现的 Adapter
channels 一个完整渠道插件需实现以下 adapter参考 [wechat_ilink](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/plugins/wechat_ilink/) 参考实现):
| Adapter | 必选性 | 核心方法 | 对 WechatOnCloud 的 API 需求 |
|---|---|---|---|
| **PullerAdapter** | 必选 | `poll(account_id, cursor)` / `getPollingConfig()` | 增量消息拉取接口 |
| **InboundAdapter** | 必选 | `normalizeInbound(raw_event)` / `verifySignature` / `downloadAttachment` | 消息格式归一化由插件层做bridge 只提供原始数据) |
| **OutboundAdapter** | 必选 | `sendMessage(account_id, peer_id, payload)` / `formatOutbound` | 发送消息接口 |
| **SessionAdapter** | 必选 | `parseSession(raw_event)` / `getPeerId` / `getChatType` | 联系人/会话查询接口 |
| **StatusAdapter** | 必选 | `classifyEvent(raw_event)` / `extractStatusPayload` | 状态事件分类(由插件层做) |
| **LoginAdapter** | 必选 | `loginWithQrStart` / `loginWithQrWait` / `logout` | 二维码获取 + 扫码状态轮询 |
| **LifecycleAdapter** | 必选 | `resolveAccountId` / `applyAccountConfig` / `afterAccountConfigWritten` | 账户生命周期管理 |
| **ProbeableAdapter** | 必选 | `probe()` | 探活接口 |
| **DoctorAdapter** | 必选 | `checkConnectivity` / `getDiagnosticItems` / `runItem` / `autoFix` | 诊断接口 |
| **WizardAdapter** | 必选 | `getWizardSteps` / `validateStep` / `applyStep` | 配置向导(由插件层做) |
| **WhitelistAdapter** | 可选 | `getWhitelist` / `addToWhitelist` / `removeFromWhitelist` | 白名单(存储在 channels 侧,无需 bridge |
### 2.2 channels 关键 DTO 字段
bridge 返回的数据需能映射到以下 DTO
#### PollResultPullerAdapter.poll 返回值)
```python
@dataclass(frozen=True)
class PollResult:
messages: tuple[dict[str, Any], ...] # 原始消息列表
next_cursor: str | None # 下次轮询的游标
error: TransportError | None # 错误信息
```
#### MessageContentInboundAdapter.normalizeInbound 返回值)
```python
@dataclass(frozen=True)
class MessageContent:
text: str
format: MessageFormat = MessageFormat.TEXT # TEXT / MARKDOWN / RICH
attachments: tuple[Attachment, ...] = ()
metadata: dict[str, Any] | None = None
@dataclass(frozen=True)
class Attachment:
type: Literal["image", "file", "audio", "video"]
url: str
mime_type: str | None = None
size: int | None = None
filename: str | None = None
width: int | None = None
height: int | None = None
duration_ms: int | None = None
```
#### SessionInfoSessionAdapter.parseSession 返回值)
```python
@dataclass(frozen=True)
class SessionInfo:
channel_type: ChannelType
account_id: str
peer_id: str # 聊天对象 wxid
chat_type: Literal["p2p", "group"]
group_id: str | None = None # 群聊 ID
topic_id: str | None = None # 话题 ID群内话题
```
#### QrLoginStartResult / QrLoginWaitResultLoginAdapter
```python
@dataclass(frozen=True)
class QrLoginStartResult:
qr_data_url: str # 二维码图片 data URL
message: str
connected: bool
@dataclass(frozen=True)
class QrLoginWaitResult:
connected: bool
message: str
qr_data_url: str | None
credentials: dict[str, str] | None # 登录成功后返回的凭据
```
### 2.3 ChannelManifest 关键字段
参考 [wechat_ilink/manifest.json](file:///d:/ForcePilot-v1.1/backend/package/yuxi/channels/plugins/wechat_ilink/manifest.json)WechatOnCloud 插件的 manifest 需声明:
| 字段 | wechat_ilink 的值 | WechatOnCloud 建议值 |
|---|---|---|
| `id` | `com.yuxi.channels.wechat_ilink` | `com.yuxi.channels.wechat_woc` |
| `channel_type` | `wechat_ilink` | `wechat_woc` |
| `credential_strategy.type` | `dynamic` | `static` |
| `credential_strategy.acquire_method` | `qr_login` | `manual` |
| `credential_strategy.required_fields` | `["bot_token", "ilink_bot_id", ...]` | `["bridge_url"]` |
| `capabilities.supports_qr_login` | `true` | `true`(通过 KasmVNC 截图) |
| `capabilities.doctor` | `true` | `true` |
| `capabilities.probeable` | `true` | `true` |
| `capabilities.wizard` | `true` | `true` |
| `capabilities.lifecycle` | `true` | `true` |
| `max_message_length` | `4000` | `2000`(个人微信限制) |
| `config_schema` | 12 字段 | 见 §5.2 |
---
## 三、两条改造路径对比
### 路径 A协议兼容WechatOnCloud 实现 iLink 协议)
**思路**:让 WechatOnCloud 的 bridge 服务实现与 iLink 完全相同的 8 个 HTTP 端点wechat_ilink 插件零改动。
| 端点 | iLink 协议 | WechatOnCloud bridge 实现 |
|---|---|---|
| `POST /ilink/bot/getupdates` | 长轮询拉取消息 | 读 DB 增量消息,构造相同响应结构 |
| `POST /ilink/bot/sendmessage` | 发送消息 | 调 xdotool 发送,返回 `client_id` |
| `GET /ilink/bot/get_bot_qrcode` | 获取二维码 | 截图微信窗口,返回二维码 data URL |
| `GET /ilink/bot/get_qrcode_status` | 查询扫码状态 | 检测微信登录态 |
| `POST /ilink/bot/getconfig` | 探活 | 返回微信账号信息 |
| `POST /ilink/bot/getuploadurl` | CDN 上传地址 | 本地文件路径直通 |
| CDN 上传端点 | 加密上传 | 本地文件写入 |
| CDN 下载端点 | 加密下载 | 本地文件读取 |
**优点**
- channels 侧零改动,复用现有 wechat_ilink 插件
- 上线最快
**缺点**
- 需逆向 iLink 协议的请求/响应结构、鉴权头、错误码
- iLink 协议有加密AES-128-ECB、防重放头、IDC 重定向等复杂逻辑
- 协议绑定深iLink 协议变更时 WechatOnCloud 也需跟着改
- WechatOnCloud 的能力被 iLink 协议天花板限制
- **不推荐**:工程量未必更小,且引入协议兼容包袱
### 路径 B原生 API + 新建插件(推荐)
**思路**WechatOnCloud 设计自己的原生 RESTful APIchannels 侧新建 `wechat_woc` 插件对接。
**优点**
- API 设计自由,贴合 WechatOnCloud 实际能力
- 不背 iLink 协议包袱
- channels 插件层职责清晰
- 长期可维护性好
**缺点**
- 需新增 channels 插件(约 1000-1500 行 Python
- 上线周期稍长
### 推荐结论
**采用路径 B**。原因:
1. iLink 协议的加密、防重放、IDC 重定向等复杂逻辑在 WechatOnCloud 场景下完全无意义
2. WechatOnCloud 是本地容器,无需 CDN 加密上传下载,直接读本地文件即可
3. 新增 channels 插件的成本可控,且能获得更干净的设计
4. 后续 WechatOnCloud 能力扩展(如群聊、媒体)不被 iLink 协议束缚
---
## 四、改造方案(路径 B
### 4.1 总体架构
```
┌──────────────────────────────────────────────────────────────┐
│ WechatOnCloud 实例容器 (woc-wx-<id>) │
│ │
│ s6-rc 服务集 │
│ ┌────────────────────┐ ┌──────────────────────────────┐ │
│ │ init / Xvfb │ │ svc-woc-bridge (新增 longrun)│ │
│ │ openbox autostart │ │ ┌────────────────────────┐ │ │
│ │ KasmVNC (:3000) │ │ │ woc-bridge (FastAPI) │ │ │
│ │ wechat (主进程) │ │ │ :8088 │ │ │
│ └────────────────────┘ │ │ ├─ /api/status │ │ │
│ ▲ │ │ ├─ /api/messages/* │ │ │
│ │ X11 :1 │ │ ├─ /api/send/* │ │ │
│ └────────────────┼──┤ ├─ /api/contacts/* │ │ │
│ │ │ ├─ /api/media/* │ │ │
│ │ │ ├─ /api/login/* │ │ │
│ │ │ └─ /api/diagnostic/* │ │ │
│ │ └────────────────────────┘ │ │
│ └──────────────────────────────┘ │
│ │
│ /config/.config/xwechat/ ←── 微信 DB │
└──────────────────────────────────────────────────────────────┘
↑ ↑
│ 3000 (桌面串流) │ 8088 (业务 API)
│ │
┌──────────────────────────────────────────────────────────────┐
│ 面板容器 (woc-panel) │
│ 反代 /desktop/:id/* → :3000 │
│ 反代 /api/bridge/:id/* → :8088 ← 新增 │
└──────────────────────────────────────────────────────────────┘
```
### 4.2 改动清单
| 文件/目录 | 改动类型 | 说明 |
|---|---|---|
| `bridge/` | **新增** | FastAPI 服务,提供业务 API |
| `bridge/server.py` | 新增 | API 主入口 |
| `bridge/xdotool_driver.py` | 新增 | xdotool 操作封装 |
| `bridge/db_reader.py` | 新增 | 微信 DB 读取 |
| `bridge/qr_capture.py` | 新增 | 微信窗口二维码截图 |
| `bridge/send_queue.py` | 新增 | 串行化发送队列 |
| `bridge/requirements.txt` | 新增 | Python 依赖 |
| `bridge/s6/woc-bridge/run` | 新增 | s6 longrun 启动脚本 |
| `bridge/s6/woc-bridge/type` | 新增 | 内容为 `longrun` |
| `docker/Dockerfile` | **修改** | 安装 Python + 依赖COPY bridge注册 s6 服务EXPOSE 8088 |
| `panel/server/src/docker.ts` | **修改** | `ExposedPorts` 增加 `8088/tcp` |
| `panel/server/src/index.ts` | **修改** | 新增 `/api/bridge/:id/*` 反代路由 |
| `.env.example` | **修改** | 增加 `WOC_BRIDGE_*` 配置项 |
---
## 五、woc-bridge HTTP API 契约
### 5.1 通用约定
- 所有响应 `Content-Type: application/json`(媒体下载除外)
- 所有时间戳为 Unix 秒UTC
- 所有 `wxid` 为字符串
- 错误响应统一结构:
```json
{
"success": false,
"error": {
"code": "WECHAT_NOT_RUNNING",
"message": "微信窗口未找到",
"details": {}
}
}
```
### 5.2 状态接口(对应 ProbeableAdapter
```
GET /api/status
```
**响应**
```json
{
"bridge_version": "1.0.0",
"wechat_running": true,
"wechat_window_found": true,
"login_state": "logged_in",
"db_accessible": true,
"current_wxid": "wxid_abc123",
"current_nickname": "张三",
"uptime_seconds": 3600,
"display": ":1"
}
```
**`login_state` 枚举**`not_running` / `not_logged_in` / `logging_in` / `logged_in` / `logged_out`
**对应 channels 调用**`ProbeableAdapter.probe()`、`DoctorAdapter.checkConnectivity()`
### 5.3 消息拉取接口(对应 PullerAdapter
```
GET /api/messages/since?cursor=<timestamp>&limit=50
```
**参数**
- `cursor`:上次拉取的最后一条消息的 `create_time`Unix 秒),首次传 `0`
- `limit`:单次拉取上限,默认 50最大 200
**响应**
```json
{
"messages": [
{
"msg_id": "wxid_abc_1700000000_123",
"talker": "wxid_abc123",
"sender": "wxid_sender",
"is_sender": false,
"type": 1,
"render_type": "text",
"content": "你好",
"create_time": 1700000000,
"session_type": "p2p"
}
],
"next_cursor": "1700000123",
"has_more": false
}
```
**消息类型映射**`type` + `render_type`
| 微信 type | render_type | channels MessageFormat | 说明 |
|---|---|---|---|
| 1 | `text` | TEXT | 文本消息 |
| 3 | `image` | TEXT + Attachment(image) | 图片消息 |
| 34 | `voice` | TEXT + Attachment(audio) | 语音消息 |
| 43 | `video` | TEXT + Attachment(video) | 视频消息 |
| 49 | `file` | TEXT + Attachment(file) | 文件消息 |
| 49 | `link` | TEXT | 链接卡片 |
| 49 | `card` | TEXT | 名片 |
| 10002 | `system` | TEXT | 系统消息 |
| 10000 | `system` | TEXT | 系统消息(撤回等) |
**对应 channels 调用**`PullerAdapter.poll(account_id, cursor)` → `PollResult`
### 5.4 发送消息接口(对应 OutboundAdapter
#### 5.4.1 发送文本
```
POST /api/send/text
Content-Type: application/json
{
"to_wxid": "wxid_abc123",
"content": "你好"
}
```
**响应**
```json
{
"success": true,
"channel_msg_id": "local_1700000000_123",
"error": null
}
```
#### 5.4.2 发送图片
```
POST /api/send/image
Content-Type: application/json
{
"to_wxid": "wxid_abc123",
"file_path": "/config/Desktop/image.jpg"
}
```
#### 5.4.3 发送文件
```
POST /api/send/file
Content-Type: application/json
{
"to_wxid": "wxid_abc123",
"file_path": "/config/Desktop/doc.pdf"
}
```
**对应 channels 调用**`OutboundAdapter.sendMessage(account_id, peer_id, payload)` → `channel_msg_id`
### 5.5 联系人接口(对应 SessionAdapter / DirectoryAdapter
#### 5.5.1 查询联系人
```
GET /api/contacts?keyword=张三&limit=50
```
**响应**
```json
{
"contacts": [
{
"wxid": "wxid_abc123",
"nickname": "张三",
"remark": "张三-客户",
"avatar_url": "/api/media/avatar/wxid_abc123",
"type": "friend"
}
],
"total": 1
}
```
#### 5.5.2 联系人详情
```
GET /api/contacts/{wxid}
```
#### 5.5.3 群聊列表
```
GET /api/groups?limit=50
```
#### 5.5.4 群成员
```
GET /api/groups/{wxid}/members
```
**响应**
```json
{
"group_wxid": "12345@chatroom",
"members": [
{
"wxid": "wxid_abc123",
"nickname": "张三",
"display_name": "张三",
"is_admin": false
}
],
"total": 50
}
```
**对应 channels 调用**`SessionAdapter.parseSession()`、`DirectoryAdapter.getGroupMembers()`
### 5.6 媒体接口(对应 InboundAdapter.downloadAttachment
```
GET /api/media/{msg_id}
```
返回二进制文件流(`Content-Type` 根据文件类型设置)。
**对应 channels 调用**`InboundAdapter.downloadAttachment(attachment)` → `Attachment`
### 5.7 登录接口(对应 LoginAdapter
#### 5.7.1 获取登录二维码
```
POST /api/login/qr/start
```
**响应**
```json
{
"qr_data_url": "data:image/png;base64,iVBOR...",
"message": "请使用微信扫描二维码",
"connected": false
}
```
**实现**bridge 截图微信窗口,裁剪出二维码区域,编码为 base64 data URL。
#### 5.7.2 轮询扫码状态
```
GET /api/login/qr/wait?timeout=30
```
**响应**
```json
{
"connected": false,
"message": "等待扫码",
"qr_data_url": null,
"credentials": null
}
```
扫码成功时:
```json
{
"connected": true,
"message": "登录成功",
"qr_data_url": null,
"credentials": {
"wxid": "wxid_abc123",
"nickname": "张三"
}
}
```
**对应 channels 调用**`LoginAdapter.loginWithQrStart()` / `loginWithQrWait()`
### 5.8 诊断接口(对应 DoctorAdapter
#### 5.8.1 连通性检查
```
GET /api/diagnostic/connectivity
```
**响应**
```json
{
"reachable": true,
"latency_ms": 50,
"error": null
}
```
#### 5.8.2 诊断项列表
```
GET /api/diagnostic/items
```
**响应**
```json
{
"items": [
{
"check_id": "wechat_running",
"name": "微信进程检查",
"severity": "critical",
"description": "检查微信客户端是否运行",
"auto_repairable": true
},
{
"check_id": "login_state",
"name": "登录状态检查",
"severity": "critical",
"description": "检查微信是否已登录",
"auto_repairable": false
},
{
"check_id": "db_accessible",
"name": "数据库可达性",
"severity": "error",
"description": "检查微信 DB 是否可读",
"auto_repairable": false
},
{
"check_id": "xdotool_available",
"name": "xdotool 可用性",
"severity": "error",
"description": "检查 xdotool 是否可用",
"auto_repairable": true
}
]
}
```
#### 5.8.3 执行诊断项
```
POST /api/diagnostic/run/{check_id}
```
**响应**
```json
{
"check_id": "wechat_running",
"passed": true,
"severity": "critical",
"message": "微信进程运行中PID=12345",
"auto_repairable": true,
"repair_plan": null
}
```
#### 5.8.4 自动修复
```
POST /api/diagnostic/autofix/{check_id}
```
**响应**
```json
{
"check_id": "wechat_running",
"passed": true,
"message": "已重启微信进程"
}
```
**对应 channels 调用**`DoctorAdapter.checkConnectivity()` / `getDiagnosticItems()` / `runItem()` / `autoFix()`
### 5.9 截图接口(调试用)
```
POST /api/screenshot
```
返回当前微信窗口截图PNG
---
## 六、错误码枚举
| 错误码 | 说明 | HTTP 状态 |
|---|---|---|
| `WECHAT_NOT_RUNNING` | 微信进程未启动 | 503 |
| `WECHAT_NOT_LOGGED_IN` | 微信未登录 | 401 |
| `WINDOW_NOT_FOUND` | 找不到微信窗口 | 503 |
| `CONTACT_NOT_FOUND` | 联系人不存在 | 404 |
| `SEND_FAILED` | 发送失败 | 500 |
| `DB_LOCKED` | DB 被锁定 | 503 |
| `DB_NOT_FOUND` | DB 文件不存在 | 500 |
| `INVALID_PARAMS` | 参数校验失败 | 400 |
| `BRIDGE_INTERNAL_ERROR` | bridge 内部错误 | 500 |
| `LOGIN_TIMEOUT` | 登录超时 | 408 |
| `MEDIA_NOT_FOUND` | 媒体文件不存在 | 404 |
| `RATE_LIMITED` | 触发限流 | 429 |
---
## 七、配置项
### 7.1 新增环境变量
`.env.example` 追加:
```bash
# ===== woc-bridge 业务 API =====
WOC_BRIDGE_PORT=8088
WOC_BRIDGE_SEND_DELAY_MS=800
WOC_BRIDGE_MAX_BATCH_SIZE=50
WOC_BRIDGE_POLL_INTERVAL_MS=2000
WOC_BRIDGE_MAX_CALLS_PER_SEC=10
```
### 7.2 channels 侧 config_schema参考
WechatOnCloud 插件的 `config_schema`(由 channels 侧 manifest 声明,非 WechatOnCloud 侧):
| key | type | required | default | hot_reloadable | 说明 |
|---|---|---|---|---|---|
| `bridge_url` | str | true | - | true | bridge 服务地址 |
| `poll_interval_ms` | int | false | 2000 | true | 轮询间隔 |
| `max_batch_size` | int | false | 50 | true | 单次拉取上限 |
| `send_delay_ms` | int | false | 800 | true | 发送间隔 |
| `max_message_length` | int | false | 2000 | true | 最大消息长度 |
| `enable_media` | bool | false | true | true | 是否启用媒体下载 |
| `enable_group` | bool | false | true | true | 是否启用群聊支持 |
---
## 八、Dockerfile 改造
在 [docker/Dockerfile](file:///d:/ForcePilot-v1.1/docs/source-code/WechatOnCloud-main/docker/Dockerfile) 现有内容基础上追加:
```dockerfile
# ===== 新增woc-bridge 业务 API 服务 =====
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv \
sqlite3; \
pip3 install --break-system-packages --no-cache-dir \
fastapi==0.115.0 \
uvicorn[standard]==0.30.0 \
python-multipart==0.0.9 \
pillow==10.4.0; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*
COPY bridge/ /opt/woc-bridge/
RUN chmod 755 /opt/woc-bridge/server.py
# 注册为 s6-rc longrun 服务
RUN mkdir -p /etc/s6-overlay/s6-rc.d/svc-woc-bridge
COPY bridge/s6/woc-bridge/run /etc/s6-overlay/s6-rc.d/svc-woc-bridge/run
COPY bridge/s6/woc-bridge/type /etc/s6-overlay/s6-rc.d/svc-woc-bridge/type
RUN chmod 755 /etc/s6-overlay/s6-rc.d/svc-woc-bridge/run \
&& touch /etc/s6-overlay/s6-rc.d/user/contents.d/svc-woc-bridge
EXPOSE 3000 3001 8088
```
---
## 九、s6 启动脚本
`bridge/s6/woc-bridge/run`
```bash
#!/usr/bin/with-contenv bash
# 等待微信窗口出现
while ! xdotool search --name "微信" >/dev/null 2>&1; do
sleep 2
done
# 以 abc 用户身份运行(与微信同 X 会话)
exec s6-setuidgid abc \
DISPLAY=${DISPLAY:-:1} \
XAUTHORITY=/config/.Xauthority \
python3 /opt/woc-bridge/server.py \
--listen 0.0.0.0:8088 \
--wechat-db /config/.config/xwechat \
--display ${DISPLAY:-:1}
```
---
## 十、面板端口暴露改造
[panel/server/src/docker.ts](file:///d:/ForcePilot-v1.1/docs/source-code/WechatOnCloud-main/panel/server/src/docker.ts) L256
```typescript
// 改造前
ExposedPorts: { '3000/tcp': {} },
// 改造后
ExposedPorts: {
'3000/tcp': {},
'8088/tcp': {},
},
```
---
## 十一、面板反代改造
[panel/server/src/index.ts](file:///d:/ForcePilot-v1.1/docs/source-code/WechatOnCloud-main/panel/server/src/index.ts) 新增反代路由:
```typescript
// ---------- 反向代理到实例的 woc-bridge 业务 API ----------
// /api/bridge/:id/* → http://woc-wx-<id>:8088/*
// 仅管理员可访问(业务 API 等同微信会话凭据)
const bridgeHandler = (req: FastifyRequest, reply: FastifyReply) => {
if (!requireAdmin(req, reply)) return;
const parsed = parseBridgeUrl(req.raw.url || '');
if (!parsed) {
reply.code(404).send({ error: 'not found' });
return;
}
const inst = findInstance(parsed.id);
if (!inst) {
reply.code(404).send({ error: '实例不存在' });
return;
}
reply.hijack();
req.raw.url = parsed.rest;
proxy.web(req.raw, reply.raw, {
target: `http://${inst.containerName}:8088`,
});
};
function parseBridgeUrl(rawUrl: string): { id: string; rest: string } | null {
const m = rawUrl.match(/^\/api\/bridge\/([0-9a-f]{6,})(\/.*|\?.*|)?$/);
if (!m) return null;
const id = m[1];
let rest = m[2] || '/';
if (rest.startsWith('?')) rest = '/' + rest;
if (rest === '') rest = '/';
return { id, rest };
}
app.all('/api/bridge/:id', bridgeHandler);
app.all('/api/bridge/:id/*', bridgeHandler);
```
---
## 十二、关键技术实现细节
### 12.1 发送文本消息xdotool + xclip
```python
async def send_text(to_wxid: str, content: str) -> str:
"""通过 xdotool 操作微信窗口发送文本消息"""
# 1. 激活微信窗口
subprocess.run(
["xdotool", "search", "--name", "微信", "windowactivate", "--sync"],
check=True,
)
# 2. Ctrl+F 打开搜索
subprocess.run(["xdotool", "key", "ctrl+f"], check=True)
await asyncio.sleep(0.5)
# 3. 用 xclip 粘贴 wxid避免输入法干扰
_paste_via_xclip(to_wxid)
await asyncio.sleep(0.8)
# 4. 回车进入会话
subprocess.run(["xdotool", "key", "Return"], check=True)
await asyncio.sleep(0.5)
# 5. 粘贴消息内容
_paste_via_xclip(content)
await asyncio.sleep(0.2)
# 6. 回车发送
subprocess.run(["xdotool", "key", "Return"], check=True)
# 7. 从 DB 读取刚发送的消息 ID
return await _query_last_sent_msg_id(to_wxid)
def _paste_via_xclip(text: str) -> None:
"""通过 xclip 剪贴板粘贴,避开 X11 keysym 中文限制"""
b64 = base64.b64encode(text.encode("utf-8")).decode()
subprocess.run(
["bash", "-c", f"echo '{b64}' | base64 -d | xclip -selection clipboard -i"],
check=True,
)
subprocess.run(["xdotool", "key", "--clearmodifiers", "ctrl+v"], check=True)
```
### 12.2 读取消息DB 快照)
```python
async def get_messages_since(cursor: int, limit: int = 50) -> dict:
"""从微信 DB 读取增量消息"""
db_path = "/config/.config/xwechat/msg_0.db"
snapshot = _snapshot_db(db_path)
conn = sqlite3.connect(snapshot)
try:
rows = conn.execute(
"SELECT msg_id, talker, is_sender, type, content, create_time "
"FROM message WHERE create_time > ? ORDER BY create_time ASC LIMIT ?",
(cursor, limit),
).fetchall()
finally:
conn.close()
os.unlink(snapshot)
messages = [_row_to_dict(r) for r in rows]
next_cursor = str(messages[-1]["create_time"]) if messages else str(cursor)
return {
"messages": messages,
"next_cursor": next_cursor,
"has_more": len(messages) == limit,
}
```
### 12.3 二维码截图
```python
async def capture_qr_code() -> str:
"""截图微信窗口的二维码区域,返回 data URL"""
# 1. 激活微信窗口
subprocess.run(
["xdotool", "search", "--name", "微信", "windowactivate", "--sync"],
check=True,
)
# 2. 截图整个窗口
window_geom = subprocess.check_output(
["xdotool", "search", "--name", "微信", "getwindowgeometry", "--shell"]
).decode()
# 3. 用 import 命令截图
screenshot_path = "/tmp/woc_qr.png"
subprocess.run(["import", "-window", "root", screenshot_path], check=True)
# 4. 用 Pillow 裁剪二维码区域(需根据微信窗口布局定位)
img = Image.open(screenshot_path)
# 二维码通常在窗口中央偏上,具体坐标需实测
qr_region = (x, y, x + w, y + h)
qr_img = img.crop(qr_region)
# 5. 编码为 base64 data URL
buf = io.BytesIO()
qr_img.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode()
return f"data:image/png;base64,{b64}"
```
### 12.4 发送队列串行化
```python
class SendQueue:
"""串行化发送队列,避免 xdotool 操作冲突"""
def __init__(self, send_delay_ms: int = 800):
self._queue: asyncio.Queue = asyncio.Queue()
self._send_delay_ms = send_delay_ms
self._worker: asyncio.Task | None = None
async def start(self):
self._worker = asyncio.create_task(self._run())
async def enqueue(self, task: SendTask) -> SendResult:
future = asyncio.get_event_loop().create_future()
await self._queue.put((task, future))
return await future
async def _run(self):
while True:
task, future = await self._queue.get()
try:
result = await task.execute()
future.set_result(result)
except Exception as e:
future.set_exception(e)
await asyncio.sleep(self._send_delay_ms / 1000)
```
---
## 十三、安全设计
### 13.1 鉴权
- bridge API 反代路由 `/api/bridge/:id/*` **仅管理员可访问**
- bridge 服务本身监听 `0.0.0.0:8088`,但在 docker 网络内,只有面板能访问
- 可选bridge 支持 API Key 鉴权(通过环境变量配置)
### 13.2 风控策略
- 发送消息串行化,避免并发
- 可配置发送间隔(默认 800ms
- 单实例每秒发送上限(默认 10 条)
- 异常时自动降速或暂停
### 13.3 数据安全
- DB 只读访问,不写入
- 不记录消息内容到日志
- 截图功能仅管理员可用
- 不暴露微信凭据
---
## 十四、风险与限制
| 风险 | 影响 | 应对 |
|---|---|---|
| xdotool 操作时序不稳定 | 发送失败或发错人 | 用 wxid 而非昵称;加状态检测;失败重试;记录截图 |
| 微信 DB 加密 | 无法直接读消息 | 阶段 1 用 UI 截图 + OCR阶段 2 引入 `libwcdb.so` |
| 微信窗口未就绪 | bridge 启动失败 | s6 longrun 等待窗口出现;健康检查 |
| 中文输入异常 | 发送乱码 | 强制走 xclip 粘贴路径 |
| 多消息并发 | UI 操作冲突 | bridge 内部串行化发送队列 |
| Linux 微信版本更新 | DB 结构或快捷键变化 | 固定版本;建立版本适配测试 |
| 二维码区域定位 | 截图裁剪坐标不准 | 实测校准;提供手动框选 fallback |
| bridge 崩溃影响微信 | 微信异常 | 独立进程s6 隔离重启 |
---
## 十五、实施路线
### 阶段 1MVP1-2 周)
**目标**:跑通"收消息 → 发消息"最小闭环
- [ ] 实现 `bridge/server.py` FastAPI 骨架
- [ ] 实现 `GET /api/status`
- [ ] 实现 `POST /api/send/text`xdotool + xclip
- [ ] 实现 `GET /api/messages/since`DB 读取)
- [ ] 改造 Dockerfile + s6 服务
- [ ] 改造面板端口暴露 + 反代
- [ ] 端到端验证:能收到消息、能发出消息
### 阶段 2登录与完善2-3 周)
- [ ] 实现 `POST /api/login/qr/start` + `GET /api/login/qr/wait`
- [ ] 联系人/群聊查询
- [ ] 图片/文件发送
- [ ] 媒体文件下载
- [ ] 发送队列 + 限流
- [ ] 错误恢复 + 重试
- [ ] 截图诊断
### 阶段 3稳定性持续
- [ ] 健康检查 + 自动恢复
- [ ] 监控告警
- [ ] 多账号支持验证
- [ ] DB 加密适配(如需)
- [ ] 面板 UI 集成(可选)
---
## 十六、验收标准
### 16.1 阶段 1 验收
| 验收项 | 标准 |
|---|---|
| bridge 服务启动 | 容器启动后 30s 内 bridge 就绪,`GET /api/status` 返回 200 |
| 发送文本消息 | 调用 `POST /api/send/text`,对方在 5s 内收到消息 |
| 拉取消息 | 调用 `GET /api/messages/since`,能返回最近 50 条消息 |
| 桌面串流不受影响 | 原有 KasmVNC 访问正常,扫码登录正常 |
| 面板反代可用 | 通过 `/api/bridge/:id/*` 能访问到 bridge API |
### 16.2 阶段 2 验收
| 验收项 | 标准 |
|---|---|
| 扫码登录 | 能获取二维码 data URL扫码后返回 `connected=true` |
| 联系人查询 | 能按 wxid 或昵称查到联系人 |
| 图片发送 | 能发送图片,对方收到且可查看 |
| 媒体下载 | 能下载图片/语音/视频文件 |
| 并发安全 | 10 条消息连续发送不冲突 |
| 错误恢复 | bridge 崩溃后 10s 内自动重启 |
---
## 十七、附录
### 17.1 文件清单
```
WechatOnCloud-main/
├── bridge/ ← 新增
│ ├── server.py # FastAPI 主服务
│ ├── xdotool_driver.py # xdotool 操作封装
│ ├── db_reader.py # 微信 DB 读取
│ ├── qr_capture.py # 二维码截图
│ ├── send_queue.py # 串行化发送队列
│ ├── models.py # 数据模型
│ ├── requirements.txt
│ └── s6/
│ └── woc-bridge/
│ ├── run # s6 启动脚本
│ └── type # 内容为 "longrun"
├── docker/
│ └── Dockerfile ← 修改(追加 Python + bridge
├── panel/
│ └── server/src/
│ ├── docker.ts ← 修改ExposedPorts 加 8088
│ └── index.ts ← 修改(加 /api/bridge/:id/* 反代)
├── .env.example ← 修改(加 WOC_BRIDGE_* 配置)
└── doc/
└── bridge-API.md ← 新增API 文档)
```
### 17.2 不改动的文件
- `docker/autostart` —— bridge 由 s6 管理,不依赖 openbox autostart
- `docker/woc-identity.sh` —— 设备伪装逻辑不变
- `docker/wechat-ctl.sh` —— 微信下载安装逻辑不变
- `docker/app-defs.sh` —— 应用定义不变
- 面板前端代码 —— bridge 状态展示为可选项,非必须
### 17.3 channels 侧 config_schema 与 WechatOnCloud 侧环境变量对应关系
| channels config_schema key | WechatOnCloud 环境变量 | 说明 |
|---|---|---|
| `bridge_url` | 无(由面板反代地址决定) | channels 侧配置 |
| `poll_interval_ms` | `WOC_BRIDGE_POLL_INTERVAL_MS` | 默认 2000 |
| `max_batch_size` | `WOC_BRIDGE_MAX_BATCH_SIZE` | 默认 50 |
| `send_delay_ms` | `WOC_BRIDGE_SEND_DELAY_MS` | 默认 800 |
### 17.4 与 channels 集成的关系
本方案**只负责把 WechatOnCloud 改造成支持业务 API**,不涉及 channels 侧的插件开发。
channels 侧的 `wechat_woc` 插件开发是**独立的另一个项目**,它消费本方案产出的 HTTP API。两者关系
```
[channels wechat_woc 插件] ← 本方案不涉及
↓ HTTP
[WechatOnCloud bridge API] ← 本方案产出
↓ xdotool / DB
[微信客户端]
```
channels 插件只需配置 `bridge_url = http://<woc面板>/api/bridge/<实例id>` 即可对接。