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,211 @@
using System.Runtime.InteropServices;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L27-2: 시스템 볼륨 제어 핸들러. "vol" 프리픽스로 사용합니다.
///
/// 예: vol → 현재 볼륨 표시
/// vol 50 → 볼륨 50% 설정
/// vol up / down → ±10% 조절
/// vol mute → 음소거 토글
/// Enter → 해당 명령 실행.
/// Windows Core Audio API (IAudioEndpointVolume) COM 인터페이스 사용.
/// </summary>
public class VolHandler : IActionHandler
{
public string? Prefix => "vol";
public PluginMetadata Metadata => new(
"볼륨 제어",
"시스템 볼륨 조절 — 설정·증감·음소거",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim().ToLowerInvariant();
var items = new List<LauncherItem>();
// 현재 볼륨 읽기
float curLevel;
bool curMuted;
try
{
using var audio = AudioEndpoint.GetDefault();
curLevel = audio.GetVolume();
curMuted = audio.GetMute();
}
catch
{
items.Add(new LauncherItem(
"오디오 장치를 찾을 수 없습니다",
"기본 재생 장치가 연결되어 있는지 확인하세요",
null, null, Symbol: Symbols.Warning));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
int pct = (int)Math.Round(curLevel * 100);
var bar = VolumeBar(pct);
var muteLabel = curMuted ? " 🔇 음소거" : "";
var symbol = curMuted ? Symbols.VolumeMute
: pct == 0 ? Symbols.VolumeMute
: pct < 50 ? Symbols.VolumeDown
: Symbols.VolumeUp;
if (string.IsNullOrWhiteSpace(q))
{
items.Add(new LauncherItem(
$"현재 볼륨: {pct}%{muteLabel}",
$"{bar} · vol 50 / vol up / vol down / vol mute",
null, null, Symbol: symbol));
items.Add(new LauncherItem("vol up", "볼륨 +10%", null, ("set", Math.Min(pct + 10, 100)), Symbol: Symbols.VolumeUp));
items.Add(new LauncherItem("vol down", "볼륨 10%", null, ("set", Math.Max(pct - 10, 0)), Symbol: Symbols.VolumeDown));
items.Add(new LauncherItem("vol mute", curMuted ? "음소거 해제" : "음소거", null, ("mute", !curMuted), Symbol: Symbols.VolumeMute));
items.Add(new LauncherItem("vol 0", "볼륨 0% (무음)", null, ("set", 0), Symbol: Symbols.VolumeMute));
items.Add(new LauncherItem("vol 50", "볼륨 50%", null, ("set", 50), Symbol: Symbols.VolumeDown));
items.Add(new LauncherItem("vol 100", "볼륨 100%", null, ("set", 100), Symbol: Symbols.VolumeUp));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 명령 파싱
if (q is "up" or "올려" or "+")
{
var target = Math.Min(pct + 10, 100);
items.Add(new LauncherItem($"볼륨 +10% → {target}%", $"{VolumeBar(target)}", null, ("set", target), Symbol: Symbols.VolumeUp));
}
else if (q is "down" or "내려" or "-")
{
var target = Math.Max(pct - 10, 0);
items.Add(new LauncherItem($"볼륨 10% → {target}%", $"{VolumeBar(target)}", null, ("set", target), Symbol: Symbols.VolumeDown));
}
else if (q is "mute" or "음소거" or "m")
{
items.Add(new LauncherItem(
curMuted ? "음소거 해제" : "음소거 설정",
$"현재: {pct}%{muteLabel}",
null, ("mute", !curMuted), Symbol: Symbols.VolumeMute));
}
else if (int.TryParse(q, out int val) && val is >= 0 and <= 100)
{
items.Add(new LauncherItem(
$"볼륨 {val}% 설정",
$"{VolumeBar(val)} (현재 {pct}%)",
null, ("set", val), Symbol: val > pct ? Symbols.VolumeUp : Symbols.VolumeDown));
}
else
{
items.Add(new LauncherItem(
$"'{query}' — 알 수 없는 명령",
"사용법: vol 50 / vol up / vol down / vol mute",
null, null, Symbol: Symbols.Warning));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
try
{
using var audio = AudioEndpoint.GetDefault();
if (item.Data is ("set", int level))
{
audio.SetVolume(level / 100f);
if (audio.GetMute()) audio.SetMute(false);
NotificationService.Notify("vol", $"볼륨 {level}%");
}
else if (item.Data is ("mute", bool mute))
{
audio.SetMute(mute);
NotificationService.Notify("vol", mute ? "음소거" : "음소거 해제");
}
}
catch (Exception ex)
{
NotificationService.Notify("vol", $"오류: {ex.Message}");
}
return Task.CompletedTask;
}
private static string VolumeBar(int pct)
{
int filled = pct / 5; // 0~20
return "[" + new string('█', filled) + new string('░', 20 - filled) + "]";
}
// ─── Core Audio API COM 래퍼 ─────────────────────────────────────────────
private sealed class AudioEndpoint : IDisposable
{
private readonly IAudioEndpointVolume _vol;
private AudioEndpoint(IAudioEndpointVolume vol) => _vol = vol;
public static AudioEndpoint GetDefault()
{
var enumerator = (IMMDeviceEnumerator)new MMDeviceEnumeratorClass();
enumerator.GetDefaultAudioEndpoint(0 /*eRender*/, 1 /*eMultimedia*/, out var device);
var iid = typeof(IAudioEndpointVolume).GUID;
device.Activate(ref iid, 1 /*CLSCTX_ALL*/, IntPtr.Zero, out var obj);
return new AudioEndpoint((IAudioEndpointVolume)obj);
}
public float GetVolume() { _vol.GetMasterVolumeLevelScalar(out float l); return l; }
public bool GetMute() { _vol.GetMute(out bool m); return m; }
public void SetVolume(float level) { var g = Guid.Empty; _vol.SetMasterVolumeLevelScalar(Math.Clamp(level, 0f, 1f), ref g); }
public void SetMute(bool mute) { var g = Guid.Empty; _vol.SetMute(mute, ref g); }
public void Dispose()
{
if (_vol is IDisposable d) d.Dispose();
if (_vol != null) Marshal.ReleaseComObject(_vol);
}
}
// ─── COM 인터페이스 정의 (Windows Core Audio API) ──────────────────────
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
private class MMDeviceEnumeratorClass { }
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMMDeviceEnumerator
{
int EnumAudioEndpoints(int dataFlow, uint stateMask, out IntPtr devices);
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice device);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMMDevice
{
int Activate(ref Guid iid, uint clsCtx, IntPtr activationParams,
[MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
}
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioEndpointVolume
{
int RegisterControlChangeNotify(IntPtr pNotify);
int UnregisterControlChangeNotify(IntPtr pNotify);
int GetChannelCount(out uint pnChannelCount);
int SetMasterVolumeLevel(float fLevelDB, ref Guid pguidEventContext);
int SetMasterVolumeLevelScalar(float fLevel, ref Guid pguidEventContext);
int GetMasterVolumeLevel(out float pfLevelDB);
int GetMasterVolumeLevelScalar(out float pfLevel);
int SetChannelVolumeLevel(uint nChannel, float fLevelDB, ref Guid pguidEventContext);
int SetChannelVolumeLevelScalar(uint nChannel, float fLevel, ref Guid pguidEventContext);
int GetChannelVolumeLevel(uint nChannel, out float pfLevelDB);
int GetChannelVolumeLevelScalar(uint nChannel, out float pfLevel);
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, ref Guid pguidEventContext);
int GetMute([MarshalAs(UnmanagedType.Bool)] out bool pbMute);
}
}