Files
sentinel-home-ai/.workbuddy/memory/2026-08-22.md
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

215 lines
21 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-22
## 核查 SS Webhook 配置字段sentinel-home-ai
- 本机 SS 版本 9.3.0-12139≥9.1.1Webhook 动作设备可用。
- 关键发现:**SS Webhook 与 motion_bp.py 接收端 schema 不匹配**。
- SS Webhook 是「用户自定义参数名 + 模板变量」机制Motion Detection 支持:
%EVENT_TIME%(字符串,非 epoch、%DEVICE_NAME%(摄像头名文本,非 camera_id
%EVENT_NAME%、%SERVER_NAME%、%THUMBNAIL_URL%。每事件单次 POSTbody 为 form/JSON 的 key=value。
- 接收端 motion_bp.py 期望 EventCenter.Event.List 风格events:[{event_id(int 主键),
camera_id, event_type=10, start_time(epoch), duration, thumbnail_url}]。
- 因 Webhook 无法提供 event_id`record_motion_events``if eid is None: continue`
导致真实 webhook 事件 100% 被静默丢弃。
- 结论轮询路径MotionNotifier._fetch_events 走 EventCenter.Event.List是唯一可靠数据源
已端到端验证。Webhook 如需启用,须改接收端做字段映射 + 与轮询去重(待用户拍板)。
## Webhook 驱动改造(去轮询)—— 已完成并端到端验证
- 用户决策:**不要轮询,改用 SS Webhook 作为唯一数据源**。
- 改造内容commit ca6d425 / e52d8e9已 push Gitea + 部署 NAS
- `motion_notifier.py``poll_enabled=false`(默认关),关闭轮询线程;新增
`refresh_camera_map()`(启动时一次性从 SS Camera API 拉 name→id 映射)、
`resolve_camera_id()``parse_ss_time_to_epoch()`EVENT_TIME 字符串→epoch按 NAS +8
`_synth_event_id()`device_name+event_time+thumbnail_url 哈希成稳定 int幂等去重
`build_event_from_webhook()`
- `motion_bp.py``/api/ss/webhook` 解析 JSON/表单、单条/数组,用 build_event_from_webhook
映射后推送甲骨文SS 端需按固定参数名配置event_time/device_name/event_name/server_name/thumbnail_url
- `config.yaml`:加 `camera_name_to_id: {"Generic_ONVIF-001": 2}` 兜底,删原 camera_ids 轮询项。
- `app.py`:启动加载摄像头映射;`start_core.sh` 修正 APP_DIR 路径 bug 并显式 source .env。
- 验证:模拟 JSON + 表单 + 重复推送,均 pushed=1甲骨文 ss_motion_events 落库正确
camera_id=2, event_type=10, start_time epoch 北京时区对齐);`has_motion_in_range_local`
窗口命中 True / 远离 False测试数据已清理。
- **待用户操作**:在 SS「行動規則 → 事件=偵測到動作 → 動作=Webhook」配置URL 填
`http://127.0.0.1:8000/api/ss/webhook`,参数名严格用 event_time/device_name/event_name/
server_name/thumbnail_url值分别对应 %EVENT_TIME%/%DEVICE_NAME%/%EVENT_NAME%/%SERVER_NAME%/%THUMBNAIL_URL%)。
- 注意:甲骨文 ss_motion_events 现存 21 条为历史轮询真实事件2026-08-22 上午),轮询已停,不再新增。
## 「webhook→回查 SS 事件列表补全」链路测试(不写生产代码)
- 用户要求先测清整条链路再决定是否实现。测试结论:**思路完全可行,各段均真机验证通过**。
- **链路 ASS 事件列表真实字段)**NAS 上 `SYNO.SurveillanceStation.EventCenter.Event` method=List
camera_ids=2, event_types=10真实返回 id/start_time/duration/thumbnail_url/thumbnail_dir。
实测 e.g. id=25536 dur=63s、id=25535 dur=95s、id=25538 dur=0。即 duration 真实存在(短动作 0~几秒,长动作数十秒)。
- **链路 A2webhook 触发时刻→补全匹配)**:模拟 webhook 触发时刻=事件 start_timeepoch
用 ±120s 窗口找时间最接近事件diff=0 精准匹配,补全出真实 event_id/start_time/duration/thumbnail。
(注:真实 %EVENT_TIME% 是分钟级字符串,解析为 epoch 与真实 start_time 差 ≤ 数秒120s 窗口足够覆盖。)
- **链路 B补全结果推甲骨文落库**POST /api/ss/motionbody 含 token + events[{
event_id,camera_id,event_type,start_time,duration,thumbnail_url}])均 stored=1
甲骨文库 ss_motion_events 字段核对正确event_id=25535 dur=95 / 25536 dur=63
- **链路 C预过滤**has_motion_in_range_local 窗口含事件=True、远离时段=False逻辑正确。
- 全部测试数据已清理,库恢复 21 条历史真实事件。
- **未真机验证的一环**SS 后台尚未配 Webhook故「真实 webhook 触发」这步是模拟(用真实 start_time 当 trigger
其余 SS 查列表 / 推送甲骨文 / 落库 / 预过滤均为真机。
- 下一步若实现:在 motion_notifier 加 enrich_event_from_ss()webhook 收到后本机回查 SS 列表补全真实字段。
## 「能否输出对应运动视频」调研——可行,直接读录像文件 + ffmpeg 裁剪
- 用户问运动事件能否输出对应视频片段。结论:**完全可行,且已真机裁剪出 22s MP4 演示**。
- **SS API 导出路径全部不可用SS 9.3.0-12139**
- `Recording.Export` 仅剩向导方法CamEnum/CheckName/Save/Load/DeleteEventArchive.jsSave 需复杂参数431
非旧文档的 RangeExport9.3 已无此方法,实测 103
- `ThirdParty/Recording/Download/v1`camId/startTime/endTime ISO 字符串)返回 401需第三方授权不可用
- **关键发现:录像文件明文直读**(无需任何 API
- 路径:`/volume1/surveillance/<摄像头名>/<YYYYMMDDAM|PM>/<摄像头名>-YYYYMMDD-HHMMSS-<起始epoch毫秒>-<seq>.mp4`
- 每 30 分钟一个片段(约 377MBh264 2880x1620@14.28fps + pcm_alaw(8kHz) 音频;
文件名含起始 epoch 毫秒,可用 (camera_id, start_time) 直接定位camera_id→目录名需 SS Camera API 映射)。
- recording_encrypt.db 存在但当前录像为明文(未启用加密)。
- **裁剪验证(真机)**:事件 id=25535start=1787369601落在
`Generic_ONVIF-001-20260822-110859-1787368139533-7.mp4`11:08:59 起 30min偏移=1787369601-1787368139=1462s。
`ffmpeg -ss 1462 -t 20 -i 片段 -an -c:v copy` → 22.13s / 4.4MB MP4copy 模式从关键帧起,±秒级偏差;需精确可重编码)。
**必须 -an**pcm_alaw 音频无法封装进 mp4 容器,-c copy 会报 "Could not find tag for codec pcm_alaw"。
- 注意NAS ffmpeg 为老版本(/usr/bin/ffmpeg不支持 -show_entries`-i 2>&1 | grep Duration` 查时长)。
- 下一步若实现motion 事件补全 start_time/duration 后NAS 端按 (camera, start) 定位片段 + ffmpeg 裁剪输出。
## Webhook vs 事件列表一致性核查(重要认知)
- 用户质疑Webhook 与事件列表是否 1:1事件列表有的会不会 webhook 没推?
- 实测NAS**ActionRule List = 0 条**SS 后台尚未配任何行动规则 → 真实 webhook 零推送);
今日 camera2 运动事件 = 141 条id 25405~25545事件稀疏突发35 个 >120s 断档,最长 4h 无动作)。
- 机制结论:**Webhook ⊂ 事件列表,不是 1:1**。EventCenter 是全量事件记录;行动规则引擎按
「摄像头范围/时间段/去抖合并/开关/规则数」过滤后才触发 Webhook。故**事件列表有、webhook 没推
是完全可能的**,纯 Webhook 驱动存在漏事件 → 预过滤误杀风险。
- 反方向webhook 有、列表无罕见仅事件落库毫秒级时序±120s 窗口 + fallback 已兜底。
- 对策待用户拍板A. Webhook 实时 + 每 5 分钟增量对账补推(推荐,对账非事件轮询,只补差异);
B. 接受漏风险(规则配全量摄像头+关去抖C. 回轮询(用户已否决)。
## Webhook 实现审查 + 用户最终决策恢复轮询主路径commit 7adc324已部署验证
- Webhook 实现审查发现 P0/P1/P2 问题:①合成 event_id 碰撞(同分钟多条事件若 thumbnail 为空 → 同 id 被
UNIQUE 吞掉);②推 Oracle 失败无重试直接丢③无对账兜底Webhook⊂事件列表④SS Webhook 请求格式
从未真机验证(行动规则 0 条);⑤时间解析失败伪造为 now(UTC) 混时区⑥camera_id=None 照常入库。
- **用户拍板:放弃 Webhook恢复轮询简单稳定**。改动:
- motion_notifier.pypoll_enabled 默认 true_fetch_events limit 100→1000
_init_cursor 优先续用 DB 游标重启补推停机期间事件DB 空才初始化为 SS 当前最大 id
_poll_once 仅推送成功批次才前进游标(失败批次下轮窗口回看重试,不丢事件);
docstring 改为轮询主路径说明。
- config.yamlpoll_enabled: true加 camera_ids: [2],注释同步。
- motion_bp.pydocstring 改为"Webhook 可选补充,非主路径"(端点保留)。
- **端到端验证(真机)**:重启后游标续用 DB=25491 → 第一轮补推 55 条停机期间事件25492~25546
第二轮增量 2 条25547/25548Oracle ss_motion_events 总数 21+55+2=**78**event_id 全为真实 SS id
25471~25548duration 真实分布0~31s/api/ss/statuspoll_enabled=true running=true
heartbeat_running=true pushed_total=57。
- 结论:轮询路径现在简单稳定、不丢事件(重启补推 + 失败重试 + 真实字段Webhook 降级为可选。
## 运动事件驱动架构 v3commit a1523b4/8d6cfad/ffcc292已部署验证
- 用户决策:**不再处理整段视频**timeline 事件轴/人物管理/统计全部用"运动时间处理后的数据"。前端不改。
- 澄清rclone 整段素材**必须保留**(在甲骨文按运动时间分割);音频**保留转码**aac
- **实测纠正认知**SS 事件 duration=0 只出现在动作进行中(查询时未结束),结束必为正数
id=25538 11:38 查 dur=012:13 查 dur=2近 1h 43 条无一条已结束且 dur=0
故分割只处理已结束事件start+duration ≤ now+grace 10s进行中的下轮再分割。
- **Oracle 端改动**oracle_db 加 motion_event_id/camera_id 列 + get_motion_events_in_range/
has_unfinished_motion_in_range/get_video_by_motion_event_idvideo_processor 素材→分割/片段→分析
双分支ffmpeg -c:v copy -c:a aac 保留音频motion_event_id 幂等video_queue 片段入队;
config 加 motion_segment 块clips_dir=/opt/fam-edge/motion_clips
- **E2E 真机验证**:素材 32711:08:59→ 分割 35 段event_id 25504-25538dur 1-132s→ 片段只分析
summary 真实:媳妇/爷爷/汤圆)→ events 绝对时间 ts → NAS 同步 → /api/ui/videos 显示 motion_ 片段
(前端契约字段全在)→ /api/ui/videos/<id> 详情 events+people 正常 → /api/proxy/frame 帧图 200。
- **踩坑**:① sqlite3.Row 无 .get()vrow 访问用下标);② ffmpeg subprocess args 首元素必须是可执行
文件(漏了 ffmpeg 路径 → "No such file or directory: '-y'");③ **Oracle fam-edge 由 systemd
fam-edge.service 守护Restart=always**——部署后必须 `sudo systemctl restart fam-edge`,手动
setsid 启动会和守护打架(端口冲突 Connection in use
- 旧整段分析的历史数据319 个 done 素材)保留展示;新素材按新逻辑分割。
## 清理旧数据重提取 + 人物管理按运动视频重设计commits 4cf4fc4/6a29e88/9664459
- **用户决策**:甲骨文+NAS 后台数据全删(整段视频提取的旧结果 + 人物数据),按新框架运动视频重新提取。
- **清理**Oracle videos(364)/events(1687)/people(44)/model_calls/service_activity 全清 + motion_clips 目录清空
(保留 ss_motion_events 93 条 + 素材文件 + 配置;先备份 oracle.db.bakNAS sync_videos/sync_events/sync_people/sync_cursor 全清。
- **重提取**systemctl restart fam-edge → producer 重新登记全部素材 → 分割8/15-21 素材无运动事件→0 段;
8/22 素材→运动片段)→ 片段分析 → 重新聚合人物3 身份:爷爷/汤圆/媳妇)。
- **问题**:历史素材分割 0 段会以"空会话"占满时间轴 → db_layer.get_sync_videos/get_sync_stats 加内容过滤
LEFT(filename,7)='motion_' OR EXISTS 有事件),前端契约不变。
- **人物管理重设计**PersonCard.vue 新增「运动片段」区块(缩略图/时间/摘要/事件数,点击跳 /timeline?video=
后端新增 GET /api/ui/people/clips?label=db_layer.get_sync_people_clips 按 label/canonical_name 匹配
person_list_json → 关联运动片段,含 first_ts/clip_eventsTimeline.vue 支持 ?video= 定位。
- **踩坑**pymysql execute 用 % 做参数占位符SQL 字面量含 'motion_%' 的 % 会报
"unsupported format character" 500 → 改用 LEFT(filename,7)='motion_'。
- 验证:/api/ui/videos 全为 motion_ 片段15 条,含重新提取的 12:34 等新片段people 3 身份;
people/clips 返回媳妇 3 个片段。前端 dist 已构建部署fam-ui/dist 不入库,构建产物单独 tar 部署)。
## 代码-文档一致性整理commits 354ff18/01a8ac2已 push
- README 全量同步 v32.1 网络要点(轮询主路径/Webhook 可选2.2 拓扑图Streamlit→Vue3、Video-Queue/分割);
3.1-3.3 模块表MotionNotifier poll 主路径+游标/心跳、UI-API、people clips、Person-Service4.1 表清单
videos 加 motion_event_id/camera_id、ss_motion_events、sync_cursor 双游标5.1-5.2 API 表(/api/ss/motion、
/api/oracle/frame|avatar、/api/ui/people-clips、/api/ui/* 全表、/api/proxy/*5.3 片段直传6.1 历史标注;
8.1-8.4 部署systemd fam-edge、start_core.sh source .env、npm build、motion_segment config、数据清理指引
9 快速开始12 进度。
- docs/DEPLOY.md 重写为 v3Vue3 构建/部署、systemd、tar 管道、验证清单含 motion_clips
- 删除过时 scripts/start_ui.shStreamlitstart_edge.sh 注明 systemd 为准。
- 补提交 fam-core/tests/test_motion_notifier.py。
- 残留扫描README 仅剩"不再依赖 Streamlit/从 Streamlit 迁移"正确表述。git 工作区干净(仅 .workbuddy/ 记忆)。
## 服务状态页「视频分割」状态卡commit cbcc5d0已部署
- 用户需求:服务状态界面要能看到视频分割状态(前端口误选方向后澄清)。
- Oracleoracle_db.get_segment_status()motion_event_id 片段 total/done/pending/failed + ss_motion_events 事件数 +
最近 segment 活动api_gateway /api/oracle/activity 加 segment 字段 + clips_filesmotion_clips 目录文件数)。
- NAS/api/ui/service-status 代理自动透传 segment前端零后端改动
- 前端ServiceStatus.vue 新增「视频分割」ServiceCard运动片段 done/total、文件数、待处理/失败、运动事件数、最近分割活动)。
- 验证Oracle activity.segment 返回 {total:75, done:54, pending:21, failed:0, clips_files:75, motion_events:99}
NAS 代理 service-status 透传正常;前端 dist 已构建部署。
## 事件-分割一致性保障 + 历史事件回灌commit 177c2a8
- **用户质疑**Oracle 的 ss_motion_events 是否=SS 全部事件?为何没有 8/22 之前的?
- **实测**SS 事件中心保留 8/15 起全部运动事件(按天 191/47/256/419/334/256/312/196 ≈ 2000 条);
Oracle 只有 8/22 起的——NAS 游标初始化故意"不回灌历史",属设计缺陷。
- **修复 1一致性缺口**SS 事件在动作进行中 duration=0NAS 首次推送后游标前进不再重推 →
Oracle duration 永久 0 → 分割过滤跳过 → 事件丢失(此前被"重启重推"掩盖。MotionNotifier 加
`_zero_dur_ids`:推送过 duration<=0 的事件记入,下轮窗口回查已结束(duration>0)补推覆盖。
- **修复 2历史回灌**NAS 一次性脚本拉取 8/15 00:00~8/22 00:00 的 SS 运动事件(分 6h 段)→
POST /api/ss/motion 幂等落库,**1815 条全部入库**与按天统计吻合Oracle ss_motion_events 达
1947 条min event_id=1重置 304 个 8/15-21 已 done 素材为 pending → 重启 Oracle 重新分割,
历史运动片段持续生成60s 内已 404 片段)。
- **一致性保障清单**①事件全覆盖SS 全量→Oracle 幂等回灌兜底②duration 真实0→补推
③游标续用/失败批次不前进(重启补推、不丢);④分割按 event_id 幂等motion_event_id 关联);
⑤start_time 对齐SS epoch ↔ 素材文件名解析整秒±0.5s 素材毫秒偏差可接受)。
- **注意**:历史 1800+ 片段分析需大量 Gemini 配额max_concurrent=1 + 429 重试),会排队跑很久。
## 事件-片段一致性对账commit 1dd9df3已部署
- 用户重复追问"如何保证 SS 事件与分割视频一致(含 start/duration"→ 补可验证的对账机制。
- oracle_db.get_segment_consistency()event_totalSS 全量 1947/ finished已结束可分割 1922/
segmented已分割去重 1793/ gap_count缺口 129动态收敛中/ gaps 明细event_id/start_time/duration/
material_range素材覆盖 8/15 10:31~8/22 12:39
- api_gateway activity.segment.consistency前端服务状态页分割卡展示"一致性:事件/已结束/已分割/缺口"。
- **缺口解读**129 缺口主要是素材仍在队列处理(今天 13:0x 事件 + 少量历史 1222-1226随分割收敛
真正无法分割的(素材窗口外/素材失败)会持续显示,可据此排查。
- 一致性三层数值SS 原样+DO UPDATE+duration=0 补推)/数量motion_event_id 幂等 1:1+对账缺口)/
文件(-ss offset -t dur±0.5s 毫秒取整、±1-2s 关键帧对齐)。
## 人物管理数据彻底清除(用户要求,无代码改动)
- 用户:"人物管理全是错的(历史数据原因),人物相关数据都删除,相关的都做下删除"。
- **删除范围**Oracle people 表(9)、videos.people_json(465)、events.person_list_json(683)/person_appearances_json(640)
NAS sync_people(7)、sync_videos.people_json(397)、sync_events 两字段(424/416)。备份 oracle.db.bak.20260822_055139。
保留 videos(2124)/events(683)/ss_motion_events(1947)/chat_history。
- **关键发现**:重启 Oracle 后 producer 会重新分析 **1646 个历史(8/15-21) pending 片段**并立即产生新人物
(爷爷/人物A/人物B…删了还会长 → 必须同时停掉历史片段分析:把 event_start_time < '2026-08-22 00:00:00'
的 pending 片段标记 **skipped_reset**(不再分析、不产生人物),只保留 8/22 当天新运动片段分析提取人物。
- 重启后 Oracle 正以 known_members=无 分析今日新片段 motion_2557913:10 事件)→ 人物管理从今日数据重新提取
(当前仅 1 个未命名"人物A")。验证:/api/ui/people=1 group、named-members=空、视频详情 people_json=None。
## 外网访问 8000 + 登录校验commits 9c51c65/aca1a67已部署
- 需求NAS :8000 参考 3000 端口Gitea通过外网访问且先登录才能访问账号 ericwyuan / 密码 iLoveJava5
- **外网机制(已查明)**NAS 跑 frpc/etc/frp/frpc.tomlS99frpc.sh 守护)→ 甲骨文 129.146.203.203:7000frps
已有 gitea(外3000→NAS3000)、wordpress(外8500→NAS8088)。新增 fam-core proxy外网 **8000→NAS 8000**
S99frpc.sh restart 生效proxy added: [fam-core gitea wordpress])。甲骨文安全组 8000 已放行(实测 200
- **登录校验(新模块 auth.py**POST /api/login凭据 FAM_AUTH_USER/FAM_AUTH_PASSNAS .env 默认 ericwyuan/iLoveJava5
/api/logout、/api/auth/check、GET /login 内置深色登录页SPA 零改动init_auth(app) 全局 before_request
页面未登录 302 /login、/api/* 未登录 401白名单免登录/login /api/login /api/logout /api/auth/check /health
/api/ss/webhook /assets/* /favicon.ico。登录态=进程内 token+HttpOnly cookie7 天),重启 fam-core 需重新登录。
- **验证(真机)**:内网+外网全链路:未登录 302→/login、登录页 200、API 401、错误密码 401、正确登录 200+cookie、
带 cookie 访问 /api/ui/videos 20015 条运动片段)、/api/auth/check authed true/false、SS webhook 免登录 200。
外网 http://129.146.203.203:8000 完整流程通过。测试注入的假 webhook 事件已清理ss_motion_events 1947
- **注意**SS Webhook/api/ss/webhook是唯一免登录入站端点SS 无法带登录态),外网暴露后有被滥发假事件的
风险,如介意可后续给 webhook 加独立 token 或 frp secretKeyHTTP 明文传输HTTPS 需另配。
## 恢复全部历史片段分析(用户决策,无代码改动)
- 用户:"所有分割后没有处理的运动视频都需要处理" → 把之前删人物数据时标记的 **1646 个 skipped_reset 历史片段
8/15-21全部改回 pending**(清空分析结果字段,文件齐全 0 缺失)。
- 机制video_queue.start() 的 `_enqueue_existing()`get_pending_videos limit=10000重启时把 pending 全部补入队;
片段走 process_video 片段分支直接分析motion_event_id 非空local_path→motion_clips
- 结果systemctl restart fam-edge → 日志"启动恢复入队 1646 个待处理视频"queued=1645 + current=1motion_25148
8/21 09:33 事件),从 8/21 往 8/15 顺序处理。单并发 + Gemini 429 重试,预计跑数小时~十数小时。