从零搭建本地 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)"
必须同时验证:
- B 进程的
DISPLAY=:99。 - B 进程沙箱里的
~/.xwechat与宿主机~/.xwechat2是同一目录。 - B 窗口登录的是 Jack。
- 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 当前可见的聊天列表。测试时应:
- 让目标私聊或群聊出现在 B 的聊天列表中;
- 服务启动完成后再发送一条新消息;
- 把触发词放在消息最开头;
- 不要使用历史消息、撤回消息或只修改旧消息。
有效示例:
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 切换群聊、触发词、回复前缀、暂停状态或恢复联系人白名单模式。

浙公网安备 33010602011771号