Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
## 4. AI 修改代码时的执行流程
- 先定位改动所属模块,再检查是否会破坏职责边界。
- 优先做最小闭环改动,避免无关重构。
- 项目中可能存在语义不清的地方,必须要谨慎分析
- 如果新增能力会被模型调用,先设计 `tools` 抽象,再接入 `runtime`。
- 如果改动涉及 provider 协议,差异收敛在 `internal/provider` 内,不向外扩散特定厂商字段。
- 如果改动涉及配置,统一经由 `ConfigManager` 或现有配置管理入口处理。
Expand Down Expand Up @@ -93,6 +94,7 @@
- 避免硬编码业务语义字符串或状态值(例如消息角色、事件类型、协议字段值);如需复用,优先收敛到共享常量、类型或统一定义处。
- 优先写清晰、可替换的实现,不要为了“未来可能需要”提前引入复杂泛化。
- 不要擅自删除、改写或遗漏原有注释;若注释需要调整,应在理解原意后做等价更新,而不是直接移除。
- 如果有需要,可以使用文档块注释,并使用 `//` 或 `/* */`
- 如果文件内容疑似乱码或编码异常,先尝试用其他编码方式查看和确认原文,再决定是否修改;未确认前不要擅自删除或重写相关内容。

## 7. 配置与安全规则
Expand Down
156 changes: 128 additions & 28 deletions docs/config-management-detail-design.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,134 @@
# 配置管理模块详细设计

## 模块职责
`config` 模块主要负责四类事情:
- 加载和保存 YAML 配置文件
- 从环境变量解析真实密钥
- 管理 NeoCode 托管目录中的配置与 `.env`
- 向运行中的系统提供并发安全的配置读写能力

`internal/config` 只负责以下几件事:

- 读取和保存 `~/.neocode/config.yaml`
- 提供静态默认值
- 组装运行时 provider 集合
- builtin provider 来自代码内置定义
- custom provider 来自 `~/.neocode/providers/<name>/provider.yaml`
- 提供并发安全的配置快照读写能力
- 在真正发起请求前,根据环境变量解析 API Key

`internal/config` 不负责以下事情:

- 不自动加载 `.env`
- 不兼容或清洗旧版 YAML 字段
- 不判断 `selected_provider/current_model` 是否已经可直接运行
- 不承担模型发现、目录缓存或运行时选择修正

最后一项由 `internal/config/state` 负责:它基于当前运行时支持情况和 catalog 结果修正选择状态,并将变更持久化。

## 核心类型
- `Config`:顶层应用配置,运行时包含 Provider 列表、当前选中 Provider、当前模型、工作目录、Shell 和循环限制等信息
- `ProviderConfig`:单个 Provider 的内建定义,包括 Base URL、默认模型、实例级模型列表和 API Key 环境变量名
- `Manager`:使用 `sync.RWMutex` 保护的配置访问器与修改器
- `Loader`:对 YAML 文件和托管 `.env` 文件的文件系统封装

## 环境变量策略
- YAML 不保存 `api_key_env`,只保存 `selected_provider`、`current_model` 和通用运行配置。
- `Loader.LoadEnvironment` 会尝试加载当前工作目录下的 `.env` 和 NeoCode 托管目录中的 `.env`。
- `ProviderConfig.ResolveAPIKey` 在真正发起请求前通过 `os.Getenv` 读取密钥。

## 运行时更新
- TUI 只能通过 `ConfigManager.Update` 修改配置。
- TUI 只负责切换当前 Provider 和当前模型,不直接修改 Provider 元数据。
- `base_url`、`api_key_env`、`driver` 和 `models` 由代码内建定义提供,不从 YAML 读写。
- 修改模型时,只更新 `current_model`;当前 Provider 的 `model` 仍表示默认模型,`models` 负责描述该实例可选模型列表。

## 默认值治理
- 默认 Provider 名称、URL、默认模型、模型列表和环境变量名统一由内建 Provider 定义提供。
- `Loader` 在加载旧配置时会丢弃 `providers` / `provider_overrides`,重新回到“YAML 只保存选择状态”的最小结构。
- Provider 的可选模型目录属于实例配置,进入运行时 `Config` 后再提供给 TUI 和 runtime,避免 TUI 或 driver 自己维护一套零散常量。

- `config.StaticDefaults()`
- 返回 config 层负责的静态默认值骨架
- 不包含 provider 装配,也不保证选择状态可运行
- `config.Loader`
- 负责 `config.yaml` 与 custom provider 文件的文件系统读写
- `config.Manager`
- 提供线程安全的快照读取、更新与保存
- `config.Config`
- 顶层运行时配置快照
- `config.Config.ValidateSnapshot()`
- 只校验配置快照本身是否结构完整
- 不等价于“当前配置已经 runtime-ready”

## 配置来源

### 1. 静态默认值

静态默认值由 `config.StaticDefaults()` 提供,覆盖:

- `workdir`
- `shell`
- `max_loops`
- `tool_timeout_sec`
- `context`
- `tools`

这些默认值只表达“缺省配置长什么样”,不代表完整可运行配置。

### 2. builtin provider

builtin provider 由 `internal/config/builtin_providers.go` 提供。这里集中维护:

- provider 名称
- driver
- base URL
- 默认模型
- API Key 对应的环境变量名

### 3. custom provider

custom provider 来自:

```text
~/.neocode/providers/<provider-name>/provider.yaml
```

当前只接受明确受支持的字段;未知字段会直接报错,不做“旧格式自动迁移”。

## 加载流程

启动时配置加载流程如下:

1. 构造 `StaticDefaults`
2. 读取 `config.yaml`
3. 严格解析 YAML
- 使用 `KnownFields(true)`
- 未知字段直接失败
4. 扫描 custom provider 目录
5. 组装 builtin/custom provider 集合
6. 应用静态默认值
7. 执行 `ValidateSnapshot()`
8. 交给 `config/state.Service.EnsureSelection()` 修正选择状态

这里有两个明确边界:

- `ValidateSnapshot()` 只验证“配置快照是否完整”
- `EnsureSelection()` 才处理“当前选择是否还能运行”

## 持久化策略

`config.yaml` 只持久化用户可编辑的最小运行时状态,不持久化 provider 元数据。

当前持久化内容包括:

- `selected_provider`
- `current_model`
- `shell`
- `max_loops`
- `tool_timeout_sec`
- `context`
- `tools`

当前不写回 `config.yaml` 的内容包括:

- `providers`
- `base_url`
- `api_key_env`
- `models`
- `workdir`

`workdir` 只来自启动默认值或 CLI 覆盖,不进入 YAML。

## 安全约束
- 读操作统一走 `Get`,并返回拷贝后的配置快照。
- 写操作统一走 `Update`,修改前后都要做校验。
- 真实密钥不能出现在日志、状态栏、聊天流或错误提示中。

- API Key 只从环境变量读取
- YAML 中不保存明文密钥
- 配置访问统一走 `Manager.Get()` / `Manager.Update()`
- `Get()` 返回克隆快照,避免共享可变状态
- `Update()` 在保存前重新执行默认值补齐与快照校验

## 设计结论

`config` 层的目标不是“替运行时兜底一切”,而是提供一份边界清晰、结构稳定、可验证的配置快照。

因此这里坚持三条规则:

- 不在 `config` 中堆兼容旧字段的逻辑
- 不把选择修正混进快照校验
- 不把 provider/catalog/runtime 语义倒灌回 `config`
10 changes: 6 additions & 4 deletions docs/guides/adding-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,11 @@ package anthropic

import (
"context"
"errors"
"net/http"

"strings"
"time"

"neo-code/internal/config"
domain "neo-code/internal/provider"
)
Expand All @@ -117,8 +120,8 @@ type Provider struct {

// New 构造函数
func New(cfg config.ResolvedProviderConfig) (*Provider, error) {
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("anthropic provider: %w", err)
if strings.TrimSpace(cfg.APIKey) == "" {
return nil, errors.New("anthropic provider: api key is empty")
}
return &Provider{
cfg: cfg,
Expand Down Expand Up @@ -267,4 +270,3 @@ func DefaultProviders() []ProviderConfig {
```

所有内置 provider 都通过代码集中注册。模型选择器展示的候选模型由默认模型、动态发现结果和本地缓存共同组成。

Loading
Loading