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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

### 修复

- 安装期不再在 livez 探测后 `taskkill` 边车:Setup 把 `start-sidecar.ps1` 写到 `%LOCALAPPDATA%\\FreeOS\\openxyos`,用当前用户(IShellDispatch2 / HKCU Run / 登录计划任务)常驻拉起 FE+BE,确认 `http://127.0.0.1:3780/api/health/livez` 后保持运行。组织页在 `.install-ready` 时自动嵌入本机 openXYOS,不再把「启动边车」当主按钮;仅自动启动失败才显示「重试启动」和中文原因。
- 「下载最新 openXYOS 源码」端到端 UTF-8:Wails 文件夹选择器以 UTF-8 Base64 回传路径(不再把 GBK/ACP 当 UTF-8);落盘前修复 CP1252/Latin-1 误读的目标路径;解压按 zip UTF-8 标志或 GBK 还原中文目录名,避免乱码。
- Windows 知识库本地文件夹挂接:选择器与列表预览按 UTF-8 处理中文路径/文件名,避免 `项目…` 这类乱码;挂接与列表预览不再因向量模型未就绪返回 409。

- 模型页注册本机 GGUF / 已拉取的 Ollama 模型时,Windows 桌面不再返回笼统的 `INTERNAL_ERROR` 500;`ollama create` 使用带引号的 POSIX `FROM` 路径,并把真实失败原因(文件不存在、权限、create 失败)返回给界面。

### 变更
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 @@ -54,6 +54,7 @@ export interface OrgOverview {
sidecar_reachable: boolean;
sidecar_url: string;
start_available: boolean;
install_ready: boolean;
start_command: string;
home: string;
last_sync: string | null;
Expand Down
15 changes: 9 additions & 6 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,15 @@
"timelineRemote": "Pushed to control plane",
"timelineGovernance": "High-risk tool blocked by governance",
"emptyTitle": "First look at Organization",
"emptyBody": "FreeOS on the left owns colleagues, experts / assistants not yet in a department, skills, MCP, and tasks. openXYOS on the right owns department employees, blueprints, modules, and approvals. Start the sidecar, or run the loop once to see a real cycle.",
"startSidecarTitle": "openXYOS sidecar is offline",
"startSidecarBody": "Live approvals and remote apply need the sidecar. Start it here; if no launcher is bundled, use the command beside the button.",
"startSidecarAction": "Start sidecar",
"sidecarStarted": "Sidecar is reachable",
"sidecarStartPending": "Start attempted; refresh in a moment",
"emptyBody": "FreeOS on the left owns colleagues, experts / assistants not yet in a department, skills, MCP, and tasks. openXYOS on the right owns department employees, blueprints, modules, and approvals. After Setup the local console opens on its own; you can also run the loop once to see a real cycle.",
"startSidecarTitle": "openXYOS did not start automatically",
"startSidecarBody": "The install-time local console is not reachable. The reason is below; click Retry after you fix it.",
"startSidecarAction": "Retry start",
"sidecarStarted": "Local openXYOS is ready",
"sidecarStartPending": "Connecting to local openXYOS…",
"sidecarRecoverHint": "Only needed when auto-start fails. A healthy install should never ask you to start the sidecar.",
"sidecarAutoStarting": "Connecting the install-time local openXYOS…",
"sidecarAutoStartFailed": "Auto-start failed: {{detail}}",
"bridgeHint": "FreeOS produces experts / assistants not yet in a department, plus skills, plugins, and MCPs. Those assets assemble into openXYOS as department employees; org blueprints and policies feed back as colleagues.",
"home": "Data home",
"sidecar": "Sidecar URL",
Expand Down
15 changes: 9 additions & 6 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,15 @@
"timelineRemote": "已推送到控制面",
"timelineGovernance": "高风险工具被治理拦截",
"emptyTitle": "第一次打开组织",
"emptyBody": "左侧 FreeOS 负责同事、尚未入部门的专家 / 智能助手、技能、MCP 与任务;右侧 openXYOS 负责部门员工、蓝图、模块与审批。先启动边车,或直接运行组织循环看一次真实闭环。",
"startSidecarTitle": "openXYOS 边车未运行",
"startSidecarBody": "控制面离线时无法做 live 审批与远程回流。可一键启动本机边车;若启动器不在当前环境,请按旁边的命令手动启动。",
"startSidecarAction": "启动边车",
"sidecarStarted": "边车已可达",
"sidecarStartPending": "已尝试启动,请稍候再刷新",
"emptyBody": "左侧 FreeOS 负责同事、尚未入部门的专家 / 智能助手、技能、MCP 与任务;右侧 openXYOS 负责部门员工、蓝图、模块与审批。安装完成后会自动打开本机 openXYOS;也可直接运行组织循环看一次真实闭环。",
"startSidecarTitle": "openXYOS 未能自动启动",
"startSidecarBody": "安装期本机控制台没有连上。下面是具体原因;修好后点「重试启动」。",
"startSidecarAction": "重试启动",
"sidecarStarted": "本机 openXYOS 已就绪",
"sidecarStartPending": "正在自动连接本机 openXYOS…",
"sidecarRecoverHint": "仅在自动启动失败时需要处理。健康安装不应再点「启动边车」。",
"sidecarAutoStarting": "正在自动连接安装期本机 openXYOS…",
"sidecarAutoStartFailed": "自动启动失败:{{detail}}",
"bridgeHint": "FreeOS 生产尚未入部门的专家 / 智能助手、技能、插件与 MCP;推入 openXYOS 后成为部门员工;组织蓝图与策略再回流,登记为本组织同事。",
"home": "数据目录",
"sidecar": "边车地址",
Expand Down
99 changes: 73 additions & 26 deletions dashboard/src/pages/Organization/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from "../../utils/desktopFolder";
import { message } from "../../utils/antdMessage";
import { resolveOpenxyosSourceDest } from "./pickSourceDest";
import { sidecarRecoverPhase } from "./sidecarRecover";
import styles from "./Organization.module.less";

type ActionKey = "assemble" | "pack" | "loop" | "sidecar" | "produce" | null;
Expand Down Expand Up @@ -66,6 +67,8 @@ export default function OrganizationPage() {
const [produceName, setProduceName] = useState("");
const [produceIma, setProduceIma] = useState("");
const [landed, setLanded] = useState<Record<string, unknown> | null>(null);
const [autoStartFailed, setAutoStartFailed] = useState(false);
const [recoverDetail, setRecoverDetail] = useState("");
const autoStartRef = useRef(false);
const iframeRef = useRef<HTMLIFrameElement | null>(null);

Expand Down Expand Up @@ -175,31 +178,49 @@ export default function OrganizationPage() {
}
};

const startSidecar = () =>
const startSidecar = (opts?: { silent?: boolean }) =>
runAction("sidecar", async () => {
setPreviewPending(true);
setAutoStartFailed(false);
try {
const result = await orgModuleApi.startSidecar();
setLastActionNotes([result.detail, result.command].filter(Boolean));
const next = result.reachable
? await applyOverview(true)
: await waitForSidecar();
message.success(
next?.sidecar_reachable
? t("organization.sidecarStarted")
: t("organization.sidecarStartPending"),
);
if (next?.sidecar_reachable) {
setRecoverDetail("");
if (!opts?.silent) {
message.success(t("organization.sidecarStarted"));
}
return;
}
const detail = result.detail || t("organization.sidecarStartPending");
setAutoStartFailed(true);
setRecoverDetail(detail);
message.error(t("organization.sidecarAutoStartFailed", { detail }));
} catch (err) {
const detail =
err instanceof Error ? err.message : t("organization.actionFailed");
setAutoStartFailed(true);
setRecoverDetail(detail);
message.error(t("organization.sidecarAutoStartFailed", { detail }));
} finally {
setPreviewPending(false);
}
});

const canSilentStart = Boolean(
overview?.start_available || overview?.install_ready,
);

useEffect(() => {
if (autoStartRef.current) return;
if (!overview?.start_available || overview.sidecar_reachable) return;
if (!overview || overview.sidecar_reachable) return;
if (!canSilentStart) return;
autoStartRef.current = true;
void startSidecar();
}, [overview?.start_available, overview?.sidecar_reachable]);
void startSidecar({ silent: true });
}, [canSilentStart, overview, overview?.sidecar_reachable]);

const toggleModule = async (key: string, enabled: boolean) => {
const next = { ...moduleToggles, [key]: enabled };
Expand Down Expand Up @@ -384,6 +405,13 @@ export default function OrganizationPage() {
)}`;
}, [overview?.sidecar_url, disabledKeys]);
const showFrame = Boolean(sidecarUp && previewUrl);
const recoverPhase = sidecarRecoverPhase({
sidecarUp,
installReady: Boolean(overview?.install_ready),
startAvailable: Boolean(overview?.start_available),
autoStarting: previewPending || busy === "sidecar",
autoStartFailed,
});

const pushTogglesToPreview = useCallback(() => {
const frame = iframeRef.current?.contentWindow;
Expand Down Expand Up @@ -459,17 +487,33 @@ export default function OrganizationPage() {
/>
</div>

{!sidecarUp && (
{recoverPhase === "starting" && !sidecarUp && (
<section className={styles.recover}>
<p className={styles.recoverTitle}>
{t("organization.sidecarAutoStarting")}
</p>
<p className={styles.recoverBody}>
{t("organization.sidecarStartPending")}
</p>
</section>
)}

{recoverPhase === "recover" && (
<section className={styles.recover}>
<p className={styles.recoverTitle}>
{t("organization.startSidecarTitle")}
</p>
<p className={styles.recoverBody}>
{t("organization.startSidecarBody")}
</p>
{recoverDetail ? (
<p className={styles.recoverBody}>{recoverDetail}</p>
) : null}
<p className={styles.recoverBody}>
{t("organization.sidecarRecoverHint")}
</p>
<Space wrap>
<Button
type="primary"
icon={<Play size={14} />}
loading={busy === "sidecar"}
disabled={!overview?.start_available && !overview}
Expand Down Expand Up @@ -882,26 +926,29 @@ export default function OrganizationPage() {
) : (
<div className={styles.previewEmpty}>
<p className={styles.recoverTitle}>
{previewPending
{recoverPhase === "starting" || previewPending
? t("organization.previewLoading")
: t("organization.startSidecarTitle")}
</p>
<p className={styles.recoverBody}>
{t("organization.previewOffline")}
{recoverPhase === "starting" || previewPending
? t("organization.sidecarAutoStarting")
: recoverDetail || t("organization.previewOffline")}
</p>
<Space wrap>
<Button
type="primary"
icon={<Play size={14} />}
loading={busy === "sidecar" || previewPending}
onClick={() => void startSidecar()}
>
{t("organization.startSidecarAction")}
</Button>
{overview?.start_command && (
<code>{overview.start_command}</code>
)}
</Space>
{recoverPhase === "recover" && (
<Space wrap>
<Button
icon={<Play size={14} />}
loading={busy === "sidecar" || previewPending}
onClick={() => void startSidecar()}
>
{t("organization.startSidecarAction")}
</Button>
{overview?.start_command && (
<code>{overview.start_command}</code>
)}
</Space>
)}
</div>
)}
</div>
Expand Down
52 changes: 52 additions & 0 deletions dashboard/src/pages/Organization/sidecarRecover.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import { sidecarRecoverPhase } from "./sidecarRecover";

describe("sidecarRecoverPhase", () => {
it("hides recover when the sidecar is already up", () => {
expect(
sidecarRecoverPhase({
sidecarUp: true,
installReady: true,
startAvailable: true,
autoStarting: false,
autoStartFailed: false,
}),
).toBe("hidden");
});

it("auto-starts silently after a healthy install instead of showing 启动边车", () => {
expect(
sidecarRecoverPhase({
sidecarUp: false,
installReady: true,
startAvailable: true,
autoStarting: false,
autoStartFailed: false,
}),
).toBe("starting");
});

it("shows recover only after auto-start fails", () => {
expect(
sidecarRecoverPhase({
sidecarUp: false,
installReady: true,
startAvailable: true,
autoStarting: false,
autoStartFailed: true,
}),
).toBe("recover");
});

it("shows recover when there is no install-time runtime to start", () => {
expect(
sidecarRecoverPhase({
sidecarUp: false,
installReady: false,
startAvailable: false,
autoStarting: false,
autoStartFailed: false,
}),
).toBe("recover");
});
});
15 changes: 15 additions & 0 deletions dashboard/src/pages/Organization/sidecarRecover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type SidecarRecoverPhase = "hidden" | "starting" | "recover";

export function sidecarRecoverPhase(input: {
sidecarUp: boolean;
installReady: boolean;
startAvailable: boolean;
autoStarting: boolean;
autoStartFailed: boolean;
}): SidecarRecoverPhase {
if (input.sidecarUp) return "hidden";
if (input.autoStarting) return "starting";
if (input.autoStartFailed) return "recover";
if (input.installReady || input.startAvailable) return "starting";
return "recover";
}
1 change: 1 addition & 0 deletions dashboard/src/utils/utf8Mojibake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function cp1252Byte(code: number): number | null {
/**
* Repair UTF-8 text that a Windows bridge exposed as Latin-1 / CP1252.
* Example: ``项目结构.png`` → ``项目结构.png``.

*/
export function repairUtf8Mojibake(text: string): string {
if (!text) return text;
Expand Down
20 changes: 15 additions & 5 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ Same precedence as the FreeOS CLI/server:
(and keeps `$INSTDIR\openxyos` / `$INSTDIR\openxyos-runtime` as backups).
Install fails if `node` or `dist` is missing, or if
`http://127.0.0.1:3780/api/health/livez` does not pass before Setup exits.
First FreeOS start only spawns the already-on-disk sidecar — no copy/unpack.
Setup then leaves a **user-level** sidecar running (HKCU Run
`FreeOS-openXYOS` + `start-sidecar.ps1`) so Organization embeds immediately
— no first-open copy and no 「启动边车」 click. First FreeOS start only
attaches to that live tree if the port is already healthy.
- Shell prefs → `{home}/desktop-settings.json`

## Windows install finish
Expand All @@ -46,8 +49,12 @@ and extracts it with Windows `tar.exe` (quoted paths, so `Program Files`
works) into the **live** workdir `%LOCALAPPDATA%\FreeOS\openxyos` during
Setup. `$INSTDIR\openxyos` and `$INSTDIR\openxyos-runtime` are sealed
backups only. Setup **aborts** if `node\node.exe` or `dist\index.html` is
missing, or if an install-time sidecar probe cannot reach
missing, or if the user-level sidecar cannot reach
`/api/health/livez` — a README-only tree is not a successful install.
The probe **does not** start Node as Administrator and **does not**
`taskkill` it afterwards (that left Organization asking to 启动边车).
PersistOpenXYOS launches `start-sidecar.ps1` with the explorer token,
registers HKCU Run `FreeOS-openXYOS`, and leaves the process up.

Why LocalAppData, written by an elevated installer: `$INSTDIR` is typically
`C:\Program Files\FreeOS`, which a later unelevated `FreeOS.exe` cannot
Expand Down Expand Up @@ -78,7 +85,8 @@ rather than cutting a new tag.
created next to the exe, extracted portable / org-sidecar leftovers if they
were written under `$INSTDIR`, and any other installer-owned tree there
- Start Menu, Desktop, and Startup shortcuts created by the installer
- Add/Remove Programs registry key and the autostart Run value
- Add/Remove Programs registry key and the autostart Run values
(`FreeOS` / `FreeOS-openXYOS`) plus the `FreeOS-openXYOS` logon task
- Program-owned WebView2 / Wails cache under `%AppData%\FreeOS.exe`,
`%LOCALAPPDATA%\FreeOS.exe.WebView2`, and `%LOCALAPPDATA%\FreeOS`
(including `WebView2\`)
Expand All @@ -104,8 +112,10 @@ On first open the shell:
2. If that live workdir is already complete, skips any copy/unpack. A
copy from `$INSTDIR` / `portable/org-sidecar` is heal-only (other
Windows users, or a tree that Setup never populated).
3. Starts the openXYOS sidecar on `http://127.0.0.1:3780` from the live
workdir and waits until `/api/health/livez` responds.
3. If `http://127.0.0.1:3780/api/health/livez` is already up (install-time
persist / HKCU Run), attaches to it. Otherwise starts the sidecar from
the live workdir and waits until livez responds. Closing FreeOS does
**not** kill that user-level sidecar.
4. Starts the FreeOS host with `FREEOS_HOME`, `FREEOS_ORG_ENABLE=1`, and
`FREEOS_OPENXYOS_HOME`.
5. Opens the desktop window on the host UI with a local guest session (no
Expand Down
Loading
Loading