Files
aiData/WordAddIn/WordAddIn/AiRibbon.cs
HuangHai ea4bc24c4b 'commit'
2026-01-24 20:54:49 +08:00

1012 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}