Files
sentinel-home-ai/PROGRESS.md
ericwyuan deaebf22b4 fix(fam-edge): 生产者扫描跳过已被 DiskGuard 清理的文件
生产者列目录之后、读 mtime 之前,DiskGuard 可能刚好把那个文件删了(两个后台线程
的正常竞态),os.path.getmtime 抛 FileNotFoundError。代价不是少处理一个文件,而是
**整轮扫描中断**——排在后面的新素材本轮全都登记不上,要等下一轮。

线上日志:生产者扫描异常: [Errno 2] No such file or directory:
/opt/fam-edge/gdrive_videos/20260911PM/Generic_ONVIF-001-20260911-234730-...mp4
(01:30:01 抛出,同一秒 DiskGuard 正在清理那批文件)

改成捕获 OSError 跳过该文件继续扫。新增 tests/test_video_queue.py:被删的跳过且
后面的照常登记、仍在写入的(mtime 太新)依然跳过。

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

651 lines
64 KiB
Markdown
Raw Permalink 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-08-31
## 服务运行状态
| 服务 | 节点 | 地址 | 状态 | 验证结果 |
|------|------|------|------|---------|
| FAM-Core | NAS | 0.0.0.0:8000 | ⚠️ 代码已完成,待生产部署 | health=ok, gunicorn 单 workerOracle-Sync + MotionNotifier 轮询 SS 事件推送(游标 DB 续用/失败重试);已部署事件时间轴删除功能;`/api/status`/`/api/ss/status` 500 bug 已修复;**登录改接 auth-hub 统一登录OIDC本地全链路验证通过生产 client 注册 + NAS `.env` 配置 + 部署重启尚未执行** |
| FAM-Edge | Oracle新机 129.146.26.249 | 0.0.0.0:5000 | ✅ 运行中 | systemd 守护fam-edge.service素材→运动片段分割→只分析片段问答改为转发 AI-Gateway队列消费正常已部署 `/api/oracle/video/delete`FFmpeg 已补装;新增 DiskGuard 磁盘守护 |
| **AI-Gateway** | Oracle | 0.0.0.0:5100 | ✅ 运行中 | systemd 守护ai-gateway.service2026-08-23 新增;独立仓库/独立部署;`/health` 正常端到端问答实测成功provider=nvidia |
| MariaDB | NAS | 127.0.0.1:3306 | ✅ 运行中 | 10.11.11, 6 张表, utf8mb4 |
| Ollama | Oracle | 127.0.0.1:11434 | ✅ 运行中 | qwen2.5:7b仅智能问答兜底不参与视觉/融合);**被 AI-Gateway 调用,不再被 FAM-Edge 直接调用** |
## 2026-08-31 FAM-Core 接入 auth-hub 统一登录OIDC
- **背景**:独立的统一登录/SSO 服务 [auth-hub](http://129.146.26.249:3000/ericwyuan/auth-hub)(跟本项目完全独立的仓库/数据库/部署OAuth2 Authorization Code + PKCE + OIDC已单独开发验证完成本地全链路 47 单测通过目标是自己的几个网站统一接到这一处身份服务不用每个网站各自维护一份账号密码。FAM-Core 是第一个接入方。
- **改造范围**`fam-core/src/fam_core/auth.py` 全量重写commit 待提交):
- 原来自己校验 `FAM_AUTH_USER`/`FAM_AUTH_PASS` 的账号密码逻辑、内置深色登录表单页全部移除;`GET /login` 改为直接 302 跳转 auth-hub `/authorize`(带 PKCE `code_challenge`/`state`,均存进程内 `_pending`10 分钟过期)
- 新增 `GET /api/auth/callback`:收 auth-hub 回跳的 `code`,服务端到服务端 `POST /token``id_token`(用 `PyJWT` + `PyJWKClient` 拉 auth-hub JWKS 验 RS256 签名 + `iss`/`aud`),验证通过后种回原有的 `fam_session` HttpOnly cookie**2 小时有效,机制不变**)——下游 `is_authed()`/`init_auth()` 全局拦截逻辑完全没动
- 接入参数 `AUTH_HUB_ISSUER`/`AUTH_HUB_CLIENT_ID`/`AUTH_HUB_CLIENT_SECRET`/`AUTH_HUB_REDIRECT_URI` 走环境变量NAS `.env`,沿用 `FAM_AUTH_*` 时代"未配置齐全直接拒绝所有登录"的 fail-closed 取舍),`AUTH_HUB_REDIRECT_URI` 必须与 auth-hub 端 `manage_clients create` 登记的 redirect_uri 逐字符一致
- `requirements.txt` 新增 `PyJWT>=2.8.0``cryptography>=42.0.0`
- **访问控制取舍(用户明确决策)**:不在 fam-core 侧加用户名白名单——任何在 auth-hub 上(自助注册 + 管理员审批后)拥有账号的人登录后都能访问本系统;不保留 `FAM_AUTH_USER`/`FAM_AUTH_PASS` 作为备用登录方式SSO 是唯一入口
- 测试:`tests/test_auth.py` 全量重写PKCE 生成、state 校验、token 交换成功/失败、id_token 验签失败、白名单、`before_request` 拦截fam-core 全量 39/39 通过
- **端到端验证(本地)**:本地起了一份 auth-hub 开发实例(:5300+ 一个仅含 `auth_bp` 的最小 Flask 壳子(模拟 fam-core跳过 MariaDB 依赖),用真实浏览器走完整 Authorization Code + PKCE 闭环——`/` 未登录 302 `/login` → auth-hub 登录页 → 登录成功回跳 `/api/auth/callback` → 换 token + 验签 → 种 cookie → 落地首页;`/api/auth/check` 确认 cookie 生效;`/api/logout` 确认清会话。验证完把临时创建的 OAuth client 和测试账号都从 auth-hub 本地库删掉了。
- **生产部署进度**
1. ✅ 已在 Oracle 生产 auth-hub`/opt/auth-hub`:5300注册正式 client `FAM-Core`client_id/secret 已生成,明文只显示过一次,未写入仓库,需要的话找 auth-hub 管理后台或 `manage_clients list` 核对 client_id
2.**NAS `.env` 待补**(本次会话没有 NAS SSH 免密权限,未执行):`AUTH_HUB_ISSUER=http://129.146.26.249:5300``AUTH_HUB_CLIENT_ID``AUTH_HUB_CLIENT_SECRET``AUTH_HUB_REDIRECT_URI=http://129.146.26.249/api/auth/callback`
3. ⏳ 部署新代码到 NAStar 管道,见 `docs/DEPLOY.md` §3`bash start_core.sh` 重启——待执行
4. ⏳ 生产环境浏览器实测一遍完整登录闭环——待执行
## 2026-08-28 Oracle 迁移故障排查FFmpeg 缺失 + 磁盘写满死循环 + DiskGuard
- **触发**:用户反馈事件时间轴历史图片丢失、"谷歌同步是不是有问题"。逐层排查,发现的是三个叠在一起的独立问题,不是一个:
1. **8/25 迁移新机器时漏装 FFmpeg**`ffprobe: command not found`。导致 `videos.duration_sec` 全部读成 0、运动片段分割 `_segment_motion_clips` 全部失败(`ffmpeg` 也没装190 个素材全部"分割 0 段"`events` 表完全是空的,`motion_clips/` 目录空的——事件时间轴从 8/25 起就没有任何新内容,图片自然也生成不出来。**已修复**`apt-get install ffmpeg`,恢复正常。
2. **8/25 之前的历史图片无法找回**:迁移时只搬了 `data/oracle.db`(数据库/文字记录),没有搬 `motion_clips/`磁盘上的运动片段视频文件缩略图的生成源旧机器129.146.203.203)已经释放销毁。历史事件的文字描述还在(早就同步进了 NAS 镜像),但配图永久丢失,无法找回。
3. **Oracle↔Google Drive 同步"只下载不删除"**:排查一度走了弯路(先怀疑 NAS CloudSync 停摆,后发现新文件其实一直在下载,是判断证据不足导致的误判)。最终精确对比 Google Drive 远端141 文件,最早 8/25vs Oracle 本地264 文件,最早 8/15实锤148 个文件是"远端已删、本地未删"的孤儿文件。根因是磁盘被写满到 100%`no space left on device`),触发 rclone 内置安全机制——同步过程中遇到 IO 错误就整体拒绝执行删除,形成"越满删不掉,删不掉越满"的死循环。**已修复**:手动删除 148 个孤儿文件,释放 41G磁盘 100%→59%;重新跑 `rclone_sync.sh` 验证 `exit=0` 无错误,下载+删除恢复正常。
- **新增 DiskGuard 磁盘守护**commit `ef56ae5`,永久性预防措施):`fam-edge/oracle_db.py` 新增 `get_oldest_purgeable_material()`(只挑最旧的、已完成分割阶段的整段素材,绝不碰运动片段和处理中的素材);`disk_guard.py` 新增后台线程5 分钟检查一次,剩余空间 <10GB 触发清理,删到 15GB 水位为止;`/api/oracle/activity` 新增 `disk` 字段(实时剩余空间 + 最近清理动作。9 个新测试fam-edge 全量 139/139 通过。
- **用户决策**8/25-28 期间因 FFmpeg 缺失而"分割 0 段"的 190 个素材/736 个运动事件,明确选择不重新处理,翻页接受这段时间的空白。
- **顺带修复**commit `09708bd``/api/status``/api/ss/status` 500——上次"移除 SS Webhook"重构(`cf3d145`)删了 `MotionNotifier``camera_name_to_id` 等属性但 `status()` 忘记同步更新,导致这两个端点自那次重构起就一直是坏的。
- **文档同步**commit `3ec79de`README 里 5 处 + `vite.config.js` 1 处仍描述"`/api/ss/webhook` 保留为可选"的过时内容,实际已被彻底删除,逐处订正。
## 2026-08-24 事件时间轴支持删除视频会话
- **决策**:用户要求事件时间轴能删除某个视频会话;调研确认项目里没有任何 delete 类端点先例,且增量同步(`get_sync_delta`)只做 upsert 感知不到 Oracle 端的物理删除NAS 镜像必须显式清理,不能靠 `trigger_now()` 拉增量。用户明确要求连同磁盘上的视频文件一起删除,明确选择不做"防止误删事件被重新分割"的额外保护机制(风险极低:素材一旦标记 done 就不会被生产者重新捡起)
- **三端实现**commit `28397e9`
- fam-edge`oracle_db.py` 新增 `delete_video(video_id)`(写锁保护,先删 events 再删 videos再删磁盘文件不清理 `ss_motion_events` 源事件);`api_gateway.py` 新增 `POST /api/oracle/video/delete`
- fam-core`oracle_sync.py` 新增 `push_video_delete()``db_layer.py` 新增 `delete_sync_video()`(项目里第一个"NAS 直接写自己镜像表"的函数);`ui_api.py` 新增 `DELETE /api/ui/videos/<id>`(先回推 Oracle 成功后才清本地镜像,回推失败 502 不改本地状态,避免数据不一致)
- fam-ui`Timeline.vue` 详情卡片新增"🗑 删除会话"按钮(原生 `confirm()` 二次确认,删除成功后从本地列表移除 + 重新选中 + 刷新统计卡);`api.js` 新增 `deleteVideo()`
- **测试**`test_oracle_db.py` 新增 6 个 `delete_video` 单元测试(正常删除/磁盘文件删除/文件已缺失不报错/不存在返回 None/不影响 ss_motion_eventsfam-edge 全量 130/130 通过
- **端到端验证**:全程用自造测试数据(`motion_TESTDELETE*`未触碰任何真实监控记录——Oracle 侧直接调 `/api/oracle/video/delete` 验证记录+文件删除+幂等 404NAS 侧手动插入镜像行模拟已同步状态,登录后调 `DELETE /api/ui/videos/<id>`,验证 Oracle 记录+文件、NAS 镜像记录均被清理
- **已知限制**:前端浏览器 UI 层面未做可视化验证fam-core 全站要求登录,出于"不代用户输入密码"的原则没有走浏览器交互式登录),只验证了 `npm run build` 编译通过 + 完整 API 链路
## 2026-08-23 问答链路抽离为独立 ai-gateway 服务
- **决策**:原本嵌在 FAM-Edge 里的问答模型降级链NVIDIA 文字模型链 → Gemini 非 flash 文字模型链 → 本地 Ollama 兜底,含 key 轮换/熔断跟视频分析业务无关是通用能力抽成独立服务OpenAI 兼容协议 `/v1/chat/completions`),除了 FAM-Edge 自己(改为转发调用),别的项目也能直接接入
- **新增独立仓库/项目 `ai-gateway`**http://192.168.50.64:3000/ericwyuan/ai-gateway`app.py`Flask`/v1/chat/completions` 流式+非流式、`/v1/models` 占位、`/health` 免鉴权)/ `auth.py`Bearer token 鉴权fail-closed/ `orchestrator.py``ChatOrchestrator`,按 provider 顺序降级,保留"未吐字才切换、已吐字后中途失败直接结束"语义)/ `adapters/`NVIDIA/Gemini/Ollama纯文本从 fam-edge 对应适配器裁剪 chat 相关代码而来);测试 37/37 通过
- **FAM-Edge 侧改动**commit `5caeb29``qa.py` 重写为 HTTP 转发客户端(调 ai-gateway `/v1/chat/completions`,翻译回原有 `run_qa`/`run_qa_stream` 契约,`api_gateway.py` 和 FAM-Core 调用方零改动);删除 `model_adapters/ollama_adapter.py` 及其测试;`gemini_adapter.py`/`nvidia_adapter.py` 移除 `chat()`/`chat_stream()`/问答专用超时(只保留 `analyze_video``app.py` 移除 Ollama 预热逻辑;`config.yaml` 移除 3 个问答专用 model 条目,新增 `ai_gateway` 客户端配置块;测试 125/125 通过
- **部署**Oracle 新建 `/opt/ai-gateway/`(独立 venvPython 3.8systemd `ai-gateway.service` 守护gunicorn 绑定 `0.0.0.0:5100`对外直接开放Bearer token 鉴权,用户明确选择"别的项目也能从这台机器之外访问"而不是仅本机);`AI_GATEWAY_TOKEN`/`NVIDIA_API_KEY`/`GEMINI_API_KEY*` 存在独立的 `/opt/ai-gateway/.env`(用户选择两份 `.env` 各自独立维护,而非复用 `/opt/fam-edge/.env`,代价是以后轮换 key 需要改两处)
- **验证**`/health` 通过;`curl` 直接测 `/v1/chat/completions`(带 token端到端成功provider=nvidiaFAM-Edge `/api/edge/chat/ask` 非流式 + `/api/edge/chat/ask/stream` 流式均验证通过,事件格式(`provider_trying`/`chunk`/`done`)不变,中文无乱码;队列/pending/failed 数量正常,无积压
- **已知代价**NVIDIA/Gemini key 现在两处各存一份(`/opt/fam-edge/.env` + `/opt/ai-gateway/.env`),非最初设计的"复用同一份",用户已知情并选择保留现状
## 2026-08-22 运动事件驱动架构v3
- **决策**不再分析整段视频。rclone 整段素材保留在 Oracle按 NAS 推送的 `ss_motion_events`start_time/duration**ffmpeg 分割成运动片段**,只分析片段。
- **改动**commit `a1523b4` + `8d6cfad`,已部署 Oracle systemd 重启):
- `oracle_db.py`videos 表兼容加 `motion_event_id`/`camera_id` 列;新增 `get_motion_events_in_range`窗口内已结束事件grace 容差)/`has_unfinished_motion_in_range`/`get_video_by_motion_event_id`
- `video_processor.py`:素材→分割(只分割已结束事件,`-c:v copy -c:a aac` 保留音频motion_event_id 幂等)/片段→只分析 双分支;修复 ffmpeg args 缺可执行文件 bug
- `video_queue.py`:素材分割出的片段入队
- `config.yaml`:新增 `motion_segment`clips_dir/keep_audio/min_duration/unfinished_grace_sec
- **E2E 验证(真机)**:素材 `Generic_ONVIF-001-20260822-110859-...mp4` → 分割 35 段(真实 event_id 25504-25538、duration 1-132s→ 片段只分析summary 真实:人物/动作)→ events绝对时间 ts→ NAS 同步 → `/api/ui/videos` 显示 motion_ 片段字段契约不变filename/event_start_time/camera_name/event_count/summary_json/compute_provider`/api/ui/videos/<id>` 详情 events + people 正常 → `/api/proxy/frame` 帧图 200 OK
- **关键认知**SS 事件 duration=0 仅在动作**进行中**(结束时必为正数,实测 id=25538 从 0→2s故只分割已结束事件start+duration ≤ now+grace进行中的等下一轮。
- **部署注意**Oracle fam-edge 由 **systemd `fam-edge.service` 守护**Restart=always代码部署后必须 `sudo systemctl restart fam-edge`(手动 setsid 会与守护打架导致端口冲突)。
## 2026-08-22 清数据重提取 + 人物管理重设计 + 文档同步commits 4cf4fc4/6a29e88/9664459
- **清数据重提取**用户决策整段提取旧数据全删按运动视频重新提取Oracle videos(364)/events(1687)/people(44)/model_calls/service_activity 全清 + motion_clips 清空(保留 ss_motion_events 93 条 + 素材 + 配置先备份NAS sync_* 四表全清重启两端自动重提取8/22 素材→运动片段8/15-21 历史素材无运动事件→0 段)
- **时间轴过滤**:历史素材"分割 0 段"空会话淹没时间轴 → db_layer.get_sync_videos/get_sync_stats 加内容过滤(`LEFT(filename,7)='motion_'` OR 有事件),前端契约不变;**踩坑pymysql execute 用 % 做占位符SQL 字面量 'motion_%' 的 % 报 "unsupported format character" 500 → 改 LEFT 判断**
- **人物管理重设计**`GET /api/ui/people/clips?label=`db_layer.get_sync_people_clips按 label/canonical_name 匹配 person_list_json → 关联运动片段,含 first_ts/clip_eventsPersonCard.vue 新增「运动片段」区块(缩略图/时间/摘要/事件数,点击跳 `/timeline?video=`Timeline.vue 支持 query 定位
- **文档同步**README 更新 2.1 网络要点/2.2 拓扑(Streamlit→Vue3)/3.1-3.3 模块表(poll 主路径+people clips)/4.1 表清单(motion_event_id/ss_motion_events)/5.1-5.2 API 表(people/clips+frame/avatar)/6.1 历史标注/8 部署(systemd+Vue3+motion_segment config)/12 进度docs/DEPLOY.md 重写为 v3已 push
- **验证**/api/ui/videos 全为 motion_ 片段15 条);/api/ui/people 3 身份people/clips 返回媳妇 3 个片段;前端 dist 已构建部署
| Tailscale | Oracle ↔ NAS | 100.74 ↔ 100.70 | ⚠️ 待修复 | Tailscale 运行但端口不通当前用公网IP |
| FAM-UI | NAS | 0.0.0.0:8501 | ✅ 运行中 | Streamlit 1.61.1, HTTP 200, health=ok |
## 环境状态
### Oracle Cloud (129.146.26.249) - FAM-Edge 节点
| 项目 | 状态 | 备注 |
|------|------|------|
| OS | Ubuntu 20.04 ARM64 | Ampere A1 2C12G |
| Python | 3.8.10 | python3-venv 已安装 |
| pip | 25.0.1 (venv内) | |
| FFmpeg | 已安装 | apt 安装 |
| Ollama | 0.32.14 | |
| qwen2.5:7b | 已拉取 | 纯文本模型,仅问答兜底(原 llava-phi3 已废弃) |
| Python 依赖 | 全部安装成功 | flask, gunicorn, requests, PyYAML, opencv, numpy |
| 模块导入验证 | PASS | FAM-Edge 全部 8 个模块导入正常 |
| 代码部署 | /opt/fam-edge/ | rsync 同步 |
| venv | /opt/fam-edge/venv/ | 已创建 |
| Tailscale | 1.102.2 已安装 | **待用户认证** |
### Synology NAS (192.168.50.64) - FAM-Core + FAM-UI 节点
| 项目 | 状态 | 备注 |
|------|------|------|
| 型号 | DS220+ (Geminilake) | DSM 7 |
| Python | 3.8.15 (系统) + 3.10.20 (venv) | FAM-Core/UI venv 使用 Python 3.10 |
| pip3 | venv 内可用 | fam-core/venv + fam-ui/venv |
| MariaDB | **10.11.11 已运行** | 端口 3306, root 密码 iLoveJava5! |
| sentinel_home_ai 库 | **6 张表已创建** | DDL 执行成功 |
| Docker | 24.0.2 (ContainerManager) | 路径 /var/packages/ContainerManager/target/usr/bin/docker |
| Tailscale | **已运行** | IP: 100.70.234.39, hostname: ericwyuan-nas |
| FAM-Core venv | ✅ 已创建 | Python 3.10.20, flask/PyMySQL/gunicorn/PyYAML/requests |
| FAM-UI venv | ✅ 已创建 | Python 3.10.20, Streamlit 1.61.1 + pandas 2.3.3 |
| Streamlit | ✅ 已安装 | 1.61.1, 运行于 0.0.0.0:8501 (headless) |
### 网络互通 (Tailscale)
| 节点 | Tailscale IP | 状态 |
|------|-------------|------|
| NAS (ericwyuan-nas) | 100.70.234.39 | 已连接 |
| Oracle (oracle-fam-edge) | 待认证 | **需用户访问认证 URL** |
| 当前方案 | Oracle Tailscale 待认证,认证后可直连 | |
## 模型可行性基准测试 (Task 1.4)
### 测试环境
- Oracle Ampere A1 2C12G ARM64
- Ollama 0.32.14 + llava-phi3:latest (2.9 GB)
- Python 3.8.10 + requests
### 测试结果
| 场景 | 加载时间 | Prompt 处理 | 生成时间 | Token 数 | 总耗时 | 是否达标(≤8s) |
|------|---------|------------|---------|---------|--------|-------------|
| 冷启动 (首次推理) | 54.56s | 37.40s | 17.22s | 86 | 109.21s | 否 (首次加载) |
| 预热 (无限制) | 0.06s | 0.20s | 13.06s | 65 | 13.35s | 否 |
| 预热 (num_predict=30) | 0.07s | 1.41s | 2.83s | 15 | **4.34s** | **PASS** |
### 结论
1. **冷启动延迟**: 首次推理需加载 2.9GB 模型到内存,耗时 ~55s。建议通过 systemd keep-alive 或定时心跳保持模型常驻
2. **预热性能**: 模型加载后,单图推理可控制在 4-5s限制输出 30 token满足 ≤8s 设计目标
3. **ARM CPU 瓶颈**: token 生成速度 ~0.1-0.2s/token多帧分析(5-8帧)需合理设置 `num_predict`(默认 500)
4. **已优化**: OllamaAdapter 新增 `num_predict` 可配置参数,默认 500
## 任务进度
### 已完成
| # | 任务 | 提交 | 日期 |
|---|------|------|------|
| 1 | 项目骨架 + 全部 Python 模块代码 | `d741ade` 等 | 2026-08-19 |
| 2 | DDL 数据库建表脚本 | `d741ade` | 2026-08-19 |
| 3 | 部署脚本 + 文档 | `d741ade` | 2026-08-19 |
| 4 | __init__.py 导出链路 | `c45464c` | 2026-08-19 |
| 5 | Storage-Cleaner 补全 | `5e4587b` | 2026-08-19 |
| 6 | 服务器凭证写入 README | `ba905d2` | 2026-08-19 |
| 7 | requirements.txt 兼容 Python 3.8 | `c1bfac5` | 2026-08-19 |
| 8 | Oracle 部署: venv + 依赖 + 导入验证 | `50d8353` | 2026-08-19 |
| 9 | 模型基准测试 (llava-phi3 ≤8s PASS) | `50d8353` | 2026-08-19 |
| 10 | OllamaAdapter 优化 (num_predict) | `50d8353` | 2026-08-19 |
| 11 | Tailscale 安装 (Oracle + NAS) + 认证 | `e5ac667` | 2026-08-19 |
| 12 | NAS MariaDB DDL 执行 (6 张表) | `e5ac667` | 2026-08-19 |
| 13 | db_layer.py 切换 PyMySQL (45KB 替代 19MB) | ✅ 完成 | 2026-08-19 |
| 14 | config_loader.py 路径修复 (3 级 dirname) | ✅ 完成 | 2026-08-19 |
| 15 | NAS Python 依赖安装 (flask/gunicorn/pymysql/PyYAML) | ✅ 完成 | 2026-08-19 |
| 16 | NAS 代码部署 + FAM-Core 导入验证 PASS | ✅ 完成 | 2026-08-19 |
| 17 | NAS DB 连接验证 PASS (PyMySQL → MariaDB 10.11.11) | ✅ 完成 | 2026-08-19 |
| 18 | config.yaml 实际配置 (FAM-Core + FAM-Edge + FAM-UI) | ✅ 完成 | 2026-08-19 |
| 19 | NAS 启动 FAM-Core (gunicorn, Python 3.10 venv) | ✅ 完成 | 2026-08-19 |
| 20 | Oracle 启动 FAM-Edge (gunicorn) | ✅ 完成 | 2026-08-19 |
| 21 | MariaDB JSON 路径兼容修复 (name_member) | ✅ 完成 | 2026-08-20 |
| 22 | FAM-Core API 全端点测试通过 | ✅ 完成 | 2026-08-20 |
| 23 | FAM-Edge 聊天代理端点 (/api/edge/chat) | ✅ 完成 | 2026-08-20 |
| 24 | FAM-UI PyMySQL 迁移 | ✅ 完成 | 2026-08-20 |
| 25 | FAM-Core 配置切换至 Oracle 公网 IP | ✅ 完成 | 2026-08-20 |
| 26 | 端到端聊天验证 (Core→Edge→Ollama) | ✅ 完成 | 2026-08-20 |
| 27 | NAS 安装 Streamlit + pandas + 部署 FAM-UI | ✅ 完成 | 2026-08-20 |
| 28 | Ollama 模型常驻内存 (OLLAMA_KEEP_ALIVE=-1) | ✅ 完成 | 2026-08-20 |
| 29 | 关键帧提取改为自适应帧数 (随视频时长动态计算) | ✅ 完成 | 2026-08-20 |
| 30 | FFmpeg 快速 seek 替代 fps 滤镜 (6-8x 提速) | `7ce8aff` | 2026-08-20 |
| 31 | 真实视频性能基准测试 (30min 360MB 视频) | ✅ 完成 | 2026-08-20 |
| 32 | 视频端到端集成测试 (task 289 → SUCCESS, 落库正确) | ✅ 完成 | 2026-08-20 |
| 33 | 生产目录切换 forward-only (285 历史视频占位跳过) | ✅ 完成 | 2026-08-20 |
| 34 | 今日 17 片段诊断根因Ollama 本地融合 300s 超时) | ✅ 完成 | 2026-08-20 |
| 35 | **架构重构**移除本地融合run_text_fusion云端 VLM 直出 JSON → format_cloud_result → 直存 DB | `babf5b0` | 2026-08-20 |
| 36 | 适配器 chat() 方法 + run_qa 三模型降级Gemini→NVIDIA→Ollama 兜底) | `babf5b0` | 2026-08-20 |
| 37 | 端点 /api/edge/chat/ask + chat_handler 改走 Edge 编排qa_url | `babf5b0` | 2026-08-20 |
| 38 | 双端部署验证Oracle 7 文件 + 重启NAS chat_handler + config 手术 + HUP实测 /api/edge/chat/ask provider=nvidia、NAS→Edge 编排 43s 落库 | ✅ 完成 | 2026-08-20 |
| 39 | **fix: NVIDIA 单帧 JSON 解析** — parse_vlm_json 要求全 schema 但 NVIDIA 只返回单帧 JSON改用轻量 _parse_single_frame_json | `853cb21` | 2026-08-20 |
| 40 | **fix: Gemini timeout 30s→90s + 熔断器 threshold 3→5 cooldown 600→300** | `853cb21` | 2026-08-20 |
| 41 | **fix: dispatcher crash on invalid failure_stage** — Edge 返回 'process' 不在 ENUM 中导致 DataError 崩溃db_layer 加容错映射 + dispatcher per-task try/except | `d75c745` | 2026-08-20 |
| 42 | **视频管线批量处理验证** — task 291/297 SUCCESSNVIDIA 6 帧分析 ~15s/任务events + event_details 落库正确 | ✅ 完成 | 2026-08-20 |
| 43 | **异步任务队列架构** — Edge 端 SQLite 队列 + TokenBucket 速率限制 (Gemini 1000 RPM, NVIDIA 40 RPM, burst 2x) + 消费者线程 | `02da23e` | 2026-08-20 |
| 44 | **NAS Dispatcher 重构为 enqueue 模式** — 上传视频后立即返回 202不等 AI 处理结果 | `02da23e` | 2026-08-20 |
| 45 | **NAS Poller 线程** — 定期从 Edge /api/edge/results 拉取处理结果写 MariaDB | `02da23e` | 2026-08-20 |
| 46 | **fix: dispatcher/poller 超时分离模式** — timeout 改为 (connect, read) 元组stale_timeout 3600→600s | `4f0f19b` | 2026-08-20 |
| 47 | **异步队列端到端验证** — task 312 (5.7MB e2e clip): 上传38s + Edge处理54s + Poller落库 = 93s 总耗时event_id=12 落库 PASS | ✅ 完成 | 2026-08-20 |
| 48 | **分块断点续传上传** — 20MB/块 + 分块级重试(3次) + 断点查询 + /assemble 合并入队 | `b6c13a9` | 2026-08-20 |
| 49 | **串行上传 + 看门狗** — Dispatcher limit=1 避免带宽争抢 + 线程存活检测(is_alive) + 60s 看门狗自动重启 + 退避缩短至 30×(n+1)s | `881ea3f` | 2026-08-20 |
| 50 | **fix: 分块大小 5MB + chunk_size 变更防护** — Edge 端 total_chunks 变更自动清理旧分块NAS 端断点续传检测 total_chunks 不匹配时从头上传timeout (60,180) | `5915cf4` | 2026-08-20 |
| 51 | **运动监测重构为 Webhook 驱动(去轮询)** — 关闭 MotionNotifier 轮询SS 事件经「行動規則/Webhook」`POST :8000/api/ss/webhook` 推送FAM-Core 映射(%DEVICE_NAME%→camera_id、%EVENT_TIME%→epoch、合成稳定 event_id`POST /api/ss/motion` 推甲骨文;甲骨文 `ss_motion_events` 落库、video_processor 改本地运动预过滤 | `ca6d425` | 2026-08-22 |
| 52 | **fix: MotionNotifier 补回 camera_ids 属性** — 避免重新开启轮询时 `_fetch_events` 引用缺失属性崩溃 | `e52d8e9` | 2026-08-22 |
| 53 | **文档同步** — README 架构章节2.2.1 运动监测链路)+ API 表 + 模块表补充 v3 Webhook 架构PROGRESS 状态/变更记录更新 | - | 2026-08-22 |
### 待完成
| # | 任务 | 依赖 | 优先级 |
|---|------|------|--------|
| 48 | 单元测试 (JSON parser, circuit breaker, schema) | - | 中 |
| 49 | Tailscale 防火墙修复 (NAS↔Oracle) | - | 低 |
| 50 | daily_summaries 每日摘要 | - | 低 |
| 51 | **视频预压缩** — 360MB 生产视频 HTTP 上传超时/断连,需在 NAS 端预压缩后再上传 Edge5MB 分块已缓解但 72 块仍需 ~18min | - | 中 |
## 技术决策记录
1. **移除 google-generativeai SDK 硬依赖** - 代码用 requests 直接调 REST API兼容 Python 3.8
2. **rsync 替代 git clone** - Oracle 无法直连 NAS Gitea (无 Tailscale),用 rsync 从本地同步
3. **num_predict 参数** - ARM CPU 上 qwen2.5:7b 生成速度较慢,限制输出 token 数控制延迟
4. **PyMySQL 替代 mysql-connector-python** - 45KB 纯 Python 替代 19MB C 扩展NAS 无需编译
5. **MariaDB JSON 路径兼容** - MariaDB 10.11 不支持 MySQL 的 `$[*]` 通配符 JSON 路径和 `->` 操作符name_member() 改用 Python 层解析 + 逐行 UPDATE
6. **Python 3.10 venv** - NAS 系统 Python 3.8 过旧,用 Synology Python3.10 包创建 venv
7. **FAM-Edge 聊天代理** - 历史:/api/edge/chat 直连 Ollama 代理;架构重构后被 `/api/edge/chat/ask` 三模型编排端点取代(旧端点保留兼容)
8. **Oracle 公网 IP 替代 Tailscale** - Tailscale 两节点在线但端口不通防火墙edge_url 和 qa_url 改用 Oracle 公网 IP 129.146.26.249
9. **Ollama 模型常驻内存** - systemd 加 `OLLAMA_KEEP_ALIVE=-1`,模型加载后永不卸载,消除 55s 冷启动延迟,常驻占用 4.3GB 内存(系统 12GB 够用)
10. **关键帧自适应帧数** - 原固定 5-8 帧对长视频太稀疏30分钟仅8帧=每3.75分钟1帧改为随视频时长自适应候选帧 `clamp(duration_min×2, 30, 120)`,关键帧上限 `clamp(duration/150s, 8, 30)`。30分钟→12帧60分钟→24帧封顶30帧
11. **云端直出直存(架构重构)** - 本地 Ollama 完全移出视频链路:云端 VLMGemini 多图单请求 / NVIDIA 逐帧聚合)直接产出结构化 JSONEdge 仅 `format_cloud_result` 格式化/校验(无模型调用)后直存 NAS DB。根因CPU 版 Ollama 对无上限融合 prompt 预填充极慢导致 300s 超时
12. **Q&A 三模型降级编排** - 智能问答由 Edge `run_qa` 编排Gemini → NVIDIA → 本地 Ollama仅两云端都失败才启用本地兜底各适配器实现 `chat()` 纯文本接口
13. **FFmpeg 快速 seek 替代 fps 滤镜** - 原方案 `ffmpeg -vf fps=1/interval` 需全解码视频30min 360MB 视频在 ARM CPU 上需 180s+(超 120s 超时)。改为逐帧 `ffmpeg -ss <ts> -frames:v 1` 快速 seek仅 33s6-8x 提速)
14. **异步任务队列架构 (SQLite + TokenBucket)** - 原 Dispatcher 同步推送 360MB 视频至 Edge 等待 AI 处理,单任务阻塞 15+ 分钟导致后续任务积压。重构为NAS Dispatcher 仅上传视频入 Edge SQLite 队列202 立即返回)→ Edge 消费者线程异步处理TokenBucket 按 API 限制 2x 速率Gemini 1000 RPM / NVIDIA 40 RPM→ NAS Poller 定期拉取结果写 MariaDB。e2e 验证5.7MB 视频全链路 93s (上传 38s + 处理 54s + 落库 1s)
15. **超时分离模式** - `requests.post(timeout=300)` 对大文件上传无总超时限制per-write 不触发),改为 `timeout=(10, 60)` 分离连接/读取超时,配合 stale_timeout 600s 回收僵尸任务
16. **生产视频预压缩(待实施)** - 360MB 30min 1080p H.264 视频跨公网 HTTP 上传超时/断连,需在 NAS 端 FFmpeg 预压缩(降分辨率/码率)后再上传
17. **串行上传策略** - Dispatcher 轮询 limit=1一次只处理一个任务避免多任务并行上传争抢带宽。NAS 上行带宽仅 ~1Mbps并行上传会导致所有任务都超时
18. **5MB 分块大小** - 实测 NAS→Oracle 上行带宽 ~1Mbps1MB 需 6s5MB 需 35s10MB 需 85s。20MB 分块在 ~1Mbps 下需 170s 超出 120s 读取超时。5MB 分块 + timeout(60,180) 在 ~35s/块 下有充裕余量72 块 360MB 约 18min 完成上传
19. **chunk_size 变更防护** - Edge 端 upload_chunk 检测 total_chunks 变更(由 chunk_size 变化导致),自动清理旧分块防止混合不同大小分块导致 assemble 后文件损坏。NAS 端 _query_uploaded_chunks 比对 expected_total != edge_total 时跳过断点续传
20. **看门狗机制** - app.py 启动 daemon 看门狗线程,每 60s 检查 scheduler/dispatcher/poller 线程 is_alive(),崩溃自动 check_and_restart()。/api/status 改用 thread.is_alive() 替代 _running 标志,防止线程假死误报
## 时区修复收尾 + 队列表 Schema 迁移 (2026-08-20 18:50, commit 9aba71c/1b82db1)
**时区修复的残留漏洞**795e8ac 的补充):该提交只改了 `CREATE TABLE` 的 DEFAULT**DEFAULT 约束固化在已存在表的 schema 中,`IF NOT EXISTS` 不会更新旧表**——enqueue INSERT 未显式传时间戳,新行 created_at 仍走旧 DEFAULT `localtime`UTC 机器上=UTC。实证行 19 (task 42) 18:14 入队 created_at=10:14:22 (UTC),同刻 UPDATE 语句已是北京时间 18:18。
**三处修复**commit 9aba71c
1. enqueue INSERT 显式写 `created_at/updated_at = datetime('now','+8 hours')`,不再依赖 schema DEFAULT
2. `_init_db` 检测旧 schema 含 `localtime` 时事务内重建表迁移(新表+复制数据+drop+rename幂等可重复执行
3. 存量数据修正:行 16/19 created_at +8h
**验证**:本地迁移单测 3 例通过(迁移+数据保留/INSERT 北京时间/幂等);线上部署后行 20 (task 44) created_at=18:43:35 为北京时间UTC 机器当时 10:43task 44 全链路 SUCCESSevent_id=25帧时间戳 22:31:15→22:56:15 与视频起点一致MariaDB monitor_events/event_details 时间戳全链路北京时间正确。
**附带修复**commit 1b82db1NAS 压缩命令补 `-f mp4` 显式指定 muxer.tmp 扩展名导致 ffmpeg41 无法推断输出格式task 43 首轮压缩失败回退 360MB 原始上传)。实测 task 43 压缩 360MB→18.9MB (19.1x) 534s 后原子 rename 成功,直传入队。
**运维教训**Edge 服务重启必须 `--bind 0.0.0.0:5000`(误用 127.0.0.1 导致 NAS 无法访问Poller 持续 RemoteDisconnected本地 curl 掩盖问题。task 43 因暂时性网络抖动三连超时标记 FAILED已重置 PENDING 待压缩缓存复用重试。
## 压缩缓存原子性修复 (2026-08-20 18:25, commit 1994de2)
**根因**task 300 损坏视频之谜的答案):服务被 kill 时 dispatcher 死亡但其 ffmpeg 子进程成为**孤儿**(父进程转 init继续写压缩产物。旧代码直接写最终路径半成品文件 `size>0` 且 mtime 较新 → 缓存复用判断误判为有效 → 重试任务上传损坏视频。多次重启叠加多个孤儿 ffmpeg 争抢 NAS ARM CPU本轮实测 3 个并行)。
**修复**:压缩输出写 `{out}.{pid}.tmp`(带 PID 防多进程冲突),成功后 `os.replace` 原子 rename——缓存目录只可能出现完整产物`_cleanup_compress_cache` 顺带清理超 1h 的 .tmp 残留。
**运维**:已杀孤儿 ffmpeg×2task 43/44并清理缓存目录、重置任务状态每次重启 NAS 服务后应检查 `ps aux | grep ffmpeg41` 是否有孤儿残留。
## 视频时间语义修正 (2026-08-20 18:05, commit 80b2fcc)
时区统一的延伸排查发现三处**时间语义** bug时区对了但语义错
| # | 位置 | 问题 | 修复 |
|---|------|------|------|
| 1 | NVIDIA 集锦偏移 | frame_timestamps 是绝对时间(`21:31:14`→当日 77474s被直接当视频内偏移seek 超出 30min 视频 → 0 帧 → 集锦恒空 → 视频模式永远降级逐帧 | `analyze_video` 增加 `event_start_time` 参数,偏移 = 帧时间 视频开始时间(跨午夜 +86400orchestrator 透传ffprobe 时长过滤超界片段fps=15 统一 CFR |
| 2 | NAS dispatcher | `event_start_time` 用文件 mtime=录制**结束**时刻),事件整体后移一个视频周期(~30min | 新增 `_probe_duration`ffmpeg 解析 DurationNAS 无独立 ffprobestart = mtime duration |
| 3 | 直传超时 | 压缩后 ~19MB 低于 20MB 阈值走直传,`timeout(10,120)` 在 1Mbps 下传 19MB 需 ~152s 必超时task 42 三连败) | 加大到 `(10, 300)` |
**两轮测试数据陷阱**:集锦 0KB/9.47s 异常均为测试时间戳超出视频时长1801s > 1800s所致单输入与多输入 concat 行为本身正常——生产帧时间戳必在视频内,已加防护过滤。
**数据治理**:历史 FAILED 全量重置后 307 PENDING其中 36 个源文件已被 NAS 清理 → 标记 FAILED'源文件已清理,跳过处理'failure_stage 枚举无 'dispatch' 用 'callback'271 个有效任务排队消化(~54h
## 时区统一北京时区 (2026-08-20 17:45, commit 795e8ac)
**背景**: Oracle Edge 机器实际为 UTC 时区(非凤凰城 MSTNAS 与视频均为北京时间,网页要求统一北京时区。
**排查结论**: NAS 端dispatcher 的 `fromtimestamp(mtime)`、MariaDB NOW()、db_layer `_dt.now()`)与 UI直读 MariaDB 时间全部正确——正常链路数据无误event 15/16/18/19 验证)。问题集中在 Edge 端UTC 机器)三处:
| # | 位置 | 问题 | 修复 |
|---|------|------|------|
| 1 | queue_manager7 处) | `datetime('now','localtime')` 在 UTC 机器=UTC队列时间戳慢 8h | 改 `datetime('now','+8 hours')`SQLite 原生时区修饰符) |
| 2 | compute_timestamps fallback | event_start_time 缺失时用 Edge 本地时间(UTC) | 改 `datetime.now(timezone(+8h))`ISO 带时区输入统一转北京 |
| 3 | nvidia_adapter._ts_to_seconds | 不支持生产格式 `YYYY-MM-DD HH:MM:SS`,解析全失败→集锦永远为空→视频模式永远降级逐帧(上一轮引入的 bugHH:MM:SS 测试格式未暴露) | partition 取时间部分drawtext 标签/提示词同步完整时间戳 |
**数据修正**: event 17task 298 手动测试缺 event_start_time 导致 start=end=落库时刻、frame 全 UTC按视频文件名 04:34:10 重算Edge 队列存量时间戳 +8h。
**验证**: 单测三例通过;部署后 task 41 队列时间正确写入北京时间 17:36:52。注意 SQLite `+8 hours` 修饰符写入的是 wall-clock 北京时间,与 MariaDB/SYSTEM 时区无关。
## NVIDIA 原生视频输入切换 (2026-08-20 17:25, commit 55633d3)
**调研结论**(免费托管 API `integrate.api.nvidia.com`40 RPM 无限次调用):
| 模型 | 状态 | 说明 |
|------|------|------|
| nvidia/nemotron-3-nano-omni-30b-a3b-reasoning | ✅ 采用 | Omni 模型,原生支持 video_url 输入MP4base64 data URI |
| meta/video-llama3-8b-instruct | ❌ 404 已下线 | 曾是 NVIDIA 官方视频理解模型 |
| qwen/qwen2.5-vl-72b-instruct | ❌ 404 | 该 ID 不存在 |
**实现**NVIDIA 从"逐帧单图调用"切换为**原生视频输入**
1. `analyze_video`:按关键帧时间点截取 ±1.5s 片段drawtext 叠加时间戳,用连字符 `00-05-00` 避免 ffmpeg 冒号转义拼成集锦视频640 宽 CRF28~35KB/片段base64 后 `video_url` 单次调用
2. 输出全 schema JSONframe_details + global_summary + entities_json归一化对齐时间戳
3. `analyze_frames` 保留为降级路径orchestrator 传 `video_path`,优先视频模式失败自动降级逐帧
4. timeout 20→120schat max_tokens 512→2048reasoning 模型 token 消耗大)
**实测**360MB 测试视频3 关键帧):集锦 107KB全程 37s动态动作识别准确走动→坐沙发→坐餐桌跨片段综合摘要正常——**显著优于旧逐帧静态识别**。
**注意**nemotron-omni 为 reasoning 模型10s 视频 ~20s 返回Gemini 仍是首选(多图 34sNVIDIA 视频模式作为同级降级链路。
## Gemini 视觉解析排查 + 业务全流程梳理 (2026-08-20 16:30, commit f4e7424)
**结论: Gemini 可以解析视频(多帧视觉分析),且有 40s 完成整段分析的实测记录,但当前被三重问题压制**
### 排查过程
1. 日志证据: 07:43:05 Gemini 返回了完整 JSON耗时 40s6 帧多图分析),但被本地 `parse_vlm_json` 拒收——"缺失字段: compute_provider"
2. 根因: `validate_schema``compute_provider` 列为必填,但提示词从不要求模型输出它(该字段是 orchestrator 解析成功后自行填充的记账字段)→ **Gemini 的有效响应被 100% 误杀**NVIDIA 走单帧轻量解析绕过校验故不受影响
3. 修复 (commit f4e7424): `compute_provider` 移出必填清单,归一为空数组由 `format_cloud_result` 覆盖填充,已部署 Edge
### 当前外部服务状态(与代码无关的临时故障)
| 模型 | 状态 | 表现 |
|------|------|------|
| Gemini | ❌ 503 过载 | "high demand, temporary",早间多次 503 |
| NVIDIA NIM | ❌ 500 推理连接错误 | llama-3.2-11b-vision 端点持续 Internal Server Error |
| Ollama qwen2.5:7b | ✅ 但纯文本 | 无法做视觉分析,故双云故障期间任务全失败 |
task 298/300 失败均因双模型同时故障NAS 端 FAILED 已重置 PENDING22 个积压),模型恢复后自动重跑。
### 强杀服务引发的边缘情况(已自愈,无需修码)
- 强杀 processing 中的 worker 后,视频文件消失但队列行卡 PROCESSING → 新 run 的 60 次 seek 全部 0.07s 快速失败,但复用上一 run 残留的帧目录完成分析
- 已通过手动 reset PROCESSING→PENDING 恢复;正常流程下不建议 kill -9 处理中的任务
### 业务全流程(当前架构)
```
[摄像头 SurveillanceStation] → 30min/段 落盘 NAS /volume1/surveillance (360MB 1080p)
↓ Scheduler (60s 扫描, 文件稳定 60s 判定)
[MariaDB process_tasks: PENDING] ← 每段视频一个任务
↓ Dispatcher (30s 轮询, 串行 limit=1)
① FFmpeg 预压缩: 480p/CRF28/veryfast, 360MB→22MB (16x), ~560s, 缓存24h
② 5MB 分块上传 Edge: 5 块, ~60-100s, 断点续传+3次块级重试
[Edge SQLite task_queue: PENDING] (UNIQUE nas_task_id, 失败重派发自动重置)
↓ Consumer 线程 (10s 轮询, TokenBucket 限速: Gemini 1000RPM/NVIDIA 40RPM)
③ 抽帧: 快速 seek 粗抽 60 候选 → MSE 帧差筛 6-12 关键帧 → 压缩 JPEG
④ 视觉分析: Gemini 多图(首选,~40s) → NVIDIA 单帧(降级,~30s) → 全失败则 FAILED
⑤ 融合: frame_details + global_summary + entities_json (compute_provider 记账)
↓ Poller (30s 拉取 /api/edge/results, SUCCESS+FAILED 均回传)
[MariaDB monitor_events + event_details 落库, event_id 递增]
↓ FAM-UI (Streamlit :8501) / Chat (Gemini→NVIDIA→Ollama 降级)
[用户查看事件时间线 / 命名成员 / 问答]
```
**单任务周期 ~12 分钟**(压缩 9.3min + 上传 1min + 分析 1min瓶颈在 NAS ARM CPU 压缩速度。
## 视频上传问题根因排查与修复 (2026-08-20 15:45, commit 8cb5553)
**问题定性**: 网络问题 + 逻辑问题双重叠加
**网络层(已解决)**: NAS→Oracle 跨境上行带宽仅 ~1Mbps360MB 原始视频上传需 10-20 分钟且频繁超时。
方案: NAS 端 FFmpeg 预压缩 (480p/CRF28/veryfast, 36 倍压缩比 360MB→22MB) + 5MB 分块断点续传。
**逻辑层(本次修复)**: 压缩方案生效后上传成功,但 Edge 端分析全部失败 "All models failed in visual analysis"
| # | Bug | 根因 | 修复 |
|---|-----|------|------|
| 1 | 全部视觉模型调用失败 | Edge 服务手动重启时未 source .envGEMINI/NVIDIA API key 丢失 | config_loader 启动时自动加载 .envexport KEY=VALUE 格式,不覆盖已有环境变量) |
| 2 | 失败结果黑洞 | get_undelivered_results 只查 SUCCESSFAILED 永不回传 NAS | 包含 FAILED 状态,/results 构造 failed 负载NAS Poller 标记任务 FAILED |
| 3 | 重派发死锁 | INSERT OR IGNORE + UNIQUE(nas_task_id),失败后重传被静默忽略 | enqueue 检测 FAILED/已交付 SUCCESS 行时重置为 PENDING 重跑 |
| 4 | Poller 落库崩溃 | db_layer valid_stages 含 'process' 但 MariaDB ENUM 不含 → pymysql DataError 1265 | ALTER TABLE 扩展 ENUM + ddl.sql 同步 |
**验证结果**:
- Task 293 (压缩后 22.2MB): NVIDIA 降级路径分析成功event_id=13 落库Gemini 临时 503 自动降级)
- Task 295: 全链路成功 event_id=14压缩 9.3min → 上传 1min → 分析 55s → Poller 落库)
- 流水线单任务周期 ~12 分钟,积压 ~20 个任务自动消化中
**运维要点**: Edge 重启务必 `source /opt/fam-edge/.env` 或使用 start_gunicorn.sh代码已兜底自动加载 .env双保险
## 真实视频性能基准测试 (2026-08-20)
测试视频: `Generic_ONVIF-001-20260819-210403` (30分钟, 360MB, 1080p H.264)
| 步骤 | 耗时 | 详情 |
|------|------|------|
| 1. 视频传输 (NAS→Oracle) | 70s | 360MB, 5.1 MB/s (Tailscale) |
| 2a. FFmpeg 快速 seek 抽帧 | 33s | 60 张候选帧 (每30s一张) |
| 2b. OpenCV 关键帧筛选 | 5s | MSE 帧差 → 12 张关键帧 |
| 2c. 压缩 | 0.6s | 12 帧, 总 1.9MB |
| 3. Ollama llava-phi3 分析 | 820s | 12 帧, avg 68s/帧, 0.9 tokens/s |
| **总计** | **929s (15.5min)** | 30 分钟视频处理 |
AI 分析瓶颈: 帧5耗时 229s (疑似 ARM CPU 热降频), 其余帧 50-65s
## FAM-Core API 测试结果 (2026-08-20)
| 端点 | 方法 | 测试数据 | 结果 |
|------|------|---------|------|
| /health | GET | - | ✅ `{"service":"fam-core","status":"ok"}` |
| /api/status | GET | - | ✅ `{"dispatcher_running":true,"scheduler_running":true}` |
| /api/member/list | GET | - | ✅ 返回 2 名成员 (人物A未命名, 人物B=汤圆) |
| /api/member/unnamed | GET | - | ✅ 返回 1 名未命名成员 (人物A) |
| /api/member/name | POST | 人物A→张三 | ✅ 成功命名, 0 条事件记录回溯更新 |
| /api/chat/ask | POST | "今天有什么事件?" | ✅ 端到端成功, 回答"今天没有观察到张三" |
## 端到端聊天链路验证 (2026-08-20新架构)
```
用户 → FAM-Core (NAS:8000) → FAM-Edge (Oracle:5000) → Gemini / NVIDIA NIM / 本地 Ollama (兜底)
/api/edge/chat/ask run_qa 三模型降级编排
```
- FAM-Core Chat-Handler 调用 `qa_url` 配置的 `http://129.146.26.249:5000/api/edge/chat/ask`(不再直连 Ollama
- FAM-Edge `run_qa` 按 Gemini → NVIDIA → 本地 Ollama 顺序调用 `chat()`,首个成功即返回 `{answer, provider}`
- **验证结果**
- Edge 单测:`/api/edge/chat/ask` → 200`provider=nvidia`Gemini 30s 超时后 NVIDIA 兜底成功,耗时 51s
- NAS E2E插入临时事件上下文后 `/api/chat/ask` → 43s 返回模型回答,`context_summary` 命中 1 条事件chat_id 落库;测试数据已清理
- 无上下文时走短路分支(直接返回提示,不调 Edge响应 <1s属正常设计
## UI 时间轴重构 + 关键帧全链路持久化 (2026-08-20 20:45, commit 0d5173f/c5c8496)
**UI 重构fam-ui/src/app.py 全量重写)**
- 深色监控面板主题CSS 变量 + 卡片化设计)
- 事件时间轴页:左侧事件列表(时间/摄像头/帧数/摘要),右侧竖向时间轴(时间点 + 关键帧缩略图 224px + 人物/动作/衣着摘要 + 模型徽章),需关注事件红色高亮
- 其余页面AI 对话、对话历史、成员命名、统计图表同步适配新主题
- 侧边栏任务队列状态(待处理/处理中/失败计数)
**关键帧持久化链路Edge → NAS → UI**
- Edge `orchestrator._attach_frame_images`:处理结果注入关键帧 base64按视觉分析输入帧位置对齐
- NAS `event_receiver._save_frame_images`base64 解码落盘到 `fam-ui/static/frames/event_{id}/frame_{idx}.jpg`(先落盘 pop 掉 base64 再入库,避免大字段进 MariaDB
- UI `load_frame_b64`:读取落盘帧图内联 data URI 渲染;旧事件无帧图显示占位符
- 配置:`fam-core/config.yaml` + `fam-ui/config.yaml` 新增 `storage.frame_image_dir`
**multipart 上传超时修复commit c5c8496**
- 现象task 50/51 直接上传 17.1MB 100% 失败 `('Connection aborted.', TimeoutError)`,恰好 10s 超时
- 根因requests/urllib3 发送 multipart body 期间 socket timeout 取的是 **connect timeout** 参数而非 read timeout`(10, 300)` 下 17MB 跨境 1.4MB/s 需 ~117s 必然超时
- 修复:`_dispatch_direct` timeout 改 `(120, 300)`
**端到端验证2026-08-20 20:24-20:41**
- task 53压缩 20.6MB → 分块上传 5/536s→ Edge 分析 → Poller 落库 event_296 帧落盘)
- task 50直接上传成功117s超时修复生效→ event_306 帧)
- task 51直接上传成功 → event_316 帧task 52重压缩 21.9MB → event_327 帧)
- UI 浏览器实测:点击事件 #29 → 时间轴 6 个时间点、6 张关键帧全部加载(夜视画面可见人物)、每帧下方人物/动作摘要正常渲染
- 重启残留处理task 52 重启时被中断的孤儿 ffmpeg 已清理(`{out}.{pid}.tmp` 原子改名机制保证无损坏文件),重置 PENDING 后自动重跑成功
**经验**
- Streamlit 1.61 窄视口(<960px下 st.columns 自动垂直堆叠,详情区被挤到事件列表下方;浏览器自动化验证需注意 window 尺寸,用户桌面浏览器不受影响
- 旧事件(新代码部署前)无落盘帧图属预期,占位符正常显示
## 历史事件关键帧补抽 (2026-08-20 22:05, commit 8a333d7)
**现象**:用户打开 UI "全是暂无帧图"。落盘链路本身正常event 29-39 持续产出),根因是**事件列表按 event_start_time 倒序,第一屏前 11 条全是帧图注入上线前的旧事件2026-08-20 的 event 8-17有帧图的新事件都是 2026-08-15 录像、排在列表深处**。
**修复**`fam-core/tools/backfill_frames.py` 从原始视频补抽关键帧
- 偏移计算offset = frame_timestamp - event_start_time5 分钟间隔帧)
- NAS ffmpeg41 没有 image2 muxer`-f image2` 报 not suitable也没有 ffprobe → 用 `-f singlejpeg` 输出 jpg用 ffmpeg header 的 `Duration:` 行解析时长
- 抽帧参数与预压缩一致480p 等比缩放 + `-q:v 5`~53KB/张)
- 幂等:单帧文件存在跳过,帧数齐全的事件整条跳过
- 实跑11 事件 65 帧全部成功8.1MB2026-08-14 的 10 个事件event 18-27视频已被 NAS 清理无法补UI 占位符兜底
- 浏览器验证:默认打开的最新事件 #11 帧图 6/6 全部加载,无占位符
## 历史事件帧图 100% 补齐 (2026-08-20 22:40, commit 80cc324)
**问题**:用户翻到 2026-08-14 21:31 事件event 27仍是"暂无帧图"——第一轮补帧时原始视频已被 Surveillance Station 保留策略删除event 18-27 共 10 个事件)。
**副本排查**(按优先级):
1. NAS 原始目录:`/volume1/surveillance/.../20260814PM/` 整目录已删,无 #recycle
2. Synology 元数据 `@SSRECMETA`Thumbnail缩略图+ Preview每 20s 一张 8KB jpg都在但保留约 8 天20260814PM 目录最早条目只到 08-15 00:31北京21:31 时段已被清掉
3. **dispatcher 压缩缓存 `/tmp/fam_compressed/task_36~45/`480p 压缩副本全部健在**(每事件 11-33MB
**修复**`backfill_frames.py` 增加压缩缓存回退——原始视频缺失时改用 `/tmp/fam_compressed/task_{task_id}/{basename}` 抽帧。实跑 event 18-27 补 78 帧 0 失败task 41 一份视频覆盖 event 21/23 两个事件。event 27 六帧校验 JPEG 魔数全部有效。**至此数据库 36 个有明细的事件帧图 100% 齐全UI 不再有占位符**(共 16MB
**注意**:压缩缓存是 /tmp重启即失 + 24h 清理策略),本次是赶在副本消失前抢救出来的;新管道从 event 29 起处理时即落盘帧图,不再依赖事后补抽。
## 事件列表带年月日 (2026-08-20 23:05, commit f50f98e)
- 跨多天的事件列表仅显示 `HH:MM` 无法区分日期,用户反馈要带上年月日
- 按钮:`05:34 · 客厅 · 6帧``08-20 05:34 · 客厅 · 6帧`MM-DD HH:MM
- 摘要行:前缀浅蓝完整日期 `2026-08-20 · 摘要…`(年月日齐全,#7dd3fc
- 摘要截断 26→30 字适配前缀长度,单行不溢出
- 浏览器验证 15 个按钮均正确显示、无排版错乱
## Gemini 全量降级 NVIDIA 排查修复 (2026-08-21 00:10, commit eaf4ef3)
**现象**UI 事件全是"模型: nvidia"Gemini 不出结果。
**排查发现的三个链路问题**
1. **Gemini 每日配额耗尽(根因)**`GenerateRequestsPerDayPerProjectPerModel-FreeTier` 每天每模型仅 **20 个请求**(实测 429 响应体 quotaValue=20模型 gemini-3.7-flash。日均 30+ 任务,配额烧光后持续 429 → 全部降级 NVIDIA最近 40 任务35 nvidia / 4 gemini。且 429/503 无重试无换模型,一次失败即放弃。
2. **Edge 日志黑洞**gunicorn 手动 nohup 启动stdout/stderr 指向 /dev/null**所有 Edge 日志全部丢失**,线上问题无法排查。
3. **配置漂移**commit 55633d3 当时 nemotron 模型名只改了 Oracle 线上 config 未回传 git 仓库;本次 scp 部署 config 时把线上 nvidia 覆盖回 llama-3.2(丢失原生视频输入能力)。
**修复**
- `gemini_adapter``_generate()` 统一模型链调用:`gemini-flash-latest → gemini-flash-lite-latest`(各自独立 20/天配额合计翻倍429 立即切换下一模型当日配额不会恢复不重试503 退避 3s 同模型重试一次再切换;视觉分析与问答统一走链
- configgemini 增 `fallback_models`nvidia 恢复 `nemotron-3-nano-omni-30b-a3b-reasoning` + timeout 120nemotron 实测仍在线 200
- **systemd 接管 Edge 服务**`fam-edge.service`Restart=always + 日志 append 到 `/opt/fam-edge/logs/fam-edge.log`),取代 nohup 黑洞
- 健康检查校验整条模型链
**端到端验证**task 7330 分钟视频 12 关键帧)—— flash 429 → lite 1s 接管 → 12 帧直出 JSON → event_52 落库 `provider=["gemini"]`。链路恢复Gemini flash(20/天) → Gemini lite(20/天) → NVIDIA nemotron 兜底。
**经验**
- Gemini 免费层配额按模型独立计算,多模型 fallback 链是免费层扩容的唯一手段429 响应体的 `quotaValue` 字段会写明每日上限
- 线上手动改过的配置必须回传 git 仓库,否则下次部署必然回退(本次真实踩坑)
## 出现人物统计去重 + 导航移页面顶部 (2026-08-21 00:40, commit 3fb86ee)
**问题 1出现人物 20 是错的**`person` 是自由文本,一帧可能是多人组合且带括号特征描述(如 `张三 (红色T恤, 白色裤子), 汤圆 (蓝色上衣), 人物3``COUNT(DISTINCT person)` 把每种组合字符串当作一个"人物"。全局 20 个 distinct 字符串实际只有 6 个真实人物。
- 修复:新增 `resolve_persons()` —— 正则去括号描述 → 按中英逗号/顿号/斜杠拆分 → `family_members` 抽象标签映射真名人物A→张三、人物X→爸→ 集合去重。统计显示 20 → 6。
**问题 2成员命名/问答界面"不见了"**。两页面实际正常(浏览器实测渲染无误),根因是导航放侧边栏 radio窄屏手机下 Streamlit 自动收起侧边栏,页面入口藏进汉堡菜单。
- 修复:导航改为页面顶部 `st.segmented_control` 横向分段控制器,任何屏宽直接可见;侧边栏保留品牌标题和任务队列状态。
浏览器验证:顶部分段导航 5 页可达、成员命名页显示 张三/汤圆/爸、AI 对话页表单正常、无报错。
## 摄像头对外入口收口为单一域名 smart-camera.zichuan.xyz (2026-09-01)
**决策**摄像头系统fam-ui 前端 + NAS fam-core :8000 后端)对外只暴露一个域名 `smart-camera.zichuan.xyz`(前端 `/` + 后端 `/api`),下线独立的 `api.zichuan.xyz`,并从 `oracle.zichuan.xyz` 移除摄像头相关路由(其 `/ai``/fam``/wordpress` 仍保留给其它服务)。
**改动(基础设施,不在本仓库)**
- Oracle Caddy`/etc/caddy/Caddyfile`):新增 `smart-camera.zichuan.xyz` 块(`/` 静态 fam-ui + `/api``/login` 反代 `127.0.0.1:8000`,经 frp 隧道回源 NAS删除 `api.zichuan.xyz` 块;`oracle.zichuan.xyz``:80` 兜底块移除摄像头 root + `/api` 路由(改 404
- DNS新增 `smart-camera.zichuan.xyz → 129.146.26.249`RecordId 2387553886
- NAS `/volume1/web/sentinel-home-ai/.env``AUTH_HUB_ISSUER` 改为 `https://auth.zichuan.xyz``AUTH_HUB_REDIRECT_URI` 改为 `https://smart-camera.zichuan.xyz/api/auth/callback`,且 `AUTH_HUB_*` 四行补全 `export` 前缀(见下)。
- auth-hub FAM-Core 客户端 `a2PJTZuKQYxt1oGh` 回调改为 `https://smart-camera.zichuan.xyz/api/auth/callback`(保留旧 IP 作兜底)。
**顺带修复两个生产 Bug**(见 `scripts/start_core.sh`
1. 脚本先 `cd "$APP_DIR"` 再算相对 `SCRIPT_DIR`,导致 `source .env` 路径错位、`set -e` 下直接退出gunicorn 起不来。
2. `.env``AUTH_HUB_*` 是裸赋值(无 `export``source` 后不进环境gunicorn 子进程读不到 → 登录一直 503 fail-closed与之前“生产未部署”一致。脚本加 `set -a` 包裹 source 修复。
**验证**`smart-camera.zichuan.xyz/` 200、`/login` 302→auth.zichuan.xyzredirect_uri=smart-camera`/api/*` 401`oracle.zichuan.xyz/` 404、`/fam` 200`api.zichuan.xyz` 已不通。fam-core 经修复脚本重启后 health ok。
## SPA 登录打通:未登录自动跳转 OIDC2026-09-01
**问题**:域名收口后访问 `https://smart-camera.zichuan.xyz/timeline` 显示「⚠ 未登录 / 登陆不了啊」。后端 `/login`302→auth-hub与 Caddy `/login` 反代此前已验证可用,根因在**前端零登录逻辑**`fam-ui``/api/*` 拿到 401 后只在页面上渲染字面错误 `data.error`"未登录"),从不发起 OIDC 跳转,也没有任何登录入口。
**改动(本仓库 `fam-ui/`**
- `src/api.js` `request()`:捕获 401 时 `window.location.href = '/login'` 触发统一登录(后端经 auth-hub 走 Authorization Code + PKCE用模块级 `_redirectingToLogin` 开关保证单次会话只跳一次,并 `return new Promise(()=>{})` 阻止调用方继续渲染错误态;`/api/auth/check` 等白名单接口不会 401不受影响。
- `src/api.js` `api` 对象新增 `authCheck: () => request('/api/auth/check')`
- `src/App.vue``onMounted``api.authCheck()` 维护 `authed` 状态;左侧栏 + 移动端顶栏新增「🔑 登录」入口(`<a href="/login">`)与「👋 退出登录」按钮(`POST /api/logout` 后回 `/` 由后端 401 自动跳登录)。
**部署**`npm run build``dist/``tar | ssh ubuntu@129.146.26.249` 覆盖 `/var/www/fam-ui`macOS `._*` 元数据已清)。
**端到端验证**
- `smart-camera.zichuan.xyz/timeline` → 200SPA
- `/login` → 302 → `auth.zichuan.xyz/authorize?...&redirect_uri=https://smart-camera.zichuan.xyz/api/auth/callback`
- `/api/auth/check`(无 cookie`{"authed":false}`(白名单,不 401
- `/api/ui/videos`(无 cookie→ 401 → 前端据此自动跳 `/login`
登录链路已通:未登录访问任意页面 → 首个 401 → 自动跳 auth-hub 登录 → 回调种 `fam_session` cookie → 回 `/timeline` 正常加载。
## 统一登录从 NAS 迁到甲骨文 fam-edge2026-09-12
**触发**`smart-camera.zichuan.xyz/login` 报 502。排查链路静态页 `/``/timeline` 200Caddy 读本机磁盘),
只有 `/login``/api/*` 502甲骨文本机 `curl 127.0.0.1:8000/health` 0.6 秒空响应curl exit 52
Caddy 日志 `msg:"EOF"`frps 正常、同隧道的 gitea 也正常 → NAS 上 fam-core 进程没了。
局域网直扫 NAS22/2222/3000/5000/5001/3306 全 OPEN**只有 8000 closed**NAS 本身没事。
根因是登录入口挂在 NAS 上,而 NAS 上的 fam-core 没有任何守护DSM 无 systemd挂了不会自启。
**决策**:登录是入口,不该依赖家里的机器。整体迁到甲骨文的 fam-edge前端静态文件、auth-hub 本来就在这台),
NAS fam-core 退化成纯数据接口。
**改动**
- 新增 `fam-edge/src/fam_edge/auth.py``/login``/api/auth/callback``/api/logout`
`/api/auth/check``/api/auth/verify`(给 Caddy forward_auth。跟旧实现三处关键差异
1. 换 token / 拉 JWKS 走 `AUTH_HUB_INTERNAL_BASE`(本机 :5300不再跨公网 TLS——旧链路上
`PyJWKClient` 用 urllib + 系统 CA群晖易 CERTIFICATE_VERIFY_FAILED、两机时钟偏差会让 `iat`
显得来自未来,这两个坑一起消失;但 `iss` 校验和浏览器跳转仍用公网 issuer
2. 会话改无状态 HS256 签名 cookie服务重启不掉线旧实现进程内 token 表)
3. **回调失败渲染错误页,不再 302 回 `/login`**——旧实现失败即跳 `/login`,而 auth-hub 只要还有
会话就立刻再签一个 code 跳回来,两边对跳成死循环,浏览器只报「重定向次数过多」,
既看不到登录页也看不到原因(这正是 9/1 那次「跳不到登录页」的成因)
- 删除 `fam-core/src/fam_core/auth.py` + `tests/test_auth.py``app.py` 去掉 `init_auth`
fam-core 不再有任何鉴权,改由甲骨文 Caddy `forward_auth` 前置拦截
- `fam-edge/tests/test_auth.py` 16 个用例(含「失败分支绝不 302」的回归测试、
「服务端走内网但 iss 按公网校验」、「cookie 无状态」fam-edge 157 / fam-core 15 全绿
- 写测试时逮到自己写的一个 bug会话 cookie 也套了 60 秒 leeway本进程自签自验根本不需要
会让每个会话白白多活 60 秒,已改成只有 id_token 用 leeway
**遗留风险(已记入 README/DEPLOY**frps 在甲骨文绑的是 `*:8000` 且 iptables 明确放行,
fam-core 去掉鉴权后,绕过 Caddy 直连 `129.146.26.249:8000` 就是无门禁的全量数据接口,
必须靠 `iptables -I INPUT 1 -p tcp --dport 8000 ! -i lo -j DROP` 兜底。
另:登录进程并到 fam-edge 后fam-edge 正在跑视频分析时登录响应可能变慢(单 worker 4 线程)。
## 全量迁云NAS 只剩推送进程2026-09-13
**触发**:前一天刚把登录迁到甲骨文,隔天 NAS 上的 fam-core 又挂了导致数据接口 502。
用户一句话点破「NAS 上只有一个无状态的通知甲骨文的服务,剩下的全部在甲骨文呀」。
盘点后确认这个判断成立——甲骨文的 SQLite 才是权威数据源videos 3113 / events 16159 /
people 60 / model_calls 9876NAS 的 MariaDB 全是它的镜像,前端读的数据本来就产自
甲骨文,绕了一圈回家又绕回来。
**改动**
- `fam-core/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 这回事了,留着名字会误导。
- 新增 `fam-core/edge_client.py`:写操作(改名/删除)、帧图头像、服务状态都打给同机
fam-edge全走 127.0.0.1,不出公网。
- 新增 `fam-notifier/`:把 `motion_notifier.py` 从 fam-core 拆出来,游标从 MariaDB
换成本地 JSON 文件。NAS 上从此没有 Flask、没有数据库、没有监听端口只有一个
单向推送进程。
- fam-core 移到甲骨文 `/opt/fam-core`systemdgunicorn -w 2**只绑 127.0.0.1:5401**——
5400 被 chat-relay 占了。Caddy 的 `/api/*` 从"frp 隧道回源 NAS"改成同机反代,
forward_auth 闸门不变。
- 前端跟着删:侧边栏同步面板、统计页同步状态、服务状态页的"NAS 同步"卡片和
"立即同步"按钮(背后的镜像层已不存在)。"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 / attention 13 /
people 59外网 `/` `/timeline` 200、`/login` 302、`/api/*` 未登录 401——**全程 NAS
上的 fam-core 是停着的**,这就是迁云的验收标准。
**待办**NAS 侧部署 fam-notifier只能用户手动密码登录chat_history 一次性迁移
`fam-core/scripts/import_chat_history.py`幂等frpc.toml 里的 8000 映射可删。
## 修复 fam-edge 多线程共用 SQLite 连接2026-09-13
**触发**:用户问「甲骨文剩余硬盘小于 10G 会删东西的逻辑怎么没了?」。查下来逻辑一直在、
也没被迁云动过(`disk_guard.py` 最后一次改动还是 8/28 新增它那次),但它 95% 的检查在空转:
```
DiskGuard「本轮清理完成」 128 次
DiskGuard「检查异常」 2837 次 ← 22 倍
```
失败原因全是 `cannot start a transaction within a transaction`。表现就是磁盘剩余空间在
2.7GB 和 16GB 之间来回荡——清理能不能成功全靠运气,赶上 rclone 集中下载
(实测 5 分钟写入 12.6GB)就掉进危险区。
**根因**`OracleDB.__init__` 建一条 `sqlite3.connect(check_same_thread=False)` 的连接
给全进程共用,而 VideoQueue / PersonService / DiskGuard 三个后台线程 + gunicorn 的 4 个
请求线程都在并发读写它。sqlite3 的连接对象本来就不是可并发共享的,事务状态互相踩踏。
同一个根因在线上刷出三类错误,累计:`database is locked` 71604 次、
`cannot start a transaction within a transaction` 378 次、`no more rows available` 92 次
(后者堆栈落在 `self._conn.commit()`,是游标被别的线程重置的典型症状)。
运动事件推送被 500 打回也是它——NAS 侧失败批次不推进游标,下一轮补推,所以没丢事件。
**修复**`_conn` 改成 `@property`,从 `threading.local()` 取当前线程的连接,没有就新建
WAL + busy_timeout=10000`close()` 相应改成收掉所有线程开过的连接。因为外部调用方
(如 `api_gateway` 的 activity 端点)也在直接用 `db._conn.execute(...)`,做成 property
可以让全部现有调用点原样工作,不用逐个改。`_write_lock` 保留,复合写语义不变。
**测试**:新增 2 个用例8 线程 × 25 轮并发读写、close 要收掉所有连接)。在旧代码上
稳定复现同族错误 `cannot commit transaction - SQL statements in progress`,修复后通过;
fam-edge 全套 159 个测试绿。
**同批修掉的第二个竞态**:生产者列目录之后、读 mtime 之前DiskGuard 可能刚好把那个
文件清掉(两个后台线程的正常竞态),`os.path.getmtime` 抛 FileNotFoundError代价是
**整轮扫描中断**——排在后面的新素材本轮全都登记不上。改成捕获 OSError 跳过该文件,
新增 `fam-edge/tests/test_video_queue.py` 两个用例覆盖(被删的跳过 / 仍在写入的照样跳过)。