Files
sentinel-home-ai/docs/DEPLOY.md
ericwyuan 1964e976f4 refactor: 全量迁云——fam-core 直读甲骨文 SQLite,NAS 只剩推送进程
前一天刚把登录迁到甲骨文,隔天 NAS 上的 fam-core 又挂了导致数据接口 502。
盘点后确认:甲骨文的 SQLite 才是权威数据源(videos 3113 / events 16159 /
people 60 / model_calls 9876),NAS 的 MariaDB 全是它的镜像——前端读的数据
本来就产自甲骨文,绕了一圈回家又绕回来。

改动:
- db_layer.py 从 725 行重写成 377 行:MySQL 镜像查询改为直读 fam-edge 的
  SQLite。5 个 upsert_sync_*(约 300 行去重逻辑,8/29 和 9/3 两次 1062 事故的
  发源地)连同 oracle_sync.py 整个删除。SQL 方言:JSON_CONTAINS -> json_each
  (前置 json_valid,历史脏数据不会把查询搞崩)、LEFT() -> substr()、%s -> ?。
  函数名 get_sync_* 一并改掉——已经没有 sync 这回事了
- 新增 edge_client.py:写操作(改名/删除)、帧图头像、服务状态都打给同机
  fam-edge,全走 127.0.0.1
- 新增 fam-notifier/:motion_notifier 从 fam-core 拆出独立成服务,游标从
  MariaDB 换成本地 JSON 文件。NAS 上从此没有 Flask、没有数据库、没有监听端口
- fam-core 移到甲骨文 /opt/fam-core(systemd,gunicorn -w 2,只绑
  127.0.0.1:5401——5400 被 chat-relay 占了)。Caddy 的 /api/* 从"frp 隧道
  回源 NAS"改成同机反代,forward_auth 闸门不变
- 前端删掉侧边栏同步面板、统计页同步状态、服务状态页的"NAS 同步"卡片与
  "立即同步"按钮(背后的镜像层已不存在);换成"NAS 运动推送"卡片,读
  fam-edge activity 新增的 motion 段(心跳年龄 + 最近事件)
- 顺带修掉一个隐蔽 bug:镜像表为保外键稳定用的是 NAS 本地自增 id,而帧图接口
  要的是甲骨文的 id,两边在 9/3 那次 id 重排后就对不上了。现在只有一套 id

测试:fam-core 21(新增 12 个 db_layer 用例:脏 JSON 不崩、人物精确匹配不误伤
"人物B"、日期过滤、统计口径、chat_history 懒建表)、fam-notifier 6、
fam-edge 157,全绿。

生产验证:甲骨文 /api/ui/stats 返回 videos 2965 / events 16159 / people 59;
NAS 侧 fam-notifier 已推送成功(事件 33070-33072 落库,心跳新鲜);
chat_history 19 条经 scripts/import_chat_history.py 迁移完成。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 08:03:32 +08:00

191 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 部署指南2026-09-13 全量迁云后同步)
> **一句话架构**除了录像本身全部跑在甲骨文。NAS 只剩 Surveillance Station
> 和一个往甲骨文推运动事件的进程fam-notifier——NAS 离线不影响网站访问。
## 1. NAS 端部署(只有 fam-notifier
2026-09-13 迁云后 NAS 上**不再有** MariaDB、FAM-Core、FAM-UI镜像层整个删除
(数据权威源本来就在甲骨文),接口和前端也都搬走了。旧部署的清理见 §1.2。
### 1.1 fam-notifier轮询 Surveillance Station → 推甲骨文)
```bash
cd /volume1/web/sentinel-home-ai/fam-notifier
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt # 只有 requests + PyYAML
cp config/config.yaml.example config/config.yaml
# 编辑 config.yamlcamera_ids、oracle_base_urlDSM 凭据与 token 走仓库根 .env
# 仓库根 .env 需提供 DSM_ACCOUNT / DSM_PASSWORD / ORACLE_SYNC_TOKEN
# 启动DSM 没有 systemd用 setsid 脱离 SSH 会话;挂了不会自启,需要人工重启)
cd /volume1/web/sentinel-home-ai/fam-notifier
setsid bash scripts/start_notifier.sh >> logs/start.log 2>&1 &
# 确认在跑(进程没有监听端口,只能看进程和日志)
ps aux | grep "[f]am_notifier"
tail -f logs/fam-notifier.log # 应每 60s 轮询、每 5min 心跳
```
游标存在 `fam-notifier/data/cursor.json`(迁云前存在 MariaDB。删掉也不致命
窗口回看会把最近的事件补推一遍,甲骨文按 event_id 幂等落库。
### 1.2 清理旧部署(迁云一次性动作)
```bash
# 1) 停掉 NAS 上的 fam-core它已经不该再跑了跑着也没用前端不再连它
ps aux | grep "[f]am-core/venv/bin/gunicorn" # 字符类写法pkill 会杀掉 SSH 自己
kill <上面的 pid>
# 2) frpc 里的 8000 映射可以删了(/etc/frp/frpc.toml 的 fam-core 段),
# 甲骨文侧对应的 iptables DROP 规则也就没有存在意义了
# 3) MariaDB 的 sentinel_home_ai 库确认 chat_history 已迁移§2.4)后再考虑删
```
### 1.3 FAM-UIVue3 SPA构建后托管在甲骨文 Caddy
```bash
cd /Users/ericwyuan/Desktop/Work/sentinel-home-ai/fam-ui # 本地开发机
npm install
npm run build # 产物 fam-ui/dist/
tar czf - -C fam-ui dist | ssh -i ~/.ssh/oracle_new ubuntu@129.146.26.249 \
'sudo tar xzf - --strip-components=1 -C /var/www/fam-ui && sudo chown -R ubuntu:ubuntu /var/www/fam-ui'
# 浏览器访问 https://smart-camera.zichuan.xyz/
```
## 2. Oracle 端部署 (FAM-Edge + FAM-Core + Ollama + FFmpeg)
### 2.1 系统依赖
```bash
sudo apt update && sudo apt install -y ffmpeg python3-opencv
# Ollama + qwen2.5:7b纯文本仅问答兜底
curl -fsSL https://ollama.com/install.sh | sh
systemctl enable ollama && systemctl start ollama
ollama pull qwen2.5:7b
```
### 2.2 FAM-Edgesystemd 守护)
```bash
cd /opt/fam-edge
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# 配置config/config.yaml素材目录/DB/模型/motion_segment.env 提供 ORACLE_SYNC_TOKEN/GEMINI_API_KEY*/NVIDIA_API_KEY
# systemd 服务(已配置 /etc/systemd/system/fam-edge.serviceRestart=always
sudo systemctl enable fam-edge
sudo systemctl restart fam-edge # 部署代码后必须用 systemctl 重启,勿手动 setsid
```
### 2.3 统一登录2026-09-12 从 NAS 迁入fam-edge + Caddy + 防火墙三件套)
登录入口不再依赖 NAS。三处缺一不可
```bash
# (1) /opt/fam-edge/.env 追加六项(缺任一项 fam-edge 拒绝所有登录fail closed
# client_secret 用 rotate-secret 现拿,明文只显示一次:
# cd /opt/auth-hub && venv/bin/python -m auth_hub.manage_clients rotate-secret <client_id>
# 会话密钥自己生成openssl rand -hex 32
export AUTH_HUB_ISSUER=https://auth.zichuan.xyz # 公网:浏览器跳转 + id_token 的 iss 校验
export AUTH_HUB_INTERNAL_BASE=http://127.0.0.1:5300 # 本机:换 token + 拉 JWKS不走公网 TLS
export AUTH_HUB_CLIENT_ID=<auth-hub 注册的 client_id>
export AUTH_HUB_CLIENT_SECRET=<rotate-secret 输出>
export AUTH_HUB_REDIRECT_URI=https://smart-camera.zichuan.xyz/api/auth/callback # 与 auth-hub 登记的逐字符一致
export FAM_SESSION_SECRET=<openssl rand -hex 32> # 会话 cookie 签名密钥,换掉即全员下线
# (2) venv 补依赖Python 3.8pip 会自动选到兼容版本)后重启
/opt/fam-edge/venv/bin/pip install "PyJWT>=2.8.0" "cryptography>=42.0.0"
sudo systemctl restart fam-edge
# (3) Caddy登录端点走本机 fam-edge其余 /api/* 先 forward_auth 再回源 NAS
# 改完先 caddy validate 再 reload见下方 Caddyfile 片段
sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile && sudo systemctl reload caddy
```
```caddyfile
smart-camera.zichuan.xyz {
handle /login { reverse_proxy 127.0.0.1:5000 }
handle /api/auth/* { reverse_proxy 127.0.0.1:5000 }
handle /api/logout { reverse_proxy 127.0.0.1:5000 }
handle /api/* {
forward_auth 127.0.0.1:5000 { uri /api/auth/verify } # 删了等于数据接口全裸
reverse_proxy 127.0.0.1:8000
}
handle { root * /var/www/fam-ui; encode gzip; try_files {path} /index.html; file_server }
}
```
```bash
# (4) 防火墙frps 把 NAS fam-core 的 :8000 暴露在公网,而 fam-core 自身已无鉴权,
# 必须只放行本机Caddy访问否则绕过 Caddy 就能读到全部数据
sudo iptables -I INPUT 1 -p tcp --dport 8000 ! -i lo -j DROP
sudo netfilter-persistent save # 否则重启后规则丢失
```
### 2.4 FAM-CoreUI/对话/成员接口2026-09-13 从 NAS 迁入)
```bash
# 代码(本地开发机 → 甲骨文)
tar czf - --exclude=venv --exclude=__pycache__ --exclude=logs --exclude='config/config.yaml' fam-core | \
ssh -i ~/.ssh/oracle_new ubuntu@129.146.26.249 'tar xzf - --strip-components=1 -C /opt/fam-core'
# 甲骨文上venv + 配置
python3 -m venv /opt/fam-core/venv
/opt/fam-core/venv/bin/pip install -r /opt/fam-core/requirements.txt
cp /opt/fam-core/config/config.yaml.example /opt/fam-core/config/config.yaml
# .env 只需要 ORACLE_SYNC_TOKEN与 fam-edge 的一致config_loader 会自动加载
grep "^export ORACLE_SYNC_TOKEN=" /opt/fam-edge/.env > /opt/fam-core/.env && chmod 600 /opt/fam-core/.env
sudo systemctl restart fam-core # 单元见 /etc/systemd/system/fam-core.service
curl -s http://127.0.0.1:5401/api/status # {"db":{"ok":true},...}
```
**端口 5401 不是笔误**5400 被这台机器上的 chat-relay 占了。fam-core 只绑
127.0.0.1,外部进不来,唯一客户端是同机 Caddy先 forward_auth 再反代)。
**chat_history 迁移(一次性)**:这是 NAS MariaDB 里唯一不是镜像的表。
在 NAS 上导出,再从本地导入甲骨文的 SQLite
```bash
# NAS 上导出(用 fam-core 旧 venv 里的 PyMySQL
ssh -p 2222 ericwyuan@192.168.50.64 '/volume1/web/sentinel-home-ai/fam-core/venv/bin/python -c "
import pymysql, json, yaml
cfg = yaml.safe_load(open(\"/volume1/web/sentinel-home-ai/fam-core/config/config.yaml\"))[\"database\"]
c = pymysql.connect(host=\"127.0.0.1\", user=cfg[\"user\"], password=cfg[\"password\"], database=cfg[\"database\"], cursorclass=pymysql.cursors.DictCursor)
cur = c.cursor(); cur.execute(\"SELECT * FROM chat_history ORDER BY chat_id\")
print(json.dumps(cur.fetchall(), ensure_ascii=False, default=str))
"' > /tmp/chat_history.json
# 本地 → 甲骨文导入(幂等:按 chat_id 跳过已存在的)
cat /tmp/chat_history.json | ssh -i ~/.ssh/oracle_new ubuntu@129.146.26.249 \
'/opt/fam-core/venv/bin/python /opt/fam-core/scripts/import_chat_history.py'
```
## 3. 代码同步tar 管道scp 在 NAS 被禁用)
```bash
# NASfam-core仓库根即部署根直接解包
tar czf - --exclude=venv --exclude=__pycache__ fam-core | \
ssh -p 2222 ericwyuan@192.168.50.64 'tar xzf - -C /volume1/web/sentinel-home-ai'
# Oraclefam-edge/opt/fam-edge 是 fam-edge 根,--strip-components=1 解临时目录再 cp
tar czf - --exclude=venv --exclude=__pycache__ --exclude=data --exclude=gdrive_videos fam-edge | \
ssh ubuntu@129.146.26.249 'mkdir -p /opt/fam-edge/tmp_d && tar xzf - --strip-components=1 -C /opt/fam-edge/tmp_d && \
cp -rf /opt/fam-edge/tmp_d/* /opt/fam-edge/ && rm -rf /opt/fam-edge/tmp_d && sudo systemctl restart fam-edge'
```
## 4. 验证清单
| 项目 | 命令 | 预期 |
|------|------|------|
| 云服务器 FAM-UI | 浏览器访问 `https://smart-camera.zichuan.xyz/` | 未登录时 `/api/*` 返回 401前端自动跳 `/login` → auth-hub 登录页;登录后跳回展示 Vue3 SPA |
| 登录不依赖 NAS | `curl -sI https://smart-camera.zichuan.xyz/login`(此时哪怕 fam-core 是停的) | 302 到 `auth.zichuan.xyz/authorize?...`**不再是 502** |
| 鉴权闸门 | `curl -s -o /dev/null -w '%{http_code}' https://smart-camera.zichuan.xyz/api/ui/videos` | 401Caddy forward_auth 拦下,没到 NAS |
| NAS fam-notifier | NAS 上 `ps aux \| grep "[f]am_notifier"` | 有进程;`fam-notifier/logs/fam-notifier.log` 每 60s 一条轮询、每 5min 一条心跳 |
| 甲骨文 FAM-Core | `curl -s http://127.0.0.1:5401/api/status` | `{"db":{"ok":true,...}}``/api/ui/stats` 返回真实计数 |
| NAS 离线也能用 | 关掉 NAS 后访问 `https://smart-camera.zichuan.xyz/timeline` | 页面与数据照常(只是不再有新运动事件);**这是本次迁云的验收标准** |
| Oracle FAM-Edge | `curl http://localhost:5000/health` | `{"status":"ok","queue_alive":true}` |
| Oracle 运动事件 | `sqlite3 /opt/fam-edge/data/oracle.db "SELECT COUNT(*) FROM ss_motion_events"` | >0NAS 推送) |
| Oracle 运动片段 | `ls /opt/fam-edge/motion_clips/` | 存在 motion_*.mp4素材分割产物 |
| Oracle Ollama | `curl http://localhost:11434/api/tags` | 模型列表含 qwen2.5:7b |