AIGC标识 从零搭建本地 AI 安全测试平台(三):Ubuntu双开微信接入本地Qwen3实战

本文不讲 Ollama 和 Qwen3 的安装。起点是:本机已经部署好 Ollama,模型名为 qwen3:14b,并且 /api/chat 可以正常返回结果。

一、目标和最终结构

Ubuntu 上已有一个日常使用的微信 A。现在增加一个独立的微信 B(昵称 Jack),只让 B 接入本地大模型:

微信联系人或群聊
        │ 发送:jackB 问题
        ▼
微信 B(独立显示 :99、独立配置目录)
        ▼
本机 Bridge(只监听 127.0.0.1:18400)
        ▼
Python AI Adapter
        ▼
Ollama / Qwen3:14b(127.0.0.1:11434)
        ▼
微信 B 回复:[千问本地小助理] ...

安全边界:

  • 微信 A 沿用默认目录 ~/.xwechat,不重启、不注入、不接入机器人。
  • 微信 B 使用 ~/.xwechat2 和 ~/Documents/xwechat_files2。
  • B 固定运行在 Xvfb 显示 :99,并使用独立 D-Bus 会话。
  • Bridge 和 Ollama 只监听本机回环地址,不向公网开放。
  • 机器人必须看到触发词 jackB 才回复,避免普通聊天误触发。
  • 事件持久化去重;发送结果不确定时不自动重发,避免重复回复。

二、先确认本地 Qwen 可用

这里不重新安装模型,只做两个检查:

curl -s http://127.0.0.1:11434/api/tags

返回中应包含 qwen3:14b。再测试聊天接口:

curl -s http://127.0.0.1:11434/api/chat \
  -H 'Content-Type: application/json' \
  -d '{
    "model":"qwen3:14b",
    "messages":[{"role":"user","content":"只回复:模型正常"}],
    "stream":false,
    "think":false
  }'

确认返回 "done": true 和“模型正常”后,再接微信。本文实测模型首次请求可能因为重新装载权重而较慢,适配器总超时设置为 180 秒。

三、准备 wechat-bridge-linux

cd ~/data/AI
git clone https://github.com/djasdh/wechat-bridge-linux.git
cd wechat-bridge-linux

先阅读仓库真实代码和 README,不要凭经验假设端口和接口:

sed -n '1,240p' README.md
find . -maxdepth 3 -type f | sort
rg -n 'listen|port|health|send|message|18400' .

安装本方案会用到的本机组件:

sudo apt update
sudo apt install -y xvfb xdotool bubblewrap \
  python3-aiohttp python3-gi python3-xlib \
  gir1.2-atspi-2.0 at-spi2-core

Linux 微信本体应已经安装到 /opt/wechat/wechat:

test -x /opt/wechat/wechat && echo "微信程序存在"

四、确认微信 A,禁止机器人误接日常账号

先查看现有进程:

ps -eo pid,ppid,cmd | grep -E '/opt/wechat/wechat|Xvfb' | grep -v grep

微信 A 使用默认桌面和默认配置:

配置:~/.xwechat
文件:~/Documents/xwechat_files
显示:日常桌面

后续脚本不得杀死所有微信进程,也不能把默认目录直接交给 Bridge。所有 B 的操作必须同时满足:显示为 :99、配置实际映射到 ~/.xwechat2。

五、为微信 B 建立独立目录

mkdir -p ~/.xwechat2
mkdir -p ~/Documents/xwechat_files2
mkdir -p ~/data/AI/wechat-bridge-linux/ai/{runtime,logs,units}
chmod 700 ~/.xwechat2 ~/Documents/xwechat_files2

微信程序内部仍会访问 ~/.xwechat。因此不能只设置一个不存在的环境变量,而是用 bubblewrap 在 B 的进程视角中进行目录映射:

宿主机 ~/.xwechat2
          ↓ B 的沙箱内部
       ~/.xwechat

同理将 xwechat_files2 映射为微信默认文件目录。

六、启动独立显示 Xvfb :99

Xvfb :99 -screen 0 1280x720x24 -ac \
  +extension GLX +extension RENDER +extension RANDR &

验证:

DISPLAY=:99 xdpyinfo >/dev/null && echo "显示 :99 正常"

在 Xvfb 中看到 vaInitialize failed、dri3 extension not supported 等 GPU 日志并不一定表示微信启动失败。只要窗口、进程和无障碍树存在,软件渲染即可工作。

七、编写微信 B 启动脚本

文件:ai/start-b.sh

#!/usr/bin/env bash
set -euo pipefail

BASE="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$BASE/runtime" "$HOME/.xwechat2" "$HOME/Documents/xwechat_files2"
umask 077

exec bwrap \
  --ro-bind / / \
  --dev-bind /dev /dev \
  --proc /proc \
  --bind /sys/kernel/security /sys/kernel/security \
  --bind /tmp /tmp \
  --tmpfs "/run/user/$(id -u)" \
  --bind "$BASE" "$BASE" \
  --bind "$HOME/.xwechat2" "$HOME/.xwechat" \
  --bind "$HOME/Documents/xwechat_files2" "$HOME/Documents/xwechat_files" \
  --setenv DISPLAY :99 \
  --setenv QT_QPA_PLATFORM xcb \
  --setenv QT_ACCESSIBILITY 1 \
  --setenv QT_LINUX_ACCESSIBILITY_ALWAYS_ON 1 \
  --setenv LIBGL_ALWAYS_SOFTWARE 1 \
  --setenv QT_QUICK_BACKEND software \
  --unsetenv WAYLAND_DISPLAY \
  --unsetenv DBUS_SESSION_BUS_ADDRESS \
  dbus-run-session -- bash -c '
    umask 077
    printf "%s" "$DBUS_SESSION_BUS_ADDRESS" > "'"$BASE"'/runtime/bus-address"
    /opt/wechat/wechat &
    child=$!
    printf "%s" "$child" > "'"$BASE"'/runtime/wechat-b.pid"
    trap '"'"'kill "$child" 2>/dev/null || true'"'"' TERM INT EXIT
    wait "$child"
  '
chmod 700 ai/start-b.sh
./ai/start-b.sh >>ai/logs/wechat-b.log 2>&1 &

/sys/kernel/security 的映射是本机微信在此沙箱方案中的兼容要求。不要把 bubblewrap 改成完全开放的宿主机环境。

首次启动会显示登录页。用手机确认 Jack 登录。机器人不会修改账号密码,也不会绕过扫码或手机确认。

八、验证 B 确实隔离

cat ai/runtime/wechat-b.pid
ps -fp "$(cat ai/runtime/wechat-b.pid)"

必须同时验证:

  1. B 进程的 DISPLAY=:99。
  2. B 进程沙箱里的 ~/.xwechat 与宿主机 ~/.xwechat2 是同一目录。
  3. B 窗口登录的是 Jack。
  4. A 仍使用宿主机默认 ~/.xwechat。

目录同一性可用 inode 检查:

pid="$(cat ai/runtime/wechat-b.pid)"
stat -c '%d:%i %n' ~/.xwechat2 "/proc/$pid/root$HOME/.xwechat"

两个 inode 应相同。只看进程数量不足以证明账号隔离。

九、为什么需要 AT-SPI

当前 Linux 微信并没有为本方案提供稳定的官方收发机器人 API。Bridge 通过 AT-SPI 无障碍树读取 B 的可见聊天列表、消息和输入框,再用本地 UI 完成回复。

启动 B 时已经启用:

QT_ACCESSIBILITY=1
QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1

B 的独立 D-Bus 地址写入:

ai/runtime/bus-address

驱动连接的是 B 在 :99 中的无障碍总线。它还会核对 B 的 PID、显示、窗口名和配置目录;任一项变化就停止自动发送。

十、Bridge 与适配器的职责

建议把微信 UI 驱动与模型调用分开:

live_bridge.py
  ├─ 校验 B 身份和隔离目录
  ├─ 轮询可见聊天列表
  ├─ 生成消息事件并通过 SSE 推送
  ├─ 提供 POST /send
  └─ 只监听 127.0.0.1:18400

adapter.py
  ├─ 验证账号、方向、消息类型和时间
  ├─ 检查触发词/联系人/群聊策略
  ├─ SQLite 去重
  ├─ 调用 127.0.0.1:11434/api/chat
  └─ 将带前缀的答案交给 Bridge

接口以实际实现为准。本方案使用:

GET  /health
GET  /messages/stream
POST /send

Bridge 与 Adapter 之间使用本地 token,token 文件权限应为 600,且服务不绑定 0.0.0.0。

十一、配置文件

文件:ai/config.json

当前“所有人和群聊都可用,但必须发送 jackB”的配置:

{
  "enabled": true,
  "account_id": "local-profile:这里填写程序核验出的B标识",
  "account_nickname": "Jack",
  "allowed_contacts": [],
  "allowed_contact_names": ["Javen"],
  "bridge_url": "http://127.0.0.1:18400",
  "ollama_url": "http://127.0.0.1:11434",
  "access_mode": "all_with_trigger",
  "trigger": "jackB",
  "allow_groups": true,
  "reply_prefix": "[千问本地小助理]",
  "ignored_reply_prefixes": ["[AI]"]
}

account_id 不能照抄示例,必须来自 B 隔离配置的本地核验结果。

1. 修改触发词

"trigger": "小助手"

修改后对方发送:

小助手 帮我总结这段内容

触发词必须位于消息开头。支持空格以及中英文冒号、逗号、句号、问号、感叹号,例如:

jackB 请回答
jackB:请回答
jackB.你还在不?
jackB。请回答

2. 修改回复前缀

"reply_prefix": "[我的本地助手]",
"ignored_reply_prefixes": ["[AI]", "[千问本地小助理]"]

把旧前缀保留在 ignored_reply_prefixes,防止历史回复被重新识别后形成回环。

3. 开启或关闭群聊

"allow_groups": true

改为 false 后只处理私聊。群聊开启时,消息仍必须以触发词开头;Bridge 会从“发送人:消息”的预览中提取消息,并回复到原群。

4. 恢复为只允许 Javen 私聊

{
  "access_mode": "allowlist",
  "allowed_contacts": ["这里填写Javen核验出的微信ID"],
  "allowed_contact_names": ["Javen"],
  "allow_groups": false
}

白名单模式必须同时使用经过 B 界面核验的联系人 ID 和精确备注名,不能只凭聊天标题猜测身份。

5. 暂停机器人

"enabled": false

暂停后微信 A、微信 B 和 Ollama 都可以继续运行,但适配器不回复。

6. 修改模型

在 adapter.py 或扩展后的配置项中,将:

"model": "qwen3:14b"

改成 ollama list 中真实存在的模型名。修改前先直接调用 /api/chat 验证。

所有配置修改后重启:

systemctl --user restart wechat-ai-bridge.service wechat-ai-adapter.service

Bridge 重启会把当时已有消息作为基线,不会追溯回复旧消息。重启完成后应重新发一条新消息测试。

十二、systemd 用户服务

1. Xvfb

~/.config/systemd/user/wechat-b-display.service:

[Unit]
Description=Virtual display for WeChat B

[Service]
ExecStart=/usr/bin/Xvfb :99 -screen 0 1280x720x24 -ac +extension GLX +extension RENDER +extension RANDR
Restart=on-failure

[Install]
WantedBy=default.target

2. 微信 B

~/.config/systemd/user/wechat-b-session.service:

[Unit]
Description=Isolated WeChat B session
After=wechat-b-display.service
Requires=wechat-b-display.service

[Service]
WorkingDirectory=%h/data/AI/wechat-bridge-linux
ExecStart=%h/data/AI/wechat-bridge-linux/ai/start-b.sh
Restart=on-failure

[Install]
WantedBy=default.target

3. Bridge

~/.config/systemd/user/wechat-ai-bridge.service:

[Unit]
Description=WeChat B local bridge
After=wechat-b-session.service

[Service]
WorkingDirectory=%h/data/AI/wechat-bridge-linux
ExecStart=/usr/bin/python3 ai/live_bridge.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

4. Adapter

~/.config/systemd/user/wechat-ai-adapter.service:

[Unit]
Description=WeChat B Qwen adapter
After=wechat-ai-bridge.service

[Service]
WorkingDirectory=%h/data/AI/wechat-bridge-linux
ExecStart=/usr/bin/python3 ai/adapter.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

加载并启用:

systemctl --user daemon-reload
systemctl --user enable --now \
  wechat-b-display.service \
  wechat-b-session.service \
  wechat-ai-bridge.service \
  wechat-ai-adapter.service

如果微信 B 退出登录,systemd 只能重新打开登录界面,仍需手机扫码或确认,不能自动绕过。

十三、启动、停止和查看日志

# 查看状态
systemctl --user status wechat-ai-bridge.service wechat-ai-adapter.service

# 查看实时日志
journalctl --user -f \
  -u wechat-ai-bridge.service \
  -u wechat-ai-adapter.service

# 暂停 AI 回复
systemctl --user stop wechat-ai-adapter.service

# 停止 B 整套链路
systemctl --user stop \
  wechat-ai-adapter.service \
  wechat-ai-bridge.service \
  wechat-b-session.service \
  wechat-b-display.service

不要使用 pkill wechat,否则会把日常微信 A 一起结束。

十四、本地测试顺序

1. 健康检查

curl -s http://127.0.0.1:18400/health

关键字段应类似:

{
  "status": "ok",
  "ready_for_ai": true,
  "access_mode": "all_with_trigger",
  "trigger": "jackB",
  "allow_groups": true
}

2. 私聊

从联系人给 Jack 新发:

jackB 请只回复:私聊联动成功

预期:

[千问本地小助理] 私聊联动成功

3. 群聊

在 B 已加入且聊天列表可见的群里发送:

jackB 请只回复:群聊联动成功

检查日志应依次出现“观察到消息”“调用模型”“本地 UI 确认回复”。群聊必须用新消息测试,不能在服务重启后等待它补处理旧消息。

十五、本次实际遇到的故障

1. GPU/VA-API 报错

vaInitialize failed 和 dri3 extension not supported 是 Xvfb 中常见的硬件加速失败。使用软件渲染后,微信和无障碍树仍可正常工作。

2. 群聊预览前面带发送人

群聊列表预览并不是直接的 jackB ...,而可能是:

Javen: jackB 请回答

驱动需要先剥离 发送人:,再判断触发词;同时还要处理未读数 [1]。

3. jackB.你还在不? 没回复

故障原因不是 Ollama、微信掉线或服务退出,而是原来的触发词边界只接受空格、冒号、逗号、问号和感叹号,不接受句点。

修复时必须同时修改两处:

  • UI 驱动的 starts_with_trigger();
  • 适配器的 prompt()。

接受字符统一为:

" \t::,,.。??!!"

并增加英文句点、中文句号的单元测试。修复后重启 Bridge 和 Adapter,再发送一条新消息。

4. 模型超时

日志若出现:

event failed (TimeoutError), not retried

先直接请求 Ollama,判断是模型加载慢还是服务异常。发送阶段超时后不要盲目重试,因为消息可能已经送达微信,只是本地确认超时;自动重试会造成重复回复。

5. Bridge 显示 blocked

如果 /health 返回 ready_for_ai: false,常见原因有:

  • B 退出登录或窗口被关闭;
  • B 的 PID 已变化;
  • B 不再位于 :99;
  • B 的配置映射不是 ~/.xwechat2;
  • 弹窗遮挡聊天窗口;
  • 微信升级后无障碍控件名称或层级变化。

先恢复 B 的正常聊天主窗口,再重启 Bridge。不要降低身份和目录校验来“强行恢复”。

十六、按链路逐层排查

出现“不回复”时不要一上来重装微信或模型。按“进程 → B 界面 → Bridge → Adapter → Ollama → 发送确认”的顺序检查,可以很快定位故障所在层。

第 1 层:确认微信 A 和 B 没有串号

ps -eo pid,ppid,cmd | grep -E '/opt/wechat/wechat|Xvfb :99' | grep -v grep
cat ~/data/AI/wechat-bridge-linux/ai/runtime/wechat-b.pid

正常情况:

  • 日常微信 A 仍在默认桌面运行;
  • Xvfb :99 存在;
  • B 的 PID 文件指向一个真实微信进程;
  • B 的 DISPLAY 是 :99;
  • B 的沙箱目录映射到 ~/.xwechat2。

如果 B 不在,先启动显示服务和 B:

systemctl --user restart wechat-b-display.service
systemctl --user restart wechat-b-session.service

如果手机要求确认登录,必须先人工确认。不要为了恢复机器人而操作微信 A。

第 2 层:确认 Bridge 是否 ready

curl -s http://127.0.0.1:18400/health | python3 -m json.tool

正常应有:

"status": "ok",
"ready_for_ai": true,
"reason": "ready"

无法连接端口时:

systemctl --user status wechat-ai-bridge.service
journalctl --user -u wechat-ai-bridge.service -n 100 --no-pager

若状态为 blocked,重点看 reason:

reason/日志 含义 处理
B PID changed B 重启后 PID 文件或驱动状态过期 重启 Bridge
unexpected display 连接的不是 :99 中的 B 检查 B 启动脚本
profile isolation mismatch B 没有使用隔离目录 停止 B,修正 bubblewrap 映射
B accessibility application unavailable 无障碍树未建立 检查 QT_ACCESSIBILITY 和独立 D-Bus
main window unavailable B 未登录或不在聊天主窗口 手机确认登录,关闭遮挡弹窗
missing or ambiguous ... 控件缺失、重复或窗口布局变化 恢复主窗口,检查微信升级带来的控件变化

第 3 层:确认新消息确实进入可见列表

本方案只观察 B 当前可见的聊天列表。测试时应:

  1. 让目标私聊或群聊出现在 B 的聊天列表中;
  2. 服务启动完成后再发送一条新消息;
  3. 把触发词放在消息最开头;
  4. 不要使用历史消息、撤回消息或只修改旧消息。

有效示例:

jackB 帮我回答
jackB:帮我回答
jackB.你还在不?

无效示例:

你好 jackB 帮我回答
jackBabc

实时观察 Bridge:

journalctl --user -f -u wechat-ai-bridge.service

发消息后完全没有“observed and broadcast”,说明问题仍在 B 界面识别层;若出现该日志,说明收信已经通过,应继续查 Adapter。

第 4 层:确认 Adapter 接到了事件

systemctl --user status wechat-ai-adapter.service
journalctl --user -u wechat-ai-adapter.service -n 100 --no-pager

常见日志:

日志 判断
SSE client connected 已连接 Bridge 事件流
event rejected by policy 账号、时间、方向、群聊开关或触发词不符合配置
duplicate skipped SQLite 已处理过该事件,去重正常
bridge unavailable or invalid stream Bridge 尚未启动、重启中或事件流中断
reply confirmed in B local UI 模型和本地发送流程完成
event failed (TimeoutError), not retried 模型或发送确认超时,出于防重复策略不重试

核对正在生效的配置:

python3 -m json.tool ~/data/AI/wechat-bridge-linux/ai/config.json

注意:修改 JSON 后必须重启两个服务。只改文件而不重启,运行中的 Python 进程仍使用旧配置。

第 5 层:绕过微信,单独测试 Ollama

curl --max-time 180 -s http://127.0.0.1:11434/api/chat \
  -H 'Content-Type: application/json' \
  -d '{
    "model":"qwen3:14b",
    "messages":[{"role":"user","content":"只回复:模型链路正常"}],
    "stream":false,
    "think":false,
    "options":{"num_predict":32}
  }'
  • 能返回:模型链路正常,继续检查 Adapter 的策略或微信发送。
  • 连接失败:检查 Ollama 服务和 11434 监听。
  • 首次请求很慢、后续变快:通常是模型重新装载,不是微信故障。
  • 超过 180 秒:检查机器内存、交换空间和 Ollama 日志,必要时降低输出长度或换更小模型。

第 6 层:判断是“没生成”还是“没发送”

Bridge 有消息日志、Ollama 也正常,但微信中没有答案时,查看 Adapter 最后一条日志:

  • 没有模型完成记录:调用模型阶段失败。
  • 已生成但 POST /send 失败:B 输入框、发送按钮或目标聊天发生变化。
  • send outcome uncertain; do not retry:本地 UI 没能确认最终气泡。先人工查看 B 是否实际发出,确认前不要再次提交同一事件。
  • reply confirmed in B local UI:B 的本地界面已出现回复;若手机端没看到,再检查微信网络连接。

第 7 层:修改配置后做完整回归

cd ~/data/AI/wechat-bridge-linux
PYTHONPATH=ai python3 -m unittest -v \
  test_adapter test_delivery test_multi_ui_driver

然后:

systemctl --user restart wechat-ai-bridge.service wechat-ai-adapter.service
sleep 5
curl -s http://127.0.0.1:18400/health | python3 -m json.tool

最后分别发送一条新的私聊和群聊测试消息。测试通过的判断应同时包含:

  • Bridge 观察到新事件;
  • Adapter 没有策略拒绝;
  • Ollama 返回完成结果;
  • B 本地 UI 出现带正确前缀的回复;
  • 同一消息没有被回复两次;
  • 微信 A 全程未被操作。

一键收集诊断信息

下面的命令只读取状态,不包含账号密码:

echo '=== services ==='
systemctl --user --no-pager --full status \
  wechat-b-display.service \
  wechat-b-session.service \
  wechat-ai-bridge.service \
  wechat-ai-adapter.service

echo '=== bridge health ==='
curl -s --max-time 3 http://127.0.0.1:18400/health

echo '=== ollama models ==='
curl -s --max-time 3 http://127.0.0.1:11434/api/tags

echo '=== recent logs ==='
journalctl --user \
  -u wechat-ai-bridge.service \
  -u wechat-ai-adapter.service \
  --since '30 minutes ago' --no-pager

公开日志前应删除本地 token、微信 ID、手机号、真实联系人名称和本机绝对路径。

十七、当前方案的边界

  • 只能处理微信 B 当前可见聊天列表中的变化。
  • 服务启动时已有的消息会作为基线跳过。
  • UI 中看到己方消息只证明本地发送动作成功,不等同于微信服务器的网络送达回执。
  • 微信升级可能改变无障碍树,需要重新检查控件结构。
  • account_id、联系人 ID 和本地 token 不应写进公开博客。
  • 不要将 18400 或 11434 通过端口映射、防火墙或反向代理暴露到公网。

完成以上配置后,日常微信 A 保持原状;独立的微信 B 通过本机 Bridge 和适配器调用 Qwen3:14b。私聊与群聊统一采用消息开头的 jackB 触发,回复前缀为 [千问本地小助理],后续可以仅通过 config.json 切换群聊、触发词、回复前缀、暂停状态或恢复联系人白名单模式。

posted @ 2026-11-06 07:30  javen_xiao  阅读(3)  评论(0)    收藏  举报