using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word;
namespace TableMagic.Skills
{
public class DocumentGenerationSkill : ISkill
{
private float? _imageHeightCm = null;
private float? _imageWidthCm = null;
private bool _maintainAspectRatio = true;
private int _pictureInsertMode = 0;
public string Name => "DocumentGeneration";
public string Description => "文档生成技能,支持根据Excel数据和Word模板批量生成文档";
public List<SkillTool> GetTools()
{
return new List<SkillTool>
{
new SkillTool
{
Name = "generate_documents",
Description = "根据Excel数据和Word模板批量生成文档。模板中使用占位符(如{列名})作为替换标记,第一列为输出文件名。当用户要求批量生成文档、批量生成Word时使用此工具。",
Parameters = new Dictionary<string, object>
{
{ "type", "object" },
{ "properties", new Dictionary<string, object>
{
{ "templatePath", new { type = "string", description = "Word模板文件路径" } },
{ "outputFolder", new { type = "string", description = "输出文件夹路径" } },
{ "sheetName", new { type = "string", description = "数据工作表名称(可选,默认当前表)" } },
{ "placeholderPattern", new { type = "string", description = "占位符格式:{列名}/[列名]/(列名)/【列名】/(列名)/**列名**///列名///##列名##,默认{列名}" } },
{ "startRow", new { type = "integer", description = "起始行(默认2)" } },
{ "endRow", new { type = "integer", description = "结束行(可选,默认到最后一行)" } },
{ "imageHeight", new { type = "number", description = "图片高度(厘米,可选)" } },
{ "imageWidth", new { type = "number", description = "图片宽度(厘米,可选)" } },
{ "maintainAspectRatio", new { type = "boolean", description = "是否保持图片宽高比(默认true)" } }
}
}
},
RequiredParameters = new List<string> { "templatePath", "outputFolder" }
},
new SkillTool
{
Name = "preview_document",
Description = "预览生成的文档内容(不保存文件)。",
Parameters = new Dictionary<string, object>
{
{ "type", "object" },
{ "properties", new Dictionary<string, object>
{
{ "templatePath", new { type = "string", description = "Word模板文件路径" } },
{ "previewRow", new { type = "integer", description = "预览行号(默认2)" } },
{ "sheetName", new { type = "string", description = "数据工作表名称(可选)" } },
{ "placeholderPattern", new { type = "string", description = "占位符格式(默认{列名})" } }
}
}
},
RequiredParameters = new List<string> { "templatePath" }
}
};
}
public async Task<SkillResult> ExecuteToolAsync(string toolName, Dictionary<string, object> arguments)
{
try
{
switch (toolName)
{
case "generate_documents":
return await GenerateDocumentsAsync(arguments);
case "preview_document":
return await PreviewDocumentAsync(arguments);
default:
return new SkillResult { Success = false, Error = $"Tool {toolName} not implemented in DocumentGenerationSkill" };
}
}
catch (Exception ex)
{
return new SkillResult { Success = false, Error = ex.Message };
}
}
private async Task<SkillResult> GenerateDocumentsAsync(Dictionary<string, object> arguments)
{
return await Task.Run(() =>
{
var templatePath = arguments["templatePath"].ToString();
var outputFolder = arguments["outputFolder"].ToString();
var sheetName = arguments.ContainsKey("sheetName") ? arguments["sheetName"].ToString() : null;
var placeholderPattern = arguments.ContainsKey("placeholderPattern") ? arguments["placeholderPattern"].ToString() : "{列名}";
var startRow = arguments.ContainsKey("startRow") ? Convert.ToInt32(arguments["startRow"]) : 2;
var endRow = arguments.ContainsKey("endRow") ? Convert.ToInt32(arguments["endRow"]) : 0;
if (arguments.ContainsKey("imageHeight") && arguments["imageHeight"] != null)
{
_imageHeightCm = Convert.ToSingle(arguments["imageHeight"]);
_pictureInsertMode = 1;
}
if (arguments.ContainsKey("imageWidth") && arguments["imageWidth"] != null)
{
_imageWidthCm = Convert.ToSingle(arguments["imageWidth"]);
_pictureInsertMode = 1;
}
if (arguments.ContainsKey("maintainAspectRatio"))
{
_maintainAspectRatio = Convert.ToBoolean(arguments["maintainAspectRatio"]);
}
if (!File.Exists(templatePath))
return new SkillResult { Success = false, Error = $"模板文件不存在: {templatePath}" };
if (!Directory.Exists(outputFolder))
Directory.CreateDirectory(outputFolder);
var excelApp = ThisAddIn.app;
var workbook = excelApp.ActiveWorkbook;
var sheet = string.IsNullOrEmpty(sheetName) ? workbook.ActiveSheet : workbook.Worksheets[sheetName];
var usedRange = sheet.UsedRange;
int columnCount = usedRange.Columns.Count;
int rowCount = usedRange.Rows.Count;
int lastRow = endRow > 0 ? Math.Min(endRow, rowCount) : rowCount;
var headers = new List<string>();
for (int col = 1; col <= columnCount; col++)
{
var cellValue = (usedRange.Cells[1, col] as Excel.Range).Value2;
headers.Add(cellValue?.ToString() ?? $"列{col}");
}
string templateExtension = Path.GetExtension(templatePath).ToLower();
var wordApp = new Word.Application { Visible = false };
int successCount = 0;
int failCount = 0;
try
{
for (int row = startRow; row <= lastRow; row++)
{
var fileNameCell = usedRange.Cells[row, 1] as Excel.Range;
if (fileNameCell.Value2 == null || string.IsNullOrWhiteSpace(fileNameCell.Value2.ToString()))
continue;
string fileName = SanitizeFilename(fileNameCell.Value2.ToString());
if (!fileName.EndsWith(templateExtension, StringComparison.OrdinalIgnoreCase))
{
fileName = Path.ChangeExtension(fileName, templateExtension);
}
try
{
var rowData = new Dictionary<string, string>();
var imageData = new Dictionary<string, string>();
for (int col = 2; col <= columnCount; col++)
{
var cell = usedRange.Cells[row, col] as Excel.Range;
string header = headers[col - 1];
string cellValue = GetCellValueAsString(cell);
if (IsImageFile(cellValue))
{
imageData[header] = cellValue;
}
else
{
rowData[header] = cellValue;
}
}
string outputPath = Path.Combine(outputFolder, fileName);
File.Copy(templatePath, outputPath, true);
Word.Document wordDoc = null;
try
{
wordDoc = wordApp.Documents.Open(outputPath);
ReplacePlaceholders(wordDoc, rowData, placeholderPattern, imageData.Keys);
foreach (var kvp in imageData)
{
string placeholder = placeholderPattern.Replace("列名", kvp.Key);
float? height = null;
float? width = null;
bool lockRatio = true;
if (_pictureInsertMode == 1)
{
height = _imageHeightCm;
width = _imageWidthCm;
lockRatio = _maintainAspectRatio;
}
InsertPictureAtPlaceholder(wordDoc, placeholder, kvp.Value, height, width, lockRatio);
}
wordDoc.Save();
successCount++;
}
finally
{
if (wordDoc != null)
{
wordDoc.Close();
Marshal.ReleaseComObject(wordDoc);
}
}
}
catch (Exception ex)
{
failCount++;
Debug.WriteLine($"生成文档时出错: {ex.Message}");
}
}
}
finally
{
wordApp.Quit();
Marshal.ReleaseComObject(wordApp);
}
return new SkillResult { Success = true, Content = $"文档生成完成\n成功: {successCount} 个\n失败: {failCount} 个\n输出目录: {outputFolder}" };
});
}
private async Task<SkillResult> PreviewDocumentAsync(Dictionary<string, object> arguments)
{
return await Task.Run(() =>
{
var templatePath = arguments["templatePath"].ToString();
var previewRow = arguments.ContainsKey("previewRow") ? Convert.ToInt32(arguments["previewRow"]) : 2;
var sheetName = arguments.ContainsKey("sheetName") ? arguments["sheetName"].ToString() : null;
var placeholderPattern = arguments.ContainsKey("placeholderPattern") ? arguments["placeholderPattern"].ToString() : "{列名}";
if (!File.Exists(templatePath))
return new SkillResult { Success = false, Error = $"模板文件不存在: {templatePath}" };
var workbook = ThisAddIn.app.ActiveWorkbook;
var sheet = string.IsNullOrEmpty(sheetName) ? workbook.ActiveSheet : workbook.Worksheets[sheetName];
var usedRange = sheet.UsedRange;
int lastCol = usedRange.Columns.Count;
var columnMap = new Dictionary<string, int>();
for (int c = 1; c <= lastCol; c++)
{
var colName = (usedRange.Cells[1, c] as Excel.Range).Value2?.ToString();
if (!string.IsNullOrEmpty(colName))
columnMap[colName] = c;
}
var previewLines = new List<string>();
foreach (var kvp in columnMap)
{
var placeholder = placeholderPattern.Replace("列名", kvp.Key);
var cell = usedRange.Cells[previewRow, kvp.Value] as Excel.Range;
var value = GetCellValueAsString(cell);
previewLines.Add($" {placeholder} => {value}");
}
return new SkillResult
{
Success = true,
Content = $"文档预览(第{previewRow}行数据):\n{string.Join("\n", previewLines)}"
};
});
}
private void ReplacePlaceholders(Word.Document doc, Dictionary<string, string> data, string pattern, ICollection<string> imageHeaders)
{
try
{
if (doc.StoryRanges != null)
{
foreach (Word.Range range in doc.StoryRanges)
{
if (range != null)
{
ReplacePlaceholdersInRange(range, data, pattern, imageHeaders);
}
}
}
}
catch (Exception ex) { Debug.WriteLine($"处理StoryRanges时出错: {ex.Message}"); }
try
{
if (doc.Sections != null && doc.Sections.Count > 0)
{
foreach (Word.Section section in doc.Sections)
{
if (section != null)
{
try
{
if (section.Headers != null)
{
Word.HeaderFooter header = section.Headers[Word.WdHeaderFooterIndex.wdHeaderFooterPrimary];
if (header != null && header.Range != null)
ReplacePlaceholdersInRange(header.Range, data, pattern, imageHeaders);
}
}
catch (Exception ex) { Debug.WriteLine($"处理页眉时出错: {ex.Message}"); }
try
{
if (section.Footers != null)
{
Word.HeaderFooter footer = section.Footers[Word.WdHeaderFooterIndex.wdHeaderFooterPrimary];
if (footer != null && footer.Range != null)
ReplacePlaceholdersInRange(footer.Range, data, pattern, imageHeaders);
}
}
catch (Exception ex) { Debug.WriteLine($"处理页脚时出错: {ex.Message}"); }
}
}
}
}
catch (Exception ex) { Debug.WriteLine($"处理Sections时出错: {ex.Message}"); }
try
{
if (doc.Shapes != null && doc.Shapes.Count > 0)
{
foreach (Word.Shape shape in doc.Shapes)
{
if (shape != null && shape.TextFrame != null && shape.TextFrame.HasText != 0)
{
Word.Range textRange = shape.TextFrame.TextRange;
if (textRange != null)
ReplacePlaceholdersInRange(textRange, data, pattern, imageHeaders);
}
}
}
}
catch (Exception ex) { Debug.WriteLine($"处理Shapes时出错: {ex.Message}"); }
try
{
if (doc.Tables != null && doc.Tables.Count > 0)
{
foreach (Word.Table table in doc.Tables)
{
if (table != null)
{
for (int row = 1; row <= table.Rows.Count; row++)
{
for (int col = 1; col <= table.Columns.Count; col++)
{
try
{
Word.Cell cell = table.Cell(row, col);
if (cell != null && cell.Range != null)
ReplacePlaceholdersInRange(cell.Range, data, pattern, imageHeaders);
}
catch (Exception ex) { Debug.WriteLine($"处理表格单元格({row},{col})时出错: {ex.Message}"); }
}
}
}
}
}
}
catch (Exception ex) { Debug.WriteLine($"处理Tables时出错: {ex.Message}"); }
}
private void ReplacePlaceholdersInRange(Word.Range range, Dictionary<string, string> data, string pattern, ICollection<string> imageHeaders)
{
if (range == null) return;
string originalText = range.Text;
foreach (var kvp in data)
{
if (imageHeaders.Contains(kvp.Key)) continue;
string placeholder = pattern.Replace("列名", kvp.Key);
string replacement = kvp.Value
.Replace("\n", "\v")
.Replace("^p", "\v")
.Replace("^l", "\v");
if (originalText.Contains(placeholder))
{
try
{
range.Find.ClearFormatting();
range.Find.Replacement.ClearFormatting();
object replaceAll = Word.WdReplace.wdReplaceAll;
object wrap = Word.WdFindWrap.wdFindContinue;
range.Find.Execute(
FindText: placeholder,
ReplaceWith: replacement,
Replace: replaceAll,
Wrap: wrap
);
}
catch (Exception ex) { Debug.WriteLine($"替换占位符 {placeholder} 时出错: {ex.Message}"); }
}
}
}
private void InsertPictureAtPlaceholder(Word.Document doc, string placeholder, string imagePath,
float? heightCm, float? widthCm, bool lockAspectRatio)
{
try
{
if (doc.StoryRanges != null)
{
foreach (Word.Range storyRange in doc.StoryRanges)
{
Word.Range current = storyRange;
while (current != null)
{
InsertPictureInRange(current, placeholder, imagePath, heightCm, widthCm, lockAspectRatio);
try { current = current.NextStoryRange; }
catch { current = null; }
}
}
}
if (doc.Shapes != null && doc.Shapes.Count > 0)
{
foreach (Word.Shape shape in doc.Shapes)
{
try
{
if (shape != null && shape.TextFrame != null && shape.TextFrame.HasText != 0)
InsertPictureInRange(shape.TextFrame.TextRange, placeholder, imagePath, heightCm, widthCm, lockAspectRatio);
}
catch (Exception ex) { Debug.WriteLine($"处理Shape时出错: {ex.Message}"); }
}
}
if (doc.Tables != null && doc.Tables.Count > 0)
{
foreach (Word.Table table in doc.Tables)
{
if (table == null) continue;
for (int r = 1; r <= table.Rows.Count; r++)
{
for (int c = 1; c <= table.Columns.Count; c++)
{
try
{
Word.Cell cell = table.Cell(r, c);
if (cell != null && cell.Range != null)
InsertPictureInRange(cell.Range, placeholder, imagePath, heightCm, widthCm, lockAspectRatio);
}
catch (Exception ex) { Debug.WriteLine($"处理表格单元格({r},{c})时出错: {ex.Message}"); }
}
}
}
}
}
catch (Exception ex) { Debug.WriteLine($"插入图片时出错: {ex.Message}"); }
}
private void InsertPictureInRange(Word.Range range, string placeholder, string imagePath,
float? heightCm, float? widthCm, bool lockAspectRatio)
{
if (range == null) return;
try
{
Word.Range searchRange = range.Duplicate;
searchRange.Find.ClearFormatting();
searchRange.Find.Replacement.ClearFormatting();
searchRange.Find.Text = placeholder;
searchRange.Find.Forward = true;
searchRange.Find.Wrap = Word.WdFindWrap.wdFindStop;
searchRange.Find.MatchCase = true;
searchRange.Find.MatchWholeWord = true;
while (searchRange.Find.Execute())
{
Word.Range foundRange = range.Document.Range(searchRange.Start, searchRange.End);
foundRange.Text = string.Empty;
Word.InlineShape insertedShape = foundRange.InlineShapes.AddPicture(imagePath, LinkToFile: false, SaveWithDocument: true);
SetImageSize(insertedShape, heightCm, widthCm, lockAspectRatio);
int newStart = insertedShape.Range.End;
int rangeEnd = range.End;
if (newStart >= rangeEnd) break;
searchRange.SetRange(newStart, rangeEnd);
searchRange.Find.ClearFormatting();
searchRange.Find.Replacement.ClearFormatting();
searchRange.Find.Text = placeholder;
searchRange.Find.Forward = true;
searchRange.Find.Wrap = Word.WdFindWrap.wdFindStop;
searchRange.Find.MatchCase = true;
searchRange.Find.MatchWholeWord = true;
}
}
catch (Exception ex) { Debug.WriteLine($"InsertPictureInRange 错误: {ex.Message}"); }
}
private void SetImageSize(Word.InlineShape shape, float? heightCm, float? widthCm, bool lockAspectRatio)
{
try
{
if (heightCm == null && widthCm == null) return;
float originalWidth = shape.Width;
float originalHeight = shape.Height;
float aspectRatio = originalWidth / originalHeight;
const float cmToPoints = 28.35f;
float? heightPoints = heightCm * cmToPoints;
float? widthPoints = widthCm * cmToPoints;
if (lockAspectRatio)
{
float? finalHeight = heightPoints;
float? finalWidth = widthPoints;
if (heightPoints.HasValue)
{
finalHeight = heightPoints.Value;
finalWidth = heightPoints.Value * aspectRatio;
}
else if (widthPoints.HasValue)
{
finalWidth = widthPoints.Value;
finalHeight = widthPoints.Value / aspectRatio;
}
shape.LockAspectRatio = Microsoft.Office.Core.MsoTriState.msoFalse;
if (finalHeight.HasValue) shape.Height = finalHeight.Value;
if (finalWidth.HasValue) shape.Width = finalWidth.Value;
}
else
{
shape.LockAspectRatio = Microsoft.Office.Core.MsoTriState.msoFalse;
if (heightPoints.HasValue) shape.Height = heightPoints.Value;
if (widthPoints.HasValue) shape.Width = widthPoints.Value;
}
}
catch (Exception ex)
{
Debug.WriteLine($"设置图片尺寸时出错: {ex.Message}");
}
}
private bool IsImageFile(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath)) return false;
try
{
if (filePath.IndexOfAny(Path.GetInvalidPathChars()) >= 0) return false;
string extension = Path.GetExtension(filePath).ToLower();
string[] imageExtensions = { ".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tif", ".tiff" };
if (!imageExtensions.Contains(extension)) return false;
return File.Exists(filePath);
}
catch { return false; }
}
private string GetCellValueAsString(Excel.Range cell)
{
try
{
if (IsDateCell(cell))
{
string formattedText = cell.Text?.ToString()?.Trim();
if (string.IsNullOrWhiteSpace(formattedText) || formattedText.Contains("####"))
{
if (cell.Value2 != null)
{
if (double.TryParse(cell.Value2.ToString(), out double oaDate))
{
try
{
DateTime date = DateTime.FromOADate(oaDate);
return date.ToString("yyyy/MM/dd");
}
catch { return cell.Value2.ToString(); }
}
else { return cell.Value2.ToString(); }
}
else { return string.Empty; }
}
return formattedText;
}
return cell.Text?.ToString()?.Trim() ?? string.Empty;
}
catch { return string.Empty; }
}
private bool IsDateCell(Excel.Range cell)
{
try
{
string format = cell.NumberFormat?.ToString() ?? "";
bool isDateFormat = format.Contains("y") || format.Contains("m") || format.Contains("d") ||
format.Contains("h") || format.Contains("s");
if (cell.Value2 != null)
{
try
{
double oaDate = Convert.ToDouble(cell.Value2);
if (oaDate > 0 && oaDate < 2958465.99999) return true;
}
catch { }
}
return isDateFormat;
}
catch { return false; }
}
private string SanitizeFilename(string filename)
{
var invalidChars = Path.GetInvalidFileNameChars();
return new string(filename.Where(c => !invalidChars.Contains(c)).ToArray());
}
}
}