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 { /// /// 服务端点配置(如 DeepSeek 官方、NVIDIA NIM) /// 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 Models { get; set; } = []; public double DefaultTemperature { get; set; } = 0.35; public double DefaultTopP { get; set; } = 0.95; public int DefaultMaxTokens { get; set; } = 16384; } /// /// AI 分析提示词配置。 /// public class AiPromptSettings { public bool UseCustomSystemPrompt { get; set; } public string CustomSystemPrompt { get; set; } = string.Empty; public string AdditionalRules { get; set; } = string.Empty; } /// /// 模型条目 /// 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 表示未知 /// /// 一次请求的总 token 软上限(prompt + 输出/推理余量)。 /// null 或 0 表示使用档位默认值:≥1M 上下文 → 160K;200K~256K → 100K;更小 → 0(不支持长录像)。 /// public int? ContextBudget { get; set; } public Dictionary ExtraParameters { get; set; } = []; /// /// 构建最终请求参数(合并 Provider 默认值、模型特有参数和运行时覆盖) /// public Dictionary BuildRequestParams( AiProvider provider, double? temperatureOverride = null, double? topPOverride = null, int? maxTokensOverride = null) { var parameters = new Dictionary { ["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; } } /// /// 每次请求前的动态配置上下文(当前选中的 Provider 和 Model) /// public record AiRequestContext(AiProvider Provider, AiModel Model) { public Dictionary BuildRequestParams( double? temperatureOverride = null, double? topPOverride = null, int? maxTokensOverride = null) { return Model.BuildRequestParams(Provider, temperatureOverride, topPOverride, maxTokensOverride); } } /// /// 内置已知模型信息(提供商无关,纯模型参数模板) /// 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; } /// /// 返回一组已知模型,包含正确的 ExtraParameters。 /// 调用方可按需复制到 Provider 的 Models 列表中。 /// public static List 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" } } ]; } } /// /// 从 OpenAI 兼容的 /v1/models 端点获取可用模型 ID 列表 /// public static class AiModelFetcher { public static async Task> FetchModelsAsync( string baseUrl, string apiKey) { baseUrl = baseUrl.TrimEnd('/') + "/"; var models = new List(); 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; } } /// /// 全局 AI 配置(多个 Provider)的持久化管理 /// public class AiSettings { public List Providers { get; set; } = []; public AiPromptSettings Prompt { get; set; } = new(); /// /// 上次选中的 Provider 名称(持久化;用于下次打开设置页时恢复)。 /// 按名称识别,Provider 被重命名/删除后自动回退到第一个 Provider。 /// public string? CurrentProviderName { get; set; } /// /// 上次选中的模型 ID(持久化;与 配合使用)。 /// 模型被删除后自动回退到该 Provider 的第一个模型。 /// public string? CurrentModelId { get; set; } /// /// 更新并持久化当前选中的 Provider 与模型。 /// public void SetCurrentSelection(AiProvider provider, AiModel model) { CurrentProviderName = provider.Name; CurrentModelId = model.ModelId; } /// /// 解析持久化的上次选择。任一标识缺失或对应 Provider/Model 已不存在时返回 null。 /// public (AiProvider Provider, AiModel Model)? ResolveLastSelection() { var providerName = CurrentProviderName?.Trim(); var modelId = CurrentModelId?.Trim(); if (string.IsNullOrEmpty(providerName) || string.IsNullOrEmpty(modelId)) { return null; } var provider = Providers.FirstOrDefault(p => string.Equals(p.Name, providerName, StringComparison.OrdinalIgnoreCase)); if (provider is null) { return null; } var model = provider.Models.FirstOrDefault(m => string.Equals(m.ModelId, modelId, StringComparison.OrdinalIgnoreCase)); if (model is null) { return null; } return (provider, model); } 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(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); } } }