# 应用导航与系统工具入口技术设计 ## 文档信息 | 项目 | 内容 | | --- | --- | | 状态 | 设计中 | | 版本 | 0.2 | | 日期 | 2026-09-08 | | 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) | | 功能逻辑 | [功能逻辑设计](./logic-design.md) | | UI 设计 | [UI 设计](./ui-design.md) | ## 1. 现状 主页面没有 URL Router,由 `App.tsx` 中的 `WorkspaceView` 和 keep-alive 状态切换: ```ts type WorkspaceView = | 'chat' | 'magic-notes' | 'knowledge' | 'heartbeat' | 'activity' | 'settings' ``` 文件、浏览器和终端已经由 `RightAssistantSidebar`、`WorkbarShell` 与 `workbar-contracts.ts` 承载。工作区是不可关闭的单实例应用,浏览器和终端是多实例应用。浏览器多实例的跨进程 实现以[浏览器多实例技术设计](../assistant-workbar/browser-tabs-technical-design.md)为准。代码中尚无资源指标采集和资源监控 面板。现有侧栏底部 `user-card` 整行打开设置,并显示“本地工作区”和平台架构。 本次改动增加入口编排、从主侧栏到工作栏的命令通道,以及本机资源监控的 Main/Preload/ Renderer 最小实现。文件、终端和浏览器后端保持不变。 ## 2. 模块边界 | 模块 | 责任 | | --- | --- | | `App.tsx` | 组合主导航、系统工具栏、设置入口和页面离开检查 | | `SidebarSystemTools`(新增) | 渲染四个系统工具按钮并发送打开意图 | | `RightAssistantSidebar.tsx` | 接收打开意图,调用工作栏实例创建或激活逻辑 | | `WorkbarShell.tsx` | 继续负责实例策略、Tab、目录和焦点行为 | | `workbar-contracts.ts` | 继续作为工作栏应用 ID 和实例布局的权威契约 | | `resource-monitor-contracts.ts`(新增) | 定义本机资源快照、可用性和错误码 | | `resource-monitor-service.ts`(新增) | 在 Main 中采集并规范化本机与 GoodBuddy 指标 | | `ResourceMonitorPanel.tsx`(新增) | 按需轮询、保留有界历史并渲染资源面板 | | `application-settings-contracts.ts` | 增加应用导航偏好 schema 和默认值 | | `application-settings-store.ts` | 迁移并原子保存应用导航偏好 | | `PlatformFeaturesSettingsSection` 或独立设置 Section | 编辑“显示在侧边栏”偏好 | 不要在 `SidebarSystemTools` 中读取文件、创建 PTY、操作浏览器或采集系统指标;该组件只发送 有界的工具 ID。 ## 3. Renderer 契约 ### 3.1 系统工具 ID ```ts export const sidebarSystemToolIds = [ 'workspace', 'terminal', 'browser', 'resources' ] as const export type SidebarSystemToolId = (typeof sidebarSystemToolIds)[number] ``` 该类型可以放在 Renderer 内共享模块。首期不暴露 IPC,因为动作只编排已有工作栏 Renderer 状态;实际能力调用继续走现有 Preload API。 ### 3.2 打开意图 `App` 向 `RightAssistantSidebar` 传入单调递增的请求或命令对象: ```ts type WorkbarOpenIntent = { requestId: number appId: 'workspace' | 'terminal' | 'browser' | 'resources' reason: 'sidebar-system-tool' context?: { conversationId?: string; projectId?: string } } ``` `requestId` 使连续点击同一工具仍能被消费。不要仅设置 `assistantSidebarTab`,因为用户可能已 关闭对应实例,且终端需要按执行空间寻找实例。 建议由 `RightAssistantSidebar` 在消费意图后通过现有 `WorkbarShell` 控制接口完成。控制器必须 接收上下文和调用原因,不能只按 `appId` 查找第一个实例: ```ts type WorkbarController = { openOrActivate(intent: WorkbarOpenIntent): Promise } ``` 若当前 `WorkbarShell` 没有命令式入口,先增加窄的回调或受控请求 Props,不引入全局事件总线。 工作区意图始终激活注册表中的不可关闭单实例;仅在迁移或布局损坏时补回。浏览器意图按 `conversationId` 激活最近使用的匹配实例,没有匹配实例时创建一个。工作栏“+”不走该去重 路径,选择浏览器时始终创建新实例。 ### 3.3 终端匹配 工作栏“+”目录选择终端时每次创建新实例;侧栏系统工具按钮执行“进入当前终端”,采用: 1. 解析当前项目对应的 `ExecutionSpaceSelection`。 2. 在现有终端实例中按目标 identity 查找 `starting` 或 `running` 状态。 3. 存在多个匹配实例时激活最近使用的实例。 4. 没有匹配实例时调用现有终端创建流程。 `closing`、`exited`、`interrupted` 和 `failed` 实例保留为历史 Tab,不阻止快捷按钮创建新终端。 工作栏“+”继续按原规则创建额外终端,不受快捷按钮去重影响。 ### 3.4 资源监控工作栏应用 在 `workbar-contracts.ts` 中增加 `resources`: ```ts { id: 'resources', instancePolicy: 'single', defaultContext: 'application' } ``` 该应用不加入升级后的默认打开实例。侧栏资源按钮与工作栏“+”目录都通过既有单实例规则创建 或聚焦它。 ## 4. 应用导航偏好 ### 4.1 设置结构 在 `ApplicationSettings` 增加独立字段: ```ts type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat' type ApplicationNavigationSettings = { hiddenBuiltInApplicationIds: BuiltInApplicationId[] } ``` 默认值为空数组,保持升级前所有可用入口可见。ID 数组可以避免为每个应用增加一个顶层布尔 字段;Zod schema 必须去重、限制数量并拒绝未知内置 ID。 首期不保存排序,因为内置应用顺序固定。插件偏好在插件系统确定稳定 identity 和卸载保留 规则后另行扩展,不提前混入当前 schema。 ### 4.2 与现有开关组合 入口可见条件为: ```ts const visible = featureEnabled && !hiddenBuiltInApplicationIds.includes(id) ``` `magicNotesEnabled` 继续控制功能启停。知识库和智能心跳若未来增加业务启停开关,也按同一组合 规则处理。功能停用时设置页保留显隐偏好但禁用显隐 Switch,并提供原功能设置入口。导航设置 不能写回现有开关。 ### 4.3 持久化与 IPC 沿用现有应用设置链路: ```text Renderer -> window.goodbuddy.updates.updateSettings -> settings:application:update -> application-settings-store -> application-settings.json ``` 更新继续由 Main 校验可信 sender、Zod 输入和原子写入。设置版本迁移只补充默认空数组,不 修改已有 `magicNotesEnabled` 或其他平台设置。 ## 5. 侧栏底部重构 现有 `user-card` 是一个覆盖整行的设置按钮。实现时替换为容器和相邻独立按钮: ```tsx
``` 不能在设置按钮内部嵌套工具按钮。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`, 保留设置预加载、焦点恢复和未保存离开确认。 系统工具按钮调用统一 `openSystemTool`: 1. 设置工作栏打开意图。 2. 展开工作栏。 3. 由工作栏创建或激活实例。 4. 按现有策略把焦点移入目标面板。 工作栏仍由窗口右上方的固定开关关闭,关闭后的焦点返回该开关。 ## 6. 工作栏状态与快捷按钮 `RightAssistantSidebar` 应向 `App` 暴露当前活动实例的 `appId` 和必要的有界状态摘要,或由 现有 `onTabChange` 扩展得到。主侧栏不读取终端输出、文件内容、URL 或活动详情。 ```ts type SidebarSystemToolState = { activeToolId?: SidebarSystemToolId statusByToolId?: Partial> } ``` 首期可以只实现 `activeToolId`,没有可靠聚合来源时不显示状态徽标。不能根据局部事件猜测错误 或运行状态。 ## 7. 应用显隐更新 应用显隐 Switch 复用平台功能设置的即时更新模式。请求开始后禁用三个应用显隐 Switch,只 允许一个完整目标集合处于提交中。Main 返回成功结果后更新 App 中的平台设置快照,主导航 随之变化。失败时恢复控件原值,在对应设置行显示可重试错误;主导航继续读取最后一次成功 保存的设置。 ## 8. 插件兼容边界 首期不增加插件注册表、插件 schema 或插件持久化字段,只保留系统 ID 不向插件开放的产品边界。 插件市场设计需要单独确定 Host、权限、签名、生命周期和稳定 identity;不得直接复用 DeepSeek Harness 的 npm 插件市场。 ## 9. 本机资源监控 ### 9.1 采集范围 首期在 Main 中使用经过依赖审查的 `systeminformation` 获取整机 CPU、内存和磁盘 IO,使用 Electron `app.getAppMetrics()` 汇总 GoodBuddy 桌面进程 CPU、内存和进程数。采集函数不接收 Renderer 提供的路径、PID、设备名或命令。 首期指标定义: - CPU:整机忙碌时间占比,规范化为 `0` 至 `100`。 - 内存:总字节数、可用字节数和 `总量 - 可用量` 得到的使用量。 - 磁盘 IO:所有可用本机磁盘的每秒读取与写入字节数。 - GoodBuddy:`app.getAppMetrics()` 返回的桌面进程聚合 CPU、工作集内存和进程数量。各进程 CPU 先相加,再按逻辑处理器数量归一化为整机容量占比 `0` 至 `100`;不能通过截断总和处理 多核数值。 受管 Runtime、MCP 和终端子进程的逐进程指标不在首期范围。磁盘 IO 在平台或权限不足时返回 不可用错误码,不能回退为 `0`。新增依赖在合入前需要完成许可证、维护状态、打包体积和供应链 检查;检查未通过时应选择提供同等跨平台语义的 Main 侧实现,不能改用 Renderer Shell 命令。 ### 9.2 快照契约 ```ts type ResourceMetric = | { status: 'available'; value: T } | { status: 'warming-up' } | { status: 'unavailable'; reason: ResourceMetricUnavailableReason } type LocalResourceSnapshot = { sampledAt: string host: { cpuPercent: ResourceMetric memory: ResourceMetric<{ usedBytes: number totalBytes: number }> diskIo: ResourceMetric<{ readBytesPerSecond: number writeBytesPerSecond: number }> } goodbuddy: { cpuPercent: ResourceMetric memoryBytes: ResourceMetric processCount: ResourceMetric } } ``` 所有数字必须有限且非负;百分比限制为 `0` 至 `100`,字节数设置跨 IPC 上限。`sampledAt` 由 Main 生成。不可用原因使用稳定枚举,例如 `unsupported-platform`、`permission-denied`、 `collector-timeout` 和 `collector-failed`,不向 Renderer 返回原始命令输出。`warming-up` 只用于 依赖两个采样点的 CPU 与磁盘 IO,不能用于掩盖采集失败。 ### 9.3 Preload 与 IPC Preload 只暴露: ```ts resourceMonitor.getLocalSnapshot(input: { resetIntervalBaseline: boolean }): Promise ``` 对应 IPC 为 `resource-monitor:local-snapshot:get`。Main 验证可信 sender;单次采集设置超时, 并合并同一时刻的并发请求,防止多个 Renderer 重复启动系统采集。采集超时后向调用方返回 有界错误,但底层 Promise 仍占用唯一单飞槽位,直到实际结束;在此之前不启动第二个采集 任务。输入只接受布尔值,接口不提供任意进程、磁盘或远程主机查询。 合并规则区分普通请求与基线重置请求: - 普通请求在途时,新的普通请求复用它。 - 普通请求在途时收到重置请求,Main 最多登记一个待执行重置;当前采集结束后再执行,不能 用普通请求结果满足重置请求。 - 多个重置请求复用同一个在途或待执行重置。 - 重置请求在途时,普通请求复用其 `warming-up` 结果,下一次时钟再取得区间值。 因此服务最多持有一个在途采集和一个待执行重置。调用方超时不取消这个顺序,也不增加队列 长度。 面板每次从非活动状态恢复后的首次请求传入 `resetIntervalBaseline: true`。Main 消费本次 `systeminformation` 与 Electron CPU 读数并重置区间基线,CPU 和磁盘 IO 返回 `warming-up`; 内存等瞬时指标正常返回。后续请求传入 `false` 并计算当前区间值,避免把面板停用期间计入 速率。 ### 9.4 轮询与历史 `ResourceMonitorPanel` 仅在工作栏展开、`resources` 为活动实例且 `document.visibilityState === 'visible'` 时启动轮询: 1. 进入活动状态后立即请求一次,并设置 `resetIntervalBaseline: true`。 2. 此后每 2 秒请求一次;前一请求未结束时跳过本次时钟。 3. Tab 失活、工作栏收起、页面变为隐藏或组件卸载时清理定时器,并忽略较晚返回的旧请求。 4. Renderer 环形队列最多保留 150 个带真实时间戳的样本。 5. 采集失败不追加伪造样本,也不持久化历史。 资源数据不写入应用设置、工作栏布局、运行记录或 SQLite。工作栏布局只保存 `resources` 实例 及其活动状态。 ## 10. 测试 ### 10.1 Renderer 测试 - 固定入口顺序不受三个应用显隐组合影响。 - 运行记录和设置没有显隐控件。 - 四个系统工具按钮具有名称、工具提示、键盘行为和选中状态。 - 文件复用不可关闭单实例;浏览器和终端按当前上下文聚焦匹配实例,工作栏“+”允许显式多实例。 - 工具按钮展开工作栏但不改变当前主页面。 - 显隐更新成功后主导航立即变化,失败时 Switch 和主导航恢复原状态。 - 窄窗口下工具按钮不重叠,焦点恢复正确。 - 资源面板只在可见活动时轮询,跳过重叠请求并把历史限制为 150 项。 - 窗口隐藏、最小化和恢复时停止轮询并重新建立区间基线。 - 资源部分可用、全部失败和重新进入活动状态均显示正确结果。 ### 10.2 Main 与契约测试 - 新设置字段默认值、合法 ID、重复 ID、数量上限和未知 ID 校验。 - 旧版本设置迁移后默认显示所有既有应用。 - 更新导航偏好不修改魔法笔记启停等无关字段。 - 原子写入失败时保留最后一次有效设置。 - 资源快照拒绝 NaN、Infinity、负数字节数、越界百分比和未知错误码。 - 采集超时、权限不足和磁盘 IO 不支持时返回有界部分结果。 - 超时采集尚未实际结束时保持单飞,不累积新的后台采集任务。 - 普通采集在途时收到基线重置请求,最多排队一个重置且不复用普通结果。 - `resetIntervalBaseline: true` 时把 CPU 与磁盘 IO 标记为 `warming-up`。 - GoodBuddy CPU 预热时,其内存和进程数仍可独立返回。 - GoodBuddy 多进程 CPU 按逻辑处理器数量归一化,结果保持在 `0` 至 `100`。 - GoodBuddy 摘要不包含 PID、命令行、环境变量或其他进程信息。 ### 10.3 回归范围 - 工作栏布局恢复和已关闭实例重建。 - 本地与托管 SSH 终端目标。 - 浏览器无会话、创建中和 Agent 操作中状态。 - 设置页未保存离开确认。 - 魔法笔记待办数和智能心跳建议数徽标。 - 浅色、深色、`220px` 与 `420px` 侧栏宽度。 - Windows、macOS 和 Linux 的真实 CPU、内存与磁盘 IO 采集;不支持项显示准确原因。 ## 11. 实施顺序 实施范围以 [PRD 分期](./prd.md#8-分期)为准。本节替代旧实施顺序;前文关于四个左侧 系统工具按钮、仅持久化隐藏 ID 的契约需在实现前同步,不能直接按旧规格开发。 1. 同步 PRD 功能要求、User Stories、逻辑、UI 与技术契约,收敛首期应用中心样式。 2. 接入现有内置应用页面与最底部应用中心入口,保留独立设置和固定运行记录入口。 3. 为常驻显隐和排序设计最小持久化契约,保留已发布的相关设置与用户数据,按保存结果更新导航。 4. 接入应用设置的共同界面与既有设置更新路径,先验证魔法笔记多入口状态一致性。 5. 验证右侧现有文件、终端、浏览器 Tab;移除计划中的左下角工具快捷条和专用于该快捷条的意图接口工作。 6. 单独实现资源快照契约、Main 采集服务、Preload API 与右侧 `resources` 单实例面板。 7. 验证键盘、焦点、浅深主题、窄窗口、设置失败、重启持久化和跨平台行为。 首期使用内置应用清单,不依赖远程商店、插件注册或安装服务。商店目录、推荐、安装与 更新机制在后续独立设计中确定;Demo 中的模拟商店不接入首期产品界面。 ## 12. 项目活动汇总 对应 [FR-14](./prd.md#fr-14-项目活动汇总),按[活动逻辑](./logic-design.md#9-项目活动汇总) 派生状态,界面行为见[活动界面](./ui-design.md#10-项目活动汇总)。 - `conversation-activity.ts` 从 `App.tsx` 的会话、`activeConversationIds`、全部可见 `assistantTasks` 和项目元数据派生会话行、全局及项目计数,不新增持久化活动副本。 `ProjectActivity` 渲染常驻汇总与项目、会话级联菜单,`ProjectSwitcher` 复用项目计数。 - `App` 向 `ProjectActivity` 传入项目元数据、活动行、侧栏可见状态与既有精确跳转回调。 菜单按项目 ID 分组并记忆化派生集合;排序仅影响展示,不改聚合状态或持久化数据。 - 两层菜单在同一 body Portal 内相接。布局在打开、层级变化、实时数据更新、窗口缩放和 滚动时按锚点重算;窄窗口隐藏项目面板并保留返回操作。菜单不调用 Modal 焦点隔离, 捕获阶段消费 Escape,防止触发窄侧栏的文档级监听;Tab 关闭后交还正常焦点顺序。 菜单关闭与焦点恢复在导航回调之前同步完成。 - `role="menu"` 使浮层直接进入共享 `browser-viewport-occlusion.ts` 相交检测。未增加 浏览器实例的特殊隐藏条件,也不关闭或重建浏览器会话。 - 持久化快照刷新时,同一条仍在流式输出的消息保留本地待审批、待回答信息,持久化终态 则清除这些信息。响应成功后对应 Task 立即退出等待状态;响应期间到达的新问题、审批或 终态不能被旧响应覆盖。 - `AssistantDatabase.listTasks()` 保留最近历史窗口(默认 100 条),额外包含窗口外当前 运行或等待处理的可见 Task;`activeVisibleTaskSelect` 与 `toTask` 的有效状态保持一致, 待调度运行规范化为 `queued`,不因旧 Task 状态被误纳入实时集合。 - `listConversations()` 保留最近 100 条活动会话,同时包含上述 Task 关联的较早活动会话, 以及含 `streaming` 消息的较早活动会话。两类列表去重后仍按原时间顺序返回,结果允许超过 历史窗口大小,避免后台工作被最近记录挤出。 - `openActivityConversation` 按确切 Conversation ID 导航;内存中缺失时刷新持久化会话 列表并合并,再查找同一 ID。刷新失败或会话已不存在时显示通知,不回退到项目最新会话。 设置离开检查通过后才提交项目、会话及界面清理状态。 - 远程恢复成功提示沿用独立的自动消失计时器;活动汇总更新不重置或替代该计时器。 验证记录见[项目活动级联实施进度](./progress.md)。本次只改 Renderer 展示与交互,远程 Runtime、Agent 协议和桌面到 Agent 的执行路径未改变。