Compare commits

..
Author SHA1 Message Date
QiuSW 120a9efee4 docs: 标记任务 #106 验收完成 2026-08-27 16:17:36 +08:00
QiuSW 356f891c9e docs: 归档任务 #106 待验收证据 2026-08-27 16:10:28 +08:00
QiuSW 0646f5effd docs: 记录 Sense Supervisor 托管方式 (#106) 2026-08-27 16:08:23 +08:00
ila d264758dee merge: 完成 Sense GoAdmin 应用外壳 (#101)
用户已于 2026-08-27 验收通过;将 #101 合入 dev,main 保持不变。
2026-08-27 15:53:22 +08:00
QiuSW 83012ad37f docs: 标记任务 #101 验收完成 2026-08-27 15:53:08 +08:00
QiuSW 54f10d894a docs: 更新工单 #101 组合验收证据 2026-08-27 15:34:22 +08:00
QiuSW 8cdd3f61cb Merge remote-tracking branch 'origin/dev' into fix/101-sense-layout-shell
# Conflicts:
#	docs/02-architecture-and-code-map.md
2026-08-27 15:23:19 +08:00
ila 46c3232da4 merge: 完成 Sense 30天登录有效期 (#104)
用户已验收通过 #104;将 PR #105 合入 dev,main 保持不变。
2026-08-17 11:46:42 +08:00
QiuSW faa96a3bea docs: 更新工单 #101 组合验收包 2026-08-16 23:52:42 +08:00
QiuSW 42dc77b1f9 Merge branch 'dev' into fix/101-sense-layout-shell
# Conflicts:
#	docs/02-architecture-and-code-map.md
#	wiki-docs.json
2026-08-16 23:43:52 +08:00
QiuSW 9ff2f6339f docs: 完成任务 #101 待验收归档 2026-08-16 23:32:42 +08:00
QiuSW e652109744 docs: 记录 Sense Layout 菜单结构 (#101) 2026-08-16 23:26:55 +08:00
QiuSW 4423d528b1 fix: 保留 Sense GoAdmin 应用外壳 (#101) 2026-08-16 23:12:53 +08:00
6 changed files with 513 additions and 2 deletions
@@ -0,0 +1,85 @@
package version
import (
"fmt"
"runtime"
"gorm.io/gorm"
"git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration"
migrationModels "git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration/models"
common "git.ilapage.cn/ila/yovision/Sense/server/common/models"
)
const senseLayoutMenuName = "SenseManage"
var sensePageMenus = []migrationModels.SysMenu{
{MenuName: "SenseDeviceManage", Title: "设备管理", Icon: "monitor", Path: "devices", MenuType: "C", Permission: "sense:device:list", Component: "/sense/device/index", Sort: 1, Visible: "0", IsFrame: "1"},
{MenuName: "SenseAdmission", Title: "视频接入", Icon: "video-camera", Path: "admission", MenuType: "C", Permission: "sense:admission:list", Component: "/sense/admission/index", Sort: 2, Visible: "0", IsFrame: "1"},
{MenuName: "SenseMedia", Title: "视频服务", Icon: "video-play", Path: "media", MenuType: "C", Permission: "sense:media:list", Component: "/sense/media/index", Sort: 3, Visible: "0", IsFrame: "1"},
{MenuName: "SenseLiveview", Title: "实时监看", Icon: "eye-open", Path: "liveview", MenuType: "C", Permission: "sense:liveview:view", Component: "/sense/liveview/index", Sort: 4, Visible: "0", IsFrame: "1"},
{MenuName: "SenseArea", Title: "区域与警戒线", Icon: "guide", Path: "area", MenuType: "C", Permission: "sense:area:list", Component: "/sense/area/index", Sort: 5, Visible: "0", IsFrame: "1"},
}
func init() {
_, fileName, _, _ := runtime.Caller(0)
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateSenseLayout)
}
func migrateSenseLayout(db *gorm.DB, version string) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := alignSenseLayout(tx); err != nil {
return err
}
return tx.Create(&common.Migration{Version: version}).Error
})
}
func alignSenseLayout(tx *gorm.DB) error {
root, err := ensureDeviceMenu(tx, migrationModels.SysMenu{
MenuName: senseLayoutMenuName,
Title: "视频感知",
Icon: "video-camera",
Path: "/sense",
MenuType: "M",
ParentId: 0,
Component: "Layout",
Sort: 5,
Visible: "0",
IsFrame: "1",
})
if err != nil {
return fmt.Errorf("ensure Sense layout menu: %w", err)
}
for _, desired := range sensePageMenus {
desired.ParentId = root.MenuId
desired.Paths = fmt.Sprintf("/0/%d", root.MenuId)
if _, err = ensureDeviceMenu(tx, desired); err != nil {
return fmt.Errorf("align Sense page menu %s: %w", desired.MenuName, err)
}
}
if err = rebuildSenseMenuPaths(tx, root.MenuId, "/0"); err != nil {
return err
}
return nil
}
func rebuildSenseMenuPaths(tx *gorm.DB, menuID int, parentPath string) error {
path := fmt.Sprintf("%s/%d", parentPath, menuID)
if err := tx.Model(&migrationModels.SysMenu{}).Where("menu_id = ?", menuID).Update("paths", path).Error; err != nil {
return fmt.Errorf("update menu %d paths: %w", menuID, err)
}
var children []migrationModels.SysMenu
if err := tx.Where("parent_id = ?", menuID).Order("sort, menu_id").Find(&children).Error; err != nil {
return fmt.Errorf("list children of menu %d: %w", menuID, err)
}
for _, child := range children {
if err := rebuildSenseMenuPaths(tx, child.MenuId, path); err != nil {
return err
}
}
return nil
}
@@ -0,0 +1,208 @@
package version
import (
"fmt"
"os"
"testing"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
migrationModels "git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration/models"
common "git.ilapage.cn/ila/yovision/Sense/server/common/models"
)
func TestAlignSenseLayoutPreservesShellAndExistingPages(t *testing.T) {
db := openSenseLayoutTestDB(t)
seedLegacySenseMenus(t, db)
associationsBefore := countRoleMenuAssociations(t, db)
if err := alignSenseLayout(db); err != nil {
t.Fatal(err)
}
// The alignment helper is intentionally idempotent because partially upgraded
// deployments may rerun the repair before the migration version is recorded.
if err := alignSenseLayout(db); err != nil {
t.Fatalf("repeat alignment: %v", err)
}
root := requireMenu(t, db, senseLayoutMenuName)
if root.ParentId != 0 || root.Component != "Layout" || root.Path != "/sense" || root.MenuType != "M" {
t.Fatalf("unexpected root menu: %#v", root)
}
if root.Paths != fmt.Sprintf("/0/%d", root.MenuId) {
t.Fatalf("root paths=%q", root.Paths)
}
for _, desired := range sensePageMenus {
page := requireMenu(t, db, desired.MenuName)
if page.ParentId != root.MenuId || page.Path != desired.Path || page.Component != desired.Component || page.Permission != desired.Permission {
t.Errorf("unexpected page %s: %#v", desired.MenuName, page)
}
wantPagePaths := fmt.Sprintf("/0/%d/%d", root.MenuId, page.MenuId)
if page.Paths != wantPagePaths {
t.Errorf("page %s paths=%q want %q", desired.MenuName, page.Paths, wantPagePaths)
}
var buttons []migrationModels.SysMenu
if err := db.Where("parent_id = ? AND menu_type = ?", page.MenuId, "F").Find(&buttons).Error; err != nil {
t.Fatal(err)
}
if len(buttons) != 1 {
t.Fatalf("page %s button count=%d", desired.MenuName, len(buttons))
}
wantButtonPaths := fmt.Sprintf("%s/%d", wantPagePaths, buttons[0].MenuId)
if buttons[0].Paths != wantButtonPaths {
t.Errorf("button %s paths=%q want %q", buttons[0].MenuName, buttons[0].Paths, wantButtonPaths)
}
}
var rootCount int64
if err := db.Model(&migrationModels.SysMenu{}).Where("menu_name = ?", senseLayoutMenuName).Count(&rootCount).Error; err != nil {
t.Fatal(err)
}
if rootCount != 1 {
t.Fatalf("Sense layout menu count=%d", rootCount)
}
if associationsAfter := countRoleMenuAssociations(t, db); associationsAfter != associationsBefore {
t.Fatalf("role-menu associations changed from %d to %d", associationsBefore, associationsAfter)
}
}
func TestMigrateSenseLayoutRollsBackWhenRequiredPageIsMissing(t *testing.T) {
db := openSenseLayoutTestDB(t)
seedLegacySenseMenus(t, db)
if err := db.Where("menu_name = ?", "SenseArea").Delete(&migrationModels.SysMenu{}).Error; err != nil {
t.Fatal(err)
}
// A missing page is recreated by the repair, so force a failure when the
// migration records its version and verify the menu transaction also rolls back.
version := "2026081623100"
if err := db.Create(&common.Migration{Version: version}).Error; err != nil {
t.Fatal(err)
}
if err := migrateSenseLayout(db, version); err == nil {
t.Fatal("expected duplicate migration version error")
}
var rootCount, areaCount int64
db.Model(&migrationModels.SysMenu{}).Where("menu_name = ?", senseLayoutMenuName).Count(&rootCount)
db.Model(&migrationModels.SysMenu{}).Where("menu_name = ?", "SenseArea").Count(&areaCount)
if rootCount != 0 || areaCount != 0 {
t.Fatalf("transaction left partial menus: root=%d area=%d", rootCount, areaCount)
}
}
func openSenseLayoutTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&migrationModels.SysRole{}, &migrationModels.SysMenu{}, &common.Migration{}); err != nil {
t.Fatal(err)
}
return db
}
func TestSenseLayoutMigrationOnPostgres(t *testing.T) {
dsn := os.Getenv("SENSE_LAYOUT_MIGRATION_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set SENSE_LAYOUT_MIGRATION_TEST_DATABASE_URL to run the PostgreSQL migration test")
}
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
const schema = "sense_layout_101_test"
if err = db.Exec("DROP SCHEMA IF EXISTS " + schema + " CASCADE").Error; err != nil {
t.Fatal(err)
}
if err = db.Exec("CREATE SCHEMA " + schema).Error; err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Exec("DROP SCHEMA IF EXISTS " + schema + " CASCADE") })
sqlDB, err := db.DB()
if err != nil {
t.Fatal(err)
}
sqlDB.SetMaxOpenConns(1)
if err = db.Exec("SET search_path TO " + schema).Error; err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&migrationModels.SysRole{}, &migrationModels.SysMenu{}, &common.Migration{}); err != nil {
t.Fatal(err)
}
seedLegacySenseMenus(t, db)
const version = "2026081623100"
if err = migrateSenseLayout(db, version); err != nil {
t.Fatal(err)
}
root := requireMenu(t, db, senseLayoutMenuName)
if root.Component != "Layout" || root.Path != "/sense" {
t.Fatalf("unexpected PostgreSQL root: %#v", root)
}
var pages, versions int64
db.Model(&migrationModels.SysMenu{}).Where("parent_id = ? AND menu_type = ?", root.MenuId, "C").Count(&pages)
db.Model(&common.Migration{}).Where("version = ?", version).Count(&versions)
if pages != int64(len(sensePageMenus)) || versions != 1 {
t.Fatalf("pages=%d versions=%d", pages, versions)
}
}
func seedLegacySenseMenus(t *testing.T, db *gorm.DB) {
t.Helper()
role := migrationModels.SysRole{RoleName: "site_admin", RoleKey: "site_admin", Status: "2"}
if err := db.Create(&role).Error; err != nil {
t.Fatal(err)
}
assigned := make([]migrationModels.SysMenu, 0, len(sensePageMenus)*2)
for index, desired := range sensePageMenus {
legacy := desired
legacy.Path = "/sense/" + desired.Path
legacy.ParentId = 0
legacy.Sort = index + 5
legacy.Paths = ""
if err := db.Create(&legacy).Error; err != nil {
t.Fatal(err)
}
button := migrationModels.SysMenu{
MenuName: desired.MenuName + "Action",
Title: "测试操作",
MenuType: "F",
Permission: desired.Permission + ":action",
ParentId: legacy.MenuId,
Paths: fmt.Sprintf("/0/%d", legacy.MenuId),
Visible: "1",
IsFrame: "1",
}
if err := db.Create(&button).Error; err != nil {
t.Fatal(err)
}
assigned = append(assigned, legacy, button)
}
if err := db.Model(&role).Association("SysMenu").Append(assigned); err != nil {
t.Fatal(err)
}
}
func countRoleMenuAssociations(t *testing.T, db *gorm.DB) int64 {
t.Helper()
var count int64
if err := db.Table("sys_role_menu").Count(&count).Error; err != nil {
t.Fatal(err)
}
return count
}
func requireMenu(t *testing.T, db *gorm.DB, name string) migrationModels.SysMenu {
t.Helper()
var menu migrationModels.SysMenu
if err := db.Where("menu_name = ?", name).First(&menu).Error; err != nil {
t.Fatal(err)
}
return menu
}
+28 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Local-Development-and-Verification
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Local-Development-and-Verification.-
wiki_revision: 84a086640dc3618e43f834c488080f1e0cb5b063
synchronized_at: 2026-08-17T02:54:49Z
wiki_revision: 925a16a72bd5840697b5c61489a97018134b3930
synchronized_at: 2026-08-27T08:07:04Z
<!-- gitea-wiki-mirror:end -->
# 本地开发与验证
@@ -313,3 +313,29 @@ corepack pnpm@9.15.1 build:prod
浏览器 smoke 至少覆盖:鼠标添加和拖动顶点;键盘 Enter 添加、方向键移动、Delete 删除;错误文字可见并具有 aria-live/alert 语义;刷新后版本、启停和重新校准状态仍可追溯。真实摄像机校准只使用明确授权设备,不记录地址、URI、凭据或视频内容。Brain、Bell 不启动时必须能独立保存、读取和预览。
<!-- sense-area:end -->
<!-- sense-supervisor:start -->
## Sense 本机 Supervisor 托管
本机开发/演示环境可由 `D:\supervisor` 托管已经构建的 Sense Windows 交付包。实例配置位于仓库外的 `D:\supervisor\programs\yovision.conf`,实例名为 `yovision-sense`;工作目录固定为 `D:\OPC\yovision\Sense\dist\sense-windows-amd64`。
Supervisor 配置只调用包内 `scripts\runtime\start-sense.ps1`,运行参数继续从包内 `config\sense.env` 读取。不得把数据库连接、JWT 密钥、摄像头凭据或其他秘密复制到 Supervisor 配置或工单。
常用命令:
```powershell
D:\supervisor\supervisord.exe ctl /c D:\supervisor\supervisord.conf status yovision-sense
D:\supervisor\supervisord.exe ctl /c D:\supervisor\supervisord.conf restart yovision-sense
D:\supervisor\supervisord.exe ctl /c D:\supervisor\supervisord.conf stop yovision-sense
D:\supervisor\supervisord.exe ctl /c D:\supervisor\supervisord.conf start yovision-sense
Get-Content D:\supervisor\logs\yovision-sense.log -Tail 100
```
新增或修改 `programs/*.conf` 后执行:
```powershell
D:\supervisor\supervisord.exe ctl /c D:\supervisor\supervisord.conf reload
```
当前 Go Supervisor 的 `reload` 会重新读取独立配置;实际受影响实例必须以命令输出和 reload 前后 PID 为准。切换托管前先停止占用 Sense 端口的非 Supervisor 实例,防止自动启动进入 Backoff。验证至少包含 Supervisor 状态为 Running、`http://127.0.0.1:18080/health` 与首页返回 200,以及受控重启后 Sense 和受管 MediaMTX PID 均更新。
<!-- sense-supervisor:end -->
+107
View File
@@ -0,0 +1,107 @@
<!-- gitea-wiki-mirror:start -->
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Task-101-Sense-GoAdmin-应用外壳
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-101-Sense-GoAdmin-%E5%BA%94%E7%94%A8%E5%A4%96%E5%A3%B3.-
wiki_revision: a32fd29a7b8c4fa42396e9f249bcdd99d0c09e74
synchronized_at: 2026-08-27T07:52:48Z
<!-- gitea-wiki-mirror:end -->
# 101 Sense-GoAdmin-应用外壳
- 类型:缺陷修复
- 所属 Epic:#7
- 所属 MVP / 版本:#8 / Sense 首个独立纵切
- 状态:已完成
- 日期:2026-08-16
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/101
- Wiki 页面:Task-101-Sense-GoAdmin-应用外壳
- Wiki revision:见本地镜像头
## 背景与目标
Sense 五个业务菜单原来都以顶层业务组件注册。go-admin-ui 只有在动态路由组件为 `Layout` 时才保留侧栏、顶部导航和标签页,因此点击设备管理等入口会用业务页面替换整个 GoAdmin 外壳。
本任务恢复冻结 GoAdmin 的标准父子菜单结构:应用外壳保持不变,模块切换只替换右侧内容区域。
## 最终方案
新增迁移 `2026081623100_sense_layout.go`,创建唯一的“视频感知”顶层目录:
- 顶层目录:`MenuType=M`、`Path=/sense`、`Component=Layout`。
- 设备管理、视频接入、视频服务、实时监看、区域与警戒线:作为相对路径的 `MenuType=C` 子页面。
- 页面下既有 `MenuType=F` 操作权限保持不变。
- 递归重算父目录、页面和按钮的完整 `paths`。
- 页面 URL、组件、权限标识、角色关联和业务 API 均不改变。
- 使用新增事务迁移兼容已经执行旧迁移的 PostgreSQL 数据库,不修改历史迁移。
与建单方案一致,没有修改 go-admin-ui 公共 Layout 或动态路由生成器。
## 修改文件
- `Sense/server/cmd/migrate/migration/version/2026081623100_sense_layout.go`:新增 GoAdmin Layout 菜单兼容迁移。
- `Sense/server/cmd/migrate/migration/version/2026081623100_sense_layout_test.go`:覆盖菜单层级、相对路径、完整 paths、角色关联保持、重复对齐、事务回滚和 PostgreSQL。
- `docs/02-architecture-and-code-map.md`:同步架构 Wiki 中的 Sense 动态菜单长期规则。
- `wiki-docs.json`、`docs/task/101-Sense-GoAdmin-应用外壳.md`:任务归档映射与镜像。
## 验收结果
| 验收标准 | 结果 |
|---|---|
| 五个业务页位于 Layout 父路由下 | 通过 |
| 设备管理进入 `#/sense/devices`,标题与路径不串位 | 通过 |
| 切换和刷新后侧栏、顶部栏、标签页保留 | 通过 |
| 页面组件、权限标识和角色关联保持 | 通过 |
| 已有 PostgreSQL 数据库迁移无重复菜单 | 通过 |
| 重复对齐安全、失败事务回滚 | 通过 |
| 后端、前端和 Windows 包验证 | 通过 |
| Wiki、归档、PR 和证据 | 通过,用户已验收 |
## 测试
- `go test ./cmd/migrate/migration/version -run 'Test(AlignSenseLayout|MigrateSenseLayout|SenseLayout)' -count=1`:通过。
- 真实 PostgreSQL 隔离 schema `TestSenseLayoutMigrationOnPostgres`:通过并清理测试 schema。
- `go test ./...`:通过。
- `go vet ./...`、后端构建:通过。
- `pnpm lint`:0 error,32 条冻结上游/既有 warning。
- `pnpm test:unit -- --runInBand`:17 suites、46 tests 全部通过。
- `pnpm build:prod`:通过,4 条既有构建 warning。
- Windows PowerShell 5.1 包测试:21 assertions 通过;包审计通过。
- 本机生产数据库迁移及启动:通过。
- Headless Edge:依次打开五个菜单及刷新区域页面,`.app-wrapper`、侧栏、navbar、TagsView 全程可见,URL 均正确。
- Windows ZIP:`Sense/dist/sense-windows-amd64.zip`,SHA-256 `BAAA973F8502BB5B0BC080F11931E60419B25EFA8FF722A6B9F9B98364399642`,源码提交 `4423d528b1dc6ebe8b6f7b8272ca5111032a1608`。
- **未验证部分**:客户全新 Windows 主机及 implementation_operator/site_admin/viewer 三种岗位的人工视觉验收;本机使用 admin 完成真实浏览器回归,用户已确认验收通过。
## 遗留问题
#99、#104 与 #101 均已由用户验收;#101 经 PR #102 合入 `dev`,`main` 保持不变。#90 的项目档案 Wiki 更新未混入本任务提交;客户全新 Windows 主机及三个非 admin 岗位的人工视觉验收仍未覆盖。
## 相关提交
- `4423d528b1dc6ebe8b6f7b8272ca5111032a1608` 修复 Sense GoAdmin 应用外壳。
- `e652109` 记录 Sense Layout 菜单长期架构。
## #99 合入后的组合验收包
- 用户验收 #99 后,PR #100 已合入 `dev`;本任务分支通过合并提交 `42dc77b1f904032131c51b3864184c4ccaea6b8f` 更新到该基线。
- 冲突处理保留 Wiki 事实源中 #99 的匿名只读 `app-config` 边界、#101 的 GoAdmin Layout 菜单规则,以及两张任务归档映射。
- 重新生成 `Sense/dist/sense-windows-amd64.zip`;包内 `source_commit=42dc77b1f904032131c51b3864184c4ccaea6b8f`。
- 新 ZIP SHA-256:`65AF5DD1CFB5D4F8339DCCC53717354495259F13630FBC0C6A4AFC1ACD794B26`。
- 固定工具链 production build、包审计、Windows PowerShell 5.1 包测试 21 项、真实 PostgreSQL 迁移/启动及 `TestRegisterBaseRouterExposesOnlyFrontendAppConfig` 均通过。
- 现场配置在构建前备份、ZIP 生成后恢复;ZIP 仍只包含模板配置。Sense 当前监听 `127.0.0.1:18080`。
## #104 合入后的最终组合验收包
- #104 已由用户验收并经 PR #105 合入 `dev@46c3232da40528d031367225b09d4f28c85a8312`;#101 分支通过合并提交 `8cdd3f61cbd8f5bbc1a972525da91aaf49a39bb0` 更新到该基线。
- 唯一合并冲突是 `Architecture-and-Code-Map` 镜像头;读取 Wiki revision `8df12aa3e136b1faee9c2dffb58ca39b045bc49f` 确认页面同时包含 #101 Layout 菜单规则和 #104 30 天登录规则后,以该事实源解决。业务代码无冲突。
- `go test ./...`、`go vet ./...`、`go build ./...` 全部通过;前端 lint 0 error(32 条既有 warning),18 suites / 48 tests 通过,production build 通过(4 条既有 warning)。
- Windows 包资源审计、包审计和 22 assertions 通过;使用现场 PostgreSQL 完成幂等迁移与生产启动,`data/runtime/settings.yml` 确认 `timeout: 2592000`。
- Chrome 真实回归依次验证设备管理、视频接入、视频服务、实时监看、区域与警戒线五个入口;URL 分别为 `#/sense/devices`、`#/sense/admission`、`#/sense/media`、`#/sense/liveview`、`#/sense/area`,侧栏、Navbar、TagsView 全程可见。直接刷新区域页面后外壳与选中标签保持,控制台无错误。
- 本机直连验证 `/health`、`/api/v1/app-config` 和 `/` 均返回 200;首次命令行请求受开发机 HTTP 代理影响,明确绕过代理后通过,不属于 Sense 服务错误。
- 组合 ZIP:`Sense/dist/sense-windows-amd64-101-104.zip`,56,461,005 bytes,SHA-256 `46B2B5654EE3F61788CA4E51F5CB66C1A7C7D1088EFFD39C0281E86E84F964A2`,包内 `source_commit=8cdd3f61cbd8f5bbc1a972525da91aaf49a39bb0`。标准 ZIP 内容相同且仅含模板配置;现场配置只恢复到解压目录。
- 当前组合服务监听 `127.0.0.1:18080`。临时现场配置备份已删除,源码目录 `node_modules` 已清理。
- **未验证部分**:客户全新 Windows 主机及 implementation_operator/site_admin/viewer 三种岗位的人工视觉验收;用户已确认当前交付验收通过。
## 验收确认
- 2026-08-27,用户明确确认“#101通过验收”。
- PR #102 按规定合入 `dev`,`main` 未变更。
- 工单 #101 状态更新为“已完成”并关闭;所属 MVP #8 的子工单索引同步勾选。
@@ -0,0 +1,77 @@
<!-- gitea-wiki-mirror:start -->
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Task-106-Sense-本机-Supervisor-实例
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-106-Sense-%E6%9C%AC%E6%9C%BA-Supervisor-%E5%AE%9E%E4%BE%8B.-
wiki_revision: fcff45a700cba3db939819cfc5deb1cc97b8155b
synchronized_at: 2026-08-27T08:17:20Z
<!-- gitea-wiki-mirror:end -->
# 106 Sense-本机-Supervisor-实例
- 类型:运维配置
- 所属 Epic:#7
- 所属 MVP / 版本:#8 / Sense 首个独立纵切
- 状态:已完成
- 日期:2026-08-27
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/106
- Wiki 页面:Task-106-Sense-本机-Supervisor-实例
- Wiki revision:见本地镜像头
## 背景与目标
本机 Sense 原由独立终端启动,不受 `D:\supervisor` 管理。目标是在不复制现场秘密、不修改 Sense 业务代码的前提下,让 Supervisor 托管现有 Windows 交付包,并提供自动启动、异常重启、进程组停止和独立日志。
当前 Brain、Bell 只有规则文件,没有可运行交付物,因此本任务只创建 `yovision-sense`,不创建空实例。
## 最终方案
- 在仓库外新增 `D:\supervisor\programs\yovision.conf`,实例名为 `yovision-sense`。
- 工作目录使用 `D:\OPC\yovision\Sense\dist\sense-windows-amd64`。
- Supervisor 直接调用包内 `scripts/runtime/start-sense.ps1`,配置仍由 `config/sense.env` 读取。
- 配置启用 autostart、autorestart、启动重试、进程组停止和 50 MB × 5 的日志轮转。
- 日志写入 `D:\supervisor\logs\yovision-sense.log`;Supervisor 配置不含 environment、数据库连接、令牌、密码或摄像头凭据。
- 使用包内停止脚本核对并停止原 Sense PID 28440,释放 18080 后执行 Supervisor reload。
原计划按最坏情况说明 reload 会重启全部实例;实际命令返回 `Added Groups: yovision-sense`。dsh/goauto PID 未变化,原先处于 Backoff 的三个 Chorus 实例在 reload 后恢复 Running,因此没有观察到既有 Running 实例被重启。
## 修改文件
- `D:\supervisor\programs\yovision.conf`:新增本机 Supervisor 实例配置;该文件位于仓库外。
- `Local-Development-and-Verification` Wiki:记录实例位置、常用命令、秘密边界和验证方式。
- `docs/04-local-development-and-verification.md`:上述 Wiki 的只读镜像。
- `wiki-docs.json`、`docs/task/106-Sense-本机-Supervisor-实例.md`:任务归档登记与镜像。
## 验收结果
| 验收标准 | 结果 |
|---|---|
| `yovision-sense` 稳定为 Running | 通过 |
| Sense 监听 18080,健康检查与首页返回 200 | 通过 |
| Sense 管理的 MediaMTX 随单实例重启更新 PID | 通过 |
| Supervisor 配置不包含现场秘密 | 通过 |
| dsh/goauto 保持运行,Chorus 从既有 Backoff 恢复 | 通过 |
| Wiki、归档、提交、PR 和证据 | 通过,用户已验收 |
## 测试
- `supervisord.exe ctl /c supervisord.conf reload`:返回新增 `yovision-sense` 配置组。
- `supervisord.exe ctl /c supervisord.conf status`:七个实例均为 Running;`yovision-sense` Supervisor PID 28000。
- `GET /health`、`GET /healthz`、`GET /`:均返回 200。
- 受控执行 `restart yovision-sense`:Sense PID 从 38408 更新为 43916,MediaMTX PID 从 27880 更新为 27212,重启后健康检查与首页仍为 200。
- 外部配置 SHA-256:`7C5BDDD574384ED6038F0C2DAA8CE364FB1EA996B256831595ABF4E22EDA4769`。
- 配置敏感字段扫描:未发现 password、token、secret、`SENSE_DATABASE_URL` 或 `environment=`。
- **未验证部分**:未通过重启 Windows 验证开机后的整体 Supervisor 自启动;未故意崩溃 Sense 验证异常自动重启,已用 Supervisor 受控重启验证停止与拉起链路。
## 遗留问题
Supervisor 管理端口当前监听 `0.0.0.0:9009` 且未配置认证,这是任务开始前已存在的安全风险,本任务按确认范围未修改,建议另建安全工单处理。
## 相关提交
- `0646f5effd53536ae3f608c5e5dffe7fe950cb8f` 记录 Sense Supervisor 托管方式。
## 验收确认
- 2026-08-27,用户明确确认“#106 验收通过”。
- PR #107 按规定合入 `dev`,`main` 未变更。
- 工单 #106 状态更新为“已完成”并关闭;所属 MVP #8 的子工单索引同步勾选。
+8
View File
@@ -144,6 +144,10 @@
"page": "Task-97-Sense免验证码登录与管理员密码重置",
"path": "docs/task/97-Sense免验证码登录与管理员密码重置.md"
},
{
"page": "Task-101-Sense-GoAdmin-应用外壳",
"path": "docs/task/101-Sense-GoAdmin-应用外壳.md"
},
{
"page": "Task-99-Sense登录页只读配置接口",
"path": "docs/task/99-Sense登录页只读配置接口.md"
@@ -151,6 +155,10 @@
{
"page": "Task-104-Sense-30天登录有效期",
"path": "docs/task/104-Sense-30天登录有效期.md"
},
{
"page": "Task-106-Sense-本机-Supervisor-实例",
"path": "docs/task/106-Sense-本机-Supervisor-实例.md"
}
]
}