06 · 自建部署

面向"要长期跑"的部署。只想试一下的话看 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_SECUREcookie 是否 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}

需要有人帮忙落地?可以联系定制开发、私有化部署、联合交付或技术咨询。