538 lines
20 KiB
C#
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 = "";
|
|
}
|
|
}
|
|
}
|