Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
F
Ftt_Common
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Ftt_Common
Commits
0020a375
Commit
0020a375
authored
Oct 21, 2026
by
段廷瑞
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: Agent日志查看增加数据库链接配置;优化对话窗口交互
parent
2ecc48c8
Changes
13
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
753 additions
and
55 deletions
+753
-55
.gitignore
.gitignore
+1
-0
KbServiceSandboxTests.cs
AgentTest/KbServiceSandboxTests.cs
+19
-0
Tools.Agent.dll
AppHome/Tools.Agent.dll
+0
-0
Tools.Agent.xml
AppHome/Tools.Agent.xml
+76
-0
AgentRuntime.cs
Tools.Agent/AgentRuntime.cs
+23
-0
AgentDbConfigForm.Designer.cs
Tools.Agent/Forms/AgentDbConfigForm.Designer.cs
+223
-0
AgentDbConfigForm.cs
Tools.Agent/Forms/AgentDbConfigForm.cs
+126
-0
AgentDbConfigForm.resx
Tools.Agent/Forms/AgentDbConfigForm.resx
+71
-0
AgentRunLogForm.Designer.cs
Tools.Agent/Forms/AgentRunLogForm.Designer.cs
+24
-10
AgentRunLogForm.cs
Tools.Agent/Forms/AgentRunLogForm.cs
+34
-2
KbService.cs
Tools.Agent/Kb/KbService.cs
+32
-23
Tools.Agent.csproj
Tools.Agent/Tools.Agent.csproj
+9
-0
wMyAgent.cs
Tools.Agent/wMyAgent.cs
+115
-20
No files found.
.gitignore
View file @
0020a375
...
...
@@ -77,3 +77,4 @@ bin
/temp
/AppHome/_backup
/Tools.Agent.ProbeKbPref
/working
AgentTest/KbServiceSandboxTests.cs
View file @
0020a375
...
...
@@ -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
()
{
...
...
AppHome/Tools.Agent.dll
View file @
0020a375
No preview for this file type
AppHome/Tools.Agent.xml
View file @
0020a375
...
...
@@ -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):从渲染文本中扫出 📎 引用行并解析 路径/章节/行号。
...
...
Tools.Agent/AgentRuntime.cs
View file @
0020a375
...
...
@@ -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
);
...
...
Tools.Agent/Forms/AgentDbConfigForm.Designer.cs
0 → 100644
View file @
0020a375
namespace
Tools.MyAgent.Forms
{
partial
class
AgentDbConfigForm
{
/// <summary>
/// 必需的设计器变量。
/// </summary>
private
System
.
ComponentModel
.
IContainer
components
=
null
;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
protected
override
void
Dispose
(
bool
disposing
)
{
if
(
disposing
&&
(
components
!=
null
))
{
components
.
Dispose
();
}
base
.
Dispose
(
disposing
);
}
#
region
Windows
窗体设计器生成的代码
/// <summary>
/// 设计器支持所需的方法 - 不要修改
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private
void
InitializeComponent
()
{
this
.
lblServer
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
txtServer
=
new
System
.
Windows
.
Forms
.
TextBox
();
this
.
lblUser
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
txtUser
=
new
System
.
Windows
.
Forms
.
TextBox
();
this
.
lblPwd
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
txtPwd
=
new
System
.
Windows
.
Forms
.
TextBox
();
this
.
lblDb
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
cmbDb
=
new
System
.
Windows
.
Forms
.
ComboBox
();
this
.
lblTable
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
txtTable
=
new
System
.
Windows
.
Forms
.
TextBox
();
this
.
chkEnable
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
btnTest
=
new
System
.
Windows
.
Forms
.
Button
();
this
.
btnOK
=
new
System
.
Windows
.
Forms
.
Button
();
this
.
btnCancel
=
new
System
.
Windows
.
Forms
.
Button
();
this
.
SuspendLayout
();
//
// lblServer
//
this
.
lblServer
.
Location
=
new
System
.
Drawing
.
Point
(
16
,
21
);
this
.
lblServer
.
Name
=
"lblServer"
;
this
.
lblServer
.
Size
=
new
System
.
Drawing
.
Size
(
90
,
23
);
this
.
lblServer
.
Text
=
"服务器 IP"
;
this
.
lblServer
.
TextAlign
=
System
.
Drawing
.
ContentAlignment
.
MiddleLeft
;
//
// txtServer
//
this
.
txtServer
.
Location
=
new
System
.
Drawing
.
Point
(
112
,
18
);
this
.
txtServer
.
Name
=
"txtServer"
;
this
.
txtServer
.
Size
=
new
System
.
Drawing
.
Size
(
300
,
23
);
this
.
txtServer
.
TabIndex
=
0
;
//
// lblUser
//
this
.
lblUser
.
Location
=
new
System
.
Drawing
.
Point
(
16
,
55
);
this
.
lblUser
.
Name
=
"lblUser"
;
this
.
lblUser
.
Size
=
new
System
.
Drawing
.
Size
(
90
,
23
);
this
.
lblUser
.
Text
=
"账号"
;
this
.
lblUser
.
TextAlign
=
System
.
Drawing
.
ContentAlignment
.
MiddleLeft
;
//
// txtUser
//
this
.
txtUser
.
Location
=
new
System
.
Drawing
.
Point
(
112
,
52
);
this
.
txtUser
.
Name
=
"txtUser"
;
this
.
txtUser
.
Size
=
new
System
.
Drawing
.
Size
(
300
,
23
);
this
.
txtUser
.
TabIndex
=
1
;
//
// lblPwd
//
this
.
lblPwd
.
Location
=
new
System
.
Drawing
.
Point
(
16
,
89
);
this
.
lblPwd
.
Name
=
"lblPwd"
;
this
.
lblPwd
.
Size
=
new
System
.
Drawing
.
Size
(
90
,
23
);
this
.
lblPwd
.
Text
=
"密码"
;
this
.
lblPwd
.
TextAlign
=
System
.
Drawing
.
ContentAlignment
.
MiddleLeft
;
//
// txtPwd
//
this
.
txtPwd
.
Location
=
new
System
.
Drawing
.
Point
(
112
,
86
);
this
.
txtPwd
.
Name
=
"txtPwd"
;
this
.
txtPwd
.
Size
=
new
System
.
Drawing
.
Size
(
300
,
23
);
this
.
txtPwd
.
TabIndex
=
2
;
this
.
txtPwd
.
UseSystemPasswordChar
=
true
;
//
// lblDb
//
this
.
lblDb
.
Location
=
new
System
.
Drawing
.
Point
(
16
,
123
);
this
.
lblDb
.
Name
=
"lblDb"
;
this
.
lblDb
.
Size
=
new
System
.
Drawing
.
Size
(
90
,
23
);
this
.
lblDb
.
Text
=
"数据库"
;
this
.
lblDb
.
TextAlign
=
System
.
Drawing
.
ContentAlignment
.
MiddleLeft
;
//
// cmbDb(可编辑下拉:填好 IP/账号/密码后展开即拉取库列表)
//
this
.
cmbDb
.
DropDownStyle
=
System
.
Windows
.
Forms
.
ComboBoxStyle
.
DropDown
;
this
.
cmbDb
.
Location
=
new
System
.
Drawing
.
Point
(
112
,
120
);
this
.
cmbDb
.
Name
=
"cmbDb"
;
this
.
cmbDb
.
Size
=
new
System
.
Drawing
.
Size
(
300
,
25
);
this
.
cmbDb
.
TabIndex
=
3
;
this
.
cmbDb
.
DropDown
+=
new
System
.
EventHandler
(
this
.
OnDbDropDown
);
//
// lblTable
//
this
.
lblTable
.
Location
=
new
System
.
Drawing
.
Point
(
16
,
157
);
this
.
lblTable
.
Name
=
"lblTable"
;
this
.
lblTable
.
Size
=
new
System
.
Drawing
.
Size
(
90
,
23
);
this
.
lblTable
.
Text
=
"日志表名"
;
this
.
lblTable
.
TextAlign
=
System
.
Drawing
.
ContentAlignment
.
MiddleLeft
;
//
// txtTable
//
this
.
txtTable
.
Location
=
new
System
.
Drawing
.
Point
(
112
,
154
);
this
.
txtTable
.
Name
=
"txtTable"
;
this
.
txtTable
.
Size
=
new
System
.
Drawing
.
Size
(
300
,
23
);
this
.
txtTable
.
TabIndex
=
4
;
//
// chkEnable
//
this
.
chkEnable
.
Checked
=
true
;
this
.
chkEnable
.
CheckState
=
System
.
Windows
.
Forms
.
CheckState
.
Checked
;
this
.
chkEnable
.
Location
=
new
System
.
Drawing
.
Point
(
112
,
186
);
this
.
chkEnable
.
Name
=
"chkEnable"
;
this
.
chkEnable
.
Size
=
new
System
.
Drawing
.
Size
(
300
,
24
);
this
.
chkEnable
.
TabIndex
=
5
;
this
.
chkEnable
.
Text
=
"同时启用对话上报(Agent 执行后记录到该库)"
;
this
.
chkEnable
.
UseVisualStyleBackColor
=
true
;
//
// btnTest
//
this
.
btnTest
.
FlatStyle
=
System
.
Windows
.
Forms
.
FlatStyle
.
Flat
;
this
.
btnTest
.
Location
=
new
System
.
Drawing
.
Point
(
112
,
222
);
this
.
btnTest
.
Name
=
"btnTest"
;
this
.
btnTest
.
Size
=
new
System
.
Drawing
.
Size
(
88
,
28
);
this
.
btnTest
.
TabIndex
=
6
;
this
.
btnTest
.
Text
=
"测试连接"
;
this
.
btnTest
.
UseVisualStyleBackColor
=
true
;
this
.
btnTest
.
Click
+=
new
System
.
EventHandler
(
this
.
OnTestClick
);
//
// btnOK
//
this
.
btnOK
.
BackColor
=
System
.
Drawing
.
Color
.
FromArgb
(((
int
)(((
byte
)(
0
)))),
((
int
)(((
byte
)(
120
)))),
((
int
)(((
byte
)(
212
)))));
this
.
btnOK
.
DialogResult
=
System
.
Windows
.
Forms
.
DialogResult
.
None
;
this
.
btnOK
.
FlatStyle
=
System
.
Windows
.
Forms
.
FlatStyle
.
Flat
;
this
.
btnOK
.
ForeColor
=
System
.
Drawing
.
Color
.
White
;
this
.
btnOK
.
Location
=
new
System
.
Drawing
.
Point
(
244
,
222
);
this
.
btnOK
.
Name
=
"btnOK"
;
this
.
btnOK
.
Size
=
new
System
.
Drawing
.
Size
(
80
,
28
);
this
.
btnOK
.
TabIndex
=
7
;
this
.
btnOK
.
Text
=
"确定"
;
this
.
btnOK
.
UseVisualStyleBackColor
=
false
;
this
.
btnOK
.
Click
+=
new
System
.
EventHandler
(
this
.
OnOkClick
);
//
// btnCancel
//
this
.
btnCancel
.
DialogResult
=
System
.
Windows
.
Forms
.
DialogResult
.
Cancel
;
this
.
btnCancel
.
FlatStyle
=
System
.
Windows
.
Forms
.
FlatStyle
.
Flat
;
this
.
btnCancel
.
Location
=
new
System
.
Drawing
.
Point
(
332
,
222
);
this
.
btnCancel
.
Name
=
"btnCancel"
;
this
.
btnCancel
.
Size
=
new
System
.
Drawing
.
Size
(
80
,
28
);
this
.
btnCancel
.
TabIndex
=
8
;
this
.
btnCancel
.
Text
=
"取消"
;
this
.
btnCancel
.
UseVisualStyleBackColor
=
true
;
//
// AgentDbConfigForm
//
this
.
AcceptButton
=
this
.
btnOK
;
this
.
AutoScaleDimensions
=
new
System
.
Drawing
.
SizeF
(
7F
,
17F
);
this
.
AutoScaleMode
=
System
.
Windows
.
Forms
.
AutoScaleMode
.
Font
;
this
.
CancelButton
=
this
.
btnCancel
;
this
.
ClientSize
=
new
System
.
Drawing
.
Size
(
428
,
266
);
this
.
Controls
.
Add
(
this
.
lblServer
);
this
.
Controls
.
Add
(
this
.
txtServer
);
this
.
Controls
.
Add
(
this
.
lblUser
);
this
.
Controls
.
Add
(
this
.
txtUser
);
this
.
Controls
.
Add
(
this
.
lblPwd
);
this
.
Controls
.
Add
(
this
.
txtPwd
);
this
.
Controls
.
Add
(
this
.
lblDb
);
this
.
Controls
.
Add
(
this
.
cmbDb
);
this
.
Controls
.
Add
(
this
.
lblTable
);
this
.
Controls
.
Add
(
this
.
txtTable
);
this
.
Controls
.
Add
(
this
.
chkEnable
);
this
.
Controls
.
Add
(
this
.
btnTest
);
this
.
Controls
.
Add
(
this
.
btnOK
);
this
.
Controls
.
Add
(
this
.
btnCancel
);
this
.
Font
=
new
System
.
Drawing
.
Font
(
"Microsoft YaHei"
,
9F
);
this
.
FormBorderStyle
=
System
.
Windows
.
Forms
.
FormBorderStyle
.
FixedDialog
;
this
.
MaximizeBox
=
false
;
this
.
MinimizeBox
=
false
;
this
.
Name
=
"AgentDbConfigForm"
;
this
.
ShowInTaskbar
=
false
;
this
.
StartPosition
=
System
.
Windows
.
Forms
.
FormStartPosition
.
CenterParent
;
this
.
Text
=
"Agent日志 - 数据库连接配置"
;
this
.
ResumeLayout
(
false
);
this
.
PerformLayout
();
}
#
endregion
private
System
.
Windows
.
Forms
.
Label
lblServer
;
private
System
.
Windows
.
Forms
.
TextBox
txtServer
;
private
System
.
Windows
.
Forms
.
Label
lblUser
;
private
System
.
Windows
.
Forms
.
TextBox
txtUser
;
private
System
.
Windows
.
Forms
.
Label
lblPwd
;
private
System
.
Windows
.
Forms
.
TextBox
txtPwd
;
private
System
.
Windows
.
Forms
.
Label
lblDb
;
private
System
.
Windows
.
Forms
.
ComboBox
cmbDb
;
private
System
.
Windows
.
Forms
.
Label
lblTable
;
private
System
.
Windows
.
Forms
.
TextBox
txtTable
;
private
System
.
Windows
.
Forms
.
CheckBox
chkEnable
;
private
System
.
Windows
.
Forms
.
Button
btnTest
;
private
System
.
Windows
.
Forms
.
Button
btnOK
;
private
System
.
Windows
.
Forms
.
Button
btnCancel
;
}
}
Tools.Agent/Forms/AgentDbConfigForm.cs
0 → 100644
View file @
0020a375
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
;
}
}
}
Tools.Agent/Forms/AgentDbConfigForm.resx
0 → 100644
View file @
0020a375
<?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>
Tools.Agent/Forms/AgentRunLogForm.Designer.cs
View file @
0020a375
...
...
@@ -28,7 +28,7 @@
/// </summary>
private
void
InitializeComponent
()
{
this
.
panelTop
=
new
System
.
Windows
.
Forms
.
Panel
();
this
.
panelTop
=
new
System
.
Windows
.
Forms
.
FlowLayout
Panel
();
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
;
...
...
Tools.Agent/Forms/AgentRunLogForm.cs
View file @
0020a375
...
...
@@ -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
)
...
...
Tools.Agent/Kb/KbService.cs
View file @
0020a375
...
...
@@ -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
(
r
oot
,
statsCts
.
Token
))
foreach
(
var
file
in
SafeEnumerateFiles
(
searchR
oot
,
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
(
Search
Root
,
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
(
Search
Root
,
ct
))
{
var
ext
=
Path
.
GetExtension
(
file
).
ToLowerInvariant
();
if
(
string
.
IsNullOrEmpty
(
ext
))
ext
=
"(无扩展名)"
;
...
...
Tools.Agent/Tools.Agent.csproj
View file @
0020a375
...
...
@@ -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>
...
...
Tools.Agent/wMyAgent.cs
View file @
0020a375
...
...
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using
System.Drawing
;
using
System.IO
;
using
System.Linq
;
using
System.Runtime.InteropServices
;
using
System.Text.RegularExpressions
;
using
System.Threading
;
using
System.Threading.Tasks
;
...
...
@@ -26,6 +27,7 @@ namespace Tools.MyAgent
private
readonly
AgentEngine
_engine
;
/// <summary>聊天区增量渲染缓存:流式更新只重绘变化的消息块</summary>
private
readonly
ChatRtfCache
_rtfCache
=
new
ChatRtfCache
();
private
string
_lastRenderedChatRtf
=
""
;
/// <summary>聊天区控件:RichTextBox(RichBox,RTF 渲染)</summary>
private
RichTextBox
_rtfFallback
;
/// <summary>思考中旋转指示器(右下角悬浮;替代「思考文本铺满问答区」)</summary>
...
...
@@ -342,6 +344,22 @@ namespace Tools.MyAgent
/// <summary>默认知识库路径:程序输出目录下的 Agent 文件夹(未设置知识库时生效)</summary>
private
string
DefaultKbRoot
=>
_agentRoot
;
/// <summary>产品上下文真正固定的搜索子目录;旧宿主把根目录作为范围时视为未固定。</summary>
private
string
ProductFixedScope
{
get
{
if
(
_productContext
==
null
||
string
.
IsNullOrEmpty
(
_productContext
.
KnowledgeBaseRoot
)
||
string
.
IsNullOrEmpty
(
_productContext
.
KnowledgeBaseScope
))
return
""
;
return
IsPathUnder
(
_productContext
.
KnowledgeBaseRoot
,
_productContext
.
KnowledgeBaseScope
)
&&
IsPathUnder
(
_productContext
.
KnowledgeBaseScope
,
_productContext
.
KnowledgeBaseRoot
)
?
""
:
_productContext
.
KnowledgeBaseScope
;
}
}
/// <summary>Agent 配置文件:默认 Agent 目录下的 config\AgentConfig.json</summary>
private
string
AgentConfigFilePath
=>
Path
.
Combine
(
DefaultKbRoot
,
"config"
,
"AgentConfig.json"
);
...
...
@@ -493,8 +511,8 @@ namespace Tools.MyAgent
_config
.
ProjectId
=
_productContext
.
ProjectId
;
if
(!
string
.
IsNullOrEmpty
(
_productContext
.
KnowledgeBaseRoot
))
_config
.
KnowledgeBasePath
=
_productContext
.
KnowledgeBaseRoot
;
if
(!
string
.
IsNullOrEmpty
(
_productContext
.
KnowledgeBase
Scope
))
_config
.
SearchScope
=
_productContext
.
KnowledgeBase
Scope
;
if
(!
string
.
IsNullOrEmpty
(
ProductFixed
Scope
))
_config
.
SearchScope
=
ProductFixed
Scope
;
}
// 旧版配置一次性迁移(明文密码加密 / 自动启用系统提示词),落盘持久化
if
(
migrated
)
...
...
@@ -543,8 +561,8 @@ namespace Tools.MyAgent
_config
.
ProjectId
=
_productContext
.
ProjectId
;
if
(!
string
.
IsNullOrEmpty
(
_productContext
.
KnowledgeBaseRoot
))
_config
.
KnowledgeBasePath
=
_productContext
.
KnowledgeBaseRoot
;
if
(!
string
.
IsNullOrEmpty
(
_productContext
.
KnowledgeBase
Scope
))
_config
.
SearchScope
=
_productContext
.
KnowledgeBase
Scope
;
if
(!
string
.
IsNullOrEmpty
(
ProductFixed
Scope
))
_config
.
SearchScope
=
ProductFixed
Scope
;
}
_engine
.
Configure
(
_config
);
_engine
.
SetOptionalTools
(
_config
.
EnableOptionalTools
);
...
...
@@ -646,6 +664,7 @@ namespace Tools.MyAgent
Load
+=
(
s
,
e
)
=>
{
ApplyWindowModePadding
();
_defaultChatCursor
=
_rtfFallback
.
Cursor
;
// 打开智能体时默认焦点落到输入框。
// 本窗体嵌入首页 TabPage(TopLevel=false),Load 时父窗体尚未显示/激活,
...
...
@@ -663,6 +682,12 @@ namespace Tools.MyAgent
VisibleChanged
+=
(
s
,
e
)
=>
FocusInput
();
}
/// <summary>独立窗口增加 10px 内边距;嵌入宿主 Tab 时保持原有布局。</summary>
private
void
ApplyWindowModePadding
()
{
Padding
=
TopLevel
&&
Parent
==
null
?
new
Padding
(
10
)
:
Padding
.
Empty
;
}
/// <summary>RichTextBox 的点击/悬停交互绑定(启动时调用;新实例不会重复绑定)</summary>
private
void
WireRtfEvents
()
{
...
...
@@ -680,10 +705,12 @@ namespace Tools.MyAgent
private
string
BuildWelcomeText
()
{
_kb
.
EnsureStats
();
// 后台统计,不阻塞 UI;结果经 StatsRefreshed 回刷
string
kbInfo
=
_kb
.
IsValid
?
$"
{
_kb
.
Root
}
(
{
FormatFileCount
(
_kb
.
FileCount
)}
)"
:
!
string
.
IsNullOrEmpty
(
_kb
.
Root
)
?
$"
{
_kb
.
Root
}
(当前不可访问,请检查共享连接)"
string
displayRoot
=
string
.
IsNullOrEmpty
(
_kb
.
Scope
)
?
_kb
.
Root
:
_kb
.
Scope
;
bool
displayValid
=
!
string
.
IsNullOrEmpty
(
displayRoot
)
&&
Directory
.
Exists
(
displayRoot
);
string
kbInfo
=
displayValid
?
$"
{
displayRoot
}
(
{
FormatFileCount
(
_kb
.
FileCount
)}
)"
:
!
string
.
IsNullOrEmpty
(
displayRoot
)
?
$"
{
displayRoot
}
(当前不可访问,请检查共享连接)"
:
"未配置(点击「设置」选择文件夹)"
;
var
lines
=
new
System
.
Collections
.
Generic
.
List
<
string
>
...
...
@@ -707,15 +734,61 @@ namespace Tools.MyAgent
private
void
RefreshChatRtf
()
{
if
(
_rtfFallback
==
null
)
return
;
bool
followBottom
=
IsChatAtBottom
(
out
int
firstVisibleLine
);
string
rtf
=
_rtfCache
.
Build
(
_messages
,
BuildWelcomeText
(),
"助手"
,
CurrentUserName
(),
IsDetailsExpanded
);
if
(
string
.
Equals
(
_lastRenderedChatRtf
,
rtf
,
StringComparison
.
Ordinal
))
{
PositionThinkingSpinner
();
return
;
}
_lastRenderedChatRtf
=
rtf
;
_rtfFallback
.
Rtf
=
rtf
;
_rtfFallback
.
Select
(
_rtfFallback
.
TextLength
,
0
);
_rtfFallback
.
ScrollToCaret
();
if
(
followBottom
)
{
_rtfFallback
.
Select
(
_rtfFallback
.
TextLength
,
0
);
_rtfFallback
.
ScrollToCaret
();
}
else
{
RestoreChatScroll
(
firstVisibleLine
);
}
_answerImageSeq
=
_rtfCache
.
LastImageSequence
;
// 缩略图点击映射随本次 RTF 重建(v1.3.5)
RebuildAnswerRefs
();
// 引用行表随重绘重建(位置随重绘失效);流式期间跳过
PositionThinkingSpinner
();
// v1.3.7:重绘/滚动到底后重算锚点(标题下方)
}
/// <summary>判断聊天区重绘前是否已经位于底部;用户上滚查看历史时保持其视口。</summary>
private
bool
IsChatAtBottom
(
out
int
firstVisibleLine
)
{
firstVisibleLine
=
0
;
if
(
_rtfFallback
==
null
||
!
_rtfFallback
.
IsHandleCreated
||
_rtfFallback
.
TextLength
==
0
)
return
true
;
firstVisibleLine
=
SendMessage
(
_rtfFallback
.
Handle
,
EM_GETFIRSTVISIBLELINE
,
IntPtr
.
Zero
,
IntPtr
.
Zero
).
ToInt32
();
int
bottomChar
=
_rtfFallback
.
GetCharIndexFromPosition
(
new
Point
(
1
,
Math
.
Max
(
1
,
_rtfFallback
.
ClientSize
.
Height
-
2
)));
int
bottomLine
=
_rtfFallback
.
GetLineFromCharIndex
(
Math
.
Max
(
0
,
bottomChar
));
int
lineCount
=
SendMessage
(
_rtfFallback
.
Handle
,
EM_GETLINECOUNT
,
IntPtr
.
Zero
,
IntPtr
.
Zero
).
ToInt32
();
return
bottomLine
>=
lineCount
-
2
;
}
/// <summary>恢复聊天区重绘前的首行位置,避免后台刷新把用户拉回回答末尾。</summary>
private
void
RestoreChatScroll
(
int
firstVisibleLine
)
{
if
(
_rtfFallback
==
null
||
!
_rtfFallback
.
IsHandleCreated
)
return
;
_rtfFallback
.
Select
(
0
,
0
);
int
currentFirst
=
SendMessage
(
_rtfFallback
.
Handle
,
EM_GETFIRSTVISIBLELINE
,
IntPtr
.
Zero
,
IntPtr
.
Zero
).
ToInt32
();
SendMessage
(
_rtfFallback
.
Handle
,
EM_LINESCROLL
,
IntPtr
.
Zero
,
new
IntPtr
(
firstVisibleLine
-
currentFirst
));
}
private
const
int
EM_GETFIRSTVISIBLELINE
=
0x00CE
;
private
const
int
EM_LINESCROLL
=
0x00B6
;
private
const
int
EM_GETLINECOUNT
=
0x00BA
;
[
DllImport
(
"user32.dll"
,
CharSet
=
CharSet
.
Unicode
)]
private
static
extern
IntPtr
SendMessage
(
IntPtr
hWnd
,
int
msg
,
IntPtr
wParam
,
IntPtr
lParam
);
/// <summary>
/// 重建引用行表(P4):从渲染文本中扫出 📎 引用行并解析 路径/章节/行号。
/// <para>时序纪律:① 每次重绘后重建,并与悬停缓存一起复位;② 流式期间不扫描(每帧全文正则是 O(n) 白耗),
...
...
@@ -1013,12 +1086,12 @@ namespace Tools.MyAgent
return
;
}
string
kbText
;
if
(
_kb
.
IsValid
)
string
displayRoot
=
_kb
.
SearchRoot
;
if
(!
string
.
IsNullOrEmpty
(
displayRoot
))
{
kbText
=
Path
.
GetFileName
(
_kb
.
Root
.
TrimEnd
(
'\\'
));
// 状态栏仅显示知识库名,不拼接文件数量
string
name
=
Path
.
GetFileName
(
displayRoot
.
TrimEnd
(
'\\'
,
'/'
));
kbText
=
Directory
.
Exists
(
displayRoot
)
?
name
:
$"
{
name
}
(不可访问)"
;
}
else
if
(!
string
.
IsNullOrEmpty
(
_kb
.
Root
))
kbText
=
$"
{
Path
.
GetFileName
(
_kb
.
Root
.
TrimEnd
(
'\\'
))}
(不可访问)"
;
else
kbText
=
"未选知识库"
;
// 用量展示优化:状态栏不再拼接用量,用量改为附加到每条回复结尾
...
...
@@ -2719,26 +2792,48 @@ namespace Tools.MyAgent
foreach
(
var
f
in
folders
)
cmbScope
.
Items
.
Add
(
f
);
// 当前范围
不是下拉项之一(历史遗留任意目录)→ 归一化为「全部」并持久化
// 当前范围
必须仍位于知识库根目录且目录可访问;否则归一化为「全部」。
string
scope
=
_config
.
SearchScope
??
""
;
int
idx
=
0
;
if
(!
string
.
IsNullOrEmpty
(
scope
)
&&
!
string
.
IsNullOrEmpty
(
root
))
{
try
{
string
rootNorm
=
Path
.
GetFullPath
(
root
).
TrimEnd
(
'\\'
,
'/'
);
string
scopeNorm
=
Path
.
GetFullPath
(
scope
).
TrimEnd
(
'\\'
,
'/'
);
bool
matched
=
false
;
for
(
int
i
=
0
;
i
<
folders
.
Count
;
i
++)
if
(
string
.
Equals
(
scopeNorm
,
rootNorm
,
StringComparison
.
OrdinalIgnoreCase
)
||
!
IsPathUnder
(
rootNorm
,
scopeNorm
)
||
!
Directory
.
Exists
(
scopeNorm
))
{
scope
=
""
;
}
else
{
string
itemNorm
=
Path
.
GetFullPath
(
Path
.
Combine
(
root
,
folders
[
i
])).
TrimEnd
(
'\\'
,
'/'
)
;
if
(
string
.
Equals
(
scopeNorm
,
itemNorm
,
StringComparison
.
OrdinalIgnoreCase
)
)
bool
matched
=
false
;
for
(
int
i
=
0
;
i
<
folders
.
Count
;
i
++
)
{
string
itemNorm
=
Path
.
GetFullPath
(
Path
.
Combine
(
rootNorm
,
folders
[
i
])).
TrimEnd
(
'\\'
,
'/'
);
if
(!
string
.
Equals
(
scopeNorm
,
itemNorm
,
StringComparison
.
OrdinalIgnoreCase
))
continue
;
idx
=
i
+
1
;
matched
=
true
;
break
;
}
// 保留合法的深层目录,即使它不在默认的一/二级候选列表中。
if
(!
matched
)
{
string
relative
=
scopeNorm
.
Substring
(
rootNorm
.
Length
).
TrimStart
(
'\\'
,
'/'
).
Replace
(
'\\'
,
'/'
);
if
(!
string
.
IsNullOrEmpty
(
relative
))
{
cmbScope
.
Items
.
Add
(
relative
);
idx
=
cmbScope
.
Items
.
Count
-
1
;
}
else
{
scope
=
""
;
}
}
}
if
(!
matched
)
scope
=
""
;
}
catch
{
scope
=
""
;
}
if
(!
string
.
Equals
(
scope
,
_config
.
SearchScope
,
StringComparison
.
Ordinal
))
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment