面向"要长期跑"的部署。只想试一下的话看 03-快速开始。
作者的生产环境就是下面这套:Linux + 本机 MySQL + venv + systemd,没有用 Docker。
1. 最小生产形态
Linux 2C4G / 40G 盘(Ubuntu 22.04+ / Debian 12)
├── MySQL 8(本机 systemd 服务)
├── venv(Python 3.14,放仓库外)
├── systemd 托管 fastapi(WorkingDirectory=fastapi/,端口 8765)
└── 可选:Caddy / Nginx 反代(只在需要 HTTPS 或对外开 /ext 时)
一台 Android 平板(推荐,也可以用手机;常开、固定充电位置、关闭电池优化)
执行端(平板 / 手机)和服务端在同一内网时,不需要任何域名、TLS、公网 IP。 要给外部客户用 /ext 控制台时,作者的方案是:云机跑 Caddy + 一个反向隧道(frp),本地服务端主动连出去,不需要公网 IP——隧道细节与安全取舍见 07-安全与边界。
2. 安装(venv + systemd)
sudo mkdir -p /opt/sevfish-comvbot && sudo chown "$USER" /opt/sevfish-comvbot
git clone https://github.com/sevfish-ai/sevfish-comvbot-server.git /opt/sevfish-comvbot
cd /opt/sevfish-comvbot
python3 -m venv /opt/venv
/opt/venv/bin/pip install -r fastapi/requirements.txt
cp .env.example .env && chmod 600 .env
/opt/sevfish-comvbot 是仓库根,服务端的真实工作目录是它下面的 fastapi/(.env、uploads/ 都是相对这个目录解析)。
MySQL 建库建号:
CREATE DATABASE aivbot CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'aivbot'@'localhost' IDENTIFIED BY '强口令';
GRANT ALL PRIVILEGES ON aivbot.* TO 'aivbot'@'localhost';
FLUSH PRIVILEGES;
.env 里对应改成 MYSQL_USER=aivbot / MYSQL_PASSWORD=强口令,并把 SITE_URL 改成实际对外地址。
先前台跑一次(最难排错的一步单独做):
cd /opt/sevfish-comvbot/fastapi && /opt/venv/bin/python run.py
预期:
Uvicorn running on http://0.0.0.0:8765
📍 MySQL: aivbot@127.0.0.1:3306/aivbot
Ctrl-C 退出,然后交给 systemd:
sudo useradd -r -s /usr/sbin/nologin sevfish 2>/dev/null || true
sudo tee /etc/systemd/system/sevfish-comvbot.service >/dev/null <<'EOF'
[Unit]
Description=SevFishComVBot server
After=network-online.target mysql.service
Wants=network-online.target
[Service]
Type=simple
User=sevfish
# 必须以 fastapi/ 为工作目录:.env 与 UPLOAD_DIR 都是相对路径
WorkingDirectory=/opt/sevfish-comvbot/fastapi
ExecStart=/opt/venv/bin/python run.py
Restart=always
RestartSec=5
# 单进程是设计前提:不要加 worker,不要开 reload
[Install]
WantedBy=multi-user.target
EOF
sudo chown -R sevfish:sevfish /opt/sevfish-comvbot
sudo systemctl daemon-reload
sudo systemctl enable --now sevfish-comvbot
把目录属主交给
sevfish之后,再rsync覆盖会Permission denied;更新代码按 §7 走。
3. 起停与日志
sudo systemctl status sevfish-comvbot
sudo systemctl restart sevfish-comvbot # 改 .env 后
journalctl -u sevfish-comvbot -f # 实时日志
journalctl -u sevfish-comvbot --since today # 今天
首次启动会:连库 → 各 service ensure_table() 建表/补列 → 在线状态清零 → 把自增值 <1000 的表抬到 1000(新 id 从 1000 起)。
4. Docker(可选,未在生产验证)
仓库带了 Dockerfile + docker-compose.yml(MySQL 一起起),适合本机试玩或想要"一条命令"的环境:
cp .env.example .env # 填 MYSQL_ROOT_PASSWORD 与 MYSQL_PASSWORD
docker compose up -d
docker compose logs -f server
docker compose restart server
docker compose down # 停(数据卷保留)
它与 systemd 方式二选一:compose 会把 MYSQL_HOST 覆盖成 mysql、把端口只绑 127.0.0.1。
5. 配置项
全部走 .env(服务端仓 .env.example 有完整模板)。没有任何密钥写在代码里,这是刻意的设计约束——提交 PR 时也请保持。
| 键 | 说明 | 默认 |
|---|---|---|
APP_NAME / APP_VERSION | 展示用 | — |
DEBUG | 调试模式(生产关掉) | false |
HOST / PORT | 监听地址与端口 | 0.0.0.0 / 8765 |
RELOAD | 必须 false:热重载会清空内存中的设备注册表,设备会假掉线 | false |
WS_HEARTBEAT_INTERVAL / WS_HEARTBEAT_TIMEOUT | 心跳间隔/超时(秒) | — |
WS_MAX_CONNECTIONS | 最大连接数 | — |
CORS_ORIGINS 等 | 跨域(只在前后端分离改造后才需要) | — |
EXT_TUNNEL_SECRET | /ext 隧道密钥;留空 = /ext 整体 403 | 空 |
EXT_SESSION_HOURS | 客户会话有效期(小时,有活动自动续期) | 72 |
EXT_COOKIE_SECURE | cookie 是否 Secure(HTTPS 下开) | — |
EXT_LOCAL_PREVIEW | 无云机时用内网预览 /ext(仅私网来源) | false |
SITE_URL | 对外地址,用于页面里的链接 | — |
COMPANY_NAME | 页面署名 | — |
MYSQL_HOST / PORT / USER / PASSWORD / DATABASE | 数据库 | — |
MYSQL_POOL_MIN_SIZE / MAX_SIZE | 连接池 | — |
LOG_LEVEL / LOG_FORMAT | 日志 | — |
UPLOAD_DIR | 上传目录(工作流/知识库文件) | — |
LLM 的 key、base_url、model 不在 .env,而是每个智能体一行存在数据库里(管理后台填)。这样多组织可以各用各的模型。
6. 反代与 HTTPS
只有当你要给客户开 /ext 时才需要。Caddy 最小配置:
your.domain.com {
encode gzip
@ext path /ext/*
handle @ext {
header_up X-Tunnel-Secret {env.EXT_TUNNEL_SECRET}
reverse_proxy 127.0.0.1:8765
}
handle /* {
# 后台默认不给公网:要么删掉这段,要么加 basic auth
basic_auth { admin <bcrypt-hash> }
reverse_proxy 127.0.0.1:8765
}
}
WebSocket 由 Caddy 自动升级;用 Nginx 时要显式写:
location /webserver/ {
proxy_pass http://127.0.0.1:8765;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
/ext 的三道门(密钥头 → cookie → 组织归属)见 07。
7. 备份与恢复
只有两样东西不可丢:MySQL 和 uploads/。
# 每天一次(crontab)
mysqldump -u aivbot -p'强口令' --single-transaction --routines aivbot | gzip > /backup/aivbot-$(date +%F).sql.gz
tar czf /backup/uploads-$(date +%F).tgz -C /opt/sevfish-comvbot fastapi/uploads
# 恢复(新机器,先建好库与账号)
gunzip -c /backup/aivbot-2026-01-01.sql.gz | mysql -u aivbot -p'强口令' aivbot
tar xzf /backup/uploads-2026-01-01.tgz -C /opt/sevfish-comvbot
保留 14 天即可;记忆与工作流全在库里,代码可以随时重新拉。
8. 升级与回滚
表结构由 ensure_table() 自适应(新列逐条 ALTER + try/except),所以升级不需要手工迁移。但:升级前先备份,因为 ensure_table 里有破坏性迁移(例如早期版本的记忆绑定表会被迁移后 DROP)。
sudo systemctl stop sevfish-comvbot
cd /opt/sevfish-comvbot
sudo -u sevfish git pull
sudo -u sevfish /opt/venv/bin/pip install -r fastapi/requirements.txt # 依赖有变时
sudo systemctl start sevfish-comvbot
run.py 是 RELOAD=False 的设计前提:改 Python 只能重启进程,没有热重载(热重载会清空设备连接注册表)。
回滚:git checkout <上一个 tag> → 重启;必要时还原数据库备份。
9. embedding 与 OCR
- 知识库默认零依赖(本地哈希向量)。要更好的检索质量,在知识库设置里填 OpenAI 兼容的 embedding 接口与模型。
- 切换 embedding 后必须重建向量(服务端仓
fastapi/tests/manual_reembed.py),否则老 chunk 是空向量,检索静默返回空;换维度必须--force。 - 需要 OCR 时才装
requirements-ocr.txt(paddleocr / paddlex / modelscope / onnxruntime,体积会涨到 2GB+)。这些依赖是懒加载的,不装不影响主流程。
10. 日常巡检
| 看什么 | 在哪 |
|---|---|
设备在线(以注册表为准,不看 robots.status) | 管理后台首页统计卡片 / /api/status |
| 指令失败原因 | 功能测试页 → 设备错误日志(type=302) |
| 工具调用审计 | /admin/audit |
| 记忆清理是否按预期只删了智能体事实 | 记忆集详情 → 记忆清理日志(before/after) |
| 工作流失败节点 | /admin/workflow/{id}/run/{run_id} |