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
Hide 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
...
...
@@ -30,7 +30,7 @@ namespace Tools.MyAgent.Agent
// #31 故障转移
private
FailoverPolicy
_failover
;
private
OpenAiCompatible
Client
_backupClient
;
private
IOpenAiChat
Client
_backupClient
;
/// <summary>工具结果回填模型的字符上限(#42:超限压缩 + 完整结果落盘)。
/// 原 4000 过小且“头+尾”压缩丢失中间内容,迫使模型对长文件分页补读、成倍消耗工具轮;
...
...
@@ -70,11 +70,31 @@ namespace Tools.MyAgent.Agent
public
bool
IsRunning
{
get
;
private
set
;
}
/// <summary>
历史消息条数上限(安全网;真正的上下文控制由 token 预算完成
)</summary>
p
rivate
const
int
MaxHistoryMessages
=
60
;
/// <summary>
本会话累计 prompt tokens(D16 用量统计;由 SendAndLogAsync 累加,NewChat 时重置
)</summary>
p
ublic
int
TotalPromptTokens
{
get
;
private
set
;
}
/// <summary>工具调用轮次上限:防止模型反复调工具死循环烧 token / 卡 UI</summary>
private
const
int
MaxToolRounds
=
15
;
/// <summary>本会话累计 completion tokens(D16)</summary>
public
int
TotalCompletionTokens
{
get
;
private
set
;
}
/// <summary>本会话累计估算费用(元,R6;按实际服务模型与价格表累计,未收录模型不计费)</summary>
public
double
TotalCost
{
get
;
private
set
;
}
/// <summary>最近一次实际服务模型(R6:故障转移时备用模型按备用价格计费)</summary>
private
string
_lastServedModel
=
""
;
/// <summary>重置会话用量统计(新对话/加载历史时调用)</summary>
public
void
ResetUsage
()
{
TotalPromptTokens
=
0
;
TotalCompletionTokens
=
0
;
TotalCost
=
0
;
}
/// <summary>历史消息条数上限(安全网;真正的上下文控制由 token 预算完成)——R12 可配置(默认 60)</summary>
private
int
_maxHistoryMessages
=
60
;
/// <summary>工具调用轮次上限:防止模型反复调工具死循环烧 token / 卡 UI——R12 可配置(默认 15)</summary>
private
int
_maxToolRounds
=
15
;
/// <summary>回答被输出上限截断(finish_reason=length)时的自动续写轮次上限,防止无限续写烧 token</summary>
private
const
int
MaxContinuationRounds
=
3
;
...
...
@@ -94,6 +114,12 @@ namespace Tools.MyAgent.Agent
/// <summary>单个工具执行超时(秒):全盘检索等同步操作无法硬中断,超时后放弃等待</summary>
private
const
int
ToolTimeoutSeconds
=
120
;
/// <summary>
/// 运行代际:每次 RunAsync 自增。工具超时后残留的后台任务继续执行,
/// 其迟到副作用(OnFileProduced / 工具轨迹)据此抑制,避免跨轮污染。
/// </summary>
private
int
_runGeneration
;
public
AgentEngine
(
KbService
kb
)
{
_kb
=
kb
;
...
...
@@ -105,29 +131,39 @@ namespace Tools.MyAgent.Agent
public
void
Configure
(
AgentConfig
cfg
)
{
if
(
cfg
==
null
)
return
;
bool
proxyChanged
=
_client
.
UseSystemProxy
!=
cfg
.
UseSystemProxy
;
if
(
proxyChanged
&&
!
IsRunning
)
{
// 代理开关变化:重建 HttpClient(处理器在构造时绑定代理)
try
{
_client
.
Dispose
();
}
catch
{
}
_client
=
new
OpenAiCompatibleClient
{
UseSystemProxy
=
cfg
.
UseSystemProxy
};
// 同步故障转移策略的主客户端引用,避免其持有已释放的旧实例(修复:提问报“无法访问已释放的对象 HttpClient”)
_failover
.
SetPrimary
(
_client
);
}
// 代理开关:主/备用客户端统一按配置设置;非运行状态下重建 HttpClient
// (处理器在构造时绑定代理。修复:此前仅重建主客户端,备用客户端永远使用旧代理)
bool
primaryProxyChanged
=
_client
.
UseSystemProxy
!=
cfg
.
UseSystemProxy
;
bool
backupProxyChanged
=
_backupClient
.
UseSystemProxy
!=
cfg
.
UseSystemProxy
;
_client
.
UseSystemProxy
=
cfg
.
UseSystemProxy
;
_backupClient
.
UseSystemProxy
=
cfg
.
UseSystemProxy
;
if
(!
IsRunning
)
{
if
(
primaryProxyChanged
)
_client
.
RebuildHttpClient
();
if
(
backupProxyChanged
)
_backupClient
.
RebuildHttpClient
();
}
_client
.
BaseUrl
=
string
.
IsNullOrEmpty
(
cfg
.
BaseUrl
)
?
"https://api.deepseek.com"
:
cfg
.
BaseUrl
;
_client
.
ApiKey
=
cfg
.
ApiKey
??
""
;
_client
.
Model
=
string
.
IsNullOrEmpty
(
cfg
.
Model
)
?
"deepseek-v4-flash"
:
cfg
.
Model
;
_client
.
Temperature
=
cfg
.
Temperature
;
_client
.
MaxTokens
=
cfg
.
MaxTokens
>
0
?
cfg
.
MaxTokens
:
8192
;
_contextTokens
=
cfg
.
MaxContextTokens
>
0
?
cfg
.
MaxContextTokens
:
32000
;
// R12:策略参数(越界回退默认,防止手改配置文件导致异常行为)
_maxToolRounds
=
ClampStrategy
(
cfg
.
MaxToolRounds
,
1
,
50
,
15
);
_maxHistoryMessages
=
ClampStrategy
(
cfg
.
MaxHistoryMessages
,
3
,
200
,
60
);
_prompts
.
CustomLayer
=
cfg
.
CustomSystemPrompt
??
""
;
_prompts
.
SystemPromptEnabled
=
cfg
.
SystemPromptEnabled
;
_imageInputEnabled
=
cfg
.
EnableImageInput
;
// v2.2 多模态:图片提问开关
CostEstimator
.
Configure
(
cfg
.
CustomPrices
);
// R6 费用估算:应用自定义价格表覆盖
// #31 故障转移配置
_failover
.
Configure
(
cfg
.
EnableFailover
,
cfg
.
BackupBaseUrl
,
cfg
.
BackupModel
,
cfg
.
BackupApiKey
);
_backupClient
.
UseSystemProxy
=
cfg
.
UseSystemProxy
;
}
/// <summary>策略参数取值钳制(R12:配置越界回退默认)</summary>
private
static
int
ClampStrategy
(
int
value
,
int
min
,
int
max
,
int
fallback
)
{
return
value
>=
min
&&
value
<=
max
?
value
:
fallback
;
}
/// <summary>配置是否启用可选/高级工具</summary>
...
...
@@ -150,13 +186,14 @@ namespace Tools.MyAgent.Agent
{
_toolTrace
.
Clear
();
_continueRounds
=
0
;
_runGeneration
++;
var
messages
=
await
BuildMessagesAsync
(
history
,
_cts
.
Token
).
ConfigureAwait
(
false
);
var
tools
=
ToolSchema
.
Build
(
_includeOptionalTools
);
var
stopwatch
=
Stopwatch
.
StartNew
();
int
toolRounds
=
0
;
var
finalAnswer
=
new
StringBuilder
();
// 跨轮累计的回答正文(UI 事件为全量替换语义)
// 工具调用循环:最多
M
axToolRounds 轮;超限/超时时做「收尾回答轮」而非直接放弃(修复「第一个问题就触发 15 轮上限、颗粒无收」)
// 工具调用循环:最多
_m
axToolRounds 轮;超限/超时时做「收尾回答轮」而非直接放弃(修复「第一个问题就触发 15 轮上限、颗粒无收」)
while
(
true
)
{
_cts
.
Token
.
ThrowIfCancellationRequested
();
...
...
@@ -212,14 +249,14 @@ namespace Tools.MyAgent.Agent
// 达到工具调用上限 / 总时长上限:不再静默丢弃已收集的信息,注入收尾指令再做一轮
// 「无工具收尾回答轮」,让模型基于已收集的信息汇总作答(修复「第一个问题触发 15 轮上限颗粒无收」)
toolRounds
++;
bool
roundsUp
=
toolRounds
>=
M
axToolRounds
;
bool
roundsUp
=
toolRounds
>=
_m
axToolRounds
;
bool
timeUp
=
stopwatch
.
Elapsed
>
MaxRunDuration
;
if
(
roundsUp
||
timeUp
)
{
AgentLogger
.
Log
(
roundsUp
?
$"工具调用轮次达到上限(
{
M
axToolRounds
}
轮),启动收尾回答轮"
?
$"工具调用轮次达到上限(
{
_m
axToolRounds
}
轮),启动收尾回答轮"
:
"单次回答总时长达到上限,启动收尾回答轮"
);
OnTextDelta
?.
Invoke
(
$"\n\n(
{(
roundsUp
?
$"已达到工具调用轮次上限(
{
M
axToolRounds
}
轮)"
:
"已达到单次回答总时长上限"
)}
,正在基于已收集的信息汇总回答…)"
);
OnTextDelta
?.
Invoke
(
$"\n\n(
{(
roundsUp
?
$"已达到工具调用轮次上限(
{
_m
axToolRounds
}
轮)"
:
"已达到单次回答总时长上限"
)}
,正在基于已收集的信息汇总回答…)"
);
messages
.
Add
(
new
JObject
{
[
"role"
]
=
"user"
,
[
"content"
]
=
FinalizePrompt
});
var
final
=
await
SendAndLogAsync
(
messages
,
null
,
_cts
.
Token
).
ConfigureAwait
(
false
);
if
(
final
.
ReasoningContent
.
Length
>
0
)
...
...
@@ -291,8 +328,16 @@ namespace Tools.MyAgent.Agent
{
var
start
=
DateTime
.
UtcNow
;
var
result
=
await
SendWithFailoverAsync
(
messages
,
tools
,
ct
).
ConfigureAwait
(
false
);
// #29 运行日志:每次调用记录 model / token / 耗时 / 工具
AgentLogger
.
LogCall
(
_client
.
Model
,
result
.
PromptTokens
,
result
.
CompletionTokens
,
// D16 用量统计:会话累计(usage 缺失的响应跳过,不计入)
if
(
result
.
PromptTokens
>=
0
)
TotalPromptTokens
+=
result
.
PromptTokens
;
if
(
result
.
CompletionTokens
>=
0
)
TotalCompletionTokens
+=
result
.
CompletionTokens
;
// R6 费用估算:按实际服务模型与用量累计(未收录模型/无用量不计费)
if
(
result
.
PromptTokens
>
0
||
result
.
CompletionTokens
>
0
)
TotalCost
+=
CostEstimator
.
Estimate
(
_lastServedModel
,
result
.
PromptTokens
,
result
.
CompletionTokens
);
// #29 运行日志:每次调用记录 model / token / 耗时 / 工具(model 取实际服务模型:故障转移时备用模型)
AgentLogger
.
LogCall
(
string
.
IsNullOrEmpty
(
_lastServedModel
)
?
_client
.
Model
:
_lastServedModel
,
result
.
PromptTokens
,
result
.
CompletionTokens
,
(
long
)(
DateTime
.
UtcNow
-
start
).
TotalMilliseconds
,
string
.
Join
(
";"
,
result
.
ToolCalls
.
Select
(
tc
=>
tc
.
Name
+
"("
+
Clip
(
tc
.
Arguments
,
40
)
+
")"
)),
result
.
FinishReason
);
...
...
@@ -347,8 +392,11 @@ namespace Tools.MyAgent.Agent
try
{
var
r
=
await
_failover
.
CurrentClient
.
SendAsync
(
messages
,
tools
,
true
,
ct
).
ConfigureAwait
(
false
);
_lastServedModel
=
_failover
.
CurrentClient
.
Model
;
// R6:备用回答时按备用模型价格计费
// 先取切换状态再记成功:RecordSuccess 会立即复位 IsActive(否则“已恢复”提示永远不触发)
bool
wasOnBackup
=
_failover
.
IsActive
;
_failover
.
RecordSuccess
();
if
(
_failover
.
IsActive
)
if
(
wasOnBackup
)
{
OnTextDelta
?.
Invoke
(
"\n\n(主模型已恢复,切回主模型继续回答)"
);
}
...
...
@@ -360,6 +408,7 @@ namespace Tools.MyAgent.Agent
{
OnTextDelta
?.
Invoke
(
"\n\n(主模型服务异常,已自动切换至备用模型继续回答)"
);
AgentLogger
.
Log
(
"FAILOVER 切换至备用模型:"
+
_failover
.
CurrentClient
.
Model
);
_lastServedModel
=
_failover
.
CurrentClient
.
Model
;
// R6:切换后按备用模型价格计费
return
await
_failover
.
CurrentClient
.
SendAsync
(
messages
,
tools
,
true
,
ct
).
ConfigureAwait
(
false
);
}
throw
;
...
...
@@ -442,7 +491,7 @@ namespace Tools.MyAgent.Agent
// 只保留最近 N 条 user / assistant(最终正文),跳过流式占位;真正的上下文控制交给 token 预算
var
tail
=
history
==
null
?
new
List
<
ChatMessage
>()
:
history
.
Skip
(
Math
.
Max
(
0
,
history
.
Count
-
MaxHistoryMessages
)).
ToList
(
);
:
TakeTail
(
history
,
_maxHistoryMessages
);
foreach
(
var
m
in
tail
)
{
...
...
@@ -473,6 +522,21 @@ namespace Tools.MyAgent.Agent
return
list
;
}
/// <summary>
/// 截取最近 N 条消息;若截断后首条为 assistant(部分 OpenAI 兼容 API 拒绝首条非 user 消息),
/// 向前多取一条(交替布局下即为对应的 user 消息),保证请求以 user 开头。
/// </summary>
private
static
List
<
ChatMessage
>
TakeTail
(
IReadOnlyList
<
ChatMessage
>
history
,
int
maxMessages
)
{
int
skip
=
Math
.
Max
(
0
,
history
.
Count
-
maxMessages
);
if
(
skip
>
0
&&
skip
<
history
.
Count
&&
string
.
Equals
(
history
[
skip
].
Role
,
"assistant"
,
StringComparison
.
OrdinalIgnoreCase
))
{
skip
--;
}
return
history
.
Skip
(
skip
).
ToList
();
}
/// <summary>
/// 组装用户消息 content:启用图片提问(v2.2)且消息带有效附图时,
/// 返回多模态内容块数组(text + 多个 image_url,base64 data URL);否则返回纯文本。
...
...
@@ -547,15 +611,20 @@ namespace Tools.MyAgent.Agent
return
content
.
Value
<
string
>();
}
/// <summary>从消息列表头部(跳过 system)裁剪最早 user/assistant 对,直到预算内或只剩 3 条</summary>
/// <summary>从消息列表头部(跳过 system)裁剪最早消息,直到预算内或只剩 3 条。</summary>
/// <remarks>
/// 修复:原实现 firstUser 小于等于 1 即 break,而正常布局(system+user+...)下最早 user 恒在 index 1,
/// 「超预算裁剪最早消息」从未生效;现改为:最早 user 在 index 1 时连同其后一条 assistant 一起移除
/// (保持首条消息为 user 的 API 交替要求),否则移除 [前一 assistant, user] 对。
/// </remarks>
private
static
int
DropOldestUntilUnder
(
JArray
list
,
int
budget
)
{
int
dropped
=
0
;
while
(
list
.
Count
>
3
&&
EstimateTokens
(
list
)
>
budget
)
{
// 找到最早
一个 user 消息
// 找到最早
的 user 消息(跳过 system;不动最后一条消息——通常是当前问题)
int
firstUser
=
-
1
;
for
(
int
i
=
1
;
i
<
list
.
Count
;
i
++)
for
(
int
i
=
1
;
i
<
list
.
Count
-
1
;
i
++)
{
if
(
string
.
Equals
(
list
[
i
][
"role"
]?.
Value
<
string
>(),
"user"
,
StringComparison
.
OrdinalIgnoreCase
))
{
...
...
@@ -563,24 +632,44 @@ namespace Tools.MyAgent.Agent
break
;
}
}
if
(
firstUser
<=
1
)
break
;
// 只剩 system + 最近一对,不再裁
// 连同其上方的 assistant 消息(如果存在)一起移除
int
start
=
(
firstUser
>
1
&&
string
.
Equals
(
list
[
firstUser
-
1
][
"role"
]?.
Value
<
string
>(),
"assistant"
,
StringComparison
.
OrdinalIgnoreCase
))
?
firstUser
-
1
:
firstUser
;
int
count
=
(
start
==
firstUser
-
1
)
?
2
:
1
;
for
(
int
i
=
0
;
i
<
count
;
i
++)
if
(
firstUser
<
0
)
break
;
// 无可裁剪的 user
int
start
,
count
;
if
(
firstUser
>
1
&&
string
.
Equals
(
list
[
firstUser
-
1
][
"role"
]?.
Value
<
string
>(),
"assistant"
,
StringComparison
.
OrdinalIgnoreCase
))
{
start
=
firstUser
-
1
;
count
=
2
;
// 移除 [前一 assistant, user] 对
}
else
if
(
firstUser
==
1
&&
list
.
Count
>
3
&&
string
.
Equals
(
list
[
2
]?[
"role"
]?.
Value
<
string
>(),
"assistant"
,
StringComparison
.
OrdinalIgnoreCase
))
{
start
=
1
;
count
=
2
;
// 移除 [user, 后一 assistant] 对,保持首条为 user
}
else
{
break
;
// 其余情况不动(首条 user 无配对等),交由 LLM 摘要压缩兜底
}
if
(
list
.
Count
-
count
<
3
)
break
;
// 保底:至少保留 system + 2 条
for
(
int
k
=
0
;
k
<
count
;
k
++)
list
.
RemoveAt
(
start
);
dropped
+=
count
;
}
return
dropped
;
}
/// <summary>
/// 历史摘要缓存(#12):头部消息未变化时复用上次压缩结果,避免每轮重复调用 LLM
/// </summary>
private
string
_summarySource
=
""
;
// 上次压缩的原始输入文本
private
string
_summaryCache
=
""
;
// 上次压缩结果
/// <summary>
/// 把列表中最早的一段(最多 6 条)user/assistant 消息压缩为一条摘要消息(LLM 异步),
/// 返回压缩后保留的消息条数(0 = 未压缩成功)。
/// <para>头部消息与上次压缩一致时直接复用缓存摘要(不重复调用 LLM,会话内追加式增长下头部不变)。</para>
/// </summary>
private
async
Task
<
int
>
SummarizeEarliestAsync
(
JArray
list
,
CancellationToken
ct
)
{
...
...
@@ -602,22 +691,32 @@ namespace Tools.MyAgent.Agent
if
(
earliestText
.
Length
==
0
)
return
0
;
string
summary
;
try
if
(
string
.
Equals
(
earliestText
,
_summarySource
,
StringComparison
.
Ordinal
)
&&
!
string
.
IsNullOrEmpty
(
_summaryCache
))
{
var
prompt
=
new
JArray
{
new
JObject
{
[
"role"
]
=
"system"
,
[
"content"
]
=
HistorySummaryPrompt
},
new
JObject
{
[
"role"
]
=
"user"
,
[
"content"
]
=
earliestText
}
};
summary
=
await
_client
.
CompleteAsync
(
prompt
,
512
,
ct
).
ConfigureAwait
(
false
);
summary
=
_summaryCache
;
// 头部消息未变化:复用上次摘要,避免重复 LLM 调用
}
catch
else
{
return
0
;
try
{
var
prompt
=
new
JArray
{
new
JObject
{
[
"role"
]
=
"system"
,
[
"content"
]
=
HistorySummaryPrompt
},
new
JObject
{
[
"role"
]
=
"user"
,
[
"content"
]
=
earliestText
}
};
summary
=
await
_client
.
CompleteAsync
(
prompt
,
512
,
ct
).
ConfigureAwait
(
false
);
}
catch
{
return
0
;
}
summary
=
(
summary
??
""
).
Trim
();
if
(
summary
.
Length
==
0
)
return
0
;
if
(
summary
.
Length
>
800
)
summary
=
summary
.
Substring
(
0
,
800
);
_summarySource
=
earliestText
;
_summaryCache
=
summary
;
}
summary
=
(
summary
??
""
).
Trim
();
if
(
summary
.
Length
==
0
)
return
0
;
if
(
summary
.
Length
>
800
)
summary
=
summary
.
Substring
(
0
,
800
);
for
(
int
i
=
1
;
i
<=
take
;
i
++)
list
.
RemoveAt
(
1
);
...
...
@@ -673,9 +772,10 @@ namespace Tools.MyAgent.Agent
/// </summary>
private
async
Task
<
string
>
ExecuteToolAsync
(
ApiToolCall
tc
,
CancellationToken
ct
)
{
int
gen
=
_runGeneration
;
try
{
var
execTask
=
Task
.
Run
(()
=>
ExecuteTool
(
tc
));
var
execTask
=
Task
.
Run
(()
=>
ExecuteTool
(
tc
,
gen
));
var
timeoutTask
=
Task
.
Delay
(
TimeSpan
.
FromSeconds
(
ToolTimeoutSeconds
),
CancellationToken
.
None
);
var
cancelTask
=
Task
.
Delay
(
Timeout
.
Infinite
,
ct
);
var
done
=
await
Task
.
WhenAny
(
execTask
,
timeoutTask
,
cancelTask
).
ConfigureAwait
(
false
);
...
...
@@ -692,7 +792,7 @@ namespace Tools.MyAgent.Agent
}
}
private
string
ExecuteTool
(
ApiToolCall
tc
)
private
string
ExecuteTool
(
ApiToolCall
tc
,
int
gen
)
{
var
sw
=
System
.
Diagnostics
.
Stopwatch
.
StartNew
();
try
...
...
@@ -717,37 +817,45 @@ namespace Tools.MyAgent.Agent
if
(
tool
.
RequiresKb
&&
(!
_kb
.
ConnectIfNeeded
()
||
!
_kb
.
IsSearchValid
))
return
"错误:搜索范围不可用(知识库目录不存在/共享未连接/限定目录不可访问)。本地路径请在设置中选择;共享路径请检查服务器与共享名(如 "
+
@"\\服务器\共享名"
+
")及账号密码配置。"
;
var
ctx
=
new
ToolContext
(
_kb
,
f
=>
OnFileProduced
?.
Invoke
(
f
));
// 超时/取消后残留的后台任务:抑制迟到副作用(输出文件误报/轨迹污染)
var
ctx
=
new
ToolContext
(
_kb
,
f
=>
{
if
(
gen
==
System
.
Threading
.
Volatile
.
Read
(
ref
_runGeneration
))
OnFileProduced
?.
Invoke
(
f
);
});
string
result
=
tool
.
Execute
(
args
,
ctx
);
sw
.
Stop
();
// #29 工具日志
AgentLogger
.
LogTool
(
tc
.
Name
,
tool
.
Describe
(
args
),
sw
.
ElapsedMilliseconds
,
SummarizeToolResult
(
result
));
// #42 结果压缩:超长结果压缩后回填模型,完整结果落盘供溯源
result
=
CompressToolResult
(
result
,
tc
.
Name
);
lock
(
_toolTrace
)
if
(
gen
==
System
.
Threading
.
Volatile
.
Read
(
ref
_runGeneration
)
)
{
_toolTrace
.
Add
(
new
ToolTraceEntry
lock
(
_toolTrace
)
{
Name
=
tc
.
Name
,
Args
=
tool
.
Describe
(
args
)
??
""
,
DurationMs
=
sw
.
ElapsedMilliseconds
,
ResultSummary
=
SummarizeToolResult
(
result
),
});
_toolTrace
.
Add
(
new
ToolTraceEntry
{
Name
=
tc
.
Name
,
Args
=
tool
.
Describe
(
args
)
??
""
,
DurationMs
=
sw
.
ElapsedMilliseconds
,
ResultSummary
=
SummarizeToolResult
(
result
),
});
}
}
return
result
;
}
catch
(
Exception
ex
)
{
sw
.
Stop
();
lock
(
_toolTrace
)
if
(
gen
==
System
.
Threading
.
Volatile
.
Read
(
ref
_runGeneration
)
)
{
_toolTrace
.
Add
(
new
ToolTraceEntry
lock
(
_toolTrace
)
{
Name
=
tc
.
Name
,
Args
=
tc
.
Name
,
DurationMs
=
sw
.
ElapsedMilliseconds
,
ResultSummary
=
"异常:"
+
ex
.
Message
,
});
_toolTrace
.
Add
(
new
ToolTraceEntry
{
Name
=
tc
.
Name
,
Args
=
tc
.
Name
,
DurationMs
=
sw
.
ElapsedMilliseconds
,
ResultSummary
=
"异常:"
+
ex
.
Message
,
});
}
}
return
$"错误:
{
ex
.
Message
}
"
;
}
...
...
@@ -798,7 +906,7 @@ namespace Tools.MyAgent.Agent
if
(
ex
is
ObjectDisposedException
||
msg
.
Contains
(
"已释放"
)
||
msg
.
Contains
(
"disposed"
))
return
"内部组件已释放(客户端可能被重建),请重试;若持续出现请重启智能体页面。"
;
if
(
msg
.
Contains
(
"401"
))
return
"API Key 无效或未配置,请在「设置」中检查。"
;
if
(
msg
.
Contains
(
"402"
))
return
"账户余额不足,请到
DeepSeek 平台充值
。"
;
if
(
msg
.
Contains
(
"402"
))
return
"账户余额不足,请到
模型服务商平台充值或检查额度
。"
;
if
(
msg
.
Contains
(
"429"
))
return
"请求过于频繁,请稍后再试。"
;
if
(
msg
.
Contains
(
"413"
))
return
"请求内容过长(上下文超出模型上限):已自动裁剪历史消息重试仍失败。请精简输入内容或开启新对话。"
;
if
(
msg
.
Contains
(
"400"
))
...
...
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
...
...
@@ -38,7 +38,9 @@ namespace Tools.MyAgent.Agent
/// </summary>
public
sealed
class
OpenAiCompatibleClient
:
IOpenAiChatClient
,
IDisposable
{
private
readonly
HttpClient
_http
;
private
readonly
object
_httpLock
=
new
object
();
private
HttpClient
_http
;
private
bool
_useSystemProxy
=
true
;
public
string
BaseUrl
{
get
;
set
;
}
=
"https://api.deepseek.com"
;
public
string
ApiKey
{
get
;
set
;
}
=
""
;
...
...
@@ -47,8 +49,37 @@ namespace Tools.MyAgent.Agent
public
int
MaxTokens
{
get
;
set
;
}
=
8192
;
/// <summary>是否使用系统代理(默认 true;公司内网常用)。
/// 优先读取 HTTPS_PROXY / HTTP_PROXY 环境变量,否则用系统 IE 代理设置。</summary>
public
bool
UseSystemProxy
{
get
;
set
;
}
=
true
;
/// 优先读取 HTTPS_PROXY / HTTP_PROXY 环境变量,否则用系统 IE 代理设置。
/// 修复:原实现属性为死字段(handler 在构造时绑定,开关从不生效);
/// 现由调用方在非运行状态调用 <see cref="RebuildHttpClient"/> 按当前值重建。</summary>
public
bool
UseSystemProxy
{
get
=>
_useSystemProxy
;
set
=>
_useSystemProxy
=
value
;
}
/// <summary>当前 HttpClient(线程安全读取)</summary>
private
HttpClient
Http
{
get
{
lock
(
_httpLock
)
return
_http
;
}
}
/// <summary>
/// 按当前 UseSystemProxy 重建 HttpClient(处理器在构造时绑定代理)。
/// <para>调用时机:引擎配置变更且非运行状态(进行中请求会因旧实例释放而失败)。</para>
/// </summary>
public
void
RebuildHttpClient
()
{
lock
(
_httpLock
)
{
var
old
=
_http
;
_http
=
new
HttpClient
(
CreateHandler
(
_useSystemProxy
))
{
Timeout
=
TimeSpan
.
FromSeconds
(
180
),
};
try
{
old
?.
Dispose
();
}
catch
{
}
}
}
/// <summary>最大尝试次数(1 次初始 + 2 次重试)</summary>
private
const
int
MaxAttempts
=
3
;
...
...
@@ -61,20 +92,41 @@ namespace Tools.MyAgent.Agent
/// <summary>推理内容增量(deepseek-reasoner 等思维链模型)</summary>
public
event
Action
<
string
>
OnReasoningDelta
;
private
CancellationTokenSource
_cts
;
private
readonly
object
_ctsLock
=
new
object
();
private
readonly
HashSet
<
CancellationTokenSource
>
_active
=
new
HashSet
<
CancellationTokenSource
>();
public
OpenAiCompatibleClient
()
{
_http
=
new
HttpClient
(
CreateHandler
())
_http
=
new
HttpClient
(
CreateHandler
(
_useSystemProxy
))
{
Timeout
=
TimeSpan
.
FromSeconds
(
180
),
};
}
/// <summary>构造 HttpClientHandler:按配置挂系统代理 / 环境变量代理;直连则禁用代理</summary>
private
static
HttpMessageHandler
CreateHandler
()
/// <summary>登记活动请求 CTS(D5:并发请求互不干扰)</summary>
private
void
RegisterCts
(
CancellationTokenSource
cts
)
{
lock
(
_ctsLock
)
_active
.
Add
(
cts
);
}
/// <summary>注销并释放请求 CTS(请求结束/失败时调用)</summary>
private
void
UnregisterCts
(
CancellationTokenSource
cts
)
{
lock
(
_ctsLock
)
_active
.
Remove
(
cts
);
try
{
cts
.
Dispose
();
}
catch
{
}
}
/// <summary>构造 HttpClientHandler:useSystemProxy=false 显式直连(不读环境变量/系统代理,
/// 避免公司代理拦截本地/内网服务);true 时按配置挂系统代理 / 环境变量代理</summary>
private
static
HttpMessageHandler
CreateHandler
(
bool
useSystemProxy
)
{
var
handler
=
new
HttpClientHandler
{
UseProxy
=
true
};
if
(!
useSystemProxy
)
{
// 显式关闭代理:直连
handler
.
UseProxy
=
false
;
return
handler
;
}
try
{
var
proxy
=
ResolveProxy
();
...
...
@@ -158,14 +210,11 @@ namespace Tools.MyAgent.Agent
body
[
"tool_choice"
]
=
"auto"
;
}
// 每个请求使用独立的 CTS
(避免并发请求互相取消
)
// 每个请求使用独立的 CTS
并登记到活动集合(D5:取消/释放只影响本请求,Cancel() 全量中断
)
var
cts
=
CancellationTokenSource
.
CreateLinkedTokenSource
(
ct
);
RegisterCts
(
cts
);
try
{
// 保存当前请求的 CTS,供 Cancel() 中断
try
{
_cts
?.
Dispose
();
}
catch
{
}
_cts
=
cts
;
int
contextTrimCount
=
0
;
bool
maxTokensReduced
=
false
;
bool
temperatureRemoved
=
false
;
...
...
@@ -227,7 +276,7 @@ namespace Tools.MyAgent.Agent
}
finally
{
try
{
cts
.
Dispose
();
}
catch
{
}
UnregisterCts
(
cts
);
}
}
...
...
@@ -242,6 +291,8 @@ namespace Tools.MyAgent.Agent
/// <summary>
/// 裁剪 messages 中最早的一对 user/assistant 消息(保留 system 与最新内容),返回是否有可裁剪内容。
/// <para>用于 413 上下文超限时的自动降级:丢失的只是最早对话,当前轮次消息完整保留。</para>
/// <para>修复:被移除的 assistant 若带 tool_calls,其后的 tool 消息引用已不存在的 tool_call_id,
/// 会残留孤立 tool 消息导致下一次请求 400——移除时一并清理。</para>
/// </summary>
private
static
bool
TrimOldestPair
(
JArray
messages
)
{
...
...
@@ -255,10 +306,18 @@ namespace Tools.MyAgent.Agent
:
i
;
for
(
int
k
=
start
;
k
<=
i
;
k
++)
messages
.
RemoveAt
(
start
);
// 清理孤立 tool 消息(紧随被移除 assistant(tool_calls) 之后)
while
(
start
<
messages
.
Count
&&
string
.
Equals
(
messages
[
start
][
"role"
]?.
Value
<
string
>(),
"tool"
,
StringComparison
.
OrdinalIgnoreCase
))
messages
.
RemoveAt
(
start
);
return
true
;
}
}
messages
.
RemoveAt
(
1
);
// 兜底路径同样清理孤立 tool 消息
while
(
1
<
messages
.
Count
&&
string
.
Equals
(
messages
[
1
][
"role"
]?.
Value
<
string
>(),
"tool"
,
StringComparison
.
OrdinalIgnoreCase
))
messages
.
RemoveAt
(
1
);
return
messages
.
Count
>
1
;
}
...
...
@@ -269,7 +328,7 @@ namespace Tools.MyAgent.Agent
req
.
Headers
.
TryAddWithoutValidation
(
"Authorization"
,
"Bearer "
+
ApiKey
);
req
.
Content
=
new
StringContent
(
body
.
ToString
(
Formatting
.
None
),
Encoding
.
UTF8
,
"application/json"
);
using
(
var
resp
=
await
_h
ttp
.
SendAsync
(
req
,
HttpCompletionOption
.
ResponseHeadersRead
,
ct
).
ConfigureAwait
(
false
))
using
(
var
resp
=
await
H
ttp
.
SendAsync
(
req
,
HttpCompletionOption
.
ResponseHeadersRead
,
ct
).
ConfigureAwait
(
false
))
{
if
(!
resp
.
IsSuccessStatusCode
)
{
...
...
@@ -303,10 +362,14 @@ namespace Tools.MyAgent.Agent
return
o
is
int
i
?
i
:
0
;
}
/// <summary>取消
当前
请求(中断流式读取,返回已取消)</summary>
/// <summary>取消
全部活动
请求(中断流式读取,返回已取消)</summary>
public
void
Cancel
()
{
try
{
_cts
?.
Cancel
();
}
catch
{
}
lock
(
_ctsLock
)
{
foreach
(
var
cts
in
_active
)
try
{
cts
.
Cancel
();
}
catch
{
}
}
}
/// <summary>连接测试:发送最小补全请求,返回(是否成功,信息)</summary>
...
...
@@ -328,7 +391,7 @@ namespace Tools.MyAgent.Agent
req
.
Headers
.
TryAddWithoutValidation
(
"Authorization"
,
"Bearer "
+
ApiKey
);
req
.
Content
=
new
StringContent
(
body
.
ToString
(
Formatting
.
None
),
Encoding
.
UTF8
,
"application/json"
);
var
resp
=
await
_h
ttp
.
SendAsync
(
req
,
HttpCompletionOption
.
ResponseHeadersRead
,
cts
.
Token
).
ConfigureAwait
(
false
);
var
resp
=
await
H
ttp
.
SendAsync
(
req
,
HttpCompletionOption
.
ResponseHeadersRead
,
cts
.
Token
).
ConfigureAwait
(
false
);
string
json
=
await
resp
.
Content
.
ReadAsStringAsync
().
ConfigureAwait
(
false
);
if
(!
resp
.
IsSuccessStatusCode
)
...
...
@@ -359,24 +422,33 @@ namespace Tools.MyAgent.Agent
[
"model"
]
=
Model
,
[
"messages"
]
=
messages
,
[
"stream"
]
=
false
,
[
"temperature"
]
=
0.3
,
[
"max_tokens"
]
=
maxTokens
,
};
// 修复:推理模型(deepseek-reasoner 等)不支持 temperature,省略避免 400
// (此前硬编码 0.3,导致会话标题生成/历史摘要压缩对推理模型静默失败)
if
(!
IsReasoningModel
(
Model
))
body
[
"temperature"
]
=
0.3
;
try
{
_cts
?.
Dispose
();
}
catch
{
}
_cts
=
CancellationTokenSource
.
CreateLinkedTokenSource
(
ct
);
_cts
.
CancelAfter
(
20000
);
var
req
=
new
HttpRequestMessage
(
HttpMethod
.
Post
,
BaseUrl
.
TrimEnd
(
'/'
)
+
"/chat/completions"
);
req
.
Headers
.
TryAddWithoutValidation
(
"Authorization"
,
"Bearer "
+
ApiKey
);
req
.
Content
=
new
StringContent
(
body
.
ToString
(
Formatting
.
None
),
Encoding
.
UTF8
,
"application/json"
);
var
cts
=
CancellationTokenSource
.
CreateLinkedTokenSource
(
ct
);
cts
.
CancelAfter
(
20000
);
RegisterCts
(
cts
);
try
{
var
req
=
new
HttpRequestMessage
(
HttpMethod
.
Post
,
BaseUrl
.
TrimEnd
(
'/'
)
+
"/chat/completions"
);
req
.
Headers
.
TryAddWithoutValidation
(
"Authorization"
,
"Bearer "
+
ApiKey
);
req
.
Content
=
new
StringContent
(
body
.
ToString
(
Formatting
.
None
),
Encoding
.
UTF8
,
"application/json"
);
using
(
var
resp
=
await
_http
.
SendAsync
(
req
,
_cts
.
Token
).
ConfigureAwait
(
false
))
using
(
var
resp
=
await
Http
.
SendAsync
(
req
,
cts
.
Token
).
ConfigureAwait
(
false
))
{
resp
.
EnsureSuccessStatusCode
();
var
json
=
await
resp
.
Content
.
ReadAsStringAsync
().
ConfigureAwait
(
false
);
var
obj
=
JObject
.
Parse
(
json
);
return
obj
[
"choices"
]?[
0
]?[
"message"
]?[
"content"
]?.
Value
<
string
>()
??
""
;
}
}
finally
{
resp
.
EnsureSuccessStatusCode
();
var
json
=
await
resp
.
Content
.
ReadAsStringAsync
().
ConfigureAwait
(
false
);
var
obj
=
JObject
.
Parse
(
json
);
return
obj
[
"choices"
]?[
0
]?[
"message"
]?[
"content"
]?.
Value
<
string
>()
??
""
;
UnregisterCts
(
cts
);
}
}
...
...
@@ -506,8 +578,17 @@ namespace Tools.MyAgent.Agent
public
void
Dispose
()
{
try
{
_cts
?.
Dispose
();
}
catch
{
}
_http
?.
Dispose
();
lock
(
_ctsLock
)
{
foreach
(
var
cts
in
_active
)
try
{
cts
.
Cancel
();
}
catch
{
}
_active
.
Clear
();
}
lock
(
_httpLock
)
{
try
{
_http
?.
Dispose
();
}
catch
{
}
_http
=
null
;
}
}
}
}
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