diff --git a/CHANGELOG.md b/CHANGELOG.md index 44488688..64f966eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ ## [Unreleased] +### 新增 + +- Organization OS 首页双循环:FreeOS 数据面与 openXYOS 控制面的实时状态、一键装配员工 / 打包回流 / 运行 org loop,以及边车离线时的启动恢复。 + +### 变更 + +- 桌面更新通道只跟踪 `github.com/XYAIStudio/FreeOS/releases`。不再读取 PyPI `octop`、腾讯云 COS 或其它 Octop 镜像;当前已是最新 FreeOS `v0.0.1` 时不会误报更新。 +- 默认主题与残留 Octop `rose` 存储一次性迁移为 XYAI 蓝 `#0033FF`(之后仍可手动选玫瑰粉)。 + ### 修复 - Windows NSIS uninstall now stops FreeOS / host / sidecar processes and recursively removes `$INSTDIR` (quoted `RMDir /r`, plus a delayed cleanup after `uninstall.exe` exits). If those processes are still running, uninstall asks first (cancel aborts and leaves them running; confirm closes them then wipes the install dir). User profile data under `%USERPROFILE%\.freeos` / `FREEOS_HOME` / legacy `~/.octop` is kept. Product version remains 0.0.1. diff --git a/dashboard/index.html b/dashboard/index.html index 8e2fa568..c7ad1306 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -1,5 +1,5 @@ - + diff --git a/dashboard/src/api/modules/orgModule.ts b/dashboard/src/api/modules/orgModule.ts index 2706479e..717a2399 100644 --- a/dashboard/src/api/modules/orgModule.ts +++ b/dashboard/src/api/modules/orgModule.ts @@ -28,8 +28,99 @@ export interface OrgModuleStatus { notes: string[]; } +export interface OrgPlaneCounts { + employees: number; + employee_states: Record; + agents: number; + spawned_colleagues: number; + org_skills: number; + skill_packages: number; + mcp: number; + tasks: number; +} + +export interface OrgControlPlane { + reachable: boolean; + url: string; + detail: string; + modules: number; + governance: boolean; + tenant_id: string; + approvals: number; +} + +export interface OrgOverview { + enabled: boolean; + sidecar_reachable: boolean; + sidecar_url: string; + start_available: boolean; + start_command: string; + home: string; + last_sync: string | null; + freeos: OrgPlaneCounts; + openxyos: OrgControlPlane; + last_loop: Record | null; + notes: string[]; + catalog: OrgCapability[]; +} + +export interface OrgSidecarStart { + started: boolean; + already: boolean; + reachable: boolean; + url: string; + command: string; + detail: string; + launcher: string; +} + +export interface OrgAssembleResult { + sidecar_reachable: boolean; + employees: string[]; + spawned: Array<{ agent_id: string; slug: string; name: string }>; + imported: Record | null; + notes: string[]; +} + +export interface OrgPackResult { + pack: { + directory: string; + skill_count: number; + plugin_count: number; + mcp_count: number; + agent_count: number; + notes: string[]; + }; + applied: { + pack_dir: string; + mirror_dir: string; + remote_applied: boolean; + mirrored: boolean; + notes: string[]; + }; +} + +export interface OrgLoopProof { + ok: boolean; + tenant_id: string; + home: string; + skills: string[]; + employees: string[]; + agents: Array>; + lifecycle: Record; + pack_dir: string; + mirror_dir: string; + remote_applied: boolean; + imported_roundtrip: Record; + governance_blocked: boolean; + governance: Record; + notes: string[]; + updated_at?: string; +} + export const orgModuleApi = { status: () => request("/org-module/status"), + overview: () => request("/org-module/overview"), catalog: () => request<{ modules: OrgCapability[] }>("/org-module/catalog"), setEnabled: (enabled: boolean, sidecar_url?: string) => request("/org-module", { @@ -37,4 +128,15 @@ export const orgModuleApi = { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled, sidecar_url }), }), + startSidecar: () => + request("/org-module/sidecar/start", { method: "POST" }), + assemble: () => + request("/org-module/assemble", { method: "POST" }), + pack: () => request("/org-module/pack", { method: "POST" }), + runLoop: () => + request("/org-module/loop/run", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }), }; diff --git a/dashboard/src/api/modules/update.ts b/dashboard/src/api/modules/update.ts index eacde5c7..70703311 100644 --- a/dashboard/src/api/modules/update.ts +++ b/dashboard/src/api/modules/update.ts @@ -7,12 +7,12 @@ export interface UpdateStatus { is_editable: boolean; /** Non-null when the process was launched via `octop service start` (systemd or launchd). */ service_mode: "systemd" | "launchd" | null; - /** True when Octop is spawned by the Wails desktop shell (or ``OCTOP_DESKTOP=1``). */ + /** True when FreeOS is spawned by the Wails desktop shell (or ``OCTOP_DESKTOP=1``). */ desktop?: boolean; error: string | null; - /** Stable error code (e.g. "pypi_unreachable") for localized UI messages; null on success. */ + /** Stable error code (e.g. "github_unreachable") for localized UI messages; null on success. */ error_code: string | null; - /** Mirror that served the version info (null/`pypi.org` = official source). */ + /** GitHub Releases source label (``github.com/XYAIStudio/FreeOS``). */ source: string | null; last_check_time: string | null; /** Markdown changelog for latest_version, null if not available. */ diff --git a/dashboard/src/hooks/useUpdateStatus.ts b/dashboard/src/hooks/useUpdateStatus.ts index 76e8fb40..272181be 100644 --- a/dashboard/src/hooks/useUpdateStatus.ts +++ b/dashboard/src/hooks/useUpdateStatus.ts @@ -8,7 +8,7 @@ import { storeUpdateStatus, } from "../utils/updateStatusCache"; -/** Shared in-flight probe so Header + Sidebar mounts don't stampede PyPI. */ +/** Shared in-flight probe so Header + Sidebar mounts don't stampede GitHub. */ let inFlight: Promise | null = null; async function probeUpdateStatus(): Promise { @@ -57,7 +57,7 @@ export function useUpdateStatus() { return () => window.removeEventListener("focus", onFocus); }, [refreshStatus]); - // Keep checking while the dashboard stays open (cache TTL still gates PyPI). + // Keep checking while the dashboard stays open (cache TTL still gates GitHub). useEffect(() => { const id = window.setInterval(() => { void refreshStatus(false); diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index a2c99d7d..fd35d5ac 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -8,17 +8,60 @@ }, "organization": { "title": "Organization OS", - "subtitle": "Grow AI employees through the FreeOS ↔ openXYOS loop. Enable the organization control plane as a sidecar.", + "subtitle": "A dual loop: FreeOS does the work, openXYOS governs the org — they grow each other.", + "heroTitle": "Organization OS", + "heroStory": "FreeOS does the work. openXYOS owns organization and governance. They grow each other.", "toggleLabel": "Enable organization module", "on": "Enabled", "off": "Disabled", - "sidecarUp": "Sidecar reachable", + "sidecarUp": "Sidecar online", "sidecarDown": "Sidecar offline", - "bridgeHint": "FreeOS produces employees, skills, plugins, and MCPs. Those assets assemble into openXYOS; org blueprints and policies feed back into FreeOS. Run `freeos org loop run` to prove the loop.", + "lastSync": "Last sync", + "neverSynced": "Not synced yet", + "dataPlane": "Data plane", + "controlPlane": "Control plane", + "flowUp": "Assets back", + "flowDown": "Blueprints out", + "metricEmployees": "Digital employees", + "metricSkills": "Org skills", + "metricMcp": "MCP / connectors", + "metricTasks": "Tasks", + "metricModules": "Governance modules", + "metricGovernance": "Governance engine", + "metricHealth": "Control-plane health", + "metricTenant": "Tenant", + "assembleTitle": "Assemble employees from a blueprint", + "assembleBody": "Compile an openXYOS blueprint (or the bundled fixture) into FreeOS digital employees and register them for chat.", + "assembleAction": "Assemble into FreeOS", + "assembleDone": "Assembled {{count}} digital employee(s)", + "packTitle": "Pack assets back to openXYOS", + "packBody": "Publish FreeOS skills, plugins, and MCP as an asset pack, mirror it, and push to the control plane when live.", + "packAction": "Pack and apply", + "packDone": "Asset pack published", + "packRemoteYes": "Attempted a live write to openXYOS", + "packRemoteNo": "Sidecar offline: wrote the local mirror only", + "loopTitle": "Run the organization loop", + "loopBody": "Blueprint → employees → pack → import → governance block, with a real timeline below.", + "loopAction": "Run org loop", + "loopOk": "Organization loop completed", + "loopPartial": "Loop ran; some steps did not finish (see timeline)", + "timelineTitle": "Loop timeline", + "timelineEmpty": "No loop proof yet. Run the organization loop to see real results here.", + "timelineOk": "Loop result", + "timelineEmployees": "Employees", + "timelineSkills": "Skills", + "timelineRemote": "Pushed to control plane", + "timelineGovernance": "High-risk tool blocked by governance", + "emptyTitle": "First look at Organization OS", + "emptyBody": "FreeOS on the left owns employees, skills, MCP, and tasks. openXYOS on the right owns 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", + "bridgeHint": "FreeOS produces employees, skills, plugins, and MCPs. Those assets assemble into openXYOS; org blueprints and policies feed back into FreeOS.", "home": "Data home", "sidecar": "Sidecar URL", - "startSidecarTitle": "Start the organization sidecar", - "startSidecarBody": "The module is on, but openXYOS is not running. From the repo root:", "notesTitle": "How this module is wired", "catalogTitle": "openXYOS capabilities", "colKey": "Key", @@ -31,6 +74,7 @@ "openSidecar": "Open in new tab", "loadFailed": "Could not load organization module status", "saveFailed": "Could not update organization module", + "actionFailed": "Action failed", "enabled": "Organization module enabled", "disabled": "Organization module disabled" }, @@ -201,7 +245,7 @@ "common": "Common", "control": "Control", "experts": "Experts", - "organization": "Organization", + "organization": "Organization OS", "tasks": "Automation", "connectors": "Connectors", "acp": "ACP", @@ -3052,9 +3096,9 @@ }, "update": { "title": "Version & Updates", - "description": "Check for the latest release and upgrade in place; use the guide on the right for other install methods.", + "description": "Checks only XYAIStudio/FreeOS GitHub Releases. Does not track upstream Octop or Tencent Cloud install feeds.", "panelTitle": "Version upgrade", - "panelDesc": "Check for a new release and install it when supported. Restart the service after a successful upgrade.", + "panelDesc": "Prompts only when GitHub has a newer FreeOS desktop build. Restart the desktop app so the shell can apply the portable zip.", "stableOnlyLabel": "Stable releases only", "stableOnlyHint": "Automatic checks do not remind you about alpha, beta, rc, or dev builds. You can still check and upgrade to a pre-release manually.", "prereleaseBadge": "Pre-release", @@ -3072,19 +3116,21 @@ "updateAvailable": "New version available", "upgradeSuccess": "Upgrade completed!", "upgradeFailed": "Upgrade failed", - "restartHint": "Please restart Octop for the update to take effect.", + "restartHint": "Restart the FreeOS desktop app for the update to take effect.", "restartCommand": "Restart command", "editableHint": "Development install detected. Use git pull to update.", - "networkError": "Could not reach update server. Check your network.", - "mirrorSource": "Version info served by mirror {{source}}; it may lag slightly behind pypi.org.", + "networkError": "Could not reach GitHub Releases. Check your network.", + "githubSource": "Version info from {{source}} (XYAIStudio/FreeOS only).", + "mirrorSource": "Version info from {{source}}.", "errors": { - "pypi_unreachable": "Could not reach PyPI. Check your network and try again." + "github_unreachable": "Could not reach GitHub Releases for XYAIStudio/FreeOS. Check your network and try again.", + "pypi_unreachable": "Could not reach GitHub Releases for XYAIStudio/FreeOS. Check your network and try again." }, "notChecked": "Not checked yet", "stageStarting": "Starting...", "stageDownloading": "Downloading...", "stageInstalling": "Installing...", - "manualUpgradeHint": "You can also run: octop update --verbose", + "manualUpgradeHint": "You can also run: freeos update --verbose", "restartServiceBtn": "Restart Service", "restarting": "Restarting...", "restartingHint": "This typically takes 1-2 minutes. Please wait.", @@ -3093,41 +3139,33 @@ "restartCancel": "Cancel", "restartSuccess": "Restarted successfully, refreshing...", "restartReadyHint": "Service is back online. Refreshing shortly.", - "restartTimeout": "Restart timed out. Run manually: octop service restart", + "restartTimeout": "Restart timed out. Quit and reopen the FreeOS desktop app.", "restartFailed": "Restart request failed. Check the service and try again.", "releaseNotes": "Release Notes", "updateModalTitle": "New Version Available", - "guideTitle": "Update by install method", - "guideIntro": "If the one-click upgrade on the left is unavailable, pick the commands for your install method. Restart after upgrading; data stays in ~/.octop (or OCTOP_HOME).", - "guideDataNote": "Upgrades do not wipe local data: accounts, agents, chats and memory, skills, channels, and model settings live in the working directory. Reinstalling or updating the package keeps using that data.", + "guideTitle": "Update FreeOS from GitHub", + "guideIntro": "FreeOS only fetches desktop builds from github.com/XYAIStudio/FreeOS/releases. Do not install upstream Octop, and do not upgrade from Tencent COS or PyPI.", + "guideDataNote": "Upgrades do not wipe local data: accounts, agents, chats, and memory stay in ~/.freeos (or FREEOS_HOME).", "guide": { "ui": { "title": "In-console upgrade", - "body": "Prefer Check for Updates and Upgrade Now on the left (package installs, not editable/source). In service mode you can Restart Service when done." + "body": "Prefer Check for Updates and Upgrade Now on the left. Desktop builds download a FreeOS portable zip; the shell applies it on the next launch." }, - "installer": { - "title": "One-line installer", - "body": "If you used the official install script, re-run the same install command to upgrade in place (it reuses the environment and data under ~/.octop)." + "github": { + "title": "GitHub Releases", + "body": "Manually download the FreeOS-desktop / FreeOS-portable asset for your architecture:" }, "cli": { - "title": "CLI: octop update", - "body": "If the octop command is on your PATH (installer or managed venv), run:" - }, - "pip": { - "title": "pip install", - "body": "If you installed into your own Python environment with pip:" + "title": "CLI: freeos update", + "body": "Check or stage a newer FreeOS desktop build from GitHub:" }, "source": { "title": "From source", - "body": "For git / editable installs, in-page upgrade is disabled. Pull the latest code, build the dashboard, then reinstall:" - }, - "docker": { - "title": "Docker", - "body": "Rebuild and restart the image, and keep the same data volume (~/.octop or octop-data) so nothing is lost:" + "body": "Pull XYAIStudio/FreeOS and rebuild the dashboard:" }, "restart": { "title": "Restart after upgrade", - "body": "The new package only takes effect after the process restarts. Use the command that matches how you launched Octop:" + "body": "Quit and reopen the desktop app so the shell can extract pending-portable.zip and keep FREEOS_STAMP." } } } diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index d7f4771f..618ff092 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -8,17 +8,60 @@ }, "organization": { "title": "组织 OS", - "subtitle": "通过 FreeOS ↔ openXYOS 闭环生产 AI 员工。将组织控制面作为边车启用。", + "subtitle": "FreeOS 数据面与 openXYOS 控制面的双循环:干活与治理互相生长。", + "heroTitle": "组织 OS", + "heroStory": "FreeOS 负责干活,openXYOS 负责组织与治理,两者互相生长。", "toggleLabel": "启用组织模块", "on": "已启用", "off": "已关闭", - "sidecarUp": "边车可达", + "sidecarUp": "边车在线", "sidecarDown": "边车未运行", - "bridgeHint": "FreeOS 生产员工、技能、插件与 MCP;这些资产组装进 openXYOS,组织蓝图与策略再回流到 FreeOS。用 `freeos org loop run` 跑通闭环。", + "lastSync": "最近同步", + "neverSynced": "尚未同步", + "dataPlane": "数据面", + "controlPlane": "控制面", + "flowUp": "资产回流", + "flowDown": "蓝图下发", + "metricEmployees": "数字员工", + "metricSkills": "组织技能", + "metricMcp": "MCP / 连接器", + "metricTasks": "任务", + "metricModules": "治理模块", + "metricGovernance": "治理引擎", + "metricHealth": "控制面健康", + "metricTenant": "租户", + "assembleTitle": "从蓝图装配员工", + "assembleBody": "把 openXYOS 蓝图(或本地示例蓝图)编译成 FreeOS 数字员工,并注册到对话列表。", + "assembleAction": "装配到 FreeOS", + "assembleDone": "已装配 {{count}} 名数字员工", + "packTitle": "打包回流 openXYOS", + "packBody": "把 FreeOS 技能、插件与 MCP 打成资产包,镜像并尽量推回控制面。", + "packAction": "打包回流", + "packDone": "资产包已发布", + "packRemoteYes": "已尝试写入 live openXYOS", + "packRemoteNo": "边车离线:仅写入本地镜像", + "loopTitle": "运行组织循环", + "loopBody": "一键跑通蓝图 → 员工 → 打包 → 回流 → 治理拦截,并在下方展示真实时间线。", + "loopAction": "运行 org loop", + "loopOk": "组织循环已跑通", + "loopPartial": "循环已执行,部分步骤未完成(见时间线)", + "timelineTitle": "循环时间线", + "timelineEmpty": "还没有循环记录。运行组织循环后,这里会显示真实结果。", + "timelineOk": "循环结果", + "timelineEmployees": "员工", + "timelineSkills": "技能数", + "timelineRemote": "已推送到控制面", + "timelineGovernance": "高风险工具被治理拦截", + "emptyTitle": "第一次打开组织 OS", + "emptyBody": "左侧 FreeOS 负责员工、技能、MCP 与任务;右侧 openXYOS 负责蓝图、模块与审批。先启动边车,或直接运行组织循环看一次真实闭环。", + "startSidecarTitle": "openXYOS 边车未运行", + "startSidecarBody": "控制面离线时无法做 live 审批与远程回流。可一键启动本机边车;若启动器不在当前环境,请按旁边的命令手动启动。", + "startSidecarAction": "启动边车", + "sidecarStarted": "边车已可达", + "sidecarStartPending": "已尝试启动,请稍候再刷新", + "bridgeHint": "FreeOS 生产员工、技能、插件与 MCP;这些资产组装进 openXYOS,组织蓝图与策略再回流到 FreeOS。", "home": "数据目录", "sidecar": "边车地址", - "startSidecarTitle": "启动组织边车", - "startSidecarBody": "模块已打开,但 openXYOS 尚未运行。在仓库根目录执行:", "notesTitle": "模块如何接入", "catalogTitle": "openXYOS 能力目录", "colKey": "键", @@ -31,6 +74,7 @@ "openSidecar": "新窗口打开", "loadFailed": "无法加载组织模块状态", "saveFailed": "无法更新组织模块", + "actionFailed": "操作失败", "enabled": "组织模块已启用", "disabled": "组织模块已关闭" }, @@ -201,7 +245,7 @@ "common": "常用", "control": "控制", "experts": "专家", - "organization": "组织", + "organization": "组织 OS", "tasks": "自动化", "connectors": "连接器", "acp": "ACP", @@ -3049,9 +3093,9 @@ }, "update": { "title": "版本与更新", - "description": "检查最新版本,一键升级 Octop;右侧可按安装方式查看完整升级说明。", + "description": "仅检查 XYAIStudio/FreeOS 的 GitHub Releases。不会跟踪上游 Octop 或腾讯云安装源。", "panelTitle": "版本升级", - "panelDesc": "检查是否有新版本,并在支持时一键安装。更新完成后请重启服务。", + "panelDesc": "仅当 GitHub 上有更新的 FreeOS 桌面构建时才提示升级。升级后请重启桌面应用以应用便携包。", "stableOnlyLabel": "只更新稳定版", "stableOnlyHint": "自动检查不会提醒测试版(alpha / beta / rc / dev)。仍可手动检查并升级到测试版。", "prereleaseBadge": "测试版", @@ -3069,19 +3113,21 @@ "updateAvailable": "发现新版本", "upgradeSuccess": "升级完成!", "upgradeFailed": "升级失败", - "restartHint": "请重启 Octop 使更新生效。", + "restartHint": "请重启 FreeOS 桌面应用使更新生效。", "restartCommand": "重启命令", "editableHint": "检测到开发版安装,请使用 git pull 更新。", - "networkError": "无法连接更新服务器,请检查网络。", - "mirrorSource": "版本信息来自镜像 {{source}},可能略滞后于官方源。", + "networkError": "无法连接 GitHub Releases,请检查网络。", + "githubSource": "版本信息来自 {{source}}(仅 XYAIStudio/FreeOS)。", + "mirrorSource": "版本信息来自 {{source}}。", "errors": { - "pypi_unreachable": "无法连接 PyPI,请检查网络后重试。" + "github_unreachable": "无法连接 GitHub Releases(XYAIStudio/FreeOS),请检查网络后重试。", + "pypi_unreachable": "无法连接 GitHub Releases(XYAIStudio/FreeOS),请检查网络后重试。" }, "notChecked": "尚未检查", "stageStarting": "启动中...", "stageDownloading": "下载中...", "stageInstalling": "安装中...", - "manualUpgradeHint": "也可手动运行:octop update --verbose", + "manualUpgradeHint": "也可手动运行:freeos update --verbose", "restartServiceBtn": "重启服务", "restarting": "正在重启...", "restartingHint": "预计需要 1-2 分钟完成,请耐心等待", @@ -3090,41 +3136,33 @@ "restartCancel": "取消", "restartSuccess": "重启成功,正在刷新页面...", "restartReadyHint": "服务已恢复,页面即将自动刷新。", - "restartTimeout": "重启超时,请手动执行:octop service restart", + "restartTimeout": "重启超时,请退出并重新打开 FreeOS 桌面应用", "restartFailed": "重启请求失败,请检查服务状态后重试", "releaseNotes": "更新说明", "updateModalTitle": "发现新版本", - "guideTitle": "按安装方式更新", - "guideIntro": "若不能使用左侧一键升级,请按你的安装方式选择下方命令。升级后请重启服务;配置与数据默认保留在 ~/.octop(或 OCTOP_HOME)。", - "guideDataNote": "升级不会清空本机数据:用户账号、Agent、对话与记忆、技能、通道与模型配置等均保存在工作目录中,重新安装或更新软件本身即可继续使用。", + "guideTitle": "从 GitHub 更新 FreeOS", + "guideIntro": "FreeOS 只从 github.com/XYAIStudio/FreeOS/releases 获取桌面构建。不要安装上游 Octop,也不要从腾讯云 COS / PyPI 升级。", + "guideDataNote": "升级不会清空本机数据:账号、Agent、对话与记忆仍在 ~/.freeos(或 FREEOS_HOME)。", "guide": { "ui": { "title": "控制台一键升级", - "body": "优先使用左侧「检查更新」与「立即升级」(非源码/可编辑安装)。服务模式可在完成后点「重启服务」。" + "body": "优先使用左侧「检查更新」与「立即升级」。桌面版会下载 FreeOS portable zip,下次启动时由壳层应用。" }, - "installer": { - "title": "一键安装脚本", - "body": "若你是用官方安装脚本装的,重新执行同一安装命令即可自动升级到最新版(会复用 ~/.octop 下的环境与数据)。" + "github": { + "title": "GitHub Releases", + "body": "手动下载当前架构的 FreeOS-desktop / FreeOS-portable 资源:" }, "cli": { - "title": "CLI:octop update", - "body": "若命令行可直接使用 octop(安装脚本或 venv 布局),在终端执行:" - }, - "pip": { - "title": "pip 安装", - "body": "若你是用 pip 装到自己的 Python 环境,可升级包本身:" + "title": "CLI:freeos update", + "body": "命令行检查或暂存 GitHub 上的更新:" }, "source": { "title": "源码安装", - "body": "从 Git 源码开发/editable 安装时,本页一键升级不可用。请拉取最新代码、构建控制台前端后重新安装到当前环境:" - }, - "docker": { - "title": "Docker", - "body": "Docker 部署请重新构建并启动镜像,并继续挂载原有数据卷(~/.octop 或 octop-data),避免丢数据:" + "body": "从 XYAIStudio/FreeOS 拉取源码并重新构建控制台:" }, "restart": { "title": "升级后重启", - "body": "包更新完成后需要重启进程才生效。任选与你的启动方式匹配的命令:" + "body": "桌面应用需退出再打开,壳层才会解压 pending-portable.zip 并保留 FREEOS_STAMP。" } } } diff --git a/dashboard/src/pages/Organization/Organization.module.less b/dashboard/src/pages/Organization/Organization.module.less new file mode 100644 index 00000000..3e8fcd9d --- /dev/null +++ b/dashboard/src/pages/Organization/Organization.module.less @@ -0,0 +1,295 @@ +.page { + display: flex; + flex-direction: column; + gap: 20px; +} + +.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%), + linear-gradient(135deg, #001a80 0%, #0033ff 48%, #4b74fa 100%); + box-shadow: 0 18px 48px rgba(0, 51, 255, 0.22); +} + +.heroTitle { + margin: 0 0 8px; + font-size: 28px; + font-weight: 720; + letter-spacing: -0.03em; +} + +.heroStory { + margin: 0; + max-width: 720px; + font-size: 16px; + line-height: 1.6; + color: rgba(255, 255, 255, 0.9); +} + +.heroMeta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 16px; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 999px; + padding: 4px 10px; + font-size: 12px; + background: rgba(255, 255, 255, 0.14); + border: 1px solid rgba(255, 255, 255, 0.18); +} + +.dual { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 16px; + align-items: stretch; +} + +@media (max-width: 960px) { + .dual { + grid-template-columns: 1fr; + } + + .flow { + min-height: 72px; + } +} + +.plane { + border-radius: 16px; + padding: 20px; + background: var(--fn-bg-primary); + border: 1px solid var(--fn-border-primary); + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04); +} + +.planeHead { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.planeLabel { + margin: 0; + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fn-text-tertiary); +} + +.planeTitle { + margin: 2px 0 0; + font-size: 20px; + font-weight: 680; +} + +.metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.metric { + border-radius: 12px; + padding: 12px; + background: var(--fn-bg-secondary); +} + +.metricValue { + display: block; + font-size: 24px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--fn-text-brand); +} + +.metricLabel { + font-size: 12px; + color: var(--fn-text-secondary); +} + +.flow { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 88px; + color: var(--fn-text-brand); +} + +.flowArrow { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; +} + +.orbit { + width: 56px; + height: 56px; + border-radius: 50%; + border: 2px dashed rgba(0, 51, 255, 0.28); + display: grid; + place-items: center; + margin: 8px 0; + animation: spin 12s linear infinite; +} + +.orbitCore { + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--fn-color-brand); + box-shadow: 0 0 0 6px rgba(0, 51, 255, 0.12); +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: reduce) { + .orbit { + animation: none; + } +} + +.actions { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +@media (max-width: 960px) { + .actions { + grid-template-columns: 1fr; + } +} + +.action { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + text-align: left; + min-height: 148px; + border-radius: 16px; + padding: 16px; + border: 1px solid var(--fn-border-primary); + background: var(--fn-bg-primary); +} + +.actionTitle { + margin: 0; + font-size: 16px; + font-weight: 650; +} + +.actionBody { + margin: 0; + flex: 1; + font-size: 13px; + line-height: 1.5; + color: var(--fn-text-secondary); +} + +.recover { + 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)); +} + +.recoverTitle { + margin: 0 0 6px; + font-size: 16px; + font-weight: 680; +} + +.recoverBody { + margin: 0 0 12px; + color: var(--fn-text-secondary); +} + +.timeline { + border-radius: 16px; + padding: 18px 20px; + background: var(--fn-bg-primary); + border: 1px solid var(--fn-border-primary); +} + +.timelineTitle { + margin: 0 0 12px; + font-size: 16px; + font-weight: 680; +} + +.timelineList { + margin: 0; + padding-left: 18px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.catalog { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 10px; +} + +.catalogItem { + border-radius: 12px; + padding: 12px; + background: var(--fn-bg-secondary); +} + +.catalogName { + margin: 0 0 4px; + font-weight: 650; +} + +.catalogDesc { + margin: 0; + font-size: 12px; + color: var(--fn-text-secondary); +} + +.embed { + width: 100%; + min-height: 640px; + border: 1px solid var(--fn-border-primary); + border-radius: 12px; + background: #fff; +} + +.empty { + border-radius: 16px; + padding: 22px; + background: var(--fn-color-brand-bg, rgba(0, 51, 255, 0.06)); + border: 1px dashed rgba(0, 51, 255, 0.28); +} + +.emptyTitle { + margin: 0 0 8px; + font-size: 18px; + font-weight: 680; +} diff --git a/dashboard/src/pages/Organization/index.tsx b/dashboard/src/pages/Organization/index.tsx index 4eed2ebd..e59cf2c9 100644 --- a/dashboard/src/pages/Organization/index.tsx +++ b/dashboard/src/pages/Organization/index.tsx @@ -1,37 +1,52 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Space, Switch, Tag } from "antd"; import { - Alert, - Button, - Card, - Space, - Switch, - Table, - Tag, - Typography, -} from "antd"; -import { Building2, ExternalLink, RefreshCw } from "lucide-react"; + ArrowDownUp, + Building2, + ExternalLink, + Package, + Play, + RefreshCw, + Shield, + Users, + Workflow, +} from "lucide-react"; import { useTranslation } from "react-i18next"; import PageShell from "../../layouts/PageShell"; import { orgModuleApi, - type OrgCapability, - type OrgModuleStatus, + type OrgAssembleResult, + type OrgLoopProof, + type OrgOverview, + type OrgPackResult, } from "../../api/modules/orgModule"; +import { formatServerIsoDateTime } from "../../utils/formatMessageTime"; +import { useServerTimezone } from "../../hooks/useServerTimezone"; import { message } from "../../utils/antdMessage"; +import styles from "./Organization.module.less"; -const { Paragraph, Text } = Typography; +type ActionKey = "assemble" | "pack" | "loop" | "sidecar" | null; + +function metric(value: number | undefined): string { + return typeof value === "number" ? String(value) : "—"; +} export default function OrganizationPage() { const { t, i18n } = useTranslation(); const isZh = i18n.language?.toLowerCase().startsWith("zh") ?? false; - const [status, setStatus] = useState(null); + const timeZone = useServerTimezone(); + const [overview, setOverview] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); + const [busy, setBusy] = useState(null); + const [loopProof, setLoopProof] = useState(null); + const [lastActionNotes, setLastActionNotes] = useState([]); const load = useCallback(async () => { setLoading(true); try { - setStatus(await orgModuleApi.status()); + const next = await orgModuleApi.overview(); + setOverview(next); } catch (err) { message.error( err instanceof Error ? err.message : t("organization.loadFailed"), @@ -45,13 +60,24 @@ export default function OrganizationPage() { void load(); }, [load]); + const sidecarUp = Boolean(overview?.sidecar_reachable); + const firstRun = useMemo(() => { + if (!overview) return false; + return ( + overview.freeos.employees === 0 && + overview.freeos.spawned_colleagues === 0 && + !overview.last_loop + ); + }, [overview]); + const toggle = async (enabled: boolean) => { setSaving(true); try { - setStatus(await orgModuleApi.setEnabled(enabled)); + await orgModuleApi.setEnabled(enabled); message.success( enabled ? t("organization.enabled") : t("organization.disabled"), ); + await load(); } catch (err) { message.error( err instanceof Error ? err.message : t("organization.saveFailed"), @@ -61,10 +87,72 @@ export default function OrganizationPage() { } }; - const catalog: OrgCapability[] = status?.catalog ?? []; - const sidecarUp = Boolean(status?.sidecar.reachable); - const embedUrl = status?.enabled && sidecarUp ? status.embed_url : ""; - const showEmbed = Boolean(embedUrl); + const runAction = async (key: Exclude, fn: () => Promise) => { + setBusy(key); + try { + await fn(); + await load(); + } catch (err) { + message.error( + err instanceof Error ? err.message : t("organization.actionFailed"), + ); + } finally { + setBusy(null); + } + }; + + 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"), + ); + }); + + const assemble = () => + runAction("assemble", async () => { + const result: OrgAssembleResult = await orgModuleApi.assemble(); + setLastActionNotes(result.notes); + message.success( + t("organization.assembleDone", { count: result.spawned.length }), + ); + }); + + const packBack = () => + runAction("pack", async () => { + const result: OrgPackResult = await orgModuleApi.pack(); + setLastActionNotes([ + ...result.pack.notes, + ...result.applied.notes, + result.applied.remote_applied + ? t("organization.packRemoteYes") + : t("organization.packRemoteNo"), + ]); + message.success(t("organization.packDone")); + }); + + const runLoop = () => + runAction("loop", async () => { + const proof = await orgModuleApi.runLoop(); + setLoopProof(proof); + setLastActionNotes(proof.notes); + message.success( + proof.ok ? t("organization.loopOk") : t("organization.loopPartial"), + ); + }); + + const lastLoop = (loopProof ?? overview?.last_loop) as OrgLoopProof | null; + const lastSync = overview?.last_sync + ? formatServerIsoDateTime(overview.last_sync, timeZone) + : t("organization.neverSynced"); + const catalog = overview?.catalog ?? []; + const embedUrl = + overview?.enabled && sidecarUp + ? `${overview.sidecar_url.replace(/\/$/, "")}/` + : ""; return ( } > - - - - -
- - {t("organization.toggleLabel")} - void toggle(checked)} - /> - - {status?.enabled +
+
+

{t("organization.heroTitle")}

+

{t("organization.heroStory")}

+
+ + {overview?.enabled + ? t("organization.on") + : t("organization.off")} + + + {sidecarUp + ? t("organization.sidecarUp") + : t("organization.sidecarDown")} + + + {t("organization.lastSync")}: {lastSync} + +
+
+ +
+ + {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")} + +
+
+
+ +
- {status?.notes?.length ? ( - - {status.notes.map((note) => ( -
  • {note}
  • - ))} - - } - /> - ) : null} - - - - rowKey="key" - size="small" - pagination={false} - dataSource={catalog} - columns={[ - { - title: t("organization.colKey"), - dataIndex: "key", - width: 140, - render: (key: string) => {key}, - }, - { - title: t("organization.colName"), - render: (_, row) => (isZh ? row.label_zh : row.label), - }, - { - title: t("organization.colDesc"), - render: (_, row) => - isZh ? row.description_zh : row.description, - }, - { - title: t("organization.colLock"), - dataIndex: "locked", - width: 100, - render: (locked: boolean) => - locked ? ( - {t("organization.locked")} - ) : ( - {t("organization.optional")} - ), - }, - ]} - /> - +
    + + + + + + +
    + +
    +

    {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. + ))} +
    + )} +
    - {showEmbed && ( - +

    {t("organization.catalogTitle")}

    +
    + {catalog.map((row) => ( +
    +

    + {isZh ? row.label_zh : row.label}{" "} + {row.locked ? t("organization.locked") : t("organization.optional")} +

    +

    + {isZh ? row.description_zh : row.description} +

    +
    + ))} +
    + + + {embedUrl && ( +
    +
    +

    {t("organization.embedTitle")}

    - } - > +