{"repo": "125w5/deepnote", "files": {"native/src/DeepNote.App/MainWindow.Settings.cs": {"f86adb1765de16a662ba7a89a436afd7ea6bcf46": {"t": 1785746218.7631025, "n": 6915, "h": "e7892938f7ea6131", "c": "using System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Runtime.InteropServices;\nusing System.Runtime.CompilerServices;\nusing System.Runtime.InteropServices.WindowsRuntime;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.Json;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing DeepNote.Core;\nusing DeepNote.Pdf;\nusing DeepNote.Storage;\nusing Microsoft.UI.Xaml;\nusing Microsoft.UI.Xaml.Controls;\nusing Microsoft.UI.Xaml.Controls.Primitives;\nusing Microsoft.UI.Xaml.Input;\nusing Microsoft.UI.Xaml.Media;\nusing Microsoft.UI.Xaml.Media.Imaging;\nusing Microsoft.UI.Xaml.Shapes;\nusing WinRT.Interop;\nusing Windows.ApplicationModel.DataTransfer;\nusing Windows.Data.Pdf;\nusing Windows.Foundation;\nusing Windows.Graphics.Imaging;\nusing Windows.Media.Ocr;\nusing Windows.Storage;\nusing Windows.Storage.FileProperties;\nusing Windows.Storage.Pickers;\nusing Windows.Storage.Streams;\nusing Windows.System;\nusing Windows.UI;\n\nnamespace DeepNote.App;\n\npublic sealed partial class MainWindow\n{\n\tprivate async void TestAiConnection_Click(object sender, RoutedEventArgs e)\n\t{\n\t\tawait TestAiConnectionAsync();\n\t}\n\n\tprivate async void SaveSettings_Click(object sender, RoutedEventArgs e)\n\t{\n\t\t_persistedState.AiProvider = GetSelectedComboText(AiProviderBox, \"OpenAI Compatible\");\n\t\t_persistedState.AiBaseUrl = AiBaseUrlBox.Text;\n\t\t_persistedState.AiModel = AiModelBox.Text;\n\t\t_persistedState.AiBudget = GetSelectedComboText(AiBudgetBox, \"低消耗\");\n\t\t_persistedState.OcrEngine = GetSelectedComboText(OcrEngineBox, \"Windows OCR\");\n\t\t_persistedState.OcrLanguage = OcrLanguageBox.Text;\n\t\t_persistedState.OcrBatchSize = OcrBatchBox.Text;\n\t\t_persistedState.AutoLoadOcrCache = AutoLoadOcrCacheBox.IsChecked == true;\n\t\t_persistedState.AutoOcrCurrentPage = AutoOcrCurrentPageBox.IsChecked == true;\n\t\t_persistedState.AutoOcrDelayMs = (int.TryParse(AutoOcrDelayBox.Text, out var delay) ? Math.Clamp(delay, 0, 5000) : 300);\n\t\t_autoLoadOcrCache = _persistedState.AutoLoadOcrCache;\n\t\t_autoOcrCurrentPage = _persistedState.AutoOcrCurrentPage;\n\t\t_autoOcrDelayMs = _persistedState.AutoOcrDelayMs;\n\t\tstring protectedApiKey = ProtectApiKey(AiApiKeyBox.Password);\n\t\tawait _database.SaveSettingAsync(\"ai\", JsonSerializer.Serialize(new\n\t\t{\n\t\t\tAiProvider = _persistedState.AiProvider,\n\t\t\tAiBaseUrl = _persistedState.AiBaseUrl,\n\t\t\tAiModel = _persistedState.AiModel,\n\t\t\tAiBudget = _persistedState.AiBudget,\n\t\t\tApiKeyProtected = protectedApiKey,\n\t\t\tApiKeyStorage = (string.IsNullOrWhiteSpace(protectedApiKey) ? \"empty\" : \"dpapi-current-user\")\n\t\t}));\n\t\tawait _database.SaveSettingAsync(\"ocr\", JsonSerializer.Serialize(new\n\t\t{\n\t\t\tOcrEngine = _persistedState.OcrEngine,\n\t\t\tOcrLanguage = _persistedState.OcrLanguage,\n\t\t\tOcrBatchSize = _persistedState.OcrBatchSize,\n\t\t\tAutoLoadOcrCache = _persistedState.AutoLoadOcrCache,\n\t\t\tAutoOcrCurrentPage = _persistedState.AutoOcrCurrentPage,\n\t\t\tAutoOcrDelayMs = _persistedState.AutoOcrDelayMs,\n\t\t\tCacheEnabled = true,\n\t\t\tMaxConcurrency = 1\n\t\t}));\n\t\tStatusText.Text = \"设置已保存到 SQLite,API Key 已使用 DPAPI 当前用户范围加密。\";\n\t\tawait SaveCurrentStateAsync();\n\t}\n\n\tprivate void ClearApiKey_Click(object sender, RoutedEventArgs e)\n\t{\n\t\tAiApiKeyBox.Password = string.Empty;\n\t\tStatusText.Text = \"API Key 已清空。\";\n\t}\n\n\tprivate void RecordShortcut_Click(object sender, RoutedEventArgs e)\n\t{\n\t\tStatusText.Text = \"快捷键重录入口已就绪。下一步会捕获按键并写入快捷键设置。\";\n\t}\n\n\tprivate void AiProviderBox_SelectionChanged(object sender, SelectionChangedEventArgs e)\n\t{\n\t\tswitch ((!(AiProviderBox.SelectedItem is ComboBoxItem { Content: var content })) ? \"\" : content?.ToString())\n\t\t{\n\t\tcase \"DeepSeek\":\n\t\t\tAiBaseUrlBox.Text = \"https://api.deepseek.com/v1\";\n\t\t\tAiModelBox.Text = \"deepseek-chat\";\n\t\t\tbreak;\n\t\tcase \"Moonshot\":\n\t\t\tAiBaseUrlBox.Text = \"https://api.moonshot.cn/v1\";\n\t\t\tAiModelBox.Text = \"moonshot-v1-8k\";\n\t\t\tbreak;\n\t\tcase \"通义千问\":\n\t\t\tAiBaseUrlBox.Text = \"https://dashscope.aliyuncs.com/compatible-mode/v1\";\n\t\t\tAiModelBox.Text = \"qwen-plus\";\n\t\t\tbreak;\n\t\tcase \"Ollama 本地\":\n\t\t\tAiBaseUrlBox.Text = \"http://localhost:11434/v1\";\n\t\t\tAiModelBox.Text = \"llama3.1\";\n\t\t\tbreak;\n\t\tcase \"OpenAI\":\n\t\t\tAiBaseUrlBox.Text = \"https://api.openai.com/v1\";\n\t\t\tAiModelBox.Text = \"gpt-4.1-mini\";\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate async Task TestAiConnectionAsync()\n\t{\n\t\tstring baseUrl = AiBaseUrlBox.Text.Trim().TrimEnd('/');\n\t\tif (string.IsNullOrWhiteSpace(baseUrl))\n\t\t{\n\t\t\tStatusText.Text = \"AI 测试失败:Base URL 不能为空。\";\n\t\t\treturn;\n\t\t}\n\t\tStatusText.Text = \"正在测试 AI 连接:\" + GetSelectedComboText(AiProviderBox, \"OpenAI Compatible\") + \" · \" + baseUrl;\n\t\ttry\n\t\t{\n\t\t\tusing HttpClient client = new HttpClient\n\t\t\t{\n\t\t\t\tTimeout = TimeSpan.FromSeconds(10L)\n\t\t\t};\n\t\t\tif (!string.IsNullOrWhiteSpace(AiApiKeyBox.Password))\n\t\t\t{\n\t\t\t\tclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", AiApiKeyBox.Password);\n\t\t\t}\n\t\t\tusing HttpResponseMessage response = await client.GetAsync(baseUrl + \"/models\");\n\t\t\tawait _database.SaveSettingAsync(\"ai.lastTest\", JsonSerializer.Serialize(new\n\t\t\t{\n\t\t\t\tBaseUrl = baseUrl,\n\t\t\t\tStatusCode = (int)response.StatusCode,\n\t\t\t\tIsSuccessStatusCode = response.IsSuccessStatusCode,\n\t\t\t\tTestedAt = DateTimeOffset.UtcNow\n\t\t\t}));\n\t\t\tStatusText.Text = (response.IsSuccessStatusCode ? \"AI 连接测试成功:/models 可访问。\" : $\"AI 连接测试失败:HTTP {response.StatusCode} {response.ReasonPhrase}\");\n\t\t}\n\t\tcatch (Exception ex)\n\t\t{\n\t\t\tawait _database.SaveSettingAsync(\"ai.lastTest\", JsonSerializer.Serialize(new\n\t\t\t{\n\t\t\t\tBaseUrl = baseUrl,\n\t\t\t\tIsSuccessStatusCode = false,\n\t\t\t\tError = ex.Message,\n\t\t\t\tTestedAt = DateTimeOffset.UtcNow\n\t\t\t}));\n\t\t\tStatusText.Text = \"AI 连接测试失败:\" + ex.Message;\n\t\t}\n\t}\n\n\tprivate static string GetSelectedComboText(ComboBox comboBox, string fallback)\n\t{\n\t\treturn (!(comboBox.SelectedItem is ComboBoxItem { Content: var content })) ? (comboBox.SelectionBoxItem?.ToString() ?? fallback) : (content?.ToString() ?? fallback);\n\t}\n\n\tprivate static string ProtectApiKey(string apiKey)\n\t{\n\t\tif (string.IsNullOrWhiteSpace(apiKey))\n\t\t{\n\t\t\treturn \"\";\n\t\t}\n\t\tbyte[] bytes = Encoding.UTF8.GetBytes(apiKey);\n\t\tbyte[] inArray = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser);\n\t\treturn Convert.ToBase64String(inArray);\n\t}\n\n\tprivate void OpenPluginFolder_Click(object sender, RoutedEventArgs e)\n\t{\n\t\tStatusText.Text = \"插件目录入口已就绪。\";\n\t}\n\n\tprivate void PluginDocs_Click(object sender, RoutedEventArgs e)\n\t{\n\t\tStatusText.Text = \"插件开发文档入口已就绪。\";\n\t}\n\n\tprivate async void SettingsButton_Click(object sender, RoutedEventArgs e)\n\t{\n\t\tawait _commands.ExecuteAsync(\"workspace.settings\");\n\t}\n\n\tprivate Task ShowSettingsAsync()\n\t{\n\t\tStatusText.Text = \"设置页入口已打开:常规、阅读器、AI、OCR、插件、快捷键、存储、诊断将逐步接入真实页面。\";\n\t\t_activeRightTab = \"ai\";\n\t\tApplyRightTabVisuals();\n\t\treturn CreateAiDraftAsync(\"AI 设置入口\", \"AI 设置将支持:OpenAI、DeepSeek、Moonshot、通义、Ollama、自定义 OpenAI Compatible;API Key 会加密保存。\");\n\t}\n}\n\n"}}}}