From c1b9901713f16e2437104149b92430331f091afc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 05:20:26 +0000 Subject: [PATCH 1/2] feat(org): Phase 3 in-host Workspace overview org-ui slice Add a thin OpenDashboard landing page at /organization/workspace that reuses GET /api/org-module/overview and links into already-migrated Organization pages. Leave the #55 workbench control plane intact. Chat is intentionally not migrated. Mark Phase 3 Open-12 host UI done. Co-authored-by: XYAI Labs --- CHANGELOG.md | 5 +- dashboard/src/locales/en.json | 6 +- dashboard/src/locales/zh.json | 6 +- dashboard/src/org-ui/api/createClient.test.ts | 10 + dashboard/src/org-ui/api/createClient.ts | 87 ++++- dashboard/src/org-ui/contract.ts | 1 + dashboard/src/org-ui/index.ts | 10 + .../pages/workspace/WorkspacePage.module.css | 167 ++++++++++ .../pages/workspace/WorkspacePage.test.tsx | 92 ++++++ .../org-ui/pages/workspace/WorkspacePage.tsx | 311 ++++++++++++++++++ .../src/org-ui/pages/workspace/labels.ts | 104 ++++++ .../src/pages/Organization/index.test.tsx | 11 + dashboard/src/pages/Organization/index.tsx | 37 ++- .../src/pages/Organization/orgPathTabs.tsx | 11 + .../pages/Organization/workspace/index.tsx | 46 +++ dashboard/src/routes/controlAdminPath.test.ts | 2 + dashboard/src/routes/index.tsx | 8 + docs/org-merge-plan.md | 46 ++- src/octop/cli/commands/org.py | 3 +- src/octop/modules/org_os/contract.py | 1 + src/octop/modules/org_os/export_standalone.py | 48 ++- tests/unit/cli/test_org_export_standalone.py | 10 + tests/unit/test_org_workspace.py | 14 + 23 files changed, 995 insertions(+), 41 deletions(-) create mode 100644 dashboard/src/org-ui/pages/workspace/WorkspacePage.module.css create mode 100644 dashboard/src/org-ui/pages/workspace/WorkspacePage.test.tsx create mode 100644 dashboard/src/org-ui/pages/workspace/WorkspacePage.tsx create mode 100644 dashboard/src/org-ui/pages/workspace/labels.ts create mode 100644 dashboard/src/pages/Organization/workspace/index.tsx create mode 100644 tests/unit/test_org_workspace.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 912b3af0..297fa226 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,9 @@ ### 新增 -- Organization Phase 3(智能体切片):宿主内 Agent Studio / 智能体定制。Dashboard `/organization/agents` 使用共享 `dashboard/src/org-ui` 的 `AgentsPage`。列表走 `GET /api/org-module/agents`;编译 / 生命周期 / 注册复用已有 `POST /api/org-module/blueprints/compile`、`/employees/transition`、`/employees/spawn`(`{FREEOS_HOME}/tenants//employees/`)。**不是** FreeOS 个性化智能体编辑器,也不是 Chat,也不依赖未挂载的 sidecar `/api/agent-studio/*`。注册后的同事出现在 Experts。`freeos org export-standalone` 同时列出 Agents。未迁:Chat、资料上传、人才市场、薄 Workspace overview。 -- Organization Phase 3(设置切片):宿主内 Organization Settings。Dashboard `/organization/settings` 使用共享 `dashboard/src/org-ui` 的 `SettingsPage`。本页只编辑 `org_os` 目录模块开关(复用 `GET/PUT /api/org-module/modules`)与组织本地偏好(`GET /api/org-module/settings` · `PUT /api/org-module/prefs`,`{FREEOS_HOME}/org-os/prefs.json`)。**不是** FreeOS 系统设置:大模型密钥、用户、时区仍链到 `/system-settings`。没有边车,也不复制 sidecar 的 AI/用户/数据库页。`freeos org export-standalone` 同时列出 Settings。未迁:Chat、Workspace、边车系统设置。 +- Organization Phase 3(工作台总览切片,Open-12 收口):宿主内薄 Workspace / OpenDashboard。Dashboard `/organization/workspace` 使用共享 `dashboard/src/org-ui` 的 `WorkspacePage`。数据复用已有 `GET /api/org-module/overview`,链到已迁的公告 / 架构 / 员工 / 技能 / 智能体 / 任务 / 知识 / 反思 / 治理 / 设置。**不是**第二套控制面(assemble / pack / loop 仍在 `/organization`)。**Chat 永久不迁。** `freeos org export-standalone` 同时列出 Workspace。Phase 3 Open-12 宿主 UI 完成,下一步 Phase 4 导出。 +- Organization Phase 3(智能体切片):宿主内 Agent Studio / 智能体定制。Dashboard `/organization/agents` 使用共享 `dashboard/src/org-ui` 的 `AgentsPage`。列表走 `GET /api/org-module/agents`;编译 / 生命周期 / 注册复用已有 `POST /api/org-module/blueprints/compile`、`/employees/transition`、`/employees/spawn`(`{FREEOS_HOME}/tenants//employees/`)。**不是** FreeOS 个性化智能体编辑器,也不是 Chat,也不依赖未挂载的 sidecar `/api/agent-studio/*`。注册后的同事出现在 Experts。`freeos org export-standalone` 同时列出 Agents。未迁:Chat、资料上传、人才市场。 +- Organization Phase 3(设置切片):宿主内 Organization Settings。Dashboard `/organization/settings` 使用共享 `dashboard/src/org-ui` 的 `SettingsPage`。本页只编辑 `org_os` 目录模块开关(复用 `GET/PUT /api/org-module/modules`)与组织本地偏好(`GET /api/org-module/settings` · `PUT /api/org-module/prefs`,`{FREEOS_HOME}/org-os/prefs.json`)。**不是** FreeOS 系统设置:大模型密钥、用户、时区仍链到 `/system-settings`。没有边车,也不复制 sidecar 的 AI/用户/数据库页。`freeos org export-standalone` 同时列出 Settings。未迁:Chat、边车系统设置。 - Organization Phase 3(反思切片):宿主内 Reflections。Dashboard `/organization/reflections` 使用共享 `dashboard/src/org-ui` 的 `ReflectionsPage`。列表/创建/删除走 `/api/org-module/reflections*`,SQLite 在 `{FREEOS_HOME}/org/reflections.sqlite`。可选 `employee_id` / `task_id` 指向同一租户的目录员工与组织任务。这是组织复盘,不是 Chat,也不是第二套技能运行时。`freeos org export-standalone` 同时列出 Reflections。未迁:Chat、边车技能统计、边车部署铁律样例、其余 Open-12。 - Organization Phase 3(任务切片):宿主内 Tasks。Dashboard `/organization/tasks` 与 `/organization/tasks/:id` 使用共享 `dashboard/src/org-ui` 的 `TasksPage` / `TaskDetailPage`。CRUD、状态流转、子任务与人工评论走 `/api/org-module/tasks*`,SQLite 在 `{FREEOS_HOME}/org/tasks.sqlite`。这是组织待办,不是 Octop cron,也不是项目/Chat 对话。`freeos org export-standalone` 同时列出 Tasks。未迁:Chat、附件上传、边车任务库、其余 Open-12。 - Organization Phase 3(知识切片):宿主内 Knowledge。Dashboard `/organization/knowledge` 使用共享 `dashboard/src/org-ui` 的 `KnowledgePage`。列表/详情/笔记走 `/api/org-module/knowledge*`,包装已有 `KnowledgeService`(Octop `knowledge_bases` / `knowledge_documents`),与 Chat 检索同一套行。不克隆 openXYOS sidecar notes/files DB。上传、文件夹、嵌入模型仍在 `/knowledge-bases`。`freeos org export-standalone` 同时列出 Knowledge。未迁:Chat、边车知识库、其余 Open-12。 diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index c9ffe118..1e470773 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -182,6 +182,7 @@ "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.", "navWorkbench": "Workbench", + "navWorkspace": "Workspace", "navAnnouncements": "Announcements", "navOrgChart": "Org chart", "navEmployees": "Employees", @@ -221,7 +222,10 @@ "openSettings": "Open settings", "agentsTitle": "Agent Studio", "agentsBody": "Compile organization digital colleagues from blueprints and manage lifecycle on the host. FreeOS Agents, Experts, and personalization stay where they are — no sidecar Agent Studio and no Chat page.", - "openAgents": "Open Agent Studio" + "openAgents": "Open Agent Studio", + "workspaceTitle": "Workspace", + "workspaceBody": "Organization landing overview: host status and links into already-migrated pages. Not a second control plane, and Chat stays on FreeOS.", + "openWorkspace": "Open workspace" }, "systemSettings": { "subtitle": "Workbench, remote desktop, and host administration in one place." diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index 9a0901a5..00306d67 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -182,6 +182,7 @@ "packLanded": "当前登录租户 {{tenant}}:员工 {{employees}},人才 {{talent}},技能 {{skills}},插件 {{plugins}}", "packPreviewEmployees": "右侧预览已打开员工页,可直接核对。", "navWorkbench": "工作台", + "navWorkspace": "总览", "navAnnouncements": "通知公告", "navOrgChart": "组织架构", "navEmployees": "员工目录", @@ -221,7 +222,10 @@ "openSettings": "打开设置", "agentsTitle": "智能体定制", "agentsBody": "在宿主内把蓝图编译成组织数字同事并管理生命周期。FreeOS 智能体、专家页和个性化保持原位——没有边车 Agent Studio,也不迁对话页。", - "openAgents": "打开智能体定制" + "openAgents": "打开智能体定制", + "workspaceTitle": "工作台总览", + "workspaceBody": "组织落地页:宿主状态摘要,并链到已迁入的子页。不是第二套控制面,对话仍在 FreeOS。", + "openWorkspace": "打开总览" }, "systemSettings": { "subtitle": "工作台、远程桌面与主机管理集中在这里。" diff --git a/dashboard/src/org-ui/api/createClient.test.ts b/dashboard/src/org-ui/api/createClient.test.ts index d5037132..86c048d8 100644 --- a/dashboard/src/org-ui/api/createClient.test.ts +++ b/dashboard/src/org-ui/api/createClient.test.ts @@ -111,6 +111,15 @@ describe("createOrgApiClient", () => { if (path === "/org-module/blueprints/compile") { return { slug: "policy-analyst", workspace: "/tmp/policy-analyst" }; } + if (path === "/org-module/overview") { + return { + enabled: true, + runtime: "in_host", + freeos: { employees: 2, spawned_colleagues: 1, org_skills: 3 }, + openxyos: { modules: 12, tenant_id: "acme" }, + catalog: [{ key: "workspace", locked: true }], + }; + } throw new Error(`unexpected ${path}`); }); const client = createOrgApiClient({ fetchJson }); @@ -148,6 +157,7 @@ describe("createOrgApiClient", () => { }) ).slug, ).toBe("policy-analyst"); + expect((await client.workspace.overview()).freeos.employees).toBe(2); expect(fetchJson).toHaveBeenCalled(); }); diff --git a/dashboard/src/org-ui/api/createClient.ts b/dashboard/src/org-ui/api/createClient.ts index c6d7deae..dcb9feae 100644 --- a/dashboard/src/org-ui/api/createClient.ts +++ b/dashboard/src/org-ui/api/createClient.ts @@ -150,10 +150,7 @@ export interface OrgEmployeesClient { } export type GovernancePauseStatus = - | "pending" - | "approved" - | "rejected" - | "expired"; + "pending" | "approved" | "rejected" | "expired"; export interface GovernancePause { pause_id: string; @@ -446,10 +443,7 @@ export interface OrgTasksClient { } export type OrgReflectionType = - | "task_completion" - | "error_learning" - | "knowledge_capture" - | "improvement"; + "task_completion" | "error_learning" | "knowledge_capture" | "improvement"; export interface OrgReflection { id: number; @@ -544,12 +538,7 @@ export interface OrgSettingsClient { } export type OrgColleagueLifecycle = - | "draft" - | "market" - | "recruit" - | "shadow" - | "active" - | "offboard"; + "draft" | "market" | "recruit" | "shadow" | "active" | "offboard"; export interface OrgColleague { slug: string; @@ -633,6 +622,53 @@ export interface OrgAgentsClient { spawn(slug: string): Promise; } +export interface OrgWorkspaceFreeos { + employees: number; + employee_states?: Record; + agents?: number; + spawned_colleagues?: number; + org_skills?: number; + skill_packages?: number; + mcp?: number; + tasks?: number; +} + +export interface OrgWorkspaceOpenxyos { + reachable?: boolean; + url?: string; + detail?: string; + modules?: number; + governance?: boolean; + tenant_id?: string; + approvals?: number; +} + +export interface OrgWorkspaceColleague { + slug: string; + name: string; + lifecycle: string; + agent_id?: string; + spawned?: boolean; +} + +export interface OrgWorkspaceOverview { + enabled: boolean; + runtime?: string; + sidecar_optional?: boolean; + last_sync?: string | null; + freeos: OrgWorkspaceFreeos; + openxyos: OrgWorkspaceOpenxyos; + colleagues?: OrgWorkspaceColleague[]; + org_surfaces?: Record; + catalog?: OrgCapability[]; + module_toggles?: Record; + notes?: string[]; +} + +export interface OrgWorkspaceClient { + overview(): Promise; +} + export interface OrgApiClient { announcements: OrgAnnouncementsClient; org: OrgChartClient; @@ -644,6 +680,7 @@ export interface OrgApiClient { reflections: OrgReflectionsClient; settings: OrgSettingsClient; agents: OrgAgentsClient; + workspace: OrgWorkspaceClient; } async function unwrap(payload: OrgEnvelope): Promise { @@ -668,6 +705,7 @@ export function createOrgApiClient(opts: { reflectionsPrefix?: string; settingsPrefix?: string; agentsPrefix?: string; + workspacePrefix?: string; }): OrgApiClient { const prefix = opts.announcementsPrefix ?? "/org-module/announcements"; const orgPrefix = opts.orgPrefix ?? "/org-module/org"; @@ -678,6 +716,7 @@ export function createOrgApiClient(opts: { const reflectionsPrefix = opts.reflectionsPrefix ?? "/org-module/reflections"; const settingsPrefix = opts.settingsPrefix ?? "/org-module"; const agentsPrefix = opts.agentsPrefix ?? "/org-module"; + const workspacePrefix = opts.workspacePrefix ?? "/org-module"; const fetchJson = opts.fetchJson; const announcements: OrgAnnouncementsClient = { @@ -1206,6 +1245,25 @@ export function createOrgApiClient(opts: { }, }; + const workspace: OrgWorkspaceClient = { + async overview() { + const raw = await fetchJson< + OrgWorkspaceOverview & + OrgEnvelope & { error?: string } + >(`${workspacePrefix}/overview`); + if (raw && typeof raw === "object" && raw.freeos) { + return raw; + } + if (raw && typeof raw === "object" && raw.success === false) { + throw new Error(raw.error || "request failed"); + } + if (raw?.data?.freeos) { + return raw.data; + } + throw new Error(raw?.error || "request failed"); + }, + }; + return { announcements, org, @@ -1217,5 +1275,6 @@ export function createOrgApiClient(opts: { reflections, settings, agents, + workspace, }; } diff --git a/dashboard/src/org-ui/contract.ts b/dashboard/src/org-ui/contract.ts index db74dda1..fb323bfc 100644 --- a/dashboard/src/org-ui/contract.ts +++ b/dashboard/src/org-ui/contract.ts @@ -10,6 +10,7 @@ export const SHARED_ORG_UI_MODULES = [ "reflections", "settings", "agents", + "workspace", ] as const; export type SharedOrgUiModule = (typeof SHARED_ORG_UI_MODULES)[number]; diff --git a/dashboard/src/org-ui/index.ts b/dashboard/src/org-ui/index.ts index df258df4..9b11d257 100644 --- a/dashboard/src/org-ui/index.ts +++ b/dashboard/src/org-ui/index.ts @@ -63,6 +63,11 @@ export type { OrgColleagueStats, OrgCompiledColleague, OrgSpawnedColleague, + OrgWorkspaceClient, + OrgWorkspaceColleague, + OrgWorkspaceFreeos, + OrgWorkspaceOpenxyos, + OrgWorkspaceOverview, OrgSkill, OrgSkillCatalogRow, OrgSkillGenerateResult, @@ -94,6 +99,11 @@ export { SettingsPage } from "./pages/settings/SettingsPage"; export type { SettingsPageProps } from "./pages/settings/SettingsPage"; export { AgentsPage } from "./pages/agents/AgentsPage"; export type { AgentsPageProps } from "./pages/agents/AgentsPage"; +export { WorkspacePage } from "./pages/workspace/WorkspacePage"; +export type { + OrgWorkspaceLinks, + WorkspacePageProps, +} from "./pages/workspace/WorkspacePage"; export type { IdentityBridge, OrgFetcher, diff --git a/dashboard/src/org-ui/pages/workspace/WorkspacePage.module.css b/dashboard/src/org-ui/pages/workspace/WorkspacePage.module.css new file mode 100644 index 00000000..a016ccf2 --- /dev/null +++ b/dashboard/src/org-ui/pages/workspace/WorkspacePage.module.css @@ -0,0 +1,167 @@ +.page { + max-width: 960px; + margin: 0 auto; + padding: 8px 4px 32px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.header { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.titleRow { + display: flex; + align-items: center; + gap: 10px; +} + +.title { + margin: 0; + font-size: 20px; + font-weight: 650; +} + +.subtitle { + margin: 4px 0 0; + color: var(--fn-text-secondary, #667085); + font-size: 13px; + max-width: 640px; + line-height: 1.5; +} + +.hint { + margin: 0; + color: var(--fn-text-secondary, #667085); + font-size: 13px; + line-height: 1.5; +} + +.callout { + border: 1px solid var(--fn-border, #e5e7eb); + border-radius: 12px; + padding: 14px 16px; + background: var(--fn-bg, #f9fafb); + display: flex; + flex-direction: column; + gap: 8px; +} + +.calloutTitle { + margin: 0; + font-size: 13px; + font-weight: 650; +} + +.links { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +@media (min-width: 720px) { + .metrics { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +.metric { + border: 1px solid var(--fn-border, #e5e7eb); + border-radius: 12px; + padding: 14px 16px; + background: var(--fn-bg-container, #fff); + display: flex; + flex-direction: column; + gap: 4px; +} + +.metricValue { + font-size: 22px; + font-weight: 650; + line-height: 1.2; +} + +.metricLabel { + font-size: 12px; + color: var(--fn-text-secondary, #667085); +} + +.card { + border: 1px solid var(--fn-border, #e5e7eb); + border-radius: 12px; + padding: 16px; + background: var(--fn-bg-container, #fff); + display: flex; + flex-direction: column; + gap: 12px; +} + +.cardTitle { + margin: 0; + font-size: 15px; + font-weight: 650; +} + +.grid { + display: grid; + grid-template-columns: 1fr; + gap: 10px; +} + +@media (min-width: 720px) { + .grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +.module { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 12px 14px; + border: 1px solid var(--fn-border, #e5e7eb); + border-radius: 10px; + text-align: left; +} + +.moduleName { + font-size: 13px; + font-weight: 600; +} + +.moduleDesc { + margin: 0; + font-size: 12px; + color: var(--fn-text-secondary, #667085); + line-height: 1.4; +} + +.error { + margin: 0; + color: #b42318; + font-size: 13px; +} + +.empty { + text-align: center; + padding: 48px 12px; + color: var(--fn-text-secondary, #667085); +} diff --git a/dashboard/src/org-ui/pages/workspace/WorkspacePage.test.tsx b/dashboard/src/org-ui/pages/workspace/WorkspacePage.test.tsx new file mode 100644 index 00000000..8bfa1599 --- /dev/null +++ b/dashboard/src/org-ui/pages/workspace/WorkspacePage.test.tsx @@ -0,0 +1,92 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { WorkspacePage } from "./WorkspacePage"; +import type { OrgWorkspaceClient } from "../../api/createClient"; + +function mockClient(): OrgWorkspaceClient { + return { + overview: vi.fn(async () => ({ + enabled: true, + runtime: "in_host", + last_sync: "2026-09-19T00:00:00Z", + freeos: { + employees: 2, + spawned_colleagues: 1, + org_skills: 3, + }, + openxyos: { + modules: 12, + tenant_id: "acme", + governance: true, + }, + catalog: [ + { + key: "announcements", + label: "Announcements", + label_zh: "通知公告", + description: "Notices", + description_zh: "公告", + locked: false, + }, + { + key: "chat", + label: "Collaboration", + label_zh: "沟通协作", + description: "Chat", + description_zh: "对话", + locked: false, + }, + ], + module_toggles: { announcements: true, chat: true, reflections: false }, + })), + }; +} + +const admin = { + userId: 1, + displayName: "Ada", + role: "admin", + isAdmin: true, +}; + +const member = { + userId: 2, + displayName: "Mo", + role: "user", + isAdmin: false, +}; + +describe("WorkspacePage", () => { + it("renders host overview without an iframe or chat module card", async () => { + const client = mockClient(); + render(); + expect(await screen.findByTestId("org-ui-workspace")).toBeInTheDocument(); + expect(screen.getByText("Workspace")).toBeInTheDocument(); + expect( + screen.getByTestId("org-workspace-chat-boundary"), + ).toBeInTheDocument(); + expect(screen.getByTestId("org-workspace-metrics")).toHaveTextContent("2"); + expect( + screen.getByTestId("org-workspace-open-announcements"), + ).toHaveAttribute("href", "/organization/announcements"); + expect(screen.getByTestId("org-workspace-open-agents")).toHaveAttribute( + "href", + "/organization/agents", + ); + expect(screen.queryByTestId("org-workspace-module-chat")).toBeNull(); + expect(screen.queryByTestId("org-workspace-module-reflections")).toBeNull(); + expect(document.querySelector("iframe")).toBeNull(); + expect(client.overview).toHaveBeenCalled(); + }); + + it("hides governance and settings from non-admins", async () => { + const client = mockClient(); + render(); + await screen.findByTestId("org-ui-workspace"); + expect(screen.queryByTestId("org-workspace-module-governance")).toBeNull(); + expect(screen.queryByTestId("org-workspace-module-settings")).toBeNull(); + expect( + screen.getByTestId("org-workspace-module-employees"), + ).toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/org-ui/pages/workspace/WorkspacePage.tsx b/dashboard/src/org-ui/pages/workspace/WorkspacePage.tsx new file mode 100644 index 00000000..8eb52b5c --- /dev/null +++ b/dashboard/src/org-ui/pages/workspace/WorkspacePage.tsx @@ -0,0 +1,311 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Tag, Typography } from "antd"; +import { LayoutDashboard } from "lucide-react"; +import type { + OrgCapability, + OrgWorkspaceClient, + OrgWorkspaceOverview, +} from "../../api/createClient"; +import type { OrgLocale, OrgSession } from "../../shell"; +import { workspaceLabels } from "./labels"; +import styles from "./WorkspacePage.module.css"; + +export interface OrgWorkspaceLinks { + workbench: string; + announcements: string; + organization: string; + employees: string; + skills: string; + agents: string; + tasks: string; + knowledge: string; + reflections: string; + governance: string; + settings: string; + chat: string; +} + +export interface WorkspacePageProps { + client: OrgWorkspaceClient; + session: OrgSession; + locale: OrgLocale; + /** Present so export App.tsx can pass SHARED_ORG_UI_MODULES without unused props. */ + modules?: readonly string[]; + workbenchHref?: string; + links?: Partial; +} + +const DEFAULT_LINKS: OrgWorkspaceLinks = { + workbench: "/organization", + announcements: "/organization/announcements", + organization: "/organization/org", + employees: "/organization/employees", + skills: "/organization/skills", + agents: "/organization/agents", + tasks: "/organization/tasks", + knowledge: "/organization/knowledge", + reflections: "/organization/reflections", + governance: "/organization/governance", + settings: "/organization/settings", + chat: "/chat", +}; + +type ModuleCard = { + key: keyof Pick< + OrgWorkspaceLinks, + | "announcements" + | "organization" + | "employees" + | "skills" + | "agents" + | "tasks" + | "knowledge" + | "reflections" + | "governance" + | "settings" + >; + catalogKey: string; + adminOnly?: boolean; + title: string; + body: string; +}; + +function metric(value: number | undefined): string { + return typeof value === "number" ? String(value) : "—"; +} + +export function WorkspacePage({ + client, + session, + locale, + workbenchHref, + links, +}: WorkspacePageProps) { + const labels = workspaceLabels(locale); + const hrefs: OrgWorkspaceLinks = { + ...DEFAULT_LINKS, + ...links, + workbench: workbenchHref || links?.workbench || DEFAULT_LINKS.workbench, + }; + const [overview, setOverview] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchAll = useCallback(async () => { + setLoading(true); + setError(""); + try { + setOverview(await client.overview()); + } catch { + setOverview(null); + setError(labels.loadFailed); + } finally { + setLoading(false); + } + }, [client, labels.loadFailed]); + + useEffect(() => { + void fetchAll(); + }, [fetchAll]); + + const cards: ModuleCard[] = useMemo( + () => [ + { + key: "announcements", + catalogKey: "announcements", + title: labels.announcements, + body: labels.announcementsBody, + }, + { + key: "organization", + catalogKey: "organization", + title: labels.organization, + body: labels.organizationBody, + }, + { + key: "employees", + catalogKey: "employees", + title: labels.employees, + body: labels.employeesBody, + }, + { + key: "skills", + catalogKey: "skills", + title: labels.skillsPage, + body: labels.skillsBody, + }, + { + key: "agents", + catalogKey: "agents", + title: labels.agents, + body: labels.agentsBody, + }, + { + key: "tasks", + catalogKey: "tasks", + title: labels.tasks, + body: labels.tasksBody, + }, + { + key: "knowledge", + catalogKey: "knowledge", + title: labels.knowledge, + body: labels.knowledgeBody, + }, + { + key: "reflections", + catalogKey: "reflections", + title: labels.reflections, + body: labels.reflectionsBody, + }, + { + key: "governance", + catalogKey: "governance", + adminOnly: true, + title: labels.governance, + body: labels.governanceBody, + }, + { + key: "settings", + catalogKey: "settings", + adminOnly: true, + title: labels.settings, + body: labels.settingsBody, + }, + ], + [labels], + ); + + const capLabel = (row: OrgCapability) => + locale === "zh" ? row.label_zh || row.label : row.label; + + const visible = cards.filter((card) => { + if (card.adminOnly && !session.isAdmin) return false; + if (overview?.module_toggles?.[card.catalogKey] === false) return false; + return true; + }); + + if (loading) { + return ( +
+
{labels.loading}
+
+ ); + } + + const lastSync = overview?.last_sync || labels.neverSynced; + const tenant = overview?.openxyos.tenant_id || "—"; + + return ( +
+
+
+
+ + + {labels.title} + +
+

{labels.subtitle}

+

{labels.hostHint}

+
+ +
+ +
+

{labels.notHereTitle}

+

{labels.notHereBody}

+
+ +
+
+ + {error ? ( +

+ {error} +

+ ) : null} + +
+

{labels.statusTitle}

+
+ {overview?.enabled ? labels.enabled : labels.disabled} + {labels.inHost} + + {labels.tenant}: {tenant} + + + {labels.lastSync}: {lastSync} + +
+
+
+ + {metric(overview?.freeos.employees)} + + {labels.colleagues} +
+
+ + {metric(overview?.freeos.spawned_colleagues)} + + {labels.spawned} +
+
+ + {metric(overview?.freeos.org_skills)} + + {labels.skills} +
+
+ + {metric(overview?.openxyos.modules)} + + {labels.modules} +
+
+
+ +
+

{labels.modulesTitle}

+

{labels.modulesHint}

+ {visible.length ? ( +
+ {visible.map((card) => { + const catalog = overview?.catalog?.find( + (row) => row.key === card.catalogKey, + ); + return ( +
+
+ {catalog ? capLabel(catalog) : card.title} +
+

{card.body}

+ +
+ ); + })} +
+ ) : ( +

{labels.emptyCatalog}

+ )} +
+
+ ); +} diff --git a/dashboard/src/org-ui/pages/workspace/labels.ts b/dashboard/src/org-ui/pages/workspace/labels.ts new file mode 100644 index 00000000..505f1719 --- /dev/null +++ b/dashboard/src/org-ui/pages/workspace/labels.ts @@ -0,0 +1,104 @@ +import type { OrgLocale } from "../../shell"; + +const EN = { + title: "Workspace", + subtitle: + "Organization landing overview. Status and links into already-migrated pages — not a second control plane.", + hostHint: + "Assemble, pack, and the growth loop stay on the Organization workbench. This page only summarizes host status.", + notHereTitle: "Chat is not here", + notHereBody: + "openXYOS Chat is not migrated. Conversations stay on FreeOS / Octop. This overview does not open a sidecar inbox.", + openChat: "FreeOS conversations", + openWorkbench: "Organization workbench", + statusTitle: "Organization status", + enabled: "On", + disabled: "Off", + inHost: "In-host", + tenant: "Tenant", + lastSync: "Last sync", + neverSynced: "Never", + colleagues: "Colleagues", + spawned: "Spawned agents", + skills: "Org skills", + modules: "Catalog modules", + modulesTitle: "Open pages", + modulesHint: + "Same host pages Dashboard mounts under Organization. Chat is intentionally absent.", + loading: "Loading organization overview…", + loadFailed: "Could not load organization overview", + emptyCatalog: "No enabled organization pages.", + announcements: "Announcements", + announcementsBody: "Publish notices and track who has read them.", + organization: "Organization chart", + organizationBody: "Departments and people on the host org tree.", + employees: "Employees", + employeesBody: "Directory of people and AI colleagues.", + skillsPage: "Skills", + skillsBody: "Generated org-API skill wrappers. Agent Skills stay on FreeOS.", + agents: "Agent Studio", + agentsBody: "Compile digital colleagues and manage lifecycle.", + tasks: "Tasks", + tasksBody: "Organization work items. Not cron and not chat.", + knowledge: "Knowledge", + knowledgeBody: "The same FreeOS knowledge bases Chat retrieves from.", + reflections: "Reflections", + reflectionsBody: "Lessons learned on the host org store.", + governance: "Governance", + governanceBody: "Pending approvals and the host audit log.", + settings: "Settings", + settingsBody: "Catalog toggles and org-local preferences.", +}; + +const ZH: typeof EN = { + title: "工作台总览", + subtitle: "组织落地页:状态摘要,并链到已迁入宿主的子页。不是第二套控制面。", + hostHint: "装配、打包和自增长循环仍在组织工作台。本页只汇总宿主状态。", + notHereTitle: "对话不在这里", + notHereBody: + "openXYOS Chat 不迁入。对话留在 FreeOS / Octop。本页不会打开边车收件箱。", + openChat: "FreeOS 对话", + openWorkbench: "组织工作台", + statusTitle: "组织状态", + enabled: "已启用", + disabled: "未启用", + inHost: "宿主内", + tenant: "租户", + lastSync: "最近同步", + neverSynced: "尚未同步", + colleagues: "数字同事", + spawned: "已注册智能体", + skills: "组织技能", + modules: "目录模块", + modulesTitle: "已迁页面", + modulesHint: "与 Dashboard Organization 下同一套宿主页。对话故意不在此列。", + loading: "正在加载组织总览…", + loadFailed: "无法加载组织总览", + emptyCatalog: "没有已启用的组织页面。", + announcements: "通知公告", + announcementsBody: "发布公告并跟踪已读。", + organization: "组织架构", + organizationBody: "宿主组织树上的部门与人员。", + employees: "员工目录", + employeesBody: "人机员工名册。", + skillsPage: "技能", + skillsBody: "组织 API 的生成包装。Agent Skills 仍在 FreeOS。", + agents: "智能体定制", + agentsBody: "编译数字同事并管理生命周期。", + tasks: "任务", + tasksBody: "组织待办。不是定时任务,也不是对话。", + knowledge: "知识", + knowledgeBody: "Chat 检索用的同一套 FreeOS 知识库。", + reflections: "反思", + reflectionsBody: "宿主组织库里的复盘与经验。", + governance: "治理", + governanceBody: "待审批与宿主审计日志。", + settings: "设置", + settingsBody: "目录开关与组织本地偏好。", +}; + +export type WorkspaceLabels = typeof EN; + +export function workspaceLabels(locale: OrgLocale): WorkspaceLabels { + return locale === "zh" ? ZH : EN; +} diff --git a/dashboard/src/pages/Organization/index.test.tsx b/dashboard/src/pages/Organization/index.test.tsx index c6dd0048..33c44a3b 100644 --- a/dashboard/src/pages/Organization/index.test.tsx +++ b/dashboard/src/pages/Organization/index.test.tsx @@ -52,6 +52,10 @@ function renderOrg() { path="/organization/agents" element={
agents
} /> + workspace} + /> experts} @@ -309,6 +313,13 @@ describe("OrganizationPage", () => { expect(screen.queryByTestId("org-browser-frame")).toBeNull(); }); + it("opens in-host workspace overview without a sidecar iframe", async () => { + renderOrg(); + fireEvent.click(await screen.findByTestId("org-open-workspace")); + expect(await screen.findByTestId("org-workspace-page")).toBeInTheDocument(); + expect(screen.queryByTestId("org-browser-frame")).toBeNull(); + }); + it("imports into FreeOS and opens Experts so colleagues are selectable", async () => { vi.mocked(orgModuleApi.assemble).mockResolvedValue({ sidecar_reachable: false, diff --git a/dashboard/src/pages/Organization/index.tsx b/dashboard/src/pages/Organization/index.tsx index 11df6c25..6af2a5d7 100644 --- a/dashboard/src/pages/Organization/index.tsx +++ b/dashboard/src/pages/Organization/index.tsx @@ -8,6 +8,7 @@ import { Brain, Building2, Download, + LayoutDashboard, ListTodo, Package, Play, @@ -342,14 +343,14 @@ export default function OrganizationPage() { busy === "assemble" ? t("organization.progressAssemble") : busy === "pack" - ? t("organization.progressPack") - : busy === "loop" - ? t("organization.progressLoop") - : busy === "produce" - ? t("organization.progressProduce") - : busy === "sidecar" - ? t("organization.autoStartingSidecar") - : null; + ? t("organization.progressPack") + : busy === "loop" + ? t("organization.progressLoop") + : busy === "produce" + ? t("organization.progressProduce") + : busy === "sidecar" + ? t("organization.autoStartingSidecar") + : null; const lastLoop = (loopProof ?? overview?.last_loop) as OrgLoopProof | null; const lastSync = overview?.last_sync @@ -407,6 +408,22 @@ export default function OrganizationPage() {

{t("organization.glossary")}

+
+ +

+ {t("organization.workspaceTitle")} +

+

+ {t("organization.workspaceBody")} +

+ +

@@ -936,8 +953,8 @@ export default function OrganizationPage() { {row.locked ? t("organization.locked") : moduleToggles[row.key] === false - ? t("organization.catalogDisabled") - : t("organization.catalogEnabled")} + ? t("organization.catalogDisabled") + : t("organization.catalogEnabled")}

diff --git a/dashboard/src/pages/Organization/orgPathTabs.tsx b/dashboard/src/pages/Organization/orgPathTabs.tsx index 6c09eaaf..9ff4114e 100644 --- a/dashboard/src/pages/Organization/orgPathTabs.tsx +++ b/dashboard/src/pages/Organization/orgPathTabs.tsx @@ -2,6 +2,7 @@ import { BookOpen, Brain, Building2, + LayoutDashboard, ListTodo, Megaphone, Network, @@ -18,6 +19,7 @@ import type { PathTabsConfig } from "../../layouts/PageShell"; export function useOrgPathTabs( active: | "workbench" + | "workspace" | "announcements" | "org" | "employees" @@ -39,6 +41,11 @@ export function useOrgPathTabs( label: t("organization.navWorkbench"), icon: , }, + { + value: "workspace", + label: t("organization.navWorkspace"), + icon: , + }, { value: "announcements", label: t("organization.navAnnouncements"), @@ -91,6 +98,10 @@ export function useOrgPathTabs( }, ], onChange: (value) => { + if (value === "workspace") { + navigate("/organization/workspace"); + return; + } if (value === "announcements") { navigate("/organization/announcements"); return; diff --git a/dashboard/src/pages/Organization/workspace/index.tsx b/dashboard/src/pages/Organization/workspace/index.tsx new file mode 100644 index 00000000..45f56f0d --- /dev/null +++ b/dashboard/src/pages/Organization/workspace/index.tsx @@ -0,0 +1,46 @@ +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { request } from "../../../api/request"; +import { useCurrentUser } from "../../../hooks/useCurrentUser"; +import PageShell from "../../../layouts/PageShell"; +import { + WorkspacePage, + createOrgApiClient, + type OrgSession, +} from "../../../org-ui"; +import { useOrgPathTabs } from "../orgPathTabs"; + +export default function OrganizationWorkspacePage() { + const { t, i18n } = useTranslation(); + const user = useCurrentUser(); + const pathTabs = useOrgPathTabs("workspace"); + const client = useMemo( + () => + createOrgApiClient({ + fetchJson: (path, init) => request(path, init), + }), + [], + ); + const locale = i18n.language?.toLowerCase().startsWith("zh") ? "zh" : "en"; + const session: OrgSession = { + userId: user?.id ?? 0, + displayName: user?.display_name || user?.username || "", + role: user?.role || "user", + isAdmin: user?.role === "admin", + }; + + return ( + + + + ); +} diff --git a/dashboard/src/routes/controlAdminPath.test.ts b/dashboard/src/routes/controlAdminPath.test.ts index e5ac583d..1cb897c5 100644 --- a/dashboard/src/routes/controlAdminPath.test.ts +++ b/dashboard/src/routes/controlAdminPath.test.ts @@ -77,6 +77,7 @@ describe("pathPermissionKeys", () => { expect(pathPermissionKeys("/organization/reflections")).toBeNull(); expect(pathPermissionKeys("/organization/settings")).toBeNull(); expect(pathPermissionKeys("/organization/agents")).toBeNull(); + expect(pathPermissionKeys("/organization/workspace")).toBeNull(); expect(pathPermissionKeys("/projects")).toBeNull(); expect(pathPermissionKeys("/tasks")).toBeNull(); expect(pathPermissionKeys("/token-usage")).toBeNull(); @@ -172,6 +173,7 @@ describe("unknown dashboard paths", () => { ); expect(resolveSelectedKey("/organization/settings")).toBe("organization"); expect(resolveSelectedKey("/organization/agents")).toBe("organization"); + expect(resolveSelectedKey("/organization/workspace")).toBe("organization"); }); it("are caught by the not-found route", () => { diff --git a/dashboard/src/routes/index.tsx b/dashboard/src/routes/index.tsx index 1905f62c..2d10a54e 100644 --- a/dashboard/src/routes/index.tsx +++ b/dashboard/src/routes/index.tsx @@ -50,6 +50,9 @@ const OrganizationSettingsPage = lazy( const OrganizationAgentsPage = lazy( () => import("../pages/Organization/agents"), ); +const OrganizationWorkspacePage = lazy( + () => import("../pages/Organization/workspace"), +); const ProjectsPage = lazy(() => import("../pages/Projects")); // Misc @@ -113,6 +116,7 @@ export const pathToKey: Record = { "/organization/reflections": "organization", "/organization/settings": "organization", "/organization/agents": "organization", + "/organization/workspace": "organization", "/projects": "projects", "/tasks": "projects", "/connectors": "personalization", @@ -291,6 +295,10 @@ export const routeConfigs: RouteConfig[] = [ path: "/organization/agents", element: , }, + { + path: "/organization/workspace", + element: , + }, { path: "/projects", element: }, { path: "/tasks", element: }, { diff --git a/docs/org-merge-plan.md b/docs/org-merge-plan.md index d72ea59f..eb555377 100644 --- a/docs/org-merge-plan.md +++ b/docs/org-merge-plan.md @@ -1,10 +1,10 @@ # 组织模块合并计划(openXYOS → FreeOS Dashboard) -**状态:** Phase 3 组织架构、员工目录、技能、治理 UI、知识、任务、反思、设置与智能体切片已落地(宿主 org tree / 员工 CRUD + skill_bridge list/read + 治理 pauses/audit + 宿主知识库桥接 + `{FREEOS_HOME}/org/tasks.sqlite` + `{FREEOS_HOME}/org/reflections.sqlite` + `{FREEOS_HOME}/org-os/prefs.json` + lifecycle 同事 / 蓝图编译 + `/organization/org` + `/organization/employees` + `/organization/skills` + `/organization/governance` + `/organization/knowledge` + `/organization/tasks` + `/organization/reflections` + `/organization/settings` + `/organization/agents` + `org-ui` OrgChartPage / EmployeesPage / SkillsPage / GovernancePage / KnowledgePage / TasksPage / ReflectionsPage / SettingsPage / AgentsPage + export-standalone 模块列表)。其余 Open-12 仍待切片。 +**状态:** Phase 3 Open-12 宿主 UI **已完成**(Chat 永久不迁,属明确非目标)。已落地:工作台总览、组织架构、员工目录、技能、治理 UI、知识、任务、反思、设置与智能体(`/organization/workspace` + `/organization/org` + `/organization/employees` + `/organization/skills` + `/organization/governance` + `/organization/knowledge` + `/organization/tasks` + `/organization/reflections` + `/organization/settings` + `/organization/agents` + `org-ui` WorkspacePage / OrgChartPage / EmployeesPage / SkillsPage / GovernancePage / KnowledgePage / TasksPage / ReflectionsPage / SettingsPage / AgentsPage + export-standalone 模块列表)。**下一步:Phase 4 导出与 IdentityBridge。** **日期:** 2026-09-19 **依据:** Phase 0 迁移图、[architecture-integration.md](architecture-integration.md)、[asset-loop.md](asset-loop.md)、[ADR 001](adr/001-single-process-model.md)、#55 宿主内 Organization -Phase 1 冻结了合同。Phase 2(通知公告)与 Phase 3 的组织架构切片已按该合同落地;其余 Open-12 页仍按阶段推进。 +Phase 1 冻结了合同。Phase 2(通知公告)与 Phase 3 Open-12 宿主页已按该合同落地;Chat 不迁。 --- @@ -25,7 +25,7 @@ Phase 1 冻结了合同。Phase 2(通知公告)与 Phase 3 的组织架构 | **0** 宿主内地基 | `org_os` 控制面、`/api/org-module/*` BFF、Dashboard 原生工作台、sidecar 改为可选 | **已完成**(#55) | | **1** 合同冻结 | ADR + 本计划:单源双交付、包布局、API 归属、IdentityBridge、Phase 2 验收 | **本文** | | **2** 垂直切片 | **通知公告 Announcements** 按合同落地(Dashboard 路由 + 可导出同一页面) | **已完成** | -| **3** Open-12 其余页 | 工作台、组织架构、员工、技能、智能体、任务、知识、反思、治理 UI、设置 | **进行中**:架构 / 员工 / 技能 / 治理 / 知识 / 任务 / 反思 / 设置 / 智能体已迁;工作台扩展(薄 Workspace overview)未开始 | +| **3** Open-12 其余页 | 工作台、组织架构、员工、技能、智能体、任务、知识、反思、治理 UI、设置 | **已完成(Chat 除外)**:架构 / 员工 / 技能 / 治理 / 知识 / 任务 / 反思 / 设置 / 智能体 / 薄 Workspace overview 已迁。**Chat 永久不迁** | | **4** 身份与 CRUD | 已迁页面走 IdentityBridge;业务 CRUD 按切片迁入宿主;未迁路由仍可代理到可选 sidecar | 与 2–3 交叉推进 | | **5** 导出与安装器 | `freeos org export-standalone` 产出独立站;默认安装器保持零 Node | 未开始 | @@ -54,7 +54,7 @@ Catalog 十二键(`catalog.py` ↔ `open-module-catalog.ts`): | 模块 key | 现状 UI | 现状 API | 合并方向 | |---|---|---|---| -| workspace | OpenApp `/app` | sidecar dashboard/inbox | Phase 3:Dashboard `/organization` 工作台扩展,复用 #55 原生页 | +| workspace | OpenApp `/app` | sidecar dashboard/inbox | **Phase 3 切片已落地**:薄 Workspace / OpenDashboard `/organization/workspace`,复用 `GET /api/org-module/overview`,链到已迁子页。**不是**第二套控制面(assemble / pack / loop 仍在 `#55` `/organization`)。Chat 不迁 | | **announcements** | OpenApp `/announcements` | sidecar `/api/announcements` | **Phase 2 切片** | | organization | OpenApp `/org` | sidecar `/api/org` | **Phase 3 切片已落地**:`/organization/org` + `/api/org-module/org` | | employees | OpenApp `/employees` | sidecar `/api/employees` + 宿主 lifecycle `/api/org-module/employees` | **Phase 3 切片已落地**:`/organization/employees` 目录 CRUD 复用 `org_chart.sqlite`;lifecycle / spawn **已在宿主,勿重复造** | @@ -416,7 +416,7 @@ openXYOS sidecar `/api/governance`(权限矩阵、通信规则、流程模板 | Chat / 从反思页发起会话 | 对话运行时留在 FreeOS/Octop | | 边车 `employee_skill_stats` / 技能使用统计 | 组织技能已有独立切片;本页不另造技能库 | | 边车「系统部署铁律」样例文案 | openXYOS 自身部署备忘,不是组织复盘数据 | -| 其余 Open-12(工作台扩展) | 后续切片 | +| 其余 Open-12(工作台扩展) | **已由 Workspace 切片完成** | | `freeos org export-standalone` 完整 Vite/Node 打包 | Phase 5 | --- @@ -449,7 +449,7 @@ openXYOS sidecar `/api/governance`(权限矩阵、通信规则、流程模板 |---|---| | 把系统设置搬进组织页 | FreeOS 已有系统设置;本页只链出去 | | 边车公司/AI/用户/数据库 Tab | sidecar SQL.js;密钥与用户在宿主 | -| Chat / Workspace 批量搬迁 | 不在本 PR | +| Chat 搬迁 | **永久不迁** | | `freeos org export-standalone` 完整 Vite/Node 打包 | Phase 5 | --- @@ -482,11 +482,43 @@ openXYOS sidecar `/api/governance`(权限矩阵、通信规则、流程模板 | Chat / 从智能体页发起会话 | 对话运行时留在 FreeOS/Octop | | 边车 `/api/agent-studio/*` 资料上传与人才市场 | sidecar 未挂载;本页不克隆上传工作室 | | 在组织页里编辑 FreeOS 智能体 / 专家 | 运行时已在个性化与 Experts;本页只编译并深链 | -| 其余 Open-12(薄 Workspace overview) | 后续切片 | +| 薄 Workspace overview | **已由 Workspace 切片完成** | | `freeos org export-standalone` 完整 Vite/Node 打包 | Phase 5 | --- +## Phase 3 进度:工作台总览(本切片,Phase 3 收口) + +按同一缝落地,**只迁薄 Workspace / OpenDashboard**。不把 `#55` `/organization` 工作台改成第二套控制面,也不迁 Chat。 + +### 与 #55 工作台的关系 + +| 表面 | 路由 | API | 含义 | +|---|---|---|---| +| 组织工作台(控制面) | `/organization` | `GET /api/org-module/overview` + assemble / pack / loop | **已有。** 装配同事、打包回写、自增长循环、可选 sidecar | +| Workspace 落地页 | `/organization/workspace`(独立站 `/app`) | **同一** `GET /api/org-module/overview` | OpenApp `/app` 等价物:状态摘要 + 链到已迁子页 | +| Chat | 不迁 | sidecar `/api/chats` | **永久不迁。** 对话走 FreeOS/Octop `/chat` | + +### 已落地 + +1. 壳无关组件:`dashboard/src/org-ui/pages/workspace`(`WorkspacePage`),数据经 `createOrgApiClient().workspace.overview()`。 +2. Dashboard 子路由:`/organization/workspace`(工作台 path tabs + 入口卡)。`/organization` 控制面保持原样。 +3. 宿主 API:**无新路由。** 复用已有 `GET /api/org-module/overview`。 +4. 导出骨架:`freeos org export-standalone` 模块列表与 `App.tsx` 同时导入 `WorkspacePage`。Chat 不在共享模块列表里。 + +### 本切片明确推迟 / 永久不做 + +| 项 | 原因 | +|---|---| +| Chat / 边车 inbox | **永久不迁。** 对话运行时留在 FreeOS/Octop | +| 把 assemble / pack / loop 搬进 Workspace | 会变成第二套控制面;工作台仍在 `/organization` | +| 边车 `/api/dashboard/overview` | 宿主 overview 已覆盖状态;不克隆 sidecar inbox | +| `freeos org export-standalone` 完整 Vite/Node 打包 | Phase 5 | + +Phase 3 Open-12 宿主 UI 至此收口。下一步是 Phase 4 导出与 IdentityBridge。 + +--- + ## 明确非目标 | 非目标 | 说明 | diff --git a/src/octop/cli/commands/org.py b/src/octop/cli/commands/org.py index 51a429f4..512a7ab3 100644 --- a/src/octop/cli/commands/org.py +++ b/src/octop/cli/commands/org.py @@ -386,7 +386,8 @@ def export_standalone(out_dir: Path) -> None: Writes the shared module list and an App.tsx that imports AnnouncementPage, OrgChartPage, EmployeesPage, SkillsPage, GovernancePage, KnowledgePage, - TasksPage, and ReflectionsPage. Full Vite + Node packaging is Phase 5 + TasksPage, ReflectionsPage, SettingsPage, AgentsPage, and WorkspacePage. + Chat is not exported. Full Vite + Node packaging is Phase 5 (TODO in the generated README). """ from octop.modules.org_os.export_standalone import write_standalone_scaffold diff --git a/src/octop/modules/org_os/contract.py b/src/octop/modules/org_os/contract.py index 86eee11e..06e6ebd9 100644 --- a/src/octop/modules/org_os/contract.py +++ b/src/octop/modules/org_os/contract.py @@ -16,6 +16,7 @@ "reflections", "settings", "agents", + "workspace", ) diff --git a/src/octop/modules/org_os/export_standalone.py b/src/octop/modules/org_os/export_standalone.py index 4fe786ec..b64eeeea 100644 --- a/src/octop/modules/org_os/export_standalone.py +++ b/src/octop/modules/org_os/export_standalone.py @@ -3,8 +3,8 @@ Full Vite + Node server packaging is Phase 5. This writes a snapshot that lists the shared org-ui modules and points at the same AnnouncementPage, OrgChartPage, EmployeesPage, SkillsPage, GovernancePage, KnowledgePage, -TasksPage, ReflectionsPage, SettingsPage, and AgentsPage sources Dashboard -mounts under ``/organization/...``. +TasksPage, ReflectionsPage, SettingsPage, AgentsPage, and WorkspacePage +sources Dashboard mounts under ``/organization/...``. """ from __future__ import annotations @@ -35,6 +35,7 @@ | reflections | `/organization/reflections` | `/reflections` | `dashboard/src/org-ui` → `ReflectionsPage` | | settings | `/organization/settings` | `/settings` | `dashboard/src/org-ui` → `SettingsPage` | | agents | `/organization/agents` | `/agents` | `dashboard/src/org-ui` → `AgentsPage` | +| workspace | `/organization/workspace` | `/app` | `dashboard/src/org-ui` → `WorkspacePage` | Do **not** copy those pages into a second tree. Edit `dashboard/src/org-ui`. @@ -57,6 +58,10 @@ lifecycle colleagues (`{FREEOS_HOME}/tenants//employees/`). This is **not** the FreeOS personalization editor, Chat runtime, or sidecar `/api/agent-studio/*`. Spawned colleagues appear on FreeOS Experts. +Organization Workspace is a thin OpenDashboard landing page. It reads +`GET /api/org-module/overview` and links into already-migrated pages. +It is **not** a second control plane (assemble / pack / loop stay on +the Organization workbench). Chat is **not** migrated. ## Phase 3 vs Phase 5 @@ -65,7 +70,8 @@ - Shared module list (`src/modules.json`) - Thin `src/App.tsx` that imports `AnnouncementPage`, `OrgChartPage`, `EmployeesPage`, `SkillsPage`, `GovernancePage`, `KnowledgePage`, - `TasksPage`, `ReflectionsPage`, `SettingsPage`, and `AgentsPage` + `TasksPage`, `ReflectionsPage`, `SettingsPage`, `AgentsPage`, + and `WorkspacePage` - Documents IdentityBridge: embedded mode uses FreeOS JWT; standalone uses local JWT Phase 5 (TODO — not implemented here): @@ -99,6 +105,7 @@ SkillsPage, TaskDetailPage, TasksPage, + WorkspacePage, } from "org-ui"; import type { OrgAgentsClient, @@ -112,6 +119,7 @@ OrgSettingsClient, OrgSkillsClient, OrgTasksClient, + OrgWorkspaceClient, } from "org-ui"; /** @@ -120,7 +128,8 @@ * /organization/announcements, /organization/org, /organization/employees, * /organization/skills, /organization/governance, /organization/knowledge, * /organization/tasks, /organization/reflections, - * /organization/settings, and /organization/agents. + * /organization/settings, /organization/agents, and + * /organization/workspace. */ const session: OrgSession = { userId: 0, @@ -140,6 +149,7 @@ reflectionsClient: OrgReflectionsClient; settingsClient: OrgSettingsClient; agentsClient: OrgAgentsClient; + workspaceClient: OrgWorkspaceClient; locale?: "zh" | "en"; }) { return ( @@ -218,6 +228,27 @@ locale={props.locale ?? "en"} modules={SHARED_ORG_UI_MODULES} /> + ); } @@ -230,7 +261,7 @@ "description": ( "Standalone Organization web export skeleton. " "Pages come from dashboard/src/org-ui " - "(Phase 3: Announcements + Org chart + Employees + Skills + Governance + Knowledge + Tasks + Reflections + Settings + Agents). " + "(Phase 3: Announcements + Org chart + Employees + Skills + Governance + Knowledge + Tasks + Reflections + Settings + Agents + Workspace). " "Full Node packaging is Phase 5." ), "type": "module", @@ -331,6 +362,13 @@ def write_standalone_scaffold(out_dir: Path) -> dict[str, Any]: "spawn_api": "/api/org-module/employees/spawn", "store": "{FREEOS_HOME}/tenants//employees", }, + "workspace": { + "component": "WorkspacePage", + "embedded_route": "/organization/workspace", + "standalone_route": "/app", + "api": "/api/org-module/overview", + "not_migrated": ["chat"], + }, }, "todo": "Phase 5: full Vite + server packaging; do not fork org-ui pages", } diff --git a/tests/unit/cli/test_org_export_standalone.py b/tests/unit/cli/test_org_export_standalone.py index b2a4ae1f..318b553e 100644 --- a/tests/unit/cli/test_org_export_standalone.py +++ b/tests/unit/cli/test_org_export_standalone.py @@ -27,6 +27,7 @@ def test_export_standalone_lists_shared_org_ui_modules(tmp_path: Path) -> None: "reflections", "settings", "agents", + "workspace", ] modules = json.loads((out / "src" / "modules.json").read_text(encoding="utf-8")) assert modules["shared_org_ui_modules"] == [ @@ -40,6 +41,7 @@ def test_export_standalone_lists_shared_org_ui_modules(tmp_path: Path) -> None: "reflections", "settings", "agents", + "workspace", ] assert modules["pages"]["announcements"]["component"] == "AnnouncementPage" assert modules["pages"]["announcements"]["embedded_route"] == "/organization/announcements" @@ -76,6 +78,11 @@ def test_export_standalone_lists_shared_org_ui_modules(tmp_path: Path) -> None: assert modules["pages"]["agents"]["api"] == "/api/org-module/agents" assert modules["pages"]["agents"]["compile_api"] == "/api/org-module/blueprints/compile" assert modules["pages"]["agents"]["store"] == "{FREEOS_HOME}/tenants//employees" + assert modules["pages"]["workspace"]["component"] == "WorkspacePage" + assert modules["pages"]["workspace"]["embedded_route"] == "/organization/workspace" + assert modules["pages"]["workspace"]["standalone_route"] == "/app" + assert modules["pages"]["workspace"]["api"] == "/api/org-module/overview" + assert modules["pages"]["workspace"]["not_migrated"] == ["chat"] app = (out / "src" / "App.tsx").read_text(encoding="utf-8") assert "AnnouncementPage" in app assert "OrgChartPage" in app @@ -89,6 +96,7 @@ def test_export_standalone_lists_shared_org_ui_modules(tmp_path: Path) -> None: assert "ReflectionsPage" in app assert "SettingsPage" in app assert "AgentsPage" in app + assert "WorkspacePage" in app assert 'from "org-ui"' in app readme = (out / "README.md").read_text(encoding="utf-8") assert "Phase 5" in readme @@ -102,3 +110,5 @@ def test_export_standalone_lists_shared_org_ui_modules(tmp_path: Path) -> None: assert "ReflectionsPage" in readme assert "SettingsPage" in readme assert "AgentsPage" in readme + assert "WorkspacePage" in readme + assert "Chat is **not** migrated" in readme diff --git a/tests/unit/test_org_workspace.py b/tests/unit/test_org_workspace.py new file mode 100644 index 00000000..a2674cb9 --- /dev/null +++ b/tests/unit/test_org_workspace.py @@ -0,0 +1,14 @@ +"""In-host Organization workspace overview is a catalog/org-ui seam only.""" + +from __future__ import annotations + +from octop.modules.org_os.contract import ( + SHARED_ORG_UI_MODULES, + assert_shared_modules_in_catalog, +) + + +def test_shared_workspace_module_is_in_catalog() -> None: + assert "workspace" in SHARED_ORG_UI_MODULES + assert "chat" not in SHARED_ORG_UI_MODULES + assert_shared_modules_in_catalog() From 84060ab9e8805ce9cd972b31f7e68ed8127139b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 05:21:39 +0000 Subject: [PATCH 2/2] fix(org): format Workspace slice with project Prettier Use dashboard Prettier 3.0.0 so CI format-check matches the repo pin. Co-authored-by: XYAI Labs --- dashboard/src/org-ui/api/createClient.ts | 17 ++++++++++++++--- dashboard/src/pages/Organization/index.tsx | 20 ++++++++++---------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/dashboard/src/org-ui/api/createClient.ts b/dashboard/src/org-ui/api/createClient.ts index dcb9feae..7657087e 100644 --- a/dashboard/src/org-ui/api/createClient.ts +++ b/dashboard/src/org-ui/api/createClient.ts @@ -150,7 +150,10 @@ export interface OrgEmployeesClient { } export type GovernancePauseStatus = - "pending" | "approved" | "rejected" | "expired"; + | "pending" + | "approved" + | "rejected" + | "expired"; export interface GovernancePause { pause_id: string; @@ -443,7 +446,10 @@ export interface OrgTasksClient { } export type OrgReflectionType = - "task_completion" | "error_learning" | "knowledge_capture" | "improvement"; + | "task_completion" + | "error_learning" + | "knowledge_capture" + | "improvement"; export interface OrgReflection { id: number; @@ -538,7 +544,12 @@ export interface OrgSettingsClient { } export type OrgColleagueLifecycle = - "draft" | "market" | "recruit" | "shadow" | "active" | "offboard"; + | "draft" + | "market" + | "recruit" + | "shadow" + | "active" + | "offboard"; export interface OrgColleague { slug: string; diff --git a/dashboard/src/pages/Organization/index.tsx b/dashboard/src/pages/Organization/index.tsx index 6af2a5d7..f6d0d7eb 100644 --- a/dashboard/src/pages/Organization/index.tsx +++ b/dashboard/src/pages/Organization/index.tsx @@ -343,14 +343,14 @@ export default function OrganizationPage() { busy === "assemble" ? t("organization.progressAssemble") : busy === "pack" - ? t("organization.progressPack") - : busy === "loop" - ? t("organization.progressLoop") - : busy === "produce" - ? t("organization.progressProduce") - : busy === "sidecar" - ? t("organization.autoStartingSidecar") - : null; + ? t("organization.progressPack") + : busy === "loop" + ? t("organization.progressLoop") + : busy === "produce" + ? t("organization.progressProduce") + : busy === "sidecar" + ? t("organization.autoStartingSidecar") + : null; const lastLoop = (loopProof ?? overview?.last_loop) as OrgLoopProof | null; const lastSync = overview?.last_sync @@ -953,8 +953,8 @@ export default function OrganizationPage() { {row.locked ? t("organization.locked") : moduleToggles[row.key] === false - ? t("organization.catalogDisabled") - : t("organization.catalogEnabled")} + ? t("organization.catalogDisabled") + : t("organization.catalogEnabled")}