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 { ["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 list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "path"; span[1] = "sections"; obj.Required = list; return obj; } } public async Task 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 parts = new List(); 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(); 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() ?? 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(); 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() ?? 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; } }