diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f0a118..5015280d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 修复 +- Windows 安装包解压 openXYOS 时修正 `extract-openxyos.cmd` 的 `cmd.exe` 标签(`:liveNodeOk`)。原先写成 `liveNodeOk:`,goto 找不到标签会以退出码 1 结束;文件已齐备时 livez 在 90 秒内未就绪改为警告并继续安装,不再整包 Abort。`.install-ready` 在 layout 通过后、livez 探测前写入,HKCU Run / 登录任务保留,组织页可随后拉起边车。 - 安装期不再在 livez 探测后 `taskkill` 边车:Setup 把 `start-sidecar.ps1` 写到 `%LOCALAPPDATA%\\FreeOS\\openxyos`,用当前用户(IShellDispatch2 / HKCU Run / 登录计划任务)常驻拉起 FE+BE,确认 `http://127.0.0.1:3780/api/health/livez` 后保持运行。组织页在 `.install-ready` 时自动嵌入本机 openXYOS,不再把「启动边车」当主按钮;仅自动启动失败才显示「重试启动」和中文原因。 - 「下载最新 openXYOS 源码」端到端 UTF-8:Wails 文件夹选择器以 UTF-8 Base64 回传路径(不再把 GBK/ACP 当 UTF-8);落盘前修复 CP1252/Latin-1 误读的目标路径;解压按 zip UTF-8 标志或 GBK 还原中文目录名,避免乱码。 - Windows 知识库本地文件夹挂接:选择器与列表预览按 UTF-8 处理中文路径/文件名,避免 `项目…` 这类乱码;挂接与列表预览不再因向量模型未就绪返回 409。 diff --git a/desktop/README.md b/desktop/README.md index b4a1f66b..91ed36a0 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -26,12 +26,14 @@ Same precedence as the FreeOS CLI/server: else `{home}/openxyos`. The NSIS installer expands a prebuilt `openxyos-runtime.zip` **directly into that live workdir** during Setup (and keeps `$INSTDIR\openxyos` / `$INSTDIR\openxyos-runtime` as backups). - Install fails if `node` or `dist` is missing, or if - `http://127.0.0.1:3780/api/health/livez` does not pass before Setup exits. - Setup then leaves a **user-level** sidecar running (HKCU Run - `FreeOS-openXYOS` + `start-sidecar.ps1`) so Organization embeds immediately - — no first-open copy and no 「启动边车」 click. First FreeOS start only - attaches to that live tree if the port is already healthy. + Install fails if `node` or `dist` is missing. If files are in place but + `http://127.0.0.1:3780/api/health/livez` is still down after the install-time + wait, Setup warns and continues — HKCU Run / the logon task stay registered + so Organization can attach later. Setup then leaves a **user-level** sidecar + running (HKCU Run `FreeOS-openXYOS` + `start-sidecar.ps1`) so Organization + embeds immediately when livez is already up — no first-open copy and no + 「启动边车」 click. First FreeOS start only attaches to that live tree if + the port is already healthy. - Shell prefs → `{home}/desktop-settings.json` ## Windows install finish @@ -48,9 +50,9 @@ also copies `openxyos-runtime.zip` (prebuilt Node + openXYOS frontend/backend) and extracts it with Windows `tar.exe` (quoted paths, so `Program Files` works) into the **live** workdir `%LOCALAPPDATA%\FreeOS\openxyos` during Setup. `$INSTDIR\openxyos` and `$INSTDIR\openxyos-runtime` are sealed -backups only. Setup **aborts** if `node\node.exe` or `dist\index.html` is -missing, or if the user-level sidecar cannot reach -`/api/health/livez` — a README-only tree is not a successful install. +backups only. Setup **aborts** only if `node\node.exe` or `dist\index.html` is +missing — a README-only tree is not a successful install. A livez +timeout after those files exist is a warning, not an abort. The probe **does not** start Node as Administrator and **does not** `taskkill` it afterwards (that left Organization asking to 启动边车). PersistOpenXYOS launches `start-sidecar.ps1` with the explorer token, diff --git a/desktop/src/build/windows/nsis/project.nsi b/desktop/src/build/windows/nsis/project.nsi index 58f3aa66..43e6c343 100644 --- a/desktop/src/build/windows/nsis/project.nsi +++ b/desktop/src/build/windows/nsis/project.nsi @@ -74,8 +74,8 @@ LangString OPENXYOS_PROBE ${LANG_SIMPCHINESE} "正在以当前用户启动本机 LangString OPENXYOS_PROBE ${LANG_ENGLISH} "Starting local openXYOS as the current user and checking http://127.0.0.1:3780/api/health/livez" LangString OPENXYOS_PROBE_OK ${LANG_SIMPCHINESE} "openXYOS 已在本机运行,并已登记用户级开机自启" LangString OPENXYOS_PROBE_OK ${LANG_ENGLISH} "openXYOS is running locally and registered for user-level auto-start" -LangString OPENXYOS_PROBE_FAIL ${LANG_SIMPCHINESE} "安装失败:已解压 openXYOS,但 http://127.0.0.1:3780/api/health/livez 未就绪。请检查 3780 端口占用后重试安装。" -LangString OPENXYOS_PROBE_FAIL ${LANG_ENGLISH} "Setup failed: openXYOS is on disk but http://127.0.0.1:3780/api/health/livez did not become ready. Free port 3780 and run Setup again." +LangString OPENXYOS_PROBE_WARN ${LANG_SIMPCHINESE} "openXYOS 文件已就绪,但 http://127.0.0.1:3780/api/health/livez 暂未响应。安装将继续;登录后会自动拉起本机服务,也可稍后在组织页重试。" +LangString OPENXYOS_PROBE_WARN ${LANG_ENGLISH} "openXYOS files are in place, but http://127.0.0.1:3780/api/health/livez is not ready yet. Setup will continue; the service auto-starts at logon, or retry from Organization later." LangString UN_FREEOS_RUNNING ${LANG_SIMPCHINESE} "检测到 FreeOS 仍在运行(主程序、主机或组织侧车)。$\r$\n$\r$\n继续将结束这些进程,并删除安装目录中的程序文件。$\r$\n用户数据(%USERPROFILE%\.freeos)会保留。$\r$\n$\r$\n要继续卸载吗?" LangString UN_FREEOS_RUNNING ${LANG_ENGLISH} "FreeOS is still running (shell, host, or organization sidecar).$\r$\n$\r$\nContinuing will stop those processes and remove program files from the install folder.$\r$\nUser data (%USERPROFILE%\.freeos) is kept.$\r$\n$\r$\nContinue uninstall?" diff --git a/desktop/src/build/windows/nsis/wails_tools.nsh b/desktop/src/build/windows/nsis/wails_tools.nsh index 8d91c051..3c37f9fc 100644 --- a/desktop/src/build/windows/nsis/wails_tools.nsh +++ b/desktop/src/build/windows/nsis/wails_tools.nsh @@ -184,10 +184,12 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" FileWrite $0 `if errorlevel 1 exit /b 8$\r$\n` FileWrite $0 `if exist "%LIVE%\node\node.exe" goto liveNodeOk$\r$\n` FileWrite $0 `if exist "$INSTDIR\openxyos-runtime\node\node.exe" xcopy /E /I /Y "$INSTDIR\openxyos-runtime\*" "%LIVE%\" >nul$\r$\n` - FileWrite $0 `liveNodeOk:$\r$\n` + # cmd.exe labels require a leading colon. "liveNodeOk:" is not a goto target. + FileWrite $0 `:liveNodeOk$\r$\n` FileWrite $0 `if not exist "%LIVE%\node\node.exe" exit /b 6$\r$\n` FileWrite $0 `if not exist "%LIVE%\openxyos\dist\index.html" if not exist "%LIVE%\dist\index.html" exit /b 7$\r$\n` FileWrite $0 `if defined USERNAME icacls "$R6\FreeOS" /grant "%USERNAME%:(OI)(CI)M" /T /C /Q >nul 2>&1$\r$\n` + FileWrite $0 `exit /b 0$\r$\n` FileClose $0 nsExec::ExecToLog '"$PLUGINSDIR\extract-openxyos.cmd"' Pop $0 @@ -208,12 +210,14 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" FileClose $0 !insertmacro wails.writeOpenXYOSAutostart Call PersistOpenXYOS - !insertmacro wails.probeOpenXYOS + # Stamp after layout + autostart, before livez wait. Probe must not Abort + # when node/dist exist; keep this write reachable on a slow sidecar. FileOpen $0 "$R6\FreeOS\openxyos\.install-ready" w FileWrite $0 "live=$R6\FreeOS\openxyos$\r$\n" FileWrite $0 "url=http://127.0.0.1:3780/api/health/livez$\r$\n" FileWrite $0 "autostart=hkcu-run:FreeOS-openXYOS$\r$\n" FileClose $0 + !insertmacro wails.probeOpenXYOS SetDetailsPrint listonly !macroend @@ -323,30 +327,29 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" # Wait for the user-level sidecar PersistOpenXYOS launched. Do not spawn # Node as admin and do not kill the probe process — that was the #29 gap. +# livez timeout is a warning only: node + dist already passed +# requireOpenXYOSLayout, and HKCU Run / the LIMITED task stay registered. !macro wails.probeOpenXYOS DetailPrint "$(OPENXYOS_PROBE)" FileOpen $0 "$PLUGINSDIR\wait-openxyos.ps1" w FileWrite $0 `$$livez = 'http://127.0.0.1:3780/api/health/livez'$\r$\n` FileWrite $0 `function Test-Livez { try { $$r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 2 -Uri $$livez; return ($$r.StatusCode -lt 500) } catch { return $$false } }$\r$\n` - FileWrite $0 `for ($$i = 0; $$i -lt 60; $$i++) { if (Test-Livez) { exit 0 }; Start-Sleep -Seconds 1 }$\r$\n` + FileWrite $0 `for ($$i = 0; $$i -lt 90; $$i++) { if (Test-Livez) { exit 0 }; if ($$i -eq 30) { schtasks.exe /Run /TN "FreeOS-openXYOS" | Out-Null }; Start-Sleep -Seconds 1 }$\r$\n` FileWrite $0 `exit 12$\r$\n` FileClose $0 nsExec::ExecToLog '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "$PLUGINSDIR\wait-openxyos.ps1"' Pop $0 DetailPrint "$(OPENXYOS_EXTRACT_CODE)$0" - IntCmp $0 0 openxyosProbeOk openxyosProbeFail openxyosProbeFail - openxyosProbeFail: - DetailPrint "$(OPENXYOS_PROBE_FAIL)" - IfSilent openxyosProbeSilent openxyosProbeLoud - openxyosProbeSilent: - SetErrorLevel 68 - Abort - openxyosProbeLoud: - MessageBox MB_OK|MB_ICONSTOP "$(OPENXYOS_PROBE_FAIL)" - SetErrorLevel 68 - Abort + IntCmp $0 0 openxyosProbeOk openxyosProbeWarn openxyosProbeWarn + openxyosProbeWarn: + DetailPrint "$(OPENXYOS_PROBE_WARN)" + IfSilent openxyosProbeDone openxyosProbeNote + openxyosProbeNote: + MessageBox MB_OK|MB_ICONINFORMATION "$(OPENXYOS_PROBE_WARN)" + Goto openxyosProbeDone openxyosProbeOk: DetailPrint "$(OPENXYOS_PROBE_OK)" + openxyosProbeDone: !macroend !macro wails.writeUninstaller diff --git a/tests/unit/desktop/test_nsis_uninstall.py b/tests/unit/desktop/test_nsis_uninstall.py index 8f704cae..0db060b0 100644 --- a/tests/unit/desktop/test_nsis_uninstall.py +++ b/tests/unit/desktop/test_nsis_uninstall.py @@ -230,9 +230,14 @@ def test_nsis_extracts_runtime_with_quoted_paths_and_aborts_if_incomplete() -> N assert "wait-openxyos.ps1" in nsh assert "probe-openxyos.ps1" not in nsh assert "/api/health/livez" in nsh - assert "SetErrorLevel 68" in nsh - assert "LangString OPENXYOS_PROBE_FAIL ${LANG_SIMPCHINESE}" in nsi - assert ".install-ready" in nsh + assert "LangString OPENXYOS_PROBE_WARN ${LANG_SIMPCHINESE}" in nsi + assert "安装将继续" in nsi + assert ".install-ready" in provision + assert provision.index(".install-ready") < provision.index("!insertmacro wails.probeOpenXYOS") + assert provision.index("!insertmacro wails.requireOpenXYOSLayout") < provision.index( + ".install-ready" + ) + assert provision.index("Call PersistOpenXYOS") < provision.index(".install-ready") assert "start-sidecar.ps1" in nsh assert "FreeOS-openXYOS" in nsi workflow = (REPO / ".github" / "workflows" / "octop-desktop.yml").read_text(encoding="utf-8") @@ -261,6 +266,38 @@ def test_desktop_readme_documents_uninstall_keep_vs_remove() -> None: assert "install-time" in text.lower() or "during Setup" in text or "安装期" in text +def _filewrite_payloads(block: str) -> list[str]: + payloads: list[str] = [] + for raw in block.splitlines(): + stripped = raw.strip() + if not stripped.startswith("FileWrite"): + continue + start = stripped.find("`") + end = stripped.rfind("`") + if start != -1 and end > start: + payloads.append(stripped[start + 1 : end].replace("$\\r$\\n", "\n")) + return payloads + + +def test_nsis_extract_cmd_uses_colon_prefixed_batch_labels() -> None: + """cmd.exe goto targets must be ':label'; 'label:' is not found.""" + nsh = NSH.read_text(encoding="utf-8") + provision = nsh[ + nsh.index("!macro wails.provisionOpenXYOS") : nsh.index( + "!macro wails.requireOpenXYOSLayout" + ) + ] + script = "".join(_filewrite_payloads(provision)) + assert "goto liveNodeOk" in script + assert ":liveNodeOk" in script + assert "exit /b 0" in script + assert "FileWrite $0 `liveNodeOk:" not in nsh + for line in script.splitlines(): + token = line.strip() + if token.endswith(":") and " " not in token and not token.startswith(":"): + raise AssertionError(f"cmd label missing leading colon: {token!r}") + + def test_nsis_leaves_user_level_sidecar_running() -> None: """#29 probed then taskkill'd Node; Organization still asked to 启动边车.""" nsi = NSI.read_text(encoding="utf-8-sig") @@ -277,7 +314,11 @@ def test_nsis_leaves_user_level_sidecar_running() -> None: assert "taskkill.exe" not in autostart assert "taskkill.exe" not in probe assert "Start-Process" not in probe + assert "Abort" not in probe + assert "SetErrorLevel 68" not in probe assert "wait-openxyos.ps1" in probe + assert "OPENXYOS_PROBE_WARN" in probe + assert "schtasks.exe /Run" in probe assert "Call PersistOpenXYOS" in nsh assert "FreeOS-openXYOS" in persist assert "A4C6892C-3BA9-11d2-9DEA-00C04FB16162" in persist