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
23830cb4
Commit
23830cb4
authored
Aug 20, 2026
by
段廷瑞
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 智能体交互优化:图片,命令行等
parent
c64815c5
Changes
10
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
793 additions
and
86 deletions
+793
-86
AgentEngine.cs
Tools/MyAgent/Agent/AgentEngine.cs
+93
-6
PromptBuilder.cs
Tools/MyAgent/Agent/PromptBuilder.cs
+0
-10
AgentConfig.cs
Tools/MyAgent/Config/AgentConfig.cs
+2
-2
AgentSettingsForm.Designer.cs
Tools/MyAgent/Forms/AgentSettingsForm.Designer.cs
+13
-11
AgentSettingsForm.cs
Tools/MyAgent/Forms/AgentSettingsForm.cs
+2
-2
wBitmapView.cs
Tools/MyAgent/Forms/wBitmapView.cs
+111
-0
ChatMessage.cs
Tools/MyAgent/Support/ChatMessage.cs
+7
-0
NoCaretRichTextBox.cs
Tools/MyAgent/Support/NoCaretRichTextBox.cs
+31
-0
wMyAgent.cs
Tools/MyAgent/wMyAgent.cs
+528
-55
Tools.csproj
Tools/Tools.csproj
+6
-0
No files found.
Tools/MyAgent/Agent/AgentEngine.cs
View file @
23830cb4
...
@@ -85,6 +85,9 @@ namespace Tools.MyAgent.Agent
...
@@ -85,6 +85,9 @@ namespace Tools.MyAgent.Agent
/// <summary>是否向模型下发可选/高级工具(默认只下发核心工具)</summary>
/// <summary>是否向模型下发可选/高级工具(默认只下发核心工具)</summary>
private
bool
_includeOptionalTools
;
private
bool
_includeOptionalTools
;
/// <summary>v2.2 多模态:是否启用图片提问(附图以 image_url 内容块发送)</summary>
private
bool
_imageInputEnabled
;
/// <summary>单次回答总时长兜底</summary>
/// <summary>单次回答总时长兜底</summary>
private
static
readonly
TimeSpan
MaxRunDuration
=
TimeSpan
.
FromMinutes
(
10
);
private
static
readonly
TimeSpan
MaxRunDuration
=
TimeSpan
.
FromMinutes
(
10
);
...
@@ -120,7 +123,7 @@ namespace Tools.MyAgent.Agent
...
@@ -120,7 +123,7 @@ namespace Tools.MyAgent.Agent
_contextTokens
=
cfg
.
MaxContextTokens
>
0
?
cfg
.
MaxContextTokens
:
32000
;
_contextTokens
=
cfg
.
MaxContextTokens
>
0
?
cfg
.
MaxContextTokens
:
32000
;
_prompts
.
CustomLayer
=
cfg
.
CustomSystemPrompt
??
""
;
_prompts
.
CustomLayer
=
cfg
.
CustomSystemPrompt
??
""
;
_prompts
.
SystemPromptEnabled
=
cfg
.
SystemPromptEnabled
;
_prompts
.
SystemPromptEnabled
=
cfg
.
SystemPromptEnabled
;
_
prompts
.
AutoMemory
=
cfg
.
EnableAutoMemory
;
// E3 问答沉淀提示(只提示确认,绝不静默写入)
_
imageInputEnabled
=
cfg
.
EnableImageInput
;
// v2.2 多模态:图片提问开关
// #31 故障转移配置
// #31 故障转移配置
_failover
.
Configure
(
cfg
.
EnableFailover
,
cfg
.
BackupBaseUrl
,
cfg
.
BackupModel
,
cfg
.
BackupApiKey
);
_failover
.
Configure
(
cfg
.
EnableFailover
,
cfg
.
BackupBaseUrl
,
cfg
.
BackupModel
,
cfg
.
BackupApiKey
);
...
@@ -445,7 +448,7 @@ namespace Tools.MyAgent.Agent
...
@@ -445,7 +448,7 @@ namespace Tools.MyAgent.Agent
{
{
if
(
m
.
Role
==
"user"
)
if
(
m
.
Role
==
"user"
)
{
{
list
.
Add
(
new
JObject
{
[
"role"
]
=
"user"
,
[
"content"
]
=
m
.
Content
??
""
});
list
.
Add
(
new
JObject
{
[
"role"
]
=
"user"
,
[
"content"
]
=
BuildUserContent
(
m
)
});
}
}
else
if
(
m
.
Role
==
"assistant"
&&
!
string
.
IsNullOrEmpty
(
m
.
Content
))
else
if
(
m
.
Role
==
"assistant"
&&
!
string
.
IsNullOrEmpty
(
m
.
Content
))
{
{
...
@@ -470,6 +473,80 @@ namespace Tools.MyAgent.Agent
...
@@ -470,6 +473,80 @@ namespace Tools.MyAgent.Agent
return
list
;
return
list
;
}
}
/// <summary>
/// 组装用户消息 content:启用图片提问(v2.2)且消息带有效附图时,
/// 返回多模态内容块数组(text + 多个 image_url,base64 data URL);否则返回纯文本。
/// <para>v2.3:支持多图(Images 列表优先,兼容旧会话单图 ImagePath);全部图片无效时降级纯文本。</para>
/// </summary>
private
JToken
BuildUserContent
(
ChatMessage
m
)
{
string
text
=
m
.
Content
??
""
;
if
(!
_imageInputEnabled
)
return
text
;
var
images
=
new
List
<
string
>();
if
(
m
.
Images
!=
null
)
foreach
(
var
p
in
m
.
Images
)
if
(!
string
.
IsNullOrEmpty
(
p
)
&&
!
images
.
Contains
(
p
))
images
.
Add
(
p
);
if
(!
string
.
IsNullOrEmpty
(
m
.
ImagePath
)
&&
!
images
.
Contains
(
m
.
ImagePath
))
images
.
Add
(
m
.
ImagePath
);
if
(
images
.
Count
==
0
)
return
text
;
var
arr
=
new
JArray
();
arr
.
Add
(
new
JObject
{
[
"type"
]
=
"text"
,
[
"text"
]
=
text
});
int
added
=
0
;
foreach
(
var
p
in
images
)
{
try
{
string
dataUrl
=
ImageToDataUrl
(
p
);
if
(
dataUrl
!=
null
)
{
arr
.
Add
(
new
JObject
{
[
"type"
]
=
"image_url"
,
[
"image_url"
]
=
new
JObject
{
[
"url"
]
=
dataUrl
}
});
added
++;
}
}
catch
{
}
}
return
added
>
0
?
(
JToken
)
arr
:
(
JToken
)
text
;
// 全部图片不可读/超限 → 降级纯文本
}
/// <summary>本地图片 → base64 data URL(原图上限 MaxImageBytes=4MB,避免超出 API 限制);失败返回 null</summary>
private
static
string
ImageToDataUrl
(
string
path
)
{
const
long
MaxImageBytes
=
4
*
1024
*
1024
;
if
(
string
.
IsNullOrEmpty
(
path
)
||
!
System
.
IO
.
File
.
Exists
(
path
))
return
null
;
var
fi
=
new
System
.
IO
.
FileInfo
(
path
);
if
(
fi
.
Length
<=
0
||
fi
.
Length
>
MaxImageBytes
)
return
null
;
string
ext
=
System
.
IO
.
Path
.
GetExtension
(
path
).
ToLowerInvariant
();
string
mime
=
ext
==
".jpg"
||
ext
==
".jpeg"
?
"image/jpeg"
:
ext
==
".gif"
?
"image/gif"
:
ext
==
".bmp"
?
"image/bmp"
:
"image/png"
;
byte
[]
bytes
=
System
.
IO
.
File
.
ReadAllBytes
(
path
);
return
"data:"
+
mime
+
";base64,"
+
Convert
.
ToBase64String
(
bytes
);
}
/// <summary>
/// 安全读取消息 content 文本(v2.2 多模态兼容):
/// content 为数组(text + image_url 内容块)时取 text 块文本(忽略图片);
/// 为字符串直接返回;其他返回 null。避免 JArray.Value<string>() 抛 InvalidCastException。
/// </summary>
private
static
string
ContentText
(
JToken
content
)
{
if
(
content
==
null
)
return
null
;
if
(
content
.
Type
==
JTokenType
.
Array
)
{
foreach
(
var
item
in
content
)
{
if
(
string
.
Equals
(
item
[
"type"
]?.
Value
<
string
>(),
"text"
,
StringComparison
.
OrdinalIgnoreCase
))
return
item
[
"text"
]?.
Value
<
string
>()
??
""
;
}
return
""
;
}
return
content
.
Value
<
string
>();
}
/// <summary>从消息列表头部(跳过 system)裁剪最早 user/assistant 对,直到预算内或只剩 3 条</summary>
/// <summary>从消息列表头部(跳过 system)裁剪最早 user/assistant 对,直到预算内或只剩 3 条</summary>
private
static
int
DropOldestUntilUnder
(
JArray
list
,
int
budget
)
private
static
int
DropOldestUntilUnder
(
JArray
list
,
int
budget
)
{
{
...
@@ -510,9 +587,19 @@ namespace Tools.MyAgent.Agent
...
@@ -510,9 +587,19 @@ namespace Tools.MyAgent.Agent
int
take
=
Math
.
Min
(
6
,
list
.
Count
-
1
);
int
take
=
Math
.
Min
(
6
,
list
.
Count
-
1
);
if
(
take
<
2
)
return
0
;
if
(
take
<
2
)
return
0
;
var
earliest
=
new
JArray
();
// 拼接最早消息文本(多模态消息仅取文本块,忽略图片,避免 data URL 大图进入压缩 prompt)
var
sb
=
new
StringBuilder
();
for
(
int
i
=
1
;
i
<=
take
;
i
++)
for
(
int
i
=
1
;
i
<=
take
;
i
++)
earliest
.
Add
(
list
[
i
]);
{
var
m
=
list
[
i
];
string
role
=
m
[
"role"
]?.
Value
<
string
>()
??
"user"
;
string
text
=
ContentText
(
m
[
"content"
]);
if
(
string
.
IsNullOrEmpty
(
text
))
continue
;
if
(
text
.
Length
>
400
)
text
=
text
.
Substring
(
0
,
400
)
+
"…"
;
sb
.
Append
(
role
).
Append
(
": "
).
Append
(
text
).
Append
(
'\n'
);
}
string
earliestText
=
sb
.
ToString
();
if
(
earliestText
.
Length
==
0
)
return
0
;
string
summary
;
string
summary
;
try
try
...
@@ -520,7 +607,7 @@ namespace Tools.MyAgent.Agent
...
@@ -520,7 +607,7 @@ namespace Tools.MyAgent.Agent
var
prompt
=
new
JArray
var
prompt
=
new
JArray
{
{
new
JObject
{
[
"role"
]
=
"system"
,
[
"content"
]
=
HistorySummaryPrompt
},
new
JObject
{
[
"role"
]
=
"system"
,
[
"content"
]
=
HistorySummaryPrompt
},
new
JObject
{
[
"role"
]
=
"user"
,
[
"content"
]
=
earliest
.
ToString
(
Newtonsoft
.
Json
.
Formatting
.
None
)
}
new
JObject
{
[
"role"
]
=
"user"
,
[
"content"
]
=
earliest
Text
}
};
};
summary
=
await
_client
.
CompleteAsync
(
prompt
,
512
,
ct
).
ConfigureAwait
(
false
);
summary
=
await
_client
.
CompleteAsync
(
prompt
,
512
,
ct
).
ConfigureAwait
(
false
);
}
}
...
@@ -554,7 +641,7 @@ namespace Tools.MyAgent.Agent
...
@@ -554,7 +641,7 @@ namespace Tools.MyAgent.Agent
foreach
(
var
m
in
messages
)
foreach
(
var
m
in
messages
)
{
{
total
+=
4
;
// role / 分隔符开销
total
+=
4
;
// role / 分隔符开销
var
content
=
m
[
"content"
]?.
Value
<
string
>(
);
var
content
=
ContentText
(
m
[
"content"
]
);
if
(!
string
.
IsNullOrEmpty
(
content
))
total
+=
EstimateTokens
(
content
);
if
(!
string
.
IsNullOrEmpty
(
content
))
total
+=
EstimateTokens
(
content
);
var
tc
=
m
[
"tool_calls"
]
as
JArray
;
var
tc
=
m
[
"tool_calls"
]
as
JArray
;
if
(
tc
!=
null
)
total
+=
8
*
tc
.
Count
;
if
(
tc
!=
null
)
total
+=
8
*
tc
.
Count
;
...
...
Tools/MyAgent/Agent/PromptBuilder.cs
View file @
23830cb4
...
@@ -18,9 +18,6 @@ namespace Tools.MyAgent.Agent
...
@@ -18,9 +18,6 @@ namespace Tools.MyAgent.Agent
/// <summary>系统提示词启用开关(由设置注入;仅启用后才注入 CustomLayer)</summary>
/// <summary>系统提示词启用开关(由设置注入;仅启用后才注入 CustomLayer)</summary>
public
bool
SystemPromptEnabled
{
get
;
set
;
}
public
bool
SystemPromptEnabled
{
get
;
set
;
}
/// <summary>E3 问答沉淀提示:开启后在工具规则尾部追加「可询问用户是否沉淀结论」(绝不静默写入)</summary>
public
bool
AutoMemory
{
get
;
set
;
}
public
string
BuildSystemMessage
(
KbService
kb
)
public
string
BuildSystemMessage
(
KbService
kb
)
{
{
// B2 提示词外置:静态层优先读 输出目录/prompts/*.md 模板(用户可定制),
// B2 提示词外置:静态层优先读 输出目录/prompts/*.md 模板(用户可定制),
...
@@ -42,13 +39,6 @@ namespace Tools.MyAgent.Agent
...
@@ -42,13 +39,6 @@ namespace Tools.MyAgent.Agent
sb
.
AppendLine
(
LoadLayer
(
promptsDir
,
"security.md"
,
SecurityLayer
));
sb
.
AppendLine
(
LoadLayer
(
promptsDir
,
"security.md"
,
SecurityLayer
));
sb
.
AppendLine
();
sb
.
AppendLine
();
sb
.
AppendLine
(
LoadLayer
(
promptsDir
,
"tool_rules.md"
,
ToolRules
));
sb
.
AppendLine
(
LoadLayer
(
promptsDir
,
"tool_rules.md"
,
ToolRules
));
if
(
AutoMemory
)
{
sb
.
AppendLine
();
sb
.
AppendLine
(
"【知识沉淀提示(E3)】"
);
sb
.
AppendLine
(
"- 当你产出了有价值的结论(问题解答、流程梳理、新知识)时,可在回答末尾询问用户是否沉淀到知识库(kb_write_kb)。"
);
sb
.
AppendLine
(
"- 未获得用户明确同意前,绝不允许写入知识库;用户拒绝则不再追问。"
);
}
return
sb
.
ToString
();
return
sb
.
ToString
();
}
}
...
...
Tools/MyAgent/Config/AgentConfig.cs
View file @
23830cb4
...
@@ -90,7 +90,7 @@
...
@@ -90,7 +90,7 @@
/// <summary>是否启用可选/高级工具(db_query、module_patch、img_search、/workflow)</summary>
/// <summary>是否启用可选/高级工具(db_query、module_patch、img_search、/workflow)</summary>
public
bool
EnableOptionalTools
{
get
;
set
;
}
=
false
;
public
bool
EnableOptionalTools
{
get
;
set
;
}
=
false
;
/// <summary>
回答结束后是否提示把有价值结论沉淀到知识库(只提示确认,绝不静默写入;默认关闭)
</summary>
/// <summary>
是否启用图片提问(多模态,v2.2):启用后粘贴截图随提问以 image_url 发送,需视觉模型支持(如 DeepSeek-VL / qwen-vl);默认启用
</summary>
public
bool
Enable
AutoMemory
{
get
;
set
;
}
=
fals
e
;
public
bool
Enable
ImageInput
{
get
;
set
;
}
=
tru
e
;
}
}
}
}
Tools/MyAgent/Forms/AgentSettingsForm.Designer.cs
View file @
23830cb4
...
@@ -82,7 +82,7 @@
...
@@ -82,7 +82,7 @@
this
.
tabOther
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
tabOther
=
new
System
.
Windows
.
Forms
.
TabPage
();
this
.
grpOpt
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
grpOpt
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
chkOpt
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
chkOpt
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
chk
AutoMemory
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
chk
ImageInput
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
grpFail
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
grpFail
=
new
System
.
Windows
.
Forms
.
GroupBox
();
this
.
chkFail
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
chkFail
=
new
System
.
Windows
.
Forms
.
CheckBox
();
this
.
lblBUrl
=
new
System
.
Windows
.
Forms
.
Label
();
this
.
lblBUrl
=
new
System
.
Windows
.
Forms
.
Label
();
...
@@ -717,7 +717,7 @@
...
@@ -717,7 +717,7 @@
// grpOpt
// grpOpt
//
//
this
.
grpOpt
.
Controls
.
Add
(
this
.
chkOpt
);
this
.
grpOpt
.
Controls
.
Add
(
this
.
chkOpt
);
this
.
grpOpt
.
Controls
.
Add
(
this
.
chk
AutoMemory
);
this
.
grpOpt
.
Controls
.
Add
(
this
.
chk
ImageInput
);
this
.
grpOpt
.
Location
=
new
System
.
Drawing
.
Point
(
8
,
12
);
this
.
grpOpt
.
Location
=
new
System
.
Drawing
.
Point
(
8
,
12
);
this
.
grpOpt
.
Name
=
"grpOpt"
;
this
.
grpOpt
.
Name
=
"grpOpt"
;
this
.
grpOpt
.
Size
=
new
System
.
Drawing
.
Size
(
596
,
92
);
this
.
grpOpt
.
Size
=
new
System
.
Drawing
.
Size
(
596
,
92
);
...
@@ -735,15 +735,17 @@
...
@@ -735,15 +735,17 @@
this
.
chkOpt
.
Text
=
"启用可选/高级工具(db_query、module_patch、img_search、/workflow)"
;
this
.
chkOpt
.
Text
=
"启用可选/高级工具(db_query、module_patch、img_search、/workflow)"
;
this
.
chkOpt
.
UseVisualStyleBackColor
=
true
;
this
.
chkOpt
.
UseVisualStyleBackColor
=
true
;
//
//
// chk
AutoMemory
// chk
ImageInput
//
//
this
.
chkAutoMemory
.
AutoSize
=
true
;
this
.
chkImageInput
.
AutoSize
=
true
;
this
.
chkAutoMemory
.
Location
=
new
System
.
Drawing
.
Point
(
16
,
54
);
this
.
chkImageInput
.
Checked
=
true
;
this
.
chkAutoMemory
.
Name
=
"chkAutoMemory"
;
this
.
chkImageInput
.
CheckState
=
System
.
Windows
.
Forms
.
CheckState
.
Checked
;
this
.
chkAutoMemory
.
Size
=
new
System
.
Drawing
.
Size
(
351
,
21
);
this
.
chkImageInput
.
Location
=
new
System
.
Drawing
.
Point
(
16
,
54
);
this
.
chkAutoMemory
.
TabIndex
=
1
;
this
.
chkImageInput
.
Name
=
"chkImageInput"
;
this
.
chkAutoMemory
.
Text
=
"回答后提示沉淀有价值结论到知识库(只提示,不静默写入)"
;
this
.
chkImageInput
.
Size
=
new
System
.
Drawing
.
Size
(
540
,
21
);
this
.
chkAutoMemory
.
UseVisualStyleBackColor
=
true
;
this
.
chkImageInput
.
TabIndex
=
1
;
this
.
chkImageInput
.
Text
=
"启用图片提问(多模态):粘贴/拖入截图随提问以 image_url 发送,需视觉模型支持(如 qwen-vl)"
;
this
.
chkImageInput
.
UseVisualStyleBackColor
=
true
;
//
//
// grpFail
// grpFail
//
//
...
@@ -957,7 +959,7 @@
...
@@ -957,7 +959,7 @@
private
System
.
Windows
.
Forms
.
TabPage
tabOther
;
private
System
.
Windows
.
Forms
.
TabPage
tabOther
;
private
System
.
Windows
.
Forms
.
GroupBox
grpOpt
;
private
System
.
Windows
.
Forms
.
GroupBox
grpOpt
;
private
System
.
Windows
.
Forms
.
CheckBox
chkOpt
;
private
System
.
Windows
.
Forms
.
CheckBox
chkOpt
;
private
System
.
Windows
.
Forms
.
CheckBox
chk
AutoMemory
;
private
System
.
Windows
.
Forms
.
CheckBox
chk
ImageInput
;
private
System
.
Windows
.
Forms
.
GroupBox
grpFail
;
private
System
.
Windows
.
Forms
.
GroupBox
grpFail
;
private
System
.
Windows
.
Forms
.
CheckBox
chkFail
;
private
System
.
Windows
.
Forms
.
CheckBox
chkFail
;
private
System
.
Windows
.
Forms
.
Label
lblBUrl
;
private
System
.
Windows
.
Forms
.
Label
lblBUrl
;
...
...
Tools/MyAgent/Forms/AgentSettingsForm.cs
View file @
23830cb4
...
@@ -137,7 +137,7 @@ namespace Tools.MyAgent.Forms
...
@@ -137,7 +137,7 @@ namespace Tools.MyAgent.Forms
// 其他:可选能力
// 其他:可选能力
chkOpt
.
Checked
=
_config
.
EnableOptionalTools
;
chkOpt
.
Checked
=
_config
.
EnableOptionalTools
;
chk
AutoMemory
.
Checked
=
_config
.
EnableAutoMemory
;
chk
ImageInput
.
Checked
=
_config
.
EnableImageInput
;
// v2.2 多模态:图片提问开关
// 其他:故障转移
// 其他:故障转移
chkFail
.
Checked
=
_config
.
EnableFailover
;
chkFail
.
Checked
=
_config
.
EnableFailover
;
...
@@ -429,7 +429,7 @@ namespace Tools.MyAgent.Forms
...
@@ -429,7 +429,7 @@ namespace Tools.MyAgent.Forms
_config
.
CustomSystemPrompt
=
txtCustomPrompt
.
Text
.
Trim
();
_config
.
CustomSystemPrompt
=
txtCustomPrompt
.
Text
.
Trim
();
// 其他:可选能力
// 其他:可选能力
_config
.
EnableOptionalTools
=
chkOpt
.
Checked
;
_config
.
EnableOptionalTools
=
chkOpt
.
Checked
;
_config
.
Enable
AutoMemory
=
chkAutoMemory
.
Checked
;
_config
.
Enable
ImageInput
=
chkImageInput
.
Checked
;
// v2.2 多模态:图片提问开关
// 其他:故障转移
// 其他:故障转移
_config
.
EnableFailover
=
chkFail
.
Checked
;
_config
.
EnableFailover
=
chkFail
.
Checked
;
_config
.
BackupBaseUrl
=
txtBUrl
.
Text
.
Trim
();
_config
.
BackupBaseUrl
=
txtBUrl
.
Text
.
Trim
();
...
...
Tools/MyAgent/Forms/wBitmapView.cs
0 → 100644
View file @
23830cb4
using
System
;
using
System.Drawing
;
using
System.Windows.Forms
;
namespace
Tools.MyAgent
{
/// <summary>
/// 图片查看器:优先按图片原始尺寸展示(1:1 像素),但不超过屏幕工作区大小;
/// 图片大于工作区时窗体限制为工作区大小,可滚动查看。带边框与标题栏(可点 ✕ 关闭),置顶悬浮。
/// <para>点击任意处或按 Esc 也可关闭。纯代码构造,无 Designer 依赖。</para>
/// </summary>
public
sealed
class
wBitmapView
:
Form
{
private
Image
_img
;
private
Label
_hint
;
private
PictureBox
_picture
;
public
wBitmapView
(
Image
img
)
{
_img
=
img
;
//
this
.
TopMost
=
true
;
this
.
ShowInTaskbar
=
false
;
this
.
StartPosition
=
FormStartPosition
.
Manual
;
// 带边框 + 标题栏(含 ✕ 关闭按钮),避免无边框窗体被误解为无法关闭
this
.
FormBorderStyle
=
FormBorderStyle
.
FixedToolWindow
;
this
.
Text
=
$"图片查看
{
img
.
Width
}
×
{
img
.
Height
}
"
;
this
.
AutoScroll
=
true
;
this
.
KeyPreview
=
true
;
// 原始尺寸:PictureBox 随图片自动扩展,窗体收缩时出现滚动条
_picture
=
new
PictureBox
{
Location
=
new
Point
(
0
,
0
),
SizeMode
=
PictureBoxSizeMode
.
AutoSize
,
Image
=
img
,
};
this
.
Controls
.
Add
(
_picture
);
// 底部提示条:图片尺寸 + 关闭方式
_hint
=
new
Label
{
Text
=
$"
{
img
.
Width
}
×
{
img
.
Height
}
· 点击任意处关闭 "
,
AutoSize
=
true
,
BackColor
=
Color
.
FromArgb
(
170
,
0
,
0
,
0
),
ForeColor
=
Color
.
White
,
Font
=
new
Font
(
"微软雅黑"
,
9F
),
Padding
=
new
Padding
(
8
,
3
,
8
,
3
),
Anchor
=
AnchorStyles
.
Bottom
,
};
this
.
Controls
.
Add
(
_hint
);
_hint
.
BringToFront
();
this
.
Resize
+=
(
s
,
e
)
=>
PositionHint
();
this
.
Scroll
+=
(
s
,
e
)
=>
PositionHint
();
// 点击任意处 / Esc 关闭
this
.
Click
+=
(
s
,
e
)
=>
this
.
Close
();
_picture
.
Click
+=
(
s
,
e
)
=>
this
.
Close
();
this
.
KeyDown
+=
(
s
,
e
)
=>
{
if
(
e
.
KeyCode
==
Keys
.
Escape
)
this
.
Close
();
};
// 关闭时释放位图
this
.
FormClosed
+=
(
s
,
e
)
=>
{
if
(
_img
!=
null
)
{
_img
.
Dispose
();
_img
=
null
;
}
};
}
/// <summary>提示条水平居中于可视区底部</summary>
private
void
PositionHint
()
{
if
(
_hint
==
null
||
!
_hint
.
IsHandleCreated
)
return
;
_hint
.
Location
=
new
Point
(
Math
.
Max
(
0
,
(
this
.
ClientSize
.
Width
-
_hint
.
Width
)
/
2
),
Math
.
Max
(
0
,
this
.
ClientSize
.
Height
-
_hint
.
Height
-
8
));
}
/// <summary>
/// 居中显示:优先按图片原始尺寸,但不超过屏幕工作区大小(超屏则限制在工作区内,滚动查看)。
/// </summary>
/// <param name="owner">调用方窗体:用于选取其所在屏幕的工作区(多屏场景),可传 null</param>
public
void
ShowView
(
Form
owner
=
null
)
{
// 选取屏幕:优先调用方窗体所在屏,其次鼠标所在屏
Rectangle
wa
;
if
(
owner
!=
null
&&
owner
.
IsHandleCreated
)
wa
=
Screen
.
FromControl
(
owner
).
WorkingArea
;
else
wa
=
Screen
.
FromPoint
(
Cursor
.
Position
).
WorkingArea
;
// 核心限制:优先原尺寸,超屏取工作区大小(至少保留最小可视尺寸)
// 先强制创建句柄,使 Width/Height 反映窗体边框+标题栏装饰,保证窗体整体不超屏
IntPtr
dummy
=
this
.
Handle
;
int
decoW
=
this
.
Width
-
this
.
ClientSize
.
Width
;
int
decoH
=
this
.
Height
-
this
.
ClientSize
.
Height
;
int
w
=
Math
.
Max
(
Math
.
Min
(
_img
.
Width
,
wa
.
Width
-
decoW
),
120
);
int
h
=
Math
.
Max
(
Math
.
Min
(
_img
.
Height
,
wa
.
Height
-
decoH
),
80
);
this
.
ClientSize
=
new
Size
(
w
,
h
);
this
.
Location
=
new
Point
(
wa
.
Left
+
(
wa
.
Width
-
this
.
Width
)
/
2
,
wa
.
Top
+
(
wa
.
Height
-
this
.
Height
)
/
2
);
this
.
Show
();
this
.
Activate
();
PositionHint
();
}
/// <summary>从文件加载图片并按原始尺寸弹出查看(超屏自动限制在工作区内;内部负责释放位图)</summary>
public
static
wBitmapView
ShowImage
(
string
file
,
Form
owner
=
null
)
{
var
img
=
Image
.
FromFile
(
file
);
var
v
=
new
wBitmapView
(
img
);
if
(
owner
!=
null
&&
owner
.
IsHandleCreated
&&
owner
.
Visible
)
v
.
Owner
=
owner
;
// 跟随主窗体:最小化/置顶关系一致
v
.
ShowView
(
owner
);
return
v
;
}
}
}
Tools/MyAgent/Support/ChatMessage.cs
View file @
23830cb4
using
System
;
using
System
;
using
System.Collections.Generic
;
namespace
Tools.MyAgent
namespace
Tools.MyAgent
{
{
...
@@ -12,5 +13,11 @@ namespace Tools.MyAgent
...
@@ -12,5 +13,11 @@ namespace Tools.MyAgent
public
string
ThinkingContent
{
get
;
set
;
}
public
string
ThinkingContent
{
get
;
set
;
}
public
bool
IsStreaming
{
get
;
set
;
}
public
bool
IsStreaming
{
get
;
set
;
}
public
DateTime
Timestamp
{
get
;
set
;
}
=
DateTime
.
Now
;
public
DateTime
Timestamp
{
get
;
set
;
}
=
DateTime
.
Now
;
/// <summary>附图(多模态,v2.2):本地图片绝对路径;启用图片提问时随消息以 image_url 内容块发送</summary>
public
string
ImagePath
{
get
;
set
;
}
/// <summary>附图列表(多图,v2.3):一次提问可携带多张图片;优先于 ImagePath(旧会话兼容保留单图字段)</summary>
public
List
<
string
>
Images
{
get
;
set
;
}
}
}
}
}
Tools/MyAgent/Support/NoCaretRichTextBox.cs
0 → 100644
View file @
23830cb4
using
System
;
using
System.Runtime.InteropServices
;
using
System.Windows.Forms
;
namespace
Tools.MyAgent
{
/// <summary>
/// 只读展示用 RichTextBox:隐藏文本插入光标(I 型插入符,不闪烁)。
/// <para>问答区不支持输入,无需显示插入光标;仍保留鼠标拖动选中与复制能力。
/// RichEdit 在获得焦点 / 鼠标按下 / 按键移动光标时会重新显示插入符,故在这些消息处理后再次隐藏。</para>
/// </summary>
public
sealed
class
NoCaretRichTextBox
:
RichTextBox
{
private
const
int
WM_SETFOCUS
=
0x0007
;
private
const
int
WM_KEYDOWN
=
0x0100
;
private
const
int
WM_LBUTTONDOWN
=
0x0201
;
private
const
int
WM_LBUTTONUP
=
0x0202
;
[
DllImport
(
"user32.dll"
)]
private
static
extern
bool
HideCaret
(
IntPtr
hWnd
);
protected
override
void
WndProc
(
ref
Message
m
)
{
base
.
WndProc
(
ref
m
);
if
(
m
.
Msg
==
WM_SETFOCUS
||
m
.
Msg
==
WM_LBUTTONDOWN
||
m
.
Msg
==
WM_LBUTTONUP
||
m
.
Msg
==
WM_KEYDOWN
)
{
try
{
HideCaret
(
m
.
HWnd
);
}
catch
{
}
}
}
}
}
Tools/MyAgent/wMyAgent.cs
View file @
23830cb4
...
@@ -33,10 +33,14 @@ namespace Tools.MyAgent
...
@@ -33,10 +33,14 @@ namespace Tools.MyAgent
private
CancellationTokenSource
_sendCts
;
private
CancellationTokenSource
_sendCts
;
private
string
_historyDir
;
private
string
_historyDir
;
private
bool
_sessionSaved
;
private
bool
_sessionSaved
;
/// <summary>v2.3 待发送附图列表:粘贴截图后暂存,随下一轮提问发送(启用图片提问时;最多 5 张)</summary>
private
readonly
List
<
string
>
_pendingImages
=
new
List
<
string
>();
/// <summary>v2.3 待发送附图缩略图栏(问答区左下角悬浮;每张图右上角叉号可取消)</summary>
private
FlowLayoutPanel
_pendingBar
;
private
readonly
List
<
string
>
_producedFiles
=
new
List
<
string
>();
private
readonly
List
<
string
>
_producedFiles
=
new
List
<
string
>();
private
Point
_mouseDownPos
=
Point
.
Empty
;
private
Point
_mouseDownPos
=
Point
.
Empty
;
private
bool
_mouseButtonDown
;
private
bool
_mouseButtonDown
;
private
Cursor
_defaultChatCursor
=
Cursors
.
IBeam
;
private
Cursor
_defaultChatCursor
=
Cursors
.
Arrow
;
// 问答区不支持输入:默认箭头光标(原 IBeam)
private
int
_hoverStart
=
-
1
;
private
int
_hoverStart
=
-
1
;
private
int
_hoverLength
=
0
;
private
int
_hoverLength
=
0
;
private
Color
_hoverOrigBack
=
Color
.
Empty
;
private
Color
_hoverOrigBack
=
Color
.
Empty
;
...
@@ -91,7 +95,7 @@ namespace Tools.MyAgent
...
@@ -91,7 +95,7 @@ namespace Tools.MyAgent
private
RichTextBox
CreateRtfFallback
()
private
RichTextBox
CreateRtfFallback
()
{
{
var
rtf
=
new
RichTextBox
var
rtf
=
new
NoCaret
RichTextBox
{
{
Dock
=
DockStyle
.
Fill
,
Dock
=
DockStyle
.
Fill
,
ReadOnly
=
true
,
ReadOnly
=
true
,
...
@@ -99,6 +103,7 @@ namespace Tools.MyAgent
...
@@ -99,6 +103,7 @@ namespace Tools.MyAgent
BorderStyle
=
BorderStyle
.
None
,
BorderStyle
=
BorderStyle
.
None
,
Font
=
new
Font
(
"微软雅黑"
,
10F
),
Font
=
new
Font
(
"微软雅黑"
,
10F
),
TabIndex
=
0
,
TabIndex
=
0
,
Cursor
=
Cursors
.
Arrow
,
// 问答区不支持输入:默认箭头光标(悬停链接时仍切换为手型)
};
};
panelChat
.
Controls
.
Add
(
rtf
);
panelChat
.
Controls
.
Add
(
rtf
);
return
rtf
;
return
rtf
;
...
@@ -110,6 +115,8 @@ namespace Tools.MyAgent
...
@@ -110,6 +115,8 @@ namespace Tools.MyAgent
partial
void
OnFormDisposing
()
partial
void
OnFormDisposing
()
{
{
SaveUnsavedSession
();
SaveUnsavedSession
();
try
{
_cmdNameFont
?.
Dispose
();
}
catch
{
}
try
{
_cmdDescFont
?.
Dispose
();
}
catch
{
}
try
{
_engine
.
Cancel
();
}
catch
{
}
try
{
_engine
.
Cancel
();
}
catch
{
}
try
{
_engine
.
Dispose
();
}
catch
{
}
try
{
_engine
.
Dispose
();
}
catch
{
}
try
{
_kb
.
ShutdownIndex
();
}
catch
{
}
// 终止索引后台构建,关闭 SQLite 连接
try
{
_kb
.
ShutdownIndex
();
}
catch
{
}
// 终止索引后台构建,关闭 SQLite 连接
...
@@ -196,7 +203,7 @@ namespace Tools.MyAgent
...
@@ -196,7 +203,7 @@ namespace Tools.MyAgent
BackupModel
=
cfg
.
BackupModel
,
BackupModel
=
cfg
.
BackupModel
,
BackupApiKey
=
cfg
.
BackupApiKey
,
BackupApiKey
=
cfg
.
BackupApiKey
,
EnableOptionalTools
=
cfg
.
EnableOptionalTools
,
EnableOptionalTools
=
cfg
.
EnableOptionalTools
,
Enable
AutoMemory
=
cfg
.
EnableAutoMemory
,
Enable
ImageInput
=
cfg
.
EnableImageInput
,
};
};
File
.
WriteAllText
(
AgentConfigFilePath
,
Newtonsoft
.
Json
.
JsonConvert
.
SerializeObject
(
toSave
),
System
.
Text
.
Encoding
.
UTF8
);
File
.
WriteAllText
(
AgentConfigFilePath
,
Newtonsoft
.
Json
.
JsonConvert
.
SerializeObject
(
toSave
),
System
.
Text
.
Encoding
.
UTF8
);
}
}
...
@@ -267,6 +274,10 @@ namespace Tools.MyAgent
...
@@ -267,6 +274,10 @@ namespace Tools.MyAgent
lblCopy
.
LinkClicked
+=
(
s
,
e
)
=>
CopyLastAnswer
();
lblCopy
.
LinkClicked
+=
(
s
,
e
)
=>
CopyLastAnswer
();
btnSend
.
Click
+=
(
s
,
e
)
=>
OnSendButtonClick
();
btnSend
.
Click
+=
(
s
,
e
)
=>
OnSendButtonClick
();
txtInput
.
KeyDown
+=
OnInputKeyDown
;
txtInput
.
KeyDown
+=
OnInputKeyDown
;
// 输入框拖入图片文件 → 加入待发送附图列表(与 Ctrl+V 粘贴同一套收图流程)
txtInput
.
AllowDrop
=
true
;
txtInput
.
DragEnter
+=
OnInputDragEnter
;
txtInput
.
DragDrop
+=
OnInputDragDrop
;
KeyDown
+=
OnFormKeyDown
;
KeyDown
+=
OnFormKeyDown
;
// RichBox 模式的点击/悬停交互(路径点击、details 折叠切换)
// RichBox 模式的点击/悬停交互(路径点击、details 折叠切换)
WireRtfEvents
();
WireRtfEvents
();
...
@@ -335,7 +346,7 @@ namespace Tools.MyAgent
...
@@ -335,7 +346,7 @@ namespace Tools.MyAgent
var
lines
=
new
System
.
Collections
.
Generic
.
List
<
string
>
var
lines
=
new
System
.
Collections
.
Generic
.
List
<
string
>
{
{
$"你好,我是本地知识库助手(模型:
{
_config
.
Model
}
)。"
,
$"你好,我是本地知识库助手(模型:
{
_config
.
Model
}
)。"
,
$"知识库:
{
kbInfo
}
。输入问题即可检索回答;
/help 查看命令
。"
,
$"知识库:
{
kbInfo
}
。输入问题即可检索回答;
输入 / 呼出命令菜单
。"
,
};
};
if
(
string
.
IsNullOrEmpty
(
_config
.
ApiKey
))
if
(
string
.
IsNullOrEmpty
(
_config
.
ApiKey
))
lines
.
Add
(
"API Key 未配置(点击「设置」填入后可开始回答)。"
);
lines
.
Add
(
"API Key 未配置(点击「设置」填入后可开始回答)。"
);
...
@@ -411,7 +422,7 @@ namespace Tools.MyAgent
...
@@ -411,7 +422,7 @@ namespace Tools.MyAgent
private
async
void
SendMessage
()
private
async
void
SendMessage
()
{
{
string
text
=
txtInput
.
Text
.
Trim
();
string
text
=
txtInput
.
Text
.
Trim
();
if
(
text
.
Length
==
0
)
return
;
if
(
text
.
Length
==
0
&&
_pendingImages
.
Count
==
0
)
return
;
// 允许「仅附图」发送(v2.2)
if
(
_engine
.
IsRunning
)
return
;
if
(
_engine
.
IsRunning
)
return
;
// 记录历史提示词(与上一条相同则跳过),并复位浏览位置
// 记录历史提示词(与上一条相同则跳过),并复位浏览位置
...
@@ -436,7 +447,14 @@ namespace Tools.MyAgent
...
@@ -436,7 +447,14 @@ namespace Tools.MyAgent
SetInputEnabled
(
false
);
SetInputEnabled
(
false
);
SetButtonState
(
true
);
SetButtonState
(
true
);
_messages
.
Add
(
new
ChatMessage
{
Role
=
"user"
,
Content
=
text
});
_messages
.
Add
(
new
ChatMessage
{
Role
=
"user"
,
Content
=
text
,
Images
=
_pendingImages
.
Count
>
0
?
new
List
<
string
>(
_pendingImages
)
:
null
,
// v2.3:待发送附图随本轮提问附加(发送后清空)
});
_pendingImages
.
Clear
();
RefreshPendingBar
();
_messages
.
Add
(
new
ChatMessage
{
Role
=
"assistant"
,
Content
=
""
,
IsStreaming
=
true
});
_messages
.
Add
(
new
ChatMessage
{
Role
=
"assistant"
,
Content
=
""
,
IsStreaming
=
true
});
_sessionSaved
=
false
;
// 对话内容已变化,下次新对话需重新记录历史
_sessionSaved
=
false
;
// 对话内容已变化,下次新对话需重新记录历史
_producedFiles
.
Clear
();
// 新一轮开始,清空上轮输出文件
_producedFiles
.
Clear
();
// 新一轮开始,清空上轮输出文件
...
@@ -471,6 +489,8 @@ namespace Tools.MyAgent
...
@@ -471,6 +489,8 @@ namespace Tools.MyAgent
_messages
.
Clear
();
_messages
.
Clear
();
_producedFiles
.
Clear
();
_producedFiles
.
Clear
();
_expandedDetails
.
Clear
();
_expandedDetails
.
Clear
();
_pendingImages
.
Clear
();
// 新对话:清空待发送附图
RefreshPendingBar
();
_sessionSaved
=
true
;
_sessionSaved
=
true
;
_rtfCache
.
Clear
();
_rtfCache
.
Clear
();
ResetInputState
();
ResetInputState
();
...
@@ -616,9 +636,20 @@ namespace Tools.MyAgent
...
@@ -616,9 +636,20 @@ namespace Tools.MyAgent
txtInput
.
Focus
();
txtInput
.
Focus
();
}
}
// ── 斜杠命令自动补全(#35)与「存知识库」按钮(#36) ──
// ── 斜杠命令菜单(#35)与「存知识库」按钮(#36) ──
// 交互:输入 / 弹出命令+说明列表;↑/↓ 选择;Tab 录入命令;Enter 录入并发送;Esc 关闭
private
ListBox
_lstCmd
;
private
ListBox
_lstCmd
;
/// <summary>命令菜单底部快捷键提示条</summary>
private
Label
_lblCmdHint
;
/// <summary>命令名/用法字体(粗体)</summary>
private
Font
_cmdNameFont
;
/// <summary>命令说明字体(小一号)</summary>
private
Font
_cmdDescFont
;
/// <summary>命令菜单最大高度</summary>
private
const
int
CmdMaxMenuHeight
=
270
;
/// <summary>快捷键提示条高度</summary>
private
const
int
CmdHintHeight
=
22
;
private
void
CreateCommandAutocomplete
()
private
void
CreateCommandAutocomplete
()
{
{
...
@@ -629,13 +660,37 @@ namespace Tools.MyAgent
...
@@ -629,13 +660,37 @@ namespace Tools.MyAgent
IntegralHeight
=
false
,
IntegralHeight
=
false
,
BorderStyle
=
BorderStyle
.
FixedSingle
,
BorderStyle
=
BorderStyle
.
FixedSingle
,
BackColor
=
Color
.
White
,
BackColor
=
Color
.
White
,
ItemHeight
=
18
,
ItemHeight
=
24
,
DrawMode
=
DrawMode
.
OwnerDrawFixed
,
TabStop
=
false
,
Anchor
=
AnchorStyles
.
Bottom
|
AnchorStyles
.
Left
,
Anchor
=
AnchorStyles
.
Bottom
|
AnchorStyles
.
Left
,
};
};
_lstCmd
.
DrawItem
+=
OnCmdDrawItem
;
// 鼠标点击仅选中,不抢走输入框焦点(选择仍以键盘 ↑/↓ 为主)
_lstCmd
.
MouseUp
+=
(
s
,
e
)
=>
FocusInput
();
_lblCmdHint
=
new
Label
{
Visible
=
false
,
AutoSize
=
false
,
Height
=
CmdHintHeight
,
BackColor
=
Color
.
FromArgb
(
245
,
245
,
245
),
ForeColor
=
Color
.
FromArgb
(
120
,
120
,
120
),
Font
=
new
Font
(
"微软雅黑"
,
8F
),
Text
=
"↑↓ 选择 Tab 录入 Enter 发送 Esc 关闭"
,
TextAlign
=
ContentAlignment
.
MiddleLeft
,
Padding
=
new
Padding
(
6
,
0
,
0
,
0
),
BorderStyle
=
BorderStyle
.
FixedSingle
,
};
_cmdNameFont
=
new
Font
(
"微软雅黑"
,
9F
,
FontStyle
.
Bold
);
_cmdDescFont
=
new
Font
(
"微软雅黑"
,
8F
);
Controls
.
Add
(
_lstCmd
);
Controls
.
Add
(
_lstCmd
);
Controls
.
Add
(
_lblCmdHint
);
_lstCmd
.
BringToFront
();
_lstCmd
.
BringToFront
();
_lblCmdHint
.
BringToFront
();
txtInput
.
TextChanged
+=
OnInputTextChanged
;
txtInput
.
TextChanged
+=
OnInputTextChanged
;
txtInput
.
LostFocus
+=
(
s
,
e
)
=>
{
if
(
_lstCmd
!=
null
)
_lstCmd
.
Visible
=
false
;
}
;
txtInput
.
LostFocus
+=
(
s
,
e
)
=>
ShowCommandMenu
(
false
)
;
}
}
private
void
OnInputTextChanged
(
object
sender
,
EventArgs
e
)
private
void
OnInputTextChanged
(
object
sender
,
EventArgs
e
)
...
@@ -647,36 +702,121 @@ namespace Tools.MyAgent
...
@@ -647,36 +702,121 @@ namespace Tools.MyAgent
{
{
if
(
_lstCmd
==
null
)
return
;
if
(
_lstCmd
==
null
)
return
;
string
text
=
txtInput
.
Text
??
""
;
string
text
=
txtInput
.
Text
??
""
;
if
(!
text
.
StartsWith
(
"/"
)
||
text
.
Length
<=
1
)
// 输入 /(含仅一个斜杠)即弹出全部命令;继续输入按命令名前缀过滤
if
(!
text
.
StartsWith
(
"/"
))
{
{
_lstCmd
.
Visible
=
false
;
ShowCommandMenu
(
false
);
_lstCmd
.
Items
.
Clear
();
return
;
return
;
}
}
string
prefix
=
text
.
Split
(
' '
)[
0
].
ToLowerInvariant
();
string
prefix
=
text
.
Split
(
' '
)[
0
].
ToLowerInvariant
();
var
matches
=
SlashCommands
.
Where
(
c
=>
c
.
StartsWith
(
prefix
,
StringComparison
.
OrdinalIgnoreCase
)).
ToList
();
var
matches
=
AllCommands
.
Where
(
c
=>
c
.
Name
.
StartsWith
(
prefix
,
StringComparison
.
OrdinalIgnoreCase
)).
ToList
();
if
(
matches
.
Count
==
0
)
if
(
matches
.
Count
==
0
)
{
{
_lstCmd
.
Visible
=
false
;
ShowCommandMenu
(
false
);
_lstCmd
.
Items
.
Clear
();
return
;
return
;
}
}
_lstCmd
.
Items
.
Clear
();
_lstCmd
.
Items
.
Clear
();
foreach
(
var
m
in
matches
)
foreach
(
var
m
in
matches
)
_lstCmd
.
Items
.
Add
(
m
);
_lstCmd
.
SelectedIndex
=
0
;
_lstCmd
.
Height
=
Math
.
Min
(
CmdMaxMenuHeight
,
matches
.
Count
*
_lstCmd
.
ItemHeight
+
4
);
_lstCmd
.
Width
=
350
;
int
x
=
panelBottom
.
Left
+
8
;
_lstCmd
.
Location
=
new
Point
(
x
,
Math
.
Max
(
0
,
panelBottom
.
Top
-
_lstCmd
.
Height
-
CmdHintHeight
));
_lblCmdHint
.
Location
=
new
Point
(
x
,
Math
.
Max
(
0
,
panelBottom
.
Top
-
CmdHintHeight
));
_lblCmdHint
.
Width
=
_lstCmd
.
Width
;
ShowCommandMenu
(
true
);
}
/// <summary>显示/隐藏命令菜单(命令列表 + 快捷键提示条)</summary>
private
void
ShowCommandMenu
(
bool
show
)
{
{
string
hint
=
m
==
"/kb"
?
" [关键词或选择目录]"
if
(
_lstCmd
==
null
)
return
;
:
m
==
"/model"
?
" <模型名>"
_lstCmd
.
Visible
=
show
;
:
m
==
"/scope"
?
" <目录>"
if
(
_lblCmdHint
!=
null
)
_lblCmdHint
.
Visible
=
show
;
:
m
==
"/export"
?
" [文件名]"
if
(!
show
)
_lstCmd
.
Items
.
Clear
();
:
""
;
_lstCmd
.
Items
.
Add
(
m
+
hint
);
}
}
_lstCmd
.
SelectedIndex
=
0
;
_lstCmd
.
Height
=
Math
.
Min
(
150
,
_lstCmd
.
Items
.
Count
*
_lstCmd
.
ItemHeight
+
4
);
/// <summary>自绘命令项:单行显示「命令+参数 说明」,过长时以 ... 省略</summary>
_lstCmd
.
Width
=
260
;
private
void
OnCmdDrawItem
(
object
sender
,
DrawItemEventArgs
e
)
_lstCmd
.
Location
=
new
Point
(
panelBottom
.
Left
+
8
,
Math
.
Max
(
0
,
panelBottom
.
Top
-
_lstCmd
.
Height
));
{
_lstCmd
.
Visible
=
true
;
if
(
e
.
Index
<
0
||
e
.
Index
>=
_lstCmd
.
Items
.
Count
)
return
;
_lstCmd
.
BringToFront
();
if
(!(
_lstCmd
.
Items
[
e
.
Index
]
is
CmdDef
def
))
return
;
bool
selected
=
(
e
.
State
&
DrawItemState
.
Selected
)
!=
0
;
using
(
var
bg
=
new
SolidBrush
(
selected
?
Color
.
FromArgb
(
232
,
241
,
251
)
:
Color
.
White
))
e
.
Graphics
.
FillRectangle
(
bg
,
e
.
Bounds
);
if
(
selected
)
{
using
(
var
pen
=
new
Pen
(
Color
.
FromArgb
(
158
,
197
,
240
)))
e
.
Graphics
.
DrawRectangle
(
pen
,
e
.
Bounds
.
X
,
e
.
Bounds
.
Y
,
e
.
Bounds
.
Width
-
1
,
e
.
Bounds
.
Height
-
1
);
}
const
int
pad
=
8
;
int
top
=
e
.
Bounds
.
Top
+
2
;
int
height
=
e
.
Bounds
.
Height
-
4
;
int
maxWidth
=
e
.
Bounds
.
Width
-
pad
*
2
;
Color
nameColor
=
selected
?
Color
.
FromArgb
(
10
,
102
,
191
)
:
Color
.
FromArgb
(
31
,
31
,
31
);
Color
descColor
=
selected
?
Color
.
FromArgb
(
70
,
100
,
130
)
:
Color
.
FromArgb
(
110
,
110
,
110
);
// 命令名+参数(加粗深色)+ 说明(灰色)拼接为单行,过长用 ... 省略
string
nameText
=
def
.
Name
+
(
string
.
IsNullOrEmpty
(
def
.
Usage
)
?
""
:
" "
+
def
.
Usage
);
int
nameWidth
=
TextRenderer
.
MeasureText
(
e
.
Graphics
,
nameText
,
_cmdNameFont
,
new
Size
(
int
.
MaxValue
,
int
.
MaxValue
),
TextFormatFlags
.
NoPrefix
|
TextFormatFlags
.
SingleLine
).
Width
;
if
(
nameWidth
>
maxWidth
)
{
// 命令本身已放不下:仅绘制命令名,末尾 ... 截断
TextRenderer
.
DrawText
(
e
.
Graphics
,
EllipsizeText
(
nameText
,
e
.
Graphics
,
_cmdNameFont
,
maxWidth
),
_cmdNameFont
,
new
Rectangle
(
e
.
Bounds
.
Left
+
pad
,
top
,
maxWidth
,
height
),
nameColor
,
TextFormatFlags
.
NoPrefix
|
TextFormatFlags
.
SingleLine
);
e
.
DrawFocusRectangle
();
return
;
}
TextRenderer
.
DrawText
(
e
.
Graphics
,
nameText
,
_cmdNameFont
,
new
Rectangle
(
e
.
Bounds
.
Left
+
pad
,
top
,
nameWidth
,
height
),
nameColor
,
TextFormatFlags
.
NoPrefix
|
TextFormatFlags
.
SingleLine
);
int
descMaxWidth
=
maxWidth
-
nameWidth
-
4
;
// 命令与说明间隔 4px
if
(
descMaxWidth
>
0
&&
!
string
.
IsNullOrEmpty
(
def
.
Desc
))
{
TextRenderer
.
DrawText
(
e
.
Graphics
,
EllipsizeText
(
def
.
Desc
,
e
.
Graphics
,
_cmdDescFont
,
descMaxWidth
),
_cmdDescFont
,
new
Rectangle
(
e
.
Bounds
.
Left
+
pad
+
nameWidth
+
4
,
top
,
descMaxWidth
,
height
),
descColor
,
TextFormatFlags
.
NoPrefix
|
TextFormatFlags
.
SingleLine
);
}
e
.
DrawFocusRectangle
();
}
/// <summary>文本超出可用宽度时以 ... 截断(按字符裁剪,避免中文被切半)</summary>
private
static
string
EllipsizeText
(
string
text
,
Graphics
g
,
Font
font
,
int
maxWidth
)
{
if
(
string
.
IsNullOrEmpty
(
text
)
||
maxWidth
<=
0
)
return
""
;
const
string
dots
=
"..."
;
int
dotsWidth
=
TextRenderer
.
MeasureText
(
g
,
dots
,
font
,
new
Size
(
int
.
MaxValue
,
int
.
MaxValue
),
TextFormatFlags
.
NoPrefix
|
TextFormatFlags
.
SingleLine
).
Width
;
if
(
TextRenderer
.
MeasureText
(
g
,
text
,
font
,
new
Size
(
int
.
MaxValue
,
int
.
MaxValue
),
TextFormatFlags
.
NoPrefix
|
TextFormatFlags
.
SingleLine
).
Width
+
dotsWidth
<=
maxWidth
)
return
text
;
string
t
=
text
;
while
(
t
.
Length
>
0
)
{
t
=
t
.
Substring
(
0
,
t
.
Length
-
1
);
if
(
TextRenderer
.
MeasureText
(
g
,
t
,
font
,
new
Size
(
int
.
MaxValue
,
int
.
MaxValue
),
TextFormatFlags
.
NoPrefix
|
TextFormatFlags
.
SingleLine
).
Width
+
dotsWidth
<=
maxWidth
)
break
;
}
return
t
+
dots
;
}
/// <summary>Tab:把菜单选中命令录入输入框(保留已输入的命令参数),不发送</summary>
private
void
CommitSelectedCommand
()
{
if
(
_lstCmd
==
null
||
!(
_lstCmd
.
SelectedItem
is
CmdDef
def
))
return
;
string
text
=
txtInput
.
Text
??
""
;
int
sp
=
text
.
IndexOf
(
' '
);
string
args
=
sp
>=
0
?
text
.
Substring
(
sp
+
1
)
:
""
;
// 无参数时补一个尾随空格,方便继续输入命令参数
txtInput
.
Text
=
args
.
Length
>
0
?
def
.
Name
+
" "
+
args
:
def
.
Name
+
" "
;
txtInput
.
SelectionStart
=
txtInput
.
TextLength
;
ShowCommandMenu
(
false
);
}
}
/// <summary>「存知识库」按钮(#36):把当前回答经确认后写入知识库(沙箱 + 覆盖确认)</summary>
/// <summary>「存知识库」按钮(#36):把当前回答经确认后写入知识库(沙箱 + 覆盖确认)</summary>
...
@@ -722,7 +862,23 @@ namespace Tools.MyAgent
...
@@ -722,7 +862,23 @@ namespace Tools.MyAgent
private
void
OnInputKeyDown
(
object
s
,
KeyEventArgs
e
)
private
void
OnInputKeyDown
(
object
s
,
KeyEventArgs
e
)
{
{
// 命令自动补全候选可见时:方向键选择 / Tab 补全 / Enter 补全并发送 / Esc 关闭
// 图片粘贴(#37/v2.2):Ctrl+V 且剪贴板含图片 → 走收图流程(输入框同样支持)
if
(
e
.
Control
&&
e
.
KeyCode
==
Keys
.
V
)
{
try
{
if
(
Clipboard
.
ContainsImage
()
||
ClipboardHasImageFile
())
{
e
.
Handled
=
true
;
e
.
SuppressKeyPress
=
true
;
OnPasteImage
();
return
;
}
}
catch
{
}
}
// 命令菜单可见时:↑/↓ 选择;Tab 录入命令;Enter 录入并发送消息;Esc 关闭
if
(
_lstCmd
!=
null
&&
_lstCmd
.
Visible
)
if
(
_lstCmd
!=
null
&&
_lstCmd
.
Visible
)
{
{
if
(
e
.
KeyCode
==
Keys
.
Down
)
if
(
e
.
KeyCode
==
Keys
.
Down
)
...
@@ -737,23 +893,25 @@ namespace Tools.MyAgent
...
@@ -737,23 +893,25 @@ namespace Tools.MyAgent
if
(
_lstCmd
.
SelectedIndex
>
0
)
_lstCmd
.
SelectedIndex
--;
if
(
_lstCmd
.
SelectedIndex
>
0
)
_lstCmd
.
SelectedIndex
--;
return
;
return
;
}
}
if
(
e
.
KeyCode
==
Keys
.
Tab
||
e
.
KeyCode
==
Keys
.
Enter
)
if
(
e
.
KeyCode
==
Keys
.
Tab
)
{
{
// Tab:仅把选中命令录入输入框(供继续编辑/输入参数),不发送
e
.
Handled
=
true
;
e
.
SuppressKeyPress
=
true
;
e
.
Handled
=
true
;
e
.
SuppressKeyPress
=
true
;
string
sel
=
_lstCmd
.
SelectedItem
?.
ToString
()
??
""
;
CommitSelectedCommand
();
int
sp
=
sel
.
IndexOf
(
' '
);
return
;
string
cmdText
=
sp
>
0
?
sel
.
Substring
(
0
,
sp
)
:
sel
;
}
txtInput
.
Text
=
cmdText
+
" "
;
txtInput
.
SelectionStart
=
txtInput
.
TextLength
;
_lstCmd
.
Visible
=
false
;
if
(
e
.
KeyCode
==
Keys
.
Enter
)
if
(
e
.
KeyCode
==
Keys
.
Enter
)
OnSendButtonClick
();
// 补全后回车直接发送
{
// Enter:录入选中命令并作为消息发送
e
.
Handled
=
true
;
e
.
SuppressKeyPress
=
true
;
CommitSelectedCommand
();
OnSendButtonClick
();
return
;
return
;
}
}
if
(
e
.
KeyCode
==
Keys
.
Escape
)
if
(
e
.
KeyCode
==
Keys
.
Escape
)
{
{
e
.
Handled
=
true
;
e
.
SuppressKeyPress
=
true
;
e
.
Handled
=
true
;
e
.
SuppressKeyPress
=
true
;
_lstCmd
.
Visible
=
false
;
ShowCommandMenu
(
false
)
;
return
;
return
;
}
}
}
}
...
@@ -876,6 +1034,15 @@ namespace Tools.MyAgent
...
@@ -876,6 +1034,15 @@ namespace Tools.MyAgent
return
;
return
;
int
charIdx
=
_rtfFallback
.
GetCharIndexFromPosition
(
e
.
Location
);
int
charIdx
=
_rtfFallback
.
GetCharIndexFromPosition
(
e
.
Location
);
// 空白点击(行尾右侧/最后一行下方)GetCharIndexFromPosition 会返回行尾/末尾字符索引,
// 导致误命中链接:先校验点击点确实落在字符上,否则视为空白点击 → 仅取消链接/文本选中
if
(!
IsClickOnChar
(
e
.
Location
,
charIdx
))
{
ClearLinkHover
();
if
(
charIdx
>=
0
&&
charIdx
<=
_rtfFallback
.
TextLength
)
_rtfFallback
.
Select
(
charIdx
,
0
);
return
;
}
// 工具轨迹折叠块:点击摘要行切换展开/折叠(对齐 WebView 的 <details> 交互)
// 工具轨迹折叠块:点击摘要行切换展开/折叠(对齐 WebView 的 <details> 交互)
if
(
TryToggleDetailsAt
(
charIdx
))
return
;
if
(
TryToggleDetailsAt
(
charIdx
))
return
;
string
path
;
string
path
;
...
@@ -888,6 +1055,18 @@ namespace Tools.MyAgent
...
@@ -888,6 +1055,18 @@ namespace Tools.MyAgent
if
(
_mouseButtonDown
)
return
;
// 拖拽选择中,不切换悬停效果
if
(
_mouseButtonDown
)
return
;
// 拖拽选择中,不切换悬停效果
int
charIdx
=
_rtfFallback
.
GetCharIndexFromPosition
(
e
.
Location
);
int
charIdx
=
_rtfFallback
.
GetCharIndexFromPosition
(
e
.
Location
);
// 空白处悬停(行尾右侧/最后一行下方)同样会返回行尾/末尾字符索引:校验不通过按空白处理,
// 且不缓存索引,保证光标移回链接时能重新命中
if
(!
IsClickOnChar
(
e
.
Location
,
charIdx
))
{
if
(
_lastHoverChar
!=
-
1
)
{
_lastHoverChar
=
-
1
;
ClearLinkHover
();
_rtfFallback
.
Cursor
=
_defaultChatCursor
;
}
return
;
}
if
(
charIdx
==
_lastHoverChar
)
return
;
if
(
charIdx
==
_lastHoverChar
)
return
;
_lastHoverChar
=
charIdx
;
_lastHoverChar
=
charIdx
;
...
@@ -924,6 +1103,43 @@ namespace Tools.MyAgent
...
@@ -924,6 +1103,43 @@ namespace Tools.MyAgent
_rtfFallback
.
Cursor
=
_defaultChatCursor
;
_rtfFallback
.
Cursor
=
_defaultChatCursor
;
}
}
/// <summary>
/// 校验点击点是否真实落在 charIdx 对应字符的占位内。
/// <para>RichTextBox.GetCharIndexFromPosition 在空白区域(行尾右侧、最后一行下方)会返回
/// 行尾/末尾字符的索引,导致空白点击/悬停误命中链接;此处用 GetPositionFromCharIndex
/// 反查字符实际坐标做命中校验,不通过即视为空白位置。</para>
/// </summary>
private
bool
IsClickOnChar
(
Point
click
,
int
charIdx
)
{
try
{
if
(
charIdx
<
0
||
charIdx
>=
_rtfFallback
.
TextLength
)
return
false
;
Point
p
=
_rtfFallback
.
GetPositionFromCharIndex
(
charIdx
);
if
(
p
.
X
<
0
||
p
.
Y
<
0
)
return
false
;
Point
pNext
=
_rtfFallback
.
GetPositionFromCharIndex
(
Math
.
Min
(
charIdx
+
1
,
_rtfFallback
.
TextLength
));
int
charWidth
,
lineHeight
;
if
(
pNext
.
X
>
p
.
X
&&
pNext
.
Y
==
p
.
Y
)
{
charWidth
=
pNext
.
X
-
p
.
X
;
// 同行:精确字符宽度
lineHeight
=
_rtfFallback
.
Font
.
Height
;
}
else
if
(
pNext
.
Y
>
p
.
Y
)
{
charWidth
=
lineHeight
=
Math
.
Max
(
pNext
.
Y
-
p
.
Y
,
_rtfFallback
.
Font
.
Height
);
// 行尾字符:按实际行高估算
}
else
{
charWidth
=
lineHeight
=
_rtfFallback
.
Font
.
Height
;
// 文档末尾:按字体估算
}
const
int
tolerance
=
2
;
return
click
.
X
>=
p
.
X
-
tolerance
&&
click
.
X
<
p
.
X
+
charWidth
+
tolerance
&&
click
.
Y
>=
p
.
Y
-
tolerance
&&
click
.
Y
<
p
.
Y
+
lineHeight
+
tolerance
;
}
catch
{
return
false
;
}
}
/// <summary>去掉路径末尾可能被正则一并吞入的标点(中文/英文),保持与实际文件一致</summary>
/// <summary>去掉路径末尾可能被正则一并吞入的标点(中文/英文),保持与实际文件一致</summary>
private
static
string
TrimPathTail
(
string
path
)
private
static
string
TrimPathTail
(
string
path
)
{
{
...
@@ -1172,40 +1388,270 @@ namespace Tools.MyAgent
...
@@ -1172,40 +1388,270 @@ namespace Tools.MyAgent
}
}
/// <summary>
/// <summary>
/// 粘贴截图处理(#37):RichBox 下 Ctrl+V 检测剪贴板图片 → 保存到 Agent/temp → 提示路径。
/// 粘贴截图处理(#37/v2.3):输入框或聊天区 Ctrl+V 检测剪贴板图片 → 保存到 Agent/temp。
/// <para>启用图片提问(设置 → 可选能力):截图进入「待发送附图列表」(左下角缩略图栏展示,最多 5 张,
/// 每张右上角 ✕ 可取消),随下一轮提问一并发送(多模态 image_url);未启用:仅保存并提示路径。</para>
/// </summary>
/// </summary>
internal
void
OnPasteImage
()
internal
void
OnPasteImage
()
{
{
try
try
{
{
if
(!
Clipboard
.
ContainsImage
())
return
;
string
file
=
TryAcquireClipboardImage
();
if
(
file
==
null
)
return
;
AddImageToPending
(
file
);
}
catch
(
Exception
ex
)
{
AddSystemMessage
(
$"处理截图失败:
{
ex
.
Message
}
"
);
}
}
/// <summary>
/// 统一收图入口(Ctrl+V 粘贴 / 拖入图片文件共用):
/// 启用图片提问 → 加入待发送附图列表(最多 5 张);未启用 → 仅保存并提示路径。
/// </summary>
/// <returns>是否成功加入待发送列表(未启用图片提问时返回 true 表示已保存提示)</returns>
private
bool
AddImageToPending
(
string
file
)
{
if
(
_config
.
EnableImageInput
)
{
if
(
_pendingImages
.
Count
>=
5
)
{
AgentUtils
.
Toast
(
"最多同时携带 5 张图片,请先发送或取消部分图片"
);
return
false
;
}
_pendingImages
.
Add
(
file
);
EnsurePendingBar
();
RefreshPendingBar
();
AgentUtils
.
Toast
(
$"已添加图片(
{
_pendingImages
.
Count
}
/5),随下一轮提问发送"
);
return
true
;
}
_messages
.
Add
(
new
ChatMessage
{
Role
=
"user"
,
Content
=
$"(已收到图片,已保存:
{
file
}
;可在设置中启用「图片提问」后随提问发送)"
});
_messages
.
Add
(
new
ChatMessage
{
Role
=
"assistant"
,
Content
=
$"已收到图片,保存位置:
{
file
}
"
});
RefreshChat
();
return
true
;
}
// ── 拖入图片(与 Ctrl+V 粘贴同一套收图流程)──
/// <summary>输入框拖入悬停:仅接受图片文件(显示可放置光标)</summary>
private
void
OnInputDragEnter
(
object
sender
,
DragEventArgs
e
)
{
e
.
Effect
=
DragDropEffects
.
None
;
try
{
if
(
e
.
Data
==
null
||
!
e
.
Data
.
GetDataPresent
(
DataFormats
.
FileDrop
))
return
;
var
files
=
(
string
[])
e
.
Data
.
GetData
(
DataFormats
.
FileDrop
);
if
(
files
!=
null
&&
files
.
Any
(
IsImageFile
))
e
.
Effect
=
DragDropEffects
.
Copy
;
}
catch
{
}
}
/// <summary>输入框拖放:图片文件逐个加入待发送附图列表(超 5 张时提示)</summary>
private
void
OnInputDragDrop
(
object
sender
,
DragEventArgs
e
)
{
try
{
if
(
e
.
Data
==
null
||
!
e
.
Data
.
GetDataPresent
(
DataFormats
.
FileDrop
))
return
;
var
files
=
(
string
[])
e
.
Data
.
GetData
(
DataFormats
.
FileDrop
);
if
(
files
==
null
)
return
;
int
added
=
0
;
foreach
(
string
f
in
files
)
{
if
(!
IsImageFile
(
f
))
continue
;
if
(
AddImageToPending
(
f
))
added
++;
}
if
(
added
==
0
)
AgentUtils
.
Toast
(
"未识别到图片文件(支持 png/jpg/jpeg/bmp/gif)"
);
}
catch
(
Exception
ex
)
{
AddSystemMessage
(
$"处理拖入图片失败:
{
ex
.
Message
}
"
);
}
}
// ── v2.3 待发送附图缩略图栏 ──
/// <summary>创建缩略图栏(问答区左下角悬浮;懒创建)</summary>
private
void
EnsurePendingBar
()
{
if
(
_pendingBar
!=
null
)
return
;
_pendingBar
=
new
FlowLayoutPanel
{
FlowDirection
=
FlowDirection
.
LeftToRight
,
WrapContents
=
false
,
AutoScroll
=
true
,
Height
=
72
,
Padding
=
new
Padding
(
4
),
BackColor
=
Color
.
FromArgb
(
250
,
250
,
252
),
BorderStyle
=
BorderStyle
.
FixedSingle
,
Visible
=
false
,
Anchor
=
AnchorStyles
.
Bottom
|
AnchorStyles
.
Left
,
};
panelChat
.
Controls
.
Add
(
_pendingBar
);
_pendingBar
.
BringToFront
();
}
/// <summary>刷新缩略图栏:按待发送列表重建缩略图块;空则隐藏</summary>
private
void
RefreshPendingBar
()
{
if
(
_pendingBar
==
null
)
return
;
foreach
(
Control
c
in
_pendingBar
.
Controls
)
DisposeThumbImages
(
c
);
_pendingBar
.
Controls
.
Clear
();
if
(
_pendingImages
.
Count
==
0
)
{
_pendingBar
.
Visible
=
false
;
return
;
}
foreach
(
var
f
in
_pendingImages
)
_pendingBar
.
Controls
.
Add
(
CreateThumb
(
f
));
_pendingBar
.
Width
=
Math
.
Min
(
panelChat
.
ClientSize
.
Width
-
8
,
_pendingImages
.
Count
*
72
+
12
);
_pendingBar
.
Location
=
new
Point
(
4
,
Math
.
Max
(
0
,
panelChat
.
ClientSize
.
Height
-
_pendingBar
.
Height
-
4
));
_pendingBar
.
Visible
=
true
;
}
/// <summary>单个缩略图块:图片预览(Zoom)+ 右上角 ✕ 取消;点击缩略图按原始尺寸放大查看</summary>
private
Control
CreateThumb
(
string
file
)
{
var
box
=
new
Panel
{
Width
=
68
,
Height
=
64
,
Margin
=
new
Padding
(
2
),
BackColor
=
Color
.
White
};
try
{
var
pic
=
new
PictureBox
{
Dock
=
DockStyle
.
Fill
,
SizeMode
=
PictureBoxSizeMode
.
Zoom
,
Image
=
Image
.
FromFile
(
file
),
Cursor
=
Cursors
.
Hand
,
};
// 点击缩略图 → 弹出原始尺寸查看器(超屏自动限制在工作区内;点击/Esc 关闭)
pic
.
Click
+=
(
s
,
e
)
=>
{
try
{
wBitmapView
.
ShowImage
(
file
,
this
);
}
catch
(
Exception
ex
)
{
AgentUtils
.
Toast
(
"查看图片失败:"
+
ex
.
Message
);
}
};
box
.
Click
+=
(
s
,
e
)
=>
{
try
{
wBitmapView
.
ShowImage
(
file
,
this
);
}
catch
(
Exception
ex
)
{
AgentUtils
.
Toast
(
"查看图片失败:"
+
ex
.
Message
);
}
};
if
(
tipScope
!=
null
)
tipScope
.
SetToolTip
(
pic
,
"点击查看原图(原始尺寸)"
);
box
.
Controls
.
Add
(
pic
);
}
catch
{
}
var
close
=
new
Label
{
Text
=
"✕"
,
AutoSize
=
false
,
Size
=
new
Size
(
17
,
17
),
Location
=
new
Point
(
box
.
Width
-
19
,
0
),
BackColor
=
Color
.
FromArgb
(
205
,
60
,
60
),
ForeColor
=
Color
.
White
,
TextAlign
=
ContentAlignment
.
MiddleCenter
,
Font
=
new
Font
(
"微软雅黑"
,
8F
,
FontStyle
.
Bold
),
Cursor
=
Cursors
.
Hand
,
};
close
.
Click
+=
(
s
,
e
)
=>
{
_pendingImages
.
Remove
(
file
);
RefreshPendingBar
();
};
box
.
Controls
.
Add
(
close
);
close
.
BringToFront
();
return
box
;
}
/// <summary>递归释放缩略图 PictureBox 的图片资源(防 GDI 对象泄漏)</summary>
private
static
void
DisposeThumbImages
(
Control
c
)
{
var
pic
=
c
as
PictureBox
;
if
(
pic
!=
null
)
{
if
(
pic
.
Image
!=
null
)
{
pic
.
Image
.
Dispose
();
pic
.
Image
=
null
;
}
return
;
}
foreach
(
Control
child
in
c
.
Controls
)
DisposeThumbImages
(
child
);
}
/// <summary>
/// 获取剪贴板图片:① 位图剪贴板(截图工具)→ 保存到 Agent/temp;
/// ② 文件列表(资源管理器复制的图片文件)→ 直接返回原文件路径。
/// 无图片返回 null。
/// </summary>
private
string
TryAcquireClipboardImage
()
{
if
(
Clipboard
.
ContainsImage
())
{
using
(
var
img
=
Clipboard
.
GetImage
())
using
(
var
img
=
Clipboard
.
GetImage
())
{
{
if
(
img
==
null
)
return
;
if
(
img
==
null
)
return
null
;
string
dir
=
Path
.
Combine
(
DefaultKbRoot
,
"temp"
);
string
dir
=
Path
.
Combine
(
DefaultKbRoot
,
"temp"
);
Directory
.
CreateDirectory
(
dir
);
Directory
.
CreateDirectory
(
dir
);
string
file
=
Path
.
Combine
(
dir
,
$"截图_
{
DateTime
.
Now
:
yyyyMMdd_HHmmss
}
.png"
);
string
file
=
Path
.
Combine
(
dir
,
$"截图_
{
DateTime
.
Now
:
yyyyMMdd_HHmmss
}
.png"
);
img
.
Save
(
file
,
System
.
Drawing
.
Imaging
.
ImageFormat
.
Png
);
img
.
Save
(
file
,
System
.
Drawing
.
Imaging
.
ImageFormat
.
Png
);
_producedFiles
.
Add
(
file
);
return
file
;
}
}
if
(
Clipboard
.
ContainsFileDropList
())
{
var
files
=
Clipboard
.
GetFileDropList
();
if
(
files
!=
null
)
foreach
(
string
f
in
files
)
if
(
IsImageFile
(
f
))
return
f
;
}
return
null
;
}
_messages
.
Add
(
new
ChatMessage
{
Role
=
"user"
,
Content
=
$"(已粘贴截图,已保存:
{
file
}
)"
});
/// <summary>剪贴板文件列表中是否含图片文件(资源管理器复制的 png/jpg/bmp/gif)</summary>
_messages
.
Add
(
new
ChatMessage
{
Role
=
"assistant"
,
Content
=
$"已收到截图,保存位置:
{
file
}
"
});
private
static
bool
ClipboardHasImageFile
()
RefreshChat
();
{
if
(!
Clipboard
.
ContainsFileDropList
())
return
false
;
try
{
var
files
=
Clipboard
.
GetFileDropList
();
if
(
files
==
null
)
return
false
;
foreach
(
string
f
in
files
)
if
(
IsImageFile
(
f
))
return
true
;
}
}
catch
{
}
return
false
;
}
}
catch
(
Exception
ex
)
/// <summary>是否为受支持的图片文件(png/jpg/jpeg/bmp/gif)</summary>
private
static
bool
IsImageFile
(
string
f
)
{
{
AddSystemMessage
(
$"处理截图失败:
{
ex
.
Message
}
"
);
string
ext
=
Path
.
GetExtension
(
f
??
""
).
ToLowerInvariant
();
return
ext
==
".png"
||
ext
==
".jpg"
||
ext
==
".jpeg"
||
ext
==
".bmp"
||
ext
==
".gif"
;
}
}
/// <summary>
/// 窗体级 Ctrl+V 图片拦截(兜底):无论焦点在输入框/聊天区/任意子控件,
/// 剪贴板含图片时统一走收图流程;纯文本粘贴不受影响(走默认)。
/// </summary>
protected
override
bool
ProcessCmdKey
(
ref
Message
msg
,
Keys
keyData
)
{
if
(
keyData
==
(
Keys
.
Control
|
Keys
.
V
))
{
try
{
if
(
Clipboard
.
ContainsImage
()
||
ClipboardHasImageFile
())
{
OnPasteImage
();
return
true
;
// 已处理(含图片时阻止默认粘贴;纯文本/非图片文件时下方放行)
}
}
catch
{
}
}
return
base
.
ProcessCmdKey
(
ref
msg
,
keyData
);
}
}
/// <summary>RichBox 截图粘贴入口:Ctrl+V 且剪贴板含图片时拦截并走
保存流程(#37
)</summary>
/// <summary>RichBox 截图粘贴入口:Ctrl+V 且剪贴板含图片时拦截并走
收图流程(#37/v2.3
)</summary>
private
void
OnChatRtfKeyDown
(
object
sender
,
KeyEventArgs
e
)
private
void
OnChatRtfKeyDown
(
object
sender
,
KeyEventArgs
e
)
{
{
if
(!
e
.
Control
||
e
.
KeyCode
!=
Keys
.
V
)
return
;
if
(!
e
.
Control
||
e
.
KeyCode
!=
Keys
.
V
)
return
;
try
try
{
{
if
(
Clipboard
.
ContainsImage
())
if
(
Clipboard
.
ContainsImage
()
||
ClipboardHasImageFile
()
)
{
{
e
.
Handled
=
true
;
e
.
Handled
=
true
;
e
.
SuppressKeyPress
=
true
;
e
.
SuppressKeyPress
=
true
;
...
@@ -1588,9 +2034,35 @@ namespace Tools.MyAgent
...
@@ -1588,9 +2034,35 @@ namespace Tools.MyAgent
// ── 斜杠命令(#35:/model /scope /export /kb 检索 /clear /settings /help) ──
// ── 斜杠命令(#35:/model /scope /export /kb 检索 /clear /settings /help) ──
/// <summary>可用斜杠命令(自动补全只保留高频项;/model /scope /settings /workflow 仍可用,不在候选列出)</summary>
/// <summary>可用斜杠命令(自动补全只保留高频项;/model /scope /settings /workflow 仍可用,不在候选列出)</summary>
private
static
readonly
string
[]
SlashCommands
=
/// <summary>斜杠命令定义(名称 / 参数占位 / 说明),供「/」命令菜单列出与过滤</summary>
private
sealed
class
CmdDef
{
public
string
Name
;
// 命令名,如 /kb
public
string
Usage
;
// 参数占位,如 [关键词];无参数为空
public
string
Desc
;
// 命令说明
public
CmdDef
(
string
name
,
string
usage
,
string
desc
)
{
Name
=
name
;
Usage
=
usage
;
Desc
=
desc
;
}
}
/// <summary>全部可用斜杠命令(命令菜单按此列表展示:命令 + 参数 + 说明)</summary>
private
static
readonly
CmdDef
[]
AllCommands
=
{
{
"/clear"
,
"/kb"
,
"/export"
,
"/backup"
,
"/restore"
,
"/help"
,
new
CmdDef
(
"/clear"
,
""
,
"清空当前对话"
),
new
CmdDef
(
"/kb"
,
"[关键词]"
,
"检索知识库;无参数时选择知识库目录"
),
new
CmdDef
(
"/export"
,
"[文件名]"
,
"导出当前会话为 Markdown"
),
new
CmdDef
(
"/backup"
,
""
,
"一键备份会话、配置与知识库摘要"
),
new
CmdDef
(
"/restore"
,
""
,
"从备份 zip 恢复会话与配置"
),
new
CmdDef
(
"/glossary"
,
"术语=标准名"
,
"向知识库术语表沉淀一条术语"
),
new
CmdDef
(
"/settings"
,
""
,
"打开智能体设置"
),
new
CmdDef
(
"/model"
,
"<模型名>"
,
"快速切换模型"
),
new
CmdDef
(
"/scope"
,
"<目录>"
,
"切换知识库内搜索范围"
),
new
CmdDef
(
"/workflow"
,
""
,
"生成知识库周报(需启用可选能力)"
),
new
CmdDef
(
"/help"
,
""
,
"查看全部命令与说明"
),
};
};
private
void
HandleSlashCommand
(
string
text
)
private
void
HandleSlashCommand
(
string
text
)
...
@@ -1662,9 +2134,10 @@ namespace Tools.MyAgent
...
@@ -1662,9 +2134,10 @@ namespace Tools.MyAgent
"- `/workflow` - 知识库周报(可选能力,需在高级设置启用)\n"
+
"- `/workflow` - 知识库周报(可选能力,需在高级设置启用)\n"
+
"- `/help` - 显示此帮助\n"
+
"- `/help` - 显示此帮助\n"
+
"\n**快捷操作**\n"
+
"\n**快捷操作**\n"
+
"- `Enter` - 发送;输入 `/` 可呼出命令自动补全(Tab 选中)\n"
+
"- 输入 `/` 呼出命令菜单:`↑/↓` 选择 · `Tab` 录入命令 · `Enter` 发送消息\n"
+
"- `Enter` - 发送(命令菜单打开时:录入选中命令并发送)\n"
+
"- `Shift+Enter` / `Ctrl+Enter` - 换行\n"
+
"- `Shift+Enter` / `Ctrl+Enter` - 换行\n"
+
"- `ESC` - 取消回复"
;
"- `ESC` -
关闭命令菜单 /
取消回复"
;
}
}
/// <summary>/kb <关键词>:直接检索知识库并展示结果</summary>
/// <summary>/kb <关键词>:直接检索知识库并展示结果</summary>
...
...
Tools/Tools.csproj
View file @
23830cb4
...
@@ -190,6 +190,9 @@
...
@@ -190,6 +190,9 @@
<Compile
Include=
"MyAgent\Forms\AgentSettingsForm.Designer.cs"
>
<Compile
Include=
"MyAgent\Forms\AgentSettingsForm.Designer.cs"
>
<DependentUpon>
AgentSettingsForm.cs
</DependentUpon>
<DependentUpon>
AgentSettingsForm.cs
</DependentUpon>
</Compile>
</Compile>
<Compile
Include=
"MyAgent\Forms\wBitmapView.cs"
>
<SubType>
Form
</SubType>
</Compile>
<Compile
Include=
"MyAgent\Kb\KbConfig.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbConfig.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbService.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbService.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbIndex.cs"
/>
<Compile
Include=
"MyAgent\Kb\KbIndex.cs"
/>
...
@@ -204,6 +207,9 @@
...
@@ -204,6 +207,9 @@
<Compile
Include=
"MyAgent\Support\ChatSession.cs"
/>
<Compile
Include=
"MyAgent\Support\ChatSession.cs"
/>
<Compile
Include=
"MyAgent\Support\ChatRtfCache.cs"
/>
<Compile
Include=
"MyAgent\Support\ChatRtfCache.cs"
/>
<Compile
Include=
"MyAgent\Support\MarkdownRenderer.cs"
/>
<Compile
Include=
"MyAgent\Support\MarkdownRenderer.cs"
/>
<Compile
Include=
"MyAgent\Support\NoCaretRichTextBox.cs"
>
<SubType>
Component
</SubType>
</Compile>
<Compile
Include=
"MyAgent\wMyAgent.cs"
>
<Compile
Include=
"MyAgent\wMyAgent.cs"
>
<SubType>
Form
</SubType>
<SubType>
Form
</SubType>
</Compile>
</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