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

34 KiB
Raw Blame History

WechatOnCloud 改造需求方案

版本v2.0 日期2026-07-05 状态:方案待评审 变更:基于 channels 渠道插件协议深度调研,重新设计 API 契约,新增两条改造路径对比

一、背景与目标

1.1 现状

WechatOnCloud 是一个容器化的「服务端微信」项目,核心能力是:

  • 在 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 桌面串流(保留人工查看/扫码登录入口)
  • 不直接对接 channelschannels 侧插件开发是独立项目)

二、channels 渠道插件协议调研结论

2.1 channels 插件需要实现的 Adapter

channels 一个完整渠道插件需实现以下 adapter参考 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 返回值)

@dataclass(frozen=True)
class PollResult:
    messages: tuple[dict[str, Any], ...]   # 原始消息列表
    next_cursor: str | None                # 下次轮询的游标
    error: TransportError | None           # 错误信息

MessageContentInboundAdapter.normalizeInbound 返回值)

@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 返回值)

@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

@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.jsonWechatOnCloud 插件的 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

三、两条改造路径对比

思路:让 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 为字符串
  • 错误响应统一结构:
{
  "success": false,
  "error": {
    "code": "WECHAT_NOT_RUNNING",
    "message": "微信窗口未找到",
    "details": {}
  }
}

5.2 状态接口(对应 ProbeableAdapter

GET /api/status

响应

{
  "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_timeUnix 秒),首次传 0
  • limit:单次拉取上限,默认 50最大 200

响应

{
  "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": "你好"
}

响应

{
  "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

响应

{
  "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

响应

{
  "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

响应

{
  "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

响应

{
  "connected": false,
  "message": "等待扫码",
  "qr_data_url": null,
  "credentials": null
}

扫码成功时:

{
  "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

响应

{
  "reachable": true,
  "latency_ms": 50,
  "error": null
}

5.8.2 诊断项列表

GET /api/diagnostic/items

响应

{
  "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}

响应

{
  "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}

响应

{
  "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 追加:

# ===== 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 现有内容基础上追加:

# ===== 新增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

#!/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 L256

// 改造前
ExposedPorts: { '3000/tcp': {} },

// 改造后
ExposedPorts: {
    '3000/tcp': {},
    '8088/tcp': {},
},

十一、面板反代改造

panel/server/src/index.ts 新增反代路由:

// ---------- 反向代理到实例的 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

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 快照)

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 二维码截图

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 发送队列串行化

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/textxdotool + xclip
  • 实现 GET /api/messages/sinceDB 读取)
  • 改造 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> 即可对接。