Commit 06b9d21a authored by 段廷瑞's avatar 段廷瑞

feat: 支持远程知识库(飞天兔大脑)

parent 12f2ed0d
...@@ -74,3 +74,4 @@ bin ...@@ -74,3 +74,4 @@ bin
/AppHome/Logs /AppHome/Logs
/AppHome/Package /AppHome/Package
/.workbuddy /.workbuddy
/temp
No preview for this file type
This diff is collapsed.
...@@ -301,6 +301,7 @@ namespace Tools.MyAgent.Agent ...@@ -301,6 +301,7 @@ namespace Tools.MyAgent.Agent
: 0.72; : 0.72;
_recall.ImagesEnabled = cfg.AnswerWithImages; // 关掉附图即不采图(省掉目录扫描) _recall.ImagesEnabled = cfg.AnswerWithImages; // 关掉附图即不采图(省掉目录扫描)
_recall.MaxImages = cfg.MaxAnswerImages > 0 ? Math.Min(cfg.MaxAnswerImages, 16) : 16; _recall.MaxImages = cfg.MaxAnswerImages > 0 ? Math.Min(cfg.MaxAnswerImages, 16) : 16;
_recall.ConfigureRemote(cfg.RemoteKbEntries); // v4.1:远程知识库条目接口模板(空 = 关闭)
CostEstimator.Configure(cfg.CustomPrices); // R6 费用估算:应用自定义价格表覆盖 CostEstimator.Configure(cfg.CustomPrices); // R6 费用估算:应用自定义价格表覆盖
AgentTelemetry.Configure(cfg.TelemetryEnabled, cfg.ObservabilityLevel); // 可观测性开关与隐私级别 AgentTelemetry.Configure(cfg.TelemetryEnabled, cfg.ObservabilityLevel); // 可观测性开关与隐私级别
...@@ -542,6 +543,18 @@ namespace Tools.MyAgent.Agent ...@@ -542,6 +543,18 @@ namespace Tools.MyAgent.Agent
OnThinkingDelta?.Invoke(result.ReasoningContent); OnThinkingDelta?.Invoke(result.ReasoningContent);
await FinishAnswerAsync(messages, result, finalAnswer, ct).ConfigureAwait(false); await FinishAnswerAsync(messages, result, finalAnswer, ct).ConfigureAwait(false);
// v4.5b 空回答回退:直答偶发空内容(长目录证据噎住模型 / 自定义端点流异常中途断流,
// 真机实证 gpt-6-luna 直答 2.7s 断流 tokens=-1+-1)时,不能拿「(模型未返回内容)」占位收场 ——
// 回退工具循环重试。finalAnswer 里的占位必须清掉,否则工具循环的回答会接在占位后面。
if (finalAnswer.ToString().IndexOf("(模型未返回内容)", StringComparison.Ordinal) >= 0)
{
finalAnswer.Clear();
while (messages.Count > mark) messages.RemoveAt(messages.Count - 1); // 回滚证据消息
AgentLogger.Log("直答返回空内容,回退工具循环重试");
return false;
}
RaiseAnswerAttachments(recall); // P5:把命中图片交给宿主渲染附图轨(无图则不发) RaiseAnswerAttachments(recall); // P5:把命中图片交给宿主渲染附图轨(无图则不发)
return true; return true;
} }
......
...@@ -5,6 +5,7 @@ using System.Linq; ...@@ -5,6 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Tools.MyAgent.Kb; using Tools.MyAgent.Kb;
using Tools.MyAgent.Kb.Remote;
namespace Tools.MyAgent.Agent namespace Tools.MyAgent.Agent
{ {
...@@ -87,6 +88,22 @@ namespace Tools.MyAgent.Agent ...@@ -87,6 +88,22 @@ namespace Tools.MyAgent.Agent
/// <summary>关联图片上限(内部默认 16;引擎 Configure 按 AgentConfig.MaxAnswerImages 注入)</summary> /// <summary>关联图片上限(内部默认 16;引擎 Configure 按 AgentConfig.MaxAnswerImages 注入)</summary>
public int MaxImages = 16; public int MaxImages = 16;
// ── 远程知识库条目补充(v4.1) ──
/// <summary>远程知识库源(解析自 AgentConfig.RemoteKbEntries;null/空 = 关闭)</summary>
private List<RemoteEntryKb.Source> _remoteSources;
/// <summary>注入远程知识库条目接口 URL 模板(引擎 Configure 期调用;无效模板静默丢弃)</summary>
public void ConfigureRemote(IEnumerable<string> urlTemplates)
{
_remoteSources = RemoteEntryKb.ParseTemplates(urlTemplates);
}
/// <summary>列表意图:命中则走远程「目录模式」(返回全部条目标题清单),而非逐词检索</summary>
private static readonly Regex RemoteListIntent = new Regex(
@"列出|列表|清单|目录|都有什么|有哪些|有什么内容|多少条|全部条目",
RegexOptions.Compiled);
/// <summary>纳入证据链的命中上限(主命中 + 其余)</summary> /// <summary>纳入证据链的命中上限(主命中 + 其余)</summary>
private const int MaxHits = 3; private const int MaxHits = 3;
...@@ -213,7 +230,7 @@ namespace Tools.MyAgent.Agent ...@@ -213,7 +230,7 @@ namespace Tools.MyAgent.Agent
} }
} }
if (acc.Count == 0) if (acc.Count == 0)
return new KbRecallResult { Mode = "model", Query = question, Keywords = keywords.Take(used).ToList() }; return Finalize(new KbRecallResult { Mode = "model", Query = question, Keywords = keywords.Take(used).ToList() }, keywords, question, ct);
} }
// ── 融合分:来源基分 + 0.25×查询词覆盖率 + 0.05×额外来源数(上限 0.95) ── // ── 融合分:来源基分 + 0.25×查询词覆盖率 + 0.05×额外来源数(上限 0.95) ──
...@@ -234,7 +251,7 @@ namespace Tools.MyAgent.Agent ...@@ -234,7 +251,7 @@ namespace Tools.MyAgent.Agent
.ThenBy(h => h.RelPath, StringComparer.OrdinalIgnoreCase) .ThenBy(h => h.RelPath, StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
if (ranked.Count == 0) if (ranked.Count == 0)
return new KbRecallResult { Mode = "model", Query = question, Keywords = keywords.Take(used).ToList() }; return Finalize(new KbRecallResult { Mode = "model", Query = question, Keywords = keywords.Take(used).ToList() }, keywords, question, ct);
var primary = ranked[0]; var primary = ranked[0];
var result = new KbRecallResult var result = new KbRecallResult
...@@ -253,18 +270,99 @@ namespace Tools.MyAgent.Agent ...@@ -253,18 +270,99 @@ namespace Tools.MyAgent.Agent
} }
else else
{ {
// 低于下界:不注入证据,回退原工具循环(保留原有检索/诊断能力,避免新通道削弱能力) // 低于下界:本地证据不入链,回退前仍尝试远程兜底(Finalize 内统一处理)
result.Mode = "model"; result.Mode = "model";
return result; return Finalize(result, keywords, question, ct);
} }
// ── 只对进入证据链的命中补齐 行号/章节/正文/图片(读文件的成本只花在少量命中上) ── // ── 只对进入证据链的命中补齐 行号/章节/正文/图片(读文件的成本只花在少量命中上) ──
Enrich(primary, MaxImages); Enrich(primary, MaxImages);
for (int i = 0; i < result.Others.Count; i++) for (int i = 0; i < result.Others.Count; i++)
Enrich(result.Others[i], 0); // 次要命中不采图,避免附图轨被无关图片占满 Enrich(result.Others[i], 0); // 次要命中不采图,避免附图轨被无关图片占满
// ── 远程知识库条目补充(v4.1):失败静默、不改变本地命中自身的分数 ──
return Finalize(result, keywords, question, ct);
}
/// <summary>统一出口:本地结果 + 远程条目补充(所有 return 路径都经过,保证 model 时远程兜底同样生效)。</summary>
private KbRecallResult Finalize(KbRecallResult result, List<string> keywords, string question, System.Threading.CancellationToken ct)
{
try { AppendRemoteEvidence(result, keywords, question, ct); }
catch (OperationCanceledException) { throw; } // 宿主取消:透传
catch { /* 静默:远程补充绝不破坏本地召回结果 */ }
return result; return result;
} }
/// <summary>
/// 远程知识库条目补充(v4.1):本地召回完成后,按本地关键词逐词并行查询远端条目接口。
/// <para>· 本地已召回(kb/mixed)→ 远程命中并入 <see cref="KbRecallResult.Others"/>;</para>
/// <para>· 本地未召回(model)→ 远程命中(含"最新条目"目录兜底)作为证据走 mixed(Reason 标注来源),
/// 让"远程知识库都有什么 / 本地没有的问题"也能得到回答;</para>
/// <para>· 纪律:失败静默;远程命中不参与本地融合打分;AbsPath 留空 —— 不 Enrich、不进图片轨,
/// 引用行走 AnswerRefPattern 兜底分支(零正则改动)。</para>
/// </summary>
private void AppendRemoteEvidence(KbRecallResult result, List<string> keywords, string question, System.Threading.CancellationToken ct)
{
if (_remoteSources == null || _remoteSources.Count == 0) return;
bool localMiss = result.Primary == null || result.Mode == "model";
// 列表意图("列出/都有什么/有哪些…"):走目录模式,返回全部条目标题清单(而非逐词检索的零散命中)
if (localMiss && !string.IsNullOrEmpty(question) && RemoteListIntent.IsMatch(question))
{
RemoteEntryKb.Hit dir;
try { dir = RemoteEntryKb.ListAll(_remoteSources, ct); }
catch (OperationCanceledException) { throw; }
catch { dir = null; }
if (dir != null)
{
result.Primary = ToKbHit(dir);
result.Mode = "mixed";
result.Reason = "本地知识库未命中,以下为远程知识库条目目录。";
return;
}
// 目录失败:继续走常规逐词/兜底路径
}
List<RemoteEntryKb.Hit> hits;
try { hits = RemoteEntryKb.Search(_remoteSources, keywords, ct); }
catch (OperationCanceledException) { throw; } // 宿主取消:透传(直答通道既有取消语义)
catch { return; } // 双保险:Search 承诺除取消外不抛
if (hits == null || hits.Count == 0) return;
if (!localMiss)
if (!localMiss)
{
foreach (var h in hits) result.Others.Add(ToKbHit(h));
return;
}
// 本地未召回:远程条目升级为 mixed 证据(首条 Primary,其余 Others)。
// Others 必须先清空 —— 此处 result.Others 可能还残留本地低分命中(<0.45,未达证据线),
// 它们已随 Enrich 采集了关联图片;混入会让"远程回答"挂上无关的本地图片(真机反馈 2026-10-08)。
var remote = hits.Select(ToKbHit).ToList();
result.Primary = remote[0];
result.Others.Clear();
result.Others.AddRange(remote.Skip(1));
result.Mode = "mixed";
result.Reason = "本地知识库未命中,以下引用来自远程知识库条目(可能不完全匹配,请结合判断)。";
}
private static KbHit ToKbHit(RemoteEntryKb.Hit h)
{
// Id=0 为目录模式 Hit:引用名显示「条目目录」(此前显示「条目#0·最新」有误导性)
string label = h.Id == 0
? "远程·" + h.SourceHost + "·条目目录"
: "远程·" + h.SourceHost + "·条目#" + h.Id + (h.IsFallback ? "·最新" : "");
return new KbHit
{
RelPath = label,
Snippet = "",
Paragraph = h.Text,
Score = h.Score,
HitKeywords = string.Join(",", h.HitWords),
};
}
/// <summary>命中 → 证据消息(作为一条 user 消息追加在问题之后,契约要求模型只据证据作答)</summary> /// <summary>命中 → 证据消息(作为一条 user 消息追加在问题之后,契约要求模型只据证据作答)</summary>
public string BuildEvidenceMessage(KbRecallResult r) public string BuildEvidenceMessage(KbRecallResult r)
{ {
......
...@@ -570,6 +570,15 @@ namespace Tools.MyAgent.Agent ...@@ -570,6 +570,15 @@ namespace Tools.MyAgent.Agent
try { obj = JObject.Parse(data); } try { obj = JObject.Parse(data); }
catch { continue; } catch { continue; }
// 用量(v4.4 修复):SSE 末帧 choices 为空数组、仅含 usage —— 必须在 choices 空判**之前**解析,
// 否则末帧被下方 continue 跳过,用量(含费用尾注)永远采集不到(真机反馈:回答尾部无用量)。
var usage = obj["usage"] as JObject;
if (usage != null)
{
result.PromptTokens = usage["prompt_tokens"]?.Value<int>() ?? -1;
result.CompletionTokens = usage["completion_tokens"]?.Value<int>() ?? -1;
}
var choices = obj["choices"] as JArray; var choices = obj["choices"] as JArray;
if (choices == null || choices.Count == 0) continue; if (choices == null || choices.Count == 0) continue;
var first = choices[0] as JObject; var first = choices[0] as JObject;
...@@ -578,14 +587,6 @@ namespace Tools.MyAgent.Agent ...@@ -578,14 +587,6 @@ namespace Tools.MyAgent.Agent
var fr = first["finish_reason"]?.Value<string>(); var fr = first["finish_reason"]?.Value<string>();
if (!string.IsNullOrEmpty(fr)) finishReason = fr; if (!string.IsNullOrEmpty(fr)) finishReason = fr;
// 用量(DeepSeek 在 SSE 末帧返回 usage)
var usage = obj["usage"] as JObject;
if (usage != null)
{
result.PromptTokens = usage["prompt_tokens"]?.Value<int>() ?? -1;
result.CompletionTokens = usage["completion_tokens"]?.Value<int>() ?? -1;
}
var delta = first["delta"] as JObject; var delta = first["delta"] as JObject;
if (delta == null) continue; if (delta == null) continue;
......
...@@ -55,6 +55,17 @@ ...@@ -55,6 +55,17 @@
/// <summary>共享文件夹访问密码(可选;落盘时经 DPAPI 加密(见 SecretProtector),内存中为明文)</summary> /// <summary>共享文件夹访问密码(可选;落盘时经 DPAPI 加密(见 SecretProtector),内存中为明文)</summary>
public string SharePassword { get; set; } = ""; public string SharePassword { get; set; } = "";
/// <summary>
/// 远程知识库条目接口 URL 模板数组(v4.1 外挂知识库)。
/// <para>每条模板必须含 <c>{q}</c> 占位符;可选 <c>{filter}</c>(服务端 source 等值过滤)/<c>{page}</c>/<c>{n}</c>。</para>
/// <para>**默认内置超级大脑条目接口**(开箱即用);设置页清空并保存 = 永久关闭。
/// 检索策略固定为「逐词并行 + 本地合并打分」(见 next_pref_plan.md §3),其余参数为代码内常量。</para>
/// </summary>
public List<string> RemoteKbEntries { get; set; } = new List<string>
{
"http://192.168.1.108:8718/api/knowledge/entries?search={q}&page_size={n}&page={page}&partition={filter}",
};
// ════════════════ 高级项:生成参数 ════════════════ // ════════════════ 高级项:生成参数 ════════════════
/// <summary>采样温度</summary> /// <summary>采样温度</summary>
...@@ -189,8 +200,11 @@ ...@@ -189,8 +200,11 @@
/// <summary>runs_*.jsonl 保留天数(默认 14)。</summary> /// <summary>runs_*.jsonl 保留天数(默认 14)。</summary>
public int RunsKeepDays { get; set; } = 14; public int RunsKeepDays { get; set; } = 14;
/// <summary>回复尾注是否带本轮摘要(耗时/工具数/通道);默认关闭,避免打扰。</summary> /// <summary>
public bool ShowRunSummary { get; set; } = false; /// 回复尾注是否带本轮摘要(耗时/工具数/通道);默认开启(真机反馈:回答结尾应有用量信息)。
/// <para>v4.4 由 ShowRunSummary 改名而来:旧配置文件中的 false 值不再生效(一次性摆脱旧默认)。</para>
/// </summary>
public bool RunSummaryEnabled { get; set; } = true;
// ── Admin 看板 ── // ── Admin 看板 ──
......
...@@ -121,12 +121,49 @@ namespace Tools.MyAgent.Forms ...@@ -121,12 +121,49 @@ namespace Tools.MyAgent.Forms
BuildDbConnectionsTab(); // P0:预存数据库连接(db_query 按 ID 引用,聊天不回显密码) BuildDbConnectionsTab(); // P0:预存数据库连接(db_query 按 ID 引用,聊天不回显密码)
BuildToolPermissionUi(); // P3:工具级权限(按角色)输入区 BuildToolPermissionUi(); // P3:工具级权限(按角色)输入区
BuildDashboardTab(); // 可观测性:统计看板入口(替代隐晦的 /admin) BuildDashboardTab(); // 可观测性:统计看板入口(替代隐晦的 /admin)
BuildRemoteKbUi(); // v4.1:远程知识库条目接口(高级 → 语义检索页底部)
// 回答形态不再提供设置页(v1.2):全部内部默认(见 AgentConfig 注释),降低设置页认知负担 // 回答形态不再提供设置页(v1.2):全部内部默认(见 AgentConfig 注释),降低设置页认知负担
BindConfig(); // 配置值回填控件(控件创建见 Designer.cs) BindConfig(); // 配置值回填控件(控件创建见 Designer.cs)
ApplyProviderMode(); // 布局联动:BaseUrl 行显隐 ApplyProviderMode(); // 布局联动:BaseUrl 行显隐
ApplyKbMode(); // 布局联动:共享 账号/密码/测试连接 行显隐 ApplyKbMode(); // 布局联动:共享 账号/密码/测试连接 行显隐
} }
// ══ v4.1:远程知识库条目接口(程序化创建,避免侵入设计器) ══
private TextBox txtRemoteKb;
/// <summary>
/// 远程知识库条目接口(next_pref_plan v4.1):追加到「高级 → 语义检索」二级页底部。
/// 多行文本框,每行一条 URL 模板(须含 {q} 占位符);留空 = 功能关闭。
/// </summary>
private void BuildRemoteKbUi()
{
var grp = new GroupBox
{
Text = "远程知识库(HTTP 外挂 · 条目接口)",
Location = new Point(4, 142),
Size = new Size(590, 118),
TabIndex = 2,
};
var lbl = new Label
{
Text = "每行一条条目接口 URL 模板(须含 {q},可选 {filter}/{page}/{n})。\r\n默认已内置超级大脑接口;清空全部行并保存 = 关闭。服务不可达时自动冷却 60s。",
Location = new Point(10, 22),
Size = new Size(570, 34),
};
txtRemoteKb = new TextBox
{
Multiline = true,
ScrollBars = ScrollBars.Vertical,
Location = new Point(10, 58),
Size = new Size(570, 52),
TabIndex = 0,
};
grp.Controls.Add(lbl);
grp.Controls.Add(txtRemoteKb);
tabSubSem.Controls.Add(grp);
}
// ══ P0:预存数据库连接选项卡(程序化创建,避免侵入设计器) ══ // ══ P0:预存数据库连接选项卡(程序化创建,避免侵入设计器) ══
private DataGridView dgvDbConn; private DataGridView dgvDbConn;
...@@ -225,7 +262,7 @@ namespace Tools.MyAgent.Forms ...@@ -225,7 +262,7 @@ namespace Tools.MyAgent.Forms
var grpKpi = new GroupBox var grpKpi = new GroupBox
{ {
Text = "运行统计(本进程实时累计)", Text = "运行统计(进程实时 + 近 7 天落盘,重启不丢)",
Location = new Point(8, 8), Location = new Point(8, 8),
Size = new Size(596, 172), Size = new Size(596, 172),
}; };
...@@ -306,7 +343,7 @@ namespace Tools.MyAgent.Forms ...@@ -306,7 +343,7 @@ namespace Tools.MyAgent.Forms
{ {
var st = Tools.MyAgent.AgentTelemetry.Snapshot(); var st = Tools.MyAgent.AgentTelemetry.Snapshot();
var sb = new System.Text.StringBuilder(); var sb = new System.Text.StringBuilder();
sb.Append("本轮进程累计:\n"); sb.Append("本进程累计(实时):\n");
sb.Append(string.Format(" 轮数 {0}(成功 {1} · 失败 {2} · 取消 {3})\n", sb.Append(string.Format(" 轮数 {0}(成功 {1} · 失败 {2} · 取消 {3})\n",
st.Runs, st.Success, st.Errors, st.Cancels)); st.Runs, st.Success, st.Errors, st.Cancels));
if (st.Duration.Count > 0) if (st.Duration.Count > 0)
...@@ -319,7 +356,43 @@ namespace Tools.MyAgent.Forms ...@@ -319,7 +356,43 @@ namespace Tools.MyAgent.Forms
if (tokens > 0) sb.Append(string.Format(" Token {0} + {1}\n", st.PromptTokens, st.CompletionTokens)); if (tokens > 0) sb.Append(string.Format(" Token {0} + {1}\n", st.PromptTokens, st.CompletionTokens));
if (st.Cost > 0) sb.Append(" 费用 " + Tools.MyAgent.CostEstimator.Format(st.Cost) + "\n"); if (st.Cost > 0) sb.Append(" 费用 " + Tools.MyAgent.CostEstimator.Format(st.Cost) + "\n");
if (st.Dropped > 0) sb.Append(" ⚠ 丢弃 " + st.Dropped + " 条(采集跟不上)\n"); if (st.Dropped > 0) sb.Append(" ⚠ 丢弃 " + st.Dropped + " 条(采集跟不上)\n");
if (st.Runs == 0) sb.Append(" (本进程尚未完成任何一轮回答;多日历史见「打开完整看板」)\n");
// 近 7 天历史(runs_*.jsonl 落盘,重启不丢;与 /admin 完整看板同源)
sb.Append("\n近 7 天历史(落盘持久化):\n");
try
{
var rows = new Tools.MyAgent.TelemetryStore().LoadRuns();
if (rows.Length == 0)
sb.Append(" (暂无落盘记录;完成问答后写入 Logs\\runs_*.jsonl)\n");
else
{
int ok = 0, err = 0, cancel = 0;
long tok = 0;
double cost = 0;
var ms = new List<long>();
foreach (var r in rows)
{
if (r.Outcome == "success") ok++;
else if (r.Outcome == "error") err++;
else if (r.Outcome == "cancel") cancel++;
tok += r.PromptTokens + r.CompletionTokens;
cost += r.Cost;
if (r.TotalMs > 0) ms.Add(r.TotalMs);
}
ms.Sort();
sb.Append(string.Format(" 轮数 {0}(成功 {1} · 失败 {2} · 取消 {3})\n", rows.Length, ok, err, cancel));
if (ms.Count > 0)
sb.Append(string.Format(" 耗时 P50 {0}ms · P95 {1}ms · 最大 {2}ms\n",
Pct(ms, 0.5), Pct(ms, 0.95), ms[ms.Count - 1]));
if (tok > 0) sb.Append(" Token " + tok + "\n");
if (cost > 0) sb.Append(" 费用 " + Tools.MyAgent.CostEstimator.Format(cost) + "\n");
if (rows.Length > 0)
sb.Append(" 最新:" + rows[0].StartedAt.ToString("MM-dd HH:mm") + "(" + rows[0].Outcome + ")\n");
}
}
catch { sb.Append(" (历史记录读取失败)\n"); }
if (st.Runs == 0) sb.Append("\n(本进程尚未完成任何一轮回答;多日历史见「打开完整看板」)\n");
_lblDashKpi.Text = sb.ToString(); _lblDashKpi.Text = sb.ToString();
} }
catch catch
...@@ -328,6 +401,16 @@ namespace Tools.MyAgent.Forms ...@@ -328,6 +401,16 @@ namespace Tools.MyAgent.Forms
} }
} }
/// <summary>已排序列表取分位数(线性下标,与直方图口径近似)。</summary>
private static long Pct(List<long> sorted, double p)
{
if (sorted == null || sorted.Count == 0) return 0;
int idx = (int)Math.Ceiling(sorted.Count * p) - 1;
if (idx < 0) idx = 0;
if (idx >= sorted.Count) idx = sorted.Count - 1;
return sorted[idx];
}
/// <summary>角色规则字典 → 编辑文本(排序保证稳定回显)</summary> /// <summary>角色规则字典 → 编辑文本(排序保证稳定回显)</summary>
private static string[] ToolRoleRulesToLines(Dictionary<string, string> rules) private static string[] ToolRoleRulesToLines(Dictionary<string, string> rules)
{ {
...@@ -406,6 +489,9 @@ namespace Tools.MyAgent.Forms ...@@ -406,6 +489,9 @@ namespace Tools.MyAgent.Forms
cmbEmbModel.Text = _config.EmbeddingModel; cmbEmbModel.Text = _config.EmbeddingModel;
txtEmbKey.Text = _config.EmbeddingApiKey; txtEmbKey.Text = _config.EmbeddingApiKey;
// 远程知识库(v4.1):URL 模板逐行回填
txtRemoteKb.Text = string.Join(Environment.NewLine, _config.RemoteKbEntries ?? new List<string>());
// 提示词:启用开关 + 角色模板回填 + 自定义提示词 // 提示词:启用开关 + 角色模板回填 + 自定义提示词
chkPromptEnabled.Checked = _config.SystemPromptEnabled; chkPromptEnabled.Checked = _config.SystemPromptEnabled;
PopulateRoles(); PopulateRoles();
...@@ -865,6 +951,11 @@ namespace Tools.MyAgent.Forms ...@@ -865,6 +951,11 @@ namespace Tools.MyAgent.Forms
_config.EmbeddingBaseUrl = txtEmbUrl.Text.Trim(); _config.EmbeddingBaseUrl = txtEmbUrl.Text.Trim();
_config.EmbeddingModel = string.IsNullOrEmpty(cmbEmbModel.Text.Trim()) ? "bge-m3" : cmbEmbModel.Text.Trim(); _config.EmbeddingModel = string.IsNullOrEmpty(cmbEmbModel.Text.Trim()) ? "bge-m3" : cmbEmbModel.Text.Trim();
_config.EmbeddingApiKey = txtEmbKey.Text.Trim(); _config.EmbeddingApiKey = txtEmbKey.Text.Trim();
// 远程知识库(v4.1):每行一条 URL 模板,空行忽略
_config.RemoteKbEntries = (txtRemoteKb != null ? txtRemoteKb.Lines : new string[0])
.Select(l => (l ?? "").Trim())
.Where(l => l.Length > 0)
.ToList();
// 提示词:启用开关 + 角色模板 + 自定义系统提示词 // 提示词:启用开关 + 角色模板 + 自定义系统提示词
_config.SystemPromptEnabled = chkPromptEnabled.Checked; _config.SystemPromptEnabled = chkPromptEnabled.Checked;
_config.RoleTemplateId = (cmbRole.SelectedItem as RoleItem)?.Id ?? ""; _config.RoleTemplateId = (cmbRole.SelectedItem as RoleItem)?.Id ?? "";
......
This diff is collapsed.
...@@ -156,6 +156,7 @@ ...@@ -156,6 +156,7 @@
<Compile Include="Kb\KbConfig.cs" /> <Compile Include="Kb\KbConfig.cs" />
<Compile Include="Kb\KbIndex.cs" /> <Compile Include="Kb\KbIndex.cs" />
<Compile Include="Kb\KbService.cs" /> <Compile Include="Kb\KbService.cs" />
<Compile Include="Kb\Remote\RemoteEntryKb.cs" />
<Compile Include="Kb\SynonymIndex.cs" /> <Compile Include="Kb\SynonymIndex.cs" />
<Compile Include="ShareHelper.cs" /> <Compile Include="ShareHelper.cs" />
<Compile Include="Support\AgentLogger.cs" /> <Compile Include="Support\AgentLogger.cs" />
......
...@@ -109,6 +109,15 @@ namespace Tools.MyAgent ...@@ -109,6 +109,15 @@ namespace Tools.MyAgent
public int Line; public int Line;
} }
/// <summary>
/// 远程知识库引用行(v4.3):形如「远程·&lt;host&gt;·条目#40」「远程·&lt;host&gt;·条目目录」。
/// 不带文件扩展名,<see cref="MarkdownRenderer.AnswerRefPattern"/> 不匹配 ⇒ 走独立正则扫描登记,
/// 点击跳转对应知识库网页(OpenRemoteKbWeb)。
/// </summary>
private static readonly Regex RemoteKbRefRegex = new Regex(
@"远程·(?<host>[^·\s]+)·条目(#(?<id>\d+)|目录)",
RegexOptions.Compiled);
/// <summary> /// <summary>
/// 当前渲染文本中的引用行表。每次 <see cref="RefreshChatRtf"/> 后清空重建(字符位置随重绘失效); /// 当前渲染文本中的引用行表。每次 <see cref="RefreshChatRtf"/> 后清空重建(字符位置随重绘失效);
/// 流式期间不扫描(完成时算一次),避免每帧全文正则扫描的 O(n) 白耗。 /// 流式期间不扫描(完成时算一次),避免每帧全文正则扫描的 O(n) 白耗。
...@@ -380,12 +389,14 @@ namespace Tools.MyAgent ...@@ -380,12 +389,14 @@ namespace Tools.MyAgent
CustomPrices = cfg.CustomPrices ?? new Dictionary<string, string>(), CustomPrices = cfg.CustomPrices ?? new Dictionary<string, string>(),
DbConnections = protectedConns, DbConnections = protectedConns,
ToolRoleAllow = cfg.ToolRoleAllow ?? new Dictionary<string, string>(), ToolRoleAllow = cfg.ToolRoleAllow ?? new Dictionary<string, string>(),
// 远程知识库条目接口(v4.1;漏登记 ⇒ 设置页保存一次即被静默清空)
RemoteKbEntries = cfg.RemoteKbEntries ?? new List<string>(),
// 可观测性 + Admin 看板(漏登记 ⇒ 设置页保存一次即被静默清空) // 可观测性 + Admin 看板(漏登记 ⇒ 设置页保存一次即被静默清空)
TelemetryEnabled = cfg.TelemetryEnabled, TelemetryEnabled = cfg.TelemetryEnabled,
ObservabilityLevel = cfg.ObservabilityLevel, ObservabilityLevel = cfg.ObservabilityLevel,
HealthCheckOnStart = cfg.HealthCheckOnStart, HealthCheckOnStart = cfg.HealthCheckOnStart,
RunsKeepDays = cfg.RunsKeepDays, RunsKeepDays = cfg.RunsKeepDays,
ShowRunSummary = cfg.ShowRunSummary, RunSummaryEnabled = cfg.RunSummaryEnabled, // v4.4 改名:旧配置的 ShowRunSummary=false 不再生效
AdminEnabled = cfg.AdminEnabled, AdminEnabled = cfg.AdminEnabled,
AdminRoles = cfg.AdminRoles, AdminRoles = cfg.AdminRoles,
DashboardDays = cfg.DashboardDays, DashboardDays = cfg.DashboardDays,
...@@ -673,6 +684,10 @@ namespace Tools.MyAgent ...@@ -673,6 +684,10 @@ namespace Tools.MyAgent
Line = line, Line = line,
}); });
} }
// 远程知识库引用行(v4.3):AnswerRefPattern 因无扩展名不匹配,独立扫描登记(点击跳知识库网页)
foreach (Match m in RemoteKbRefRegex.Matches(text))
_answerRefs.Add(new AnswerRef { Start = m.Index, Length = m.Length, Rel = m.Value });
} }
catch { _answerRefs.Clear(); } catch { _answerRefs.Clear(); }
} }
...@@ -797,6 +812,14 @@ namespace Tools.MyAgent ...@@ -797,6 +812,14 @@ namespace Tools.MyAgent
private void OpenAnswerRef(AnswerRef r) private void OpenAnswerRef(AnswerRef r)
{ {
if (r == null || string.IsNullOrEmpty(r.Rel)) return; if (r == null || string.IsNullOrEmpty(r.Rel)) return;
// 远程知识库引用行(v4.3):点击跳转对应知识库网页(origin 取自配置的接口模板)
if (r.Rel.StartsWith("远程·", StringComparison.Ordinal))
{
OpenRemoteKbWeb(r.Rel);
return;
}
string abs = null; string abs = null;
bool dialect = false; bool dialect = false;
try try
...@@ -851,6 +874,50 @@ namespace Tools.MyAgent ...@@ -851,6 +874,50 @@ namespace Tools.MyAgent
OpenLinkTarget(abs); OpenLinkTarget(abs);
} }
/// <summary>
/// 远程知识库引用行点击(v4.3):跳转对应知识库网页。
/// 网页地址 = 接口模板的 origin(scheme://host:port,从设置页 RemoteKbEntries 按 host 匹配解析);
/// 找不到匹配模板(配置已删源)时提示。
/// </summary>
private void OpenRemoteKbWeb(string rel)
{
string host = null;
var m = RemoteKbRefRegex.Match(rel ?? "");
if (m.Success) host = m.Groups["host"].Value;
string origin = ResolveRemoteKbOrigin(host);
if (string.IsNullOrEmpty(origin))
{
AgentUtils.Toast("未找到该远程知识库的网页地址(请检查设置页「远程知识库」的接口模板)", 4000);
return;
}
try { System.Diagnostics.Process.Start(origin); }
catch (Exception ex) { AgentUtils.Toast("打开浏览器失败:" + ex.Message, 4000); }
}
/// <summary>按 host 匹配配置中的远程知识库模板,返回其 origin(如 http://192.168.1.108:8718/)。</summary>
private string ResolveRemoteKbOrigin(string host)
{
if (string.IsNullOrEmpty(host)) return null;
try
{
var entries = _config != null ? _config.RemoteKbEntries : null;
if (entries != null)
foreach (var t in entries)
{
var s = (t ?? "").Trim();
if (s.Length == 0) continue;
Uri u;
try { u = new Uri(s.Replace("{q}", "x")); }
catch { continue; }
if (u.Scheme != Uri.UriSchemeHttp && u.Scheme != Uri.UriSchemeHttps) continue;
if (string.Equals(u.Host, host, StringComparison.OrdinalIgnoreCase))
return u.GetLeftPart(UriPartial.Authority) + "/";
}
}
catch { }
return null;
}
/// <summary>指定消息的工具轨迹(&lt;details&gt;)是否展开</summary> /// <summary>指定消息的工具轨迹(&lt;details&gt;)是否展开</summary>
private bool IsDetailsExpanded(int messageIndex) => _expandedDetails.Contains(messageIndex); private bool IsDetailsExpanded(int messageIndex) => _expandedDetails.Contains(messageIndex);
...@@ -1066,8 +1133,8 @@ namespace Tools.MyAgent ...@@ -1066,8 +1133,8 @@ namespace Tools.MyAgent
string text = $"本回复 {prompt}+{completion} tokens"; string text = $"本回复 {prompt}+{completion} tokens";
double cost = _engine.TotalCost - _replyCostBase; double cost = _engine.TotalCost - _replyCostBase;
if (cost > 0) text += $"(约 {CostEstimator.Format(cost)})"; if (cost > 0) text += $"(约 {CostEstimator.Format(cost)})";
// M4:本轮摘要(耗时/工具数/通道)—— 默认关闭,避免打扰(AgentConfig.ShowRunSummary) // M4:本轮摘要(耗时/工具数/通道)—— v4.4 起默认开启(AgentConfig.RunSummaryEnabled)
if (_config.ShowRunSummary && _lastRunSummary.Length > 0) text += " · " + _lastRunSummary; if (_config.RunSummaryEnabled && _lastRunSummary.Length > 0) text += " · " + _lastRunSummary;
return text; return text;
} }
......
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