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

### 修复

- 组织页「把 FreeOS 资产推到 openXYOS」不再把行写进 tenant 1 却让嵌入式登录(`demo@demo.com` → tenant 2)看不见。ingest 以当前登录 / 最近登录 / 本地演示租户为准,并把员工/人才写成列表页会显示的状态;打包回执带落地数量,预览跳到员工页。
- 组织页「运行自增长循环」在员工已是 `active`(或生命周期不允许再跳到 `market`)时不再 500。循环只沿合法边晋升,已到达或非法的步骤记为 skip,接口返回成功的循环证明而不是 `INTERNAL_ERROR`。
- Windows 覆盖安装时,openXYOS 预配不再因为旧 Node 锁住 `%LOCALAPPDATA%\FreeOS\openxyos` 里的文件、`tar.exe` 返回非 0 就直接退出码 3。安装前只停止 FreeOS 自己的 openXYOS(与启动脚本相同的 `Stop-OpenXYOSNode`:`start.pid` / live 与 `$INSTDIR\openxyos` 路径),先解到 LocalAppData 临时目录再合入工作目录,并把 tar 的 stdout/stderr 写入 `provision.log`。tar 非 0 但布局可修复时记警告并继续;只有 extract+heal 之后布局仍不完整才退出 3。幂等短路要求完整布局、`.install-ready`,以及 3780 上是我们的 FreeOS openXYOS,而不是任意 livez。
- 组织页嵌入本机 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` 不受影响。
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/api/modules/orgModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export interface OrgPackResult {
mirrored: boolean;
notes: string[];
landed?: Record<string, unknown>;
tenant_id?: number | null;
preview_path?: string;
};
}

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 @@ -123,7 +123,9 @@
"receiptTitle": "What just happened",
"assembleReceipt": "Imported {{count}} employee(s) assigned to a department. In group chat call them colleagues, not experts.",
"assembleNext": "Next: open Conversations and invite colleagues to a group. Unassigned helpers stay on Experts.",
"packReceipt": "Packed and tried to push to openXYOS. Details below."
"packReceipt": "Packed and pushed to the signed-in openXYOS tenant.",
"packLanded": "Signed-in tenant {{tenant}}: employees {{employees}}, talent {{talent}}, skills {{skills}}, plugins {{plugins}}",
"packPreviewEmployees": "The preview now opens Employees so you can confirm the result."
},
"systemSettings": {
"subtitle": "Workbench, remote desktop, and host administration in one place."
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 @@ -123,7 +123,9 @@
"receiptTitle": "刚才的结果",
"assembleReceipt": "已导入 {{count}} 名员工(已入编部门)。群聊里请称他们为同事,不要叫专家。",
"assembleNext": "下一步:打开「对话」建群,把同事拉进来。还没入编的帮手在「专家」页。",
"packReceipt": "已打包并尝试推送到 openXYOS。下面是落地说明。"
"packReceipt": "已打包并推送到当前登录的 openXYOS 租户。",
"packLanded": "当前登录租户 {{tenant}}:员工 {{employees}},人才 {{talent}},技能 {{skills}},插件 {{plugins}}",
"packPreviewEmployees": "右侧预览已打开员工页,可直接核对。"
},
"systemSettings": {
"subtitle": "工作台、远程桌面与主机管理集中在这里。"
Expand Down
54 changes: 54 additions & 0 deletions dashboard/src/pages/Organization/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,58 @@ describe("OrganizationPage", () => {
screen.getByText("organization.sidecarEmbedFailed"),
).toBeInTheDocument();
});

it("shows pack landed counts and opens the employees preview", async () => {
vi.mocked(orgModuleApi.overview).mockResolvedValue(overview);
vi.mocked(orgModuleApi.pack).mockResolvedValue({
pack: {
directory: "/tmp/pack",
skill_count: 12,
plugin_count: 11,
mcp_count: 1,
agent_count: 2,
notes: [],
},
applied: {
pack_dir: "/tmp/pack",
mirror_dir: "/tmp/mirror",
remote_applied: true,
mirrored: true,
notes: ["control plane accepted the FreeOS ingest"],
tenant_id: 2,
preview_path: "/employees",
landed: {
tenant_id: 2,
preview: "/employees",
landed: {
employees: { created: 2, updated: 0 },
talent: { created: 2, updated: 0 },
plugins: { created: 11, updated: 0 },
skills: { created: 12, updated: 0 },
mcp: { created: 1, updated: 0 },
},
},
},
});

render(<OrganizationPage />);
fireEvent.click(await screen.findByTestId("org-manage-os"));
fireEvent.click(screen.getByText("organization.packAction"));

expect(await screen.findByTestId("org-last-receipt")).toHaveTextContent(
"organization.packReceipt",
);
expect(screen.getByTestId("org-last-receipt")).toHaveTextContent(
"organization.packLanded",
);
expect(screen.getByTestId("org-last-receipt")).toHaveTextContent(
"organization.packPreviewEmployees",
);
await waitFor(() => {
expect(screen.getByTitle("organization.previewTitle")).toHaveAttribute(
"src",
expect.stringContaining("/employees"),
);
});
});
});
85 changes: 77 additions & 8 deletions dashboard/src/pages/Organization/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,32 @@ function metric(value: number | undefined): string {
return typeof value === "number" ? String(value) : "—";
}

function landedCount(
landed: Record<string, unknown> | null | undefined,
surface: string,
): number {
const inner =
landed && typeof landed.landed === "object" && landed.landed
? (landed.landed as Record<
string,
{ created?: number; updated?: number }
>)
: (landed as Record<
string,
{ created?: number; updated?: number }
> | null);
const row = inner?.[surface];
return Number(row?.created || 0) + Number(row?.updated || 0);
}

function landedTenant(
landed: Record<string, unknown> | null | undefined,
fallback?: number | null,
): string {
const raw = landed?.tenant_id ?? fallback;
return raw === undefined || raw === null || raw === "" ? "—" : String(raw);
}

export default function OrganizationPage() {
const { t, i18n } = useTranslation();
const isZh = i18n.language?.toLowerCase().startsWith("zh") ?? false;
Expand All @@ -67,6 +93,13 @@ export default function OrganizationPage() {
const [landed, setLanded] = useState<Record<string, unknown> | null>(null);
const autoStartRef = useRef(false);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [previewPath, setPreviewPath] = useState("/");
const [previewNonce, setPreviewNonce] = useState("");

const showPreview = useCallback((path: string) => {
setPreviewPath(path.startsWith("/") ? path : `/${path}`);
setPreviewNonce(String(Date.now()));
}, []);

const applyOverview = useCallback(
async (quiet = false) => {
Expand Down Expand Up @@ -325,15 +358,33 @@ export default function OrganizationPage() {
const remote = result.applied.remote_applied
? t("organization.packRemoteYes")
: t("organization.packRemoteNo");
const landedLines = result.applied.remote_applied
? [
t("organization.packLanded", {
tenant: landedTenant(
result.applied.landed,
result.applied.tenant_id,
),
employees: landedCount(result.applied.landed, "employees"),
talent: landedCount(result.applied.landed, "talent"),
skills: landedCount(result.applied.landed, "skills"),
plugins: landedCount(result.applied.landed, "plugins"),
}),
t("organization.packPreviewEmployees"),
]
: [];
setLastActionNotes([
...result.pack.notes,
...result.applied.notes,
remote,
]);
setLastReceipt({
kind: "pack",
lines: [t("organization.packReceipt"), remote],
lines: [t("organization.packReceipt"), remote, ...landedLines],
});
if (result.applied.remote_applied) {
showPreview(result.applied.preview_path || "/employees");
}
message.success(t("organization.packDone"));
});

Expand All @@ -350,8 +401,23 @@ export default function OrganizationPage() {
`${t("organization.timelineRemote")}: ${flagLabel(
Boolean(proof.remote_applied),
)}`,
...(proof.remote_applied
? [
t("organization.packLanded", {
tenant: landedTenant(proof.landed),
employees: landedCount(proof.landed, "employees"),
talent: landedCount(proof.landed, "talent"),
skills: landedCount(proof.landed, "skills"),
plugins: landedCount(proof.landed, "plugins"),
}),
t("organization.packPreviewEmployees"),
]
: []),
],
});
if (proof.remote_applied) {
showPreview("/employees");
}
message.success(
proof.ok ? t("organization.loopOk") : t("organization.loopPartial"),
);
Expand Down Expand Up @@ -384,12 +450,15 @@ export default function OrganizationPage() {
overview?.sidecar_url || "http://127.0.0.1:3780"
).replace(/\/$/, "");
const previewUrl = useMemo(() => {
const base = `${localConsoleUrl}/`;
if (!disabledKeys.length) return base;
return `${base}?freeos_disabled=${encodeURIComponent(
disabledKeys.join(","),
)}`;
}, [localConsoleUrl, disabledKeys]);
const path = previewPath.startsWith("/") ? previewPath : `/${previewPath}`;
const qs = new URLSearchParams();
if (disabledKeys.length) {
qs.set("freeos_disabled", disabledKeys.join(","));
}
if (previewNonce) qs.set("freeos_sync", previewNonce);
const query = qs.toString();
return `${localConsoleUrl}${path}${query ? `?${query}` : ""}`;
}, [localConsoleUrl, disabledKeys, previewPath, previewNonce]);
const showFrame = Boolean(previewUrl);

const pushTogglesToPreview = useCallback(() => {
Expand Down Expand Up @@ -755,7 +824,7 @@ export default function OrganizationPage() {
</section>

{lastReceipt ? (
<section className={styles.timeline}>
<section className={styles.timeline} data-testid="org-last-receipt">
<p className={styles.timelineTitle}>
{t("organization.receiptTitle")}
</p>
Expand Down
2 changes: 1 addition & 1 deletion docs/asset-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ uv run freeos org employee export-profile my-analyst --tenant-id 1
```

`assets publish` writes `{FREEOS_HOME}/asset-packs/latest/`.
`assets apply` POSTs/PUTs those drafts to the control plane when reachable and always writes `{FREEOS_HOME}/openxyos-mirror/<tenant>/`. Nothing is auto-enabled on the sidecar.
`assets apply` POSTs to `/api/freeos/ingest` when the sidecar is reachable and always writes `{FREEOS_HOME}/openxyos-mirror/<tenant>/`. Ingest targets the **signed-in openXYOS tenant** (the embed login), so employees / talent / skills / plugins show in the normal list UIs.

## Where files live

Expand Down
Loading
Loading