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

@@ -0,0 +1,57 @@
"""把 NAS MariaDB 导出的 chat_history 导入甲骨文 SQLite2026-09-13 迁云一次性脚本)。
chat_history 是 NAS 那套库里唯一"不是镜像"的表——其余 sync_* 都能从甲骨文重新
读出来,只有问答历史是本地产生的,迁云时必须搬过来。
用法JSON 从 stdin 进来,导出命令见 docs/DEPLOY.md §2.4
cat chat_history.json | /opt/fam-core/venv/bin/python scripts/import_chat_history.py
幂等:按 chat_id 跳过已存在的行,重复执行不会产生重复记录。
"""
import json
import os
import sys
sys.path.insert(0, os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'src'))
from fam_core import db_layer # noqa: E402
_COLS = ('chat_id', 'user_question', 'ai_answer', 'context_summary',
'queried_date', 'queried_person', 'created_at')
def main():
try:
rows = json.load(sys.stdin)
except ValueError as e:
print(f"stdin 不是合法 JSON: {e}", file=sys.stderr)
return 1
if not isinstance(rows, list):
print("期望一个 JSON 数组", file=sys.stderr)
return 1
conn = db_layer.get_conn()
try:
db_layer._ensure_chat_schema(conn)
inserted = skipped = 0
for r in rows:
cid = r.get('chat_id')
if cid is not None and conn.execute(
"SELECT 1 FROM chat_history WHERE chat_id=?", (cid,)).fetchone():
skipped += 1
continue
conn.execute(
"INSERT INTO chat_history ({}) VALUES ({})".format(
','.join(_COLS), ','.join('?' * len(_COLS))),
tuple(None if r.get(c) is None else str(r.get(c)) for c in _COLS))
inserted += 1
conn.commit()
finally:
conn.close()
print(f"导入 {inserted} 条,跳过 {skipped}chat_id 已存在)")
return 0
if __name__ == '__main__':
sys.exit(main())