sign-proxy 的 Linux .node (src/main/qqProtocol/direct/sign-proxy/) 现在有 musl 变体
(sign-proxy.linux-x64-musl.node / sign-proxy.linux-arm64-musl.node), loader
(sign-proxy/index.ts 的 pickTriple / isMusl) 在 musl 环境自动选它, 所以能用
node:24-alpine base。镜像 ~540MB, 比 debian (~880MB) 小近一半。
历史 (2026-07 前曾结论"必须 glibc/debian, 不能 alpine"): 当时只有 glibc 链接的 .node (NEEDED
libc.so.6, 要GLIBC_2.29), alpine 加载不了。现已被 musl 变体推翻。glibc 版 (sign-proxy.linux-x64-glibc.node/linux-arm64-glibc.node) 仍保留, 非 musl 环境 (debian/裸机) 走它。glibc/musl 双份是硬约束: musl cdylib 无法静态链接 (rustccrt-static-allows-dylibs=false), 动态 musl .node 又只能在 alpine 跑, 谁都替代不了谁。
llbot.js 顶层 import 链就 require sign-proxy (base.ts → direct → sign.ts → sign-proxy),
require 在 import 期即发生, 所以 .node 必须能加载 —— musl 变体保证 alpine 下不崩。
Windows 本机即可交叉编译 (靠 zig, 不用 alpine 机器)。一键 (确保 std target -> 编两颗 -> 只同步 musl 到 Bot):
npm run build:musl-bot # scripts/build-musl.mjs, 一条龙
拆开手动跑:
rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl # 一次性
npm run build:linux-x64-musl # 或 build:linux-arm64-musl / build:linux-musl(两个)
npm run sync-to-bot # 注意: 这个拷 dist 下全部 .node, 会覆盖 gnu/win 等; 只要
# musl 用 build:musl-bot (只同步两颗 musl)
国内装 musl std 的坑:
rustup target add拉rust-std-<ver>-*-musl时, 官方源常卡死 (0 KB/s), tuna 镜像可能没同步新版本 (404)。用 rsproxy:RUSTUP_DIST_SERVER=https://rsproxy.cn rustup target add aarch64-unknown-linux-musl秒下。zig 交叉本身不需要网络。
命门 (踩过的坑):
--cross-compile让 napi 用 zig 当交叉 C 工具链 + musl 链接器 (不用 alpine/docker)。.cargo/config.toml给 musl target 设rustflags = -C target-feature=-crt-static: .node 是 cdylib (.so), musl 默认+crt-static会把 libc 静态焊进 .so → alpine dlopen 出问题 且体积大; 关掉 → 动态链系统 musl (/lib/ld-musl-*.so.1), 正常加载。验证过: clean 重编产物 跟手敲RUSTFLAGS=-crt-static逐字节一致 (config.toml 确被 zig 交叉吃到)。- crypto 能跑因为 SecureSDK 用 rustls + ring (非 openssl), 全 musl 兼容 (SignToken 握手成功即证)。
postbuild-rename.mjs特意保留 -musl 后缀 (只折叠 gnu/msvc), 否则 linux-x64-musl 被改名成 linux-x64 会覆盖掉 glibc 那颗。
想在 x86_64 本机验证 arm 部署 (不换真 ARM 机器), 靠 Docker Desktop 自带的 QEMU 模拟。
坑在于 compose 容易变成混架构: linyuchen/pmhq:*-arm 是 arm64, 但 linyuchen/llbot:test
默认构的是 amd64 (test-build-amd64.ps1 / build.bat 都是 amd64 单架构或 CI 多架构 push,
本地 daemon 里只有 amd64 那颗)。要整套 arm 得补两样:
-
构一颗 arm64 llbot 镜像 (dist 里已有
sign-proxy.linux-arm64-musl.node, alpine base 的 loader 自动选它, 现成Dockerfile.test直接能构; 跨平台构建走 buildx container driver 而非默认 docker driver, 构完--load进本地 daemon):docker buildx build --builder mybuilder -f docker/Dockerfile.test \ --platform linux/arm64 -t linyuchen/llbot:test-arm --load . -
compose 每个服务显式加
platform: linux/arm64—— 本机是 amd64, 不声明的话 Docker 按 宿主架构拉/跑, 架构不匹配会报错或行为不定。pmhq + llbot 两个服务都要加。
验证跑对了架构 (应输出 aarch64):
docker exec <container> uname -m
命门:
- QEMU 模拟只适合测试/验证, 慢; 生产 arm 还是得放真 ARM 机器原生跑。
--load出来的test-arm是纯本地镜像, 没 push registry; 换机器要重构或docker save/load。- 首次跑 arm 镜像前确认模拟器在:
docker run --rm --platform linux/arm64 alpine uname -m应回aarch64(Docker Desktop 一般自带 binfmt, 缺的话docker run --privileged --rm tonistiigi/binfmt --install arm64)。
install 脚本开头让用户选连接模式 (存 PROTOCOL_MODE), startup.sh 按此 env 分发:
-
直连 (
PROTOCOL_MODE未设/非 pmhq): 纯代码复刻协议, 省内存。单 llbot 服务。node llbot.js [-q <uin>], 启动哪个号由用户决定, 两条路:AUTO_LOGIN_QQenv 设了 →-q <uin>恢复该号 (无头部署重启后免扫码自动恢复);- 没设 → 起在 WebUI 登录页, 用户从快速登录列表点选账号 (或扫码)。
install 脚本生成的 compose 里始终带
AUTO_LOGIN_QQ=(留空), 方便用户后填。 不再 "data 里恰好一个 session 就自动用它" (2026-07 改): 那是替用户做了选择, 跟 WebUI 快速登录列表的设计冲突。
-
PMHQ 有头 (
PROTOCOL_MODE=pmhq): 真实 QQ 客户端跑在独立linyuchen/pmhq容器 (有头, 收发 PB), 更稳不易掉线, 代价是吃内存 + 拉一个额外镜像。startup.sh 走node llbot.js --pmhq-port=$PMHQ_PORT --pmhq-host=$PMHQ_HOST(缺省 13000 / pmhq), 参数名与pmhq.ts getPMHQHostPort()对齐, 代码侧靠isPmhqMode()(检--pmhq-port=argv) 触发。compose 是 pmhq + llbot 双服务, 同app_network, 共享./llbot_config卷; llbotdepends_on: pmhq。llbot 靠网络 (--pmhq-host=pmhq) 连 pmhq, 自身不挂 QQ 目录。- PMHQ 分支固定有头, 不保留旧脚本的无头 y/n 问句 (无头虽省内存但易掉线, 想省内存的直接选直连模式)。
- 账号登录由 pmhq 容器的
AUTO_LOGIN_QQ处理, llbot 侧不传-q。 - 镜像源检测在 pmhq 模式下要求 llbot + pmhq 两个镜像都在该镜像源可用才命中。
直连 session 的敏感字段 (d2/tgt 等) 落盘前用 AES-256-GCM 加密, key 由 getMachineKey()
提供 (src/main/qqProtocol/direct/session.ts)。非容器绑 OS machine id;容器里
/etc/machine-id 随重建而变, 绑它 = 每次重建都要重新扫码, 所以 isDockerEnvironment()
为真时改从 data/machine_guid.bin (设备 GUID, machineGuid.ts 管理, 随 data volume
持久化) 派生。startup.sh 不碰 /etc/machine-id, 也没有额外的 key 文件。
权衡 (有意取舍, 别改回去): machine_guid.bin 的值 == session 文件里明文的 guid 字段
(machineGuid.ts overwriteMachineGuid <-> saveSession 双向同步), 拿到 session 文件即可还原
key —— 容器场景这层加密不防"单独泄露 session 文件", 防线实为整个 data 卷的访问边界。
曾实现过独立随机 session-key.bin 来堵这一点, 按维护者决定撤掉了: 卷内多一个 key 文件
与密文同卷, 实际防线相同, 不值得多一套文件/逻辑。收益: 备份/迁移整个 data 卷后 session
直接可用, 免重新扫码。
startup.sh 改 default_config.json 的 sed 用 [[:space:]] (POSIX 字符类) 而非 \s
(GNU 扩展)。alpine 自带 busybox sed 不认 \s, 会静默失配 (port/host/ffmpeg 路径都不替换,
且不报错); [[:space:]] 则 busybox 和 GNU sed 都认, 所以不用 apk add sed。
(踩过: 早期版本用 \s + 装 GNU sed 覆盖, 后来改 POSIX 去掉这层依赖。)
换源那处 sed -i "s|dl-cdn...|" 是简单替换, busybox sed 本就 OK。
startup.sh shebang 是 #!/bin/sh (POSIX, alpine 的 /bin/sh = busybox ash 也能跑)。
alpine / debian slim 都没有 curl, llbot 容器的 healthcheck 用 node 内置 fetch 探 WebUI:
node -e "fetch('http://127.0.0.1:'+(process.env.WEBUI_PORT||3080))..."。
(用户在 WebUI 里关掉 webui 的话会显示 unhealthy, 只影响状态展示, 不影响运行。)
Dockerfile.local / Dockerfile.test 有 ARG BUILD_PROXY(默认 http://192.168.1.101:7890),
只在 build 期的 RUN 里 export http_proxy/https_proxy(apk / yarn / npm 走它), 不写进运行时
ENV —— 否则容器自己出站会去连这个 LAN 代理。覆盖: --build-arg BUILD_PROXY=http://host:port;
关掉: --build-arg BUILD_PROXY=(空 = 直连)。生产 Dockerfile 无此 ARG (CI 直连)。
test.yml 的 e2e 构建就传 --build-arg BUILD_PROXY= 走直连。
| 文件 | 用途 |
|---|---|
docker/Dockerfile |
发布镜像 (alpine), 从 GitHub release 下载 LLBot.zip; 生产/CI 用, 无代理无注释; release zip 须含 musl .node (v8.0.8+) |
docker/Dockerfile.local |
本地两阶段构建 (builder debian 跑 yarn build, production alpine); 走 BUILD_PROXY 代理 |
docker/Dockerfile.test |
本地测试, COPY 本机预构建 dist/ (需先 yarn build), alpine; 走 BUILD_PROXY 代理; test-build-amd64.ps1 用 |
docker/startup.sh |
容器入口, 按 PROTOCOL_MODE 分发直连/PMHQ; shebang 是 #!/bin/sh (POSIX, alpine ash 兼容) |
script/install-llbot-docker.sh |
交互式向导, 连接模式二选一: 直连(单 llbot 服务) / PMHQ 有头(pmhq+llbot 双服务) |