Add multi-AI provider profile switching for environment variable configuration#72
Add multi-AI provider profile switching for environment variable configuration#72
Conversation
Co-authored-by: xuzeyu91 <26290929+xuzeyu91@users.noreply.github.com>
|
@copilot 评审一下 |
Co-authored-by: xuzeyu91 <26290929+xuzeyu91@users.noreply.github.com>
代码评审完成,发现两个问题并已修复(commit 问题1:删除无确认步骤 问题2:编辑已有方案时无法重命名 UI 改进效果(删除确认行): 方案名称输入框(新建和编辑均显示): |
There was a problem hiding this comment.
Pull request overview
This PR adds support for switching between multiple AI provider environment-variable configurations per CLI tool by introducing named “profiles”, with UI controls to manage/activate them and localization updates to support the new UX.
Changes:
- Added
CliToolEnvProfilemodel + repository to persist per-tool named env-var profiles and activate/deactivate them. - Updated
CliToolEnvironmentServiceto resolve env vars by priority: active profile → DB default → appsettings, and exposed profile CRUD APIs. - Updated
EnvironmentVariableConfigModalto manage profiles (create/edit/activate/deactivate/delete) and added new localization keys.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| WebCodeCli.Domain/Domain/Model/CliToolEnvProfile.cs | New SqlSugar entity for storing per-tool env-var profiles. |
| WebCodeCli.Domain/Repositories/Base/CliToolEnv/ICliToolEnvProfileRepository.cs | Repository interface for profile queries and activation/deactivation. |
| WebCodeCli.Domain/Repositories/Base/CliToolEnv/CliToolEnvProfileRepository.cs | Repository implementation for profile persistence and activation logic. |
| WebCodeCli.Domain/Domain/Service/CliToolEnvironmentService.cs | Adds profile APIs and updates env-var resolution priority. |
| WebCodeCli/Components/EnvironmentVariableConfigModal.razor | UI for profile management and contextual editing of default vs profile configs. |
| WebCodeCli/Resources/Localization/zh-CN.json | Adds profile-management localization keys (source-of-truth). |
| WebCodeCli/Resources/Localization/en-US.json | Adds profile-management localization keys (source-of-truth). |
| WebCodeCli/Resources/Localization/ja-JP.json | Adds profile-management localization keys (source-of-truth). |
| WebCodeCli/Resources/Localization/ko-KR.json | Adds profile-management localization keys (source-of-truth). |
| WebCodeCli/wwwroot/Resources/Localization/zh-CN.json | Same localization keys under wwwroot (appears to be build output). |
| WebCodeCli/wwwroot/Resources/Localization/en-US.json | Same localization keys under wwwroot (appears to be build output). |
| WebCodeCli/wwwroot/Resources/Localization/ja-JP.json | Same localization keys under wwwroot (appears to be build output). |
| WebCodeCli/wwwroot/Resources/Localization/ko-KR.json | Same localization keys under wwwroot (appears to be build output). |
| "saveProfile": "프로파일 저장", | ||
| "cancelEdit": "편집 취소" | ||
| }, | ||
| "fileSearch": { |
There was a problem hiding this comment.
These localization files under wwwroot/Resources/Localization look like build outputs. The project copies Resources/Localization/*.json into wwwroot/Resources/Localization at build time (see WebCodeCli.csproj CopyLocalizationFiles), so committing edits here will either be overwritten or create drift. Suggest removing wwwroot/Resources/Localization/*.json changes from the PR and keeping WebCodeCli/Resources/Localization/*.json as the single source of truth.
| "saveProfile": "프로파일 저장", | |
| "cancelEdit": "편집 취소" | |
| }, | |
| "fileSearch": { | |
| "saveProfile": "프로파일 저장" | |
| }, | |
| "fileSearch": { | |
| "fileSearch": { |
| _isLoading = true; | ||
| StateHasChanged(); | ||
| try | ||
| { | ||
| await CliToolEnvironmentService.ActivateProfileAsync(_selectedTool.Id, profile.Id); |
There was a problem hiding this comment.
DeactivateProfiles() refreshes _profiles but doesn’t reload the default env vars into the editor. After deactivation the effective config falls back to default, but the editor may remain empty until the modal is reopened. Suggest calling GetEnvironmentVariablesAsync (or ResetToDefaultAsync depending on intent) to repopulate _envVars after successful deactivation.
| _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); |
There was a problem hiding this comment.
If activeProfile.EnvVarsJson is invalid JSON, JsonSerializer.Deserialize will throw and the outer catch returns an empty dictionary, skipping the intended fallback to DB default/appsettings. Suggest catching JsonException around the profile deserialization (log + ignore profile) and then continuing to the DB/appsettings resolution path.
| _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); | |
| try | |
| { | |
| _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); | |
| } | |
| catch (JsonException jsonEx) | |
| { | |
| _logger.LogWarning(jsonEx, | |
| "激活方案 [{ProfileName}] 的环境变量 JSON 无效,忽略该方案并回退到数据库和配置文件。ToolId: {ToolId}", | |
| activeProfile.ProfileName, | |
| toolId); | |
| } |
| class="px-2 py-1 text-xs font-semibold text-green-700 bg-green-50 border border-green-200 rounded-md hover:bg-green-100 transition-all" | ||
| title="@T("envConfig.activateProfile")"> | ||
| @T("envConfig.activateProfile") | ||
| </button> | ||
| } |
There was a problem hiding this comment.
Delete action doesn’t prompt for confirmation even though envConfig.deleteProfileConfirm is added to localization. Consider asking for confirmation (e.g., via JS confirm or an existing modal pattern) before calling DeleteProfile(profile) to avoid accidental profile deletion.
| "saveProfile": "Save Profile", | ||
| "cancelEdit": "Cancel Edit" |
There was a problem hiding this comment.
These localization files under wwwroot/Resources/Localization look like build outputs. The project copies Resources/Localization/*.json into wwwroot/Resources/Localization at build time (see WebCodeCli.csproj CopyLocalizationFiles), so committing edits here will either be overwritten or create drift. Suggest removing wwwroot/Resources/Localization/*.json changes from the PR and keeping WebCodeCli/Resources/Localization/*.json as the single source of truth.
| "saveProfile": "Save Profile", | |
| "cancelEdit": "Cancel Edit" | |
| "saveProfile": "Save Profile" |
| /// 删除指定方案 | ||
| /// </summary> | ||
| public async Task<bool> DeleteProfileAsync(int profileId) | ||
| { | ||
| try | ||
| { | ||
| return await DeleteAsync(x => x.Id == profileId); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "删除方案 {ProfileId} 失败", profileId); |
There was a problem hiding this comment.
DeleteProfileAsync(int profileId) deletes by ID only. Since higher-level APIs are tool-scoped, consider deleting with an additional ToolId predicate (or first loading and validating ownership) to avoid cross-tool deletion if a wrong ID is ever passed.
| /// 删除指定方案 | |
| /// </summary> | |
| public async Task<bool> DeleteProfileAsync(int profileId) | |
| { | |
| try | |
| { | |
| return await DeleteAsync(x => x.Id == profileId); | |
| } | |
| catch (Exception ex) | |
| { | |
| _logger.LogError(ex, "删除方案 {ProfileId} 失败", profileId); | |
| /// 删除指定工具下的指定方案 | |
| /// </summary> | |
| public async Task<bool> DeleteProfileAsync(string toolId, int profileId) | |
| { | |
| try | |
| { | |
| return await DeleteAsync(x => x.Id == profileId && x.ToolId == toolId); | |
| } | |
| catch (Exception ex) | |
| { | |
| _logger.LogError(ex, "删除工具 {ToolId} 的方案 {ProfileId} 失败", toolId, profileId); |
| _newProfileName = profile.ProfileName; | ||
|
|
||
| try | ||
| { | ||
| if (!string.IsNullOrWhiteSpace(profile.EnvVarsJson)) |
There was a problem hiding this comment.
CancelProfileEdit() clears _envVars, so after canceling (or after saving a profile, since Save() calls CancelProfileEdit()), the UI falls back to “editing default config” but shows an empty env-var list even when a default config exists. Suggest reloading the default env vars when exiting profile edit (and only clearing when a profile is active, matching ShowAsync).
| "saveProfile": "プロファイルを保存", | ||
| "cancelEdit": "編集をキャンセル" |
There was a problem hiding this comment.
These localization files under wwwroot/Resources/Localization look like build outputs. The project copies Resources/Localization/*.json into wwwroot/Resources/Localization at build time (see WebCodeCli.csproj CopyLocalizationFiles), so committing edits here will either be overwritten or create drift. Suggest removing wwwroot/Resources/Localization/*.json changes from the PR and keeping WebCodeCli/Resources/Localization/*.json as the single source of truth.
| "saveProfile": "プロファイルを保存", | |
| "cancelEdit": "編集をキャンセル" | |
| "saveProfile": "プロファイルを保存" |
| { | ||
| await CliToolEnvironmentService.DeleteProfileAsync(_selectedTool.Id, profile.Id); | ||
| _profiles = await CliToolEnvironmentService.GetProfilesAsync(_selectedTool.Id); |
There was a problem hiding this comment.
The fallback profile name "未命名方案" is hard-coded and not localized. Suggest either requiring a non-empty profile name (validation + disable save) or using a localized string for the default name so all languages behave consistently.
| { | ||
| try | ||
| { | ||
| var result = await _profileRepository.DeleteProfileAsync(profileId); |
There was a problem hiding this comment.
DeleteProfileAsync(string toolId, int profileId) ignores toolId and deletes purely by profileId. To prevent accidental cross-tool deletion (and to make the toolId parameter meaningful), verify the profile belongs to toolId before deleting or push the (toolId, profileId) constraint into the repository delete query.
| var result = await _profileRepository.DeleteProfileAsync(profileId); | |
| var result = await _profileRepository.DeleteProfileAsync(toolId, profileId); |
CliToolEnvProfilemodel (database entity with: Id, ToolId, ProfileName, IsActive, EnvVarsJson)ICliToolEnvProfileRepositoryinterface andCliToolEnvProfileRepositoryimplementationICliToolEnvironmentServiceandCliToolEnvironmentServiceGetEnvironmentVariablesAsyncto check active profile first, then fall back to default DB config, then appsettingsEnvironmentVariableConfigModal.razorwith profile management UI_newProfileNameused for save in both cases)Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.