From b643e42535475d375def5860f302f7ea51d4c02a Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Tue, 4 Aug 2026 00:02:20 +0800 Subject: [PATCH 01/14] docs(tasks): claim T-004 --- docs/tasks/T-004.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/tasks/T-004.md b/docs/tasks/T-004.md index 2fddcd5..b580e23 100644 --- a/docs/tasks/T-004.md +++ b/docs/tasks/T-004.md @@ -3,12 +3,12 @@ id: T-004 title: 设计 Sense 与 Bell 交互原型 phase: 0 deps: [T-002] -status: TODO +status: DOING created: 2026-08-03 issue: 6 -context_ref: null -claim_branch: null -work_branch: null +context_ref: 32b9859a405a4d64c03b6d3ae765340bc37fdfa0 +claim_branch: claims/T-004 +work_branch: agent/codex/T-004 write_paths: - docs/tasks/T-004.md - docs/design/sense/index.html @@ -56,7 +56,7 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, - 责任 Agent:codex。 - 唯一写入者:codex。 - 委派:不启用。 -- Gitea:主 Issue 为 #6;领取必须从精确 `context_ref` 建立 claim 与工作分支。 +- Gitea:主 Issue 为 #6;`context_ref` 为 `32b9859a405a4d64c03b6d3ae765340bc37fdfa0`,claim 为 `claims/T-004`,工作分支为 `agent/codex/T-004`。 任何新增写路径先检查与其他活跃任务是否重叠;同一时刻只有一个 Agent 修改本任务的 `write_paths`。 @@ -66,3 +66,8 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, - 依据用户请求、US-001~US-006、IX-001~IX-013 和 MiBeeNvr 本地只读截图定义两个单文件原型范围。 - 人工验收为必需门禁;原型完成后先进入评审,不自动视为最终设计。 + +### 2026-08-03 领取与基线 + +- dispatcher `ila` 已将 Issue #6 分配给 `codex`,claim 与工作分支均从精确 `context_ref` 创建并读回一致。 +- 在独立 worktree `D:\OPC\yovision-T-004` 开始工作;写入范围仅限本任务声明的三个路径。 -- 2.55.0 From f43a4efa805b64a72d66dda205ec18cf11300091 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Tue, 4 Aug 2026 00:23:55 +0800 Subject: [PATCH 02/14] docs(design): add Sense and Bell prototypes --- docs/design/bell/index.html | 443 ++++++++++++++++++++++++++++++ docs/design/sense/index.html | 513 +++++++++++++++++++++++++++++++++++ docs/tasks/T-004.md | 14 + 3 files changed, 970 insertions(+) create mode 100644 docs/design/bell/index.html create mode 100644 docs/design/sense/index.html diff --git a/docs/design/bell/index.html b/docs/design/bell/index.html new file mode 100644 index 0000000..496057e --- /dev/null +++ b/docs/design/bell/index.html @@ -0,0 +1,443 @@ + + + + + + + + YoVision Bell · 预警处置原型 + + + + + +
PROTOTYPE - 仅供枚举交互,非实现依据
+
+ + +
+
+
青藤寄宿学校 · 值班中心客户侧私有实例 · 当前仅匿名安全规则
+
+ + + +
+
+ +
+
+ +
+

实时值班台

先确认高危预警,再查看证据与升级状态;重复投递不会生成重复处置。

值班组在线
+
+
+

待处置预警

6 个未 ack
+
+
+
+ +
+
高危 · S1

危险区域闯入

北围墙 01 · 23:14:08 · EVT-01J8…7A2

+
+
+ + + person · 0.92北围墙 01 · 事件片段事发前 8s · 事发后 12s · 匿名目标 + +
00:11 / 00:20
+
+
+
规则非授权人员进入
置信度0.92
持续时间8.4s
证据状态已校验
+
+
仅显示与本事件关联的 20 秒证据。常态录像仍在客户 NVR,本页不暴露流地址或摄像头凭据。
+
+ + +
+
+ +
+

事件中心

事件是不可变事实;预警与处置结果作为关联记录单独展示。

+
+
今日事件47较昨日 -8%
已触发预警18规则命中后独立创建
误报反馈5进入标注队列
证据可用率97.9%1 个片段可重试
+
+
共 47 条
+ + + +
事件编号时间点位规则事实预警结果证据
EVT-01J8…7A223:14:08北围墙 01非授权人员进入已确认Alert #1842处置中可用
EVT-01J8…69P22:48:31宿舍东通道区域聚集已记录未触发试运行样本可用
EVT-01J8…52C22:19:04实验楼入口危险区域闯入已确认Alert #1839误报重试中
+
+ +
+

规则策略

继承来源、试运行和正式发布清晰分开;空间区域在 Sense 编辑。

+
+
正式生效

非授权人员进入

危险区域 × 全天 × 持续 3 秒

覆盖范围
4 个点位
继承来源
站点模板
今日命中
8
+
试运行

区域内聚集

人数 ≥ 6 × 持续 20 秒 × 夜间

覆盖范围
3 个点位
继承来源
设备覆盖
样本命中
14
+
草稿

方向越线

警戒线 A → B × 上学时段

覆盖范围
1 个点位
继承来源
设备草稿
最近修改
2h 前
+
+
+ +
+

升级链

生产前至少两条独立路径;无回执不能当作成功。

+

校园高危事件 · 默认链

已发布 v4
+
1
值班室 Web + 本地声光立即 · 站点局域网内
主路径
+
2
当班值班员短信立即 · 送达后等待 2 分钟
独立路径
+
3
校级负责人语音未 ack 时升级 · 等待 3 分钟
兜底
+

静默规则

无永久选项

静默仅影响选定规则和点位,到时自动恢复;S1 事件不可静默。

+
+ +
+

运营报表

算法效果和系统 SLA 分开统计;图表同时提供表格导出。

+

近 7 日预警量

按站点日期汇总;可切换到每路每天误报数。

处置结果

本月已闭环事件。

正常处置 61%误报 22%未闭环 17%
+
+ +
+

审计日志

查询、ack、规则发布和证据下载均可追溯;不记录密码与可复用连接串。

+
只读
+ +
时间角色动作对象结果来源
23:14:18站点值班员ACK_ALERTALT-1842成功值班室 Web
22:52:06站点管理员PUBLISH_RULERULE-007成功管理端
22:20:31只读用户VIEW_EVIDENCEEVT-01J8…52C拒绝响应式 H5
+
+
+
+
+ + + +

标记为误报

这只会追加事件 outcome,不会修改原始事件事实和证据。

+ +

创建限时静默

到期自动恢复;S1 高危事件与设备离线运维告警不受此静默影响。

+
+ + + + diff --git a/docs/design/sense/index.html b/docs/design/sense/index.html new file mode 100644 index 0000000..96fde3a --- /dev/null +++ b/docs/design/sense/index.html @@ -0,0 +1,513 @@ + + + + + + + + YoVision Sense · 设备与视频接入原型 + + + + + +
PROTOTYPE - 仅供枚举交互,非实现依据
+
+ + +
+
+
青藤寄宿学校 / 主校区 · 16 / 16 路
+
+ + + +
+
+ +
+
+ +
+

运行总览

先判断是否影响业务,再进入设备或分片定位原因。

+
+
在线设备
15 / 1693.8% · 1 路重连中
+
活跃媒体流
15主/子码流均衡
+
待收敛项
2配置 1 · 时间漂移 1
+
今日设备告警
3与业务预警分开
+
+
+

分片健康

全部可用
+
media-018 / 32
+
media-028 / 32
+
推理绑定16 / 16
+
+

需要处理

2 项
+

北围墙 03
断线 02:14 · 正在第 4 次重连

+

实验楼入口 02
时间漂移 +3.8s · 建议重新校时

+
+
+
+ +
+

实时监控

默认只并发预览 4 路子码流;切页会释放上一页播放器。

第 1 / 4 页
+
+
+ +
+

摄像头

期望态与实际态分开展示;连接地址和凭据始终脱敏。

+
+
已用 16 / 128
+
设备期望态实际态最后在线时间漂移媒体分片操作
+
+
+ +
+

区域与警戒线

在子码流快照上定义规则空间;保存草稿不会立即影响正式策略。

草稿未发布
+
+ + +
+ +
+ + + + + + + 北围墙 01 · 子码流预览CAM-S2-001 · 704 × 576 · 5 FPS + +
多边形:点击画面添加顶点,双击闭合;也可在右侧输入坐标。
+
+
当前草稿:0 个点缩放 100% · 网格吸附关闭
+
+ + +
+
+ +
+

接入任务

默认 16 路也支持批量导入;128 路时不依赖逐路手工操作。

+
+

导入摄像头

配额 16 / 128
选择 CSV 文件或拖到这里

先本地校验列、重复序列号和配额,不上传明文密码。

+

当前任务

进行中

宿舍区第二批 · 8 路

已验证 5 / 8 · 2 路待补凭据 · 1 路重试中

+
+
+ +
+

系统状态

MediaMTX、数据库和对账器只是可观察组件,不替代设备台账真相。

+
+

Sense 服务

状态
健康
运行时间
12d 06h
待处理队列
2
+

MediaMTX

实例
2 / 2
Readers
15
重连
1
+

本地资源

CPU
22%
内存
4.8 / 16 GB
事件缓存
86 MB
+
+
+
+
+
+ + + + +
+

添加摄像头

+
+
+
+
原型使用保留地址;保存后列表仅显示脱敏端点。
+
+
+
+
+

保存前先验证 Profiles、StreamUri 与校时能力。

+
+
+
+
+ + + + diff --git a/docs/tasks/T-004.md b/docs/tasks/T-004.md index b580e23..994af69 100644 --- a/docs/tasks/T-004.md +++ b/docs/tasks/T-004.md @@ -71,3 +71,17 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, - dispatcher `ila` 已将 Issue #6 分配给 `codex`,claim 与工作分支均从精确 `context_ref` 创建并读回一致。 - 在独立 worktree `D:\OPC\yovision-T-004` 开始工作;写入范围仅限本任务声明的三个路径。 + +### 2026-08-04 原型实现 + +- 使用 `ui-ux-pro-max` 生成并复核统一设计系统:深色专业安防控制台、高信息密度、系统字体、语义状态色、150–300ms 微交互、明确焦点和 reduced-motion;Sense 使用青蓝强调接入/画面,Bell 使用紫色强调处置/状态机。 +- `docs/design/sense/index.html` 已覆盖运行总览、4 路按需监控、摄像头期望态/实际态、批量接入、系统健康,以及多边形/方向警戒线的鼠标绘制、坐标键盘替代、撤销、草稿、试运行和发布确认。 +- `docs/design/bell/index.html` 已覆盖预警队列、事件事实与证据、一次点击 ack、升级倒计时和投递事实、处置结果、误报反馈、规则、升级链、报表和审计;弱网时先显示结构化事实并允许证据后台重试。 +- 两个文件均为单文件内联 CSS/JS,无外部依赖、真实视频、客户数据、连接串或人脸入口;顶部均固定显示原型用途提示。 + +### 2026-08-04 自测结果 + +- 标准库 HTML 结构审计通过:两文件均有 viewport、原型横幅、reduced-motion,无重复 `id`、无未命名按钮、无不可识别表单控件和外部 HTTP 资源;Sense 统计 40 个按钮/21 个控件,Bell 统计 42 个按钮/14 个控件。 +- Playwright 在 1440×900 与 375×812 视口实测:两页页面级 `scrollWidth == clientWidth`,桌面三栏和移动底部导航正常,浏览器控制台 0 error / 0 warning;桌面与移动关键状态截图已人工检查,临时 QA 图片未写入仓库。 +- Sense 实测坐标点添加、草稿状态、保存反馈、设备导航、添加摄像头对话框和 Esc 退出;Bell 实测一键 ack、升级计时停止、弱网状态、误报必填校验、错误焦点和 Esc 退出。 +- 尚待项目负责人分别打开两个 HTML,确认页面结构、控件集合和主要工作流;在收到确认前任务保持 `DOING`。 -- 2.55.0 From 2715e557a960d7637d2dadf04801399a45068a01 Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 4 Aug 2026 09:04:50 +0800 Subject: [PATCH 03/14] docs(design): update Sense workflow --- docs/design/sense/index.html | 218 ++++++++++------------------------- 1 file changed, 63 insertions(+), 155 deletions(-) diff --git a/docs/design/sense/index.html b/docs/design/sense/index.html index 96fde3a..8fe99c2 100644 --- a/docs/design/sense/index.html +++ b/docs/design/sense/index.html @@ -99,7 +99,7 @@ .top-actions .select { width: auto; min-width: 126px; } .btn { min-height: 40px; border: 1px solid var(--line); border-radius: 9px; padding: 8px 13px; background: var(--surface-2); color: var(--text); - display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-weight: 700; transition: border-color .2s ease, background .2s ease, opacity .2s ease; + display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-weight: 700; text-decoration: none; transition: border-color .2s ease, background .2s ease, opacity .2s ease; } .btn:hover { border-color: var(--primary); } .btn-primary { background: var(--primary-strong); border-color: var(--primary-strong); color: #fff; } @@ -141,7 +141,7 @@ .search .icon { position: absolute; left: 11px; top: 11px; color: var(--subtle); pointer-events: none; } .search input { padding-left: 38px; } .table-wrap { overflow-x: auto; } - table { width: 100%; border-collapse: collapse; min-width: 820px; } + table { width: 100%; border-collapse: collapse; min-width: 960px; } th, td { padding: 12px 14px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; } th { color: var(--muted); background: var(--surface-2); font-size: 12px; font-weight: 700; } td { font-variant-numeric: tabular-nums; } @@ -157,7 +157,17 @@ .scene { position: absolute; inset: 0; width: 100%; height: 100%; } .camera-meta { padding: 12px 14px; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .camera-meta small { color: var(--muted); } - .zone-layout { display: grid; grid-template-columns: 230px minmax(420px, 1fr) 310px; gap: 12px; min-height: calc(100dvh - 162px); } + .breadcrumb { margin-bottom: 10px; display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px; } + .breadcrumb button { min-height: 32px; padding: 4px 8px; border: 0; border-radius: 7px; background: transparent; color: var(--primary); font-weight: 700; } + .breadcrumb button:hover { background: var(--primary-soft); } + .detail-tabs { margin-bottom: 12px; padding: 5px; display: flex; gap: 5px; overflow-x: auto; border: 1px solid var(--line); border-radius: 11px; background: var(--surface); } + .detail-tabs button { min-height: 40px; padding: 7px 13px; border: 0; border-radius: 8px; background: transparent; color: var(--muted); white-space: nowrap; } + .detail-tabs button.active { background: var(--primary-soft); color: var(--text); box-shadow: inset 0 -2px var(--primary); } + .detail-panel[hidden] { display: none; } + .context-strip { margin-bottom: 12px; padding: 11px 13px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid rgba(167,139,250,.38); border-radius: 10px; background: rgba(167,139,250,.1); } + .context-strip[hidden] { display: none; } + .context-strip p { margin: 0; color: var(--muted); } + .zone-layout { display: grid; grid-template-columns: minmax(420px, 1fr) 320px; gap: 12px; min-height: calc(100dvh - 260px); } .zone-camera-list, .zone-settings, .zone-workspace { min-width: 0; } .compact-list { display: grid; gap: 6px; padding: 10px; } .compact-list button { width: 100%; min-height: 54px; border: 1px solid transparent; border-radius: 9px; background: transparent; color: var(--text); display: flex; align-items: center; gap: 10px; padding: 8px; text-align: left; } @@ -188,6 +198,10 @@ .coordinate-box h3 { margin: 0 0 4px; font-size: 13px; } .coordinate-box p { margin: 0 0 10px; color: var(--muted); font-size: 12px; } .coordinate-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: 6px; } + .linked-rules { margin-top: 12px; padding: 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); } + .linked-rules h3 { margin: 0 0 4px; font-size: 13px; } + .linked-rules p { margin: 0 0 10px; color: var(--muted); font-size: 12px; } + .linked-rules ul { margin: 0 0 11px; padding-left: 18px; color: var(--muted); } .settings-actions { position: sticky; bottom: 0; margin: 14px -16px -16px; padding: 12px 16px; display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid var(--line); background: var(--surface); } .drop-zone { padding: 28px; border: 1px dashed var(--line); border-radius: 12px; text-align: center; background: var(--surface-2); } .drop-zone .icon { width: 28px; height: 28px; color: var(--primary); } @@ -223,8 +237,8 @@ .brand { justify-content: center; padding-inline: 0; } .nav button { justify-content: center; padding: 0; } .nav .count { position: absolute; margin: -24px 0 0 25px; } - .zone-layout { grid-template-columns: 190px minmax(400px, 1fr); } - .zone-settings { grid-column: 1/-1; } + .zone-layout { grid-template-columns: 1fr; } + .zone-settings { grid-column: auto; } .settings-actions { position: static; margin-bottom: -16px; } } @media (max-width: 820px) { @@ -237,142 +251,14 @@ .content { padding: 14px 12px; } .stats, .system-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .split, .zone-layout { grid-template-columns: 1fr; } + .editor-stage { min-height: 0; } + .context-strip, .linked-zone { display: grid; } .zone-camera-list { order: 2; } .zone-workspace { order: 1; } .zone-settings { order: 3; } .camera-grid { grid-template-columns: 1fr; } .mobile-nav { position: fixed; z-index: 50; left: 8px; right: 8px; bottom: 8px; display: grid; grid-template-columns: repeat(5, 1fr); padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); } - .mobile-nav button { min-height: 48px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); display: grid; place-items: center; font-size: 11px; } - .mobile-nav button.active { color: var(--primary); background: var(--primary-soft); } - } - @media (max-width: 560px) { - .stats, .system-grid { grid-template-columns: 1fr; } - .page-head { display: grid; } - .page-actions { width: 100%; } - .page-actions .btn { flex: 1; } - .form-grid { grid-template-columns: 1fr; } - .form-field.full { grid-column: auto; } - .editor-stage { min-height: 230px; } - .coordinate-row { grid-template-columns: 1fr 1fr; } - .coordinate-row .btn { grid-column: 1/-1; } - } - @media (prefers-reduced-motion: reduce) { - *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } - } - - - - - -
PROTOTYPE - 仅供枚举交互,非实现依据
-
- - -
-
-
青藤寄宿学校 / 主校区 · 16 / 16 路
-
- - - -
-
- -
-
- -
-

运行总览

先判断是否影响业务,再进入设备或分片定位原因。

-
-
在线设备
15 / 1693.8% · 1 路重连中
-
活跃媒体流
15主/子码流均衡
-
待收敛项
2配置 1 · 时间漂移 1
-
今日设备告警
3与业务预警分开
-
-
-

分片健康

全部可用
-
media-018 / 32
-
media-028 / 32
-
推理绑定16 / 16
-
-

需要处理

2 项
-

北围墙 03
断线 02:14 · 正在第 4 次重连

-

实验楼入口 02
时间漂移 +3.8s · 建议重新校时

-
-
-
- -
-

实时监控

默认只并发预览 4 路子码流;切页会释放上一页播放器。

第 1 / 4 页
-
-
- -
-

摄像头

期望态与实际态分开展示;连接地址和凭据始终脱敏。

-
-
已用 16 / 128
-
设备期望态实际态最后在线时间漂移媒体分片操作
-
-
- -
-

区域与警戒线

在子码流快照上定义规则空间;保存草稿不会立即影响正式策略。

草稿未发布
-
- - -
- -
- - - + .mobile-nav button { min-h…3676 tokens truncated…> @@ -383,20 +269,21 @@
当前草稿:0 个点缩放 100% · 网格吸附关闭
-
+ + +
@@ -420,7 +307,7 @@
@@ -448,24 +335,39 @@ function notify(message) { toast.textContent = message; toast.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('show'), 3600); } const deviceData = [ - ['北围墙 01','CAM-S2-001','启用','在线','刚刚','+0.2s','media-01','success'], - ['北围墙 03','CAM-S2-003','启用','重连中','2 分钟前','—','media-01','warning'], - ['实验楼入口 02','CAM-S2-006','启用','在线','12 秒前','+3.8s','media-02','warning'], - ['宿舍东通道','CAM-S2-011','启用','在线','4 秒前','+0.1s','media-02','success'], - ['配电房门口','CAM-S2-015','停用','已停止','1 天前','—','未绑定','info'] + ['北围墙 01','CAM-S2-001','启用','在线','刚刚','+0.2s','media-01','success','2 个区域','success'], + ['北围墙 03','CAM-S2-003','启用','重连中','2 分钟前','—','media-01','warning','待校准','warning'], + ['实验楼入口 02','CAM-S2-006','启用','在线','12 秒前','+3.8s','media-02','warning','1 个区域','success'], + ['宿舍东通道','CAM-S2-011','启用','在线','4 秒前','+0.1s','media-02','success','3 个区域','success'], + ['配电房门口','CAM-S2-015','停用','已停止','1 天前','—','未绑定','info','未配置','info'] ]; - $('#deviceRows').innerHTML = deviceData.map(d => `
${d[0]}${d[1]}
${d[2]}${d[3]}${d[4]}${d[5]}${d[6]}`).join(''); + $('#deviceRows').innerHTML = deviceData.map(d => `
${d[0]}${d[1]}
${d[2]}${d[3]}${d[4]}${d[5]}${d[6]}${d[8]}`).join(''); const cameraNames = ['北围墙 01','实验楼入口 02','宿舍东通道','配电房门口']; $('#cameraGrid').innerHTML = cameraNames.map((name, i) => `
${name} · 子码流
${name}704 × 576 · 5 FPS在线
`).join(''); function switchView(name) { $$('.view').forEach(v => v.classList.toggle('active', v.dataset.page === name)); - $$('[data-view]').forEach(b => { const current = b.dataset.view === name; b.toggleAttribute('aria-current', current); b.classList.toggle('active', current); }); + const navName = name === 'device-detail' ? 'devices' : name; + $$('[data-view]').forEach(b => { const current = b.dataset.view === navName; b.toggleAttribute('aria-current', current); b.classList.toggle('active', current); }); $('#stateSelect').value = 'normal'; renderState('normal'); $('#main-content').focus({preventScroll:true}); } $$('[data-view]').forEach(b => b.addEventListener('click', () => switchView(b.dataset.view))); + function openDeviceDetail(serial = 'CAM-S2-001') { + const device = deviceData.find(d => d[1] === serial) || deviceData[0]; + $('#deviceDetailName').textContent = device[0]; $('#deviceBreadcrumb').textContent = device[0]; $('#deviceDetailSerial').textContent = device[1]; + const bellHref = `../bell/index.html?view=rules&camera=${encodeURIComponent(device[1])}&zone=ZONE-007`; + $('#configureRules').href = bellHref; $('#returnToBell').href = bellHref; + switchView('device-detail'); + } + $$('[data-open-camera]').forEach(b => b.addEventListener('click', () => openDeviceDetail(b.dataset.openCamera))); + $('#backToDevices').addEventListener('click', () => switchView('devices')); + $$('.detail-tabs button').forEach(button => button.addEventListener('click', () => { + const name = button.dataset.detailTab; + $$('.detail-tabs button').forEach(b => { const on = b === button; b.classList.toggle('active', on); b.setAttribute('aria-selected', on); }); + $$('[data-detail-panel]').forEach(panel => panel.hidden = panel.dataset.detailPanel !== name); + })); function renderState(state) { const layer = $('#stateLayer'); const views = $$('.view'); @@ -488,7 +390,8 @@ draftLayer.innerHTML = shape + points.map((p,i) => `点 ${i+1}`).join(''); $('#pointStatus').textContent = `当前草稿:${points.length} 个点`; $('#pointList').innerHTML = points.length ? points.map((p,i) => `
  • 点 ${i+1}:${(p.x/9.6).toFixed(1)}%, ${(p.y/5.4).toFixed(1)}%
  • `).join('') : '
  • 尚未添加草稿点
  • '; - $('#draftBadge').textContent = points.length ? '草稿有未保存修改' : '草稿未发布'; + $('#draftBadge').className = points.length ? 'badge warning' : 'badge info'; + $('#draftBadge').textContent = points.length ? '区域有未保存修改' : '区域 v7'; } function addPoint(x,y) { if (activeTool === 'pointer') return; if (activeTool === 'line' && points.length >= 2) points.length = 0; points.push({x:Math.max(0,Math.min(960,x)),y:Math.max(0,Math.min(540,y))}); drawDraft(); } zoneCanvas.addEventListener('click', e => { const r = zoneCanvas.getBoundingClientRect(); addPoint((e.clientX-r.left)/r.width*960,(e.clientY-r.top)/r.height*540); }); @@ -497,10 +400,9 @@ $('#undoPoint').addEventListener('click', () => { points.pop(); drawDraft(); }); $('#clearPoints').addEventListener('click', () => { if (points.length && confirm('清空当前未保存的绘制草稿?')) { points.length=0; drawDraft(); } }); $('#addCoordinate').addEventListener('click', () => { const x=Number($('#coordX').value), y=Number($('#coordY').value); if (!Number.isFinite(x)||!Number.isFinite(y)||x<0||x>100||y<0||y>100) { notify('坐标必须在 0–100% 之间。'); return; } addPoint(x*9.6,y*5.4); }); - $('#discardDraft').addEventListener('click', () => { if (!points.length || confirm('放弃未保存的区域修改?')) { points.length=0; drawDraft(); notify('已放弃本次修改,正式规则未受影响。'); } }); - $('#saveDraft').addEventListener('click', () => { if ((activeTool==='polygon'&&points.length<3)||(activeTool==='line'&&points.length<2)) { notify(activeTool==='line'?'警戒线至少需要 2 个点。':'多边形至少需要 3 个点。'); return; } notify('草稿已保存,尚未正式发布。'); $('#draftBadge').textContent='草稿已保存'; }); - $('#publishRule').addEventListener('click', () => { if (confirm('发布后将影响“北围墙 01”的正式规则。确认发布当前已保存草稿?')) { $('#draftBadge').className='badge success'; $('#draftBadge').textContent='规则已发布'; notify('规则版本 v7 已发布,可在版本记录中回滚。'); } }); - $('#dryRun').addEventListener('change', e => notify(e.target.checked?'已启用试运行:命中不会触发正式预警。':'已关闭试运行:发布后将进入正式链路。')); + $('#discardDraft').addEventListener('click', () => { if (!points.length || confirm('放弃未保存的区域修改?')) { points.length=0; drawDraft(); notify('已放弃本次修改,已保存区域和 Bell 规则均未改变。'); } }); + $('#saveDraft').addEventListener('click', () => { if ((activeTool==='polygon'&&points.length<3)||(activeTool==='line'&&points.length<2)) { notify(activeTool==='line'?'警戒线至少需要 2 个点。':'多边形至少需要 3 个点。'); return; } points.length=0; drawDraft(); $('#draftBadge').className='badge success'; $('#draftBadge').textContent='区域 v8 已保存'; notify('检测区域 v8 已保存;关联的 Bell 规则不会被自动发布。'); }); + $('#simulateCalibration').addEventListener('click', () => { $('#calibrationBadge').className='badge warning'; $('#calibrationBadge').textContent='区域待校准'; notify('画面分辨率或旋转发生变化,现有区域已标记为待校准。'); }); const dialog = $('#cameraDialog'); $('#openCameraDialog').addEventListener('click', () => dialog.showModal()); @@ -508,6 +410,12 @@ $('#saveCamera').addEventListener('click', () => notify('摄像头已保存为“待激活”,凭据不会显示在列表中。')); $('#fakeUpload').addEventListener('click', () => notify('演示:已选择 cameras-demo.csv,检测到 1 个重复序列号。')); drawDraft(); + const query = new URLSearchParams(location.search); + if (query.get('view') === 'device-detail' || query.has('camera')) { + openDeviceDetail(query.get('camera') || 'CAM-S2-001'); + if (query.get('return') === 'bell-rule') $('#ruleReturnContext').hidden = false; + } + -- 2.55.0 From 5251199205d25eec5b544e7556e9f1426c7f8019 Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 4 Aug 2026 09:04:52 +0800 Subject: [PATCH 04/14] docs(design): update Bell workflow --- docs/design/bell/index.html | 187 ++++++++---------------------------- 1 file changed, 42 insertions(+), 145 deletions(-) diff --git a/docs/design/bell/index.html b/docs/design/bell/index.html index 496057e..eaceefe 100644 --- a/docs/design/bell/index.html +++ b/docs/design/bell/index.html @@ -70,7 +70,7 @@ .site strong { display: block; font-size: 15px; } .site span { color: var(--muted); font-size: 12px; } .top-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; } - .btn { min-height: 40px; border: 1px solid var(--line); border-radius: 9px; padding: 8px 13px; background: var(--surface-2); color: var(--text); display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-weight: 750; transition: .2s ease; } + .btn { min-height: 40px; border: 1px solid var(--line); border-radius: 9px; padding: 8px 13px; background: var(--surface-2); color: var(--text); display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-weight: 750; text-decoration: none; transition: .2s ease; } .btn:hover { border-color: var(--primary); } .btn:disabled { cursor: not-allowed; opacity: .5; } .btn-primary { background: var(--primary-strong); border-color: var(--primary-strong); color: white; } @@ -173,6 +173,14 @@ .rule-card p { margin: 0 0 14px; color: var(--muted); } .rule-card dl { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 7px; color: var(--muted); } .rule-card dd { margin: 0; color: var(--text); } + .rule-card-actions { margin-top: 14px; padding-top: 12px; display: flex; flex-wrap: wrap; gap: 7px; border-top: 1px solid var(--line); } + .rule-context { margin-bottom: 12px; padding: 11px 13px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid rgba(56,189,248,.34); border-radius: 10px; background: rgba(56,189,248,.08); } + .rule-context[hidden] { display: none; } + .rule-context p { margin: 0; color: var(--muted); } + .linked-zone { grid-column: 1/-1; padding: 11px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); } + .linked-zone strong, .linked-zone small { display: block; } + .linked-zone small { color: var(--muted); } + .workflow-note { grid-column: 1/-1; margin: 0; padding: 10px 11px; border-left: 3px solid var(--primary); background: var(--primary-soft); color: var(--muted); font-size: 12px; } .chain-grid { grid-template-columns: minmax(0, 1.4fr) minmax(300px, .8fr); } .chain-row { padding: 13px 14px; display: grid; grid-template-columns: 40px 1fr auto; gap: 11px; align-items: center; border-bottom: 1px solid var(--line); } .chain-row:last-child { border-bottom: 0; } @@ -199,6 +207,7 @@ .skeleton i:nth-child(2) { width: 72%; } @keyframes shimmer { to { background-position: -200% 0; } } dialog { width: min(610px, calc(100% - 28px)); max-height: 88dvh; padding: 0; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); color: var(--text); box-shadow: 0 24px 80px rgba(0,0,0,.5); } + #ruleDialog { width: min(760px, calc(100% - 28px)); } dialog::backdrop { background: rgba(4,7,15,.68); backdrop-filter: blur(4px); } .dialog-head, .dialog-foot { padding: 14px 18px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 10px; } .dialog-head h2 { margin: 0; font-size: 17px; } @@ -221,150 +230,8 @@ .nav .count { position: absolute; margin: -24px 0 0 25px; } .duty-grid { grid-template-columns: 285px minmax(390px, 1fr); } .action-panel { grid-column: 1/-1; } - .action-scroll { max-height: none; display: grid; grid-template-columns: repeat(3, 1fr); } - .disposition { border-top: 0; border-left: 1px solid var(--line); } - } - @media (max-width: 850px) { - :root { --sidebar: 0px; } - .app { display: block; padding-bottom: 66px; } - .sidebar { display: none; } - .topbar { padding: 8px 12px; } - .site span, .top-actions .select { display: none; } - .content { padding: 12px; } - .duty-grid, .chain-grid, .chart-grid { grid-template-columns: 1fr; } - .alert-list, .action-scroll { max-height: none; } - .action-scroll { display: block; } - .disposition { border-left: 0; border-top: 1px solid var(--line); } - .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .rule-grid { grid-template-columns: 1fr 1fr; } - .fact-grid { grid-template-columns: repeat(2, 1fr); } - .mobile-nav { position: fixed; z-index: 50; left: 8px; right: 8px; bottom: 8px; display: grid; grid-template-columns: repeat(5, 1fr); padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); } - .mobile-nav button { min-height: 48px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); display: grid; place-items: center; font-size: 11px; } - .mobile-nav button.active { color: #c9c1ff; background: var(--primary-soft); } - } - @media (max-width: 560px) { - .page-head { display: grid; } - .stats, .rule-grid { grid-template-columns: 1fr; } - .form-grid { grid-template-columns: 1fr; } - .form-field.full { grid-column: auto; } - .video-stage { margin-inline: 8px; } - .fact-grid { margin-inline: 8px; } - .evidence-summary { display: grid; } - .disposition-actions { grid-template-columns: 1fr; } - } - @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } } - - - - - -
    PROTOTYPE - 仅供枚举交互,非实现依据
    -
    - - -
    -
    -
    青藤寄宿学校 · 值班中心客户侧私有实例 · 当前仅匿名安全规则
    -
    - - - -
    -
    - -
    -
    - -
    -

    实时值班台

    先确认高危预警,再查看证据与升级状态;重复投递不会生成重复处置。

    值班组在线
    -
    -
    -

    待处置预警

    6 个未 ack
    -
    -
    -
    - -
    -
    高危 · S1

    危险区域闯入

    北围墙 01 · 23:14:08 · EVT-01J8…7A2

    -
    -
    - - - person · 0.92北围墙 01 · 事件片段事发前 8s · 事发后 12s · 匿名目标 - -
    00:11 / 00:20
    -
    -
    -
    规则非授权人员进入
    置信度0.92
    持续时间8.4s
    证据状态已校验
    -
    -
    仅显示与本事件关联的 20 秒证据。常态录像仍在客户 NVR,本页不暴露流地址或摄像头凭据。
    -
    - - -
    -
    - -
    -

    事件中心

    事件是不可变事实;预警与处置结果作为关联记录单独展示。

    -
    -
    今日事件47较昨日 -8%
    已触发预警18规则命中后独立创建
    误报反馈5进入标注队列
    证据可用率97.9%1 个片段可重试
    -
    -
    共 47 条
    - - - -
    事件编号时间点位规则事实预警结果证据
    EVT-01J8…7A223:14:08北围墙 01非授权人员进入已确认Alert #1842处置中可用
    EVT-01J8…69P22:48:31宿舍东通道区域聚集已记录未触发试运行样本可用
    EVT-01J8…52C22:19:04实验楼入口危险区域闯入已确认Alert #1839误报重试中
    -
    - -
    -

    规则策略

    继承来源、试运行和正式发布清晰分开;空间区域在 Sense 编辑。

    -
    -
    正式生效

    非授权人员进入

    危险区域 × 全天 × 持续 3 秒

    覆盖范围
    4 个点位
    继承来源
    站点模板
    今日命中
    8
    -
    试运行

    区域内聚集

    人数 ≥ 6 × 持续 20 秒 × 夜间

    覆盖范围
    3 个点位
    继承来源
    设备覆盖
    样本命中
    14
    -
    草稿

    方向越线

    警戒线 A → B × 上学时段

    覆盖范围
    1 个点位
    继承来源
    设备草稿
    最近修改
    2h 前
    + .action-scroll { max-height: none; display: grid; grid-template-columns: rep…4257 tokens truncated…t>覆盖范围
    3 个点位
    检测区域
    3 个区域版本
    继承来源
    设备覆盖
    样本命中
    14
    +
    草稿

    方向越线

    警戒线 A → B × 上学时段

    覆盖范围
    1 个点位
    检测区域
    宿舍东通道 · 警戒线 v3
    继承来源
    设备草稿
    最近修改
    2h 前
    查看警戒线
    @@ -394,6 +261,18 @@ +

    配置告警规则

    规则草稿 · 尚未正式生效
    +
    +
    +
    +
    +
    空间坐标由摄像头详情维护区域版本变化不会绕过规则试运行或自动正式发布。编辑检测区域
    +
    +
    +
    +

    产品流程:先确认摄像头和检测区域,再配置业务条件;试运行样本达到现场评审要求后,才允许正式发布。

    +
    +

    标记为误报

    这只会追加事件 outcome,不会修改原始事件事实和证据。

    创建限时静默

    到期自动恢复;S1 高危事件与设备离线运维告警不受此静默影响。

    @@ -436,8 +315,26 @@ $('#muteButton').addEventListener('click',()=>$('#muteDialog').showModal()); $('#confirmMute').addEventListener('click',e=>{if(!$('#muteCause').value.trim()){e.preventDefault();notify('请填写静默原因。');$('#muteCause').focus();return;}notify('已创建限时静默,到期将自动恢复并通知值班组。');}); + const ruleDialog = $('#ruleDialog'); + function openRuleEditor() { ruleDialog.showModal(); } + $('#openRuleDialog').addEventListener('click', openRuleEditor); + $('#continueRuleDraft').addEventListener('click', openRuleEditor); + $$('[data-edit-rule]').forEach(button=>button.addEventListener('click', openRuleEditor)); + $('#ruleCamera').addEventListener('change',e=>{ $('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent(e.target.value)}&return=bell-rule`; notify('已切换摄像头,请确认检测区域版本与画面仍匹配。'); }); + $('#saveRuleDraft').addEventListener('click',()=>notify($('#startDryRun').checked?'规则草稿已保存并进入试运行,不会触发正式预警。':'规则草稿已保存,尚未开始试运行。')); + $('#publishTrialRule').addEventListener('click',()=>{if(confirm('正式发布后将影响 3 个点位并启动预警链。确认发布当前试运行版本?')){$('#trialBadge').className='badge success';$('#trialBadge').textContent='正式生效';notify('规则版本已正式发布,变更已记录审计并可回滚。');}}); + let remain=102; setInterval(()=>{if($('#ackState').textContent==='已 ack'||remain<=0)return;remain--;const m=String(Math.floor(remain/60)).padStart(2,'0'),s=String(remain%60).padStart(2,'0');$('#countdown').textContent=`${m}:${s}`;},1000); + const routeParams = new URLSearchParams(location.search); + if (routeParams.get('view')) switchView(routeParams.get('view')); + if (routeParams.has('camera')) { + switchView('rules'); $('#ruleContext').hidden=false; + const camera=routeParams.get('camera'); if([...$('#ruleCamera').options].some(option=>option.value===camera)) $('#ruleCamera').value=camera; + $('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent(camera)}&return=bell-rule`; + notify('已保留摄像头与检测区域上下文,可继续配置规则。'); + } + -- 2.55.0 From 618174cbf2880a03de34d2394a47202b636c9421 Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 4 Aug 2026 09:04:58 +0800 Subject: [PATCH 05/14] docs(task): record product review revision --- docs/tasks/T-004.md | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/tasks/T-004.md b/docs/tasks/T-004.md index 994af69..e25a7bc 100644 --- a/docs/tasks/T-004.md +++ b/docs/tasks/T-004.md @@ -19,30 +19,34 @@ write_paths: Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前,需要用两个零构建依赖的单文件 HTML 原型枚举导航、控件、主要状态与高频工作流,避免直接从实现代码猜交互。Sense 可参考 MiBeeNvr 的监控、摄像头与仪表盘信息架构,但必须补齐 YoVision 的设备健康、分片/期望态、批量开通和区域/警戒线编辑;Bell 必须体现事件与 Alert 分离、ack、升级链、证据、误报和权限边界。 +产品评审进一步区分“检测区域”和“告警规则”:前者依附具体摄像头画面、分辨率与坐标系,应在摄像头详情中编辑;后者包含场景、继承、时段、持续时间、试运行、发布与升级链,应由 Bell 管理。后端系统边界不得迫使用户丢失当前摄像头或规则草稿上下文。 + ## 关联需求与交互(如适用) -- 用户故事:Sense 关联 US-001、US-002、US-005;Bell 关联 US-003~US-006。 -- 交互清单:Sense 关联 IX-001~IX-004、IX-010~IX-011;Bell 关联 IX-005~IX-013。 +- 用户故事:Sense 关联 US-001、US-002,并为 US-005 提供摄像头空间配置;Bell 关联 US-003~US-006,并拥有 US-005 的业务规则工作流。 +- 交互清单:Sense 关联 IX-001~IX-004,并提供 IX-010 所需的区域/警戒线几何;Bell 关联 IX-005~IX-013,负责 IX-010~IX-011 的规则继承、试运行和发布。 - 相关页面 / 路由:原型阶段为 `docs/design/sense/index.html` 与 `docs/design/bell/index.html`,不定义生产路由。 ## 方案 1. 使用统一的 YoVision 深色安防控制台设计系统,Sense 与 Bell 保持品牌、状态色、控件和导航规律一致。 -2. Sense 制作为单页可交互工作台:总览、监控墙、设备、区域规则、接入任务和系统状态;区域编辑器支持多边形、警戒线、撤销、清空、键盘替代和保存反馈。 -3. Bell 制作为单页可交互值班台:预警队列、事件证据、ack/处置、升级时间线、误报反馈、规则和升级链概览。 -4. 两个原型均内联 CSS/JS,使用假数据,不加载真实视频、客户信息、生产地址或外部依赖;顶部固定标注原型用途。 -5. 完成桌面、窄屏、键盘焦点、加载/空态/错误切换和 `prefers-reduced-motion` 检查,提交人工评审;产品确认前不把任务标为 `DONE`。 +2. Sense 制作为单页可交互工作台:总览、监控墙、摄像头、接入任务和系统状态;不设置一级“区域规则”。从摄像头列表进入详情,在“画面与检测区域”页签完成多边形、警戒线、撤销、清空、键盘替代、区域版本和待校准反馈。 +3. Sense 只保存摄像头空间几何并只读显示关联规则,不提供业务规则绑定、时段、持续时间、试运行或正式发布;摄像头画面参数变化时将区域标记为待校准。 +4. Bell 制作为单页可交互值班台:除预警队列、事件证据、ack/处置、升级时间线、误报反馈和升级链外,规则编辑必须选择摄像头与检测区域版本,并负责继承、时段、持续时间、试运行、发布和回滚。 +5. Sense 摄像头详情与 Bell 规则草稿通过可演示的相对深链互相进入,携带摄像头/区域上下文;原型可以跨文件,但生产产品不得要求用户重新查找同一摄像头或丢失未完成草稿。 +6. 两个原型均内联 CSS/JS,使用假数据,不加载真实视频、客户信息、生产地址或外部依赖;顶部固定标注原型用途。 +7. 完成桌面、窄屏、键盘焦点、加载/空态/错误切换和 `prefers-reduced-motion` 检查,提交人工评审;产品确认前不把任务标为 `DONE`。 ## 不可变约束 - 阈值 / 数值边界:默认 16 路、单站点上限 128 路;静默最长 4 小时;原型不得暗示 128 路同时播放主码流。 -- 判定式 / 状态转换:事件事实不可被处置改写;Alert 的首次投递、送达、看到、ack 和升级必须分开展示;区域规则正式发布与试运行必须可区分。 +- 判定式 / 状态转换:事件事实不可被处置改写;Alert 的首次投递、送达、看到、ack 和升级必须分开展示;检测区域版本与告警规则版本是不同对象;规则正式发布与试运行必须可区分,保存区域不得自动发布规则。 - 安全边界:不得展示摄像头密码、完整 RTSP/ONVIF 地址、token、真实客户/未成年人数据或人脸入口;破坏性动作须确认。 - 既有契约:原型不修改事件 v0.1、API、路由和生产技术栈,不作为生产实现代码。 ## 验收要点 -- 任务相关验证:两个 HTML 可本地打开、无外部依赖;关键按钮和状态切换可用;运行 HTML 结构检查、`git diff --check` 与 `./init.ps1`。 +- 任务相关验证:两个 HTML 可本地打开、无外部依赖;关键按钮和状态切换可用;Sense 一级导航无“区域规则”,摄像头详情可编辑检测区域;Bell 可选择摄像头/区域并演示试运行与正式发布;双向深链保留上下文;运行 HTML 结构检查、`git diff --check` 与 `./init.ps1`。 - 完整门禁:本任务不修改 schema/API/路由,因此不触发契约与导航实现门禁;若范围变化则先更新任务。 - 人工 / 设备验收:必需。项目负责人分别打开 Sense 与 Bell 原型,确认页面结构、控件集合和主要工作流;未确认前不得标记 `DONE` 或合并为最终设计。 - 构建产物:`docs/design/sense/index.html`、`docs/design/bell/index.html`;双击浏览器打开,零构建依赖。 @@ -75,7 +79,7 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, ### 2026-08-04 原型实现 - 使用 `ui-ux-pro-max` 生成并复核统一设计系统:深色专业安防控制台、高信息密度、系统字体、语义状态色、150–300ms 微交互、明确焦点和 reduced-motion;Sense 使用青蓝强调接入/画面,Bell 使用紫色强调处置/状态机。 -- `docs/design/sense/index.html` 已覆盖运行总览、4 路按需监控、摄像头期望态/实际态、批量接入、系统健康,以及多边形/方向警戒线的鼠标绘制、坐标键盘替代、撤销、草稿、试运行和发布确认。 +- 首版 `docs/design/sense/index.html` 覆盖运行总览、4 路按需监控、摄像头期望态/实际态、批量接入、系统健康,以及多边形/方向警戒线的鼠标绘制、坐标键盘替代、撤销、草稿、试运行和发布确认;后续产品评审修订见下文。 - `docs/design/bell/index.html` 已覆盖预警队列、事件事实与证据、一次点击 ack、升级倒计时和投递事实、处置结果、误报反馈、规则、升级链、报表和审计;弱网时先显示结构化事实并允许证据后台重试。 - 两个文件均为单文件内联 CSS/JS,无外部依赖、真实视频、客户数据、连接串或人脸入口;顶部均固定显示原型用途提示。 @@ -85,3 +89,17 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, - Playwright 在 1440×900 与 375×812 视口实测:两页页面级 `scrollWidth == clientWidth`,桌面三栏和移动底部导航正常,浏览器控制台 0 error / 0 warning;桌面与移动关键状态截图已人工检查,临时 QA 图片未写入仓库。 - Sense 实测坐标点添加、草稿状态、保存反馈、设备导航、添加摄像头对话框和 Esc 退出;Bell 实测一键 ack、升级计时停止、弱网状态、误报必填校验、错误焦点和 Esc 退出。 - 尚待项目负责人分别打开两个 HTML,确认页面结构、控件集合和主要工作流;在收到确认前任务保持 `DOING`。 + +### 2026-08-04 产品评审修订 + +- 项目负责人指出 Sense 的区域规则没有融合到摄像头模块。复核 US-001/US-002 与 US-005 的角色和任务后,确认原型把“摄像头空间几何”和“业务告警规则”混为同一一级模块,且让微服务边界影响了用户操作路径。 +- 采纳“领域分离、体验连续”的产品方案:Sense 摄像头详情维护检测区域与校准状态;Bell 维护业务规则、试运行和正式发布;通过携带摄像头/区域上下文的深链连接两个原型。 +- `ui-ux-pro-max` 的导航层级、渐进披露、深链与状态保持原则用于本轮修订;默认 16 路从摄像头详情进入,128 路仍通过设备列表的筛选、分页与区域状态列治理,不恢复独立的一级区域编辑入口。 + +### 2026-08-04 修订后自测 + +- 标准库 HTML 审计通过:Sense 69 个唯一 `id`、40 个有名称按钮、16 个有标签控件;Bell 67 个唯一 `id`、50 个有名称按钮、21 个有标签控件;两页均无外部 HTTP 资源。专项断言确认 Sense 不再存在一级 `zones` 页面、规则发布按钮或试运行开关。 +- Node.js 对两个内联脚本执行语法编译检查通过。Edge Headless/CDP 在 1440×900 与 375×812 验证摄像头详情、坐标绘制、区域保存、画面参数变化后待校准、Sense→Bell 深链、Bell 规则草稿、Bell→Sense 返回链和正式发布确认;页面无横向溢出,控制台 0 error,运行时 0 exception。 +- 浏览器测试首次发现 Sense 移动端区域画布受 `min-height + aspect-ratio` 影响发生横向溢出,并发现条件上下文条的 `hidden` 被组件 `display` 覆盖;两项均已修复并重新通过桌面/移动截图人工检查。临时 QA 截图未写入仓库。 +- `./init.ps1` 通过:上下文校验、16 个治理单测与完整治理检查均成功;`git diff --check` 通过。任务继续保持 `DOING`,等待项目负责人确认修订后的信息架构与工作流。 + -- 2.55.0 From cce38003cb237bd49d2d0d2b34e10f4d0070f0ff Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 4 Aug 2026 09:07:18 +0800 Subject: [PATCH 06/14] docs(design): restore complete Sense prototype --- docs/design/sense/index.html | 140 ++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 2 deletions(-) diff --git a/docs/design/sense/index.html b/docs/design/sense/index.html index 8fe99c2..5fffcad 100644 --- a/docs/design/sense/index.html +++ b/docs/design/sense/index.html @@ -258,7 +258,144 @@ .zone-settings { order: 3; } .camera-grid { grid-template-columns: 1fr; } .mobile-nav { position: fixed; z-index: 50; left: 8px; right: 8px; bottom: 8px; display: grid; grid-template-columns: repeat(5, 1fr); padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); } - .mobile-nav button { min-h…3676 tokens truncated…> + .mobile-nav button { min-height: 48px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); display: grid; place-items: center; font-size: 11px; } + .mobile-nav button.active { color: var(--primary); background: var(--primary-soft); } + } + @media (max-width: 560px) { + .stats, .system-grid { grid-template-columns: 1fr; } + .page-head { display: grid; } + .page-actions { width: 100%; } + .page-actions .btn { flex: 1; } + .form-grid { grid-template-columns: 1fr; } + .form-field.full { grid-column: auto; } + .editor-stage { min-height: 0; } + .coordinate-row { grid-template-columns: 1fr 1fr; } + .coordinate-row .btn { grid-column: 1/-1; } + } + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } + } + + + + + +
    PROTOTYPE - 仅供枚举交互,非实现依据
    +
    + + +
    +
    +
    青藤寄宿学校 / 主校区 · 16 / 16 路
    +
    + + + +
    +
    + +
    +
    + +
    +

    运行总览

    先判断是否影响业务,再进入设备或分片定位原因。

    +
    +
    在线设备
    15 / 1693.8% · 1 路重连中
    +
    活跃媒体流
    15主/子码流均衡
    +
    待收敛项
    2配置 1 · 时间漂移 1
    +
    今日设备告警
    3与业务预警分开
    +
    +
    +

    分片健康

    全部可用
    +
    media-018 / 32
    +
    media-028 / 32
    +
    推理绑定16 / 16
    +
    +

    需要处理

    2 项
    +

    北围墙 03
    断线 02:14 · 正在第 4 次重连

    +

    实验楼入口 02
    时间漂移 +3.8s · 建议重新校时

    +
    +
    +
    + +
    +

    实时监控

    默认只并发预览 4 路子码流;切页会释放上一页播放器。

    第 1 / 4 页
    +
    +
    + +
    +

    摄像头

    期望态与实际态分开展示;连接地址和凭据始终脱敏。

    +
    +
    已用 16 / 128
    +
    设备期望态实际态最后在线时间漂移媒体分片检测区域操作
    +
    +
    + +
    + + +

    北围墙 01

    CAM-S2-001 · 当前摄像头的接入、画面与空间配置。

    区域已校准
    +
    + + + + +
    + + + +
    +

    画面与检测区域

    区域依附当前摄像头坐标系;这里只保存空间几何,不发布告警规则。

    区域 v7
    +
    +
    + +
    + + + @@ -418,4 +555,3 @@ - -- 2.55.0 From 9a82b17b10158097a36e7bfb596ffdb9738712c8 Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 4 Aug 2026 09:07:23 +0800 Subject: [PATCH 07/14] docs(design): restore complete Bell prototype --- docs/design/bell/index.html | 146 +++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 2 deletions(-) diff --git a/docs/design/bell/index.html b/docs/design/bell/index.html index eaceefe..377d517 100644 --- a/docs/design/bell/index.html +++ b/docs/design/bell/index.html @@ -230,7 +230,150 @@ .nav .count { position: absolute; margin: -24px 0 0 25px; } .duty-grid { grid-template-columns: 285px minmax(390px, 1fr); } .action-panel { grid-column: 1/-1; } - .action-scroll { max-height: none; display: grid; grid-template-columns: rep…4257 tokens truncated…t>覆盖范围
    3 个点位
    检测区域
    3 个区域版本
    继承来源
    设备覆盖
    样本命中
    14
    + .action-scroll { max-height: none; display: grid; grid-template-columns: repeat(3, 1fr); } + .disposition { border-top: 0; border-left: 1px solid var(--line); } + } + @media (max-width: 850px) { + :root { --sidebar: 0px; } + .app { display: block; padding-bottom: 66px; } + .sidebar { display: none; } + .topbar { padding: 8px 12px; } + .site span, .top-actions .select { display: none; } + .content { padding: 12px; } + .duty-grid, .chain-grid, .chart-grid { grid-template-columns: 1fr; } + .alert-list, .action-scroll { max-height: none; } + .action-scroll { display: block; } + .disposition { border-left: 0; border-top: 1px solid var(--line); } + .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .rule-grid { grid-template-columns: 1fr 1fr; } + .fact-grid { grid-template-columns: repeat(2, 1fr); } + .mobile-nav { position: fixed; z-index: 50; left: 8px; right: 8px; bottom: 8px; display: grid; grid-template-columns: repeat(5, 1fr); padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); } + .mobile-nav button { min-height: 48px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); display: grid; place-items: center; font-size: 11px; } + .mobile-nav button.active { color: #c9c1ff; background: var(--primary-soft); } + } + @media (max-width: 560px) { + .page-head { display: grid; } + .stats, .rule-grid { grid-template-columns: 1fr; } + .form-grid { grid-template-columns: 1fr; } + .form-field.full { grid-column: auto; } + .video-stage { margin-inline: 8px; } + .fact-grid { margin-inline: 8px; } + .evidence-summary { display: grid; } + .disposition-actions { grid-template-columns: 1fr; } + } + @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } } + + + + + +
    PROTOTYPE - 仅供枚举交互,非实现依据
    +
    + + +
    +
    +
    青藤寄宿学校 · 值班中心客户侧私有实例 · 当前仅匿名安全规则
    +
    + + + +
    +
    + +
    +
    + +
    +

    实时值班台

    先确认高危预警,再查看证据与升级状态;重复投递不会生成重复处置。

    值班组在线
    +
    +
    +

    待处置预警

    6 个未 ack
    +
    +
    +
    + +
    +
    高危 · S1

    危险区域闯入

    北围墙 01 · 23:14:08 · EVT-01J8…7A2

    +
    +
    + + + person · 0.92北围墙 01 · 事件片段事发前 8s · 事发后 12s · 匿名目标 + +
    00:11 / 00:20
    +
    +
    +
    规则非授权人员进入
    置信度0.92
    持续时间8.4s
    证据状态已校验
    +
    +
    仅显示与本事件关联的 20 秒证据。常态录像仍在客户 NVR,本页不暴露流地址或摄像头凭据。
    +
    + + +
    +
    + +
    +

    事件中心

    事件是不可变事实;预警与处置结果作为关联记录单独展示。

    +
    +
    今日事件47较昨日 -8%
    已触发预警18规则命中后独立创建
    误报反馈5进入标注队列
    证据可用率97.9%1 个片段可重试
    +
    +
    共 47 条
    + + + +
    事件编号时间点位规则事实预警结果证据
    EVT-01J8…7A223:14:08北围墙 01非授权人员进入已确认Alert #1842处置中可用
    EVT-01J8…69P22:48:31宿舍东通道区域聚集已记录未触发试运行样本可用
    EVT-01J8…52C22:19:04实验楼入口危险区域闯入已确认Alert #1839误报重试中
    +
    + +
    + +

    规则策略

    Bell 管理场景、范围、时段、持续时间、试运行与发布;检测区域来自对应摄像头的版本化空间配置。

    +
    +
    正式生效

    非授权人员进入

    危险区域 × 全天 × 持续 3 秒

    覆盖范围
    4 个点位
    检测区域
    北围墙危险区域 v7
    继承来源
    站点模板
    今日命中
    8
    校准检测区域
    +
    试运行

    区域内聚集

    人数 ≥ 6 × 持续 20 秒 × 夜间

    覆盖范围
    3 个点位
    检测区域
    3 个区域版本
    继承来源
    设备覆盖
    样本命中
    14
    草稿

    方向越线

    警戒线 A → B × 上学时段

    覆盖范围
    1 个点位
    检测区域
    宿舍东通道 · 警戒线 v3
    继承来源
    设备草稿
    最近修改
    2h 前
    查看警戒线
    @@ -337,4 +480,3 @@ - -- 2.55.0 From 122cde493817e72f2d7d9fb6ed3c40cbdd6e1821 Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 4 Aug 2026 09:07:26 +0800 Subject: [PATCH 08/14] docs(task): preserve complete T-004 revision record --- docs/tasks/T-004.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/tasks/T-004.md b/docs/tasks/T-004.md index e25a7bc..a69bfce 100644 --- a/docs/tasks/T-004.md +++ b/docs/tasks/T-004.md @@ -102,4 +102,3 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, - Node.js 对两个内联脚本执行语法编译检查通过。Edge Headless/CDP 在 1440×900 与 375×812 验证摄像头详情、坐标绘制、区域保存、画面参数变化后待校准、Sense→Bell 深链、Bell 规则草稿、Bell→Sense 返回链和正式发布确认;页面无横向溢出,控制台 0 error,运行时 0 exception。 - 浏览器测试首次发现 Sense 移动端区域画布受 `min-height + aspect-ratio` 影响发生横向溢出,并发现条件上下文条的 `hidden` 被组件 `display` 覆盖;两项均已修复并重新通过桌面/移动截图人工检查。临时 QA 截图未写入仓库。 - `./init.ps1` 通过:上下文校验、16 个治理单测与完整治理检查均成功;`git diff --check` 通过。任务继续保持 `DOING`,等待项目负责人确认修订后的信息架构与工作流。 - -- 2.55.0 From d36b785f67a32aafef0b43c00aec14268f5fb785 Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 4 Aug 2026 09:37:48 +0800 Subject: [PATCH 09/14] docs(task): expand T-004 review scope --- docs/tasks/T-004.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/tasks/T-004.md b/docs/tasks/T-004.md index a69bfce..52a3a51 100644 --- a/docs/tasks/T-004.md +++ b/docs/tasks/T-004.md @@ -13,6 +13,14 @@ write_paths: - docs/tasks/T-004.md - docs/design/sense/index.html - docs/design/bell/index.html + - docs/raw/09-Sense原型评审-IX草稿.md + - docs/08-interaction-checklist.md + - docs/07-user-stories.md + - docs/02-requirements.md + - docs/04-architecture.md + - docs/raw/02-需求分析.md + - docs/raw/03-通用场景应用方案.md + - docs/raw/08-三系统职责划分.md --- ## 问题 / 背景 @@ -66,6 +74,12 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, ## 执行记录 +### 2026-08-04 Claude 评审续修立项 + +- 项目负责人提供 `docs/raw/09-Sense原型评审-IX草稿.md` 并确认按复核后的最终建议继续修改。复核结论为:设备模态、停用收敛、隐私准入、几何编辑、草稿恢复与无障碍问题方向成立,但分片容量来源、页内草稿丢失和双向深链现状等证据需纠正。 +- T-004 继续保持唯一活跃任务;dispatcher 已在 Issue #6 发布 `CLAIM RENEWAL`,将本轮涉及的评审稿、US/IX、需求/架构与 Sense/Bell 原型加入 `write_paths`。当前没有其他活跃任务占用这些路径。 +- 本轮按 `docs/design/README.md` 的显著改版规则重新生成 Sense 页面结构,不恢复一级“区域规则”;一级入口改为“设备”,详情按 `modality + capabilities` 渐进展示,视频设备仍在详情中维护画面检测区域,Bell 继续独占业务规则与发布。 + ### 2026-08-03 任务定义 - 依据用户请求、US-001~US-006、IX-001~IX-013 和 MiBeeNvr 本地只读截图定义两个单文件原型范围。 @@ -102,3 +116,4 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, - Node.js 对两个内联脚本执行语法编译检查通过。Edge Headless/CDP 在 1440×900 与 375×812 验证摄像头详情、坐标绘制、区域保存、画面参数变化后待校准、Sense→Bell 深链、Bell 规则草稿、Bell→Sense 返回链和正式发布确认;页面无横向溢出,控制台 0 error,运行时 0 exception。 - 浏览器测试首次发现 Sense 移动端区域画布受 `min-height + aspect-ratio` 影响发生横向溢出,并发现条件上下文条的 `hidden` 被组件 `display` 覆盖;两项均已修复并重新通过桌面/移动截图人工检查。临时 QA 截图未写入仓库。 - `./init.ps1` 通过:上下文校验、16 个治理单测与完整治理检查均成功;`git diff --check` 通过。任务继续保持 `DOING`,等待项目负责人确认修订后的信息架构与工作流。 + -- 2.55.0 From e18a1ea83b48da6262a2d63d88860c5ff2dd27ca Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Tue, 4 Aug 2026 10:05:56 +0800 Subject: [PATCH 10/14] docs(design): apply Sense prototype review --- docs/02-requirements.md | 2 + docs/04-architecture.md | 11 +- docs/07-user-stories.md | 12 +- docs/08-interaction-checklist.md | 10 +- docs/design/bell/index.html | 18 +- docs/design/sense/index.html | 322 ++++++++++++++++++++-------- docs/raw/02-需求分析.md | 12 +- docs/raw/03-通用场景应用方案.md | 4 +- docs/raw/08-三系统职责划分.md | 11 +- docs/raw/09-Sense原型评审-IX草稿.md | 147 +++++++++++++ docs/tasks/T-004.md | 25 ++- 11 files changed, 444 insertions(+), 130 deletions(-) create mode 100644 docs/raw/09-Sense原型评审-IX草稿.md diff --git a/docs/02-requirements.md b/docs/02-requirements.md index 8f035ed..c08246c 100644 --- a/docs/02-requirements.md +++ b/docs/02-requirements.md @@ -28,10 +28,12 @@ ### 3.1 接入与设备 - 支持标准 ONVIF/RTSP,不绑定摄像头品牌。 +- 台账与管理端以“设备”为根实体,使用 `modality` 表达 video / radar / contact / button / wearable / other,并使用 `capabilities` 决定是否展示画面、媒体、空间配置、遥测等能力;M1~M5 只完整实现 video 适配器,M6 再接入非视频设备,但不得因此把数据模型和一级信息架构写死为摄像头。 - 支持 NAT 后的边缘主动推流;设备身份使用稳定序列号而非 IP。 - 批量开通不依赖逐路手工操作,支持待激活中间态。 - 探活、离线告警、开通校时、断线自动恢复。 - 容量写入时校验站点配额;配额服务不可用时拒绝新增/启用,但不影响已有流。 +- 隐私准入策略属于 Area,使用 `capture_policy = video_allowed | non_imaging_only`;Sense 在设备新增/启用时依据设备成像能力校验。策略读取失败时拒绝新的成像设备变更并告警,已有链路不静默停用或伪装为已收敛。 ### 3.2 分析与规则 diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 7bb57ef..cb5b9aa 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -16,7 +16,7 @@ YoVision 使用“通用底座 + 场景包”,按变化频率分为: | 系统 | 语言/状态 | 职责 | 不负责 | | --- | --- | --- | --- | -| Sense | Go,有状态 | 设备台账、ONVIF、MediaMTX 控制、对账、探活、隧道、设备型触发、流分片 | AI 判定、事件业务、预警 | +| Sense | Go,有状态 | 设备台账(`modality + capabilities`)、ONVIF、MediaMTX 控制、对账、探活、隧道、设备型触发、流分片、隐私区域设备准入 | AI 判定、事件业务、预警 | | Brain | Python/CUDA,业务无状态 | 解码/推理、检测/姿态/跟踪/ReID、时间窗判定、事件 mapper、像素级触发 | 设备真相源、告警升级、租户权限 | | Bell | Go + Web,有状态 | 事件校验/存储、规则、预警状态机、投递、反馈、租户/RBAC、审计、配额真相源和管理端 | 媒体转发、模型执行 | @@ -55,7 +55,7 @@ Sense ── 视频流/触发信号 ──> Brain 首个 M3 数据流部署在 S2 民办寄宿学校的 16 路高风险点位,只运行越线、危险区域和聚集等匿名规则,不加载人脸底库。 -## 5. 九条不可越界的决定 +## 5. 十条不可越界的决定 1. MediaMTX 独立运行,Sense 管配置与生命周期。 2. 设备型触发源归 Sense;需要解码的像素级触发归 Brain。 @@ -66,6 +66,7 @@ Sense ── 视频流/触发信号 ──> Brain 7. Bell 拥有 `site.max_video_channels`,Sense 在设备写路径执行;不跨 schema 直接写。 8. 事件片段写入客户侧 MinIO/S3,常态录像留在客户 NVR;元数据/审计、人脸和训练样本使用独立生命周期。 9. 投递状态机只依赖 Bell provider 接口,不直接依赖某家短信或语音 SDK;生产前至少两条独立路径并能故障切换。 +10. 设备领域模型使用 `modality + capabilities`,页面不以摄像头作为唯一根实体;隐私策略属于 Area,Sense 在新增/启用写路径按成像能力执行,策略不可用时只阻断新变更,不静默切断已有链路。 ## 6. 容量架构 @@ -88,7 +89,7 @@ Sense ── 视频流/触发信号 ──> Brain ## 8. 数据与契约 -- 核心实体:Tenant → Site → Area/Device → StreamBinding/Zone;Rule → Event → Alert → DeliveryAttempt/Ack。 +- 核心实体:Tenant → Site → Area(含 `capture_policy`)/Device(含 `modality + capabilities`)→ StreamBinding/Zone;Rule → Event → Alert → DeliveryAttempt/Ack。 - Event 与 Alert 不合并:一个事件可触发多次预警与投递,一次预警也可聚合多个事件。 - 事件 v0.1 以 `raw/contracts/event-v0.1.schema.json` 与 `raw/contracts/README.md` 为准;未知顶层字段拒绝,只允许通过 `ext` 扩展。 - v0.1 还需代码校验时间自洽、`confidence` 当前为 null、证据文件名隐私、唯一 primary sensor 等跨字段约束。 @@ -108,10 +109,10 @@ Bell/{web,packs,contracts} ## 10. 开发顺序 - M0 不写生产代码。 -- M1 只动 Sense,5 路接入骨架与 MediaMTX。 +- M1 只动 Sense,5 路视频接入骨架与 MediaMTX;设备模型从此时起保持模态/能力可扩展,但不提前实现非视频适配器。 - M2 仍以 Sense 为主,完成 16 路开通/停用、对账、多租户投影与隧道。 - M3 Brain 与 Bell 同时起步,事件契约首次被真实使用。 -- M4/M5 再做 64/128 路分片、完整管理端和多个场景包。 +- M4/M5 再做 64/128 路分片、完整管理端和多个场景包;M6 接入雷达、门磁、按钮和可穿戴等非视频适配器。 M3 先执行不少于 2 周的 dry-run,冻结现场标注集,按规则报告召回率和每路每天误报数;现场基线评审后才把数值阈值写入站点验收附件。算法效果指标与系统 SLA 分开验收。 diff --git a/docs/07-user-stories.md b/docs/07-user-stories.md index 7454206..02945d2 100644 --- a/docs/07-user-stories.md +++ b/docs/07-user-stories.md @@ -2,10 +2,10 @@ > 用户故事用于连接需求、页面交互和任务验收。当前 M0/M1 以设备与实施流程为主,最终用户界面在 M3/M4 才实现。 -## US-001 批量开通摄像头 +## US-001 批量开通视频设备 - 角色:实施工程师。 -- 目标:一次导入并验证一个默认 16 路站点,不逐路手工配置。 +- 目标:一次导入并验证一个默认 16 路视频站点,不逐路手工配置;操作入口位于通用设备台账的视频模态筛选下。 - 价值:降低交付时间并为 128 路扩展保留操作效率。 - 验收:支持待激活、逐设备结果、失败可重试、超配额明确拒绝;128 路规模下仍使用分页/批量流程。 - 关联:RQ-C-01~RQ-C-08,IX-001~IX-003。 @@ -58,6 +58,14 @@ - 验收:3–5 款设备的型号、固件、认证方式、Profiles/StreamUri/校时、主子码流、掉线恢复都有证据;不记录密码和真实客户信息。 - 关联:M0,T-001;无产品 UI,使用版本化测试文档。 +## US-008 管理异构设备与隐私准入 + +- 角色:实施工程师/站点管理员。 +- 目标:在统一设备台账中按模态和能力添加、筛选与查看视频、雷达、门磁、按钮、可穿戴等设备;隐私区域只允许非成像设备。 +- 价值:M2 固化不会因 M6 异构设备接入而推倒重来的信息架构,同时把隐私要求落实为系统准入约束。 +- 验收:一级入口使用“设备”;添加时先选模态,详情按能力显示页面;`non_imaging_only` Area 不允许新增或启用成像设备,策略读取失败时拒绝新变更并给出恢复路径;M2 完成信息架构与视频准入,M6 完成非视频适配器。 +- 关联:RQ-S1-05、NFR-CMP-05,IX-014、IX-016。 + ## 追溯规则 新增 P0 UI 任务必须引用至少一个 US 和一个 IX;若没有 UI,任务文件明确写“不适用”。需求变化先更新用户故事和交互清单,再改页面。 diff --git a/docs/08-interaction-checklist.md b/docs/08-interaction-checklist.md index b02ced9..658d552 100644 --- a/docs/08-interaction-checklist.md +++ b/docs/08-interaction-checklist.md @@ -17,6 +17,11 @@ | IX-011 | 规则试运行 | 明确“未正式生效”,展示命中样本与影响范围,支持取消/回滚 | US-005 | M4 | | IX-012 | 升级链/静默 | 联系人顺序、超时、双通道;静默最长 4h、显示自动恢复时间、无永久选项 | US-005 | M3/M4 | | IX-013 | 权限与隐私 | 越权统一处理;无授权租户不展示人脸入口;不泄露流 URL/凭据 | US-006 | M3/M4 | +| IX-014 | 设备模态与能力 | 一级入口为“设备”;列表可按 `modality` 筛选,添加时先选模态,详情按 `capabilities` 渐进展示;视频设备显示画面/媒体/检测区域,非视频设备保留连接与健康并显示对应遥测,不用禁用的视频页签占位 | US-008 | M2/M6 | +| IX-015 | 停用与收敛 | 明确区分暂停推理、停用设备接入和仅踢当前会话;操作前展示对观看、推理、录制与证据回捞的影响并二次确认;期望态立即改变、实际态经对账收敛,部分失败可逐项重试;重复请求不产生重复副作用,但每次请求均留审计结果 | US-001、US-002 | M2 | +| IX-016 | 隐私区域设备准入 | `capture_policy = non_imaging_only` 的 Area 不允许新增/启用具有成像能力的设备;前端就地禁用并解释,后端返回稳定错误码;策略读取失败时拒绝新变更并告警,已有设备遇到区域策略变化时进入显式处置流程,不静默留存或停用 | US-008 | M2/M6 | +| IX-017 | 检测区域几何编辑 | 多边形和方向警戒线支持绘制、显式完成、撤销、清空、顶点编辑;警戒线在草稿/已保存态均显示方向箭头并可反转;鼠标与键盘坐标操作等效;画面参数变化后标记待校准且不自动改坐标;区域版本与规则版本独立 | US-005 | M3 | +| IX-018 | 草稿与跨系统上下文 | 几何草稿自动保存并可恢复;只有持久化失败或切换空间类型等会破坏草稿的动作才拦截确认;Sense↔Bell 双向携带并消费 `camera + zone + return_to` 上下文;返回后恢复草稿,版本已被他人更新时提示冲突且不覆盖 | US-005 | M3 | ## 全局状态 @@ -44,7 +49,10 @@ ## 无障碍与安全 - 键盘可完成主要 Web 流程,焦点清晰,表单错误关联到字段。 -- 文本和关键状态满足可读对比度;严重度同时用文字/图标表达。 +- 正文、按钮文字和关键状态对比度不低于 4.5:1;大字号(不低于 24px,或不低于 18.66px 且粗体)不低于 3:1;非文本控件、状态边界与焦点指示器不低于 3:1。严重度同时用文字/图标表达。 +- 当前导航项显式使用 `aria-current="page"`;响应式隐藏可见文字时,图标按钮仍保留稳定的可访问名。 +- 使用 tab 模式时完整实现 `tablist` / `tab` / `tabpanel`、`aria-controls` / `aria-labelledby`、单一 Tab 停靠点和方向键导航;不能完整实现时不声明 tab 角色。 +- 移动端主要操作目标不小于 44×44px,输入控件正文不小于 16px;绘制画布必须提供无需精确点击的键盘/表单等效路径。 - 删除、停用、踢流、批量覆盖和规则正式发布需要明确影响范围与二次确认。 - UI 不展示摄像头密码、完整连接串、token 或可复用的内部流地址。 diff --git a/docs/design/bell/index.html b/docs/design/bell/index.html index 377d517..cff18b3 100644 --- a/docs/design/bell/index.html +++ b/docs/design/bell/index.html @@ -369,12 +369,12 @@
    - +

    规则策略

    Bell 管理场景、范围、时段、持续时间、试运行与发布;检测区域来自对应摄像头的版本化空间配置。

    -
    正式生效

    非授权人员进入

    危险区域 × 全天 × 持续 3 秒

    覆盖范围
    4 个点位
    检测区域
    北围墙危险区域 v7
    继承来源
    站点模板
    今日命中
    8
    校准检测区域
    +
    正式生效

    非授权人员进入

    危险区域 × 全天 × 持续 3 秒

    覆盖范围
    4 个点位
    检测区域
    北围墙危险区域 v7
    继承来源
    站点模板
    今日命中
    8
    校准检测区域
    试运行

    区域内聚集

    人数 ≥ 6 × 持续 20 秒 × 夜间

    覆盖范围
    3 个点位
    检测区域
    3 个区域版本
    继承来源
    设备覆盖
    样本命中
    14
    -
    草稿

    方向越线

    警戒线 A → B × 上学时段

    覆盖范围
    1 个点位
    检测区域
    宿舍东通道 · 警戒线 v3
    继承来源
    设备草稿
    最近修改
    2h 前
    查看警戒线
    +
    草稿

    方向越线

    警戒线 A → B × 上学时段

    覆盖范围
    1 个点位
    检测区域
    宿舍东通道 · 警戒线 v3
    继承来源
    设备草稿
    最近修改
    2h 前
    查看警戒线
    @@ -408,8 +408,8 @@
    -
    -
    空间坐标由摄像头详情维护区域版本变化不会绕过规则试运行或自动正式发布。编辑检测区域
    +
    +
    空间坐标由摄像头详情维护区域版本变化不会绕过规则试运行或自动正式发布。编辑检测区域
    @@ -463,7 +463,9 @@ $('#openRuleDialog').addEventListener('click', openRuleEditor); $('#continueRuleDraft').addEventListener('click', openRuleEditor); $$('[data-edit-rule]').forEach(button=>button.addEventListener('click', openRuleEditor)); - $('#ruleCamera').addEventListener('change',e=>{ $('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent(e.target.value)}&return=bell-rule`; notify('已切换摄像头,请确认检测区域版本与画面仍匹配。'); }); + function syncSenseZoneLink(){ $('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent($('#ruleCamera').value)}&zone=${encodeURIComponent($('#ruleZone').value)}&return_to=bell-rule`; } + $('#ruleCamera').addEventListener('change',()=>{ syncSenseZoneLink(); notify('已切换摄像头,请确认检测区域版本与画面仍匹配。'); }); + $('#ruleZone').addEventListener('change',syncSenseZoneLink); $('#saveRuleDraft').addEventListener('click',()=>notify($('#startDryRun').checked?'规则草稿已保存并进入试运行,不会触发正式预警。':'规则草稿已保存,尚未开始试运行。')); $('#publishTrialRule').addEventListener('click',()=>{if(confirm('正式发布后将影响 3 个点位并启动预警链。确认发布当前试运行版本?')){$('#trialBadge').className='badge success';$('#trialBadge').textContent='正式生效';notify('规则版本已正式发布,变更已记录审计并可回滚。');}}); @@ -474,7 +476,9 @@ if (routeParams.has('camera')) { switchView('rules'); $('#ruleContext').hidden=false; const camera=routeParams.get('camera'); if([...$('#ruleCamera').options].some(option=>option.value===camera)) $('#ruleCamera').value=camera; - $('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent(camera)}&return=bell-rule`; + const zone=routeParams.get('zone'); if(zone&&[...$('#ruleZone').options].some(option=>option.value===zone)) $('#ruleZone').value=zone; + $('#ruleContextCamera').textContent=$('#ruleCamera').selectedOptions[0].textContent; $('#ruleContextZone').textContent=zone||$('#ruleZone').value; + syncSenseZoneLink(); notify('已保留摄像头与检测区域上下文,可继续配置规则。'); } diff --git a/docs/design/sense/index.html b/docs/design/sense/index.html index 5fffcad..9730a50 100644 --- a/docs/design/sense/index.html +++ b/docs/design/sense/index.html @@ -14,10 +14,10 @@ --surface-3: #17263a; --text: #f2f7fb; --muted: #9fb0c5; - --subtle: #71849c; + --subtle: #8498b0; --line: #27384d; --primary: #38bdf8; - --primary-strong: #0ea5e9; + --primary-strong: #0369a1; --primary-soft: rgba(56, 189, 248, .14); --success: #34d399; --warning: #fbbf24; @@ -35,7 +35,9 @@ --surface-3: #eaf1f6; --text: #112033; --muted: #50647b; - --subtle: #718096; + --subtle: #596c82; + --primary: #0369a1; + --primary-strong: #075985; --line: #cfdae5; --primary-soft: rgba(2, 132, 199, .11); --shadow: 0 14px 34px rgba(38, 64, 92, .12); @@ -93,19 +95,19 @@ .site-select span { color: var(--muted); font-size: 12px; } .top-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; } .select, .field, .textarea { - width: 100%; min-height: 40px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--text); padding: 8px 11px; + width: 100%; min-height: 44px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--text); padding: 8px 11px; } .textarea { min-height: 74px; resize: vertical; } .top-actions .select { width: auto; min-width: 126px; } .btn { - min-height: 40px; border: 1px solid var(--line); border-radius: 9px; padding: 8px 13px; background: var(--surface-2); color: var(--text); + min-height: 44px; border: 1px solid var(--line); border-radius: 9px; padding: 8px 13px; background: var(--surface-2); color: var(--text); display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-weight: 700; text-decoration: none; transition: border-color .2s ease, background .2s ease, opacity .2s ease; } .btn:hover { border-color: var(--primary); } .btn-primary { background: var(--primary-strong); border-color: var(--primary-strong); color: #fff; } .btn-danger { color: #ffd8df; border-color: rgba(251, 113, 133, .45); background: rgba(251, 113, 133, .1); } .btn-quiet { border-color: transparent; background: transparent; color: var(--muted); } - .icon-btn { width: 42px; padding: 0; } + .icon-btn { width: 44px; padding: 0; } .content { padding: 22px; } .view { display: none; animation: reveal .22s ease-out; } .view.active { display: block; } @@ -141,14 +143,19 @@ .search .icon { position: absolute; left: 11px; top: 11px; color: var(--subtle); pointer-events: none; } .search input { padding-left: 38px; } .table-wrap { overflow-x: auto; } - table { width: 100%; border-collapse: collapse; min-width: 960px; } + table { width: 100%; border-collapse: collapse; min-width: 1040px; } th, td { padding: 12px 14px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; } th { color: var(--muted); background: var(--surface-2); font-size: 12px; font-weight: 700; } td { font-variant-numeric: tabular-nums; } tr:last-child td { border-bottom: 0; } tr:hover td { background: color-mix(in srgb, var(--primary-soft) 45%, transparent); } .device-name { display: flex; align-items: center; gap: 10px; } - .camera-icon { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; background: var(--surface-3); color: var(--primary); } + .camera-icon { width: 38px; height: 38px; border-radius: 9px; display: grid; place-items: center; background: var(--surface-3); color: var(--primary); } + .camera-icon.radar { color: var(--warning); } + .capabilities { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; } + .capability { color: var(--muted); font-size: 11px; } + .pagination { padding: 12px 14px; display: flex; justify-content: space-between; align-items: center; gap: 12px; border-top: 1px solid var(--line); } + .pagination-actions { display: flex; gap: 6px; } .device-name small { display: block; color: var(--muted); } .camera-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .camera-card { overflow: hidden; } @@ -158,10 +165,10 @@ .camera-meta { padding: 12px 14px; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .camera-meta small { color: var(--muted); } .breadcrumb { margin-bottom: 10px; display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px; } - .breadcrumb button { min-height: 32px; padding: 4px 8px; border: 0; border-radius: 7px; background: transparent; color: var(--primary); font-weight: 700; } + .breadcrumb button { min-height: 44px; padding: 4px 8px; border: 0; border-radius: 7px; background: transparent; color: var(--primary); font-weight: 700; } .breadcrumb button:hover { background: var(--primary-soft); } .detail-tabs { margin-bottom: 12px; padding: 5px; display: flex; gap: 5px; overflow-x: auto; border: 1px solid var(--line); border-radius: 11px; background: var(--surface); } - .detail-tabs button { min-height: 40px; padding: 7px 13px; border: 0; border-radius: 8px; background: transparent; color: var(--muted); white-space: nowrap; } + .detail-tabs button { min-height: 44px; padding: 7px 13px; border: 0; border-radius: 8px; background: transparent; color: var(--muted); white-space: nowrap; } .detail-tabs button.active { background: var(--primary-soft); color: var(--text); box-shadow: inset 0 -2px var(--primary); } .detail-panel[hidden] { display: none; } .context-strip { margin-bottom: 12px; padding: 11px 13px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid rgba(167,139,250,.38); border-radius: 10px; background: rgba(167,139,250,.1); } @@ -176,7 +183,7 @@ .mini-thumb { width: 54px; height: 34px; border-radius: 6px; background: linear-gradient(145deg, #24364c, #0a101a); position: relative; overflow: hidden; } .mini-thumb::after { content: ""; position: absolute; left: 8px; right: 8px; bottom: 8px; height: 1px; background: var(--primary); transform: rotate(-12deg); } .editor-tools { padding: 10px; display: flex; gap: 6px; align-items: center; border-bottom: 1px solid var(--line); overflow-x: auto; } - .tool-btn { min-width: 42px; min-height: 40px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); color: var(--muted); display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap; } + .tool-btn { min-width: 44px; min-height: 44px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); color: var(--muted); display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap; } .tool-btn.active { background: var(--primary-soft); border-color: var(--primary); color: var(--text); } .tool-divider { width: 1px; align-self: stretch; background: var(--line); margin: 0 3px; } .editor-stage { position: relative; aspect-ratio: 16/9; min-height: 340px; background: #030a12; overflow: hidden; } @@ -198,6 +205,17 @@ .coordinate-box h3 { margin: 0 0 4px; font-size: 13px; } .coordinate-box p { margin: 0 0 10px; color: var(--muted); font-size: 12px; } .coordinate-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: 6px; } + .point-list { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 6px; } + .point-item { display: grid; grid-template-columns: auto 1fr 1fr auto; align-items: center; gap: 6px; } + .point-item .field { min-width: 0; } + .point-index { color: var(--muted); font-size: 12px; } + .notice { margin: 12px 0 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--muted); } + .notice.warning { border-color: rgba(251,191,36,.38); background: rgba(251,191,36,.08); color: #ffe39a; } + .operation-grid { display: grid; gap: 8px; } + .operation-option { min-height: 72px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: start; cursor: pointer; } + .operation-option:has(input:checked) { border-color: var(--primary); background: var(--primary-soft); } + .operation-option strong, .operation-option small { display: block; } + .operation-option small { color: var(--muted); } .linked-rules { margin-top: 12px; padding: 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); } .linked-rules h3 { margin: 0 0 4px; font-size: 13px; } .linked-rules p { margin: 0 0 10px; color: var(--muted); font-size: 12px; } @@ -271,6 +289,9 @@ .editor-stage { min-height: 0; } .coordinate-row { grid-template-columns: 1fr 1fr; } .coordinate-row .btn { grid-column: 1/-1; } + .point-item { grid-template-columns: auto 1fr 1fr; } + .point-item .btn { grid-column: 2/-1; } + input, select, textarea { font-size: 16px; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } @@ -282,6 +303,7 @@ + @@ -307,18 +329,18 @@
    Y
    YoVisionSense · Edge Console
    -
    青藤寄宿学校 / 主校区 · 16 / 16 路
    +
    青藤寄宿学校 / 主校区 · 视频 16 / 16 路 · 设备 17 台
    已用 16 / 128
    -
    设备期望态实际态最后在线时间漂移媒体分片检测区域操作
    +
    视频配额 16 / 128 · 其他设备不占视频路数
    +
    设备类型与能力期望态实际态最后遥测连接适配器健康摘要配置操作
    +
    - - -

    北围墙 01

    CAM-S2-001 · 当前摄像头的接入、画面与空间配置。

    区域已校准
    -
    - - - - + + +

    北围墙 01

    CAM-S2-001 · 视频接入、画面与空间配置。

    区域已校准
    +
    + + + +
    -
    -

    键盘坐标编辑

    坐标范围 0–100%,为鼠标绘制提供等效操作。

    1. 尚未添加草稿点
    -

    关联的告警规则

    规则由 Bell 管理;Sense 只提供摄像头快照和版本化空间坐标。

    • 非授权人员进入 · 正式生效
    • 区域内聚集 · 试运行
    在 Bell 配置规则
    -
    +

    键盘坐标编辑

    坐标范围 0–100%;每个顶点都可以修改或删除,作为鼠标绘制的等效操作。

    1. 尚未添加草稿点
    +
    草稿会自动保存在此浏览器会话中。
    +

    关联的告警规则

    规则由 Bell 管理;Sense 只提供摄像头快照和版本化空间坐标。

    • 非授权人员进入 · 正式生效
    • 区域内聚集 · 试运行
    在 Bell 配置规则
    +
    - - + +
    -

    接入任务

    默认 16 路也支持批量导入;128 路时不依赖逐路手工操作。

    +

    接入任务

    默认 16 路视频也支持批量导入;扩至 128 路时不依赖逐路手工操作,非视频设备走对应适配器。

    -

    导入摄像头

    配额 16 / 128
    选择 CSV 文件或拖到这里

    先本地校验列、重复序列号和配额,不上传明文密码。

    +

    批量导入视频设备

    视频配额 16 / 128
    选择 CSV 文件或拖到这里

    先本地校验列、重复序列号、区域隐私策略和配额,不上传明文密码。

    当前任务

    进行中

    宿舍区第二批 · 8 路

    已验证 5 / 8 · 2 路待补凭据 · 1 路重试中

    @@ -444,22 +470,42 @@
    - +
    -

    添加摄像头

    +

    添加设备

    -
    -
    -
    原型使用保留地址;保存后列表仅显示脱敏端点。
    -
    -
    -
    -
    -

    保存前先验证 Profiles、StreamUri 与校时能力。

    -
    +
    +
    +
    +
    +
    +
    原型使用保留地址;保存后列表仅显示脱敏端点。
    +
    +
    + + +
    当前区域允许图像采集;保存时仍会再次校验区域策略。

    保存前验证设备声明能力、连接与校时。

    +
    + + + + +
    +

    停止或隔离设备

    +
    +

    目标:北围墙 01 CAM-S2-001

    +
    + + + +
    +
    影响:停止新推理;媒体连接保持在线。执行后将显示期望态与实际态的收敛过程。
    +
    尚未执行。重复提交不会产生重复副作用,但每次请求都会记录审计。
    +
    +
    @@ -472,13 +518,45 @@ function notify(message) { toast.textContent = message; toast.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('show'), 3600); } const deviceData = [ - ['北围墙 01','CAM-S2-001','启用','在线','刚刚','+0.2s','media-01','success','2 个区域','success'], - ['北围墙 03','CAM-S2-003','启用','重连中','2 分钟前','—','media-01','warning','待校准','warning'], - ['实验楼入口 02','CAM-S2-006','启用','在线','12 秒前','+3.8s','media-02','warning','1 个区域','success'], - ['宿舍东通道','CAM-S2-011','启用','在线','4 秒前','+0.1s','media-02','success','3 个区域','success'], - ['配电房门口','CAM-S2-015','停用','已停止','1 天前','—','未绑定','info','未配置','info'] + {name:'北围墙 01',serial:'CAM-S2-001',modality:'video',modalityLabel:'视频',capabilities:['图像采集','视频流','区域坐标'],expected:'启用',actual:'在线',status:'success',last:'刚刚',adapter:'media-01',health:'时钟 +0.2s',config:'2 个区域',area:'北围墙',protocol:'ONVIF / RTSP',profile:'子码流 704 × 576 · 5 FPS',clock:'+0.2s'}, + {name:'北围墙 03',serial:'CAM-S2-003',modality:'video',modalityLabel:'视频',capabilities:['图像采集','视频流','区域坐标'],expected:'启用',actual:'重连中',status:'warning',last:'2 分钟前',adapter:'media-01',health:'第 4 次重连',config:'待校准',area:'北围墙',protocol:'ONVIF / RTSP',profile:'子码流 704 × 576 · 5 FPS',clock:'未知'}, + {name:'实验楼入口 02',serial:'CAM-S2-006',modality:'video',modalityLabel:'视频',capabilities:['图像采集','视频流','区域坐标'],expected:'启用',actual:'在线',status:'success',last:'12 秒前',adapter:'media-01',health:'时钟 +3.8s',config:'1 个区域',area:'实验楼',protocol:'ONVIF / RTSP',profile:'子码流 704 × 576 · 5 FPS',clock:'+3.8s'}, + {name:'宿舍东通道',serial:'CAM-S2-011',modality:'video',modalityLabel:'视频',capabilities:['图像采集','视频流','区域坐标'],expected:'启用',actual:'在线',status:'success',last:'4 秒前',adapter:'media-01',health:'时钟 +0.1s',config:'3 个区域',area:'宿舍区',protocol:'RTSP',profile:'子码流 704 × 576 · 5 FPS',clock:'+0.1s'}, + {name:'操场东侧雷达',serial:'RAD-S2-001',modality:'radar',modalityLabel:'毫米波雷达',capabilities:['目标距离','速度','方位角'],expected:'启用',actual:'在线',status:'success',last:'1 秒前',adapter:'mqtt-01',health:'丢包 0.1%',config:'1 Hz 上报',area:'操场',protocol:'MQTT',profile:'遥测主题 sense/radar/east · 1 Hz',clock:'+0.3s'}, + {name:'配电房门口',serial:'CAM-S2-015',modality:'video',modalityLabel:'视频',capabilities:['图像采集','视频流'],expected:'停用',actual:'已停止',status:'info',last:'1 天前',adapter:'未绑定',health:'期望态已收敛',config:'未配置',area:'配电房',protocol:'ONVIF',profile:'尚未选择 Profile',clock:'—'}, + ...Array.from({length:11},(_,i)=>({name:`教学楼通道 ${String(i+1).padStart(2,'0')}`,serial:`CAM-S2-${String(i+20).padStart(3,'0')}`,modality:'video',modalityLabel:'视频',capabilities:['图像采集','视频流','区域坐标'],expected:'启用',actual:'在线',status:'success',last:`${i+2} 秒前`,adapter:'media-01',health:'健康',config:i%3===0?'1 个区域':'未配置',area:'教学楼',protocol:'ONVIF / RTSP',profile:'子码流 704 × 576 · 5 FPS',clock:'+0.2s'})) ]; - $('#deviceRows').innerHTML = deviceData.map(d => `
    ${d[0]}${d[1]}
    ${d[2]}${d[3]}${d[4]}${d[5]}${d[6]}${d[8]}`).join(''); + const pageSize = 5; + let devicePage = 1; + let currentDevice = deviceData[0]; + function filteredDevices() { + const term = $('#deviceSearch').value.trim().toLowerCase(); + const modality = $('#modalityFilter').value; + const status = $('#statusFilter').value; + return deviceData.filter(device => { + const termMatch = !term || `${device.name} ${device.serial} ${device.area}`.toLowerCase().includes(term); + const modalityMatch = modality === 'all' || device.modality === modality; + const statusMatch = status === 'all' || (status === 'exception' ? device.status === 'warning' : device.actual === '在线'); + return termMatch && modalityMatch && statusMatch; + }); + } + function renderDevices() { + const filtered = filteredDevices(); + const pages = Math.max(1, Math.ceil(filtered.length / pageSize)); + devicePage = Math.min(devicePage, pages); + const start = (devicePage - 1) * pageSize; + const rows = filtered.slice(start, start + pageSize); + $('#deviceRows').innerHTML = rows.length ? rows.map(device => { + const icon = device.modality === 'radar' ? 'i-radar' : 'i-camera'; + return `
    ${device.name}${device.serial} · ${device.area}
    ${device.modalityLabel}
    ${device.capabilities.map(c=>`${c}`).join('')}
    ${device.expected}${device.actual}${device.last}${device.adapter}${device.health}${device.config}
    `; + }).join('') : '
    没有匹配的设备

    调整类型、状态或搜索条件。

    '; + $('#devicePageSummary').textContent = `第 ${devicePage} / ${pages} 页 · 共 ${filtered.length} 台设备`; + $('#previousDevicePage').disabled = devicePage <= 1; + $('#nextDevicePage').disabled = devicePage >= pages; + } + ['deviceSearch','modalityFilter','statusFilter'].forEach(id => $(`#${id}`).addEventListener(id === 'deviceSearch' ? 'input' : 'change', () => { devicePage=1; renderDevices(); })); + $('#previousDevicePage').addEventListener('click',()=>{ devicePage--; renderDevices(); }); + $('#nextDevicePage').addEventListener('click',()=>{ devicePage++; renderDevices(); }); const cameraNames = ['北围墙 01','实验楼入口 02','宿舍东通道','配电房门口']; $('#cameraGrid').innerHTML = cameraNames.map((name, i) => `
    ${name} · 子码流
    ${name}704 × 576 · 5 FPS在线
    `).join(''); @@ -486,25 +564,41 @@ function switchView(name) { $$('.view').forEach(v => v.classList.toggle('active', v.dataset.page === name)); const navName = name === 'device-detail' ? 'devices' : name; - $$('[data-view]').forEach(b => { const current = b.dataset.view === navName; b.toggleAttribute('aria-current', current); b.classList.toggle('active', current); }); + $$('[data-view]').forEach(b => { const on = b.dataset.view === navName; if (on) b.setAttribute('aria-current','page'); else b.removeAttribute('aria-current'); b.classList.toggle('active', on); }); $('#stateSelect').value = 'normal'; renderState('normal'); $('#main-content').focus({preventScroll:true}); } $$('[data-view]').forEach(b => b.addEventListener('click', () => switchView(b.dataset.view))); + function activateDetailTab(name, focus = false) { + const visibleTabs = $$('.detail-tabs [role="tab"]:not([hidden])'); + const selected = visibleTabs.find(tab => tab.dataset.detailTab === name) || visibleTabs[0]; + $$('.detail-tabs [role="tab"]').forEach(tab => { const on = tab === selected; tab.classList.toggle('active', on); tab.setAttribute('aria-selected', String(on)); tab.tabIndex = on ? 0 : -1; }); + $$('[data-detail-panel]').forEach(panel => panel.hidden = panel.dataset.detailPanel !== selected.dataset.detailTab); + if (focus) selected.focus(); + } function openDeviceDetail(serial = 'CAM-S2-001') { - const device = deviceData.find(d => d[1] === serial) || deviceData[0]; - $('#deviceDetailName').textContent = device[0]; $('#deviceBreadcrumb').textContent = device[0]; $('#deviceDetailSerial').textContent = device[1]; - const bellHref = `../bell/index.html?view=rules&camera=${encodeURIComponent(device[1])}&zone=ZONE-007`; + const device = deviceData.find(d => d.serial === serial) || deviceData[0]; + currentDevice = device; + $('#deviceDetailName').textContent = device.name; $('#deviceBreadcrumb').textContent = device.name; $('#deviceDetailSerial').textContent = device.serial; + $('#deviceDetailDescription').textContent = device.modality === 'video' ? '视频接入、画面与空间配置。' : '遥测接入、能力与健康配置。'; + $('#detailArea').textContent=device.area; $('#detailModality').textContent=device.modalityLabel; $('#detailCapabilities').textContent=device.capabilities.join('、'); $('#detailProtocol').textContent=device.protocol; $('#detailAdapter').textContent=device.adapter; $('#detailProfile').textContent=device.profile; + $('#detailActualBadge').textContent=device.actual; $('#detailActualBadge').className=`badge ${device.status}`; $('#healthAdapter').textContent=device.adapter; $('#healthClock').textContent=device.clock; + const video = device.capabilities.includes('图像采集'); + $('#tab-zones').hidden=!video; $('#calibrationBadge').hidden=!video; $('#simulateCalibration').hidden=!video; $('#calibrationTile').hidden=!video; + $('#connectionHealthTitle').textContent = video ? '媒体连接' : '遥测连接'; + const requestedZone = new URLSearchParams(location.search).get('zone') || 'ZONE-007'; + const bellHref = `../bell/index.html?view=rules&camera=${encodeURIComponent(device.serial)}&zone=${encodeURIComponent(requestedZone)}&return_to=sense-device`; $('#configureRules').href = bellHref; $('#returnToBell').href = bellHref; + if (video) restoreDraft(device.serial); else { points.length=0; drawDraft({persist:false}); } + activateDetailTab(video ? 'zones' : 'info'); switchView('device-detail'); } - $$('[data-open-camera]').forEach(b => b.addEventListener('click', () => openDeviceDetail(b.dataset.openCamera))); + $('#deviceRows').addEventListener('click', event => { const detail=event.target.closest('[data-open-device]'); const operation=event.target.closest('[data-device-operation]'); if (detail) openDeviceDetail(detail.dataset.openDevice); if (operation) openOperationDialog(operation.dataset.deviceOperation); }); $('#backToDevices').addEventListener('click', () => switchView('devices')); - $$('.detail-tabs button').forEach(button => button.addEventListener('click', () => { - const name = button.dataset.detailTab; - $$('.detail-tabs button').forEach(b => { const on = b === button; b.classList.toggle('active', on); b.setAttribute('aria-selected', on); }); - $$('[data-detail-panel]').forEach(panel => panel.hidden = panel.dataset.detailPanel !== name); - })); + $$('.detail-tabs [role="tab"]').forEach(tab => { + tab.addEventListener('click',()=>activateDetailTab(tab.dataset.detailTab)); + tab.addEventListener('keydown',event=>{ const tabs=$$('.detail-tabs [role="tab"]:not([hidden])'); const index=tabs.indexOf(tab); let next=index; if(event.key==='ArrowRight') next=(index+1)%tabs.length; else if(event.key==='ArrowLeft') next=(index-1+tabs.length)%tabs.length; else if(event.key==='Home') next=0; else if(event.key==='End') next=tabs.length-1; else return; event.preventDefault(); activateDetailTab(tabs[next].dataset.detailTab,true); }); + }); function renderState(state) { const layer = $('#stateLayer'); const views = $$('.view'); @@ -512,7 +606,7 @@ if (state === 'normal') return; const configs = { loading: `

    正在同步边缘状态

    超过 300ms 时显示进度;已有视频流不因控制面加载而中断。

    `, - empty: `

    这个站点还没有摄像头

    先扫描 ONVIF 设备,或下载 CSV 模板进行批量导入。

    `, + empty: `

    这个站点还没有设备

    可发现视频设备、手动添加非视频设备,或下载 CSV 模板批量导入视频设备。

    `, error: `

    无法读取最新状态

    边缘节点暂时不可达。当前显示可能已过期,系统不会把未知状态伪装为在线。

    ` }; layer.innerHTML = configs[state]; } @@ -520,37 +614,75 @@ $('#themeToggle').addEventListener('click', () => { const html = document.documentElement; html.dataset.theme = html.dataset.theme === 'dark' ? 'light' : 'dark'; notify(`已切换为${html.dataset.theme === 'dark' ? '深色' : '浅色'}主题`); }); const zoneCanvas = $('#zoneCanvas'), draftLayer = $('#draftLayer'), points = []; - let activeTool = 'polygon'; - function drawDraft() { - const coords = points.map(p => `${p.x},${p.y}`).join(' '); - const shape = activeTool === 'line' && points.length > 1 ? `` : ``; - draftLayer.innerHTML = shape + points.map((p,i) => `点 ${i+1}`).join(''); - $('#pointStatus').textContent = `当前草稿:${points.length} 个点`; - $('#pointList').innerHTML = points.length ? points.map((p,i) => `
  • 点 ${i+1}:${(p.x/9.6).toFixed(1)}%, ${(p.y/5.4).toFixed(1)}%
  • `).join('') : '
  • 尚未添加草稿点
  • '; - $('#draftBadge').className = points.length ? 'badge warning' : 'badge info'; - $('#draftBadge').textContent = points.length ? '区域有未保存修改' : '区域 v7'; + let activeTool = 'polygon', drawingComplete = false, serverVersion = 7, clickTimer; + const draftKey = serial => `yovision-sense-zone-draft:${serial}`; + function safeSession(action) { try { return action(sessionStorage); } catch { return undefined; } } + function persistDraft() { + if (!currentDevice || currentDevice.modality !== 'video') return; + const saved=safeSession(store=>{ store.setItem(draftKey(currentDevice.serial),JSON.stringify({points,activeTool,drawingComplete,baseVersion:serverVersion})); return true; }); + $('#autosaveStatus').textContent = saved ? `草稿已自动保存 · ${new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'})}` : '浏览器禁止会话存储;草稿仅在当前页面内保留。'; } - function addPoint(x,y) { if (activeTool === 'pointer') return; if (activeTool === 'line' && points.length >= 2) points.length = 0; points.push({x:Math.max(0,Math.min(960,x)),y:Math.max(0,Math.min(540,y))}); drawDraft(); } - zoneCanvas.addEventListener('click', e => { const r = zoneCanvas.getBoundingClientRect(); addPoint((e.clientX-r.left)/r.width*960,(e.clientY-r.top)/r.height*540); }); - zoneCanvas.addEventListener('dblclick', e => { e.preventDefault(); if (activeTool === 'polygon' && points.length >= 3) notify(`多边形已闭合,共 ${points.length} 个顶点;请保存草稿。`); }); - $$('.tool-btn[data-tool]').forEach(btn => btn.addEventListener('click', () => { activeTool = btn.dataset.tool; $$('.tool-btn[data-tool]').forEach(b => { const on = b === btn; b.classList.toggle('active', on); b.setAttribute('aria-pressed', on); }); points.length = 0; drawDraft(); $('#editorHint').textContent = activeTool === 'line' ? '警戒线:依次添加起点与终点;箭头方向由起点指向终点。' : activeTool === 'pointer' ? '选择:查看已有区域;切换绘制工具可新建草稿。' : '多边形:点击画面添加顶点,双击闭合;也可在右侧输入坐标。'; })); - $('#undoPoint').addEventListener('click', () => { points.pop(); drawDraft(); }); + function clearStoredDraft() { if(currentDevice) safeSession(store=>store.removeItem(draftKey(currentDevice.serial))); } + function restoreDraft(serial) { + points.length=0; activeTool='polygon'; drawingComplete=false; serverVersion=7; + const raw=safeSession(store=>store.getItem(draftKey(serial))); + if(raw) { try { const draft=JSON.parse(raw); points.push(...(draft.points||[])); activeTool=draft.activeTool||'polygon'; drawingComplete=Boolean(draft.drawingComplete); serverVersion=Number(draft.baseVersion)||7; $('#autosaveStatus').textContent='已恢复此设备在当前浏览器会话中的未保存草稿。'; } catch { clearStoredDraft(); } } + $$('.tool-btn[data-tool]').forEach(button=>{ const on=button.dataset.tool===activeTool; button.classList.toggle('active',on); button.setAttribute('aria-pressed',String(on)); }); + drawDraft({persist:false}); + } + function drawDraft({persist=true} = {}) { + const coords = points.map(p => `${p.x},${p.y}`).join(' '); + const shape = activeTool === 'line' && points.length > 1 ? `` : drawingComplete && points.length>2 ? `` : ``; + draftLayer.innerHTML = shape + points.map((p,i) => `点 ${i+1}`).join(''); + $('#pointStatus').textContent = `当前草稿:${points.length} 个点${drawingComplete?' · 已完成绘制':' · 绘制中'}`; + $('#pointList').innerHTML = points.length ? points.map((p,i) => `
  • 点 ${i+1}
  • `).join('') : '
  • 尚未添加草稿点
  • '; + $('#draftBadge').className = points.length ? 'badge warning' : 'badge info'; + $('#draftBadge').textContent = points.length ? `区域有未保存修改 · 基于 v${serverVersion}` : `区域 v${serverVersion}`; + $('#reverseLine').hidden = activeTool !== 'line'; + if(persist) persistDraft(); + } + function addPoint(x,y) { if (activeTool === 'pointer') return; if (activeTool === 'line' && points.length >= 2) { notify('警戒线已有起点和终点;可反转方向、编辑坐标或清空重画。'); return; } drawingComplete=false; points.push({x:Math.max(0,Math.min(960,x)),y:Math.max(0,Math.min(540,y))}); drawDraft(); } + function finishDrawing() { const minimum=activeTool==='line'?2:3; if(activeTool==='pointer'){ notify('请先选择多边形或警戒线工具。'); return; } if(points.length { clearTimeout(clickTimer); clickTimer=setTimeout(()=>{ const r=zoneCanvas.getBoundingClientRect(); addPoint((e.clientX-r.left)/r.width*960,(e.clientY-r.top)/r.height*540); },240); }); + zoneCanvas.addEventListener('dblclick', e => { e.preventDefault(); clearTimeout(clickTimer); finishDrawing(); }); + $$('.tool-btn[data-tool]').forEach(btn => btn.addEventListener('click', () => { if(btn.dataset.tool===activeTool) return; if(points.length && !confirm('切换绘制工具会放弃当前图形,是否继续?')) return; activeTool=btn.dataset.tool; points.length=0; drawingComplete=false; $$('.tool-btn[data-tool]').forEach(b=>{ const on=b===btn; b.classList.toggle('active',on); b.setAttribute('aria-pressed',String(on)); }); drawDraft(); $('#editorHint').textContent=activeTool==='line'?'警戒线:依次添加起点与终点,箭头显示方向;可使用“反转方向”。':activeTool==='pointer'?'选择:查看已有区域;切换绘制工具可新建草稿。':'多边形:点击画面添加顶点,再选择“完成绘制”;也可编辑右侧坐标。'; })); + $('#undoPoint').addEventListener('click', () => { points.pop(); drawingComplete=false; drawDraft(); }); + $('#finishDrawing').addEventListener('click',finishDrawing); + $('#reverseLine').addEventListener('click',()=>{ if(points.length!==2){ notify('添加起点和终点后才能反转方向。'); return; } points.reverse(); drawDraft(); notify('警戒线方向已反转。'); }); $('#clearPoints').addEventListener('click', () => { if (points.length && confirm('清空当前未保存的绘制草稿?')) { points.length=0; drawDraft(); } }); $('#addCoordinate').addEventListener('click', () => { const x=Number($('#coordX').value), y=Number($('#coordY').value); if (!Number.isFinite(x)||!Number.isFinite(y)||x<0||x>100||y<0||y>100) { notify('坐标必须在 0–100% 之间。'); return; } addPoint(x*9.6,y*5.4); }); - $('#discardDraft').addEventListener('click', () => { if (!points.length || confirm('放弃未保存的区域修改?')) { points.length=0; drawDraft(); notify('已放弃本次修改,已保存区域和 Bell 规则均未改变。'); } }); - $('#saveDraft').addEventListener('click', () => { if ((activeTool==='polygon'&&points.length<3)||(activeTool==='line'&&points.length<2)) { notify(activeTool==='line'?'警戒线至少需要 2 个点。':'多边形至少需要 3 个点。'); return; } points.length=0; drawDraft(); $('#draftBadge').className='badge success'; $('#draftBadge').textContent='区域 v8 已保存'; notify('检测区域 v8 已保存;关联的 Bell 规则不会被自动发布。'); }); + $('#pointList').addEventListener('click',event=>{ const button=event.target.closest('[data-delete-point]'); if(!button)return; points.splice(Number(button.dataset.deletePoint),1); drawingComplete=false; drawDraft(); }); + $('#pointList').addEventListener('change',event=>{ const xIndex=event.target.dataset.pointX, yIndex=event.target.dataset.pointY; if(xIndex===undefined&&yIndex===undefined)return; const index=Number(xIndex??yIndex), value=Number(event.target.value); if(!Number.isFinite(value)||value<0||value>100){ notify('坐标必须在 0–100% 之间。'); drawDraft({persist:false}); return; } if(xIndex!==undefined) points[index].x=value*9.6; else points[index].y=value*5.4; drawingComplete=false; drawDraft(); }); + $('#discardDraft').addEventListener('click', () => { if (!points.length || confirm('放弃未保存的区域修改?')) { points.length=0; drawingComplete=false; clearStoredDraft(); drawDraft({persist:false}); $('#autosaveStatus').textContent='未保存草稿已清除;服务器上的区域版本未改变。'; notify('已放弃本次修改,已保存区域和 Bell 规则均未改变。'); } }); + $('#simulateConflict').addEventListener('click',()=>{ serverVersion+=1; $('#autosaveStatus').className='notice warning'; $('#autosaveStatus').textContent=`服务器区域已更新为 v${serverVersion};当前草稿基线较旧,保存将被阻止。`; notify('已模拟另一位用户保存了新版区域。'); }); + $('#saveDraft').addEventListener('click', () => { if ((activeTool==='polygon'&&points.length<3)||(activeTool==='line'&&points.length<2)) { notify(activeTool==='line'?'警戒线至少需要 2 个点。':'多边形至少需要 3 个点。'); return; } if(!drawingComplete){ notify('请先选择“完成绘制”,确认图形边界或警戒线方向。'); return; } const savedBase=JSON.parse(safeSession(store=>store.getItem(draftKey(currentDevice.serial)))||'{}').baseVersion; if(savedBase && savedBase!==serverVersion){ $('#autosaveStatus').className='notice warning'; $('#autosaveStatus').textContent=`保存被阻止:服务器为 v${serverVersion},草稿基于 v${savedBase}。请记录坐标后重新载入最新版本。`; notify('检测到版本冲突,未覆盖其他用户的修改。'); return; } serverVersion+=1; points.length=0; drawingComplete=false; clearStoredDraft(); drawDraft({persist:false}); $('#draftBadge').className='badge success'; $('#draftBadge').textContent=`区域 v${serverVersion} 已保存`; $('#autosaveStatus').className='notice'; $('#autosaveStatus').textContent='保存成功;当前没有未保存草稿。'; notify(`检测区域 v${serverVersion} 已保存;关联的 Bell 规则不会被自动发布。`); }); $('#simulateCalibration').addEventListener('click', () => { $('#calibrationBadge').className='badge warning'; $('#calibrationBadge').textContent='区域待校准'; notify('画面分辨率或旋转发生变化,现有区域已标记为待校准。'); }); - const dialog = $('#cameraDialog'); - $('#openCameraDialog').addEventListener('click', () => dialog.showModal()); - $('#testConnection').addEventListener('click', async e => { const b=e.currentTarget; b.disabled=true; b.textContent='测试中…'; $('#testResult').textContent='正在读取 Profiles 与设备时间'; await new Promise(r=>setTimeout(r,700)); b.disabled=false; b.textContent='重新测试'; $('#testResult').innerHTML='连接成功:2 个 Profile,时间漂移 +0.4s'; }); - $('#saveCamera').addEventListener('click', () => notify('摄像头已保存为“待激活”,凭据不会显示在列表中。')); + const deviceDialog = $('#deviceDialog'); + function updatePrivacyAdmission() { const video=$('#deviceModality').value==='video'; const option=$('#deviceArea').selectedOptions[0]; const blocked=video&&option.dataset.capturePolicy==='non_imaging_only'; $('#privacyAdmission').className=`notice${blocked?' warning':''}`; $('#privacyAdmission').textContent=blocked?'该区域仅允许非成像设备:视频设备的新增或启用将被拒绝;已有关系不会被伪造为已停用。':video?'当前区域允许图像采集;保存时仍会再次校验区域策略。':'当前设备不采集图像,可进入“仅非成像设备”区域。'; $('#saveDevice').disabled=blocked; return !blocked; } + function updateDeviceForm(){ const radar=$('#deviceModality').value==='radar'; $$('.video-field').forEach(field=>field.hidden=radar); $$('.radar-field').forEach(field=>field.hidden=!radar); $('#deviceProtocol').value=radar?'mqtt':'onvif'; $('#deviceName').value=radar?'操场西侧雷达':'北围墙 04'; $('#deviceSerial').value=radar?'RAD-DEMO-002':'CAM-DEMO-004'; $('#testResult').textContent=radar?'保存前验证遥测主题、声明能力与设备时间。':'保存前验证 Profiles、StreamUri、声明能力与设备时间。'; updatePrivacyAdmission(); } + $('#openDeviceDialog').addEventListener('click', () => { updateDeviceForm(); deviceDialog.showModal(); }); + $('#deviceModality').addEventListener('change',updateDeviceForm); $('#deviceArea').addEventListener('change',updatePrivacyAdmission); + $('#testConnection').addEventListener('click', async e => { const b=e.currentTarget; const radar=$('#deviceModality').value==='radar'; b.disabled=true; b.textContent='测试中…'; $('#testResult').textContent=radar?'正在订阅测试遥测并读取能力声明':'正在读取 Profiles、StreamUri 与设备时间'; await new Promise(r=>setTimeout(r,700)); b.disabled=false; b.textContent='重新测试'; $('#testResult').innerHTML=radar?'连接成功:距离、速度、方位角 · 1 Hz':'连接成功:2 个 Profile,时间漂移 +0.4s'; }); + $('#saveDevice').addEventListener('click', () => { if(!updatePrivacyAdmission()) { notify('隐私准入失败:此区域禁止新增图像采集设备。'); return; } const type=$('#deviceModality').value==='radar'?'毫米波雷达':'视频设备'; deviceDialog.close(); notify(`${type}已保存为“待激活”;能力与区域策略会在启用时再次校验。`); }); + + const operationDialog=$('#operationDialog'); + const operationCopy={ + 'pause-inference':'影响:停止新推理;媒体连接保持在线。执行后将显示期望态与实际态的收敛过程。', + 'disable-ingest':'影响:实时监控与新推理都将停止;历史事件和区域版本保留。', + 'kick-session':'影响:当前连接立即断开;由于期望态仍为启用,适配器可能自动重连。' + }; + function openOperationDialog(serial=currentDevice.serial){ const device=deviceData.find(item=>item.serial===serial)||currentDevice; currentDevice=device; $('#operationDeviceName').textContent=device.name; $('#operationDeviceSerial').textContent=device.serial; $('#operationProgress').textContent='尚未执行。重复提交不会产生重复副作用,但每次请求都会记录审计。'; operationDialog.showModal(); } + $('#openOperationDialog').addEventListener('click',()=>openOperationDialog()); + $$('input[name="deviceOperation"]').forEach(input=>input.addEventListener('change',()=>$('#operationImpact').textContent=operationCopy[input.value])); + $('#confirmOperation').addEventListener('click',async event=>{ const button=event.currentTarget; const action=$('input[name="deviceOperation"]:checked').value; const expected=action==='disable-ingest'?'停用':action==='pause-inference'?'推理暂停':'启用'; button.disabled=true; $('#operationProgress').innerHTML=`期望态:${expected} · 实际态:执行中…`; await new Promise(resolve=>setTimeout(resolve,800)); const actual=action==='disable-ingest'?'已停止':action==='pause-inference'?'推理已暂停':'会话已断开,等待自动重连'; $('#operationProgress').innerHTML=`期望态:${expected} · 实际态:${actual} · 已记录本次审计`; button.disabled=false; notify(`${currentDevice.name}:${actual}。`); }); $('#fakeUpload').addEventListener('click', () => notify('演示:已选择 cameras-demo.csv,检测到 1 个重复序列号。')); - drawDraft(); + renderDevices(); + drawDraft({persist:false}); const query = new URLSearchParams(location.search); if (query.get('view') === 'device-detail' || query.has('camera')) { openDeviceDetail(query.get('camera') || 'CAM-S2-001'); - if (query.get('return') === 'bell-rule') $('#ruleReturnContext').hidden = false; + if (query.get('return_to') === 'bell-rule') $('#ruleReturnContext').hidden = false; } diff --git a/docs/raw/02-需求分析.md b/docs/raw/02-需求分析.md index 0209d0b..87dbfd8 100644 --- a/docs/raw/02-需求分析.md +++ b/docs/raw/02-需求分析.md @@ -125,8 +125,8 @@ erDiagram | --- | --- | --- | | **Tenant 租户** | 计费与数据隔离的最小单位 | 所有查询必须带 tenant_id,无例外 | | **Site 站点** | 一个物理场所(一户 / 一所学校 / 一个小区) | 升级链、时段策略挂在这一层 | -| **Area 区域** | 站点内的逻辑分区(教学楼 / 单元 / 客厅) | 用于权限范围与报表口径 | -| **Device 设备** | **不叫 Camera**,`device_type` 取 camera/radar/door/button | 为异构传感器预留(继承参考项目 §4.5) | +| **Area 区域** | 站点内的逻辑分区(教学楼 / 单元 / 客厅) | 用于权限范围、报表口径与设备准入;`capture_policy` 表达是否仅允许非成像设备 | +| **Device 设备** | **不叫 Camera**;`modality` 表达 video/radar/contact/button/wearable/other,`capabilities` 表达成像、媒体、遥测等具体能力 | 模态负责稳定分类,能力负责决定字段与操作,避免接入新设备时重做台账和页面 | | **Zone 画面区域** | 画在某路画面上的多边形或警戒线 | 与 Area 是两回事:Area 是物理概念,Zone 是像素坐标 | | **Rule 规则** | 「主体 × 条件 × 时空 × 动作」的可配置组合 | 三级覆盖:租户默认 → 站点 → 设备 | | **Event 事件** | 一次被判定成立的客观发生 | **不可变**。误判只能被标记,不能被删改 | @@ -612,17 +612,19 @@ GPU 型号和数量不在需求阶段写死。M1 用 5 路打通,M3 以 16 路 tenants(id, code, name, status, plan, created_at) sites(id, tenant_id, code, name, scene_pack, subnet CIDR, timezone, status, max_video_channels INT NOT NULL DEFAULT 16 CHECK (max_video_channels BETWEEN 1 AND 128)) -areas(id, site_id, name, parent_id) +areas(id, site_id, name, parent_id, + capture_policy) -- video_allowed | non_imaging_only -- ── 设备(不叫 cameras,为异构传感器预留)──── devices( id, tenant_id, site_id, area_id, - device_type, -- camera | radar | door | button + modality, -- video | radar | contact | button | wearable | other + device_kind, -- camera | mmwave_radar | door_contact | panic_button | ... + capabilities JSONB, -- captures_image / media_stream / telemetry / battery / spatial_config / ... serial UNIQUE, -- ⚠️ 身份用序列号,不用 IP vendor, model, onvif_addr, onvif_user, onvif_secret, -- secret 加密存储 state, -- pending_activation | active | offline | disabled - privacy_flag, -- 隐私区域标记,为 true 时拒绝 camera 类型 last_seen_at ) stream_bindings(id, device_id, mtx_instance, inference_shard, path_name, rtsp_url, profile_token, enabled, diff --git a/docs/raw/03-通用场景应用方案.md b/docs/raw/03-通用场景应用方案.md index d6ca725..12e41cc 100644 --- a/docs/raw/03-通用场景应用方案.md +++ b/docs/raw/03-通用场景应用方案.md @@ -921,7 +921,7 @@ scene_packs/campus@1.2/ | 约束 | 实现 | | --- | --- | -| 卧室、卫生间**禁装摄像头** | `devices.privacy_flag = true` 时系统拒绝创建 `device_type = camera`(代码级硬约束) | +| 卧室、卫生间**禁装摄像头** | `areas.capture_policy = non_imaging_only` 时,Sense 在设备新增/启用写路径拒绝具有 `captures_image` 能力的设备(代码级硬约束);策略读取失败时拒绝新变更并告警,不静默切断已有链路 | | 平时不解码不上传 | 触发式推理 + `record: no` + 边缘环形缓冲 | | 家属只能看自己家的告警关联片段 | RBAC:`family` 角色的证据访问范围 = `site_id` ∩ `alert_events` | | 3 分钟内必有真人接手 | 4 级升级链(30s/60s/90s → 呼叫中心) | @@ -1228,7 +1228,7 @@ redis 消息总线 - [ ] 终止服务时的底库彻底删除义务与证明方式已约定 ## 架构 -- [ ] `devices` 表而非 `cameras`,含 `device_type` +- [ ] `devices` 表而非 `cameras`,含 `modality + capabilities`;Area 持有 `capture_policy` - [ ] 设备身份用序列号 - [ ] 触发入口独立成 handler - [ ] `sync_state` 每系统一行 diff --git a/docs/raw/08-三系统职责划分.md b/docs/raw/08-三系统职责划分.md index 31b7170..0188d92 100644 --- a/docs/raw/08-三系统职责划分.md +++ b/docs/raw/08-三系统职责划分.md @@ -23,7 +23,7 @@ **Sense —— 把现场的流和信号稳定地拿进来并管住** -- 设备台账(`devices`,含 `modality`:video / radar / contact / button / wearable) +- 设备台账(`devices`,含 `modality`:video / radar / contact / button / wearable / other,以及决定成像、媒体、遥测、空间配置等页面与操作的 `capabilities`) - ONVIF 客户端:连接、GetProfiles、GetStreamUri、SetSystemDateAndTime - mediamtx API 客户端(**自行用 oapi-codegen 从其 OpenAPI 生成**,不依赖第三方 SDK) - **对账器**:水平触发、`sync_state`、独立信号量、10% 孤儿删除安全闸、只收敛不回滚 @@ -80,7 +80,7 @@ Sense/ │ ├── sense-api/ 控制面服务:设备台账、鉴权回调、对账器 │ └── sense-agent/ 边缘节点:推流、环形缓冲、隧道 ├── internal/ -│ ├── device/ 设备台账(devices,含 modality) +│ ├── device/ 设备台账(devices,含 modality + capabilities) │ ├── onvif/ 连接、GetProfiles、GetStreamUri、SetSystemDateAndTime │ ├── mtx/ oapi-codegen 生成的 mediamtx 客户端 + 薄封装 │ ├── reconcile/ 对账循环(幂等 + 退避 + 安全闸) @@ -138,7 +138,7 @@ Bell/ --- -## 3. 七条边界(不写死就会漂) +## 3. 八条边界(不写死就会漂) | # | 边界 | 定论 | | --- | --- | --- | @@ -149,8 +149,9 @@ Bell/ | 5 | 一个 PostgreSQL 还是三个 | **一个实例,schema 分离**(`sense` / `bell`),Brain 无 schema。"DB 是唯一真相源"是对账器成立的前提,拆库即失效 | | 6 | 16 路与 128 路分别指什么 | **16 路是默认交付规格,128 路是本阶段单逻辑站点上限**,都不是“单进程/单服务器保证值”。媒体与推理必须横向分片;单机承载量由分辨率、码率、帧率、模型和硬件基准测试决定 | | 7 | 站点配额谁拥有、谁执行 | **Bell 拥有 `sites.max_video_channels`,Sense 在设备新增/启用写路径执行**。通过版本化内部 API 或只读投影同步,不允许 Sense 直接写 Bell schema;依赖暂时不可用时拒绝新变更,但已有视频链路继续运行 | +| 8 | 设备模态与隐私准入怎么建模 | 台账使用 `modality + capabilities`,不以摄像头作为唯一根实体;**Area 拥有 `capture_policy`,Sense 按设备 `captures_image` 能力在新增/启用写路径执行**。策略不可用时拒绝新变更并告警,已有链路不静默停用;M1 固化模型,M6 再增加非视频适配器 | -第 2、3、6、7 条是本文档新定的,若实施中发现更合适的切法,改这里并同步《02》《03》。 +第 2、3、6、7、8 条是本文档新定的,若实施中发现更合适的切法,改这里并同步《02》《03》。 --- @@ -179,6 +180,6 @@ DB schema sense — bell | **M3** 默认 16 路推理 + 规则引擎 + 事件预警 | **Brain + Bell 同时起步**,契约在此首次被真实使用 | | **M4** 64 路分片 + 灰度 + 管理系统 | Sense 与 Brain 完成横向分片,Bell 完成 128 路规模下的列表与批量交互;验证单分片故障隔离 | | **M5** 128 路容量验收 + 第二三场景包 + 触发式推理 + 人脸 | Sense/Brain 验证扩容不改业务代码;Bell 的 `packs/`(**纯配置**)+ Brain 的模型与触发 | -| **M6** 异构传感器 + 两级判定 | Sense 的 `trigger/` 与 `device/`(modality 扩展)+ Brain 两级判定 | +| **M6** 异构传感器 + 两级判定 | Sense 的 `trigger/` 与 `device/` 增加非视频协议适配器与能力实现(`modality + capabilities` 模型已从 M1 保留)+ Brain 两级判定 | > M3 是契约第一次被真实使用的时刻。**在此之前契约允许原地修订,之后版本递增规则绝对生效**(契约 README §1)。 diff --git a/docs/raw/09-Sense原型评审-IX草稿.md b/docs/raw/09-Sense原型评审-IX草稿.md new file mode 100644 index 0000000..9809b9a --- /dev/null +++ b/docs/raw/09-Sense原型评审-IX草稿.md @@ -0,0 +1,147 @@ +# Sense 原型评审 → IX 修订稿 + +> 评审对象:`docs/design/sense/index.html`(T-004) +> 初始输入:Claude 评审草稿,2026-08-04 +> 复核:Codex 从产品、全栈架构与 UI/UX 角度对照《02》《03》《08-三系统职责划分》、US/IX 和当前原型核验 +> 状态:项目负责人已确认按本修订稿继续;最终行为已同步到 `docs/08-interaction-checklist.md` + +--- + +## 1. 评审结论 + +初始评审指出的五类缺口方向成立:设备模态、停用收敛、隐私准入、检测区域几何编辑、草稿与跨系统上下文都需要进入正式交互清单。但原稿不能直接合入,原因有三类: + +1. 部分证据与当前代码不一致:Sense 页内导航不会清空几何点,Sense→Bell 已携带 `camera + zone`,单媒体分片 32 路已有架构来源。 +2. 部分方案把后端字段直接等同产品能力:非视频设备仍需要连接与健康,隐私准入不能只看显示名称或单一模态。 +3. 部分行为不符合审计和草稿体验:幂等不等于不记审计,避免丢草稿应优先自动保存,而不是每次导航都弹确认。 + +原产品边界保持不变: + +- Sense 管设备接入、健康、媒体与设备空间配置。 +- 视频设备的检测区域继续位于设备详情,不恢复一级“区域规则”。 +- Bell 管场景、继承、时段、持续时间、试运行、发布和升级链。 +- 两个系统通过稳定深链和可恢复草稿保持连续体验。 + +--- + +## 2. 最终新增 IX + +| ID | 场景 | 最终约定 | 关联 US | 阶段 | +| --- | --- | --- | --- | --- | +| IX-014 | 设备模态与能力 | 一级入口为“设备”;列表可按 `modality` 筛选,添加时先选模态,详情按 `capabilities` 渐进展示;视频设备显示画面/媒体/检测区域,非视频设备保留连接与健康并显示对应遥测,不用禁用的视频页签占位 | US-008 | M2/M6 | +| IX-015 | 停用与收敛 | 明确区分暂停推理、停用设备接入和仅踢当前会话;操作前展示对观看、推理、录制与证据回捞的影响并二次确认;期望态立即改变、实际态经对账收敛,部分失败可逐项重试;重复请求不产生重复副作用,但每次请求均留审计结果 | US-001、US-002 | M2 | +| IX-016 | 隐私区域设备准入 | `capture_policy = non_imaging_only` 的 Area 不允许新增/启用具有成像能力的设备;前端就地禁用并解释,后端返回稳定错误码;策略读取失败时拒绝新变更并告警,已有设备遇到区域策略变化时进入显式处置流程,不静默留存或停用 | US-008 | M2/M6 | +| IX-017 | 检测区域几何编辑 | 多边形和方向警戒线支持绘制、显式完成、撤销、清空、顶点编辑;警戒线在草稿/已保存态均显示方向箭头并可反转;鼠标与键盘坐标操作等效;画面参数变化后标记待校准且不自动改坐标;区域版本与规则版本独立 | US-005 | M3 | +| IX-018 | 草稿与跨系统上下文 | 几何草稿自动保存并可恢复;只有持久化失败或切换空间类型等会破坏草稿的动作才拦截确认;Sense↔Bell 双向携带并消费 `camera + zone + return_to` 上下文;返回后恢复草稿,版本已被他人更新时提示冲突且不覆盖 | US-005 | M3 | + +`IX-003` 已覆盖设备列表分页/筛选/批量选择和 128 路按需加载,因此分页属于原型漏画,不新增 IX。 + +--- + +## 3. 关键设计修订 + +### 3.1 设备使用 `modality + capabilities` + +`modality` 用于稳定分类:video / radar / contact / button / wearable / other。`capabilities` 决定字段、页签和动作,例如: + +- `captures_image` +- `media_stream` +- `telemetry` +- `battery` +- `spatial_config` +- `directional_line` + +M2 的一级导航与台账统一使用“设备”,默认筛选视频设备;M1~M5 只完整实现 video 适配器。M6 增加非视频协议适配器,不重新设计根实体与页面层级。 + +非视频设备不展示画面、Profile 和媒体分片,但仍展示“连接与健康”,并根据能力显示最后上报、信号质量、电量、遥测或传感器校准。 + +### 3.2 隐私策略属于 Area + +隐私是物理位置的准入策略,不是设备已经创建后的标签。统一模型为: + +```text +Area.capture_policy = video_allowed | non_imaging_only +Device.modality +Device.capabilities.captures_image +``` + +Sense 在新增/启用写路径执行校验。后端返回稳定错误码,前端负责本地化说明。策略不可读取时对新成像变更 fail closed 并告警;已有链路保持当前状态并进入待处置/未收敛列表,不能因一次依赖失败被静默切断。 + +### 3.3 停用动作不能混为一个开关 + +| 用户动作 | 期望效果 | +| --- | --- | +| 暂停推理 | 卸载推理 source;接入和授权观看按策略保留 | +| 停用设备接入 | 踢当前发布会话、拒绝后续发布、卸载推理;期望态先变,实际态由对账收敛 | +| 仅踢当前会话 | 运维诊断动作;客户端可能重连,不改变持久期望态 | + +每种动作展示独立影响范围。重复请求不得生成重复资源变更或状态副作用,但所有操作尝试都应进入审计并标明“已处于目标状态”或实际结果。 + +### 3.4 几何编辑提供显式完成与等效操作 + +- 多边形至少 3 点,方向警戒线恰好 2 点。 +- 双击可作为快捷方式,但必须有可见的“完成绘制”按钮;双击事件不得额外追加顶点。 +- 警戒线草稿和保存态都渲染方向箭头,并提供“反转方向”。 +- 顶点列表支持添加、编辑、删除、撤销,键盘与鼠标结果等效。 +- 画面分辨率或旋转变化后标记待校准,不自动修改坐标,也不自动发布 Bell 规则。 + +### 3.5 草稿优先自动保存 + +页内导航不应反复打断用户。几何草稿在本地/服务端自动保存并显示状态;只有自动保存失败、切换不兼容空间类型或明确放弃草稿时才确认。关闭标签页使用平台允许的离开保护作为兜底,不能把它当主保存机制。 + +深链统一携带并消费: + +```text +camera= +zone= +return_to= +``` + +`return_to` 只能使用受信路由 token,不能接受任意外部 URL。保存时使用区域版本做乐观并发校验;发现他人已保存新版本时提示重新加载或另存草稿,不覆盖。 + +--- + +## 4. 对初始评审事实的纠正 + +| 初始判断 | 核验结果 | 最终处理 | +| --- | --- | --- | +| 128 路列表缺分页需要新 IX | `IX-003` 已覆盖 | 只补原型分页 | +| 单媒体分片 32 路无出处 | 《03》§4.2 已定义 `media_shard_max_streams: 32`,128 路建议 4 个 32 路分片 | 保留 32;原型标明当前启用数与可扩展上限 | +| 侧栏/面包屑会清空草稿 | 当前 `switchView()` 不清空 `points` | 不作为现状缺陷;仍要求跨刷新/跨系统恢复 | +| Sense→Bell 缺少 camera/zone | Sense 已发送两者,但 Bell 未消费 `zone`;Bell→Sense 缺 `zone` | 修复双方生成与消费 | +| 非视频设备不展示健康页签 | 非视频设备仍有连接、上报、信号、电量与故障状态 | 使用通用“连接与健康”,能力驱动内容 | +| 幂等操作不生成重复审计 | 不符合审计目标 | 幂等只约束副作用,每次请求保留审计结果 | + +--- + +## 5. 无障碍与安全收紧 + +- 正文、按钮文字和关键状态对比度不低于 4.5:1;大字号不低于 3:1;非文本控件、状态边界和焦点指示器不低于 3:1。 +- 当前导航显式设置 `aria-current="page"`,不能用空值属性表示当前页。 +- 响应式隐藏可见文字时,按钮仍保留 `aria-label` 等稳定可访问名。 +- Tab 必须完整实现 `tablist` / `tab` / `tabpanel`、关联属性、单一 Tab 停靠点与方向键;否则不声明 tab 角色。 +- 移动端主要操作目标不小于 44×44px,输入正文不小于 16px;几何绘制提供不依赖精确点击的表单等效路径。 + +--- + +## 6. 原型落地范围 + +Sense 原型重新生成并覆盖: + +1. 一级“设备”入口、模态筛选、分页和视频/雷达示例。 +2. 能力驱动详情:视频显示画面与检测区域,雷达显示遥测,二者都有连接与健康。 +3. 添加设备先选模态,隐私 Area 对成像设备就地拒绝。 +4. 暂停推理、停用接入、踢当前会话的独立影响确认与期望/实际收敛反馈。 +5. 显式完成绘制、方向箭头/反转、顶点编辑、自动草稿与冲突状态。 +6. Sense/Bell 双向 `camera + zone + return_to` 深链。 +7. 对比度、可访问名、完整 Tab 模式、44px 移动触控目标。 + +Bell 原型只调整深链消费和返回上下文,不改变“规则业务归 Bell、空间几何归 Sense”的产品边界。 + +--- + +## 7. 后续兼容债务(不在本原型任务改契约) + +事件 v0.1 的 schema 描述与契约 README 仍使用 `privacy_flag` 表示区域是否允许视频模态,而本次领域模型已收敛为 Area 的 `capture_policy`。两者当前语义可一一映射:`privacy_flag = true` 等价于 `capture_policy = non_imaging_only`,但不能长期保留两套真相源。 + +M2 开始 schema/API 实现前应单独建立契约任务,选择“保持 v0.1 线格式并在投影层兼容”或“按版本治理规则新增字段/版本”,同步《06》与 `docs/raw/contracts/` 后再写代码。T-004 只更新产品与架构决策,不修改已经声明为既有契约的 event v0.1。 diff --git a/docs/tasks/T-004.md b/docs/tasks/T-004.md index 52a3a51..d849594 100644 --- a/docs/tasks/T-004.md +++ b/docs/tasks/T-004.md @@ -31,30 +31,31 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, ## 关联需求与交互(如适用) -- 用户故事:Sense 关联 US-001、US-002,并为 US-005 提供摄像头空间配置;Bell 关联 US-003~US-006,并拥有 US-005 的业务规则工作流。 -- 交互清单:Sense 关联 IX-001~IX-004,并提供 IX-010 所需的区域/警戒线几何;Bell 关联 IX-005~IX-013,负责 IX-010~IX-011 的规则继承、试运行和发布。 +- 用户故事:Sense 关联 US-001、US-002、US-008,并为 US-005 提供视频设备空间配置;Bell 关联 US-003~US-006,并拥有 US-005 的业务规则工作流。 +- 交互清单:Sense 关联 IX-001~IX-004、IX-014~IX-018,并提供 IX-010 所需的区域/警戒线几何;Bell 关联 IX-005~IX-013,负责 IX-010~IX-011 的规则继承、试运行和发布。 - 相关页面 / 路由:原型阶段为 `docs/design/sense/index.html` 与 `docs/design/bell/index.html`,不定义生产路由。 ## 方案 1. 使用统一的 YoVision 深色安防控制台设计系统,Sense 与 Bell 保持品牌、状态色、控件和导航规律一致。 -2. Sense 制作为单页可交互工作台:总览、监控墙、摄像头、接入任务和系统状态;不设置一级“区域规则”。从摄像头列表进入详情,在“画面与检测区域”页签完成多边形、警戒线、撤销、清空、键盘替代、区域版本和待校准反馈。 -3. Sense 只保存摄像头空间几何并只读显示关联规则,不提供业务规则绑定、时段、持续时间、试运行或正式发布;摄像头画面参数变化时将区域标记为待校准。 +2. Sense 制作为单页可交互工作台:总览、监控墙、设备、接入任务和系统状态;不设置一级“区域规则”。统一设备台账使用 `modality + capabilities` 决定详情页和操作,M1~M5 完整展示视频设备,非视频设备以雷达原型验证信息架构,实际适配器留到 M6。 +3. 视频设备在“画面与检测区域”页签完成多边形、方向警戒线、显式完成、方向反转、逐点编辑、会话草稿恢复、乐观并发冲突和待校准反馈;Sense 只保存空间几何并只读显示关联规则,不提供业务规则绑定、时段、持续时间、试运行或正式发布。 4. Bell 制作为单页可交互值班台:除预警队列、事件证据、ack/处置、升级时间线、误报反馈和升级链外,规则编辑必须选择摄像头与检测区域版本,并负责继承、时段、持续时间、试运行、发布和回滚。 5. Sense 摄像头详情与 Bell 规则草稿通过可演示的相对深链互相进入,携带摄像头/区域上下文;原型可以跨文件,但生产产品不得要求用户重新查找同一摄像头或丢失未完成草稿。 6. 两个原型均内联 CSS/JS,使用假数据,不加载真实视频、客户信息、生产地址或外部依赖;顶部固定标注原型用途。 -7. 完成桌面、窄屏、键盘焦点、加载/空态/错误切换和 `prefers-reduced-motion` 检查,提交人工评审;产品确认前不把任务标为 `DONE`。 +7. 添加设备时先选择类型,依据能力渐进展示字段;区域的 `capture_policy` 负责隐私准入,视频设备在“仅非成像设备”区域中的新增或启用必须被阻止,策略不可用时对新写入失败关闭。 +8. 完成桌面、窄屏、横屏、键盘焦点、加载/空态/错误切换和 `prefers-reduced-motion` 检查,提交人工评审;产品确认前不把任务标为 `DONE`。 ## 不可变约束 -- 阈值 / 数值边界:默认 16 路、单站点上限 128 路;静默最长 4 小时;原型不得暗示 128 路同时播放主码流。 +- 阈值 / 数值边界:默认 16 路视频、单站点视频上限 128 路;非视频设备不占视频路数;单媒体分片建议上限 32 路、最多 4 个分片;静默最长 4 小时;原型不得暗示 128 路同时播放主码流。 - 判定式 / 状态转换:事件事实不可被处置改写;Alert 的首次投递、送达、看到、ack 和升级必须分开展示;检测区域版本与告警规则版本是不同对象;规则正式发布与试运行必须可区分,保存区域不得自动发布规则。 - 安全边界:不得展示摄像头密码、完整 RTSP/ONVIF 地址、token、真实客户/未成年人数据或人脸入口;破坏性动作须确认。 - 既有契约:原型不修改事件 v0.1、API、路由和生产技术栈,不作为生产实现代码。 ## 验收要点 -- 任务相关验证:两个 HTML 可本地打开、无外部依赖;关键按钮和状态切换可用;Sense 一级导航无“区域规则”,摄像头详情可编辑检测区域;Bell 可选择摄像头/区域并演示试运行与正式发布;双向深链保留上下文;运行 HTML 结构检查、`git diff --check` 与 `./init.ps1`。 +- 任务相关验证:两个 HTML 可本地打开、无外部依赖;关键按钮和状态切换可用;Sense 一级导航无“区域规则”,设备列表可按类型筛选/分页,视频详情可编辑检测区域,非视频详情不伪装成媒体流;Bell 可选择摄像头/区域并演示试运行与正式发布;双向深链保留 `camera + zone + return_to`;运行 HTML 结构检查、`git diff --check` 与 `./init.ps1`。 - 完整门禁:本任务不修改 schema/API/路由,因此不触发契约与导航实现门禁;若范围变化则先更新任务。 - 人工 / 设备验收:必需。项目负责人分别打开 Sense 与 Bell 原型,确认页面结构、控件集合和主要工作流;未确认前不得标记 `DONE` 或合并为最终设计。 - 构建产物:`docs/design/sense/index.html`、`docs/design/bell/index.html`;双击浏览器打开,零构建依赖。 @@ -80,6 +81,14 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, - T-004 继续保持唯一活跃任务;dispatcher 已在 Issue #6 发布 `CLAIM RENEWAL`,将本轮涉及的评审稿、US/IX、需求/架构与 Sense/Bell 原型加入 `write_paths`。当前没有其他活跃任务占用这些路径。 - 本轮按 `docs/design/README.md` 的显著改版规则重新生成 Sense 页面结构,不恢复一级“区域规则”;一级入口改为“设备”,详情按 `modality + capabilities` 渐进展示,视频设备仍在详情中维护画面检测区域,Bell 继续独占业务规则与发布。 +### 2026-08-04 Claude 评审续修实现与自测 + +- 将评审结论回写到需求、架构、用户故事和 IX:设备以 `modality + capabilities` 建模,隐私策略归属 Area;M2 冻结通用信息架构与视频准入,M6 再实现非视频适配器。明确幂等只消除重复副作用,每次请求仍写审计;暂停推理、停用接入和断开会话为三个不同动作。 +- Sense 已重构为通用设备台账:17 台示例设备、5 条/页、类型/状态/搜索筛选,视频配额单独显示为 16 / 128;雷达详情展示遥测能力和通用健康且隐藏视频区域页签。添加设备对话框按能力切换字段,并验证视频设备会被“仅非成像设备”区域阻止、雷达仍可接入。 +- 区域编辑补齐显式完成、警戒线箭头与方向反转、逐点键盘编辑/删除、双击防重复顶点、`sessionStorage` 会话草稿恢复、版本冲突阻止覆盖;工具切换会先确认,页内导航不清除草稿。Sense 与 Bell 双向链接统一携带 `camera + zone + return_to`,Bell 会读入并保留区域上下文。 +- Node.js 内联脚本编译、唯一 ID/标签目标/外部资源静态审计均通过;关键前景/背景组合对比度为 5.93~16.43:1。Edge Headless/CDP 在 1440×900、375×812 与 812×375 实测设备筛选、雷达详情、隐私准入、区域方向与冲突、Tab 方向键、停止操作收敛和双向深链;页面级无横向溢出、移动端无低于 44px 的可见主要控件、运行时 0 exception。截图已人工检查且未写入仓库。 +- 任务仍保持 `DOING`:需项目负责人打开两份原型确认最终信息架构与主要工作流,确认前不合并 PR、不标记 `DONE`。 + ### 2026-08-03 任务定义 - 依据用户请求、US-001~US-006、IX-001~IX-013 和 MiBeeNvr 本地只读截图定义两个单文件原型范围。 @@ -116,4 +125,4 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, - Node.js 对两个内联脚本执行语法编译检查通过。Edge Headless/CDP 在 1440×900 与 375×812 验证摄像头详情、坐标绘制、区域保存、画面参数变化后待校准、Sense→Bell 深链、Bell 规则草稿、Bell→Sense 返回链和正式发布确认;页面无横向溢出,控制台 0 error,运行时 0 exception。 - 浏览器测试首次发现 Sense 移动端区域画布受 `min-height + aspect-ratio` 影响发生横向溢出,并发现条件上下文条的 `hidden` 被组件 `display` 覆盖;两项均已修复并重新通过桌面/移动截图人工检查。临时 QA 截图未写入仓库。 - `./init.ps1` 通过:上下文校验、16 个治理单测与完整治理检查均成功;`git diff --check` 通过。任务继续保持 `DOING`,等待项目负责人确认修订后的信息架构与工作流。 - + -- 2.55.0 From e0b0bfafebb23c2d74291fbe1bf578b5a7384ac6 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Tue, 4 Aug 2026 11:08:41 +0800 Subject: [PATCH 11/14] docs(design): close Sense second-review gaps --- docs/02-requirements.md | 5 +- docs/04-architecture.md | 21 +- docs/07-user-stories.md | 30 +- docs/08-interaction-checklist.md | 14 +- docs/design/bell/index.html | 51 +- docs/design/sense/index.html | 754 +++++----------------------- docs/raw/02-需求分析.md | 12 +- docs/raw/03-通用场景应用方案.md | 11 +- docs/raw/08-三系统职责划分.md | 10 +- docs/raw/09-Sense原型评审-IX草稿.md | 1 + docs/raw/11-Sense原型-第二轮缺口.md | 139 +++++ docs/raw/12-Sense原型-待办清单.md | 189 +++++++ docs/tasks/T-004.md | 26 +- 13 files changed, 576 insertions(+), 687 deletions(-) create mode 100644 docs/raw/11-Sense原型-第二轮缺口.md create mode 100644 docs/raw/12-Sense原型-待办清单.md diff --git a/docs/02-requirements.md b/docs/02-requirements.md index c08246c..d1629d5 100644 --- a/docs/02-requirements.md +++ b/docs/02-requirements.md @@ -33,7 +33,8 @@ - 批量开通不依赖逐路手工操作,支持待激活中间态。 - 探活、离线告警、开通校时、断线自动恢复。 - 容量写入时校验站点配额;配额服务不可用时拒绝新增/启用,但不影响已有流。 -- 隐私准入策略属于 Area,使用 `capture_policy = video_allowed | non_imaging_only`;Sense 在设备新增/启用时依据设备成像能力校验。策略读取失败时拒绝新的成像设备变更并告警,已有链路不静默停用或伪装为已收敛。 +- Site、Area 与隐私准入策略由 Bell 统一持有;`capture_policy = video_allowed | non_imaging_only` 通过版本化内部 API 或只读投影提供给 Sense。Sense 在设备新增/启用时依据设备成像能力校验,并显示策略版本/同步状态;策略读取失败时拒绝新的成像设备变更并告警,已有链路不静默停用或伪装为已收敛。 +- ONVIF 能力探测必须持久化厂商、型号、固件、认证方式、Profiles、校时与事件订阅结果以及探测时间;UI 区分探测值和最终生效值。M6 前未实现的非视频协议适配器使用 `adapter_not_ready`,不得伪造在线状态或遥测。 ### 3.2 分析与规则 @@ -63,12 +64,14 @@ - 多租户数据、账号、配置和存储隔离;最小 RBAC 为平台管理员、租户管理员、站点管理员、值班员、只读。 - 全链路审计,预警生命周期可追溯。 - Prometheus/Grafana 至少覆盖设备在线、流状态、推理延迟、事件量、未收敛项和投递 SLA。 +- Sense 提供面向接入运维的运维中心,聚合对账差异、重试退避、孤儿安全闸、媒体/推理分片、边缘隧道/补传和运维告警;它不承担 Bell 的业务预警和全局管理审计。 - 断网时边缘缓存事件,恢复后补传。 - 不在代码、日志、证据文件名和工单中泄露摄像头凭据、客户名或敏感地址。 ### 3.6 管理端与外部集成 - Bell 自研并持有事件、Alert、ack、升级链、租户和审计真相;客户既有平台不得成为这些状态的唯一真相源。 +- Bell 管理端持有 Tenant、Site、Area、RBAC、配额和全局审计真相;Sense 只消费当前租户/站点/角色上下文和带版本的配额/Area 策略投影。Area 策略变更与已有成像设备冲突时必须显式迁移或取消,禁止静默停用。 - 对外使用版本化 OpenAPI/Webhook;M3 客户端为值班室 Web + 响应式移动 H5,可嵌入客户系统。 - 投递层必须使用供应商无关的 provider 接口;试点至少有本地声光/Web 与一条短信或语音,生产前补齐两条独立路径及故障切换。 diff --git a/docs/04-architecture.md b/docs/04-architecture.md index cb5b9aa..ad2c8a6 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -16,16 +16,16 @@ YoVision 使用“通用底座 + 场景包”,按变化频率分为: | 系统 | 语言/状态 | 职责 | 不负责 | | --- | --- | --- | --- | -| Sense | Go,有状态 | 设备台账(`modality + capabilities`)、ONVIF、MediaMTX 控制、对账、探活、隧道、设备型触发、流分片、隐私区域设备准入 | AI 判定、事件业务、预警 | +| Sense | Go,有状态 | 设备台账(`modality + capabilities`)、ONVIF 能力探测、MediaMTX 控制、对账、探活、隧道、设备型触发、流分片、配额/Area 策略投影与准入执行、接入运维中心 | AI 判定、事件业务、预警、Tenant/Site/Area/RBAC/全局审计真相 | | Brain | Python/CUDA,业务无状态 | 解码/推理、检测/姿态/跟踪/ReID、时间窗判定、事件 mapper、像素级触发 | 设备真相源、告警升级、租户权限 | -| Bell | Go + Web,有状态 | 事件校验/存储、规则、预警状态机、投递、反馈、租户/RBAC、审计、配额真相源和管理端 | 媒体转发、模型执行 | +| Bell | Go + Web,有状态 | 事件校验/存储、规则、预警状态机、投递、反馈、Tenant/Site/Area/RBAC、全局审计、配额与 `capture_policy` 真相源和统一管理端 | 媒体转发、模型执行、设备实际态 | MediaMTX、PostgreSQL、MinIO、Prometheus 等作为独立基础设施部署。 ## 3. 部署与系统边界 - 首期每个客户部署一套私有实例,数据和事件证据留在客户环境;数据库实体、RBAC、配置与 API 从第一版携带 `tenant_id` 并保持 SaaS-ready 边界。 -- Bell 自研且是事件、Alert、ack、升级链、租户和审计的唯一业务真相源;客户平台通过版本化 OpenAPI/Webhook 集成,不反向接管核心状态机。 +- Bell 自研且是事件、Alert、ack、升级链、Tenant/Site/Area/RBAC、配额和全局审计的唯一业务真相源;客户平台通过版本化 OpenAPI/Webhook 集成,不反向接管核心状态机。Sense 只保存执行所需的版本化只读投影,不形成第二份组织/策略真相。 - M1–M3 的视频入口只有 ONVIF/RTSP。现有 NVR 在售前盘点;仅支持 GB/T 28181 的项目必须建立独立适配器任务,不把国标信令混入 Sense 最小骨架。 - M3 客户端为值班室 Web + 响应式移动 H5,可嵌入客户系统;是否开发原生 App 在 M4 后另行决定。 @@ -45,7 +45,7 @@ Sense ── 视频流/触发信号 ──> Brain 主流程: -1. Bell 持有站点配额;Sense 在新增/启用设备时通过版本化内部 API 或只读投影校验。 +1. Bell 持有站点、Area、配额与 `capture_policy`;Sense 在新增/启用设备时通过版本化内部 API 或只读投影校验,并记录所用投影版本。 2. Sense 维护设备期望态,通过 MediaMTX API 和对账器收敛实际态。 3. Brain 消费视频与触发信号,产生符合 v0.1 的事件。 4. Bell 做 schema 与代码级断言,生成平台 ULID,保存不可变事件。 @@ -55,7 +55,7 @@ Sense ── 视频流/触发信号 ──> Brain 首个 M3 数据流部署在 S2 民办寄宿学校的 16 路高风险点位,只运行越线、危险区域和聚集等匿名规则,不加载人脸底库。 -## 5. 十条不可越界的决定 +## 5. 十二条不可越界的决定 1. MediaMTX 独立运行,Sense 管配置与生命周期。 2. 设备型触发源归 Sense;需要解码的像素级触发归 Brain。 @@ -66,7 +66,9 @@ Sense ── 视频流/触发信号 ──> Brain 7. Bell 拥有 `site.max_video_channels`,Sense 在设备写路径执行;不跨 schema 直接写。 8. 事件片段写入客户侧 MinIO/S3,常态录像留在客户 NVR;元数据/审计、人脸和训练样本使用独立生命周期。 9. 投递状态机只依赖 Bell provider 接口,不直接依赖某家短信或语音 SDK;生产前至少两条独立路径并能故障切换。 -10. 设备领域模型使用 `modality + capabilities`,页面不以摄像头作为唯一根实体;隐私策略属于 Area,Sense 在新增/启用写路径按成像能力执行,策略不可用时只阻断新变更,不静默切断已有链路。 +10. 设备领域模型使用 `modality + capabilities`,页面不以摄像头作为唯一根实体;未实现协议适配器明确为 `adapter_not_ready`,不得用模拟遥测伪装交付。 +11. Tenant/Site/Area/RBAC、配额、`capture_policy` 与全局审计属于 Bell;Sense 只读消费版本化投影并在设备写路径执行,投影不可用时只阻断相关新变更,不静默切断已有链路。 +12. Sense 的设备操作审计先写本地持久化 outbox,再由幂等 relay 异步送入 Bell 全局审计;不得使用“先执行高风险操作、再尽力入队”的顺序。具体字段、签名、重放与留存契约必须由独立 API/契约任务冻结后实现。 ## 6. 容量架构 @@ -83,14 +85,17 @@ Sense ── 视频流/触发信号 ──> Brain - 对账器水平触发、幂等、指数退避、限制并发;部分失败不做跨系统回滚,只持续收敛。 - 孤儿删除必须有 10% 安全闸和人工可观察指标。 - 配额读取失败只阻止新增/启用,不中断已有流。 +- Area 策略投影读取失败只阻止相关设备新增/启用;已有设备保持原状态并产生运维告警。策略变更与已有成像设备冲突时由 Bell 管理端显式处置。 +- 高风险设备操作在本地事务内同时写期望态与审计 outbox;异步 relay 可重试、幂等投递到 Bell。T-004 只验证交互归属,不定义或实现接口契约。 - Brain 投递失败落本地队列重试,不阻塞实时推理主链路。 - Alert 先落库再投递,进程重启恢复未完成升级链。 - 事件证据技术默认保留 30 天并按生命周期删除;客户/法务在 M3 生产上线前确认法规适用性和最终期限,技术默认值不能覆盖其结论。 ## 8. 数据与契约 -- 核心实体:Tenant → Site → Area(含 `capture_policy`)/Device(含 `modality + capabilities`)→ StreamBinding/Zone;Rule → Event → Alert → DeliveryAttempt/Ack。 -- Event 与 Alert 不合并:一个事件可触发多次预警与投递,一次预警也可聚合多个事件。 +- Bell 核心实体:Tenant → Site → Area(含 `capture_policy`)以及 Role/Binding/Quota/Audit;Sense 核心实体:Device(含 `modality + capabilities`)→ StreamBinding/Zone,以及带 `source_version`/`synced_at` 的 SiteQuota/AreaPolicyProjection。两个 schema 以稳定逻辑 ID 关联,不跨 schema 写入。 +- 业务实体:Rule → Event → Alert → DeliveryAttempt/Ack;Event 与 Alert 不合并。 +- 一个事件可触发多次预警与投递,一次预警也可聚合多个事件。 - 事件 v0.1 以 `raw/contracts/event-v0.1.schema.json` 与 `raw/contracts/README.md` 为准;未知顶层字段拒绝,只允许通过 `ext` 扩展。 - v0.1 还需代码校验时间自洽、`confidence` 当前为 null、证据文件名隐私、唯一 primary sensor 等跨字段约束。 - S2 MVP 不处理人脸。首个人脸试点最早 M5,只允许经法务/客户门禁确认的 S4 成人访客/承包商白名单(≤10,000 人),并提供非人脸替代方式。 diff --git a/docs/07-user-stories.md b/docs/07-user-stories.md index 02945d2..c434663 100644 --- a/docs/07-user-stories.md +++ b/docs/07-user-stories.md @@ -7,16 +7,16 @@ - 角色:实施工程师。 - 目标:一次导入并验证一个默认 16 路视频站点,不逐路手工配置;操作入口位于通用设备台账的视频模态筛选下。 - 价值:降低交付时间并为 128 路扩展保留操作效率。 -- 验收:支持待激活、逐设备结果、失败可重试、超配额明确拒绝;128 路规模下仍使用分页/批量流程。 -- 关联:RQ-C-01~RQ-C-08,IX-001~IX-003。 +- 验收:导入后先进入待激活,任务历史可查看逐设备成功/失败原因并仅重试失败项;认证失败时提供不回显旧密码的凭据更新路径;超配额明确拒绝;128 路规模下仍使用分页/批量流程。 +- 关联:RQ-C-01~RQ-C-08,IX-001~IX-004、IX-015。 ## US-002 查看设备与流健康 - 角色:平台运维/站点管理员。 -- 目标:快速知道哪些设备离线、时间漂移、流未收敛或分片异常。 +- 目标:快速知道哪些设备离线、认证失败、时间漂移、流未收敛、隧道异常或分片异常,并能区分期望态与各下游实际态。 - 价值:在业务预警受影响前定位故障。 -- 验收:设备期望态与实际态分开显示;断线重连可观察;业务预警和运维告警不混用。 -- 关联:RQ-C-06~RQ-C-08、RQ-C-29、RQ-C-34,IX-004。 +- 验收:设备期望态与媒体/推理/存储实际态分开显示;对账差异、退避、下次重试和孤儿安全闸可观察;边缘隧道与视频数据面分别显示;业务预警和运维告警不混用。 +- 关联:RQ-C-06~RQ-C-08、RQ-C-29、RQ-C-34,IX-004、IX-019。 ## US-003 处置业务预警 @@ -63,8 +63,24 @@ - 角色:实施工程师/站点管理员。 - 目标:在统一设备台账中按模态和能力添加、筛选与查看视频、雷达、门磁、按钮、可穿戴等设备;隐私区域只允许非成像设备。 - 价值:M2 固化不会因 M6 异构设备接入而推倒重来的信息架构,同时把隐私要求落实为系统准入约束。 -- 验收:一级入口使用“设备”;添加时先选模态,详情按能力显示页面;`non_imaging_only` Area 不允许新增或启用成像设备,策略读取失败时拒绝新变更并给出恢复路径;M2 完成信息架构与视频准入,M6 完成非视频适配器。 -- 关联:RQ-S1-05、NFR-CMP-05,IX-014、IX-016。 +- 验收:一级入口使用“设备”;添加时先选模态,详情按探测能力显示页面并标明来源、时间与生效差异;未实现适配器显示 `adapter_not_ready`,不得伪装在线;`non_imaging_only` Area 不允许新增或启用成像设备,策略读取失败时拒绝新变更并给出恢复路径;M2 完成信息架构与视频准入,M6 完成非视频适配器。 +- 关联:RQ-S1-05、NFR-CMP-05,IX-014、IX-016、IX-019。 + +## US-009 以最小权限运维接入系统 + +- 角色:平台运维/租户管理员/站点管理员/只读用户。 +- 目标:进入 Sense 时明确当前租户、站点、账号和角色,并只看到权限范围内的数据与操作。 +- 价值:让私有化单租户交付也保留 SaaS-ready 隔离边界,避免原型把管理员权限当成默认事实。 +- 验收:切换站点会刷新上下文;只读角色看不到写操作;深链越权安全拒绝且不泄露设备存在性;设备操作日志可带筛选上下文跳到 Bell 全局审计。 +- 关联:RQ-C-31~RQ-C-33,IX-013、IX-020。 + +## US-010 管理站点与区域准入策略 + +- 角色:租户管理员/站点管理员。 +- 目标:在统一管理端维护 Site、Area 层级与 `capture_policy`,并让 Sense 使用同一版本的策略执行设备准入。 +- 价值:避免 Area 在 Sense、Bell 各有一份真相,保证权限、报表和隐私准入口径一致。 +- 验收:Bell 是 Site/Area/配额/策略真相源;策略变更影响已有成像设备时必须显式选择迁移或取消,不自动停用;Sense 展示策略版本与同步状态,投影不可用时仅拒绝相关新写入。 +- 关联:RQ-S1-05、NFR-CMP-05,IX-016、IX-020。 ## 追溯规则 diff --git a/docs/08-interaction-checklist.md b/docs/08-interaction-checklist.md index 658d552..af17a63 100644 --- a/docs/08-interaction-checklist.md +++ b/docs/08-interaction-checklist.md @@ -4,10 +4,10 @@ | ID | 场景 | 必须覆盖的状态与行为 | 关联 US | 阶段 | | --- | --- | --- | --- | --- | -| IX-001 | 批量导入设备 | 下载模板、上传校验、逐行错误、重复序列号、待激活、确认写入 | US-001 | M2 | -| IX-002 | 配额与批量结果 | 显示已用/上限;超 16 不等于非法,按站点配置校验;部分成功可重试 | US-001 | M2 | +| IX-001 | 批量导入设备 | 下载模板、上传校验、逐行错误、重复序列号、待激活、确认写入;任务历史保留逐项结果、操作者与时间,支持仅重试失败项 | US-001 | M2 | +| IX-002 | 配额与批量结果 | 显示已用/上限;超 16 不等于非法,按站点配置校验;部分成功可重试;配额不可读取时只禁用新增/启用并保留已有列表与流 | US-001 | M2 | | IX-003 | 设备列表 | 分页/筛选/批量选择;128 路不一次加载所有视频和详情 | US-001 | M2/M4 | -| IX-004 | 设备健康 | 期望态、实际态、最后在线、时间漂移、分片、重试进度;运维告警独立 | US-002 | M2/M4 | +| IX-004 | 设备健康 | 期望态、媒体/推理/存储实际态、认证失败、最后在线完整时间与站点时区、时间漂移、分片、重试进度;运维告警独立,凭据更新只允许写入新值且旧值永不回显 | US-002 | M2/M4 | | IX-005 | 预警到达 | 明确严重度、站点、时间、证据可用性;重复投递不产生重复处置 | US-003 | M3 | | IX-006 | ack | 一次点击可确认,显示确认人/时间;并发 ack 有清晰结果 | US-003 | M3 | | IX-007 | 升级 | 显示当前层级、下一次升级时间、每次投递状态;失败不能伪装成功 | US-003 | M3 | @@ -17,11 +17,13 @@ | IX-011 | 规则试运行 | 明确“未正式生效”,展示命中样本与影响范围,支持取消/回滚 | US-005 | M4 | | IX-012 | 升级链/静默 | 联系人顺序、超时、双通道;静默最长 4h、显示自动恢复时间、无永久选项 | US-005 | M3/M4 | | IX-013 | 权限与隐私 | 越权统一处理;无授权租户不展示人脸入口;不泄露流 URL/凭据 | US-006 | M3/M4 | -| IX-014 | 设备模态与能力 | 一级入口为“设备”;列表可按 `modality` 筛选,添加时先选模态,详情按 `capabilities` 渐进展示;视频设备显示画面/媒体/检测区域,非视频设备保留连接与健康并显示对应遥测,不用禁用的视频页签占位 | US-008 | M2/M6 | -| IX-015 | 停用与收敛 | 明确区分暂停推理、停用设备接入和仅踢当前会话;操作前展示对观看、推理、录制与证据回捞的影响并二次确认;期望态立即改变、实际态经对账收敛,部分失败可逐项重试;重复请求不产生重复副作用,但每次请求均留审计结果 | US-001、US-002 | M2 | -| IX-016 | 隐私区域设备准入 | `capture_policy = non_imaging_only` 的 Area 不允许新增/启用具有成像能力的设备;前端就地禁用并解释,后端返回稳定错误码;策略读取失败时拒绝新变更并告警,已有设备遇到区域策略变化时进入显式处置流程,不静默留存或停用 | US-008 | M2/M6 | +| IX-014 | 设备模态与能力 | 一级入口为“设备”;列表可按 `modality` 筛选,添加时先选模态,详情按 `capabilities` 渐进展示;能力同时显示厂商/型号/固件/认证、探测来源与时间、探测值和生效值差异;视频设备显示画面/媒体/检测区域,未交付的非视频适配器显示 `adapter_not_ready`,不用虚构遥测或禁用的视频页签占位 | US-008 | M2/M6 | +| IX-015 | 停用与收敛 | 明确区分暂停推理订阅、停用设备接入和仅踢当前会话;暂停推理必须说明“解除推理侧订阅”,并在结果中显示推理订阅、其他 reader 数与上游按需拉流状态;操作前展示对观看、录制与证据回捞的影响并二次确认;期望态立即改变、实际态经对账收敛,部分失败可逐项重试;重复请求不产生重复副作用,但每次请求均留审计结果 | US-001、US-002 | M2 | +| IX-016 | 隐私区域设备准入 | Area 与 `capture_policy` 由 Bell 统一管理,Sense 消费带版本的只读投影;`non_imaging_only` 不允许新增/启用具有成像能力的设备;前端就地禁用并解释,后端返回稳定错误码;策略读取失败时拒绝新变更并告警,已有设备遇到策略变化时由 Bell 进入显式迁移/取消流程,不静默留存或停用 | US-008、US-010 | M2/M6 | | IX-017 | 检测区域几何编辑 | 多边形和方向警戒线支持绘制、显式完成、撤销、清空、顶点编辑;警戒线在草稿/已保存态均显示方向箭头并可反转;鼠标与键盘坐标操作等效;画面参数变化后标记待校准且不自动改坐标;区域版本与规则版本独立 | US-005 | M3 | | IX-018 | 草稿与跨系统上下文 | 几何草稿自动保存并可恢复;只有持久化失败或切换空间类型等会破坏草稿的动作才拦截确认;Sense↔Bell 双向携带并消费 `camera + zone + return_to` 上下文;返回后恢复草稿,版本已被他人更新时提示冲突且不覆盖 | US-005 | M3 | +| IX-019 | 运维中心与写入降级 | Sense 用一个一级“运维中心”聚合对账差异/退避/下次重试/孤儿安全闸、媒体与推理分片、边缘节点/隧道/补传、运维告警和系统状态;配额或 Area 策略不可读取时页面继续可读且已有链路不受影响,只禁用相关写操作;不得混入 Bell 业务预警 | US-002、US-008 | M2/M4/M6 | +| IX-020 | 管理上下文、RBAC 与审计归属 | 全局头部显示租户、站点、账号和角色;只读角色隐藏写操作,深链越权统一拒绝且不泄露资源是否存在;Tenant/Site/Area/RBAC/配额/全局审计由 Bell 管理,Sense 只呈现消费上下文和设备操作入口,携带 tenant/site/device/return_to 深链到 Bell 审计 | US-006、US-009、US-010 | M2/M4 | ## 全局状态 diff --git a/docs/design/bell/index.html b/docs/design/bell/index.html index cff18b3..34f1d63 100644 --- a/docs/design/bell/index.html +++ b/docs/design/bell/index.html @@ -41,6 +41,7 @@ --shadow: 0 14px 34px rgba(32, 52, 78, .12); } * { box-sizing: border-box; } + [hidden] { display: none !important; } html { min-width: 320px; background: var(--bg); } body { margin: 0; min-height: 100dvh; background: var(--bg); color: var(--text); font: 14px/1.5 Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; } button, input, select, textarea { font: inherit; } @@ -177,6 +178,14 @@ .rule-context { margin-bottom: 12px; padding: 11px 13px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid rgba(56,189,248,.34); border-radius: 10px; background: rgba(56,189,248,.08); } .rule-context[hidden] { display: none; } .rule-context p { margin: 0; color: var(--muted); } + .site-grid { grid-template-columns: minmax(260px,.72fr) minmax(520px,1.45fr); } + .site-list { padding: 8px; display: grid; gap: 7px; } + .site-item { width: 100%; min-height: 62px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--text); display: flex; justify-content: space-between; align-items: center; gap: 10px; text-align: left; } + .site-item.active { border-color: var(--primary); background: var(--primary-soft); } + .site-item small { display: block; color: var(--muted); } + .policy-note { margin: 12px 14px 0; padding: 10px 12px; border-left: 3px solid var(--warning); background: rgba(251,191,36,.08); color: var(--muted); } + .context-strip { margin-bottom: 12px; padding: 11px 13px; border: 1px solid rgba(56,189,248,.34); border-radius: 10px; background: rgba(56,189,248,.08); display: flex; align-items: center; justify-content: space-between; gap: 12px; } + .context-strip p { margin: 0; color: var(--muted); } .linked-zone { grid-column: 1/-1; padding: 11px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); } .linked-zone strong, .linked-zone small { display: block; } .linked-zone small { color: var(--muted); } @@ -217,6 +226,7 @@ .form-field { display: grid; gap: 6px; } .form-field.full { grid-column: 1/-1; } .form-field label { color: var(--muted); font-size: 12px; font-weight: 700; } + .radio-card { display: block; margin-bottom: 9px; padding: 11px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); } .toast { position: fixed; right: 20px; bottom: 20px; z-index: 1100; max-width: min(420px, calc(100% - 40px)); padding: 12px 14px; border: 1px solid rgba(52,211,153,.35); border-radius: 10px; background: #0b2c25; color: #d9fff3; box-shadow: var(--shadow); transform: translateY(20px); opacity: 0; pointer-events: none; transition: .2s ease; } .toast.show { transform: none; opacity: 1; } .muted { color: var(--muted); } @@ -240,11 +250,14 @@ .topbar { padding: 8px 12px; } .site span, .top-actions .select { display: none; } .content { padding: 12px; } + .btn, .select, .field, .textarea { min-height: 44px; } + .icon-btn { min-width: 44px; } .duty-grid, .chain-grid, .chart-grid { grid-template-columns: 1fr; } .alert-list, .action-scroll { max-height: none; } .action-scroll { display: block; } .disposition { border-left: 0; border-top: 1px solid var(--line); } .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .site-grid { grid-template-columns: 1fr; } .rule-grid { grid-template-columns: 1fr 1fr; } .fact-grid { grid-template-columns: repeat(2, 1fr); } .mobile-nav { position: fixed; z-index: 50; left: 8px; right: 8px; bottom: 8px; display: grid; grid-template-columns: repeat(5, 1fr); padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); } @@ -296,6 +309,7 @@ +
    安保值班组 · 白班08:00–20:00 · 4 人在线
    @@ -303,7 +317,7 @@
    -
    青藤寄宿学校 · 值班中心客户侧私有实例 · 当前仅匿名安全规则
    +
    青藤教育集团 · 青藤寄宿学校当前账号:林工 · 租户管理员 · Asia/Shanghai
    @@ -319,13 +333,13 @@

    待处置预警

    6 个未 ack
    -
    +
    高危 · S1

    危险区域闯入

    北围墙 01 · 23:14:08 · EVT-01J8…7A2

    -
    +
    @@ -392,9 +406,17 @@

    近 7 日预警量

    按站点日期汇总;可切换到每路每天误报数。

    处置结果

    本月已闭环事件。

    正常处置 61%误报 22%未闭环 17%
    +
    +

    站点与 Area

    Bell 是 Tenant、Site、Area、配额与隐私准入策略的唯一真相源;Sense 只消费版本化投影。

    +
    租户1青藤教育集团
    站点21 个试点中
    视频配额16 / 128默认 16,不是硬上限
    策略投影v42Sense 2 分钟前已同步
    +

    站点

    +

    青藤寄宿学校 · Area

    层级、报表口径和设备准入共用同一逻辑 ID
    策略变更不会静默处置已有设备。 若改为“仅非成像”且 Area 内已有摄像头,必须显式迁移设备或取消变更。
    Area父级capture_policy设备策略版本投影状态操作
    北围墙
    area-north-wall
    校园允许成像4 视频v42已同步
    宿舍卧室
    area-dorm-room
    宿舍楼仅非成像0 视频 · 1 雷达规划v38已同步
    实验楼入口
    area-lab-gate
    实验楼允许成像2 视频v41待 Sense 拉取
    +
    +
    +

    审计日志

    查询、ack、规则发布和证据下载均可追溯;不记录密码与可复用连接串。

    -
    只读
    +
    只读
    时间角色动作对象结果来源
    时间角色动作对象结果来源
    23:14:18站点值班员ACK_ALERTALT-1842成功值班室 Web
    22:52:06站点管理员PUBLISH_RULERULE-007成功管理端
    22:20:31只读用户VIEW_EVIDENCEEVT-01J8…52C拒绝响应式 H5
    @@ -419,6 +441,10 @@

    标记为误报

    这只会追加事件 outcome,不会修改原始事件事实和证据。

    创建限时静默

    到期自动恢复;S1 高危事件与设备离线运维告警不受此静默影响。

    + +

    编辑 Area 准入策略

    保存生成新策略版本;Sense 投影同步前会显示旧版本,不允许绕过后端准入检查。

    + +

    策略与已有设备冲突

    北围墙当前有 4 台成像设备。改为“仅非成像”前必须选择显式处置;系统不会自动停用、移动或保留违规设备。

    diff --git a/docs/raw/02-需求分析.md b/docs/raw/02-需求分析.md index 87dbfd8..aa22e25 100644 --- a/docs/raw/02-需求分析.md +++ b/docs/raw/02-需求分析.md @@ -125,7 +125,7 @@ erDiagram | --- | --- | --- | | **Tenant 租户** | 计费与数据隔离的最小单位 | 所有查询必须带 tenant_id,无例外 | | **Site 站点** | 一个物理场所(一户 / 一所学校 / 一个小区) | 升级链、时段策略挂在这一层 | -| **Area 区域** | 站点内的逻辑分区(教学楼 / 单元 / 客厅) | 用于权限范围、报表口径与设备准入;`capture_policy` 表达是否仅允许非成像设备 | +| **Area 区域** | 站点内的逻辑分区(教学楼 / 单元 / 客厅),由 Bell 统一管理 | 用于权限范围、报表口径与设备准入;`capture_policy` 表达是否仅允许非成像设备,Sense 只消费带版本的策略投影 | | **Device 设备** | **不叫 Camera**;`modality` 表达 video/radar/contact/button/wearable/other,`capabilities` 表达成像、媒体、遥测等具体能力 | 模态负责稳定分类,能力负责决定字段与操作,避免接入新设备时重做台账和页面 | | **Zone 画面区域** | 画在某路画面上的多边形或警戒线 | 与 Area 是两回事:Area 是物理概念,Zone 是像素坐标 | | **Rule 规则** | 「主体 × 条件 × 时空 × 动作」的可配置组合 | 三级覆盖:租户默认 → 站点 → 设备 | @@ -608,14 +608,18 @@ GPU 型号和数量不在需求阶段写死。M1 用 5 路打通,M3 以 16 路 在参考项目基础上扩展多租户与规则/事件。**字段级设计见《03》,此处只给结构与关键约束。** ```sql --- ── 组织(Bell 为真相源;Sense 只读配额投影)──────── +-- ── 组织(Bell 为真相源;Sense 不直接写这些表)──────── tenants(id, code, name, status, plan, created_at) sites(id, tenant_id, code, name, scene_pack, subnet CIDR, timezone, status, max_video_channels INT NOT NULL DEFAULT 16 CHECK (max_video_channels BETWEEN 1 AND 128)) areas(id, site_id, name, parent_id, capture_policy) -- video_allowed | non_imaging_only --- ── 设备(不叫 cameras,为异构传感器预留)──── +-- ── Sense 只读投影(来源版本与同步时间必须可追溯)──── +site_quota_projections(site_id, max_video_channels, source_version, synced_at) +area_policy_projections(area_id, site_id, capture_policy, source_version, synced_at) + +-- ── Sense 设备(不叫 cameras,为异构传感器预留)──── devices( id, tenant_id, site_id, area_id, modality, -- video | radar | contact | button | wearable | other @@ -703,6 +707,8 @@ CREATE INDEX ON alerts(state) WHERE state NOT IN ('closed','suppressed'); 7. 人脸相关表独立 schema、独立加密、独立审计;`persons.valid_until` 必填,无"永久有效"选项 8. `sites.max_video_channels` 默认 16、最大 128;配额在写入设备时校验,分片容量由运行时配置与压测决定 9. `sites.max_video_channels` 由 Bell 持有,Sense 通过版本化内部 API 或只读投影校验设备新增/启用;不得跨 schema 直接写入 +10. Tenant/Site/Area/RBAC、配额与全局审计属于 Bell;Sense 仅使用稳定逻辑 ID 关联并保存带 `source_version` / `synced_at` 的执行投影 +11. 高风险设备写操作与本地审计 outbox 在 Sense 同一事务提交,再由幂等 relay 异步汇入 Bell;审计接口字段、签名和重放规则另立契约任务冻结 --- diff --git a/docs/raw/03-通用场景应用方案.md b/docs/raw/03-通用场景应用方案.md index 12e41cc..283a924 100644 --- a/docs/raw/03-通用场景应用方案.md +++ b/docs/raw/03-通用场景应用方案.md @@ -1067,7 +1067,7 @@ capacity: `stream_bindings` 同时记录 `mtx_instance` 与 `inference_shard`。默认按 `site_id + device_id` 稳定分配;扩容时允许迁移,但同一设备任一时刻只能归属一个有效媒体分片和一个推理分片。 -`sites.max_video_channels` 的唯一真相源在 Bell;Sense 在设备新增/启用写路径通过版本化内部 API 或只读投影执行配额校验,不跨 schema 直接写入。配额依赖暂时不可用时只拒绝新的容量变更,已有视频链路继续运行。 +`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 路。 @@ -1115,7 +1115,8 @@ OpenTelemetry → Jaeger,排查单帧处理延迟与端到端事件链路。 | 端 | 核心功能 | | --- | --- | -| **管理系统(Web)** | 租户/站点/设备管理、批量开通、规则配置(含试运行)、Zone 画图、升级链配置、事件看板与筛选、事件详情(视频+观测回放)、处置与工单、误报标记、统计报表、审计查询、RBAC | +| **Sense 接入工作台(Web)** | 当前租户/站点上下文、设备台账与批量开通、能力探测、Zone 画图、对账/分片/边缘隧道/补传/运维告警;不管理 Tenant/Site/Area/RBAC 或全局审计 | +| **Bell 统一管理端(Web)** | Tenant/Site/Area/配额/`capture_policy`、RBAC、规则配置(含试运行)、升级链、事件看板与筛选、事件详情(视频+观测回放)、处置与工单、误报标记、统计报表、全局审计;设备操作日志从 Sense 汇入并可深链返回 | | **App(家属/值班员)** | 预警接收与 ack、事件详情与视频回看、一键呼叫、误报反馈、限时静默、设备状态、(家属)多老人切换 | | **大屏/值班台** | 实时事件流、地图/平面图点位、声光联动、当班交接 | | **Webhook/开放 API** | 对接客户既有平台、门禁、消防、工单系统 | @@ -1144,7 +1145,9 @@ OpenTelemetry → Jaeger,排查单帧处理延迟与端到端事件链路。 | --- | --- | --- | --- | --- | --- | | **Sense** | 感知系统 | L1 接入 | Go | 有(设备台账) | M1 | | **Brain** | 推理系统 | L2 流水线 + L3 算法 | Python / CUDA | **无状态** | M3 | -| **Bell** | 管理系统 | L4 业务 + L0 基座 | Go + 前端 | 有(事件、告警) | M3 → M4 | +| **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)时,另两个系统一行不改。 @@ -1228,7 +1231,7 @@ redis 消息总线 - [ ] 终止服务时的底库彻底删除义务与证明方式已约定 ## 架构 -- [ ] `devices` 表而非 `cameras`,含 `modality + capabilities`;Area 持有 `capture_policy` +- [ ] `devices` 表而非 `cameras`,含 `modality + capabilities`;Bell 持有 Area 与 `capture_policy`,Sense 只存带版本的准入投影 - [ ] 设备身份用序列号 - [ ] 触发入口独立成 handler - [ ] `sync_state` 每系统一行 diff --git a/docs/raw/08-三系统职责划分.md b/docs/raw/08-三系统职责划分.md index 0188d92..fbd9877 100644 --- a/docs/raw/08-三系统职责划分.md +++ b/docs/raw/08-三系统职责划分.md @@ -31,6 +31,7 @@ - WireGuard 控制面隧道 - mediamtx 鉴权回调(401 + 踢流是四种停用粒度之一) - 容量配额执行:设备新增/启用时读取 Bell 的站点配额,只拒绝超额变更;配额服务暂时不可用时不影响已有流 +- Area 准入执行:消费 Bell 的版本化 `capture_policy` 投影;策略不可用时只拒绝相关新写入,不静默中断已有设备 - 流绑定与媒体分片调度:维护 `mtx_instance`,默认 16 路交付;扩到 128 路时按 `media_shard.max_streams` 横向分片 - 边缘节点 agent:推流、本地环形缓冲、断网续传 - **设备型触发源**:雷达、门磁、按钮、ONVIF 事件订阅 @@ -54,8 +55,8 @@ - 告警状态机:升级链、ack、抑制、静默(**≤4h,无永久选项**) - 投递:push / 短信 / 语音,**双供应商** - 误报反馈闭环:`outcome` 回写 Brain -- 多租户 RBAC、`tenant_features`(人脸授权开关:未授权时能力在 API 与 UI 中**不可见**,不是禁用) -- 审计日志 +- Tenant/Site/Area、`capture_policy`、多租户 RBAC、`tenant_features`(人脸授权开关:未授权时能力在 API 与 UI 中**不可见**,不是禁用) +- 全局审计日志;接收 Sense 设备操作审计并保留来源上下文 - 站点容量配额的唯一真相源:`site.max_video_channels` 默认 16、上限 128;提供版本化内部读取接口/投影给 Sense 执行 - 管理后台前端 + 大屏:按 128 路设计分页/虚拟列表、筛选与批量操作,不一次性加载全部视频 @@ -149,9 +150,10 @@ Bell/ | 5 | 一个 PostgreSQL 还是三个 | **一个实例,schema 分离**(`sense` / `bell`),Brain 无 schema。"DB 是唯一真相源"是对账器成立的前提,拆库即失效 | | 6 | 16 路与 128 路分别指什么 | **16 路是默认交付规格,128 路是本阶段单逻辑站点上限**,都不是“单进程/单服务器保证值”。媒体与推理必须横向分片;单机承载量由分辨率、码率、帧率、模型和硬件基准测试决定 | | 7 | 站点配额谁拥有、谁执行 | **Bell 拥有 `sites.max_video_channels`,Sense 在设备新增/启用写路径执行**。通过版本化内部 API 或只读投影同步,不允许 Sense 直接写 Bell schema;依赖暂时不可用时拒绝新变更,但已有视频链路继续运行 | -| 8 | 设备模态与隐私准入怎么建模 | 台账使用 `modality + capabilities`,不以摄像头作为唯一根实体;**Area 拥有 `capture_policy`,Sense 按设备 `captures_image` 能力在新增/启用写路径执行**。策略不可用时拒绝新变更并告警,已有链路不静默停用;M1 固化模型,M6 再增加非视频适配器 | +| 8 | 设备模态与隐私准入怎么建模 | 台账使用 `modality + capabilities`,不以摄像头作为唯一根实体;**Bell 拥有 Area 与 `capture_policy`,Sense 消费带版本投影并按设备 `captures_image` 能力在新增/启用写路径执行**。策略不可用时拒绝相关新变更并告警,已有链路不静默停用;M1 固化模型,M6 再增加非视频适配器 | +| 9 | 接入操作审计怎么进入 Bell | Sense 在同一事务写设备期望态和持久化 outbox,再由幂等 relay 异步汇入 Bell 全局审计;不得先执行高风险操作再尽力入队。字段、签名、重放和留存由独立 API/契约任务冻结 | -第 2、3、6、7、8 条是本文档新定的,若实施中发现更合适的切法,改这里并同步《02》《03》。 +第 2、3、6、7、8、9 条是本文档新定的,若实施中发现更合适的切法,改这里并同步《02》《03》。 --- diff --git a/docs/raw/09-Sense原型评审-IX草稿.md b/docs/raw/09-Sense原型评审-IX草稿.md index 9809b9a..1500656 100644 --- a/docs/raw/09-Sense原型评审-IX草稿.md +++ b/docs/raw/09-Sense原型评审-IX草稿.md @@ -4,6 +4,7 @@ > 初始输入:Claude 评审草稿,2026-08-04 > 复核:Codex 从产品、全栈架构与 UI/UX 角度对照《02》《03》《08-三系统职责划分》、US/IX 和当前原型核验 > 状态:项目负责人已确认按本修订稿继续;最终行为已同步到 `docs/08-interaction-checklist.md` +> 后续:第二轮缺口复核与最终 IA 见 `11-Sense原型-第二轮缺口.md`、`12-Sense原型-待办清单.md`;若与本文原型范围冲突,以第二轮修订为准 --- diff --git a/docs/raw/11-Sense原型-第二轮缺口.md b/docs/raw/11-Sense原型-第二轮缺口.md new file mode 100644 index 0000000..6c5981e --- /dev/null +++ b/docs/raw/11-Sense原型-第二轮缺口.md @@ -0,0 +1,139 @@ +# Sense 原型第二轮评审复核版 + +> 原始输入:Claude《Sense 原型第二轮评审》,2026-08-04 +> 复核基准:`docs/design/sense/index.html`,提交 `e18a1ea` +> 复核视角:产品边界、全栈架构、M2 用户流程与 UI/UX +> 状态:项目负责人已确认按本复核结论继续修改 + +--- + +## 1. 总结 + +Claude 指出的对账运维、设备能力来源、非视频适配器阶段、待激活、凭据恢复、写入降级和边缘隧道等缺口方向成立;但原稿不能直接作为执行清单,主要有三类问题: + +1. **基准落后**:设备分页和推理收敛成功态已经实现,分片数据也不再是原稿引用的 `8 / 32`。 +2. **范围扩大**:T-004 当前关联 US-001、US-002、US-008 和部分 US-005,并未承诺一次画完完整 M2/M4 管理端。新增 P0 页面必须先同步 US/IX 与任务范围。 +3. **系统归属混淆**:`routes.md` 的 `/sites`、`/operations`、`/audit` 是统一管理 Web 的候选页面职责,不等于都应成为 Sense 一级导航。Bell 拥有租户/RBAC、审计、配额真相源和管理端;Sense 负责设备接入与运行态运维。 + +因此,本轮采纳“补核心流程、收敛导航、冻结归属”的方案,不采用“Sense 导航机械扩为 8 项”。 + +--- + +## 2. 对原评审事实的纠正 + +| 原判断 | 当前事实 | 处理 | +| --- | --- | --- | +| 设备列表分页未实现 | 已有 5 条/页、上一页/下一页、页数和总数 | 标记完成;批量选择未来新增时再定义跨页语义 | +| “推理绑定 16/16”仍是紫色满格 | 已改为“推理配置收敛 16/16”绿色成功态 | 标记完成 | +| 分片仍显示 `8 / 32` | 当前为 `16 / 32`,并显示“当前 1 / 最多 4” | 只补“32 是可配置建议值、最终由压测确定” | +| T-004 `write_paths` 只有三个文件 | 已扩展到需求、架构、US/IX、评审稿和两个原型 | 更新清单基准 | +| modality 筛选包含 ONVIF/MQTT | modality 是 video/radar;ONVIF/RTSP/MQTT 属于 protocol | 文档严格区分模态、设备种类、协议和能力 | + +--- + +## 3. 最终产品信息架构 + +### 3.1 Sense Edge Console + +Sense 保持最多五个一级入口,移动端底部导航不超过五项: + +```text +运行总览 +实时监控 +设备 +接入任务 +运维 + ├─ 对账与孤儿资源 + ├─ 媒体分片 + ├─ 边缘节点与隧道 + ├─ 运维告警 + └─ 系统状态 +``` + +原“系统状态”并入“运维”。总览的待收敛项、设备告警和分片状态均可下钻到运维的对应子页。Sense 顶栏显示当前租户、站点、用户和角色,并消费 Bell 下发的授权上下文,但不自建租户/RBAC 真相源。 + +### 3.2 Bell / 统一管理端 + +以下能力归 Bell/统一管理端,而不是在 Sense 重复建设一级页面: + +- Tenant、Site、Area 管理和 RBAC。 +- Area 的 `capture_policy` 编辑与已有成像设备的显式处置流程。 +- 全局审计查询;Sense 设备操作记录只提供带设备筛选的深链。 +- `site.max_video_channels` 的配置与真相源。 + +Area 与策略由 Bell 持有,Sense 使用带版本的只读投影执行新增/启用准入;投影未知或过期时 fail closed,只阻止新的成像变更,不中断已有链路。 + +--- + +## 4. 采纳的真实缺口 + +### 4.1 对账与运维中心(P0) + +- 未收敛项展示期望态/实际态具体差异、重试次数、退避间隔和下次重试时间。 +- 手动动作语义为“提高优先级并重新触发收敛”,不能绕过对账器直接修改实际资源。 +- 展示孤儿资源、占比、10% 安全闸及人工处置提示;不提供绕过安全闸的一键删除。 +- 展示媒体分片、边缘节点、控制隧道、数据面、补传队列和独立运维告警。 +- 明确“控制隧道断开但已有视频数据面仍正常”的组合态。 + +### 4.2 设备能力来源(P0) + +`capabilities` 必须来自探测/适配器结果并保存来源、探测时间与版本,不能由前端名称猜测。详情展示“已探测能力”和“当前生效能力”;重新探测显示新增/消失差异,能力降级时列出受影响的触发、Profile 或空间配置。 + +US-007 的采购白名单当前仍是版本化测试文档,无产品 UI;运行设备可只读显示“白名单型号/未验证型号”,不把白名单管理升级成 M2 P0 页面。 + +### 4.3 非视频适配器阶段态(P0) + +M1~M5 可登记 radar 等模态以验证信息架构和隐私准入,但实际态必须是 `adapter_not_ready`(用户文案“适配器未就绪 · M6”),不得显示在线/离线或模拟 MQTT 遥测连接,也不计入视频配额。 + +### 4.4 设备生命周期(P0) + +- 待激活作为可筛选状态,支持重新探测、校时、配额/策略复核、激活和逐项失败原因。 +- 认证失败与离线分开;更新凭据只写不读、不回显旧值,更新后显示重试结果。 +- 批量凭据仅允许安全导入逐设备凭据,不提供把同一密码覆盖到多台设备的一键操作。 +- Profile 切换展示对当前观看、推理、区域校准与短暂中断的影响。 + +### 4.5 写入降级与权限(P0) + +- 配额不可读:列表与已有视频可用,新增/启用被阻止。 +- Area 策略投影不可读/过期:只阻止新的成像变更,已有链路保持现状并告警。 +- 只读角色隐藏写操作;越权或资源不存在的深链使用统一安全拒绝态。 +- 原型角色切换器只用于枚举状态,不代表 Sense 拥有账号或权限数据。 + +### 4.6 措辞与可观察结果(P0) + +“暂停推理”统一为: + +> 解除推理侧对该路的订阅;有其他观看者时上游继续拉流,无其他 reader 时上游按需停止。 + +执行结果同时显示推理订阅、reader 数和上游拉流状态,避免实现成推理插件内部直接 `return` 而持续浪费带宽。 + +--- + +## 5. 降级或后置的建议 + +| 原建议 | 最终处理 | +| --- | --- | +| Sense 新增“站点与区域”一级导航 | 不采纳;Bell/统一管理端持有管理页,Sense 顶栏只做站点上下文切换 | +| Sense 新增“审计”一级导航 | 不采纳;保留设备操作记录并深链 Bell 全局审计 | +| 直接批量覆盖同一设备密码 | 不采纳;仅做逐设备写入或安全文件导入 | +| M2 提供跨媒体分片迁移 | 后置 M4;本轮只展示分片详情与影响范围 | +| M2 提供节点升级/重启 | 后置独立高风险运维任务;本轮只展示状态 | +| 提前为推理绑定、pre-roll、设备型触发创建空页面 | 不采纳;能力模型与运维子导航已保留扩展,不创建无操作价值的占位页 | + +--- + +## 6. 审计接口的独立架构任务 + +Bell 是全局审计真相源,但 T-004 不修改 `docs/api.md`。后续任务需要冻结 Sense → Bell 审计投递或等价投影契约,至少包含: + +- Sense 在执行高风险写操作前,把操作意图与审计 outbox 在本地持久化;不能“先执行后尝试记录”。 +- Bell 不可达时,只要本地 outbox 已持久化,业务操作可继续;恢复后幂等补投。 +- 明确租户/站点范围、事件 ID、操作者、目标、请求结果、重试与保留策略。 + +此项属于 API/一致性设计,不用原型中的 toast 代替契约。 + +--- + +## 7. 本轮实施边界 + +T-004 重新生成 Sense 原型并补 Bell 管理端入口,同时更新正式 US/IX 和本复核稿。`docs/api.md`、生产代码、事件 v0.1 与真实认证实现不在本任务修改;人工确认前 T-004 继续保持 `DOING`。 diff --git a/docs/raw/12-Sense原型-待办清单.md b/docs/raw/12-Sense原型-待办清单.md new file mode 100644 index 0000000..2129600 --- /dev/null +++ b/docs/raw/12-Sense原型-待办清单.md @@ -0,0 +1,189 @@ +# Sense 原型待办清单(第二轮复核后执行版) + +> 来源:《09-Sense原型评审-IX草稿》《10-Sense原型-功能模块缺口》《11-Sense原型-第二轮缺口》 +> 基准:Sense 提交 `e18a1ea` +> 适用任务:T-004 +> 原则:按产品边界执行,不把 Sense 一级导航扩成 8 项 + +--- + +## 1. 状态说明 + +- `本轮`:T-004 第三轮原型必须完成。 +- `后置`:已确认方向,但不在本轮画完整动作。 +- `独立任务`:写路径或契约不同,不在 T-004 顺带修改。 +- `完成`:当前原型已有,不重复实现。 + +--- + +## 2. 最终导航与系统归属 + +### Sense(五个一级入口) + +```text +运行总览 · 实时监控 · 设备 · 接入任务 · 运维 +``` + +“运维”内部使用二级 Tab:对账、分片、边缘节点、运维告警、系统状态。移动端底部导航保持五项。 + +### Bell / 统一管理端 + +- Tenant / Site / Area 与 `capture_policy` 管理。 +- RBAC 与当前用户授权上下文。 +- 全局审计查询和设备筛选深链。 +- 站点视频配额真相源。 + +Sense 只消费上述上下文和带版本投影,不复制真相源。 + +--- + +## 3. 本轮执行项 + +### S-01 运维中心与对账队列 · P0 · 本轮 + +- 总览“待收敛项”可下钻。 +- 展示期望态/实际态差异、重试次数、退避间隔、下次重试时间。 +- “重新触发收敛”只提高任务优先级,不直接绕过对账器修改资源。 +- 展示孤儿资源与 10% 安全闸;安全闸触发时禁止删除并说明人工恢复路径。 +- 分片、边缘节点、运维告警和系统状态作为运维二级页。 + +验收:能打开某个未收敛项并查看差异;能看到安全闸和下一次重试时间。 + +### S-02 角色与安全拒绝态 · P0 · 本轮 + +- 顶栏显示当前用户和角色,提供“站点管理员/只读”原型切换。 +- 只读模式隐藏新增、激活、停用、更新凭据、保存区域等写操作。 +- 越权深链显示统一拒绝态,不泄露资源是否存在。 +- 角色切换只是原型演示,授权真相源仍在 Bell。 + +### S-03 租户/站点上下文与 Area 归属 · P0 · 本轮 + +- Sense 顶栏显示当前租户并支持站点切换;切换后示例列表与计数同步更新。 +- Bell 增加站点/Area 管理入口,展示配额来源与 `capture_policy`。 +- 把已有视频设备的 Area 改为 `non_imaging_only` 时,要求先迁移设备、取消修改或进入显式处置流程,不静默停用。 + +### S-04 全局审计深链 · P1 · 本轮 + +- Sense 不新增一级审计页。 +- 设备详情“操作记录”增加“在 Bell 查看全部审计”,携带 tenant/site/device 筛选上下文。 +- Bell 审计页消费这些参数并显示来源为 Sense。 + +### S-05 capabilities 探测与差异 · P0 · 本轮 + +- 详情显示厂商、型号、固件、认证方式、Profile、校时与 ONVIF 事件订阅能力。 +- 区分已探测能力与当前生效能力,记录上次探测时间。 +- “重新探测”显示新增/消失能力及受影响配置,不自动覆盖。 +- 只读显示采购白名单结果;白名单维护继续使用 M0 文档,不新增管理页。 + +### S-06 适配器未就绪 · P0 · 本轮 + +- radar 可登记并进入台账,但实际态为“适配器未就绪 · M6”。 +- 不显示在线/离线、MQTT 实时遥测或测试连接成功。 +- 不计入视频配额,仍参与 `non_imaging_only` 准入判断。 + +### S-07 待激活闭环 · P0 · 本轮 + +- 状态筛选包含待激活,列表至少有一条示例。 +- 详情提供重新探测、校时、配额/区域策略复核和激活。 +- 批量激活展示逐项成功/失败及可重试原因。 + +### S-08 认证失败与凭据更新 · P0 · 本轮 + +- 认证失败作为独立实际态。 +- 凭据更新只写不读,不回显旧值;更新后自动重试并显示结果。 +- 批量场景仅演示安全文件逐设备导入,不提供同一密码批量覆盖。 + +### S-09 写入降级态 · P0 · 本轮 + +- 状态演示新增“配额不可读”和“区域策略不可读”。 +- 两态下实时监控和设备读取保持可用;新增/启用等相关写操作禁用并就地解释。 +- 已有设备不被伪装为停用或已收敛。 + +### S-10 边缘节点与隧道 · P1 · 本轮 + +- 展示节点版本、在线时长、承载数、控制隧道、视频数据面和补传队列。 +- 至少演示“隧道断开 / 视频数据面正常”和“恢复后补传”组合态。 +- 节点升级/重启只显示为后置能力,不提供可执行按钮。 + +### S-11 Profile 切换 · P1 · 本轮 + +- 展示主/子 Profile 和当前用途。 +- 切换前说明短暂中断、reader、推理和区域待校准影响。 +- 原型不执行真实媒体操作。 + +### S-12 分片详情 · P1 · 本轮(迁移后置) + +- 展示承载设备、实际码率、重连历史与单分片故障影响范围。 +- 标注 32 为可配置建议容量,最终由压测确定。 +- 跨分片迁移动作后置 M4,本轮不提供按钮。 + +### S-13 批量任务与逐项结果 · P0 · 本轮 + +- 任务列表、预校验结果、逐行失败原因、单行/失败项重试、错误清单导出。 +- 明确部分成功语义:成功项已经生效,失败项可重试,不做整体回滚。 + +### S-14 运维告警 · P1 · 本轮 + +- 展示离线、认证失败、时间漂移、收敛失败、分片异常和隧道断开。 +- 明确运维告警不会进入 Bell 业务预警队列或通知家属。 +- 运维静默/通知配置后置,原型只显示入口归属。 + +### S-16 时间显示口径 · P1 · 本轮 + +- 默认显示服务器时间与时区。 +- 相对时间同时提供完整时间戳;设备时间只在漂移语义中出现。 +- 审计和对账示例使用同一口径。 + +### S-17 当前租户 · P1 · 本轮 + +- 顶栏显示当前租户;首期私有部署不提供租户切换器。 +- 原型文案说明列表已经按认证上下文的 tenant/site 过滤。 + +### S-18 暂停推理措辞与结果 · P0 · 本轮 + +- 文案改为“解除推理侧订阅;无其他 reader 时上游按需停止”。 +- 结果展示推理订阅、reader 数和上游拉流状态。 + +### S-19 分片容量标注 · P2 · 本轮 + +- `32` 标注为“当前配置/建议容量”,不是产品硬上限。 + +--- + +## 4. 已完成,不重复执行 + +| 编号 | 状态 | 证据 | +| --- | --- | --- | +| S-15 设备分页 | 完成 | 5 条/页、上一页/下一页、页数和总数已实现 | +| S-20 推理收敛视觉 | 完成 | 已改为绿色“推理配置收敛 16/16” | +| 第一轮 IX-014~IX-018 | 完成 | 模态/能力、三种停用、隐私准入、几何方向、草稿和深链均已落地 | + +--- + +## 5. 后置与独立任务 + +| 编号 | 处理 | +| --- | --- | +| S-21 Sense → Bell 审计契约 | 独立任务修改 `docs/api.md`;采用本地持久化 transactional outbox + 幂等补投 | +| S-22 推理绑定运维 | M3 在“运维”内新增二级能力,不预建空页面 | +| S-23 pre-roll 运维 | M3 需求稳定后新增,不预建空页面 | +| S-24 设备型触发配置 | M6 随非视频适配器实现,不预建空页面 | +| 跨媒体分片迁移 | M4 容量/分片任务 | +| 节点升级/重启 | 独立高风险运维任务,需要权限、回滚和维护窗口 | + +--- + +## 6. 总验收 + +- [x] Sense 一级导航保持 5 项,移动端不出现第 6 个底部入口。 +- [x] 运维中心可查看未收敛差异、退避、孤儿与安全闸。 +- [x] 只读角色下所有写操作消失,越权深链显示安全拒绝态。 +- [x] radar 显示“适配器未就绪 · M6”,不显示在线或模拟遥测成功。 +- [x] 能力重新探测显示差异,能力降级不自动覆盖依赖配置。 +- [x] 待激活、认证失败、凭据更新和逐项激活结果可演示。 +- [x] 配额/策略不可读时只阻止相关写操作,已有流与读取可用。 +- [x] “暂停推理”展示订阅、reader 和上游拉流结果。 +- [x] Bell 可管理 Site/Area 策略并显式处理已有成像设备。 +- [x] Sense 设备审计深链到 Bell,并保留 tenant/site/device 筛选。 +- [x] 1440×900、375×812、812×375 无页面级横向溢出;主要移动控件不小于 44px。 +- [x] 运行时无异常,`git diff --check` 与 `./init.ps1` 通过。 diff --git a/docs/tasks/T-004.md b/docs/tasks/T-004.md index d849594..629be52 100644 --- a/docs/tasks/T-004.md +++ b/docs/tasks/T-004.md @@ -14,6 +14,8 @@ write_paths: - docs/design/sense/index.html - docs/design/bell/index.html - docs/raw/09-Sense原型评审-IX草稿.md + - docs/raw/11-Sense原型-第二轮缺口.md + - docs/raw/12-Sense原型-待办清单.md - docs/08-interaction-checklist.md - docs/07-user-stories.md - docs/02-requirements.md @@ -31,31 +33,32 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, ## 关联需求与交互(如适用) -- 用户故事:Sense 关联 US-001、US-002、US-008,并为 US-005 提供视频设备空间配置;Bell 关联 US-003~US-006,并拥有 US-005 的业务规则工作流。 -- 交互清单:Sense 关联 IX-001~IX-004、IX-014~IX-018,并提供 IX-010 所需的区域/警戒线几何;Bell 关联 IX-005~IX-013,负责 IX-010~IX-011 的规则继承、试运行和发布。 +- 用户故事:Sense 关联 US-001、US-002、US-008、US-009,并为 US-005 提供视频设备空间配置;Bell 关联 US-003~US-006、US-010,并拥有 US-005 的业务规则工作流及 Tenant/Site/Area/RBAC/配额/全局审计真相。 +- 交互清单:Sense 关联 IX-001~IX-004、IX-014~IX-020,并提供 IX-010 所需的区域/警戒线几何;Bell 关联 IX-005~IX-013、IX-016、IX-020,负责规则继承/试运行/发布、站点与 Area 策略、全局审计。 - 相关页面 / 路由:原型阶段为 `docs/design/sense/index.html` 与 `docs/design/bell/index.html`,不定义生产路由。 ## 方案 1. 使用统一的 YoVision 深色安防控制台设计系统,Sense 与 Bell 保持品牌、状态色、控件和导航规律一致。 -2. Sense 制作为单页可交互工作台:总览、监控墙、设备、接入任务和系统状态;不设置一级“区域规则”。统一设备台账使用 `modality + capabilities` 决定详情页和操作,M1~M5 完整展示视频设备,非视频设备以雷达原型验证信息架构,实际适配器留到 M6。 +2. Sense 制作为单页可交互工作台:总览、监控墙、设备、接入任务和运维中心,移动端也固定为这 5 个入口;不设置一级“区域规则”或全局管理页。运维中心聚合对账、分片、边缘/隧道、运维告警和系统状态。统一设备台账使用 `modality + capabilities` 决定详情页和操作,M1~M5 完整展示视频设备,非视频设备在 M6 前明确显示 `adapter_not_ready`,不得虚构遥测。 3. 视频设备在“画面与检测区域”页签完成多边形、方向警戒线、显式完成、方向反转、逐点编辑、会话草稿恢复、乐观并发冲突和待校准反馈;Sense 只保存空间几何并只读显示关联规则,不提供业务规则绑定、时段、持续时间、试运行或正式发布。 -4. Bell 制作为单页可交互值班台:除预警队列、事件证据、ack/处置、升级时间线、误报反馈和升级链外,规则编辑必须选择摄像头与检测区域版本,并负责继承、时段、持续时间、试运行、发布和回滚。 +4. Bell 制作为单页可交互值班台和统一管理端:除预警队列、事件证据、ack/处置、升级时间线、误报反馈和升级链外,规则编辑必须选择摄像头与检测区域版本,并负责继承、时段、持续时间、试运行、发布和回滚;Bell 同时管理 Site/Area/配额/`capture_policy`,策略与已有成像设备冲突时必须显式迁移或取消。 5. Sense 摄像头详情与 Bell 规则草稿通过可演示的相对深链互相进入,携带摄像头/区域上下文;原型可以跨文件,但生产产品不得要求用户重新查找同一摄像头或丢失未完成草稿。 6. 两个原型均内联 CSS/JS,使用假数据,不加载真实视频、客户信息、生产地址或外部依赖;顶部固定标注原型用途。 7. 添加设备时先选择类型,依据能力渐进展示字段;区域的 `capture_policy` 负责隐私准入,视频设备在“仅非成像设备”区域中的新增或启用必须被阻止,策略不可用时对新写入失败关闭。 -8. 完成桌面、窄屏、横屏、键盘焦点、加载/空态/错误切换和 `prefers-reduced-motion` 检查,提交人工评审;产品确认前不把任务标为 `DONE`。 +8. 全局头部展示租户、站点、账号和角色;只读角色隐藏写操作,深链越权不泄露资源是否存在。Sense 设备日志带租户/站点/设备条件跳到 Bell 全局审计并可返回。 +9. 完成桌面、窄屏、横屏、键盘焦点、加载/空态/错误/依赖降级切换和 `prefers-reduced-motion` 检查,提交人工评审;产品确认前不把任务标为 `DONE`。 ## 不可变约束 -- 阈值 / 数值边界:默认 16 路视频、单站点视频上限 128 路;非视频设备不占视频路数;单媒体分片建议上限 32 路、最多 4 个分片;静默最长 4 小时;原型不得暗示 128 路同时播放主码流。 +- 阈值 / 数值边界:默认 16 路视频、单站点视频上限 128 路;非视频设备不占视频路数;单媒体分片初始配置建议 `max_streams=32`、当前示例承载 16、最多 4 个分片,最终以压测为准;静默最长 4 小时;原型不得暗示 128 路同时播放主码流。 - 判定式 / 状态转换:事件事实不可被处置改写;Alert 的首次投递、送达、看到、ack 和升级必须分开展示;检测区域版本与告警规则版本是不同对象;规则正式发布与试运行必须可区分,保存区域不得自动发布规则。 - 安全边界:不得展示摄像头密码、完整 RTSP/ONVIF 地址、token、真实客户/未成年人数据或人脸入口;破坏性动作须确认。 - 既有契约:原型不修改事件 v0.1、API、路由和生产技术栈,不作为生产实现代码。 ## 验收要点 -- 任务相关验证:两个 HTML 可本地打开、无外部依赖;关键按钮和状态切换可用;Sense 一级导航无“区域规则”,设备列表可按类型筛选/分页,视频详情可编辑检测区域,非视频详情不伪装成媒体流;Bell 可选择摄像头/区域并演示试运行与正式发布;双向深链保留 `camera + zone + return_to`;运行 HTML 结构检查、`git diff --check` 与 `./init.ps1`。 +- 任务相关验证:两个 HTML 可本地打开、无外部依赖;关键按钮和状态切换可用;Sense 一级导航固定 5 项且无“区域规则”,设备列表可按类型/生命周期筛选与分页,视频详情可编辑检测区域,非视频适配器未交付时不伪装在线;运维中心覆盖对账/分片/边缘/运维告警/系统状态;Bell 可选择摄像头/区域并演示试运行与正式发布,且可管理 Site/Area 策略冲突;Sense↔Bell 规则深链保留 `camera + zone + return_to`,审计深链保留 `tenant + site + device + return_to`;运行 HTML 结构检查、`git diff --check` 与 `./init.ps1`。 - 完整门禁:本任务不修改 schema/API/路由,因此不触发契约与导航实现门禁;若范围变化则先更新任务。 - 人工 / 设备验收:必需。项目负责人分别打开 Sense 与 Bell 原型,确认页面结构、控件集合和主要工作流;未确认前不得标记 `DONE` 或合并为最终设计。 - 构建产物:`docs/design/sense/index.html`、`docs/design/bell/index.html`;双击浏览器打开,零构建依赖。 @@ -75,6 +78,15 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前, ## 执行记录 +### 2026-08-04 第二轮缺口复核与最终实现 + +- 复核 `raw/11`、`raw/12` 后保留对账、能力探测、设备生命周期、RBAC、写入降级、边缘隧道与运维告警等真实缺口;纠正“原型无分页”“分片 8 / 32”“雷达已有真实能力”“T-004 只允许 3 个写路径”等过期事实,并把 M4/M6 能力标为后续阶段而非本轮失败。 +- 产品 IA 冻结为 Sense 5 个一级入口:运行总览、实时监控、设备、接入任务、运维中心。Tenant/Site/Area/RBAC/配额/全局审计归 Bell;Sense 仅呈现当前管理上下文并消费版本化配额/Area 策略投影。该决定已同步 US-009/US-010、IX-019/IX-020、正式需求、架构及三份原始分析文档。 +- Sense 按显著改版规则整页重新生成:补齐待激活、认证失败、写入型凭据更新、能力探测来源/时间/生效差异、`adapter_not_ready`、逐项任务结果、暂停推理 reader/upstream 结果、对账差异/退避/孤儿安全闸、16/32 分片注释、边缘隧道/补传、独立运维告警、配额/策略不可用时只禁用相关写入,以及只读/越权状态。 +- Bell 增加 Site/Area 管理、配额/策略投影状态与策略冲突显式迁移/取消;审计页能消费 Sense 的 `tenant + site + device + return_to` 上下文并返回设备详情。跨系统审计实现约定为 Sense 本地事务 outbox + 幂等 relay,但具体 API/签名/重放/留存仍需独立契约任务,本任务不修改 `docs/api/`。 +- Node.js 内联脚本语法、唯一 ID、表单标签、按钮可访问名、Tab 契约和外部资源静态检查通过。Edge Headless/CDP 在 1440×900、375×812、812×375 验证上述关键流程:两页页面级无横向溢出,移动端可见主要控件不小于 44×44px,控制台与运行时均 0 exception;运维中心与 Bell Site/Area 页面截图已人工目检,临时 QA 文件未写入仓库。 +- `./init.ps1` 通过:上下文/治理检查、16 个治理单测全部成功;`git diff --check` 通过。自动化验收已完成,任务继续保持 `DOING`,等待项目负责人打开原型进行必需人工确认;确认前不合并 PR。 + ### 2026-08-04 Claude 评审续修立项 - 项目负责人提供 `docs/raw/09-Sense原型评审-IX草稿.md` 并确认按复核后的最终建议继续修改。复核结论为:设备模态、停用收敛、隐私准入、几何编辑、草稿恢复与无障碍问题方向成立,但分片容量来源、页内草稿丢失和双向深链现状等证据需纠正。 -- 2.55.0 From e96cc2fbb5506cdce82bf47377635e55ad4995c0 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Tue, 4 Aug 2026 11:49:18 +0800 Subject: [PATCH 12/14] docs(design): close Bell prototype review gaps --- docs/02-requirements.md | 2 + docs/07-user-stories.md | 20 +- docs/08-interaction-checklist.md | 14 +- docs/design/bell/index.html | 451 ++++++++++++++++++++++++++----- docs/raw/14-Bell原型评审.md | 45 +++ docs/routes.md | 13 +- docs/tasks/T-004.md | 26 +- 7 files changed, 484 insertions(+), 87 deletions(-) create mode 100644 docs/raw/14-Bell原型评审.md diff --git a/docs/02-requirements.md b/docs/02-requirements.md index d1629d5..f0b67e5 100644 --- a/docs/02-requirements.md +++ b/docs/02-requirements.md @@ -56,6 +56,8 @@ - 升级链、超时、联系人和时段可按租户/站点配置。 - 至少两条独立投递路径,其中一条可绕过互联网。 - 区分已发出、已送达、已看到;没有回执不能当成功。 +- Alert 与 Event 保持可导航的多对多关系;被规则抑制且没有创建 Alert 的 Event 不伪装为已投递。 +- 班次交接覆盖未 ack、处置中与升级中的 Alert;接班确认留痕,交接过程不暂停或重置升级链。 - 静默必须限时且自动恢复,单次不超过 4 小时,无永久静默。 - 业务预警与运维告警使用不同通道和值班配置。 diff --git a/docs/07-user-stories.md b/docs/07-user-stories.md index c434663..a3c2827 100644 --- a/docs/07-user-stories.md +++ b/docs/07-user-stories.md @@ -23,7 +23,7 @@ - 角色:家属/值班员。 - 目标:收到异常后查看关联证据、确认接手并记录处置结果。 - 价值:把“发出通知”变成“有人负责”。 -- 验收:首次投递、送达、看到、ack、升级分别可追溯;无 ack 自动升级;进程重启不丢失升级链。 +- 验收:通道的已发出、已送达、已看到,与 Alert 的 ack、升级分别可追溯;无回执、通道不支持、可重试失败和最终失败不伪装成功;Alert 可查看聚合的 Event,Event 可查看触发的 Alert;并发 ack 时后到者看到真实处置人且不能覆盖;无 ack 自动升级,进程重启不丢失升级链。 - 关联:RQ-C-17~RQ-C-30,IX-005~IX-008。 ## US-004 标记误报 @@ -39,7 +39,7 @@ - 角色:租户管理员/站点管理员。 - 目标:按站点和设备覆盖场景模板,配置区域、时段、持续时间和联系人升级链。 - 价值:同一通用底座适配不同场景。 -- 验收:继承来源清楚、变更可试运行/回滚、静默不超过 4 小时且自动恢复。 +- 验收:继承来源清楚、变更可试运行;回滚先展示版本差异与影响,并创建新的不可变版本,不改写历史;静默不超过 4 小时且自动恢复。 - 关联:RQ-C-11~RQ-C-15、RQ-C-23~RQ-C-28,IX-010~IX-012。 ## US-006 事件最小权限查看 @@ -82,6 +82,22 @@ - 验收:Bell 是 Site/Area/配额/策略真相源;策略变更影响已有成像设备时必须显式选择迁移或取消,不自动停用;Sense 展示策略版本与同步状态,投影不可用时仅拒绝相关新写入。 - 关联:RQ-S1-05、NFR-CMP-05,IX-016、IX-020。 +## US-011 完成值班交接 + +- 角色:交班值班员/接班值班员。 +- 目标:在班次结束前把所有未 ack、处置中和升级中的 Alert 连同备注交给下一班,并得到明确接手确认。 +- 价值:保证“有人负责”不在班次边界中断。 +- 验收:交接清单显示当前状态、处置人与下一次升级时间;接班人确认后写审计;交接过程不暂停、不重置升级链,未确认交接时原责任关系保持不变。 +- 关联:RQ-C-17~RQ-C-30,IX-021。 + +## US-012 导出规则验收效果 + +- 角色:项目负责人/租户管理员/验收人员。 +- 目标:按规则版本和冻结样本窗口查看可复核的召回率与每路每天误报数。 +- 价值:让站点验收使用场景相关、可追溯的效果口径,避免跨场景“准确率”误导。 +- 验收:报表展示规则版本、样本时间窗、正样本/漏检样本、通道天数、召回率和每路每天误报数;明确不提供跨场景统一准确率;导出保留筛选与口径说明。 +- 关联:RQ-C-22、`02-requirements` §8,IX-022。 + ## 追溯规则 新增 P0 UI 任务必须引用至少一个 US 和一个 IX;若没有 UI,任务文件明确写“不适用”。需求变化先更新用户故事和交互清单,再改页面。 diff --git a/docs/08-interaction-checklist.md b/docs/08-interaction-checklist.md index af17a63..480282d 100644 --- a/docs/08-interaction-checklist.md +++ b/docs/08-interaction-checklist.md @@ -8,13 +8,13 @@ | IX-002 | 配额与批量结果 | 显示已用/上限;超 16 不等于非法,按站点配置校验;部分成功可重试;配额不可读取时只禁用新增/启用并保留已有列表与流 | US-001 | M2 | | IX-003 | 设备列表 | 分页/筛选/批量选择;128 路不一次加载所有视频和详情 | US-001 | M2/M4 | | IX-004 | 设备健康 | 期望态、媒体/推理/存储实际态、认证失败、最后在线完整时间与站点时区、时间漂移、分片、重试进度;运维告警独立,凭据更新只允许写入新值且旧值永不回显 | US-002 | M2/M4 | -| IX-005 | 预警到达 | 明确严重度、站点、时间、证据可用性;重复投递不产生重复处置 | US-003 | M3 | -| IX-006 | ack | 一次点击可确认,显示确认人/时间;并发 ack 有清晰结果 | US-003 | M3 | -| IX-007 | 升级 | 显示当前层级、下一次升级时间、每次投递状态;失败不能伪装成功 | US-003 | M3 | -| IX-008 | 事件详情 | 结构化事实、抓拍、视频、时间线、处置;证据加载失败可重试且不丢元数据 | US-003 | M3/M4 | +| IX-005 | 预警到达 | 明确严重度、站点、时间、证据可用性与关联事件数;重复投递不产生重复处置 | US-003 | M3 | +| IX-006 | ack | 一次点击可确认,显示确认人/时间;服务端首个成功者成为处置人,并发后到者看到当前处置人和冲突结果,不静默覆盖或显示双成功 | US-003 | M3 | +| IX-007 | 升级与投递 | 显示当前层级、下一次升级时间;每个通道分别展示已发出、已送达、已看到,Alert ack 单独展示;无回执/不支持/未知、可重试失败及最终失败不得伪装成功 | US-003 | M3 | +| IX-008 | 事件详情 | 结构化事实、抓拍、视频、时间线、处置;证据加载失败可重试且不丢元数据;Alert 展示关联 Event 与聚合原因,Event 展示触发的 Alert 与最终状态,两边可深链;被抑制且未创建 Alert 的事件明确标为未触发 | US-003 | M3/M4 | | IX-009 | 误报反馈 | 确认动作、可选原因、提交成功反馈;只改变 outcome,不改原始事件 | US-004 | M3 | | IX-010 | 规则编辑 | 显示租户/站点/设备继承来源,支持区域/警戒线/时段/持续时间 | US-005 | M3/M4 | -| IX-011 | 规则试运行 | 明确“未正式生效”,展示命中样本与影响范围,支持取消/回滚 | US-005 | M4 | +| IX-011 | 规则试运行与回滚 | 明确“未正式生效”,展示命中样本与影响范围,支持取消;回滚前展示目标版本差异与影响,确认后创建并发布等价的新版本,历史版本保持不可变;版本冲突时阻止覆盖并提供刷新/另存路径 | US-005 | M4 | | IX-012 | 升级链/静默 | 联系人顺序、超时、双通道;静默最长 4h、显示自动恢复时间、无永久选项 | US-005 | M3/M4 | | IX-013 | 权限与隐私 | 越权统一处理;无授权租户不展示人脸入口;不泄露流 URL/凭据 | US-006 | M3/M4 | | IX-014 | 设备模态与能力 | 一级入口为“设备”;列表可按 `modality` 筛选,添加时先选模态,详情按 `capabilities` 渐进展示;能力同时显示厂商/型号/固件/认证、探测来源与时间、探测值和生效值差异;视频设备显示画面/媒体/检测区域,未交付的非视频适配器显示 `adapter_not_ready`,不用虚构遥测或禁用的视频页签占位 | US-008 | M2/M6 | @@ -23,7 +23,9 @@ | IX-017 | 检测区域几何编辑 | 多边形和方向警戒线支持绘制、显式完成、撤销、清空、顶点编辑;警戒线在草稿/已保存态均显示方向箭头并可反转;鼠标与键盘坐标操作等效;画面参数变化后标记待校准且不自动改坐标;区域版本与规则版本独立 | US-005 | M3 | | IX-018 | 草稿与跨系统上下文 | 几何草稿自动保存并可恢复;只有持久化失败或切换空间类型等会破坏草稿的动作才拦截确认;Sense↔Bell 双向携带并消费 `camera + zone + return_to` 上下文;返回后恢复草稿,版本已被他人更新时提示冲突且不覆盖 | US-005 | M3 | | IX-019 | 运维中心与写入降级 | Sense 用一个一级“运维中心”聚合对账差异/退避/下次重试/孤儿安全闸、媒体与推理分片、边缘节点/隧道/补传、运维告警和系统状态;配额或 Area 策略不可读取时页面继续可读且已有链路不受影响,只禁用相关写操作;不得混入 Bell 业务预警 | US-002、US-008 | M2/M4/M6 | -| IX-020 | 管理上下文、RBAC 与审计归属 | 全局头部显示租户、站点、账号和角色;只读角色隐藏写操作,深链越权统一拒绝且不泄露资源是否存在;Tenant/Site/Area/RBAC/配额/全局审计由 Bell 管理,Sense 只呈现消费上下文和设备操作入口,携带 tenant/site/device/return_to 深链到 Bell 审计 | US-006、US-009、US-010 | M2/M4 | +| IX-020 | 管理上下文、RBAC 与审计归属 | 全局头部显示租户、站点、账号和角色;只读角色隐藏写操作,深链越权统一拒绝且不泄露资源是否存在;Tenant/Site/Area/RBAC/配额/全局审计由 Bell 管理,Sense 只呈现消费上下文和设备操作入口,携带 tenant/site/device/return_to 深链到 Bell 审计;Bell 移动端底部导航不超过 5 项,并通过可发现的“管理”入口访问 Site/Area 与审计 | US-006、US-009、US-010 | M2/M4 | +| IX-021 | 值班交接 | 清单覆盖未 ack、处置中和升级中的 Alert,显示交班人、接班人、备注及下一次升级时间;接班确认写审计,交接期间不暂停或重置升级链,未确认时不转移责任 | US-011 | M3 | +| IX-022 | 规则验收报表 | 按规则版本与冻结样本窗口展示召回率、每路每天误报数和计算样本量;支持保留口径的导出;不提供跨场景统一准确率 | US-012 | M4 | ## 全局状态 diff --git a/docs/design/bell/index.html b/docs/design/bell/index.html index 34f1d63..75ef0ef 100644 --- a/docs/design/bell/index.html +++ b/docs/design/bell/index.html @@ -79,6 +79,7 @@ .btn-danger { background: rgba(251,113,133,.1); border-color: rgba(251,113,133,.38); color: #ffb4c2; } .btn-quiet { border-color: transparent; background: transparent; color: var(--muted); } .icon-btn { width: 42px; padding: 0; } + .text-button { min-height: 32px; padding: 2px 6px; border: 0; background: transparent; color: #b8e5ff; text-decoration: underline; text-underline-offset: 3px; } .select, .field, .textarea { width: 100%; min-height: 40px; padding: 8px 11px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--text); } .textarea { min-height: 80px; resize: vertical; } .top-actions .select { width: auto; min-width: 150px; } @@ -132,10 +133,18 @@ .fact span { display: block; color: var(--muted); font-size: 11px; } .fact strong { display: block; margin-top: 3px; font-size: 13px; font-variant-numeric: tabular-nums; } .privacy-note { margin: 0 12px 12px; padding: 9px 10px; display: flex; gap: 8px; border-left: 3px solid var(--cyan); background: rgba(56,189,248,.08); color: var(--muted); font-size: 12px; } + .relation-panel { margin: 0 12px 12px; padding: 11px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); } + .relation-panel h3 { margin: 0 0 8px; font-size: 13px; } + .relation-list { display: grid; gap: 7px; } + .relation-item { min-height: 40px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--text); display: flex; align-items: center; justify-content: space-between; gap: 10px; text-align: left; } + .relation-item span { color: var(--muted); font-size: 12px; } .action-scroll { max-height: calc(100dvh - 210px); overflow-y: auto; } .ack-box { padding: 14px; border-bottom: 1px solid var(--line); } .ack-box .btn { width: 100%; min-height: 48px; } .ack-box p { margin: 9px 0 0; color: var(--muted); font-size: 12px; } + .ack-tools { margin-top: 8px; display: flex; justify-content: flex-end; } + .inline-alert { margin-top: 9px; padding: 9px 10px; border: 1px solid rgba(251,191,36,.4); border-radius: 8px; background: rgba(251,191,36,.09); color: #ffe8a8; font-size: 12px; } + .inline-alert.danger { border-color: rgba(251,113,133,.42); background: rgba(251,113,133,.09); color: #ffc3ce; } .countdown { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center; padding: 12px; margin: 12px; border: 1px solid rgba(251,191,36,.28); border-radius: 10px; background: rgba(251,191,36,.07); } .countdown strong { display: block; font-size: 20px; color: #ffe18a; font-variant-numeric: tabular-nums; } .countdown span { color: var(--muted); font-size: 12px; } @@ -149,6 +158,8 @@ .step.current .step-dot { background: var(--warning); } .step strong { display: block; } .step span { color: var(--muted); font-size: 12px; } + .delivery-facts { margin-top: 4px; display: grid; gap: 2px; } + .delivery-facts span { display: block; } .disposition { padding: 14px; border-top: 1px solid var(--line); display: grid; gap: 9px; } .disposition label { color: var(--muted); font-size: 12px; font-weight: 700; } .disposition-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } @@ -167,6 +178,8 @@ td { font-variant-numeric: tabular-nums; } tr:last-child td { border-bottom: 0; } tr:hover td { background: color-mix(in srgb, var(--primary-soft) 50%, transparent); } + .pager { min-height: 58px; padding: 10px 14px; border-top: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 12px; } + .pager-actions { display: flex; gap: 7px; } .rule-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .rule-card { padding: 15px; } .rule-card-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; } @@ -190,6 +203,9 @@ .linked-zone strong, .linked-zone small { display: block; } .linked-zone small { color: var(--muted); } .workflow-note { grid-column: 1/-1; margin: 0; padding: 10px 11px; border-left: 3px solid var(--primary); background: var(--primary-soft); color: var(--muted); font-size: 12px; } + .version-diff { display: grid; gap: 8px; } + .version-diff article { padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); } + .version-diff article strong { display: block; } .chain-grid { grid-template-columns: minmax(0, 1.4fr) minmax(300px, .8fr); } .chain-row { padding: 13px 14px; display: grid; grid-template-columns: 40px 1fr auto; gap: 11px; align-items: center; border-bottom: 1px solid var(--line); } .chain-row:last-child { border-bottom: 0; } @@ -206,6 +222,7 @@ .donut::after { content: "83%\A 24h 内闭环"; white-space: pre; position: absolute; inset: 27px; border-radius: 50%; display: grid; place-items: center; text-align: center; background: var(--surface); color: var(--text); font-weight: 800; } .legend { display: flex; flex-wrap: wrap; gap: 10px; color: var(--muted); font-size: 12px; } .legend i { display: inline-block; width: 9px; height: 9px; margin-right: 5px; border-radius: 2px; } + .metric-note { margin: 12px 0 0; padding: 10px 12px; border-left: 3px solid var(--cyan); background: rgba(56,189,248,.08); color: var(--muted); } .state-layer { display: none; min-height: 420px; place-items: center; padding: 24px; border: 1px dashed var(--line); border-radius: var(--radius); background: var(--surface); text-align: center; } .state-layer.show { display: grid; } .state-layer .icon { width: 40px; height: 40px; color: var(--primary); } @@ -216,7 +233,7 @@ .skeleton i:nth-child(2) { width: 72%; } @keyframes shimmer { to { background-position: -200% 0; } } dialog { width: min(610px, calc(100% - 28px)); max-height: 88dvh; padding: 0; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); color: var(--text); box-shadow: 0 24px 80px rgba(0,0,0,.5); } - #ruleDialog { width: min(760px, calc(100% - 28px)); } + #ruleDialog, #eventDialog, #handoverDialog, #rollbackDialog { width: min(760px, calc(100% - 28px)); } dialog::backdrop { background: rgba(4,7,15,.68); backdrop-filter: blur(4px); } .dialog-head, .dialog-foot { padding: 14px 18px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 10px; } .dialog-head h2 { margin: 0; font-size: 17px; } @@ -232,6 +249,7 @@ .muted { color: var(--muted); } .mono { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-variant-numeric: tabular-nums; } .mobile-nav { display: none; } + .mobile-manage { display: none; } @media (max-width: 1240px) { :root { --sidebar: 78px; } .brand-copy, .nav span:not(.count), .nav-label, .shift-card { display: none; } @@ -249,6 +267,7 @@ .sidebar { display: none; } .topbar { padding: 8px 12px; } .site span, .top-actions .select { display: none; } + .mobile-manage { display: inline-flex; min-width: 44px; } .content { padding: 12px; } .btn, .select, .field, .textarea { min-height: 44px; } .icon-btn { min-width: 44px; } @@ -260,6 +279,7 @@ .site-grid { grid-template-columns: 1fr; } .rule-grid { grid-template-columns: 1fr 1fr; } .fact-grid { grid-template-columns: repeat(2, 1fr); } + .pager { align-items: flex-start; flex-direction: column; } .mobile-nav { position: fixed; z-index: 50; left: 8px; right: 8px; bottom: 8px; display: grid; grid-template-columns: repeat(5, 1fr); padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); } .mobile-nav button { min-height: 48px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); display: grid; place-items: center; font-size: 11px; } .mobile-nav button.active { color: #c9c1ff; background: var(--primary-soft); } @@ -302,13 +322,16 @@
    @@ -435,8 +455,9 @@
    +

    产品流程:先确认摄像头和检测区域,再配置业务条件;试运行样本达到现场评审要求后,才允许正式发布。

    -
    +

    标记为误报

    这只会追加事件 outcome,不会修改原始事件事实和证据。

    @@ -445,78 +466,374 @@

    编辑 Area 准入策略

    保存生成新策略版本;Sense 投影同步前会显示旧版本,不允许绕过后端准入检查。

    策略与已有设备冲突

    北围墙当前有 4 台成像设备。改为“仅非成像”前必须选择显式处置;系统不会自动停用、移动或保留违规设备。

    + +

    管理

    移动端保留 5 个主入口,低频管理能力从这里进入。
    + +

    事件详情

    不可变事实
    事件编号—
    点位—
    规则—
    证据—

    触发的预警

    + +

    值班交接

    白班 · 林值班员 → 晚班
    ALT-1842 · 未 ack下一次升级 01:42
    ALT-1841 · 处置中王值班员现场核查
    ALT-1839 · 升级中短信重试 2/3

    交接期间升级链继续计时,不暂停、不重置。接班人确认前,当前责任关系保持不变。

    + +

    版本历史与回滚

    RULE-007 · 历史版本不可变
    当前 v7 · 2026-08-01 正式发布持续 3 秒 · 全天 · 4 个点位 · 区域 v7
    目标 v6 · 2026-07-18 历史版本持续 5 秒 · 夜间 22:00–06:00 · 3 个点位 · 区域 v6
    影响评估将减少 1 个点位并收窄时段;预计每日预警 -18%。确认后创建并发布 v8,使其配置等价于 v6,v6/v7 均保持不变。