192 lines
8.1 KiB
C#
192 lines
8.1 KiB
C#
using System.IO;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using Markdig;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
/// <summary>
|
|
/// 문서 포맷 변환 도구.
|
|
/// Markdown → HTML, HTML → 텍스트, CSV → HTML 테이블 등 경량 변환을 수행합니다.
|
|
/// 복잡한 변환(DOCX↔HTML)은 원본 파일을 읽고 적절한 생성 스킬(docx_create, html_create)로
|
|
/// 재생성하도록 LLM에 안내합니다.
|
|
/// </summary>
|
|
public class FormatConvertTool : IAgentTool
|
|
{
|
|
public string Name => "format_convert";
|
|
public string Description =>
|
|
"Convert a document between formats. Supports: " +
|
|
"md→html (Markdown to styled HTML with mood CSS), " +
|
|
"html→text (strip HTML tags to plain text), " +
|
|
"csv→html (CSV to HTML table). " +
|
|
"For complex conversions (docx↔html, xlsx↔csv), read the source with document_read/file_read, " +
|
|
"then use the appropriate creation skill (html_create, docx_create, etc.).";
|
|
|
|
public ToolParameterSchema Parameters => new()
|
|
{
|
|
Properties = new()
|
|
{
|
|
["source"] = new() { Type = "string", Description = "Source file path to convert" },
|
|
["target"] = new() { Type = "string", Description = "Target output file path (extension determines format)" },
|
|
["mood"] = new() { Type = "string", Description = "Design mood for HTML output (default: modern). Only used for md→html conversion." },
|
|
},
|
|
Required = ["source", "target"]
|
|
};
|
|
|
|
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
|
|
{
|
|
var source = args.GetProperty("source").GetString() ?? "";
|
|
var target = args.GetProperty("target").GetString() ?? "";
|
|
var mood = args.TryGetProperty("mood", out var m) ? m.GetString() ?? "modern" : "modern";
|
|
|
|
var srcPath = FileReadTool.ResolvePath(source, context.WorkFolder);
|
|
var tgtPath = FileReadTool.ResolvePath(target, context.WorkFolder);
|
|
if (context.ActiveTab == "Cowork") tgtPath = AgentContext.EnsureTimestampedPath(tgtPath);
|
|
|
|
if (!context.IsPathAllowed(srcPath))
|
|
return ToolResult.Fail($"소스 경로 접근 차단: {srcPath}");
|
|
if (!context.IsPathAllowed(tgtPath))
|
|
return ToolResult.Fail($"대상 경로 접근 차단: {tgtPath}");
|
|
if (!File.Exists(srcPath))
|
|
return ToolResult.Fail($"소스 파일 없음: {srcPath}");
|
|
|
|
if (!await context.CheckWritePermissionAsync("format_convert", tgtPath))
|
|
return ToolResult.Fail("쓰기 권한이 거부되었습니다.");
|
|
|
|
var srcExt = Path.GetExtension(srcPath).ToLowerInvariant();
|
|
var tgtExt = Path.GetExtension(tgtPath).ToLowerInvariant();
|
|
var convKey = $"{srcExt}→{tgtExt}";
|
|
|
|
try
|
|
{
|
|
var srcContent = (await TextFileCodec.ReadAllTextAsync(srcPath, ct)).Text;
|
|
|
|
string result;
|
|
switch (convKey)
|
|
{
|
|
case ".md→.html":
|
|
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
|
|
var bodyHtml = Markdown.ToHtml(srcContent, pipeline);
|
|
var css = TemplateService.GetCss(mood);
|
|
result = $"<!DOCTYPE html>\n<html lang=\"ko\">\n<head>\n<meta charset=\"UTF-8\"/>\n" +
|
|
$"<style>{css}</style>\n</head>\n<body>\n<div class=\"container\">\n{bodyHtml}\n</div>\n</body>\n</html>";
|
|
break;
|
|
|
|
case ".html→.txt" or ".htm→.txt":
|
|
result = StripHtmlTags(srcContent);
|
|
break;
|
|
|
|
case ".csv→.html":
|
|
result = CsvToHtmlTable(srcContent, mood);
|
|
break;
|
|
|
|
case ".md→.txt":
|
|
result = StripMarkdown(srcContent);
|
|
break;
|
|
|
|
default:
|
|
return ToolResult.Fail(
|
|
$"직접 변환 미지원: {convKey}\n" +
|
|
"대안: source를 file_read/document_read로 읽은 뒤, " +
|
|
"적절한 생성 스킬(html_create, docx_create, excel_create 등)을 사용하세요.");
|
|
}
|
|
|
|
await TextFileCodec.WriteAllTextAsync(tgtPath, result, TextFileCodec.Utf8NoBom, ct);
|
|
|
|
var srcName = Path.GetFileName(srcPath);
|
|
var tgtName = Path.GetFileName(tgtPath);
|
|
return ToolResult.Ok(
|
|
$"변환 완료: {srcName} → {tgtName}\n" +
|
|
$"변환 유형: {convKey}\n" +
|
|
$"출력 크기: {result.Length:N0}자",
|
|
tgtPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ToolResult.Fail($"변환 오류: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static string StripHtmlTags(string html)
|
|
{
|
|
// script/style 제거
|
|
var cleaned = Regex.Replace(html, @"<(script|style)[^>]*>.*?</\1>", "", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
|
// 줄바꿈 태그 처리
|
|
cleaned = Regex.Replace(cleaned, @"<br\s*/?>", "\n", RegexOptions.IgnoreCase);
|
|
cleaned = Regex.Replace(cleaned, @"</(p|div|h[1-6]|li|tr)>", "\n", RegexOptions.IgnoreCase);
|
|
// 태그 제거
|
|
cleaned = Regex.Replace(cleaned, @"<[^>]+>", "");
|
|
// HTML 엔티티 디코딩
|
|
cleaned = System.Net.WebUtility.HtmlDecode(cleaned);
|
|
// 연속 빈줄 정리
|
|
cleaned = Regex.Replace(cleaned, @"\n{3,}", "\n\n");
|
|
return cleaned.Trim();
|
|
}
|
|
|
|
private static string StripMarkdown(string md)
|
|
{
|
|
var result = md;
|
|
result = Regex.Replace(result, @"^#{1,6}\s+", "", RegexOptions.Multiline); // 헤딩
|
|
result = Regex.Replace(result, @"\*\*(.+?)\*\*", "$1"); // 볼드
|
|
result = Regex.Replace(result, @"\*(.+?)\*", "$1"); // 이탤릭
|
|
result = Regex.Replace(result, @"`(.+?)`", "$1"); // 인라인 코드
|
|
result = Regex.Replace(result, @"^\s*[-*+]\s+", "", RegexOptions.Multiline); // 리스트
|
|
result = Regex.Replace(result, @"^\s*\d+\.\s+", "", RegexOptions.Multiline); // 번호 리스트
|
|
result = Regex.Replace(result, @"\[(.+?)\]\(.+?\)", "$1"); // 링크
|
|
return result.Trim();
|
|
}
|
|
|
|
private static string CsvToHtmlTable(string csv, string mood)
|
|
{
|
|
var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
|
if (lines.Length == 0) return "<p>빈 CSV 파일</p>";
|
|
|
|
var cssStr = TemplateService.GetCss(mood);
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("<!DOCTYPE html>\n<html lang=\"ko\">\n<head>\n<meta charset=\"UTF-8\"/>");
|
|
sb.AppendLine($"<style>{cssStr}</style>\n</head>\n<body>\n<div class=\"container\">");
|
|
sb.AppendLine("<table><thead><tr>");
|
|
|
|
var headers = ParseCsvLine(lines[0]);
|
|
foreach (var h in headers) sb.Append($"<th>{System.Net.WebUtility.HtmlEncode(h)}</th>");
|
|
sb.AppendLine("</tr></thead><tbody>");
|
|
|
|
for (int i = 1; i < Math.Min(lines.Length, 1001); i++)
|
|
{
|
|
var vals = ParseCsvLine(lines[i]);
|
|
sb.Append("<tr>");
|
|
foreach (var v in vals) sb.Append($"<td>{System.Net.WebUtility.HtmlEncode(v)}</td>");
|
|
sb.AppendLine("</tr>");
|
|
}
|
|
|
|
sb.AppendLine("</tbody></table>\n</div>\n</body>\n</html>");
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string[] ParseCsvLine(string line)
|
|
{
|
|
var fields = new List<string>();
|
|
var current = new StringBuilder();
|
|
bool inQuotes = false;
|
|
for (int i = 0; i < line.Length; i++)
|
|
{
|
|
var c = line[i];
|
|
if (inQuotes)
|
|
{
|
|
if (c == '"' && i + 1 < line.Length && line[i + 1] == '"') { current.Append('"'); i++; }
|
|
else if (c == '"') inQuotes = false;
|
|
else current.Append(c);
|
|
}
|
|
else
|
|
{
|
|
if (c == '"') inQuotes = true;
|
|
else if (c == ',') { fields.Add(current.ToString()); current.Clear(); }
|
|
else current.Append(c);
|
|
}
|
|
}
|
|
fields.Add(current.ToString());
|
|
return fields.ToArray();
|
|
}
|
|
}
|