AX Commander 비교본 런처 기능 대량 이식

변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다.

핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다.

핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다.

문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다.

검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
This commit is contained in:
2026-04-05 00:59:45 +09:00
parent 0929778ca7
commit 0336904258
115 changed files with 30749 additions and 1 deletions

View File

@@ -0,0 +1,300 @@
using System.Security.Cryptography;
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L10-2: UUID/GUID 생성기 핸들러. "uuid" 프리픽스로 사용합니다.
///
/// 예: uuid → UUID v4 1개 생성
/// uuid 5 → UUID v4 5개 생성
/// uuid upper → 대문자 UUID 생성
/// uuid v4 → UUID v4 (랜덤)
/// uuid seq → 순차 UUID (시간 기반, 정렬 가능)
/// uuid short → 짧은 고유 ID (8자리 hex)
/// uuid nil → Nil UUID (00000000-…)
/// uuid parse <uuid> → UUID 분석 (버전, 타임스탬프 등)
/// Enter → 결과를 클립보드에 복사.
/// </summary>
public class UuidHandler : IActionHandler
{
public string? Prefix => "uuid";
public PluginMetadata Metadata => new(
"UUID",
"UUID/GUID 생성기 — v4 · 순차 · 짧은 ID · 분석",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
if (string.IsNullOrWhiteSpace(q))
{
// 기본: v4 1개 생성
var uuid = Guid.NewGuid().ToString();
items.Add(new LauncherItem(
uuid,
"UUID v4 (랜덤) · Enter 복사",
null,
("copy", uuid),
Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid 5", "5개 생성", null, null, Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid upper", "대문자 UUID", null, null, Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid seq", "순차 UUID (정렬가능)", null, null, Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid short", "짧은 ID (8자리)", null, null, Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid parse …", "UUID 분석", null, null, Symbol: "\uF0E2"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
// "uuid parse <value>"
if (sub == "parse" && parts.Length >= 2)
{
items.AddRange(ParseUuid(string.Join(" ", parts.Skip(1))));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid nil"
if (sub == "nil")
{
var nil = "00000000-0000-0000-0000-000000000000";
items.Add(new LauncherItem(nil, "Nil UUID", null, ("copy", nil), Symbol: "\uF0E2"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid seq"
if (sub == "seq")
{
items.AddRange(GenerateSequential(5));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid short"
if (sub == "short")
{
items.AddRange(GenerateShort(5));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid upper"
if (sub == "upper")
{
var upper = Guid.NewGuid().ToString().ToUpperInvariant();
items.Add(new LauncherItem(upper, "대문자 UUID v4 · Enter 복사", null, ("copy", upper), Symbol: "\uF0E2"));
for (var i = 0; i < 4; i++)
{
var u = Guid.NewGuid().ToString().ToUpperInvariant();
items.Add(new LauncherItem(u, "대문자 UUID v4", null, ("copy", u), Symbol: "\uF0E2"));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid v4"
if (sub == "v4")
{
items.AddRange(GenerateV4(5));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid <숫자>" — N개 생성
if (int.TryParse(sub, out var count))
{
count = Math.Clamp(count, 1, 20);
items.AddRange(GenerateV4(count));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// UUID 자체를 입력한 경우 → parse
if (Guid.TryParse(q, out _))
{
items.AddRange(ParseUuid(q));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem("알 수 없는 명령",
"예: uuid / uuid 5 / uuid upper / uuid seq / uuid short / uuid parse",
null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ("copy", string text))
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(text));
NotificationService.Notify("UUID", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── 생성 헬퍼 ─────────────────────────────────────────────────────────────
private static IEnumerable<LauncherItem> GenerateV4(int count)
{
var all = new List<string>();
for (var i = 0; i < count; i++)
{
var uuid = Guid.NewGuid().ToString();
all.Add(uuid);
}
if (count > 1)
{
yield return new LauncherItem(
$"UUID v4 {count}개",
"전체 복사: Enter",
null,
("copy", string.Join("\n", all)),
Symbol: "\uF0E2");
}
foreach (var uuid in all)
yield return new LauncherItem(uuid, "UUID v4 · Enter 복사", null, ("copy", uuid), Symbol: "\uF0E2");
}
/// <summary>
/// 시간 기반 순차 UUID (UUIDv7 스타일: 밀리초 타임스탬프 + 랜덤 비트).
/// 정렬 가능하고 시간 정보 포함.
/// </summary>
private static IEnumerable<LauncherItem> GenerateSequential(int count)
{
var all = new List<string>();
for (var i = 0; i < count; i++)
{
if (i > 0) System.Threading.Thread.Sleep(1); // 밀리초 차이 보장
var uuid = NewSequentialGuid();
all.Add(uuid);
}
yield return new LauncherItem(
$"순차 UUID {count}개",
"시간 기반 정렬 가능 · 전체 복사: Enter",
null,
("copy", string.Join("\n", all)),
Symbol: "\uF0E2");
foreach (var uuid in all)
yield return new LauncherItem(uuid, "순차 UUID · Enter 복사", null, ("copy", uuid), Symbol: "\uF0E2");
}
private static IEnumerable<LauncherItem> GenerateShort(int count)
{
var all = new List<string>();
for (var i = 0; i < count; i++)
all.Add(NewShortId());
yield return new LauncherItem(
$"짧은 ID {count}개",
"8자리 hex · 전체 복사: Enter",
null,
("copy", string.Join("\n", all)),
Symbol: "\uF0E2");
foreach (var id in all)
yield return new LauncherItem(id, "짧은 ID (8자리) · Enter 복사", null, ("copy", id), Symbol: "\uF0E2");
}
private static IEnumerable<LauncherItem> ParseUuid(string raw)
{
raw = raw.Trim();
if (!Guid.TryParse(raw, out var guid))
{
yield return new LauncherItem("UUID 파싱 실패", $"'{raw}'은 유효한 UUID가 아닙니다", null, null, Symbol: "\uE783");
yield break;
}
var bytes = guid.ToByteArray();
var version = (bytes[7] >> 4) & 0x0F;
var variant = (bytes[8] >> 6) & 0x03;
yield return new LauncherItem(
guid.ToString(),
$"버전 {version} · 변형 {variant}",
null,
("copy", guid.ToString()),
Symbol: "\uF0E2");
yield return new LauncherItem("소문자", guid.ToString(), null, ("copy", guid.ToString()), Symbol: "\uF0E2");
yield return new LauncherItem("대문자", guid.ToString().ToUpper(), null, ("copy", guid.ToString().ToUpper()), Symbol: "\uF0E2");
yield return new LauncherItem("중괄호", $"{{{guid}}}", null, ("copy", $"{{{guid}}}"), Symbol: "\uF0E2");
yield return new LauncherItem("대시 없음", guid.ToString("N"), null, ("copy", guid.ToString("N")), Symbol: "\uF0E2");
yield return new LauncherItem("버전", $"UUID v{version}", null, null, Symbol: "\uF0E2");
yield return new LauncherItem("변형", variant == 2 ? "RFC 4122" : variant == 3 ? "Microsoft" : $"변형 {variant}", null, null, Symbol: "\uF0E2");
// 시간 기반 버전 (v1)이면 타임스탬프 복원 시도
if (version == 1)
{
var ts = ExtractV1Timestamp(bytes);
if (ts.HasValue)
yield return new LauncherItem("타임스탬프", ts.Value.ToString("yyyy-MM-dd HH:mm:ss.fff UTC"), null, null, Symbol: "\uF0E2");
}
}
// ── UUID 생성 구현 ────────────────────────────────────────────────────────
/// <summary>UUIDv7 스타일 순차 GUID: 상위 48비트 = Unix ms 타임스탬프, 하위 = 랜덤</summary>
private static string NewSequentialGuid()
{
var ms = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var rand = RandomNumberGenerator.GetBytes(10);
var bytes = new byte[16];
// 상위 6바이트 = 타임스탬프 (big-endian ms)
bytes[0] = (byte)(ms >> 40);
bytes[1] = (byte)(ms >> 32);
bytes[2] = (byte)(ms >> 24);
bytes[3] = (byte)(ms >> 16);
bytes[4] = (byte)(ms >> 8);
bytes[5] = (byte)(ms);
// 버전 비트 (v7 = 0111)
bytes[6] = (byte)((rand[0] & 0x0F) | 0x70);
bytes[7] = rand[1];
// 변형 비트 (RFC 4122 = 10xx)
bytes[8] = (byte)((rand[2] & 0x3F) | 0x80);
bytes[9] = rand[3];
// 나머지 랜덤
Array.Copy(rand, 4, bytes, 10, 6);
return new Guid(bytes).ToString();
}
private static string NewShortId()
{
var bytes = RandomNumberGenerator.GetBytes(4);
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
}
private static DateTime? ExtractV1Timestamp(byte[] bytes)
{
try
{
// UUID v1: time_low(4) + time_mid(2) + time_hi_version(2)
var timeLow = (long)((uint)((bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]));
var timeMid = (long)((ushort)((bytes[5] << 8) | bytes[4]));
var timeHigh = (long)((ushort)((bytes[7] << 8) | bytes[6]) & 0x0FFF);
var ticks = (timeHigh << 48) | (timeMid << 32) | timeLow;
// UUID epoch = Oct 15, 1582
var uuidEpoch = new DateTime(1582, 10, 15, 0, 0, 0, DateTimeKind.Utc);
return uuidEpoch.AddTicks(ticks);
}
catch { return null; }
}
}