From e6128c7e5b5062c643519afcddd23d0b3758ac11 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 13 Aug 2026 17:00:40 +0800 Subject: [PATCH] build(windows-node): add batch deployment workflow --- .gitignore | 1 + RUN.md | 22 ++- tests/test_windows_node_source.py | 54 ++++++- windows-node/README.md | 25 +-- .../Zcbot.WindowsNode/OriginWorkerRunner.cs | 2 +- .../Zcbot.WindowsNode.csproj | 11 ++ .../Zcbot.WindowsNode/packages.lock.json | 3 +- windows-node/install-origin-runtime.ps1 | 32 ---- windows-node/install-startup.ps1 | 37 ----- windows-node/install-windows-node.bat | 151 ++++++++++++++++++ windows-node/package-windows-node.bat | 108 +++++++++++++ 11 files changed, 347 insertions(+), 99 deletions(-) delete mode 100644 windows-node/install-origin-runtime.ps1 delete mode 100644 windows-node/install-startup.ps1 create mode 100644 windows-node/install-windows-node.bat create mode 100644 windows-node/package-windows-node.bat diff --git a/.gitignore b/.gitignore index cc05c98..be99af1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ htmlcov/ # .NET Windows Node build outputs windows-node/**/bin/ windows-node/**/obj/ +windows-node/dist*/ # Virtualenv .venv/ diff --git a/RUN.md b/RUN.md index e4cac07..4d4376b 100644 --- a/RUN.md +++ b/RUN.md @@ -1069,26 +1069,32 @@ Node API 只能绑定受控内网地址并由安全组限制来源 IP。当前 H Windows 端源码位于 `windows-node/`,要求 .NET 10 SDK: +```bat +windows-node\package-windows-node.bat +``` + +默认生成不包含 .NET Runtime 的 `windows-node\dist\zcbot-windows-node--win-x64.zip` 和对应 `.sha256`,目标机器需预装 .NET 10 Desktop Runtime;需要免安装 .NET 的离线包时使用 `package-windows-node.bat --self-contained`。解压后可通过界面注册,也可执行: + ```powershell -dotnet build windows-node/Zcbot.WindowsNode/Zcbot.WindowsNode.csproj -windows-node/Zcbot.WindowsNode/bin/Debug/net10.0-windows/Zcbot.WindowsNode.exe enroll ` +Zcbot.WindowsNode.exe enroll ` --server http://zcbot.internal:8765 ` --name win-origin-01 ` --code ZCN-... -windows-node/Zcbot.WindowsNode/bin/Debug/net10.0-windows/Zcbot.WindowsNode.exe +Zcbot.WindowsNode.exe ``` -Origin Worker 使用独立固定 Python 运行时。先确认该交互式 Windows 账号已安装并可启动 Origin/OriginPro,再由管理员安装运行时(不要复用服务端 `.venv`): +Windows Node 发布目录包含纯 BAT 安装入口,不依赖 PowerShell。使用实际运行 Node 的专用 Windows 账号双击 `install-windows-node.bat`;安装器会检测 Origin/OriginPro,检测到后自动通过 `py -3.12` 或 PATH 定位 Python 3.12 并安装固定 runtime,未检测到则跳过,最后通过 `schtasks.exe` 注册该账号登录后的启动任务,结束后保留窗口。也可在命令提示符执行: -```powershell -windows-node\install-origin-runtime.ps1 -BootstrapPython D:\programs\Python312\python.exe +```bat +cd /d D:\ZcbotNode +install-windows-node.bat ``` -默认解释器为 `%ProgramData%\Zcbot\WindowsNode\runtimes\origin\python.exe`。如需放在其他受管目录,设置机器级 `ZCBOT_ORIGIN_PYTHON` 为绝对 `python.exe` 路径后重启 Node。运行时固定依赖见 `windows-node/origin-worker/requirements.txt`;任务请求无权选择解释器、脚本或路径。当前 Worker 支持 CSV/XLSX/JSON 输入,`line`、`scatter`、`line_scatter` 与 OPJU/PNG/SVG/PDF 输出。成功产物由 Node 流式上传,全部校验通过后发布到任务工作目录 `origin//`;上传中断会在重连时幂等续传。 +若 Python 未加入 PATH,可把绝对路径作为第一个参数,例如 `install-windows-node.bat "C:\Python312\python.exe"`。默认解释器为 `%ProgramData%\Zcbot\WindowsNode\runtimes\origin\Scripts\python.exe`。如需使用其他受管解释器,设置机器级 `ZCBOT_ORIGIN_PYTHON` 为绝对 `python.exe` 路径后重启 Node。`node.json`、可恢复任务和 runtime 集中保存在 `%ProgramData%\Zcbot\WindowsNode\`,不会因替换程序目录而丢失。运行时固定依赖见发布目录的 `origin-worker/requirements.txt`;任务请求无权选择解释器、脚本或路径。当前 Worker 支持 CSV/XLSX/JSON 输入,`line`、`scatter`、`line_scatter` 与 OPJU/PNG/SVG/PDF 输出。成功产物由 Node 流式上传,全部校验通过后发布到任务工作目录 `origin//`;上传中断会在重连时幂等续传。 Web 用户登录后,右下角 Job 中心会聚合本人最近任务。活动任务约 4 秒刷新一次,空闲时降为约 30 秒;停止已派发任务是协作取消,状态先显示“正在停止”,Node 在线时立即接收,断线后在下次连接或心跳时重放。Agent 可调用 `software_capability_list`、`software_job_submit`、`software_job_status` 和 `software_job_cancel`;提交工具只创建固定 schema 的持久任务,不会阻塞当前对话等待完成。 -注册配置写入 `%ProgramData%\Zcbot\WindowsNode\node.json`;Token 使用 DPAPI `LocalMachine` 加密,ACL 仅允许注册账号和 `SYSTEM`。应始终用同一专用 Windows 账号注册并运行 Node。当前 MVP 以该账号的登录后计划任务启动,不安装 Windows Service。 +注册配置写入 `%ProgramData%\Zcbot\WindowsNode\node.json`;Token 使用 DPAPI `LocalMachine` 加密,ACL 仅允许注册账号和 `SYSTEM`。应始终用同一专用 Windows 账号执行统一安装器、注册并运行 Node。当前 MVP 以该账号的登录后计划任务启动,不安装 Windows Service。 直接双击 EXE 启动托盘 UI:红点为未注册/身份失效,黄点为连接中,绿点为在线;双击托盘图标打开配置窗。原 CLI 注册入口继续保留,无 UI 模式使用 `Zcbot.WindowsNode.exe run --headless`。 diff --git a/tests/test_windows_node_source.py b/tests/test_windows_node_source.py index a60d55c..c0de4f1 100644 --- a/tests/test_windows_node_source.py +++ b/tests/test_windows_node_source.py @@ -81,13 +81,48 @@ class WindowsNodeSourceTests(unittest.TestCase): tray = (PROJECT / "TrayApplicationContext.cs").read_text(encoding="utf-8") self.assertIn("form.ReconnectRequested += RestartConnection", tray) - def test_startup_task_is_login_scoped_and_runs_the_fixed_node_executable(self) -> None: - script = (ROOT / "install-startup.ps1").read_text(encoding="utf-8") - self.assertIn('GetFileName($resolvedExecutable) -ne "Zcbot.WindowsNode.exe"', script) - self.assertIn("New-ScheduledTaskTrigger -AtLogOn -User $currentUser", script) - self.assertIn("-LogonType Interactive", script) - self.assertIn("-RunLevel Limited", script) - self.assertNotIn("-RunLevel Highest", script) + def test_unified_installer_registers_login_startup(self) -> None: + launcher = (ROOT / "install-windows-node.bat").read_text(encoding="utf-8") + self.assertNotIn("powershell", launcher.lower()) + self.assertIn('set "NODE_EXE=%~dp0Zcbot.WindowsNode.exe"', launcher) + self.assertIn('schtasks.exe /Create /TN "Zcbot Windows Node"', launcher) + self.assertIn("/SC ONLOGON", launcher) + self.assertIn("/RL LIMITED", launcher) + self.assertIn("/IT", launcher) + self.assertIn("pause >nul", launcher) + + def test_unified_installer_detects_origin_and_python_312(self) -> None: + script = (ROOT / "install-windows-node.bat").read_text(encoding="utf-8") + self.assertIn(":detect_origin", script) + self.assertIn('reg.exe query "HKCR\\Origin.ApplicationSI\\CLSID"', script) + self.assertIn(":find_python", script) + self.assertIn("py.exe -3.12", script) + self.assertIn("sys.version_info[:2] == (3, 12)", script) + self.assertIn( + 'set "RUNTIME_DIR=%ProgramData%\\Zcbot\\WindowsNode\\runtimes\\origin"', + script, + ) + self.assertIn('set "RUNTIME_PYTHON=%RUNTIME_DIR%\\Scripts\\python.exe"', script) + self.assertIn('call :run_python -m venv "%RUNTIME_DIR%"', script) + + def test_publish_output_contains_the_complete_installer_payload(self) -> None: + project = (PROJECT / "Zcbot.WindowsNode.csproj").read_text(encoding="utf-8") + self.assertIn("..\\install-windows-node.bat", project) + self.assertIn("..\\origin-worker\\requirements.txt", project) + self.assertFalse((ROOT / "install-windows-node.ps1").exists()) + self.assertFalse((ROOT / "install-origin-runtime.ps1").exists()) + self.assertFalse((ROOT / "install-startup.ps1").exists()) + + def test_packager_defaults_to_framework_dependent_verified_zip(self) -> None: + script = (ROOT / "package-windows-node.bat").read_text(encoding="utf-8") + self.assertNotIn("powershell", script.lower()) + self.assertIn('set "SELF_CONTAINED=false"', script) + self.assertIn('if /I "%~1"=="--self-contained"', script) + self.assertIn("dotnet publish", script) + self.assertIn('"install-windows-node.bat"', script) + self.assertIn("tar.exe -a -c -f", script) + self.assertIn("certutil.exe -hashfile", script) + self.assertFalse((ROOT / "package-windows-node.ps1").exists()) def test_auth_rejection_is_distinct_from_http_websocket_handshake_failure(self) -> None: connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8") @@ -163,7 +198,10 @@ class WindowsNodeSourceTests(unittest.TestCase): project = (PROJECT / "Zcbot.WindowsNode.csproj").read_text(encoding="utf-8") worker = (ROOT / "origin-worker" / "worker.py").read_text(encoding="utf-8") self.assertIn('Environment.GetEnvironmentVariable("ZCBOT_ORIGIN_PYTHON")', runner) - self.assertIn('Path.Combine(paths.RootDirectory, "runtimes", "origin", "python.exe")', runner) + self.assertIn( + 'Path.Combine(paths.RootDirectory, "runtimes", "origin", "Scripts", "python.exe")', + runner, + ) self.assertIn("UseShellExecute = false", runner) self.assertIn("startInfo.ArgumentList.Add(workerScript)", runner) self.assertIn("startInfo.ArgumentList.Add(jobDirectory)", runner) diff --git a/windows-node/README.md b/windows-node/README.md index b95d732..65d9ea1 100644 --- a/windows-node/README.md +++ b/windows-node/README.md @@ -4,13 +4,21 @@ 当前实现托盘状态角标、小型配置窗口、注册、DPAPI/ACL 配置保存、WebSocket `hello`/心跳和退避重连,并只读探测 Origin/OriginPro 安装版本、COM 自动化组件与桌面会话状态。Node 可以接收受控的 `origin.plot@v1` offer,在本机任务目录原子保存请求后回报 accept/reject;随后以 Node 身份流式下载任务绑定的 CSV/XLSX/JSON,校验大小与 SHA-256 后原子保存。固定 Origin Worker 独立于单次 WebSocket 执行,断线不终止已启动绘图。成功产物按 manifest 逐项流式上传并由云端复核大小与 SHA-256,全部完成后原子发布到任务工作目录的 `origin//`;中断后按本地 `upload-complete.json` 幂等续传。 -固定 Origin Worker 已支持 `line`、`scatter` 和 `line_scatter`,生成 OPJU、PNG、SVG、PDF、plot spec、provenance 与原子 `terminal.json`。运行时独立于 zcbot 服务端 Python,管理员执行: +在仓库根目录执行一条命令生成可分发 ZIP: -```powershell -.\install-origin-runtime.ps1 -BootstrapPython D:\programs\Python312\python.exe +```bat +windows-node\package-windows-node.bat ``` -默认安装到 `%ProgramData%\Zcbot\WindowsNode\runtimes\origin\python.exe`;也可由管理员用绝对路径环境变量 `ZCBOT_ORIGIN_PYTHON` 指向固定解释器。任务请求不能指定解释器、脚本或路径。 +产物位于 `windows-node\dist\zcbot-windows-node--win-x64.zip`,旁边同时生成 SHA-256 文件。默认不包含 .NET Runtime,目标机器需预装 .NET 10 Desktop Runtime;需要免安装 .NET 的离线包时执行 `package-windows-node.bat --self-contained`。打包脚本是纯 BAT,使用 .NET SDK 以及 Windows 自带的 `tar.exe`、`certutil.exe`。 + +固定 Origin Worker 已支持 `line`、`scatter` 和 `line_scatter`,生成 OPJU、PNG、SVG、PDF、plot spec、provenance 与原子 `terminal.json`。运行时独立于 zcbot 服务端 Python。发布目录包含统一安装入口;使用实际运行 Node 的专用 Windows 账号直接双击: + +```text +install-windows-node.bat +``` + +安装器是纯 BAT,不依赖 PowerShell:它先通过注册表检测本机软件;检测到 Origin/OriginPro 或其 COM 自动化组件时,依次尝试 `py -3.12` 和 PATH 中的 `python.exe`,安装 Origin runtime;未检测到则明确跳过。最后通过 `schtasks.exe` 为当前专用账号注册登录后启动任务,成功或失败后都会保留窗口。Python 未加入 PATH 时,可在命令提示符执行 `install-windows-node.bat "C:\Python312\python.exe"`。默认 runtime 位于 `%ProgramData%\Zcbot\WindowsNode\runtimes\origin\Scripts\python.exe`。 注册和运行必须使用同一专用 Windows 账号。MVP 通过该账号的登录后计划任务自动启动,不以 Windows Service 在 Session 0 运行。 @@ -29,11 +37,4 @@ windows-node/Zcbot.WindowsNode/bin/Debug/net10.0-windows/Zcbot.WindowsNode.exe Zcbot.WindowsNode.exe run --headless ``` -确认前台连接正常后,为当前专用账号注册登录后启动任务: - -```powershell -windows-node/install-startup.ps1 ` - -ExecutablePath windows-node/Zcbot.WindowsNode/bin/Debug/net10.0-windows/Zcbot.WindowsNode.exe -``` - -注册结果保存在 `%ProgramData%\Zcbot\WindowsNode\node.json`。Token 使用 DPAPI `LocalMachine` 加密,目录与文件 ACL 只保留注册时的运行账号和 `SYSTEM`。复制配置到另一台机器不能解密 Token;Windows 重装、运行账号变更或身份丢失时应由管理员禁用旧节点并重新注册。 +注册结果仍保存在 `%ProgramData%\Zcbot\WindowsNode\node.json`,可恢复任务保存在同级 `jobs`;它们不随程序包更新。Token 使用 DPAPI `LocalMachine` 加密,目录与文件 ACL 只保留注册时的运行账号和 `SYSTEM`。复制配置到另一台机器不能解密 Token;Windows 重装、运行账号变更或身份丢失时应由管理员禁用旧节点并重新注册。 diff --git a/windows-node/Zcbot.WindowsNode/OriginWorkerRunner.cs b/windows-node/Zcbot.WindowsNode/OriginWorkerRunner.cs index 513e653..75dcf2f 100644 --- a/windows-node/Zcbot.WindowsNode/OriginWorkerRunner.cs +++ b/windows-node/Zcbot.WindowsNode/OriginWorkerRunner.cs @@ -152,7 +152,7 @@ internal static class OriginWorkerRuntime var paths = NodePaths.ForCurrentMachine(); var configured = Environment.GetEnvironmentVariable("ZCBOT_ORIGIN_PYTHON"); var candidate = string.IsNullOrWhiteSpace(configured) - ? Path.Combine(paths.RootDirectory, "runtimes", "origin", "python.exe") + ? Path.Combine(paths.RootDirectory, "runtimes", "origin", "Scripts", "python.exe") : configured; if (!Path.IsPathFullyQualified(candidate)) return null; var resolved = Path.GetFullPath(candidate); diff --git a/windows-node/Zcbot.WindowsNode/Zcbot.WindowsNode.csproj b/windows-node/Zcbot.WindowsNode/Zcbot.WindowsNode.csproj index 003de32..f8f88c2 100644 --- a/windows-node/Zcbot.WindowsNode/Zcbot.WindowsNode.csproj +++ b/windows-node/Zcbot.WindowsNode/Zcbot.WindowsNode.csproj @@ -15,6 +15,17 @@ origin-worker\worker.py PreserveNewest + PreserveNewest + + + origin-worker\requirements.txt + PreserveNewest + PreserveNewest + + + install-windows-node.bat + PreserveNewest + PreserveNewest diff --git a/windows-node/Zcbot.WindowsNode/packages.lock.json b/windows-node/Zcbot.WindowsNode/packages.lock.json index 2a64009..77d53a0 100644 --- a/windows-node/Zcbot.WindowsNode/packages.lock.json +++ b/windows-node/Zcbot.WindowsNode/packages.lock.json @@ -1,6 +1,7 @@ { "version": 1, "dependencies": { - "net10.0-windows7.0": {} + "net10.0-windows7.0": {}, + "net10.0-windows7.0/win-x64": {} } } \ No newline at end of file diff --git a/windows-node/install-origin-runtime.ps1 b/windows-node/install-origin-runtime.ps1 deleted file mode 100644 index cce7f0c..0000000 --- a/windows-node/install-origin-runtime.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -param( - [Parameter(Mandatory = $true)] - [string]$BootstrapPython, - [string]$RuntimeDirectory = "$env:ProgramData\Zcbot\WindowsNode\runtimes\origin" -) - -$ErrorActionPreference = "Stop" -$python = (Resolve-Path -LiteralPath $BootstrapPython).Path -if ([IO.Path]::GetFileName($python) -ne "python.exe") { - throw "BootstrapPython must point to python.exe." -} -$requirements = Join-Path $PSScriptRoot "origin-worker\requirements.txt" -if (-not (Test-Path -LiteralPath $requirements -PathType Leaf)) { - throw "Pinned Origin worker requirements are missing." -} -$runtime = [IO.Path]::GetFullPath($RuntimeDirectory) -if ($runtime -eq [IO.Path]::GetPathRoot($runtime)) { - throw "RuntimeDirectory cannot be a drive root." -} - -if (-not (Test-Path -LiteralPath (Join-Path $runtime "python.exe"))) { - & $python -m venv $runtime - if ($LASTEXITCODE -ne 0) { throw "Failed to create the Origin runtime." } -} -$runtimePython = Join-Path $runtime "python.exe" -& $runtimePython -m pip install --requirement $requirements -if ($LASTEXITCODE -ne 0) { throw "Failed to install the pinned Origin runtime packages." } -& $runtimePython -c "import originpro, openpyxl; print('[OK] Origin worker Python packages are available.')" -if ($LASTEXITCODE -ne 0) { throw "Origin runtime import verification failed." } - -Write-Output "[OK] Fixed Origin runtime installed: $runtimePython" -Write-Output "[INFO] Restart zcbot Windows Node to refresh runtime health." diff --git a/windows-node/install-startup.ps1 b/windows-node/install-startup.ps1 deleted file mode 100644 index fc6db13..0000000 --- a/windows-node/install-startup.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -param( - [Parameter(Mandatory = $true)] - [string]$ExecutablePath -) - -$ErrorActionPreference = "Stop" -$resolvedExecutable = (Resolve-Path -LiteralPath $ExecutablePath).Path -if ([System.IO.Path]::GetFileName($resolvedExecutable) -ne "Zcbot.WindowsNode.exe") { - throw "ExecutablePath must point to Zcbot.WindowsNode.exe" -} - -$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name -$workingDirectory = Split-Path -Parent $resolvedExecutable -$action = New-ScheduledTaskAction ` - -Execute $resolvedExecutable ` - -WorkingDirectory $workingDirectory -$trigger = New-ScheduledTaskTrigger -AtLogOn -User $currentUser -$principal = New-ScheduledTaskPrincipal ` - -UserId $currentUser ` - -LogonType Interactive ` - -RunLevel Limited -$settings = New-ScheduledTaskSettingsSet ` - -ExecutionTimeLimit ([TimeSpan]::Zero) ` - -RestartCount 3 ` - -RestartInterval (New-TimeSpan -Minutes 1) ` - -MultipleInstances IgnoreNew - -Register-ScheduledTask ` - -TaskName "Zcbot Windows Node" ` - -Description "Starts the zcbot Windows execution node after this account logs on." ` - -Action $action ` - -Trigger $trigger ` - -Principal $principal ` - -Settings $settings ` - -Force | Out-Null - -Write-Output "[OK] Startup task registered for $currentUser." diff --git a/windows-node/install-windows-node.bat b/windows-node/install-windows-node.bat new file mode 100644 index 0000000..a61fd87 --- /dev/null +++ b/windows-node/install-windows-node.bat @@ -0,0 +1,151 @@ +@echo off +setlocal EnableExtensions +cd /d "%~dp0" + +set "NODE_EXE=%~dp0Zcbot.WindowsNode.exe" +set "REQUIREMENTS=%~dp0origin-worker\requirements.txt" +set "RUNTIME_DIR=%ProgramData%\Zcbot\WindowsNode\runtimes\origin" +set "RUNTIME_PYTHON=%RUNTIME_DIR%\Scripts\python.exe" +set "PYTHON_EXE=" +set "PYTHON_ARGS=" + +echo [INFO] Installing zcbot Windows Node... + +if not exist "%NODE_EXE%" ( + echo [ERR] Zcbot.WindowsNode.exe was not found beside this installer. + goto :failed +) + +call :detect_origin +if errorlevel 1 ( + echo [WARN] Origin/OriginPro was not detected; skipped the Origin runtime. + goto :install_startup +) + +echo [INFO] Origin/OriginPro detected. +if not exist "%REQUIREMENTS%" ( + echo [ERR] Origin worker requirements are missing: %REQUIREMENTS% + goto :failed +) + +call :find_python "%~1" +if errorlevel 1 goto :failed + +if not exist "%RUNTIME_PYTHON%" ( + echo [INFO] Creating Origin Python runtime... + call :run_python -m venv "%RUNTIME_DIR%" + if errorlevel 1 ( + echo [ERR] Failed to create the Origin Python runtime. + goto :failed + ) +) +if not exist "%RUNTIME_PYTHON%" ( + echo [ERR] Python venv did not create its interpreter: %RUNTIME_PYTHON% + goto :failed +) + +"%RUNTIME_PYTHON%" -c "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 12) else 1)" +if errorlevel 1 ( + echo [ERR] Existing Origin runtime is not Python 3.12: %RUNTIME_PYTHON% + goto :failed +) + +echo [INFO] Installing Origin worker dependencies... +"%RUNTIME_PYTHON%" -m pip install --requirement "%REQUIREMENTS%" +if errorlevel 1 ( + echo [ERR] Failed to install the Origin worker dependencies. + goto :failed +) +"%RUNTIME_PYTHON%" -c "import originpro, openpyxl; print('[OK] Origin worker Python packages are available.')" +if errorlevel 1 ( + echo [ERR] Origin runtime import verification failed. + goto :failed +) +echo [OK] Origin runtime installed: %RUNTIME_PYTHON% + +:install_startup +echo [INFO] Registering startup task for %USERDOMAIN%\%USERNAME%... +schtasks.exe /Create /TN "Zcbot Windows Node" /SC ONLOGON /RU "%USERDOMAIN%\%USERNAME%" /RL LIMITED /IT /TR "\"%NODE_EXE%\"" /F +if errorlevel 1 ( + echo [ERR] Failed to register the Windows Node startup task. + goto :failed +) + +echo [OK] Windows Node installation completed. +echo [INFO] Start Zcbot.WindowsNode.exe to register or connect the node. +goto :finish_ok + +:detect_origin +reg.exe query "HKCR\Origin.ApplicationSI\CLSID" >nul 2>&1 +if not errorlevel 1 exit /b 0 +reg.exe query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" /s /f "OriginLab" /d >nul 2>&1 +if not errorlevel 1 exit /b 0 +reg.exe query "HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" /s /f "OriginLab" /d >nul 2>&1 +if not errorlevel 1 exit /b 0 +reg.exe query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" /s /f "OriginLab" /d >nul 2>&1 +if not errorlevel 1 exit /b 0 +exit /b 1 + +:find_python +if not "%~1"=="" ( + if not exist "%~1" ( + echo [ERR] Python executable was not found: %~1 + exit /b 1 + ) + "%~1" -c "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 12) else 1)" + if errorlevel 1 ( + echo [ERR] The specified Python must be Python 3.12: %~1 + exit /b 1 + ) + set "PYTHON_EXE=%~1" + set "PYTHON_ARGS=" + echo [INFO] Using Python 3.12: %~1 + exit /b 0 +) + +where.exe py.exe >nul 2>&1 +if not errorlevel 1 ( + py.exe -3.12 -c "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 12) else 1)" >nul 2>&1 + if not errorlevel 1 ( + set "PYTHON_EXE=py.exe" + set "PYTHON_ARGS=-3.12" + echo [INFO] Using Python 3.12 through py.exe. + exit /b 0 + ) +) + +where.exe python.exe >nul 2>&1 +if not errorlevel 1 ( + python.exe -c "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 12) else 1)" >nul 2>&1 + if not errorlevel 1 ( + set "PYTHON_EXE=python.exe" + set "PYTHON_ARGS=" + echo [INFO] Using Python 3.12 from PATH. + exit /b 0 + ) +) + +echo [ERR] Python 3.12 was not found. +echo [INFO] Add Python 3.12 to PATH or run: install-windows-node.bat "C:\path\python.exe" +exit /b 1 + +:run_python +if defined PYTHON_ARGS ( + "%PYTHON_EXE%" %PYTHON_ARGS% %* +) else ( + "%PYTHON_EXE%" %* +) +exit /b %ERRORLEVEL% + +:failed +echo. +echo [ERR] Installation failed. +echo [INFO] Press any key to close this window. +pause >nul +exit /b 1 + +:finish_ok +echo. +echo [INFO] Press any key to close this window. +pause >nul +exit /b 0 diff --git a/windows-node/package-windows-node.bat b/windows-node/package-windows-node.bat new file mode 100644 index 0000000..b694f5a --- /dev/null +++ b/windows-node/package-windows-node.bat @@ -0,0 +1,108 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion +cd /d "%~dp0" + +set "SELF_CONTAINED=false" +if /I "%~1"=="--self-contained" ( + set "SELF_CONTAINED=true" + shift +) + +set "OUTPUT_ROOT=%~f1" +if "%~1"=="" set "OUTPUT_ROOT=%~dp0dist" +set "PROJECT=%~dp0Zcbot.WindowsNode\Zcbot.WindowsNode.csproj" +set "RUNTIME_ID=win-x64" + +if not exist "%PROJECT%" ( + echo [ERR] Windows Node project was not found: %PROJECT% + goto :failed +) +where.exe dotnet.exe >nul 2>&1 +if errorlevel 1 ( + echo [ERR] .NET SDK was not found in PATH. + goto :failed +) +where.exe tar.exe >nul 2>&1 +if errorlevel 1 ( + echo [ERR] Windows tar.exe was not found. + goto :failed +) +where.exe certutil.exe >nul 2>&1 +if errorlevel 1 ( + echo [ERR] Windows certutil.exe was not found. + goto :failed +) + +set "VERSION=" +for /f "usebackq tokens=*" %%V in (`dotnet msbuild "%PROJECT%" -nologo -getProperty:Version`) do ( + if not "%%V"=="" set "VERSION=%%V" +) +if not defined VERSION ( + echo [ERR] Could not read the Windows Node version. + goto :failed +) + +set "PACKAGE_NAME=zcbot-windows-node-!VERSION!-!RUNTIME_ID!" +set "PUBLISH_DIR=!OUTPUT_ROOT!\!PACKAGE_NAME!" +set "ARCHIVE_PATH=!OUTPUT_ROOT!\!PACKAGE_NAME!.zip" +set "HASH_PATH=!ARCHIVE_PATH!.sha256" + +if not exist "!OUTPUT_ROOT!" mkdir "!OUTPUT_ROOT!" +if errorlevel 1 goto :failed +if exist "!PUBLISH_DIR!" rmdir /s /q "!PUBLISH_DIR!" +if exist "!ARCHIVE_PATH!" del /f /q "!ARCHIVE_PATH!" +if exist "!HASH_PATH!" del /f /q "!HASH_PATH!" + +echo [INFO] Publishing !PACKAGE_NAME! (self-contained=!SELF_CONTAINED!) +dotnet publish "%PROJECT%" --configuration Release --runtime !RUNTIME_ID! --self-contained !SELF_CONTAINED! --output "!PUBLISH_DIR!" +if errorlevel 1 ( + echo [ERR] dotnet publish failed. + goto :failed +) + +for %%F in ( + "Zcbot.WindowsNode.exe" + "install-windows-node.bat" + "origin-worker\worker.py" + "origin-worker\requirements.txt" +) do ( + if not exist "!PUBLISH_DIR!\%%~F" ( + echo [ERR] Published package is incomplete: %%~F + goto :failed + ) +) + +tar.exe -a -c -f "!ARCHIVE_PATH!" -C "!OUTPUT_ROOT!" "!PACKAGE_NAME!" +if errorlevel 1 ( + echo [ERR] Failed to create the ZIP package. + goto :failed +) + +set "SHA256=" +for /f "tokens=*" %%H in ('certutil.exe -hashfile "!ARCHIVE_PATH!" SHA256 ^| findstr.exe /R /C:"^[0-9A-Fa-f][0-9A-Fa-f ]*[0-9A-Fa-f]$"') do ( + set "SHA256=%%H" +) +set "SHA256=!SHA256: =!" +if not defined SHA256 ( + echo [ERR] Failed to calculate the ZIP SHA-256. + goto :failed +) +>"!HASH_PATH!" echo !SHA256! !PACKAGE_NAME!.zip + +echo [OK] Package: !ARCHIVE_PATH! +echo [OK] SHA256: !SHA256! +echo [INFO] Extract the ZIP and double-click install-windows-node.bat. +goto :finish_ok + +:failed +echo. +echo [ERR] Windows Node packaging failed. +echo [INFO] Press any key to close this window. +pause >nul +exit /b 1 + +:finish_ok +echo. +echo [INFO] Press any key to close this window. +pause >nul +exit /b 0