Initial commit to new repository
This commit is contained in:
511
.decompiledproj/AxCopilot/Services/Agent/ExcelSkill.cs
Normal file
511
.decompiledproj/AxCopilot/Services/Agent/ExcelSkill.cs
Normal file
@@ -0,0 +1,511 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
public class ExcelSkill : IAgentTool
|
||||
{
|
||||
public string Name => "excel_create";
|
||||
|
||||
public string Description => "Create a styled Excel (.xlsx) file. Supports: header styling (bold white text on blue background), striped rows, column auto-width, formulas (=SUM, =AVERAGE, etc), cell merge, freeze panes (freeze header row), and number formatting.";
|
||||
|
||||
public ToolParameterSchema Parameters
|
||||
{
|
||||
get
|
||||
{
|
||||
ToolParameterSchema obj = new ToolParameterSchema
|
||||
{
|
||||
Properties = new Dictionary<string, ToolProperty>
|
||||
{
|
||||
["path"] = new ToolProperty
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Output file path (.xlsx). Relative to work folder."
|
||||
},
|
||||
["sheet_name"] = new ToolProperty
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Sheet name. Default: 'Sheet1'."
|
||||
},
|
||||
["headers"] = new ToolProperty
|
||||
{
|
||||
Type = "array",
|
||||
Description = "Column headers as JSON array of strings.",
|
||||
Items = new ToolProperty
|
||||
{
|
||||
Type = "string"
|
||||
}
|
||||
},
|
||||
["rows"] = new ToolProperty
|
||||
{
|
||||
Type = "array",
|
||||
Description = "Data rows as JSON array of arrays. Use string starting with '=' for formulas (e.g. '=SUM(A2:A10)').",
|
||||
Items = new ToolProperty
|
||||
{
|
||||
Type = "array",
|
||||
Items = new ToolProperty
|
||||
{
|
||||
Type = "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
["style"] = new ToolProperty
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Table style: 'styled' (blue header, striped rows, borders) or 'plain'. Default: 'styled'"
|
||||
},
|
||||
["col_widths"] = new ToolProperty
|
||||
{
|
||||
Type = "array",
|
||||
Description = "Column widths as JSON array of numbers (in characters). e.g. [15, 10, 20]. Auto-fit if omitted.",
|
||||
Items = new ToolProperty
|
||||
{
|
||||
Type = "number"
|
||||
}
|
||||
},
|
||||
["freeze_header"] = new ToolProperty
|
||||
{
|
||||
Type = "boolean",
|
||||
Description = "Freeze the header row. Default: true for styled."
|
||||
},
|
||||
["merges"] = new ToolProperty
|
||||
{
|
||||
Type = "array",
|
||||
Description = "Cell merge ranges. e.g. [\"A1:C1\", \"D5:D8\"]",
|
||||
Items = new ToolProperty
|
||||
{
|
||||
Type = "string"
|
||||
}
|
||||
},
|
||||
["summary_row"] = new ToolProperty
|
||||
{
|
||||
Type = "object",
|
||||
Description = "Auto-generate summary row. {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}. Adds formulas at bottom."
|
||||
}
|
||||
}
|
||||
};
|
||||
int num = 3;
|
||||
List<string> list = new List<string>(num);
|
||||
CollectionsMarshal.SetCount(list, num);
|
||||
Span<string> span = CollectionsMarshal.AsSpan(list);
|
||||
span[0] = "path";
|
||||
span[1] = "headers";
|
||||
span[2] = "rows";
|
||||
obj.Required = list;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
|
||||
{
|
||||
JsonElement value = args.GetProperty("path");
|
||||
string path = value.GetString() ?? "";
|
||||
JsonElement sn;
|
||||
string sheetName = (args.TryGetProperty("sheet_name", out sn) ? (sn.GetString() ?? "Sheet1") : "Sheet1");
|
||||
JsonElement st;
|
||||
string tableStyle = (args.TryGetProperty("style", out st) ? (st.GetString() ?? "styled") : "styled");
|
||||
bool isStyled = tableStyle != "plain";
|
||||
JsonElement fh;
|
||||
bool freezeHeader = (args.TryGetProperty("freeze_header", out fh) ? fh.GetBoolean() : isStyled);
|
||||
string fullPath = FileReadTool.ResolvePath(path, context.WorkFolder);
|
||||
if (context.ActiveTab == "Cowork")
|
||||
{
|
||||
fullPath = AgentContext.EnsureTimestampedPath(fullPath);
|
||||
}
|
||||
if (!fullPath.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
fullPath += ".xlsx";
|
||||
}
|
||||
if (!context.IsPathAllowed(fullPath))
|
||||
{
|
||||
return ToolResult.Fail("경로 접근 차단: " + fullPath);
|
||||
}
|
||||
if (!(await context.CheckWritePermissionAsync(Name, fullPath)))
|
||||
{
|
||||
return ToolResult.Fail("쓰기 권한 거부: " + fullPath);
|
||||
}
|
||||
try
|
||||
{
|
||||
JsonElement headers = args.GetProperty("headers");
|
||||
JsonElement rows = args.GetProperty("rows");
|
||||
string dir = Path.GetDirectoryName(fullPath);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
using SpreadsheetDocument spreadsheet = SpreadsheetDocument.Create(fullPath, SpreadsheetDocumentType.Workbook);
|
||||
WorkbookPart workbookPart = spreadsheet.AddWorkbookPart();
|
||||
workbookPart.Workbook = new Workbook();
|
||||
WorkbookStylesPart stylesPart = workbookPart.AddNewPart<WorkbookStylesPart>();
|
||||
stylesPart.Stylesheet = CreateStylesheet(isStyled);
|
||||
stylesPart.Stylesheet.Save();
|
||||
WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
|
||||
worksheetPart.Worksheet = new Worksheet();
|
||||
int colCount = headers.GetArrayLength();
|
||||
Columns columns = CreateColumns(args, colCount);
|
||||
if (columns != null)
|
||||
{
|
||||
worksheetPart.Worksheet.Append(columns);
|
||||
}
|
||||
SheetData sheetData = new SheetData();
|
||||
worksheetPart.Worksheet.Append(sheetData);
|
||||
Sheets sheets = workbookPart.Workbook.AppendChild(new Sheets());
|
||||
sheets.Append(new Sheet
|
||||
{
|
||||
Id = workbookPart.GetIdOfPart(worksheetPart),
|
||||
SheetId = 1u,
|
||||
Name = sheetName
|
||||
});
|
||||
Row headerRow = new Row
|
||||
{
|
||||
RowIndex = 1u
|
||||
};
|
||||
int colIdx = 0;
|
||||
foreach (JsonElement h in headers.EnumerateArray())
|
||||
{
|
||||
string cellRef = GetCellReference(colIdx, 0);
|
||||
Cell cell = new Cell
|
||||
{
|
||||
CellReference = cellRef,
|
||||
DataType = CellValues.String,
|
||||
CellValue = new CellValue(h.GetString() ?? ""),
|
||||
StyleIndex = (isStyled ? 1u : 0u)
|
||||
};
|
||||
headerRow.Append(cell);
|
||||
colIdx++;
|
||||
}
|
||||
sheetData.Append(headerRow);
|
||||
int rowCount = 0;
|
||||
uint rowNum = 2u;
|
||||
foreach (JsonElement row in rows.EnumerateArray())
|
||||
{
|
||||
Row dataRow = new Row
|
||||
{
|
||||
RowIndex = rowNum
|
||||
};
|
||||
int ci = 0;
|
||||
foreach (JsonElement cellVal in row.EnumerateArray())
|
||||
{
|
||||
string cellRef2 = GetCellReference(ci, (int)(rowNum - 1));
|
||||
Cell cell2 = new Cell
|
||||
{
|
||||
CellReference = cellRef2
|
||||
};
|
||||
if (isStyled && rowCount % 2 == 0)
|
||||
{
|
||||
cell2.StyleIndex = 2u;
|
||||
}
|
||||
string strVal = cellVal.ToString();
|
||||
if (strVal.StartsWith('='))
|
||||
{
|
||||
cell2.CellFormula = new CellFormula(strVal);
|
||||
cell2.DataType = null;
|
||||
}
|
||||
else if (cellVal.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
cell2.DataType = CellValues.Number;
|
||||
cell2.CellValue = new CellValue(cellVal.GetDouble().ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
cell2.DataType = CellValues.String;
|
||||
cell2.CellValue = new CellValue(strVal);
|
||||
}
|
||||
dataRow.Append(cell2);
|
||||
ci++;
|
||||
}
|
||||
sheetData.Append(dataRow);
|
||||
rowCount++;
|
||||
rowNum++;
|
||||
}
|
||||
if (args.TryGetProperty("summary_row", out var summary))
|
||||
{
|
||||
AddSummaryRow(sheetData, summary, rowNum, colCount, rowCount, isStyled);
|
||||
}
|
||||
if (args.TryGetProperty("merges", out var merges) && merges.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
MergeCells mergeCells = new MergeCells();
|
||||
foreach (JsonElement item in merges.EnumerateArray())
|
||||
{
|
||||
string range = item.GetString();
|
||||
if (!string.IsNullOrEmpty(range))
|
||||
{
|
||||
mergeCells.Append(new MergeCell
|
||||
{
|
||||
Reference = range
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mergeCells.HasChildren)
|
||||
{
|
||||
worksheetPart.Worksheet.InsertAfter(mergeCells, sheetData);
|
||||
}
|
||||
}
|
||||
if (freezeHeader)
|
||||
{
|
||||
SheetViews sheetViews = new SheetViews(new SheetView(new Pane
|
||||
{
|
||||
VerticalSplit = 1.0,
|
||||
TopLeftCell = "A2",
|
||||
ActivePane = PaneValues.BottomLeft,
|
||||
State = PaneStateValues.Frozen
|
||||
}, new Selection
|
||||
{
|
||||
Pane = PaneValues.BottomLeft,
|
||||
ActiveCell = "A2",
|
||||
SequenceOfReferences = new ListValue<StringValue>
|
||||
{
|
||||
InnerText = "A2"
|
||||
}
|
||||
})
|
||||
{
|
||||
TabSelected = true,
|
||||
WorkbookViewId = 0u
|
||||
});
|
||||
OpenXmlElement insertBefore = (OpenXmlElement)(((object)worksheetPart.Worksheet.GetFirstChild<Columns>()) ?? ((object)worksheetPart.Worksheet.GetFirstChild<SheetData>()));
|
||||
worksheetPart.Worksheet.InsertBefore(sheetViews, insertBefore);
|
||||
}
|
||||
workbookPart.Workbook.Save();
|
||||
List<string> features = new List<string>();
|
||||
if (isStyled)
|
||||
{
|
||||
features.Add("스타일 적용");
|
||||
}
|
||||
if (freezeHeader)
|
||||
{
|
||||
features.Add("틀 고정");
|
||||
}
|
||||
if (args.TryGetProperty("merges", out value))
|
||||
{
|
||||
features.Add("셀 병합");
|
||||
}
|
||||
if (args.TryGetProperty("summary_row", out value))
|
||||
{
|
||||
features.Add("요약행");
|
||||
}
|
||||
string featureStr = ((features.Count > 0) ? (" [" + string.Join(", ", features) + "]") : "");
|
||||
return ToolResult.Ok($"Excel 파일 생성 완료: {fullPath}\n시트: {sheetName}, 열: {colCount}, 행: {rowCount}{featureStr}", fullPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ToolResult.Fail("Excel 생성 실패: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static Stylesheet CreateStylesheet(bool isStyled)
|
||||
{
|
||||
Stylesheet stylesheet = new Stylesheet();
|
||||
Fonts fonts = new Fonts(new Font(new FontSize
|
||||
{
|
||||
Val = 11.0
|
||||
}, new FontName
|
||||
{
|
||||
Val = "맑은 고딕"
|
||||
}), new Font(new Bold(), new FontSize
|
||||
{
|
||||
Val = 11.0
|
||||
}, new Color
|
||||
{
|
||||
Rgb = "FFFFFFFF"
|
||||
}, new FontName
|
||||
{
|
||||
Val = "맑은 고딕"
|
||||
}), new Font(new Bold(), new FontSize
|
||||
{
|
||||
Val = 11.0
|
||||
}, new FontName
|
||||
{
|
||||
Val = "맑은 고딕"
|
||||
}));
|
||||
stylesheet.Append(fonts);
|
||||
Fills fills = new Fills(new Fill(new PatternFill
|
||||
{
|
||||
PatternType = PatternValues.None
|
||||
}), new Fill(new PatternFill
|
||||
{
|
||||
PatternType = PatternValues.Gray125
|
||||
}), new Fill(new PatternFill
|
||||
{
|
||||
PatternType = PatternValues.Solid,
|
||||
ForegroundColor = new ForegroundColor
|
||||
{
|
||||
Rgb = "FF2E74B5"
|
||||
},
|
||||
BackgroundColor = new BackgroundColor
|
||||
{
|
||||
Indexed = 64u
|
||||
}
|
||||
}), new Fill(new PatternFill
|
||||
{
|
||||
PatternType = PatternValues.Solid,
|
||||
ForegroundColor = new ForegroundColor
|
||||
{
|
||||
Rgb = "FFF2F7FB"
|
||||
},
|
||||
BackgroundColor = new BackgroundColor
|
||||
{
|
||||
Indexed = 64u
|
||||
}
|
||||
}), new Fill(new PatternFill
|
||||
{
|
||||
PatternType = PatternValues.Solid,
|
||||
ForegroundColor = new ForegroundColor
|
||||
{
|
||||
Rgb = "FFE8E8E8"
|
||||
},
|
||||
BackgroundColor = new BackgroundColor
|
||||
{
|
||||
Indexed = 64u
|
||||
}
|
||||
}));
|
||||
stylesheet.Append(fills);
|
||||
Borders borders = new Borders(new Border(new LeftBorder(), new RightBorder(), new TopBorder(), new BottomBorder(), new DiagonalBorder()), new Border(new LeftBorder(new Color
|
||||
{
|
||||
Rgb = "FFD9D9D9"
|
||||
})
|
||||
{
|
||||
Style = BorderStyleValues.Thin
|
||||
}, new RightBorder(new Color
|
||||
{
|
||||
Rgb = "FFD9D9D9"
|
||||
})
|
||||
{
|
||||
Style = BorderStyleValues.Thin
|
||||
}, new TopBorder(new Color
|
||||
{
|
||||
Rgb = "FFD9D9D9"
|
||||
})
|
||||
{
|
||||
Style = BorderStyleValues.Thin
|
||||
}, new BottomBorder(new Color
|
||||
{
|
||||
Rgb = "FFD9D9D9"
|
||||
})
|
||||
{
|
||||
Style = BorderStyleValues.Thin
|
||||
}, new DiagonalBorder()));
|
||||
stylesheet.Append(borders);
|
||||
CellFormats cellFormats = new CellFormats(new CellFormat
|
||||
{
|
||||
FontId = 0u,
|
||||
FillId = 0u,
|
||||
BorderId = (isStyled ? 1u : 0u),
|
||||
ApplyBorder = isStyled
|
||||
}, new CellFormat
|
||||
{
|
||||
FontId = 1u,
|
||||
FillId = 2u,
|
||||
BorderId = 1u,
|
||||
ApplyFont = true,
|
||||
ApplyFill = true,
|
||||
ApplyBorder = true,
|
||||
Alignment = new Alignment
|
||||
{
|
||||
Horizontal = HorizontalAlignmentValues.Center,
|
||||
Vertical = VerticalAlignmentValues.Center
|
||||
}
|
||||
}, new CellFormat
|
||||
{
|
||||
FontId = 0u,
|
||||
FillId = 3u,
|
||||
BorderId = 1u,
|
||||
ApplyFill = true,
|
||||
ApplyBorder = true
|
||||
}, new CellFormat
|
||||
{
|
||||
FontId = 2u,
|
||||
FillId = 4u,
|
||||
BorderId = 1u,
|
||||
ApplyFont = true,
|
||||
ApplyFill = true,
|
||||
ApplyBorder = true
|
||||
});
|
||||
stylesheet.Append(cellFormats);
|
||||
return stylesheet;
|
||||
}
|
||||
|
||||
private static Columns? CreateColumns(JsonElement args, int colCount)
|
||||
{
|
||||
JsonElement value;
|
||||
bool flag = args.TryGetProperty("col_widths", out value) && value.ValueKind == JsonValueKind.Array;
|
||||
Columns columns = new Columns();
|
||||
for (int i = 0; i < colCount; i++)
|
||||
{
|
||||
double num = 15.0;
|
||||
if (flag && i < value.GetArrayLength())
|
||||
{
|
||||
num = value[i].GetDouble();
|
||||
}
|
||||
columns.Append(new Column
|
||||
{
|
||||
Min = (uint)(i + 1),
|
||||
Max = (uint)(i + 1),
|
||||
Width = num,
|
||||
CustomWidth = true
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static void AddSummaryRow(SheetData sheetData, JsonElement summary, uint rowNum, int colCount, int dataRowCount, bool isStyled)
|
||||
{
|
||||
JsonElement value;
|
||||
string text = (summary.TryGetProperty("label", out value) ? (value.GetString() ?? "합계") : "합계");
|
||||
JsonElement value2;
|
||||
JsonElement jsonElement = (summary.TryGetProperty("columns", out value2) ? value2 : default(JsonElement));
|
||||
Row row = new Row
|
||||
{
|
||||
RowIndex = rowNum
|
||||
};
|
||||
Cell cell = new Cell
|
||||
{
|
||||
CellReference = GetCellReference(0, (int)(rowNum - 1)),
|
||||
DataType = CellValues.String,
|
||||
CellValue = new CellValue(text),
|
||||
StyleIndex = (isStyled ? 3u : 0u)
|
||||
};
|
||||
row.Append(cell);
|
||||
for (int i = 1; i < colCount; i++)
|
||||
{
|
||||
string columnLetter = GetColumnLetter(i);
|
||||
Cell cell2 = new Cell
|
||||
{
|
||||
CellReference = GetCellReference(i, (int)(rowNum - 1)),
|
||||
StyleIndex = (isStyled ? 3u : 0u)
|
||||
};
|
||||
if (jsonElement.ValueKind == JsonValueKind.Object && jsonElement.TryGetProperty(columnLetter, out var value3))
|
||||
{
|
||||
string value4 = value3.GetString()?.ToUpper() ?? "SUM";
|
||||
int num = 2;
|
||||
int value5 = num + dataRowCount - 1;
|
||||
cell2.CellFormula = new CellFormula($"={value4}({columnLetter}{num}:{columnLetter}{value5})");
|
||||
}
|
||||
row.Append(cell2);
|
||||
}
|
||||
sheetData.Append(row);
|
||||
}
|
||||
|
||||
private static string GetColumnLetter(int colIndex)
|
||||
{
|
||||
string text = "";
|
||||
while (colIndex >= 0)
|
||||
{
|
||||
text = (char)(65 + colIndex % 26) + text;
|
||||
colIndex = colIndex / 26 - 1;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static string GetCellReference(int colIndex, int rowIndex)
|
||||
{
|
||||
return $"{GetColumnLetter(colIndex)}{rowIndex + 1}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user