diff --git a/docs/guides/adding-providers.md b/docs/guides/adding-providers.md index 03fe7441..8880a882 100644 --- a/docs/guides/adding-providers.md +++ b/docs/guides/adding-providers.md @@ -280,6 +280,3 @@ func DefaultProviders() []ProviderConfig { 所有内置 provider 都复用 `openai` 驱动,配置集中在 `builtin_providers.go`。 -## 相关文档 - -- [Provider Architecture Optimization PR](../provider-architecture-optimization-pr.md) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index bc2a834f..226b9e5b 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -20,7 +20,6 @@ NeoCode 采用"内置 Provider 优先"的策略: ✅ **安全第一** - API Key 只从环境变量读取,永不写入 YAML - 不硬编码在源码中 -- 支持通过 `.env` 文件管理 ### 当前内置 Provider @@ -110,7 +109,7 @@ tools: ### 环境变量管理 -#### 方式一:系统环境变量 +#### 方式:系统环境变量 **Linux/macOS**: ```bash @@ -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 @@ -204,7 +182,6 @@ NeoCode 提供以下 slash 命令用于快速切换配置: ### 核心功能 - ✅ YAML 加载与保存 -- ✅ `.env` 文件自动加载 - ✅ 默认值管理 - ✅ 并发安全访问 - ✅ 配置校验 @@ -218,8 +195,6 @@ NeoCode 提供以下 slash 命令用于快速切换配置: ↓ 应用内置 defaults (来自 builtin_providers.go) ↓ -合并 .env 文件环境变量 - ↓ 验证配置完整性 ↓ 运行时使用 @@ -307,9 +282,6 @@ max_loops: 10 ```bash # 检查环境变量 echo $OPENAI_API_KEY - -# 或检查 .env 文件 -cat ~/.neocode/.env ``` ### Provider 不存在 @@ -322,6 +294,4 @@ cat ~/.neocode/.env ## 相关文档 -- [配置管理详细设计](../config-management-detail-design.md) -- [Provider 架构优化 PR](../provider-architecture-optimization-pr.md) - [添加新 Provider](./adding-providers.md) diff --git a/internal/config/builtin_providers.go b/internal/config/builtin_providers.go index 8d7fa8b9..90916086 100644 --- a/internal/config/builtin_providers.go +++ b/internal/config/builtin_providers.go @@ -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{ @@ -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{ @@ -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(), } } diff --git a/internal/config/builtin_providers_test.go b/internal/config/builtin_providers_test.go index a9fe0c83..5234e4f7 100644 --- a/internal/config/builtin_providers_test.go +++ b/internal/config/builtin_providers_test.go @@ -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) @@ -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") - } -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 8f8c82d5..97a677b9 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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())) diff --git a/internal/config/loader.go b/internal/config/loader.go index 0c40c051..5973f662 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -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 { @@ -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() } @@ -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) } @@ -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 { diff --git a/internal/config/loader_test.go b/internal/config/loader_test.go index fc20a2d1..1b54453a 100644 --- a/internal/config/loader_test.go +++ b/internal/config/loader_test.go @@ -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() diff --git a/internal/provider/builtin/builtin_more_test.go b/internal/provider/builtin/builtin_more_test.go deleted file mode 100644 index 50e8ccf3..00000000 --- a/internal/provider/builtin/builtin_more_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package builtin - -import ( - "context" - "errors" - "testing" - - "neo-code/internal/config" - "neo-code/internal/provider" - "neo-code/internal/provider/openai" -) - -func TestRegisterRegistersOpenAIDriver(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - reg *provider.Registry - wantErr string - }{ - { - name: "nil registry", - reg: nil, - wantErr: "builtin provider registry is nil", - }, - { - name: "registers openai driver", - reg: provider.NewRegistry(), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - err := Register(tt.reg) - if tt.wantErr != "" { - if err == nil || err.Error() != tt.wantErr { - t.Fatalf("expected error %q, got %v", tt.wantErr, err) - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !tt.reg.Supports(openai.DriverName) { - t.Fatalf("expected registry to support %q", openai.DriverName) - } - }) - } -} - -func TestNewRegistryIncludesOpenAIDriver(t *testing.T) { - t.Parallel() - - reg, err := NewRegistry() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if reg == nil { - t.Fatal("expected registry") - } - if !reg.Supports(openai.DriverName) { - t.Fatalf("expected builtin registry to support %q", openai.DriverName) - } -} - -func TestNewRegistryBuildsRegisteredDriver(t *testing.T) { - t.Parallel() - - reg, err := NewRegistry() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - _, err = reg.Build(context.Background(), config.ResolvedProviderConfig{ - ProviderConfig: config.ProviderConfig{ - Driver: openai.DriverName, - }, - }) - if err == nil { - t.Fatal("expected build to fail without api key or required config") - } - if errors.Is(err, provider.ErrDriverNotFound) { - t.Fatalf("expected registered driver error, got %v", err) - } -} diff --git a/internal/provider/builtin/builtin_test.go b/internal/provider/builtin/builtin_test.go index 67496213..f3d9242d 100644 --- a/internal/provider/builtin/builtin_test.go +++ b/internal/provider/builtin/builtin_test.go @@ -4,15 +4,14 @@ import ( "testing" "neo-code/internal/config" - "neo-code/internal/provider" ) func TestDefaultConfigIncludesBuiltinProviders(t *testing.T) { t.Parallel() cfg := DefaultConfig() - if len(cfg.Providers) != 3 { - t.Fatalf("expected 3 builtin providers, got %d", len(cfg.Providers)) + if len(cfg.Providers) != 4 { + t.Fatalf("expected 4 builtin providers, got %d", len(cfg.Providers)) } if cfg.Providers[0].Name != config.OpenAIName { t.Fatalf("expected first provider %q, got %q", config.OpenAIName, cfg.Providers[0].Name) @@ -23,6 +22,9 @@ func TestDefaultConfigIncludesBuiltinProviders(t *testing.T) { if cfg.Providers[2].Name != config.OpenLLName { t.Fatalf("expected third provider %q, got %q", config.OpenLLName, cfg.Providers[2].Name) } + if cfg.Providers[3].Name != config.QiniuName { + t.Fatalf("expected fourth provider %q, got %q", config.QiniuName, cfg.Providers[3].Name) + } if cfg.SelectedProvider != config.OpenAIName { t.Fatalf("expected selected provider %q, got %q", config.OpenAIName, cfg.SelectedProvider) } @@ -43,27 +45,6 @@ func TestNewRegistry(t *testing.T) { } } -func TestRegister(t *testing.T) { - t.Parallel() - - t.Run("success", func(t *testing.T) { - t.Parallel() - registry := provider.NewRegistry() - err := Register(registry) - if err != nil { - t.Fatalf("Register() error = %v", err) - } - }) - - t.Run("nil registry", func(t *testing.T) { - t.Parallel() - err := Register(nil) - if err == nil { - t.Fatal("expected error for nil registry") - } - }) -} - func TestDefaultConfigValidates(t *testing.T) { t.Parallel() diff --git a/internal/provider/registry_test.go b/internal/provider/registry_test.go index ad9b57d8..16c022ca 100644 --- a/internal/provider/registry_test.go +++ b/internal/provider/registry_test.go @@ -125,6 +125,7 @@ func TestServiceListProvidersUsesConfiguredMetadata(t *testing.T) { config.OpenAIName: len(config.OpenAIProvider().Models), config.GeminiName: len(config.GeminiProvider().Models), config.OpenLLName: len(config.OpenLLProvider().Models), + config.QiniuName: len(config.QiniuProvider().Models), } if len(items) != len(expectedModels) { t.Fatalf("expected only supported providers, got %d", len(items)) diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index d5ebeab6..193e7295 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -120,7 +120,7 @@ func (s *Service) Run(ctx context.Context, input UserInput) error { go s.forwardProviderEvents(ctx, input.RunID, session.ID, streamEvents, streamDone) resp, err := modelProvider.Chat(ctx, provider.ChatRequest{ - Model: resolvedProvider.Model, + Model: cfg.CurrentModel, SystemPrompt: s.systemPrompt(), Messages: s.trimMessages(session.Messages), Tools: s.toolRegistry.GetSpecs(),