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
3 changes: 0 additions & 3 deletions docs/guides/adding-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,3 @@ func DefaultProviders() []ProviderConfig {

所有内置 provider 都复用 `openai` 驱动,配置集中在 `builtin_providers.go`。

## 相关文档

- [Provider Architecture Optimization PR](../provider-architecture-optimization-pr.md)
36 changes: 3 additions & 33 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ NeoCode 采用"内置 Provider 优先"的策略:
✅ **安全第一**
- API Key 只从环境变量读取,永不写入 YAML
- 不硬编码在源码中
- 支持通过 `.env` 文件管理

### 当前内置 Provider

Expand Down Expand Up @@ -110,7 +109,7 @@ tools:

### 环境变量管理

#### 方式一:系统环境变量
#### 方式:系统环境变量

**Linux/macOS**:
```bash
Expand All @@ -126,37 +125,16 @@ $env:GEMINI_API_KEY="AI..."
$env:AI_API_KEY="your-key"
```

#### 方式二:.env 文件

**项目根目录 `.env`**:
```env
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=AI...
AI_API_KEY=your-key
```

**NeoCode 管理目录 `~/.neocode/.env`**:
```env
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=AI...
```

NeoCode 会自动加载这两个位置的 `.env` 文件。

### 安全最佳实践

⚠️ **重要安全提示**:

1. **永不提交 API Key**
- 确保 `.env` 文件在 `.gitignore` 中
- 不要将包含 API Key 的文件提交到版本控制

2. **使用环境变量**
1**使用环境变量**
- API Key 仅从环境变量读取
- 永不写入配置文件
- 不硬编码在代码中

3. **密钥轮换**
2**密钥轮换**
- 定期更换 API Key
- 不要在多个环境使用同一个 Key

Expand Down Expand Up @@ -204,7 +182,6 @@ NeoCode 提供以下 slash 命令用于快速切换配置:
### 核心功能

- ✅ YAML 加载与保存
- ✅ `.env` 文件自动加载
- ✅ 默认值管理
- ✅ 并发安全访问
- ✅ 配置校验
Expand All @@ -218,8 +195,6 @@ NeoCode 提供以下 slash 命令用于快速切换配置:
应用内置 defaults (来自 builtin_providers.go)
合并 .env 文件环境变量
验证配置完整性
运行时使用
Expand Down Expand Up @@ -307,9 +282,6 @@ max_loops: 10
```bash
# 检查环境变量
echo $OPENAI_API_KEY

# 或检查 .env 文件
cat ~/.neocode/.env
```

### Provider 不存在
Expand All @@ -322,6 +294,4 @@ cat ~/.neocode/.env

## 相关文档

- [配置管理详细设计](../config-management-detail-design.md)
- [Provider 架构优化 PR](../provider-architecture-optimization-pr.md)
- [添加新 Provider](./adding-providers.md)
22 changes: 22 additions & 0 deletions internal/config/builtin_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const (
OpenLLDefaultBaseURL = "https://www.openll.top/v1"
OpenLLDefaultModel = "gpt-5.4"
OpenLLDefaultAPIKeyEnv = "AI_API_KEY"

QiniuName = "qiniu"
QiniuDefaultBaseURL = "https://api.qnaigc.com/v1"
QiniuDefaultModel = "openai/gpt-5"
QiniuDefaultAPIKeyEnv = "QINIU_API_KEY"
)

var openAIModels = []string{
Expand All @@ -36,6 +41,11 @@ var openLLModels = []string{
"gpt-5.3-turbo",
}

var qiniuModels = []string{
QiniuDefaultModel,
"openai/gpt-5.2-codex",
}

// OpenAIProvider 返回 OpenAI provider 的默认配置。
func OpenAIProvider() ProviderConfig {
return ProviderConfig{
Expand Down Expand Up @@ -72,11 +82,23 @@ func OpenLLProvider() ProviderConfig {
}
}

func QiniuProvider() ProviderConfig {
return ProviderConfig{
Name: QiniuName,
Driver: "openai",
BaseURL: QiniuDefaultBaseURL,
Model: QiniuDefaultModel,
Models: append([]string(nil), qiniuModels...),
APIKeyEnv: QiniuDefaultAPIKeyEnv,
}
}

// DefaultProviders 返回所有内置 provider 配置列表。
func DefaultProviders() []ProviderConfig {
return []ProviderConfig{
OpenAIProvider(),
GeminiProvider(),
OpenLLProvider(),
QiniuProvider(),
}
}
50 changes: 3 additions & 47 deletions internal/config/builtin_providers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ func TestDefaultProvidersReturnsAllBuiltins(t *testing.T) {
t.Parallel()

providers := DefaultProviders()
if len(providers) != 3 {
t.Fatalf("expected 3 builtin providers, got %d", len(providers))
if len(providers) != 4 {
t.Fatalf("expected 4 builtin providers, got %d", len(providers))
}

expectedNames := []string{OpenAIName, GeminiName, OpenLLName}
expectedNames := []string{OpenAIName, GeminiName, OpenLLName, QiniuName}
for i, provider := range providers {
if provider.Name != expectedNames[i] {
t.Fatalf("expected provider[%d] name %q, got %q", i, expectedNames[i], provider.Name)
Expand Down Expand Up @@ -134,47 +134,3 @@ func TestDefaultProvidersReturnsIndependentSlices(t *testing.T) {
t.Fatalf("expected first provider name %q, got %q", OpenAIName, providers2[0].Name)
}
}

func TestProviderConstants(t *testing.T) {
t.Parallel()

// Verify all constants are non-empty
if OpenAIName == "" {
t.Fatal("OpenAIName should not be empty")
}
if OpenAIDefaultBaseURL == "" {
t.Fatal("OpenAIDefaultBaseURL should not be empty")
}
if OpenAIDefaultModel == "" {
t.Fatal("OpenAIDefaultModel should not be empty")
}
if OpenAIDefaultAPIKeyEnv == "" {
t.Fatal("OpenAIDefaultAPIKeyEnv should not be empty")
}

if GeminiName == "" {
t.Fatal("GeminiName should not be empty")
}
if GeminiDefaultBaseURL == "" {
t.Fatal("GeminiDefaultBaseURL should not be empty")
}
if GeminiDefaultModel == "" {
t.Fatal("GeminiDefaultModel should not be empty")
}
if GeminiDefaultAPIKeyEnv == "" {
t.Fatal("GeminiDefaultAPIKeyEnv should not be empty")
}

if OpenLLName == "" {
t.Fatal("OpenLLName should not be empty")
}
if OpenLLDefaultBaseURL == "" {
t.Fatal("OpenLLDefaultBaseURL should not be empty")
}
if OpenLLDefaultModel == "" {
t.Fatal("OpenLLDefaultModel should not be empty")
}
if OpenLLDefaultAPIKeyEnv == "" {
t.Fatal("OpenLLDefaultAPIKeyEnv should not be empty")
}
}
74 changes: 0 additions & 74 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,80 +268,6 @@ func TestConfigMethodErrorPaths(t *testing.T) {
})
}

func TestLoaderLoadEnvironmentSources(t *testing.T) {
tests := []struct {
name string
processDotEnv string
managedDotEnv string
expectedAPIKey string
}{
{
name: "loads key from managed env",
managedDotEnv: "OPENAI_API_KEY=managed-key\n",
expectedAPIKey: "managed-key",
},
{
name: "falls back to cwd dotenv",
processDotEnv: "OPENAI_API_KEY=process-key\n",
expectedAPIKey: "process-key",
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
restoreEnv(t, testAPIKeyEnv)
_ = os.Unsetenv(testAPIKeyEnv)

previousWD, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd() error = %v", err)
}
if err := os.Chdir(tempDir); err != nil {
t.Fatalf("Chdir() error = %v", err)
}
t.Cleanup(func() {
_ = os.Chdir(previousWD)
})

if tt.processDotEnv != "" {
if err := os.WriteFile(filepath.Join(tempDir, ".env"), []byte(tt.processDotEnv), 0o644); err != nil {
t.Fatalf("write process .env: %v", err)
}
}

loader := NewLoader(filepath.Join(tempDir, ".neocode"), testDefaultConfig())
if tt.managedDotEnv != "" {
if err := os.MkdirAll(loader.BaseDir(), 0o755); err != nil {
t.Fatalf("mkdir managed dir: %v", err)
}
if err := os.WriteFile(loader.EnvPath(), []byte(tt.managedDotEnv), 0o644); err != nil {
t.Fatalf("write managed .env: %v", err)
}
}

loader.LoadEnvironment()

provider := ProviderConfig{
Name: testProviderName,
Driver: testProviderName,
BaseURL: testBaseURL,
Model: testModel,
APIKeyEnv: testAPIKeyEnv,
}

key, err := provider.ResolveAPIKey()
if err != nil {
t.Fatalf("ResolveAPIKey() error = %v", err)
}
if key != tt.expectedAPIKey {
t.Fatalf("expected %q, got %q", tt.expectedAPIKey, key)
}
})
}
}

func TestManagerConcurrentAccess(t *testing.T) {
tempDir := t.TempDir()
manager := NewManager(NewLoader(tempDir, testDefaultConfig()))
Expand Down
13 changes: 0 additions & 13 deletions internal/config/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@ import (
"path/filepath"
"strings"

"github.com/joho/godotenv"
"gopkg.in/yaml.v3"
)

const (
dirName = ".neocode"
configName = "config.yaml"
envName = ".env"
)

type Loader struct {
Expand Down Expand Up @@ -62,10 +60,6 @@ func (l *Loader) ConfigPath() string {
return filepath.Join(l.baseDir, configName)
}

func (l *Loader) EnvPath() string {
return filepath.Join(l.baseDir, envName)
}

func (l *Loader) DefaultConfig() Config {
return l.defaults.Clone()
}
Expand All @@ -75,8 +69,6 @@ func (l *Loader) Load(ctx context.Context) (*Config, error) {
return nil, err
}

l.LoadEnvironment()

if err := os.MkdirAll(l.baseDir, 0o755); err != nil {
return nil, fmt.Errorf("config: create config dir: %w", err)
}
Expand Down Expand Up @@ -149,11 +141,6 @@ func (l *Loader) Save(ctx context.Context, cfg *Config) error {
return nil
}

func (l *Loader) LoadEnvironment() {
_ = godotenv.Load()
_ = godotenv.Load(l.EnvPath())
}

func defaultBaseDir() string {
home, err := os.UserHomeDir()
if err != nil {
Expand Down
32 changes: 0 additions & 32 deletions internal/config/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,38 +45,6 @@ func TestLoaderLoadMalformedYAML(t *testing.T) {
}
}

func TestLoaderLoadEnvironmentSilentlyIgnoresDotEnvFailures(t *testing.T) {
tempDir := t.TempDir()
restoreEnv(t, testAPIKeyEnv)
_ = os.Unsetenv(testAPIKeyEnv)

previousWD, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd() error = %v", err)
}
if err := os.Chdir(tempDir); err != nil {
t.Fatalf("Chdir() error = %v", err)
}
t.Cleanup(func() {
_ = os.Chdir(previousWD)
})

if err := os.MkdirAll(filepath.Join(tempDir, ".env"), 0o755); err != nil {
t.Fatalf("mkdir cwd .env dir: %v", err)
}

loader := NewLoader(filepath.Join(tempDir, ".neocode"), testDefaultConfig())
if err := os.MkdirAll(loader.EnvPath(), 0o755); err != nil {
t.Fatalf("mkdir managed .env dir: %v", err)
}

loader.LoadEnvironment()

if got := os.Getenv(testAPIKeyEnv); got != "" {
t.Fatalf("expected env to stay empty when dotenv loading fails, got %q", got)
}
}

func TestLoaderLoadInvalidBaseDir(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading