Commit 67a4bda7 authored by 段廷瑞's avatar 段廷瑞

feat: Agent问答交互功能优化

parent 8d4b5d0b
No preview for this file type
This diff is collapsed.
...@@ -297,7 +297,7 @@ namespace Tools.MyAgent.Agent ...@@ -297,7 +297,7 @@ namespace Tools.MyAgent.Agent
? cfg.KbHitThreshold ? cfg.KbHitThreshold
: 0.72; : 0.72;
_recall.ImagesEnabled = cfg.AnswerWithImages; // 关掉附图即不采图(省掉目录扫描) _recall.ImagesEnabled = cfg.AnswerWithImages; // 关掉附图即不采图(省掉目录扫描)
_recall.MaxImages = cfg.MaxAnswerImages > 0 ? Math.Min(cfg.MaxAnswerImages, 12) : 4; _recall.MaxImages = cfg.MaxAnswerImages > 0 ? Math.Min(cfg.MaxAnswerImages, 16) : 16;
CostEstimator.Configure(cfg.CustomPrices); // R6 费用估算:应用自定义价格表覆盖 CostEstimator.Configure(cfg.CustomPrices); // R6 费用估算:应用自定义价格表覆盖
// P0 连接串脱敏 / P3 工具权限:预存连接与角色规则随配置注入 // P0 连接串脱敏 / P3 工具权限:预存连接与角色规则随配置注入
......
...@@ -84,8 +84,8 @@ namespace Tools.MyAgent.Agent ...@@ -84,8 +84,8 @@ namespace Tools.MyAgent.Agent
/// <summary>是否采集关联图片</summary> /// <summary>是否采集关联图片</summary>
public bool ImagesEnabled = true; public bool ImagesEnabled = true;
/// <summary>关联图片上限</summary> /// <summary>关联图片上限(内部默认 16;引擎 Configure 按 AgentConfig.MaxAnswerImages 注入)</summary>
public int MaxImages = 4; public int MaxImages = 16;
/// <summary>纳入证据链的命中上限(主命中 + 其余)</summary> /// <summary>纳入证据链的命中上限(主命中 + 其余)</summary>
private const int MaxHits = 3; private const int MaxHits = 3;
...@@ -404,9 +404,10 @@ namespace Tools.MyAgent.Agent ...@@ -404,9 +404,10 @@ namespace Tools.MyAgent.Agent
} }
/// <summary> /// <summary>
/// 采集关联图片(三条规则合并去重,最多 max 张,全部过沙箱校验): /// 采集关联图片(四条规则合并去重,最多 max 张,全部过校验):
/// ① 命中段落内的 markdown 图片引用;② 命中行 ±20 行内的图片引用; /// ① 命中段落内的 markdown 图片引用;② 命中行 ±20 行内的图片引用;
/// ③ 同目录与 images/图片/assets/imgs 下与文档名或命中关键词有字面交集的图片。 /// ③ 同目录与 images/图片/assets/imgs 下与文档名或命中关键词有字面交集的图片;
/// ④ docx 内嵌位图(v1.3):Word 图片在 zip 包内(word/media/),文本提取与同目录规则都够不着 ⇒ 解包提取。
/// </summary> /// </summary>
private void CollectImages(KbHit hit, string[] lines, int max) private void CollectImages(KbHit hit, string[] lines, int max)
{ {
...@@ -459,6 +460,26 @@ namespace Tools.MyAgent.Agent ...@@ -459,6 +460,26 @@ namespace Tools.MyAgent.Agent
} }
catch { } catch { }
} }
// ④ Word 内嵌位图(v1.3):docx 的图片在 zip 包内(word/media/),①②③ 均够不着 ⇒ 显式解包。
// 提取落点在本机缓存(OutputRoot/kb_media/),不在知识库沙箱内 —— 路径由我们生成、可信,
// 故不走 AddImageAbs 的知识库根校验(那里会因「不在 KB 根下」把缓存图全部拒掉),只做存在/扩展/去重。
try
{
if (hit.Images.Count < max
&& string.Equals(Path.GetExtension(hit.AbsPath ?? ""), ".docx", StringComparison.OrdinalIgnoreCase))
{
var embedded = _kb.ExtractDocxImages(hit.AbsPath, max - hit.Images.Count);
foreach (var p in embedded)
{
if (hit.Images.Count >= max) break;
if (string.IsNullOrEmpty(p) || !File.Exists(p)) continue;
if (!seen.Add(p)) continue; // 扩展白名单已在 KbService 侧校验
hit.Images.Add(p);
}
}
}
catch { }
} }
/// <summary>命中段落里的图片链接 → 绝对路径(相对文档目录解析),校验通过后加入</summary> /// <summary>命中段落里的图片链接 → 绝对路径(相对文档目录解析),校验通过后加入</summary>
......
...@@ -111,6 +111,8 @@ ...@@ -111,6 +111,8 @@
public bool EnableImageInput { get; set; } = true; public bool EnableImageInput { get; set; } = true;
// ════════════════ 高级项:回答形态(旋转指示 / 本地直答 / 契约化输出 / 附图) ════════════════ // ════════════════ 高级项:回答形态(旋转指示 / 本地直答 / 契约化输出 / 附图) ════════════════
// 说明(v1.2):本组不提供设置页 —— 均为内部默认值(降低设置页认知负担);
// 保留可序列化属性,供手改配置文件微调,或将来需要时再挂 UI。
/// <summary> /// <summary>
/// 本地召回直答(回答形态改造 P3):本轮问题在知识库命中达阈值时,走「单轮直答」通道(不下发工具), /// 本地召回直答(回答形态改造 P3):本轮问题在知识库命中达阈值时,走「单轮直答」通道(不下发工具),
...@@ -133,8 +135,8 @@ ...@@ -133,8 +135,8 @@
/// <summary>回答附图(P5):本轮回答命中文档关联的图片以缩略图栏展示,点击放大(复用 wBitmapView)</summary> /// <summary>回答附图(P5):本轮回答命中文档关联的图片以缩略图栏展示,点击放大(复用 wBitmapView)</summary>
public bool AnswerWithImages { get; set; } = true; public bool AnswerWithImages { get; set; } = true;
/// <summary>回答附图上限(张,1~8)</summary> /// <summary>回答附图上限(张,1~16;暂定 16,无设置页,手改配置文件可调)</summary>
public int MaxAnswerImages { get; set; } = 4; public int MaxAnswerImages { get; set; } = 16;
/// <summary> /// <summary>
/// 本地直答阈值(0.45~0.95):融合匹配度 ≥ 该值走单轮直答(mode=kb); /// 本地直答阈值(0.45~0.95):融合匹配度 ≥ 该值走单轮直答(mode=kb);
......
...@@ -120,7 +120,7 @@ namespace Tools.MyAgent.Forms ...@@ -120,7 +120,7 @@ namespace Tools.MyAgent.Forms
InitializeComponent(); InitializeComponent();
BuildDbConnectionsTab(); // P0:预存数据库连接(db_query 按 ID 引用,聊天不回显密码) BuildDbConnectionsTab(); // P0:预存数据库连接(db_query 按 ID 引用,聊天不回显密码)
BuildToolPermissionUi(); // P3:工具级权限(按角色)输入区 BuildToolPermissionUi(); // P3:工具级权限(按角色)输入区
BuildAnswerShapeTab(); // 回答形态:旋转思考指示 / 本地直答 / 契约化输出 / 回答附图 // 回答形态不再提供设置页(v1.2):全部内部默认(见 AgentConfig 注释),降低设置页认知负担
BindConfig(); // 配置值回填控件(控件创建见 Designer.cs) BindConfig(); // 配置值回填控件(控件创建见 Designer.cs)
ApplyProviderMode(); // 布局联动:BaseUrl 行显隐 ApplyProviderMode(); // 布局联动:BaseUrl 行显隐
ApplyKbMode(); // 布局联动:共享 账号/密码/测试连接 行显隐 ApplyKbMode(); // 布局联动:共享 账号/密码/测试连接 行显隐
...@@ -236,99 +236,6 @@ namespace Tools.MyAgent.Forms ...@@ -236,99 +236,6 @@ namespace Tools.MyAgent.Forms
return rules; return rules;
} }
// ══ 回答形态:旋转思考指示 / 本地召回直答 / 契约化输出 / 回答附图 ══
private CheckBox chkQuickAnswer;
private CheckBox chkConciseAnswer;
private CheckBox chkShowThinking;
private CheckBox chkAnswerImages;
private NumericUpDown numMaxAnswerImages;
private NumericUpDown numKbHitThreshold;
/// <summary>
/// 新增「回答形态」选项卡(程序化创建,避免侵入设计器):
/// 思考呈现方式、本地召回直答、契约化输出、回答附图与直答阈值。
/// </summary>
private void BuildAnswerShapeTab()
{
var tabShape = new TabPage { Text = "回答形态", Padding = new Padding(3) };
var grp = new GroupBox
{
Text = "回答形态(思考指示 / 本地直答 / 契约化输出 / 附图)",
Location = new Point(8, 12),
Size = new Size(596, 324),
};
chkQuickAnswer = new CheckBox
{
Text = "本地召回直答:知识库命中达阈值时单轮直答(跳过工具循环);召回异常自动降级",
Location = new Point(16, 28),
AutoSize = true,
};
chkConciseAnswer = new CheckBox
{
Text = "契约化输出:知识问答三段式(原因/方案/引用),排查类六段式",
Location = new Point(16, 56),
AutoSize = true,
};
chkShowThinking = new CheckBox
{
Text = "仍在问答区渲染思考文本(默认关闭:仅右下旋转指示;调试用)",
Location = new Point(16, 84),
AutoSize = true,
};
chkAnswerImages = new CheckBox
{
Text = "回答附图:命中文档关联的图片以缩略图栏展示(点击放大)",
Location = new Point(16, 112),
AutoSize = true,
};
var lblThreshold = new Label { Text = "直答阈值", Location = new Point(16, 150), AutoSize = true };
numKbHitThreshold = new NumericUpDown
{
Location = new Point(96, 146),
Size = new Size(70, 23),
DecimalPlaces = 2,
Increment = 0.01M,
Minimum = 0.45M,
Maximum = 0.95M,
Value = 0.72M,
};
var lblMaxImages = new Label { Text = "附图上限", Location = new Point(196, 150), AutoSize = true };
numMaxAnswerImages = new NumericUpDown
{
Location = new Point(276, 146),
Size = new Size(70, 23),
Minimum = 1,
Maximum = 8,
Value = 4,
};
var hint = new Label
{
Text = "直答阈值:[阈值, 0.95] 直接以证据作答;[0.45, 阈值) 标注「部分命中」并允许模型补充;"
+ "低于 0.45 视为未召回,回退原多轮工具循环。\r\n"
+ "关闭「本地召回直答」后行为与改造前完全一致(多轮工具循环 + 原回答形态)。",
Location = new Point(16, 186),
AutoSize = false,
Size = new Size(564, 60),
ForeColor = Color.Gray,
};
grp.Controls.Add(chkQuickAnswer);
grp.Controls.Add(chkConciseAnswer);
grp.Controls.Add(chkShowThinking);
grp.Controls.Add(chkAnswerImages);
grp.Controls.Add(lblThreshold);
grp.Controls.Add(numKbHitThreshold);
grp.Controls.Add(lblMaxImages);
grp.Controls.Add(numMaxAnswerImages);
grp.Controls.Add(hint);
tabShape.Controls.Add(grp);
tabMain.TabPages.Add(tabShape);
}
// ══ 配置绑定:把 AgentConfig 值回填到控件(设计器仅设默认值) ══ // ══ 配置绑定:把 AgentConfig 值回填到控件(设计器仅设默认值) ══
private void BindConfig() private void BindConfig()
...@@ -390,13 +297,7 @@ namespace Tools.MyAgent.Forms ...@@ -390,13 +297,7 @@ namespace Tools.MyAgent.Forms
// 可选能力页:工具级权限(P3,角色白名单) // 可选能力页:工具级权限(P3,角色白名单)
txtToolRoles.Lines = ToolRoleRulesToLines(_config.ToolRoleAllow); txtToolRoles.Lines = ToolRoleRulesToLines(_config.ToolRoleAllow);
// 回答形态页:思考指示 / 本地直答 / 契约化输出 / 附图 / 阈值 // 回答形态组不绑定控件(无设置页):AgentConfig 保持内部默认/配置文件值
chkQuickAnswer.Checked = _config.QuickAnswer;
chkConciseAnswer.Checked = _config.ConciseAnswer;
chkShowThinking.Checked = _config.ShowThinking;
chkAnswerImages.Checked = _config.AnswerWithImages;
numMaxAnswerImages.Value = ClampToControl(_config.MaxAnswerImages, numMaxAnswerImages, 4);
numKbHitThreshold.Value = ClampDecimalToControl(_config.KbHitThreshold, numKbHitThreshold, 0.72M);
// 数据库连接(P0):预存连接串(内存明文态)回填表格 // 数据库连接(P0):预存连接串(内存明文态)回填表格
dgvDbConn.Rows.Clear(); dgvDbConn.Rows.Clear();
...@@ -853,13 +754,7 @@ namespace Tools.MyAgent.Forms ...@@ -853,13 +754,7 @@ namespace Tools.MyAgent.Forms
_config.EnableImageInput = chkImageInput.Checked; // v2.2 多模态:图片提问开关 _config.EnableImageInput = chkImageInput.Checked; // v2.2 多模态:图片提问开关
// 可选能力页:工具级权限(P3) // 可选能力页:工具级权限(P3)
_config.ToolRoleAllow = ParseToolRoleRules(txtToolRoles.Text); _config.ToolRoleAllow = ParseToolRoleRules(txtToolRoles.Text);
// 回答形态页:思考指示 / 本地直答 / 契约化输出 / 附图 / 阈值 // 回答形态组不落盘(无设置页):AgentConfig 保持内部默认/配置文件值,投影由 wMyAgent.SaveAgentConfig 负责
_config.QuickAnswer = chkQuickAnswer.Checked;
_config.ConciseAnswer = chkConciseAnswer.Checked;
_config.ShowThinking = chkShowThinking.Checked;
_config.AnswerWithImages = chkAnswerImages.Checked;
_config.MaxAnswerImages = (int)numMaxAnswerImages.Value;
_config.KbHitThreshold = (double)numKbHitThreshold.Value;
// 数据库连接(P0):表格 → 字典(ID 大小写不敏感,空格清理;空 ID 行忽略) // 数据库连接(P0):表格 → 字典(ID 大小写不敏感,空格清理;空 ID 行忽略)
var conns = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var conns = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (DataGridViewRow r in dgvDbConn.Rows) foreach (DataGridViewRow r in dgvDbConn.Rows)
......
...@@ -1871,6 +1871,67 @@ namespace Tools.MyAgent.Kb ...@@ -1871,6 +1871,67 @@ namespace Tools.MyAgent.Kb
return sb.ToString(); return sb.ToString();
} }
/// <summary>
/// 提取 docx 内嵌位图到本地缓存并返回绝对路径(v1.3:Word 内嵌图片进入回答附图轨)。
/// <para>docx 是 zip 包,图片在 word/media/* 下,正文提取(<see cref="ReadDocx"/>)不含图片引用,
/// 故附图采集必须显式解包。提取落点 {OutputRoot}/kb_media/{md5(路径)}/{安全文件名} ——
/// 在本机数据目录而**不在知识库内**(共享/只读知识库可安全使用);同名文件已存在直接复用(幂等)。
/// 只返回白名单位图扩展(emf/wmf 矢量图、>10MB 巨图跳过);返回顺序即包内文档序(近似正文出现序)。</para>
/// </summary>
public List<string> ExtractDocxImages(string docxPath, int max)
{
var result = new List<string>();
if (max <= 0 || string.IsNullOrEmpty(docxPath) || !File.Exists(docxPath)) return result;
if (string.IsNullOrEmpty(OutputRoot) || !IsOutputValid) return result;
try
{
string cacheDir = Path.Combine(OutputRoot, "kb_media", StableHash64(NormPath(docxPath)).Substring(0, 12)); // 12 位短哈希:caption 可读;本地缓存 48bit 碰撞可忽略
Directory.CreateDirectory(cacheDir);
using (var zip = ZipFile.OpenRead(docxPath))
{
foreach (var entry in zip.Entries)
{
if (result.Count >= max) break;
string fullName = entry.FullName ?? "";
// 只认 word/media/ 下的资源(文档正文的内嵌图;页眉页脚/批注图也在此目录,一并可接受)
if (fullName.IndexOf("word/media/", StringComparison.OrdinalIgnoreCase) < 0) continue;
string name = Path.GetFileName(fullName.Replace('\\', '/')) ?? "";
// 安全文件名:ZipArchiveEntry 名可含路径段/特殊字符,只取末段并拒绝 .. 与非法字符
if (name.Length == 0 || name.IndexOf("..", StringComparison.Ordinal) >= 0
|| name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) continue;
string ext = Path.GetExtension(name).ToLowerInvariant();
if (!IsImageExtension(ext)) continue; // emf/wmf 等矢量图不进附图轨
if (entry.Length > 10 * 1024 * 1024) continue; // >10MB 巨图跳过(缩略图加载不值)
string abs = Path.Combine(cacheDir, name);
if (!File.Exists(abs) || new FileInfo(abs).Length != entry.Length)
{
try { entry.ExtractToFile(abs, true); }
catch { continue; }
}
if (File.Exists(abs)) result.Add(abs);
}
}
}
catch (Exception ex)
{
try { AgentLogger.Log("提取 docx 内嵌图失败:" + ex.Message); } catch { }
}
return result;
}
/// <summary>路径 → 16 进制短哈希(缓存目录键;MD5 足够,非安全用途)</summary>
private static string StableHash64(string s)
{
using (var md5 = System.Security.Cryptography.MD5.Create())
{
var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(s ?? ""));
var sb = new StringBuilder(32);
foreach (byte b in bytes)
sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
/// <summary> /// <summary>
/// 提取 PDF 全文(PdfPig net45,支持文本层和中文字体映射)。 /// 提取 PDF 全文(PdfPig net45,支持文本层和中文字体映射)。
/// 扫描版(无文字层)PDF 返回 null,由调用方提示 OCR。 /// 扫描版(无文字层)PDF 返回 null,由调用方提示 OCR。
......
...@@ -16,6 +16,14 @@ namespace Tools.MyAgent ...@@ -16,6 +16,14 @@ namespace Tools.MyAgent
private string _userName = ""; private string _userName = "";
private readonly List<string> _keys = new List<string>(); private readonly List<string> _keys = new List<string>();
private readonly List<string> _blocks = new List<string>(); private readonly List<string> _blocks = new List<string>();
private readonly List<List<string>> _msgImages = new List<List<string>>();
/// <summary>
/// 最近一次 Build 的缩略图序列(v1.3.5):全部消息成功内联成 \pict 的图片绝对路径按渲染顺序拼接。
/// 与 IRichEditOle 对象表(文档序,wMyAgent.RebuildAnswerRefs 读取)按下标一一对应,
/// 供「点击缩略图放大」映射。命中缓存的块沿用上次记录。
/// </summary>
public List<string> LastImageSequence { get; private set; }
public string Build(List<ChatMessage> messages, string welcomeText, string assistantName, string userName, Func<int, bool> isDetailsExpanded = null) public string Build(List<ChatMessage> messages, string welcomeText, string assistantName, string userName, Func<int, bool> isDetailsExpanded = null)
{ {
...@@ -37,6 +45,7 @@ namespace Tools.MyAgent ...@@ -37,6 +45,7 @@ namespace Tools.MyAgent
{ {
_keys.Add(null); _keys.Add(null);
_blocks.Add(null); _blocks.Add(null);
_msgImages.Add(null);
} }
var sb = new StringBuilder(4096); var sb = new StringBuilder(4096);
...@@ -53,7 +62,11 @@ namespace Tools.MyAgent ...@@ -53,7 +62,11 @@ namespace Tools.MyAgent
string key = MessageKey(messages[i], _assistantName, _userName, expanded); string key = MessageKey(messages[i], _assistantName, _userName, expanded);
if (!string.Equals(_keys[i], key, StringComparison.Ordinal)) if (!string.Equals(_keys[i], key, StringComparison.Ordinal))
{ {
// v1.3.5:重渲染该块时借 MarkdownRenderer.ImageCollector 记录成功内联的图片(渲染顺序)
MarkdownRenderer.ImageCollector = new List<string>();
_blocks[i] = MarkdownRenderer.RenderMessageBlock(messages[i], _assistantName, _userName, expanded); _blocks[i] = MarkdownRenderer.RenderMessageBlock(messages[i], _assistantName, _userName, expanded);
_msgImages[i] = MarkdownRenderer.ImageCollector ?? new List<string>();
MarkdownRenderer.ImageCollector = null;
_keys[i] = key; _keys[i] = key;
} }
sb.Append(_blocks[i]); sb.Append(_blocks[i]);
...@@ -61,6 +74,16 @@ namespace Tools.MyAgent ...@@ -61,6 +74,16 @@ namespace Tools.MyAgent
sb.Append(@"\par"); sb.Append(@"\par");
} }
} }
// 缩略图点击映射(v1.3.5):文档内 \pict 按消息顺序排列,与 _msgImages 拼接顺序一致
var seq = new List<string>();
for (int i = 0; i < count; i++)
{
var imgs = i < _msgImages.Count ? _msgImages[i] : null;
if (imgs != null) seq.AddRange(imgs);
}
LastImageSequence = seq;
sb.Append(MarkdownRenderer.RtfSuffix); sb.Append(MarkdownRenderer.RtfSuffix);
return sb.ToString(); return sb.ToString();
} }
...@@ -70,6 +93,8 @@ namespace Tools.MyAgent ...@@ -70,6 +93,8 @@ namespace Tools.MyAgent
{ {
_keys.Clear(); _keys.Clear();
_blocks.Clear(); _blocks.Clear();
_msgImages.Clear();
LastImageSequence = new List<string>();
_welcomeKey = ""; _welcomeKey = "";
_welcomeRtf = ""; _welcomeRtf = "";
_assistantName = ""; _assistantName = "";
......
This diff is collapsed.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace Tools.MyAgent
{
/// <summary>
/// RichEdit 内联对象位置表(v1.3.5 缩略图点击):经 EM_GETOLEINTERFACE 取 IRichEditOle,
/// 逐个读取 REOBJECT.cp —— \pict 在 RichEdit20W 文本流中占一个对象字符,cp 即它在 Text 里的下标;
/// 对象按文档序返回,与渲染序列(ChatRtfCache.LastImageSequence)按下标一一对应。
/// <para>背景:实测 \pict 的对象字符在 RichTextBox.Text 里表现为 0x20(与真实空格无法区分),文本扫描不可行;
/// 本互操作是唯一可靠的命中通道。任何异常/可疑数据都返回空表(功能静默降级为不可点,不影响其余交互)。</para>
/// </summary>
internal static class RichEditObjects
{
private const int EmGetOleInterface = 0x043C; // WM_USER + 60
/// <summary>REO_GETOBJ_NO_INTERFACES:只取结构体(cp/clsid),不取 COM 接口指针</summary>
private const int ReoGetObjNoInterfaces = 0x0;
/// <summary>对象数上限(超限视为互操作异常数据,整体弃用)</summary>
private const int MaxObjects = 4096;
[StructLayout(LayoutKind.Sequential)]
private struct ReObject
{
public int cbObject;
public int cp;
public Guid clsid;
public IntPtr oleobj;
public IntPtr stg;
public int dvAspect;
public int dwFlags;
public IntPtr dwUser;
}
[ComImport, Guid("00020D00-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IRichEditOle
{
// vtable 顺序与 richole.h 严格一致;.NET interop 允许只声明用到的前缀方法
[PreserveSig] int GetClientSite(out IntPtr callback);
[PreserveSig] int GetObjectCount();
[PreserveSig] int GetObject(int iob, ref ReObject reo, int flags);
}
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, out IntPtr lParam);
/// <summary>返回控件里全部内联对象(\pict 缩略图)的字符位置(文档序);异常或数据可疑时返回空表。</summary>
internal static List<int> GetObjectCharPositions(RichTextBox rtb)
{
var positions = new List<int>();
try
{
if (rtb == null || !rtb.IsHandleCreated) return positions;
IntPtr punk;
SendMessage(rtb.Handle, EmGetOleInterface, 0, out punk);
if (punk == IntPtr.Zero) return positions;
try
{
var ole = (IRichEditOle)Marshal.GetObjectForIUnknown(punk);
try
{
int count = ole.GetObjectCount();
if (count < 0 || count > MaxObjects) return positions;
for (int i = 0; i < count; i++)
{
var reo = new ReObject();
reo.cbObject = Marshal.SizeOf(typeof(ReObject));
if (ole.GetObject(i, ref reo, ReoGetObjNoInterfaces) != 0) break;
positions.Add(reo.cp);
}
}
finally { Marshal.ReleaseComObject(ole); }
}
finally { Marshal.Release(punk); }
// 完整性校验:位置须严格递增且落在文本范围内,否则视为互操作异常整体弃用
int textLen = rtb.TextLength;
for (int i = 0; i < positions.Count; i++)
{
if (positions[i] < 0 || positions[i] >= textLen
|| (i > 0 && positions[i] <= positions[i - 1]))
{
positions.Clear();
break;
}
}
}
catch { positions.Clear(); }
return positions;
}
}
}
...@@ -164,6 +164,7 @@ ...@@ -164,6 +164,7 @@
<Compile Include="Support\NoCaretRichTextBox.cs"> <Compile Include="Support\NoCaretRichTextBox.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
</Compile> </Compile>
<Compile Include="Support\RichEditObjects.cs" />
<Compile Include="Support\StorageCleanup.cs" /> <Compile Include="Support\StorageCleanup.cs" />
<Compile Include="Support\TextEncoding.cs" /> <Compile Include="Support\TextEncoding.cs" />
<Compile Include="Support\TextPatterns.cs" /> <Compile Include="Support\TextPatterns.cs" />
......
This diff is collapsed.
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