Commit Graph

174 Commits

Author SHA1 Message Date
ericwyuan
a494d60361 docs: 补入 2026-09-03 工作日志(同步 1062 复发 + 甲骨文过载)
这份日志一直没入库,同目录 8/20 到 8/31 的都在。内容是 9/3 那次排查记录:
Oracle 侧 SQLite id 整体重排导致 sync_videos/sync_people 二次 1062、游标卡死
(修复即 0238030),以及同一天甲骨文整机过载挂死、SSH 握手被拒的环境问题。

后半段那个「Oracle 盒子过载」当时记为未解决,现在有了后续:运动事件推送从那天
起断流十天,直到 2026-09-13 全量迁云时才发现并恢复(见 PROGRESS 同日条目)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 08:55:08 +08:00
ericwyuan
da60365214 feat(fam-notifier): 补开机自启脚本,NAS 重启后推送不再静默断流
NAS 上没有 systemd,rc.d 是唯一的守护手段。旁边 frpc / nas-monitor / comic-api
都有 S99 脚本,唯独这个推送进程没有——它一旦随重启消失,运动事件就悄无声息地
断流,而且不打开网站根本发现不了。2026-09-04 到 09-12 断档整整十天就是这个剧本
(当时它还在 fam-core 里,fam-core 挂了没人重启)。

- 照抄 S99nas-monitor.sh 的写法:等 volume1 挂载就绪(最多 90s)、pidfile 判重
- 以 ericwyuan 而非 root 运行,保持 logs/ 与 data/cursor.json 属主跟手动启动一致
- DSM 上没有 pgrep,用 grep 字符类找 pid
- su 自身的 stdio 也断开:否则通过 SSH 执行时命令跑完了会话却不退出(实测踩到)

已装到 NAS 的 /usr/local/etc/rc.d/ 并验证 start/stop/restart/status;
restart 后游标从本地文件正确读回(33081),没有被重置成 SS 当前最大 id。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 08:35:36 +08:00
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
ericwyuan
dc284f4bf5 feat(fam-edge): 统一登录从 NAS fam-core 迁入,回调失败不再对跳死循环
登录入口原先跑在 NAS 的 fam-core 里,NAS 或 frp 隧道一挂,
smart-camera.zichuan.xyz/login 直接 502——连登录页都打不开。登录是入口,
不该依赖家里那台机器,整体搬到甲骨文(前端静态文件和 auth-hub 本来就在这台)。

跟旧实现的三处关键差异:
- 换 token / 拉 JWKS 走 AUTH_HUB_INTERNAL_BASE(本机 :5300),不再跨公网 TLS。
  旧链路上 PyJWKClient 用 urllib + 系统 CA(群晖易 CERTIFICATE_VERIFY_FAILED)、
  两机时钟偏差会让 iat 显得来自未来,这两个坑一起消失;iss 校验和浏览器跳转
  仍用公网 issuer
- 会话改无状态 HS256 签名 cookie,服务重启不掉线(旧实现是进程内 token 表)
- 回调失败渲染错误页,不再 302 回 /login。旧实现失败即跳 /login,而 auth-hub
  只要还有会话就立刻再签一个 code 跳回来,两边对跳成死循环,浏览器只报
  「重定向次数过多」,既看不到登录页也看不到原因

fam-core 侧删除 auth.py 与 test_auth.py,鉴权改由甲骨文 Caddy 的 forward_auth
前置拦截(打 /api/auth/verify)。

测试:fam-edge 新增 16 个用例,含「失败分支绝不 302」回归测试、
「服务端走内网但 iss 按公网校验」、「cookie 无状态」。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 08:02:59 +08:00
ericwyuan
0238030128 fix(sync): 修复 Oracle id 重排导致的 sync_videos/sync_people 1062
Oracle 库重建/重排后 videos/people 的 id 跳变(2303 段→3408/4600+ 段),
旧 upsert 按 Oracle id 作主键插入时与已存在同 filename/label 行在 UNIQUE 键
上二次冲突报 1062,整批中止、游标不推进。

- upsert_sync_videos: 改为以 filename 业务键去重,命中就地 UPDATE 保留 NAS 原
  id(防 sync_events/model_calls/identity_map 的 video_id 外键失效),Oracle id
  落 oracle_id 列溯源;未命中插入优先用 Oracle id 对齐子表引用,主键冲突回退自增
- upsert_sync_people: 同模式,以 label 业务键去重(people.id 无外键引用)
- ddl.sql: sync_videos/sync_people 的 id 改为 NAS 本地自增主键 + 新增 oracle_id 列
- NAS 现网已迁移(加列/回填/改自增)并重启验证:单批补拉 videos+348 events+1017
  people+9 model_calls+500 identity_map+238,游标 09-02 13:55:21 → 09-03 08:51:11
2026-09-03 09:00:50 +08:00
ericwyuan
2ad0472578 fix(fam-ui): 未登录自动跳转 OIDC + 登录/退出入口,打通 smart-camera 登录 2026-09-01 12:23:48 +08:00
ericwyuan
db4f46140b fix(start_core): export AUTH_HUB_* env + fix .env path; docs: cutover to smart-camera.zichuan.xyz 2026-09-01 12:14:51 +08:00
ericwyuan
9c4db682d5 docs: 记录 8/29 同步 1062 修复与 8/31 auth-hub SSO 接入/清理部署;gitignore 排除独立项目 ai-gateway 2026-08-31 22:38:29 +08:00
ericwyuan
feb096ff3d docs: 订正 FAM-UI 部署位置描述(NAS -> 云服务器 Caddy)
多处历史遗留描述仍写 FAM-UI 部署在 NAS(含不存在的 static_app.py),
实际已迁至甲骨文云服务器由 Caddy 静态托管,逐处订正。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 21:50:42 +08:00
ericwyuan
5e1230d9b0 docs: 记录 auth-hub 生产 client 已注册,NAS 部署待执行
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 21:33:16 +08:00
ericwyuan
8120d2ae6a feat(fam-core): 登录改接 auth-hub 统一登录(OAuth2 Authorization Code + PKCE / OIDC)
移除本地 FAM_AUTH_USER/PASS 账号密码校验和内置登录表单,/login 改为 302
跳转 auth-hub /authorize,新增 /api/auth/callback 完成 code 换 token +
id_token 签名验证(PyJWT + JWKS),验证通过后种回原有 fam_session cookie,
is_authed()/全局登录拦截逻辑不变。接入参数走环境变量,未配置齐全 fail
closed。本地起 auth-hub 开发实例 + 真实浏览器验证过完整登录/登出闭环。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 21:32:26 +08:00
ericwyuan
988c63a8f9 fix(fam-core): 修复 sync_identity_map 增量同步 1062 主键冲突导致游标卡死
Oracle→NAS 增量同步反复报 (1062, "Duplicate entry '1073-汤圆' for key
'uq_video_raw_uid'"),整批失败、游标永不推进,同步状态页卡在"同步中"。

根因:sync_identity_map 以 Oracle person_identity_map.id(代理自增键)
作 NAS 主键,但 Oracle 库重建/恢复后这个 id 会被复用——实测 Oracle 现在
的 id=744 对应 (1073,'汤圆'),NAS 上 id=744 还是旧的 (1302,'人物E')。
upsert 先按 PK id 命中旧行,UPDATE 成 (1073,'汤圆') 后与已存在的同名行
在 uq_video_raw_uid 上二次冲突。

修复:镜像表改为 NAS 本地自增 id 主键,业务键 (video_id, raw_uid) 唯一,
Oracle 的 id 只落 oracle_id 列做溯源;upsert 的 UPDATE 子句不再改写
video_id/raw_uid 这两个键列,只更新 oracle_id/canonical_name/source 等。
ddl.sql 同步更新表定义。

已在 NAS 上完成现网迁移(ADD COLUMN oracle_id -> 回填 -> 主键改自增)并
验证:补拉同步成功恢复,游标正常推进,手动触发同步验证通过。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 23:11:10 +08:00
ericwyuan
05c7a8b70a feat(fam-edge): 闭集人物识别新增奶奶(成年女性按年龄段区分媳妇/奶奶)
家里从 4 人变成 5 人后,"媳妇=唯一成年女性"这条免费规则的前提被打破——
人物管理页面已经能看到一条 label="奶奶" 但从未稳定命中的记录(只出现过
1 次,特征都没落库),说明现在系统大概率把奶奶也误判成了媳妇。

先用免费的年龄段字段区分(老年=奶奶,其余=媳妇),不引入额外的模型调用
(区别于爷爷/爸爸那种"两个成年男性区分不开必须靠视觉比对参考图"的方案)
——如果后续观察发现年龄段判断不稳定导致误判,再考虑改成视觉比对。

新增 3 个单元测试覆盖(中年→媳妇/老年→奶奶/年龄段缺失默认媳妇),fam-edge
全量 142/142 通过。已部署 Oracle 并重启验证。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 22:42:26 +08:00
ericwyuan
c9354ed1a1 docs: 同步 8/28 迁移故障排查记录(FFmpeg 缺失 + 磁盘写满死循环 + DiskGuard)
README.md 新增 Disk-Guard 模块表条目、修正一处遗漏的机器规格描述
(2C12G -> 4C23G)、当前进度补充这次完整排查记录;PROGRESS.md 服务运行
状态表更新 + 新增详细的故障排查时间线记录。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 12:10:14 +08:00
ericwyuan
ef56ae5662 feat(fam-edge): 新增 DiskGuard 磁盘空间守护,剩余空间不足自动清理旧素材
背景:Oracle 磁盘曾经被写满(gdrive_videos 持续下载新素材、旧文件迟迟没
清理),触发 rclone 的一个安全机制——同步遇到 IO 错误就整体拒绝执行删除,
形成"越满越删不掉,越删不掉越满"的死循环,最终连新视频都下载不了。这次
排查+手动清理已经解决了当次故障,但需要一道独立于 rclone 同步之外的兜底,
防止再次悄悄写满没人发现。

- oracle_db.py 新增 get_oldest_purgeable_material():只挑最旧的、已完成
  分割阶段(status='done')的整段素材(非 motion_ 前缀),绝不碰运动片段
  (事件时间轴/人物头像依赖它)和还在处理中的素材
- disk_guard.py 新增 DiskGuard 后台线程:5 分钟检查一次,剩余空间 <10GB
  触发清理,删到 15GB 水位为止(留缓冲避免刚清完又立刻触发),复用已有的
  delete_video() 完成实际删除
- app.py 启动这个后台服务;/api/oracle/activity 新增 disk 字段(实时剩余
  空间 + 最近一次清理动作),供服务状态页展示
- 新增 9 个单元测试,全部通过(139/139)

已部署 Oracle 验证:DiskGuard 正常启动,配置生效。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 12:08:04 +08:00
ericwyuan
3ec79de911 docs: 清理已删除的 /api/ss/webhook 端点在文档中的残留描述
上次 SS Webhook 重构(cf3d145)已经彻底移除了 /api/ss/webhook 接收端点
(motion_bp.py 不再注册该路由),但 README.md 里 5 处描述、鉴权白名单
说明都还停留在"保留为可选低延迟补充",跟实际代码不符,逐处订正。

顺带更新 vite.config.js 里一句过时注释(还写"生产环境由 fam-core 的
Flask 同源提供",实际前端已经迁到云服务器 Caddy 托管)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 09:31:41 +08:00
ericwyuan
09708bd9a4 fix(fam-core): 修复 /api/status 和 /api/ss/status 500(移除 Webhook 重构遗留死引用)
上次"移除 SS Webhook 可选路径"重构(cf3d145)删掉了 MotionNotifier 的
camera_name_to_id/_ss_name_to_id/_camera_loaded 三个属性初始化,但
status() 方法忘记同步删除对它们的引用,导致任何调用 status() 的地方
(/api/status、/api/ss/status)直接抛 AttributeError 500——这两个端点
自那次重构部署后就一直是坏的,服务状态页无法访问。

顺带修 test_auth.py 里断言 /api/ss/webhook 应该被鉴权白名单放行的过时
测试——这个端点本身已经在同一次重构里被彻底删除(motion_bp.py 不再
注册该路由),白名单移除是对的,是测试没跟上。

已在 NAS 上部署验证:登录后 /api/status 恢复正常返回。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 09:29:09 +08:00
ericwyuan
cf3d145c57 refactor(fam-core): 移除 SS Webhook 可选路径与 NAS 静态前端托管,兜底 Oracle 地址更新为 129.146.26.249 2026-08-25 23:56:02 +08:00
ericwyuan
7948f55b02 docs: 同步 8/25 迁移(前端迁云 Caddy:80、Oracle 换新机 129.146.26.249、旧 IP 清理、Drive 中转设计说明) 2026-08-25 23:55:57 +08:00
ericwyuan
3cb08c9749 docs: 同步事件时间轴删除视频功能到 README/PROGRESS
补充 §5.1/§5.2 新增的 POST /api/oracle/video/delete + DELETE /api/ui/videos/<id>
端点说明、FAM-UI 页面表格、§12 当前进度;PROGRESS.md 记录完整变更 + 端到端
验证过程。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 04:28:55 +08:00
ericwyuan
28397e9e6a feat(timeline): 事件时间轴支持删除视频会话
三端联动实现(fam-ui -> fam-core -> fam-edge),沿用项目里"NAS 转发写请求
到 Oracle"的既有模式(照抄 identity_correct 那套骨架):

- fam-edge:oracle_db.py 新增 delete_video()(删 events+videos 行 + 磁盘上
  的运动片段文件;不清理 ss_motion_events 源事件,因为素材一旦处理完就不会
  被生产者重新捡起,删片段不会触发重新分割);api_gateway.py 新增
  POST /api/oracle/video/delete
- fam-core:新增 db_layer.delete_sync_video()(第一个"NAS 直接写自己镜像表"
  的函数——增量同步只做 upsert 感知不到删除,不能像纠错那样靠 trigger_now()
  拉增量顺带清理)+ oracle_sync.push_video_delete() + ui_api.py 新增
  DELETE /api/ui/videos/<id>(先回推 Oracle,成功后才清本地镜像,避免数据
  不一致)
- fam-ui:Timeline.vue 详情卡片加删除按钮(原生 confirm() 二次确认,项目里
  之前没有确认弹窗组件先例);api.js 新增 deleteVideo()

新增 6 个 delete_video 单元测试;已在 Oracle/NAS 用自造测试数据完整跑通端
到端链路(转发成功、两端记录清理、磁盘文件删除、幂等 404),未触碰任何真
实监控数据。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 04:25:43 +08:00
ericwyuan
2e370ab668 docs: 同步问答链路抽离到 ai-gateway 后的架构文档
README.md/PROGRESS.md 里大量描述还停留在"FAM-Edge 自己维护 Gemini→NVIDIA→
Ollama 问答降级链"的旧架构,跟实际代码(FAM-Edge 已改为转发客户端,模型链
整个搬到独立的 ai-gateway 服务)不一致,逐处订正:

- 新增 §3.4 AI-Gateway 模块章节、部署拓扑图、Gitea 仓库表新增 ai-gateway 条目
- 模块表(FAM-Core/FAM-Edge)、API 文档、数据库 compute_provider 说明更新
- §6.2/6.3 本地 Ollama 与模型适配器章节:去掉已删除的 OllamaAdapter/role=text,
  问答降级链路图重画为"FAM-Edge 转发 -> AI-Gateway 内部降级"
- 修复已经损坏(合并冲突残留)且过时的 fam-edge/config.yaml 示例,新增
  ai-gateway/config.yaml 示例;如实记录两份 .env 各自独立维护的实际部署状态
- PROGRESS.md 补一条 2026-08-23 变更记录 + 服务运行状态表新增 AI-Gateway 行

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 17:40:55 +08:00
ericwyuan
5caeb299a4 refactor(fam-edge): 问答链路抽离到独立 ai-gateway 服务,fam-edge 改为转发客户端
原本嵌在 fam-edge 里的问答模型降级链(NVIDIA 文字模型 -> Gemini 非 flash 文字
模型 -> 本地 Ollama 兜底,含 key 轮换/熔断)跟视频分析业务无关,是通用能力,
抽成独立 ai-gateway 服务(OpenAI 兼容协议),除了 fam-edge 自己,别的项目也能
直接接入。

- 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 预热逻辑(现在由 ai-gateway 自己负责)
- config.yaml 移除 3 个问答专用 model 条目,新增 ai_gateway 客户端配置块

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 14:12:01 +08:00
ericwyuan
9dff19e6ac feat(fam-edge): 赤膊成年人直判爸爸(免费规则) - 用户确认家里只有爸爸会光膀子
用户原话:"赤裸的大人都是爸爸,家里没有其他人会赤裸"——加进闭集识别的免费
规则层(跟汤圆/媳妇同级),命中就不用再调用视觉大模型比对参考图,比 auto_id
更快更准。只对成年人生效(判断顺序上幼儿/儿童年龄档在前面,小孩光膀子玩很
正常,不适用这条规则)。

排查历史数据时用这条规则筛出了一个真实误判:video 1092 的一次赤膊出现被
auto_id(NVIDIA 视觉比对)错误识别成"爷爷",实际是"爸爸"。已用
correct_video_identity 手动纠正(标记 source=manual,受保护不会被以后的自动
识别覆盖回去),描述文字已确认同步更新。

顺带确认了另一批命中同样关键词的历史数据(74 条里有 3 条是"汤圆"光膀子)
是正确的——3 岁小孩光膀子玩正常,是通过幼儿/儿童年龄档规则识别出来的,跟
赤膊无关,不是误判,没有动。

新增 4 个测试:赤膊成年男性直判爸爸且不调用大模型(用会报错的假
person_identifier 验证短路生效)、覆盖各种赤膊近义词表述、赤膊小孩仍然
正确判成汤圆不受影响、没有赤膊关键词时行为不变照常走大模型比对。
2026-08-23 11:00:43 +08:00
ericwyuan
5f7b27bf04 fix(fam-edge): 人物识别结果没同步进事件描述文字
用户反馈:事件卡片上方的人物徽章正确显示"媳妇/爸爸",但描述文字里还是
"人物B双手叉腰站在客厅中央;人物A在远处厨房"这种原始 uid——两处对不上。

根因:rewrite_event_person_names() 只改了 person_list_json /
person_appearances_json[].uid 这些结构化字段,从没碰过 description(事件
描述)和 videos.summary_json(会话摘要)——这两个是大模型写的自然语言,
"人物A"这类 uid 是直接以文字形式嵌在句子里的,结构化字段改名不会带着改。

现在 rewrite_event_person_names() 额外对 description/summary_json 做文本
替换。替换顺序按 rename_map 的 key 长度降序——uid 可能带 "#2"/"#3" 这类
同名冲突后缀,"人物A" 是 "人物A#2" 的前缀,短的先替换会把长的错误地部分
命中,长的先替换才对。

同时跑了两次一次性回填修历史数据:
1. 对所有已有 person_identity_map 映射的 1265 个视频重新跑一遍
   rewrite_event_person_names,把之前只改了徽章、没改描述文字的历史数据
   补上(events 里"纯人物X不带真名"的还剩 258 条,涉及 94 个视频——都是
   性别/年龄特征本来就是 unknown 的画面,闭集识别本来就该留空不瞎猜,
   不是回填没覆盖到,是真的没法从图像信息判断是谁)
2. 找到 9 个从未跑过闭集识别的漏网视频(应该是 person_identifier 功能上线
   前处理的),重新识别后 1 个(video 2194)成功解析出汤圆+爸爸,其余 8 个
   同样是性别未知,正确保持未解析

新增 2 个测试覆盖 description/summary_json 文本替换,以及带后缀 label 的
替换顺序正确性。
2026-08-23 10:50:52 +08:00
ericwyuan
63b28663f1 feat(fam-edge): 启动时预热 Ollama,避免兜底触发时冷启动等 1-2 分钟
排查"Ollama 模型没有常驻内存":systemd 里 OLLAMA_KEEP_ALIVE=-1 其实一直配置
正确,但这个环境变量只保证"加载过之后不因为空闲被换出",不负责启动时主动
预加载。查 model_calls 表,ollama provider 从来没有一条调用记录——因为它是
问答链路最后一级兜底,前面 NVIDIA/Gemini 一直调用成功,从未真正轮到它,
自然也就从未被加载进内存过。

新增 OllamaAdapter.warm_up():送一条 num_predict=1 的最小请求强制模型加载,
超时给够 180s(冷启动实测能到 1-2 分钟)。app.py 启动时在后台线程调用,不
阻塞主服务启动;找不到 ollama 配置或预热失败都只记警告,不影响服务本身。

实测部署验证:重启后 36 秒完成预热,ollama ps 显示 qwen2.5:7b 已加载,
expires_at 显示不过期(keep_alive=-1 生效)——真正需要兜底的那一刻不会再有
冷启动延迟。

新增 test_ollama_adapter.py 4 个用例覆盖预热成功/HTTP失败/异常不上抛/超时
时长。
2026-08-23 09:33:49 +08:00
ericwyuan
bdf561a415 fix(fam-core): 登录密码不再硬编码兜底(fail-closed) + 会话有效期改 2 小时
之前 FAM_AUTH_USER/FAM_AUTH_PASS 没配置时会退回代码里写死的 ericwyuan/
iLoveJava5——这两个值跟 NAS SSH 密码是同一个,且这个登录页已经通过 frp 暴露在
公网上。.env 万一没配置好(这个项目里已经发生过好几次"忘了 source .env"的
情况),公网入口就会用一个和 SSH 密码相同、且写在源码里的已知密码兜底,双重
风险叠一起。

现在改成 fail-closed:FAM_AUTH_USER/FAM_AUTH_PASS 只要有一个没配置,直接拒绝
所有登录(打一条 ERROR 日志提醒去配 .env),不再有任何硬编码默认值。

会话有效期从 7 天改成 2 小时(按用户要求)。

新增 test_auth.py 11 个用例覆盖:未配置时 fail-closed、正确/错误凭据校验、
只配了一半也要拒绝、session 过期判定、白名单路径匹配。

已实测验证:当前 .env 里 FAM_AUTH_USER/PASS 仍是配置好的(还是 ericwyuan/
iLoveJava5,这个值本身没改——只是不再硬编码在代码里,运维如果想换成不同于
SSH 的密码,现在改 .env 就行,不用改代码),登录/拒绝错误密码都验证正常,
cookie Max-Age=7200 确认生效。
2026-08-23 07:56:33 +08:00
ericwyuan
9bba7b7e14 fix(chat): 流式问答中文乱码 + 问答模型链换成专用文字大模型
## 乱码修复
Gemini 流式接口 resp.iter_lines(decode_unicode=True) 没有显式设置
resp.encoding,requests 自己猜编码猜错了(Gemini 的 SSE 响应 Content-Type
不带 charset 参数),导致中文变成典型的 UTF-8 被当 Latin-1 解码的乱码。
fam-core 转发这一跳的 requests.iter_lines 调用有同样的坑,一并修了。
两处响应的 Content-Type 都补上 charset=utf-8,减少下游再犯同样错误的机会。
顺手用 latin-1/utf-8 往返把 NAS chat_history 里已经存了乱码的 3 条历史记录
恢复成正常文字。

## 问答模型链重新设计(跟视频分析完全独立)
用户要求问答不再用 flash/omni,优先用 NVIDIA 免费文字模型里上下文最大的几个,
不行再退 Gemini 非 flash 文字模型,最后本地 Ollama 兜底。

- base_adapter 新增 usage 字段(纯标记,不影响行为)
- qa.py 的 QAOrchestrator 只挑 role='text' 的适配器参与问答(按 config 出现
  顺序决定降级顺序),role='vision' 的视频分析适配器(gemini-flash-latest /
  nvidia omni)不再参与问答,两条链路彻底解耦
- 新增问答专用 NVIDIA 文字模型链(同一适配器内部 model_chain 降级):
  nemotron-3-ultra-550b-a55b(1M上下文/561B) -> nemotron-3-super-120b-a12b
  (1M上下文/124B) -> openai/gpt-oss-120b(131K上下文/117B)。三个都用真实
  API 调用验证过当前账号可访问;同时验证过 llama-3.1-70b-instruct 和
  nemotron-super-49b-v1.5 都收到"08/25/2026 起弃用"通知,排除;
  nemotron-ultra-253b/mistral-large-2/nemotron-4-340b/kimi-k2.6 在目录里
  能看到但实测调用返回 404"账号无权限",排除
- 新增问答专用 Gemini 非 flash 文字模型:gemini-pro-latest -> gemini-2.5-pro
  (1M 上下文,复用视频分析同一批多 key 轮换)
- /api/edge/chat/ask(/stream) 默认 max_tokens 从 1024 提到 3072:新链路
  优先用的都是"推理"模型,回答前会先吐一段思考过程,1024 经常在思考阶段
  就被截断,用户永远看不到真正答案

新增 test_qa.py::test_init_only_keeps_text_role_adapters_in_config_order
验证角色过滤+顺序正确。

本地检查过 Ollama 服务本身:systemd enabled+running(8h 正常运行,
qwen2.5:7b 已加载),并非没启动——用户观察到的现象另有原因。
2026-08-23 07:46:48 +08:00
ericwyuan
22bd4d4ff9 fix(fam-edge): 停用旧 person_service LLM 合并 - 已被闭集识别取代且仍在持续产生错误合并
person_identifier 闭集识别(汤圆/媳妇走性别年龄规则,爷爷/爸爸走视觉大模型比对
参考图)已经稳定跑起来了,people 表里 4 个人的真实姓名行数据干净(汤圆692/
爸爸323/爷爷320/媳妇290)。但发现 person_service 的老 LLM 合并调度还在跑(每
30 分钟一次),持续把不可靠的猜测结果(如"人物E"→"人物T"这类无意义合并)写进
canonical_name,跟新系统打架,是 people 表里 392 条历史垃圾行的根源。

停用 person_service.enabled,改用新系统。順帶清理 Oracle 库里已经产生的 392
条垃圾行:10 条能对应上真实姓名的(如"幼儿"→汤圆 100 次、"人物A"→爷爷 37 次)
appearances 累加合并回真正的规范行,其余 382 条(老 LLM 合并的无意义中间结果,
canonical_name 对不上任何真实姓名)直接删除——不影响 events/videos 里已经落库
的原始分析数据,只是清理 people 表的聚合展示行。
2026-08-23 02:23:00 +08:00
ericwyuan
2c5bf950c5 feat: 事件时间轴缩略帧 + 人物管理头像 + 人物合并硬规则校验
## 新架构:Oracle 集中计算 + NAS 代理展示

### Oracle 端 (fam-edge)
- 新增 frame_service: ffmpeg 视频抽帧 + VLM 人物定位裁剪头像(磁盘缓存)
- 新增 /api/oracle/frame: 按 video_id+ts 抽帧返回 jpeg(带 token)
- 新增 /api/oracle/avatar: 按 label 生成人物头像(VLM 定位人物 + 兜底整帧居中)
- 新增 person_identifier: 人物身份识别模块
- Gemini 适配器支持 flash/flash-lite 双模型切换,429 自动降级
- frame_service VLM 全模型 429 时进入 10 分钟熔断,避免每次请求白打配额
- 兜底头像不落缓存,配额恢复后自动重试 VLM 精确定位

### 人物合并硬规则校验(框架级修复)
- person_service: LLM 合并结果落库前加硬冲突检测
  - 性别冲突 → 绝不合并
  - 年龄档跨未成年/成年 → 绝不合并(防止把爷爷/宝宝并进同一人)
- oracle_db: upsert_person 入口剥离括号后缀(人物A(别名:人物B) → 人物A),消灭垃圾人物行
- 修复 set_canonical 丢弃 source 参数的 bug(旧代码硬编码 'manual' 导致错误合并被永久固化)
- get_events_for_label: 只提取该身份组的特征文本,头像定位更精准

### NAS 端 (fam-core)
- 新增 img_proxy: /api/proxy/frame 和 /api/proxy/avatar 代理 Oracle 图片
- app.py 注册 img_bp 蓝图
- oracle_sync / db_layer / member_manager 同步人物表

### UI 端 (fam-ui)
- 事件时间轴: 每条事件卡片加时间点缩略帧
- 人物管理: 每人卡片加头像(150x150 圆角)
- parse_persons: 剥离括号备注,与 Oracle 归一化一致
- 新增 EventItem 组件、Timeline 页改造
- Chat / ServiceStatus 页相应调整

### 数据库
- scripts/ddl.sql: 同步表结构更新
- Oracle people 表: features_json / display_uid / source 字段完善
2026-08-23 00:13:56 +08:00
ericwyuan
aca1a674b1 [文档] README 补充登录校验(auth 模块/凭据/白名单)与 frp 外网 8000 映射说明 2026-08-22 14:20:40 +08:00
ericwyuan
9c51c65e62 [功能] FAM-Core 登录校验 + frp 外网暴露 8000 - 新增 auth 模块(POST /api/login 校验 FAM_AUTH_USER/PASS 默认 ericwyuan/iLoveJava5, HttpOnly cookie 7天, 页面未登录 302 /login, /api/* 未登录 401; 白名单: /login /health /api/ss/webhook /assets/*); 内置深色登录页(SPA 零改动); frpc.toml 增加 fam-core 代理 外网8000->NAS8000 2026-08-22 14:19:54 +08:00
ericwyuan
1dd9df34c2 [功能] 事件-片段一致性对账 - /api/oracle/activity 加 segment.consistency(事件总数/已结束/已分割/缺口+明细/素材覆盖范围), 服务状态页展示一致性数字 2026-08-22 13:40:42 +08:00
ericwyuan
177c2a83b7 [修复] 进行中事件 duration=0 分割丢失 - 推送过 duration<=0 的事件记入 _zero_dur_ids, 下轮窗口回查已结束(duration>0)补推覆盖 Oracle; 保证 ss_motion_events 最终持有真实 duration, 分割不再跳过 2026-08-22 13:25:20 +08:00
ericwyuan
cbcc5d0325 [功能] 服务状态页新增「视频分割」状态 - Oracle /api/oracle/activity 加 segment(运动片段 done/pending/failed/文件数/最近分割活动), ServiceStatus.vue 加视频分割卡片 2026-08-22 13:18:57 +08:00
ericwyuan
01a8ac2a3f [文档] 拓扑图 FAM-UI 标注同步为 Vue3(清理残留 Streamlit :8501) 2026-08-22 13:15:04 +08:00
ericwyuan
354ff18101 [文档] 代码-文档一致性整理 - README 同步 v3 全量(2.1网络/3模块表/4表结构/5 API含people-clips/6.1历史标注/8部署systemd+Vue3+config/12进度); docs/DEPLOY.md 重写为 v3; 删除过时 start_ui.sh(Streamlit); start_edge.sh 注明 systemd 为准; 补提交 test_motion_notifier 2026-08-22 13:14:38 +08:00
ericwyuan
96644599d3 [修复] pymysql execute 字面 % 冲突 - SQL 中 motion_% 的 % 被当参数占位符导致 /api/ui/videos 500, 改用 LEFT(filename,7)='motion_' 判断前缀 2026-08-22 13:06:52 +08:00
ericwyuan
6a29e88d86 [优化] 时间轴/统计过滤空会话 - 只展示运动片段(motion_)或有事件的视频, 整段素材分割0段的空会话不占时间轴; get_sync_videos/get_sync_stats 加内容过滤 2026-08-22 12:59:21 +08:00
ericwyuan
4cf4fc4bc2 [功能] 人物管理按运动视频重设计 - 人物卡新增「运动片段」区块(缩略图/时间/摘要/事件数,点击跳时间轴定位); 新增 GET /api/ui/people/clips 后端接口(按 label/canonical_name 查关联运动片段,含 first_ts/clip_events); Timeline 支持 ?video= 定位 2026-08-22 12:55:30 +08:00
ericwyuan
ffcc292cce [文档] 同步 v3 运动事件驱动架构 - 整段素材分割运动片段再分析(不再整段送云端): README 1.1/2.2/2.2.1/2.3/模块表, PROGRESS 运行状态+变更记录; 记录 systemd 守护部署方式 2026-08-22 12:46:33 +08:00
ericwyuan
8d6cfad6c0 [修复] 运动片段分割 ffmpeg 调用缺可执行文件 - args 首元素补 ffmpeg 路径(此前被当成可执行文件导致全部分割失败) 2026-08-22 12:41:06 +08:00
ericwyuan
a1523b46c8 [重构] 运动事件驱动架构 - 整段素材按 ss_motion_events 分割运动片段再分析(不再整段送云端): oracle_db 加 motion_event_id/camera_id 列 + get_motion_events_in_range/has_unfinished_motion_in_range/get_video_by_motion_event_id; video_processor 素材→分割/片段→分析双分支(ffmpeg -c:v copy -c:a aac 保留音频); video_queue 片段入队; config 加 motion_segment 块 2026-08-22 12:19:17 +08:00
ericwyuan
2d9d96de56 [功能] fam-edge 运动心跳支持 - /api/ss/motion 空 events 心跳刷新, has_motion_in_range_local 按 max_heartbeat_age_sec(900s) fail-open 2026-08-22 12:08:00 +08:00
ericwyuan
7adc32486a [重构] 运动监测恢复轮询主路径(简单稳定) - poll_enabled 默认开启;修复3隐患: 推送失败批次不前进游标(下轮重试不丢事件)、重启优先续用DB游标(停机期间事件补推)、fetch limit 提到1000;Webhook降级为可选补充 2026-08-22 12:00:03 +08:00
ericwyuan
b2b537dde5 [文档] 同步 v3 运动监测架构(Webhook 驱动/去轮询) - README 补充 2.2.1 链路/API 表/模块表,PROGRESS 更新运行状态与变更记录 51-53 2026-08-22 11:11:15 +08:00
ericwyuan
e52d8e9103 [修复] MotionNotifier 补回 camera_ids 属性 - 避免重新开启轮询时 _fetch_events 引用缺失属性崩溃 2026-08-22 10:58:56 +08:00
ericwyuan
ca6d425b19 [重构] 运动监测改为 Webhook 驱动(去轮询) - 关闭 MotionNotifier 轮询,NAS 端合成稳定 event_id、摄像头名映射 camera_id、EVENT_TIME 解析为 epoch,SS Webhook 直接推送甲骨文 2026-08-22 10:53:03 +08:00
ericwyuan
01bbb39750 [新增] NAS 运动监测通知服务 MotionNotifier - 轮询/接收 SS 事件后主动 POST 推送到甲骨文 /api/ss/motion(单向 NAS->Oracle):新增 motion_notifier 子包、/api/ss/webhook 实时转发蓝图、db_layer 运动游标、app.py 接线与 config 块 2026-08-22 10:26:03 +08:00
ericwyuan
38dea6ba2e [重构] 运动预过滤改为本地事件(NAS推送) - 甲骨文不再反向访问NAS:新增 ss_motion_events 表/record_motion_events/has_motion_in_range_local,/api/ss/motion 接收端点,video_processor 改用本地运动事件预过滤,停用 dsm_motion_client 反向查询 2026-08-22 10:25:56 +08:00