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 @@ -13,6 +13,7 @@
- 组织页互生长通道两端都落到可选用状态:推送到 openXYOS 时员工/人才写成列表页能看到的 `active` / `internal` / `available`;从 openXYOS 回流时即使没有技能字段也会登记为同事,技能/插件/MCP 写入 `org-skills` / `org-plugins` / `org-mcps`,并把聊天专家挂到当前用户(修复 `user_id=NULL` 导致「专家」页看不见)。导入后跳到「专家」页,推送/循环后关闭管理抽屉并刷新预览。
- 组织页内嵌浏览器补上后退 / 前进 / 刷新;openXYOS 首页卡通助手改为打包进前端资源(并保留 `/assets/xyai-mascot.webp` 回退),不再显示破碎图片。
- 启动 FreeOS 时就会拉起本机 openXYOS 前后端(桌面 `startOrgSidecar` + 主机 `ensure_sidecar`),不必先进入组织页;登录不再因为 3780 未监听而 `Failed to fetch`。
- 组织页打开本机测试页会先探测 `livez`:前后端未起来时不再把登录 iframe 暴露成 `Failed to fetch`,而是提示点击「重启前后端服务」,并高亮状态栏重启按钮。重启过程改为 XYAI Studio 风格进度卡(吉祥物对话气泡、转圈、逐步推进的停进程 / 本机完整目录 / 启动 API 与页面 / 等待端口 / 加载登录页),并真实停启 FreeOS openXYOS 节点。
- 组织页「最近同步」左侧增加「重启 openXYOS 前后端服务」:XYAI 吉祥物动画、真实停启进程,livez 通过后回到本机测试首页。

- 组织页「把 FreeOS 资产推到 openXYOS」不再把行写进 tenant 1 却让嵌入式登录(`demo@demo.com` → tenant 2)看不见。ingest 以当前登录 / 最近登录 / 本地演示租户为准,并把员工/人才写成列表页会显示的状态;打包回执带落地数量,预览跳到员工页。
Expand Down
4 changes: 4 additions & 0 deletions dashboard/src/api/modules/orgModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ export const orgModuleApi = {
request<OrgSidecarStart>("/org-module/sidecar/start", { method: "POST" }),
restartSidecar: () =>
request<OrgSidecarStart>("/org-module/sidecar/restart", { method: "POST" }),
probeLivez: () =>
request<{ reachable: boolean; url: string; detail: string }>(
"/org-module/sidecar/livez",
),
assemble: () =>
request<OrgAssembleResult>("/org-module/assemble", { method: "POST" }),
produce: (body: {
Expand Down
8 changes: 8 additions & 0 deletions dashboard/src/layouts/sidebarNav.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ describe("sidebarNav", () => {
expect(zh.organization.downloadSourceBar).toBe("下载最新源码");
expect(zh.organization.downloadSource).toBe("下载最新 openXYOS 源码");
expect(zh.organization.restartSidecar).toBe("重启 openXYOS 前后端服务");
expect(zh.organization.restartSidecarCta).toBe("重启前后端服务");
expect(zh.organization.restartOverlayTitle).toBe("正在重启前后端服务");
expect(zh.organization.browserReload).toBe("刷新");
expect(en.organization.enableModule).toBe("Enable organization module");
expect(en.organization.manageOs).toBe("Organization OS");
Expand All @@ -94,6 +96,12 @@ describe("sidebarNav", () => {
expect(en.organization.restartSidecar).toBe(
"Restart openXYOS frontend and backend",
);
expect(en.organization.restartSidecarCta).toBe(
"Restart frontend and backend",
);
expect(en.organization.restartOverlayTitle).toBe(
"Restarting frontend and backend",
);
expect(en.organization.browserReload).toBe("Reload");
expect(zh.chat.modelPickerManage).toBe("模型管理");
expect(en.chat.modelPickerManage).toBe("Model management");
Expand Down
26 changes: 26 additions & 0 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,34 @@
"sidecarOpening": "Opening local openXYOS",
"lastSync": "Last sync",
"restartSidecar": "Restart openXYOS frontend and backend",
"restartSidecarCta": "Restart frontend and backend",
"restartSidecarHint": "Stopping and starting the local services, then returning to the test home page.",
"restartingSidecar": "Restarting openXYOS frontend and backend…",
"restartSidecarDone": "Local openXYOS restarted",
"restartSidecarFailed": "Restart failed: {{detail}}",
"restartOverlayTitle": "Restarting frontend and backend",
"restartWaitHint": "Please wait — the little helper is working…",
"restartStep": {
"stop": "Stopping the old openXYOS process. Please wait.",
"preferLayout": "Prefer the complete local source tree so frontend and backend can start.",
"startApi": "Starting the API and page server. This usually takes a few seconds.",
"waitPort": "Waiting for the local port to be ready. Do not close this window.",
"loadLogin": "Frontend is ready. Loading the sign-in page."
},
"restartSpeech": {
"stopTitle": "I’ll stop the old service first~",
"stopBody": "Stopping the old openXYOS process. Please wait.",
"preferLayoutTitle": "Using the complete local tree~",
"preferLayoutBody": "Prefer the complete local source tree so frontend and backend can start.",
"startApiTitle": "Bringing frontend and backend up~",
"startApiBody": "Starting the API and page server. This usually takes a few seconds.",
"waitPortTitle": "The local port is almost ready~",
"waitPortBody": "Waiting for the local port to be ready. Do not close this window.",
"loadLoginTitle": "You can sign in now~",
"loadLoginBody": "Frontend is ready. Loading the sign-in page.",
"needsRestartTitle": "Local frontend and backend are down~",
"needsRestartBody": "Opening the test page now would fail with Failed to fetch. Click Restart frontend and backend first."
},
"neverSynced": "Not synced yet",
"dataPlane": "Data plane",
"controlPlane": "Control plane",
Expand Down Expand Up @@ -116,6 +140,8 @@
"disabled": "Organization module disabled",
"previewTitle": "openXYOS preview",
"previewOffline": "Opening local openXYOS (http://127.0.0.1:3780)…",
"previewNeedsRestartTitle": "Local openXYOS is not ready",
"previewNeedsRestart": "Local frontend and backend are down, so sign-in would fail with Failed to fetch. Click Restart frontend and backend first.",
"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",
Expand Down
26 changes: 26 additions & 0 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,34 @@
"sidecarOpening": "正在打开本机 openXYOS",
"lastSync": "最近同步",
"restartSidecar": "重启 openXYOS 前后端服务",
"restartSidecarCta": "重启前后端服务",
"restartSidecarHint": "正在停止并重新拉起本机前后端,完成后回到测试首页。",
"restartingSidecar": "正在重启 openXYOS 前后端服务…",
"restartSidecarDone": "本机 openXYOS 已重启",
"restartSidecarFailed": "重启失败:{{detail}}",
"restartOverlayTitle": "正在重启前后端服务",
"restartWaitHint": "请耐心等待,小精灵正在干活中...",
"restartStep": {
"stop": "正在停止旧的 openXYOS 进程,请稍候。",
"preferLayout": "会优先使用本机完整源码目录,好启动前后端。",
"startApi": "正在启动 API 与页面服务,通常需要几十秒。",
"waitPort": "正在等待本机端口就绪,不要关闭窗口。",
"loadLogin": "前端已就绪,正在加载可登录注册的页面。"
},
"restartSpeech": {
"stopTitle": "我先帮你把旧服务停掉~",
"stopBody": "正在停止旧的 openXYOS 进程,请稍候。",
"preferLayoutTitle": "先用本机完整目录来启动~",
"preferLayoutBody": "会优先使用本机完整源码目录,好启动前后端。",
"startApiTitle": "正在把前后端拉起来~",
"startApiBody": "正在启动 API 与页面服务,通常需要几十秒。",
"waitPortTitle": "本机端口马上就好~",
"waitPortBody": "正在等待本机端口就绪,不要关闭窗口。",
"loadLoginTitle": "可以登录啦~",
"loadLoginBody": "前端已就绪,正在加载可登录注册的页面。",
"needsRestartTitle": "本机前后端还没起来~",
"needsRestartBody": "直接打开测试页会 Failed to fetch。请先点击「重启前后端服务」。"
},
"neverSynced": "尚未同步",
"dataPlane": "数据面",
"controlPlane": "控制面",
Expand Down Expand Up @@ -116,6 +140,8 @@
"disabled": "组织模块已关闭",
"previewTitle": "openXYOS 预览",
"previewOffline": "正在打开本机 openXYOS(http://127.0.0.1:3780)…",
"previewNeedsRestartTitle": "本机 openXYOS 尚未就绪",
"previewNeedsRestart": "本机前后端还没起来,登录页会 Failed to fetch。请先点击「重启前后端服务」。",
"previewBlank": "本机 openXYOS 健康检查已通过,但预览页是空白的:页面自己的地址(http://127.0.0.1:3780)被跨域拦截,脚本和样式没有加载。请完全退出并重新打开 FreeOS,用当前白名单重启本机服务。",
"previewLoading": "正在打开本机 openXYOS…",
"fullscreen": "全屏",
Expand Down
29 changes: 24 additions & 5 deletions dashboard/src/pages/Organization/OrgMiniBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
iframeSandboxFor,
type OrgBrowserTab,
} from "./orgBrowser";
import { shouldLoadSidecarFrame } from "./sidecarLivez";
import styles from "./Organization.module.less";

type OrgMiniBrowserProps = {
Expand All @@ -20,7 +21,9 @@ type OrgMiniBrowserProps = {
disabledKeys: string[];
previewNonce: string;
sidecarUp: boolean;
livezOk: boolean;
previewBlank: boolean;
recoverPhase: "hidden" | "opening" | "needsRestart";
iframeRefs: MutableRefObject<Record<string, HTMLIFrameElement | null>>;
onAddressChange: (value: string) => void;
onAddressSubmit: () => void;
Expand All @@ -41,7 +44,9 @@ export default function OrgMiniBrowser({
disabledKeys,
previewNonce,
sidecarUp,
livezOk,
previewBlank,
recoverPhase,
iframeRefs,
onAddressChange,
onAddressSubmit,
Expand Down Expand Up @@ -89,6 +94,7 @@ export default function OrgMiniBrowser({
<Tooltip title={t("organization.browserBack")}>
<Button
data-testid="org-browser-back"
htmlType="button"
aria-label={t("organization.browserBack")}
icon={<ArrowLeft size={14} />}
disabled={!canGoBack(activeTab)}
Expand All @@ -98,6 +104,7 @@ export default function OrgMiniBrowser({
<Tooltip title={t("organization.browserForward")}>
<Button
data-testid="org-browser-forward"
htmlType="button"
aria-label={t("organization.browserForward")}
icon={<ArrowRight size={14} />}
disabled={!canGoForward(activeTab)}
Expand All @@ -107,6 +114,7 @@ export default function OrgMiniBrowser({
<Tooltip title={t("organization.browserReload")}>
<Button
data-testid="org-browser-reload"
htmlType="button"
aria-label={t("organization.browserReload")}
icon={<RotateCcw size={14} />}
onClick={onReload}
Expand All @@ -124,7 +132,7 @@ export default function OrgMiniBrowser({
</Button>
</form>
<div className={styles.embedPane}>
{!sidecarUp && (
{recoverPhase === "opening" && (
<div className={styles.previewOffline}>
{t("organization.previewOffline")}
</div>
Expand All @@ -135,11 +143,18 @@ export default function OrgMiniBrowser({
</div>
)}
{tabs.map((tab) => {
const src = buildEmbedSrc(tab.srcUrl, {
const allowFrame = shouldLoadSidecarFrame({
url: tab.srcUrl,
sidecarOrigin,
disabledKeys,
nonce: previewNonce,
livezOk: livezOk && sidecarUp,
});
const src = allowFrame
? buildEmbedSrc(tab.srcUrl, {
sidecarOrigin,
disabledKeys,
nonce: previewNonce,
})
: "about:blank";
const active = tab.id === activeId;
return (
<iframe
Expand All @@ -155,7 +170,11 @@ export default function OrgMiniBrowser({
data-testid="org-browser-frame"
data-active={active ? "true" : "false"}
data-tab-id={tab.id}
sandbox={iframeSandboxFor(src, sidecarOrigin)}
sandbox={
allowFrame
? iframeSandboxFor(src, sidecarOrigin)
: "allow-same-origin"
}
allow="clipboard-read; clipboard-write"
onLoad={onFrameLoad}
/>
Expand Down
129 changes: 129 additions & 0 deletions dashboard/src/pages/Organization/OrgRestartOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useEffect, useState } from "react";
import { Button } from "antd";
import { useTranslation } from "react-i18next";
import { xyaiMascotSrc } from "../../assets/mascot";
import {
SIDECAR_RESTART_STEPS,
sidecarRestartSpeechKeys,
sidecarRestartStepId,
sidecarRestartStepIndex,
sidecarRestartStepLabelKey,
} from "./sidecarRestart";
import styles from "./Organization.module.less";

type OrgRestartOverlayProps = {
mode: "restarting" | "needsRestart";
finished?: boolean;
onRestart: () => void;
};

export default function OrgRestartOverlay({
mode,
finished = false,
onRestart,
}: OrgRestartOverlayProps) {
const { t } = useTranslation();
const [elapsedMs, setElapsedMs] = useState(0);

useEffect(() => {
if (mode !== "restarting") {
setElapsedMs(0);
return;
}
const started = Date.now();
const id = window.setInterval(() => {
setElapsedMs(Date.now() - started);
}, 200);
return () => window.clearInterval(id);
}, [mode]);

const stepIndex = sidecarRestartStepIndex(elapsedMs, { finished });
const stepId = sidecarRestartStepId(elapsedMs, { finished });
const speech =
mode === "needsRestart"
? {
title: "organization.restartSpeech.needsRestartTitle",
body: "organization.restartSpeech.needsRestartBody",
}
: sidecarRestartSpeechKeys(stepId);

return (
<div
className={styles.restartOverlay}
data-testid={
mode === "restarting" ? "org-restart-overlay" : "org-sidecar-gate"
}
role="status"
aria-live="polite"
aria-busy={mode === "restarting"}
>
<div className={styles.restartCard}>
<div className={styles.restartHero}>
<img
className={styles.restartMascot}
src={xyaiMascotSrc("work")}
alt=""
aria-hidden
draggable={false}
/>
<div className={styles.restartSpeech}>
<p className={styles.restartSpeechTitle}>{t(speech.title)}</p>
<p className={styles.restartSpeechBody}>{t(speech.body)}</p>
{mode === "restarting" ? (
<p className={styles.restartSpeechWait}>
{t("organization.restartWaitHint")}
</p>
) : null}
</div>
</div>

{mode === "restarting" ? (
<>
<div className={styles.restartSpinner} aria-hidden />
<h2 className={styles.restartTitle}>
{t("organization.restartOverlayTitle")}
</h2>
<ol className={styles.restartSteps}>
{SIDECAR_RESTART_STEPS.map((id, index) => {
const state =
index < stepIndex
? "done"
: index === stepIndex
? "current"
: "pending";
return (
<li
key={id}
className={styles.restartStep}
data-state={state}
data-testid="org-restart-step"
data-step={id}
>
<span className={styles.restartStepDot} aria-hidden />
<span>{t(sidecarRestartStepLabelKey(id))}</span>
</li>
);
})}
</ol>
</>
) : (
<div className={styles.restartGate}>
<h2 className={styles.restartTitle}>
{t("organization.previewNeedsRestartTitle")}
</h2>
<p className={styles.restartGateBody}>
{t("organization.previewNeedsRestart")}
</p>
<Button
type="primary"
data-testid="org-sidecar-gate-restart"
onClick={onRestart}
>
{t("organization.restartSidecarCta")}
</Button>
</div>
)}
</div>
</div>
);
}
Loading
Loading