Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### 修复

- Windows 安装成功后删除 `$INSTDIR\openxyos-runtime` 目录和 `openxyos-runtime.zip`(以及 `%LOCALAPPDATA%\FreeOS` 下同名暂存)。预配失败时保留这些文件以便排查。最终工作目录 `$INSTDIR\openxyos` 与 `%LOCALAPPDATA%\FreeOS\openxyos` 不受影响。
- ima 知识库挂接对齐官方 Agent Interface(https://ima.qq.com/agent-interface,落地页现发 ima-skill 1.1.10):用 Client ID / API Key 连接后,按 `search_knowledge_base` + `get_knowledge_base` 选知识库,按 `get_knowledge_list` / `search_knowledge` 自选文档;文件夹 ID 用接口返回的 `media_id`(不自造 `folder_` 前缀),选择会写入挂接。
- 模型页注册的本机 Ollama / GGUF 现在会出现在对话区模型选择器里,并走本机 `127.0.0.1:11434` 调用。注册会补齐 Ollama 供应商默认值、打开服务开关、热加载对话列表;Ollama 未启动时给出可操作错误,而不是静默失败。
- 对话模型选择器底部增加「模型管理」,跳到侧栏同一条「模型」路由(`/models`)。
Expand Down
9 changes: 7 additions & 2 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Same precedence as the FreeOS CLI/server:
else `{home}/openxyos`. During Setup the FreeOS installer `nsExec`s a
shipped **provisioner subprocess** (`provision-openxyos.ps1`) that expands
`openxyos-runtime.zip` with `tar.exe` into that live workdir (and keeps
`$INSTDIR\openxyos` / `$INSTDIR\openxyos-runtime` as backups), starts FE/BE
`$INSTDIR\openxyos` as a sealed copy; the `openxyos-runtime` zip and folder
are deleted after a successful provision), starts FE/BE
at medium integrity (Node inherits `CORS_ORIGIN` / `NODE_ENV`; stdout/stderr
go to `start.log`), and writes `.install-ready` only after
`http://127.0.0.1:3780/api/health/livez` is healthy. If Node dies before
Expand Down Expand Up @@ -54,7 +55,11 @@ a child process** and waits for exit 0. The child extracts with Windows
`%LOCALAPPDATA%\FreeOS\openxyos`, starts Node at medium integrity
(IShellDispatch2 — never High-IL Node from the elevated installer), waits
for livez, and writes `.install-ready` only when healthy. `$INSTDIR\openxyos`
and `$INSTDIR\openxyos-runtime` are sealed backups. A README-only tree or a
is the sealed install copy. After success the provisioner removes
`$INSTDIR\openxyos-runtime.zip` (and similarly named archives) plus the
`$INSTDIR\openxyos-runtime` folder, and a sibling `openxyos-runtime` under
`%LOCALAPPDATA%\FreeOS` if one was staged there. A failed provision leaves
those artifacts for debugging. A README-only tree or a
failed health check is a provision failure, not a skipped success. Setup does
**not** register HKCU Run or a logon scheduled task.

Expand Down
97 changes: 86 additions & 11 deletions desktop/src/build/windows/nsis/provision-openxyos.ps1
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Install-time openXYOS provisioner (child process of the FreeOS NSIS Setup).
# Extract + normalize + start once at medium IL + wait livez + write .install-ready.
# Exit 0 only when http://127.0.0.1:3780/api/health/livez is healthy.
# On success, deletes $InstallDir\openxyos-runtime (folder + archive) and a
# sibling openxyos-runtime under the live parent. Never deletes $LiveDir or
# $InstallDir\openxyos. Failed runs leave staging in place for debugging.
# Does NOT register HKCU Run / scheduled-task logon autostart.
#
# Exit codes (NSIS maps these to localized MessageBox text):
# 0 healthy (.install-ready written)
# 0 healthy (.install-ready written; openxyos-runtime staging removed)
# 2 zip missing
# 3 tar extract into live dir failed
# 5 tar.exe missing
Expand Down Expand Up @@ -300,15 +303,91 @@ function Invoke-TarExtract {
return ($LASTEXITCODE -eq 0)
}

function Test-SamePath {
param([string]$Left, [string]$Right)
if (-not $Left -or -not $Right) { return $false }
try {
$a = [IO.Path]::GetFullPath($Left)
$b = [IO.Path]::GetFullPath($Right)
} catch {
return $false
}
return $a.Equals($b, [StringComparison]::OrdinalIgnoreCase)
}

function Test-PathUnderRoot {
param([string]$Path, [string]$Root)
if (-not $Path -or -not $Root) { return $false }
try {
$full = [IO.Path]::GetFullPath($Path)
$rootFull = [IO.Path]::GetFullPath($Root).TrimEnd('\', '/')
} catch {
return $false
}
if ($full.Equals($rootFull, [StringComparison]::OrdinalIgnoreCase)) { return $true }
$prefix = $rootFull + [IO.Path]::DirectorySeparatorChar
return $full.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)
}

function Remove-OpenXYOSStaging {
$keepLive = $LiveDir
$keepInstall = Join-Path $InstallDir 'openxyos'
$stageDirs = New-Object System.Collections.Generic.List[string]
$stageDirs.Add((Join-Path $InstallDir 'openxyos-runtime'))
$liveParent = ''
if ($LiveDir) {
$liveParent = Split-Path -Parent $LiveDir
if ($liveParent) {
$stageDirs.Add((Join-Path $liveParent 'openxyos-runtime'))
}
}
foreach ($dir in $stageDirs) {
if (-not (Test-Path -LiteralPath $dir)) { continue }
if ((Test-SamePath $dir $keepLive) -or (Test-SamePath $dir $keepInstall)) {
Write-ProvLog "Skipping staging cleanup of protected path $dir"
continue
}
Write-ProvLog "Removing staging directory $dir"
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
}
$archiveRoots = New-Object System.Collections.Generic.List[string]
if ($InstallDir) { $archiveRoots.Add($InstallDir) }
if ($liveParent) { $archiveRoots.Add($liveParent) }
foreach ($root in $archiveRoots) {
if (-not (Test-Path -LiteralPath $root)) { continue }
Get-ChildItem -LiteralPath $root -File -ErrorAction SilentlyContinue | ForEach-Object {
if ($_.Name -notmatch '(?i)^openxyos-runtime(\.(zip|7z|tgz|tar|tar\.gz))?$') { return }
Write-ProvLog "Removing staging archive $($_.FullName)"
Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue
}
}
if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) {
$zipIsStaging = (Test-PathUnderRoot $ZipPath $InstallDir) -or (
$liveParent -and (Test-PathUnderRoot $ZipPath $liveParent)
)
if ($zipIsStaging) {
Write-ProvLog "Removing staging zip $ZipPath"
Remove-Item -LiteralPath $ZipPath -Force -ErrorAction SilentlyContinue
}
}
}

function Complete-OpenXYOSSuccess {
if (-not (Write-InstallReady $LiveDir)) {
exit 13
}
Remove-OpenXYOSStaging
exit 0
}

# --- main ---
New-Item -ItemType Directory -Force -Path $LiveDir | Out-Null
Write-ProvLog "provision-openxyos start zip=$ZipPath install=$InstallDir live=$LiveDir"

$marker = Join-Path $LiveDir '.install-ready'
if ((Test-OpenXYOSLayout $LiveDir) -and (Test-OpenXYOSLivez)) {
Write-ProvLog 'Already extracted and livez healthy (idempotent)'
if (Write-InstallReady $LiveDir) { exit 0 }
exit 13
Complete-OpenXYOSSuccess
}
if (Test-Path -LiteralPath $marker) {
Write-ProvLog 'Removing stale .install-ready (livez not healthy)'
Expand Down Expand Up @@ -392,8 +471,7 @@ if (-not (Test-OpenXYOSCompiledSql $LiveDir)) {
$readme = @(
'FreeOS local openXYOS environment'
"Live workdir (writable): $LiveDir"
"Install backup: $backupOpen"
"Sealed backup: $backupRuntime"
"Install copy: $backupOpen"
'URL: http://127.0.0.1:3780'
) -join "`r`n"
Set-Content -LiteralPath (Join-Path $LiveDir 'README.txt') -Value $readme -Encoding ASCII
Expand All @@ -411,8 +489,7 @@ Install-StartHelpers $LiveDir

if (Test-OpenXYOSLivez) {
Write-ProvLog 'livez already healthy after extract'
if (Write-InstallReady $LiveDir) { exit 0 }
exit 13
Complete-OpenXYOSSuccess
}

if (-not (Start-OpenXYOSUnelevated $LiveDir)) {
Expand All @@ -422,8 +499,7 @@ if (-not (Start-OpenXYOSUnelevated $LiveDir)) {
}

if (Wait-OpenXYOSLivez $LivezTimeoutSec) {
if (Write-InstallReady $LiveDir) { exit 0 }
exit 13
Complete-OpenXYOSSuccess
}

Write-ProvLog "livez not ready after ${LivezTimeoutSec}s; re-invoking start once"
Expand All @@ -433,8 +509,7 @@ if (-not (Start-OpenXYOSUnelevated $LiveDir)) {
exit 10
}
if (Wait-OpenXYOSLivez $RetryTimeoutSec) {
if (Write-InstallReady $LiveDir) { exit 0 }
exit 13
Complete-OpenXYOSSuccess
}

Write-ProvLog 'livez timeout after start retry — see this log and start.log (not assumed to be port 3780 in use)'
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/desktop/test_nsis_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ def test_nsis_runs_openxyos_provisioner_subprocess() -> None:
assert "nsExec::ExecToLog $R4" in provision
assert r"$INSTDIR\openxyos-runtime.zip" in provision
assert r"$R6\FreeOS\openxyos" in provision
# Cleanup after success lives in the provisioner, not NSIS (failed
# provision must keep the zip / openxyos-runtime folder for debug).
assert 'Delete "$INSTDIR\\openxyos-runtime.zip"' not in provision
assert 'RMDir /r "$INSTDIR\\openxyos-runtime"' not in provision
assert "Pop $0" in provision
assert "Abort" in provision
assert ".install-ready" in provision
Expand Down
36 changes: 34 additions & 2 deletions tests/unit/desktop/test_openxyos_provisioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,43 @@ def test_provisioner_fail_fast_when_node_dies() -> None:
def test_provisioner_writes_marker_only_when_healthy() -> None:
text = PROVISION_PS1.read_text(encoding="utf-8")
write_fn = text.index("function Write-InstallReady")
first_write = text.index("if (Write-InstallReady $LiveDir) { exit 0 }")
assert write_fn < first_write
complete_fn = text.index("function Complete-OpenXYOSSuccess")
first_complete = text.index(
"Complete-OpenXYOSSuccess", complete_fn + len("function Complete-OpenXYOSSuccess")
)
assert write_fn < complete_fn < first_complete
assert "Test-OpenXYOSLivez" in text
assert ".install-ready" in text
assert write_fn < text.index("status=healthy")
assert "if (Write-InstallReady $LiveDir) { exit 0 }" not in text


def test_provisioner_removes_staging_only_after_success() -> None:
"""Zip + openxyos-runtime folder go away only after .install-ready."""
text = PROVISION_PS1.read_text(encoding="utf-8")
clean_fn = text.index("function Remove-OpenXYOSStaging")
complete_fn = text.index("function Complete-OpenXYOSSuccess")
main = text.index("# --- main ---")
assert clean_fn < complete_fn < main
complete_body = text[complete_fn:main]
assert complete_body.index("Write-InstallReady") < complete_body.index("Remove-OpenXYOSStaging")
assert complete_body.index("Remove-OpenXYOSStaging") < complete_body.index("exit 0")
assert "exit 13" in complete_body
clean_body = text[clean_fn:complete_fn]
assert "Join-Path $InstallDir 'openxyos-runtime'" in clean_body
assert "Join-Path $liveParent 'openxyos-runtime'" in clean_body
assert "openxyos-runtime" in clean_body
assert "SilentlyContinue" in clean_body
assert "Join-Path $InstallDir 'openxyos'" in clean_body
assert "Skipping staging cleanup of protected path" in clean_body
# Failure exits must not call cleanup (keep zip/folder for debug).
for code in ("exit 2", "exit 3", "exit 5", "exit 6", "exit 7", "exit 10", "exit 12"):
idx = text.index(code)
window = text[max(0, idx - 120) : idx]
assert "Remove-OpenXYOSStaging" not in window, code
assert "Complete-OpenXYOSSuccess" not in window, code
assert text.count("Complete-OpenXYOSSuccess") >= 5
assert "Sealed backup:" not in text


def test_wrappers_have_no_goto_labels() -> None:
Expand Down
Loading