358 lines
12 KiB
C#
358 lines
12 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net.Http;
|
||
using System.Text.Json;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace AnotherReplayReader
|
||
{
|
||
/// <summary>
|
||
/// 服务端点配置(如 DeepSeek 官方、NVIDIA NIM)
|
||
/// </summary>
|
||
public class AiProvider
|
||
{
|
||
public string Name { get; set; } = string.Empty;
|
||
public string BaseUrl { get; set; } = string.Empty;
|
||
public string ApiKey { get; set; } = string.Empty;
|
||
public List<AiModel> Models { get; set; } = [];
|
||
|
||
public double DefaultTemperature { get; set; } = 0.35;
|
||
public double DefaultTopP { get; set; } = 0.95;
|
||
public int DefaultMaxTokens { get; set; } = 16384;
|
||
}
|
||
|
||
/// <summary>
|
||
/// AI 分析提示词配置。
|
||
/// </summary>
|
||
public class AiPromptSettings
|
||
{
|
||
public bool UseCustomSystemPrompt { get; set; }
|
||
public string CustomSystemPrompt { get; set; } = string.Empty;
|
||
public string AdditionalRules { get; set; } = string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 模型条目
|
||
/// </summary>
|
||
public class AiModel
|
||
{
|
||
public string ModelId { get; set; } = string.Empty;
|
||
public string? DisplayName { get; set; }
|
||
public bool IsStream { get; set; }
|
||
public int ContextLength { get; set; } // 0 表示未知
|
||
|
||
public Dictionary<string, object> ExtraParameters { get; set; } = [];
|
||
|
||
/// <summary>
|
||
/// 构建最终请求参数(合并 Provider 默认值、模型特有参数和运行时覆盖)
|
||
/// </summary>
|
||
public Dictionary<string, object> BuildRequestParams(
|
||
AiProvider provider,
|
||
double? temperatureOverride = null,
|
||
double? topPOverride = null,
|
||
int? maxTokensOverride = null)
|
||
{
|
||
var parameters = new Dictionary<string, object>
|
||
{
|
||
["model"] = ModelId,
|
||
["temperature"] = temperatureOverride ?? provider.DefaultTemperature,
|
||
["top_p"] = topPOverride ?? provider.DefaultTopP,
|
||
["max_tokens"] = maxTokensOverride ?? provider.DefaultMaxTokens,
|
||
["stream"] = IsStream
|
||
};
|
||
|
||
foreach (var kv in this.ExtraParameters)
|
||
{
|
||
parameters[kv.Key] = kv.Value;
|
||
}
|
||
|
||
return parameters;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每次请求前的动态配置上下文(当前选中的 Provider 和 Model)
|
||
/// </summary>
|
||
public record AiRequestContext(AiProvider Provider, AiModel Model)
|
||
{
|
||
public Dictionary<string, object> BuildRequestParams(
|
||
double? temperatureOverride = null,
|
||
double? topPOverride = null,
|
||
int? maxTokensOverride = null)
|
||
{
|
||
return Model.BuildRequestParams(Provider,
|
||
temperatureOverride, topPOverride, maxTokensOverride);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 内置已知模型信息(提供商无关,纯模型参数模板)
|
||
/// </summary>
|
||
public static class KnownModels
|
||
{
|
||
public const int SimilarityThreshold = 80;
|
||
|
||
public static int GetSimilarity(string sourceModelId, string targetModelId)
|
||
{
|
||
// prefer exact match
|
||
if (sourceModelId.Equals(targetModelId, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return 100;
|
||
}
|
||
// match the part after slash, e.g. "deepseek-ai/deepseek-v4-flash" vs "deepseek-v4-flash"
|
||
// if last part matches, return 90
|
||
var sourceModelIdLastPart = sourceModelId.Split('/').LastOrDefault() ?? sourceModelId;
|
||
var targetModelIdLastPart = targetModelId.Split('/').LastOrDefault() ?? targetModelId;
|
||
if (sourceModelIdLastPart.Equals(targetModelIdLastPart, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return 90;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回一组已知模型,包含正确的 ExtraParameters。
|
||
/// 调用方可按需复制到 Provider 的 Models 列表中。
|
||
/// </summary>
|
||
public static List<AiModel> GetAll()
|
||
{
|
||
return
|
||
[
|
||
// DeepSeek 官方
|
||
new()
|
||
{
|
||
ModelId = "deepseek-v4-flash",
|
||
DisplayName = "DeepSeek V4 Flash",
|
||
IsStream = true,
|
||
ContextLength = 1_000_000,
|
||
ExtraParameters = new()
|
||
{
|
||
["thinking"] = new { type = "enabled" },
|
||
["reasoning_effort"] = "high"
|
||
}
|
||
},
|
||
new()
|
||
{
|
||
ModelId = "deepseek-v4-pro",
|
||
DisplayName = "DeepSeek V4 Pro",
|
||
IsStream = true,
|
||
ContextLength = 1_000_000,
|
||
ExtraParameters = new()
|
||
{
|
||
["thinking"] = new { type = "enabled" },
|
||
["reasoning_effort"] = "high"
|
||
}
|
||
},
|
||
// NVIDIA NIM 上的 DeepSeek 模型
|
||
new()
|
||
{
|
||
ModelId = "deepseek-ai/deepseek-v4-flash",
|
||
DisplayName = "DeepSeek V4 Flash (NIM)",
|
||
IsStream = true,
|
||
ContextLength = 1_000_000,
|
||
ExtraParameters = new()
|
||
{
|
||
// ["chat_template_kwargs"] = new { thinking = true },
|
||
["thinking"] = new { type = "enabled" },
|
||
["reasoning_effort"] = "high",
|
||
}
|
||
},
|
||
new()
|
||
{
|
||
ModelId = "deepseek-ai/deepseek-v4-pro",
|
||
DisplayName = "DeepSeek V4 Pro (NIM)",
|
||
IsStream = true,
|
||
ContextLength = 1_000_000,
|
||
ExtraParameters = new()
|
||
{
|
||
// ["chat_template_kwargs"] = new { thinking = true },
|
||
["thinking"] = new { type = "enabled" },
|
||
["reasoning_effort"] = "high",
|
||
}
|
||
},
|
||
// NVIDIA Nemotron
|
||
new()
|
||
{
|
||
ModelId = "nvidia/nemotron-3-super-120b-a12b",
|
||
DisplayName = "Nemotron Super 120B (NIM)",
|
||
IsStream = true,
|
||
ContextLength = 1_000_000,
|
||
ExtraParameters = new()
|
||
{
|
||
["reasoning_budget"] = 16384
|
||
}
|
||
},
|
||
// Minimax
|
||
new()
|
||
{
|
||
ModelId = "minimaxai/minimax-m3",
|
||
DisplayName = "MiniMax-M3 (NIM)",
|
||
IsStream = false,
|
||
ContextLength = 1_000_000,
|
||
ExtraParameters = []
|
||
},
|
||
// Kimi
|
||
new()
|
||
{
|
||
ModelId = "moonshotai/kimi-k2.6",
|
||
DisplayName = "Kimi-K2.6 (NIM)",
|
||
IsStream = false,
|
||
ContextLength = 256_000,
|
||
ExtraParameters = []
|
||
},
|
||
// Google DiffusionGemma
|
||
new()
|
||
{
|
||
ModelId = "google/diffusiongemma-26b-a4b-it",
|
||
DisplayName = "DiffusionGemma 26B A4B IT (NIM)",
|
||
IsStream = false,
|
||
ContextLength = 250_000,
|
||
ExtraParameters = new()
|
||
{
|
||
["chat_template_kwargs"] = new { enable_thinking = true },
|
||
}
|
||
},
|
||
// OpenAI 兼容
|
||
new()
|
||
{
|
||
ModelId = "openai/gpt-oss-120b",
|
||
DisplayName = "GPT OSS 120B (NIM)",
|
||
IsStream = true,
|
||
ContextLength = 128_000,
|
||
ExtraParameters = new()
|
||
{
|
||
["reasoning_effort"] = "medium"
|
||
}
|
||
}
|
||
];
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 OpenAI 兼容的 /v1/models 端点获取可用模型 ID 列表
|
||
/// </summary>
|
||
public static class AiModelFetcher
|
||
{
|
||
public static async Task<List<string>> FetchModelsAsync(
|
||
string baseUrl, string apiKey)
|
||
{
|
||
baseUrl = baseUrl.TrimEnd('/') + "/";
|
||
|
||
var models = new List<string>();
|
||
|
||
using var client = new HttpClient();
|
||
var request = new HttpRequestMessage(
|
||
HttpMethod.Get, new Uri(new(baseUrl), "models"));
|
||
request.Headers.Add("Authorization", $"Bearer {apiKey}");
|
||
|
||
var response = await client.SendAsync(request);
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
throw new Exception(
|
||
$"获取模型列表失败: HTTP {(int)response.StatusCode}");
|
||
}
|
||
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
using var doc = JsonDocument.Parse(json);
|
||
if (doc.RootElement.TryGetProperty("data", out var dataArray))
|
||
{
|
||
foreach (var item in dataArray.EnumerateArray())
|
||
{
|
||
if (item.TryGetProperty("id", out var idProp))
|
||
{
|
||
var id = idProp.GetString();
|
||
if (!string.IsNullOrEmpty(id))
|
||
{
|
||
models.Add(id!);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return models;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 全局 AI 配置(多个 Provider)的持久化管理
|
||
/// </summary>
|
||
public class AiSettings
|
||
{
|
||
public List<AiProvider> Providers { get; set; } = [];
|
||
public AiPromptSettings Prompt { get; set; } = new();
|
||
|
||
// 以下两个不持久化,由 UI 层维护当前选中项
|
||
[System.Text.Json.Serialization.JsonIgnore]
|
||
public int CurrentProviderIndex { get; set; }
|
||
|
||
private static readonly string ConfigPath = Path.Combine(
|
||
AppContext.BaseDirectory,
|
||
"AnotherReplayReader.ai_settings.json");
|
||
|
||
public static AiSettings Load()
|
||
{
|
||
try
|
||
{
|
||
if (File.Exists(ConfigPath))
|
||
{
|
||
var json = File.ReadAllText(ConfigPath);
|
||
var settings = JsonSerializer.Deserialize<AiSettings>(json);
|
||
if (settings is { } value && value.Providers.Count > 0)
|
||
{
|
||
value.Prompt ??= new AiPromptSettings();
|
||
return settings;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 返回默认配置
|
||
Debug.Instance.DebugMessage += $"加载 AI 配置失败: {ex}\r\n";
|
||
}
|
||
|
||
// 返回默认配置:包含两个常用 Provider,各附一个内置模型
|
||
var defaults = new AiSettings();
|
||
var nimProvider = new AiProvider
|
||
{
|
||
Name = "NVIDIA NIM",
|
||
BaseUrl = "https://integrate.api.nvidia.com/v1",
|
||
ApiKey = "",
|
||
Models =
|
||
[
|
||
KnownModels.GetAll().First(m => m.ModelId == "deepseek-ai/deepseek-v4-flash")
|
||
]
|
||
};
|
||
var deepseekProvider = new AiProvider
|
||
{
|
||
Name = "DeepSeek 官方",
|
||
BaseUrl = "https://api.deepseek.com",
|
||
ApiKey = "",
|
||
Models =
|
||
[
|
||
KnownModels.GetAll().First(m => m.ModelId == "deepseek-v4-flash")
|
||
]
|
||
};
|
||
defaults.Providers.Add(nimProvider);
|
||
defaults.Providers.Add(deepseekProvider);
|
||
|
||
return defaults;
|
||
}
|
||
|
||
public void Save()
|
||
{
|
||
var dir = Path.GetDirectoryName(ConfigPath);
|
||
if (dir is not null)
|
||
{
|
||
Directory.CreateDirectory(dir);
|
||
}
|
||
|
||
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions
|
||
{
|
||
WriteIndented = true
|
||
});
|
||
File.WriteAllText(ConfigPath, json);
|
||
}
|
||
}
|
||
}
|