部署
本地 python main.py 跑通之后,下一步通常是把它放到服务器上长期运行:进程要能自拉起、端口要只对必要来源开放、数据要定期备份。按目标环境挑一条路径即可。
上线前测试门禁
发布到服务器前先跑测试,不要让回归靠线上日志暴露:
# 依赖安装完成后,跑完整 pytest 回归(CI 与本地同一条命令)
python -m pip install pytest
python -m pytest tests/test_smoke.py tests/test_html_assembler.py \
tests/test_event_buffer.py tests/test_qq_official.py \
tests/test_scheduler.py tests/test_db_regression.py tests/test_loop_fix_regression.py -q- 仓库
.github/workflows/tests.yml在每次 push / pull_request 时自动执行上述套件,未通过的分支不应合入主干; - 手动部署(服务器
git pull、Docker 构建、systemd 重启)前,在目标机器或 CI 上跑一遍 pytest 全绿后再切换流量; - 其余
test_*.py(如test_plugin_imports.py、test_dual_core.py、test_perm.py)是模块级脚本式回归,CI 中单独执行,本地可在部署前一并跑完。
直接运行(开发/小规模)
python main.py适合本地调试与小规模使用。管理后台默认由 waitress(生产级 WSGI 服务器, 依赖缺失时回退到 werkzeug)在独立线程提供服务。
远程部署脚本(宝塔 / Linux)
仓库自带一条从本地到服务器的部署链路,把线上代码更新成当前工作区版本:
SSH_KEY=/path/to/id_ed25519 \
SSH_HOST=root@1.2.3.4 \
REMOTE_DIR=/www/wwwroot/bot.zgric.top/zcbot \
WEB_PORT=6080 \
bash tools/deploy_remote.sh它做的事,以及为什么这么做:
- 只同步代码:
config.yaml/core_plugins.yaml/data//plugins/一律不动 —— 线上这些是真实配置与数据(含数据库口令),覆盖即事故; - 排除原生库(
*.so/*.pyd/image_renderer/native):本地可能是别的平台构建, 覆盖会让线上渲染 / 加速模块直接崩; - 用 tar 解包(不带删除语义),线上独有文件(如
start.sh)不会被删; - 重启用目录全路径匹配进程:宽泛的
zcbot会连同机其它站点(如dbcj_zcbot)一起杀掉; - 部署前在
/root/zcbot-backup-<时间戳>/留一份代码 + 配置备份,可回滚; web.port被宝塔占用时(8080 默认归 BT-Panel)自动改成WEB_PORT。
DRY_RUN=1 只打包并自检,不上传。
无 TTY 接入运行中的框架(调试控制台)
systemd / 宝塔托管时进程没有交互终端。框架启动会在 127.0.0.1 上开一个调试控制台端口, 端口与超长随机 token 自动生成并写入数据库(console_access 表),之后每次启动复用:
# 在服务器上(无需 TTY,进程照常在跑)
python main.py attach # 交互式:直接敲终端命令
python main.py attach status # 一次性执行一条命令
python main.py -a plugins # 同上,-a 是 attach 简写
python main.py -a --agent # 文本通道里跟 AI 智能体对话- 凭证存放位置跟随
database配置:SQLite 存data/zcbot.db,MySQL 存对应库的console_access表 —— 两种部署attach都能用(无需手工填端口/token); - 只绑回环地址,token 用常量时间比较;
config.yaml的console段可配 (enabled/host/port/token_bits/backlog/read_timeout); - 端口被占用会重试;若该端口上已有本框架的控制台在跑(多实例),新实例放弃监听且 不覆盖凭证,避免把先跑那个实例的接入入口打掉。
Linux systemd 常驻
# /etc/systemd/system/zcbot.service
[Unit]
Description=ZCBOT
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/zcbot
ExecStart=/opt/zcbot/.venv/bin/python /opt/zcbot/main.py
Restart=always
RestartSec=5
# 安全加固(按需)
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now zcbot
journalctl -u zcbot -f # 查看实时日志单实例锁与安全模式(与 systemd 配合)
- 单实例锁:
data/zcbot.lock跨进程互斥,第二个实例拿不到锁会立即退出且不做任何重活,日志打印「检测到另一个 ZCBOT 实例(PID X)」;进程退出自动释放(Windows 端 atexit 兜底删文件),不会因残留锁卡死后续启动。 - 安全模式(
framework.crash.safe_mode,默认true):崩溃达上限时不退出,而是打印 banner 后挂起(Type=simple下守护进程误以为启动成功、停止反复拉起,性能税≈0)。此时需人工介入:- 删除
data/error.log*(崩溃计数文件,否则重启仍触发守卫死循环); - 修复根因(通常是持久配置错误,如 db 连不上);
kill挂起进程,由 systemd 拉起干净实例(新实例正常启动会自行清零计数)。
- 删除
- 若你的守护用
Type=notify+WatchdogSec,应把safe_mode设为false,否则看门狗会判定无响应而反复重启。
Windows 后台运行
- 简单方式:
pythonw main.py(无控制台窗口),或用「任务计划程序」设置开机启动; - 稳定方式:用 NSSM 把
python main.py注册为 Windows 服务, 可设置崩溃自动重启。
Docker(自行构建)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
COPY . .
EXPOSE 8080 6830
CMD ["python", "main.py"]docker build -t zcbot .
docker run -d --name zcbot \
-v $(pwd)/data:/app/data \
-v $(pwd)/plugins:/app/plugins \
-p 127.0.0.1:8080:8080 \
-p 6830:6830 \
zcbotOneBot 容器互通
OneBot 客户端要能反向连到容器的 6830 端口;若客户端在宿主机/其他容器, 注意网络与地址(容器内 127.0.0.1 只指向容器自身)。
反向代理与 HTTPS
生产环境建议让 web.host=127.0.0.1,由 Nginx/Caddy 终结 TLS 后反代到 8080:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
}WebSocket 反向连接端口(6830)按需单独放行或代理(需要 Upgrade 头)。
为什么这里必须覆盖式写 XFF
security.trusted_proxies 一旦非空,框架的 get_client_ip() 就取 X-Forwarded-For 的 第一段。而 nginx 的 $proxy_add_x_forwarded_for 是追加——客户端自带的 XFF 会留在最 左边。攻击者只要发一个 X-Forwarded-For: 127.0.0.1,就能让风控把攻击者识别成本机/内网 IP (内网 IP 自动豁免拉黑)。所以面向公网的那一跳必须用 $remote_addr 覆盖。
FRP 内网穿透
FRP 自己不会写 X-Forwarded-For:frps 的 vhostHTTP 只是把流量转给 frpc,frpc 再转给本地 端口,链路上没人补这个头。结果是所有请求的 remote_addr 都是 127.0.0.1,框架的 IP 风控 (登录限速、黑名单、蜜罐命中、审计日志)全部失效。
标准拓扑是「公网先进一层 Nginx,再入 FRP」:
用户 ──HTTPS──> 公网 Nginx ──> frps ──> frpc ──> ZCBOT (127.0.0.1:8080)
└─ 这一跳写真实客户端 IP(覆盖式)① 公网 Nginx
server {
listen 443 ssl;
server_name zcbot.example.com;
location / {
proxy_pass http://127.0.0.1:<frps 监听端口>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
}
}② 框架 config.yaml
security:
whitelist_ips: [] # 不要再留 127.0.0.1:FRP 下所有请求都是它,留它等于全网免检
trusted_proxies:
- "127.0.0.1" # frpc 与框架同机;不同机/容器则填那台的内网 IP③ 验证:从外网访问一次后,看审计日志 / /api/security/honeypot(需 super)/ ip_blacklist 表里记的是不是真实来源 IP,而不是 127.0.0.1。
多账号部署
- 多个 OneBot 客户端(不同账号)都反向连到同一个 6830,框架按连接区分
bot_name; - 发送消息时不传
bot会自动跟随消息来源账号;主动任务可用get_connected_bots()枚举在线实例。
MySQL 部署
默认的 SQLite 只适合小环境与开发环境,不适合大环境:群多、并发高、 多进程部署时把 database.type 切到 mysql,提前建库(如 zcbot, utf8mb4),框架会自动适配 DDL 并建表;需要 pymysql、DBUtils (切换时会提示/自动安装)。
安全清单
- 在
core_plugins.yaml给onebot_adapter.access_token设强随机令牌,OneBot 客户端保持一致; - 管理后台默认密码
admin/admin123登录后后台会建议修改(建议尽快改、不强制); web.host保持127.0.0.1,公网访问走反代 + HTTPS + IP 白名单;- 配置
security.whitelist_ips限制管理接口来源; - 定期备份
data/(SQLite 文件、日志、插件数据)与 MySQL 数据库; - 以非 root 用户运行 systemd/Docker 服务。
备份与迁移
需要备份的内容:
data/:SQLite 数据库、日志、plugins_dat/插件数据;config.yaml:全局配置;core_plugins.yaml:官方插件的开关与配置;plugins/:自行开发/定制的插件代码。
迁移到新机器:复制上述内容 → 安装依赖 → python main.py 即可。