Commit 0fff0a7b authored by 段廷瑞's avatar 段廷瑞

pref: 智能体模型选择与交互优化

parent a3a8c203
...@@ -48,6 +48,61 @@ namespace Tools.MyAgent.Agent ...@@ -48,6 +48,61 @@ namespace Tools.MyAgent.Agent
public double Temperature { get; set; } = 0.3; public double Temperature { get; set; } = 0.3;
public int MaxTokens { get; set; } = 8192; public int MaxTokens { get; set; } = 8192;
/// <summary>获取当前兼容接口公开的模型列表(不影响聊天请求)</summary>
public async Task<(bool Ok, string Message, List<string> Models)> FetchModelsAsync(CancellationToken ct)
{
try
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
cts.CancelAfter(15000);
string baseUrl = BaseUrl?.Trim().TrimEnd('/');
if (string.IsNullOrEmpty(baseUrl))
return (false, "BaseUrl 不能为空", null);
string modelsUrl = baseUrl.EndsWith("/v1", StringComparison.OrdinalIgnoreCase)
? baseUrl + "/models"
: baseUrl + "/v1/models";
using (var req = new HttpRequestMessage(HttpMethod.Get, modelsUrl))
{
if (!string.IsNullOrWhiteSpace(ApiKey))
req.Headers.TryAddWithoutValidation("Authorization", "Bearer " + ApiKey);
using (var resp = await Http.SendAsync(req, cts.Token).ConfigureAwait(false))
{
string json = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
return (false, $"HTTP {(int)resp.StatusCode}:{TruncateModelResponse(json, 200)}", null);
var obj = JObject.Parse(json);
var data = obj["data"] as JArray;
var models = data == null ? new List<string>() : data
.OfType<JObject>()
.Select(x => x["id"]?.Value<string>())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
.ToList();
return models.Count == 0
? (false, "接口未返回可用模型", null)
: (true, $"获取到 {models.Count} 个模型", models);
}
}
}
}
catch (OperationCanceledException)
{
return (false, "获取模型列表超时,请检查网络与 BaseUrl。", null);
}
catch (Exception ex)
{
return (false, ex.Message, null);
}
}
private static string TruncateModelResponse(string text, int max)
{
if (string.IsNullOrEmpty(text)) return "";
return text.Length <= max ? text : text.Substring(0, max) + "…";
}
/// <summary>是否使用系统代理(默认 true;公司内网常用)。 /// <summary>是否使用系统代理(默认 true;公司内网常用)。
/// 优先读取 HTTPS_PROXY / HTTP_PROXY 环境变量,否则用系统 IE 代理设置。 /// 优先读取 HTTPS_PROXY / HTTP_PROXY 环境变量,否则用系统 IE 代理设置。
/// 修复:原实现属性为死字段(handler 在构造时绑定,开关从不生效); /// 修复:原实现属性为死字段(handler 在构造时绑定,开关从不生效);
...@@ -321,10 +376,9 @@ namespace Tools.MyAgent.Agent ...@@ -321,10 +376,9 @@ namespace Tools.MyAgent.Agent
return messages.Count > 1; return messages.Count > 1;
} }
/// <summary>发送一次请求并读取 SSE 流;非成功状态码抛 HttpRequestException(附 Data["HttpStatusCode"])</summary>
private async Task<ChatResult> SendOnceAsync(JObject body, CancellationToken ct) private async Task<ChatResult> SendOnceAsync(JObject body, CancellationToken ct)
{ {
var req = new HttpRequestMessage(HttpMethod.Post, BaseUrl.TrimEnd('/') + "/chat/completions"); var req = new HttpRequestMessage(HttpMethod.Post, ResolveChatEndpoint(BaseUrl));
req.Headers.TryAddWithoutValidation("Authorization", "Bearer " + ApiKey); req.Headers.TryAddWithoutValidation("Authorization", "Bearer " + ApiKey);
req.Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json"); req.Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json");
...@@ -372,7 +426,15 @@ namespace Tools.MyAgent.Agent ...@@ -372,7 +426,15 @@ namespace Tools.MyAgent.Agent
} }
} }
/// <summary>连接测试:发送最小补全请求,返回(是否成功,信息)</summary> private static string ResolveChatEndpoint(string baseUrl)
{
string value = (baseUrl ?? "").Trim().TrimEnd('/');
if (value.EndsWith("/chat/completions", StringComparison.OrdinalIgnoreCase)) return value;
return value.EndsWith("/v1", StringComparison.OrdinalIgnoreCase)
? value + "/chat/completions"
: value + "/v1/chat/completions";
}
public async Task<(bool Ok, string Message)> TestConnectionAsync(CancellationToken ct) public async Task<(bool Ok, string Message)> TestConnectionAsync(CancellationToken ct)
{ {
try try
...@@ -387,7 +449,7 @@ namespace Tools.MyAgent.Agent ...@@ -387,7 +449,7 @@ namespace Tools.MyAgent.Agent
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct)) using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
{ {
cts.CancelAfter(15000); cts.CancelAfter(15000);
var req = new HttpRequestMessage(HttpMethod.Post, BaseUrl.TrimEnd('/') + "/chat/completions"); var req = new HttpRequestMessage(HttpMethod.Post, ResolveChatEndpoint(BaseUrl));
req.Headers.TryAddWithoutValidation("Authorization", "Bearer " + ApiKey); req.Headers.TryAddWithoutValidation("Authorization", "Bearer " + ApiKey);
req.Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json"); req.Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json");
...@@ -396,6 +458,8 @@ namespace Tools.MyAgent.Agent ...@@ -396,6 +458,8 @@ namespace Tools.MyAgent.Agent
string json = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); string json = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!resp.IsSuccessStatusCode) if (!resp.IsSuccessStatusCode)
return (false, $"HTTP {(int)resp.StatusCode}:{Truncate(json, 200)}"); return (false, $"HTTP {(int)resp.StatusCode}:{Truncate(json, 200)}");
if (json.TrimStart().StartsWith("<", StringComparison.Ordinal))
return (false, "接口返回 HTML,请检查 BaseUrl 是否正确(通常应填写服务根地址或 /v1 地址)。");
var obj = JObject.Parse(json); var obj = JObject.Parse(json);
var content = obj["choices"]?[0]?["message"]?["content"]?.Value<string>(); var content = obj["choices"]?[0]?["message"]?["content"]?.Value<string>();
......
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
/// <summary>【核心】模型名称(可填写任意 OpenAI 兼容模型,如 Ollama 本地模型)</summary> /// <summary>【核心】模型名称(可填写任意 OpenAI 兼容模型,如 Ollama 本地模型)</summary>
public string Model { get; set; } = "deepseek-v4-flash"; public string Model { get; set; } = "deepseek-v4-flash";
/// <summary>按提供商保存的模型列表(获取成功后持久化,兼容旧配置缺省为空)</summary>
public Dictionary<string, List<string>> ModelLists { get; set; } = new Dictionary<string, List<string>>();
/// <summary>【核心】知识库根目录(支持本地路径或网络共享 UNC 路径,如 \\服务器\共享名)</summary> /// <summary>【核心】知识库根目录(支持本地路径或网络共享 UNC 路径,如 \\服务器\共享名)</summary>
public string KnowledgeBasePath { get; set; } = ""; public string KnowledgeBasePath { get; set; } = "";
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
this.lblApiKey = new System.Windows.Forms.Label(); this.lblApiKey = new System.Windows.Forms.Label();
this.txtApiKey = new System.Windows.Forms.TextBox(); this.txtApiKey = new System.Windows.Forms.TextBox();
this.btnTestApi = new System.Windows.Forms.Button(); this.btnTestApi = new System.Windows.Forms.Button();
this.btnFetchModels = new System.Windows.Forms.Button();
this.lblBaseUrl = new System.Windows.Forms.Label(); this.lblBaseUrl = new System.Windows.Forms.Label();
this.txtBaseUrl = new System.Windows.Forms.TextBox(); this.txtBaseUrl = new System.Windows.Forms.TextBox();
this.lblModel = new System.Windows.Forms.Label(); this.lblModel = new System.Windows.Forms.Label();
...@@ -165,6 +166,7 @@ ...@@ -165,6 +166,7 @@
this.grpModel.Controls.Add(this.txtBaseUrl); this.grpModel.Controls.Add(this.txtBaseUrl);
this.grpModel.Controls.Add(this.lblModel); this.grpModel.Controls.Add(this.lblModel);
this.grpModel.Controls.Add(this.cmbModel); this.grpModel.Controls.Add(this.cmbModel);
this.grpModel.Controls.Add(this.btnFetchModels);
this.grpModel.Location = new System.Drawing.Point(8, 12); this.grpModel.Location = new System.Drawing.Point(8, 12);
this.grpModel.Name = "grpModel"; this.grpModel.Name = "grpModel";
this.grpModel.Size = new System.Drawing.Size(596, 164); this.grpModel.Size = new System.Drawing.Size(596, 164);
...@@ -260,9 +262,19 @@ ...@@ -260,9 +262,19 @@
"deepseek-r1"}); "deepseek-r1"});
this.cmbModel.Location = new System.Drawing.Point(92, 124); this.cmbModel.Location = new System.Drawing.Point(92, 124);
this.cmbModel.Name = "cmbModel"; this.cmbModel.Name = "cmbModel";
this.cmbModel.Size = new System.Drawing.Size(486, 25); this.cmbModel.Size = new System.Drawing.Size(306, 25);
this.cmbModel.TabIndex = 8; this.cmbModel.TabIndex = 8;
// //
// btnFetchModels
//
this.btnFetchModels.Location = new System.Drawing.Point(406, 123);
this.btnFetchModels.Name = "btnFetchModels";
this.btnFetchModels.Size = new System.Drawing.Size(172, 25);
this.btnFetchModels.TabIndex = 9;
this.btnFetchModels.Text = "获取模型列表";
this.btnFetchModels.UseVisualStyleBackColor = true;
this.btnFetchModels.Click += new System.EventHandler(this.OnFetchModels);
//
// grpKb // grpKb
// //
this.grpKb.Controls.Add(this.lblMode); this.grpKb.Controls.Add(this.lblMode);
...@@ -1084,6 +1096,7 @@ ...@@ -1084,6 +1096,7 @@
private System.Windows.Forms.Label lblApiKey; private System.Windows.Forms.Label lblApiKey;
private System.Windows.Forms.TextBox txtApiKey; private System.Windows.Forms.TextBox txtApiKey;
private System.Windows.Forms.Button btnTestApi; private System.Windows.Forms.Button btnTestApi;
private System.Windows.Forms.Button btnFetchModels;
private System.Windows.Forms.Label lblBaseUrl; private System.Windows.Forms.Label lblBaseUrl;
private System.Windows.Forms.TextBox txtBaseUrl; private System.Windows.Forms.TextBox txtBaseUrl;
private System.Windows.Forms.Label lblModel; private System.Windows.Forms.Label lblModel;
......
...@@ -21,8 +21,33 @@ namespace Tools.MyAgent.Forms ...@@ -21,8 +21,33 @@ namespace Tools.MyAgent.Forms
/// </summary> /// </summary>
public partial class AgentSettingsForm : Form public partial class AgentSettingsForm : Form
{ {
/// <summary>仅支持的模型</summary> /// <summary>模型建议列表(下拉仅作建议,模型框仍允许输入任意兼容模型)</summary>
public static readonly string[] SupportedModels = { "deepseek-v4-flash", "deepseek-v4-pro" }; private static readonly string[][] ProviderModelSuggestions =
{
new[] { "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-reasoner" },
new[] { "qwen-plus", "qwen-turbo", "qwen-max", "qwen-vl-plus" },
new[] { "gpt-4o-mini", "gpt-4o", "gpt-4.1-mini" },
new[] { "qwen2.5", "qwen2.5:7b", "llama3.1", "deepseek-r1" },
new string[0],
};
/// <summary>兼容旧配置中的提供商名称(保存时统一为当前预设名称)</summary>
private static readonly string[][] ProviderAliases =
{
new[] { "deepseek", "DeepSeek" },
new[] { "qwen", "通义", "通义千问", "aliyun", "dashscope" },
new[] { "openai" },
new[] { "ollama", "本地ollama", "本地 Ollama" },
new[] { "custom", "自定义" },
};
/// <summary>兼容旧调用方的模型集合;设置页实际按提供商显示建议。</summary>
public static readonly string[] SupportedModels =
{
"deepseek-v4-flash", "deepseek-v4-pro", "deepseek-reasoner",
"qwen-plus", "qwen-turbo", "qwen-max", "qwen-vl-plus",
"gpt-4o-mini", "gpt-4o", "gpt-4.1-mini",
"qwen2.5", "qwen2.5:7b", "llama3.1", "deepseek-r1",
};
/// <summary>提供商预设(选择后自动填充 BaseUrl / 模型名;「自定义」保留 BaseUrl 自由输入)</summary> /// <summary>提供商预设(选择后自动填充 BaseUrl / 模型名;「自定义」保留 BaseUrl 自由输入)</summary>
public static readonly (string Name, string BaseUrl, string Model)[] ProviderPresets = public static readonly (string Name, string BaseUrl, string Model)[] ProviderPresets =
...@@ -41,13 +66,10 @@ namespace Tools.MyAgent.Forms ...@@ -41,13 +66,10 @@ namespace Tools.MyAgent.Forms
public static int CurrentProviderIndex(AgentConfig config) public static int CurrentProviderIndex(AgentConfig config)
{ {
string provider = (config?.Provider ?? "").Trim(); string provider = (config?.Provider ?? "").Trim();
int aliasIndex = FindProviderAlias(provider);
if (aliasIndex >= 0) return aliasIndex;
if (provider.Length > 0) if (provider.Length > 0)
{
for (int i = 0; i < ProviderPresets.Length; i++)
if (string.Equals(ProviderPresets[i].Name, provider, StringComparison.OrdinalIgnoreCase))
return i;
return CustomProviderIndex; // 保存的提供商不在预设内 → 自定义 return CustomProviderIndex; // 保存的提供商不在预设内 → 自定义
}
string baseUrl = (config?.BaseUrl ?? "").ToLowerInvariant(); string baseUrl = (config?.BaseUrl ?? "").ToLowerInvariant();
if (baseUrl.Contains("localhost") || baseUrl.Contains("11434")) return 3; // Ollama if (baseUrl.Contains("localhost") || baseUrl.Contains("11434")) return 3; // Ollama
if (baseUrl.Contains("dashscope") || baseUrl.Contains("aliyun")) return 1; // 通义 if (baseUrl.Contains("dashscope") || baseUrl.Contains("aliyun")) return 1; // 通义
...@@ -58,6 +80,12 @@ namespace Tools.MyAgent.Forms ...@@ -58,6 +80,12 @@ namespace Tools.MyAgent.Forms
private readonly AgentConfig _config; private readonly AgentConfig _config;
// 各提供商独立保存 API Key、当前模型和已获取的模型列表,切换时互不覆盖。
private readonly Dictionary<int, string> _cachedApiKeys = new Dictionary<int, string>();
private readonly Dictionary<int, string> _cachedModels = new Dictionary<int, string>();
private readonly Dictionary<int, List<string>> _cachedModelItems = new Dictionary<int, List<string>>();
private int _lastProviderIndex = -1;
// 本地/共享两种方式各自的路径缓存(切换方式时互相恢复) // 本地/共享两种方式各自的路径缓存(切换方式时互相恢复)
private string _cachedLocalPath = "", _cachedSharedPath = ""; private string _cachedLocalPath = "", _cachedSharedPath = "";
private bool _lastSharedMode; private bool _lastSharedMode;
...@@ -204,13 +232,15 @@ namespace Tools.MyAgent.Forms ...@@ -204,13 +232,15 @@ namespace Tools.MyAgent.Forms
private void BindConfig() private void BindConfig()
{ {
// 基础配置:模型(先设提供商以触发预设联动,再以配置覆盖模型/Key) // 基础配置:模型(先设提供商以触发预设联动,再以配置覆盖模型/Key)
cmbProvider.SelectedIndex = CurrentProviderIndex(_config); int providerIndex = CurrentProviderIndex(_config);
cmbProvider.SelectedIndex = providerIndex;
LoadModelCache();
_lastProviderIndex = providerIndex;
_cachedModels[providerIndex] = _config.Model ?? "";
txtApiKey.Text = _config.ApiKey; txtApiKey.Text = _config.ApiKey;
txtBaseUrl.Text = _config.BaseUrl; txtBaseUrl.Text = _config.BaseUrl;
if (SupportedModels.Contains(_config.Model ?? "")) RefreshModelItems();
cmbModel.SelectedItem = _config.Model; cmbModel.Text = _config.Model ?? "";
else
cmbModel.Text = _config.Model ?? "";
// 基础配置:知识库(先写路径/账号缓存,再切方式,避免 SyncPathWithMode 误动) // 基础配置:知识库(先写路径/账号缓存,再切方式,避免 SyncPathWithMode 误动)
string savedMode = (_config.KbMode ?? "").Trim().ToLowerInvariant(); string savedMode = (_config.KbMode ?? "").Trim().ToLowerInvariant();
...@@ -304,6 +334,134 @@ namespace Tools.MyAgent.Forms ...@@ -304,6 +334,134 @@ namespace Tools.MyAgent.Forms
} }
} }
/// <summary>更新模型建议;当前配置始终保留在可选项中,兼容新发布模型和旧配置。</summary>
private static void SetModelSuggestions(ComboBox combo, int providerIndex, string currentModel)
{
if (combo == null) return;
string[] suggestions = providerIndex >= 0 && providerIndex < ProviderModelSuggestions.Length
? ProviderModelSuggestions[providerIndex]
: new string[0];
string current = (currentModel ?? "").Trim();
combo.Items.Clear();
foreach (string model in suggestions)
combo.Items.Add(model);
if (current.Length > 0 && !suggestions.Contains(current, StringComparer.OrdinalIgnoreCase))
combo.Items.Add(current);
}
private static bool IsSuggestedModel(string model)
{
string value = (model ?? "").Trim();
if (value.Length == 0) return true;
foreach (string[] suggestions in ProviderModelSuggestions)
if (suggestions.Contains(value, StringComparer.OrdinalIgnoreCase)) return true;
return false;
}
/// <summary>根据旧配置的 Provider 或 BaseUrl 推断当前预设。</summary>
private static int FindProviderAlias(string provider)
{
string value = (provider ?? "").Trim();
for (int i = 0; i < ProviderAliases.Length; i++)
foreach (string alias in ProviderAliases[i])
if (string.Equals(alias, value, StringComparison.OrdinalIgnoreCase)) return i;
return -1;
}
/// <summary>按提供商刷新模型下拉项:优先使用已保存的远程列表,没有时使用内置建议。</summary>
private void RefreshModelItems()
{
int index = cmbProvider.SelectedIndex;
if (index < 0) return;
string current = cmbModel.Text.Trim();
if (_cachedModelItems.TryGetValue(index, out var cached) && cached.Count > 0)
{
cmbModel.Items.Clear();
cmbModel.Items.AddRange(cached.ToArray());
if (current.Length > 0 && !cached.Contains(current, StringComparer.OrdinalIgnoreCase))
cmbModel.Items.Add(current);
}
else
{
SetModelSuggestions(cmbModel, index, current);
}
}
private void LoadModelCache()
{
_cachedModelItems.Clear();
var saved = _config.ModelLists;
if (saved == null) return;
for (int i = 0; i < ProviderPresets.Length; i++)
{
var hit = saved.FirstOrDefault(kv => string.Equals(kv.Key, ProviderPresets[i].Name, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(hit.Key) && hit.Value != null)
_cachedModelItems[i] = hit.Value.Where(m => !string.IsNullOrWhiteSpace(m)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
}
private void SaveModelCache()
{
var saved = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in _cachedModelItems)
if (pair.Key >= 0 && pair.Key < ProviderPresets.Length && pair.Value != null)
saved[ProviderPresets[pair.Key].Name] = pair.Value.ToList();
_config.ModelLists = saved;
}
private static bool IsLocalEndpoint(string baseUrl)
{
if (string.IsNullOrWhiteSpace(baseUrl)) return false;
string value = baseUrl.ToLowerInvariant();
return value.Contains("localhost") || value.Contains("127.0.0.1") || value.Contains("11434");
}
private async void OnFetchModels(object sender, EventArgs e)
{
string baseUrl = EffectiveBaseUrl();
if (string.IsNullOrWhiteSpace(baseUrl))
{
AgentUtils.Toast("请先填写 BaseUrl", 3000);
return;
}
if (string.IsNullOrWhiteSpace(txtApiKey.Text) && !IsLocalEndpoint(baseUrl))
{
AgentUtils.Toast("请先填写 API Key", 3000);
return;
}
var btn = (Button)sender;
btn.Enabled = false;
btn.Text = "获取中...";
try
{
using (var client = new OpenAiCompatibleClient
{
BaseUrl = baseUrl,
ApiKey = txtApiKey.Text.Trim(),
})
{
var result = await client.FetchModelsAsync(CancellationToken.None);
if (!result.Ok)
{
AgentUtils.Toast("获取模型列表失败:" + result.Message, 5000);
return;
}
int index = cmbProvider.SelectedIndex;
_cachedModelItems[index] = result.Models;
string current = cmbModel.Text.Trim();
RefreshModelItems();
if (string.IsNullOrEmpty(current) && cmbModel.Items.Count > 0)
cmbModel.SelectedIndex = 0;
AgentUtils.Toast(result.Message + ",已填充下拉选项", 2500);
}
}
finally
{
btn.Enabled = true;
btn.Text = "获取模型列表";
}
}
// ══ 基础配置页布局联动 ══ // ══ 基础配置页布局联动 ══
/// <summary>提供商联动:仅「自定义」显示 BaseUrl 行,其余预设隐藏(减少心智负担)</summary> /// <summary>提供商联动:仅「自定义」显示 BaseUrl 行,其余预设隐藏(减少心智负担)</summary>
...@@ -312,10 +470,11 @@ namespace Tools.MyAgent.Forms ...@@ -312,10 +470,11 @@ namespace Tools.MyAgent.Forms
bool custom = cmbProvider.SelectedIndex == CustomProviderIndex; bool custom = cmbProvider.SelectedIndex == CustomProviderIndex;
lblBaseUrl.Visible = custom; lblBaseUrl.Visible = custom;
txtBaseUrl.Visible = custom; txtBaseUrl.Visible = custom;
var lblModel = grpModel.Controls["lblModel"];
int modelY = custom ? 128 : 96; int modelY = custom ? 128 : 96;
if (lblModel != null) lblModel.Top = modelY; var modelLabel = grpModel.Controls["lblModel"];
if (modelLabel != null) modelLabel.Top = modelY;
cmbModel.Top = modelY - 4; cmbModel.Top = modelY - 4;
btnFetchModels.Top = modelY - 5;
grpModel.Height = custom ? 164 : 132; grpModel.Height = custom ? 164 : 132;
ApplyBasicLayout(); ApplyBasicLayout();
} }
...@@ -376,13 +535,18 @@ namespace Tools.MyAgent.Forms ...@@ -376,13 +535,18 @@ namespace Tools.MyAgent.Forms
{ {
if (cmbProvider.SelectedIndex < 0) return; if (cmbProvider.SelectedIndex < 0) return;
ApplyProviderMode(); ApplyProviderMode();
if (cmbProvider.SelectedIndex != CustomProviderIndex) if (_lastProviderIndex >= 0)
{ {
var p = ProviderPresets[cmbProvider.SelectedIndex]; _cachedApiKeys[_lastProviderIndex] = txtApiKey.Text.Trim();
cmbModel.Text = p.Model; _cachedModels[_lastProviderIndex] = cmbModel.Text.Trim();
if (p.Name == "本地 Ollama" && string.IsNullOrEmpty(txtApiKey.Text))
txtApiKey.Text = "ollama";
} }
_lastProviderIndex = cmbProvider.SelectedIndex;
var p = ProviderPresets[cmbProvider.SelectedIndex];
txtApiKey.Text = _cachedApiKeys.TryGetValue(_lastProviderIndex, out string key)
? key : (p.Name == "本地 Ollama" ? "ollama" : "");
RefreshModelItems();
cmbModel.Text = _cachedModels.TryGetValue(_lastProviderIndex, out string model) && !string.IsNullOrWhiteSpace(model)
? model : (cmbProvider.SelectedIndex == CustomProviderIndex ? "" : p.Model);
} }
/// <summary>知识库方式切换:布局联动 + 路径缓存互切(初始化回填由 BindConfig 抑制)</summary> /// <summary>知识库方式切换:布局联动 + 路径缓存互切(初始化回填由 BindConfig 抑制)</summary>
...@@ -540,6 +704,9 @@ namespace Tools.MyAgent.Forms ...@@ -540,6 +704,9 @@ namespace Tools.MyAgent.Forms
_config.Provider = cmbProvider.SelectedIndex >= 0 ? ProviderPresets[cmbProvider.SelectedIndex].Name : ""; _config.Provider = cmbProvider.SelectedIndex >= 0 ? ProviderPresets[cmbProvider.SelectedIndex].Name : "";
_config.BaseUrl = custom ? txtBaseUrl.Text.Trim() : EffectiveBaseUrl(); _config.BaseUrl = custom ? txtBaseUrl.Text.Trim() : EffectiveBaseUrl();
_config.Model = string.IsNullOrEmpty(cmbModel.Text.Trim()) ? SupportedModels[0] : cmbModel.Text.Trim(); _config.Model = string.IsNullOrEmpty(cmbModel.Text.Trim()) ? SupportedModels[0] : cmbModel.Text.Trim();
if (cmbProvider.SelectedIndex >= 0)
_cachedModels[cmbProvider.SelectedIndex] = _config.Model;
SaveModelCache();
// 基础配置:知识库(方式以用户选择为准,避免重新打开时被路径自动识别回退) // 基础配置:知识库(方式以用户选择为准,避免重新打开时被路径自动识别回退)
_config.KnowledgeBasePath = txtKb.Text.Trim(); _config.KnowledgeBasePath = txtKb.Text.Trim();
_config.KbMode = rdoShared.Checked ? "shared" : "local"; _config.KbMode = rdoShared.Checked ? "shared" : "local";
......
...@@ -216,6 +216,7 @@ namespace Tools.MyAgent ...@@ -216,6 +216,7 @@ namespace Tools.MyAgent
BaseUrl = cfg.BaseUrl, BaseUrl = cfg.BaseUrl,
Model = cfg.Model, Model = cfg.Model,
Provider = cfg.Provider, Provider = cfg.Provider,
ModelLists = cfg.ModelLists ?? new Dictionary<string, List<string>>(),
KnowledgeBasePath = cfg.KnowledgeBasePath, KnowledgeBasePath = cfg.KnowledgeBasePath,
KbMode = cfg.KbMode, KbMode = cfg.KbMode,
ShareUser = cfg.ShareUser, ShareUser = cfg.ShareUser,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment