변경 목적: 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개를 확인했습니다.
250 lines
8.9 KiB
C#
250 lines
8.9 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Windows;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// L9-1: 비밀번호 생성기 핸들러. "pwd" 프리픽스로 사용합니다.
|
|
///
|
|
/// 예: pwd → 기본 16자 비밀번호 5개 생성
|
|
/// pwd 24 → 24자 비밀번호 5개 생성
|
|
/// pwd 32 strong → 32자 강력 옵션 (대소문자+숫자+특수)
|
|
/// pwd 16 alpha → 알파벳+숫자만 (특수문자 제외)
|
|
/// pwd 20 pin → 숫자만 (PIN 코드)
|
|
/// pwd passphrase → 단어 조합 기억하기 쉬운 패스프레이즈
|
|
/// Enter → 클립보드에 복사.
|
|
/// </summary>
|
|
public class PasswordGenHandler : IActionHandler
|
|
{
|
|
public string? Prefix => "pwd";
|
|
|
|
public PluginMetadata Metadata => new(
|
|
"PasswordGen",
|
|
"비밀번호 생성기 — 길이 · 복잡도 · 패스프레이즈",
|
|
"1.0",
|
|
"AX");
|
|
|
|
private const string LowerChars = "abcdefghijklmnopqrstuvwxyz";
|
|
private const string UpperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
private const string DigitChars = "0123456789";
|
|
private const string SpecialChars = "!@#$%^&*()-_=+[]{}|;:,.<>?";
|
|
|
|
// 기억하기 쉬운 단어 목록 (간결한 영단어)
|
|
private static readonly string[] WordList =
|
|
[
|
|
"apple","bridge","cloud","dawn","eagle","flame","grape","harbor",
|
|
"ivory","jungle","kite","lemon","maple","night","ocean","pearl",
|
|
"quartz","river","storm","tiger","ultra","violet","water","xenon",
|
|
"yellow","zenith","amber","blaze","cedar","delta","ember","frost",
|
|
"glass","honey","iron","jade","knot","lunar","mango","nova",
|
|
"orbit","prism","quest","range","solar","track","umbra","valor",
|
|
];
|
|
|
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
var q = query.Trim().ToLowerInvariant();
|
|
var items = new List<LauncherItem>();
|
|
|
|
// 패스프레이즈 모드
|
|
if (q.StartsWith("passphrase") || q.StartsWith("phrase"))
|
|
{
|
|
var wordCount = 4;
|
|
var parts2 = q.Split(' ');
|
|
if (parts2.Length >= 2 && int.TryParse(parts2[1], out var wc))
|
|
wordCount = Math.Clamp(wc, 2, 8);
|
|
|
|
items.Add(new LauncherItem(
|
|
"패스프레이즈 생성",
|
|
$"{wordCount}단어 조합 · 기억하기 쉬운 형식",
|
|
null, null, Symbol: "\uE8D4"));
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
var phrase = GeneratePassphrase(wordCount);
|
|
var strength = EstimateEntropy(phrase);
|
|
items.Add(new LauncherItem(
|
|
phrase,
|
|
$"엔트로피: ~{strength}bit",
|
|
null,
|
|
("copy", phrase),
|
|
Symbol: "\uE8D4"));
|
|
}
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// 파라미터 파싱: [길이] [모드]
|
|
int length = 16;
|
|
string mode = "strong";
|
|
|
|
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length >= 1 && int.TryParse(parts[0], out var l))
|
|
{
|
|
length = Math.Clamp(l, 4, 128);
|
|
if (parts.Length >= 2) mode = parts[1];
|
|
}
|
|
else if (parts.Length >= 1 && parts[0] is "strong" or "alpha" or "pin" or "simple")
|
|
{
|
|
mode = parts[0];
|
|
}
|
|
|
|
// 문자 집합 결정
|
|
var charset = BuildCharset(mode);
|
|
|
|
// 강도 표시
|
|
var modeLabel = mode switch
|
|
{
|
|
"alpha" => "알파뉴메릭 (대소문자+숫자)",
|
|
"pin" => "숫자 PIN",
|
|
"simple" => "간단 (소문자+숫자)",
|
|
_ => "강력 (대소문자+숫자+특수)",
|
|
};
|
|
|
|
items.Add(new LauncherItem(
|
|
$"비밀번호 생성 {length}자",
|
|
modeLabel,
|
|
null, null, Symbol: "\uE8D4"));
|
|
|
|
// 5개 후보 생성
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
var pw = GeneratePassword(charset, length, mode);
|
|
var strength = GetStrengthLabel(pw);
|
|
items.Add(new LauncherItem(
|
|
pw,
|
|
strength,
|
|
null,
|
|
("copy", pw),
|
|
Symbol: "\uE8D4"));
|
|
}
|
|
|
|
// 옵션 안내
|
|
items.Add(new LauncherItem(
|
|
"pwd <길이> alpha",
|
|
"알파뉴메릭 (특수문자 제외)",
|
|
null, null, Symbol: "\uE946"));
|
|
items.Add(new LauncherItem(
|
|
"pwd <길이> pin",
|
|
"숫자만 (PIN 코드)",
|
|
null, null, Symbol: "\uE946"));
|
|
items.Add(new LauncherItem(
|
|
"pwd passphrase",
|
|
"단어 조합 패스프레이즈",
|
|
null, null, Symbol: "\uE946"));
|
|
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
|
{
|
|
if (item.Data is ("copy", string pw))
|
|
{
|
|
try
|
|
{
|
|
System.Windows.Application.Current.Dispatcher.Invoke(
|
|
() => Clipboard.SetText(pw));
|
|
NotificationService.Notify("비밀번호", "클립보드에 복사했습니다.");
|
|
}
|
|
catch { /* 비핵심 */ }
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
|
|
|
private static string BuildCharset(string mode) => mode switch
|
|
{
|
|
"pin" => DigitChars,
|
|
"alpha" => LowerChars + UpperChars + DigitChars,
|
|
"simple" => LowerChars + DigitChars,
|
|
_ => LowerChars + UpperChars + DigitChars + SpecialChars,
|
|
};
|
|
|
|
private static string GeneratePassword(string charset, int length, string mode)
|
|
{
|
|
// strong 모드: 각 카테고리 최소 1개 보장
|
|
if (mode == "strong" && length >= 4)
|
|
{
|
|
var mandatory = new[]
|
|
{
|
|
RandomChar(LowerChars),
|
|
RandomChar(UpperChars),
|
|
RandomChar(DigitChars),
|
|
RandomChar(SpecialChars),
|
|
};
|
|
|
|
var remaining = length - mandatory.Length;
|
|
var bulk = Enumerable.Range(0, remaining)
|
|
.Select(_ => RandomChar(charset))
|
|
.ToList();
|
|
|
|
var all = mandatory.Concat(bulk).ToArray();
|
|
Shuffle(all);
|
|
return new string(all);
|
|
}
|
|
|
|
// alpha/pin/simple
|
|
return new string(Enumerable.Range(0, length)
|
|
.Select(_ => RandomChar(charset))
|
|
.ToArray());
|
|
}
|
|
|
|
private static string GeneratePassphrase(int wordCount)
|
|
{
|
|
var words = Enumerable.Range(0, wordCount)
|
|
.Select(_ => WordList[RandomInt(WordList.Length)]);
|
|
var num = RandomInt(9000) + 1000;
|
|
var sep = new[] { "-", "_", ".", "!" }[RandomInt(4)];
|
|
return string.Join(sep, words) + sep + num;
|
|
}
|
|
|
|
private static char RandomChar(string charset) =>
|
|
charset[RandomInt(charset.Length)];
|
|
|
|
private static int RandomInt(int max) =>
|
|
(int)(RandomNumberGenerator.GetInt32(int.MaxValue) % max);
|
|
|
|
private static void Shuffle<T>(T[] arr)
|
|
{
|
|
for (int i = arr.Length - 1; i > 0; i--)
|
|
{
|
|
var j = RandomInt(i + 1);
|
|
(arr[i], arr[j]) = (arr[j], arr[i]);
|
|
}
|
|
}
|
|
|
|
private static string GetStrengthLabel(string pw)
|
|
{
|
|
var hasLower = pw.Any(char.IsLower);
|
|
var hasUpper = pw.Any(char.IsUpper);
|
|
var hasDigit = pw.Any(char.IsDigit);
|
|
var hasSpecial = pw.Any(c => SpecialChars.Contains(c));
|
|
var types = new[] { hasLower, hasUpper, hasDigit, hasSpecial }.Count(b => b);
|
|
|
|
var strength = (pw.Length, types) switch
|
|
{
|
|
( >= 24, >= 4) => "매우 강함 🔐",
|
|
( >= 16, >= 3) => "강함 🔒",
|
|
( >= 12, >= 2) => "보통 🔑",
|
|
_ => "약함 ⚠",
|
|
};
|
|
return $"{strength} · {pw.Length}자";
|
|
}
|
|
|
|
private static int EstimateEntropy(string pw)
|
|
{
|
|
// 간략 엔트로피 추정: log2(charset^length)
|
|
var hasLower = pw.Any(char.IsLower);
|
|
var hasUpper = pw.Any(char.IsUpper);
|
|
var hasDigit = pw.Any(char.IsDigit);
|
|
var hasSpecial = pw.Any(c => !char.IsLetterOrDigit(c));
|
|
var pool = (hasLower ? 26 : 0) + (hasUpper ? 26 : 0)
|
|
+ (hasDigit ? 10 : 0) + (hasSpecial ? 32 : 0);
|
|
if (pool == 0) pool = 36;
|
|
return (int)(pw.Length * Math.Log2(pool));
|
|
}
|
|
}
|