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
3b9cf83e
Commit
3b9cf83e
authored
Aug 21, 2026
by
段廷瑞
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 智能体底层加固
parent
23830cb4
Changes
18
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
847 additions
and
135 deletions
+847
-135
.gitignore
.gitignore
+1
-0
AgentConfig.json
AppHome/Agent/config/AgentConfig.json
+0
-1
AgentEngine.cs
Tools/MyAgent/Agent/AgentEngine.cs
+173
-65
FailoverPolicy.cs
Tools/MyAgent/Agent/FailoverPolicy.cs
+16
-2
IOpenAiChatClient.cs
Tools/MyAgent/Agent/IOpenAiChatClient.cs
+3
-0
OpenAiCompatibleClient.cs
Tools/MyAgent/Agent/OpenAiCompatibleClient.cs
+113
-32
AgentConfig.cs
Tools/MyAgent/Config/AgentConfig.cs
+15
-0
AgentHistoryForm.cs
Tools/MyAgent/Forms/AgentHistoryForm.cs
+5
-0
AgentSettingsForm.Designer.cs
Tools/MyAgent/Forms/AgentSettingsForm.Designer.cs
+111
-0
AgentSettingsForm.cs
Tools/MyAgent/Forms/AgentSettingsForm.cs
+14
-0
BackupHelper.cs
Tools/MyAgent/Support/BackupHelper.cs
+2
-26
ChatSession.cs
Tools/MyAgent/Support/ChatSession.cs
+55
-0
CostEstimator.cs
Tools/MyAgent/Support/CostEstimator.cs
+139
-0
MarkdownRenderer.cs
Tools/MyAgent/Support/MarkdownRenderer.cs
+4
-8
StorageCleanup.cs
Tools/MyAgent/Support/StorageCleanup.cs
+98
-0
TextPatterns.cs
Tools/MyAgent/Support/TextPatterns.cs
+42
-0
wMyAgent.cs
Tools/MyAgent/wMyAgent.cs
+53
-1
Tools.csproj
Tools/Tools.csproj
+3
-0
No files found.
.gitignore
View file @
3b9cf83e
...
...
@@ -66,3 +66,4 @@ bin
/AppHome/AgentChat
/AppHome/Agent/history
/AppHome/Agent
/AppHome/Agent/config/AgentConfig.json
AppHome/Agent/config/AgentConfig.json
deleted
100644 → 0
View file @
23830cb4
{
"ApiKey"
:
"sk-bfaffa679dac4f7e9b2f128ee022022d"
,
"BaseUrl"
:
"https://api.deepseek.com"
,
"Model"
:
"deepseek-v4-flash"
,
"KnowledgeBasePath"
:
"D:
\\
working
\\
docs
\\
个人文档"
,
"Temperature"
:
0.3
,
"MaxTokens"
:
2048
}
\ No newline at end of file
Tools/MyAgent/Agent/AgentEngine.cs
View file @
3b9cf83e
This diff is collapsed.
Click to expand it.
Tools/MyAgent/Agent/FailoverPolicy.cs
View file @
3b9cf83e
...
...
@@ -41,6 +41,7 @@ namespace Tools.MyAgent.Agent
if
(!
enabled
&&
_failedOver
)
{
RestorePrimary
();
_failedOver
=
false
;
// 修复:复位备用状态标记,避免禁用后 IsActive 残留(误报“主模型已恢复”)
}
_consecutiveFailures
=
0
;
}
...
...
@@ -96,12 +97,25 @@ namespace Tools.MyAgent.Agent
_current
=
_primary
;
}
/// <summary>
/// 是否服务类故障(触发故障转移):限流(429)/服务端错误(5xx)/超时/连接失败。
/// <para>D4:带 HTTP 状态码的 4xx(401 Key 错误、400 参数错误等)属于配置/请求问题,
/// 不触发故障转移,直接报错提示用户修正,避免掩盖真实配置错误。</para>
/// </summary>
private
static
bool
IsServiceError
(
Exception
ex
)
{
var
msg
=
ex
?.
InnerException
?.
Message
??
ex
?.
Message
??
""
;
if
(
ex
==
null
)
return
false
;
var
msg
=
ex
.
InnerException
?.
Message
??
ex
.
Message
??
""
;
// 带状态码的 HTTP 失败:仅 429 / 5xx 视为服务故障;其余 4xx 不是
if
(
ex
is
System
.
Net
.
Http
.
HttpRequestException
)
{
if
(
ex
.
Data
?[
"HttpStatusCode"
]
is
int
status
)
return
status
==
429
||
status
>=
500
;
return
true
;
// 无状态码 = 未收到响应(连接层失败),视为服务故障
}
return
msg
.
Contains
(
"429"
)
||
msg
.
Contains
(
"500"
)
||
msg
.
Contains
(
"502"
)
||
msg
.
Contains
(
"503"
)
||
msg
.
Contains
(
"504"
)
||
msg
.
Contains
(
"超时"
)
||
msg
.
Contains
(
"timeout"
)
||
msg
.
Contains
(
"timed out"
)
||
ex
is
System
.
Net
.
Http
.
HttpRequestException
||
msg
.
Contains
(
"connection"
)
||
msg
.
Contains
(
"连接"
);
}
}
...
...
Tools/MyAgent/Agent/IOpenAiChatClient.cs
View file @
3b9cf83e
...
...
@@ -26,6 +26,9 @@ namespace Tools.MyAgent.Agent
/// <summary>连接测试</summary>
Task
<(
bool
Ok
,
string
Message
)>
TestConnectionAsync
(
CancellationToken
ct
);
/// <summary>按当前 UseSystemProxy 重建 HttpClient(代理开关变化时由调用方在非运行状态调用)</summary>
void
RebuildHttpClient
();
/// <summary>取消当前请求</summary>
void
Cancel
();
...
...
Tools/MyAgent/Agent/OpenAiCompatibleClient.cs
View file @
3b9cf83e
This diff is collapsed.
Click to expand it.
Tools/MyAgent/Config/AgentConfig.cs
View file @
3b9cf83e
namespace
Tools.MyAgent.Config
{
using
System.Collections.Generic
;
/// <summary>
/// 自研智能体配置(保存于 Agent/config/AgentConfig.json)。
/// <para>属性分两层:**核心项**(主设置页可见,首次配置只需这几项)与
...
...
@@ -92,5 +94,18 @@
/// <summary>是否启用图片提问(多模态,v2.2):启用后粘贴截图随提问以 image_url 发送,需视觉模型支持(如 DeepSeek-VL / qwen-vl);默认启用</summary>
public
bool
EnableImageInput
{
get
;
set
;
}
=
true
;
// ════════════════ 高级项:策略参数(R12) ════════════════
/// <summary>工具调用轮次上限(默认 15;超限后进入收尾回答轮,基于已收集信息汇总作答)</summary>
public
int
MaxToolRounds
{
get
;
set
;
}
=
15
;
/// <summary>历史消息条数上限(默认 60;安全网,真正的上下文控制由 token 预算完成)</summary>
public
int
MaxHistoryMessages
{
get
;
set
;
}
=
60
;
// ════════════════ 高级项:费用估算 ════════════════
/// <summary>费用估算价格覆盖(R6):模型名(子串匹配)→ "输入价,输出价"(元/百万 token);留空/缺省用内置价格表</summary>
public
Dictionary
<
string
,
string
>
CustomPrices
{
get
;
set
;
}
=
new
Dictionary
<
string
,
string
>();
}
}
Tools/MyAgent/Forms/AgentHistoryForm.cs
View file @
3b9cf83e
...
...
@@ -97,6 +97,11 @@ namespace Tools.MyAgent.Forms
})
{
if
(
dlg
.
ShowDialog
(
this
)
!=
DialogResult
.
OK
)
return
;
// D13:导出覆盖确认
if
(
File
.
Exists
(
dlg
.
FileName
)
&&
MessageBox
.
Show
(
this
,
$"导出文件已存在,是否覆盖?\n\n
{
dlg
.
FileName
}
"
,
"确认覆盖"
,
MessageBoxButtons
.
YesNo
,
MessageBoxIcon
.
Warning
)
!=
DialogResult
.
Yes
)
return
;
var
saved
=
HistoryManager
.
ExportSessionMarkdown
(
summary
.
FilePath
,
dlg
.
FileName
);
if
(
saved
!=
null
)
AgentUtils
.
Toast
(
$"已导出:
{
saved
}
"
,
3000
);
...
...
Tools/MyAgent/Forms/AgentSettingsForm.Designer.cs
View file @
3b9cf83e
...
...
@@ -63,6 +63,12 @@
this
.
numContext
=
new
System
.
Windows
.
Forms
.
NumericUpDown
();
this
.
lblContextHint
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
chkProxy
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
grpStrategy
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
lblToolRounds
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
numToolRounds
=
new
System
.
Windows
.
Forms
.
NumericUpDown
();
this
.
lblHistory
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
numHistory
=
new
System
.
Windows
.
Forms
.
NumericUpDown
();
this
.
lblStrategyHint
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
grpSem
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
chkSem
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
lblEmbUrl
=
new
System
.
Windows
.
Forms
.
Label
();
...
...
@@ -103,6 +109,9 @@
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numTemp
)).
BeginInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numMaxTokens
)).
BeginInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numContext
)).
BeginInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numToolRounds
)).
BeginInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numHistory
)).
BeginInit
();
this
.
grpStrategy
.
SuspendLayout
();
this
.
grpSem
.
SuspendLayout
();
this
.
tabPrompt
.
SuspendLayout
();
this
.
grpPrompt
.
SuspendLayout
();
...
...
@@ -371,6 +380,7 @@
//
this
.
tabAdv
.
AutoScroll
=
true
;
this
.
tabAdv
.
Controls
.
Add
(
this
.
grpGen
);
this
.
tabAdv
.
Controls
.
Add
(
this
.
grpStrategy
);
this
.
tabAdv
.
Controls
.
Add
(
this
.
grpSem
);
this
.
tabAdv
.
Location
=
new
System
.
Drawing
.
Point
(
4
,
26
);
this
.
tabAdv
.
Name
=
"tabAdv"
;
...
...
@@ -624,6 +634,97 @@
this
.
lblEmbHint
.
TabIndex
=
7
;
this
.
lblEmbHint
.
Text
=
"(在线服务需填 API Key;本地 Ollama 留空)"
;
//
// grpStrategy
//
this
.
grpStrategy
.
Controls
.
Add
(
this
.
lblStrategyHint
);
this
.
grpStrategy
.
Controls
.
Add
(
this
.
lblToolRounds
);
this
.
grpStrategy
.
Controls
.
Add
(
this
.
numToolRounds
);
this
.
grpStrategy
.
Controls
.
Add
(
this
.
lblHistory
);
this
.
grpStrategy
.
Controls
.
Add
(
this
.
numHistory
);
this
.
grpStrategy
.
Location
=
new
System
.
Drawing
.
Point
(
8
,
284
);
this
.
grpStrategy
.
Name
=
"grpStrategy"
;
this
.
grpStrategy
.
Size
=
new
System
.
Drawing
.
Size
(
596
,
92
);
this
.
grpStrategy
.
TabIndex
=
2
;
this
.
grpStrategy
.
TabStop
=
false
;
this
.
grpStrategy
.
Text
=
"策略参数"
;
//
// lblToolRounds
//
this
.
lblToolRounds
.
AutoSize
=
true
;
this
.
lblToolRounds
.
Location
=
new
System
.
Drawing
.
Point
(
16
,
32
);
this
.
lblToolRounds
.
Name
=
"lblToolRounds"
;
this
.
lblToolRounds
.
Size
=
new
System
.
Drawing
.
Size
(
70
,
17
);
this
.
lblToolRounds
.
TabIndex
=
0
;
this
.
lblToolRounds
.
Text
=
"工具轮上限"
;
//
// numToolRounds
//
this
.
numToolRounds
.
Location
=
new
System
.
Drawing
.
Point
(
92
,
28
);
this
.
numToolRounds
.
Maximum
=
new
decimal
(
new
int
[]
{
50
,
0
,
0
,
0
});
this
.
numToolRounds
.
Minimum
=
new
decimal
(
new
int
[]
{
1
,
0
,
0
,
0
});
this
.
numToolRounds
.
Name
=
"numToolRounds"
;
this
.
numToolRounds
.
Size
=
new
System
.
Drawing
.
Size
(
70
,
23
);
this
.
numToolRounds
.
TabIndex
=
1
;
this
.
numToolRounds
.
Value
=
new
decimal
(
new
int
[]
{
15
,
0
,
0
,
0
});
//
// lblHistory
//
this
.
lblHistory
.
AutoSize
=
true
;
this
.
lblHistory
.
Location
=
new
System
.
Drawing
.
Point
(
303
,
32
);
this
.
lblHistory
.
Name
=
"lblHistory"
;
this
.
lblHistory
.
Size
=
new
System
.
Drawing
.
Size
(
70
,
17
);
this
.
lblHistory
.
TabIndex
=
2
;
this
.
lblHistory
.
Text
=
"历史消息数"
;
//
// numHistory
//
this
.
numHistory
.
Increment
=
new
decimal
(
new
int
[]
{
5
,
0
,
0
,
0
});
this
.
numHistory
.
Location
=
new
System
.
Drawing
.
Point
(
383
,
28
);
this
.
numHistory
.
Maximum
=
new
decimal
(
new
int
[]
{
200
,
0
,
0
,
0
});
this
.
numHistory
.
Minimum
=
new
decimal
(
new
int
[]
{
3
,
0
,
0
,
0
});
this
.
numHistory
.
Name
=
"numHistory"
;
this
.
numHistory
.
Size
=
new
System
.
Drawing
.
Size
(
70
,
23
);
this
.
numHistory
.
TabIndex
=
3
;
this
.
numHistory
.
Value
=
new
decimal
(
new
int
[]
{
60
,
0
,
0
,
0
});
//
// lblStrategyHint
//
this
.
lblStrategyHint
.
AutoSize
=
true
;
this
.
lblStrategyHint
.
ForeColor
=
System
.
Drawing
.
Color
.
Gray
;
this
.
lblStrategyHint
.
Location
=
new
System
.
Drawing
.
Point
(
16
,
64
);
this
.
lblStrategyHint
.
MaximumSize
=
new
System
.
Drawing
.
Size
(
564
,
0
);
// 限制宽度自动换行,避免说明超出界面产生横向滚动条
this
.
lblStrategyHint
.
Name
=
"lblStrategyHint"
;
this
.
lblStrategyHint
.
TabIndex
=
8
;
this
.
lblStrategyHint
.
Text
=
"(轮 = 单次回答中工具调用的轮数上限;条 = 发送给模型的历史消息条数上限)"
;
//
// tabPrompt
//
this
.
tabPrompt
.
AutoScroll
=
true
;
...
...
@@ -888,6 +989,10 @@
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numTemp
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numMaxTokens
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numContext
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numToolRounds
)).
EndInit
();
((
System
.
ComponentModel
.
ISupportInitialize
)(
this
.
numHistory
)).
EndInit
();
this
.
grpStrategy
.
ResumeLayout
(
false
);
this
.
grpStrategy
.
PerformLayout
();
this
.
grpSem
.
ResumeLayout
(
false
);
this
.
grpSem
.
PerformLayout
();
this
.
tabPrompt
.
ResumeLayout
(
false
);
...
...
@@ -940,6 +1045,12 @@
private
System
.
Windows
.
Forms
.
NumericUpDown
numContext
;
private
System
.
Windows
.
Forms
.
Label
lblContextHint
;
private
System
.
Windows
.
Forms
.
CheckBox
chkProxy
;
private
System
.
Windows
.
Forms
.
GroupBox
grpStrategy
;
private
System
.
Windows
.
Forms
.
Label
lblToolRounds
;
private
System
.
Windows
.
Forms
.
NumericUpDown
numToolRounds
;
private
System
.
Windows
.
Forms
.
Label
lblHistory
;
private
System
.
Windows
.
Forms
.
NumericUpDown
numHistory
;
private
System
.
Windows
.
Forms
.
Label
lblStrategyHint
;
private
System
.
Windows
.
Forms
.
GroupBox
grpSem
;
private
System
.
Windows
.
Forms
.
CheckBox
chkSem
;
private
System
.
Windows
.
Forms
.
Label
lblEmbUrl
;
...
...
Tools/MyAgent/Forms/AgentSettingsForm.cs
View file @
3b9cf83e
...
...
@@ -125,6 +125,10 @@ namespace Tools.MyAgent.Forms
numContext
.
Value
=
_config
.
MaxContextTokens
>
0
?
_config
.
MaxContextTokens
:
32000
;
chkProxy
.
Checked
=
_config
.
UseSystemProxy
;
// 高级配置:策略参数(R12;越界回退默认,避免 NumericUpDown 赋值抛异常)
numToolRounds
.
Value
=
ClampToControl
(
_config
.
MaxToolRounds
,
numToolRounds
,
15
);
numHistory
.
Value
=
ClampToControl
(
_config
.
MaxHistoryMessages
,
numHistory
,
60
);
// 高级配置:语义检索
chkSem
.
Checked
=
_config
.
EnableSemanticSearch
;
txtEmbUrl
.
Text
=
_config
.
EmbeddingBaseUrl
;
...
...
@@ -399,6 +403,13 @@ namespace Tools.MyAgent.Forms
}
}
/// <summary>配置值回填 NumericUpDown 时做范围钳制(越界/非法回退默认,避免赋值抛 ArgumentOutOfRangeException)</summary>
private
static
decimal
ClampToControl
(
int
value
,
NumericUpDown
ctrl
,
int
fallback
)
{
if
(
value
<
(
int
)
ctrl
.
Minimum
||
value
>
(
int
)
ctrl
.
Maximum
)
value
=
fallback
;
return
value
;
}
/// <summary>保存:一次性回写全部选项卡改动(取消则放弃全部改动)</summary>
private
void
OnSave
(
object
sender
,
EventArgs
e
)
{
...
...
@@ -418,6 +429,9 @@ namespace Tools.MyAgent.Forms
_config
.
MaxTokens
=
(
int
)
numMaxTokens
.
Value
;
_config
.
MaxContextTokens
=
(
int
)
numContext
.
Value
;
_config
.
UseSystemProxy
=
chkProxy
.
Checked
;
// 高级配置:策略参数(R12)
_config
.
MaxToolRounds
=
(
int
)
numToolRounds
.
Value
;
_config
.
MaxHistoryMessages
=
(
int
)
numHistory
.
Value
;
// 高级配置:语义检索
_config
.
EnableSemanticSearch
=
chkSem
.
Checked
;
_config
.
EmbeddingBaseUrl
=
txtEmbUrl
.
Text
.
Trim
();
...
...
Tools/MyAgent/Support/BackupHelper.cs
View file @
3b9cf83e
...
...
@@ -55,36 +55,12 @@ namespace Tools.MyAgent.Support
}
}
/// <summary>导出当前会话为 Markdown(
复用 HistoryManager.ExportSessionMarkdown
)</summary>
/// <summary>导出当前会话为 Markdown(
D6:统一复用 HistoryManager.ExportSessionMarkdown 核心逻辑
)</summary>
public
static
string
ExportSession
(
ChatSession
session
,
string
outputPath
)
{
try
{
if
(
session
?.
Messages
==
null
)
return
null
;
var
sb
=
new
StringBuilder
();
sb
.
AppendLine
(
$"#
{
session
.
Title
??
"会话"
}
"
);
sb
.
AppendLine
();
sb
.
AppendLine
(
$"> 导出时间:
{
DateTime
.
Now
:
yyyy
-
MM
-
dd
HH
:
mm
}
"
);
sb
.
AppendLine
(
$"> 会话创建:
{
session
.
CreatedAt
:
yyyy
-
MM
-
dd
HH
:
mm
}
"
);
sb
.
AppendLine
();
foreach
(
var
m
in
session
.
Messages
)
{
string
role
=
string
.
Equals
(
m
.
Role
,
"user"
,
StringComparison
.
OrdinalIgnoreCase
)
?
"用户"
:
"助手"
;
sb
.
AppendLine
(
$"##
{
role
}
(
{
m
.
Timestamp
:
HH
:
mm
}
)"
);
sb
.
AppendLine
();
if
(!
string
.
IsNullOrEmpty
(
m
.
ThinkingContent
))
{
sb
.
AppendLine
(
$"> 思考:
{
m
.
ThinkingContent
.
Replace
(
"\r\n"
,
" "
).
Replace
(
'\r'
,
' '
).
Replace
(
'\n'
,
' '
)}
"
);
sb
.
AppendLine
();
}
sb
.
AppendLine
(
m
.
Content
??
""
);
sb
.
AppendLine
();
sb
.
AppendLine
(
"---"
);
sb
.
AppendLine
();
}
Directory
.
CreateDirectory
(
Path
.
GetDirectoryName
(
outputPath
));
File
.
WriteAllText
(
outputPath
,
sb
.
ToString
(),
new
UTF8Encoding
(
false
));
return
outputPath
;
return
HistoryManager
.
ExportSessionMarkdown
(
session
,
outputPath
);
}
catch
{
...
...
Tools/MyAgent/Support/ChatSession.cs
View file @
3b9cf83e
...
...
@@ -67,6 +67,51 @@ namespace Tools.MyAgent
return
idx
>=
0
?
fileName
.
Substring
(
idx
+
1
)
:
fileName
;
}
// ── D12 会话草稿(崩溃恢复):固定 _draft.json,不进入历史列表,下次启动自动恢复 ──
/// <summary>草稿文件名(固定名,历史列表不展示)</summary>
public
const
string
DraftFileName
=
"_draft"
;
/// <summary>草稿文件路径</summary>
public
static
string
DraftFilePath
(
string
historyDir
)
{
return
Path
.
Combine
(
historyDir
,
DraftFileName
+
".json"
);
}
/// <summary>保存会话草稿(覆盖式,供崩溃恢复;不产生历史条目)</summary>
public
static
void
SaveDraft
(
string
historyDir
,
List
<
ChatMessage
>
messages
)
{
try
{
SaveSession
(
historyDir
,
messages
,
DraftFileName
);
}
catch
{
}
}
/// <summary>读取会话草稿;不存在/损坏返回 null</summary>
public
static
List
<
ChatMessage
>
LoadDraft
(
string
historyDir
)
{
try
{
string
path
=
DraftFilePath
(
historyDir
);
if
(!
File
.
Exists
(
path
))
return
null
;
var
session
=
LoadSession
(
path
);
return
session
?.
Messages
!=
null
&&
session
.
Messages
.
Count
>
0
?
session
.
Messages
:
null
;
}
catch
{
return
null
;
}
}
/// <summary>删除会话草稿(新对话/恢复成功后调用)</summary>
public
static
void
DeleteDraft
(
string
historyDir
)
{
try
{
string
path
=
DraftFilePath
(
historyDir
);
if
(
File
.
Exists
(
path
))
File
.
Delete
(
path
);
}
catch
{
}
}
/// <summary>更新会话文件的标题(智能体生成主题后回写)</summary>
public
static
void
UpdateTitle
(
string
filePath
,
string
title
)
{
...
...
@@ -96,6 +141,9 @@ namespace Tools.MyAgent
var
files
=
Directory
.
GetFiles
(
historyDir
,
"*.json"
);
foreach
(
var
file
in
files
)
{
// D12:草稿文件是崩溃恢复用的临时会话,不进入历史列表
if
(
string
.
Equals
(
Path
.
GetFileNameWithoutExtension
(
file
),
HistoryManager
.
DraftFileName
,
StringComparison
.
OrdinalIgnoreCase
))
continue
;
try
{
var
session
=
LoadSession
(
file
);
...
...
@@ -181,6 +229,13 @@ namespace Tools.MyAgent
public
static
string
ExportSessionMarkdown
(
string
filePath
,
string
outPath
)
{
var
session
=
LoadSession
(
filePath
);
if
(
session
?.
Messages
==
null
)
return
null
;
return
ExportSessionMarkdown
(
session
,
outPath
);
}
/// <summary>会话对象 → Markdown 报告(导出核心逻辑;文件路径版与 BackupHelper 共用,D6 去重)</summary>
public
static
string
ExportSessionMarkdown
(
ChatSession
session
,
string
outPath
)
{
if
(
session
?.
Messages
==
null
)
return
null
;
var
sb
=
new
System
.
Text
.
StringBuilder
();
sb
.
AppendLine
(
$"#
{
session
.
Title
??
"会话"
}
"
);
...
...
Tools/MyAgent/Support/CostEstimator.cs
0 → 100644
View file @
3b9cf83e
using
System
;
using
System.Collections.Generic
;
namespace
Tools.MyAgent
{
/// <summary>
/// 会话费用估算(R6):内置常见模型价格表(元/百万 token),按模型名子串匹配(最长匹配优先);
/// 支持通过 AgentConfig.CustomPrices 覆盖/新增价格(配置化)。未收录模型返回 0,不产生误导性费用展示。
/// </summary>
public
static
class
CostEstimator
{
/// <summary>价格条目:每百万 token 价格(元,人民币)</summary>
public
sealed
class
Price
{
public
readonly
double
InputPer1M
;
// 每百万输入 token 价格(元)
public
readonly
double
OutputPer1M
;
// 每百万输出 token 价格(元)
public
Price
(
double
input
,
double
output
)
{
InputPer1M
=
input
;
OutputPer1M
=
output
;
}
}
// 内置价格表(元/百万 token,按 2026-08 常见公开价整理,近似值;可经 CustomPrices 覆盖)。
// 匹配规则:模型名包含 Key 即命中,多个命中取最长 Key(避免 gpt-4o 误配 gpt-4o-mini)。
private
static
readonly
(
string
Key
,
double
In
,
double
Out
)[]
BuiltinTable
=
{
(
"deepseek-reasoner"
,
4.0
,
16.0
),
(
"deepseek-chat"
,
2.0
,
8.0
),
(
"deepseek-v4-flash"
,
1.0
,
4.0
),
(
"claude-3-5-sonnet"
,
21.6
,
108.0
),
(
"claude-3-5-haiku"
,
2.9
,
14.4
),
(
"gpt-4o-mini"
,
1.1
,
4.4
),
(
"gpt-4o"
,
18.0
,
72.0
),
(
"gpt-4-turbo"
,
7.2
,
28.8
),
(
"gpt-4"
,
21.6
,
86.4
),
(
"gpt-3.5-turbo"
,
3.6
,
14.4
),
(
"qwen-vl"
,
2.0
,
8.0
),
(
"qwen-max"
,
2.4
,
9.6
),
(
"qwen-plus"
,
0.8
,
2.0
),
(
"qwen-turbo"
,
0.3
,
0.6
),
(
"glm-4-flash"
,
0.0
,
0.0
),
(
"glm-4"
,
1.0
,
1.0
),
(
"moonshot"
,
2.0
,
12.0
),
(
"kimi"
,
2.0
,
12.0
),
(
"ernie"
,
0.8
,
2.0
),
(
"doubao"
,
0.8
,
2.0
),
// 本地/开源模型:免费(Ollama、llama 系列、qwen 开源版、embedding 模型)
(
"ollama"
,
0.0
,
0.0
),
(
"llama"
,
0.0
,
0.0
),
(
"qwen2.5"
,
0.0
,
0.0
),
(
"bge-m3"
,
0.0
,
0.0
),
};
private
static
readonly
object
_lock
=
new
object
();
private
static
Dictionary
<
string
,
Price
>
_custom
=
new
Dictionary
<
string
,
Price
>(
StringComparer
.
OrdinalIgnoreCase
);
/// <summary>
/// 配置自定义价格覆盖(AgentConfig.CustomPrices):键 = 模型名子串,值 = "输入价,输出价"(元/百万 token)。
/// 自定义表优先于内置表(同为最长子串匹配);传入 null/空集合清除覆盖。
/// </summary>
public
static
void
Configure
(
IDictionary
<
string
,
string
>
customPrices
)
{
lock
(
_lock
)
{
_custom
=
new
Dictionary
<
string
,
Price
>(
StringComparer
.
OrdinalIgnoreCase
);
if
(
customPrices
==
null
)
return
;
foreach
(
var
kv
in
customPrices
)
{
if
(
string
.
IsNullOrWhiteSpace
(
kv
.
Key
))
continue
;
var
parts
=
(
kv
.
Value
??
""
).
Split
(
','
);
double
input
,
output
;
if
(
parts
.
Length
>=
2
&&
double
.
TryParse
(
parts
[
0
].
Trim
(),
out
input
)
&&
double
.
TryParse
(
parts
[
1
].
Trim
(),
out
output
)
&&
input
>=
0
&&
output
>=
0
)
{
_custom
[
kv
.
Key
.
Trim
()]
=
new
Price
(
input
,
output
);
}
}
}
}
/// <summary>估算一次调用费用(元);模型未收录或无用量返回 0</summary>
public
static
double
Estimate
(
string
model
,
int
promptTokens
,
int
completionTokens
)
{
if
(
promptTokens
<=
0
&&
completionTokens
<=
0
)
return
0
;
var
p
=
FindPrice
(
model
);
if
(
p
==
null
)
return
0
;
return
Math
.
Max
(
0
,
promptTokens
)
/
1
e6
*
p
.
InputPer1M
+
Math
.
Max
(
0
,
completionTokens
)
/
1
e6
*
p
.
OutputPer1M
;
}
/// <summary>费用文本:0/未收录显示 "—",小额保留 4 位小数,常规保留 2 位</summary>
public
static
string
Format
(
double
cost
)
{
if
(
cost
<=
0
)
return
"—"
;
return
cost
<
0.01
?
$"¥
{
cost
:
0.
####
}
"
:
$"¥
{
cost
:
0.
##
}
"
;
}
/// <summary>匹配价格条目:自定义表优先,其次内置表;均为最长子串匹配;未收录返回 null</summary>
internal
static
Price
FindPrice
(
string
model
)
{
if
(
string
.
IsNullOrEmpty
(
model
))
return
null
;
// 自定义表(最长子串匹配)
Price
bestCustom
=
null
;
int
bestLen
=
-
1
;
lock
(
_lock
)
{
foreach
(
var
kv
in
_custom
)
{
if
(
model
.
IndexOf
(
kv
.
Key
,
StringComparison
.
OrdinalIgnoreCase
)
>=
0
&&
kv
.
Key
.
Length
>
bestLen
)
{
bestLen
=
kv
.
Key
.
Length
;
bestCustom
=
kv
.
Value
;
}
}
}
if
(
bestCustom
!=
null
)
return
bestCustom
;
// 内置表(最长子串匹配)
string
bestKey
=
null
;
bestLen
=
-
1
;
foreach
(
var
t
in
BuiltinTable
)
{
if
(
model
.
IndexOf
(
t
.
Key
,
StringComparison
.
OrdinalIgnoreCase
)
>=
0
&&
t
.
Key
.
Length
>
bestLen
)
{
bestLen
=
t
.
Key
.
Length
;
bestKey
=
t
.
Key
;
}
}
if
(
bestKey
==
null
)
return
null
;
foreach
(
var
t
in
BuiltinTable
)
if
(
t
.
Key
==
bestKey
)
return
new
Price
(
t
.
In
,
t
.
Out
);
return
null
;
}
}
}
Tools/MyAgent/Support/MarkdownRenderer.cs
View file @
3b9cf83e
...
...
@@ -49,15 +49,11 @@ namespace Tools.MyAgent
private
const
int
ClLink
=
10
;
private
const
int
ClDeleted
=
11
;
// Matches Windows absolute paths: C:\..., D:\... with optional file extension
private
static
readonly
Regex
FilePathPattern
=
new
Regex
(
@"\b([A-Za-z]:[\\/](?:[^\s\\/:*?""<>|]+[\\/])*[^\s\\/:*?""<>|]+\.[a-zA-Z0-9]{1,10})\b"
,
RegexOptions
.
Compiled
);
// Matches Windows absolute paths: C:\..., D:\... with optional file extension(D6:与 wMyAgent 命中检测共用 TextPatterns)
private
static
readonly
Regex
FilePathPattern
=
TextPatterns
.
WindowsFilePath
;
// Matches UNC network paths: \\server\share\...(到空白/引号/常见句读标点为止)
private
static
readonly
Regex
UncFilePathPattern
=
new
Regex
(
@"\\\\[^\s<\"",。;:、!?))】》>]+"
,
RegexOptions
.
Compiled
);
// Matches UNC network paths: \\server\share\...(D6:共用 TextPatterns)
private
static
readonly
Regex
UncFilePathPattern
=
TextPatterns
.
UncFilePath
;
// Matches inline code that looks like a shell command
private
static
readonly
Regex
CommandPattern
=
new
Regex
(
...
...
Tools/MyAgent/Support/StorageCleanup.cs
0 → 100644
View file @
3b9cf83e
using
System
;
using
System.IO
;
namespace
Tools.MyAgent
{
/// <summary>
/// 存储自动清理(评估缺陷 #1):日志与中间产物按保留期自动清理,防止长期运行磁盘无限膨胀。
/// <para>口径:运行日志 / 检索空白 / 查询审计 / 工具结果 30 天;kb_view_image 中间产物 7 天;
/// _patch 补丁暂存 30 天;kb_write / doc_export 正式产出 90 天。
/// 会话历史为用户数据,保留历史弹窗的手动「清理 30 天前」按钮,不自动删除。</para>
/// </summary>
public
static
class
StorageCleanup
{
/// <summary>日志类文件保留天数(agent 日志 / search_misses / db 审计 / tool_results)</summary>
public
const
int
LogKeepDays
=
30
;
/// <summary>中间产物保留天数(kb_view_image 扫描件图片,可再生)</summary>
public
const
int
ImageKeepDays
=
7
;
/// <summary>补丁暂存保留天数(_patch)</summary>
public
const
int
PatchKeepDays
=
30
;
/// <summary>正式产出保留天数(kb_write / doc_export)</summary>
public
const
int
OutputKeepDays
=
90
;
/// <summary>
/// 执行清理(启动 / 配置变更时调用;内部容错,单项失败不影响其他)。
/// </summary>
/// <param name="agentRoot">默认 Agent 目录(output 位于其下);日志目录按程序基目录 Logs 计算</param>
/// <param name="logsRoot">日志根目录(默认 程序基目录/Logs;测试可注入临时目录)</param>
public
static
void
Run
(
string
agentRoot
,
string
logsRoot
=
null
)
{
if
(
logsRoot
==
null
)
logsRoot
=
Path
.
Combine
(
AppDomain
.
CurrentDomain
.
BaseDirectory
,
"Logs"
);
if
(
string
.
IsNullOrEmpty
(
logsRoot
))
return
;
var
now
=
DateTime
.
Now
;
int
removed
=
0
;
try
{
// 1. Logs 根目录:运行日志 / 检索空白 / 查询审计
if
(
Directory
.
Exists
(
logsRoot
))
{
removed
+=
CleanDir
(
logsRoot
,
"agent_*.log"
,
LogKeepDays
,
now
);
removed
+=
CleanDir
(
logsRoot
,
"search_misses.csv"
,
LogKeepDays
,
now
);
removed
+=
CleanDir
(
logsRoot
,
"db_query_audit_*.csv"
,
LogKeepDays
,
now
);
// 2. 工具结果落盘(超长工具结果溯源文件)
string
toolResults
=
Path
.
Combine
(
logsRoot
,
"tool_results"
);
if
(
Directory
.
Exists
(
toolResults
))
removed
+=
CleanDir
(
toolResults
,
"*.txt"
,
LogKeepDays
,
now
);
}
// 3. 输出目录:中间产物短保留,正式产出长保留
if
(!
string
.
IsNullOrEmpty
(
agentRoot
))
{
string
output
=
Path
.
Combine
(
agentRoot
,
"output"
);
if
(
Directory
.
Exists
(
output
))
{
removed
+=
CleanDir
(
Path
.
Combine
(
output
,
"kb_view_image"
),
"*.*"
,
ImageKeepDays
,
now
);
removed
+=
CleanDir
(
Path
.
Combine
(
output
,
"temp"
),
"*.*"
,
ImageKeepDays
,
now
);
// 截图粘贴中间产物(可再生,与 kb_view_image 同口径)
removed
+=
CleanDir
(
Path
.
Combine
(
output
,
"_patch"
),
"*.*"
,
PatchKeepDays
,
now
);
removed
+=
CleanDir
(
Path
.
Combine
(
output
,
"kb_write"
),
"*.*"
,
OutputKeepDays
,
now
);
removed
+=
CleanDir
(
Path
.
Combine
(
output
,
"doc_export"
),
"*.*"
,
OutputKeepDays
,
now
);
}
}
}
catch
{
/* 清理失败不影响启动 */
}
if
(
removed
>
0
)
AgentLogger
.
Log
(
$"存储自动清理完成:删除
{
removed
}
个过期文件(日志
{
LogKeepDays
}
天 / 中间产物
{
ImageKeepDays
}
~
{
PatchKeepDays
}
天 / 正式产出
{
OutputKeepDays
}
天)"
);
}
/// <summary>删除目录内超过保留期的文件;返回删除数(单项失败跳过)</summary>
private
static
int
CleanDir
(
string
dir
,
string
pattern
,
int
keepDays
,
DateTime
now
)
{
if
(!
Directory
.
Exists
(
dir
))
return
0
;
int
removed
=
0
;
try
{
foreach
(
var
f
in
Directory
.
GetFiles
(
dir
,
pattern
))
{
try
{
var
fi
=
new
FileInfo
(
f
);
if
((
now
-
fi
.
LastWriteTime
).
TotalDays
>
keepDays
)
{
File
.
Delete
(
f
);
removed
++;
}
}
catch
{
}
}
}
catch
{
}
return
removed
;
}
}
}
Tools/MyAgent/Support/TextPatterns.cs
0 → 100644
View file @
3b9cf83e
using
System.Text.RegularExpressions
;
namespace
Tools.MyAgent
{
/// <summary>
/// 智能体文本模式共享定义(D6 去重):MarkdownRenderer(RTF 渲染着色)与 wMyAgent(点击/悬停命中检测)
/// 共用同一组正则,避免两处定义漂移导致「渲染为链接但点不中 / 能点中但未着色」。
/// </summary>
internal
static
class
TextPatterns
{
/// <summary>Windows 绝对路径(带扩展名),如 C:\...\file.txt</summary>
public
static
readonly
Regex
WindowsFilePath
=
new
Regex
(
@"\b([A-Za-z]:[\\/](?:[^\s\\/:*?""<>|]+[\\/])*[^\s\\/:*?""<>|]+\.[a-zA-Z0-9]{1,10})\b"
,
RegexOptions
.
Compiled
);
/// <summary>UNC 网络路径 \\server\share\...(到空白/引号/常见句读标点为止)</summary>
public
static
readonly
Regex
UncFilePath
=
new
Regex
(
@"\\\\[^\s<\"",。;:、!?))】》>]+"
,
RegexOptions
.
Compiled
);
/// <summary>http/https 链接</summary>
public
static
readonly
Regex
HttpUrl
=
new
Regex
(
@"https?://[^\s\r\n<]+"
,
RegexOptions
.
Compiled
);
/// <summary>
/// 提示词注入标记(评估#4 参数级硬校验):写入内容命中这些语句时告警+留痕。
/// 覆盖中英文常见指令注入句式(“忽略以上规则/你是管理员/system prompt/ignore previous”等)。
/// </summary>
public
static
readonly
Regex
PromptInjectionMarker
=
new
Regex
(
@"(?i)(忽略以上|忽略之前|无视以上|无视之前|以上规则|你是管理员|你现在是|系统提示词|系统指令|系统提示|ignore\s+(all\s+)?(previous|prior|above)|disregard\s+(all\s+)?(previous|prior|above)|you\s+are\s+now|act\s+as\s+an?\s+admin|system\s*prompt)"
,
RegexOptions
.
Compiled
);
/// <summary>检测文本中的提示词注入标记,返回首个命中;无命中返回 null(评估#4)</summary>
public
static
string
FindInjectionMarker
(
string
text
)
{
if
(
string
.
IsNullOrEmpty
(
text
))
return
null
;
var
m
=
PromptInjectionMarker
.
Match
(
text
);
return
m
.
Success
?
m
.
Value
:
null
;
}
}
}
Tools/MyAgent/wMyAgent.cs
View file @
3b9cf83e
...
...
@@ -250,6 +250,8 @@ namespace Tools.MyAgent
// 输出目录固定为默认 Agent 文件夹(与所选知识库无关),确保其存在
try
{
Directory
.
CreateDirectory
(
DefaultKbRoot
);
}
catch
{
}
_kb
.
SetOutputRoot
(
DefaultKbRoot
);
// 评估#1:存储自动清理(日志 30 天 / 中间产物 7~30 天 / 正式产出 90 天;后台执行不阻塞启动)
try
{
System
.
Threading
.
Tasks
.
Task
.
Run
(()
=>
StorageCleanup
.
Run
(
DefaultKbRoot
));
}
catch
{
}
_kb
.
EnsureIndexStarted
();
// 后台构建知识库检索索引(含 docx/xlsx 内容),检索提速
RefreshScopeCombo
();
// 先归一化搜索范围(命中下拉项/归一化为全部),再应用
_kb
.
SetScope
(
_config
.
SearchScope
);
...
...
@@ -311,6 +313,8 @@ namespace Tools.MyAgent
// 父窗体激活会夺焦,故在 Shown / Activated / VisibleChanged 中再次确保焦点。
ActiveControl
=
txtInput
;
FocusInput
();
// D12:崩溃恢复——上次未保存的会话草稿载入聊天区
RestoreDraftOnStart
();
// 知识库文件数:启动时再触发一次后台统计(窗体句柄已就绪,可回刷欢迎语/状态栏)
_kb
.
EnsureStats
();
UpdateStatusIndicator
();
...
...
@@ -397,7 +401,13 @@ namespace Tools.MyAgent
kbText
=
$"
{
Path
.
GetFileName
(
_kb
.
Root
.
TrimEnd
(
'\\'
))}
(不可访问)"
;
else
kbText
=
"未选知识库"
;
lblStatus
.
Text
=
$"
{
_config
.
Model
}
·
{
kbText
}
"
;
// D16 用量统计:会话累计 token(prompt+completion),有调用记录时展示
// R6 费用估算:累计费用 > 0 时附带估算金额(按实际服务模型与价格表)
string
usage
=
_engine
.
TotalPromptTokens
+
_engine
.
TotalCompletionTokens
>
0
?
$" · 本会话
{
_engine
.
TotalPromptTokens
}
+
{
_engine
.
TotalCompletionTokens
}
tokens"
+
(
_engine
.
TotalCost
>
0
?
$"(约
{
CostEstimator
.
Format
(
_engine
.
TotalCost
)}
)"
:
""
)
:
""
;
lblStatus
.
Text
=
$"
{
_config
.
Model
}
·
{
kbText
}{
usage
}
"
;
lblStatus
.
ForeColor
=
Color
.
Gray
;
}
...
...
@@ -493,8 +503,11 @@ namespace Tools.MyAgent
RefreshPendingBar
();
_sessionSaved
=
true
;
_rtfCache
.
Clear
();
_engine
.
ResetUsage
();
// D16:新对话重置会话用量统计
HistoryManager
.
DeleteDraft
(
_historyDir
);
// D12:已存为历史,删除草稿
ResetInputState
();
RefreshChat
();
UpdateStatusIndicator
();
}
// ── 引擎事件处理 ─────────────────────────────────
...
...
@@ -534,6 +547,8 @@ namespace Tools.MyAgent
}
ResetInputState
();
RefreshChat
();
UpdateStatusIndicator
();
// D16:刷新会话累计用量
SaveDraftSession
();
// D12:每轮回答完成即更新草稿(崩溃恢复)
}
/// <summary>工具轨迹(#16):回答末尾追加可折叠 details,展示工具调用链(先宽后窄的检索过程)</summary>
...
...
@@ -582,6 +597,8 @@ namespace Tools.MyAgent
}
ResetInputState
();
RefreshChat
();
UpdateStatusIndicator
();
// D16:错误结束也刷新用量
SaveDraftSession
();
// D12:出错也保留当前对话为草稿
}
private
ChatMessage
LastAssistantMessage
()
...
...
@@ -2417,6 +2434,41 @@ namespace Tools.MyAgent
SaveCurrentSession
(
false
);
}
/// <summary>
/// 保存会话草稿(D12):覆盖写 _draft.json,供崩溃/异常退出后下次启动恢复;不产生历史条目。
/// 在每轮回答完成/出错后调用,草稿始终是最新状态。
/// </summary>
private
void
SaveDraftSession
()
{
if
(
_messages
.
Count
==
0
||
_engine
.
IsRunning
)
return
;
_historyDir
=
HistoryManager
.
EnsureHistoryDir
(
WorkingDirForHistory
());
HistoryManager
.
SaveDraft
(
_historyDir
,
_messages
);
}
/// <summary>
/// 启动时恢复上次未保存的会话草稿(D12):存在草稿则载入聊天区并删除草稿文件。
/// 在窗体 Load(句柄就绪)后调用。
/// </summary>
private
void
RestoreDraftOnStart
()
{
try
{
_historyDir
=
HistoryManager
.
EnsureHistoryDir
(
WorkingDirForHistory
());
var
draft
=
HistoryManager
.
LoadDraft
(
_historyDir
);
if
(
draft
==
null
||
draft
.
Count
==
0
)
return
;
_messages
.
Clear
();
_messages
.
AddRange
(
draft
);
_rtfCache
.
Clear
();
// 修复:恢复的草稿立即落为历史(否则删除草稿后若用户未继续对话直接关闭,
// 会话既不在历史中也无草稿,整段对话永久丢失)
SaveCurrentSession
(
false
);
HistoryManager
.
DeleteDraft
(
_historyDir
);
RefreshChat
();
AddSystemMessage
(
"(已恢复上次未保存的对话)"
);
}
catch
{
}
}
private
async
void
FireAndForgetRenameTopic
(
string
historyDir
,
string
date
,
string
fileName
,
List
<
ChatMessage
>
snapshot
)
{
try
...
...
Tools/Tools.csproj
View file @
3b9cf83e
...
...
@@ -206,10 +206,13 @@
<Compile
Include=
"MyAgent\Support\ChatMessage.cs"
/>
<Compile
Include=
"MyAgent\Support\ChatSession.cs"
/>
<Compile
Include=
"MyAgent\Support\ChatRtfCache.cs"
/>
<Compile
Include=
"MyAgent\Support\CostEstimator.cs"
/>
<Compile
Include=
"MyAgent\Support\MarkdownRenderer.cs"
/>
<Compile
Include=
"MyAgent\Support\NoCaretRichTextBox.cs"
>
<SubType>
Component
</SubType>
</Compile>
<Compile
Include=
"MyAgent\Support\StorageCleanup.cs"
/>
<Compile
Include=
"MyAgent\Support\TextPatterns.cs"
/>
<Compile
Include=
"MyAgent\wMyAgent.cs"
>
<SubType>
Form
</SubType>
</Compile>
...
...
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