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 @@ -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`。
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 预览的侧栏消失。",
Expand Down
110 changes: 69 additions & 41 deletions dashboard/src/pages/Organization/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Record<string, unknown> | null>(null);
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -331,34 +377,8 @@ export default function OrganizationPage() {
{(sidecarUp || overview?.enabled) && (
<Button
icon={<Package size={14} />}
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")}
</Button>
Expand Down Expand Up @@ -741,15 +761,23 @@ export default function OrganizationPage() {
{t("organization.downloadSourceHint")}
</p>
<Space wrap style={{ marginTop: 12 }}>
<Input
value={sourceDest}
onChange={(event) => setSourceDest(event.target.value)}
placeholder={t("organization.downloadSourceDest")}
style={{ minWidth: 260 }}
/>
<Space.Compact style={{ minWidth: 320 }}>
<Input
value={sourceDest}
onChange={(event) => setSourceDest(event.target.value)}
placeholder={t("organization.downloadSourceDest")}
readOnly={canPickDesktopFolder()}
/>
<Button
loading={pickingFolder}
onClick={() => void browseSourceDest()}
>
{t("organization.browseDest")}
</Button>
</Space.Compact>
<Button
type="primary"
loading={downloading}
loading={downloading || pickingFolder}
onClick={() => void downloadSource()}
>
{t("organization.downloadSource")}
Expand Down
35 changes: 35 additions & 0 deletions dashboard/src/pages/Organization/pickSourceDest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it, vi } from "vitest";
import { resolveOpenxyosSourceDest } from "./pickSourceDest";

describe("resolveOpenxyosSourceDest", () => {
it("uses the native folder picker on desktop and ignores the typed path", async () => {
const pickNative = vi.fn().mockResolvedValue("D:\\src\\openxyos");
const dest = await resolveOpenxyosSourceDest({
canPickNative: true,
pickNative,
typedDest: "C:\\typed-only",
});
expect(pickNative).toHaveBeenCalled();
expect(dest).toBe("D:\\src\\openxyos");
});

it("returns null when the native picker is cancelled", async () => {
const dest = await resolveOpenxyosSourceDest({
canPickNative: true,
pickNative: async () => null,
typedDest: "C:\\typed-only",
});
expect(dest).toBeNull();
});

it("falls back to a typed path only outside the desktop shell", async () => {
const pickNative = vi.fn();
const dest = await resolveOpenxyosSourceDest({
canPickNative: false,
pickNative,
typedDest: " /home/me/src ",
});
expect(pickNative).not.toHaveBeenCalled();
expect(dest).toBe("/home/me/src");
});
});
16 changes: 16 additions & 0 deletions dashboard/src/pages/Organization/pickSourceDest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Resolve where to save the latest openXYOS source zip.
* Desktop: native folder picker (any drive). Browser: typed path fallback.
*/
export async function resolveOpenxyosSourceDest(options: {
canPickNative: boolean;
pickNative: () => Promise<string | null>;
typedDest: string;
}): Promise<string | null> {
if (options.canPickNative) {
const picked = await options.pickNative();
return picked && picked.trim() ? picked.trim() : null;
}
const typed = options.typedDest.trim();
return typed || null;
}
21 changes: 14 additions & 7 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ Same precedence as the FreeOS CLI/server:
- Green runtime extract → `{home}/portable/`
- Organization sidecar data → `{home}/org-os/`
- Local openXYOS workdir (FE+BE) → `%LOCALAPPDATA%\FreeOS\openxyos` on Windows,
else `{home}/openxyos`. The NSIS installer also expands a prebuilt
`openxyos-runtime.zip` into `$INSTDIR\openxyos`. First unelevated start copies
else `{home}/openxyos`. The NSIS installer expands a prebuilt
`openxyos-runtime.zip` into `$INSTDIR\openxyos` and `$INSTDIR\openxyos-runtime`
(install fails if `node` or `dist` is missing). First unelevated start copies
that tree into the workdir and starts `http://127.0.0.1:3780`.
- Shell prefs → `{home}/desktop-settings.json`

Expand All @@ -39,11 +40,17 @@ with `makensis -INPUTCHARSET UTF8` from a UTF-8 BOM `project.nsi`.

The install log is no longer only `FreeOS.exe` + shortcuts. A healthy package
also copies `openxyos-runtime.zip` (prebuilt Node + openXYOS frontend/backend)
and extracts it to `$INSTDIR\openxyos`. The durable workdir is
`%LOCALAPPDATA%\FreeOS\openxyos` (and `{home}/openxyos`). Program Files is
read-only, so first start — not the elevated installer — writes the user
workdir. Organization then embeds `http://127.0.0.1:3780` without a manual
「启动边车」 click.
and extracts it with Windows `tar.exe` (quoted paths, so `Program Files`
works) into **both** `$INSTDIR\openxyos` and `$INSTDIR\openxyos-runtime`.
Setup **aborts** if `node\node.exe` or `dist\index.html` is missing after
extract — a README-only `$INSTDIR\openxyos` is not a successful install.
The durable workdir is `%LOCALAPPDATA%\FreeOS\openxyos` (and `{home}/openxyos`).
Program Files is read-only, so first start — not the elevated installer —
copies the complete tree into the user workdir (and will heal from
`openxyos-runtime` if `openxyos` was left as a stub). Organization then
embeds `http://127.0.0.1:3780` without a manual 「启动边车」 click.
Downloading the latest openXYOS source uses the same native folder picker
as the project workdir (any drive), not a typed path only.

Override the workdir with `FREEOS_OPENXYOS_HOME`.

Expand Down
24 changes: 24 additions & 0 deletions desktop/src/build/stage_openxyos_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@
)


def layout_ready(root: Path) -> bool:
"""True when *root* is a complete sidecar tree, not README-only."""
node_ok = (root / "node" / "node.exe").is_file() or (root / "node" / "bin" / "node").is_file()
fe_ok = (root / "openxyos" / "dist" / "index.html").is_file() or (
root / "dist" / "index.html"
).is_file()
be_ok = (root / "openxyos" / "backend" / "server.ts").is_file() or (
root / "backend" / "server.ts"
).is_file()
return node_ok and fe_ok and be_ok


def verify_runtime_zip(path: Path) -> None:
"""Fail if the zip would install as README-only (missing node or dist)."""
with zipfile.ZipFile(path) as zf:
names = {name.replace("\\", "/").lstrip("./") for name in zf.namelist()}
node_ok = "node/node.exe" in names or "node/bin/node" in names
fe_ok = "openxyos/dist/index.html" in names or "dist/index.html" in names
be_ok = "openxyos/backend/server.ts" in names or "backend/server.ts" in names
if not (node_ok and fe_ok and be_ok):
raise SystemExit(f"incomplete openxyos-runtime.zip (need node + dist/index.html): {path}")


def _bundle_root(extracted: Path) -> Path | None:
direct = extracted
if _ready(direct):
Expand Down Expand Up @@ -70,6 +93,7 @@ def stage(source: Path, dest_zip: Path) -> Path:
continue
rel = path.relative_to(root).as_posix()
zf.write(path, rel)
verify_runtime_zip(dest_zip)
readme = dest_zip.with_name("openxyos-README.txt")
readme.write_text(
"FreeOS openXYOS runtime\n"
Expand Down
12 changes: 6 additions & 6 deletions desktop/src/build/windows/nsis/project.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ LangString OPENXYOS_EXTRACT ${LANG_SIMPCHINESE} "解压 openXYOS 前端与后端
LangString OPENXYOS_EXTRACT ${LANG_ENGLISH} "Extract openXYOS frontend and backend into the install dir\\openxyos"
LangString OPENXYOS_FE_OK ${LANG_SIMPCHINESE} "已就绪:openxyos\\dist\\index.html"
LangString OPENXYOS_FE_OK ${LANG_ENGLISH} "Ready: openxyos\\dist\\index.html"
LangString OPENXYOS_FE_MISSING ${LANG_SIMPCHINESE} "警告:未找到 openXYOS 前端构建,首次启动将再试"
LangString OPENXYOS_FE_MISSING ${LANG_ENGLISH} "Warning: openXYOS frontend build missing; first launch will retry"
LangString OPENXYOS_NODE_OK ${LANG_SIMPCHINESE} "已就绪:bundled Node(node.exe)"
LangString OPENXYOS_NODE_OK ${LANG_ENGLISH} "Ready: bundled Node (node.exe)"
LangString OPENXYOS_NODE_MISSING ${LANG_SIMPCHINESE} "警告:未找到 bundled Node,首次启动将再试"
LangString OPENXYOS_NODE_MISSING ${LANG_ENGLISH} "Warning: bundled Node missing; first launch will retry"
LangString OPENXYOS_ZIP_MISSING ${LANG_SIMPCHINESE} "安装包未含独立 openXYOS zip;首次启动会从内置 portable 解压"
LangString OPENXYOS_ZIP_MISSING ${LANG_ENGLISH} "Installer has no separate openXYOS zip; first launch unpacks the bundled portable"
LangString OPENXYOS_STAGED_OK ${LANG_SIMPCHINESE} "已从 openxyos-runtime 展开完整运行树"
LangString OPENXYOS_STAGED_OK ${LANG_ENGLISH} "Expanded the full runtime from openxyos-runtime"
LangString OPENXYOS_EXTRACT_CODE ${LANG_SIMPCHINESE} "解压结束,退出码 "
LangString OPENXYOS_EXTRACT_CODE ${LANG_ENGLISH} "Extract finished, exit code "
LangString OPENXYOS_EXTRACT_FAIL ${LANG_SIMPCHINESE} "安装失败:未能解压完整的 openXYOS 运行环境(需要 node\\node.exe 与 dist\\index.html)。请重新下载安装包。"
LangString OPENXYOS_EXTRACT_FAIL ${LANG_ENGLISH} "Setup failed: openXYOS runtime is incomplete (need node\\node.exe and dist\\index.html). Download the installer again."
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?"

Expand Down
Loading
Loading