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 @@

### 修复

- 组织页嵌入本机 openXYOS 不再因为边车「已连接」却空白:`CORS_ORIGIN` 即使漏了 `http://127.0.0.1:3780` / `localhost` / `[::1]`,服务端也会合并监听自源,模块脚本和登录不再 500。FreeOS 启动会先停掉 live 目录里旧的 Node,再用当前 `start-sidecar.ps1` 拉起;嵌套的 `openxyos\dist` 会在启动前提升到 live 根。若 livez 正常但自源请求失败,状态栏和预览区会明确提示,而不是一片白。
- 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 未启动时给出可操作错误,而不是静默失败。
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/api/modules/orgModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface OrgControlPlane {
export interface OrgOverview {
enabled: boolean;
sidecar_reachable: boolean;
sidecar_embed_ok?: boolean;
sidecar_url: string;
start_available: boolean;
install_ready: boolean;
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"on": "Enabled",
"off": "Disabled",
"sidecarUp": "Local openXYOS connected",
"sidecarEmbedFailed": "Preview failed to load",
"sidecarDown": "Opening local openXYOS",
"sidecarOpening": "Opening local openXYOS",
"lastSync": "Last sync",
Expand Down Expand Up @@ -103,6 +104,7 @@
"disabled": "Organization module disabled",
"previewTitle": "openXYOS preview",
"previewOffline": "Opening local openXYOS (http://127.0.0.1:3780)…",
"previewBlank": "Local openXYOS is healthy, but the preview stayed blank: requests from its own origin (http://127.0.0.1:3780) were blocked by CORS, so scripts and styles never loaded. Fully quit and reopen FreeOS so the sidecar restarts with the current allow-list.",
"previewLoading": "Opening local openXYOS…",
"fullscreen": "Fullscreen",
"exitFullscreen": "Exit fullscreen",
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"on": "已启用",
"off": "已关闭",
"sidecarUp": "本机 openXYOS 已连接",
"sidecarEmbedFailed": "预览页未能加载",
"sidecarDown": "正在打开本机 openXYOS",
"sidecarOpening": "正在打开本机 openXYOS",
"lastSync": "最近同步",
Expand Down Expand Up @@ -103,6 +104,7 @@
"disabled": "组织模块已关闭",
"previewTitle": "openXYOS 预览",
"previewOffline": "正在打开本机 openXYOS(http://127.0.0.1:3780)…",
"previewBlank": "本机 openXYOS 健康检查已通过,但预览页是空白的:页面自己的地址(http://127.0.0.1:3780)被跨域拦截,脚本和样式没有加载。请完全退出并重新打开 FreeOS,用当前白名单重启本机服务。",
"previewLoading": "正在打开本机 openXYOS…",
"fullscreen": "全屏",
"exitFullscreen": "退出全屏",
Expand Down
15 changes: 15 additions & 0 deletions dashboard/src/pages/Organization/Organization.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@
pointer-events: none;
}

.previewBlank {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 32px 40px;
max-width: none;
text-align: center;
line-height: 1.6;
color: var(--fn-text-primary);
background: rgba(255, 255, 255, 0.94);
}

.drawerBody {
display: flex;
flex-direction: column;
Expand Down
18 changes: 17 additions & 1 deletion dashboard/src/pages/Organization/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import OrganizationPage from "./index";
import type { OrgOverview } from "../../api/modules/orgModule";
import { orgModuleApi, type OrgOverview } from "../../api/modules/orgModule";

const overview: OrgOverview = {
enabled: true,
sidecar_reachable: true,
sidecar_embed_ok: true,
sidecar_url: "http://127.0.0.1:3780",
start_available: false,
install_ready: true,
Expand Down Expand Up @@ -92,5 +93,20 @@ describe("OrganizationPage", () => {
expect(
screen.getAllByText("organization.downloadSource").length,
).toBeGreaterThan(0);
expect(screen.queryByTestId("org-preview-blank")).toBeNull();
});

it("surfaces a blank-preview error when livez is up but embed origin fails", async () => {
vi.mocked(orgModuleApi.overview).mockResolvedValue({
...overview,
sidecar_embed_ok: false,
});
render(<OrganizationPage />);
expect(await screen.findByTestId("org-preview-blank")).toHaveTextContent(
"organization.previewBlank",
);
expect(
screen.getByText("organization.sidecarEmbedFailed"),
).toBeInTheDocument();
});
});
32 changes: 28 additions & 4 deletions dashboard/src/pages/Organization/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from "../../utils/desktopFolder";
import { message } from "../../utils/antdMessage";
import { resolveOpenxyosSourceDest } from "./pickSourceDest";
import { shouldShowPreviewBlank } from "./sidecarRecover";
import styles from "./Organization.module.less";

type ActionKey = "assemble" | "pack" | "loop" | "sidecar" | "produce" | null;
Expand Down Expand Up @@ -110,6 +111,11 @@ export default function OrganizationPage() {
}, [applyOverview, overview?.sidecar_reachable]);

const sidecarUp = Boolean(overview?.sidecar_reachable);
const embedOk = overview?.sidecar_embed_ok;
const previewBlank = shouldShowPreviewBlank({
sidecarUp,
embedOk,
});
const firstRun = useMemo(() => {
if (!overview) return false;
return (
Expand Down Expand Up @@ -186,11 +192,19 @@ export default function OrganizationPage() {

useEffect(() => {
if (autoStartRef.current) return;
if (!overview || overview.sidecar_reachable) return;
if (!canSilentStart) return;
if (!overview) return;
const embedBroken =
overview.sidecar_reachable && overview.sidecar_embed_ok === false;
if (overview.sidecar_reachable && !embedBroken) return;
if (!embedBroken && !canSilentStart) return;
autoStartRef.current = true;
void startSidecar();
}, [canSilentStart, overview, overview?.sidecar_reachable]);
}, [
canSilentStart,
overview,
overview?.sidecar_reachable,
overview?.sidecar_embed_ok,
]);

const toggleModule = async (key: string, enabled: boolean) => {
const next = { ...moduleToggles, [key]: enabled };
Expand Down Expand Up @@ -404,7 +418,9 @@ export default function OrganizationPage() {
{overview?.enabled ? t("organization.on") : t("organization.off")}
</span>
<span className={styles.chip}>
{sidecarUp
{previewBlank
? t("organization.sidecarEmbedFailed")
: sidecarUp
? t("organization.sidecarUp")
: t("organization.sidecarOpening")}
</span>
Expand Down Expand Up @@ -449,6 +465,14 @@ export default function OrganizationPage() {
{t("organization.previewOffline")}
</div>
)}
{previewBlank && (
<div
className={styles.previewBlank}
data-testid="org-preview-blank"
>
{t("organization.previewBlank")}
</div>
)}
<iframe
key={sidecarUp ? "open" : "opening"}
ref={iframeRef}
Expand Down
22 changes: 21 additions & 1 deletion dashboard/src/pages/Organization/sidecarRecover.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { sidecarRecoverPhase } from "./sidecarRecover";
import { shouldShowPreviewBlank, sidecarRecoverPhase } from "./sidecarRecover";

describe("sidecarRecoverPhase", () => {
it("hides overlay when the local console is already up", () => {
Expand Down Expand Up @@ -44,3 +44,23 @@ describe("sidecarRecoverPhase", () => {
).toBe("opening");
});
});

describe("shouldShowPreviewBlank", () => {
it("stays quiet when livez is down or embed origin is allowed", () => {
expect(shouldShowPreviewBlank({ sidecarUp: false, embedOk: false })).toBe(
false,
);
expect(shouldShowPreviewBlank({ sidecarUp: true, embedOk: true })).toBe(
false,
);
expect(shouldShowPreviewBlank({ sidecarUp: true, embedOk: null })).toBe(
false,
);
});

it("surfaces a blank-preview error when livez is up but self-origin fails", () => {
expect(shouldShowPreviewBlank({ sidecarUp: true, embedOk: false })).toBe(
true,
);
});
});
8 changes: 8 additions & 0 deletions dashboard/src/pages/Organization/sidecarRecover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ export function sidecarRecoverPhase(input: {
if (input.sidecarUp) return "hidden";
return "opening";
}

/** livez can be green while iframe assets 500 on the sidecar's own Origin. */
export function shouldShowPreviewBlank(input: {
sidecarUp: boolean;
embedOk: boolean | null | undefined;
}): boolean {
return Boolean(input.sidecarUp) && input.embedOk === false;
}
108 changes: 106 additions & 2 deletions desktop/src/build/windows/nsis/start-sidecar.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Internal helper: start local openXYOS (FE+BE) at medium integrity.
# Used by the install-time provisioner and by explorers that open this file.
# Used by the install-time provisioner and by FreeOS launch / Organization.
# Always stop a previous live-dir Node first so a day-old process cannot
# keep a stale CORS_ORIGIN (missing http://127.0.0.1:3780) and blank the iframe.
# Never start Node while this process is elevated (High IL).
$ErrorActionPreference = 'Continue'
$live = Split-Path -Parent $MyInvocation.MyCommand.Path
Expand All @@ -14,7 +16,109 @@ function Test-Livez {
}
}

if (Test-Livez) { exit 0 }
function Test-OpenXYOSLayout {
param([string]$Root)
$node = Join-Path $Root 'node\node.exe'
if (-not (Test-Path -LiteralPath $node)) { return $false }
$nested = Join-Path $Root 'openxyos\dist\index.html'
$flat = Join-Path $Root 'dist\index.html'
return (Test-Path -LiteralPath $nested) -or (Test-Path -LiteralPath $flat)
}

function Copy-OpenXYOSTree {
param([string]$Src, [string]$Dest)
New-Item -ItemType Directory -Force -Path $Dest | Out-Null
$xcopy = Join-Path $env:SystemRoot 'System32\xcopy.exe'
if (Test-Path -LiteralPath $xcopy) {
& $xcopy /E /I /Y "$Src\*" "$Dest\" | Out-Null
return
}
Copy-Item -Path (Join-Path $Src '*') -Destination $Dest -Recurse -Force
}

function Find-OpenXYOSBundleRoot {
param([string]$Root)
if (Test-OpenXYOSLayout $Root) { return $Root }
$candidates = New-Object System.Collections.Generic.List[string]
foreach ($name in @('openxyos', 'org-sidecar')) {
$candidates.Add((Join-Path $Root $name))
}
if (Test-Path -LiteralPath $Root) {
Get-ChildItem -LiteralPath $Root -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$candidates.Add($_.FullName)
$candidates.Add((Join-Path $_.FullName 'org-sidecar'))
$candidates.Add((Join-Path $_.FullName 'openxyos'))
}
}
foreach ($cand in $candidates) {
if (Test-OpenXYOSLayout $cand) { return $cand }
}
return $null
}

function Repair-OpenXYOSLayout {
param([string]$Root)
$nestedFe = Join-Path $Root 'openxyos\dist\index.html'
$flatFe = Join-Path $Root 'dist\index.html'
$nestedBe = Join-Path $Root 'openxyos\backend-dist\server.js'
$flatBe = Join-Path $Root 'backend-dist\server.js'
if ((Test-Path -LiteralPath $nestedFe) -and -not (Test-Path -LiteralPath $flatFe)) {
Copy-OpenXYOSTree (Join-Path $Root 'openxyos') $Root
} elseif ((Test-Path -LiteralPath $nestedBe) -and -not (Test-Path -LiteralPath $flatBe)) {
Copy-OpenXYOSTree (Join-Path $Root 'openxyos') $Root
}
if (Test-OpenXYOSLayout $Root) { return $true }
$found = Find-OpenXYOSBundleRoot $Root
if (-not $found) { return $false }
$fullFound = [IO.Path]::GetFullPath($found)
$fullRoot = [IO.Path]::GetFullPath($Root)
if ($fullFound -eq $fullRoot) { return $true }
Copy-OpenXYOSTree $found $Root
return (Test-OpenXYOSLayout $Root)
}

function Stop-OpenXYOSNode {
param([string]$Root)
$pidFile = Join-Path $Root 'start.pid'
if (Test-Path -LiteralPath $pidFile) {
$raw = (Get-Content -LiteralPath $pidFile -Raw -ErrorAction SilentlyContinue)
$nid = 0
if ($null -ne $raw -and [int]::TryParse($raw.Trim(), [ref]$nid) -and $nid -gt 0) {
Stop-Process -Id $nid -Force -ErrorAction SilentlyContinue
}
Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue
}
$nodeExe = Join-Path $Root 'node\node.exe'
$want = ''
if (Test-Path -LiteralPath $nodeExe) {
try { $want = [IO.Path]::GetFullPath($nodeExe) } catch { $want = $nodeExe }
}
$fullRoot = ''
try { $fullRoot = [IO.Path]::GetFullPath($Root) } catch { $fullRoot = $Root }
Get-CimInstance Win32_Process -Filter "Name = 'node.exe'" -ErrorAction SilentlyContinue |
Where-Object {
if ($want -and $_.ExecutablePath) {
try {
return ([IO.Path]::GetFullPath($_.ExecutablePath) -eq $want)
} catch {
}
}
if ($fullRoot -and $_.CommandLine) {
return ($_.CommandLine -like "*$fullRoot*")
}
return $false
} |
ForEach-Object {
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
}
}

$null = Repair-OpenXYOSLayout $live
Stop-OpenXYOSNode $live
for ($i = 0; $i -lt 8; $i++) {
if (-not (Test-Livez)) { break }
Start-Sleep -Milliseconds 250
}

function Test-IsElevated {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
Expand Down
Loading
Loading