# 部署指南(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.yaml:camera_ids、oracle_base_url;DSM 凭据与 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-UI(Vue3 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-Edge(systemd 守护) ```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.service,Restart=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 # 会话密钥自己生成: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= export AUTH_HUB_CLIENT_SECRET= export AUTH_HUB_REDIRECT_URI=https://smart-camera.zichuan.xyz/api/auth/callback # 与 auth-hub 登记的逐字符一致 export FAM_SESSION_SECRET= # 会话 cookie 签名密钥,换掉即全员下线 # (2) venv 补依赖(Python 3.8,pip 会自动选到兼容版本)后重启 /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-Core(UI/对话/成员接口,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 # NAS(fam-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' # Oracle(fam-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` | 401(Caddy 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"` | >0(NAS 推送) | | Oracle 运动片段 | `ls /opt/fam-edge/motion_clips/` | 存在 motion_*.mp4(素材分割产物) | | Oracle Ollama | `curl http://localhost:11434/api/tags` | 模型列表含 qwen2.5:7b |