Commit 23830cb4 authored by 段廷瑞's avatar 段廷瑞

feat: 智能体交互优化:图片,命令行等

parent c64815c5
......@@ -85,6 +85,9 @@ namespace Tools.MyAgent.Agent
/// <summary>是否向模型下发可选/高级工具(默认只下发核心工具)</summary>
private bool _includeOptionalTools;
/// <summary>v2.2 多模态:是否启用图片提问(附图以 image_url 内容块发送)</summary>
private bool _imageInputEnabled;
/// <summary>单次回答总时长兜底</summary>
private static readonly TimeSpan MaxRunDuration = TimeSpan.FromMinutes(10);
......@@ -120,7 +123,7 @@ namespace Tools.MyAgent.Agent
_contextTokens = cfg.MaxContextTokens > 0 ? cfg.MaxContextTokens : 32000;
_prompts.CustomLayer = cfg.CustomSystemPrompt ?? "";
_prompts.SystemPromptEnabled = cfg.SystemPromptEnabled;
_prompts.AutoMemory = cfg.EnableAutoMemory; // E3 问答沉淀提示(只提示确认,绝不静默写入)
_imageInputEnabled = cfg.EnableImageInput; // v2.2 多模态:图片提问开关
// #31 故障转移配置
_failover.Configure(cfg.EnableFailover, cfg.BackupBaseUrl, cfg.BackupModel, cfg.BackupApiKey);
......@@ -445,7 +448,7 @@ namespace Tools.MyAgent.Agent
{
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))
{
......@@ -470,6 +473,80 @@ namespace Tools.MyAgent.Agent
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&lt;string&gt;() 抛 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>
private static int DropOldestUntilUnder(JArray list, int budget)
{
......@@ -510,9 +587,19 @@ namespace Tools.MyAgent.Agent
int take = Math.Min(6, list.Count - 1);
if (take < 2) return 0;
var earliest = new JArray();
// 拼接最早消息文本(多模态消息仅取文本块,忽略图片,避免 data URL 大图进入压缩 prompt)
var sb = new StringBuilder();
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;
try
......@@ -520,7 +607,7 @@ namespace Tools.MyAgent.Agent
var prompt = new JArray
{
new JObject { ["role"] = "system", ["content"] = HistorySummaryPrompt },
new JObject { ["role"] = "user", ["content"] = earliest.ToString(Newtonsoft.Json.Formatting.None) }
new JObject { ["role"] = "user", ["content"] = earliestText }
};
summary = await _client.CompleteAsync(prompt, 512, ct).ConfigureAwait(false);
}
......@@ -554,7 +641,7 @@ namespace Tools.MyAgent.Agent
foreach (var m in messages)
{
total += 4; // role / 分隔符开销
var content = m["content"]?.Value<string>();
var content = ContentText(m["content"]);
if (!string.IsNullOrEmpty(content)) total += EstimateTokens(content);
var tc = m["tool_calls"] as JArray;
if (tc != null) total += 8 * tc.Count;
......
......@@ -18,9 +18,6 @@ namespace Tools.MyAgent.Agent
/// <summary>系统提示词启用开关(由设置注入;仅启用后才注入 CustomLayer)</summary>
public bool SystemPromptEnabled { get; set; }
/// <summary>E3 问答沉淀提示:开启后在工具规则尾部追加「可询问用户是否沉淀结论」(绝不静默写入)</summary>
public bool AutoMemory { get; set; }
public string BuildSystemMessage(KbService kb)
{
// B2 提示词外置:静态层优先读 输出目录/prompts/*.md 模板(用户可定制),
......@@ -42,13 +39,6 @@ namespace Tools.MyAgent.Agent
sb.AppendLine(LoadLayer(promptsDir, "security.md", SecurityLayer));
sb.AppendLine();
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();
}
......
......@@ -90,7 +90,7 @@
/// <summary>是否启用可选/高级工具(db_query、module_patch、img_search、/workflow)</summary>
public bool EnableOptionalTools { get; set; } = false;
/// <summary>回答结束后是否提示把有价值结论沉淀到知识库(只提示确认,绝不静默写入;默认关闭)</summary>
public bool EnableAutoMemory { get; set; } = false;
/// <summary>是否启用图片提问(多模态,v2.2):启用后粘贴截图随提问以 image_url 发送,需视觉模型支持(如 DeepSeek-VL / qwen-vl);默认启用</summary>
public bool EnableImageInput { get; set; } = true;
}
}
......@@ -82,7 +82,7 @@
this.tabOther = new System.Windows.Forms.TabPage();
this.grpOpt = new System.Windows.Forms.GroupBox();
this.chkOpt = new System.Windows.Forms.CheckBox();
this.chkAutoMemory = new System.Windows.Forms.CheckBox();
this.chkImageInput = new System.Windows.Forms.CheckBox();
this.grpFail = new System.Windows.Forms.GroupBox();
this.chkFail = new System.Windows.Forms.CheckBox();
this.lblBUrl = new System.Windows.Forms.Label();
......@@ -717,7 +717,7 @@
// grpOpt
//
this.grpOpt.Controls.Add(this.chkOpt);
this.grpOpt.Controls.Add(this.chkAutoMemory);
this.grpOpt.Controls.Add(this.chkImageInput);
this.grpOpt.Location = new System.Drawing.Point(8, 12);
this.grpOpt.Name = "grpOpt";
this.grpOpt.Size = new System.Drawing.Size(596, 92);
......@@ -735,15 +735,17 @@
this.chkOpt.Text = "启用可选/高级工具(db_query、module_patch、img_search、/workflow)";
this.chkOpt.UseVisualStyleBackColor = true;
//
// chkAutoMemory
// chkImageInput
//
this.chkAutoMemory.AutoSize = true;
this.chkAutoMemory.Location = new System.Drawing.Point(16, 54);
this.chkAutoMemory.Name = "chkAutoMemory";
this.chkAutoMemory.Size = new System.Drawing.Size(351, 21);
this.chkAutoMemory.TabIndex = 1;
this.chkAutoMemory.Text = "回答后提示沉淀有价值结论到知识库(只提示,不静默写入)";
this.chkAutoMemory.UseVisualStyleBackColor = true;
this.chkImageInput.AutoSize = true;
this.chkImageInput.Checked = true;
this.chkImageInput.CheckState = System.Windows.Forms.CheckState.Checked;
this.chkImageInput.Location = new System.Drawing.Point(16, 54);
this.chkImageInput.Name = "chkImageInput";
this.chkImageInput.Size = new System.Drawing.Size(540, 21);
this.chkImageInput.TabIndex = 1;
this.chkImageInput.Text = "启用图片提问(多模态):粘贴/拖入截图随提问以 image_url 发送,需视觉模型支持(如 qwen-vl)";
this.chkImageInput.UseVisualStyleBackColor = true;
//
// grpFail
//
......@@ -957,7 +959,7 @@
private System.Windows.Forms.TabPage tabOther;
private System.Windows.Forms.GroupBox grpOpt;
private System.Windows.Forms.CheckBox chkOpt;
private System.Windows.Forms.CheckBox chkAutoMemory;
private System.Windows.Forms.CheckBox chkImageInput;
private System.Windows.Forms.GroupBox grpFail;
private System.Windows.Forms.CheckBox chkFail;
private System.Windows.Forms.Label lblBUrl;
......
......@@ -137,7 +137,7 @@ namespace Tools.MyAgent.Forms
// 其他:可选能力
chkOpt.Checked = _config.EnableOptionalTools;
chkAutoMemory.Checked = _config.EnableAutoMemory;
chkImageInput.Checked = _config.EnableImageInput; // v2.2 多模态:图片提问开关
// 其他:故障转移
chkFail.Checked = _config.EnableFailover;
......@@ -429,7 +429,7 @@ namespace Tools.MyAgent.Forms
_config.CustomSystemPrompt = txtCustomPrompt.Text.Trim();
// 其他:可选能力
_config.EnableOptionalTools = chkOpt.Checked;
_config.EnableAutoMemory = chkAutoMemory.Checked;
_config.EnableImageInput = chkImageInput.Checked; // v2.2 多模态:图片提问开关
// 其他:故障转移
_config.EnableFailover = chkFail.Checked;
_config.BackupBaseUrl = txtBUrl.Text.Trim();
......
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;
}
}
}
using System;
using System.Collections.Generic;
namespace Tools.MyAgent
{
......@@ -12,5 +13,11 @@ namespace Tools.MyAgent
public string ThinkingContent { get; set; }
public bool IsStreaming { get; set; }
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; }
}
}
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 { }
}
}
}
}
......@@ -33,10 +33,14 @@ namespace Tools.MyAgent
private CancellationTokenSource _sendCts;
private string _historyDir;
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 Point _mouseDownPos = Point.Empty;
private bool _mouseButtonDown;
private Cursor _defaultChatCursor = Cursors.IBeam;
private Cursor _defaultChatCursor = Cursors.Arrow; // 问答区不支持输入:默认箭头光标(原 IBeam)
private int _hoverStart = -1;
private int _hoverLength = 0;
private Color _hoverOrigBack = Color.Empty;
......@@ -91,7 +95,7 @@ namespace Tools.MyAgent
private RichTextBox CreateRtfFallback()
{
var rtf = new RichTextBox
var rtf = new NoCaretRichTextBox
{
Dock = DockStyle.Fill,
ReadOnly = true,
......@@ -99,6 +103,7 @@ namespace Tools.MyAgent
BorderStyle = BorderStyle.None,
Font = new Font("微软雅黑", 10F),
TabIndex = 0,
Cursor = Cursors.Arrow, // 问答区不支持输入:默认箭头光标(悬停链接时仍切换为手型)
};
panelChat.Controls.Add(rtf);
return rtf;
......@@ -110,6 +115,8 @@ namespace Tools.MyAgent
partial void OnFormDisposing()
{
SaveUnsavedSession();
try { _cmdNameFont?.Dispose(); } catch { }
try { _cmdDescFont?.Dispose(); } catch { }
try { _engine.Cancel(); } catch { }
try { _engine.Dispose(); } catch { }
try { _kb.ShutdownIndex(); } catch { } // 终止索引后台构建,关闭 SQLite 连接
......@@ -196,7 +203,7 @@ namespace Tools.MyAgent
BackupModel = cfg.BackupModel,
BackupApiKey = cfg.BackupApiKey,
EnableOptionalTools = cfg.EnableOptionalTools,
EnableAutoMemory = cfg.EnableAutoMemory,
EnableImageInput = cfg.EnableImageInput,
};
File.WriteAllText(AgentConfigFilePath, Newtonsoft.Json.JsonConvert.SerializeObject(toSave), System.Text.Encoding.UTF8);
}
......@@ -267,6 +274,10 @@ namespace Tools.MyAgent
lblCopy.LinkClicked += (s, e) => CopyLastAnswer();
btnSend.Click += (s, e) => OnSendButtonClick();
txtInput.KeyDown += OnInputKeyDown;
// 输入框拖入图片文件 → 加入待发送附图列表(与 Ctrl+V 粘贴同一套收图流程)
txtInput.AllowDrop = true;
txtInput.DragEnter += OnInputDragEnter;
txtInput.DragDrop += OnInputDragDrop;
KeyDown += OnFormKeyDown;
// RichBox 模式的点击/悬停交互(路径点击、details 折叠切换)
WireRtfEvents();
......@@ -335,7 +346,7 @@ namespace Tools.MyAgent
var lines = new System.Collections.Generic.List<string>
{
$"你好,我是本地知识库助手(模型:{_config.Model})。",
$"知识库:{kbInfo}。输入问题即可检索回答;/help 查看命令。",
$"知识库:{kbInfo}。输入问题即可检索回答;输入 / 呼出命令菜单。",
};
if (string.IsNullOrEmpty(_config.ApiKey))
lines.Add("API Key 未配置(点击「设置」填入后可开始回答)。");
......@@ -411,7 +422,7 @@ namespace Tools.MyAgent
private async void SendMessage()
{
string text = txtInput.Text.Trim();
if (text.Length == 0) return;
if (text.Length == 0 && _pendingImages.Count == 0) return; // 允许「仅附图」发送(v2.2)
if (_engine.IsRunning) return;
// 记录历史提示词(与上一条相同则跳过),并复位浏览位置
......@@ -436,7 +447,14 @@ namespace Tools.MyAgent
SetInputEnabled(false);
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 });
_sessionSaved = false; // 对话内容已变化,下次新对话需重新记录历史
_producedFiles.Clear(); // 新一轮开始,清空上轮输出文件
......@@ -471,6 +489,8 @@ namespace Tools.MyAgent
_messages.Clear();
_producedFiles.Clear();
_expandedDetails.Clear();
_pendingImages.Clear(); // 新对话:清空待发送附图
RefreshPendingBar();
_sessionSaved = true;
_rtfCache.Clear();
ResetInputState();
......@@ -616,9 +636,20 @@ namespace Tools.MyAgent
txtInput.Focus();
}
// ── 斜杠命令自动补全(#35)与「存知识库」按钮(#36) ──
// ── 斜杠命令菜单(#35)与「存知识库」按钮(#36) ──
// 交互:输入 / 弹出命令+说明列表;↑/↓ 选择;Tab 录入命令;Enter 录入并发送;Esc 关闭
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()
{
......@@ -629,13 +660,37 @@ namespace Tools.MyAgent
IntegralHeight = false,
BorderStyle = BorderStyle.FixedSingle,
BackColor = Color.White,
ItemHeight = 18,
ItemHeight = 24,
DrawMode = DrawMode.OwnerDrawFixed,
TabStop = false,
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(_lblCmdHint);
_lstCmd.BringToFront();
_lblCmdHint.BringToFront();
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)
......@@ -647,36 +702,121 @@ namespace Tools.MyAgent
{
if (_lstCmd == null) return;
string text = txtInput.Text ?? "";
if (!text.StartsWith("/") || text.Length <= 1)
// 输入 /(含仅一个斜杠)即弹出全部命令;继续输入按命令名前缀过滤
if (!text.StartsWith("/"))
{
_lstCmd.Visible = false;
_lstCmd.Items.Clear();
ShowCommandMenu(false);
return;
}
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)
{
_lstCmd.Visible = false;
_lstCmd.Items.Clear();
ShowCommandMenu(false);
return;
}
_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)
{
if (_lstCmd == null) return;
_lstCmd.Visible = show;
if (_lblCmdHint != null) _lblCmdHint.Visible = show;
if (!show) _lstCmd.Items.Clear();
}
/// <summary>自绘命令项:单行显示「命令+参数 说明」,过长时以 ... 省略</summary>
private void OnCmdDrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0 || e.Index >= _lstCmd.Items.Count) return;
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)
{
string hint = m == "/kb" ? " [关键词或选择目录]"
: m == "/model" ? " <模型名>"
: m == "/scope" ? " <目录>"
: m == "/export" ? " [文件名]"
: "";
_lstCmd.Items.Add(m + hint);
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);
}
_lstCmd.SelectedIndex = 0;
_lstCmd.Height = Math.Min(150, _lstCmd.Items.Count * _lstCmd.ItemHeight + 4);
_lstCmd.Width = 260;
_lstCmd.Location = new Point(panelBottom.Left + 8, Math.Max(0, panelBottom.Top - _lstCmd.Height));
_lstCmd.Visible = true;
_lstCmd.BringToFront();
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>
......@@ -722,7 +862,23 @@ namespace Tools.MyAgent
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 (e.KeyCode == Keys.Down)
......@@ -737,23 +893,25 @@ namespace Tools.MyAgent
if (_lstCmd.SelectedIndex > 0) _lstCmd.SelectedIndex--;
return;
}
if (e.KeyCode == Keys.Tab || e.KeyCode == Keys.Enter)
if (e.KeyCode == Keys.Tab)
{
// Tab:仅把选中命令录入输入框(供继续编辑/输入参数),不发送
e.Handled = true; e.SuppressKeyPress = true;
string sel = _lstCmd.SelectedItem?.ToString() ?? "";
int sp = sel.IndexOf(' ');
string cmdText = sp > 0 ? sel.Substring(0, sp) : sel;
txtInput.Text = cmdText + " ";
txtInput.SelectionStart = txtInput.TextLength;
_lstCmd.Visible = false;
if (e.KeyCode == Keys.Enter)
OnSendButtonClick(); // 补全后回车直接发送
CommitSelectedCommand();
return;
}
if (e.KeyCode == Keys.Enter)
{
// Enter:录入选中命令并作为消息发送
e.Handled = true; e.SuppressKeyPress = true;
CommitSelectedCommand();
OnSendButtonClick();
return;
}
if (e.KeyCode == Keys.Escape)
{
e.Handled = true; e.SuppressKeyPress = true;
_lstCmd.Visible = false;
ShowCommandMenu(false);
return;
}
}
......@@ -876,6 +1034,15 @@ namespace Tools.MyAgent
return;
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 的 &lt;details&gt; 交互)
if (TryToggleDetailsAt(charIdx)) return;
string path;
......@@ -888,6 +1055,18 @@ namespace Tools.MyAgent
if (_mouseButtonDown) return; // 拖拽选择中,不切换悬停效果
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;
_lastHoverChar = charIdx;
......@@ -924,6 +1103,43 @@ namespace Tools.MyAgent
_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>
private static string TrimPathTail(string path)
{
......@@ -1172,40 +1388,270 @@ namespace Tools.MyAgent
}
/// <summary>
/// 粘贴截图处理(#37):RichBox 下 Ctrl+V 检测剪贴板图片 → 保存到 Agent/temp → 提示路径。
/// 粘贴截图处理(#37/v2.3):输入框或聊天区 Ctrl+V 检测剪贴板图片 → 保存到 Agent/temp。
/// <para>启用图片提问(设置 → 可选能力):截图进入「待发送附图列表」(左下角缩略图栏展示,最多 5 张,
/// 每张右上角 ✕ 可取消),随下一轮提问一并发送(多模态 image_url);未启用:仅保存并提示路径。</para>
/// </summary>
internal void OnPasteImage()
{
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())
{
if (img == null) return;
if (img == null) return null;
string dir = Path.Combine(DefaultKbRoot, "temp");
Directory.CreateDirectory(dir);
string file = Path.Combine(dir, $"截图_{DateTime.Now:yyyyMMdd_HHmmss}.png");
img.Save(file, System.Drawing.Imaging.ImageFormat.Png);
_producedFiles.Add(file);
_messages.Add(new ChatMessage { Role = "user", Content = $"(已粘贴截图,已保存:{file})" });
_messages.Add(new ChatMessage { Role = "assistant", Content = $"已收到截图,保存位置:{file}" });
RefreshChat();
return file;
}
}
catch (Exception ex)
if (Clipboard.ContainsFileDropList())
{
AddSystemMessage($"处理截图失败:{ex.Message}");
var files = Clipboard.GetFileDropList();
if (files != null)
foreach (string f in files)
if (IsImageFile(f)) return f;
}
return null;
}
/// <summary>RichBox 截图粘贴入口:Ctrl+V 且剪贴板含图片时拦截并走保存流程(#37)</summary>
/// <summary>剪贴板文件列表中是否含图片文件(资源管理器复制的 png/jpg/bmp/gif)</summary>
private static bool ClipboardHasImageFile()
{
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;
}
/// <summary>是否为受支持的图片文件(png/jpg/jpeg/bmp/gif)</summary>
private static bool IsImageFile(string f)
{
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/v2.3)</summary>
private void OnChatRtfKeyDown(object sender, KeyEventArgs e)
{
if (!e.Control || e.KeyCode != Keys.V) return;
try
{
if (Clipboard.ContainsImage())
if (Clipboard.ContainsImage() || ClipboardHasImageFile())
{
e.Handled = true;
e.SuppressKeyPress = true;
......@@ -1588,9 +2034,35 @@ namespace Tools.MyAgent
// ── 斜杠命令(#35:/model /scope /export /kb 检索 /clear /settings /help) ──
/// <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)
......@@ -1662,9 +2134,10 @@ namespace Tools.MyAgent
"- `/workflow` - 知识库周报(可选能力,需在高级设置启用)\n" +
"- `/help` - 显示此帮助\n" +
"\n**快捷操作**\n" +
"- `Enter` - 发送;输入 `/` 可呼出命令自动补全(Tab 选中)\n" +
"- 输入 `/` 呼出命令菜单:`↑/↓` 选择 · `Tab` 录入命令 · `Enter` 发送消息\n" +
"- `Enter` - 发送(命令菜单打开时:录入选中命令并发送)\n" +
"- `Shift+Enter` / `Ctrl+Enter` - 换行\n" +
"- `ESC` - 取消回复";
"- `ESC` - 关闭命令菜单 / 取消回复";
}
/// <summary>/kb <关键词>:直接检索知识库并展示结果</summary>
......
......@@ -190,6 +190,9 @@
<Compile Include="MyAgent\Forms\AgentSettingsForm.Designer.cs">
<DependentUpon>AgentSettingsForm.cs</DependentUpon>
</Compile>
<Compile Include="MyAgent\Forms\wBitmapView.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="MyAgent\Kb\KbConfig.cs" />
<Compile Include="MyAgent\Kb\KbService.cs" />
<Compile Include="MyAgent\Kb\KbIndex.cs" />
......@@ -204,6 +207,9 @@
<Compile Include="MyAgent\Support\ChatSession.cs" />
<Compile Include="MyAgent\Support\ChatRtfCache.cs" />
<Compile Include="MyAgent\Support\MarkdownRenderer.cs" />
<Compile Include="MyAgent\Support\NoCaretRichTextBox.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="MyAgent\wMyAgent.cs">
<SubType>Form</SubType>
</Compile>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment