Commit 0020a375 authored by 段廷瑞's avatar 段廷瑞

feat: Agent日志查看增加数据库链接配置;优化对话窗口交互

parent 2ecc48c8
......@@ -77,3 +77,4 @@ bin
/temp
/AppHome/_backup
/Tools.Agent.ProbeKbPref
/working
......@@ -138,6 +138,25 @@ namespace AgentTest
}
}
[Test]
public void FileCount_RefreshesWhenScopeChanges()
{
File.WriteAllText(Path.Combine(_root, "root.md"), "root");
File.WriteAllText(Path.Combine(_root, "docs", "b.md"), "b");
using (var refreshed = new System.Threading.ManualResetEventSlim(false))
{
_kb.StatsRefreshed += () => refreshed.Set();
_kb.EnsureStats();
Assert.True(refreshed.Wait(5000), "根目录统计应在后台完成");
Assert.Equal(3, _kb.FileCount, "根目录文件数应包含所有知识库文件");
refreshed.Reset();
_kb.SetScope(Path.Combine(_root, "docs"));
Assert.True(refreshed.Wait(5000), "切换范围后应重新统计");
Assert.Equal(2, _kb.FileCount, "切换范围后文件数应只包含当前目录");
}
}
[Test]
public void SetScope_OutsideRoot_FallsBackToRoot()
{
......
No preview for this file type
......@@ -59,6 +59,13 @@
RunDbConnection 已解密为明文。供无上下文的工具模块(Activator 无参创建)自取上报配置;失败返回 null。
</summary>
</member>
<member name="M:Tools.MyAgent.AgentRuntime.SaveLocalRunDbConfig(System.String,System.String,System.Boolean)">
<summary>
把对话上报连接(Agent 日志窗口"连接配置"保存)写回本机 AgentConfig.json。
<para>以 JObject 原文补丁方式只改 RunDb* 三个字段,**不触碰其他配置**(避免全量投影覆盖宿主注入项);
连接串经 <see cref="T:Tools.MyAgent.Config.SecretProtector"/> DPAPI 加密落盘,密码不落明文。返回是否成功。</para>
</summary>
</member>
<member name="T:Tools.MyAgent.AgentProductContext">
<summary>
宿主声明的产品上下文。产品身份来自注册表,项目路径由宿主按权限注入。
......@@ -1581,6 +1588,12 @@
<para>控件布局由 AgentRunLogForm.Designer.cs(设计器)实现;本文件仅含逻辑。</para>
</summary>
</member>
<member name="F:Tools.MyAgent.Forms.AgentRunLogForm._connection">
<summary>当前上报连接(明文,窗口内可经"连接配置"切换)。</summary>
</member>
<member name="F:Tools.MyAgent.Forms.AgentRunLogForm._table">
<summary>当前日志表名。</summary>
</member>
<member name="F:Tools.MyAgent.Forms.AgentRunLogForm._currentRunId">
<summary>当前选中行的 RunId(详情二次查询用)。</summary>
</member>
......@@ -1610,6 +1623,12 @@
RichEdit 静默解析失败 ⇒ 空白)。
</summary>
</member>
<member name="M:Tools.MyAgent.Forms.AgentRunLogForm.OnConnConfigClick(System.Object,System.EventArgs)">
<summary>
"连接配置":IP → 账号 → 密码 → 库(下拉拉取 sys.databases);
确定后写回 AgentConfig.json(DPAPI 加密)并刷新本窗口的连接与列表。
</summary>
</member>
<member name="M:Tools.MyAgent.Forms.AgentRunLogForm.Q(System.String)">
<summary>表名标识(白名单已在 Reporter/配置层校验;此处再防御)。</summary>
</member>
......@@ -1630,6 +1649,51 @@
使用代码编辑器修改此方法的内容。
</summary>
</member>
<member name="T:Tools.MyAgent.Forms.AgentDbConfigForm">
<summary>
Agent 日志窗口的数据库连接配置对话框(2026-10-19):IP / 数据库 / 账号 / 密码 / 表名,
支持测试连接;确定后由 AgentRunLogForm 经 <see cref="M:Tools.MyAgent.AgentRuntime.SaveLocalRunDbConfig(System.String,System.String,System.Boolean)"/>
写回 AgentConfig.json(连接串 DPAPI 加密落盘,密码不落明文)。
</summary>
</member>
<member name="F:Tools.MyAgent.Forms.AgentDbConfigForm._existing">
<summary>现有连接串(预填用;可能为空)。</summary>
</member>
<member name="M:Tools.MyAgent.Forms.AgentDbConfigForm.Prefill">
<summary>用现有连接串预填各输入框(解析失败保持空白,不阻断)。</summary>
</member>
<member name="M:Tools.MyAgent.Forms.AgentDbConfigForm.OnDbDropDown(System.Object,System.EventArgs)">
<summary>
数据库下拉展开时按当前 IP/账号/密码拉取实例库列表(SELECT name FROM sys.databases);
拉取失败静默保留已输入值(可手填)。每次展开都刷新,改过连接信息后无需重开对话框。
</summary>
</member>
<member name="M:Tools.MyAgent.Forms.AgentDbConfigForm.BuildConnectionString">
<summary>按输入拼装连接串(SqlConnectionStringBuilder 负责转义)。</summary>
</member>
<member name="P:Tools.MyAgent.Forms.AgentDbConfigForm.Table">
<summary>表名(含校验,非法字符在保存侧再防御一层)。</summary>
</member>
<member name="P:Tools.MyAgent.Forms.AgentDbConfigForm.EnableReporting">
<summary>是否同时启用对话上报(默认勾选,避免"测试通过但不落库"的旧坑)。</summary>
</member>
<member name="F:Tools.MyAgent.Forms.AgentDbConfigForm.components">
<summary>
必需的设计器变量。
</summary>
</member>
<member name="M:Tools.MyAgent.Forms.AgentDbConfigForm.Dispose(System.Boolean)">
<summary>
清理所有正在使用的资源。
</summary>
<param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
</member>
<member name="M:Tools.MyAgent.Forms.AgentDbConfigForm.InitializeComponent">
<summary>
设计器支持所需的方法 - 不要修改
使用代码编辑器修改此方法的内容。
</summary>
</member>
<member name="T:Tools.MyAgent.Forms.DocLineViewer">
<summary>
文档行定位查看器(P4 引用行点击):只读展示文本类文件,并定位高亮到指定行。
......@@ -3664,6 +3728,9 @@
<member name="P:Tools.MyAgent.wMyAgent.DefaultKbRoot">
<summary>默认知识库路径:程序输出目录下的 Agent 文件夹(未设置知识库时生效)</summary>
</member>
<member name="P:Tools.MyAgent.wMyAgent.ProductFixedScope">
<summary>产品上下文真正固定的搜索子目录;旧宿主把根目录作为范围时视为未固定。</summary>
</member>
<member name="P:Tools.MyAgent.wMyAgent.AgentConfigFilePath">
<summary>Agent 配置文件:默认 Agent 目录下的 config\AgentConfig.json</summary>
</member>
......@@ -3683,12 +3750,21 @@
<member name="M:Tools.MyAgent.wMyAgent.WorkingDirForHistory">
<summary>历史记录固定保存到默认 Agent 目录(与所选知识库无关)</summary>
</member>
<member name="M:Tools.MyAgent.wMyAgent.ApplyWindowModePadding">
<summary>独立窗口增加 10px 内边距;嵌入宿主 Tab 时保持原有布局。</summary>
</member>
<member name="M:Tools.MyAgent.wMyAgent.WireRtfEvents">
<summary>RichTextBox 的点击/悬停交互绑定(启动时调用;新实例不会重复绑定)</summary>
</member>
<member name="M:Tools.MyAgent.wMyAgent.RefreshChatRtf">
<summary>RichBox 模式:整段 RTF 重建(增量渲染缓存)</summary>
</member>
<member name="M:Tools.MyAgent.wMyAgent.IsChatAtBottom(System.Int32@)">
<summary>判断聊天区重绘前是否已经位于底部;用户上滚查看历史时保持其视口。</summary>
</member>
<member name="M:Tools.MyAgent.wMyAgent.RestoreChatScroll(System.Int32)">
<summary>恢复聊天区重绘前的首行位置,避免后台刷新把用户拉回回答末尾。</summary>
</member>
<member name="M:Tools.MyAgent.wMyAgent.RebuildAnswerRefs">
<summary>
重建引用行表(P4):从渲染文本中扫出 📎 引用行并解析 路径/章节/行号。
......
......@@ -134,6 +134,29 @@ namespace Tools.MyAgent
catch { return null; }
}
/// <summary>
/// 把对话上报连接(Agent 日志窗口"连接配置"保存)写回本机 AgentConfig.json。
/// <para>以 JObject 原文补丁方式只改 RunDb* 三个字段,**不触碰其他配置**(避免全量投影覆盖宿主注入项);
/// 连接串经 <see cref="Config.SecretProtector"/> DPAPI 加密落盘,密码不落明文。返回是否成功。</para>
/// </summary>
public static bool SaveLocalRunDbConfig(string plainConnection, string table, bool enabled)
{
try
{
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Agent", "config", "AgentConfig.json");
var root = File.Exists(path)
? Newtonsoft.Json.Linq.JObject.Parse(File.ReadAllText(path, System.Text.Encoding.UTF8))
: new Newtonsoft.Json.Linq.JObject();
root["RunDbEnabled"] = enabled;
root["RunDbConnection"] = Config.SecretProtector.Protect(plainConnection ?? "");
root["RunDbTable"] = string.IsNullOrWhiteSpace(table) ? "Agent_RunLog" : table.Trim();
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path));
File.WriteAllText(path, root.ToString(Newtonsoft.Json.Formatting.Indented), System.Text.Encoding.UTF8);
return true;
}
catch { return false; }
}
private void EnsureDefaultResources()
{
Directory.CreateDirectory(DataRoot);
......
This diff is collapsed.
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Windows.Forms;
namespace Tools.MyAgent.Forms
{
/// <summary>
/// Agent 日志窗口的数据库连接配置对话框(2026-10-19):IP / 数据库 / 账号 / 密码 / 表名,
/// 支持测试连接;确定后由 AgentRunLogForm 经 <see cref="AgentRuntime.SaveLocalRunDbConfig"/>
/// 写回 AgentConfig.json(连接串 DPAPI 加密落盘,密码不落明文)。
/// </summary>
public partial class AgentDbConfigForm : Form
{
/// <summary>现有连接串(预填用;可能为空)。</summary>
private readonly string _existing;
public AgentDbConfigForm(string existingConnection, string table)
{
_existing = existingConnection ?? "";
InitializeComponent();
Prefill();
txtTable.Text = string.IsNullOrWhiteSpace(table) ? "Agent_RunLog" : table.Trim();
}
/// <summary>用现有连接串预填各输入框(解析失败保持空白,不阻断)。</summary>
private void Prefill()
{
if (_existing.Trim().Length == 0) return;
try
{
var b = new SqlConnectionStringBuilder(_existing);
txtServer.Text = b.DataSource ?? "";
txtUser.Text = b.UserID ?? "";
txtPwd.Text = b.Password ?? "";
cmbDb.Text = b.InitialCatalog ?? "";
}
catch { /* 旧连接串解析失败 → 留空白让用户重填 */ }
}
/// <summary>
/// 数据库下拉展开时按当前 IP/账号/密码拉取实例库列表(SELECT name FROM sys.databases);
/// 拉取失败静默保留已输入值(可手填)。每次展开都刷新,改过连接信息后无需重开对话框。
/// </summary>
private void OnDbDropDown(object sender, EventArgs e)
{
try
{
if (txtServer.Text.Trim().Length == 0) return;
string current = cmbDb.Text; // 保留手填值
var names = new List<string>();
using (var conn = new SqlConnection(BuildConnectionString()))
using (var cmd = new SqlCommand(
"SELECT name FROM sys.databases WHERE state = 0 ORDER BY name", conn))
{
conn.Open();
using (var reader = cmd.ExecuteReader())
while (reader.Read())
names.Add(Convert.ToString(reader["name"]));
}
cmbDb.Items.Clear();
cmbDb.Items.AddRange(names.ToArray());
cmbDb.Text = current;
}
catch { /* 连不上/权限不足 → 下拉为空,用户仍可手填 */ }
}
/// <summary>按输入拼装连接串(SqlConnectionStringBuilder 负责转义)。</summary>
public string BuildConnectionString()
{
return new SqlConnectionStringBuilder
{
DataSource = txtServer.Text.Trim(),
InitialCatalog = cmbDb.Text.Trim(),
UserID = txtUser.Text.Trim(),
Password = txtPwd.Text,
ConnectTimeout = 5,
}.ConnectionString;
}
/// <summary>表名(含校验,非法字符在保存侧再防御一层)。</summary>
public string Table => txtTable.Text.Trim();
/// <summary>是否同时启用对话上报(默认勾选,避免"测试通过但不落库"的旧坑)。</summary>
public bool EnableReporting => chkEnable.Checked;
private void OnTestClick(object sender, EventArgs e)
{
try
{
if (txtServer.Text.Trim().Length == 0 || cmbDb.Text.Trim().Length == 0)
{
MessageBox.Show(this, "请先填写服务器与数据库。", "测试连接",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
Cursor = Cursors.WaitCursor;
using (var conn = new SqlConnection(BuildConnectionString()))
using (var cmd = new SqlCommand("SELECT 1", conn))
{
conn.Open();
cmd.ExecuteScalar();
}
Cursor = Cursors.Default;
MessageBox.Show(this, "连接成功。", "测试连接", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Cursor = Cursors.Default;
MessageBox.Show(this, "连接失败:" + ex.Message, "测试连接",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void OnOkClick(object sender, EventArgs e)
{
if (txtServer.Text.Trim().Length == 0 || cmbDb.Text.Trim().Length == 0)
{
MessageBox.Show(this, "服务器与数据库为必填项。", "保存",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
DialogResult = DialogResult.OK;
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
......@@ -28,7 +28,7 @@
/// </summary>
private void InitializeComponent()
{
this.panelTop = new System.Windows.Forms.Panel();
this.panelTop = new System.Windows.Forms.FlowLayoutPanel();
this.lblFrom = new System.Windows.Forms.Label();
this.dtpFrom = new System.Windows.Forms.DateTimePicker();
this.lblTo = new System.Windows.Forms.Label();
......@@ -39,6 +39,7 @@
this.cmbOutcome = new System.Windows.Forms.ComboBox();
this.btnRefresh = new System.Windows.Forms.Button();
this.btnExport = new System.Windows.Forms.Button();
this.btnConn = new System.Windows.Forms.Button();
this.splitMain = new System.Windows.Forms.SplitContainer();
this.dgv = new System.Windows.Forms.DataGridView();
this.tabDetails = new System.Windows.Forms.TabControl();
......@@ -60,8 +61,10 @@
this.statusStrip.SuspendLayout();
this.SuspendLayout();
//
// panelTop
// panelTop(FlowLayoutPanel:控件从左到右自动排列,嵌入窄宿主时自动换行,永不重叠)
//
this.panelTop.AutoSize = true;
this.panelTop.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.panelTop.Controls.Add(this.lblFrom);
this.panelTop.Controls.Add(this.dtpFrom);
this.panelTop.Controls.Add(this.lblTo);
......@@ -72,17 +75,17 @@
this.panelTop.Controls.Add(this.cmbOutcome);
this.panelTop.Controls.Add(this.btnRefresh);
this.panelTop.Controls.Add(this.btnExport);
this.panelTop.Controls.Add(this.btnConn);
this.panelTop.Dock = System.Windows.Forms.DockStyle.Top;
this.panelTop.Location = new System.Drawing.Point(0, 0);
this.panelTop.Name = "panelTop";
this.panelTop.Padding = new System.Windows.Forms.Padding(8, 6, 8, 4);
this.panelTop.Size = new System.Drawing.Size(1024, 46);
this.panelTop.Padding = new System.Windows.Forms.Padding(8, 2, 8, 4);
this.panelTop.TabIndex = 0;
//
// lblFrom
//
this.lblFrom.AutoSize = true;
this.lblFrom.Location = new System.Drawing.Point(11, 15);
this.lblFrom.Margin = new System.Windows.Forms.Padding(3, 10, 2, 0);
this.lblFrom.Name = "lblFrom";
this.lblFrom.Text = "开始";
//
......@@ -97,7 +100,7 @@
// lblTo
//
this.lblTo.AutoSize = true;
this.lblTo.Location = new System.Drawing.Point(159, 15);
this.lblTo.Margin = new System.Windows.Forms.Padding(6, 10, 2, 0);
this.lblTo.Name = "lblTo";
this.lblTo.Text = "结束";
//
......@@ -112,7 +115,7 @@
// lblUser
//
this.lblUser.AutoSize = true;
this.lblUser.Location = new System.Drawing.Point(309, 15);
this.lblUser.Margin = new System.Windows.Forms.Padding(12, 10, 2, 0);
this.lblUser.Name = "lblUser";
this.lblUser.Text = "用户";
//
......@@ -127,7 +130,7 @@
// lblOutcome
//
this.lblOutcome.AutoSize = true;
this.lblOutcome.Location = new System.Drawing.Point(491, 15);
this.lblOutcome.Margin = new System.Windows.Forms.Padding(12, 10, 2, 0);
this.lblOutcome.Name = "lblOutcome";
this.lblOutcome.Text = "结果";
//
......@@ -147,7 +150,7 @@
this.btnRefresh.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(120)))), ((int)(((byte)(212)))));
this.btnRefresh.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnRefresh.ForeColor = System.Drawing.Color.White;
this.btnRefresh.Location = new System.Drawing.Point(619, 9);
this.btnRefresh.Margin = new System.Windows.Forms.Padding(12, 4, 3, 0);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(76, 28);
this.btnRefresh.Text = "刷新";
......@@ -157,13 +160,23 @@
// btnExport
//
this.btnExport.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnExport.Location = new System.Drawing.Point(703, 9);
this.btnExport.Margin = new System.Windows.Forms.Padding(6, 4, 3, 0);
this.btnExport.Name = "btnExport";
this.btnExport.Size = new System.Drawing.Size(76, 28);
this.btnExport.Text = "导出CSV";
this.btnExport.UseVisualStyleBackColor = true;
this.btnExport.Click += new System.EventHandler(this.OnExportCsvClick);
//
// btnConn(窗口内数据库连接配置:IP/账号/密码/库下拉)
//
this.btnConn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnConn.Margin = new System.Windows.Forms.Padding(6, 4, 3, 0);
this.btnConn.Name = "btnConn";
this.btnConn.Size = new System.Drawing.Size(88, 28);
this.btnConn.Text = "连接配置";
this.btnConn.UseVisualStyleBackColor = true;
this.btnConn.Click += new System.EventHandler(this.OnConnConfigClick);
//
// splitMain
//
this.splitMain.Dock = System.Windows.Forms.DockStyle.Fill;
......@@ -320,6 +333,7 @@
private System.Windows.Forms.ComboBox cmbOutcome;
private System.Windows.Forms.Button btnRefresh;
private System.Windows.Forms.Button btnExport;
private System.Windows.Forms.Button btnConn;
private System.Windows.Forms.SplitContainer splitMain;
private System.Windows.Forms.DataGridView dgv;
private System.Windows.Forms.TabControl tabDetails;
......
......@@ -21,8 +21,10 @@ namespace Tools.MyAgent.Forms
{
private const int MaxRows = 500;
private readonly string _connection;
private readonly string _table;
/// <summary>当前上报连接(明文,窗口内可经"连接配置"切换)。</summary>
private string _connection;
/// <summary>当前日志表名。</summary>
private string _table;
/// <summary>当前选中行的 RunId(详情二次查询用)。</summary>
private string _currentRunId;
......@@ -295,6 +297,36 @@ namespace Tools.MyAgent.Forms
txtMetrics.Text = metrics;
}
// ══════════════ 连接配置(窗口内直配,2026-10-19) ══════════════
/// <summary>
/// "连接配置":IP → 账号 → 密码 → 库(下拉拉取 sys.databases);
/// 确定后写回 AgentConfig.json(DPAPI 加密)并刷新本窗口的连接与列表。
/// </summary>
private void OnConnConfigClick(object sender, EventArgs e)
{
using (var dlg = new AgentDbConfigForm(_connection, _table))
{
if (dlg.ShowDialog(this) != DialogResult.OK) return;
string plain = dlg.BuildConnectionString();
string table = dlg.Table;
if (!AgentRuntime.SaveLocalRunDbConfig(plain, table, dlg.EnableReporting))
{
AgentUtils.Toast("保存到配置文件失败(AgentConfig.json 不可写)", 4000);
return;
}
_connection = plain; // 本窗口立即生效,无需重开
_table = table;
_currentRunId = null;
SetDetailText("(已切换连接,请选择记录)", "(已切换连接)");
LoadUsers();
RefreshList();
AgentUtils.Toast(dlg.EnableReporting
? "已保存并启用对话上报;引擎侧在下次应用配置(重启客户端或设置页确定)后生效"
: "已保存连接;对话上报保持关闭(仅本窗口查询用)", 4000);
}
}
// ══════════════ 导出 ══════════════
private void OnExportCsvClick(object sender, EventArgs e)
......
......@@ -116,6 +116,7 @@ namespace Tools.MyAgent.Kb
private volatile int _fileCount = FileCountUnknown;
private volatile string _formatsSummary = "";
private volatile string _structureDigest = "";
private volatile string _statsScope = "";
private DateTime _fileCountStamp = DateTime.MinValue;
private readonly object _statsLock = new object();
private bool _statsRunning;
......@@ -146,7 +147,9 @@ namespace Tools.MyAgent.Kb
_structureDigest = "";
return;
}
if (_fileCount >= 0 && DateTime.Now - _fileCountStamp < StatsTtl)
if (_fileCount >= 0
&& string.Equals(_statsScope, SearchRoot ?? "", StringComparison.OrdinalIgnoreCase)
&& DateTime.Now - _fileCountStamp < StatsTtl)
return;
lock (_statsLock)
......@@ -156,7 +159,6 @@ namespace Tools.MyAgent.Kb
}
EnsureWatcher();
var root = Root;
var searchRoot = SearchRoot;
// P1 扫描可中断:统计扫描带超时取消(60 秒),超时后真正终止后台枚举,避免残留任务占满 IO
var statsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
......@@ -168,7 +170,7 @@ namespace Tools.MyAgent.Kb
var groups = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
try
{
foreach (var file in SafeEnumerateFiles(root, statsCts.Token))
foreach (var file in SafeEnumerateFiles(searchRoot, statsCts.Token))
{
count++;
var ext = Path.GetExtension(file).ToLowerInvariant();
......@@ -184,12 +186,19 @@ namespace Tools.MyAgent.Kb
catch (OperationCanceledException) { count = FileCountUnknown; }
catch { count = FileCountUnknown; }
try { digest = BuildStructureDigest(searchRoot, 30, 1000, IsConfiguredFileAllowed); } catch { digest = ""; }
bool scopeChanged = !string.Equals(SearchRoot, searchRoot, StringComparison.OrdinalIgnoreCase);
lock (_statsLock) { _statsRunning = false; }
try { statsCts.Dispose(); } catch { }
if (scopeChanged)
{
EnsureStats();
return;
}
_fileCount = count;
_formatsSummary = formats;
_structureDigest = digest;
_statsScope = searchRoot ?? "";
_fileCountStamp = DateTime.Now;
lock (_statsLock) { _statsRunning = false; }
try { statsCts.Dispose(); } catch { }
StatsRefreshed?.Invoke();
});
}
......@@ -221,6 +230,10 @@ namespace Tools.MyAgent.Kb
private void InvalidateStats()
{
_fileCount = FileCountUnknown;
_fileCountStamp = DateTime.MinValue;
_statsScope = "";
_formatsSummary = "";
_structureDigest = "";
EnsureStats();
// 索引不再全量失效:变更由 FileSystemWatcher → KbIndex 单文件增量处理(P1),
// KbIndex 内部 15 分钟兜底重扫负责补齐丢事件场景
......@@ -241,6 +254,7 @@ namespace Tools.MyAgent.Kb
_statsWatcher = null;
_fileCount = FileCountUnknown;
_fileCountStamp = DateTime.MinValue;
_statsScope = "";
_formatsSummary = "";
_structureDigest = "";
}
......@@ -382,26 +396,21 @@ namespace Tools.MyAgent.Kb
/// <summary>设置搜索范围;空表示整个知识库根目录。越界或非法路径会回退为整个知识库,避免越权访问。</summary>
public void SetScope(string path)
{
if (string.IsNullOrWhiteSpace(path))
string next = "";
if (!string.IsNullOrWhiteSpace(path))
{
Scope = "";
return;
}
try
{
string scope = NormPath(path);
if (string.IsNullOrEmpty(Root) || !IsWithin(NormPath(Root), scope))
try
{
Scope = "";
return;
string scope = NormPath(path);
if (!string.IsNullOrEmpty(Root) && IsWithin(NormPath(Root), scope))
next = scope;
}
Scope = scope;
}
catch
{
Scope = "";
catch { }
}
if (string.Equals(Scope, next, StringComparison.OrdinalIgnoreCase)) return;
Scope = next;
InvalidateStats();
}
/// <summary>实际检索根目录:限定目录优先,否则为知识库根目录</summary>
......@@ -678,7 +687,7 @@ namespace Tools.MyAgent.Kb
try
{
int count = 0;
foreach (var _ in SafeEnumerateFiles(Root, ct)) count++;
foreach (var _ in SafeEnumerateFiles(SearchRoot, ct)) count++;
return count;
}
catch (OperationCanceledException) { return 0; }
......@@ -692,7 +701,7 @@ namespace Tools.MyAgent.Kb
var groups = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
try
{
foreach (var file in SafeEnumerateFiles(Root, ct))
foreach (var file in SafeEnumerateFiles(SearchRoot, ct))
{
var ext = Path.GetExtension(file).ToLowerInvariant();
if (string.IsNullOrEmpty(ext)) ext = "(无扩展名)";
......
......@@ -147,6 +147,12 @@
<Compile Include="Forms\AgentRunLogForm.Designer.cs">
<DependentUpon>AgentRunLogForm.cs</DependentUpon>
</Compile>
<Compile Include="Forms\AgentDbConfigForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Forms\AgentDbConfigForm.Designer.cs">
<DependentUpon>AgentDbConfigForm.cs</DependentUpon>
</Compile>
<Compile Include="Forms\DocLineViewer.cs">
<SubType>Form</SubType>
</Compile>
......@@ -197,6 +203,9 @@
<EmbeddedResource Include="Forms\AgentRunLogForm.resx">
<DependentUpon>AgentRunLogForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Forms\AgentDbConfigForm.resx">
<DependentUpon>AgentDbConfigForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Forms\AgentSettingsForm.resx">
<DependentUpon>AgentSettingsForm.cs</DependentUpon>
</EmbeddedResource>
......
This diff is collapsed.
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