diff --git a/.workbuddy/memory/2026-08-25.md b/.workbuddy/memory/2026-08-25.md index 9c145b4..0093d7a 100644 --- a/.workbuddy/memory/2026-08-25.md +++ b/.workbuddy/memory/2026-08-25.md @@ -14,3 +14,7 @@ - 孤儿云库 sentinel_home_ai + famcore 用户已 DROP(新服务器)。 - SS Webhook:代码中为可选补充(motion_bp.py 文档明确),SS 未配行动规则故休眠, 是否删除待用户拍板。 +- **用户拍板:删除 webhook + NAS 前端托管,并已提交推送**(两个 commit 已 push 到 Gitea: + `7948f55` docs 迁移文档、`cf3d145` refactor 移除 webhook/static_app 共删 329 行)。 + **待办:代码尚未部署到 NAS**——需同步 fam-core 到 NAS、删 NAS fam-ui 目录 + start_ui.sh、 + 重启 fam-core 验证 /api 与登录(云端 :80 前端不受影响)。 diff --git a/.workbuddy/memory/2026-08-29.md b/.workbuddy/memory/2026-08-29.md new file mode 100644 index 0000000..f2a62ea --- /dev/null +++ b/.workbuddy/memory/2026-08-29.md @@ -0,0 +1,37 @@ +# 2026-08-29 工作日志 + +## 修复 Oracle→NAS 增量同步 1062 (uq_video_raw_uid 重复键) — 已部署验证 + +### 现象 +- 同步状态页:状态"同步中"、`最近 —`、`本次增量 —`、游标卡在 `2026-08-29 04:57:15` +- 报错:`(1062, "Duplicate entry '1073-汤圆' for key 'uq_video_raw_uid'")`,游标永不推进 + +### 根因(两层) +1. **镜像表主键设计缺陷(主因)**:`sync_identity_map` 以 Oracle `person_identity_map.id` 作主键。 + Oracle 端 id 是 AUTOINCREMENT 代理键,但 Oracle 库重建/恢复后 id 会被复用——实测 Oracle + 现在的 id=744 是 `(1073,'汤圆')`,而 NAS 上 id=744 还是旧的 `(1302,'人物E')`。旧 upsert 的 + `ON DUPLICATE KEY UPDATE` 先按 **PK id** 命中旧行,再把它 UPDATE 成 `(1073,'汤圆')`,与 + 已有的 `(1073,'汤圆')` 行在 `uq_video_raw_uid` 上二次冲突 → 1062 → 整批失败 → 游标不推进 + → 每 30 分钟重复失败。 +2. **陈旧 __pycache__(部署陷阱)**:NAS 上 `.pyc` 时间戳比 `.py` 新(部署时 `.py` 带旧 mtime 拷贝), + Python 信任 pyc 加载了旧逻辑;另曾有两个 gunicorn master 并存(8487/10914),只有一个绑 8000。 + +### 修复 +- **db_layer.py `upsert_sync_identity_map` v2**:镜像表改为 NAS 本地自增 `id` 主键, + 业务键 `(video_id, raw_uid)` 唯一;Oracle id 只落 `oracle_id` 列溯源;UPDATE 子句不再改写 + video_id/raw_uid(只更新 oracle_id/canonical_name/source/updated_at/synced_at)。 +- **scripts/ddl.sql**:同步更新表定义(`id INT AUTO_INCREMENT PRIMARY KEY` + `oracle_id INT`)。 +- **现网迁移**:`ADD COLUMN oracle_id` → 回填 `oracle_id=id` → `MODIFY id AUTO_INCREMENT`(无重复对, + 迁移安全);修复早前测试误改的 3061 行 canonical_name。 +- **部署**:db_layer.py 用 stdin 管道覆盖 NAS;**清空全部 __pycache__**;杀掉双 gunicorn,单实例重启。 + +### 验证(通过) +- 09:55 一次补拉成功:`videos+260 events+1456 people+37 model_calls+744 identity_map+285`,游标推进到 09:55:06 +- `/api/status`:`last_error=null`、`last_sync_at` 有值;手动 `/api/sync/trigger` ok(拉 2videos/2events/1people/6calls/1identity) +- `(1073,'汤圆')` 行已自愈:`oracle_id=744`;NAS 旧 `(744→1302,'人物E')` 保留不冲突 + +### 待办/注意 +- 本地 repo 有未提交改动:`fam-core/src/fam_core/db_layer.py`、`scripts/ddl.sql`(等用户拍板提交) +- 同类隐患:sync_videos/sync_events/sync_model_calls 同样以 Oracle id 为主键,但 Oracle 侧这些表 + 只插入不 UPDATE、id 稳定,风险低;若将来 Oracle 也重建,需同样改法 +- NAS 部署后必须清 __pycache__(或 touch .py),否则旧 pyc 被加载(已踩坑) diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 2a3a6d3..a474f3b 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -27,6 +27,12 @@ 素材 `/opt/fam-edge/gdrive_videos`;logs 目录 ubuntu 可写。 **前端亦托管于此机 Caddy :80**(fam-ui SPA + /api 反代回 NAS fam-core :8000 经 frp 隧道);旧机 129.146.203.203 已退役。 - 部署:本地 git 提交 push Gitea → tar 管道(NAS 直接解包;Oracle `--strip-components=1` 到临时目录再 cp,避免动 data/venv)。 +- **NAS 部署坑(2026-08-29 踩过)**:拷贝 `.py` 会带旧 mtime,若 `.pyc` 比 `.py` 新,Python 加载陈旧字节码 + (旧逻辑上线,曾导致 1062 假象)。部署后必须 `find src -name __pycache__ -exec rm -rf {} +` 再重启; + `pkill -f` 会匹配 SSH 自身命令导致断连(用 PID 文件或精确 pattern)。 +- **镜像表键设计(2026-08-29 定稿)**:`sync_identity_map` 不再以 Oracle `person_identity_map.id` 作主键 + (Oracle 库重建会复用 id),改 NAS 本地自增 `id` + 唯一键 `(video_id, raw_uid)`,Oracle id 落 `oracle_id` 列溯源; + 同批 upsert 的 UPDATE 子句不改写键列,避免 uq 二次冲突 1062。sync_videos/events/model_calls 同理(风险低未改)。 ## 提交规范(强制) - 格式 `[阶段X.Y子任务号] 子任务名称 - 完成内容简述`;Bug 修复 `fix(模块): 问题简述` diff --git a/fam-core/src/fam_core/db_layer.py b/fam-core/src/fam_core/db_layer.py index cdad331..45825d7 100644 --- a/fam-core/src/fam_core/db_layer.py +++ b/fam-core/src/fam_core/db_layer.py @@ -390,8 +390,12 @@ def upsert_sync_model_calls(rows: List[Dict]) -> int: def upsert_sync_identity_map(rows: List[Dict]) -> int: """批量 upsert Oracle 传来的 person_identity_map 增量(幂等,重复覆盖)。 - 甲骨文不稳定,提取出的有效数据(含闭集人物识别结果/纠错记录)都要同步到 - NAS 防丢失——这张表跟 sync_people/sync_model_calls 走一样的镜像模式。 + v2 修复 (2026-08-29):不再以 Oracle 的 person_identity_map.id 作为 NAS 主键。 + 该 id 只是 Oracle 端代理主键,一旦 Oracle 库重建/恢复,id 会被复用(实测出现 + id=744 同时对应 NAS 上的 (1302,'人物E') 与 Oracle 现在的 (1073,'汤圆')), + 旧写法会先按 id 撞主键、再 UPDATE 成新组合,触发 uq_video_raw_uid 二次冲突 + 报 1062。现改为:NAS 本地自增 id 做主键,业务键 (video_id, raw_uid) 唯一, + Oracle 的 id 只落 oracle_id 列作溯源参考。 """ if not rows: return 0 @@ -402,11 +406,10 @@ def upsert_sync_identity_map(rows: List[Dict]) -> int: for r in rows: cur.execute( """INSERT INTO sync_identity_map - (id, video_id, raw_uid, canonical_name, source, updated_at, synced_at) + (oracle_id, video_id, raw_uid, canonical_name, source, updated_at, synced_at) VALUES (%s,%s,%s,%s,%s,%s, NOW()) ON DUPLICATE KEY UPDATE - video_id=VALUES(video_id), - raw_uid=VALUES(raw_uid), + oracle_id=VALUES(oracle_id), canonical_name=VALUES(canonical_name), source=VALUES(source), updated_at=VALUES(updated_at), diff --git a/scripts/ddl.sql b/scripts/ddl.sql index d94d62f..422643f 100644 --- a/scripts/ddl.sql +++ b/scripts/ddl.sql @@ -188,8 +188,12 @@ CREATE TABLE IF NOT EXISTS sync_cursor ( -- 7.5 人物对应关系表(Oracle person_identity_map 镜像,2026-08-22 新增) -- 闭集人物识别:某视频里 Gemini 给的原始 uid 与解析出的规范名(爷爷/爸爸/媳妇/ -- 汤圆)之间的映射,供事件时间轴"纠错"按钮定位、追溯识别来源 +-- v2 (2026-08-29):id 改为 NAS 本地自增主键,业务键 (video_id, raw_uid) 唯一; +-- Oracle 的 person_identity_map.id 只落 oracle_id 列溯源。原以 Oracle id 为主键, +-- 一旦 Oracle 库重建/复用 id 会引发 uq_video_raw_uid 二次冲突 (1062)。 CREATE TABLE IF NOT EXISTS sync_identity_map ( - id INT PRIMARY KEY COMMENT 'Oracle person_identity_map.id', + id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'NAS 本地自增主键', + oracle_id INT COMMENT 'Oracle person_identity_map.id(仅溯源参考)', video_id INT NOT NULL COMMENT '关联 sync_videos.id', raw_uid VARCHAR(100) COMMENT '该视频里 Gemini 给的原始 uid', canonical_name VARCHAR(100) COMMENT '当前生效的规范名',