Files

731 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
namespace AxCopilot.Services.Agent;
public class DocxSkill : IAgentTool
{
public string Name => "docx_create";
public string Description => "Create a rich Word (.docx) document. Supports: sections with heading+body, tables with optional header styling, text formatting (bold, italic, color, highlight, shading), headers/footers with page numbers, page breaks between sections, and numbered/bulleted lists.";
public ToolParameterSchema Parameters
{
get
{
ToolParameterSchema obj = new ToolParameterSchema
{
Properties = new Dictionary<string, ToolProperty>
{
["path"] = new ToolProperty
{
Type = "string",
Description = "Output file path (.docx). Relative to work folder."
},
["title"] = new ToolProperty
{
Type = "string",
Description = "Document title (optional)."
},
["sections"] = new ToolProperty
{
Type = "array",
Description = "Array of content blocks. Each block is one of:\n• Section: {\"heading\": \"...\", \"body\": \"...\", \"level\": 1|2}\n• Table: {\"type\": \"table\", \"headers\": [\"A\",\"B\"], \"rows\": [[\"1\",\"2\"]], \"style\": \"striped|plain\"}\n• PageBreak: {\"type\": \"pagebreak\"}\n• List: {\"type\": \"list\", \"style\": \"bullet|number\", \"items\": [\"item1\", \"item2\"]}\nBody text supports inline formatting: **bold**, *italic*, `code`.",
Items = new ToolProperty
{
Type = "object"
}
},
["header"] = new ToolProperty
{
Type = "string",
Description = "Header text shown at top of every page (optional)."
},
["footer"] = new ToolProperty
{
Type = "string",
Description = "Footer text. Use {page} for page number. Default: 'AX Copilot · {page}' if header is set."
},
["page_numbers"] = new ToolProperty
{
Type = "boolean",
Description = "Show page numbers in footer. Default: true if header or footer is set."
}
}
};
int num = 2;
List<string> list = new List<string>(num);
CollectionsMarshal.SetCount(list, num);
Span<string> span = CollectionsMarshal.AsSpan(list);
span[0] = "path";
span[1] = "sections";
obj.Required = list;
return obj;
}
}
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
{
string path = args.GetProperty("path").GetString() ?? "";
JsonElement t;
string title = (args.TryGetProperty("title", out t) ? (t.GetString() ?? "") : "");
JsonElement hdr;
string headerText = (args.TryGetProperty("header", out hdr) ? hdr.GetString() : null);
JsonElement ftr;
string footerText = (args.TryGetProperty("footer", out ftr) ? ftr.GetString() : null);
JsonElement pn;
bool showPageNumbers = (args.TryGetProperty("page_numbers", out pn) ? pn.GetBoolean() : (headerText != null || footerText != null));
string fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
if (context.ActiveTab == "Cowork")
{
fullPath = AgentContext.EnsureTimestampedPath(fullPath);
}
if (!fullPath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase))
{
fullPath += ".docx";
}
if (!context.IsPathAllowed(fullPath))
{
return ToolResult.Fail("경로 접근 차단: " + fullPath);
}
if (!(await context.CheckWritePermissionAsync(Name, fullPath)))
{
return ToolResult.Fail("쓰기 권한 거부: " + fullPath);
}
try
{
JsonElement sections = args.GetProperty("sections");
string dir = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
using WordprocessingDocument doc = WordprocessingDocument.Create(fullPath, WordprocessingDocumentType.Document);
MainDocumentPart mainPart = doc.AddMainDocumentPart();
mainPart.Document = new Document();
Body body = mainPart.Document.AppendChild(new Body());
if (headerText != null || footerText != null || showPageNumbers)
{
AddHeaderFooter(mainPart, body, headerText, footerText, showPageNumbers);
}
if (!string.IsNullOrEmpty(title))
{
body.Append(CreateTitleParagraph(title));
body.Append(new Paragraph(new ParagraphProperties
{
ParagraphBorders = new ParagraphBorders(new BottomBorder
{
Val = BorderValues.Single,
Size = 6u,
Color = "4472C4",
Space = 1u
}),
SpacingBetweenLines = new SpacingBetweenLines
{
After = "300"
}
}));
}
int sectionCount = 0;
int tableCount = 0;
foreach (JsonElement section in sections.EnumerateArray())
{
JsonElement bt;
switch ((!section.TryGetProperty("type", out bt)) ? null : bt.GetString()?.ToLower())
{
case "pagebreak":
body.Append(CreatePageBreak());
continue;
case "table":
body.Append(CreateTable(section));
tableCount++;
continue;
case "list":
AppendList(body, section);
continue;
}
JsonElement h;
string heading = (section.TryGetProperty("heading", out h) ? (h.GetString() ?? "") : "");
JsonElement b;
string bodyText = (section.TryGetProperty("body", out b) ? (b.GetString() ?? "") : "");
JsonElement lv;
int level = ((!section.TryGetProperty("level", out lv)) ? 1 : lv.GetInt32());
if (!string.IsNullOrEmpty(heading))
{
body.Append(CreateHeadingParagraph(heading, level));
}
if (!string.IsNullOrEmpty(bodyText))
{
string[] array = bodyText.Split('\n');
foreach (string line in array)
{
body.Append(CreateBodyParagraph(line));
}
}
sectionCount++;
bt = default(JsonElement);
h = default(JsonElement);
b = default(JsonElement);
lv = default(JsonElement);
}
mainPart.Document.Save();
List<string> parts = new List<string>();
if (!string.IsNullOrEmpty(title))
{
parts.Add("제목: " + title);
}
if (sectionCount > 0)
{
parts.Add($"섹션: {sectionCount}개");
}
if (tableCount > 0)
{
parts.Add($"테이블: {tableCount}개");
}
if (headerText != null)
{
parts.Add("머리글");
}
if (showPageNumbers)
{
parts.Add("페이지번호");
}
return ToolResult.Ok("Word 문서 생성 완료: " + fullPath + "\n" + string.Join(", ", parts), fullPath);
}
catch (Exception ex)
{
return ToolResult.Fail("Word 문서 생성 실패: " + ex.Message);
}
}
private static Paragraph CreateTitleParagraph(string text)
{
Paragraph paragraph = new Paragraph();
paragraph.ParagraphProperties = new ParagraphProperties
{
Justification = new Justification
{
Val = JustificationValues.Center
},
SpacingBetweenLines = new SpacingBetweenLines
{
After = "100"
}
};
Run run = new Run(new Text(text));
run.RunProperties = new RunProperties
{
Bold = new Bold(),
FontSize = new FontSize
{
Val = "44"
},
Color = new Color
{
Val = "1F3864"
}
};
paragraph.Append(run);
return paragraph;
}
private static Paragraph CreateHeadingParagraph(string text, int level)
{
Paragraph paragraph = new Paragraph();
string text2 = ((level <= 1) ? "32" : "26");
string text3 = ((level <= 1) ? "2E74B5" : "404040");
paragraph.ParagraphProperties = new ParagraphProperties
{
SpacingBetweenLines = new SpacingBetweenLines
{
Before = ((level <= 1) ? "360" : "240"),
After = "120"
}
};
if (level <= 1)
{
paragraph.ParagraphProperties.ParagraphBorders = new ParagraphBorders(new BottomBorder
{
Val = BorderValues.Single,
Size = 4u,
Color = "B4C6E7",
Space = 1u
});
}
Run run = new Run(new Text(text));
run.RunProperties = new RunProperties
{
Bold = new Bold(),
FontSize = new FontSize
{
Val = text2
},
Color = new Color
{
Val = text3
}
};
paragraph.Append(run);
return paragraph;
}
private static Paragraph CreateBodyParagraph(string text)
{
Paragraph paragraph = new Paragraph();
paragraph.ParagraphProperties = new ParagraphProperties
{
SpacingBetweenLines = new SpacingBetweenLines
{
Line = "360"
}
};
AppendFormattedRuns(paragraph, text);
return paragraph;
}
private static void AppendFormattedRuns(Paragraph para, string text)
{
Regex regex = new Regex("\\*\\*(.+?)\\*\\*|\\*(.+?)\\*|`(.+?)`");
int num = 0;
foreach (Match item in regex.Matches(text))
{
if (item.Index > num)
{
OpenXmlElement[] array = new OpenXmlElement[1];
int num2 = num;
array[0] = CreateRun(text.Substring(num2, item.Index - num2));
para.Append(array);
}
if (item.Groups[1].Success)
{
Run run = CreateRun(item.Groups[1].Value);
Run run2 = run;
if (run2.RunProperties == null)
{
RunProperties runProperties = (run2.RunProperties = new RunProperties());
}
run.RunProperties.Bold = new Bold();
para.Append(run);
}
else if (item.Groups[2].Success)
{
Run run3 = CreateRun(item.Groups[2].Value);
Run run2 = run3;
if (run2.RunProperties == null)
{
RunProperties runProperties = (run2.RunProperties = new RunProperties());
}
run3.RunProperties.Italic = new Italic();
para.Append(run3);
}
else if (item.Groups[3].Success)
{
Run run4 = CreateRun(item.Groups[3].Value);
Run run2 = run4;
if (run2.RunProperties == null)
{
RunProperties runProperties = (run2.RunProperties = new RunProperties());
}
run4.RunProperties.RunFonts = new RunFonts
{
Ascii = "Consolas",
HighAnsi = "Consolas"
};
run4.RunProperties.FontSize = new FontSize
{
Val = "20"
};
run4.RunProperties.Shading = new Shading
{
Val = ShadingPatternValues.Clear,
Fill = "F2F2F2",
Color = "auto"
};
para.Append(run4);
}
num = item.Index + item.Length;
}
if (num < text.Length)
{
OpenXmlElement[] array2 = new OpenXmlElement[1];
int num2 = num;
array2[0] = CreateRun(text.Substring(num2, text.Length - num2));
para.Append(array2);
}
if (num == 0 && text.Length == 0)
{
para.Append(CreateRun(""));
}
}
private static Run CreateRun(string text)
{
Run run = new Run(new Text(text)
{
Space = SpaceProcessingModeValues.Preserve
});
run.RunProperties = new RunProperties
{
FontSize = new FontSize
{
Val = "22"
}
};
return run;
}
private static Table CreateTable(JsonElement section)
{
JsonElement value;
JsonElement jsonElement = (section.TryGetProperty("headers", out value) ? value : default(JsonElement));
JsonElement value2;
JsonElement jsonElement2 = (section.TryGetProperty("rows", out value2) ? value2 : default(JsonElement));
JsonElement value3;
string text = (section.TryGetProperty("style", out value3) ? (value3.GetString() ?? "striped") : "striped");
Table table = new Table();
TableProperties newChild = new TableProperties(new TableBorders(new TopBorder
{
Val = BorderValues.Single,
Size = 4u,
Color = "D9D9D9"
}, new BottomBorder
{
Val = BorderValues.Single,
Size = 4u,
Color = "D9D9D9"
}, new LeftBorder
{
Val = BorderValues.Single,
Size = 4u,
Color = "D9D9D9"
}, new RightBorder
{
Val = BorderValues.Single,
Size = 4u,
Color = "D9D9D9"
}, new InsideHorizontalBorder
{
Val = BorderValues.Single,
Size = 4u,
Color = "D9D9D9"
}, new InsideVerticalBorder
{
Val = BorderValues.Single,
Size = 4u,
Color = "D9D9D9"
}), new TableWidth
{
Width = "5000",
Type = TableWidthUnitValues.Pct
});
table.AppendChild(newChild);
if (jsonElement.ValueKind == JsonValueKind.Array)
{
TableRow tableRow = new TableRow();
foreach (JsonElement item in jsonElement.EnumerateArray())
{
TableCell tableCell = new TableCell();
tableCell.TableCellProperties = new TableCellProperties
{
Shading = new Shading
{
Val = ShadingPatternValues.Clear,
Fill = "2E74B5",
Color = "auto"
},
TableCellVerticalAlignment = new TableCellVerticalAlignment
{
Val = TableVerticalAlignmentValues.Center
}
};
Paragraph paragraph = new Paragraph(new Run(new Text(item.GetString() ?? ""))
{
RunProperties = new RunProperties
{
Bold = new Bold(),
FontSize = new FontSize
{
Val = "20"
},
Color = new Color
{
Val = "FFFFFF"
}
}
});
paragraph.ParagraphProperties = new ParagraphProperties
{
SpacingBetweenLines = new SpacingBetweenLines
{
Before = "40",
After = "40"
}
};
tableCell.Append(paragraph);
tableRow.Append(tableCell);
}
table.Append(tableRow);
}
if (jsonElement2.ValueKind == JsonValueKind.Array)
{
int num = 0;
foreach (JsonElement item2 in jsonElement2.EnumerateArray())
{
TableRow tableRow2 = new TableRow();
foreach (JsonElement item3 in item2.EnumerateArray())
{
TableCell tableCell2 = new TableCell();
if (text == "striped" && num % 2 == 0)
{
tableCell2.TableCellProperties = new TableCellProperties
{
Shading = new Shading
{
Val = ShadingPatternValues.Clear,
Fill = "F2F7FB",
Color = "auto"
}
};
}
Paragraph paragraph2 = new Paragraph(new Run(new Text(item3.ToString())
{
Space = SpaceProcessingModeValues.Preserve
})
{
RunProperties = new RunProperties
{
FontSize = new FontSize
{
Val = "20"
}
}
});
paragraph2.ParagraphProperties = new ParagraphProperties
{
SpacingBetweenLines = new SpacingBetweenLines
{
Before = "20",
After = "20"
}
};
tableCell2.Append(paragraph2);
tableRow2.Append(tableCell2);
}
table.Append(tableRow2);
num++;
}
}
return table;
}
private static void AppendList(Body body, JsonElement section)
{
JsonElement value;
JsonElement jsonElement = (section.TryGetProperty("items", out value) ? value : default(JsonElement));
JsonElement value2;
string text = (section.TryGetProperty("style", out value2) ? (value2.GetString() ?? "bullet") : "bullet");
if (jsonElement.ValueKind != JsonValueKind.Array)
{
return;
}
int num = 1;
foreach (JsonElement item in jsonElement.EnumerateArray())
{
string text2 = item.GetString() ?? item.ToString();
string text3 = ((text == "number") ? $"{num}. " : "• ");
Paragraph paragraph = new Paragraph();
paragraph.ParagraphProperties = new ParagraphProperties
{
Indentation = new Indentation
{
Left = "720"
},
SpacingBetweenLines = new SpacingBetweenLines
{
Line = "320"
}
};
Run run = new Run(new Text(text3)
{
Space = SpaceProcessingModeValues.Preserve
});
run.RunProperties = new RunProperties
{
FontSize = new FontSize
{
Val = "22"
},
Bold = ((text == "number") ? new Bold() : null)
};
paragraph.Append(run);
Run run2 = new Run(new Text(text2)
{
Space = SpaceProcessingModeValues.Preserve
});
run2.RunProperties = new RunProperties
{
FontSize = new FontSize
{
Val = "22"
}
};
paragraph.Append(run2);
body.Append(paragraph);
num++;
}
}
private static Paragraph CreatePageBreak()
{
Paragraph paragraph = new Paragraph();
Run run = new Run(new Break
{
Type = BreakValues.Page
});
paragraph.Append(run);
return paragraph;
}
private static void AddHeaderFooter(MainDocumentPart mainPart, Body body, string? headerText, string? footerText, bool showPageNumbers)
{
if (!string.IsNullOrEmpty(headerText))
{
HeaderPart headerPart = mainPart.AddNewPart<HeaderPart>();
Header header = new Header();
Paragraph paragraph = new Paragraph(new Run(new Text(headerText))
{
RunProperties = new RunProperties
{
FontSize = new FontSize
{
Val = "18"
},
Color = new Color
{
Val = "808080"
}
}
});
paragraph.ParagraphProperties = new ParagraphProperties
{
Justification = new Justification
{
Val = JustificationValues.Right
}
};
header.Append(paragraph);
headerPart.Header = header;
SectionProperties sectionProperties = body.GetFirstChild<SectionProperties>() ?? body.AppendChild(new SectionProperties());
sectionProperties.Append(new HeaderReference
{
Type = HeaderFooterValues.Default,
Id = mainPart.GetIdOfPart(headerPart)
});
}
if (!(!string.IsNullOrEmpty(footerText) || showPageNumbers))
{
return;
}
FooterPart footerPart = mainPart.AddNewPart<FooterPart>();
Footer footer = new Footer();
Paragraph paragraph2 = new Paragraph();
paragraph2.ParagraphProperties = new ParagraphProperties
{
Justification = new Justification
{
Val = JustificationValues.Center
}
};
string text = footerText ?? "AX Copilot";
if (showPageNumbers)
{
if (text.Contains("{page}"))
{
string[] array = text.Split("{page}");
paragraph2.Append(CreateFooterRun(array[0]));
paragraph2.Append(CreatePageNumberRun());
if (array.Length > 1)
{
paragraph2.Append(CreateFooterRun(array[1]));
}
}
else
{
paragraph2.Append(CreateFooterRun(text + " · "));
paragraph2.Append(CreatePageNumberRun());
}
}
else
{
paragraph2.Append(CreateFooterRun(text));
}
footer.Append(paragraph2);
footerPart.Footer = footer;
SectionProperties sectionProperties2 = body.GetFirstChild<SectionProperties>() ?? body.AppendChild(new SectionProperties());
sectionProperties2.Append(new FooterReference
{
Type = HeaderFooterValues.Default,
Id = mainPart.GetIdOfPart(footerPart)
});
}
private static Run CreateFooterRun(string text)
{
return new Run(new Text(text)
{
Space = SpaceProcessingModeValues.Preserve
})
{
RunProperties = new RunProperties
{
FontSize = new FontSize
{
Val = "16"
},
Color = new Color
{
Val = "999999"
}
}
};
}
private static Run CreatePageNumberRun()
{
Run run = new Run();
run.RunProperties = new RunProperties
{
FontSize = new FontSize
{
Val = "16"
},
Color = new Color
{
Val = "999999"
}
};
run.Append(new FieldChar
{
FieldCharType = FieldCharValues.Begin
});
run.Append(new FieldCode(" PAGE ")
{
Space = SpaceProcessingModeValues.Preserve
});
run.Append(new FieldChar
{
FieldCharType = FieldCharValues.End
});
return run;
}
}