diff --git a/llmdoc/dashboard/canvas-architecture.mdx b/llmdoc/dashboard/canvas-architecture.mdx index b5fdf55e..6ef15baa 100644 --- a/llmdoc/dashboard/canvas-architecture.mdx +++ b/llmdoc/dashboard/canvas-architecture.mdx @@ -1,5 +1,5 @@ --- -description: Dashboard 架构:纯 HTTP 客户端边界、canvas 分层、三挂载入口的 credential rollback/OAuth 编排、离线设备可发现性、Store 与 profile 隔离。 +description: Dashboard 工作台架构:工具列表与独立调用深链接、全局命令搜索、本机收藏与历史隔离、能力树兼容、挂载授权完成态及纯 HTTP 客户端边界。 kind: architecture relations: related: @@ -27,6 +27,17 @@ code: - packages/dashboard/src/lib/management.ts - packages/dashboard/src/App.tsx - packages/dashboard/src/components/layout/navigation.ts + - packages/dashboard/src/pages/WorkspacePage.tsx + - packages/dashboard/src/pages/ToolsPage.tsx + - packages/dashboard/src/pages/ToolPage.tsx + - packages/dashboard/src/components/CommandPalette.tsx + - packages/dashboard/src/components/add-tool/CatalogConfigStep.tsx + - packages/dashboard/src/components/add-tool/MountCompletion.tsx + - packages/dashboard/src/lib/favorites.ts + - packages/dashboard/src/lib/useFavorites.ts + - packages/dashboard/src/lib/toolNavigation.ts + - packages/dashboard/src/lib/session.tsx + - packages/dashboard/src/index.css - packages/dashboard/src/lib/queries.ts - packages/dashboard/src/lib/store.ts - packages/dashboard/src/lib/store-queries.ts @@ -68,7 +79,7 @@ code: - packages/sdk/src/store/wire.ts --- -# Dashboard 画布架构 +# Dashboard 工作台与画布架构 Dashboard 是纯 HTTP 客户端,不 import core。固定控制面 wire 类型与 runtime parser 从 `@tool-bridge/sdk/client` 消费;`packages/dashboard/src/lib/types.ts` 只保留 Dashboard 的组合/展示类型和尚未属于固定面的动态形状。接口变更仍要同轮更新 core schema、SDK parser、CLI/Dashboard 适配与 fixture。 @@ -77,34 +88,35 @@ Dashboard 是纯 HTTP 客户端,不 import core。固定控制面 wire 类型与 - `lib/api.ts`:profile/session 到 `@tool-bridge/sdk/client` 的 browser 适配、Dashboard API error 桥接,以及尚未进入固定 client 的动态 HTTP 能力。通用 URL、timeout、错误 body 与固定响应校验不在此重写。 - `lib/queries.ts`:TanStack Query key、分页、mutation 与失效。 - `lib/session*`:profile、认证、连接上下文。 -- `canvas/`:工作区骨架与画布。`WorkspaceShell`(顶栏 + 库务栏 + Outlet)、`CanvasPage`(index 与 `/nodes/*` 同一组件:左画布 + 右 Inspector 抽屉,选中态由 URL 驱动)、`TreeCanvas`(React Flow 渲染)、`NodeInspector`(选中节点后右侧滑出,原样复用 node 子组件)。`treeGraph.ts` / `useCanvasTree.ts` 是无 DOM 的纯逻辑(工作树→图布局、展开集合、按需懒加载合并),性能边界:root depth=1、本地 lazy=1、remote lazy=3。 +- `WorkspaceShell`:统一导航、连接上下文、全局搜索与添加工具入口;工作台优先呈现日常使用入口,实例管理保留独立导航。`WorkspacePage` 承接本机收藏与最近使用,`ToolsPage` 按当前目录加载一层可见树和 owner help,避免为了列表预取全部远端命令;`ToolPage` 只定位命令,调用逻辑复用 `CmdPanel`。 +- `canvas/`:能力树与节点检查。`CanvasPage`(`/canvas` 与兼容 `/nodes/*`:左画布 + 右 Inspector 抽屉,选中态由 URL 驱动)、`TreeCanvas`(React Flow 渲染)、`NodeInspector`(选中节点后右侧滑出,原样复用 node 子组件)。`treeGraph.ts` / `useCanvasTree.ts` 是无 DOM 的纯逻辑(工作树→图布局、展开集合、按需懒加载合并),性能边界:root depth=1、本地 lazy=1、remote lazy=3。 - 节点动作 ownership:`CanvasNode` 只展示并上报语义动作,`TreeCanvas` 编排展开、懒加载与选中,`CanvasPage` 持有挂载/卸载 mutation 及确认交互;可操作性在图 view model 层统一派生。 -- `components/node/`:Inspector 内的节点子组件。`CommandWorkspace`(整宽命令目录,点命令开 Dialog)、`CmdPanel`(单条 cmd 调用面板,accordion 折叠行 / dialog 弹窗两态)、`ResultView`、`CliHint`、反馈与 note。`CmdPanel`、实际 `invoke` 与 `CliHint` 共同信任 owner `~help` 返回的 `cmds[].path`——它已是含命令叶子的完整直连路径,除前导斜杠规范化外必须原样传递:不得用 owner path + `cmd.name` 重建、不得另传 `tool`、不得把 body 包成信封。设备 command metadata 的 `delivery` 只决定可选 policy;CmdPanel、SDK invoke 与 CliHint 统一在同一次普通 POST / `tb call` 中加入顶层 `~delivery`,不生成第二个 enqueue URL。 -- `components/add-tool/`:「添加工具」向导,按来源分流;不重复造挂载逻辑,内置集成走 `buildIntegrationCalls`、自定义 kind 走 `buildRegistryMountCalls`。`mountDiagnostics.ts` 把真实 `system/registry write` 的失败按 TBError code 归类,不另起与真实挂载不一致的 dry-run 探测。真正的 secret→registry→条件回滚链由 `pages/system/forms/mountOrchestration.ts` 单点持有,`useMountRunner`、`MountDialog` 与 `IntegrationDialog` 只负责表单/交互适配。 +- `components/node/`:Inspector 内的节点子组件。`CommandWorkspace`(命令目录,选择命令进入独立调用页;旧节点深链接仍可自动打开 Dialog)、`CmdPanel`(单条 cmd 的 accordion / dialog / page 三种展示,共用表单、确认与执行)、`ResultView`、`CliHint`、反馈与 note。`CmdPanel`、实际 `invoke` 与 `CliHint` 共同信任 owner `~help` 返回的 `cmds[].path`——它已是含命令叶子的完整直连路径,除前导斜杠规范化外必须原样传递:不得用 owner path + `cmd.name` 重建、不得另传 `tool`、不得把 body 包成信封。设备 command metadata 的 `delivery` 只决定可选 policy;CmdPanel、SDK invoke 与 CliHint 统一在同一次普通 POST / `tb call` 中加入顶层 `~delivery`,不生成第二个 enqueue URL。 +- `components/add-tool/`:「添加工具」向导,通过受控 open/onOpenChange 供全局入口复用,也支持自带 trigger,按来源分流;不重复造挂载逻辑,内置集成走 `buildIntegrationCalls`、自定义 kind 走 `buildRegistryMountCalls`。`mountDiagnostics.ts` 把真实 `system/registry write` 的失败按 TBError code 归类,不另起与真实挂载不一致的 dry-run 探测。真正的 secret→registry→条件回滚链由 `pages/system/forms/mountOrchestration.ts` 单点持有,`useMountRunner`、`MountDialog` 与 `IntegrationDialog` 只负责表单/交互适配。 - `pages/system/`:SK、secret、registry、plugin、catalog、federation、annotation。`pages/system/forms/`:将表单状态编译成 wire payload 的纯函数。 - `pages/SearchPage.tsx` 通过 SDK fixed wire 执行 compact search,展示 coverage、effect、source 与 partial 状态,并把 federation/matching/minCoverage/pathPrefix/effects 编译集中在 `lib/toolSearch.ts`;UI 不按 - description 猜 effect,也不在客户端对服务端排序二次打分。 + description 猜 effect,也不在客户端对服务端排序二次打分。`CommandPalette` 复用同一服务端命令搜索,关闭 cmdk 客户端过滤;本地页面/操作/节点匹配与服务端命令结果保持各自语义,partial 和失败不能呈现为完整成功。 - `pages/system/StorePage.tsx` 对应 `/manage/store`,是独立 deployment Store 管理面,不属于 canvas/Registry tree,也不复用 ContextBrowser。`lib/store.ts` 把 browser File/session 适配到 `@tool-bridge/sdk/store`; create→relay/direct PUT→direct complete、descriptor/ref parser 与敏感错误裁剪的真源在 SDK。 `lib/store-queries.ts` 负责 owner 列表分页和 mutation,并按当前 profile 失效共享 query cache。 -- `pages/system/DevicesPage.tsx` 在 presence 表下装配 `DeviceMailboxPanel`:按 deviceId 分页列出 operation, +- `pages/system/DevicesPage.tsx` 以设备列表为主,`?connect=1` 驱动接入 Dialog,关闭时移除该 query;在 presence 表下装配 `DeviceMailboxPanel`:按 deviceId 分页列出 operation, 支持详情与取消;operation 创建归命令 `CmdPanel` 的统一 invoke。固定 wire/HTTP 仍由 `@tool-bridge/sdk/client` 提供,`lib/api.ts` 只做 session 适配,`lib/queries.ts` 以 profile + deviceId 隔离列表/详情 cache;`lib/deviceMailbox.ts` 只保留 - raw JSON object 的无 DOM 校验,不复制服务端 delivery、scope 或 lease 规则。设备选择项必须使用 Registry + raw JSON object 的无 DOM 校验,不复制服务端 delivery、scope 或 lease 规则。Registry 存储态的设备根以 `kind === 'directory'` 且 `online` 字段存在识别,不能按产品名猜 `kind === 'device'`,也不能按 `online` 真值排除离线设备;当前可路由状态仍由 presence 派生。设备选择项必须使用 Registry TreeNode 的系统写入 `deviceId` 作为数据面身份,并单独保留 node.path 作为 mountPath/命令路径提示;不得 把规范化 mount path 直接当裸 deviceId。 -- JSON Schema 表单只有一个懒加载的 `SchemaFormRenderer`,统一持有 RJSF/AJV validator;`SchemaFields` 和命令面板只投影 props/UI,不得各自实例化另一套 schema renderer。 -- `DashboardThemeProvider` 是主题持久化与 DOM class 的唯一真源,由 `next-themes` 处理初始注入与同步;当前默认 dark 且明确禁用 system theme。业务组件只消费当前 theme,不直接维护 localStorage/class。 +- JSON Schema 表单只有一个懒加载的 `SchemaFormRenderer`,统一持有 RJSF/AJV validator;`SchemaFields` 和命令面板只投影 props/UI,不得各自实例化另一套 schema renderer。需要工具级 help 补水时,读取中或失败阻止调用;失败提供原地重试,不能当作无 schema 而开放原始 JSON 调用。 +- `DashboardThemeProvider` 是主题持久化与 DOM class 的唯一真源,由 `next-themes` 处理初始注入与同步;当前默认 dark 且明确禁用 system theme。业务组件只消费当前 theme,不直接维护 localStorage/class。视觉语义集中在 `index.css`:中性实底、文字与表面层次、少量品牌黄;浅色背景的强调前景使用可读深金,亮黄只作配深色文字的实底。 表单编译与图布局逻辑优先抽成无 React 依赖的函数(`forms/*.ts`、`canvas/treeGraph.ts`),用 Node Vitest 断言最终 payload、布局结果、互斥项、必填项和 fail-closed 分支;DOM 测试再覆盖交互顺序、焦点和敏感值展示。依赖懒加载 `SchemaFormRenderer` 的业务交互套件在模块收集期预热真实 renderer,让断言只衡量 RJSF/AJV 与业务交互,不把并行 CI 下的 Vite 冷转换时延当作产品契约;不得为此放大全局异步超时、mock renderer 或删除生产 `lazy`/`Suspense`,实际拆包仍由 production build 与 UI integration 验证。 ## 当前约束 - 内置集成表单只读 `CatalogListItem.exportDetails`;不保留旧 host 聚合字段 fallback。 -- 新凭证先写 SecretStore,再写 registry;挂载失败只有在完整 secret list 证明本轮创建了新槽、且该操作不是 replacement 时才能回滚,不能用局部探测或错误类型猜测后误删既有凭证。编辑挂载时留空表示沿用现有 `authRef`,因为 SecretStore 不可回读明文;OAuth 授权是 registry 写入成功后的独立 follow-up,不并入凭证回滚事务。 +- 新凭证先写 SecretStore,再写 registry;挂载失败只有在完整 secret list 证明本轮创建了新槽、且该操作不是 replacement 时才能回滚,不能用局部探测或错误类型猜测后误删既有凭证。编辑挂载时留空表示沿用现有 `authRef`,因为 SecretStore 不可回读明文;OAuth 授权是 registry 写入成功后的独立 follow-up,不并入凭证回滚事务。`MountCompletion` 区分不需授权、已确认授权、待授权与授权失败;得到 authorization URL 只代表待授权。挂载成功不等于命令可用,授权失败引导到节点继续授权,不要求重复添加。 - Registry MCP 表单的 OAuth 模式可选预注册 `clientId` 与 `clientSecretRef`:两者都空表示沿用 DCR,只有 clientId 表示 PKCE public client,secret ref 非空时必须同时有 clientId 并编译成独立 `oauthClient`,不能落入 bearer `authRef`。切离 OAuth 模式时残留 client 字段必须拒绝;挂载成功后继续通过既有 `~authorize` 流程发起 @@ -113,13 +125,14 @@ Dashboard 是纯 HTTP 客户端,不 import core。固定控制面 wire 类型与 - query cache、history 和状态按 profile identity 隔离:queryKey 以 profile 标识为前缀,缓存失效(`useInvalidate`)始终限定当前 profile——不带参失效整个 profile,带域名参数只失效对应域段,绝不跨 profile 失效。 - 列表超过服务端默认页时必须可继续分页,客户端筛选不能吞 cursor。 - 画布保留 HTBP 的唯一 `/` 总根语义:领域路径 `path: ''` 继续用于路由与数据查询,React Flow 使用独立的非空 `ROOT_NODE_ID`;单根、可见性与边完整性由纯图函数测试固定。 -- 画布选中态由 URL 驱动(`/nodes/`,BrowserRouter basename `/ui`):命令面板跳转、deep-link、前进后退都要能定位节点并开对应 Inspector,`/` 不选中(全景)。命令叶和搜索生成的 `?tool=` 必须由 `NodeInspector` 消费,切到调用 tab 并由 `CommandWorkspace` 打开对应命令;切换路径或 URL 状态时 Inspector 整体 remount,不跨节点残留 tab/表单状态。 +- BrowserRouter basename 保持 `/ui`。`/` 是工作台;`/tools?path=` 是目录浏览;`/tools/?tool=` 是独立调用页,根 owner 的 `/tools?tool=` 也必须正确分流。搜索、收藏、最近使用和命令目录使用同一 `toolHref`;URL 只存入口身份,打开后重新读取实时 help,实际调用路径仍以 `cmd.path` 为准。返回位置只允许已知应用路由及导航 query,剔除其他 query/hash;不把 history state 当可信 URL。 +- `/canvas` 是未选中的能力树全景,旧 `/nodes/?tool=` 继续由 `NodeInspector` 消费并在 `CommandWorkspace` 打开命令 Dialog;`?tab=invoke` 打开命令目录。画布命令叶保留这条兼容路径;切换路径或 URL 状态时 Inspector 整体 remount,不跨节点残留 tab/表单状态。独立调用页按 profile、gateway、session revision 和命令身份重建表单,不跨连接复用参数或结果。 - 画布只渲染"已加载 + 已展开"的节点:truncated/remote 子树按需懒加载,节点总数超阈值时默认只展根层。规模控制依赖懒加载、折叠和节点阈值,不得再通过删除 offline 设备降噪。presence 只描述 realtime 当前是否可路由,只影响状态呈现与 realtime 提示;offline 设备及其已声明命令仍须可发现、可选中,mailbox-capable command 仍可从统一 invoke 面入队。 - `~tree` 只承载实体节点;命令是用户点击 owner 的独立开关后,复用节点级 `~help` query 投影出的「owner → 命令叶」虚拟子树,不增加 commandGroup 中转或二次折叠。虚拟角色与图 ID 必须独立,不复用实体节点的 `childCount`、挂载权限或 `expanded`;每个 owner 在画布最多直接展示 10 条命令,超出部分统一进入 Inspector 的可筛选完整目录。只订阅用户已打开且当前可见的 owner;真实树折叠使 owner 不可见时停止 observer,不留虚拟节点或悬边。 - 每个 profile 首次产生可见布局时只自动执行一次 fit view;之后展开/收起实体树、显隐命令或切换选中都不得重置用户的平移与缩放。重置只通过"适应视图"显式触发。 - React Flow 节点适配必须传递 dagre 产出的 `width` / `height`,避免 MiniMap 矩形退化。React Flow `colorMode` 与 Dashboard 主题同步;MiniMap 的背景、mask、节点 fill/stroke 使用显式高对比 SVG 配色,不盲用卡片的暗色 token,且与画布底部操作提示分区布局、不互相遮挡。 - Inspector 内部布局必须以实际容器宽度为准,不能只凭 viewport breakpoint 推导双栏。Context 正文等高密度详情统一放入宽 Dialog,header/footer 固定、正文独立滚动,长路径与预格式内容允许换行或横向滚动,并保留返回所属工具的链接。 -- "最近调用"是当前浏览器按 profile 与 gateway 隔离的本地历史,不是服务端全局审计;任何展示都必须明示"本机/当前 profile"边界。 +- 收藏和最近调用都按 profile 与 gateway 隔离,仅属于当前浏览器,展示必须明示“本机/当前连接”边界。收藏仅持久化 owner `path` 与命令 `tool`,存储不可用时退化为会话内收藏;删除 profile 同时清理收藏。最近调用的 `historyIdentity` 与直连 `commandPath` 分离:前者保存可重新发现的 owner/name,后者原样用于 wire,不能从特殊路由的末段反推 owner;`historyIdentity` 不发送到网关。两者均不保存调用参数、结果或凭证,重新使用仍需实时 help 与授权。 - Store 页面支持本地文件上传、当前 owner 的 ready 列表、stat、下载、创建/撤销短期 share 和删除。上传 读取 grant 的 `maxBytes`,relay PUT 直接消费 descriptor,presigned PUT 后用 `x-tb-store-upload` 调 complete;标准 Node S3 部署目前只提供 relay,不把通用 client 分支当作 signer 已存在。 @@ -142,7 +155,7 @@ Dashboard 是纯 HTTP 客户端,不 import core。固定控制面 wire 类型与 - 可逆 UI 动作(如展开/收起)不要在同一事件里混用 toggle 与 force-open 两种语义,会产生用户无法撤销的状态。 - 组件布局按实际容器宽度判断,不按 viewport breakpoint 推导——Inspector 抽屉里的"桌面"宽度远小于 viewport。 - SVG/画布类视觉故障(MiniMap 退化、节点错位)先查真实 DOM/SVG 尺寸属性,不要对着截图猜 CSS。 -- URL 驱动的状态(`/nodes/`、`?tool=`)必须 producer 与 consumer 成对验证:只测跳转生成的 URL 或只测直开 URL 都不算覆盖。 +- URL 驱动的状态(`/tools?path=`、`/tools/?tool=`、根 owner 命令、兼容 `/nodes/?tool=`、设备 `?connect=1`)必须 producer 与 consumer 成对验证,并覆盖返回来源和前进后退;只测 URL 字符串或只测直开页面都不算覆盖。 ## 验证 diff --git a/llmdoc/meta.json b/llmdoc/meta.json index 63dcc6de..0c2503af 100644 --- a/llmdoc/meta.json +++ b/llmdoc/meta.json @@ -21,7 +21,7 @@ "validatedRevision": "2b45dc99c607f59351d214845fe8e0c6b912f478" }, "dashboard/canvas-architecture.mdx": { - "validatedRevision": "2b45dc99c607f59351d214845fe8e0c6b912f478" + "validatedRevision": "41c39bccb8055f2d3bffa904b5cc2e30c0b43c5b" }, "device/device-daemon.mdx": { "validatedRevision": "2b45dc99c607f59351d214845fe8e0c6b912f478" diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index c2e887da..6a644097 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@tool-bridge/dashboard", - "version": "0.28.0", + "version": "0.29.0", "description": "Tool Bridge self-hosted Dashboard as prebuilt static assets", "type": "module", "license": "MIT", diff --git a/packages/dashboard/src/App.tsx b/packages/dashboard/src/App.tsx index 5edc32a1..15e35e69 100644 --- a/packages/dashboard/src/App.tsx +++ b/packages/dashboard/src/App.tsx @@ -1,4 +1,4 @@ -import { Navigate, Route, Routes, useLocation } from 'react-router' +import { Navigate, Route, Routes, useLocation, useSearchParams } from 'react-router' import { lazy, type ReactNode, Suspense } from 'react' import { useSession } from '@/lib/session-context' @@ -13,6 +13,15 @@ const LoginPage = lazy(() => const CanvasPage = lazy(() => import('@/canvas/CanvasPage').then(module => ({ default: module.CanvasPage })), ) +const WorkspacePage = lazy(() => import('@/pages/WorkspacePage').then(module => ({ default: module.WorkspacePage }))) +const ToolsPage = lazy(() => import('@/pages/ToolsPage').then(module => ({ default: module.ToolsPage }))) +const ToolPage = lazy(() => import('@/pages/ToolPage').then(module => ({ default: module.ToolPage }))) + +function ToolsIndex() { + const [params] = useSearchParams() + return params.has('tool') ? : +} + const SearchPage = lazy(() => import('@/pages/SearchPage').then(module => ({ default: module.SearchPage })), ) @@ -53,7 +62,7 @@ function AppBooting() {
- control plane / loading + 正在打开工作区
) @@ -91,6 +100,9 @@ export default function App() { }> }> + } path="tools" /> + } path="tools/*" /> + } path="canvas" /> } path="manage/keys" /> } path="manage/maintenance" /> } path="manage/deployment" /> @@ -115,7 +127,7 @@ export default function App() { - + )} index diff --git a/packages/dashboard/src/canvas/CanvasPage.tsx b/packages/dashboard/src/canvas/CanvasPage.tsx index 247021fe..8845949e 100644 --- a/packages/dashboard/src/canvas/CanvasPage.tsx +++ b/packages/dashboard/src/canvas/CanvasPage.tsx @@ -86,7 +86,7 @@ export function CanvasPage() { (path: string) => navigate(`/nodes/${encodeTreePath(path)}`), [navigate], ) - const close = useCallback(() => navigate('/'), [navigate]) + const close = useCallback(() => navigate('/canvas'), [navigate]) const openCommand = useCallback( (path: string, commandName: string) => { navigate(`/nodes/${encodeTreePath(path)}?tool=${encodeURIComponent(commandName)}`) @@ -100,7 +100,7 @@ export function CanvasPage() { const onUnmounted = useCallback( (path: string) => { const parent = parentPath(path) - navigate(parent === '' ? '/' : `/nodes/${encodeTreePath(parent)}`) + navigate(parent === '' ? '/canvas' : `/nodes/${encodeTreePath(parent)}`) }, [navigate], ) diff --git a/packages/dashboard/src/canvas/WorkspaceShell.tsx b/packages/dashboard/src/canvas/WorkspaceShell.tsx index ee02522b..57cbe9de 100644 --- a/packages/dashboard/src/canvas/WorkspaceShell.tsx +++ b/packages/dashboard/src/canvas/WorkspaceShell.tsx @@ -1,242 +1,129 @@ -import { Command, GitBranch, Menu, Moon, Search, Sun } from 'lucide-react' +import { ChevronDown, Menu, Moon, PanelLeftClose, PanelLeftOpen, Plus, Search, Settings, Sun } from 'lucide-react' import { NavLink, Outlet, useLocation, useNavigate } from 'react-router' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useTheme } from 'next-themes' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { type ManageLink, RESOURCE_LINKS, SETTINGS_LINKS, WORKSPACE_LINKS } from '@/components/layout/navigation' +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { MANAGE_LINKS } from '@/components/layout/navigation' +import { AddToolWizard } from '@/components/add-tool/AddToolWizard' import { CommandPalette } from '@/components/CommandPalette' -import { useHealthz, useStatus } from '@/lib/queries' import { useSession } from '@/lib/session-context' import { Button } from '@/components/ui/button' +import { useHealthz } from '@/lib/queries' import { cn } from '@/lib/utils' const isMac = typeof navigator !== 'undefined' && /Mac/.test(navigator.platform) -function healthDot(healthy: boolean | undefined, error: boolean): string { - if (error) return 'bg-destructive' - if (healthy) return 'bg-ok shadow-[0_0_7px_var(--ok)]' - return 'bg-warn' -} - -function healthText(healthy: boolean | undefined, error: boolean): string { - if (error) return '网关不可达' - if (healthy) return '网关运行正常' - return '正在检查网关' -} - -/** 顶栏:品牌 + 全局搜索/跳转 + 健康 + 主题 + profile。 */ -function TopBar({ - onOpenPalette, - onToggleRail, -}: { - onOpenPalette: () => void - onToggleRail: () => void -}) { - const health = useHealthz() - const status = useStatus() - const { resolvedTheme, setTheme } = useTheme() - const isDark = resolvedTheme !== 'light' - const navigate = useNavigate() +function ConnectionMenu() { const { active, profiles, switchTo, logout } = useSession() - const healthy = health.data?.healthy - + const navigate = useNavigate() + const health = useHealthz() + const status = health.isError ? '网关不可达' : health.data?.healthy ? '网关运行正常' : '正在检查网关' return ( -
- - - - - tool - - - bridge - - - - - -
- - - - - - - - - - - - - - 连接档案 - - {active?.baseUrl || window.location.origin} - - - - {profiles.map(profile => ( - { - switchTo(profile.name) - navigate('/') - }} - > - {profile.name} - {profile.id === active?.id && } - - ))} - - - 退出登录 - - - -
-
+ + + + 当前连接 + {active?.baseUrl || window.location.origin} + + + {profiles.map(profile => ( + { + switchTo(profile.name) + navigate('/') + }} + > + {profile.name} + {profile.id === active?.id && 当前} + + ))} + + 退出登录 + + ) } -function RailLink({ - to, - label, - icon, - collapsed, - exact, -}: { - collapsed: boolean - exact?: boolean - icon: React.ReactNode - label: string - to: string -}) { +function RailLink({ item, collapsed }: { collapsed: boolean, item: ManageLink }) { + const location = useLocation() + const { icon: Icon, label, to } = item + const related = (to === '/tools' && location.pathname === '/search') || (to === '/canvas' && location.pathname.startsWith('/nodes/')) const link = ( - cn( - 'flex h-9 items-center gap-2.5 rounded-lg px-2.5 text-sm text-foreground/80 transition-colors', - 'hover:bg-secondary/60 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', - isActive && 'bg-primary/12 font-medium text-primary', - collapsed && 'justify-center px-0', - )} - end={exact} + aria-label={label} + className={({ isActive }) => cn('flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm text-muted-foreground transition-colors hover:bg-secondary/60 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', (isActive || related) && 'bg-secondary font-medium text-foreground', collapsed && 'justify-center px-0')} + end={to === '/'} to={to} > - {icon} + {!collapsed && {label}} ) - if (!collapsed) return link - return ( - - {link} - - {label} - - - ) + return collapsed + ? ( + + {link} + {label} + + ) + : link } -/** 左侧库务栏:画布入口 + 管理页。桌面常驻(可折叠成图标),移动经抽屉。 */ -function ManageRail({ collapsed }: { collapsed: boolean }) { +function Navigation({ collapsed }: { collapsed: boolean }) { + const location = useLocation() + const inSettings = SETTINGS_LINKS.some(item => item.to === location.pathname) + const [settingsOpen, setSettingsOpen] = useState(inSettings) + useEffect(() => { + if (inSettings) setSettingsOpen(true) + }, [inSettings]) return ( -