Sense M1/M2 接入骨架
本目录是 YoVision Sense 的接入与 NVR 管理面。数据库保存期望态,ONVIF 和 MediaMTX 通过端口隔离;M1 默认使用 SQLite,T-009~T-016 增加 PostgreSQL 双 schema、Area 准入、本地审计 Outbox、Control API v1、多实例调和 fencing、孤儿受控处置和到 Bell 的审计 relay。T-018 增加默认关闭的回环工程控制台,用于展示设备、配额、收敛状态和最多 4 路按需 MediaMTX WebRTC 预览;它不包含录像/回放,也不是生产公网入口。默认关闭真实 ONVIF、公共业务路由、控制台和 relay;T-006 的真实样机结论仅覆盖已批准的精确海康基线,不能据此宣称多品牌兼容。
常用命令
cd Sense
go mod download
go generate ./internal/mtx ./internal/controlapi
go test ./...
go vet ./...
go build -o bin/sense-api.exe ./cmd/sense-api
go run ./cmd/sense-api
Unix 将构建产物改为 bin/sense-api。服务默认监听 127.0.0.1:8080,SQLite 默认写入 Sense/data/sense.db,MediaMTX 控制 API 默认是 http://127.0.0.1:9997。默认运行暴露 /healthz、/readyz 和不含租户/设备标签的 /metrics;只有显式选择 PostgreSQL 并完成安全配置后才注册 7 个 /api/v1 Control API 路由,/sense-console/ 还需独立显式开启。
常用环境变量:
| 变量 | 默认值 | 说明 |
|---|---|---|
SENSE_HTTP_ADDR |
127.0.0.1:8080 |
HTTP 监听地址 |
SENSE_ALLOW_NON_LOOPBACK |
false |
显式允许监听非回环地址;只应在可信网络及外部认证/防火墙就绪后开启 |
SENSE_DB_DRIVER |
sqlite |
sqlite 或 postgres;生产切换必须显式选择 postgres |
SENSE_DB_DSN |
file:data/sense.db |
所选 driver 的私有 DSN;不得写入日志、文档或仓库,PostgreSQL 密码优先由 PGPASSFILE/环境密钥提供 |
SENSE_MEDIAMTX_URL |
http://127.0.0.1:9997 |
MediaMTX 控制 API;不得包含 userinfo |
SENSE_RECONCILE_INTERVAL |
5s |
对账周期 |
SENSE_RECONCILE_LEASE_DURATION |
30s |
PostgreSQL due-row 租期;最大 5 分钟 |
SENSE_RECONCILE_OPERATION_TIMEOUT |
20s |
单项 ONVIF/MediaMTX deadline;必须严格短于租期 |
SENSE_PROBE_INTERVAL |
10s |
path 探活周期 |
SENSE_INSTANCE_ID |
随进程随机生成 | 最多 64 位低基数字符串;多实例部署建议显式注入唯一实例 ID |
SENSE_METRICS_ENABLED |
true |
是否注册低基数 Prometheus 文本 /metrics |
SENSE_ORPHAN_SCAN_ENABLED |
PostgreSQL 为 true,SQLite 为 false |
周期执行只读 MediaMTX Path 差异报告;从不自动删除 |
SENSE_ORPHAN_SCAN_INTERVAL |
1m |
孤儿只读扫描周期,最短 10 秒 |
SENSE_ONVIF_MODE |
disabled |
disabled 或 standard;默认不访问真实摄像头 |
SENSE_ONVIF_RTSP_REWRITE_HOST |
空 | NAT 或故障代理场景下重写 ONVIF 返回的 RTSP 主机 |
SENSE_ONVIF_RTSP_REWRITE_PORT |
0 |
非零时重写 ONVIF 返回的 RTSP 端口 |
SENSE_ONVIF_RTSP_STRIP_QUERY |
false |
仅在已验证设备返回不可用查询串时显式移除;默认保留标准 URI 语义 |
SENSE_CONTROL_API_ENABLED |
false |
显式开启 Control API v1;只允许与 PostgreSQL 一起使用 |
SENSE_CONTROL_AUTH_MODE |
static-sha256 |
首版外部摘要注册表适配器;token 格式不属于公共 API 契约 |
SENSE_CONTROL_AUTH_FILE |
空 | 仓库外绝对路径;version 1 JSON 只保存 token SHA-256、主体、tenant、Site scope 和权限 |
SENSE_CONTROL_CURSOR_KEY_FILE |
空 | 仓库外绝对路径;内容为至少 32 字节随机值的无填充 base64url |
SENSE_CONTROL_ALLOW_INSECURE_HTTP |
false |
Control API 非回环明文监听的独立风险接受;正常部署应保持回环并在受控代理终止 TLS |
SENSE_CONSOLE_ENABLED |
false |
显式开启 /sense-console/;T-018 只允许与回环 Control API 一起使用 |
SENSE_CONSOLE_WEBRTC_BASE_URL |
http://127.0.0.1:8889 |
MediaMTX WebRTC 浏览器入口基地址;必须为无 userinfo/path/query/fragment 的显式回环 HTTP(S) URL |
SENSE_AUDIT_RELAY_ENABLED |
false |
显式开启 PostgreSQL Outbox → Bell relay;SQLite 不支持 |
SENSE_AUDIT_RELAY_URL |
空 | 精确指向 Bell /internal/v1/audit-events:batch;非回环必须 HTTPS |
SENSE_AUDIT_RELAY_KEY_FILE |
空 | 仓库外绝对路径 version 1 JSON key 文件,secret 至少 32 字节 |
SENSE_AUDIT_RELAY_KEY_ID |
空 | 本实例用于签名的 key ID |
SENSE_AUDIT_RELAY_INTERVAL |
1s |
队列轮询间隔,最短 1 秒 |
设备台账只保存 env://<key> 凭据引用。真实适配器从进程环境读取以下变量,不把秘密写入 SQLite、日志或 MediaMTX 错误:
SENSE_CREDENTIAL_<KEY>_ONVIF_USERNAME
SENSE_CREDENTIAL_<KEY>_ONVIF_PASSWORD
SENSE_CREDENTIAL_<KEY>_RTSP_USERNAME
SENSE_CREDENTIAL_<KEY>_RTSP_PASSWORD
cmd/sense-lab 是回环实验室播种与脱敏收敛查询工具,不是已冻结的公共设备管理 API。cmd/rtsp-fault-proxy 只用于 T-006 控制真实上游网络路径故障。
MediaMTX v1.19.3 应作为独立二进制启动并只在可信网络开放 API。获取与 SHA-256 校验值见 docs/03-tech-stack.md。生成客户端使用固定版本工具和 vendored 官方 OpenAPI;internal/mtx/generated/client.gen.go 不可手改。
T-009~T-012 PostgreSQL 17.10
初始化与增量 SQL 位于 deploy/postgres/,由高权限部署步骤按文件名前缀执行;Sense 进程不会自动创建角色、schema 或 Bell 对象。bell_app 拥有 Site/Area、配额、capture_policy 及两个版本化视图,sense_app 只能读取两个视图,不能读取或写入 Bell 源表。T-011 的 v4 schema 增加资源版本、24 小时幂等收据和 batch operation;T-012 的 v5 schema 增加数据库时钟租约、Path 历史归属及脱敏孤儿报告/处置结果。表中不保存 MediaMTX source URI。应用登录角色和密码由部署环境创建,不进入仓库。
PostgreSQL 新建设备必须携带匹配 tenant/Site 的 area_id。具有 video_capture 能力的设备在创建、移动 Area 和从 disabled 切到 enabled 时执行 Area 准入;non_imaging_only 拒绝成像设备但允许非成像设备。投影缺失、非法或版本回退只拒绝新变更,不关闭已有流。创建、配置修改和期望态受理都与对应脱敏 Outbox 事实同事务;停用后调和器只删除该设备的精确 MediaMTX path 并收敛为 offline,不枚举未知 path。开启 relay 后,Sense 用数据库 lease/fencing 批量投递,成功、重试和 dead letter 均保留本地事实;Sense 不访问 Bell schema。
Windows 本机集成测试从仓库根目录执行:
./scripts/test_postgres.ps1 -PgRoot D:\pgsql17
脚本要求冻结的 PostgreSQL 17.10,使用 initdb 创建临时 trust 集群并只监听随机回环端口,重放 migration、执行权限断言和 PostgreSQL repository 测试后停止并清理。它会核对现有 5432 listener 前后未变化,不读取或修改 D:\pgsql17\data。生产安装和恢复边界见 deploy/postgres/README.md。
选择 PostgreSQL 运行前,管理员必须已经安装 migration,并私下设置无密码回显的连接环境:
$env:SENSE_DB_DRIVER = 'postgres'
$env:SENSE_DB_DSN = '由部署环境私下设置'
go run ./cmd/sense-api
PostgreSQL 基础启动会检查 Sense v5 migration、当前角色对两个 Bell 投影视图和本地控制/对账表的最小权限;启用 relay 时额外要求 v6 和本地 Outbox 权限,并确认当前 Sense 登录不能访问 Bell 全局审计表。默认 SQLite 路径和 cmd/sense-lab 保持不变,但 SQLite 不实现生产 Area/Outbox、多实例租约、孤儿处置或 relay 语义,Control API、孤儿扫描与 relay 在 SQLite 下不会启动。
孤儿报告与受控处置
sense-api 的 PostgreSQL 模式默认每分钟只读枚举一次 MediaMTX 配置 Path,并持久化汇总。unowned 表示没有 Sense 历史归属证据,永远不会由 Sense 删除;只有 owned_stale 可以进入处置候选。需要人工处置时从 Sense/ 执行:
go run ./cmd/sense-orphan -mode report
go run ./cmd/sense-orphan -mode apply -scan-id scan_... -actor operator-id -confirm "DELETE scan_..."
report 输出不含 source URI 的 JSON 摘要。apply 会再次读取数据库和 MediaMTX,只处理仍属于原快照的 owned_stale,并要求快照不超过 15 分钟、候选不超过 128 且候选占当前全部配置 Path 不超过 10%;任何门禁失败均为零删除,没有 force。完整流程与恢复方法见 ../docs/runbooks/sense-reconciliation.md。
开启 Control API
复制 api/control-auth.example.json 到仓库外受限目录并替换占位项。token_sha256 是至少 128 bit 随机 Bearer token 的 64 位小写 SHA-256,不是 token 明文;site_ids 支持精确 ID 或 "*",权限只接受 sense.devices.read、sense.devices.write。注册表在启动时读取,轮换后需要受控重启。
另在仓库外生成 cursor key。PowerShell 示例只把 key 写入指定秘密文件,不把值打印到日志:
$cursorBytes = [byte[]]::new(32)
[Security.Cryptography.RandomNumberGenerator]::Fill($cursorBytes)
$cursorKey = [Convert]::ToBase64String($cursorBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
Set-Content -LiteralPath 'D:\private\sense-cursor.key' -Value $cursorKey -NoNewline
完成 PostgreSQL migration、专用登录角色和外部文件权限后,以私有环境开启:
$env:SENSE_DB_DRIVER = 'postgres'
$env:SENSE_DB_DSN = '由部署环境私下设置'
$env:SENSE_CONTROL_API_ENABLED = 'true'
$env:SENSE_CONTROL_AUTH_FILE = 'D:\private\sense-auth.json'
$env:SENSE_CONTROL_CURSOR_KEY_FILE = 'D:\private\sense-cursor.key'
go run ./cmd/sense-api
业务响应使用 Cache-Control: no-store;ETag 是写并发令牌,cursor 与认证 tenant/Site/筛选绑定。静态摘要文件只是首版私有部署适配器;公网/TLS、Bell 会话、JWT/OIDC 与热加载需后续任务,不能靠设置 SENSE_CONTROL_ALLOW_INSECURE_HTTP=true 冒充完成。
开启 T-018 回环控制台
先按上一节完成 PostgreSQL、Control API、仓库外 auth/cursor 文件和 MediaMTX 启动,再增加:
$env:SENSE_CONSOLE_ENABLED = 'true'
$env:SENSE_CONSOLE_WEBRTC_BASE_URL = 'http://127.0.0.1:8889'
go run ./cmd/sense-api
浏览器打开 http://127.0.0.1:8080/sense-console/,输入已授权的 Site ID 和原始 Bearer token。token 只保留在当前页面 JavaScript 内存,输入框随即清空,刷新页面后必须重新输入;不得把 token 放进 URL、截图、命令历史或文档。设备列表默认每页 16 项,只有 video_capture + enabled + online + converged 的设备可选择,最多同时启动 4 路预览。
播放使用 MediaMTX v1.19.3 自带浏览器 WebRTC 页面,Sense 不代理媒体字节,也不复制播放器源码。浏览器是否能解码取决于摄像头编码;首选已验收的低码率 H.264 子码流,H.265 或带 B-frame 的 H.264 不能因“Path 在线”就宣称浏览器可播放。本版 Sense 和播放端都必须显式回环;非回环 HTTPS、正式会话认证和 MediaMTX 外部鉴权需另立任务。控制台不会实现或暗示常态录像、录像计划和回放。
Windows 本地准备 MediaMTX(从仓库根目录执行):
$asset = "mediamtx_v1.19.3_windows_amd64.zip"
Invoke-WebRequest "https://github.com/bluenviron/mediamtx/releases/download/v1.19.3/$asset" -OutFile "$env:TEMP\$asset"
if ((Get-FileHash "$env:TEMP\$asset" -Algorithm SHA256).Hash.ToLowerInvariant() -ne "5d82148d1032a6a190d9909a2997d9989457aaadf49af87dd02cd4512d31bebe") { throw "MediaMTX checksum mismatch" }
Expand-Archive "$env:TEMP\$asset" -DestinationPath "$env:TEMP\yovision-mediamtx-v1.19.3" -Force
& "$env:TEMP\yovision-mediamtx-v1.19.3\mediamtx.exe" "Sense\deploy\mediamtx.yml"
Linux amd64 使用同版 mediamtx_v1.19.3_linux_amd64.tar.gz,SHA-256 为 a7ba21268fccda3ebc43fdad76b87fddb85ce77e725b5cb637bca724b5394fbe。不要把下载的二进制或摄像头凭据提交到仓库。
T-006 Windows 集成验证
脚本会启动两套独立 MediaMTX、4 个独立 FFmpeg publisher、真实摄像头网络故障代理和 Sense,在临时目录播种 5 条期望态,执行四类恢复后再连续观察 30 分钟。脚本只输出脱敏计数与时间,不保存视频:
./Sense/scripts/t006-integration.ps1 -CameraEnv D:\path\to\ip_camera.env
ip_camera.env 必须保持在 Git 忽略范围内。调试时可把 -ObservationMinutes 降为 1;正式 T-006 证据必须使用默认 30 分钟,且最终 maximum_unconverged、final_unconverged 都为 0。
T-014 本地 16 路容量基线
T-014 不访问摄像头或客户网络。脚本使用隔离 PostgreSQL v5、真实 Control API、两套 MediaMTX 和 16 个独立的 FFmpeg -c copy 合成 publisher,验证 17 路配额拒绝、三轮批量启停、16 → 0 → 16 Path 收敛、固定四路故障隔离/恢复,以及 30 分钟稳定性和资源观测。
从仓库根目录执行预检、短窗口调试和正式验收:
./Sense/scripts/t014-capacity.ps1 -PgRoot D:\pgsql17 -PreflightOnly
./Sense/scripts/t014-capacity.ps1 -PgRoot D:\pgsql17 -ObservationMinutes 1 -OutputPath (Join-Path $env:TEMP 'yovision-t014-smoke.json')
./Sense/scripts/t014-capacity.ps1 -PgRoot D:\pgsql17 -OutputPath (Join-Path $env:TEMP 'yovision-t014-formal.json')
只有默认不少于 30 分钟且输出 formal_eligible=true 的单次完整运行可作正式证据;调试窗口固定标记为 false。脚本要求 PostgreSQL 17.10、MediaMTX v1.19.3、FFmpeg 8.1.2 和 Sense/ 模块的 Go 1.26.5,只使用随机回环端口,运行后清理临时媒体、二进制、PGDATA 和秘密文件。结果 JSON 不含 token、DSN、端口、设备 ID 或流地址。
2026-08-10 正式结果为 1800.1 s / 180 个 10 秒样本,最大/最终 unconverged=0、最终在线 Path 16、帧错误 0;完整版本、资源数据、失败记录和适用边界见 ../docs/research/sense-16-stream-capacity.md。该结果只是本机低码率合成负载的软件基线,不代表真实 16 摄像头、客户网络、录像、AI/GPU、64/128 路或生产 SLA。