diff --git a/CLAUDE.md b/CLAUDE.md index 649b99b4..1dafdb52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,14 @@ codegraph explore "binding Service Install" ## 文档维护 -README、workflow、Taskfile 和 AI Agent Kit 里的命令必须对应当前仓库文件。历史 ADR 与已完成的计划文档可以保留背景,但必须明确标记状态,不能作为操作指南。 +`docs/` 按受众分层,新增文档时先选对位置: + +- `docs/` 根:当前有效的规范与政策,读者可以直接照着执行。 +- `docs/ai-agent-kit/`:面向使用者的操作文档。默认路径是下载发行包,不是从源码构建。 +- `docs/decisions/`:ADR。被推翻的决策保留原文并标注 Superseded 及其去向,不删改历史。 +- `docs/internal/`:维护者视角的完工记录与验证清单。不放未实施的计划——那属于 issue。 + +README、workflow、Taskfile 和 AI Agent Kit 里的命令必须对应当前仓库文件。`docs/internal/` +里引用已移除工具的命令块用 ` ```text ` 而不是 ` ```bash `,避免被当成可运行指令。 `LICENSE` 是 Apache-2.0,`NOTICE` 是第三方归属的真源。新增随包分发的依赖,或在界面里加入新的第三方标识时,必须同步 `NOTICE`——`docs/distribution-compliance-policy.md` 把它列为发布前置条件。 diff --git a/README.md b/README.md index 04fb92b9..e2e7674d 100644 --- a/README.md +++ b/README.md @@ -139,14 +139,27 @@ GitHub Actions 而移除。第三方归属现在维护在仓库根的 [NOTICE](N ## 文档 -- [Wails v3 迁移收尾计划](docs/wails-v3-migration-plan.md) -- [发行与合规政策](docs/distribution-compliance-policy.md) +`docs/` 按受众分三层:根下是当前有效的规范,`decisions/` 是架构决策, +`internal/` 是维护者视角的历史记录。 + +**使用与规范** + +- [AI Agent Kit](docs/ai-agent-kit/00-start-here.md):从零配置一个 Agent 环境 +- [产品边界基线](docs/product-boundary-baseline.md):做什么、不做什么,以及为什么 +- [分发与合规政策](docs/distribution-compliance-policy.md):发行前的权利、安全与渠道要求 - [公开站运营手册](docs/public-site-operations.md) -- [Provider RC 测试说明](docs/provider-rc-testing.md) -- [AI Agent Kit](docs/ai-agent-kit/00-start-here.md) -- [Wails 架构 ADR](docs/decisions/ADR-007-wails-v3-go-migration.md) -- [按 Agent 协议验证 ADR](docs/decisions/ADR-004-per-agent-protocol-verification.md) -- [历史 Python 发行 ADR(已废弃)](docs/decisions/ADR-003-three-platform-python-core-and-release-policy.md) + +**架构决策** + +- [decisions/](docs/decisions/):ADR-001 至 ADR-009,含已被取代的决策及其去向 +- [Wails v3 / Go 迁移](docs/decisions/ADR-007-wails-v3-go-migration.md) +- [按 Agent 协议验证](docs/decisions/ADR-004-per-agent-protocol-verification.md) +- [凭据写入 Agent 配置文件](docs/decisions/ADR-008-credentials-in-agent-config-files.md) + +**内部实现记录** + +- [internal/](docs/internal/README.md):各次改造的完工记录与验证清单。里面的命令可能已随 + 工具移除而失效,该目录的 README 说明了当前的替代入口。 ## 许可证 diff --git a/docs/ai-agent-kit-plan.md b/docs/ai-agent-kit-plan.md deleted file mode 100644 index 6f460a99..00000000 --- a/docs/ai-agent-kit-plan.md +++ /dev/null @@ -1,362 +0,0 @@ -# OneAgent AI 开发环境激活器产品设计 - -> 本文是 OneAgent 的通用产品规划。产品边界以 [OneAgent 产品边界基线](product-boundary-baseline.md) 为准。 - -## 1. 产品定义 - -OneAgent 是一个帮助用户激活、配置和启动本地 AI 开发环境的工具。 - -它连接三类对象: - -```text -模型服务 Provider - ↓ -本地配置和 Profile - ↓ -AI Agent / IDE / Gateway 工具 -``` - -OneAgent 的结果不是“下载了多少软件”,而是: - -> 用户能否在自己的设备上完成第一次成功的 AI Agent 请求。 - -## 2. 核心边界 - -### OneAgent 负责 - -- 引导用户注册或登录 PPIO 等模型服务。 -- 引导用户创建自己的 API Key。 -- 检测本机 Agent 和运行环境。 -- 调用官方安装源,或提供官方安装命令。 -- 写入已经确认的 Agent 配置入口。 -- 获取模型列表并选择模型 ID。 -- 完成最小请求验证。 -- 提供 Agent、配置工具和项目模板文档。 - -### OneAgent 不负责 - -- VPN、代理、专线或绕过网络限制。 -- 共享 API Key 或默认统一网关。 -- 自动操作第三方网站登录、验证码和账户权益。 -- 未授权重新分发商业 Agent 包体。 -- 默认上传 API Key、Prompt、源代码或完整请求日志。 - -## 3. 用户范围 - -OneAgent 面向所有需要本地 AI 开发环境的用户,包括: - -- 第一次使用 AI Agent 的个人用户。 -- 已有 PPIO 账号和 API Key 的开发者。 -- 需要多个 Provider 或模型 Profile 的高级用户。 -- 公司、社区和其他组织发行方。 - -组织发行方可以提供自己的说明、下载入口和项目模板,但这些内容属于发行方配置,不进入 OneAgent 核心数据模型。 - -## 4. 产品组成 - -### 4.1 Activation Center - -线上入口负责: - -- 解释产品能做什么。 -- 引导用户前往 Provider 官方注册页。 -- 提供 API Key 和模型配置说明。 -- 提供启动器下载。 -- 展示当前版本、兼容范围和安全边界。 - -它不是复杂的营销落地页,而是一个面向操作的激活入口。 - -### 4.2 Local Launcher - -本地启动器负责: - -- 启动本地 GUI。 -- 检测 Agent。 -- 选择 Provider。 -- 输入 API Key。 -- 获取模型列表。 -- 选择配置方式。 -- 安装和写入配置。 -- 测试第一次请求。 - -### 4.3 Configuration Tools - -配置工具是独立的可选层: - -- OneAgent 内置配置:默认路径。 -- CC Switch:多 Provider、多账号、多 Profile 场景。 -- 手动配置:高级用户和故障排查路径。 - -OneAgent 不把第三方配置工具打进启动包,也不默认自动安装。 - -### 4.4 Agent and Project Guides - -文档包包含: - -- Agent 安装说明。 -- Agent 配置说明。 -- Provider 字段说明。 -- 配置工具说明。 -- 第一次请求示例。 -- 可运行的开源项目模板。 - -## 5. 用户流程 - -### 5.1 首次用户 - -```text -开始配置 -→ 注册或登录 Provider -→ 创建 API Key -→ 选择 Agent -→ 选择配置工具 -→ 获取模型 -→ 写入配置 -→ 第一次请求 -→ 进入项目模板 -``` - -### 5.2 已有 API Key 的用户 - -```text -输入已有 API Key -→ 获取模型 -→ 选择 Agent -→ 写入配置 -→ 第一次请求 -``` - -### 5.3 多 Provider 用户 - -```text -完成一个可用 Profile -→ 选择 CC Switch 或手动管理 -→ 保存多个 Provider Profile -→ 切换后重新启动 Agent -→ 验证当前 Profile -``` - -### 5.4 只安装 Agent 的用户 - -```text -选择 Agent -→ 只做检测或官方安装 -→ 跳过 Provider 配置 -→ 提示用户之后自行登录和配置 -``` - -## 6. GUI 页面结构 - -### 页面 0:选择路径 - -```text -我还没有 Provider 账号 -我已有账号但没有 API Key -我已有 API Key -我只想安装 Agent -``` - -### 页面 1:选择 Agent - -按使用场景和配置方式分组: - -- 命令行 Agent:Codex、Claude Code、OpenCode、Aider。 -- IDE / 编辑器 Agent:Cursor、Cline、Continue、Qwen Code、Kilo VS Code。 -- Gateway Agent:OpenClaw、Hermes。 -- 其他官方账号型 Agent:Kiro、Gemini CLI。 - -用户可以多选,但默认只推荐少量适合当前场景的 Agent。 - -### 页面 2:选择 Provider - -默认 Provider 为 PPIO,也可以选择 Novita 或 Custom OpenAI-compatible 服务。 - -配置字段统一为: - -```text -Provider -Base URL -API Key -Model ID -``` - -### 页面 3:选择配置工具 - -```text -OneAgent 内置配置 -适合第一次使用 - -CC Switch -适合多个 Profile 切换 - -手动配置 -适合高级用户和问题排查 -``` - -### 页面 4:获取模型 - -请求: - -```text -GET /v1/models -``` - -成功时展示模型列表;失败时显示原因并允许手动输入模型 ID。 - -### 页面 5:确认与执行 - -展示: - -- 选中的 Agent。 -- Provider 和 Base URL。 -- 选中的模型。 -- 配置工具。 -- 将写入的配置文件。 -- 备份策略。 -- 将执行的安装命令。 - -### 页面 6:完成 - -展示: - -- Provider 连接结果。 -- Agent 配置结果。 -- 第一次请求结果。 -- 下一步命令。 -- 项目模板入口。 -- 配置工具后续使用说明。 - -## 7. Agent 分类和自动化策略 - -### 可自动配置 - -- Codex。 -- Claude Code。 -- OpenCode。 -- Aider。 - -这些 Agent 只有在官方配置入口和安装源经过验证后,才允许加入自动化 allowlist。 - -### 只做官方引导 - -- OpenClaw。 -- Hermes。 -- Cursor。 -- Kiro。 -- Gemini CLI。 -- Cline。 -- Continue。 -- Qwen Code。 -- Kilo VS Code。 - -这类工具的官方账号、Gateway、IDE 私有状态或配置契约不稳定时,OneAgent 只显示安装和配置指引,不修改私有状态。 - -## 8. 软件获取策略 - -```text -官方安装源 -→ 授权镜像 -→ 用户手动安装 -→ OneAgent 检测 -→ 文档引导 -``` - -如果官方源不可达,产品只能提示用户使用其所在组织或网络服务商提供的合规网络接入,或者手动安装后返回检测。OneAgent 不提供 VPN、代理、节点订阅或网络绕过功能。 - -## 9. API Key 和数据策略 - -默认模式为用户自己的 Provider 账号和 API Key: - -```text -用户创建 Key -→ Key 进入本地配置流程 -→ Key 写入系统环境变量、目标配置或本机 Profile -→ OneAgent 不接收服务端副本 -``` - -禁止: - -- 把 Key 放入压缩包。 -- 把 Key 放进命令行参数。 -- 将 Key 写入日志、截图和遥测。 -- 默认上传 Prompt、源代码和完整请求。 - -## 10. 配置工具策略 - -### OneAgent 内置配置 - -默认、低复杂度、最容易测试。 - -### CC Switch - -可选的本机 Profile 管理工具,适合多 Provider、多账号、多模型场景。它不属于 Agent,不承担网络访问功能,也不替代 PPIO。 - -### 手动配置 - -用于处理未支持 Agent、配置恢复和高级用户需求。只提供已验证字段和配置路径。 - -## 11. 分发形态 - -### Web 激活入口 - -适合公开用户,内容可以持续更新。 - -### 本地启动器 - -适合实际执行安装和配置,默认只监听本机。 - -### 离线压缩包 - -适合组织或个人离线携带脚本、文档和项目模板。压缩包是发行形式,不是产品边界。 - -组织发行方可以在包外提供自己的项目、兑换码或权益说明,但不得要求 OneAgent 核心内置这些内容。 - -## 12. V1 范围 - -### 包含 - -- PPIO、Novita、Custom Provider。 -- Codex、Claude Code、OpenCode、Aider 自动配置。 -- 其他 Agent 的官方安装和配置指引。 -- 模型列表获取。 -- 第一次请求验证。 -- OneAgent 内置配置。 -- CC Switch 可选文档。 -- 手动配置和备份恢复。 -- 通用项目模板。 - -### 不包含 - -- 组织发行方的专属业务逻辑。 -- 固定免费额度承诺。 -- VPN、代理和跨境网络中转。 -- 共享 PPIO Key。 -- 统一 API 网关。 -- 未授权 Agent 包体分发。 -- 默认遥测。 - -## 13. 核心指标 - -核心指标是“激活成功”,而不是下载量: - -- 启动器启动。 -- Provider 连接成功。 -- API Key 配置完成。 -- 模型列表获取成功。 -- Agent 配置完成。 -- 第一次请求成功。 -- 第一个项目模板运行成功。 - -如果开启匿名统计,只记录版本、Agent、Provider、模型和结果状态,不记录 Key、Prompt 或源代码。 - -## 14. 验收标准 - -- 用户不需要组织身份、专属代码或额外权益即可理解和使用核心流程。 -- 用户有 API Key 时可以快速完成配置。 -- 用户没有 API Key 时可以获得清晰的 Provider 官方注册引导。 -- 用户只想安装 Agent 时可以跳过 Provider 配置。 -- CC Switch 是可选路径,不是隐藏依赖。 -- 官方源不可达时不会引导网络绕过。 -- API Key 不出现在日志、命令行、截图和服务端。 -- 所有文档和启动包不包含组织发行方专属字段。 -- 所有自动化 Agent 都能在临时 HOME 下测试。 diff --git a/docs/ai-agent-kit/00-start-here.md b/docs/ai-agent-kit/00-start-here.md index 768228b4..6d0e98f2 100644 --- a/docs/ai-agent-kit/00-start-here.md +++ b/docs/ai-agent-kit/00-start-here.md @@ -29,14 +29,18 @@ ## 启动桌面应用 +下载对应平台的 `technical-preview-unsigned` 包,解压后直接启动:macOS 是 +`OneAgent.app`,Windows 是 `oneagent-desktop.exe`。当前阶段的包未签名也未公证, +首次打开需要在系统里手动允许。 + +需要从源码运行(开发或审查用): + ```bash cd frontend && pnpm install --frozen-lockfile && pnpm run build cd .. go run -tags wails ./cmd/oneagent-desktop ``` -也可以下载对应平台的 `technical-preview-unsigned` 包后直接启动 Wails 应用。 - ## 三条安全规则 1. 不要把 API Key 发到聊天、Issue、作业或截图中。 diff --git a/docs/ai-agent-kit/01-ppio-account.md b/docs/ai-agent-kit/01-ppio-account.md index 979aef45..f9b509ce 100644 --- a/docs/ai-agent-kit/01-ppio-account.md +++ b/docs/ai-agent-kit/01-ppio-account.md @@ -1,5 +1,8 @@ # PPIO 账号和 Provider 准备 +> 本文用 PPIO 举例。内置的 Novita 和自定义 OpenAI-compatible 端点步骤相同,把官网入口和 +> Base URL 换成对应 Provider 的即可。 + ## 1. 打开官方入口 请从 OneAgent 或官方文档打开 PPIO 官网,不要从不明群聊链接下载脚本或提交账号信息。 diff --git a/docs/ai-agent-kit/03-config-tools.md b/docs/ai-agent-kit/03-config-tools.md index 8b92e83c..1ce4603d 100644 --- a/docs/ai-agent-kit/03-config-tools.md +++ b/docs/ai-agent-kit/03-config-tools.md @@ -26,7 +26,7 @@ OneAgent 提供三种配置方式。第一次使用建议选择内置配置; - 需要在 PPIO、其他 OpenAI-compatible 服务和官方账号之间切换。 - 需要为不同项目保存不同配置。 -- 经常在 Claude Code、Codex、Gemini CLI、OpenCode 等工具之间切换。 +- 经常在 Claude Code、Codex、OpenCode 等工具之间切换。 CC Switch 是可选的本机配置工具,不是 OneAgent 的必需依赖。请从其官方项目入口获取当前版本,OneAgent 不把它的二进制或安装脚本重新打进启动包。 diff --git a/docs/ai-agent-kit/04-agent-guides.md b/docs/ai-agent-kit/04-agent-guides.md index 22270c8d..42005c80 100644 --- a/docs/ai-agent-kit/04-agent-guides.md +++ b/docs/ai-agent-kit/04-agent-guides.md @@ -21,10 +21,15 @@ - Base URL 使用 `https://api.ppio.com/openai/v1` 形式。 - API Key 直接写入该文件的 `provider.oneagent.options.apiKey`,文件权限收紧到 0600,不依赖环境变量。 +### Kilo CLI + +- 使用 OpenAI-compatible Provider,OneAgent 写入 `~/.config/kilo/kilo.jsonc`。 +- API Key 与 OpenCode 同样写在配置文件的 `options.apiKey`,权限收紧到 0600。 + ### Aider -- 只有选择 Aider 安装时才使用 `uv tool` 和本机已有的 Python 3.12;其他 Agent 和 OneAgent 自身不需要 Python。 -- 缺少 `uv` 或 Python 3.12 时先完成 Aider 官方前置条件安装;OneAgent 不自动下载运行时。 +- 只有选择 Aider 安装时才需要 Python 3.12,由 `uv` 自己解析:本机有匹配版本就复用,否则下载一份托管 CPython 到 `~/.oneagent/runtimes/python`。其他 Agent 和 OneAgent 自身不需要 Python。 +- `uv` 本身由 OneAgent 作为运行时安装到 `~/.oneagent/runtimes`,不需要预装。 - 使用独立环境文件保存 PPIO 配置。 - 启动时通过 `aider --env-file ~/.oneagent/aider.env` 由 Aider 自己加载,不需要在 shell 中 source。 - 使用 `openai/` 形式时,以 Aider 当前版本说明为准。 @@ -41,15 +46,13 @@ OneAgent 只提供安装和模型配置说明,不自动写入私有配置或 ## 官方账号型 Agent -### Cursor、Kiro、Gemini CLI - -这些工具优先使用各自官方账号、订阅或登录流程。不要把 PPIO Base URL 强行写进没有稳定官方配置契约的工具。 +### Cursor -## IDE 扩展型 Agent +优先使用官方账号、订阅或登录流程。不要把 Provider 的 Base URL 强行写进没有稳定官方配置契约的工具,OneAgent 也不会为它写私有配置。 -### Cline、Continue、Qwen Code、Kilo VS Code +## 其他工具 -优先在 IDE 扩展的 Provider 设置中配置 OpenAI-compatible 服务。OneAgent 提供可复制的字段说明,但不直接修改 IDE 私有状态文件。 +`agents.lock.json` 是 Agent 目录的唯一真源;上面没有的工具(各类 IDE 扩展、其他终端 Agent)OneAgent 不检测也不配置。这类工具通常在自己的 Provider 设置里填 OpenAI-compatible 端点,字段口径可以照 [第一次请求](./05-first-request.md) 的说明复制,但请在该工具自己的界面里操作——OneAgent 不修改 IDE 的私有状态文件。 ## 统一排查顺序 diff --git a/docs/ai-agent-kit/README.md b/docs/ai-agent-kit/README.md index 26c97f13..50f24720 100644 --- a/docs/ai-agent-kit/README.md +++ b/docs/ai-agent-kit/README.md @@ -1,6 +1,10 @@ # OneAgent AI 开发环境激活文档 -这是一套面向个人开发者、团队和组织发行方的通用 PPIO + AI Agent 使用文档。 +这是一套面向个人开发者、团队和组织发行方的 AI Agent 环境配置文档。 + +文中以 **PPIO** 作为贯穿示例。OneAgent 内置的另一个 Provider 是 **Novita**,此外还支持 +自定义 OpenAI-compatible 端点;三者在应用里的配置步骤相同,只是 Base URL 和 Key 来源不同, +所以下面的流程对它们同样适用。内置 Provider 的真源是仓库根的 `providers.lock.json`。 产品边界以 [OneAgent 产品边界基线](../product-boundary-baseline.md) 为准。组织发行方可以在项目外部提供自己的兑换码或项目说明,但不改变 OneAgent 核心流程。 diff --git a/docs/ai-agent-kit/manifest.md b/docs/ai-agent-kit/manifest.md index 60146711..1728fbb4 100644 --- a/docs/ai-agent-kit/manifest.md +++ b/docs/ai-agent-kit/manifest.md @@ -4,9 +4,11 @@ | 文件 | 作用 | 是否需要密钥 | | --- | --- | --- | -| `launcher` | 启动本地 GUI | 否 | -| `start.sh` | macOS / Linux 启动入口 | 否 | -| `start.command` | macOS 双击入口 | 否 | +| `OneAgent.app`(macOS)/ `oneagent-desktop.exe`(Windows) | 桌面应用 | 否 | +| `oneagent` / `oneagent.exe` | headless CLI | 否 | + +发行包由 `.github/workflows/build-artifacts.yml` 构建。早期版本用 `launcher`、 +`start.sh`、`start.command` 三个脚本启动本地 GUI,Go/Wails 迁移后不再需要。 ## 文档文件 diff --git a/docs/decisions/ADR-003-three-platform-python-core-and-release-policy.md b/docs/decisions/ADR-003-three-platform-python-core-and-release-policy.md index 58f61e9c..18ce5223 100644 --- a/docs/decisions/ADR-003-three-platform-python-core-and-release-policy.md +++ b/docs/decisions/ADR-003-three-platform-python-core-and-release-policy.md @@ -30,4 +30,4 @@ ## 迁移记录 -Python 实现、Python 测试、PyInstaller/wheel/setuptools 配置和相关工作流已删除。新的验收清单见 [Wails v3 迁移收尾计划](../wails-v3-migration-plan.md)。 +Python 实现、Python 测试、PyInstaller/wheel/setuptools 配置和相关工作流已删除。新的验收清单见 [Wails v3 迁移收尾计划](../internal/wails-v3-migration-plan.md)。 diff --git a/docs/internal/README.md b/docs/internal/README.md new file mode 100644 index 00000000..897a091a --- /dev/null +++ b/docs/internal/README.md @@ -0,0 +1,44 @@ +# 内部实现记录 + +这个目录是**维护者视角的历史记录**,不是使用说明。里面的文件回答「当时为什么这样做、 +做完之后验证了什么」,而不是「现在该怎么用」。 + +面向使用者的文档在上一层:[产品边界基线](../product-boundary-baseline.md)、 +[分发与合规政策](../distribution-compliance-policy.md)、 +[AI Agent Kit](../ai-agent-kit/00-start-here.md);架构决策在 +[decisions/](../decisions/)。 + +## 为什么分开放 + +这些文件大多写于某次改造完成时,标题里带「计划」二字是遗留——它们已经是既成事实的记录。 +混在 `docs/` 根下的问题是:读者分不清哪份是「已经这样了」、哪份是「打算这样做」,于是 +照着一份过期文档执行命令。分到这里之后,`docs/` 根下只剩当前有效的规范和用户文档。 + +## 读这里的文件时注意 + +**命令可能已经不能执行。** `cmd/oneagent-release`、`cmd/oneagent-rc`、 +`cmd/oneagent-provider-smoke` 已于 `23805b0` 移除,职责交给 +`.github/workflows/build-artifacts.yml`;`tests/` 目录也已不存在。引用它们的地方用 +` ```text ` 而不是 ` ```bash ` 标注,表示那是历史命令而非可运行指令。 + +**当前可运行的验证入口只有两处**:`.github/workflows/ci.yml`(每个 PR 自动跑 +`go vet`、`go test -race`、前端 test 和 build),以及 +`.github/workflows/build-artifacts.yml`(手动触发的发行构建)。本地命令见 +[CLAUDE.md](../../CLAUDE.md)。 + +## 目录 + +| 文件 | 记录了什么 | +| --- | --- | +| [wails-v3-migration-plan.md](wails-v3-migration-plan.md) | Go/Wails 迁移的收尾验收,本目录最完整的一份架构说明 | +| [config-chain-audit.md](config-chain-audit.md) | 从 `agents.lock.json` 到磁盘写入的完整配置链 | +| [config-discovery-plan.md](config-discovery-plan.md) | 只读配置发现器的行为契约 | +| [per-agent-config-plan.md](per-agent-config-plan.md) | 各 Agent 的适配器与凭据位置 | +| [frontend-management-console-plan.md](frontend-management-console-plan.md) | React 管理页面与 Key 处理边界 | +| [frontend-component-redesign-plan.md](frontend-component-redesign-plan.md) | 前端构建链与发布门禁 | +| [provider-rc-testing.md](provider-rc-testing.md) | Provider 三协议验证要求(执行入口已移除) | +| [blank-machine-verification-plan.md](blank-machine-verification-plan.md) | 空白机器验证的断言清单(执行入口已移除) | +| [recent-work-summary.md](recent-work-summary.md) | 一次阶段性完工清单,已被上面几份取代 | +| [mvp-agent-installer-plan.md](mvp-agent-installer-plan.md) | 最初的 HTTP/Python 原型,仅存背景 | +| [cc-switch-reference-notes.md](cc-switch-reference-notes.md) | 对 CC Switch 的定向调研与采纳/不采纳结论 | +| [release-evidence/](release-evidence/) | 历史发行包的 SHA-256 与 cleanroom 记录 | diff --git a/docs/blank-machine-verification-plan.md b/docs/internal/blank-machine-verification-plan.md similarity index 100% rename from docs/blank-machine-verification-plan.md rename to docs/internal/blank-machine-verification-plan.md diff --git a/docs/cc-switch-reference-notes.md b/docs/internal/cc-switch-reference-notes.md similarity index 94% rename from docs/cc-switch-reference-notes.md rename to docs/internal/cc-switch-reference-notes.md index 7f1e8fb4..c1c8bd05 100644 --- a/docs/cc-switch-reference-notes.md +++ b/docs/internal/cc-switch-reference-notes.md @@ -12,9 +12,9 @@ | 职责 | Provider 配置的**存取与切换** | Agent 的**检测、安装、配置** | | 配置模型 | 整块存原始配置对象 | 结构化字段,由适配器翻译 | | 外围能力 | 本地代理、熔断器、故障转移、用量统计、MCP、skills、prompts、sessions | 无 | -| 表单规模 | 共享编排器 `ProviderForm.tsx` 2693 行 + 共享 hooks/字段约 6000 行 + 各应用专属区块 | `AgentDetailPage.tsx` 223 行 | +| 表单规模 | 共享编排器 `ProviderForm.tsx` 2693 行 + 共享 hooks/字段约 6000 行 + 各应用专属区块 | `AgentProfilePage.tsx` 263 行 | -它的 `src/components/` 下有 19 个子目录(mcp、skills、prompts、sessions、proxy、usage…)。这些不在 OneAgent 的产品边界内(见 [产品边界基线](product-boundary-baseline.md)),尤其**本地代理与故障转移属于明确禁止范围**,不予借鉴。 +它的 `src/components/` 下有 19 个子目录(mcp、skills、prompts、sessions、proxy、usage…)。这些不在 OneAgent 的产品边界内(见 [产品边界基线](../product-boundary-baseline.md)),尤其**本地代理与故障转移属于明确禁止范围**,不予借鉴。 ## 2. 配置模型:它存原始对象,我们存结构化字段 @@ -143,7 +143,7 @@ CC Switch 依赖 **`@lobehub/icons-static-svg`**(MIT,723 个 AI 品牌 SVG **与 OneAgent 的 `profiles/` 模板方向相反**:它的预设是**内置的服务商清单**(我们出,用户选),我们的模板是**用户自存的组合**(用户出,用户复用)。两者不冲突,可以并存——内置预设降低首次配置成本,用户模板降低重复配置成本。 -若将来要做内置预设,注意 `isPartner` / `primePartner` / `partnerPromotionKey` 这类商业合作字段**不要引入**:[产品边界基线](product-boundary-baseline.md) 第 8 节要求公开权益文案不得承诺固定额度,带推广属性的预设排序会踩这条线。 +若将来要做内置预设,注意 `isPartner` / `primePartner` / `partnerPromotionKey` 这类商业合作字段**不要引入**:[产品边界基线](../product-boundary-baseline.md) 第 8 节要求公开权益文案不得承诺固定额度,带推广属性的预设排序会踩这条线。 ## 6. UI 布局:与我们已高度一致 @@ -165,4 +165,4 @@ CC Switch 依赖 **`@lobehub/icons-static-svg`**(MIT,723 个 AI 品牌 SVG - 商业合作伙伴字段与推广排序 —— 与公开权益文案约束冲突。 - 拖拽排序、健康徽章 —— 当前无对应需求。 -相关文档:[前端管理控制台改造计划](frontend-management-console-plan.md)、[产品边界基线](product-boundary-baseline.md)、[CC Switch 可选配置说明](ai-agent-kit/tools/cc-switch.md)。 +相关文档:[前端管理控制台改造计划](frontend-management-console-plan.md)、[产品边界基线](../product-boundary-baseline.md)、[CC Switch 可选配置说明](../ai-agent-kit/tools/cc-switch.md)。 diff --git a/docs/config-chain-audit.md b/docs/internal/config-chain-audit.md similarity index 85% rename from docs/config-chain-audit.md rename to docs/internal/config-chain-audit.md index d4597c66..ed5b6c1e 100644 --- a/docs/config-chain-audit.md +++ b/docs/internal/config-chain-audit.md @@ -28,7 +28,7 @@ Wails service or cmd/oneagent - Agent 命令、配置路径、平台、package manager 和包名从 catalog manifest 读取;版本默认由包管理器解析。 - 配置适配器只在 Go 中按 adapter 分派,格式差异不伪装成数据配置。 -- 每个 Agent 的凭据交付方式由 `credential_delivery` 和 `env_vars` 声明。 +- 每个 Agent 需要哪些环境变量名由 `env_vars` 声明;凭据写到哪个文件由适配器代码决定(`internal/config/write.go`),不再有 `credential_delivery` 字段,见 [ADR-008](../decisions/ADR-008-credentials-in-agent-config-files.md)。 - Claude Code 的 native env 与配置文件同步写入,避免出现配置显示完成但运行时未登录。 - Codex、Claude Code、OpenCode、Kilo CLI、Aider 按各自协议探测。 - 备份、临时文件、权限和原子替换由 `securefs` 统一处理。 diff --git a/docs/config-discovery-plan.md b/docs/internal/config-discovery-plan.md similarity index 83% rename from docs/config-discovery-plan.md rename to docs/internal/config-discovery-plan.md index fa3c9072..214cea00 100644 --- a/docs/config-discovery-plan.md +++ b/docs/internal/config-discovery-plan.md @@ -15,5 +15,5 @@ - 检测响应不含 API Key、Token 或 Key 是否存在的推断。 - 错误只包含路径和解析诊断,不回显文件内容。 -- 覆盖非 OneAgent 配置前,前端显示警告并保留备份。 +- 覆盖非 OneAgent 配置前保留备份(`internal/securefs`)。确认页说明会创建时间戳备份;针对「这份配置不是 OneAgent 写的」的专门警告目前没有界面入口。 - guide-only Agent 不生成 `detected`。 diff --git a/docs/frontend-component-redesign-plan.md b/docs/internal/frontend-component-redesign-plan.md similarity index 100% rename from docs/frontend-component-redesign-plan.md rename to docs/internal/frontend-component-redesign-plan.md diff --git a/docs/frontend-management-console-plan.md b/docs/internal/frontend-management-console-plan.md similarity index 89% rename from docs/frontend-management-console-plan.md rename to docs/internal/frontend-management-console-plan.md index 5c1d1e51..d99dd9f0 100644 --- a/docs/frontend-management-console-plan.md +++ b/docs/internal/frontend-management-console-plan.md @@ -5,7 +5,7 @@ ## 已交付 - 首屏环境总览、Agent 行式管理、Provider 和 Profile 页面。 -- Agent 详情页支持单 Agent 激活、Claude fast model 和配置发现警告。 +- 单 Agent 页面(`/agents/:agentId`)由 `AgentProfilePage` 承担:切换 Provider、模型和 Profile。 - Key 不进入 reducer、浏览器存储或 binding 公开摘要;仅在用户主动打开 Provider 编辑/配置表单时通过本机 binding 读入密码字段。 - 生成的 `frontend/bindings` 是后端 DTO 的唯一类型来源。 - Wails server/e2e fake runner 覆盖导航、重试、错误 cause 和临时 HOME;生产构建不使用 server tag。 diff --git a/docs/mvp-agent-installer-plan.md b/docs/internal/mvp-agent-installer-plan.md similarity index 85% rename from docs/mvp-agent-installer-plan.md rename to docs/internal/mvp-agent-installer-plan.md index b5debd74..c936edb1 100644 --- a/docs/mvp-agent-installer-plan.md +++ b/docs/internal/mvp-agent-installer-plan.md @@ -1,6 +1,6 @@ # MVP Agent Installer 计划(已废弃) -> 状态:**Superseded**(2026-07-31)。本文件记录最初的本地 HTTP/Python 原型,不能作为当前操作指南。当前入口请看 [README](../README.md) 和 [Wails 迁移收尾计划](wails-v3-migration-plan.md)。 +> 状态:**Superseded**(2026-07-31)。本文件记录最初的本地 HTTP/Python 原型,不能作为当前操作指南。当前入口请看 [README](../../README.md) 和 [Wails 迁移收尾计划](wails-v3-migration-plan.md)。 早期原型使用标准库 HTTP GUI、单 Agent CLI 和脚本式配置写入。该原型已由以下实现替代: diff --git a/docs/internal/per-agent-config-plan.md b/docs/internal/per-agent-config-plan.md new file mode 100644 index 00000000..814f020d --- /dev/null +++ b/docs/internal/per-agent-config-plan.md @@ -0,0 +1,38 @@ +# Per-Agent 配置计划(已实施) + +> 当前实现位于 `internal/config` 和 `internal/app`。本文保留设计结论,旧脚本路径不再适用。 + +> 补注(2026-08-04):本文提到的 `cmd/oneagent-release`、`cmd/oneagent-rc`、 +> `cmd/oneagent-provider-smoke` 已于 `23805b0` 移除,职责交给 +> `.github/workflows/build-artifacts.yml`。相关命令是历史背景,不可执行。 + +## 适配器 + +| Agent | 配置适配器 | 凭据位置 | +| --- | --- | --- | +| Codex | TOML provider | `~/.codex/auth.json` 的 `OPENAI_API_KEY` | +| Claude Code | settings JSON | `settings.json` 的 `env.ANTHROPIC_AUTH_TOKEN` | +| OpenCode / Kilo CLI | OpenAI-compatible JSON | 配置文件里的 `options.apiKey` | +| Aider | env 文件 | `~/.oneagent/aider.env`(Aider 用 `--env-file` 加载) | + +凭据位置由适配器代码决定,**不是** manifest 字段。早期设计用过 `credential_delivery` +声明,该字段已随 env 文件方案一起删除,见 +[ADR-008](../decisions/ADR-008-credentials-in-agent-config-files.md)。 + +适配器由 `agents.lock.json.config_adapter` 选择;新增 Agent 不复制版本、命令或路径常量。配置写入会保留用户未管理字段、创建安全备份并原子替换。 + +## Claude Code fast model + +`small_fast_model` 是可选字段。为空时回退主模型(`internal/config/write.go`);有值时同时写入 settings 和 native env。Go binding 与 CLI 的 `--small-fast-model` 使用同一字段。 + +当前**桌面界面没有输入它的地方**:`AgentProfilePage` 与 `AgentQuickSwitch` 都传空值,因此 GUI 路径始终走回退。要设置非默认的 fast model 只能用 CLI。 + +## 验收 + +```text +go test ./internal/config ./internal/app +bash tests/install_test.sh +go run ./cmd/oneagent-rc adopted +``` + +配置采用检查使用丢弃端口和假 Key,确保 Agent 读取了配置后才会在网络层失败;它不需要真实 Provider Key。 diff --git a/docs/provider-rc-testing.md b/docs/internal/provider-rc-testing.md similarity index 100% rename from docs/provider-rc-testing.md rename to docs/internal/provider-rc-testing.md diff --git a/docs/recent-work-summary.md b/docs/internal/recent-work-summary.md similarity index 100% rename from docs/recent-work-summary.md rename to docs/internal/recent-work-summary.md diff --git a/docs/release-evidence/0.2.0-dev-macos-arm64.md b/docs/internal/release-evidence/0.2.0-dev-macos-arm64.md similarity index 100% rename from docs/release-evidence/0.2.0-dev-macos-arm64.md rename to docs/internal/release-evidence/0.2.0-dev-macos-arm64.md diff --git a/docs/wails-v3-migration-plan.md b/docs/internal/wails-v3-migration-plan.md similarity index 100% rename from docs/wails-v3-migration-plan.md rename to docs/internal/wails-v3-migration-plan.md diff --git a/docs/per-agent-config-plan.md b/docs/per-agent-config-plan.md deleted file mode 100644 index 897fc658..00000000 --- a/docs/per-agent-config-plan.md +++ /dev/null @@ -1,32 +0,0 @@ -# Per-Agent 配置计划(已实施) - -> 当前实现位于 `internal/config` 和 `internal/app`。本文保留设计结论,旧脚本路径不再适用。 - -> 补注(2026-08-04):本文提到的 `cmd/oneagent-release`、`cmd/oneagent-rc`、 -> `cmd/oneagent-provider-smoke` 已于 `23805b0` 移除,职责交给 -> `.github/workflows/build-artifacts.yml`。相关命令是历史背景,不可执行。 - -## 适配器 - -| Agent | 配置适配器 | 凭据交付 | -| --- | --- | --- | -| Codex | TOML provider + 专属 env | `oneagent_env` | -| Claude Code | settings JSON + native env | `native_env` | -| OpenCode/Kilo CLI | OpenAI-compatible JSON + 专属 env | `oneagent_env` | -| Aider | env 脚本 | `config_file` | - -适配器由 `agents.lock.json.config_adapter` 选择;新增 Agent 不复制版本、命令或路径常量。配置写入会保留用户未管理字段、创建安全备份并原子替换。 - -## Claude Code fast model - -`small_fast_model` 是可选字段。为空时回退主模型;有值时同时写入 settings 和 native env。前端 Advanced section、Go binding 和 CLI 使用同一字段。 - -## 验收 - -```text -go test ./internal/config ./internal/app -bash tests/install_test.sh -go run ./cmd/oneagent-rc adopted -``` - -配置采用检查使用丢弃端口和假 Key,确保 Agent 读取了配置后才会在网络层失败;它不需要真实 Provider Key。 diff --git a/frontend/src/pages/AgentDetailPage.test.tsx b/frontend/src/pages/AgentDetailPage.test.tsx deleted file mode 100644 index b818f68d..00000000 --- a/frontend/src/pages/AgentDetailPage.test.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { MemoryRouter, Route, Routes } from "react-router-dom"; -import { describe, expect, it, vi } from "vitest"; - -import { api } from "../backend/api"; -import type { StatusResponse } from "../types/api"; -import { AgentDetailPage } from "./AgentDetailPage"; - -vi.mock("../state/WizardContext", () => ({ - useWizard: () => ({ state: mockState, dispatch: vi.fn(), refreshStatus: vi.fn() }), -})); - -let mockState: { status: StatusResponse | null; statusState: string }; - -function status(): StatusResponse { - return { - apiVersion: 1, - platform: { os: "macos", arch: "arm64", shell: "bash" }, - runtimes: [], - capabilities: { canInstall: { codex: true }, missingRuntime: {}, supportedAgentIds: ["codex"] }, - agents: { - codex: { - installed: true, - configured: true, - guideOnly: false, - config: "/home/u/.codex/config.toml", - version: "0.144.4", - lockedVersion: "0.145.0", - canInstall: true, - provider: "ppio", - profileId: null, - model: "deepseek/deepseek-v3", - baseUrl: "https://api.ppio.com/openai", - updatedAt: "2026-07-27T00:00:00Z", - detected: null, - }, - }, - catalog: [ - { - id: "codex", - name: "Codex", - group: "auto", - configMode: "auto", - guideOnly: false, - lockedVersion: "0.145.0", - protocol: "responses", - platforms: ["macos"], - platformNote: "", - rank: 1, - }, - ], - groups: [], - providers: { ppio: { name: "PPIO", home: "https://ppio.com/", base_url: "https://api.ppio.com/openai" } }, - mirrors: [], - paths: { codex_config: "/home/u/.codex/config.toml" }, - backups: { codex: true }, - environment: null, - environmentError: null, - desktopAgent: { id: "desktop-agent", name: "ChatGPT Desktop", installed: false, supported: false, version: null, source: "unknown" }, - profiles: [], - activeProfile: null, - firstRun: false, - }; -} - -function claudeStatus(): StatusResponse { - const base = status(); - return { - ...base, - runtimes: [], - capabilities: { canInstall: { "claude-code": true }, missingRuntime: {}, supportedAgentIds: ["claude-code"] }, - agents: { - "claude-code": { - installed: true, - configured: true, - guideOnly: false, - config: "/home/u/.claude/settings.json", - version: "2.1.217", - lockedVersion: "2.1.217", - canInstall: true, - provider: "ppio", - profileId: null, - model: "model-a", - baseUrl: "https://api.ppio.com/anthropic", - updatedAt: "2026-07-27T00:00:00Z", - detected: null, - }, - }, - catalog: [ - { - id: "claude-code", - name: "Claude Code", - group: "auto", - configMode: "auto", - guideOnly: false, - lockedVersion: "2.1.217", - protocol: "anthropic", - platforms: ["macos"], - platformNote: "", - rank: 2, - }, - ], - paths: { "claude-code_config": "/home/u/.claude/settings.json" }, - backups: { "claude-code": false }, - }; -} - -function renderPage(agentId = "codex", override?: StatusResponse) { - mockState = { status: override ?? status(), statusState: "success" }; - render( - - - } /> - 新增页占位} /> - 总览占位} /> - - , - ); -} - -function passingProbe() { - return { ok: true, reachable: true, status: 200, message: "ok", error_code: null, retryable: false }; -} - -describe("AgentDetailPage", () => { - it("shows what the Agent is pointed at and where its files live", () => { - renderPage(); - expect(screen.getByRole("heading", { name: /Codex/ })).toBeTruthy(); - expect(screen.getByText(/deepseek\/deepseek-v3/)).toBeTruthy(); - // Detail view has the room for this; a list row did not. - expect(screen.getByText("/home/u/.codex/config.toml")).toBeTruthy(); - }); - - it("keeps apply disabled when no key exists anywhere", () => { - // ActivateAgent needs a key from somewhere (internal/app/agent.go:104), and - // this Provider has none stored, so there is nothing to resolve. - renderPage(); - expect(screen.getByRole("button", { name: /^应用/ }).hasAttribute("disabled")).toBe(true); - }); - - it("allows applying on a Provider that already has a key, without a probe", async () => { - // The old gate required a freshly typed key AND a passing probe. The backend - // asks for neither: it falls back to the Profile secret, then the Provider's - // stored key (agent.go:92-102). A user changing only the model was forced to - // paste their key again. - const activate = vi.spyOn(api, "activateAgent").mockResolvedValue({ - ok: true, agent: "codex", restart: "重启 Codex", next: "codex", backup: null, - } as never); - const withKey = status(); - withKey.providers.ppio.has_key = true; - renderPage("codex", withKey); - - const apply = screen.getByRole("button", { name: /^应用/ }); - expect(apply.hasAttribute("disabled")).toBe(false); - expect(screen.getByText(/留空则使用该 Provider 已保存的 Key/)).toBeTruthy(); - expect(screen.getByText(/未测试连接也可以应用/)).toBeTruthy(); - - await userEvent.click(apply); - // The empty key is deliberate: the backend resolves it. - await waitFor(() => expect(activate).toHaveBeenCalledWith("codex", expect.objectContaining({ apiKey: "" }))); - }); - - it("offers user Providers in the configuration menu", () => { - const withUserProvider = status(); - withUserProvider.providers.acme = { name: "Acme", home: "", base_url: "https://api.acme.test", custom: true }; - renderPage("codex", withUserProvider); - expect(screen.getByRole("option", { name: "Acme" })).toBeTruthy(); - expect(screen.queryByRole("option", { name: "自定义端点" })).toBeNull(); - }); - - it("opens the Provider creation page from the picker", () => { - renderPage(); - fireEvent.click(screen.getByRole("button", { name: "新增 Provider" })); - expect(screen.getByText("新增页占位")).toBeTruthy(); - }); - - it("drops a passing verdict when the key is edited afterwards", async () => { - // Otherwise a wrong key rides in on the previous verdict. The verdict itself - // is what must clear; Apply is no longer gated on it, so asserting the button - // would no longer prove anything. - vi.spyOn(api, "probe").mockResolvedValue(passingProbe()); - renderPage(); - fireEvent.change(screen.getByLabelText(/API Key/i), { target: { value: "sk-good" } }); - fireEvent.click(screen.getByRole("button", { name: /测试连接/ })); - // ConnectionStatus renders the probe's own message on success. - expect(await screen.findByRole("status")).toHaveTextContent("ok"); - - fireEvent.change(screen.getByLabelText(/API Key/i), { target: { value: "sk-other" } }); - // Back to "untested", and the hint that says so reappears. - expect(screen.getByText(/未测试连接也可以应用/)).toBeTruthy(); - }); - - it("reports the restart instruction and clears the key after applying", async () => { - // Constraints 3 and 4: an Agent reads its config at startup, so silence - // reads as failure; and a key left in a visible field outlives its request. - vi.spyOn(api, "probe").mockResolvedValue(passingProbe()); - vi.spyOn(api, "activateAgent").mockResolvedValue({ - ok: true, - agent: "codex", - config: "/home/u/.codex/config.toml", - provider: "ppio", - model: "deepseek/deepseek-v3", - restart: "Quit any running codex process, then start it again", - next: "codex", - }); - - renderPage(); - const field = screen.getByLabelText(/API Key/i) as HTMLInputElement; - fireEvent.change(field, { target: { value: "sk-secret" } }); - fireEvent.click(screen.getByRole("button", { name: /测试连接/ })); - await waitFor(() => - expect(screen.getByRole("button", { name: /^应用/ }).hasAttribute("disabled")).toBe(false), - ); - - fireEvent.click(screen.getByRole("button", { name: /^应用/ })); - await waitFor(() => expect(screen.getByText(/Quit any running codex/)).toBeTruthy()); - await waitFor(() => - expect((screen.getByLabelText(/API Key/i) as HTMLInputElement).value).toBe(""), - ); - }); - - it("masks the key and keeps it out of browser storage", () => { - renderPage(); - const field = screen.getByLabelText(/API Key/i) as HTMLInputElement; - fireEvent.change(field, { target: { value: "sk-secret-value" } }); - expect(field.type).toBe("password"); - expect(JSON.stringify(localStorage)).not.toContain("sk-secret-value"); - expect(JSON.stringify(sessionStorage)).not.toContain("sk-secret-value"); - expect(document.cookie).not.toContain("sk-secret-value"); - }); - - it("keeps the optional model field out of the common path", () => { - // Leaving the model blank lets the endpoint's own list decide, so it is a - // choice rather than a step. The main form is Provider, key, test. - renderPage(); - expect(screen.queryByLabelText("模型")).toBeNull(); - fireEvent.click(screen.getByRole("button", { name: /高级选项/ })); - expect(screen.getByLabelText("模型")).toBeTruthy(); - }); - - it("refuses an Agent that has no managed configuration", () => { - renderPage("no-such-agent"); - expect(screen.getByText(/找不到/)).toBeTruthy(); - }); - - it("offers Claude Code a fast small-model field and sends it on activate", async () => { - // The one user-facing difference between adapters: Claude Code runs its - // background work on a second, optionally cheaper model. - vi.spyOn(api, "probe").mockResolvedValue(passingProbe()); - const activate = vi.spyOn(api, "activateAgent").mockResolvedValue({ - ok: true, - agent: "claude-code", - config: "/home/u/.claude/settings.json", - provider: "ppio", - model: "model-a", - restart: "Quit any running claude process, then start it again", - next: "claude", - }); - // The spy persists across tests in this file; drop earlier calls so calls[0] - // is this test's activation. - activate.mockClear(); - - renderPage("claude-code", claudeStatus()); - fireEvent.click(screen.getByRole("button", { name: /高级选项/ })); - fireEvent.change(screen.getByLabelText("快速小模型"), { target: { value: "model-fast" } }); - fireEvent.change(screen.getByLabelText(/API Key/i), { target: { value: "sk-secret" } }); - fireEvent.click(screen.getByRole("button", { name: /测试连接/ })); - await waitFor(() => - expect(screen.getByRole("button", { name: /^应用/ }).hasAttribute("disabled")).toBe(false), - ); - - fireEvent.click(screen.getByRole("button", { name: /^应用/ })); - await waitFor(() => expect(activate.mock.calls[0][1].smallFastModel).toBe("model-fast")); - }); - - it("does not show the small-model field for non-Claude Agents", () => { - renderPage(); - fireEvent.click(screen.getByRole("button", { name: /高级选项/ })); - expect(screen.getByLabelText("模型")).toBeTruthy(); - expect(screen.queryByLabelText("快速小模型")).toBeNull(); - }); - it("warns before replacing a configuration OneAgent did not write", () => { - // A backup is taken either way, but a user who configured this Agent by hand - // has no way to know Apply will replace it unless the page says so. - const external = status(); - external.agents.codex.provider = null; - external.agents.codex.model = null; - external.agents.codex.detected = { - baseUrl: "https://api.other-vendor.com/v1", - model: "gpt-5-mini", - managedByOneAgent: false, - unreadable: null, - }; - renderPage("codex", external); - const warning = screen.getByText(/不是 OneAgent 写入/).closest(".notice-warning"); - expect(warning).not.toBeNull(); - // The endpoint appears in the facts row too; what matters is that the - // warning itself names what is about to be replaced, and the backup. - expect(warning?.textContent).toContain("api.other-vendor.com"); - expect(warning?.textContent).toContain("备份"); - }); - - it("does not warn about a configuration it wrote itself", () => { - const ours = status(); - ours.agents.codex.detected = { - baseUrl: "https://api.ppio.com/openai", - model: "deepseek/deepseek-v3", - managedByOneAgent: true, - unreadable: null, - }; - renderPage("codex", ours); - expect(screen.queryByText(/不是 OneAgent 写入/)).toBeNull(); - }); - - it("shows the Provider key again when the page is reopened", async () => { - const saved = status(); - saved.providers.ppio.has_key = true; - vi.spyOn(api, "getProvider").mockResolvedValue({ - id: "ppio", name: "PPIO", home: "https://ppio.com/", base_url: "https://api.ppio.com/openai", - anthropic_base_url: "https://api.ppio.com/anthropic", api_key: "sk-persisted", built_in: true, - }); - renderPage("codex", saved); - await waitFor(() => expect(screen.getByLabelText(/API Key/i)).toHaveValue("sk-persisted")); - }); -}); diff --git a/frontend/src/pages/AgentDetailPage.tsx b/frontend/src/pages/AgentDetailPage.tsx deleted file mode 100644 index bf0b0fae..00000000 --- a/frontend/src/pages/AgentDetailPage.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import { FlaskConical } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useNavigate, useParams, useSearchParams } from "react-router-dom"; - -import { api, describeError } from "../backend/api"; -import { AdvancedSection } from "../components/AdvancedSection"; -import { ConnectionStatus } from "../components/ConnectionStatus"; -import { AgentIcon, agentTagline } from "../components/icons/agents"; -import { AgentQuickSwitch } from "../components/AgentQuickSwitch"; -import { PageScaffold } from "../components/PageScaffold"; -import { ProviderSegment } from "../components/ProviderSegment"; -import { SecureKeyField } from "../components/SecureKeyField"; -import { targetSummary, versionNote } from "../components/AgentManageRow"; -import { useI18n } from "../i18n"; -import { useWizard } from "../state/WizardContext"; -import { PROTOCOL_LABELS } from "../types/api"; -import type { ProbeResponse, ProviderId } from "../types/api"; - -export function AgentDetailPage() { - const { agentId = "" } = useParams(); - const [params] = useSearchParams(); - const navigate = useNavigate(); - const { locale, t } = useI18n(); - const { state, refreshStatus } = useWizard(); - const status = state.status; - const agent = status?.agents[agentId]; - const catalog = status?.catalog.find((item) => item.id === agentId); - - const initialProvider = agent?.provider && status?.providers[agent.provider] ? agent.provider : "ppio"; - const [provider, setProvider] = useState(initialProvider); - const [apiKey, setApiKey] = useState(""); - const [model, setModel] = useState(agent?.model || ""); - const [smallFastModel, setSmallFastModel] = useState(""); - const [probe, setProbe] = useState(null); - const [probeState, setProbeState] = useState<"idle" | "loading" | "success" | "error">("idle"); - const [applying, setApplying] = useState(false); - const [applied, setApplied] = useState<{ restart: string; next: string } | null>(null); - const [failure, setFailure] = useState(""); - // SecureKeyField echoes from its own state by design, so clearing the value - // cannot clear the field; remounting it does. - const [keyFieldId, setKeyFieldId] = useState(0); - - useEffect(() => { - if (!status?.providers[provider]?.has_key) return; - let active = true; - void api.getProvider(provider) - .then((entry) => { - if (active) setApiKey(entry.api_key); - }) - .catch((error) => { - if (active) setFailure(describeError(error, t("无法读取已保存的 API Key")).message); - }); - return () => { active = false; }; - }, [provider, status?.providers]); - - if (!status || !agent || !catalog || catalog.configMode !== "auto") { - return ( - navigate("/overview")} - > -
- {t("找不到可配置的 Agent")} - {agentId ? t("{id} 不在可一键配置的范围内。", { id: agentId }) : t("未指定 Agent。")} -
-
- ); - } - - const version = versionNote(agent, t); - const target = targetSummary(agent, status.providers, t); - // A configuration OneAgent did not write is the case worth warning about: - // applying replaces it, and the user may not know it is there. A backup is - // taken either way, but saying so beforehand is the point. - const willOverwrite = - agent.detected && !agent.detected.managedByOneAgent && !agent.detected.unreadable - ? agent.detected - : null; - const canProbe = Boolean(apiKey); - // ActivateAgent resolves the key from the request, then the Profile secret, - // then the Provider's stored one (internal/app/agent.go:92-102), so a typed key - // is not the only way to have one. Requiring it here meant a user who only - // wanted to change the model had to paste their key again. - const providerHasKey = Boolean(status.providers[provider]?.has_key); - const hasKeySomewhere = Boolean(apiKey) || providerHasKey; - // A probe is a check, not a gate: the backend never asks for one. Applying - // without it is allowed and warned about rather than blocked. - const canApply = hasKeySomewhere && probeState !== "loading" && !applying; - const untested = probeState !== "success"; - - const resetVerdict = () => { - setProbe(null); - setProbeState("idle"); - setApplied(null); - setFailure(""); - }; - - const testConnection = async () => { - setProbeState("loading"); - setFailure(""); - try { - const result = await api.probe({ - provider, - apiBaseUrl: "", - apiKey, - model, - agents: [agentId], - }); - setProbe(result); - setProbeState(result.ok ? "success" : "error"); - } catch (error) { - setProbeState("error"); - setFailure(describeError(error, t("连接测试失败")).message); - } - }; - - const apply = async () => { - setApplying(true); - setFailure(""); - try { - const result = await api.activateAgent(agentId, { - provider, - apiBaseUrl: "", - apiKey, - model, - profileId: params.get("profile") || undefined, - smallFastModel, - }); - setApplied({ restart: result.restart, next: result.next }); - setApiKey(""); - setKeyFieldId((value) => value + 1); - setProbeState("idle"); - setProbe(null); - void refreshStatus(); - } catch (error) { - setFailure(describeError(error, t("应用配置失败")).message); - } finally { - setApplying(false); - } - }; - - return ( - navigate("/overview")} - primaryLabel={applying ? t("应用中") : t("应用")} - onPrimary={() => void apply()} - primaryDisabled={!canApply} - > -
- - - -
-
-
{t("当前指向")}
-
- {target.text} - {target.note ? {target.note} : null} -
-
-
-
{t("版本")}
-
{version?.text || t("未安装")}
-
-
-
{t("配置文件")}
-
{agent.config || "—"}
-
-
-
{t("备份")}
-
{status.backups[agentId] ? t("已有历史备份") : t("暂无")}
-
-
-
- - {/* Above the form: switching between existing Profiles is the common case, - and editing fields by hand is the fallback for when none of them fit. */} - refreshStatus()} - /> - -
- {willOverwrite ? ( -
- {t("这个 Agent 已有配置,不是 OneAgent 写入的")} - - {t("当前指向 {target}。应用后会被替换,原文件会先备份到同目录的", { - target: [willOverwrite.baseUrl || t("未知端点"), willOverwrite.model].filter(Boolean).join(" · "), - })} - {" "} - *.backup-<{t("时间戳")}>{locale === "en" ? "." : "。"} - -
- ) : null} - navigate(`/providers/new?returnTo=${encodeURIComponent(`/agents/${agentId}`)}`)} - onChange={(next) => { - setProvider(next); - setApiKey(""); - resetVerdict(); - }} - /> - { - setApiKey(value); - resetVerdict(); - }} - /> - -
- - -
- {catalog.protocol ? ( - {t("将测试 {protocol} 协议", { protocol: PROTOCOL_LABELS[catalog.protocol] })} - ) : null} - {/* Say where the key is coming from, and that a skipped test is allowed. - Without this the empty field reads as "no key" on a Provider that has - one, and the enabled Apply button looks like a bug. */} - {!apiKey && providerHasKey ? ( - {t("留空则使用该 Provider 已保存的 Key。")} - ) : null} - {canApply && untested ? ( - {t("未测试连接也可以应用,配置文件会先备份。")} - ) : null} - - -
- - { - setModel(event.target.value); - resetVerdict(); - }} - placeholder={t("留空则由端点的模型列表自动选择")} - /> -
- {agentId === "claude-code" ? ( -
- - setSmallFastModel(event.target.value)} - placeholder={t("留空则与主模型相同")} - /> -
- ) : null} -
- - {failure ?

{failure}

: null} - {applied ? ( -
- {t("已写入配置")} - {applied.restart} - {applied.next ?
{applied.next}
: null} -
- ) : null} -
-
- ); -}