-
Notifications
You must be signed in to change notification settings - Fork 38
Add multi-AI provider profile switching for environment variable configuration #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| using SqlSugar; | ||
|
|
||
| namespace WebCodeCli.Domain.Domain.Model; | ||
|
|
||
| /// <summary> | ||
| /// CLI 工具环境变量配置方案(支持多套 AI 配置快速切换) | ||
| /// </summary> | ||
| [SugarTable("cli_tool_env_profiles")] | ||
| public class CliToolEnvProfile | ||
| { | ||
| /// <summary> | ||
| /// 主键 ID | ||
| /// </summary> | ||
| [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] | ||
| public int Id { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// CLI 工具 ID | ||
| /// </summary> | ||
| [SugarColumn(Length = 50, IsNullable = false, ColumnDescription = "CLI工具ID")] | ||
| public string ToolId { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// 方案名称(如 "OpenAI", "DeepSeek", "Anthropic") | ||
| /// </summary> | ||
| [SugarColumn(Length = 100, IsNullable = false, ColumnDescription = "方案名称")] | ||
| public string ProfileName { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// 是否为当前激活方案 | ||
| /// </summary> | ||
| [SugarColumn(IsNullable = false, ColumnDescription = "是否激活")] | ||
| public bool IsActive { get; set; } = false; | ||
|
|
||
| /// <summary> | ||
| /// 环境变量(JSON 格式存储键值对) | ||
| /// </summary> | ||
| [SugarColumn(Length = 8000, IsNullable = true, ColumnDescription = "环境变量JSON")] | ||
| public string? EnvVarsJson { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// 创建时间 | ||
| /// </summary> | ||
| [SugarColumn(IsNullable = false, ColumnDescription = "创建时间")] | ||
| public DateTime CreatedAt { get; set; } = DateTime.Now; | ||
|
|
||
| /// <summary> | ||
| /// 更新时间 | ||
| /// </summary> | ||
| [SugarColumn(IsNullable = false, ColumnDescription = "更新时间")] | ||
| public DateTime UpdatedAt { get; set; } = DateTime.Now; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,8 +1,10 @@ | ||||||
| using System.Text.Json; | ||||||
| using Microsoft.Extensions.DependencyInjection; | ||||||
| using Microsoft.Extensions.Logging; | ||||||
| using Microsoft.Extensions.Options; | ||||||
| using WebCodeCli.Domain.Common.Extensions; | ||||||
| using WebCodeCli.Domain.Common.Options; | ||||||
| using WebCodeCli.Domain.Domain.Model; | ||||||
| using WebCodeCli.Domain.Repositories.Base.CliToolEnv; | ||||||
|
|
||||||
| namespace WebCodeCli.Domain.Domain.Service; | ||||||
|
|
@@ -13,12 +15,12 @@ namespace WebCodeCli.Domain.Domain.Service; | |||||
| public interface ICliToolEnvironmentService | ||||||
| { | ||||||
| /// <summary> | ||||||
| /// 获取指定工具的环境变量配置(优先从数据库读取,否则从appsettings读取) | ||||||
| /// 获取指定工具的环境变量配置(优先使用激活方案,其次数据库默认配置,最后 appsettings) | ||||||
| /// </summary> | ||||||
| Task<Dictionary<string, string>> GetEnvironmentVariablesAsync(string toolId); | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 保存指定工具的环境变量配置到数据库 | ||||||
| /// 保存指定工具的默认环境变量配置到数据库 | ||||||
| /// </summary> | ||||||
| Task<bool> SaveEnvironmentVariablesAsync(string toolId, Dictionary<string, string> envVars); | ||||||
|
|
||||||
|
|
@@ -31,6 +33,33 @@ public interface ICliToolEnvironmentService | |||||
| /// 重置为appsettings中的默认配置 | ||||||
| /// </summary> | ||||||
| Task<Dictionary<string, string>> ResetToDefaultAsync(string toolId); | ||||||
|
|
||||||
| // ── 配置方案(多套 AI 环境变量) ── | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 获取指定工具的所有配置方案 | ||||||
| /// </summary> | ||||||
| Task<List<CliToolEnvProfile>> GetProfilesAsync(string toolId); | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 保存(新建或更新)一个配置方案 | ||||||
| /// </summary> | ||||||
| Task<CliToolEnvProfile?> SaveProfileAsync(string toolId, int profileId, string profileName, Dictionary<string, string> envVars); | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 激活指定配置方案(将其设为当前生效方案) | ||||||
| /// </summary> | ||||||
| Task<bool> ActivateProfileAsync(string toolId, int profileId); | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 取消所有方案激活,回退到默认配置 | ||||||
| /// </summary> | ||||||
| Task<bool> DeactivateProfilesAsync(string toolId); | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 删除指定配置方案 | ||||||
| /// </summary> | ||||||
| Task<bool> DeleteProfileAsync(string toolId, int profileId); | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
|
|
@@ -42,43 +71,54 @@ public class CliToolEnvironmentService : ICliToolEnvironmentService | |||||
| private readonly ILogger<CliToolEnvironmentService> _logger; | ||||||
| private readonly CliToolsOption _options; | ||||||
| private readonly ICliToolEnvironmentVariableRepository _repository; | ||||||
| private readonly ICliToolEnvProfileRepository _profileRepository; | ||||||
|
|
||||||
| public CliToolEnvironmentService( | ||||||
| ILogger<CliToolEnvironmentService> logger, | ||||||
| IOptions<CliToolsOption> options, | ||||||
| ICliToolEnvironmentVariableRepository repository) | ||||||
| ICliToolEnvironmentVariableRepository repository, | ||||||
| ICliToolEnvProfileRepository profileRepository) | ||||||
| { | ||||||
| _logger = logger; | ||||||
| _options = options.Value; | ||||||
| _repository = repository; | ||||||
| _profileRepository = profileRepository; | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 获取指定工具的环境变量配置(优先从数据库读取,否则从appsettings读取) | ||||||
| /// 获取指定工具的环境变量配置 | ||||||
| /// 优先级:激活的配置方案 > 数据库默认配置 > appsettings 配置 | ||||||
| /// </summary> | ||||||
| public async Task<Dictionary<string, string>> GetEnvironmentVariablesAsync(string toolId) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| // 尝试从数据库读取 | ||||||
| // 1. 优先使用激活的配置方案 | ||||||
| var activeProfile = await _profileRepository.GetActiveProfileAsync(toolId); | ||||||
| if (activeProfile != null && !string.IsNullOrWhiteSpace(activeProfile.EnvVarsJson)) | ||||||
| { | ||||||
| _logger.LogInformation("从激活方案 [{ProfileName}] 加载工具 {ToolId} 的环境变量配置", activeProfile.ProfileName, toolId); | ||||||
| var profileVars = JsonSerializer.Deserialize<Dictionary<string, string>>(activeProfile.EnvVarsJson) ?? new(); | ||||||
| return profileVars | ||||||
| .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value)) | ||||||
| .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | ||||||
| } | ||||||
|
|
||||||
| // 2. 从数据库默认配置读取 | ||||||
| var dbEnvVars = await _repository.GetEnvironmentVariablesByToolIdAsync(toolId); | ||||||
|
|
||||||
| // 如果数据库中有配置,则使用数据库配置(过滤空值) | ||||||
| if (dbEnvVars.Any()) | ||||||
| { | ||||||
| _logger.LogInformation("从数据库加载工具 {ToolId} 的环境变量配置", toolId); | ||||||
| // 过滤掉空值的环境变量,避免空字符串覆盖系统默认配置 | ||||||
| return dbEnvVars | ||||||
| .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value)) | ||||||
| .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | ||||||
| } | ||||||
|
|
||||||
| // 否则从appsettings读取 | ||||||
| // 3. 从 appsettings 读取 | ||||||
| var tool = _options.Tools.FirstOrDefault(t => t.Id == toolId); | ||||||
| if (tool?.EnvironmentVariables != null && tool.EnvironmentVariables.Any()) | ||||||
| { | ||||||
| _logger.LogInformation("从配置文件加载工具 {ToolId} 的环境变量配置", toolId); | ||||||
| // 同样过滤掉空值 | ||||||
| return tool.EnvironmentVariables | ||||||
| .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value)) | ||||||
| .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | ||||||
|
|
@@ -161,4 +201,135 @@ public async Task<Dictionary<string, string>> ResetToDefaultAsync(string toolId) | |||||
| return new Dictionary<string, string>(); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // ── 配置方案(多套 AI 环境变量) ── | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 获取指定工具的所有配置方案 | ||||||
| /// </summary> | ||||||
| public async Task<List<CliToolEnvProfile>> GetProfilesAsync(string toolId) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| return await _profileRepository.GetProfilesByToolIdAsync(toolId); | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| _logger.LogError(ex, "获取工具 {ToolId} 的配置方案列表失败", toolId); | ||||||
| return new List<CliToolEnvProfile>(); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 保存(新建或更新)一个配置方案 | ||||||
| /// </summary> | ||||||
| public async Task<CliToolEnvProfile?> SaveProfileAsync(string toolId, int profileId, string profileName, Dictionary<string, string> envVars) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| var envVarsJson = JsonSerializer.Serialize(envVars); | ||||||
|
|
||||||
| if (profileId <= 0) | ||||||
| { | ||||||
| // 新建方案 | ||||||
| var newProfile = new CliToolEnvProfile | ||||||
| { | ||||||
| ToolId = toolId, | ||||||
| ProfileName = profileName, | ||||||
| IsActive = false, | ||||||
| EnvVarsJson = envVarsJson, | ||||||
| CreatedAt = DateTime.Now, | ||||||
| UpdatedAt = DateTime.Now | ||||||
| }; | ||||||
| var newId = await _profileRepository.InsertReturnIdentityAsync(newProfile); | ||||||
| newProfile.Id = newId; | ||||||
| _logger.LogInformation("成功新建工具 {ToolId} 的配置方案 [{ProfileName}]", toolId, profileName); | ||||||
| return newProfile; | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| // 更新已有方案 | ||||||
| var existing = await _profileRepository.GetByIdAsync(profileId); | ||||||
| if (existing == null || existing.ToolId != toolId) | ||||||
| { | ||||||
| _logger.LogWarning("未找到工具 {ToolId} 的配置方案 {ProfileId}", toolId, profileId); | ||||||
| return null; | ||||||
| } | ||||||
| existing.ProfileName = profileName; | ||||||
| existing.EnvVarsJson = envVarsJson; | ||||||
| existing.UpdatedAt = DateTime.Now; | ||||||
| await _profileRepository.UpdateAsync(existing); | ||||||
| _logger.LogInformation("成功更新工具 {ToolId} 的配置方案 [{ProfileName}]", toolId, profileName); | ||||||
| return existing; | ||||||
| } | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| _logger.LogError(ex, "保存工具 {ToolId} 的配置方案失败", toolId); | ||||||
| return null; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 激活指定配置方案 | ||||||
| /// </summary> | ||||||
| public async Task<bool> ActivateProfileAsync(string toolId, int profileId) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| var result = await _profileRepository.ActivateProfileAsync(toolId, profileId); | ||||||
| if (result) | ||||||
| { | ||||||
| _logger.LogInformation("成功激活工具 {ToolId} 的配置方案 {ProfileId}", toolId, profileId); | ||||||
| } | ||||||
| return result; | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| _logger.LogError(ex, "激活工具 {ToolId} 的配置方案 {ProfileId} 失败", toolId, profileId); | ||||||
| return false; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 取消所有方案激活,回退到默认配置 | ||||||
| /// </summary> | ||||||
| public async Task<bool> DeactivateProfilesAsync(string toolId) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| var result = await _profileRepository.DeactivateAllProfilesAsync(toolId); | ||||||
| if (result) | ||||||
| { | ||||||
| _logger.LogInformation("已取消工具 {ToolId} 的所有方案激活状态", toolId); | ||||||
| } | ||||||
| return result; | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| _logger.LogError(ex, "取消工具 {ToolId} 方案激活状态失败", toolId); | ||||||
| return false; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// 删除指定配置方案 | ||||||
| /// </summary> | ||||||
| public async Task<bool> DeleteProfileAsync(string toolId, int profileId) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| var result = await _profileRepository.DeleteProfileAsync(profileId); | ||||||
|
||||||
| var result = await _profileRepository.DeleteProfileAsync(profileId); | |
| var result = await _profileRepository.DeleteProfileAsync(toolId, profileId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
activeProfile.EnvVarsJsonis invalid JSON,JsonSerializer.Deserializewill throw and the outer catch returns an empty dictionary, skipping the intended fallback to DB default/appsettings. Suggest catchingJsonExceptionaround the profile deserialization (log + ignore profile) and then continuing to the DB/appsettings resolution path.