From 4400ecd81c2f89068f5809073e74c7cce08693f6 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Wed, 16 Sep 2026 05:10:52 +0000
Subject: [PATCH] Fix Windows openXYOS extract and use a native folder picker
for source download.
NSIS no longer calls PowerShell to unzip into Program Files (spaces broke
quoting and left $INSTDIR\openxyos as README-only). Setup now expands the
prebuilt zip with tar.exe into both openxyos and openxyos-runtime, and
aborts if node.exe or dist/index.html is missing. First launch copies the
complete tree, including the staged runtime, to LocalAppData.
Organization "download latest openXYOS source" opens the same Wails folder
picker as project workdirs instead of a typed path or window.prompt.
Co-authored-by: XYAI Labs
---
CHANGELOG.md | 1 +
dashboard/src/locales/en.json | 4 +-
dashboard/src/locales/zh.json | 4 +-
dashboard/src/pages/Organization/index.tsx | 110 +++++++++++-------
.../pages/Organization/pickSourceDest.test.ts | 35 ++++++
.../src/pages/Organization/pickSourceDest.ts | 16 +++
desktop/README.md | 21 ++--
desktop/src/build/stage_openxyos_runtime.py | 24 ++++
desktop/src/build/windows/nsis/project.nsi | 12 +-
.../src/build/windows/nsis/wails_tools.nsh | 99 ++++++++++++----
desktop/src/process_sidecar.go | 30 ++++-
desktop/src/process_sidecar_test.go | 109 +++++++++++++++++
src/octop/modules/org_os/sidecar_launch.py | 6 +-
tests/unit/desktop/test_nsis_uninstall.py | 43 ++++++-
.../desktop/test_stage_openxyos_runtime.py | 18 +++
tests/unit/test_sidecar_launch.py | 13 +++
16 files changed, 463 insertions(+), 82 deletions(-)
create mode 100644 dashboard/src/pages/Organization/pickSourceDest.test.ts
create mode 100644 dashboard/src/pages/Organization/pickSourceDest.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54cd2301..94e858ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,6 +33,7 @@
### 修复
+- Windows 安装把预构建 openXYOS 运行包用 `tar.exe` 解到 `$INSTDIR\openxyos` 与 `$INSTDIR\openxyos-runtime`(带空格的 `Program Files` 也能解全)。缺少 `node\node.exe` 或 `dist\index.html` 时安装失败,不再留下只有 README 的工作目录。首次启动会从完整树复制到 `%LOCALAPPDATA%\FreeOS\openxyos` 并自动拉起边车。组织页「下载最新 openXYOS 源码」走系统文件夹选择器(任意盘符),不再只用手输路径。
- 桌面 / 本地会话不再被 `setup_required` 短路:`GET /setup/status` 之后仍可调用 `POST /auth/local-session`,不会误跳进服务端密码向导。
- 组织页「启动边车」会拉起捆绑的 Node + openXYOS(Windows 用 `node.exe` / `cmd /c`,不再直接 Popen `.bat`),并在右侧预览区嵌入控制台(支持全屏)。openXYOS 在 FreeOS 桌面环境下关闭 `X-Frame-Options`,避免 iframe 空白。安装 / 桌面启动时宿主与 Wails 都会确保边车常开(需 `dist/index.html` 已打包)。
- Windows 打开时不再因 WebView2 `Navigate`/`SetURL` 在 Chromium 未就绪时触发 Go panic。主窗口等 WebView 点火后再跳转;`SetURL` 失败会重试并显示 FreeOS 提示,而不是原始堆栈。设置窗口延后创建,避免两个 WebView2 同时初始化同一用户目录。用户数据目录必须可写,并设置 `WEBVIEW2_USER_DATA_FOLDER`。
diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json
index 7660e674..f99907f0 100644
--- a/dashboard/src/locales/en.json
+++ b/dashboard/src/locales/en.json
@@ -98,10 +98,12 @@
"fullscreen": "Fullscreen",
"exitFullscreen": "Exit fullscreen",
"downloadSource": "Download latest openXYOS source",
- "downloadSourceHint": "Download the latest openXYOS main-branch source zip from GitHub into a folder you choose, for other deployments.",
+ "downloadSourceHint": "Pick any folder on any drive with the system dialog. FreeOS then downloads the latest openXYOS main-branch source from GitHub.",
"downloadSourceDest": "Save to folder",
"downloadSourceDone": "Downloaded to {{path}}",
"downloadSourceFailed": "Could not download the source",
+ "browseDest": "Browse…",
+ "pickFolderFailed": "Could not open the folder picker",
"catalogEnabled": "Shown",
"catalogDisabled": "Hidden",
"catalogToggleHint": "When off, the module disappears from the openXYOS preview sidebar.",
diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json
index 48a45841..b6487b3b 100644
--- a/dashboard/src/locales/zh.json
+++ b/dashboard/src/locales/zh.json
@@ -98,10 +98,12 @@
"fullscreen": "全屏",
"exitFullscreen": "退出全屏",
"downloadSource": "下载最新 openXYOS 源码",
- "downloadSourceHint": "从 GitHub 下载 openXYOS 主分支源码 zip,保存到你选择的文件夹,便于在其他环境部署。",
+ "downloadSourceHint": "用系统文件夹选择器任选盘符与目录,FreeOS 会从 GitHub 下载最新 openXYOS 主分支源码。",
"downloadSourceDest": "保存到文件夹",
"downloadSourceDone": "已下载到 {{path}}",
"downloadSourceFailed": "下载源码失败",
+ "browseDest": "浏览…",
+ "pickFolderFailed": "无法打开文件夹选择器",
"catalogEnabled": "已显示",
"catalogDisabled": "已隐藏",
"catalogToggleHint": "关闭后,该模块会从右侧 openXYOS 预览的侧栏消失。",
diff --git a/dashboard/src/pages/Organization/index.tsx b/dashboard/src/pages/Organization/index.tsx
index 1b6e33b4..bc85210f 100644
--- a/dashboard/src/pages/Organization/index.tsx
+++ b/dashboard/src/pages/Organization/index.tsx
@@ -25,7 +25,12 @@ import {
} from "../../api/modules/orgModule";
import { formatServerIsoDateTime } from "../../utils/formatMessageTime";
import { useServerTimezone } from "../../hooks/useServerTimezone";
+import {
+ canPickDesktopFolder,
+ pickDesktopFolder,
+} from "../../utils/desktopFolder";
import { message } from "../../utils/antdMessage";
+import { resolveOpenxyosSourceDest } from "./pickSourceDest";
import styles from "./Organization.module.less";
type ActionKey = "assemble" | "pack" | "loop" | "sidecar" | "produce" | null;
@@ -51,6 +56,7 @@ export default function OrganizationPage() {
);
const [sourceDest, setSourceDest] = useState("");
const [downloading, setDownloading] = useState(false);
+ const [pickingFolder, setPickingFolder] = useState(false);
const [produceName, setProduceName] = useState("");
const [produceIma, setProduceIma] = useState("");
const [landed, setLanded] = useState | null>(null);
@@ -203,12 +209,7 @@ export default function OrganizationPage() {
}
};
- const downloadSource = async () => {
- const dest = sourceDest.trim();
- if (!dest) {
- message.error(t("organization.downloadSourceDest"));
- return;
- }
+ const saveSourceTo = async (dest: string) => {
setDownloading(true);
try {
const result = await orgModuleApi.downloadSource(dest);
@@ -226,6 +227,51 @@ export default function OrganizationPage() {
}
};
+ const browseSourceDest = async () => {
+ setPickingFolder(true);
+ try {
+ const path = await pickDesktopFolder();
+ if (path) {
+ setSourceDest(path);
+ } else if (!canPickDesktopFolder()) {
+ message.info(t("organization.pickFolderFailed"));
+ }
+ } catch (err) {
+ message.error(
+ err instanceof Error ? err.message : t("organization.pickFolderFailed"),
+ );
+ } finally {
+ setPickingFolder(false);
+ }
+ };
+
+ const downloadSource = async () => {
+ setPickingFolder(true);
+ let dest: string | null = null;
+ try {
+ dest = await resolveOpenxyosSourceDest({
+ canPickNative: canPickDesktopFolder(),
+ pickNative: pickDesktopFolder,
+ typedDest: sourceDest,
+ });
+ } catch (err) {
+ message.error(
+ err instanceof Error ? err.message : t("organization.pickFolderFailed"),
+ );
+ setPickingFolder(false);
+ return;
+ }
+ setPickingFolder(false);
+ if (!dest) {
+ if (!canPickDesktopFolder()) {
+ message.error(t("organization.downloadSourceDest"));
+ }
+ return;
+ }
+ setSourceDest(dest);
+ await saveSourceTo(dest);
+ };
+
const assemble = () =>
runAction("assemble", async () => {
const result: OrgAssembleResult = await orgModuleApi.assemble();
@@ -331,34 +377,8 @@ export default function OrganizationPage() {
{(sidecarUp || overview?.enabled) && (
}
- onClick={() => {
- const dest = window.prompt(
- t("organization.downloadSourceDest"),
- );
- if (dest) {
- setSourceDest(dest);
- void (async () => {
- setDownloading(true);
- try {
- const result = await orgModuleApi.downloadSource(dest);
- message.success(
- t("organization.downloadSourceDone", {
- path: result.path,
- }),
- );
- } catch (err) {
- message.error(
- err instanceof Error
- ? err.message
- : t("organization.downloadSourceFailed"),
- );
- } finally {
- setDownloading(false);
- }
- })();
- }
- }}
- loading={downloading}
+ onClick={() => void downloadSource()}
+ loading={downloading || pickingFolder}
>
{t("organization.downloadSource")}
@@ -741,15 +761,23 @@ export default function OrganizationPage() {
{t("organization.downloadSourceHint")}
- setSourceDest(event.target.value)}
- placeholder={t("organization.downloadSourceDest")}
- style={{ minWidth: 260 }}
- />
+
+ setSourceDest(event.target.value)}
+ placeholder={t("organization.downloadSourceDest")}
+ readOnly={canPickDesktopFolder()}
+ />
+
+