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()}
+ />
+
+