Files
AnotherReplayReader/AIProviderSettingsControl.xaml.cs
T
2026-09-08 18:39:04 +02:00

538 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json;
using System.Web.Routing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
namespace AnotherReplayReader
{
public partial class AIProviderSettingsControl : UserControl
{
// ---------- fields ----------
private AiSettings _settings;
private AiProvider? _currentProvider;
private AiModel? _currentModel;
// 为了下拉框显示,内部包装
private record ModelDisplayItem(AiModel Model)
{
public string DisplayText =>
$"{Model.ModelId}{(Model.ContextLength == 0 ? " (未知性能)" : "")}";
}
// ---------- constructor ----------
public AIProviderSettingsControl()
{
InitializeComponent();
_settings = AiSettings.Load();
RefreshPromptFields();
RefreshProviderList();
if (_settings.Providers.Count > 0)
{
var lastSelection = _settings.ResolveLastSelection();
if (lastSelection is { } last)
{
// 恢复上次选中的 Provider 与模型。
// OnProviderSelectionChanged 会从持久化的 CurrentModelId 恢复模型;
// 显式调用 SelectModel 作为双保险(模型仍存在则精确恢复)。
_providerListBox.SelectedItem = last.Provider;
SelectModel(last.Model);
}
else
{
_providerListBox.SelectedIndex = 0;
}
}
}
// ---------- public API ----------
public AiRequestContext? GetCurrentContext()
{
if (_currentProvider is null || _currentModel is null)
{
return null;
}
return new AiRequestContext(_currentProvider, _currentModel);
}
public AiPromptSettings GetPromptSettings()
{
return _settings.Prompt;
}
/// <summary>
/// 把当前选中的 Provider/模型保存到设置文件(供窗口关闭时调用,
/// 下次打开 AI 设置页时恢复上次选择)。
/// </summary>
public void SaveCurrentSelection()
{
if (_currentProvider is not null && _currentModel is not null)
{
_settings.SetCurrentSelection(_currentProvider, _currentModel);
_settings.Save();
}
}
private void RefreshPromptFields()
{
_useCustomPromptCheck.IsChecked = _settings.Prompt.UseCustomSystemPrompt;
_customPromptBox.Text = _settings.Prompt.CustomSystemPrompt;
_additionalRulesBox.Text = _settings.Prompt.AdditionalRules;
}
private void OnApplyPromptClick(object sender, RoutedEventArgs e)
{
_settings.Prompt.UseCustomSystemPrompt = _useCustomPromptCheck.IsChecked == true;
_settings.Prompt.CustomSystemPrompt = _customPromptBox.Text;
_settings.Prompt.AdditionalRules = _additionalRulesBox.Text;
_settings.Save();
MessageBox.Show("提示词配置已保存", "信息", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void OnResetPromptClick(object sender, RoutedEventArgs e)
{
var result = MessageBox.Show(
"确定要恢复内置 System Prompt 吗?自定义 System Prompt 会被清空,补充规则会保留。",
"恢复默认提示词",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
{
return;
}
_settings.Prompt.UseCustomSystemPrompt = false;
_settings.Prompt.CustomSystemPrompt = string.Empty;
_settings.Save();
RefreshPromptFields();
}
private void OnClearAdditionalRulesClick(object sender, RoutedEventArgs e)
{
_settings.Prompt.AdditionalRules = string.Empty;
_settings.Save();
RefreshPromptFields();
}
// ---------- Provider 列表管理 ----------
private void RefreshProviderList()
{
_providerListBox.ItemsSource = new ObservableCollection<AiProvider>(_settings.Providers);
_providerListBox.DisplayMemberPath = "Name";
}
private void OnProviderSelectionChanged(object sender, SelectionChangedEventArgs e)
{
// 记住当前模型选择,切换 Provider 后若新 Provider 存在同名模型则保持选择
var previousModelId = _settings.CurrentModelId;
_currentProvider = _providerListBox.SelectedItem as AiProvider;
if (_currentProvider is null)
{
return;
}
_providerNameBox.Text = _currentProvider.Name;
_providerUrlBox.Text = _currentProvider.BaseUrl;
_providerKeyBox.Password = _currentProvider.ApiKey;
_temperatureBox.Text = _currentProvider.DefaultTemperature.ToString();
_topPBox.Text = _currentProvider.DefaultTopP.ToString();
_maxTokensBox.Text = _currentProvider.DefaultMaxTokens.ToString();
// 记录上次选中的 Provider(模型在 OnModelSelectionChanged 中记录)
_settings.CurrentProviderName = _currentProvider.Name;
RefreshModelList();
// 尝试恢复上一个选中的模型(启动恢复与 Provider 切换共用同一路径)
if (!string.IsNullOrWhiteSpace(previousModelId))
{
var previousItem = _modelComboBox.Items
.OfType<ModelDisplayItem>()
.FirstOrDefault(m => string.Equals(
m.Model.ModelId, previousModelId, StringComparison.OrdinalIgnoreCase));
if (previousItem is not null)
{
_modelComboBox.SelectedItem = previousItem;
}
}
// 无任何模型/无可恢复目标时回退到第一个模型
if (_currentModel is null && _modelComboBox.Items.Count > 0)
{
_modelComboBox.SelectedIndex = 0;
}
}
private void OnAddProviderClick(object sender, RoutedEventArgs e)
{
var newProvider = new AiProvider
{
Name = "新服务",
BaseUrl = "https://api.openai.com/v1"
};
_settings.Providers.Add(newProvider);
_settings.Save();
RefreshProviderList();
_providerListBox.SelectedItem = newProvider;
}
private void OnRemoveProviderClick(object sender, RoutedEventArgs e)
{
if (_currentProvider is null)
{
return;
}
var result = MessageBox.Show(
$"确定要删除服务 \"{_currentProvider.Name}\" 吗?",
"确认删除", MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
{
return;
}
_settings.Providers.Remove(_currentProvider);
_settings.CurrentProviderName = null;
_settings.CurrentModelId = null;
_settings.Save();
RefreshProviderList();
if (_settings.Providers.Count > 0)
{
_providerListBox.SelectedIndex = 0;
}
else
{
_currentProvider = null;
_currentModel = null;
ClearProviderFields();
}
}
private void OnApplyProviderClick(object sender, RoutedEventArgs e)
{
if (_currentProvider is null)
{
return;
}
_currentProvider.Name = _providerNameBox.Text;
_currentProvider.BaseUrl = _providerUrlBox.Text;
_currentProvider.ApiKey = _providerKeyBox.Password;
double.TryParse(_temperatureBox.Text, out double temp);
_currentProvider.DefaultTemperature = temp;
double.TryParse(_topPBox.Text, out double topP);
_currentProvider.DefaultTopP = topP;
int.TryParse(_maxTokensBox.Text, out int maxTokens);
_currentProvider.DefaultMaxTokens = maxTokens;
_settings.CurrentProviderName = _currentProvider.Name;
_settings.Save();
RefreshProviderList();
_providerListBox.SelectedItem = _currentProvider;
MessageBox.Show("服务配置已保存", "信息", MessageBoxButton.OK,
MessageBoxImage.Information);
}
private void ClearProviderFields()
{
_providerNameBox.Text = "";
_providerUrlBox.Text = "";
_providerKeyBox.Password = "";
_temperatureBox.Text = "";
_topPBox.Text = "";
_maxTokensBox.Text = "";
_modelComboBox.ItemsSource = null;
}
// ---------- 模型管理 ----------
private void RefreshModelList()
{
if (_currentProvider is null)
{
_modelComboBox.ItemsSource = null;
return;
}
var items = _currentProvider.Models
.Select(m => new ModelDisplayItem(Model: m ))
.ToList();
_modelComboBox.ItemsSource = new ObservableCollection<ModelDisplayItem>(items);
if (items.Count > 0)
{
_modelComboBox.SelectedIndex = 0;
}
else
{
// 当前 Provider 没有任何模型:清空当前模型,避免残留上一个 Provider 的模型
_currentModel = null;
_settings.CurrentModelId = null;
ClearModelFields();
}
}
/// <summary>
/// 在模型下拉框中选中指定模型;找不到时回退到第一个模型。
/// 仅供启动恢复使用(避免刷新列表后触发首次默认选中)。
/// </summary>
private void SelectModel(AiModel model)
{
var item = _modelComboBox.Items
.OfType<ModelDisplayItem>()
.FirstOrDefault(m => string.Equals(
m.Model.ModelId, model.ModelId, StringComparison.OrdinalIgnoreCase));
if (item is not null)
{
_modelComboBox.SelectedItem = item;
}
else if (_modelComboBox.Items.Count > 0)
{
_modelComboBox.SelectedIndex = 0;
}
}
private void OnModelSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selected = _modelComboBox.SelectedItem as ModelDisplayItem;
_currentModel = selected?.Model;
if (_currentModel is null)
{
_settings.CurrentModelId = null;
ClearModelFields();
return;
}
_settings.CurrentModelId = _currentModel.ModelId;
_modelIdBox.Text = _currentModel.ModelId;
_contextLengthBox.Text = _currentModel.ContextLength.ToString();
_contextBudgetBox.Text = _currentModel.ContextBudget is { } budget ? budget.ToString() : "0";
_supportsSseCheck.IsChecked = _currentModel.IsStream;
// 显示 ExtraParameters 为缩进 JSON
var json = JsonSerializer.Serialize(
_currentModel.ExtraParameters,
new JsonSerializerOptions { WriteIndented = true });
_extraParamsBox.Text = json;
}
private void OnFetchModelsClick(object sender, RoutedEventArgs e)
{
if (_currentProvider is null)
{
MessageBox.Show("请先选择一个服务");
return;
}
if (string.IsNullOrEmpty(_currentProvider.BaseUrl))
{
MessageBox.Show("请先填写 Base URL");
return;
}
if (string.IsNullOrEmpty(_currentProvider.ApiKey))
{
MessageBox.Show("请先填写 API Key");
return;
}
// 异步获取
Dispatcher.Invoke(async () =>
{
try
{
_modelStatusText.Text = "正在获取模型列表...";
var ids = await AiModelFetcher.FetchModelsAsync(
_currentProvider!.BaseUrl,
_currentProvider.ApiKey);
// 将获取的 ID 与现有模型合并,新 ID 若不存在则添加
var knownModels = KnownModels.GetAll();
foreach (var id in ids)
{
if (_currentProvider.Models.All(m => m.ModelId != id))
{
var known = knownModels
.OrderBy(k => KnownModels.GetSimilarity(k.ModelId, id))
.FirstOrDefault();
if (known != null && KnownModels.GetSimilarity(known.ModelId, id) >= KnownModels.SimilarityThreshold)
{
_currentProvider.Models.Add(new AiModel
{
ModelId = id,
DisplayName = id,
ContextLength = known.ContextLength,
ContextBudget = known.ContextBudget,
IsStream = known.IsStream,
ExtraParameters = new Dictionary<string, object>(known.ExtraParameters)
});
}
else
{
_currentProvider.Models.Add(new AiModel
{
ModelId = id,
ContextLength = 0,
IsStream = false
});
}
}
}
_settings.Save();
RefreshModelList();
// 保持上次选中的模型(若仍存在),否则回退到第一个
if (_settings.CurrentModelId is { } lastModelId)
{
var lastItem = _modelComboBox.Items
.OfType<ModelDisplayItem>()
.FirstOrDefault(m => string.Equals(
m.Model.ModelId, lastModelId, StringComparison.OrdinalIgnoreCase));
if (lastItem is not null)
{
_modelComboBox.SelectedItem = lastItem;
}
}
_modelStatusText.Text = $"获取成功,共 {ids.Count} 个模型";
}
catch (Exception ex)
{
_modelStatusText.Text = $"获取失败: {ex.Message}";
}
});
}
private void OnAddCustomModelClick(object sender, RoutedEventArgs e)
{
if (_currentProvider is null)
{
return;
}
var newModel = new AiModel
{
ModelId = "custom-model-id",
ContextLength = 0,
IsStream = false
};
_currentProvider.Models.Add(newModel);
_settings.Save();
RefreshModelList();
_modelComboBox.SelectedItem = _modelComboBox.Items
.OfType<ModelDisplayItem>()
.Last();
}
private void OnRemoveModelClick(object sender, RoutedEventArgs e)
{
if (_currentProvider is null || _currentModel is null)
{
return;
}
var result = MessageBox.Show(
$"确定要删除模型 \"{_currentModel.ModelId}\" 吗?",
"确认删除", MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
{
return;
}
_currentProvider.Models.Remove(_currentModel);
_settings.Save();
// 被删除模型不再是上次选择;RefreshModelList 会回退到第一个模型
_settings.CurrentModelId = null;
RefreshModelList();
}
private void OnApplyModelClick(object sender, RoutedEventArgs e)
{
if (_currentModel is null)
{
return;
}
_currentModel.ModelId = _modelIdBox.Text;
if (int.TryParse(_contextLengthBox.Text, out int ctxLen))
{
_currentModel.ContextLength = ctxLen;
}
if (int.TryParse(_contextBudgetBox.Text, out int budget) && budget > 0)
{
_currentModel.ContextBudget = budget;
}
else
{
_currentModel.ContextBudget = null;
}
_currentModel.IsStream = _supportsSseCheck.IsChecked == true;
try
{
var dict = JsonSerializer.Deserialize<Dictionary<string, object>>(
_extraParamsBox.Text);
if (dict is not null)
{
_currentModel.ExtraParameters = dict;
}
}
catch
{
MessageBox.Show("高级参数格式错误,应为 JSON 键值对",
"错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
_settings.Save();
// 模型 ID 已修改:更新持久化选择,避免下一次启动仍按旧 ID 解析
_settings.CurrentModelId = _currentModel.ModelId;
RefreshModelList();
_modelComboBox.SelectedItem = _modelComboBox.Items
.OfType<ModelDisplayItem>()
.FirstOrDefault(m => string.Equals(
m.Model.ModelId, _currentModel.ModelId, StringComparison.OrdinalIgnoreCase));
MessageBox.Show("模型修改已保存", "信息", MessageBoxButton.OK);
}
private void OnFillFromKnownModelsClick(object sender, RoutedEventArgs e)
{
if (_currentProvider is null || _currentModel is null)
{
return;
}
var known = KnownModels.GetAll()
.OrderBy(k => KnownModels.GetSimilarity(k.ModelId, _currentModel.ModelId))
.FirstOrDefault();
if (known is not null
&& KnownModels.GetSimilarity(known.ModelId, _currentModel.ModelId) >= KnownModels.SimilarityThreshold)
{
_currentModel.ExtraParameters = new Dictionary<string, object>(known.ExtraParameters);
_currentModel.IsStream = known.IsStream;
_currentModel.ContextLength = known.ContextLength;
_currentModel.ContextBudget = known.ContextBudget;
RefreshModelList();
_modelComboBox.SelectedItem = _modelComboBox.Items
.OfType<ModelDisplayItem>()
.FirstOrDefault(m => string.Equals(
m.Model.ModelId, _currentModel.ModelId, StringComparison.OrdinalIgnoreCase));
MessageBox.Show("已从已知模板填充", "信息", MessageBoxButton.OK);
}
else
{
MessageBox.Show("未找到已知模板", "信息", MessageBoxButton.OK);
}
}
private void ClearModelFields()
{
_modelIdBox.Text = "";
_contextLengthBox.Text = "";
_contextBudgetBox.Text = "";
_supportsSseCheck.IsChecked = false;
_extraParamsBox.Text = "";
}
}
}