using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TableMagic
{
public partial class Form8 : Form
{
private bool _isCloudConnection = true;
private CancellationTokenSource _modelFetchCts = null;
private System.Windows.Forms.Timer _debounceTimer = null;
public Form8()
{
InitializeComponent();
pbxPassword.BackgroundImage = Properties.Resources.eye_hide;
txbKey.UseSystemPasswordChar = true;
InitializeLLMOptions();
_debounceTimer = new System.Windows.Forms.Timer();
_debounceTimer.Interval = 1000;
_debounceTimer.Tick += DebounceTimer_Tick;
cbxLLM.SelectedIndexChanged += CbxLLM_SelectedIndexChanged;
}
private void InitializeLLMOptions()
{
cbxLLM.Items.Clear();
cbxLLM.Items.AddRange(new string[]
{
"DeepSeek",
"Qwen",
"Kimi",
"GLM",
"MinMax",
"OpenRoute",
"Claude Code",
"Gemini",
"OpenAI",
"Ollama",
"vLLM",
"LM Studio",
"llama.cpp",
"自定义"
});
cbxLLM.SelectedIndex = 0;
}
private void CbxLLM_SelectedIndexChanged(object sender, EventArgs e)
{
if (cbxLLM == null) return;
string provider = cbxLLM.Text;
string defaultUrl = GetDefaultApiUrl(provider);
txbUrl.Text = defaultUrl;
txbUrl.ReadOnly = provider != "自定义";
cbxModel.Items.Clear();
cbxModel.Text = "";
if (provider != "自定义")
{
string apiKey = txbKey.Text ?? "";
string apiUrl = txbUrl.Text;
Task.Run(() => FetchModelsAsync(provider, apiKey, apiUrl));
}
}
private string GetDefaultApiUrl(string provider)
{
switch (provider)
{
case "DeepSeek":
return "https://api.deepseek.com/v1/chat/completions";
case "Qwen":
return "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
case "Kimi":
return "https://api.moonshot.cn/v1/chat/completions";
case "GLM":
return "https://open.bigmodel.cn/api/paas/v4/chat/completions";
case "MinMax":
return "https://api.minimax.chat/v1/text/chatcompletion_pro";
case "OpenRoute":
return "https://openrouter.ai/api/v1/chat/completions";
case "Claude Code":
return "https://api.anthropic.com/v1/messages";
case "Gemini":
return "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent";
case "OpenAI":
return "https://api.openai.com/v1/chat/completions";
case "Ollama":
return "http://localhost:11434/v1/chat/completions";
case "vLLM":
return "http://localhost:8000/v1/chat/completions";
case "LM Studio":
return "http://localhost:1234/v1/chat/completions";
case "llama.cpp":
return "http://localhost:8080/v1/chat/completions";
case "自定义":
return "";
default:
return "";
}
}
private void Form8_Load(object sender, EventArgs e)
{
DecodeConfig();
txbKey.TextChanged += TxbKey_TextChanged;
}
private void TxbKey_TextChanged(object sender, EventArgs e)
{
_debounceTimer.Stop();
_debounceTimer.Start();
}
private void DebounceTimer_Tick(object sender, EventArgs e)
{
_debounceTimer.Stop();
string provider = cbxLLM.Text;
string apiKey = txbKey.Text;
string apiUrl = txbUrl.Text;
Task.Run(() => FetchModelsAsync(provider, apiKey, apiUrl));
}
private async Task FetchModelsAsync(string provider, string apiKey, string apiUrl)
{
string[] localProviders = { "Ollama", "vLLM", "LM Studio", "llama.cpp" };
bool isLocalProvider = localProviders.Contains(provider);
if (provider == "自定义" || (!isLocalProvider && string.IsNullOrEmpty(apiKey)) || string.IsNullOrEmpty(apiUrl))
return;
if (_modelFetchCts != null)
{
_modelFetchCts.Cancel();
_modelFetchCts.Dispose();
}
_modelFetchCts = new CancellationTokenSource();
CancellationToken token = _modelFetchCts.Token;
try
{
this.Invoke(new Action(() =>
{
lblResult.Text = "正在获取模型列表...";
cbxModel.Items.Clear();
cbxModel.Text = "";
}));
var (models, errorType, errorMessage) = await GetModelsAsync(provider, apiKey, apiUrl, token);
if (token.IsCancellationRequested)
return;
this.Invoke(new Action(() =>
{
if (models.Count > 0)
{
cbxModel.Items.AddRange(models.ToArray());
cbxModel.SelectedIndex = 0;
lblResult.Text = $"成功获取 {models.Count} 个模型";
}
else if (!string.IsNullOrEmpty(errorType))
{
switch (errorType)
{
case "api_key_error":
lblResult.Text = "需要填写正确的API Key后才能选择模型";
break;
case "network_error":
case "timeout":
lblResult.Text = "API地址无法访问,请检查API地址是否正确";
break;
case "endpoint_not_found":
lblResult.Text = "API地址不正确,请检查API地址";
break;
default:
lblResult.Text = errorMessage ?? "未获取到模型列表";
break;
}
}
else
{
lblResult.Text = "未获取到模型列表";
}
}));
}
catch (Exception ex)
{
if (!token.IsCancellationRequested)
{
this.Invoke(new Action(() =>
{
lblResult.Text = $"获取模型列表失败: {ex.Message}";
}));
}
}
finally
{
if (_modelFetchCts != null)
{
_modelFetchCts.Dispose();
_modelFetchCts = null;
}
}
}
private async Task<(List<string> models, string errorType, string errorMessage)> GetModelsAsync(string provider, string apiKey, string apiUrl, CancellationToken token)
{
List<string> models = new List<string>();
string errorType = null;
string errorMessage = null;
try
{
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(15);
if (provider != "Ollama" && provider != "vLLM" && provider != "LM Studio" && provider != "llama.cpp")
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
}
string modelsEndpoint = GetModelsEndpoint(provider, apiUrl);
if (string.IsNullOrEmpty(modelsEndpoint))
{
System.Diagnostics.Debug.WriteLine($"[模型获取] 未找到模型端点,provider: {provider}");
errorType = "endpoint_not_found";
errorMessage = "未找到模型列表API端点";
return (models, errorType, errorMessage);
}
System.Diagnostics.Debug.WriteLine($"[模型获取] 请求端点: {modelsEndpoint}");
var response = await client.GetAsync(modelsEndpoint, token);
System.Diagnostics.Debug.WriteLine($"[模型获取] 响应状态码: {response.StatusCode}");
if (!response.IsSuccessStatusCode)
{
string errorContent = await response.Content.ReadAsStringAsync();
System.Diagnostics.Debug.WriteLine($"[模型获取] 请求失败: {response.StatusCode}, 错误: {errorContent}");
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
errorContent.Contains("api_key") || errorContent.Contains("apiKey") ||
errorContent.Contains("invalid") || errorContent.Contains("unauthorized") ||
errorContent.Contains("authentication") || errorContent.Contains("Authorization"))
{
errorType = "api_key_error";
errorMessage = "API Key 无效或未填写,请检查API Key是否正确";
}
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
errorType = "endpoint_not_found";
errorMessage = "API地址不正确,请检查API地址";
}
else
{
errorType = "api_error";
errorMessage = $"API错误: {response.StatusCode}";
}
return (models, errorType, errorMessage);
}
string responseContent = await response.Content.ReadAsStringAsync();
System.Diagnostics.Debug.WriteLine($"[模型获取] 响应内容: {responseContent.Substring(0, Math.Min(500, responseContent.Length))}...");
models = ParseModelsResponse(provider, responseContent);
System.Diagnostics.Debug.WriteLine($"[模型获取] 解析到 {models.Count} 个模型");
}
}
catch (HttpRequestException ex)
{
System.Diagnostics.Debug.WriteLine($"[模型获取] 网络异常: {ex.Message}");
errorType = "network_error";
errorMessage = "无法连接到API地址,请检查网络连接和API地址是否正确";
}
catch (TaskCanceledException)
{
System.Diagnostics.Debug.WriteLine($"[模型获取] 请求超时");
errorType = "timeout";
errorMessage = "请求超时,请检查API地址是否正确或网络是否畅通";
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[模型获取] 异常: {ex.GetType().Name} - {ex.Message}");
if (ex.InnerException != null)
{
System.Diagnostics.Debug.WriteLine($"[模型获取] 内部异常: {ex.InnerException.Message}");
}
errorType = "unknown_error";
errorMessage = $"未知错误: {ex.Message}";
}
return (models, errorType, errorMessage);
}
private string GetModelsEndpoint(string provider, string apiUrl)
{
switch (provider)
{
case "DeepSeek":
return "https://api.deepseek.com/v1/models";
case "Qwen":
return "https://dashscope.aliyuncs.com/compatible-mode/v1/models";
case "Kimi":
return "https://api.moonshot.cn/v1/models";
case "OpenAI":
return "https://api.openai.com/v1/models";
case "OpenRoute":
return "https://openrouter.ai/api/v1/models";
case "GLM":
return "https://open.bigmodel.cn/api/paas/v4/models";
case "MinMax":
return "https://api.minimax.chat/v1/models";
case "Claude Code":
return "https://api.anthropic.com/v1/models";
case "Gemini":
return "https://generativelanguage.googleapis.com/v1beta/models";
case "Ollama":
return "http://localhost:11434/api/tags";
case "vLLM":
case "LM Studio":
case "llama.cpp":
return apiUrl.Replace("/chat/completions", "/models");
default:
return "";
}
}
private List<string> ParseModelsResponse(string provider, string responseContent)
{
List<string> models = new List<string>();
try
{
using (var jsonDoc = System.Text.Json.JsonDocument.Parse(responseContent))
{
var root = jsonDoc.RootElement;
switch (provider)
{
case "DeepSeek":
case "Qwen":
case "Kimi":
case "OpenAI":
case "OpenRoute":
case "GLM":
case "vLLM":
case "LM Studio":
case "llama.cpp":
if (root.TryGetProperty("data", out var dataArray) && dataArray.ValueKind == JsonValueKind.Array)
{
foreach (var item in dataArray.EnumerateArray())
{
if (item.TryGetProperty("id", out var idProp))
{
models.Add(idProp.GetString());
}
}
}
break;
case "MinMax":
if (root.TryGetProperty("models", out var minMaxModels) && minMaxModels.ValueKind == JsonValueKind.Array)
{
foreach (var item in minMaxModels.EnumerateArray())
{
if (item.TryGetProperty("model", out var modelProp))
{
models.Add(modelProp.GetString());
}
else if (item.TryGetProperty("name", out var nameProp))
{
models.Add(nameProp.GetString());
}
}
}
break;
case "Ollama":
System.Diagnostics.Debug.WriteLine($"[模型获取] Ollama响应内容: {responseContent}");
if (root.TryGetProperty("models", out var ollamaModels) && ollamaModels.ValueKind == JsonValueKind.Array)
{
foreach (var item in ollamaModels.EnumerateArray())
{
if (item.TryGetProperty("name", out var nameProp))
{
models.Add(nameProp.GetString());
}
else if (item.TryGetProperty("model", out var modelProp))
{
models.Add(modelProp.GetString());
}
}
}
System.Diagnostics.Debug.WriteLine($"[模型获取] Ollama解析结果: {models.Count} 个模型");
break;
default:
if (root.TryGetProperty("data", out var genericData) && genericData.ValueKind == JsonValueKind.Array)
{
foreach (var item in genericData.EnumerateArray())
{
if (item.TryGetProperty("id", out var idProp))
models.Add(idProp.GetString());
else if (item.TryGetProperty("name", out var nameProp))
models.Add(nameProp.GetString());
else if (item.TryGetProperty("model", out var modelProp))
models.Add(modelProp.GetString());
}
}
else if (root.TryGetProperty("models", out var genericModels) && genericModels.ValueKind == JsonValueKind.Array)
{
foreach (var item in genericModels.EnumerateArray())
{
if (item.TryGetProperty("name", out var nameProp))
models.Add(nameProp.GetString());
else if (item.TryGetProperty("model", out var modelProp))
models.Add(modelProp.GetString());
else if (item.TryGetProperty("id", out var idProp))
models.Add(idProp.GetString());
}
}
break;
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[模型获取] 解析异常: {ex.Message}");
}
return models;
}
public class OpenAIModelsResponse
{
public List<OpenAIModel> data { get; set; }
}
public class OpenAIModel
{
public string id { get; set; }
public string type { get; set; }
public int created { get; set; }
public string owned_by { get; set; }
}
public class MinMaxModelsResponse
{
public List<MinMaxModel> models { get; set; }
}
public class MinMaxModel
{
public string model { get; set; }
public string name { get; set; }
public string description { get; set; }
}
public class OllamaModelsResponse
{
public List<OllamaModel> models { get; set; }
}
public class OllamaModel
{
public string name { get; set; }
public string model { get; set; }
public OllamaModelDetails details { get; set; }
}
public class OllamaModelDetails
{
public string parent_model { get; set; }
public string format { get; set; }
public string family { get; set; }
public List<string> families { get; set; }
public string parameter_size { get; set; }
public string quantization_level { get; set; }
}
private void tabControl1_DrawItem(object sender, DrawItemEventArgs e)
{
SolidBrush _Brush = new SolidBrush(Color.Black);
RectangleF _TabTextArea = (RectangleF)tabControl1.GetTabRect(e.Index);
StringFormat _sf = new StringFormat();
_sf.LineAlignment = StringAlignment.Center;
_sf.Alignment = StringAlignment.Center;
e.Graphics.DrawString(tabControl1.Controls[e.Index].Text, SystemInformation.MenuFont, _Brush, _TabTextArea, _sf);
}
private async Task<string> GetAIResponse(string apiKey, string model)
{
string apiUrl = txbUrl.Text;
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
var requestBody = new
{
model = model,
messages = new[]
{
new { role = "user", content = "Hello!" }
}
};
try
{
var response = await client.PostAsJsonAsync(apiUrl, requestBody);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return "连接成功";
}
else
{
return $"测试失败,错误代码:{response.StatusCode},错误信息:{responseContent}";
}
}
catch (HttpRequestException ex)
{
return $"测试失败,发生异常:{ex.Message}";
}
}
}
private async void btnTest_Click(object sender, EventArgs e)
{
string provider = cbxLLM.Text;
lblResult.Text = "开始检测......";
txbKey.ReadOnly = true;
cbxModel.Enabled = false;
cbxEnterKey.Enabled = false;
btnTest.Enabled = false;
btnSave.Enabled = false;
btnQuit.Enabled = false;
this.Refresh();
string result;
if (!_isCloudConnection || provider == "Ollama" || provider == "vLLM" || provider == "LM Studio" || provider == "llama.cpp")
{
string apiUrl = txbUrl.Text;
string modelsUrl = apiUrl;
if (apiUrl.EndsWith("/v1/chat/completions"))
{
modelsUrl = apiUrl.Replace("/v1/chat/completions", "/v1/models");
}
else if (apiUrl.EndsWith("/chat/completions"))
{
modelsUrl = apiUrl.Replace("/chat/completions", "/models");
}
else if (provider == "Ollama")
{
modelsUrl = "http://localhost:11434/api/tags";
}
try
{
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(10);
var response = await client.GetAsync(modelsUrl);
if (response.IsSuccessStatusCode)
{
result = "本地连接成功";
}
else
{
result = $"本地连接测试失败,状态码: {response.StatusCode}";
}
}
}
catch (Exception ex)
{
result = $"本地连接测试异常: {ex.Message}";
}
}
else
{
string model = cbxModel.Text;
if (string.IsNullOrEmpty(model))
{
lblResult.Text = "请选择模型";
txbKey.ReadOnly = false;
btnTest.Enabled = true;
cbxModel.Enabled = true;
cbxEnterKey.Enabled = true;
btnSave.Enabled = true;
btnQuit.Enabled = true;
return;
}
result = await TestCloudConnection(txbKey.Text, model, provider);
}
if (!string.IsNullOrEmpty(result))
{
lblResult.Text = result;
}
txbKey.ReadOnly = false;
btnTest.Enabled = true;
cbxModel.Enabled = true;
cbxEnterKey.Enabled = true;
btnSave.Enabled = true;
btnQuit.Enabled = true;
}
private async Task<string> TestCloudConnection(string apiKey, string model, string provider)
{
string apiUrl = txbUrl.Text;
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
var requestBody = GetRequestBodyForProvider(provider, model);
try
{
var response = await client.PostAsJsonAsync(apiUrl, requestBody);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
return "连接成功";
}
else
{
return $"测试失败,错误代码:{response.StatusCode},错误信息:{responseContent}";
}
}
catch (HttpRequestException ex)
{
return $"测试失败,发生异常:{ex.Message}";
}
}
}
private object GetRequestBodyForProvider(string provider, string model)
{
switch (provider)
{
case "Claude Code":
return new
{
model = model,
messages = new[]
{
new { role = "user", content = "Hello, Claude!" }
},
max_tokens = 100
};
case "Gemini":
return new
{
contents = new[]
{
new {
parts = new[]
{
new { text = "Hello, Gemini!" }
}
}
}
};
case "DeepSeek":
default:
return new
{
model = model,
messages = new[]
{
new { role = "user", content = "Hello!" }
},
max_tokens = 100
};
}
}
private const string KeyFilePath = "encryption.key";
private const string ConfigFilePath = "config.encrypted";
private void btnSave_Click(object sender, EventArgs e)
{
string provider = cbxLLM.Text;
string apiKey = txbKey.Text.Trim();
string model = cbxModel.Text;
string apiUrl = txbUrl.Text.Trim();
string enterMode = "0";
switch (cbxEnterKey.SelectedIndex)
{
case 0:
enterMode = "0";
break;
case 1:
enterMode = "1";
break;
case 2:
enterMode = "2";
break;
}
string connectionType = _isCloudConnection ? "cloud" : "local";
string isCloudModel = _isCloudConnection ? "true" : "false";
if (string.IsNullOrEmpty(apiKey) && _isCloudConnection && provider != "Ollama" && provider != "vLLM" && provider != "LM Studio" && provider != "llama.cpp")
{
lblResult.Text = "API KEY不能为空";
return;
}
if (string.IsNullOrEmpty(apiUrl))
{
lblResult.Text = "API地址不能为空";
return;
}
if (string.IsNullOrEmpty(model))
{
lblResult.Text = "请选择模型";
return;
}
string content = $"api-key^{apiKey};model^{model};api-url^{apiUrl};enter-mode^{enterMode};connection-type^{connectionType};is-cloud-model^{isCloudModel};timeout-minutes^{txbWaitingTime.Text.Trim()};provider^{provider}";
try
{
(byte[] key, byte[] iv) = GetOrCreateEncryptionKey();
string encryptedContent = EncryptString(content, key, iv);
File.WriteAllText(ConfigFilePath, encryptedContent);
lblResult.Text = "保存成功";
this.Refresh();
Thread.Sleep(1000);
this.Dispose();
}
catch (Exception ex)
{
lblResult.Text = $"发生错误:{ex.Message}";
}
}
private void DecodeConfig()
{
if (!File.Exists(ConfigFilePath))
{
lblResult.Text = "未初始化配置";
txbKey.Text = "";
cbxEnterKey.SelectedIndex = 0;
txbWaitingTime.Text = "5";
return;
}
try
{
(byte[] key, byte[] iv) = GetEncryptionKey();
string encryptedContent = File.ReadAllText(ConfigFilePath);
string decryptedContent = DecryptString(encryptedContent, key, iv);
var parts = decryptedContent.Split(';');
string apiKey = parts[0].Split('^')[1];
string model = parts[1].Split('^')[1];
string apiUrl = parts[2].Split('^')[1];
string enterMode = parts[3].Split('^')[1];
string connectionType = "cloud";
if (parts.Length >= 5)
{
connectionType = parts[4].Split('^')[1];
}
string provider = "Qwen";
if (parts.Length >= 8)
{
provider = parts[7].Split('^')[1];
}
_isCloudConnection = (connectionType == "cloud");
txbKey.Text = apiKey;
txbUrl.Text = apiUrl;
int providerIndex = cbxLLM.FindStringExact(provider);
if (providerIndex >= 0)
{
cbxLLM.SelectedIndex = providerIndex;
}
else
{
cbxLLM.SelectedIndex = 0;
}
cbxModel.Text = model;
switch (enterMode)
{
case "0":
cbxEnterKey.SelectedIndex = 0;
break;
case "1":
cbxEnterKey.SelectedIndex = 1;
break;
case "2":
cbxEnterKey.SelectedIndex = 2;
break;
}
if (parts.Length >= 7)
{
string timeoutMinutes = parts[6].Split('^')[1];
if (!string.IsNullOrEmpty(timeoutMinutes))
{
txbWaitingTime.Text = timeoutMinutes;
}
else
{
txbWaitingTime.Text = "5";
}
}
else
{
txbWaitingTime.Text = "5";
}
if (!string.IsNullOrEmpty(apiKey))
{
Task.Run(() => FetchModelsAsync(provider, apiKey, apiUrl));
}
}
catch (Exception ex)
{
lblResult.Text = $"发生错误:{ex.Message}";
}
}
private void btnQuit_Click(object sender, EventArgs e)
{
this.Dispose();
}
private (byte[], byte[]) GetOrCreateEncryptionKey()
{
if (File.Exists(KeyFilePath))
{
string[] lines = File.ReadAllLines(KeyFilePath);
return (Convert.FromBase64String(lines[0]), Convert.FromBase64String(lines[1]));
}
else
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.GenerateKey();
aesAlg.GenerateIV();
byte[] key = aesAlg.Key;
byte[] iv = aesAlg.IV;
File.WriteAllLines(KeyFilePath, new[] { Convert.ToBase64String(key), Convert.ToBase64String(iv) });
return (key, iv);
}
}
}
private (byte[], byte[]) GetEncryptionKey()
{
if (!File.Exists(KeyFilePath))
{
throw new FileNotFoundException("密钥文件不存在,请先进行加密操作。");
}
string[] lines = File.ReadAllLines(KeyFilePath);
return (Convert.FromBase64String(lines[0]), Convert.FromBase64String(lines[1]));
}
private string EncryptString(string plainText, byte[] key, byte[] iv)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = key;
aesAlg.IV = iv;
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
byte[] encryptedBytes = encryptor.TransformFinalBlock(plainTextBytes, 0, plainTextBytes.Length);
return Convert.ToBase64String(encryptedBytes);
}
}
private string DecryptString(string cipherText, byte[] key, byte[] iv)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = key;
aesAlg.IV = iv;
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
byte[] cipherTextBytes = Convert.FromBase64String(cipherText);
byte[] decryptedBytes = decryptor.TransformFinalBlock(cipherTextBytes, 0, cipherTextBytes.Length);
return Encoding.UTF8.GetString(decryptedBytes);
}
}
private void btnClear_Click(object sender, EventArgs e)
{
if (File.Exists("encryption.key")) File.Delete("encryption.key");
if (File.Exists("config.encrypted")) File.Delete("config.encrypted");
txbKey.Text = "";
cbxModel.SelectedIndex = 0;
cbxEnterKey.SelectedIndex = 0;
lblResult.Text = "";
txbUrl.Text = "";
txbUrl.ReadOnly = false;
_isCloudConnection = false;
txbKey.Enabled = false;
}
private void pbxPassword_Click(object sender, EventArgs e)
{
if (txbKey.UseSystemPasswordChar)
{
pbxPassword.BackgroundImage = Properties.Resources.eye_open;
txbKey.UseSystemPasswordChar = false;
}
else
{
pbxPassword.BackgroundImage = Properties.Resources.eye_hide;
txbKey.UseSystemPasswordChar = true;
}
}
private bool IsLocalApiUrl(string url)
{
try
{
Uri uri = new Uri(url);
string host = uri.Host.ToLower();
if (host == "localhost" || host == "127.0.0.1")
return true;
if (host.StartsWith("192.168."))
return true;
if (host.StartsWith("10."))
return true;
string[] parts = host.Split('.');
if (parts.Length == 4 && parts[0] == "172")
{
if (int.TryParse(parts[1], out int second))
{
if (second >= 16 && second <= 31)
return true;
}
}
return false;
}
catch
{
return false;
}
}
private async Task<string[]> GetLocalModels(string apiUrl)
{
try
{
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(10);
string modelsUrl;
if (apiUrl.Contains("/v1/chat/completions"))
{
modelsUrl = apiUrl.Replace("/v1/chat/completions", "/v1/models");
}
else if (apiUrl.Contains("/chat/completions"))
{
modelsUrl = apiUrl.Replace("/chat/completions", "/models");
}
else
{
Uri baseUri = new Uri(apiUrl);
string baseUrl = $"{baseUri.Scheme}://{baseUri.Authority}";
modelsUrl = baseUrl.TrimEnd('/') + "/v1/models";
}
System.Diagnostics.Debug.WriteLine($"原始API URL: {apiUrl}");
System.Diagnostics.Debug.WriteLine($"获取模型列表URL: {modelsUrl}");
var response = await client.GetAsync(modelsUrl);
System.Diagnostics.Debug.WriteLine($"响应状态码: {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
System.Diagnostics.Debug.WriteLine($"响应内容: {content.Substring(0, Math.Min(500, content.Length))}...");
var models = new System.Collections.Generic.List<string>();
using (var doc = System.Text.Json.JsonDocument.Parse(content))
{
if (doc.RootElement.TryGetProperty("data", out var data))
{
foreach (var model in data.EnumerateArray())
{
if (model.TryGetProperty("id", out var id))
{
models.Add(id.GetString());
}
}
}
else if (doc.RootElement.TryGetProperty("models", out var ollamaModels))
{
foreach (var model in ollamaModels.EnumerateArray())
{
if (model.TryGetProperty("name", out var name))
{
models.Add(name.GetString());
}
else if (model.TryGetProperty("model", out var modelName))
{
models.Add(modelName.GetString());
}
}
}
else if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array)
{
foreach (var model in doc.RootElement.EnumerateArray())
{
if (model.ValueKind == System.Text.Json.JsonValueKind.String)
{
models.Add(model.GetString());
}
else if (model.TryGetProperty("id", out var id))
{
models.Add(id.GetString());
}
else if (model.TryGetProperty("name", out var name))
{
models.Add(name.GetString());
}
}
}
}
System.Diagnostics.Debug.WriteLine($"获取到 {models.Count} 个模型");
if (models.Count == 0)
{
System.Diagnostics.Debug.WriteLine("尝试Ollama /api/tags 端点...");
Uri baseUri = new Uri(apiUrl);
string ollamaTagsUrl = $"{baseUri.Scheme}://{baseUri.Authority}/api/tags";
try
{
var ollamaResponse = await client.GetAsync(ollamaTagsUrl);
if (ollamaResponse.IsSuccessStatusCode)
{
var ollamaContent = await ollamaResponse.Content.ReadAsStringAsync();
System.Diagnostics.Debug.WriteLine($"Ollama响应: {ollamaContent.Substring(0, Math.Min(500, ollamaContent.Length))}...");
using (var ollamaDoc = System.Text.Json.JsonDocument.Parse(ollamaContent))
{
if (ollamaDoc.RootElement.TryGetProperty("models", out var ollamaModelList))
{
foreach (var model in ollamaModelList.EnumerateArray())
{
if (model.TryGetProperty("name", out var name))
{
models.Add(name.GetString());
}
}
}
}
System.Diagnostics.Debug.WriteLine($"从Ollama获取到 {models.Count} 个模型");
}
}
catch (Exception ollamaEx)
{
System.Diagnostics.Debug.WriteLine($"Ollama端点请求失败: {ollamaEx.Message}");
}
}
return models.ToArray();
}
else
{
var errorContent = await response.Content.ReadAsStringAsync();
System.Diagnostics.Debug.WriteLine($"获取模型列表失败: {response.StatusCode}");
System.Diagnostics.Debug.WriteLine($"错误内容: {errorContent}");
System.Diagnostics.Debug.WriteLine("尝试Ollama /api/tags 端点...");
Uri baseUri = new Uri(apiUrl);
string ollamaTagsUrl = $"{baseUri.Scheme}://{baseUri.Authority}/api/tags";
try
{
var ollamaResponse = await client.GetAsync(ollamaTagsUrl);
if (ollamaResponse.IsSuccessStatusCode)
{
var ollamaContent = await ollamaResponse.Content.ReadAsStringAsync();
var models = new System.Collections.Generic.List<string>();
using (var ollamaDoc = System.Text.Json.JsonDocument.Parse(ollamaContent))
{
if (ollamaDoc.RootElement.TryGetProperty("models", out var ollamaModelList))
{
foreach (var model in ollamaModelList.EnumerateArray())
{
if (model.TryGetProperty("name", out var name))
{
models.Add(name.GetString());
}
}
}
}
System.Diagnostics.Debug.WriteLine($"从Ollama获取到 {models.Count} 个模型");
return models.ToArray();
}
}
catch (Exception ollamaEx)
{
System.Diagnostics.Debug.WriteLine($"Ollama端点请求失败: {ollamaEx.Message}");
}
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"获取模型列表异常: {ex.GetType().Name} - {ex.Message}");
if (ex.InnerException != null)
{
System.Diagnostics.Debug.WriteLine($"内部异常: {ex.InnerException.Message}");
}
}
return new string[0];
}
private async void txbUrl_TextChanged(object sender, EventArgs e)
{
string url = txbUrl.Text.Trim();
if (string.IsNullOrEmpty(url))
return;
_modelFetchCts?.Cancel();
_modelFetchCts = new CancellationTokenSource();
var currentCts = _modelFetchCts;
_debounceTimer?.Stop();
_debounceTimer?.Dispose();
if (IsLocalApiUrl(url))
{
_isCloudConnection = false;
txbKey.Enabled = false;
lblResult.Text = "检测到本地API地址,正在获取本地模型列表...";
_debounceTimer = new System.Windows.Forms.Timer();
_debounceTimer.Interval = 500;
_debounceTimer.Tick += async (s, args) =>
{
_debounceTimer.Stop();
if (currentCts.IsCancellationRequested)
return;
string currentModel = cbxModel.Text;
var models = await GetLocalModels(url);
if (currentCts.IsCancellationRequested)
return;
if (models.Length > 0)
{
cbxModel.Items.Clear();
foreach (var model in models)
{
cbxModel.Items.Add(model);
}
if (!string.IsNullOrEmpty(currentModel) && cbxModel.Items.Contains(currentModel))
{
cbxModel.Text = currentModel;
}
else if (cbxModel.Items.Count > 0)
{
cbxModel.SelectedIndex = 0;
}
lblResult.Text = $"已获取 {models.Length} 个本地模型";
}
else
{
lblResult.Text = "无法获取本地模型列表,请确保API服务正在运行";
}
};
_debounceTimer.Start();
}
else
{
_isCloudConnection = true;
txbKey.Enabled = true;
lblResult.Text = "云端 API 地址";
}
}
private void txbUrl_DoubleClick(object sender, EventArgs e)
{
txbUrl.Clear();
txbUrl.Focus();
}
}
}