Files
yovision/docs/raw/03-通用场景应用方案.md
QiuSW e0b0bfafeb
Harness governance / validate (push) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
docs(design): close Sense second-review gaps
2026-08-04 11:08:41 +08:00

1259 lines
68 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.
# 🏗️ YoVision 智能视频事件平台 — 通用场景应用方案
> 版本 v0.4 · 2026-08-03
> 上游约束:《02-需求分析》§1 的 C1–C9
> 本文档描述**如何用一套底座支撑多个场景**,以及每个场景怎么落到配置上
---
# 1. 方案总纲
## 1.1 一句话
**一套通用底座(接入 → 流水线 → 能力 → 编排),场景差异全部收敛到「场景包」这一层配置里。**
新增一个场景 = 写一个场景包(规则模板 + 升级策略 + 报表口径 + 话术),**不改核心代码**。这是《02》M5 的验收标准。
## 1.2 分层落地
```mermaid
flowchart TD
subgraph L5["L5 场景包(配置)"]
SP1["居家养老包"]:::pack
SP2["校园包"]:::pack
SP3["社区包"]:::pack
end
subgraph L4["L4 业务编排(Go)"]
RE["规则引擎"] --> EV["事件生成器<br/>回捞 · 抓拍 · 去重"]
EV --> AL["预警状态机<br/>ack · 升级 · 回执"]
AL --> DL["投递适配器<br/>推送/短信/语音/Webhook"]
end
subgraph L3["L3 能力层(模型)"]
DET["检测+跟踪<br/>YOLO"] --> POSE["姿态<br/>YOLO-pose"]
DET --> ATTR["属性"]
DET --> REID["ReID 匿名同一性<br/>默认手段"]
DET -.旁路.-> FACE["人脸比对<br/>租户授权后"]
POSE --> TEMP["时序行为分析"]
end
subgraph L2["L2 流水线(Savant / 等价框架)"]
TRIG["触发器<br/>运动/ONVIF事件/传感器"] --> PIPE["解码·抽帧·推理调度"]
BUF["磁盘缓冲"] --> PIPE
end
subgraph L1["L1 接入(Go + mediamtx)"]
DEV["设备台账"] --> RECON["对账器"]
RECON --> MTX["媒体路由"]
TUN["WireGuard 隧道"] --> DEV
end
subgraph L0["L0 基座"]
MT["多租户 · RBAC"]
ST["对象存储"]
OBS["Prometheus + Grafana + 审计"]
end
L5 -.驱动.-> L4
L4 --> L3 --> L2 --> L1 --> L0
classDef pack fill:#e8f0fe,stroke:#4285f4
```
## 1.3 组件选型
| 层 | 组件 | 许可 | 角色 | 备注 |
| --- | --- | --- | --- | --- |
| L1 媒体路由 | **mediamtx** (bluenviron) | MIT | 协议互转、按需拉流、录制、鉴权回调、Prometheus | Go 单二进制 |
| L1 RTSP 库 | **gortsplib** | MIT | 自研 worker 拉流 + 帧级丢弃 | 同作者 |
| L1 ONVIF | **IOTechSystems/onvif** 或 use-go/onvif | Apache-2.0 | 设备控制 | 前者是 EdgeX device-onvif-camera 内部使用的增强版 |
| L1 API 客户端 | 自行用 **oapi-codegen** 从 mediamtx OpenAPI 生成 | — | 控制 mediamtx | ⚠️ 不依赖第三方 SDK 的更新节奏 |
| L1 边缘推流 | mediamtx 或 **go2rtc** | MIT | 边缘盒子 | 杂牌摄像头兼容性差时用 go2rtc 兜底 |
| L1 隧道 | **WireGuard** | GPL-2.0(内核态,不链接) | 控制面通道 | |
| L2 流水线 | **Savant** (insight-platform) | Apache-2.0 | 多路视频分析框架 | 基于 NVIDIA DeepStream;备选见 §6.2 |
| L3 检测 | **YOLO**(Ultralytics v8/v11) | AGPL-3.0 → **购 Enterprise License** | 目标检测(人、车、电动车、堆物多类) | 已决策沿用;授权采购见 §2.4 |
| L3 姿态 | **YOLO-pose**(COCO 17 点) | 同上 | 关键点 | 备选 RTMPose/RTMO(Apache-2.0,同为 COCO 17 点,可直接替换) |
| L3 跟踪 | ByteTrack / BoT-SORT | MIT | 多目标跟踪 | |
| L3 ReID | **ReID 模型(匿名同一性)** | 视模型,选 Apache/MIT 系 | 跨镜同一性,**默认手段** | 产出 `anon_id`,不涉及身份 |
| L3 人脸 | 人脸检测 + 特征提取(InsightFace 系等) | ⚠️ 权重许可需逐个核对 | 身份比对,**按租户授权开启** | 见 §2.8.3 |
| L3 向量检索 | pgvector 或 Milvus | 开源 | 底库比对 | 底库量小用 pgvector 即可 |
| L4 全部 | **自研 Go** | — | 规则、事件、预警、投递 | 业务定制度高,通用工具都要大改 |
| L4 投递 | Novu / Apprise / ntfy 或直连云厂商 | — | 只解决"怎么发" | 短信/语音**双供应商** |
| L0 数据库 | PostgreSQL | — | 唯一真相源 | |
| L0 存储 | MinIO / S3 兼容 | — | 证据文件 | |
| L0 可观测 | Prometheus + Grafana + Jaeger | — | 指标/看板/链路 | mediamtx 与 Savant 都出 Prometheus 指标,一个 Grafana 全收 |
| L0 运维告警 | Alertmanager | — | **仅运维**,不碰业务预警 | |
### 明确不用的东西
| 组件 | 不用的理由 |
| --- | --- |
| **Grafana OnCall (OSS)** | 已于 2026-03-24 归档,仓库只读仅修 CVSS 7.0+;**移动推送、短信、电话在归档当日停止工作**(依赖 Grafana Cloud 中转)。送不出去的告警系统等于没有 |
| **完整 MiBeeNvr** | 可作实验室兼容性测试台与选择性源码参考,**不整仓进入生产**。它已支持 RTMP/SRT push-in、远程访问等跨网络能力,但其单体媒体、录像、SQLite、设备管理和 UI 与本项目的 MediaMTX/Sense/Bell 边界重叠;未见公开的 128 路多分片验收数据,也没有本项目需要的多租户真相源与配额模型 |
| **EdgeX Foundry(本期)** | 白送的能力约值一两周开发量,代价是长期多背 4 个服务;且它假设设备在同一局域网,与 NAT 场景冲突。接入雷达/门磁/Modbus 设备时再评估切换(切换成本低:ONVIF 部分两边几乎一样,对账器不用改) |
## 1.4 NVR 开源项目选型结论(已定,2026-08-03)
本项目不 fork 一个完整 NVR 作为生产系统。当前默认由客户已有 NVR 承担常态录像,YoVision 负责统一接入、AI 分析和事件证据,因此选型拆成三类:**MediaMTX 是生产媒体数据面,MiBeeNvr 是 Sense 的主源码参考,Frigate 是事件录像与回看交互参考**。
| 项目 | 许可 / 技术栈 | 与本项目的匹配 | 决策 |
| --- | --- | --- | --- |
| [MediaMTX](https://github.com/bluenviron/mediamtx) | MIT / Go | 协议路由、按需拉流、fMP4/MPEG-TS 录像、按时间回放、HTTP/JWT 鉴权、Control API、Prometheus;官方提供横向扩展示例 | **直接部署为生产依赖,不复制源码**;Sense 通过自行生成的 API 客户端驱动 |
| [MiBeeNvr](https://github.com/Mi-Bee-Studio/MiBeeNvr) | MIT / Go + Svelte | ONVIF、设备发现、按序列号重新定位 IP、健康监控、退避重连、REST API 与现代 UI 最接近 Sense;但完整系统与 MediaMTX/Bell 重叠,内部以单机 NVR 为中心 | **首选源码参考,按白名单选择性重写/移植,不整仓 fork,不加入 go.mod** |
| [Frigate](https://github.com/blakeblackshear/frigate) | MIT / Python + FFmpeg + go2rtc | 主/子码流角色、低成本运动过滤、pre/post-capture、事件保留策略和 Review/History 时间轴成熟;自带推理进程会与 Brain/Savant 重复 | **只借鉴事件录像语义和 UI 工作流,不引入其推理与进程架构** |
| [go2rtc](https://github.com/AlexxIT/go2rtc) | MIT / Go | 特殊流格式、WebRTC/MSE、双向语音与 RTSP restream 兼容性强 | **仅作为边缘兼容插件**;不成为中心真相源,管理页面默认不暴露 |
| [Moonfire NVR](https://github.com/scottlamb/moonfire-nvr) | GPL-3.0-or-later / Rust | 不解码、不转码的低开销录像索引和任意时间段 MP4 导出值得研究;仍为 pre-1.0 | **只看设计,不复制代码**,避免 GPL 传染和存储格式兼容风险 |
| [ZoneMinder](https://github.com/ZoneMinder/zoneminder) | GPL-2.0 / C++、Perl、PHP | 传统集成式 VMS,功能完整但技术栈、部署模型和系统边界与 Sense/Brain/Bell 差异大 | **不作为实现基线** |
| [Shinobi](https://gitlab.com/Shinobi-Systems/Shinobi) | 许可证按采用版本单独核验 / Node.js | 同属完整 VMS,主仓已迁至 GitLab;引入后仍需拆除其媒体、用户和事件体系 | **不作为实现基线** |
核验资料:MediaMTX 官方[录像](https://mediamtx.org/docs/features/record)、[回放](https://mediamtx.org/docs/features/playback)、[横向扩展](https://mediamtx.org/docs/features/scalability)与 [Control API](https://mediamtx.org/docs/features/control-api);Frigate 官方[视频流水线](https://docs.frigate.video/frigate/video_pipeline/)与[录像保留策略](https://docs.frigate.video/configuration/record/)。MiBeeNvr 源码分析基线为本地 `_reference/mibeenvr` 的提交 `b8e7dd426d73d3b1910254c196d0937db9517a0f`(2026-08-01);实施前须重新核对上游版本、许可证与安全公告。
### MiBeeNvr 是否包含 MediaMTX 的功能
**部分包含,但不能等价替代。** MiBeeNvr 覆盖了单机 NVR 常用的摄像头接入、直播和录像功能;MediaMTX 的核心价值则是作为通用媒体路由器,为多个发布者/读者提供稳定 path、细粒度鉴权、会话控制和横向扩展。两者的功能交集很大,架构角色不同。
| 能力 | MiBeeNvr(当前分析基线) | MediaMTX | 对 YoVision 的影响 |
| --- | --- | --- | --- |
| ONVIF、设备发现、PTZ、序列号/IP 自愈 | **有,且更接近设备管理产品** | 不负责设备实体 | 借鉴 MiBeeNvr,落入 Sense |
| HLS/LL-HLS、WebRTC、浏览器直播 | 有,另含 HTTP-FLV/WebSocket | 有 HLS/WebRTC,侧重协议路由 | 初期界面演示可直接用 MiBeeNvr |
| RTMP/SRT 接入与 RTMP/RTSP 转推 | 有 | 有,且可在多种协议间发布、读取和代理 | 都能覆盖常见接入,但不能仅按协议名称判断可替换 |
| 通用 RTSP path 供 Brain、播放器、录像器共同读取 | 当前只确认摄像头 RTSP 输入与向外部目标转推,**未发现等价的通用 RTSP 读服务** | 核心能力:publish/read/proxy,多读者共享 | 含 Brain 的端到端版本应从 M1 引入 MediaMTX,否则 Brain 需要重复直连摄像头或再补一个媒体服务器 |
| 分段录像、保留、列表、时间轴与回放 | 有,MP4/SQLite/文件系统一体化 | 有 fMP4/MPEG-TS 录像与按时间回放,业务索引由上层负责 | 演示可用 MiBeeNvr;生产事件证据仍按 Sense/Bell 边界实现 |
| 鉴权 | 本地 BasicAuth、会话 token、API key | internal/HTTP/JWT,可按 path 与 publish/read/playback/api 等 action 控制 | MiBeeNvr 鉴权不能替代多租户 RBAC 与媒体动作授权 |
| 管理 API | 面向摄像头、录像、设置和 UI 的 NVR REST API | 面向 path、配置、发布者/读者和协议会话的 Control API,可查询/踢会话 | M1 对账器依赖 MediaMTX 的控制语义 |
| 横向分片与故障域 | 未见公开的多实例编排和 128 路分片验收 | 官方提供 origin/read-replica 与负载均衡方案 | 64/128 路不能以单体 MiBeeNvr 为扩展基线 |
以上“未发现”是基于指定提交的源码与文档审查,不代表上游永久不支持;升级前必须重新核验。
### 前期能否只用 MiBeeNvr
| 阶段/用途 | 是否允许只用 MiBeeNvr | 边界 |
| --- | --- | --- |
| **M0 实验室兼容性与 UI 演示**(3–5 款摄像头) | **可以,且优先直接运行而不是复制源码** | 只接自购测试设备;允许随 M0 结束丢弃,不形成生产数据 |
| 一次性内部 Alpha(最多 16 路) | 可以,但必须标记为 **disposable prototype** | 单站点、单用户、无生产 SLA;不承诺原地升级到 128 路 |
| **M1 首个可持续演进的生产骨架** | **不可以** | MediaMTX 必须进入;Sense 建立自有设备 ID、台账、API 客户端与对账器,Brain 以后只依赖稳定媒体地址 |
| M2/M3 默认 16 路真实试点 | 不可以 | MiBeeNvr 只保留为实验室测试台/源码参考;真实租户、配额、审计与事件契约不得落入其单机模型 |
| M4/M5 64/128 路 | 不可以 | 必须使用媒体/推理分片和故障隔离 |
> **决策规则**:如果“初版”指可丢弃的验证版,可以只用 MiBeeNvr;如果“初版”指以后原地升级的第一个正式版本,MediaMTX 必须从 M1 进入。若确需修改 MiBeeNvr,应保留为独立 fork/目录及上游 Git 历史,不把整仓源码粘贴进 Sense。
### MiBeeNvr 验证版安全红线
当前分析提交在 `internal/api/handler.go` 的 `registerAnonymousRoutes` 中将录像 `download`、`merged` 回放端点注册为匿名且不受该文件中的公共限流组保护。这对隔离实验室演示可以接受,**对居家老人、学校和工厂真实视频不可接受**。
验证版必须满足:
1. 仅监听隔离测试网或 localhost;若需远程访问,必须置于带认证的反向代理/VPN 后。
2. 不接入真实住户、学生、员工视频;不得用匿名录像 URL 作为生产分享机制。
3. 关闭不需要的 WebDAV、FTP、浏览器 AI、自动更新和公网管理端口,减少攻击面。
4. 若进入任何真实试点,先把录像下载/回放纳入 Bell 鉴权与审计,或直接切换到 M1 的 MediaMTX + Sense 生产骨架。
### MiBeeNvr 借鉴白名单
| 可借鉴内容 | 落入本项目 | 处理方式 |
| --- | --- | --- |
| `internal/onvif/` 的发现、WS-Security、Profile 选择、设备时间处理 | `Sense/internal/onvif/` | 按本项目接口重写;只保留必要协议能力和测试向量 |
| `internal/camera/rediscovery*` 的序列号识别与 IP 自愈 | `Sense/internal/device/`、`probe/` | 身份仍以序列号为准;适配 Postgres 设备台账和对账状态机 |
| `internal/health/`、退避与冻结检测 | `Sense/internal/probe/` | 统一成指标和状态事件,不直接驱动录像/告警业务 |
| 摄像头表单、直播卡片、录像列表的交互模式 | `Bell/web/` | 只借鉴交互;按多租户 RBAC、分页和 128 路虚拟列表重做 |
| ONVIF、录像异常与存储故障测试用例的组织方式 | Sense 测试目录 | 复用测试思想和公开协议样例;不得携带真实设备密码/视频 |
若逐行复制 MIT 源码,必须保留原版权和许可证声明,并在第三方清单记录文件、提交号和修改范围。优先采用“读懂后按 Sense 接口重写”,避免把 MiBeeNvr 的 `config/model/recorder/storage/event` 耦合链一起带入。
### 明确禁止复制的部分
- MiBeeNvr 的 `StreamHub`、HLS/WebRTC/RTMP/SRT 服务和录像主链:与 MediaMTX 重复。
- MiBeeNvr 的 SQLite 真相源、单机 `CameraManager` 生命周期:与 Postgres、多租户、16→128 路分片冲突。
- MiBeeNvr 的浏览器 AI 和 Frigate 的检测进程:AI 统一归 Brain/Savant。
- 任一 NVR 的 BasicAuth/本地用户模型:身份、租户、RBAC 与审计统一归 Bell。
- Frigate/Home Assistant/MQTT 的业务对象模型:只吸收事件录像与时间轴语义,不进入平台核心契约。
### 进入生产的验收门槛
1. 依赖固定版本与校验值,生成 SBOM 和第三方许可证清单;上游升级必须先过回归测试。
2. M0 完成 3–5 款摄像头的 ONVIF/RTSP 兼容测试;M3 建立 16 路基线,M4/M5 分别完成 64/128 路分片验收。
3. 分别测接入/录像带宽、同时解码、AI 推理和证据存储,不以“NVR 能录多少路”替代 AI 容量结论。
4. MediaMTX、go2rtc 的管理 API/UI 默认只监听内网或 localhost,经 Bell/Sense 授权代理后才可访问。
---
# 2. 通用能力底座
## 2.1 接入层:控制面/数据面分离
这是解决 NAT 的关键,也是三个场景可以用同一套接入的原因。
```mermaid
flowchart LR
subgraph SITE["站点(家庭/学校/小区)"]
CAM["IP 摄像头"]
SENSOR["传感器<br/>雷达/门磁/按钮"]
EDGE["边缘节点<br/>推流 + 本地缓存 + WG"]
CAM --> EDGE
SENSOR --> EDGE
end
subgraph CENTER["中心平台"]
MTX["mediamtx 集群<br/>(分片)"]
CTRL["控制面 Go 服务<br/>台账 · 对账 · 鉴权回调"]
PIPE["推理流水线"]
BIZ["业务编排"]
DB[("PostgreSQL")]
OBJ[("对象存储")]
end
EDGE -->|"数据面:主动推流<br/>RTSP announce / SRT"| MTX
CTRL -.->|"控制面:WireGuard 隧道<br/>ONVIF 配置与探活"| EDGE
MTX -->|鉴权回调| CTRL
MTX --> PIPE --> BIZ
CTRL --> DB
BIZ --> DB
BIZ --> OBJ
BIZ --> APP["App / 短信 / 语音 / Webhook / 大屏"]
```
| 平面 | 方向 | 通道 | 特点 |
| --- | --- | --- | --- |
| 控制面 | 中心 → 设备 | WireGuard 隧道 | 流量极小,仅配置与探活时产生 |
| 数据面 | 边缘 → 中心 | 主动推流 | 不走隧道,避免隧道成为带宽瓶颈 |
**控制面失联不影响视频推流**——这条解耦让隧道故障从 P0 降为 P2。
### IP 规划
每站点分配不冲突的内网段,设备地址确定化:
```
10.<站点高位>.<站点低位>.0/24
摄像头固定从 .10 起分配,传感器从 .100 起
```
设备地址可预测,不依赖自动发现,便于对账。**校园/社区场景若已有既定网段,在 `sites.subnet` 里登记实际值即可,逻辑不变。**
### mediamtx 配置基线
```yaml
api: yes
apiAddress: :9997
metrics: yes
metricsAddress: :9998
authMethod: http
authHTTPAddress: http://control-plane:8080/mtx-auth
pathDefaults:
sourceOnDemand: yes # 有 reader 才拉流
sourceOnDemandStartTimeout: 10s
sourceOnDemandCloseAfter: 30s
rtspTransport: tcp # 家庭/无线网络下 UDP 丢包严重
record: no # 按场景包覆盖
recordDeleteAfter: 7d
runOnOnline: curl -s -X POST http://control-plane:8080/hooks/online?path=$MTX_PATH
runOnOffline: curl -s -X POST http://control-plane:8080/hooks/offline?path=$MTX_PATH
paths:
"~^t(\\d+)-s(\\d+)-d(\\d+)$": # tenant-site-device,一条正则兜底
source: publisher
```
> 用一条正则 path 兜底,**不要写 N 条静态配置**。
>
> ⚠️ v1.19.x 的钩子命名为 `runOnAvailable` / `runOnUnavailable` / `runOnOnline` / `runOnOffline`,与旧版 `runOnReady` / `runOnNotReady` 不同。部署前核对所用版本的配置参考。
### 必须遵守的接入约定(血泪清单)
| 约定 | 原因 |
| --- | --- |
| 设备身份用序列号,不用 IP | DHCP / Wi-Fi 漫游会换 IP |
| 建 path 前先查是否存在 | 盲目 add 会踢掉正在推流的 publisher |
| 改配置前比对哈希,没变绝不调 API | 任何 path 配置变更都会触发 reload,断开正在推流的 source,**即使改动与它无关** |
| 重试用指数退避 | 无事务,只能靠幂等重试 |
| 并发限流(每系统信号量 8) | 避免打爆 API;每系统独立预算,不共享 |
| 设备状态含 `pending_activation` | 批量开通必然有凭据待补的中间态 |
| API 改的配置不落盘 | mediamtx 重启后 API 添加的 path 全部消失,服务启动时必须从 DB replay |
| 探活用独立 ONVIF 调用 | 「设备离线」与「无人订阅」在 mediamtx 侧表现相同 |
| 开通时 `SetSystemDateAndTime`,定期校时 | 时钟漂移会让事件时间戳全乱,审计失效 |
### 停用的四种粒度(副作用从轻到重)
| 需求 | 做法 | 效果 |
| --- | --- | --- |
| 暂停分析,流保留 | **卸载推理 source** | 配合 sourceOnDemand 自动停止拉流,随时恢复 |
| 立即断开,允许重连 | `POST /v3/rtspsessions/kick/{id}` | 断一次,客户端会重连 |
| 永久拒绝 | 鉴权回调返回 401 + kick | 推不上来,无需改 mediamtx 配置 |
| 彻底移除 | `DELETE /v3/config/paths/delete/{name}` | path 消失,重启后靠自有库恢复 |
标准组合:**先 kick 断开、再让鉴权拒绝** = 立刻生效且不会重连。
> ⚠️ **关键耦合**:`sourceOnDemand: yes` 的语义是「有 reader 才拉流」,而**推理 source 挂上去就是一个 reader**。因此「暂停分析」必须通过**卸载 source** 实现,不能只在推理插件里 `return`——后者会让 mediamtx 持续拉流、带宽白烧,而监控上看起来一切正常。
## 2.2 对账器:多系统一致性
数据库是唯一真相源,mediamtx / 推理流水线 / 存储都是被驱动的从属状态。
**水平触发(level-triggered)而非边沿触发**:对账器不处理"事件",只不断比对期望状态与实际状态并向期望收敛。这是处理部分失败的唯一可行方案。
```go
func (r *Reconciler) Sync(ctx context.Context) error {
desired := r.db.EnabledBindings() // 真相源
if len(desired) == 0 && r.lastDesiredCount > 0 {
return ErrSuspiciousEmptyDesired // 安全闸:期望集合意外为空
}
mtxActual, err1 := r.mtx.ListPaths(ctx) // 分片内所有实例
infActual, err2 := r.inference.ListSources(ctx)
if err1 != nil || err2 != nil {
return errors.Join(err1, err2) // 拿不到实际状态就不动作
}
plan := buildPlan(desired, mtxActual, infActual)
// plan 内部已按顺序约束排序:
// setup: mtx 先 → inference 后 (流必须先存在)
// teardown: inference 先 → mtx 后 (先停消费再断流)
// ⚠️ 建立与拆除是逆序的,很容易被后来人改错
for _, step := range plan {
if !step.DueForRetry(now) { continue } // 退避窗口未到
if step.DesiredHash == step.AppliedHash { continue } // 没变,绝不调 API
sem[step.System].Acquire(ctx, 1) // 每系统独立限流
go func() {
defer sem[step.System].Release(1)
if err := step.Apply(ctx); err != nil {
r.db.MarkFailed(step, err) // 退避,不回滚
return
}
r.db.MarkApplied(step) // applied_hash = desired_hash
}()
}
return nil
}
```
### 部分失败:不回滚,只收敛
典型场景:mediamtx path 建好了,推理 source 挂载失败。
- ❌ **错误做法**:回滚删掉 path。回滚本身也会失败,那时你需要回滚的回滚,无限递归。
- ✅ **正确做法**:保留已成功的部分,把失败那行 `sync_state` 标为未收敛 + 退避重试。
代价是会有一个无人消费的 path 短暂存在——但因为 `sourceOnDemand`,没有 reader 就不拉流,**成本接近零**。这是 sourceOnDemand 给多系统对账带来的额外好处:**中间态是安全的**。
### 重试预算与安全闸
| 参数 | 值 |
| --- | --- |
| 初始退避 | 5s |
| 退避倍率 | 2×,上限 5min |
| 告警阈值 | `attempt > 5` 且持续 > 15min → 告警 |
| 放弃阈值 | **不放弃**。持续重试,靠告警引入人工 |
| 循环周期 | 正常 30s;有未收敛项降到 5s;每 10min 全量对账(含孤儿扫描) |
| 孤儿清理安全闸 | 单轮删除量 > 总量 10% 时**停止并告警**,不执行 |
> 不设放弃阈值是故意的:对账器放弃了,设备就静默地不工作了,这在监护场景是不可接受的失败模式。
>
> 安全闸防的是数据库连接异常或查询 bug 导致的"期望集合意外为空"——那会把全部路数拆掉。
### 孤儿资源三类
| 类型 | 成因 | 处理 |
| --- | --- | --- |
| mediamtx 有 path,DB 无 | 人工手改、旧版本残留 | 删除(先记日志,不静默) |
| 推理有 source,DB 无 | 同上 | 卸载 |
| DB 有 binding,两边都无且长期未收敛 | 开通失败后无人处理 | 告警,**不自动删 DB** |
## 2.3 流水线层:故障隔离与触发式推理
### 为什么用框架而非完全自研
开源社区没有成品的「批量摄像头行为检测」(算法是差异化,不会有人开源),但有成熟的**多路视频分析流水线框架**。**算法是自己的,工程化用现成的。**
选定 **Savant**,三个决定性理由:
1. **故障隔离** —— 原生 DeepStream 中数据流与 pipeline 紧耦合,任一源崩溃则整个 pipeline 崩溃,重启加载模型要数秒且期间丢数据。Savant 的 adapter 通过 ZeroMQ 与框架通信,**adapter 挂掉不影响 pipeline**。家庭网络掉线是常态,这是规模化的生死线。
2. **可配置降级** —— 实时模式(算力不足时主动丢帧)或高吞吐模式(保证处理全部数据)。**本系统选实时模式,丢帧优于积压崩溃。**
3. **Replay(按需与非线性处理)** —— REST 控制的录制/回放/转推服务,直接对应触发式二级确认与 pre-roll 回捞。
同一套代码可跑在 Jetson 和数据中心 GPU 上,与边缘/中心混合部署契合。
**代价**:必须 NVIDIA 硬件,与 DeepStream 版本强绑定,学习曲线陡,无企业支持。
### Pipeline 架构
```
边缘推流 → mediamtx(汇聚,按需拉流)
↓ RTSP
RTSP source adapter(多路复用 + 故障隔离)
↓ ZeroMQ / Protobuf
Buffer Adapter(磁盘持久化缓冲,抗突发) ← 必须开启
↓
推理 module
├─ 检测 + 跟踪(YOLO + ByteTrack)
├─ 姿态(YOLO-pose,COCO 17 点)
├─ 属性 / ReID
├─ 人脸旁路(授权租户,异步不阻塞)
└─ Python 插件:时序窗口 + 行为判定(**自研部分**)
↓ ZeroMQ / Protobuf
Go 业务编排(规则引擎 → 事件 → 预警)
```
**需要自己写的只有中间那个 Python 插件,前后两段是现成的。**
### 关键配置决策
| 决策 | 说明 |
| --- | --- |
| **必须开启 Buffer Adapter** | adapter 与 module 之间的持久化事务性磁盘缓冲,抗流量突发,条目数与大小导出到 Prometheus。本场景天然有突发(夜里安静、早上集中活动、上下学时段)。**一开始就加上,别等出问题再补** |
| **AO-RTSP Sink 仅用于调试** | ⚠️ GeForce 系列显卡**同时硬件编码流数上限为 3**。给多路挂可视化回推会直接卡在 3 路,需 Quadro/Tesla/L4 类专业卡。**不要做成常态功能** |
| **开发期开 Development Server** | 改代码动态重载不重启 pipeline,可从 IDE 远程开发跑在 Jetson/服务器上的代码。原生 DeepStream 改一行要重启等几十秒 |
| **实时模式** | 算力不足时主动丢帧,不积压 |
### 与 Go 控制面的对接契约
| 需求 | 通道 | 语言无关 |
| --- | --- | --- |
| 接收检测结果 | 订阅 sink 的 ZeroMQ 端点,解 protobuf | ✅ |
| 动态增删流 | 动态挂载/卸载 source 与 sink,无需重载 pipeline | ✅ |
| 回放/回捞(pre-roll) | Replay 服务的 REST 接口 | ✅ |
| 程序化灌数据/调试 | Client SDK(**仅 Python**) | ❌ |
> **设计约束:优先走 ZeroMQ 和 REST 两条语言无关通道,Go 服务不得依赖 Python 中间层。** Client SDK 仅用于算法调试和测试。
>
> 因为两边都是 API 驱动,**对账器可以同时驱动 mediamtx 和推理流水线**:开通 = 建 path + 挂 source;停用 = kick + 卸 source。逻辑写在同一个 reconciler 里。
### 触发式推理(成本的数量级优化)
常态全量推理不可持续。三种触发源,按场景选:
```mermaid
flowchart LR
T1["运动侦测<br/>(软件,零成本)"] --> GATE{"触发门"}
T2["ONVIF 事件订阅<br/>(摄像头侧)"] --> GATE
T3["传感器<br/>雷达/门磁/按钮"] --> GATE
GATE -->|唤醒| INF["推理流水线<br/>+ Replay 回捞前 N 秒"]
GATE -->|静默| IDLE["不解码 不推理"]
```
| 触发源 | 成本 | 可靠性 | 推荐场景 |
| --- | --- | --- | --- |
| 运动侦测(参考 Frigate 思路) | 零 | 中(光照/树叶会误触) | 通用兜底、无雷达时的过渡方案 |
| ONVIF 事件订阅 | 零 | 中 | 支持的型号,节省中心解码 |
| 毫米波雷达 | 硬件成本 | 高 | 家庭卧室/卫生间(**同时解决隐私**) |
| 常态全量 | 高 | 最高 | 仅高价值点位(校园危险区域、社区出入口) |
> **重要澄清**:Savant 这类框架主要解决多路调度、复用与故障隔离,不会自动降低单帧推理成本。触发式推理可以显著降低平均占空比,但实际容量必须按模型、分辨率、FPS、batch、解码方式和硬件实测。
### 架构预留:触发入口独立
```go
// 所有触发源汇入同一个入口,接新传感器时不改动流控制逻辑
type TriggerSource interface {
Name() string
Subscribe(ctx context.Context) (<-chan TriggerEvent, error)
}
type TriggerEvent struct {
DeviceID int64
SiteID int64
Kind string // motion | onvif_event | radar | door | button
At time.Time
PreRoll time.Duration // 需要回捞多久
Payload json.RawMessage
}
```
## 2.4 能力层:检测什么
### 能力清单与场景映射
| 能力 | 模型/方法 | 输出 | S1 | S2 | S3 |
| --- | --- | --- | :-: | :-: | :-: |
| 人体检测 + 跟踪 | **YOLO** + ByteTrack | bbox, track_id | ✅ | ✅ | ✅ |
| 姿态关键点 | **YOLO-pose** (COCO 17) | 17 点坐标 + 置信度 | ✅ | ✅ | ✅ |
| 跌倒/倒地 | 姿态 + 时序窗口 | 7 分类动作 | ✅ | ✅ | ✅ |
| 区域入侵 / 越线 | 几何判定(Zone) | 进入/离开/越线方向 | ○ | ✅ | ✅ |
| 停留 / 徘徊 | track 时长统计 | 停留秒数 | ✅ | ✅ | ✅ |
| 静止 / 无活动 | 位移统计 | 无位移时长 | ✅ | ○ | ✅ |
| 人群聚集 / 密度 | 检测框计数 + 空间聚类 | 人数、密度 | — | ✅ | ○ |
| 剧烈动作 / 打架 | 姿态时序 + 光流 | 分类置信度 | — | ✅ | ○ |
| 攀爬 / 翻越 | 姿态 + 高度变化 + Zone | 分类 | — | ✅ | ✅ |
| 非人目标 | 检测模型多类 | e_bike / obstacle | — | ○ | ✅ |
| **ReID 跨镜(匿名)** | ReID 模型 | `anon_id` | ○ | ✅ | ✅ |
| 身份比对 | 人脸底库(§2.8.3) | `identity{ref_id, group, score}` | 🔐 | 🔐 | 🔐 |
✅ 核心 ○ 可选 — 不需要 🔐 需租户授权后启用
> **能力选择原则**:「同一性」问题走 **ReID**,「是谁」问题才走**人脸**。徘徊、尾随、跟随、跨镜追踪一律用 ReID 实现;人脸只用于白名单放行、关注名单、访客核验这类必须知道身份的场景。
### 算法分层(模型只输出观测,不做主观判定)
| 层 | 内容 | 位置 |
| --- | --- | --- |
| 姿态/检测提取 | YOLO / YOLO-pose | 推理单元(TensorRT) |
| 单帧启发式 | 包围框宽高比、重心高度、躯干角度 | Python 插件 |
| 时序判定 | 30 帧滑窗,静态位置 + 动态速度特征 | Python 插件 |
| **规则判定** | **主体过滤 × 时空 × 条件组合** | **Go 规则引擎(不在插件里)** |
| 事件后处理 | 去重、防抖、多源融合 | Go 业务编排 |
> **这条边界是《02》C4 的落地**:插件输出「有个人、姿态是躺倒、在多边形 A 内停留 12 秒」这类**客观观测**;规则引擎决定「这是否需要报警」。阈值、时段、联系人**一律不进插件**。
### 参考实现与经验
| 项目 | 借鉴点 | 注意 |
| --- | --- | --- |
| `GajuuzZ/Human-Falling-Detect-Tracks` | ST-GCN 每 30 帧预测动作,**7 分类**(站立/行走/坐下/躺下/起身/坐下动作/跌倒) | **七分类而非二分类是降误报的关键** |
| `kelsonbatista/fall-detection-ai-project` | 逐帧抽 128 维特征(静态位置 + 动态速度),30 帧窗口喂 LSTM | ⚠️ GPL-3.0,只读思路不抄代码 |
| `Y-B-Class-Projects/Human-Fall-Detection` | **负面经验**:约 500 张网络图片训 LSTM 完全学不会,最终退回 if-else 规则 | **小数据量下时序模型学不动,先解决数据再上模型** |
| Frigate | 运动侦测过滤后才跑推理 | 无雷达时的触发式过渡方案 |
### 模型许可(已决策:继续用 YOLO)
**主路径:使用 Ultralytics YOLO + 购买 Enterprise License。**
| # | 必办事项 | 时限 |
| --- | --- | --- |
| 1 | 采购 Ultralytics Enterprise License | **M3 前完成** |
| 2 | 书面确认授权覆盖:私有化交付的每个客户实例、边缘盒子内的模型文件、基于官方权重二次训练的产出 | 采购时 |
| 3 | 人脸/ReID 模型的**权重许可**单独核对(常与代码许可不一致,部分权重仅限非商业用途) | 启用前 |
**逃生通道(授权未获批时启用)**:
| 环节 | 替代 | 迁移代价 |
| --- | --- | --- |
| 检测 | **YOLOX**(Megvii,Apache-2.0,仍属 YOLO 家族) | 低,接口相近 |
| 姿态 | **RTMPose / RTMO**(MMPose,Apache-2.0) | **极低,关键点定义同为 COCO 17 点,判定算法不用改** |
> 为让逃生通道随时可用,**推理封装必须与具体模型解耦**:定义统一的 `Detector` / `PoseEstimator` 接口,模型只作为可替换实现。这条约束成本很低,但决定了授权谈判失败时是"换个实现"还是"重写一遍"。
>
> 附带好处:RTMPose 通过 MMDeploy 支持 ONNXRuntime、TensorRT、ncnn、OpenVINO、**RKNN** 多后端。家庭场景的 RK3588 盒子若跑不动 YOLO,可在边缘侧单独换成 RTMPose 而不影响中心侧。
## 2.5 编排层:规则引擎
### 规则 DSL
```yaml
# 规则的完整形态。场景包提供模板,租户/站点/设备逐级覆盖。
rule:
id: R-SCH-003
code: danger_zone_intrusion
name: 危险区域非授权进入
scene_pack: campus@1.2
enabled: true
dry_run: false # 试运行:命中但不通知,仅记录
scope: # 时空:哪些设备、哪些时段
devices: [tag:lab_area] # 支持标签选择,不写死设备 ID
zones: [danger_zone_1] # 画面区域,仅设备级可定义
schedule:
calendar: school_term # 引用日历(学期/课表/节假日)
windows: ["!class_time"] # 非上课时段(! 为取反)
subject: # 主体:检测什么人/物
class: person
filters:
- attr: age_group
in: [child, teen]
- identity: # 身份类,identity 为 null 时按 unknown 处理
not_in_group: authorized_staff
on_unavailable: treat_as_unknown # ⚠️ 身份服务不可用时不得漏报
condition: # 条件:客观观测量的组合
all:
- type: zone_enter
zone: danger_zone_1
- type: dwell
gte: 3s
- type: not_accompanied_by
role: teacher
radius_px: 200
suppression: # 抑制
cooldown: 300s
aggregate_by: [site_id, zone]
respect_silence_window: true
evidence: # 证据规格
pre_roll: 10s
post_roll: 15s
snapshots: 3
include_observations: true # 结构化观测序列,供后续训练
action:
severity: high
event_kind: danger_zone_intrusion
notify:
contact_chain: campus_security # 引用站点的升级链
start_level: 0
```
### 三级覆盖语义
```
场景包默认 ──继承──> 租户 ──覆盖──> 站点 ──覆盖──> 设备
```
| 字段类型 | 覆盖语义 | 理由 |
| --- | --- | --- |
| 标量(阈值、时长、severity) | 就近覆盖 | 直觉一致 |
| 列表(联系人、通道、devices) | **整体替换,不合并** | 合并会意外扩大通知面 |
| 开关 enabled | 就近覆盖,下级可关闭上级规则 | 单点位豁免是常见需求 |
| Zone 坐标 | **只能设备级定义**,上级不可下发 | 像素坐标与具体机位绑定 |
> 覆盖语义必须在文档和 UI 上明示(显示"此值继承自站点/租户"),否则运维会被"为什么这条没生效"折磨死。
### 规则生命周期
| 能力 | 说明 |
| --- | --- |
| 热生效 | 规则变更不重启 pipeline(一次重启影响同机全部路数) |
| 版本化 | `rule_versions` 表,记录变更人与时间,事故复盘用 |
| **试运行(dry_run)** | 命中但不通知,仅落库。新规则上线前评估误报量 |
| 质量指标 | 每条规则的命中量、误报率可查询 |
| 灰度 | 按站点比例放量 |
> **试运行模式是被低估的需求。** 没有它,每条新规则上线都是拿真实用户做实验。
## 2.6 事件实例
### 定义
「命中规则的画面组成事件实例」的具体产物:
```json
{
"id": "evt_01J8X...",
"tenant_id": 1001,
"site_id": 20301,
"device_id": 5012,
"rule": { "id": "R-SCH-003", "version": 7, "code": "danger_zone_intrusion" },
"kind": "danger_zone_intrusion",
"severity": "high",
"confidence": 0.87,
"occurred_at": "2026-08-01T14:23:07.412Z",
"detected_at": "2026-08-01T14:23:08.905Z",
"subject": {
"class": "person",
"track_id": "trk_88213",
"attributes": { "age_group": "teen" },
"anon_id": "pid_7f3a91",
"identity": null,
"identity_status": "not_enabled"
},
"observation": {
"zone": "danger_zone_1",
"dwell_sec": 3.4,
"bbox_seq_uri": "s3://.../evt_01J8X/obs.jsonl",
"keypoint_seq_uri": "s3://.../evt_01J8X/pose.jsonl"
},
"evidence": {
"clip_uri": "s3://.../evt_01J8X/clip.mp4",
"clip_range": ["2026-08-01T14:22:57Z", "2026-08-01T14:23:22Z"],
"snapshot_uris": ["s3://.../s0.jpg", "s3://.../s1.jpg", "s3://.../s2.jpg"]
},
"dedup_key": "20301:danger_zone_1:danger_zone_intrusion:2026-08-01T14:20",
"aggregated_into": null,
"outcome": "unknown",
"outcome_reason": null
}
```
### 三条硬约束
| 约束 | 理由 |
| --- | --- |
| **事件不可变** | 误判只能通过 `outcome` 标记,不能删改。合规与举证要求 |
| **`occurred_at` 与 `detected_at` 都要存** | 前者是事发时刻(决定证据回捞窗口),后者是判定时刻(决定 SLA 计算) |
| **必须含结构化观测序列** | 只有视频无法直接用于训练;`bbox_seq` 与 `keypoint_seq` 是《02》C9 数据闭环的原料 |
| **`anon_id` 与 `identity` 分开存** | 前者是 ReID 匿名标识(会话内有效),后者是人脸身份;不可互相顶替 |
| **`identity_status` 必须显式** | 取 `not_enabled` / `pending` / `matched` / `below_threshold` / `no_candidate` / `timeout`。只写 `null` 无法区分"没开这功能"和"比对失败",后者是需要排查的故障 |
### 证据回捞(pre-roll)
事件是"事后才知道"的——判定成立时,事发画面已经过去了。三种回捞方式:
| 方式 | 实现 | 适用 |
| --- | --- | --- |
| 边缘环形缓冲 | 边缘节点持续录最近 N 秒到本地环 | 家庭(不上云,隐私最优) |
| 中心持续录制 | mediamtx `record: yes` + `recordDeleteAfter` | 校园/社区(允许常态录像) |
| Replay 服务回捞 | 推理框架的 Replay REST 接口 | 通用 |
> ⚠️ pre-roll 的实际起点受 **GOP 长度**限制——只能从关键帧切。这是《01》§6.1 把"支持修改 GOP 长度"列为加分项的原因。GOP 2s 时,请求 10s pre-roll 实际可能拿到 10–12s。
### 去重与聚合
| 规则 | 参数 | 场景 |
| --- | --- | --- |
| 同设备冷却期 | 默认 5min | 同一事件多次触发 |
| 同站点聚合 | `aggregate_by` 键相同则合并 | 两个机位拍到同一次跌倒 |
| 已处置抑制 | 已 ack 期间不再新建同类 | 人已到现场 |
| 静默窗口 | 用户主动静默 | **强制限时(≤4h),到期前提醒,无"永久"选项** |
> **永久静默是这类系统最常见的事故成因。**
## 2.7 预警投递
### 状态机
```
[触发] ─→ [投递中] ─→ [已送达] ─→ [已确认 ack] ─→ [处置中] ─→ [关闭]
│ │ │ │
│ │ └─超时未 ack─→ [升级] ─→ 下一级投递 │
│ └─投递失败─→ [升级] │
└─防抖命中─→ [抑制] [标记误报] ←───┘
```
**硬约束:告警先落库再投递。** 进程崩溃重启后扫描所有非终态告警并接续升级链——与对账器的水平触发是同一思路。
### 升级链配置(场景差异的主要载体)
```yaml
contact_chain:
id: home_elderly_default
levels:
- level: 0
timeout_sec: 30
contacts: [{ role: family_primary, channels: [push] }]
- level: 1
timeout_sec: 60
contacts: [{ role: family_primary, channels: [sms, voice] }]
- level: 2
timeout_sec: 90
contacts: [{ role: backup, channels: [sms, voice] }] # 邻居/物业/护理员
- level: 3
timeout_sec: null
contacts: [{ role: call_center, channels: [voice, ticket] }]
time_policy:
- window: "08:00-22:00"
override: { level0_role: family_primary }
- window: "22:00-08:00"
override: { level0_role: cohabitant } # 夜间找同住者
```
### 通道选型与冗余
**必须至少两条独立路径,且电话通道要能绕过互联网。** 推送依赖 APNs/FCM,短信依赖运营商,任何单一通道都可能整体故障。
| 层 | 选型 | 说明 |
| --- | --- | --- |
| 状态机、升级链、ack、审计 | **自研 Go** | 业务定制度高,通用工具都要大改 |
| 多通道投递 | Novu / Apprise / ntfy 或直连云厂商 | 只解决"怎么发" |
| 短信 + 语音 | 云厂商,**双供应商** | `provider` 字段就是为切换准备的 |
### 三个不同的事实
| 事实 | 含义 | 字段 |
| --- | --- | --- |
| 发出去了 | 调用了上游 API | `sent_at` |
| 送达了 | 上游回执确认到达终端 | `receipt_at` |
| 被看到了 | 用户主动 ack | `alerts.acked_at` |
> **没有回执就当失败并升级**,不做乐观假设。推送可能被系统杀掉、短信可能延迟、电话可能占线。
### 业务预警 vs 运维告警
| | 运维告警 | 业务预警 |
| --- | --- | --- |
| 方案 | Prometheus + Alertmanager | 自研状态机 + 复用投递通道 |
| 收件人 | 运维团队 | 家属 / 值班员 / 呼叫中心 |
| 延迟容忍 | 分钟级 | 秒级 |
> **两套系统不得共用投递通道和值班配置**,避免运维噪声淡化业务预警。
## 2.8 同一性与身份:ReID 与人脸识别
### 2.8.1 两条路径的分工
```mermaid
flowchart LR
TRK["跟踪目标 Track"] --> Q{"需要回答什么"}
Q -->|"是不是同一个人"| REID["ReID 匿名同一性<br/>默认开启"]
Q -->|"他是谁"| FACE["人脸比对<br/>租户授权后开启"]
REID --> AID["anon_id<br/>会话内有效"]
FACE --> IDT["identity{ref_id, group, score}"]
AID --> RULE["规则引擎"]
IDT --> RULE
```
| 能力 | 产出 | 生命周期 | 合规负担 | 典型用途 |
| --- | --- | --- | --- | --- |
| **ReID** | `anon_id` | 会话/时间窗内有效,**不跨天持久化为人员画像** | 中 | 徘徊、尾随、跟随、跨镜追踪、同一人多次出现计数 |
| **人脸** | `identity` | 持久(底库) | **高** | 白名单放行、关注名单、访客核验 |
> **默认走 ReID。** 一条规则若想用 `identity`,需在评审中回答"为什么 anon_id 不够"。
### 2.8.2 ReID 实现要点
| 要点 | 做法 |
| --- | --- |
| 特征时效 | `anon_id` 绑定滑动时间窗(默认 30min),超窗即失效重编号,避免变相形成长期追踪 |
| 跨镜匹配范围 | 限定在同一站点内,不跨租户、不跨站点 |
| 存储 | 特征向量仅驻内存/短期缓存,**不入长期库** |
| 事件中的表现 | 只写 `anon_id` 字符串,不写特征向量 |
> 第 1 条和第 3 条是有意的自我约束:ReID 之所以合规负担低于人脸,正是因为它不建立可长期关联到自然人的档案。若把 ReID 特征长期入库并跨天关联,它在实质上就变成了无同意的身份追踪——合规优势荡然无存。
### 2.8.3 人脸识别子系统
#### 架构:异步旁路,不在主链路上
```mermaid
flowchart TD
subgraph MAIN["主链路(实时,不可阻塞)"]
D["检测+跟踪"] --> P["姿态/行为"] --> R["规则引擎"] --> E["事件生成"]
end
subgraph SIDE["旁路(可延迟、可失败)"]
FD["人脸检测"] --> QC["质量筛选<br/>角度·清晰度·遮挡·尺寸"]
QC --> FE["特征提取"] --> M["底库比对<br/>向量检索"]
end
D -.抽取人脸 ROI.-> FD
M -.时间窗内回填.-> E
M --> LOG[("face_match_logs<br/>只追加审计")]
GATE{"租户授权开关"} -.控制.-> SIDE
```
**为什么必须旁路**:把比对放进主链路,一次底库慢查询会拖垮整条 pipeline 的实时性;而跌倒、打架这类人身安全事件根本不需要知道是谁。
| 参数 | 值 |
| --- | --- |
| 回填时间窗 | 默认 2s,超窗事件以 `identity: null` 定稿 |
| 质量筛选 | 人脸像素 ≥ 64×64、偏航角 ≤ 30°、遮挡比 < 30%、清晰度阈值 |
| 比对失败/超时 | 不阻塞事件生成,记 `decision = no_candidate / timeout` |
#### 授权是租户级主开关
```yaml
tenant_features:
tenant_id: 1001
feature: face_recognition
enabled: true
authorized_by: "客户法务签署 · 2026-08-01"
expires_at: "2027-08-01" # 授权有到期日,到期需续签
doc_uri: "s3://compliance/auth/1001-face.pdf"
scope:
sites: [20301] # 可进一步限定站点
purposes: [whitelist, visitor] # 未含 watchlist 则关注名单功能不可用
```
| 层级 | 语义 |
| --- | --- |
| 平台级 | 能力是否部署(私有化交付可整体不装) |
| **租户级** | 主开关。未授权租户在 **API 与 UI 层均不可见**,不是"看得到点不动" |
| 站点级 | 已授权租户内的生效范围(如只在校门口,不在教室) |
| 规则级 | 单条规则是否使用 `identity` 过滤 |
#### 底库管理
| 要求 | 实现 |
| --- | --- |
| 独立 schema,独立加密密钥 | 与业务库分离,泄露面隔离 |
| **默认只存特征向量,不存原图** | 需展示原图的场景(访客核验)单独授权,原图分库存储 |
| **人员必须有有效期** | `valid_until` 非空约束;学生按学年、住户按合同、访客按当日 |
| 到期自动失效 | 定时任务将过期人员移出检索集合,不依赖人工清理 |
| 三类库分开 | `whitelist` / `watchlist` / `visitor`,阈值与处置策略各不相同 |
| 整体导出与彻底删除 | 客户终止服务时的义务,需留删除证明 |
| 每次比对与每次查询均审计 | `face_match_logs` 只追加,含操作人/设备/分数/决策 |
#### 阈值分级(误识别后果不对称)
| 库类型 | 假阳性后果 | 阈值 | 附加控制 |
| --- | --- | --- | --- |
| `whitelist` 白名单放行 | 陌生人被当成住户放行 → 安防失效 | 高 | — |
| `watchlist` 关注名单 | **无辜者被当成关注对象 → 对个人的实质伤害** | **最高** | **强制人工复核** |
| `visitor` 访客核验 | 核验错误 | 中 | 人工在场 |
> **产品红线(L4 编排层硬约束)**:`watchlist` 类规则**不得自动触发对人的处置动作**(自动锁门、自动通报、自动广播),只能生成待人工确认的事件。这条写进代码,不靠流程约束。
#### 规则中的降级语义
```yaml
subject:
class: person
filters:
- identity:
library: campus_students
not_in_group: authorized_staff
min_score: 0.75
on_unavailable: treat_as_unknown # 默认;另一取值 skip
```
| 取值 | 语义 | 适用 |
| --- | --- | --- |
| **`treat_as_unknown`(默认)** | 比对不可用/未命中时按"未知人员"处理,**倾向报警** | 监护与安防场景 |
| `skip` | 比对不可用时不产生事件,**倾向不报** | 仅用于低风险规则,需显式写明并有审批记录 |
> 默认必须是 `treat_as_unknown`:人脸角度不佳、服务抖动、底库超时都会产生 null,若默认跳过,系统会在最需要报警的时候安静下来。
---
# 3. 场景包
## 3.1 场景包是什么
一个可版本化的配置集合,**不含代码**:
```
scene_packs/campus@1.2/
├── manifest.yaml # 名称、版本、依赖的能力与最低平台版本
├── rules/ # 规则模板(DSL)
│ ├── fight.yaml
│ ├── climb_fence.yaml
│ ├── danger_zone.yaml
│ └── ...
├── zones/ # 区域模板(相对坐标 + 引导文案,实施时按机位调)
├── chains/ # 升级链模板
├── calendars/ # 日历/时段模板(学期、课表、作息)
├── copy/ # 通知话术模板(多语言)
├── dashboards/ # Grafana 看板 JSON + 报表口径
└── onboarding/ # 实施checklist:机位建议、安装高度、俯角、光照要求
```
## 3.2 居家养老包(S1)
### 规则集
| 规则 | 条件 | 严重度 | 备注 |
| --- | --- | --- | --- |
| 跌倒 | 姿态 7 分类命中 fall,持续 ≥ 2s | critical | 核心 |
| 长时间静止 | 无位移 ≥ 30min(白天)/ 卫生间 ≥ 20min | high | 阈值按户可调 |
| 夜间离床未归 | 22:00–06:00 离开卧室 ≥ 20min 未返回 | high | 需雷达或门磁 |
| 陌生人入室 | 非住户人员出现 | medium | 优先 ReID;家属若要"认出保姆/子女"可授权开人脸白名单库 |
| 活动量异常 | 日活动量较基线下降 > 50% | low | 跨天统计,非实时 |
### 场景特有约束
| 约束 | 实现 |
| --- | --- |
| 卧室、卫生间**禁装摄像头** | `areas.capture_policy = non_imaging_only` 时,Sense 在设备新增/启用写路径拒绝具有 `captures_image` 能力的设备(代码级硬约束);策略读取失败时拒绝新变更并告警,不静默切断已有链路 |
| 平时不解码不上传 | 触发式推理 + `record: no` + 边缘环形缓冲 |
| 家属只能看自己家的告警关联片段 | RBAC:`family` 角色的证据访问范围 = `site_id` ∩ `alert_events` |
| 3 分钟内必有真人接手 | 4 级升级链(30s/60s/90s → 呼叫中心) |
### 时间线
| 时刻 | 发生什么 |
| --- | --- |
| T+0s | 事件发生,一级触发源捕获(雷达/运动) |
| T+3s | 唤醒推理,回捞前 30s 画面 |
| T+10s | 判定成立,事件落库,首次投递(App 推送) |
| T+40s | 未 ack → 短信 + 语音电话 |
| T+100s | 未 ack → 备用联系人(邻居/物业/护理员) |
| T+190s | 未 ack → 呼叫中心人工介入 |
### 部署形态
```mermaid
flowchart LR
subgraph HOME["用户家中"]
CAM["客厅 IP 摄像头"]
RADAR["毫米波雷达<br/>卧室 / 卫生间"]
BOX["边缘盒子 RK3588<br/>录像环缓冲 + 推流 + WG"]
CAM --> BOX
RADAR --> BOX
end
subgraph CLOUD["云端平台"]
MTX["mediamtx"] --> INF["推理"]
INF --> BIZ["业务编排"]
BIZ --> DB[("事件与审计库")]
end
BOX -->|"仅上传事件片段"| MTX
BIZ --> FAM["家属 App / 短信 / 语音"]
BIZ --> CC["呼叫中心"]
```
**一户一盒子,互不影响。** 断网时本地缓存,恢复后补传;断网本身触发运维告警。
## 3.3 校园包(S2)
### 规则集
| 规则 | 条件 | 严重度 | 时段 |
| --- | --- | --- | --- |
| 打架/推搡 | 剧烈动作分类 + 人数 ≥ 2 + 持续 ≥ 3s | critical | 全天 |
| 翻越围墙 | 攀爬姿态 + 周界 Zone + 高度变化 | critical | 全天 |
| 危险区域闯入 | Zone 进入 + 停留 ≥ 3s + 非授权角色 | high | 非开放时段 |
| 尾随进校 | 门禁事件 1 次 + 检测到人数 ≥ 2 | medium | 上下学时段 |
| 上课时段徘徊 | 楼道 Zone 停留 ≥ 5min | low | 课表内 |
| 宿舍晚归 | 22:30 后进入宿舍楼 | low | 夜间 |
| 学生跌倒/晕倒 | 复用 S1 跌倒能力 | high | 全天 |
| 人群聚集 | 单 Zone 人数 ≥ N 且持续 ≥ 30s | medium | 可配 |
### 场景特有
| 特点 | 方案 |
| --- | --- |
| **未成年人合规** | 人脸能力即使租户已授权,**校园场景仍需专项合规意见方可开启**(独立阻塞项);证据留存期限从严;查看权限按角色最小化;采集告示牌 |
| 危险区域的"授权人员"判定 | 优先用**角色标签 + 工牌/工服属性**;确需人脸时用 `whitelist` 库(教职工),学生不入库 |
| 尾随进校 | **ReID + 门禁事件融合**,不依赖人脸 |
| 课表/校历驱动 | `calendars/school_term.yaml` + 课表导入,规则用 `!class_time` 这类语义引用 |
| 值班室 7×24 有人 | 升级链只需 2 级(值班室 → 校领导),超时 2min/10min |
| 批量事件处理 | **需要看板与工单**(S1 不需要):列表、批量 ack、按区域筛选、当班交接 |
| 大屏联动 | Webhook 推到值班室大屏 + 声光 |
| 带宽充足 | 可中心部署,可用常态推理覆盖高价值点位 |
### 部署形态
校内边缘节点(Jetson Orin NX 或 x86+GPU)承担推理,事件与元数据上中心平台;视频不出校(若客户要求)。
## 3.4 社区/物业包(S3)
### 规则集
| 规则 | 条件 | 严重度 | 备注 |
| --- | --- | --- | --- |
| 周界入侵 | 越线(方向性)或 Zone 进入 | high | 夜间加权 |
| 电动车进电梯 | 检测 class = e_bike + 电梯 Zone | high | **非人目标** |
| 消防通道占用 | 静态变化检测 + 持续 ≥ 10min | medium | 需持续态表达 |
| 陌生人徘徊 | ReID 会话在单元门 Zone 停留 ≥ 5min | medium | **用 ReID 不用人脸** |
| 独居老人异常 | 连续 N 天未在出入口出现 | medium | 跨天统计 |
| 高空抛物 | — | — | ⚠️ 需专用仰拍机位与独立算法,**不在本包,建议独立立项** |
### 场景特有
| 特点 | 方案 |
| --- | --- |
| 持续态事件(消防通道堆物) | 事件带 `state: ongoing`,整改后关闭;**不是每帧生成新事件** |
| 夜间可能无人值班 | 升级链 3 级:值班 → 主管 → 业主群/物业总值班 |
| 非人目标检测 | 检测模型需含 e_bike / obstacle 类,与人体检测共用 backbone |
| 陌生人判定 | **优先 ReID(匿名)**,避免人脸底库的合规负担 |
## 3.5 场景包对比速查
| 维度 | 居家养老 | 校园 | 社区 |
| --- | --- | --- | --- |
| 单站点路数 | 1–3 | 默认 16,单站点 ≤128 | 默认 16,单站点 ≤128 |
| 部署 | 边缘盒子(必须) | 校内边缘/中心 | 混合 |
| 推理模式 | 触发式 | 混合(重点常态) | 触发式为主 |
| 常态上云录像 | ❌ 禁止 | ✅ | ✅ |
| 升级链级数 | 4 | 2 | 3 |
| 首级超时 | 30s | 2min | 1min |
| 接收端 | App(个人) | 值班大屏 + 工单 | App + 值班 |
| 同一性/身份 | ReID 为主;人脸可授权开白名单库 | ReID 为主;**人脸需专项合规意见**(未成年人) | ReID 为主;人脸可用于访客核验 |
| 误报容忍 | 极低 | 中 | 中 |
| 主要成本 | 硬件(一户一盒) | GPU | GPU + 存储 |
---
# 4. 部署形态
## 4.1 三种形态
| 形态 | 推理位置 | 适用 | 优点 | 代价 |
| --- | --- | --- | --- | --- |
| **边缘为主** | 站点内 | 家庭、隐私敏感、带宽受限 | 视频不出户,带宽省 | 硬件成本×站点数,运维难 |
| **中心为主** | 中心机房 | 校园、社区(有专网) | 算力集中利用率高,好运维 | 带宽压力,视频出站点 |
| **混合** | 一级边缘 + 二级中心 | 大多数实际项目 | 成本与隐私平衡 | 架构复杂度 |
**推荐默认:混合。** 边缘做触发与初筛(运动侦测、轻量检测),中心做重推理与二次确认。
## 4.2 容量与分片
容量规格分为 16 / 32 / 64 / 128 四档。默认交付 16 路,单站点本阶段上限 128 路;16 是默认配额,不是数据库或代码硬限制。
| 项 | 默认 16 路 | 128 路上限 | 设计要求 |
| --- | ---: | ---: | --- |
| 子码流 640×360 @ 512 kbps | ≈ 8 Mbps | ≈ 64 Mbps | 推理优先使用子码流 |
| 主码流按 4–6 Mbps | ≈ 64–96 Mbps | ≈ 512–768 Mbps | 录像与证据回捞预算;不默认重复保存客户全量录像 |
| 抽帧 5 fps | 80 帧/秒 | 640 帧/秒 | 解码与推理均需实测 |
| mediamtx | 1 个分片 | 建议 4 个 32 路分片起步 | 若需缩小故障域,可改为 8 个 16 路分片 |
| 推理 | 1 个 16 路逻辑分片 | 最多 8 个 16 路逻辑分片 | 多个逻辑分片可落在同一 Worker,实际比例由压测决定 |
推荐配置:
```yaml
capacity:
site_default_video_channels: 16
site_max_video_channels: 128
media_shard_max_streams: 32
inference_profile_max_sources: 16
```
`stream_bindings` 同时记录 `mtx_instance` 与 `inference_shard`。默认按 `site_id + device_id` 稳定分配;扩容时允许迁移,但同一设备任一时刻只能归属一个有效媒体分片和一个推理分片。
`sites.max_video_channels`、Site/Area 层级和 `areas.capture_policy` 的唯一真相源在 Bell;Sense 在设备新增/启用写路径通过版本化内部 API 或带 `source_version` / `synced_at` 的只读投影执行校验,不跨 schema 直接写入。配额或 Area 策略依赖暂时不可用时只拒绝相关新变更,已有视频链路继续运行并产生运维告警。
> **不要单实例硬扛 128 路**:扩容必须通过增加分片与 Worker 完成。单分片故障只允许影响该分片,不能拖垮整个站点。GPU 型号与数量不在架构文档写死,M1 用 5 路打通,M3 建立 16 路基线,M4/M5 再验证 64/128 路。
## 4.3 硬件选型
| 平台 | 用途 | 注意 |
| --- | --- | --- |
| RK3588 | 家庭边缘盒 | 6 TOPS NPU,无风扇低噪;模型需 MMDeploy 转 RKNN |
| Jetson Orin Nano/NX/AGX | 校园/社区边缘 | 与 Savant/DeepStream 原生契合,同一套代码 |
| x86 + L4 / A2 / Quadro | 中心推理 | ⚠️ **GeForce 卡同时硬件编码流数上限 3**,做可视化回推必须用专业卡 |
---
# 5. 可观测与运维
## 5.1 一个 Grafana 全收
mediamtx 与 Savant 都原生导出 Prometheus 指标,**前期不用写一行前端代码**就有运营看板。
| 类别 | 关键指标 |
| --- | --- |
| 接入 | 设备在线率、path 数、推流断连次数、探活失败率 |
| **对账** | `reconciler_unconverged_bindings{system}`(持续 > 0 即异常)、`reconciler_orphans_detected{type}`、`reconciler_safety_brake_triggered`(**应当直接呼人**)、`reconciler_loop_duration_seconds`(超时 = 该分片了) |
| 流水线 | 每路人体检出率、推理延迟 P50/P99、丢帧率、buffer adapter 条目数与大小 |
| 业务 | 事件数(按规则/站点/类型)、**误报率(按规则)**、预警投递成功率、ack 时长 P50/P95、升级到各级的比例 |
| SLA | 事件 → 首次投递延迟、投递 → 回执延迟、全链路无响应比例 |
> **把每路的检出率、事件候选数、推理延迟声明成自定义指标** → Grafana 上就是现成的运营看板。
## 5.2 追踪
OpenTelemetry → Jaeger,排查单帧处理延迟与端到端事件链路。
## 5.3 管理界面
生产组合(mediamtx + 自研服务)**没有现成的管理界面**,三个选择:
1. **前期不做完整 UI** —— REST API + 脚本撑过接入验证期;即使默认只有 16 路,也不应依赖逐路手工配置。正式管理端从第一版起按 128 路设计分页、虚拟列表、筛选与批量操作。
2. 临时用 go2rtc 的 1984 页面看流状态 —— ⚠️ **默认无密码,局域网内任何人可访问**,必须在配置里关闭或用防火墙隔离。
3. **正式做时长在自研服务上** —— 因为要展示的是「租户 → 站点 → 设备 → 流状态」的业务视图;第三方面板展示的是「path 列表」,不是一回事。
> 社区面板(mediamtx-dashboard、lgcshy/mediamtx-ui、nabaco/mediamtx-ui、bcanfield/mediamtx-connect)均为独立进程通过 API 对接,可按需临时使用。mediamtx 作者明确表示精力有限、优先保证核心稳定。
## 5.4 管理系统与 App 的功能边界
| 端 | 核心功能 |
| --- | --- |
| **Sense 接入工作台(Web)** | 当前租户/站点上下文、设备台账与批量开通、能力探测、Zone 画图、对账/分片/边缘隧道/补传/运维告警;不管理 Tenant/Site/Area/RBAC 或全局审计 |
| **Bell 统一管理端(Web)** | Tenant/Site/Area/配额/`capture_policy`、RBAC、规则配置(含试运行)、升级链、事件看板与筛选、事件详情(视频+观测回放)、处置与工单、误报标记、统计报表、全局审计;设备操作日志从 Sense 汇入并可深链返回 |
| **App(家属/值班员)** | 预警接收与 ack、事件详情与视频回看、一键呼叫、误报反馈、限时静默、设备状态、(家属)多老人切换 |
| **大屏/值班台** | 实时事件流、地图/平面图点位、声光联动、当班交接 |
| **Webhook/开放 API** | 对接客户既有平台、门禁、消防、工单系统 |
---
# 6. 演进路径与备选
## 6.1 里程碑
| 阶段 | 内容 | 出口标准 |
| --- | --- | --- |
| **M0** | 摄像头兼容性验证 + 需求定稿 | 可直接运行 MiBeeNvr 作为隔离实验室测试台;3–5 款型号跑通 GetProfiles/GetStreamUri/SetSystemDateAndTime,采购白名单归档;M0 代码不得作为生产基线 |
| **M1** | 接入骨架 + mediamtx | **MediaMTX 正式进入且不得由完整 MiBeeNvr 替代**;5 路自动建 path、探活、断线重建 |
| **M2** | 对账器 + 多租户 + 隧道 | 10 站点试点,至少一个站点完成 16 路开通/停用全流程,`unconverged = 0` 稳定 |
| **M3** | 推理 + 规则引擎 + 事件预警 | **默认 16 路**端到端稳定;跑通首个场景三类规则;拿到现场误报基线,数据闭环已收数据 |
| **M4** | 64 路分片 + 灰度 + 管理系统 | 64 路稳定运行;单分片故障不扩散;看板、批量操作与处置流可用 |
| **M5** | 128 路容量验收 + 第二、三场景包 + 触发式推理 + **人脸识别子系统** | **128 路横向扩展不改业务代码**;场景包为纯配置交付;人脸在一个已授权租户跑通(底库有效期、异步回填、降级、审计) |
| **M6** | 异构传感器 + 两级判定 | 误报率显著下降,视频常态采集下降 |
### 代码归属:三个系统
本方案的实现拆为三个可独立开发的系统,**完整职责与目录结构见《08-三系统职责划分》**:
| 系统 | 中文 | 层 | 语言 | 状态 | 首次交付 |
| --- | --- | --- | --- | --- | --- |
| **Sense** | 感知系统 | L1 接入 | Go | 有(设备台账) | M1 |
| **Brain** | 推理系统 | L2 流水线 + L3 算法 | Python / CUDA | **无状态** | M3 |
| **Bell** | 管理系统 | L4 业务 + L0 基座 | Go + 前端 | 有(事件、告警、组织与策略真相) | M3 → M4 |
Sense 的高风险设备操作不能采用“先执行、再尽力记录审计”。实现时应在本地事务内同时写期望态与持久化 outbox,再由幂等 relay 异步汇入 Bell 全局审计。该跨系统接口的字段、签名、重放和留存必须另立 API/契约任务,不在 UI 原型任务中顺带冻结。
三者之间只有一条契约——**事件实例 JSON**(§2.6,规范本体见 `contracts/event-v0.1.schema.json`)。Sense 向 Brain 供流与信号,Brain 产出事件,Bell 消费事件。这条边界让推理侧换模型、换框架(Savant → Pipeless、YOLO → YOLOX)时,另两个系统一行不改。
> mediamtx 是独立二进制,不属于任何一个系统。Sense 管它的配置与生命周期。
#### M1 骨架(约 200 行起步,全部是生产代码,**只动 Sense**)
```
Sense/
cmd/sense-api/main.go
internal/
onvif/ 连接、GetProfiles、GetStreamUri、SetSystemDateAndTime
store/ 先 SQLite,schema 与生产 Postgres 保持一致
mtx/ oapi-codegen 生成的客户端 + 薄封装
reconcile/ 最小对账循环(幂等 + 退避)
probe/ 探活 goroutine
```
跑通之后就已经站在生产架构上了,后面是往上加(Postgres、多租户、隧道、分片),**不是推倒重来**。Brain 与 Bell 到 M3 才起步——那也是事件契约第一次被真实使用的时刻。
### 演示与验证阶段的使用红线
| 允许 | 禁止 |
| --- | --- |
| 实验室接自购测试摄像头做兼容性验证 | 接入真实住户/学校的摄像头 |
| M0 直接运行现成 NVR(如 MiBeeNvr)给干系人做效果演示 | 把它作为可持续演进的生产系统第一版上线;将匿名录像端点暴露到真实网络 |
| 按 §1.4 白名单阅读/移植 ONVIF、IP 自愈、健康检测和 UI 交互 | 复制媒体内核、SQLite 真相源、AI 或本地用户体系;作为长期依赖引入 go.mod |
> 理由:现成 NVR 的目标通常是单机一体化交付,而本项目是多租户控制面 + 可分片媒体面 + 独立推理面。许可证宽松不等于架构适配或已通过 128 路压测;生产采用范围必须由本项目自己的兼容性、容量、安全和故障隔离测试决定。
## 6.2 备选与逃生通道
| 主选 | 备选 | 触发切换的条件 |
| --- | --- | --- |
| Savant (DeepStream) | **Pipeless**(Rust,不绑 NVIDIA,支持 ONNX Runtime/OpenVINO/TensorRT,HTTP 动态增删流) | 硬件选型变更、规模缩小、需信创环境 |
| **YOLO + Ultralytics Enterprise License** | YOLOX(检测)+ RTMPose(姿态),均 Apache-2.0 | 授权采购未获批;或边缘 NPU 跑不动 YOLO |
| 人脸识别(授权租户) | 纯 ReID + 角色标签/工牌属性 | 合规意见未通过,或客户撤回授权 |
| 自研设备管理 | EdgeX Foundry 最小服务集 | 确定接入雷达/门磁/Modbus 等异构设备时 |
| 中心推理 | 边缘推理 | 隐私要求提升或带宽成本超预期 |
> **推理代码必须与框架解耦**——模型推理、时序判定、特征工程写成独立模块,框架只负责调度。这是切换成本能否控制的关键。
### EdgeX 最小服务集(若切换)
```
core-metadata 设备注册表
core-command 命令下发入口
device-onvif-camera ONVIF <-> REST
redis 消息总线
(consul 可用 -cp=none 省掉)
```
砍掉 core-data、app-service-*、security 全套。⚠️ discovery 在本场景基本不可用:netscan 对大量 /24 段是数万次探测,应使用预定义 deviceList 或 REST 批量录入,但保留 `EnableStatusCheck` / `CheckStatusInterval` 做探活。另注意 EdgeX 3.x 起 API 路径是 `/api/v3`,网上大量 2.x 文档写的是 `/api/v2`。
---
# 7. 落地检查清单
开工前逐项确认:
## 硬件与网络
- [ ] 3–5 款候选摄像头完成 §2.1 必过项实测,白名单归档
- [ ] 各场景站点的网络出口带宽、是否有公网 IP 已摸底
- [ ] 边缘硬件选型确定(RK3588 / Jetson / x86+GPU)
- [ ] 中心 GPU 型号确认(若需可视化回推,**必须专业卡**)
## 许可与合规
- [ ] **Ultralytics Enterprise License 已采购**,覆盖私有化实例 / 边缘模型 / 二次训练权重
- [ ] 人脸与 ReID 模型的**权重许可**已逐个核对(可能与代码许可不一致)
- [ ] 视频证据留存期限已由法务确认
- [ ] 各角色查看范围的法定边界已确认
- [ ] 被拍摄者知情同意的取得方式与留档流程已确定
## 人脸能力启用前(逐租户执行)
- [ ] 租户授权书已签署并录入 `tenant_features`(含用途、范围、**到期日**)
- [ ] 人脸信息的**单独同意**取得方式已确定(通常不能与一般隐私政策打包)
- [ ] 底库来源合法性由客户书面承诺
- [ ] 底库人员的知情/查询/删除权响应流程与时限已定
- [ ] **未成年人场景:专项合规意见已出**(S2 独立阻塞项)
- [ ] 人脸数据留存期限已单独配置(独立于视频证据)
- [ ] 终止服务时的底库彻底删除义务与证明方式已约定
## 架构
- [ ] `devices` 表而非 `cameras`,含 `modality + capabilities`;Bell 持有 Area 与 `capture_policy`,Sense 只存带版本的准入投影
- [ ] 设备身份用序列号
- [ ] 触发入口独立成 handler
- [ ] `sync_state` 每系统一行
- [ ] 对账器安全闸已实现并有指标
- [ ] 隐私区域拒绝 camera 类型是**代码级**约束
- [ ] 所有查询强制带 tenant_id,在数据访问层统一拦截
- [ ] 推理封装与具体模型解耦(`Detector` / `PoseEstimator` 接口),逃生通道随时可用
- [ ] 人脸比对为**异步旁路**,超时不阻塞事件生成
- [ ] 依赖 identity 的规则默认 `on_unavailable: treat_as_unknown`
- [ ] `watchlist` 类规则**不得自动触发对人的处置动作**(代码级)
- [ ] 未授权租户在 API 与 UI 层均不可见人脸能力
- [ ] `persons.valid_until` 非空约束 + 到期自动失效任务
- [ ] 告警先落库再投递,重启可续跑升级链
- [ ] 静默功能无"永久"选项
## 数据闭环
- [ ] 事件证据含结构化观测序列(bbox_seq / keypoint_seq)
- [ ] 误报标记含归因分类
- [ ] 标注队列从 M3 第一天就在收数据
- [ ] 每条规则的命中量与误报率可查
---
> 本方案的所有技术决策均可追溯到《02-需求分析》的 C1–C9。若实施中发现某条决策不成立,应先回到《02》修改分析结论,再同步修订本文档——**不要在实现里悄悄绕过**。