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>
This commit is contained in:
ericwyuan
2026-09-13 08:03:32 +08:00
parent dc284f4bf5
commit 1964e976f4
33 changed files with 1178 additions and 1187 deletions

View File

@@ -63,9 +63,9 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级Gemini → NVIDIA N
| 节点 | 角色 | 硬件 | IP | 服务与端口 |
|------|------|------|-----|-----------|
| Synology NAS | FAM-Core + 数据库(采集 + 聚合 + 镜像层 | DS220+ (Geminilake), DSM 7 | 家庭局域网 192.168.50.64 | FAM-Core :8000API 聚合 + 轮询 SS + OracleSync, MariaDB :3306, Surveillance Station :5000FAM-UI 已迁至云服务器(见下) |
| Oracle Cloud云服务器 | FAM-Edge + AI-Gateway + Caddy前端 | Ampere A1 4C23G ARM64无 GPU, Ubuntu 20.04 | 公网 129.146.26.249 / Tailscale已安装未启用备用 | FAM-Edge :5000systemd 守护,视频分析 + 转发问答), **AI-Gateway :5100systemd 守护,对外监听,问答模型降级链)**, Caddy :80(托管 FAM-UI SPA + 反代 /api/* → NAS FAM-Core, Ollama :11434仅本地,被 AI-Gateway 调用 |
| 家庭网络 | 用户入口 | 普通终端 | 公网 / 家庭网络 | 浏览器访问 `http://129.146.26.249/`Caddy 托管前端,/api 经 frp 隧道回源 NAS FAM-Core :8000 |
| Synology NAS | 摄像头录像 + 运动事件推送(**仅此而已**2026-09-13 起 | DS220+ (Geminilake), DSM 7 | 家庭局域网 192.168.50.64 | Surveillance Station :5000录像机本体搬不走, **fam-notifier**(无端口的推送进程,轮询 SS → 推甲骨文。MariaDB 与 FAM-Core 已随镜像层一起下线 |
| Oracle Cloud云服务器 | **除录像外的全部**:视频分析 + 数据 + 接口 + 登录 + 前端 | Ampere A1 4C23G ARM64无 GPU, Ubuntu 20.04 | 公网 129.146.26.249 / Tailscale已安装未启用备用 | FAM-Edge :5000systemd视频分析 + 统一登录 + SQLite 权威库), **FAM-Core :5401systemdUI/对话/成员接口,仅监听 127.0.0.1**, AI-Gateway :5100systemd问答模型降级链, Caddy :80/:443前端 + 反代 + forward_auth 鉴权, Ollama :11434仅本地 |
| 家庭网络 | 用户入口 | 普通终端 | 公网 / 家庭网络 | 浏览器访问 `https://smart-camera.zichuan.xyz/`(全部由甲骨文提供,**不依赖 NAS 在线** |
**网络要点(推送模式)**
- 服务间通信只有两条出站:**NAS → Oracle 公网 IP:5000**(① `GET /api/oracle/sync` 拉增量 + `POST /api/oracle/people/correct` 命名回推 ② `POST /api/ss/motion` 推送运动侦测事件。Edge 不需要反向访问 NAS
@@ -98,19 +98,22 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级Gemini → NVIDIA N
│ ├─ /v1/chat/completionsNVIDIA → Gemini多 Key 轮换)→ 本地 Ollama 降级链 │
│ ├─ Bearer token 鉴权fail-closed对外 0.0.0.0:5100非本系统专属 │
│ └─ 独立 .env/opt/ai-gateway/.envNVIDIA/Gemini key 与 FAM-Edge 各自一份) │
Caddy :80托管 FAM-UI SPA + 反代 /api/* → NAS FAM-Corefrp 隧道 :8000
└───────────────────────────────┬───────────────────────────────────────────────┘
│ HTTP GET /api/oracle/sync?since=&token= (每 30 分钟)
FAM-Core (Flask :5401仅 127.0.0.1)
│ ├─ UI-API: /api/ui/*(直读下面那个 SQLite无镜像、无后台线程
│ ├─ Chat-Handler: /api/chat/ask查 events 拼上下文 → 转 fam-edge 编排) │
│ ├─ Member-Manager: /api/member/name|merge转 fam-edge它负责合并人物
│ └─ chat_history: 本服务唯一写的表,落在同一个 SQLite 里 │
│ SQLite /opt/fam-edge/data/oracle.dbWALvideos / events / people / │
│ model_calls / person_identity_map / ss_motion_events / chat_history │
│ Caddy :80/:443前端静态 + /login 与 /api/auth/* → fam-edge │
│ + /api/* 先 forward_auth 再反代 fam-core唯一鉴权闸门
└───────────────────────────────▲───────────────────────────────────────────────┘
│ POST /api/ss/motion单向推送 + 5 分钟心跳)
┌─────────────────────────────── NAS (192.168.50.64) ────────────────────────────┐
FAM-Core (Flask :8000)
├─ Oracle-Sync: 唯一后台线程,拉增量写 MariaDB 镜像 + 维护 sync_cursor
├─ Chat-Handler: /api/chat/ask查 sync_events 拼上下文 → 走 Oracle 编排)
│ ├─ Member-Manager: /api/member/name|merge回推 Oracle + 即时拉回) │
│ ▼ │
│ MariaDB (sentinel_home_ai): sync_videos / sync_events / sync_people / │
│ sync_cursor / chat_history │
FAM-UI 已迁至云服务器 Caddy :80见上
Surveillance Station :5000(录像机本体,摄像头插在这台上)
fam-notifier轮询 SS EventCenter → 推运动事件给甲骨文。无端口、无数据库,
游标存本地 JSON 文件。挂了重启即可,不影响网站。
└─────────────────────────────────────────────────────────────────────────────────┘
```
@@ -315,15 +318,23 @@ chat_history 独立表(问答上下文摘要留存)
**GET /api/oracle/avatar** — 人物头像:`?label=&w=` → jpeg从该人物候选事件 `person_appearances` bbox 裁剪)
**GET /health** — 服务状态(含已处理视频数)
### 5.2 FAM-CoreNAS :8000
**登录相关端点**2026-09-12 从 NAS FAM-Core 迁入,见 `fam-edge/src/fam_edge/auth.py`
| 端点 | 方法 | 说明 |
|------|------|------|
| `/login` | GET | 登录入口:生成 PKCE 参数后 302 跳 auth-hub 公网 `/authorize` |
| `/api/auth/callback` | GET | auth-hub 回跳:用 `AUTH_HUB_INTERNAL_BASE`(本机 :5300换 token + 拉 JWKS 验签,成功后种 HttpOnly cookie `fam_session`HS256 无状态签名2 小时);**任何一步失败都渲染错误页,绝不 302 回 `/login`**——旧实现失败即跳 `/login`,而 auth-hub 有会话时会立刻再签发 code两边对跳成死循环 |
| `/api/logout` | POST | 退出登录(清 cookie不影响 auth-hub 上的 SSO 会话) |
| `/api/auth/check` | GET | 登录态检查:`{"authed": true\|false, "username": "..."}` |
| `/api/auth/verify` | ANY | 给甲骨文 Caddy 的 `forward_auth` 用:已登录 204未登录 401 JSON |
### 5.2 FAM-Core甲骨文 127.0.0.1:5401
> 2026-09-13 起 FAM-Core 与 FAM-Edge 同机,直读它的 SQLite不再有 MariaDB 镜像层),且**只监听 127.0.0.1**——唯一客户端是同机 Caddy。登录端点在 FAM-Edge见 §5.1),鉴权由 Caddy 的 `forward_auth` 前置完成,所以本服务自身不做任何校验。
| 端点 | 方法 | 说明 |
|------|------|------|
| `/health` | GET | 服务健康(**免登录** |
| `/login` | GET | 登录入口302 跳转 auth-hub `/authorize`Authorization Code + PKCE**免登录** |
| `/api/auth/callback` | GET | auth-hub 登录回跳:拿 `code` 换 token、验 `id_token` 签名后种 HttpOnly cookie `fam_session`2 小时,**免登录** |
| `/api/logout` | POST | 退出登录(清本地 cookie不影响 auth-hub 上的登录态) |
| `/api/auth/check` | GET | 登录态检查:`{"authed": true\|false}`**免登录** |
| `/api/status` | GET | Oracle-Sync + MotionNotifier 状态sync running/cursormotion poll_enabled/pushed_total/heartbeat |
| `/api/chat/ask` | POST | 用户问答:`{"question","queried_person","queried_date"}``{"answer","context_summary","chat_id"}`(上下文来自 sync_events |
| `/api/chat/history` | GET | 对话历史(`?date=``?person=&limit=` |
@@ -540,17 +551,27 @@ task_id=28930s 测试片段)全链路打通:推送 5.7MB → Edge 分析
| 组件 | 节点 | 路径 | 启动 |
|------|------|------|------|
| FAM-Core | NAS | `/volume1/web/sentinel-home-ai/fam-core/` | `bash start_core.sh`gunicorn -w 1 :8000source .env 注入 DSM_*/ORACLE_SYNC_TOKEN/AUTH_HUB_* |
| FAM-Core | Oracle | `/opt/fam-core/` | **systemd `fam-core.service`**gunicorn -w 2 --threads 4绑 127.0.0.1:5401部署代码后 `sudo systemctl restart fam-core` |
| fam-notifier | NAS | `/volume1/web/sentinel-home-ai/fam-notifier/` | `setsid bash scripts/start_notifier.sh &`DSM 无 systemd挂了不自启NAS 上唯一运行的本项目进程 |
| FAM-UI | 云服务器甲骨文129.146.26.249 | `/var/www/fam-ui/` | Vue3 构建产物,由 Caddy :80 静态托管(无需独立进程);本地改代码后 `npm run build``dist/` rsync/tar 到云服务器 |
| FAM-Edge | Oracle | `/opt/fam-edge/` | **systemd `fam-edge.service` 守护**Restart=always部署代码后 `sudo systemctl restart fam-edge`(勿手动 setsid会端口冲突 |
| **AI-Gateway** | Oracle | `/opt/ai-gateway/` | **systemd `ai-gateway.service` 守护**Restart=always独立 venvPython 3.8gunicorn 绑定 `0.0.0.0:5100`(对外直接开放,非仅本机);部署代码后 `sudo systemctl restart ai-gateway` |
| Ollama | Oracle | systemd 托管 | 环境变量 `OLLAMA_KEEP_ALIVE=-1`**被 AI-Gateway 调用,不再被 FAM-Edge 调用** |
> **外网访问frp 内网穿透)**NAS 跑 `frpc``/etc/frp/frpc.toml`S99frpc.sh 守护),映射到云服务器 `129.146.26.249`frps :7000
> - `3000` → NAS Gitea、`8500` → NAS WordPress(8088)、**`8000` → NAS FAM-Core本系统**
> - 前端入口 `http://129.146.26.249/`Caddy 托管 FAM-UI/api 经 frp 隧道反代回 NAS FAM-Core :8000**需登录**(见 §5.2 `/login`
> - `3000` → NAS Gitea、`8500` → NAS WordPress(8088)
> - **`8000` → NAS FAM-Core 这条已于 2026-09-13 迁云后废弃**(本系统不再有任何"回源 NAS"的流量,可从 frpc.toml 删除
> - 前端入口 `https://smart-camera.zichuan.xyz/`:静态页、接口、登录全部由甲骨文提供,**NAS 离线也能正常访问**(只是不再有新的运动事件推进来),**需登录**(见 §5.1 `/login`
>
> **登录校验2026-08-22 新增2026-08-23 改为 fail-closed2026-08-31 接入 auth-hub 统一登录)**FAM-Core 不再自己保存/校验密码,全权委托给独立部署的 [auth-hub](http://129.146.26.249:5300)OAuth2 Authorization Code + PKCE / OIDC同一 Oracle 主机 :5300与本系统完全独立部署——`/login` 302 跳到 auth-hub `/authorize`,登录后带 `code` 跳回本服务 `/api/auth/callback`,服务端换 token、验完 `id_token` 签名JWKS后种本地会话 cookie。全站拦截仍在 `auth.py`:页面未登录 302 `/login``/api/*` 未登录 401接入参数 `AUTH_HUB_ISSUER`/`AUTH_HUB_CLIENT_ID`/`AUTH_HUB_CLIENT_SECRET`/`AUTH_HUB_REDIRECT_URI`NAS `.env` 配置,**无硬编码默认值**任一没配置直接拒绝所有登录fail closed`AUTH_HUB_REDIRECT_URI` 必须与 auth-hub 用 `manage_clients create` 登记的 redirect_uri 逐字符一致auth-hub 只做精确匹配,不做前缀/子串匹配)。白名单免登录:`/login` `/api/auth/callback` `/api/logout` `/api/auth/check` `/health` `/assets/*``/api/ss/webhook` 已随端点一起移除2026-08-25。本地会话仍是进程内 token + HttpOnly cookie**2 小时**),过期或重启 fam-core 需重新走一遍 auth-hub 登录auth-hub 侧注册是任何人都能自助注册(管理员审批后才能登录),**未对 fam-core 侧再加用户名白名单**——任何在 auth-hub 上审批通过的账号登录后都能访问本系统,这是有意识的取舍(自用场景,见项目记忆)
> **登录校验2026-08-22 新增2026-08-31 接入 auth-hub 统一登录2026-09-12 整体迁到甲骨文 FAM-Edge**:登录流程和会话校验现在都在甲骨文 FAM-Edge 的 `auth.py`§5.1NAS FAM-Core 自身不再做任何鉴权。迁移的直接原因NAS 或 frp 隧道一挂,`smart-camera.zichuan.xyz/login` 直接 502**连登录页都打不开**——登录是入口,不该依赖家里那台机器
>
> - **链路**`/login`Caddy → 本机 fam-edge :5000→ 302 到 auth-hub 公网 `/authorize`Authorization Code + PKCE→ 用户在 [auth-hub](https://auth.zichuan.xyz) 登录 → 回跳 `/api/auth/callback`(仍是 fam-edge→ 走**本机** `http://127.0.0.1:5300` 换 token、拉 JWKS 验 `id_token` 签名 → 种 cookie `fam_session`
> - **为什么服务端调用走本机**:旧实现是 NAS 跨公网访问 `https://auth.zichuan.xyz`,那条链路上 `jwt.PyJWKClient` 用 urllib + 系统 CA不像 requests 自带 certifi群晖上容易 `CERTIFICATE_VERIFY_FAILED`;两台机器的时钟偏差还会让 `iat` 看起来来自未来。同机直连把这两个坑一起消掉。但 `iss` 校验和浏览器跳转仍用公网 `AUTH_HUB_ISSUER`
> - **拦截**:甲骨文 Caddy 对 `/api/*` 做 `forward_auth` → fam-edge `/api/auth/verify`200/204 才反代回 NAS`/login`、`/api/auth/*`、`/api/logout` 直接由 fam-edge 处理Caddy 按路径具体程度排序,这几条稳定排在 `/api/*` 前面)
> - **NAS :8000 必须靠防火墙兜底**frps 把它暴露在公网(历史入口 `http://129.146.26.249:8000`),绕过 Caddy 直连就没有任何鉴权。甲骨文 iptables 只允许本机访问 :8000见 `docs/DEPLOY.md` §2.3
> - **参数**`/opt/fam-edge/.env`**无硬编码默认值**,缺任一项拒绝所有登录 fail closed`AUTH_HUB_ISSUER`(公网,浏览器跳转 + `iss` 校验)/ `AUTH_HUB_INTERNAL_BASE`(本机,换 token + JWKS/ `AUTH_HUB_CLIENT_ID` / `AUTH_HUB_CLIENT_SECRET` / `AUTH_HUB_REDIRECT_URI`(必须跟 auth-hub 登记的逐字符一致,只做精确匹配)/ `FAM_SESSION_SECRET`会话签名密钥≥32 字节)
> - **会话是无状态签名 cookie**HS2562 小时fam-edge 重启不掉线(旧实现是进程内 token 表fam-core 一重启全员下线);要强制全员下线就换掉 `FAM_SESSION_SECRET` 再重启
> - auth-hub 侧任何审批通过的账号登录后都能访问本系统,**未再加用户名白名单**——自用场景的有意取舍(见项目记忆)
> Oracle 部署方式:本地 git 提交 push Gitea → tar 管道到 `/opt/fam-edge``--strip-components=1` 解临时目录再 cp避免动 data/venv/gdrive_videos。**AI-Gateway 是独立 git 仓库**http://192.168.50.64:3000/ericwyuan/ai-gateway见 §10.4),同样 tar 管道部署到 `/opt/ai-gateway`,互不影响。