1012 lines
41 KiB
C#
1012 lines
41 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Reflection;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using System.Windows.Forms;
|
||
using Office = Microsoft.Office.Core;
|
||
using Word = Microsoft.Office.Interop.Word;
|
||
using WordAddIn.Properties;
|
||
using WordAddIn.Models;
|
||
using Newtonsoft.Json;
|
||
|
||
namespace WordAddIn
|
||
{
|
||
/// <summary>
|
||
/// Word 插件的主入口类,负责处理 Ribbon 功能区的交互逻辑。
|
||
/// 实现了 IRibbonExtensibility 接口,用于自定义 Word 的功能区界面。
|
||
/// 包含了所有 Ribbon 按钮的点击事件处理方法。
|
||
/// </summary>
|
||
[ComVisible(true)]
|
||
public class AiRibbon : Office.IRibbonExtensibility
|
||
{
|
||
// Ribbon UI 实例引用,用于刷新界面等操作
|
||
private Office.IRibbonUI ribbon;
|
||
|
||
// AI 服务实例,封装了所有与后端 API 的通信逻辑
|
||
private AiService _aiService = new AiService();
|
||
|
||
// 登录状态标识 (静态变量,确保在插件生命周期内保持状态)
|
||
private static bool _isLoggedIn = false;
|
||
|
||
// 存储文档的全文分析上下文 (通过"通读全文"功能生成),用于辅助 AI 理解文档背景
|
||
private string _documentContext = null;
|
||
|
||
// 存储外部参考文档内容 (通过"补充背景"功能导入),用于辅助 AI 进行写作
|
||
private string _referenceContext = null;
|
||
|
||
/// <summary>
|
||
/// 默认构造函数
|
||
/// </summary>
|
||
public AiRibbon()
|
||
{
|
||
}
|
||
|
||
#region IRibbonExtensibility 成员
|
||
|
||
/// <summary>
|
||
/// 加载自定义 Ribbon XML 界面定义。
|
||
/// 该方法由 VSTO 运行时调用。
|
||
/// </summary>
|
||
/// <param name="ribbonID">Ribbon ID</param>
|
||
/// <returns>包含 Ribbon 定义的 XML 字符串</returns>
|
||
public string GetCustomUI(string ribbonID)
|
||
{
|
||
return GetResourceText("WordAddIn.AiRibbon.xml");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Ribbon 回调
|
||
|
||
/// <summary>
|
||
/// Ribbon 加载完成时的回调方法。
|
||
/// 保存 Ribbon UI 实例引用,以便后续可以使用它来刷新 Ribbon 控件的状态。
|
||
/// </summary>
|
||
/// <param name="ribbonUI">Ribbon UI 实例</param>
|
||
public void Ribbon_Load(Office.IRibbonUI ribbonUI)
|
||
{
|
||
this.ribbon = ribbonUI;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取组合的上下文信息,包含全文背景和外部参考资料。
|
||
/// 将 _documentContext (全文背景) 和 _referenceContext (外部参考) 拼接成一个字符串。
|
||
/// 用于发送给 AI 以提供更准确的生成结果,确保生成内容与文档上下文和参考资料保持一致。
|
||
/// </summary>
|
||
/// <returns>组合后的上下文描述字符串</returns>
|
||
private string GetCombinedContext()
|
||
{
|
||
StringBuilder sb = new StringBuilder();
|
||
if (!string.IsNullOrWhiteSpace(_documentContext))
|
||
{
|
||
sb.AppendLine("【当前文档全文背景】:");
|
||
sb.AppendLine(_documentContext);
|
||
sb.AppendLine();
|
||
}
|
||
if (!string.IsNullOrWhiteSpace(_referenceContext))
|
||
{
|
||
sb.AppendLine("【外部参考资料】:");
|
||
sb.AppendLine(_referenceContext);
|
||
sb.AppendLine();
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【补充背景】按钮点击事件处理。
|
||
/// 允许用户导入外部文档 (txt, doc, docx) 作为背景资料。
|
||
/// 读取的文件内容将存储在 _referenceContext 中,供后续 AI 生成任务使用。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件</param>
|
||
public void OnImportReferenceClick(Office.IRibbonControl control)
|
||
{
|
||
// 检查用户是否已登录,未登录则弹出登录框
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
using (OpenFileDialog openFileDialog = new OpenFileDialog())
|
||
{
|
||
openFileDialog.Multiselect = true; // 允许选择多个文件
|
||
openFileDialog.Title = "补充背景 - 导入文档";
|
||
openFileDialog.Filter = "文档文件|*.docx;*.doc;*.txt|Word 文档|*.docx;*.doc|文本文件|*.txt|所有文件|*.*";
|
||
|
||
if (openFileDialog.ShowDialog() == DialogResult.OK)
|
||
{
|
||
StringBuilder sb = new StringBuilder();
|
||
int successCount = 0;
|
||
|
||
foreach (string file in openFileDialog.FileNames)
|
||
{
|
||
try
|
||
{
|
||
string ext = Path.GetExtension(file).ToLower();
|
||
string content = "";
|
||
if (ext == ".txt")
|
||
{
|
||
content = File.ReadAllText(file);
|
||
}
|
||
else if (ext == ".docx" || ext == ".doc")
|
||
{
|
||
// 使用 Word Interop 打开并读取文档内容
|
||
dynamic wordApp = Globals.ThisAddIn.Application;
|
||
dynamic doc = wordApp.Documents.Open(FileName: file, Visible: false, ReadOnly: true, AddToRecentFiles: false);
|
||
content = doc.Content.Text;
|
||
doc.Close(SaveChanges: false);
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(content))
|
||
{
|
||
sb.AppendLine($"--- 背景资料: {Path.GetFileName(file)} ---");
|
||
sb.AppendLine(content);
|
||
sb.AppendLine();
|
||
successCount++;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show($"读取文件 {Path.GetFileName(file)} 失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
_referenceContext = sb.ToString();
|
||
if (successCount > 0)
|
||
{
|
||
MessageBox.Show($"成功导入 {successCount} 个背景文档。\nAI 在后续的【智能扩写】、【润色加工】、【深度升华】等操作中将自动结合这些背景资料。");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【通读全文】按钮点击事件处理。
|
||
/// 读取当前文档的全部文本内容,调用 AI 进行深度语义分析。
|
||
/// 生成的文档画像(包括摘要、风格、术语等)将存储在 _documentContext 中。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件</param>
|
||
public async void OnReadFullClick(Office.IRibbonControl control)
|
||
{
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
LoadingForm loading = null;
|
||
try
|
||
{
|
||
var doc = Globals.ThisAddIn.Application.ActiveDocument;
|
||
string fullText = doc.Content.Text;
|
||
|
||
if (string.IsNullOrWhiteSpace(fullText) || fullText.Length < 10)
|
||
{
|
||
MessageBox.Show("文档内容过少,无法进行有效分析。");
|
||
return;
|
||
}
|
||
|
||
loading = new LoadingForm("AI 正在通读全文并建立上下文,请稍候...");
|
||
loading.Show();
|
||
loading.Refresh();
|
||
|
||
// 限制长度防止 Token 溢出 (假设取前 20000 字符,通常足够分析风格)
|
||
if (fullText.Length > 20000)
|
||
{
|
||
fullText = fullText.Substring(0, 20000);
|
||
}
|
||
|
||
// 调用 AI 服务进行文档分析
|
||
_documentContext = await Task.Run(() => _aiService.AnalyzeDocument(fullText));
|
||
|
||
if (!string.IsNullOrEmpty(_documentContext))
|
||
{
|
||
MessageBox.Show($"通读完成!\n\n【文档分析结果】:\n{_documentContext}\n\n后续的“智能扩写”与“素材写作”将自动应用此上下文信息。", "AI 助手");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("全文分析失败: " + ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
if (loading != null)
|
||
{
|
||
loading.CloseSafe();
|
||
loading.Dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【润色加工】按钮点击事件处理。
|
||
/// 功能描述:
|
||
/// 1. 获取选中文本(或剪贴板内容)。
|
||
/// 2. 调用 AI 润色接口,优化语言流畅度和专业性。
|
||
/// 3. 将润色后的文本替换回文档。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件</param>
|
||
public async void OnPolishClick(Office.IRibbonControl control)
|
||
{
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
LoadingForm loading = null;
|
||
try
|
||
{
|
||
var selection = Globals.ThisAddIn.Application.Selection;
|
||
|
||
// 1. 判断是否有选中文本
|
||
bool hasSelection = selection.Type == Word.WdSelectionType.wdSelectionNormal ||
|
||
(!string.IsNullOrEmpty(selection.Text) && selection.Text.Length > 1);
|
||
|
||
if (!hasSelection)
|
||
{
|
||
// 2. 尝试从剪贴板读取
|
||
IDataObject data = Clipboard.GetDataObject();
|
||
if (data != null && data.GetDataPresent(DataFormats.Text))
|
||
{
|
||
string clipboardText = (string)data.GetData(DataFormats.Text);
|
||
if (!string.IsNullOrWhiteSpace(clipboardText))
|
||
{
|
||
// 剪贴板有内容,逻辑继续
|
||
}
|
||
else
|
||
{
|
||
MessageBox.Show("剪贴板中内容为空,请先选择一段需要润色的文字,或者复制文字到剪贴板。");
|
||
return;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
MessageBox.Show("请先选择一段需要润色的文字,或者复制文字到剪贴板。");
|
||
return;
|
||
}
|
||
}
|
||
|
||
string originalText;
|
||
if (hasSelection)
|
||
{
|
||
originalText = selection.Text.Trim();
|
||
}
|
||
else
|
||
{
|
||
IDataObject data = Clipboard.GetDataObject();
|
||
originalText = (string)data.GetData(DataFormats.Text);
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(originalText))
|
||
{
|
||
MessageBox.Show("获取文字失败,请重试。");
|
||
return;
|
||
}
|
||
|
||
// 显示加载窗体
|
||
loading = new LoadingForm("AI 正在润色加工中,请稍候...");
|
||
loading.Show();
|
||
loading.Refresh();
|
||
|
||
// 3. 在后台线程运行 AI 请求,传入原文和组合上下文
|
||
string polishedText = await Task.Run(() => _aiService.PolishText(originalText, GetCombinedContext()));
|
||
|
||
if (!string.IsNullOrEmpty(polishedText))
|
||
{
|
||
// 4. 使用智能插入替换原文
|
||
SmartInsert(selection, polishedText);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("润色失败: " + ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
if (loading != null)
|
||
{
|
||
loading.CloseSafe();
|
||
loading.Dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【深度升华】按钮点击事件处理。
|
||
/// 功能描述:
|
||
/// 1. 获取选中文本。
|
||
/// 2. 调用 AI 升华接口,挖掘思想深度,提升价值。
|
||
/// 3. 将升华后的文本插入文档。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件</param>
|
||
public async void OnElevateClick(Office.IRibbonControl control)
|
||
{
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
LoadingForm loading = null;
|
||
try
|
||
{
|
||
var selection = Globals.ThisAddIn.Application.Selection;
|
||
|
||
// 1. 判断是否有选中文本或剪贴板内容
|
||
bool hasSelection = selection.Type == Word.WdSelectionType.wdSelectionNormal ||
|
||
(!string.IsNullOrEmpty(selection.Text) && selection.Text.Length > 1);
|
||
|
||
if (!hasSelection)
|
||
{
|
||
IDataObject data = Clipboard.GetDataObject();
|
||
if (data != null && data.GetDataPresent(DataFormats.Text))
|
||
{
|
||
string clipboardText = (string)data.GetData(DataFormats.Text);
|
||
if (string.IsNullOrWhiteSpace(clipboardText))
|
||
{
|
||
MessageBox.Show("剪贴板中内容为空,请先选择一段需要升华的文字,或者复制文字到剪贴板。");
|
||
return;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
MessageBox.Show("请先选择一段需要升华的文字,或者复制文字到剪贴板。");
|
||
return;
|
||
}
|
||
}
|
||
|
||
string originalText;
|
||
if (hasSelection)
|
||
{
|
||
originalText = selection.Text.Trim();
|
||
}
|
||
else
|
||
{
|
||
IDataObject data = Clipboard.GetDataObject();
|
||
originalText = (string)data.GetData(DataFormats.Text);
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(originalText))
|
||
{
|
||
MessageBox.Show("获取文字失败,请重试。");
|
||
return;
|
||
}
|
||
|
||
loading = new LoadingForm("AI 正在深度升华中,挖掘核心价值...");
|
||
loading.Show();
|
||
loading.Refresh();
|
||
|
||
// 2. 调用 AI 服务进行深度升华
|
||
string elevatedText = await Task.Run(() => _aiService.ElevateText(originalText, GetCombinedContext()));
|
||
|
||
if (!string.IsNullOrEmpty(elevatedText))
|
||
{
|
||
// 3. 智能插入结果
|
||
SmartInsert(selection, elevatedText);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("升华失败: " + ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
if (loading != null)
|
||
{
|
||
loading.CloseSafe();
|
||
loading.Dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【智能扩写】按钮点击事件处理。
|
||
/// 功能描述:
|
||
/// 1. 获取用户选中的文本(或剪贴板内容)。
|
||
/// 2. 弹出选项窗体,允许用户输入自定义扩写提示词。
|
||
/// 3. 调用 AI 扩写接口,传入原文、用户提示和全文上下文。
|
||
/// 4. 将 AI 生成的扩写内容替换原有选区,或插入到光标位置。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件</param>
|
||
public async void OnExpandClick(Office.IRibbonControl control)
|
||
{
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
LoadingForm loading = null;
|
||
try
|
||
{
|
||
var selection = Globals.ThisAddIn.Application.Selection;
|
||
|
||
// 1. 判断是否有选中文本
|
||
// wdSelectionNormal 表示有选区,否则检查 Text 属性
|
||
bool hasSelection = selection.Type == Word.WdSelectionType.wdSelectionNormal ||
|
||
(!string.IsNullOrEmpty(selection.Text) && selection.Text.Length > 1);
|
||
|
||
if (!hasSelection)
|
||
{
|
||
// 2. 尝试从剪贴板读取
|
||
IDataObject data = Clipboard.GetDataObject();
|
||
if (data != null && data.GetDataPresent(DataFormats.Text))
|
||
{
|
||
string clipboardText = (string)data.GetData(DataFormats.Text);
|
||
if (string.IsNullOrWhiteSpace(clipboardText))
|
||
{
|
||
MessageBox.Show("剪贴板中内容为空,请先选择一段需要扩写的文字,或者复制文字到剪贴板。");
|
||
return;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
MessageBox.Show("请先选择一段需要扩写的文字,或者复制文字到剪贴板。");
|
||
return;
|
||
}
|
||
}
|
||
|
||
string originalText;
|
||
if (hasSelection)
|
||
{
|
||
originalText = selection.Text.Trim();
|
||
}
|
||
else
|
||
{
|
||
// 使用剪贴板内容
|
||
IDataObject data = Clipboard.GetDataObject();
|
||
originalText = (string)data.GetData(DataFormats.Text);
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(originalText))
|
||
{
|
||
MessageBox.Show("获取文字失败,请重试。");
|
||
return;
|
||
}
|
||
|
||
// 3. 弹出选项设置窗体,让用户输入自定义扩写提示词
|
||
string customPrompt = null;
|
||
using (var optionsForm = new ExpandOptionsForm())
|
||
{
|
||
if (optionsForm.ShowDialog() != DialogResult.OK)
|
||
{
|
||
return; // 用户取消
|
||
}
|
||
customPrompt = optionsForm.SelectedPrompt;
|
||
}
|
||
|
||
loading = new LoadingForm("AI 正在扩写中,请稍候...");
|
||
loading.Show();
|
||
loading.Refresh();
|
||
|
||
// 4. 在后台线程运行 AI 请求,防止阻塞 UI
|
||
// 传入 GetCombinedContext() 以利用文档背景和参考资料
|
||
string expandedText = await Task.Run(() => _aiService.ExpandText(originalText, customPrompt, GetCombinedContext()));
|
||
|
||
if (!string.IsNullOrEmpty(expandedText))
|
||
{
|
||
// 5. 智能插入:尝试保持格式
|
||
SmartInsert(selection, expandedText);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("扩写失败: " + ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
if (loading != null)
|
||
{
|
||
loading.CloseSafe();
|
||
loading.Dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【素材写作】按钮点击事件处理。
|
||
/// 功能描述:
|
||
/// 1. 获取选中文本作为上下文(可选)。
|
||
/// 2. 弹出素材写作配置窗体,允许用户输入文本素材或上传文档素材。
|
||
/// 3. 解析上传的文件内容(支持 .txt, .doc, .docx)。
|
||
/// 4. 调用 AI 接口,根据素材生成新的内容。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件</param>
|
||
public async void OnReferenceGenerateClick(Office.IRibbonControl control)
|
||
{
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
LoadingForm loading = null;
|
||
try
|
||
{
|
||
var selection = Globals.ThisAddIn.Application.Selection;
|
||
string originalText = selection.Text; // 可以为空,作为生成的上下文参考
|
||
|
||
string referenceContent = null;
|
||
int wordLimit = 500;
|
||
|
||
// 1. 弹出素材写作配置窗体
|
||
using (var form = new ReferenceGeneratorForm())
|
||
{
|
||
if (form.ShowDialog() != DialogResult.OK)
|
||
{
|
||
return;
|
||
}
|
||
|
||
wordLimit = form.WordLimit;
|
||
|
||
// 2. 处理素材内容
|
||
if (form.IsFileMode)
|
||
{
|
||
// 文件模式:读取文件内容
|
||
string path = form.SelectedFilePath;
|
||
string ext = Path.GetExtension(path).ToLower();
|
||
|
||
if (ext == ".txt")
|
||
{
|
||
referenceContent = File.ReadAllText(path);
|
||
}
|
||
else
|
||
{
|
||
// 尝试用 Word Interop 打开读取 (针对 .doc, .docx)
|
||
try
|
||
{
|
||
dynamic wordApp = Globals.ThisAddIn.Application;
|
||
dynamic doc = wordApp.Documents.Open(FileName: path, Visible: false, ReadOnly: true, AddToRecentFiles: false);
|
||
referenceContent = doc.Content.Text;
|
||
doc.Close(SaveChanges: false);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show($"读取文档失败: {ex.Message}");
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 文本模式:直接获取输入的文本
|
||
referenceContent = form.ReferenceContent;
|
||
}
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(referenceContent))
|
||
{
|
||
MessageBox.Show("素材内容为空,无法生成。");
|
||
return;
|
||
}
|
||
|
||
loading = new LoadingForm("AI 正在进行素材写作,请稍候...");
|
||
loading.Show();
|
||
loading.Refresh();
|
||
|
||
// 3. 调用 AI 服务进行生成
|
||
// 传入原文(上下文)、参考素材、字数限制、全文背景
|
||
string result = await Task.Run(() => _aiService.GenerateWithReference(originalText, referenceContent, wordLimit, GetCombinedContext()));
|
||
|
||
if (!string.IsNullOrEmpty(result))
|
||
{
|
||
SmartInsert(selection, result);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("生成失败: " + ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
if (loading != null)
|
||
{
|
||
loading.CloseSafe();
|
||
loading.Dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 智能插入辅助方法。
|
||
/// 使用 TypeText 方法插入文本,以保持当前光标处的格式。
|
||
/// 如果 TypeText 失败,则回退到直接赋值 Text 属性。
|
||
/// </summary>
|
||
/// <param name="selection">Word 选区对象</param>
|
||
/// <param name="text">要插入的文本</param>
|
||
private void SmartInsert(Word.Selection selection, string text)
|
||
{
|
||
try
|
||
{
|
||
// 使用 TypeText 插入,让 Word 自动应用当前光标位置的格式
|
||
selection.TypeText(text);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 出错时回退到简单赋值
|
||
selection.Text = text;
|
||
Console.WriteLine("Smart insert failed: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【格式美化】按钮点击事件处理。
|
||
/// 功能描述:
|
||
/// 1. 读取当前文档全文。
|
||
/// 2. 调用 AI 分析文档结构,识别各级标题。
|
||
/// 3. 遍历文档段落,根据 AI 返回的标题列表,将匹配的段落设置为对应的标题样式 (Heading 1-5)。
|
||
/// 4. 对非标题的正文段落应用首行缩进。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件</param>
|
||
public async void OnFormatClick(Office.IRibbonControl control)
|
||
{
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
LoadingForm loading = null;
|
||
try
|
||
{
|
||
var doc = Globals.ThisAddIn.Application.ActiveDocument;
|
||
string fullText = doc.Content.Text;
|
||
|
||
// 1. 基础校验:文档内容太少则不处理
|
||
if (string.IsNullOrWhiteSpace(fullText) || fullText.Length < 10)
|
||
{
|
||
MessageBox.Show("文档内容过少,无法进行格式美化。");
|
||
return;
|
||
}
|
||
|
||
// 2. 截断处理:限制长度防止 Token 溢出 (保留前 20000 字符进行结构分析)
|
||
if (fullText.Length > 20000)
|
||
{
|
||
fullText = fullText.Substring(0, 20000);
|
||
}
|
||
|
||
loading = new LoadingForm("AI 正在分析文档结构并进行格式美化...");
|
||
loading.Show();
|
||
loading.Refresh();
|
||
|
||
// 3. AI 分析结构:识别标题及其层级
|
||
// 这一步是关键,利用大模型理解语义来判断哪些行是标题,比正则更准确
|
||
var headings = await Task.Run(() => _aiService.AnalyzeStructure(fullText));
|
||
|
||
// 即使 headings 为空,我们也要继续处理正文缩进,所以这里不再直接 return
|
||
if (headings == null)
|
||
{
|
||
headings = new List<WordAddIn.AiService.HeadingInfo>();
|
||
}
|
||
|
||
// 4. 应用样式 (在 UI 线程操作 Word 对象)
|
||
// 遍历所有段落,性能可能受文档长度影响,但对于一般文档可接受
|
||
int changeCount = 0;
|
||
int indentCount = 0;
|
||
foreach (Word.Paragraph p in doc.Paragraphs)
|
||
{
|
||
string pText = p.Range.Text.Trim();
|
||
if (string.IsNullOrWhiteSpace(pText)) continue;
|
||
|
||
// 5. 查找匹配:检查当前段落文本是否在 AI 识别的标题列表中
|
||
// 注意:这里使用 Contains 模糊匹配,因为 Word 段落可能包含额外的隐藏字符
|
||
var match = headings.FirstOrDefault(h => pText.Contains(h.text));
|
||
|
||
if (match != null)
|
||
{
|
||
// 是标题:设置对应的 Word 内置标题样式
|
||
Word.WdBuiltinStyle styleEnum = Word.WdBuiltinStyle.wdStyleNormal;
|
||
switch (match.level)
|
||
{
|
||
case 1: styleEnum = Word.WdBuiltinStyle.wdStyleHeading1; break;
|
||
case 2: styleEnum = Word.WdBuiltinStyle.wdStyleHeading2; break;
|
||
case 3: styleEnum = Word.WdBuiltinStyle.wdStyleHeading3; break;
|
||
case 4: styleEnum = Word.WdBuiltinStyle.wdStyleHeading4; break;
|
||
case 5: styleEnum = Word.WdBuiltinStyle.wdStyleHeading5; break;
|
||
default: styleEnum = Word.WdBuiltinStyle.wdStyleHeading1; break;
|
||
}
|
||
|
||
p.set_Style(styleEnum);
|
||
|
||
// 标题通常不需要首行缩进,强制清除
|
||
p.Format.CharacterUnitFirstLineIndent = 0;
|
||
p.Format.FirstLineIndent = 0;
|
||
|
||
changeCount++;
|
||
}
|
||
else
|
||
{
|
||
// 6. 非标题(正文):统一设置首行缩进 2 字符
|
||
// 让文档看起来更规范
|
||
p.Format.CharacterUnitFirstLineIndent = 2;
|
||
indentCount++;
|
||
}
|
||
}
|
||
|
||
string msg = $"格式美化完成!\n- 已调整 {changeCount} 处标题格式。";
|
||
if (indentCount > 0)
|
||
{
|
||
msg += $"\n- 已对 {indentCount} 个段落应用首行缩进。";
|
||
}
|
||
MessageBox.Show(msg);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("格式美化失败: " + ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
if (loading != null)
|
||
{
|
||
loading.CloseSafe();
|
||
loading.Dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【配图生成】按钮点击事件处理。
|
||
/// 功能描述:
|
||
/// 1. 获取用户选中的文字作为绘图描述 (Prompt)。
|
||
/// 2. 弹出图片风格设置窗体,让用户选择风格(如写实、卡通、水墨等)和补充要求。
|
||
/// 3. 调用 AI 绘图接口 (DALL-E 3 / Gemini Image),获取生成图片的 URL。
|
||
/// 4. 下载图片并插入到 Word 文档中。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件</param>
|
||
public async void OnImageClick(Office.IRibbonControl control)
|
||
{
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
LoadingForm loading = null;
|
||
try
|
||
{
|
||
var selection = Globals.ThisAddIn.Application.Selection;
|
||
string prompt = selection.Text;
|
||
if (string.IsNullOrWhiteSpace(prompt))
|
||
{
|
||
MessageBox.Show("请先选择一段描述图片的文字。");
|
||
return;
|
||
}
|
||
|
||
// 1. 弹出图片风格设置窗体
|
||
string style = null;
|
||
string additionalPrompt = null;
|
||
using (var imgOptions = new ImageOptionsForm())
|
||
{
|
||
if (imgOptions.ShowDialog() != DialogResult.OK)
|
||
{
|
||
return; // 用户取消
|
||
}
|
||
style = imgOptions.SelectedStyle;
|
||
additionalPrompt = imgOptions.AdditionalPrompt;
|
||
}
|
||
|
||
// 2. 组合 Prompt:原文 + 补充要求
|
||
if (!string.IsNullOrWhiteSpace(additionalPrompt))
|
||
{
|
||
prompt += $"\n补充要求: {additionalPrompt}";
|
||
}
|
||
|
||
loading = new LoadingForm("AI 正在绘制图片,请稍候...");
|
||
loading.Show();
|
||
loading.Refresh();
|
||
|
||
// 3. 调用 AI 服务生成图片 URL
|
||
// 注意:生图通常比较慢,需要耐心等待
|
||
string imageUrl = await Task.Run(() => _aiService.GenerateImageUrl(prompt, style));
|
||
|
||
if (!string.IsNullOrEmpty(imageUrl))
|
||
{
|
||
// 4. 插入图片
|
||
InsertImageFromUrl(imageUrl);
|
||
}
|
||
else
|
||
{
|
||
MessageBox.Show("未生成图片 URL。");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("配图生成失败: " + ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
if (loading != null)
|
||
{
|
||
loading.CloseSafe();
|
||
loading.Dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【创建大纲】按钮点击事件处理。
|
||
/// 功能描述:
|
||
/// 1. 弹出模态对话框让用户输入大纲主题。
|
||
/// 2. 调用 AI 服务生成结构化的 JSON 大纲数据。
|
||
/// 3. 解析 JSON 数据,遍历大纲节点。
|
||
/// 4. 将大纲内容插入 Word 文档,并根据层级应用预设的字体、字号、对齐方式等样式。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件 (由 VSTO 运行时传入)</param>
|
||
public async void OnCreateOutlineClick(Office.IRibbonControl control)
|
||
{
|
||
// 1. 安全检查:确保用户已登录,否则无法使用 AI 功能
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
string topic = null;
|
||
|
||
// 2. 弹出“创建大纲”输入窗体
|
||
using (var form = new CreateOutlineForm())
|
||
{
|
||
if (form.ShowDialog() != DialogResult.OK) return; // 用户点击取消
|
||
topic = form.Topic;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(topic)) return;
|
||
|
||
// 3. 显示加载提示,提升用户体验
|
||
LoadingForm loading = new LoadingForm("AI 正在构思大纲结构...");
|
||
loading.Show();
|
||
loading.Refresh(); // 强制刷新 UI,确保 loading 窗口立即显示
|
||
|
||
try
|
||
{
|
||
// 4. 异步调用 AI 服务生成大纲
|
||
// 使用 Task.Run 将耗时操作放入后台线程,避免阻塞 Word UI 主线程
|
||
List<OutlineItem> outline = await Task.Run(() => _aiService.GenerateOutline(topic));
|
||
|
||
if (outline == null || outline.Count == 0)
|
||
{
|
||
MessageBox.Show("未能生成大纲,请重试。");
|
||
return;
|
||
}
|
||
|
||
// 5. 获取大纲样式配置
|
||
// 从用户设置中读取 JSON 字符串并反序列化,如果为空则使用默认样式
|
||
string styleJson = Settings.Default.OutlineStyles;
|
||
OutlineStyleConfig styleConfig = string.IsNullOrEmpty(styleJson)
|
||
? new OutlineStyleConfig()
|
||
: JsonConvert.DeserializeObject<OutlineStyleConfig>(styleJson);
|
||
|
||
var doc = Globals.ThisAddIn.Application.ActiveDocument;
|
||
var selection = Globals.ThisAddIn.Application.Selection;
|
||
Word.Range insertRange = selection.Range;
|
||
insertRange.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
|
||
|
||
// 6. 遍历生成的大纲项,逐行插入文档
|
||
foreach (var item in outline)
|
||
{
|
||
// 在当前光标位置添加新段落
|
||
Word.Paragraph p = doc.Content.Paragraphs.Add(insertRange);
|
||
p.Range.Text = item.Title + "\n";
|
||
|
||
// 7. 根据层级应用对应的样式 (字体、大小、加粗、对齐)
|
||
TextStyle ts = null;
|
||
switch (item.Level)
|
||
{
|
||
case 0: // 主标题
|
||
ts = styleConfig.MainTitle;
|
||
p.set_Style(Word.WdBuiltinStyle.wdStyleTitle);
|
||
break;
|
||
case 1: // 一级标题
|
||
ts = styleConfig.Heading1;
|
||
p.set_Style(Word.WdBuiltinStyle.wdStyleHeading1);
|
||
break;
|
||
case 2: // 二级标题
|
||
ts = styleConfig.Heading2;
|
||
p.set_Style(Word.WdBuiltinStyle.wdStyleHeading2);
|
||
break;
|
||
case 3: // 三级标题
|
||
ts = styleConfig.Heading3;
|
||
p.set_Style(Word.WdBuiltinStyle.wdStyleHeading3);
|
||
break;
|
||
default:
|
||
ts = styleConfig.Heading3;
|
||
break;
|
||
}
|
||
|
||
// 应用自定义样式属性
|
||
if (ts != null)
|
||
{
|
||
p.Range.Font.Name = ts.FontName;
|
||
p.Range.Font.Size = ts.FontSize;
|
||
p.Range.Font.Bold = ts.IsBold ? 1 : 0;
|
||
p.Range.Font.Color = Word.WdColor.wdColorBlack; // 强制设为黑色,避免出现默认蓝色的情况
|
||
|
||
if (item.Level == 0) // 仅主标题应用自定义对齐方式
|
||
{
|
||
p.Format.Alignment = (Word.WdParagraphAlignment)ts.Alignment;
|
||
}
|
||
}
|
||
|
||
// 更新插入位置到当前段落之后,准备插入下一行
|
||
insertRange = p.Range;
|
||
insertRange.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 异常处理:捕获并显示错误信息,防止插件崩溃
|
||
MessageBox.Show("生成大纲失败: " + ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
// 8. 无论成功与否,都要关闭 Loading 窗口
|
||
loading.CloseSafe();
|
||
loading.Dispose();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 【大纲配置】按钮点击事件处理。
|
||
/// 打开大纲样式配置窗口,允许用户自定义各级标题的字体、字号等。
|
||
/// </summary>
|
||
/// <param name="control">触发事件的 Ribbon 控件</param>
|
||
public void OnOutlineSettingClick(Office.IRibbonControl control)
|
||
{
|
||
if (!EnsureLoggedIn()) return;
|
||
|
||
using (var form = new OutlineSettingsForm())
|
||
{
|
||
form.ShowDialog();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 URL 插入图片到 Word 文档中。
|
||
/// 下载图片并保存为临时文件,然后插入到当前光标位置。
|
||
/// </summary>
|
||
/// <param name="imageUrl">图片的 URL 地址</param>
|
||
private void InsertImageFromUrl(string imageUrl)
|
||
{
|
||
try
|
||
{
|
||
string tempPath = Path.GetTempFileName() + ".png";
|
||
using (WebClient client = new WebClient())
|
||
{
|
||
client.DownloadFile(imageUrl, tempPath);
|
||
}
|
||
|
||
var selection = Globals.ThisAddIn.Application.Selection;
|
||
var inlineShape = selection.InlineShapes.AddPicture(FileName: tempPath, LinkToFile: false, SaveWithDocument: true);
|
||
|
||
// 简单的图片调整
|
||
inlineShape.LockAspectRatio = Microsoft.Office.Core.MsoTriState.msoTrue;
|
||
if (inlineShape.Width > 400)
|
||
{
|
||
inlineShape.Width = 400; // 限制最大宽度
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("插入图片失败: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保用户已登录。
|
||
/// 检查 _isLoggedIn 状态,如果未登录则弹出登录窗口。
|
||
/// </summary>
|
||
/// <returns>如果已登录或登录成功返回 true,否则返回 false</returns>
|
||
private bool EnsureLoggedIn()
|
||
{
|
||
if (_isLoggedIn) return true;
|
||
|
||
using (LoginForm login = new LoginForm())
|
||
{
|
||
if (login.ShowDialog() == DialogResult.OK)
|
||
{
|
||
_isLoggedIn = true;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从嵌入资源中获取文本内容。
|
||
/// 用于读取 Ribbon XML 定义文件。
|
||
/// </summary>
|
||
/// <param name="resourceName">资源名称</param>
|
||
/// <returns>资源文本内容</returns>
|
||
private static string GetResourceText(string resourceName)
|
||
{
|
||
Assembly asm = Assembly.GetExecutingAssembly();
|
||
string[] resourceNames = asm.GetManifestResourceNames();
|
||
for (int i = 0; i < resourceNames.Length; ++i)
|
||
{
|
||
if (string.Compare(resourceName, resourceNames[i], StringComparison.OrdinalIgnoreCase) == 0)
|
||
{
|
||
using (StreamReader resourceReader = new StreamReader(asm.GetManifestResourceStream(resourceNames[i])))
|
||
{
|
||
if (resourceReader != null)
|
||
{
|
||
return resourceReader.ReadToEnd();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
} |