diff --git a/UI-DESIGN.md b/UI-DESIGN.md index 631ae4d2..315c406c 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -201,6 +201,7 @@ - 项目过多时优先重组信息架构,不把一级页签做成多行。 - `PageTabs` 可使用默认视觉或共享的 `segmented` 视觉变体。紧凑主从工作台中的 2 至 4 个同级面板可使用与模型设置一致的分段外观,但不得因此改用按钮组语义。 - 视觉变体不改变组件含义:分段外观的 `PageTabs` 仍使用页签语义、单一激活面板、游标焦点和方向键切换,不复制页面专属样式。 +- 在独立滚动的 Flex 页面中,`PageTabs` 及其直接包装层必须保持 `flex: 0 0 auto`,不得被剩余内容压缩到零高度。新增页签容器时必须提供对应的 CSS 回归测试。 ### 6.2 SegmentedControl diff --git a/docs/features/AGENTS.md b/docs/features/AGENTS.md index 36566d6e..1476c5ad 100644 --- a/docs/features/AGENTS.md +++ b/docs/features/AGENTS.md @@ -100,6 +100,18 @@ Do not renumber stable IDs merely for presentation after implementation begins. - Reference cross-feature documents rather than copying their definitions. - When two features share a global domain model, keep that model in the feature family that owns the domain or in cross-feature architecture if ownership is genuinely global. +## Managed External Artifacts + +When a feature downloads external artifacts managed by GoodBuddy: + +- Define the exact setting that selects the source and the setting scope. +- Use immutable artifact targets with expected byte size and SHA-256. +- Freeze the selected source when an operation starts. +- Fail explicitly when the selected source is unavailable or invalid. Never silently request another source. +- Keep download URLs and redirect allowlists out of Renderer-controlled input. +- State whether the setting affects models, application updates, Agent packages, managed tool runtimes, package-manager registries, or user-configured URLs. Do not imply a broader scope than implemented. +- When two targets are presented as an upstream/original address and its OSS mirror, require byte-identical content with one shared size and SHA-256. A mirror must not rebuild, recompress, or modify the upstream artifact. + ## Migration Rules When moving existing documentation: diff --git a/docs/features/local-tool-environment/README.md b/docs/features/local-tool-environment/README.md index 3dc6bb57..cb2a59f7 100644 --- a/docs/features/local-tool-environment/README.md +++ b/docs/features/local-tool-environment/README.md @@ -1,10 +1,12 @@ -# 本机工具环境 +# 工具执行环境 -本目录是 GoodBuddy“本机工具环境”功能的唯一文档入口。该功能为本机 SKILL 和本机 +本目录是 GoodBuddy“工具执行环境”功能的唯一文档入口。该功能为本机 SKILL 和本机 stdio MCP 提供明确、可诊断的 Node.js 与 Python 环境,同时允许专业用户选择自己维护的 解释器。 本功能不作用于 SSH Host 上的 GoodBuddy Agent、固定 Node、签名 Runtime、SKILL 或 MCP。 +用户从设置中心的“能力与工具”进入,并在 `Skills`、`MCP`、`工具执行环境` 三个水平 +Tab 之间切换。 ## 文档导航 @@ -21,9 +23,11 @@ stdio MCP 提供明确、可诊断的 Node.js 与 Python 环境,同时允许 | 术语 | 定义 | | --- | --- | -| 本机工具环境 | GoodBuddy 在桌面所在设备上向本机 Runtime 和 stdio MCP 提供的工具命令环境 | +| 能力与工具 | 设置中心中统一承载 Skills、MCP 和工具执行环境的一级分类 | +| 工具执行环境 | GoodBuddy 在桌面所在设备上向本机 Runtime 和 stdio MCP 提供的工具命令环境 | | GoodBuddy 托管 | 由 GoodBuddy 提供、验证并解析的 Node.js 或 Python | | 自定义环境 | 用户从检测结果中选择或直接指定的外部解释器 | +| 工具下载源 | GoodBuddy 固定目录受管工具工件使用的原生地址或 OSS 镜像 | | 工具就绪 | 解释器及配套命令通过真实诊断 | | 能力依赖就绪 | 某个 SKILL 或 MCP 所需的第三方包和入口通过实际验证 | diff --git a/docs/features/local-tool-environment/logic-design.md b/docs/features/local-tool-environment/logic-design.md index ea89c0e6..c61d4cb2 100644 --- a/docs/features/local-tool-environment/logic-design.md +++ b/docs/features/local-tool-environment/logic-design.md @@ -1,4 +1,4 @@ -# 本机工具环境功能逻辑设计 +# 工具执行环境功能逻辑设计 ## 文档信息 @@ -7,8 +7,8 @@ | 状态 | 计划 | | 版本 | 0.1 | | 日期 | 2026-09-01 | -| 关联 PRD | [本机工具环境 PRD](./prd.md) | -| 关联场景 | [本机工具环境 User Stories](./user-stories.md) | +| 关联 PRD | [工具执行环境 PRD](./prd.md) | +| 关联场景 | [工具执行环境 User Stories](./user-stories.md) | 本文是本功能业务规则、状态转换、决策表和跨场景一致性的权威来源。它不定义组件视觉,也 不规定代码模块如何实现。 @@ -65,6 +65,19 @@ stdio MCP 配置的绝对 command 表达用户对该 MCP 的明确覆盖,原 能力没有专属实际诊断时,状态只能是“未验证”,不能根据解释器版本或文件存在推断“已就绪”。 +### INV-9 受管工具工件来源明确且冻结 + +每个受管工具外部工件同时声明固定原生地址和 OSS Target。用户选择一个下载源后,任务 +启动时冻结该来源;失败不回退。OSS 必须是原生工件的字节级副本。 + +### INV-10 设置导航不改变业务状态 + +“能力与工具”的三个水平 Tab 只组织界面,不改变 Skill、MCP 或工具环境设置。切换 Tab +不得提交、丢弃或重置当前设置面板会话中的编辑状态;关闭后不持久化外层 Tab。 + +分阶段交付期间,未实现的“工具执行环境”不进入 Tab 状态集合。第一阶段状态集合只有 +`skills | mcp`,后续实现完成后才扩展为 `skills | mcp | tool-environment`。 + ## 3. 状态维度 ### 3.1 来源选择 @@ -123,6 +136,15 @@ error - `incompatible` 表示解释器版本、架构或 ABI 不满足已声明要求。 - `error` 表示诊断本身没有得出有效结论。 +### 3.5 工具下载源 + +```text +native +oss +``` + +这是独立于 Node/Python 环境来源的持久设置。全新用户默认 `native`。 + ## 4. 来源解析决策表 | 保存来源 | 当前事实 | 解析结果 | 是否回退 | @@ -250,6 +272,20 @@ not-installed 每个工具调用的活动记录应归属于实际承载它的进程,不根据最新设置追改历史来源。 +### 8.3 设置导航 + +| 入口或操作 | 打开的外层 Tab | 数据行为 | +| --- | --- | --- | +| 普通“能力与工具”入口 | Skills | 不修改业务数据 | +| 用户点击 Skills | Skills | 不修改业务数据 | +| 用户点击 MCP | MCP | 不修改业务数据 | +| 未来的定向入口 | 指定 Tab | 不修改业务数据 | +| 同一设置面板会话中切换 Tab | 用户选择的 Tab | 保留三个面板的会话内状态 | +| 关闭后重新打开普通入口 | Skills | 不恢复上次外层 Tab | + +MCP 内层页签状态属于 MCP 面板,不与外层 Tab 合并。外层 Tab 的默认值或定向入口不能改写 +MCP 的连接、凭据、启用或 Runtime 分配状态。 + ## 9. 能力依赖逻辑 ### 9.1 诊断输入 @@ -298,7 +334,9 @@ not-installed ### 11.2 托管 Python -- 首期支持当前官方 GitHub/镜像来源。 +- Windows 原生 Target 固定到官方 NuGet 包;macOS/Linux 原生 Target 固定到 Astral + `python-build-standalone`。 +- OSS Target 是对应原生工件的字节级副本。 - 下载源不可达时保持未安装或旧版可用状态,并显示准确原因。 - 首期不提供 Python 离线包导入,避免引入未经选型的包格式和导入流程。 - 完全离线用户可以选择已经离线安装的自定义 Python。 @@ -306,6 +344,20 @@ not-installed 未来若加入离线导入,必须在 PRD、User Stories、逻辑状态和技术包校验中同时定义,不能只 增加一个文件按钮。 +### 11.3 工具下载源决策 + +| 保存选择 | 目录事实 | 行为 | +| --- | --- | --- | +| 原生地址 | 原生 Target 完整 | 请求固定上游 URL | +| 原生地址 | 原生 Target 缺失 | 工件不可下载,不请求 OSS | +| OSS 镜像 | OSS Target 完整 | 只请求固定 OSS URL | +| OSS 镜像 | OSS Target 缺失 | 工件不可下载,不请求原生地址 | +| 任意 | 下载后字节数/SHA-256 不符 | 删除暂存并失败 | +| 任意 | 设置在任务期间变化 | 当前任务不变,后续任务使用新选择 | + +该选择只覆盖受管工具目录中的外部工件,不影响模型、应用更新、Agent、pip/npm 注册表或 +用户 URL。 + ## 12. 冲突与一致性检查 | 可能冲突 | 唯一规则 | @@ -318,6 +370,10 @@ not-installed | Ask 环境包含解释器 vs Ask 只读 | Ask 权限边界优先 | | 本机设置 vs 远程项目 | 远程隔离优先 | | 镜像不可达 vs 其他来源可用 | 不在同一次操作中静默切换来源 | +| 工具下载源 vs 模型/更新下载源 | 各自独立,工具执行环境设置不覆盖其他功能 | +| OSS 对象 vs 原生工件 | 必须字节、大小和 SHA-256 相同 | +| 合并设置入口 vs 原有 Skills/MCP 数据 | 只重组导航,不迁移或重写业务数据 | +| 外层 Tab vs MCP 内层 Tab | 保持两个独立 tablist、标识和键盘焦点范围 | ## 13. 逻辑评估结论 @@ -338,6 +394,8 @@ not-installed | 远程边界 | 通过 | 本机配置不进入 Host | | 受限网络 | 通过 | 来源失败可见,不在同次操作静默切换 | | 完全离线 | 有明确限制 | Node 可用;Python 首期要求已有自定义环境 | +| 受管工具工件来源 | 通过 | 原生/OSS 独立选择、任务冻结、禁止回退 | +| 设置导航 | 通过 | 单一一级入口、分阶段水平 Tab、业务数据不变 | ### 13.2 一致性评估 @@ -363,12 +421,15 @@ not-installed | 逻辑范围 | PRD | User Stories | | --- | --- | --- | +| 首次使用默认状态 | FR-1、FR-2、FR-6 | US-A0 | | 明确来源与禁止回退 | FR-3、FR-8 | US-C1、US-C2、US-D3 | | 托管 Node | FR-1 | US-A1、US-B2 | | 托管 Python 生命周期 | FR-2、FR-9 | US-A2、US-D2、US-D4、US-D5 | | 三层状态 | FR-6 | US-A3、US-A4 | +| SKILL 命令解析 | FR-4 | US-A1、US-A3 | | MCP 命令解析 | FR-5 | US-B1、US-B2、US-B3 | | 生效时机 | FR-7 | US-C3、US-C4 | | Ask/Execute | FR-10 | US-D6 | | 远程隔离 | FR-7、FR-9 | US-D1 | -| 受限网络与离线 | FR-2 | US-E1、US-E2 | +| 受限网络与离线 | FR-2、FR-11 | US-E1、US-E2、US-E3 | +| 统一设置入口 | FR-12 | US-F1、US-F2 | diff --git a/docs/features/local-tool-environment/prd.md b/docs/features/local-tool-environment/prd.md index 92b15ac9..f84ee03d 100644 --- a/docs/features/local-tool-environment/prd.md +++ b/docs/features/local-tool-environment/prd.md @@ -1,4 +1,4 @@ -# 本机工具环境 PRD +# 工具执行环境 PRD ## 文档信息 @@ -26,8 +26,10 @@ Python、虚拟环境或 Conda 环境时,也不能被 GoodBuddy 静默替换 2. 用户可在 GoodBuddy 内按需安装 Python,并运行 Python SKILL 和 stdio MCP。 3. 专业用户可以选择检测到的解释器或指定其他可执行文件。 4. 设置始终展示实际来源、版本、路径、工具状态和能力依赖状态。 -5. 本机工具环境不修改系统环境,也不传播到远程主机。 +5. 工具执行环境不修改系统环境,也不传播到远程主机。 6. 保持 Ask/Execute、Runtime、MCP、取消和进程清理的现有边界。 +7. 用户可以在“工具执行环境”Tab 中明确选择受管工具工件使用原生地址还是 OSS 镜像。 +8. Skills、MCP 和工具执行环境通过一个“能力与工具”设置入口形成清晰的能力配置链路。 ## 3. 核心决策 @@ -50,6 +52,11 @@ Python、虚拟环境或 Conda 环境时,也不能被 GoodBuddy 静默替换 解释器通过诊断后标记“工具环境已就绪”。只有 SKILL 或 MCP 的实际入口及所需第三方包 通过验证后,才能标记“能力依赖已就绪”。GoodBuddy 首期不自动扫描和安装任意能力依赖。 +### 3.4 分阶段交付统一设置入口 + +第一阶段先把现有 Skills 和 MCP 合并到“能力与工具”,只显示两个真实可用的水平 Tab。 +“工具执行环境”实现完成后再加入第三个 Tab;未实现期间不展示禁用项、空白页或开发中占位。 + ## 4. 功能范围 ### 4.1 包含 @@ -60,6 +67,8 @@ Python、虚拟环境或 Conda 环境时,也不能被 GoodBuddy 静默替换 - 按需下载安装的托管 Python 3、pip、venv。 - 系统解释器探测、自定义文件选择和真实诊断。 - 工具来源、版本、路径、安装、删除、更新和错误状态。 +- 受管工具工件的原生地址/OSS 镜像选择。 +- “能力与工具”一级分类及 `Skills`、`MCP`、`工具执行环境` 三个水平 Tab。 - 新启动本机 Runtime 和 MCP 的 PATH 注入。 ### 4.2 不包含 @@ -74,6 +83,7 @@ Python、虚拟环境或 Conda 环境时,也不能被 GoodBuddy 静默替换 - 修改系统 PATH、注册表或 Shell Profile。 - 无条件把完整 Python 打入桌面安装包。 - 首期导入托管 Python 离线包;完全离线用户可选择已有的自定义 Python。 +- 修改模型下载源、应用更新源、Agent 下载源、pip/npm 注册表或用户配置的网络地址。 ## 5. 功能需求 @@ -87,7 +97,10 @@ Python、虚拟环境或 Conda 环境时,也不能被 GoodBuddy 静默替换 ### FR-2 Python 托管环境 - Python 未安装时展示安装入口,不把完整 Python 放入所有桌面安装包。 -- 下载复用“关于与更新”的官方 GitHub/镜像来源。 +- Windows x64 使用固定官方 `python` NuGet 工件,Windows ARM64 使用固定官方 + `pythonarm64` NuGet 工件;macOS/Linux 使用固定 Astral `python-build-standalone` + 工件,最终版本以真实诊断和共同支持矩阵为准。 +- 原生地址直接指向精确上游工件;OSS 镜像保存该工件的字节完全相同副本。 - 展示下载、校验、安装、取消、可更新、删除和失败状态。 - 下载失败保留已验证旧安装。 @@ -145,6 +158,33 @@ Python、虚拟环境或 Conda 环境时,也不能被 GoodBuddy 静默替换 - Execute 继续使用当前本机账号的完整授权。 - 不为工具环境增加第二套批准流程。 +### FR-11 工具下载源 + +- “工具执行环境”Tab 提供两个互斥选项:`原生地址` 和 `OSS 镜像`。 +- 全新用户默认使用原生地址;该选择独立于“关于与更新”和模型下载源。 +- 选择原生地址时,每个受管工件使用目录中固定的上游 URL。 +- 选择 OSS 镜像时,使用 GoodBuddy OSS 中固定的对应对象。 +- 两个 Target 必须声明同一逻辑工件、相同字节数和 SHA-256;OSS 不得重打包或修改内容。 +- 下载开始时冻结来源;运行期间切换设置只影响后续下载。 +- 当前来源失败、缺少工件或校验失败时明确失败,不自动请求另一个来源。 +- 该设置适用于当前 Python Runtime 和未来纳入受管工具目录的 Runtime/工具链。 +- 该设置不影响用户执行的 pip/npm 依赖安装、模型下载、应用更新、Agent 下载、HTTP/SSE + MCP 或用户配置 URL。 + +### FR-12 统一设置入口 + +- 设置中心只提供一个“能力与工具”一级分类,不再并列显示独立的 Skills、MCP 或工具执行 + 环境分类。 +- 第一阶段提供 `Skills`、`MCP` 两个水平 Tab;工具执行环境完成后依次提供 + `Skills`、`MCP`、`工具执行环境` 三个水平 Tab。始终默认打开 `Skills`。 +- 三个 Tab 使用共享 `PageTabs`,保留 `tablist`、`tab`、`tabpanel`、方向键和可见焦点。 +- `Skills` 继续承载现有 Skill 启停、导入、状态和 Runtime 分配。 +- `MCP` 继续承载现有 MCP 配置、凭据、状态和 Runtime 分配;其现有内部分组页签保持不变。 +- `工具执行环境` 承载工具下载源、Node.js、Python、诊断和生效范围。 +- 合并导航不改写 Skills/MCP 业务数据;未来新增定向入口时必须进入对应水平 Tab。 +- 切换水平 Tab 不丢弃当前编辑状态;不把当前水平 Tab 持久化为产品设置。 +- 未实现的工具执行环境不得以禁用 Tab 或占位内容提前显示。 + ## 6. 产品验收 - 无系统 Node.js 时,托管 Node 可以真实执行 JavaScript SKILL 和 stdio MCP。 @@ -153,6 +193,9 @@ Python、虚拟环境或 Conda 环境时,也不能被 GoodBuddy 静默替换 - 用户可明确选择并锁定自定义解释器。 - 自定义路径失效不触发静默切换。 - 工具就绪与能力依赖就绪不会混为同一状态。 -- 本机 Skills、MCP 和工具环境不会应用于远程主机。 +- 本机 Skills、MCP 和工具执行环境不会应用于远程主机。 +- 工具下载使用用户明确选择的原生地址或 OSS,且不会静默回退。 +- 第一阶段通过“能力与工具”统一入口展示 Skills/MCP 两个水平 Tab;工具执行环境完成后 + 追加第三个 Tab。 - 中英文界面、键盘操作、焦点和状态反馈符合统一设计系统。 - 全量测试、类型检查、Lint 和生产构建通过。 diff --git a/docs/features/local-tool-environment/progress.md b/docs/features/local-tool-environment/progress.md index ecfd805a..1a74d9f7 100644 --- a/docs/features/local-tool-environment/progress.md +++ b/docs/features/local-tool-environment/progress.md @@ -1,4 +1,4 @@ -# 本机工具环境功能进度 +# 工具执行环境功能进度 ## 文档信息 @@ -8,22 +8,27 @@ | 状态 | 计划中 | | 版本 | 0.1 | | 日期 | 2026-09-01 | -| 关联入口 | [本机工具环境](./README.md) | +| 关联入口 | [工具执行环境](./README.md) | ## 当前结论 -功能尚未进入源码实施。产品范围、User Stories、功能逻辑评估、UI 一致性和技术方案已经 -形成首版文档。 +设置菜单结构第一阶段已经进入源码:原 Skills/MCP 一级分类合并为“能力与工具”,内部 +显示两个水平 Tab。工具执行环境、Node/Python 和下载源尚未实施。 ## 已完成 - [x] 明确功能只服务本机 SKILL 和本机 stdio MCP。 -- [x] 明确远程 Host 不接收本机工具环境、SKILL 或 MCP。 +- [x] 明确远程 Host 不接收工具执行环境、SKILL 或 MCP。 - [x] 将来源收敛为“GoodBuddy 托管”和“自定义环境”。 - [x] 明确 Node、npm、npx 同源,Python、python3、pip 同源。 - [x] 区分工具环境就绪与能力依赖就绪。 - [x] 定义现有用户解释器的一次性兼容迁移。 - [x] 定义状态机、来源和命令决策表、失败恢复与生效时机。 +- [x] 定义受管工具外部工件的原生地址/OSS 镜像规则。 +- [x] 将 Skills、MCP、工具执行环境收敛到“能力与工具”一级分类和三个水平 Tab。 +- [x] 实现“能力与工具”一级菜单及 `Skills`、`MCP` 两个水平 Tab。 +- [x] 移除原 Skills/MCP 一级入口,并保留原有业务功能和 MCP 内层页签。 +- [x] 未提前显示“工具执行环境”禁用项或占位页。 - [x] 定义设置页结构、状态、文案和无障碍要求。 - [x] 定义 Main 服务、shim、探测、诊断、PATH 注入和 Python 生命周期。 - [x] 远程主机设置副标题已说明本机 Skills 和 MCP 不作用于远程主机。 @@ -36,7 +41,9 @@ - [ ] 旧用户迁移。 - [ ] Node/Python 候选探测。 - [ ] 完整环境诊断。 -- [ ] Agent Runtime 设置中的本机工具环境卡片。 +- [x] “能力与工具”统一设置分类及 Skills/MCP 水平 Tab。 +- [ ] 工具执行环境完成后追加第三个水平 Tab。 +- [ ] 工具下载源、Node.js、Python 和诊断卡片。 ### 阶段 2:托管 Node @@ -50,6 +57,7 @@ - [ ] 选型并锁定六平台 CPython 工件。 - [ ] 完成许可证和下载目录。 +- [ ] 为每个平台工件固定原生 Target 和字节相同的 OSS Target。 - [ ] 下载、校验、安装、更新、取消和删除。 - [ ] 提供 python/python3/pip。 - [ ] Python 标准库、SSL、pip、venv 真实诊断。 @@ -67,10 +75,24 @@ ## 当前阻塞项 - 尚未完成六平台可再分发 CPython 工件选型,因此托管 Python 版本、体积和下载摘要未锁定。 -- 尚未开始源码接入。 +- 工具执行环境的 Node/Python、下载源、诊断和进程注入尚未开始源码接入。 ## 已验证证据 +### 2026-09-02 + +- `npm run typecheck` 通过。 +- `npm run lint` 通过。 +- `npm test -- src/renderer/src/SettingsPanel.test.tsx` 通过:`80/80`。 +- 最近一次 `npm test` 为 `3410` 通过、`50` 跳过、`1` 失败;失败是并行修改中的浏览器 + 控制用例超时,该用例随后单独复跑通过。本功能聚焦测试全部通过。 +- 设置页测试确认左侧不再显示独立 Skills/MCP,统一入口默认打开 Skills,MCP 原内层页签 + 保持可用,且不显示工具执行环境占位。 +- 修复能力与工具 Tab 包装层在可滚动设置布局中被 Flexbox 压缩到零高度的问题,并增加与 + 平台功能相同的 `flex: 0 0 auto` CSS 回归测试;聚焦测试 `110/110` 通过。 +- Electron 自动化检查被本机已运行正式版的单实例锁和隔离开发版原生启动错误阻塞;未停止 + 或修改正式版进程,已清理本次隔离进程和临时用户数据。 + ### 2026-08-31 - 确认安装包携带 Electron Node 能力和锁定 npm CLI,但未向通用 SKILL/MCP 提供 `node` diff --git a/docs/features/local-tool-environment/technical-design.md b/docs/features/local-tool-environment/technical-design.md index 04e69d32..4eda092a 100644 --- a/docs/features/local-tool-environment/technical-design.md +++ b/docs/features/local-tool-environment/technical-design.md @@ -1,4 +1,4 @@ -# 本机工具环境技术设计 +# 工具执行环境技术设计 ## 文档信息 @@ -7,8 +7,8 @@ | 状态 | 计划 | | 版本 | 0.1 | | 日期 | 2026-09-01 | -| 关联 PRD | [本机工具环境 PRD](./prd.md) | -| 功能逻辑 | [本机工具环境功能逻辑设计](./logic-design.md) | +| 关联 PRD | [工具执行环境 PRD](./prd.md) | +| 功能逻辑 | [工具执行环境功能逻辑设计](./logic-design.md) | | 相关架构 | [DeepSeek Harness Runtime 设计](../deepseek-harness/technical-design.md)、[SSH 远程主机与 GoodBuddy Agent 实现说明](../remote-host/technical-design.md) | ## 1. 当前基线 @@ -36,6 +36,7 @@ type LocalToolEnvironmentSelection = type LocalToolEnvironmentSettings = { node: LocalToolEnvironmentSelection python: LocalToolEnvironmentSelection + artifactDownloadSource: 'native' | 'oss' } ``` @@ -48,6 +49,7 @@ type LocalToolEnvironmentSettings = { - 旧设置没有该字段时,Main 按升级前 PATH 分别解析有效 Node 和 Python 3。 - 有效候选写成 `custom + 规范化绝对路径`。 - 没有有效候选时写成 `managed`。 +- 新增的 `artifactDownloadSource` 默认迁移为 `native`。 - 迁移只执行一次,后续 PATH 变化不改变选择。 ## 3. Main 服务 @@ -114,12 +116,41 @@ ELECTRON_RUN_AS_NODE=1 - 使用包含 pip、venv、SSL 和标准库的固定 CPython 独立发行包。 - 每个平台/架构锁定一个版本。 -- 目录包含版本、平台、架构、来源、字节数、SHA-256 和许可证。 -- 下载复用当前官方 GitHub/镜像来源,不增加来源设置。 +- Windows x64 原生工件来自官方 `python` NuGet 包,Windows ARM64 来自官方 + `pythonarm64` NuGet 包。 +- macOS/Linux 原生工件来自 Astral `python-build-standalone`。 +- 目录包含版本、平台、架构、字节数、SHA-256、许可证、原生 Target 和 OSS Target。 +- OSS 对象直接复制原生工件字节,不重新压缩或转换。 - 安装包不无条件携带完整 Python。 具体发行包和版本必须在实现前完成六平台工件、许可证、维护状态和 pip/venv 可用性核验。 +目录示例: + +```ts +type ManagedToolArtifact = { + id: string + version: string + platform: NodeJS.Platform + architecture: string + size: number + sha256: string + downloads: { + native: { + url: string + redirectHosts: string[] + } + oss: { + url: string + } + } + licenseFiles: string[] +} +``` + +URL 只存在于 Main/构建目录,不进入 Renderer。两个 Target 共用顶层 `size` 和 `sha256`, +从 schema 上禁止声明不同字节。 + ### 6.2 安装 ```text @@ -211,6 +242,27 @@ stdio MCP: ## 11. Renderer 状态与 IPC +### 11.1 统一设置分类 + +- `settingsCategoryList` 用一个 `capabilities` 分类替换现有 `skills`、`mcp` 分类,不增加 + `sdk` 分类。 +- 新增 `CapabilitiesAndToolsSettingsSection`,在统一分类标题下使用共享 `PageTabs` 渲染 + 第一阶段的 `skills | mcp`。 +- `SkillsSettingsSection` 和 `McpSettingsSection` 继续拥有各自业务状态;嵌入时不重复渲染 + 一级分类标题。MCP 原有内层 `PageTabs` 保持独立。 +- `ToolEnvironmentSettingsSection` 只承载工具下载源、Node.js、Python 和诊断。 +- 当前源码没有 Settings 外部 Skills/MCP 定向入口,第一阶段不增加未使用的 + `initialCapabilityTab`。未来出现真实定向入口时再增加会话级初始 Tab 参数。 +- 普通进入 `capabilities` 时默认 `skills`。外层选中值只存在于当前 Renderer 会话,不写入 + Shared 设置。 +- 为避免切换时丢失编辑状态,外层面板首次访问时挂载,之后在本次设置面板会话中保留并用 + `hidden` 控制可见性;关闭设置面板后按现有生命周期释放。 +- 工具执行环境生产路径完成时,把 `tool-environment` 追加到同一个组件;完成前不注册 + Tab、不渲染占位面板。 +- 页签直接包装层使用 `flex: 0 0 auto`,并由 CSS 回归测试保证在滚动设置布局中不可收缩。 + +### 11.2 工具环境状态与 IPC + Renderer 接收: - 当前选择。 @@ -219,11 +271,24 @@ Renderer 接收: - 托管安装状态、版本、占用空间。 - 工具诊断分项结果。 - 安装进度和脱敏错误。 +- 受管工具工件下载源选择,以及活动任务实际冻结的来源。 Renderer 不能提交 PATH、环境变量、下载 URL、摘要、安装目录、shim 内容或任意命令参数。 所有 IPC 输入使用共享 Zod schema,Main 重新验证路径和解释器。 -## 12. 权限与远程边界 +## 12. 受管工具工件下载 + +- 设置值为 `native | oss`,默认 `native`。 +- 下载操作开始时读取并冻结设置。 +- 解析目录中对应 Target;缺失即返回“当前下载源不可用”。 +- 原生 Target 按每个上游声明严格 Host/重定向 allowlist。 +- OSS Target 只允许 GoodBuddy 固定 OSS Host 和不可变对象前缀。 +- 两种来源共用大小和 SHA-256 校验、暂存、取消、发布和清理逻辑。 +- 失败不重试另一来源。 +- 构建/发布流程先下载原生工件、校验,再把原字节同步到 OSS,并复核公开对象。 +- 该设置不传给 pip/npm,不改写其 registry;也不参与模型、应用更新和 Agent 目录选择。 + +## 13. 权限与远程边界 - Ask/Execute 语义不变。 - 不新增解释器批准流程。 @@ -232,11 +297,13 @@ Renderer 不能提交 PATH、环境变量、下载 URL、摘要、安装目录 - 不把本机 SKILL 或 stdio MCP 传给远程 ACP Session。 - 远程 Agent 固定 Node 继续只服务 Agent、模型桥和签名 Runtime。 -## 13. 实施顺序 +## 14. 实施顺序 ### 阶段 1:契约、迁移、探测和 UI - Shared schema、设置迁移、IPC。 +- 用“能力与工具”分类合并现有 Skills/MCP 入口,增加三个外层水平 Tab。 +- 增加“工具执行环境”面板及原生/OSS 下载源设置。 - Node/Python 候选探测和完整诊断。 - 设置卡片和状态。 @@ -263,10 +330,13 @@ Renderer 不能提交 PATH、环境变量、下载 URL、摘要、安装目录 这些阶段是开发顺序,不是可长期发布的残缺功能。合并能力前完成普通用户和专业用户的完整 生产路径。 -## 14. 验证 +## 15. 验证 单元测试覆盖设置迁移、探测、版本解析、shim 转义、PATH 顺序、失效路径、下载摘要和暂存 -清理。 +清理,以及原生/OSS 选择冻结、Target 缺失和禁止回退。 + +Renderer 测试覆盖统一一级分类、三个外层 Tab、默认 Skills、Skills/MCP 定向入口、会话内 +状态保留、MCP 内外层 tablist 隔离、键盘切换和窄屏单行滚动。 集成测试覆盖安装包 Electron Node、npm/npx、托管 Python、三个本机 Runtime 和两类 stdio MCP 的实际进程路径。 diff --git a/docs/features/local-tool-environment/ui-design.md b/docs/features/local-tool-environment/ui-design.md index ae9c8d36..9e797c27 100644 --- a/docs/features/local-tool-environment/ui-design.md +++ b/docs/features/local-tool-environment/ui-design.md @@ -1,4 +1,4 @@ -# 本机工具环境 UI 设计 +# 工具执行环境 UI 设计 ## 文档信息 @@ -7,8 +7,8 @@ | 状态 | 计划 | | 版本 | 0.1 | | 日期 | 2026-09-01 | -| 关联 PRD | [本机工具环境 PRD](./prd.md) | -| 功能逻辑 | [本机工具环境功能逻辑设计](./logic-design.md) | +| 关联 PRD | [工具执行环境 PRD](./prd.md) | +| 功能逻辑 | [工具执行环境功能逻辑设计](./logic-design.md) | | 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) | 本文只定义本功能的界面结构和交互。颜色、字体、间距、通知、Switch、焦点和响应式行为以 @@ -16,36 +16,122 @@ ## 1. 信息架构 -“本机工具环境”位于 `Agent Runtime` 设置分类,处于默认工作区之后、具体 Runtime 高级 -设置之前。它是本机 OpenCode、Continue、DeepSeek Harness 和 stdio MCP 的共享设置, -不放入任一 Runtime 专属卡片。 +设置中心把现有 `Skills`、`MCP` 一级分类和新增的工具执行环境合并为一个“能力与工具” +一级分类。内容区使用三个水平 Tab: -区块顶部持续显示: +```text +[ Skills ] [ MCP ] [ 工具执行环境 ] +``` + +一级分类副标题固定为: + +> 管理 Skills、MCP 和本机工具执行环境 + +“工具执行环境”不放入任一 Runtime 专属卡片。该 Tab 顶部持续显示: > 用于新启动的本机 Runtime 和 stdio MCP;不修改系统环境,也不应用于普通终端或远程主机。 +### 1.1 分阶段显示 + +第一阶段只实现统一菜单和现有能力页签: + +```text +[ Skills ] [ MCP ] +``` + +此阶段不显示“工具执行环境”禁用 Tab、空页面或“开发中”占位。Node.js、Python 和工具下载源 +真实可用后,再按本设计追加第三个水平 Tab。 + +“能力与工具”的页签条位于独立滚动的 Flex 设置内容区,直接包装层必须保持 +`flex: 0 0 auto`,避免被下方长列表压缩到零高度。该规则同时服从根 +[`UI-DESIGN.md`](../../../UI-DESIGN.md) 的 `PageTabs` 规范。 + ## 2. 页面结构 ```text -本机工具环境 -范围说明 +能力与工具 +管理 Skills、MCP 和本机工具执行环境 -Node.js 卡片 - 来源 - 当前状态 - 自定义候选或路径操作 - 测试环境 +[ Skills ] [ MCP ] [ 工具执行环境 ] -Python 卡片 - 来源 - 当前状态 - 安装/更新/删除或自定义路径操作 - 测试环境 +Skills 面板 + 现有 Skill 启停、导入、状态和 Runtime 分配 + +MCP 面板 + 现有 MCP 配置、凭据、状态和 Runtime 分配 + [内置 MCP] [直连模型] [自定义 MCP] [电脑控制] + +工具执行环境面板 + 范围说明 + + 工具下载源 + 原生地址 / OSS 镜像 + + Node.js 卡片 + 来源 + 当前状态 + 自定义候选或路径操作 + 测试环境 + + Python 卡片 + 来源 + 当前状态 + 安装/更新/删除或自定义路径操作 + 测试环境 ``` -卡片顺序固定为 Node.js、Python。页面不增加二级页签。 +工具执行环境面板内的区块顺序固定为工具下载源、Node.js、Python,不再增加页签。 -## 3. 来源选择 +## 3. 三个水平 Tab + +外层页签使用共享 `PageTabs` 的 `segmented` 视觉变体,不使用页面自定义 Tab 或普通 +`SegmentedControl`: + +```tsx + +``` + +交互规则: + +- 普通入口默认打开 `Skills`。 +- 现有 Skills/MCP 应用内入口分别定位到对应水平 Tab。 +- 切换外层 Tab 不保存为产品设置,不修改任何 Skill、MCP 或工具环境值。 +- 三个面板在同一次设置面板会话中保留编辑状态;切换后返回不得重置草稿。 +- MCP 原有四个内层页签继续保留,外层和内层使用不同的 `aria-label` 与 `idPrefix`。 +- 窄屏时外层页签保持单行并允许水平滚动,不折成多行,不替换为下拉框。 + +## 4. 工具下载源 + +使用原生 Radio Group: + +```text +工具下载源 +选择 GoodBuddy 受管 Runtime 和工具链的下载通道。 + +● 原生地址(默认) + 从各工件的固定上游地址下载。 + +○ OSS 镜像 + 从 GoodBuddy OSS 下载相同工件。 +``` + +邻近说明: + +> 下载开始后会固定本次来源;失败时不会自动切换。该设置不影响模型、应用更新、Agent、 +> pip/npm 依赖或用户配置的网络地址。 + +切换设置即时保存,不触发下载,不取消活动任务。成功通过应用通知反馈;保存失败恢复最后 +确认值并在 Radio Group 附近显示错误。 + +## 5. 工具环境来源选择 每张卡片使用原生单选语义,只提供: @@ -64,7 +150,7 @@ Python 3.13.2 · C:\Python313\python.exe 版本和绝对路径不能只放在 Tooltip 中。 -## 4. Node.js 卡片 +## 6. Node.js 卡片 ```text Node.js @@ -84,7 +170,7 @@ Node.js Node 成功代替整套组合成功。找不到配套 npm/npx 时,自定义 Node 可以保存,但状态显示 “Node.js 已就绪,npm/npx 不可用”。 -## 5. Python 卡片 +## 7. Python 卡片 托管 Python 未安装: @@ -121,7 +207,7 @@ Python > 删除后,依赖托管 Python 的本机 SKILL 和 stdio MCP 将不可用。系统 Python、自定义环境 > 和项目文件不会被删除。 -## 6. 两层状态 +## 8. 两层状态 每张工具卡展示工具环境状态: @@ -142,7 +228,7 @@ SKILL/MCP 的能力详情展示能力依赖状态: 不得把工具卡的“已就绪”复用为某个 SKILL 或 MCP 的“完全可用”。 -## 7. 生效与刷新 +## 9. 生效与刷新 保存后在卡片中持续显示: @@ -151,7 +237,7 @@ SKILL/MCP 的能力详情展示能力依赖状态: 设置保存成功、安装成功、删除成功和重新检测成功使用应用通知。版本、路径、安装进度和 阻塞错误留在卡片内,不同时显示重复通知。 -## 8. 错误与恢复 +## 10. 错误与恢复 - 自定义路径失效:保留路径和来源选择,显示“重新检测”和“选择其他文件”。 - 下载失败:保留已验证旧安装和安装按钮。 @@ -159,9 +245,12 @@ SKILL/MCP 的能力详情展示能力依赖状态: - 取消安装:恢复“尚未安装”或原已安装版本。 - 字段错误:就地关联到选择控件。 - Runtime/MCP 已运行:不显示为错误,只说明新进程生效。 +- 工具下载源失败:保留当前选择,显示实际来源和重试,不提示自动回退。 -## 9. 无障碍与键盘 +## 11. 无障碍与键盘 +- 外层使用 `tablist`/`tab`/`tabpanel`,支持左右方向键、Home、End 和 roving focus。 +- 每个外层 `tab` 通过共享组件准确关联自己的 `tabpanel`。 - 来源使用 `radiogroup`/`radio` 语义。 - 安装、测试、更新、删除和文件选择使用语义按钮。 - 下载进度使用可命名的 `progressbar`。 @@ -171,17 +260,23 @@ SKILL/MCP 的能力详情展示能力依赖状态: - 对话框关闭后焦点回到触发操作。 - 窄屏时卡片操作换行,不压缩版本和路径到不可读宽度。 -## 10. 文案词汇 +## 12. 文案词汇 固定使用: -- 本机工具环境 +- 能力与工具 +- Skills +- MCP +- 工具执行环境 - GoodBuddy 托管 - 自定义环境 - 工具环境已就绪 - 能力依赖已验证 - 新启动的本机 Runtime 和 stdio MCP - 不应用于远程主机 +- 工具下载源 +- 原生地址 +- OSS 镜像 不使用: @@ -190,3 +285,4 @@ SKILL/MCP 的能力详情展示能力依赖状态: - 内置开发机 - 全局 Python - 远程托管环境 +- 开发环境 diff --git a/docs/features/local-tool-environment/user-stories.md b/docs/features/local-tool-environment/user-stories.md index 4dbefd46..0d92db31 100644 --- a/docs/features/local-tool-environment/user-stories.md +++ b/docs/features/local-tool-environment/user-stories.md @@ -1,4 +1,4 @@ -# 本机工具环境 User Stories +# 工具执行环境 User Stories ## 文档信息 @@ -7,8 +7,8 @@ | 状态 | 计划 | | 版本 | 0.2 | | 日期 | 2026-09-01 | -| 关联 PRD | [本机工具环境 PRD](./prd.md) | -| 关联逻辑 | [本机工具环境功能逻辑设计](./logic-design.md) | +| 关联 PRD | [工具执行环境 PRD](./prd.md) | +| 关联逻辑 | [工具执行环境功能逻辑设计](./logic-design.md) | ## 1. 角色 @@ -41,7 +41,7 @@ - 托管 Node 立即执行真实诊断并显示版本 - 未安装的托管 Python 显示“尚未安装”和安装入口 - 检测到的系统解释器只作为自定义候选,不改变默认来源 -- 页面持续说明只对新启动的本机 Runtime 和 stdio MCP 生效 +- “工具执行环境”Tab 持续说明只对新启动的本机 Runtime 和 stdio MCP 生效 ### US-A1 直接使用托管 Node @@ -121,7 +121,7 @@ - HTTP/SSE 配置不显示本机解释器要求 - GoodBuddy 不管理远端 MCP 服务端依赖 -- 切换本机工具环境不重建 HTTP/SSE 服务 +- 切换工具执行环境不重建 HTTP/SSE 服务 ## 4. Epic C:专业用户明确控制环境 @@ -177,7 +177,7 @@ 验收: -- 本机工具环境页显示范围说明 +- “工具执行环境”Tab 显示范围说明 - 远程主机设置页显示本机 Skills 和 MCP 不适用 - Host PATH、Agent 包和 Runtime 包不包含本机选择 @@ -242,14 +242,15 @@ ### US-E1 通过所选官方来源安装 Python -作为内网用户,我希望托管 Python 使用“关于与更新”中选择的官方来源,以便符合当前网络 -条件。 +作为内网用户,我希望在“工具执行环境”Tab 中选择原生地址或 OSS 镜像,以便符合当前 +网络条件。 验收: -- Python 安装复用当前 GitHub/镜像来源 -- 来源不可达时显示来源和准确失败原因 -- 同一次安装不静默切换到另一来源 +- 默认使用 Python 工件的固定原生地址 +- 选择 OSS 后,Python 只请求固定 OSS 对象 +- 页面显示本次下载冻结的来源和准确失败原因 +- 同一次安装不在原生地址和 OSS 之间静默切换 - 失败保留健康旧版本或未安装状态 ### US-E2 完全离线时使用已有环境 @@ -262,3 +263,48 @@ - 托管 Python 未缓存且来源不可达时明确说明无法下载 - 首期不展示未实现的 Python 离线导入入口 - 用户可以选择设备上已经离线安装的自定义 Python + +### US-E3 所有受管工具工件遵循同一下载通道 + +作为用户,我希望未来新增的受管 Runtime 和工具链遵循同一个下载源选择,以免每种工具 +产生不同规则。 + +验收: + +- 受管工具目录中的每个外部工件同时声明原生地址和 OSS Target +- 两个 Target 对应相同字节数和 SHA-256 +- 下载任务开始后冻结来源 +- 所选来源缺少该工件时明确显示不可用 +- pip/npm 依赖、模型、应用更新、Agent 和用户 URL 不受该设置影响 + +## 7. Epic F:统一管理能力与执行环境 + +### US-F1 从一个入口管理相关能力 + +作为用户,我希望从一个设置入口访问 Skills、MCP 和工具执行环境,以便理解能力配置及其 +本机依赖之间的关系。 + +验收: + +- 设置中心只显示一个“能力与工具”一级分类 +- 第一阶段依次显示 `Skills`、`MCP` 两个水平 Tab +- 工具执行环境完成后在同一组中追加第三个 `工具执行环境` Tab +- 未完成前不显示禁用或占位的 `工具执行环境` +- 首次进入默认打开 `Skills` +- 每个 Tab 使用独立、可命名的 `tabpanel` +- 水平 Tab 支持方向键、Home、End 和可见焦点 +- 窄屏时页签保持单行横向滚动,不换成多行或下拉框 + +### US-F2 保持业务数据和编辑状态 + +作为现有用户,我希望合并设置导航后原有 Skills 和 MCP 配置保持不变,并且切换水平 Tab +不丢失正在编辑的信息。 + +验收: + +- 原独立 Skills/MCP 左侧分类由一个“能力与工具”分类替代,不保留重复入口 +- 合并导航不修改 Skill 启停、导入、分配或 MCP 配置、凭据和分配数据 +- MCP 原有内部分组页签继续位于 MCP 面板内 +- 切换三个外层 Tab 后返回时保留本次设置面板会话中的编辑状态 +- 关闭并重新打开设置时,普通入口仍默认显示 Skills,不持久化上次外层 Tab +- 未来新增 Skills/MCP 定向入口时打开对应水平 Tab diff --git a/docs/features/remote-host/technical-design.md b/docs/features/remote-host/technical-design.md index 6d7bf9b9..c77616ba 100644 --- a/docs/features/remote-host/technical-design.md +++ b/docs/features/remote-host/technical-design.md @@ -175,8 +175,8 @@ Main 传入已规范化的绝对 POSIX root。Agent 返回 Workspace identity、 直连模型、Continue 或 DeepSeek Harness。激活旧远程会话时,若其保存了其他 Runtime selection,Renderer 会恢复为当前 OpenCode 配置;Main 的远程请求校验仍是最终边界。 本机配置的 SKILL 包和 stdio MCP Server 不上传或分配给远程 Runtime;远程 ACP Session -继续使用空 MCP Server 列表。Node.js、Python 等本机工具环境同样不修改或同步到 Host, -完整边界见[本机工具环境](../local-tool-environment/README.md)。 +继续使用空 MCP Server 列表。Node.js、Python 等工具执行环境同样不修改或同步到 Host, +完整边界见[工具执行环境](../local-tool-environment/README.md)。 ### Ask diff --git a/src/renderer/src/CapabilitiesAndToolsSettingsSection.tsx b/src/renderer/src/CapabilitiesAndToolsSettingsSection.tsx new file mode 100644 index 00000000..6c693a88 --- /dev/null +++ b/src/renderer/src/CapabilitiesAndToolsSettingsSection.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { McpSettingsSection } from './McpSettingsSection' +import { SettingsCategoryHeader } from './SettingsPrimitives' +import { SkillsSettingsSection } from './SkillsSettingsSection' +import { PageTabs } from './WorkspacePrimitives' + +type CapabilitySettingsTab = 'skills' | 'mcp' + +export function CapabilitiesAndToolsSettingsSection({ + magicNotesEnabled +}: { + magicNotesEnabled: boolean +}): React.JSX.Element { + const { t } = useTranslation('settings') + const [activeTab, setActiveTab] = + useState('skills') + const [mcpVisited, setMcpVisited] = useState(false) + + return ( + <> + +
+ { + setActiveTab(tab) + if (tab === 'mcp') { + setMcpVisited(true) + } + }} + tabs={[ + { + id: 'skills', + label: t('capabilities.tabs.skills') + }, + { + id: 'mcp', + label: t('capabilities.tabs.mcp') + } + ]} + value={activeTab} + variant="segmented" + /> +
+ + + + ) +} diff --git a/src/renderer/src/McpSettingsSection.tsx b/src/renderer/src/McpSettingsSection.tsx index dd5bd04a..661693ea 100644 --- a/src/renderer/src/McpSettingsSection.tsx +++ b/src/renderer/src/McpSettingsSection.tsx @@ -32,7 +32,7 @@ import type { } from '../../shared/capability-contracts' import { trapTabFocus } from './dialog-focus' import { - SettingsCategoryHeader, + SettingsSectionHeader, SettingsWarningList } from './SettingsPrimitives' import { PageTabs } from './WorkspacePrimitives' @@ -394,7 +394,7 @@ export function McpSettingsSection({ return ( <> - ) : undefined } - category="mcp" + description={t('mcp.description')} error={!editor ? error : undefined} + headingLevel={3} headingId="mcp-settings-heading" + title={t('mcp.title')} />
diff --git a/src/renderer/src/SettingsPanel.test.tsx b/src/renderer/src/SettingsPanel.test.tsx index 805ab00a..00bf35fd 100644 --- a/src/renderer/src/SettingsPanel.test.tsx +++ b/src/renderer/src/SettingsPanel.test.tsx @@ -53,6 +53,17 @@ import type { BrandingPreferences } from './branding' const modelProfileId = '00000000-0000-4000-8000-000000000001' const nativeImage = globalThis.Image +function openCapabilitySettingsTab(tab: 'Skills' | 'MCP'): void { + fireEvent.click( + screen.getByRole('tab', { name: '能力与工具' }) + ) + fireEvent.click( + within( + screen.getByRole('tablist', { name: '能力与工具设置' }) + ).getByRole('tab', { name: tab }) + ) +} + class DecodableBrandImage { naturalWidth = 64 naturalHeight = 64 @@ -1619,7 +1630,7 @@ describe('SettingsPanel runtime files', () => { ) - fireEvent.click(screen.getByRole('tab', { name: 'MCP' })) + openCapabilitySettingsTab('MCP') const noteServerToggle = await screen.findByRole('button', { name: '展开服务器 笔记' }) @@ -1642,7 +1653,7 @@ describe('SettingsPanel runtime files', () => { }) ) - fireEvent.click(screen.getByRole('tab', { name: 'MCP' })) + openCapabilitySettingsTab('MCP') await waitFor(() => expect( screen @@ -4802,7 +4813,7 @@ describe('SettingsPanel runtime files', () => { expect(screen.queryByText('智能心跳')).not.toBeInTheDocument() }) - it('shows Skills and MCP as first-class settings tabs', async () => { + it('groups Skills and MCP under Capabilities and tools', async () => { render( { /> ) - fireEvent.click(screen.getByRole('tab', { name: 'Skills' })) + const categoryTabs = screen.getByRole('tablist', { + name: '设置分类' + }) + expect( + within(categoryTabs).queryByRole('tab', { name: 'Skills' }) + ).not.toBeInTheDocument() + expect( + within(categoryTabs).queryByRole('tab', { name: 'MCP' }) + ).not.toBeInTheDocument() + + fireEvent.click( + within(categoryTabs).getByRole('tab', { name: '能力与工具' }) + ) + const capabilityTabs = screen.getByRole('tablist', { + name: '能力与工具设置' + }) + expect( + within(capabilityTabs) + .getAllByRole('tab') + .map((tab) => tab.textContent) + ).toEqual(['Skills', 'MCP']) + expect( + within(capabilityTabs).getByRole('tab', { name: 'Skills' }) + ).toHaveAttribute('aria-selected', 'true') expect(await screen.findByText('文档写作')).toBeInTheDocument() expect( screen.getByText( @@ -4848,7 +4882,15 @@ describe('SettingsPanel runtime files', () => { ) ) - fireEvent.click(screen.getByRole('tab', { name: 'MCP' })) + expect( + within(capabilityTabs).queryByRole('tab', { + name: '工具执行环境' + }) + ).not.toBeInTheDocument() + + fireEvent.click( + within(capabilityTabs).getByRole('tab', { name: 'MCP' }) + ) const mcpTabs = screen.getByRole('tablist', { name: 'MCP 设置分类' }) @@ -5160,7 +5202,7 @@ describe('SettingsPanel runtime files', () => { /> ) - fireEvent.click(screen.getByRole('tab', { name: 'MCP' })) + openCapabilitySettingsTab('MCP') fireEvent.click(screen.getByRole('tab', { name: '自定义 MCP' })) const addServer = await screen.findByRole('button', { name: '添加 Server' @@ -5235,22 +5277,24 @@ describe('SettingsPanel runtime files', () => { }) it('opens an existing MCP Server in the same modal and returns focus on Escape', async () => { - getCapabilitySnapshot.mockResolvedValueOnce({ - ...capabilitySnapshot, - mcpServers: [ - { - id: '00000000-0000-4000-8000-000000000301', - name: '团队知识服务', - description: '公司内部 MCP', - enabled: true, - allowDynamicTools: true, - assignments: ['model'], - secretConfigured: true, - transport: 'http', - url: 'https://mcp.example.com/mcp' - } - ] - }) + getCapabilitySnapshot + .mockResolvedValueOnce(capabilitySnapshot) + .mockResolvedValueOnce({ + ...capabilitySnapshot, + mcpServers: [ + { + id: '00000000-0000-4000-8000-000000000301', + name: '团队知识服务', + description: '公司内部 MCP', + enabled: true, + allowDynamicTools: true, + assignments: ['model'], + secretConfigured: true, + transport: 'http', + url: 'https://mcp.example.com/mcp' + } + ] + }) render( { /> ) - fireEvent.click(screen.getByRole('tab', { name: 'MCP' })) + openCapabilitySettingsTab('MCP') fireEvent.click(screen.getByRole('tab', { name: '自定义 MCP' })) const editButton = await screen.findByRole('button', { name: '编辑 团队知识服务' @@ -5287,22 +5331,24 @@ describe('SettingsPanel runtime files', () => { }) it('shows custom MCP tools under their expandable server after testing', async () => { - getCapabilitySnapshot.mockResolvedValueOnce({ - ...capabilitySnapshot, - mcpServers: [ - { - id: '00000000-0000-4000-8000-000000000302', - name: '团队工具服务', - description: '公司内部工具', - enabled: true, - allowDynamicTools: true, - assignments: ['model'], - secretConfigured: false, - transport: 'http', - url: 'https://mcp.example.com/mcp' - } - ] - }) + getCapabilitySnapshot + .mockResolvedValueOnce(capabilitySnapshot) + .mockResolvedValueOnce({ + ...capabilitySnapshot, + mcpServers: [ + { + id: '00000000-0000-4000-8000-000000000302', + name: '团队工具服务', + description: '公司内部工具', + enabled: true, + allowDynamicTools: true, + assignments: ['model'], + secretConfigured: false, + transport: 'http', + url: 'https://mcp.example.com/mcp' + } + ] + }) vi.mocked( window.goodbuddy.capabilities.testMcpServer ).mockResolvedValueOnce({ @@ -5352,7 +5398,7 @@ describe('SettingsPanel runtime files', () => { /> ) - fireEvent.click(screen.getByRole('tab', { name: 'MCP' })) + openCapabilitySettingsTab('MCP') fireEvent.click(screen.getByRole('tab', { name: '自定义 MCP' })) const serverToggle = await screen.findByRole('button', { name: '展开服务器 团队工具服务' diff --git a/src/renderer/src/SettingsPanel.tsx b/src/renderer/src/SettingsPanel.tsx index e53eeb15..350cb333 100644 --- a/src/renderer/src/SettingsPanel.tsx +++ b/src/renderer/src/SettingsPanel.tsx @@ -41,9 +41,8 @@ import { isAgentRuntimeModelProtocol, isDeepSeekHarnessModelProfile } from '../../shared/contracts' -import { McpSettingsSection } from './McpSettingsSection' import { RolePromptSettingsSection } from './RolePromptSettingsSection' -import { SkillsSettingsSection } from './SkillsSettingsSection' +import { CapabilitiesAndToolsSettingsSection } from './CapabilitiesAndToolsSettingsSection' import { ChannelSettingsSection } from './ChannelSettingsSection' import { UpdateSettingsSection } from './UpdateSettingsSection' import { PlatformFeaturesSettingsSection } from './PlatformFeaturesSettingsSection' @@ -842,8 +841,7 @@ export function SettingsPanel({ activeTab === 'document-parsing' || activeTab === 'ssh-hosts' || activeTab === 'channels' || - activeTab === 'skills' || - activeTab === 'mcp' || + activeTab === 'capabilities' || activeTab === 'about' const savedConfiguredSettings = settings @@ -3930,9 +3928,8 @@ export function SettingsPanel({ /> )} - {activeTab === 'skills' && } - {activeTab === 'mcp' && ( - )} diff --git a/src/renderer/src/SettingsPrimitives.tsx b/src/renderer/src/SettingsPrimitives.tsx index 2bd7f989..fe79fada 100644 --- a/src/renderer/src/SettingsPrimitives.tsx +++ b/src/renderer/src/SettingsPrimitives.tsx @@ -46,11 +46,38 @@ export function SettingsCategoryHeader({ headingId?: string }): React.JSX.Element { const definition = settingsCategories[category] + return ( + + ) +} + +export function SettingsSectionHeader({ + actions, + description, + error, + headingLevel = 2, + headingId, + title +}: { + actions?: ReactNode + description: string + error?: string + headingLevel?: 2 | 3 + headingId: string + title: string +}): React.JSX.Element { + const Heading = headingLevel === 3 ? 'h3' : 'h2' return (
-

{definition.label}

-

{definition.description}

+ {title} +

{description}

{actions && (
diff --git a/src/renderer/src/SkillsSettingsSection.tsx b/src/renderer/src/SkillsSettingsSection.tsx index b4442951..cacbbf8d 100644 --- a/src/renderer/src/SkillsSettingsSection.tsx +++ b/src/renderer/src/SkillsSettingsSection.tsx @@ -6,7 +6,7 @@ import type { CapabilitySnapshot, RuntimeTarget } from '../../shared/capability-contracts' -import { SettingsCategoryHeader } from './SettingsPrimitives' +import { SettingsSectionHeader } from './SettingsPrimitives' export function SkillsSettingsSection(): React.JSX.Element { const { t } = useTranslation('settingsSections') @@ -62,7 +62,7 @@ export function SkillsSettingsSection(): React.JSX.Element { return ( <> -