diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf85b6d..daa78481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### 变更 - 桌面端(`OCTOP_DESKTOP` / `FREEOS_DESKTOP`)首次打开不再经过验证启动密码、选择数据库、创建管理员三步:自动绑定本地 SQLite 并使用已有 guest / local-session。首屏只保留配置 LLM API Key,可「跳过,进入工作台」,之后仍可在设置里配模型。自托管服务端向导不变。 +- 侧栏「设置」分组改名为「能力」/ Capabilities。原「控制」与「管理」子模块并入顶层「系统设置」/ System Settings(页内分节;工作台 / 远程桌面仍走全屏路由)。旧 `/admin/*`、`/acp`、`/agent-config` 地址会重定向。 - 用户可见吉祥物从章鱼换成 XYAI 白色机器人姿势包(欢迎 / 空态 / 探头 / 思考 / 打字 / 任务);桌面启动页与 openXYOS 首页使用同一角色,不再加载章鱼 WebM。 - 桌面更新通道只跟踪 `github.com/XYAIStudio/FreeOS/releases`。不再读取 PyPI `octop`、腾讯云 COS 或其它 Octop 镜像;当前已是最新 FreeOS `v0.0.1` 时不会误报更新。 - 默认主题与残留 Octop `rose` 存储一次性迁移为 XYAI 蓝 `#0033FF`(之后仍可手动选玫瑰粉)。 @@ -21,6 +22,7 @@ ### 修复 - 桌面 / 本地会话不再被 `setup_required` 短路:`GET /setup/status` 之后仍可调用 `POST /auth/local-session`,不会误跳进服务端密码向导。 +- 组织页「启动边车」会拉起捆绑的 Node + openXYOS(Windows 用 `node.exe` / `cmd /c`,不再直接 Popen `.bat`),并在右侧预览区嵌入控制台(支持全屏)。openXYOS 在 FreeOS 桌面环境下关闭 `X-Frame-Options`,避免 iframe 空白。安装 / 桌面启动时宿主与 Wails 都会确保边车常开(需 `dist/index.html` 已打包)。 - Windows 打开时不再因 WebView2 `Navigate`/`SetURL` 在 Chromium 未就绪时触发 Go panic。主窗口等 WebView 点火后再跳转;`SetURL` 失败会重试并显示 FreeOS 提示,而不是原始堆栈。设置窗口延后创建,避免两个 WebView2 同时初始化同一用户目录。用户数据目录必须可写,并设置 `WEBVIEW2_USER_DATA_FOLDER`。 - 已安装的 FreeOS 不再继承残留的 `~/.octop` 作为桌面家目录。无 `FREEOS_STAMP` 的 `~/.octop/portable` 会被丢弃;健康检查要求 `product=freeos`,避免连上旧 Octop 进程后出现红标登录墙。桌面端 `?desktop=1` 会记住本地会话并持续重试,不再落到登录页。 - Windows NSIS 安装完成后双击桌面 / 开始菜单快捷方式(或 `FreeOS.exe`)能打开窗口:快捷方式工作目录固定为 `$INSTDIR`,WebView2 用户数据写到 `%LOCALAPPDATA%\FreeOS\WebView2`(不再落到 Program Files),结束页用未提权 token 启动以免把 `~/.freeos` 标成 High integrity,并忽略非 `FreeOS.exe` 的残留 `desktop.pid`。 diff --git a/dashboard/src/layouts/Sidebar.tsx b/dashboard/src/layouts/Sidebar.tsx index 586cd892..eed7090f 100644 --- a/dashboard/src/layouts/Sidebar.tsx +++ b/dashboard/src/layouts/Sidebar.tsx @@ -36,8 +36,8 @@ import { typeSize } from "../utils/mobileTypeScale"; import { DESKTOP_DRAG_REGION_CLASS } from "../utils/desktopChrome"; const NAV_GROUPS_STORAGE_KEY = "octop:sidebar-nav-groups"; -/** Minimal settings pane: skip the "设置" group header (duplicates the pane title). */ -const MINIMAL_SETTINGS_HIDDEN_HEADERS = new Set(["nav.settings"]); +/** Minimal settings pane: skip the Capabilities group header (duplicates the pane title). */ +const MINIMAL_SETTINGS_HIDDEN_HEADERS = new Set(["nav.capabilities"]); function loadCollapsedGroups(): Set { try { @@ -190,7 +190,7 @@ function NavItemButton({ }} > {t(item.labelKey)} - {item.key === "admin-advanced" && role === "admin" && hasUpdate ? ( + {item.key === "system-settings" && role === "admin" && hasUpdate ? ( {t("nav.newVersionBadge", "有新版本")} @@ -254,7 +254,7 @@ function NavList({ isMobile?: boolean; isGroupCollapsed: (groupKey: string) => boolean; toggleGroup: (groupKey: string) => void; - /** all = classic; primary = top flat entries; grouped = settings/control/admin */ + /** all = classic; primary = top flat entries; grouped = capabilities */ sectionFilter?: "all" | "primary" | "grouped"; hideGroupHeaderKeys?: ReadonlySet; }) { diff --git a/dashboard/src/layouts/SidebarCollapsedIconNav.tsx b/dashboard/src/layouts/SidebarCollapsedIconNav.tsx index dbc2c5c7..e765151d 100644 --- a/dashboard/src/layouts/SidebarCollapsedIconNav.tsx +++ b/dashboard/src/layouts/SidebarCollapsedIconNav.tsx @@ -24,7 +24,7 @@ export default function SidebarCollapsedIconNav({ {items.map((item) => { const active = selectedKey === item.key; const showUpdateBadge = - item.key === "admin-advanced" && role === "admin" && hasUpdate; + item.key === "system-settings" && role === "admin" && hasUpdate; return ( { } expect(isGroupedNavKey("chat")).toBe(false); expect(isGroupedNavKey("experts")).toBe(false); + expect(isGroupedNavKey("system-settings")).toBe(false); + expect(isGroupedNavKey("workbench")).toBe(false); }); it("places grouped keys only under sections with groupKey", () => { @@ -42,4 +44,38 @@ describe("sidebarNav", () => { expect(isGroupedNavKey(key)).toBe(false); } }); + + it("renames settings to capabilities and folds control/admin into system-settings", () => { + const sections = buildNavSections(adminUser, { mobileEnabled: true }); + expect(sections.map((s) => s.groupKey)).toEqual([ + undefined, + "nav.capabilities", + ]); + const primary = sections[0].items.map((i) => i.key); + expect(primary).toContain("system-settings"); + expect(primary).not.toContain("workbench"); + expect(primary).not.toContain("admin-users"); + const grouped = sections[1].items.map((i) => i.key); + expect(grouped).toEqual([ + "personalization", + "channels", + "connectors", + "skill-packages", + "knowledge-bases", + ]); + }); + + it("hides system-settings when the user has no control or admin modules", () => { + const guest = { + id: 2, + username: "guest", + role: "user", + permissions: [], + } as OctopUser; + const sections = buildNavSections(guest); + const keys = sections.flatMap((s) => s.items.map((i) => i.key)); + expect(keys).not.toContain("system-settings"); + expect(keys).not.toContain("workbench"); + expect(keys).toContain("personalization"); + }); }); diff --git a/dashboard/src/layouts/sidebarNav.tsx b/dashboard/src/layouts/sidebarNav.tsx index 4ae19757..d75e90b2 100644 --- a/dashboard/src/layouts/sidebarNav.tsx +++ b/dashboard/src/layouts/sidebarNav.tsx @@ -1,27 +1,20 @@ import type { ReactNode } from "react"; import { - Monitor, MessageSquareText, Timer, SlidersHorizontal, Waypoints, Link2, Database, - Cpu, - Users as UsersIcon, Activity, - Share2, Sparkles, - Puzzle, Package, - HardDrive, GraduationCap, - Shield, - PanelsTopLeft, Building2, } from "lucide-react"; import type { OctopUser } from "../api/modules/auth"; -import { navAllowed, userCan } from "../utils/permissions"; +import { canSeeSystemSettings } from "../pages/SystemSettings/tabs"; +import { navAllowed } from "../utils/permissions"; export const EXPANDED_WIDTH = 220; export const COLLAPSED_WIDTH = 56; @@ -44,7 +37,7 @@ export interface NavSection { } /** - * Catalog of nav item keys that live under settings / control / admin groups. + * Catalog of nav item keys that live under the Capabilities group. * Permission-independent — used for pane/route helpers; visibility still * comes from {@link buildNavSections}. */ @@ -54,16 +47,6 @@ export const SIDEBAR_GROUPED_NAV_KEYS = [ "connectors", "skill-packages", "knowledge-bases", - "workbench", - "remote-desktop", - "acp", - "admin-users", - "models", - "admin-storage", - "admin-plugins", - "admin-security", - "admin-advanced", - "agent-config", ] as const; const GROUPED_NAV_KEY_SET = new Set(SIDEBAR_GROUPED_NAV_KEYS); @@ -109,6 +92,18 @@ export function buildNavSections( icon: , labelKey: "nav.tokenUsage", }, + ...(canSeeSystemSettings(user, { mobileEnabled: opts?.mobileEnabled }) + ? [ + { + key: "system-settings", + path: "/system-settings", + icon: ( + + ), + labelKey: "nav.systemSettings", + } satisfies NavItem, + ] + : []), ], }, ]; @@ -154,94 +149,7 @@ export function buildNavSections( }); } if (settingsItems.length > 0) { - sections.push({ groupKey: "nav.settings", items: settingsItems }); - } - - const controlItems: NavItem[] = []; - if (navAllowed(user, "workbench")) { - controlItems.push({ - key: "workbench", - path: "/workbench", - icon: , - labelKey: "nav.workbench", - }); - } - // Server desktop + phone share one nav entry; phone tab also needs host capability. - if ( - userCan(user, "desktop") || - (opts?.mobileEnabled && userCan(user, "mobile")) - ) { - controlItems.push({ - key: "remote-desktop", - path: "/remote-desktop", - icon: , - labelKey: "nav.remoteDesktop", - }); - } - // ACP: no module key this round — admin role only. - if (navAllowed(user, "acp")) { - controlItems.push({ - key: "acp", - path: "/acp", - icon: , - labelKey: "nav.acp", - }); - } - if (controlItems.length > 0) { - sections.push({ groupKey: "nav.control", items: controlItems }); - } - - const adminItems: NavItem[] = []; - if (navAllowed(user, "admin-users")) { - adminItems.push({ - key: "admin-users", - path: "/admin/users", - icon: , - labelKey: "nav.adminUsers", - }); - } - if (navAllowed(user, "models")) { - adminItems.push({ - key: "models", - path: "/admin/models", - icon: , - labelKey: "nav.models", - }); - } - if (navAllowed(user, "admin-storage")) { - adminItems.push({ - key: "admin-storage", - path: "/admin/backend", - icon: , - labelKey: "nav.adminStorage", - }); - } - if (navAllowed(user, "admin-plugins")) { - adminItems.push({ - key: "admin-plugins", - path: "/admin/plugins", - icon: , - labelKey: "nav.adminPlugins", - }); - } - if (navAllowed(user, "admin-security")) { - adminItems.push({ - key: "admin-security", - path: "/admin/security", - icon: , - labelKey: "nav.security", - }); - } - if (navAllowed(user, "admin-advanced")) { - adminItems.push({ - key: "admin-advanced", - path: "/admin/advanced", - icon: , - labelKey: "nav.adminAdvanced", - }); - } - if (adminItems.length > 0) { - sections.push({ groupKey: "nav.admin", items: adminItems }); + sections.push({ groupKey: "nav.capabilities", items: settingsItems }); } return sections; } diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index 2cf602ca..05afe2fa 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -76,7 +76,15 @@ "saveFailed": "Could not update organization module", "actionFailed": "Action failed", "enabled": "Organization module enabled", - "disabled": "Organization module disabled" + "disabled": "Organization module disabled", + "previewTitle": "openXYOS preview", + "previewOffline": "Sidecar is offline. Start it to preview the organization console here.", + "previewLoading": "Starting openXYOS…", + "fullscreen": "Fullscreen", + "exitFullscreen": "Exit fullscreen" + }, + "systemSettings": { + "subtitle": "Workbench, remote desktop, and host administration in one place." }, "desktopChrome": { "group": "Window controls", @@ -263,6 +271,8 @@ "skills": "Skills", "agentConfig": "Configuration", "settings": "Settings", + "capabilities": "Capabilities", + "systemSettings": "System Settings", "voice": "Voice", "observability": "Observability", "https": "HTTPS", diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index 6e255d06..76f87524 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -76,7 +76,15 @@ "saveFailed": "无法更新组织模块", "actionFailed": "操作失败", "enabled": "组织模块已启用", - "disabled": "组织模块已关闭" + "disabled": "组织模块已关闭", + "previewTitle": "openXYOS 预览", + "previewOffline": "边车未运行。启动后可在此预览组织控制台。", + "previewLoading": "正在启动 openXYOS…", + "fullscreen": "全屏", + "exitFullscreen": "退出全屏" + }, + "systemSettings": { + "subtitle": "工作台、远程桌面与主机管理集中在这里。" }, "desktopChrome": { "group": "窗口控制", @@ -263,6 +271,8 @@ "skills": "技能", "agentConfig": "运行配置", "settings": "设置", + "capabilities": "能力", + "systemSettings": "系统设置", "voice": "语音", "observability": "可观测", "https": "HTTPS", diff --git a/dashboard/src/pages/Organization/Organization.module.less b/dashboard/src/pages/Organization/Organization.module.less index 3e8fcd9d..05137182 100644 --- a/dashboard/src/pages/Organization/Organization.module.less +++ b/dashboard/src/pages/Organization/Organization.module.less @@ -4,15 +4,100 @@ gap: 20px; } +.workspace { + display: grid; + grid-template-columns: minmax(320px, 42%) minmax(0, 1fr); + gap: 16px; + align-items: stretch; + min-height: 0; +} + +.column { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 0; +} + +.preview { + display: flex; + flex-direction: column; + min-height: 560px; + border-radius: 16px; + border: 1px solid var(--fn-border-primary); + background: var(--fn-bg-primary); + overflow: hidden; +} + +.previewFullscreen { + position: fixed; + inset: 0; + z-index: 1200; + min-height: 0; + border-radius: 0; + background: var(--fn-bg-primary); +} + +.previewHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + border-bottom: 1px solid var(--fn-border-primary); +} + +.previewTitle { + margin: 0; + font-size: 15px; + font-weight: 650; +} + +.previewBody { + position: relative; + flex: 1; + min-height: 0; + background: #fff; +} + +.previewEmpty { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 12px; + padding: 24px; + background: var(--fn-bg-secondary); +} + +@media (max-width: 1100px) { + .workspace { + grid-template-columns: 1fr; + } + + .preview { + min-height: 420px; + } +} + .hero { position: relative; overflow: hidden; border-radius: 20px; padding: 28px 28px 24px; color: #fff; - background: - radial-gradient(1200px 400px at 10% -20%, rgba(107, 140, 255, 0.55), transparent 60%), - radial-gradient(800px 360px at 90% 120%, rgba(0, 51, 255, 0.45), transparent 55%), + background: radial-gradient( + 1200px 400px at 10% -20%, + rgba(107, 140, 255, 0.55), + transparent 60% + ), + radial-gradient( + 800px 360px at 90% 120%, + rgba(0, 51, 255, 0.45), + transparent 55% + ), linear-gradient(135deg, #001a80 0%, #0033ff 48%, #4b74fa 100%); box-shadow: 0 18px 48px rgba(0, 51, 255, 0.22); } @@ -215,7 +300,11 @@ border-radius: 16px; padding: 18px 20px; border: 1px solid rgba(245, 158, 11, 0.35); - background: linear-gradient(180deg, rgba(245, 158, 11, 0.08), var(--fn-bg-primary)); + background: linear-gradient( + 180deg, + rgba(245, 158, 11, 0.08), + var(--fn-bg-primary) + ); } .recoverTitle { @@ -274,10 +363,11 @@ } .embed { + display: block; width: 100%; - min-height: 640px; - border: 1px solid var(--fn-border-primary); - border-radius: 12px; + height: 100%; + min-height: 100%; + border: 0; background: #fff; } diff --git a/dashboard/src/pages/Organization/index.tsx b/dashboard/src/pages/Organization/index.tsx index e59cf2c9..07f61d60 100644 --- a/dashboard/src/pages/Organization/index.tsx +++ b/dashboard/src/pages/Organization/index.tsx @@ -4,6 +4,8 @@ import { ArrowDownUp, Building2, ExternalLink, + Maximize2, + Minimize2, Package, Play, RefreshCw, @@ -41,25 +43,55 @@ export default function OrganizationPage() { const [busy, setBusy] = useState(null); const [loopProof, setLoopProof] = useState(null); const [lastActionNotes, setLastActionNotes] = useState([]); + const [previewFullscreen, setPreviewFullscreen] = useState(false); + const [previewPending, setPreviewPending] = useState(false); + + const applyOverview = useCallback( + async (quiet = false) => { + if (!quiet) setLoading(true); + try { + const next = await orgModuleApi.overview(); + setOverview(next); + return next; + } catch (err) { + if (!quiet) { + message.error( + err instanceof Error ? err.message : t("organization.loadFailed"), + ); + } + return null; + } finally { + if (!quiet) setLoading(false); + } + }, + [t], + ); const load = useCallback(async () => { - setLoading(true); - try { - const next = await orgModuleApi.overview(); - setOverview(next); - } catch (err) { - message.error( - err instanceof Error ? err.message : t("organization.loadFailed"), - ); - } finally { - setLoading(false); - } - }, [t]); + await applyOverview(false); + }, [applyOverview]); useEffect(() => { void load(); }, [load]); + useEffect(() => { + if (overview?.sidecar_reachable) return; + const id = window.setInterval(() => { + void applyOverview(true); + }, 4000); + return () => window.clearInterval(id); + }, [applyOverview, overview?.sidecar_reachable]); + + useEffect(() => { + if (!previewFullscreen) return; + const onKey = (event: KeyboardEvent) => { + if (event.key === "Escape") setPreviewFullscreen(false); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [previewFullscreen]); + const sidecarUp = Boolean(overview?.sidecar_reachable); const firstRun = useMemo(() => { if (!overview) return false; @@ -77,7 +109,7 @@ export default function OrganizationPage() { message.success( enabled ? t("organization.enabled") : t("organization.disabled"), ); - await load(); + await applyOverview(false); } catch (err) { message.error( err instanceof Error ? err.message : t("organization.saveFailed"), @@ -87,11 +119,25 @@ export default function OrganizationPage() { } }; - const runAction = async (key: Exclude, fn: () => Promise) => { + const waitForSidecar = async () => { + for (let i = 0; i < 30; i += 1) { + const next = await applyOverview(true); + if (next?.sidecar_reachable) return next; + await new Promise((resolve) => window.setTimeout(resolve, 1000)); + } + return applyOverview(true); + }; + + const runAction = async ( + key: Exclude, + fn: () => Promise, + ) => { setBusy(key); try { await fn(); - await load(); + if (key !== "sidecar") { + await applyOverview(false); + } } catch (err) { message.error( err instanceof Error ? err.message : t("organization.actionFailed"), @@ -103,13 +149,21 @@ export default function OrganizationPage() { const startSidecar = () => runAction("sidecar", async () => { - const result = await orgModuleApi.startSidecar(); - setLastActionNotes([result.detail, result.command].filter(Boolean)); - message.success( - result.reachable - ? t("organization.sidecarStarted") - : t("organization.sidecarStartPending"), - ); + setPreviewPending(true); + 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"), + ); + } finally { + setPreviewPending(false); + } }); const assemble = () => @@ -149,10 +203,10 @@ export default function OrganizationPage() { ? formatServerIsoDateTime(overview.last_sync, timeZone) : t("organization.neverSynced"); const catalog = overview?.catalog ?? []; - const embedUrl = - overview?.enabled && sidecarUp - ? `${overview.sidecar_url.replace(/\/$/, "")}/` - : ""; + const previewUrl = overview?.sidecar_url + ? `${overview.sidecar_url.replace(/\/$/, "")}/` + : ""; + const showFrame = Boolean(sidecarUp && previewUrl); return ( {t("organization.heroStory")}

- {overview?.enabled - ? t("organization.on") - : t("organization.off")} + {overview?.enabled ? t("organization.on") : t("organization.off")} {sidecarUp @@ -187,288 +239,365 @@ export default function OrganizationPage() {
-
- - {t("organization.toggleLabel")} - void toggle(checked)} - /> -
- - {!sidecarUp && ( -
-

- {t("organization.startSidecarTitle")} -

-

- {t("organization.startSidecarBody")} -

- - - {overview?.start_command && ( - {overview.start_command} - )} - -
- )} - - {firstRun && ( -
-

{t("organization.emptyTitle")}

-

{t("organization.emptyBody")}

-
- )} - -
-
-
-
-

{t("organization.dataPlane")}

-

FreeOS

-
- -
-
-
- - {metric(overview?.freeos.employees)} - - - {t("organization.metricEmployees")} - -
-
- - {metric(overview?.freeos.org_skills)} - - - {t("organization.metricSkills")} - -
-
- - {metric(overview?.freeos.mcp)} - - - {t("organization.metricMcp")} - -
-
- - {metric(overview?.freeos.tasks)} - - - {t("organization.metricTasks")} - -
+
+
+
+ + {t("organization.toggleLabel")} + void toggle(checked)} + />
-
-
+

{t("organization.emptyBody")}

+ + )} -
- - - - - - -
+
+
+
+
+

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

+

FreeOS

+
+ +
+
+
+ + {metric(overview?.freeos.employees)} + + + {t("organization.metricEmployees")} + +
+
+ + {metric(overview?.freeos.org_skills)} + + + {t("organization.metricSkills")} + +
+
+ + {metric(overview?.freeos.mcp)} + + + {t("organization.metricMcp")} + +
+
+ + {metric(overview?.freeos.tasks)} + + + {t("organization.metricTasks")} + +
+
+
-
-

{t("organization.timelineTitle")}

- {lastLoop ? ( -
    -
  1. - {t("organization.timelineOk")}: {String(lastLoop.ok)} -
  2. - {lastLoop.employees?.length ? ( -
  3. - {t("organization.timelineEmployees")}:{" "} - {lastLoop.employees.join(", ")} -
  4. - ) : null} - {lastLoop.skills?.length ? ( -
  5. - {t("organization.timelineSkills")}: {lastLoop.skills.length} -
  6. - ) : null} -
  7. - {t("organization.timelineRemote")}:{" "} - {String(Boolean(lastLoop.remote_applied))} -
  8. -
  9. - {t("organization.timelineGovernance")}:{" "} - {String(Boolean(lastLoop.governance_blocked))} -
  10. - {(lastLoop.notes ?? []).slice(0, 6).map((note) => ( -
  11. {note}
  12. - ))} -
- ) : ( -

{t("organization.timelineEmpty")}

- )} - {lastActionNotes.length > 0 && ( -
    - {lastActionNotes.slice(0, 8).map((note) => ( -
  1. {note}
  2. - ))} -
- )} -
+ -
- {embedUrl && ( -
-
-

{t("organization.embedTitle")}

- +