Fix advanced paste settings migration issue (#43459)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request introduces a robust migration system for legacy
Advanced Paste AI provider settings and credentials, refactoring both
the migration logic and credential handling to be more maintainable and
reliable. It also standardizes the default model names for AI providers
and updates related UI placeholders to reflect these changes. The
migration logic is now centralized in a new helper class, and legacy
credential migration is handled more cleanly in both the settings and
view model layers.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #43456
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
This commit is contained in:
Shawn Yuan
2025-11-12 16:24:21 +08:00
committed by GitHub
parent a0a2f493c5
commit 5884375e9d
6 changed files with 518 additions and 49 deletions

View File

@@ -163,28 +163,143 @@ namespace AdvancedPaste.Settings
return false;
}
if (settings.Properties.IsAIEnabled || !LegacyOpenAIKeyExists())
var properties = settings.Properties;
var configuration = properties.PasteAIConfiguration;
if (configuration is null)
{
configuration = new PasteAIConfiguration();
properties.PasteAIConfiguration = configuration;
}
bool hasLegacyProviders = configuration.LegacyProviderConfigurations is { Count: > 0 };
bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
if (!hasLegacyProviders && legacyCredential is null && !legacyAdvancedAIConsumed)
{
return false;
}
settings.Properties.IsAIEnabled = true;
return true;
bool configurationUpdated = false;
if (hasLegacyProviders)
{
configurationUpdated |= AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(configuration);
}
PasteAIProviderDefinition openAIProvider = null;
if (legacyCredential is not null || hasLegacyProviders || legacyAdvancedAIConsumed)
{
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
openAIProvider = ensureResult.Provider;
configurationUpdated |= ensureResult.Updated;
}
if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
{
openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled;
configurationUpdated = true;
}
if (legacyCredential is not null && openAIProvider is not null)
{
StoreMigratedOpenAICredential(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password);
RemoveLegacyOpenAICredential();
}
bool enabledUpdated = false;
if (!properties.IsAIEnabled && legacyCredential is not null)
{
properties.IsAIEnabled = true;
enabledUpdated = true;
}
return configurationUpdated || enabledUpdated || legacyAdvancedAIConsumed;
}
private static bool LegacyOpenAIKeyExists()
private static PasswordCredential TryGetLegacyOpenAICredential()
{
try
{
PasswordVault vault = new();
return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null;
var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
credential?.RetrievePassword();
return credential;
}
catch (Exception)
{
return false;
return null;
}
}
private static void RemoveLegacyOpenAICredential()
{
try
{
PasswordVault vault = new();
TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
}
catch (Exception)
{
}
}
private static void StoreMigratedOpenAICredential(string providerId, string serviceType, string password)
{
if (string.IsNullOrWhiteSpace(password))
{
return;
}
try
{
var serviceKind = serviceType.ToAIServiceType();
if (serviceKind != AIServiceType.OpenAI)
{
return;
}
string resource = "https://platform.openai.com/api-keys";
string username = $"PowerToys_AdvancedPaste_PasteAI_openai_{NormalizeProviderIdentifier(providerId)}";
PasswordVault vault = new();
TryRemoveCredential(vault, resource, username);
PasswordCredential credential = new(resource, username, password);
vault.Add(credential);
}
catch (Exception ex)
{
Logger.LogError("Failed to migrate legacy OpenAI credential", ex);
}
}
private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName)
{
try
{
PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
vault.Remove(existingCred);
}
catch (Exception)
{
// Credential doesn't exist, which is fine
}
}
private static string NormalizeProviderIdentifier(string providerId)
{
if (string.IsNullOrWhiteSpace(providerId))
{
return "default";
}
var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray());
return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant();
}
public async Task SetActiveAIProviderAsync(string providerId)
{
if (string.IsNullOrWhiteSpace(providerId))