311 lines
9.8 KiB
C#
311 lines
9.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
public class TaskTrackerTool : IAgentTool
|
|
{
|
|
private class TaskItem
|
|
{
|
|
[JsonPropertyName("id")]
|
|
public int Id { get; set; }
|
|
|
|
[JsonPropertyName("title")]
|
|
public string Title { get; set; } = "";
|
|
|
|
[JsonPropertyName("priority")]
|
|
public string Priority { get; set; } = "medium";
|
|
|
|
[JsonPropertyName("created")]
|
|
public string Created { get; set; } = "";
|
|
|
|
[JsonPropertyName("done")]
|
|
public bool Done { get; set; }
|
|
}
|
|
|
|
private const string TaskFileName = ".ax/tasks.json";
|
|
|
|
public string Name => "task_tracker";
|
|
|
|
public string Description => "Track tasks/TODOs in the working folder. Actions: 'scan' — scan source files for TODO/FIXME/HACK/BUG comments; 'add' — add a task to .ax/tasks.json; 'list' — list tasks from .ax/tasks.json; 'done' — mark a task as completed.";
|
|
|
|
public ToolParameterSchema Parameters
|
|
{
|
|
get
|
|
{
|
|
ToolParameterSchema toolParameterSchema = new ToolParameterSchema();
|
|
Dictionary<string, ToolProperty> dictionary = new Dictionary<string, ToolProperty>();
|
|
ToolProperty obj = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Action: scan, add, list, done"
|
|
};
|
|
int num = 4;
|
|
List<string> list = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list, num);
|
|
Span<string> span = CollectionsMarshal.AsSpan(list);
|
|
span[0] = "scan";
|
|
span[1] = "add";
|
|
span[2] = "list";
|
|
span[3] = "done";
|
|
obj.Enum = list;
|
|
dictionary["action"] = obj;
|
|
dictionary["title"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Task title (for 'add')"
|
|
};
|
|
dictionary["priority"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Priority: high, medium, low (for 'add', default: medium)"
|
|
};
|
|
dictionary["id"] = new ToolProperty
|
|
{
|
|
Type = "integer",
|
|
Description = "Task ID (for 'done')"
|
|
};
|
|
dictionary["extensions"] = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "File extensions to scan, comma-separated (default: .cs,.py,.js,.ts,.java,.cpp,.c)"
|
|
};
|
|
toolParameterSchema.Properties = dictionary;
|
|
num = 1;
|
|
List<string> list2 = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list2, num);
|
|
CollectionsMarshal.AsSpan(list2)[0] = "action";
|
|
toolParameterSchema.Required = list2;
|
|
return toolParameterSchema;
|
|
}
|
|
}
|
|
|
|
public Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default(CancellationToken))
|
|
{
|
|
string text = args.GetProperty("action").GetString() ?? "";
|
|
try
|
|
{
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
Task<ToolResult> result = text switch
|
|
{
|
|
"scan" => Task.FromResult(ScanTodos(args, context)),
|
|
"add" => Task.FromResult(AddTask(args, context)),
|
|
"list" => Task.FromResult(ListTasks(context)),
|
|
"done" => Task.FromResult(MarkDone(args, context)),
|
|
_ => Task.FromResult(ToolResult.Fail("Unknown action: " + text)),
|
|
};
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Task.FromResult(ToolResult.Fail("태스크 추적 오류: " + ex.Message));
|
|
}
|
|
}
|
|
|
|
private static ToolResult ScanTodos(JsonElement args, AgentContext context)
|
|
{
|
|
JsonElement value;
|
|
string text = (args.TryGetProperty("extensions", out value) ? (value.GetString() ?? ".cs,.py,.js,.ts,.java,.cpp,.c") : ".cs,.py,.js,.ts,.java,.cpp,.c");
|
|
HashSet<string> hashSet = new HashSet<string>(from s in text.Split(',')
|
|
select s.Trim().StartsWith('.') ? s.Trim() : ("." + s.Trim()), StringComparer.OrdinalIgnoreCase);
|
|
string[] array = new string[5] { "TODO", "FIXME", "HACK", "BUG", "XXX" };
|
|
List<(string, int, string, string)> list = new List<(string, int, string, string)>();
|
|
string workFolder = context.WorkFolder;
|
|
if (!Directory.Exists(workFolder))
|
|
{
|
|
return ToolResult.Fail("작업 폴더 없음: " + workFolder);
|
|
}
|
|
foreach (string item5 in Directory.EnumerateFiles(workFolder, "*", SearchOption.AllDirectories))
|
|
{
|
|
if (!hashSet.Contains(Path.GetExtension(item5)) || item5.Contains("bin") || item5.Contains("obj") || item5.Contains("node_modules"))
|
|
{
|
|
continue;
|
|
}
|
|
int num = 0;
|
|
foreach (string item6 in File.ReadLines(item5))
|
|
{
|
|
num++;
|
|
string[] array2 = array;
|
|
foreach (string text2 in array2)
|
|
{
|
|
int num3 = item6.IndexOf(text2, StringComparison.OrdinalIgnoreCase);
|
|
if (num3 >= 0)
|
|
{
|
|
string text3 = item6;
|
|
int num4 = num3 + text2.Length;
|
|
string text4 = text3.Substring(num4, text3.Length - num4).TrimStart(':', ' ');
|
|
if (text4.Length > 100)
|
|
{
|
|
text4 = text4.Substring(0, 100) + "...";
|
|
}
|
|
string relativePath = Path.GetRelativePath(workFolder, item5);
|
|
list.Add((relativePath, num, text2, text4));
|
|
break;
|
|
}
|
|
}
|
|
if (list.Count >= 200)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (list.Count < 200)
|
|
{
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (list.Count == 0)
|
|
{
|
|
return ToolResult.Ok("TODO/FIXME 코멘트가 발견되지 않았습니다.");
|
|
}
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder3 = stringBuilder2;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder2);
|
|
handler.AppendLiteral("발견된 TODO 코멘트: ");
|
|
handler.AppendFormatted(list.Count);
|
|
handler.AppendLiteral("개");
|
|
stringBuilder3.AppendLine(ref handler);
|
|
foreach (var item7 in list)
|
|
{
|
|
string item = item7.Item1;
|
|
int item2 = item7.Item2;
|
|
string item3 = item7.Item3;
|
|
string item4 = item7.Item4;
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder4 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(9, 4, stringBuilder2);
|
|
handler.AppendLiteral(" [");
|
|
handler.AppendFormatted(item3);
|
|
handler.AppendLiteral("] ");
|
|
handler.AppendFormatted(item);
|
|
handler.AppendLiteral(":");
|
|
handler.AppendFormatted(item2);
|
|
handler.AppendLiteral(" — ");
|
|
handler.AppendFormatted(item4);
|
|
stringBuilder4.AppendLine(ref handler);
|
|
}
|
|
return ToolResult.Ok(stringBuilder.ToString());
|
|
}
|
|
|
|
private static ToolResult AddTask(JsonElement args, AgentContext context)
|
|
{
|
|
JsonElement value;
|
|
string text = (args.TryGetProperty("title", out value) ? (value.GetString() ?? "") : "");
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return ToolResult.Fail("'title'이 필요합니다.");
|
|
}
|
|
JsonElement value2;
|
|
string text2 = (args.TryGetProperty("priority", out value2) ? (value2.GetString() ?? "medium") : "medium");
|
|
List<TaskItem> list = LoadTasks(context);
|
|
int num = ((list.Count > 0) ? list.Max((TaskItem t2) => t2.Id) : 0);
|
|
list.Add(new TaskItem
|
|
{
|
|
Id = num + 1,
|
|
Title = text,
|
|
Priority = text2,
|
|
Created = DateTime.Now.ToString("yyyy-MM-dd HH:mm"),
|
|
Done = false
|
|
});
|
|
SaveTasks(context, list);
|
|
return ToolResult.Ok($"태스크 추가: #{num + 1} — {text} (우선순위: {text2})");
|
|
}
|
|
|
|
private static ToolResult ListTasks(AgentContext context)
|
|
{
|
|
List<TaskItem> list = LoadTasks(context);
|
|
if (list.Count == 0)
|
|
{
|
|
return ToolResult.Ok("등록된 태스크가 없습니다.");
|
|
}
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder3 = stringBuilder2;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder2);
|
|
handler.AppendLiteral("태스크 목록 (");
|
|
handler.AppendFormatted(list.Count);
|
|
handler.AppendLiteral("개):");
|
|
stringBuilder3.AppendLine(ref handler);
|
|
foreach (TaskItem item in from t in list
|
|
orderby t.Done, (!(t.Priority == "high")) ? ((t.Priority == "medium") ? 1 : 2) : 0 descending
|
|
select t)
|
|
{
|
|
string value = (item.Done ? "✓" : "○");
|
|
stringBuilder2 = stringBuilder;
|
|
StringBuilder stringBuilder4 = stringBuilder2;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(11, 5, stringBuilder2);
|
|
handler.AppendLiteral(" ");
|
|
handler.AppendFormatted(value);
|
|
handler.AppendLiteral(" #");
|
|
handler.AppendFormatted(item.Id);
|
|
handler.AppendLiteral(" [");
|
|
handler.AppendFormatted(item.Priority);
|
|
handler.AppendLiteral("] ");
|
|
handler.AppendFormatted(item.Title);
|
|
handler.AppendLiteral(" (");
|
|
handler.AppendFormatted(item.Created);
|
|
handler.AppendLiteral(")");
|
|
stringBuilder4.AppendLine(ref handler);
|
|
}
|
|
return ToolResult.Ok(stringBuilder.ToString());
|
|
}
|
|
|
|
private static ToolResult MarkDone(JsonElement args, AgentContext context)
|
|
{
|
|
if (!args.TryGetProperty("id", out var value))
|
|
{
|
|
return ToolResult.Fail("'id'가 필요합니다.");
|
|
}
|
|
int id = value.GetInt32();
|
|
List<TaskItem> list = LoadTasks(context);
|
|
TaskItem taskItem = list.FirstOrDefault((TaskItem t) => t.Id == id);
|
|
if (taskItem == null)
|
|
{
|
|
return ToolResult.Fail($"태스크 #{id}를 찾을 수 없습니다.");
|
|
}
|
|
taskItem.Done = true;
|
|
SaveTasks(context, list);
|
|
return ToolResult.Ok($"태스크 #{id} 완료: {taskItem.Title}");
|
|
}
|
|
|
|
private static List<TaskItem> LoadTasks(AgentContext context)
|
|
{
|
|
string path = Path.Combine(context.WorkFolder, ".ax/tasks.json");
|
|
if (!File.Exists(path))
|
|
{
|
|
return new List<TaskItem>();
|
|
}
|
|
string json = File.ReadAllText(path);
|
|
return JsonSerializer.Deserialize<List<TaskItem>>(json) ?? new List<TaskItem>();
|
|
}
|
|
|
|
private static void SaveTasks(AgentContext context, List<TaskItem> tasks)
|
|
{
|
|
string path = Path.Combine(context.WorkFolder, ".ax/tasks.json");
|
|
string directoryName = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
|
|
{
|
|
Directory.CreateDirectory(directoryName);
|
|
}
|
|
string contents = JsonSerializer.Serialize(tasks, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true
|
|
});
|
|
File.WriteAllText(path, contents);
|
|
}
|
|
}
|