diff --git a/Sense/scripts/package-windows.bat b/Sense/scripts/package-windows.bat index 04fd475..fcb08b8 100644 --- a/Sense/scripts/package-windows.bat +++ b/Sense/scripts/package-windows.bat @@ -5,6 +5,7 @@ for %%I in ("%~dp0..") do set "SENSE_ROOT=%%~fI" for %%I in ("%SENSE_ROOT%\dist") do set "OUTPUT_ROOT=%%~fI" for %%I in ("%OUTPUT_ROOT%\sense-windows-amd64") do set "PACKAGE_DIR=%%~fI" for %%I in ("%OUTPUT_ROOT%\sense-windows-amd64.zip") do set "ZIP_PATH=%%~fI" +for %%I in ("%OUTPUT_ROOT%\.sense.env.preserve") do set "PRESERVED_ENV=%%~fI" for %%I in ("%SENSE_ROOT%\ui\dist") do set "UI_DIST=%%~fI" if /I not "%PACKAGE_DIR%"=="%SENSE_ROOT%\dist\sense-windows-amd64" ( @@ -15,6 +16,10 @@ if /I not "%ZIP_PATH%"=="%SENSE_ROOT%\dist\sense-windows-amd64.zip" ( echo [ERROR] Unsafe ZIP path: %ZIP_PATH% exit /b 1 ) +if /I not "%PRESERVED_ENV%"=="%SENSE_ROOT%\dist\.sense.env.preserve" ( + echo [ERROR] Unsafe preserved configuration path: %PRESERVED_ENV% + exit /b 1 +) where go >nul 2>nul || goto :missing_go for /f "tokens=3" %%V in ('go version') do set "GO_VERSION=%%V" @@ -45,6 +50,14 @@ echo [2/5] Building frontend... call corepack pnpm@9.15.1 build || goto :failed_popd popd +if exist "%PRESERVED_ENV%" ( + echo [ERROR] Preserved configuration already exists: %PRESERVED_ENV% + echo Move it back to config\sense.env or remove it after confirming it is obsolete. + exit /b 1 +) +if exist "%PACKAGE_DIR%\config\sense.env" ( + copy /y "%PACKAGE_DIR%\config\sense.env" "%PRESERVED_ENV%" >nul || goto :failed +) if exist "%PACKAGE_DIR%" rmdir /s /q "%PACKAGE_DIR%" if exist "%ZIP_PATH%" del /q "%ZIP_PATH%" mkdir "%PACKAGE_DIR%\config" || goto :failed @@ -64,10 +77,14 @@ robocopy "%SENSE_ROOT%\LICENSES" "%PACKAGE_DIR%\LICENSES" /E /NFL /NDL /NJH /NJS if errorlevel 8 goto :failed copy /y "%SENSE_ROOT%\config\sense.env.example" "%PACKAGE_DIR%\config\sense.env.example" >nul || goto :failed copy /y "%SENSE_ROOT%\scripts\runtime\start-sense.bat" "%PACKAGE_DIR%\start-sense.bat" >nul || goto :failed +copy /y "%SENSE_ROOT%\scripts\runtime\start-sense.ps1" "%PACKAGE_DIR%\start-sense.ps1" >nul || goto :failed copy /y "%SENSE_ROOT%\scripts\runtime\README-WINDOWS.md" "%PACKAGE_DIR%\README-WINDOWS.md" >nul || goto :failed echo [5/5] Creating ZIP... powershell -NoProfile -ExecutionPolicy Bypass -Command "Compress-Archive -Path '%PACKAGE_DIR%' -DestinationPath '%ZIP_PATH%' -Force" || goto :failed +if exist "%PRESERVED_ENV%" ( + move /y "%PRESERVED_ENV%" "%PACKAGE_DIR%\config\sense.env" >nul || goto :failed +) echo. echo Package directory: %PACKAGE_DIR% diff --git a/Sense/scripts/runtime/README-WINDOWS.md b/Sense/scripts/runtime/README-WINDOWS.md index 3bd3ae6..5854a04 100644 --- a/Sense/scripts/runtime/README-WINDOWS.md +++ b/Sense/scripts/runtime/README-WINDOWS.md @@ -14,7 +14,7 @@ start-sense.bat demo ## 生产启动 -生产环境必须先安装并准备独立 PostgreSQL,然后在仓库和运行包之外安全设置以下环境变量: +生产环境必须先安装并准备独立 PostgreSQL。把配置写入运行目录的 `config\sense.env`,或在 Windows 进程环境中提供;已存在的非空进程环境变量优先于文件: - `SENSE_DATABASE_URL`:PostgreSQL 连接地址。 - `SENSE_IDENTITY_SIGNING_KEY`:至少 32 个字符的会话签名密钥。 @@ -27,6 +27,12 @@ start-sense.bat demo start-sense.bat ``` -完整变量名可参考 `config\sense.env.example`。示例文件只有空值和非秘密默认值,启动脚本不会自动读取它;请使用 Windows 环境变量或外部秘密管理工具注入真实值。 +启动前可只检查配置,不连接数据库也不启动服务: + +```bat +start-sense.bat check +``` + +完整变量名可参考 `config\sense.env.example`。复制为 `config\sense.env` 后填写真实值;该文件不会进入 ZIP 或 Git。本机在同一运行目录重新打包时会保留该文件,但交付 ZIP 始终不包含它。启动器只读取 `SENSE_*` 键,忽略空行与 `#` 注释,且不会打印配置值。生产环境仍建议使用 Windows 环境变量或外部秘密管理工具注入真实值。 健康检查为 `GET /healthz` 和 `GET /readyz`。MediaMTX 仍是独立程序,本运行包不会安装 PostgreSQL、MediaMTX 或 Windows 服务。 diff --git a/Sense/scripts/runtime/start-sense.bat b/Sense/scripts/runtime/start-sense.bat index 4e6c9ab..adb3591 100644 --- a/Sense/scripts/runtime/start-sense.bat +++ b/Sense/scripts/runtime/start-sense.bat @@ -1,45 +1,5 @@ @echo off setlocal -cd /d "%~dp0" -set "SENSE_UI_STATIC_DIR=%~dp0ui" - -if /I "%~1"=="demo" goto :demo - -set "SENSE_DATABASE_MODE=postgres" -if "%SENSE_DATABASE_URL%"=="" goto :missing_database_url -if "%SENSE_IDENTITY_SIGNING_KEY%"=="" goto :missing_identity_key -if "%SENSE_BOOTSTRAP_TOKEN%"=="" goto :missing_bootstrap_token -if "%SENSE_CREDENTIAL_KEY%"=="" goto :missing_credential_key - -echo Starting Sense in production mode at %SENSE_HTTP_ADDRESS%... -"%~dp0sense-server.exe" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0start-sense.ps1" %* exit /b %ERRORLEVEL% - -:demo -set "SENSE_DATABASE_MODE=memory" -if "%SENSE_HTTP_ADDRESS%"=="" set "SENSE_HTTP_ADDRESS=127.0.0.1:18080" -echo Starting Sense in temporary demo mode at http://%SENSE_HTTP_ADDRESS% ... -echo Demo data is discarded when the process stops. Do not use this mode in production. -"%~dp0sense-server.exe" -exit /b %ERRORLEVEL% - -:missing_database_url -echo [ERROR] SENSE_DATABASE_URL is required in production mode. -goto :configuration_help - -:missing_identity_key -echo [ERROR] SENSE_IDENTITY_SIGNING_KEY is required in production mode. -goto :configuration_help - -:missing_bootstrap_token -echo [ERROR] SENSE_BOOTSTRAP_TOKEN is required in production mode. -goto :configuration_help - -:missing_credential_key -echo [ERROR] SENSE_CREDENTIAL_KEY is required in production mode. - -:configuration_help -echo Set the required environment variables outside this directory. -echo See README-WINDOWS.md and config\sense.env.example. -exit /b 1 diff --git a/Sense/scripts/runtime/start-sense.ps1 b/Sense/scripts/runtime/start-sense.ps1 new file mode 100644 index 0000000..7a140f8 --- /dev/null +++ b/Sense/scripts/runtime/start-sense.ps1 @@ -0,0 +1,91 @@ +param( + [ValidateSet('production', 'demo', 'check')] + [string]$Mode = 'production' +) + +$ErrorActionPreference = 'Stop' + +function Import-SenseEnvironment { + param([string]$Path) + + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return + } + + $lineNumber = 0 + foreach ($line in Get-Content -LiteralPath $Path -Encoding UTF8) { + $lineNumber++ + $trimmed = $line.Trim() + if ($trimmed.Length -eq 0 -or $trimmed.StartsWith('#')) { + continue + } + if ($line -notmatch '^\s*(SENSE_[A-Z0-9_]+)\s*=(.*)$') { + throw "config\sense.env line $lineNumber must use SENSE_NAME=value format" + } + + $name = $Matches[1] + $value = $Matches[2].Trim() + if ($value.Length -ge 2) { + $first = $value[0] + $last = $value[$value.Length - 1] + if (($first -eq '"' -and $last -eq '"') -or ($first -eq "'" -and $last -eq "'")) { + $value = $value.Substring(1, $value.Length - 2) + } + } + + $current = [Environment]::GetEnvironmentVariable($name, 'Process') + if ([string]::IsNullOrEmpty($current)) { + [Environment]::SetEnvironmentVariable($name, $value, 'Process') + } + } +} + +function Assert-RequiredEnvironment { + param([string[]]$Names) + + foreach ($name in $Names) { + if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name, 'Process'))) { + throw "$name is required in production mode" + } + } +} + +try { + $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path + Import-SenseEnvironment -Path (Join-Path $runtimeRoot 'config\sense.env') + + [Environment]::SetEnvironmentVariable('SENSE_UI_STATIC_DIR', (Join-Path $runtimeRoot 'ui'), 'Process') + if ([string]::IsNullOrWhiteSpace($env:SENSE_HTTP_ADDRESS)) { + $env:SENSE_HTTP_ADDRESS = '127.0.0.1:18080' + } + + if ($Mode -eq 'demo') { + $env:SENSE_DATABASE_MODE = 'memory' + Write-Host "Starting Sense in temporary demo mode at http://$($env:SENSE_HTTP_ADDRESS) ..." + Write-Host 'Demo data is discarded when the process stops. Do not use this mode in production.' + } else { + $env:SENSE_DATABASE_MODE = 'postgres' + Assert-RequiredEnvironment -Names @( + 'SENSE_DATABASE_URL', + 'SENSE_IDENTITY_SIGNING_KEY', + 'SENSE_BOOTSTRAP_TOKEN', + 'SENSE_CREDENTIAL_KEY' + ) + if ($Mode -eq 'check') { + Write-Host 'Sense production configuration check passed.' + exit 0 + } + Write-Host "Starting Sense in production mode at $($env:SENSE_HTTP_ADDRESS) ..." + } + + $server = Join-Path $runtimeRoot 'sense-server.exe' + if (-not (Test-Path -LiteralPath $server -PathType Leaf)) { + throw 'sense-server.exe was not found beside the start script' + } + & $server + exit $LASTEXITCODE +} catch { + Write-Host "[ERROR] $($_.Exception.Message)" + Write-Host 'See README-WINDOWS.md and config\sense.env.example.' + exit 1 +} diff --git a/docs/04-local-development-and-verification.md b/docs/04-local-development-and-verification.md index d63bdfe..e891b71 100644 --- a/docs/04-local-development-and-verification.md +++ b/docs/04-local-development-and-verification.md @@ -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: 3bf0987ec9d7ddf543602e465faa088aead46531 -synchronized_at: 2026-08-12T10:22:02Z +wiki_revision: 4f3e1f218d29e17a3f8fabd43316d1a562c69d07 +synchronized_at: 2026-08-13T02:35:03Z # 本地开发与验证 @@ -167,7 +167,7 @@ cmd /c .\Sense\scripts\package-windows.bat Get-FileHash .\Sense\dist\sense-windows-amd64.zip -Algorithm SHA256 ``` -脚本精确校验 Node 22.22.1 和 pnpm 9.15.1,冻结安装并构建前端,以 Windows amd64/CGO 关闭方式编译后端,然后生成被 Git 忽略的 `Sense\dist\sense-windows-amd64\` 和 ZIP。可重复运行只会覆盖这两个固定产物。 +脚本精确校验 Node 22.22.1 和 pnpm 9.15.1,冻结安装并构建前端,以 Windows amd64/CGO 关闭方式编译后端,然后生成被 Git 忽略的 `Sense\dist\sense-windows-amd64\` 和 ZIP。同一运行目录重新打包时会保留已有 `config\sense.env`,但该文件不会进入 ZIP。 -解压后运行 `start-sense.bat demo` 可做内存模式临时预览;生产运行 `start-sense.bat` 前必须从仓库和运行包外注入 PostgreSQL URL、签名密钥、引导令牌和摄像头凭据加密密钥。包内不含 PostgreSQL、MediaMTX、系统服务、客户数据或秘密。 +解压后运行 `start-sense.bat demo` 可做内存模式临时预览。生产配置可写入运行目录的 `config\sense.env`,也可通过 Windows 进程环境注入;非空进程环境变量优先,启动器只读取 `SENSE_*` 键且不打印配置值。运行 `start-sense.bat check` 可在不连接数据库、不启动服务的情况下检查必填配置,然后用 `start-sense.bat` 启动生产模式。包内不含 PostgreSQL、MediaMTX、系统服务、客户数据或秘密,真实 `sense.env` 不得提交或重新打入交付 ZIP。 diff --git a/docs/06-troubleshooting.md b/docs/06-troubleshooting.md index f6dd1fd..d5f99ad 100644 --- a/docs/06-troubleshooting.md +++ b/docs/06-troubleshooting.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Troubleshooting wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Troubleshooting -wiki_revision: b8e778ae4255ba3fc36a1cd6bcb735e29bd19eee -synchronized_at: 2026-08-13T01:26:22Z +wiki_revision: 84d5d336475613d77030a7c722ea9c4d18b307fe +synchronized_at: 2026-08-13T02:35:08Z # 故障排查 @@ -66,7 +66,7 @@ synchronized_at: 2026-08-13T01:26:22Z | 现象 | 检查与处理 | |---|---| | 打包提示 Go/Node/pnpm 版本不符 | 对照根 `goadmin-baseline.json` 安装精确版本,并把 Go 1.26.5 放到 `PATH` 首位;不要修改脚本绕过版本检查。 | -| 生产启动提示缺少环境变量 | 在运行包外设置 `SENSE_DATABASE_URL`、`SENSE_IDENTITY_SIGNING_KEY`、`SENSE_BOOTSTRAP_TOKEN`、`SENSE_CREDENTIAL_KEY`;启动脚本不会读取示例文件。 | +| 生产启动提示缺少环境变量 | 确认运行目录中存在 `config\sense.env`(不是只保留 `sense.env.example`),并填写 `SENSE_DATABASE_URL`、`SENSE_IDENTITY_SIGNING_KEY`、`SENSE_BOOTSTRAP_TOKEN`、`SENSE_CREDENTIAL_KEY`;也可在进程环境中设置,非空进程环境变量优先。运行 `start-sense.bat check` 定位缺失的变量名,脚本不会打印变量值。 | | 本机健康检查被代理返回空响应 | localhost 可能被 `HTTP_PROXY` 接管;测试工具应对 `127.0.0.1` 使用 no-proxy,再检查 `SENSE_HTTP_ADDRESS` 端口占用。 | | 页面可打开但视频不可用 | Windows 运行包不包含 MediaMTX;按包内说明配置独立 MediaMTX 二进制、配置和 Control API。 | diff --git a/docs/delivery/README.md b/docs/delivery/README.md index 4569c73..33fcb0d 100644 --- a/docs/delivery/README.md +++ b/docs/delivery/README.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Delivery-Documentation-Guide wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Delivery-Documentation-Guide.- -wiki_revision: 04d9a7f0916262c3578e7934a2e98327dae55c7a -synchronized_at: 2026-08-13T01:15:15Z +wiki_revision: d4f2bfcef46493361ba019839b36957ed9947003 +synchronized_at: 2026-08-13T02:35:26Z # 交付文档指南 @@ -98,7 +98,7 @@ Sense 面向网管、实施人员和非技术现场人员,菜单按日常任 交付对象为实施和运维人员。`sense-windows-amd64.zip` 包含后端程序、已构建前端、空值示例配置、上游许可证、启动脚本与包内说明;不包含 PostgreSQL、MediaMTX、Windows 服务、生产数据或秘密。 - 临时查看必须显式运行 `start-sense.bat demo`,其内存数据在进程结束后丢失,不能当作生产部署。 -- 生产启动前由运维在运行包外安全注入数据库、身份签名、初始化令牌和凭据加密配置,再运行 `start-sense.bat`。 +- 生产配置可由运维写入解压目录的 `config\sense.env`,或通过 Windows 进程环境安全注入;进程环境优先。先运行 `start-sense.bat check` 检查必填项,再运行 `start-sense.bat`。 - 交付时记录 ZIP SHA-256,并至少验证 `/healthz` 与首页;真实 PostgreSQL、MediaMTX、摄像机和目标浏览器仍需在获准环境验收。 -- 包内 `README-WINDOWS.md` 是现场操作入口;不得把真实连接串、密码或令牌回填进示例文件后重新分发。 +- 包内 `README-WINDOWS.md` 是现场操作入口;真实 `config\sense.env` 只留在具体部署目录,不得提交 Git 或重新打入交付 ZIP,交付 ZIP 只保留 `sense.env.example`。 diff --git a/docs/task/46-Sense-Windows包内配置加载.md b/docs/task/46-Sense-Windows包内配置加载.md new file mode 100644 index 0000000..135ddec --- /dev/null +++ b/docs/task/46-Sense-Windows包内配置加载.md @@ -0,0 +1,72 @@ + +generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) +wiki_page: Task-46-Sense-Windows包内配置加载 +wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-46-Sense-Windows%E5%8C%85%E5%86%85%E9%85%8D%E7%BD%AE%E5%8A%A0%E8%BD%BD.- +wiki_revision: eab38da7cae8089d9cf7ed38e48d3e52962b68b4 +synchronized_at: 2026-08-13T02:42:05Z + + +# 46 Sense Windows包内配置加载 + +- 类型:缺陷 +- 所属 Epic:#7 +- 所属 MVP / 版本:#8 +- 状态:待验收 +- 日期:2026-08-13 +- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/46 +- Wiki 页面:Task-46-Sense-Windows包内配置加载 +- Wiki revision:见本地镜像头 + +## 背景与目标 + +Windows 打包目录已有 `config\sense.env`,但旧版 `start-sense.bat` 只读取进程环境,导致生产启动误报缺少 `SENSE_DATABASE_URL`。本工单让启动入口安全读取包内配置,同时确保真实配置不进入 Git 或交付 ZIP。 + +## 最终方案 + +- `start-sense.bat` 保持现场入口,委托同目录 `start-sense.ps1` 加载配置并启动服务。 +- 加载器只接受 `SENSE_*` 键,忽略空行和 `#` 注释;不执行配置内容、不输出配置值,外部非空进程环境变量优先。 +- 默认生产模式强制 PostgreSQL,验证数据库连接、身份签名、初始化令牌和凭据加密四个必填配置;`check` 模式只验证配置,不连接数据库、不启动服务。 +- `demo` 仍强制内存模式;UI 路径固定为包内 `ui`。 +- 重打包会安全暂存并恢复已有部署目录的 `config\sense.env`;交付 ZIP 只包含 `sense.env.example`,不包含真实配置。 + +## 修改文件 + +- `Sense/scripts/runtime/start-sense.bat`:委托 PowerShell 启动器并传递退出码。 +- `Sense/scripts/runtime/start-sense.ps1`:加载、校验配置并处理 production、demo、check 模式。 +- `Sense/scripts/runtime/README-WINDOWS.md`:补充包内配置、优先级和检查命令。 +- `Sense/scripts/package-windows.bat`:复制 PowerShell 启动器,重打包时保留本机配置且从 ZIP 排除。 +- `docs/04-local-development-and-verification.md`、`docs/06-troubleshooting.md`、`docs/delivery/README.md`:由 Gitea Wiki 同步生成的长期说明。 + +## 验收结果 + +| 验收标准 | 结果 | +|---|---| +| 包内已有 `config\sense.env` 时无需手工 `set` 即可通过生产配置检查 | 通过 | +| `check` 不连接数据库、不启动服务且不输出配置值 | 通过 | +| 外部非空进程环境优先,加载器只接受 `SENSE_*` 且不执行配置内容 | 通过(代码检查) | +| demo 强制内存模式,production 强制 PostgreSQL 和包内 UI | 通过(代码检查) | +| 重打包后原配置不变 | 通过;重建前后 SHA-256 一致 | +| ZIP 含 BAT、PowerShell 启动器、示例与说明,不含真实 `sense.env` | 通过 | + +## 测试 + +- 执行命令:`cmd /c Sense\scripts\package-windows.bat` +- 结果:前后端打包成功,生成 Windows amd64 目录和 ZIP。 +- 执行命令:`cmd /c Sense\dist\sense-windows-amd64\start-sense.bat check` +- 结果:输出 `Sense production configuration check passed.`,退出码 0。 +- 执行命令:检查 ZIP 条目和 SHA-256。 +- 结果:`start-sense.ps1` 与 `sense.env.example` 存在,真实 `config/sense.env` 不存在;ZIP SHA-256 为 `6311B29F1899691AF793A4758CD59096C5388D3C85214209B9EAFBCAD232BA8A`。 +- 执行命令:`python dev_scripts/sync_wiki_docs.py --check` +- 结果:Wiki 镜像检查通过。 +- 执行命令:`python dev_scripts/check_harness.py --strict` +- 结果:未通过;被基线中工单 #44 归档缺少“最终方案”章节阻塞,与本工单修改无关。 +- **未验证部分**:未连接用户 PostgreSQL 启动生产服务;未使用真实摄像机或 MediaMTX;因本工单不修改业务服务逻辑,这些留待部署验收。 + +## 遗留问题 + +- 基线工单 #44 的任务归档需在其自身验收闭环中补齐“最终方案”章节,之后再运行严格 Harness。 + +## 相关提交 + +- `5a17898` 修复 Windows 包内配置加载。 +- `6bbaa07` 更新 Wiki 镜像中的启动、排错和交付说明。 diff --git a/wiki-docs.json b/wiki-docs.json index 10d3b4a..27c97a6 100644 --- a/wiki-docs.json +++ b/wiki-docs.json @@ -127,6 +127,10 @@ { "page": "Task-44-Sense管理员侧栏模块为空修复", "path": "docs/task/44-Sense管理员侧栏模块为空修复.md" + }, + { + "page": "Task-46-Sense-Windows包内配置加载", + "path": "docs/task/46-Sense-Windows包内配置加载.md" } ] }