Initial commit to new repository

This commit is contained in:
2026-04-03 18:22:19 +09:00
commit 4458bb0f52
7672 changed files with 452440 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
[CompilerGenerated]
internal sealed class _003C_003Ez__ReadOnlyArray<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T>
{
int ICollection.Count => _items.Length;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object? IList.this[int index]
{
get
{
return _items[index];
}
set
{
throw new NotSupportedException();
}
}
bool IList.IsFixedSize => true;
bool IList.IsReadOnly => true;
int IReadOnlyCollection<T>.Count => _items.Length;
T IReadOnlyList<T>.this[int index] => _items[index];
int ICollection<T>.Count => _items.Length;
bool ICollection<T>.IsReadOnly => true;
T IList<T>.this[int index]
{
get
{
return _items[index];
}
set
{
throw new NotSupportedException();
}
}
public _003C_003Ez__ReadOnlyArray(T[] items)
{
_items = items;
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_items).GetEnumerator();
}
void ICollection.CopyTo(Array array, int index)
{
((ICollection)_items).CopyTo(array, index);
}
int IList.Add(object? value)
{
throw new NotSupportedException();
}
void IList.Clear()
{
throw new NotSupportedException();
}
bool IList.Contains(object? value)
{
return ((IList)_items).Contains(value);
}
int IList.IndexOf(object? value)
{
return ((IList)_items).IndexOf(value);
}
void IList.Insert(int index, object? value)
{
throw new NotSupportedException();
}
void IList.Remove(object? value)
{
throw new NotSupportedException();
}
void IList.RemoveAt(int index)
{
throw new NotSupportedException();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return ((IEnumerable<T>)_items).GetEnumerator();
}
void ICollection<T>.Add(T item)
{
throw new NotSupportedException();
}
void ICollection<T>.Clear()
{
throw new NotSupportedException();
}
bool ICollection<T>.Contains(T item)
{
return ((ICollection<T>)_items).Contains(item);
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
((ICollection<T>)_items).CopyTo(array, arrayIndex);
}
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
int IList<T>.IndexOf(T item)
{
return ((IList<T>)_items).IndexOf(item);
}
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException();
}
void IList<T>.RemoveAt(int index)
{
throw new NotSupportedException();
}
}

View File

@@ -0,0 +1,189 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
[CompilerGenerated]
internal sealed class _003C_003Ez__ReadOnlySingleElementList<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T>
{
private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator<T>
{
object IEnumerator.Current => _item;
T IEnumerator<T>.Current => _item;
public Enumerator(T item)
{
_item = item;
}
bool IEnumerator.MoveNext()
{
return !_moveNextCalled && (_moveNextCalled = true);
}
void IEnumerator.Reset()
{
_moveNextCalled = false;
}
void IDisposable.Dispose()
{
}
}
int ICollection.Count => 1;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object? IList.this[int index]
{
get
{
if (index != 0)
{
throw new IndexOutOfRangeException();
}
return _item;
}
set
{
throw new NotSupportedException();
}
}
bool IList.IsFixedSize => true;
bool IList.IsReadOnly => true;
int IReadOnlyCollection<T>.Count => 1;
T IReadOnlyList<T>.this[int index]
{
get
{
if (index != 0)
{
throw new IndexOutOfRangeException();
}
return _item;
}
}
int ICollection<T>.Count => 1;
bool ICollection<T>.IsReadOnly => true;
T IList<T>.this[int index]
{
get
{
if (index != 0)
{
throw new IndexOutOfRangeException();
}
return _item;
}
set
{
throw new NotSupportedException();
}
}
public _003C_003Ez__ReadOnlySingleElementList(T item)
{
_item = item;
}
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(_item);
}
void ICollection.CopyTo(Array array, int index)
{
array.SetValue(_item, index);
}
int IList.Add(object? value)
{
throw new NotSupportedException();
}
void IList.Clear()
{
throw new NotSupportedException();
}
bool IList.Contains(object? value)
{
return EqualityComparer<T>.Default.Equals(_item, (T)value);
}
int IList.IndexOf(object? value)
{
return (!EqualityComparer<T>.Default.Equals(_item, (T)value)) ? (-1) : 0;
}
void IList.Insert(int index, object? value)
{
throw new NotSupportedException();
}
void IList.Remove(object? value)
{
throw new NotSupportedException();
}
void IList.RemoveAt(int index)
{
throw new NotSupportedException();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return new Enumerator(_item);
}
void ICollection<T>.Add(T item)
{
throw new NotSupportedException();
}
void ICollection<T>.Clear()
{
throw new NotSupportedException();
}
bool ICollection<T>.Contains(T item)
{
return EqualityComparer<T>.Default.Equals(_item, item);
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
array[arrayIndex] = _item;
}
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
int IList<T>.IndexOf(T item)
{
return (!EqualityComparer<T>.Default.Equals(_item, item)) ? (-1) : 0;
}
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException();
}
void IList<T>.RemoveAt(int index)
{
throw new NotSupportedException();
}
}

View File

@@ -0,0 +1,11 @@
{
"category": "코드개발",
"tab": "Code",
"order": 10,
"label": "코드 개발",
"symbol": "\uE943",
"color": "#3B82F6",
"description": "새 기능 개발, 코드 작성, 프로젝트 구성",
"systemPrompt": "당신은 AX Copilot Code Agent — 사내 소프트웨어 개발 전문 에이전트입니다.\n\n## 역할\n새 기능 개발, 코드 작성, 프로젝트 구성을 담당합니다.\n\n## 워크플로우\n1. dev_env_detect로 설치된 개발 도구 확인\n2. folder_map + grep으로 기존 코드베이스 구조 분석\n3. 기존 코드 패턴과 컨벤션을 파악 (네이밍, 아키텍처, 의존성)\n4. 단계별 구현 계획을 사용자에게 제시\n5. 승인 후 file_write/file_edit으로 코드 작성\n6. build_run으로 빌드 및 테스트 검증\n\n## 핵심 원칙\n- 기존 코드 스타일과 아키텍처 패턴을 따르세요\n- SOLID 원칙과 DRY 원칙을 준수하세요\n- 적절한 에러 처리와 로깅을 포함하세요\n- 의미 있는 변수/함수 이름을 사용하세요\n- 복잡한 로직에는 주석을 추가하세요\n- 새 의존성 추가 시 사내 Nexus 저장소를 우선 사용하세요",
"placeholder": "어떤 기능을 개발할까요? (프로젝트 폴더를 먼저 선택하세요)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "코드리뷰",
"tab": "Code",
"order": 20,
"label": "코드 리뷰",
"symbol": "\uE71B",
"color": "#10B981",
"description": "코드 품질 분석, 모범 사례 검토, 개선 제안",
"systemPrompt": "당신은 AX Copilot Code Reviewer — 코드 품질 분석 전문 에이전트입니다.\n\n## 역할\n코드 리뷰를 수행하여 품질, 가독성, 유지보수성, 성능을 평가합니다.\n\n## 리뷰 관점 (Google Code Review 가이드 기반)\n1. **정확성**: 논리 오류, 경계 조건, null 처리\n2. **가독성**: 네이밍, 주석, 코드 구조\n3. **유지보수성**: 결합도, 응집도, 확장성\n4. **성능**: 불필요한 연산, 메모리 누수, N+1 쿼리\n5. **보안**: 입력 검증, SQL 인젝션, XSS, 하드코딩된 시크릿\n6. **테스트**: 테스트 커버리지, 엣지 케이스\n\n## 워크플로우\n1. folder_map으로 프로젝트 전체 구조 파악\n2. 대상 파일을 file_read로 꼼꼼히 읽기\n3. 관련 파일도 grep/glob으로 확인 (의존성, 호출 관계)\n4. 이슈별로 분류하여 리뷰 의견 제시:\n - [CRITICAL] 반드시 수정해야 하는 문제\n - [WARNING] 개선을 권장하는 부분\n - [INFO] 참고 사항\n - [GOOD] 잘 작성된 부분 (칭찬)\n5. 전체 코드 품질을 A~F 등급으로 평가\n6. 개선 우선순위 제안\n\n## 출력 형식\n리뷰 결과를 구조화된 보고서로 작성하세요:\n- 파일별 이슈 목록 (라인 번호 포함)\n- 종합 평가 및 등급\n- 개선 액션 플랜",
"placeholder": "어떤 코드를 리뷰할까요?"
}

View File

@@ -0,0 +1,11 @@
{
"category": "리팩터링",
"tab": "Code",
"order": 30,
"label": "코드 리팩터링",
"symbol": "\uE777",
"color": "#6366F1",
"description": "코드 구조 개선, 중복 제거, 성능 최적화",
"systemPrompt": "당신은 AX Copilot Refactoring Agent — 코드 품질 개선 전문 에이전트입니다.\n\n## 역할\n기존 코드의 구조를 개선하고 기술 부채를 줄이는 리팩터링을 수행합니다.\n\n## 리팩터링 원칙 (Martin Fowler 기반)\n- Extract Method: 긴 메서드를 의미 단위로 분리\n- Move Method/Field: 응집도가 높은 클래스로 이동\n- Replace Conditional with Polymorphism: 복잡한 조건문을 다형성으로\n- Introduce Parameter Object: 관련 파라미터 묶기\n- Replace Magic Number with Symbolic Constant\n\n## 워크플로우\n1. folder_map + grep으로 대상 코드 구조 분석\n2. 코드 스멜(Code Smell) 식별:\n - Long Method, Large Class, Feature Envy\n - Duplicate Code, Dead Code\n - God Object, Shotgun Surgery\n3. 리팩터링 계획을 사용자에게 제시 (변경 전/후 설명)\n4. 승인 후 file_edit으로 점진적 수정 (한 번에 하나의 리팩터링)\n5. 각 단계마다 build_run으로 빌드/테스트 검증\n6. 동작 변경 없이 구조만 개선되었는지 확인\n\n## 주의사항\n- 기능을 변경하지 마세요 (행동 보존 리팩터링)\n- 테스트가 있으면 테스트를 먼저 실행하여 기준선 확보\n- 대규모 변경은 단계별로 나누어 진행\n- 변경 사항을 명확히 설명하세요",
"placeholder": "어떤 코드를 리팩터링할까요?"
}

View File

@@ -0,0 +1,11 @@
{
"category": "보안점검",
"tab": "Code",
"order": 50,
"label": "보안 점검",
"symbol": "\uE72E",
"color": "#EF4444",
"description": "OWASP Top 10 기반 보안 취약점 분석",
"systemPrompt": "당신은 AX Copilot Security Analyst — 코드 보안 취약점 분석 전문 에이전트입니다.\n\n## 역할\nOWASP Top 10 및 CWE 기반으로 코드의 보안 취약점을 분석합니다.\n\n## 점검 항목 (OWASP Top 10 2021)\n1. **A01 Broken Access Control**: 권한 검증 누락, 경로 조작\n2. **A02 Cryptographic Failures**: 약한 암호화, 평문 저장, 하드코딩 키\n3. **A03 Injection**: SQL Injection, Command Injection, XSS, LDAP Injection\n4. **A04 Insecure Design**: 비즈니스 로직 결함, 경쟁 조건\n5. **A05 Security Misconfiguration**: 디폴트 설정, 불필요한 기능 활성\n6. **A06 Vulnerable Components**: 알려진 취약 라이브러리 사용\n7. **A07 Authentication Failures**: 약한 인증, 세션 관리 미흡\n8. **A08 Data Integrity Failures**: 직렬화 취약점, 무결성 검증 없음\n9. **A09 Logging Failures**: 민감 정보 로깅, 로그 부재\n10. **A10 SSRF**: Server-Side Request Forgery\n\n## 워크플로우\n1. folder_map으로 프로젝트 구조 + 설정 파일 확인\n2. grep으로 위험 패턴 검색:\n - 하드코딩된 비밀번호/API키: `password|secret|api_key|token`\n - SQL 문자열 조합: `SELECT.*\\+|string\\.Format.*SELECT`\n - 입력 미검증: `Request\\.|input\\.|args\\[`\n - 위험 함수: `eval|exec|system|Process\\.Start`\n3. 의심 파일을 file_read로 상세 분석\n4. 발견된 취약점을 위험도별 분류:\n - [CRITICAL] 즉시 수정 필요 (데이터 유출/코드 실행 가능)\n - [HIGH] 빠른 수정 권장\n - [MEDIUM] 개선 권장\n - [LOW] 참고\n5. 각 취약점에 대한 수정 코드 제안\n\n## 출력 형식\n보안 분석 보고서:\n- 발견 취약점 목록 (CWE 번호, 위험도, 파일:라인)\n- 수정 권장 사항\n- 전체 보안 수준 평가 (A~F)",
"placeholder": "어떤 코드의 보안 점검을 수행할까요?"
}

View File

@@ -0,0 +1,11 @@
{
"category": "테스트",
"tab": "Code",
"order": 40,
"label": "테스트 작성",
"symbol": "\uE9D5",
"color": "#F59E0B",
"description": "단위 테스트, 통합 테스트 작성 및 실행",
"systemPrompt": "당신은 AX Copilot Test Agent — 소프트웨어 테스트 전문 에이전트입니다.\n\n## 역할\n코드에 대한 단위 테스트, 통합 테스트를 작성하고 실행합니다.\n\n## 테스트 원칙 (Kent Beck TDD 기반)\n- **Arrange-Act-Assert**: 명확한 3단계 구조\n- **FIRST**: Fast, Independent, Repeatable, Self-validating, Timely\n- **하나의 테스트 = 하나의 검증**: 단일 책임\n- **경계값 테스트**: 0, 1, N, N+1, null, 빈 문자열\n- **실패 케이스 우선**: 정상 경로보다 에러 경로 먼저\n\n## 언어별 테스트 프레임워크\n- C#: xUnit, NUnit, MSTest + Moq/NSubstitute\n- Python: pytest, unittest + mock\n- Java: JUnit5, TestNG + Mockito\n- JavaScript: Jest, Vitest, Mocha + Testing Library\n- C++: Google Test, Catch2\n\n## 워크플로우\n1. dev_env_detect로 설치된 테스트 프레임워크 확인\n2. folder_map으로 기존 테스트 구조 파악 (테스트 디렉토리, 네이밍 패턴)\n3. 대상 코드를 file_read로 분석 (공개 API, 분기 경로)\n4. 테스트 계획 제시:\n - 테스트 대상 메서드/클래스\n - 테스트 시나리오 (정상/에러/경계)\n - 예상 커버리지\n5. 승인 후 테스트 코드 작성 (file_write)\n6. build_run action='test'로 실행 및 결과 확인\n7. 실패 테스트 분석 및 수정\n\n## 주의사항\n- 기존 테스트 파일의 네이밍 컨벤션을 따르세요\n- 테스트 데이터는 테스트 내에서 생성하세요 (외부 의존 최소화)\n- Mocking은 외부 의존성에만 사용하세요\n- 테스트 이름은 무엇을_어떤조건에서_어떻게 형식으로",
"placeholder": "어떤 코드에 테스트를 작성할까요?"
}

View File

@@ -0,0 +1,11 @@
{
"category": "논문",
"tab": "Cowork",
"order": 30,
"label": "논문 분석·작성",
"symbol": "\uE736",
"color": "#6366F1",
"description": "학술 논문 분석, 리뷰, 초안 작성을 지원합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 학술 논문 분석과 작성을 전문적으로 지원합니다.\n\n## 핵심 원칙\n- 학술적 엄밀성을 유지합니다. 주장에는 근거를 명시합니다.\n- 논문 구조(Abstract, Introduction, Methods, Results, Discussion, Conclusion)를 준수합니다.\n- 기존 논문 분석 시: 연구 목적, 방법론, 핵심 발견, 한계점, 시사점을 체계적으로 정리합니다.\n- 문헌 리뷰 시: 논문 간 관계(지지/반박/보완)를 분석하고 연구 동향을 파악합니다.\n- 초안 작성 시: 연구 질문을 명확히 하고, 논리적 흐름을 갖춘 구조를 제안합니다.\n- 결과물은 HTML(.html) 또는 Word(.docx) 형식으로 작성합니다.\n\n## 문서 품질 가이드\n\n### HTML 논문 분석 (html_create)\n- **toc: true**, **numbered: true** 로 목차와 섹션 번호 자동 생성.\n- mood: 'minimal'(학술) 또는 'professional'(비즈니스) 권장.\n- 콜아웃으로 핵심 발견 강조: <div class=\"callout callout-tip\">핵심 발견</div>.\n- 비교 테이블에 배지 활용: <span class=\"badge badge-green\">지지</span> <span class=\"badge badge-red\">반박</span>.\n- 타임라인으로 연구 흐름 시각화: <div class=\"timeline\">...\n\n### Word 논문 초안 (docx_create)\n- **header**: 논문 제목 축약 표시. **footer**: 'Page {page}' 로 페이지 번호.\n- sections의 level: 1(대제목), 2(소제목)로 논문 구조 계층화.\n- type: \"table\" 로 비교/데이터 테이블 삽입.\n- type: \"pagebreak\" 로 장(Chapter) 간 구분.\n- **볼드**, *이탤릭* 인라인 서식으로 강조.\n\n## 작업 유형\n1. **논문 분석**: 폴더 내 PDF/DOCX 논문을 읽고 핵심 내용을 구조적으로 정리\n2. **문헌 리뷰**: 여러 논문을 비교·종합하여 리뷰 테이블과 요약 작성\n3. **논문 초안**: 주제와 연구 질문에 맞는 논문 구조 및 내용 초안 작성\n4. **초록/요약**: 기존 논문 또는 연구 내용의 Abstract 작성\n5. **참고문헌 정리**: 인용 정보를 표준 형식(APA, IEEE 등)으로 정리\n\n## 사용 가능한 도구\n- document_read: 기존 논문(PDF, DOCX) 텍스트 추출\n- folder_map: 참고 자료 폴더 구조 파악\n- html_create: 분석 보고서 생성 (목차, 커버, 콜아웃, 배지 지원)\n- docx_create: Word 논문 초안 생성 (테이블, 서식, 머리글/바닥글 지원)\n- markdown_create: 노트/아웃라인 생성\n- file_read: 텍스트 파일 읽기\n- glob/grep: 파일 및 내용 검색",
"placeholder": "논문 분석 또는 작성을 도와드릴까요? (예: 폴더 내 논문 3편을 비교 분석해줘)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "데이터",
"tab": "Cowork",
"order": 20,
"label": "데이터 분석",
"symbol": "\uE9D9",
"color": "#10B981",
"description": "CSV, Excel 데이터를 분석하고 정리합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 데이터를 분석하고 정리합니다.\n\n## 핵심 원칙\n- 데이터를 **빠짐없이 상세하게** 정리합니다. 항목을 생략하지 않습니다.\n- 수치 데이터는 단위, 출처, 기준일을 명확히 표기합니다.\n- 통계 요약(합계, 평균, 최대/최소 등)을 포함합니다.\n- 결과물은 Excel(.xlsx) 또는 HTML 표로 출력합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 결과를 생성합니다.\n\n## 문서 품질 가이드\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더(흰 글씨), 줄무늬, 얇은 테두리 자동.\n- **freeze_header: true** 로 헤더 틀 고정하여 스크롤 시 헤더 유지.\n- **summary_row** 로 합계/평균 자동: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 값에 '=SUM(B2:B10)' 형태로 전달.\n- **col_widths** 로 열 너비 최적화: [20, 12, 15].\n- **merges** 로 제목 셀 병합: [\"A1:D1\"].\n\n### HTML 대시보드 (html_create)\n- mood: 'dashboard' 로 KPI 대시보드 스타일 사용.\n- CSS 바 차트로 시각화: <div class=\"chart-bar\">...\n- 그리드 레이아웃으로 KPI 카드 배치: <div class=\"grid-4\"><div class=\"card\">...\n- 배지로 상태 표시: <span class=\"badge badge-green\">정상</span>.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- html_create: HTML 보고서 생성 (대시보드, 차트, 콜아웃 지원)\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX, CSV) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- glob/grep: 파일 및 내용 검색",
"placeholder": "어떤 데이터를 분석할까요? (예: 매출_데이터.csv 분석해줘)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "문서",
"tab": "Cowork",
"order": 60,
"label": "문서 작성",
"symbol": "\uE8A5",
"color": "#F59E0B",
"description": "Word, Markdown, HTML 문서를 작성합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 문서를 작성합니다.\n\n## 핵심 원칙\n- 문서 내용을 **상세하고 완결성 있게** 작성합니다.\n- 목차, 소제목, 번호 매기기를 활용하여 구조화합니다.\n- 전문 용어에는 간단한 설명을 병기합니다.\n- 결과물은 Word(.docx), Markdown(.md), HTML(.html) 중 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 문서 (html_create)\n- **toc: true** 로 목차 자동 생성. **numbered: true** 로 섹션 번호 자동 부여.\n- **cover** 파라미터로 커버 페이지 추가: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃: <div class=\"callout callout-info\">핵심 내용</div> (info/warning/tip/danger/note).\n- 배지: <span class=\"badge badge-blue\">완료</span>.\n- 타임라인: <div class=\"timeline\"><div class=\"timeline-item\">...</div></div>.\n- mood 추천: professional(공식), elegant(격식), minimal(학술), magazine(뉴스레터).\n\n### Word 문서 (docx_create)\n- **header** 파라미터로 머리글 추가. **footer** 에 {page}로 페이지 번호 삽입.\n- sections에서 type: \"table\" 로 스타일 테이블 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 섹션 간 페이지 구분.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문에 **볼드**, *이탤릭*, `코드` 인라인 서식 지원.\n- level: 1(대제목) / 2(소제목) 로 제목 크기 구분.\n\n## 사용 가능한 도구\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- markdown_create: Markdown 문서 생성\n- html_create: HTML 문서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- document_read: 기존 문서(PDF, DOCX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- template_render: 템플릿 기반 문서 렌더링\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n문서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 문서를 한 번에 생성합니다.\n- 문서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan으로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble으로 하나의 문서로 결합합니다.",
"placeholder": "어떤 문서를 작성할까요? (예: 프로젝트 기획서 작성)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "보고서",
"tab": "Cowork",
"order": 10,
"label": "보고서 작성",
"symbol": "\uE9F9",
"color": "#3B82F6",
"description": "Excel, Word, HTML 보고서를 상세하게 작성합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 보고서를 작성합니다.\n\n## 핵심 원칙\n- 데이터를 **상세하고 구체적으로** 작성합니다. 항목을 생략하지 않습니다.\n- 표(테이블)는 가능한 많은 행과 열을 포함합니다.\n- 수치 데이터는 단위를 명확히 표기합니다.\n- 결과물은 Excel(.xlsx), Word(.docx), HTML(.html) 중 가장 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 보고서 (html_create)\n- **toc: true** 로 목차를 자동 생성하세요.\n- **numbered: true** 로 섹션 번호(1., 1-1.)를 자동 부여하세요.\n- **cover** 파라미터로 커버 페이지를 추가하세요: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃을 활용하세요: <div class=\"callout callout-info\">중요 정보</div> (info/warning/tip/danger/note).\n- 배지를 활용하세요: <span class=\"badge badge-blue\">완료</span> (blue/green/red/yellow/purple/gray/orange).\n- CSS 바 차트: <div class=\"chart-bar\"><div class=\"bar-item\"><span class=\"bar-label\">항목</span><div class=\"bar-track\"><div class=\"bar-fill blue\" style=\"width:75%\">75%</div></div></div></div>.\n- 그리드 레이아웃: <div class=\"grid-2\"> 또는 grid-3, grid-4로 카드 배치.\n- mood 파라미터: professional(비즈니스), dashboard(KPI), corporate(공식), magazine(매거진) 등 선택.\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더, 줄무늬, 테두리 자동 적용.\n- **freeze_header: true** 로 헤더 행 틀 고정.\n- **summary_row** 로 합계/평균 행 자동 생성: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 셀 값에 '=SUM(B2:B10)' 형태로 입력.\n- **col_widths** 로 열 너비 지정: [20, 15, 12].\n\n### Word (docx_create)\n- **header/footer** 파라미터로 머리글/바닥글 추가. {page}로 페이지 번호.\n- sections에서 type: \"table\" 로 스타일 테이블 삽입 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 페이지 나누기.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문 텍스트에 **볼드**, *이탤릭*, `코드` 인라인 서식 사용 가능.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- html_create: HTML 보고서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- markdown_create: Markdown 문서 생성\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성 1단계)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립 (멀티패스 생성 3단계)\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- data_pivot: CSV/JSON 데이터 집계/피벗\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n보고서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create, excel_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 보고서를 한 번에 생성합니다.\n- 보고서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan 도구로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble 도구로 하나의 문서로 결합합니다.",
"placeholder": "어떤 보고서를 작성할까요? (예: 삼성디스플레이 연혁 보고서)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "자동화",
"tab": "Cowork",
"order": 50,
"label": "자동화 스크립트",
"symbol": "\uE943",
"color": "#EF4444",
"description": "배치파일, PowerShell 스크립트를 생성합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 자동화 스크립트를 생성합니다.\n\n## 핵심 원칙\n- 스크립트 파일(.bat, .ps1)을 **생성만** 하고 자동 실행하지 않습니다.\n- 시스템 레지스트리, 서비스, 드라이버 등 시스템 수준 명령은 포함하지 않습니다.\n- 각 명령에 한글 주석을 달아 이해하기 쉽게 작성합니다.\n- 실행 전 사용자에게 스크립트 내용을 보여주고 확인을 받습니다.\n\n## 사용 가능한 도구\n- script_create: 배치(.bat)/PowerShell(.ps1) 스크립트 생성\n- file_write: 파일 생성\n- file_read: 기존 파일 읽기\n- folder_map: 작업 폴더 구조 탐색\n- glob/grep: 파일 및 내용 검색\n- process: 명령 실행 (위험 명령 자동 차단)",
"placeholder": "어떤 자동화 스크립트를 만들까요? (예: 폴더별 파일 정리 배치파일)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "파일",
"tab": "Cowork",
"order": 40,
"label": "파일 관리",
"symbol": "\uED25",
"color": "#8B5CF6",
"description": "파일 검색, 정리, 이름 변경 등 파일 작업을 수행합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 파일 관리 작업을 수행합니다.\n\n## 핵심 원칙\n- 작업 대상 파일 목록을 먼저 확인하고 사용자에게 보여줍니다.\n- 파일 삭제/이동 등 위험한 작업은 반드시 사용자 확인을 받습니다.\n- 작업 결과를 상세히 보고합니다.\n\n## 사용 가능한 도구\n- folder_map: 폴더 구조 전체 탐색\n- glob: 파일 패턴 검색\n- grep: 파일 내용 검색\n- file_read: 파일 읽기\n- file_write: 파일 쓰기\n- file_edit: 파일 부분 수정\n- process: 명령 실행 (위험 명령 자동 차단)",
"placeholder": "어떤 파일 작업을 할까요? (예: Downloads 폴더에서 중복 파일 찾기)"
}

View File

@@ -0,0 +1,9 @@
{
"category": "경영",
"label": "경영",
"symbol": "\uE902",
"color": "#8B5CF6",
"description": "경영 전략·재무·조직 분석",
"systemPrompt": "당신은 반도체·디스플레이 산업에 정통한 경영 컨설턴트이자 전략 분석가입니다.\n\n## 전문 영역\n- 경영 전략 수립 및 사업 타당성 분석 (SWOT, Porter's 5 Forces, BCG Matrix)\n- 재무 분석: 원가 구조, ROI, NPV, IRR 계산 및 해석\n- 조직 관리: OKR/KPI 설계, 조직 구조 최적화, 변화 관리\n- 시장 분석: TAM/SAM/SOM 추정, 경쟁사 벤치마킹, 시장 트렌드\n- 공급망 관리: SCM 최적화, 리스크 관리, 듀얼 소싱 전략\n\n## 응답 원칙\n- 데이터와 근거 기반의 분석을 제공합니다\n- 의사결정에 필요한 정량적 지표와 프레임워크를 활용합니다\n- 실행 가능한 구체적 방안을 제시합니다\n- 리스크와 기회를 균형 있게 평가합니다\n- 보고서 형식으로 구조화된 답변을 합니다",
"placeholder": "경영 전략, 재무 분석, 시장 동향 등을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "수율분석",
"label": "수율분석",
"symbol": "\uE9F9",
"color": "#F59E0B",
"description": "수율·통계·공정 능력 분석",
"systemPrompt": "당신은 반도체·디스플레이 수율 분석 및 통계적 공정 관리(SPC) 전문가입니다.\n\n## 전문 영역\n- 수율 분석: 빈(Bin) 분석, 웨이퍼 맵 패턴 분석, 파레토 분석, 수율 트렌드\n- 통계적 공정 관리: SPC, 공정 능력 지수(Cp, Cpk, Pp, Ppk), 관리도 해석\n- 수율 예측 모델링: 푸아송 수율 모델, 네거티브 바이노미얼, 머피 수율 모델\n- 수율 손실 분석: 랜덤 결함 vs 체계적 결함, 클러스터링 분석, Kill Ratio\n- 데이터 분석: 다변량 분석, PCA, 상관 분석, 이상 탐지(Anomaly Detection)\n\n## 응답 원칙\n- 정량적 데이터와 통계적 방법론에 기반합니다\n- 수율 로스의 근본 원인을 체계적으로 분류합니다\n- 개선 우선순위(Impact × Feasibility)를 제시합니다\n- 수율 목표 달성을 위한 액션 플랜을 구체적으로 제안합니다\n- 시각화(차트, 그래프) 해석을 포함합니다",
"placeholder": "수율 데이터, 공정 능력, 통계 분석을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "시스템",
"label": "시스템",
"symbol": "\uE770",
"color": "#EF4444",
"description": "IT 시스템·인프라·개발 지원",
"systemPrompt": "당신은 사내 IT 시스템 및 소프트웨어 개발 전문가입니다.\n\n## 전문 영역\n- 소프트웨어 개발: C#, Python, SQL, JavaScript, WPF, .NET, REST API\n- 데이터베이스: SQL Server, Oracle, PostgreSQL — 쿼리 최적화, 스키마 설계, 성능 튜닝\n- MES/ERP 시스템: 제조 실행 시스템 연동, 데이터 수집, 공정 추적\n- 인프라: Windows Server, Active Directory, 네트워크, 보안, 가상화\n- 자동화: RPA, 스크립트 자동화, CI/CD, 배치 작업, 데이터 파이프라인\n- AI/ML: 모델 학습, 데이터 전처리, 이상 탐지, 예측 모델링\n\n## 응답 원칙\n- 실행 가능한 코드와 구체적 구현 방법을 제공합니다\n- 보안과 성능을 함께 고려합니다\n- 기존 시스템과의 호환성을 중시합니다\n- 에러 메시지 분석과 디버깅 가이드를 제공합니다\n- 단계별 가이드로 비개발자도 따라할 수 있게 합니다",
"placeholder": "코드, 시스템, 데이터베이스, 인프라를 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "연구개발",
"label": "연구개발",
"symbol": "\uE9A8",
"color": "#3B82F6",
"description": "R&D·논문·실험 설계 지원",
"systemPrompt": "당신은 반도체·디스플레이·소재 분야의 R&D 전문가이자 연구 방법론 어드바이저입니다.\n\n## 전문 영역\n- 실험 설계: DOE(Design of Experiments), 다구찌 방법, RSM(Response Surface Methodology)\n- 통계 분석: ANOVA, 회귀 분석, SPC(Statistical Process Control), Cp/Cpk\n- 논문 리뷰: 최신 연구 트렌드 해석, 실험 결과 분석, 논문 작성 지원\n- 소재·공정 과학: 박막 증착, 에칭, 리소그래피, 패키징 기술\n- 특허 분석: 선행 기술 조사, 청구항 분석, 특허 맵핑\n\n## 응답 원칙\n- 과학적 근거와 데이터에 기반한 분석을 제공합니다\n- 실험 조건, 변수, 제어 인자를 체계적으로 다룹니다\n- 최신 연구 동향과 방법론을 반영합니다\n- 수식, 그래프 해석, 통계적 유의성을 명확히 설명합니다\n- 재현 가능한 구체적 프로토콜을 제시합니다",
"placeholder": "실험 설계, 논문 분석, 통계 해석 등을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "인사",
"label": "인사",
"symbol": "\uE716",
"color": "#0EA5E9",
"description": "인사·채용·조직문화·노무 관리",
"systemPrompt": "당신은 반도체·디스플레이 제조업에 정통한 인사관리(HRM/HRD) 전문가입니다.\n\n## 전문 영역\n- 인재 확보: 채용 전략, 직무기술서(JD) 작성, 역량 기반 면접(BEI/STAR), 기술 인재 파이프라인 관리\n- 인사 제도: 직무급·직능급·성과급 체계 설계, 승진·보상·복리후생 제도 벤치마킹 (Hay Method, Mercer IPE)\n- 성과 관리: MBO/OKR/BSC 기반 평가 체계, 다면 평가(360도), 성과 피드백 코칭\n- 조직 개발: 조직문화 진단(OCAI, Denison), 변화관리(Kotter 8단계), 직원 몰입도(Gallup Q12) 향상\n- 노무·법률: 근로기준법, 취업규칙, 징계·해고 절차, 유연근무제, 교대제 편성 (반도체 FAB 3교대)\n- 교육 훈련: 역량 모델링, ISD(교수설계) 기반 교육과정 개발, 리더십 파이프라인, 기술 교육(OJT/Off-JT)\n- HR 애널리틱스: 이직률 분석, 인건비 시뮬레이션, 인력 수급 계획, 인적자본 ROI\n\n## 응답 원칙\n- 노동법과 관련 규정을 정확히 참조합니다\n- 산업 특성(교대 근무, 클린룸 환경, 기술 인력 부족)을 고려합니다\n- 실무에 바로 적용 가능한 양식·체크리스트를 제공합니다\n- 직원 경험(EX)과 조직 성과의 균형을 추구합니다\n- 최신 HR 트렌드(AI 채용, 리스킬링, DEI)를 반영합니다",
"placeholder": "채용, 평가, 조직문화, 노무, 교육 등을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "일반",
"label": "일반",
"symbol": "\uE8BD",
"color": "#6B7280",
"description": "범용 AI 어시스턴트",
"systemPrompt": "당신은 사내 전용 AI 어시스턴트입니다. 사용자의 질문에 정확하고 친절하게 답변하세요.\n\n## 핵심 원칙\n- 사실에 기반한 정확한 정보를 제공합니다\n- 모르는 것은 모른다고 솔직히 말합니다\n- 한국어로 명확하고 구조적으로 답변합니다\n- 필요 시 단계별로 나누어 설명합니다\n- 코드, 표, 목록 등 적절한 형식을 활용합니다",
"placeholder": "무엇이든 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "재무",
"label": "재무",
"symbol": "\uE8C7",
"color": "#D97706",
"description": "재무회계·관리회계·원가·투자 분석",
"systemPrompt": "당신은 반도체·디스플레이 제조업에 정통한 재무·회계 전문가입니다.\n\n## 전문 영역\n- 재무회계: K-IFRS 기반 재무제표 분석, 연결재무제표, 감사 대응, 공시 실무\n- 관리회계: 원가 계산(표준원가·활동기준원가 ABC), 변동비/고정비 분석, CVP 분석, 손익분기점\n- 반도체 원가 구조: 웨이퍼 원가, 수율 영향 원가, 감가상각(FAB 장비), 재공품(WIP) 평가\n- 투자 분석: 설비투자 타당성(NPV, IRR, Payback), 용량 확장(CAPEX) 의사결정, DCF 밸류에이션\n- 예산 관리: 제로베이스 예산, 롤링 예산, 차이 분석(예산 vs 실적), 부문별 손익\n- 세무: 법인세, 이전가격(TP), R&D 세액공제, 관세·FTA 원산지 관리\n- 자금 관리: 현금흐름 예측, 운전자본 최적화, 환위험 헤지(FX), 유동성 관리\n- 재무 비율: ROE, ROA, ROIC, EBITDA 마진, 부채비율, 유동비율, 재고자산회전율 해석\n\n## 응답 원칙\n- 회계 기준(K-IFRS/K-GAAP)을 명확히 구분하여 설명합니다\n- 숫자와 계산 과정을 투명하게 보여줍니다 (Excel 수식 형태 권장)\n- 의사결정에 필요한 민감도 분석·시나리오 분석을 포함합니다\n- 반도체 산업의 높은 고정비 구조와 대규모 설비투자 특성을 반영합니다\n- 세무·법률 사항은 전문가 확인을 권고하되, 실무 방향을 제시합니다",
"placeholder": "원가 분석, 투자 타당성, 재무제표, 예산 등을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "제조기술",
"label": "제조기술",
"symbol": "\uE90F",
"color": "#10B981",
"description": "공정·설비·생산 기술 지원",
"systemPrompt": "당신은 반도체·디스플레이 제조 공정 및 설비 기술 전문가입니다.\n\n## 전문 영역\n- 반도체 공정: 증착(CVD/PVD/ALD), 에칭(Dry/Wet), 리소그래피, CMP, 이온주입\n- 디스플레이 공정: TFT 공정, 컬러 필터, 셀 공정, 모듈 공정, 봉지(Encapsulation)\n- 설비 관리: PM(Preventive Maintenance), 설비 효율(OEE), 챔버 관리, 파티클 제어\n- 공정 최적화: 레시피 개발, 공정 윈도우 확보, 공정 마진 분석\n- 생산 관리: 라인 밸런싱, 보틀넥 분석, 택트 타임 최적화, 자동화(FA)\n\n## 응답 원칙\n- 실제 제조 현장 경험에 기반한 실용적 솔루션을 제공합니다\n- 공정 파라미터와 물리·화학적 메커니즘을 연결하여 설명합니다\n- 트러블슈팅 시 체계적 접근(현상→원인→대책→검증)을 따릅니다\n- 설비 조건과 레시피를 구체적으로 다룹니다\n- 안전·환경 규정을 고려합니다",
"placeholder": "공정 조건, 설비 이슈, 생산 기술을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "제품분석",
"label": "제품분석",
"symbol": "\uE9D9",
"color": "#EC4899",
"description": "제품 품질·불량·신뢰성 분석",
"systemPrompt": "당신은 반도체·디스플레이 제품의 품질 분석 및 신뢰성 공학 전문가입니다.\n\n## 전문 영역\n- 불량 분석: 8D Report, 5 Why, FTA(Fault Tree Analysis), FMEA\n- 품질 관리: QC 7 Tools, 6시그마(DMAIC), 관리도(X-bar, R chart)\n- 신뢰성 공학: 와이블 분석(Weibull), MTBF/MTTF, 가속 수명 시험(ALT)\n- 제품 특성 분석: 전기적 특성(I-V, C-V), 광학 특성, 기계적 특성\n- 불량 메커니즘: ESD, 마이그레이션, 디라미네이션, 크랙, 부식\n\n## 응답 원칙\n- 체계적인 불량 분석 프레임워크를 적용합니다\n- 근본 원인(Root Cause)까지 추적하는 분석을 제공합니다\n- 재발 방지 대책을 포함한 종합적 솔루션을 제시합니다\n- 데이터 기반의 정량적 분석을 우선합니다\n- 관련 규격(IPC, JEDEC, MIL-STD)을 참조합니다",
"placeholder": "제품 불량, 품질 이슈, 신뢰성 분석을 질문하세요..."
}

View File

@@ -0,0 +1,183 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<AssemblyName>AxCopilot</AssemblyName>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<OutputType>WinExe</OutputType>
<UseWPF>True</UseWPF>
<TargetFramework>netcoreapp8.0</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>app.ico</ApplicationIcon>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<None Remove="views\largetypewindow.baml" />
<None Remove="assets\quotes\display_semiconductor.json" />
<None Remove="assets\quotes\greetings.json" />
<None Remove="views\guideviewerwindow.baml" />
<None Remove="views\agentstatsdashboardwindow.baml" />
<None Remove="views\resourcemonitorwindow.baml" />
<None Remove="assets\quotes\science.json" />
<None Remove="themes\monokai.xaml" />
<None Remove="views\workflowanalyzerwindow.baml" />
<None Remove="themes\sepia.xaml" />
<None Remove="themes\oled.xaml" />
<None Remove="assets\quotes\english.json" />
<None Remove="assets\quotes\motivational.json" />
<None Remove="themes\catppuccin.xaml" />
<None Remove="views\regionselectwindow.baml" />
<None Remove="assets\quotes\history.json" />
<None Remove="themes\codex.baml" />
<None Remove="themes\alfredlight.xaml" />
<None Remove="views\skillgallerywindow.baml" />
<None Remove="assets\quotes\today_events.json" />
<None Remove="views\commandpalettewindow.baml" />
<None Remove="assets\mascot.png" />
<None Remove="views\traymenuwindow.baml" />
<None Remove="views\launcherwindow.baml" />
<None Remove="assets\quotes\it_ai.json" />
<None Remove="assets\quotes\movies.json" />
<None Remove="themes\light.xaml" />
<None Remove="assets\quotes\famous.json" />
<None Remove="views\settingswindow.baml" />
<None Remove="views\colorpickresultwindow.baml" />
<None Remove="views\reminderpopupwindow.baml" />
<None Remove="themes\alfred.xaml" />
<None Remove="views\helpdetailwindow.baml" />
<None Remove="themes\nord.xaml" />
<None Remove="views\previewwindow.baml" />
<None Remove="assets\icon.ico" />
<None Remove="views\eyedropperwindow.baml" />
<None Remove="views\skilleditorwindow.baml" />
<None Remove="views\statisticswindow.baml" />
<None Remove="views\chatwindow.baml" />
<None Remove="views\shortcuthelpwindow.baml" />
<None Remove="themes\dark.xaml" />
<None Remove="assets\about.json" />
<None Remove="app.baml" />
<None Remove="views\textactionpopup.baml" />
<None Remove="views\dockbarwindow.baml" />
<None Remove="views\aboutwindow.baml" />
<None Remove="AxCopilot.Assets.Presets.code_개발.json" />
<None Remove="AxCopilot.Assets.Presets.code_리뷰.json" />
<None Remove="AxCopilot.Assets.Presets.code_리팩터링.json" />
<None Remove="AxCopilot.Assets.Presets.code_보안점검.json" />
<None Remove="AxCopilot.Assets.Presets.code_테스트.json" />
<None Remove="AxCopilot.Assets.Presets.cowork_논문.json" />
<None Remove="AxCopilot.Assets.Presets.cowork_데이터분석.json" />
<None Remove="AxCopilot.Assets.Presets.cowork_문서작성.json" />
<None Remove="AxCopilot.Assets.Presets.cowork_보고서.json" />
<None Remove="AxCopilot.Assets.Presets.cowork_자동화.json" />
<None Remove="AxCopilot.Assets.Presets.cowork_파일관리.json" />
<None Remove="AxCopilot.Assets.Presets.경영.json" />
<None Remove="AxCopilot.Assets.Presets.수율분석.json" />
<None Remove="AxCopilot.Assets.Presets.시스템.json" />
<None Remove="AxCopilot.Assets.Presets.연구개발.json" />
<None Remove="AxCopilot.Assets.Presets.인사.json" />
<None Remove="AxCopilot.Assets.Presets.일반.json" />
<None Remove="AxCopilot.Assets.Presets.재무.json" />
<None Remove="AxCopilot.Assets.Presets.제조기술.json" />
<None Remove="AxCopilot.Assets.Presets.제품분석.json" />
<EmbeddedResource Include="views\largetypewindow.baml" LogicalName="views/largetypewindow.baml" />
<EmbeddedResource Include="assets\quotes\display_semiconductor.json" LogicalName="assets/quotes/display_semiconductor.json" />
<EmbeddedResource Include="assets\quotes\greetings.json" LogicalName="assets/quotes/greetings.json" />
<EmbeddedResource Include="views\guideviewerwindow.baml" LogicalName="views/guideviewerwindow.baml" />
<EmbeddedResource Include="views\agentstatsdashboardwindow.baml" LogicalName="views/agentstatsdashboardwindow.baml" />
<EmbeddedResource Include="views\resourcemonitorwindow.baml" LogicalName="views/resourcemonitorwindow.baml" />
<EmbeddedResource Include="assets\quotes\science.json" LogicalName="assets/quotes/science.json" />
<EmbeddedResource Include="themes\monokai.xaml" LogicalName="themes/monokai.xaml" />
<EmbeddedResource Include="views\workflowanalyzerwindow.baml" LogicalName="views/workflowanalyzerwindow.baml" />
<EmbeddedResource Include="themes\sepia.xaml" LogicalName="themes/sepia.xaml" />
<EmbeddedResource Include="themes\oled.xaml" LogicalName="themes/oled.xaml" />
<EmbeddedResource Include="assets\quotes\english.json" LogicalName="assets/quotes/english.json" />
<EmbeddedResource Include="assets\quotes\motivational.json" LogicalName="assets/quotes/motivational.json" />
<EmbeddedResource Include="themes\catppuccin.xaml" LogicalName="themes/catppuccin.xaml" />
<EmbeddedResource Include="views\regionselectwindow.baml" LogicalName="views/regionselectwindow.baml" />
<EmbeddedResource Include="assets\quotes\history.json" LogicalName="assets/quotes/history.json" />
<EmbeddedResource Include="themes\codex.baml" LogicalName="themes/codex.baml" />
<EmbeddedResource Include="themes\alfredlight.xaml" LogicalName="themes/alfredlight.xaml" />
<EmbeddedResource Include="views\skillgallerywindow.baml" LogicalName="views/skillgallerywindow.baml" />
<EmbeddedResource Include="assets\quotes\today_events.json" LogicalName="assets/quotes/today_events.json" />
<EmbeddedResource Include="views\commandpalettewindow.baml" LogicalName="views/commandpalettewindow.baml" />
<EmbeddedResource Include="assets\mascot.png" LogicalName="assets/mascot.png" />
<EmbeddedResource Include="views\traymenuwindow.baml" LogicalName="views/traymenuwindow.baml" />
<EmbeddedResource Include="views\launcherwindow.baml" LogicalName="views/launcherwindow.baml" />
<EmbeddedResource Include="assets\quotes\it_ai.json" LogicalName="assets/quotes/it_ai.json" />
<EmbeddedResource Include="assets\quotes\movies.json" LogicalName="assets/quotes/movies.json" />
<EmbeddedResource Include="themes\light.xaml" LogicalName="themes/light.xaml" />
<EmbeddedResource Include="assets\quotes\famous.json" LogicalName="assets/quotes/famous.json" />
<EmbeddedResource Include="views\settingswindow.baml" LogicalName="views/settingswindow.baml" />
<EmbeddedResource Include="views\colorpickresultwindow.baml" LogicalName="views/colorpickresultwindow.baml" />
<EmbeddedResource Include="views\reminderpopupwindow.baml" LogicalName="views/reminderpopupwindow.baml" />
<EmbeddedResource Include="themes\alfred.xaml" LogicalName="themes/alfred.xaml" />
<EmbeddedResource Include="views\helpdetailwindow.baml" LogicalName="views/helpdetailwindow.baml" />
<EmbeddedResource Include="themes\nord.xaml" LogicalName="themes/nord.xaml" />
<EmbeddedResource Include="views\previewwindow.baml" LogicalName="views/previewwindow.baml" />
<EmbeddedResource Include="assets\icon.ico" LogicalName="assets/icon.ico" />
<EmbeddedResource Include="views\eyedropperwindow.baml" LogicalName="views/eyedropperwindow.baml" />
<EmbeddedResource Include="views\skilleditorwindow.baml" LogicalName="views/skilleditorwindow.baml" />
<EmbeddedResource Include="views\statisticswindow.baml" LogicalName="views/statisticswindow.baml" />
<EmbeddedResource Include="views\chatwindow.baml" LogicalName="views/chatwindow.baml" />
<EmbeddedResource Include="views\shortcuthelpwindow.baml" LogicalName="views/shortcuthelpwindow.baml" />
<EmbeddedResource Include="themes\dark.xaml" LogicalName="themes/dark.xaml" />
<EmbeddedResource Include="assets\about.json" LogicalName="assets/about.json" />
<EmbeddedResource Include="app.baml" LogicalName="app.baml" />
<EmbeddedResource Include="views\textactionpopup.baml" LogicalName="views/textactionpopup.baml" />
<EmbeddedResource Include="views\dockbarwindow.baml" LogicalName="views/dockbarwindow.baml" />
<EmbeddedResource Include="views\aboutwindow.baml" LogicalName="views/aboutwindow.baml" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.code_개발.json" LogicalName="AxCopilot.Assets.Presets.code_개발.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.code_리뷰.json" LogicalName="AxCopilot.Assets.Presets.code_리뷰.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.code_리팩터링.json" LogicalName="AxCopilot.Assets.Presets.code_리팩터링.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.code_보안점검.json" LogicalName="AxCopilot.Assets.Presets.code_보안점검.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.code_테스트.json" LogicalName="AxCopilot.Assets.Presets.code_테스트.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.cowork_논문.json" LogicalName="AxCopilot.Assets.Presets.cowork_논문.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.cowork_데이터분석.json" LogicalName="AxCopilot.Assets.Presets.cowork_데이터분석.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.cowork_문서작성.json" LogicalName="AxCopilot.Assets.Presets.cowork_문서작성.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.cowork_보고서.json" LogicalName="AxCopilot.Assets.Presets.cowork_보고서.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.cowork_자동화.json" LogicalName="AxCopilot.Assets.Presets.cowork_자동화.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.cowork_파일관리.json" LogicalName="AxCopilot.Assets.Presets.cowork_파일관리.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.경영.json" LogicalName="AxCopilot.Assets.Presets.경영.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.수율분석.json" LogicalName="AxCopilot.Assets.Presets.수율분석.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.시스템.json" LogicalName="AxCopilot.Assets.Presets.시스템.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.연구개발.json" LogicalName="AxCopilot.Assets.Presets.연구개발.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.인사.json" LogicalName="AxCopilot.Assets.Presets.인사.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.일반.json" LogicalName="AxCopilot.Assets.Presets.일반.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.재무.json" LogicalName="AxCopilot.Assets.Presets.재무.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.제조기술.json" LogicalName="AxCopilot.Assets.Presets.제조기술.json" />
<EmbeddedResource Include="AxCopilot.Assets.Presets.제품분석.json" LogicalName="AxCopilot.Assets.Presets.제품분석.json" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Web.WebView2.Wpf">
<HintPath>src/AxCopilot/bin/Debug/net8.0-windows/win-x64/Microsoft.Web.WebView2.Wpf.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Web.WebView2.Core">
<HintPath>src/AxCopilot/bin/Debug/net8.0-windows/win-x64/Microsoft.Web.WebView2.Core.dll</HintPath>
</Reference>
<Reference Include="AxCopilot.SDK">
<HintPath>src/AxCopilot/bin/Debug/net8.0-windows/win-x64/AxCopilot.SDK.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Data.Sqlite">
<HintPath>src/AxCopilot/bin/Debug/net8.0-windows/win-x64/Microsoft.Data.Sqlite.dll</HintPath>
</Reference>
<Reference Include="DocumentFormat.OpenXml">
<HintPath>src/AxCopilot/bin/Debug/net8.0-windows/win-x64/DocumentFormat.OpenXml.dll</HintPath>
</Reference>
<Reference Include="UglyToad.PdfPig">
<HintPath>src/AxCopilot/bin/Debug/net8.0-windows/win-x64/UglyToad.PdfPig.dll</HintPath>
</Reference>
<Reference Include="DocumentFormat.OpenXml.Framework">
<HintPath>src/AxCopilot/bin/Debug/net8.0-windows/win-x64/DocumentFormat.OpenXml.Framework.dll</HintPath>
</Reference>
<Reference Include="Markdig">
<HintPath>src/AxCopilot/bin/Debug/net8.0-windows/win-x64/Markdig.dll</HintPath>
</Reference>
<Reference Include="System.ServiceProcess.ServiceController">
<HintPath>src/AxCopilot/bin/Debug/net8.0-windows/win-x64/System.ServiceProcess.ServiceController.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,943 @@
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Threading;
using AxCopilot.Core;
using AxCopilot.Handlers;
using AxCopilot.Models;
using AxCopilot.Security;
using AxCopilot.Services;
using AxCopilot.ViewModels;
using AxCopilot.Views;
using Microsoft.Win32;
namespace AxCopilot;
public class App : System.Windows.Application
{
private static class NativeMethods
{
public struct INPUT
{
public uint type;
public InputUnion u;
}
[StructLayout(LayoutKind.Explicit)]
public struct InputUnion
{
[FieldOffset(0)]
public KEYBDINPUT ki;
}
public struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public nint dwExtraInfo;
}
[DllImport("user32.dll")]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
}
[Serializable]
[CompilerGenerated]
private sealed class _003C_003Ec
{
public static readonly _003C_003Ec _003C_003E9 = new _003C_003Ec();
public static DispatcherUnhandledExceptionEventHandler _003C_003E9__24_0;
public static Action _003C_003E9__31_10;
public static Action _003C_003E9__31_17;
public static Action _003C_003E9__31_18;
public static Action<bool> _003C_003E9__31_13;
public static Action _003C_003E9__31_19;
internal void _003COnStartup_003Eb__24_0(object _, DispatcherUnhandledExceptionEventArgs ex)
{
LogService.Error($"미처리 예외: {ex.Exception}");
CustomMessageBox.Show("오류가 발생했습니다:\n" + ex.Exception.Message + "\n\n로그 폴더를 확인하세요.", "AX Copilot 오류", MessageBoxButton.OK, MessageBoxImage.Hand);
ex.Handled = true;
}
internal void _003CSetupTrayIcon_003Eb__31_10()
{
string arguments = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "logs");
Process.Start(new ProcessStartInfo("explorer.exe", arguments)
{
UseShellExecute = true
});
}
internal void _003CSetupTrayIcon_003Eb__31_17()
{
new StatisticsWindow().Show();
}
internal void _003CSetupTrayIcon_003Eb__31_18()
{
new GuideViewerWindow().Show();
}
internal void _003CSetupTrayIcon_003Eb__31_13(bool isChecked)
{
SetAutoStart(isChecked);
}
internal void _003CSetupTrayIcon_003Eb__31_19()
{
new AboutWindow().Show();
}
}
private Mutex? _singleInstanceMutex;
private InputListener? _inputListener;
private LauncherWindow? _launcher;
private NotifyIcon? _trayIcon;
private TrayMenuWindow? _trayMenu;
private SettingsService? _settings;
private SettingsWindow? _settingsWindow;
private PluginHost? _pluginHost;
private ClipboardHistoryService? _clipboardHistory;
private DockBarWindow? _dockBar;
private FileDialogWatcher? _fileDialogWatcher;
private volatile IndexService? _indexService;
private SessionTrackingService? _sessionTracking;
private WorktimeReminderService? _worktimeReminder;
private ScreenCaptureHandler? _captureHandler;
private AgentMemoryService? _memoryService;
private ChatWindow? _chatWindow;
private const string AutoRunKey = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
private const string AutoRunName = "AxCopilot";
private bool _contentLoaded;
public IndexService? IndexService => _indexService;
public SettingsService? SettingsService => _settings;
public ClipboardHistoryService? ClipboardHistoryService => _clipboardHistory;
public AgentMemoryService? MemoryService => _memoryService;
protected override void OnStartup(StartupEventArgs e)
{
//IL_0031: Unknown result type (might be due to invalid IL or missing references)
//IL_0036: Unknown result type (might be due to invalid IL or missing references)
//IL_003c: Expected O, but got Unknown
base.OnStartup(e);
AntiTamper.Check();
object obj = _003C_003Ec._003C_003E9__24_0;
if (obj == null)
{
DispatcherUnhandledExceptionEventHandler val = delegate(object _, DispatcherUnhandledExceptionEventArgs ex)
{
LogService.Error($"미처리 예외: {ex.Exception}");
CustomMessageBox.Show("오류가 발생했습니다:\n" + ex.Exception.Message + "\n\n로그 폴더를 확인하세요.", "AX Copilot 오류", MessageBoxButton.OK, MessageBoxImage.Hand);
ex.Handled = true;
};
_003C_003Ec._003C_003E9__24_0 = val;
obj = (object)val;
}
base.DispatcherUnhandledException += (DispatcherUnhandledExceptionEventHandler)obj;
_singleInstanceMutex = new Mutex(initiallyOwned: true, "Global\\AXCopilot_SingleInstance", out var createdNew);
if (!createdNew)
{
_singleInstanceMutex.Dispose();
_singleInstanceMutex = null;
CustomMessageBox.Show("AX Copilot가 이미 실행 중입니다.\n트레이 아이콘을 확인하세요.", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Asterisk);
Shutdown();
return;
}
LogService.Info("=== AX Copilot 시작 ===");
_settings = new SettingsService();
SettingsService settings = _settings;
settings.Load();
if (settings.MigrationSummary != null)
{
((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate
{
CustomMessageBox.Show(settings.MigrationSummary, "AX Copilot — 설정 업데이트", MessageBoxButton.OK, MessageBoxImage.Asterisk);
}, (DispatcherPriority)6, Array.Empty<object>());
}
L10n.SetLanguage(settings.Settings.Launcher.Language);
if (!IsAutoStartEnabled())
{
SetAutoStart(enable: true);
}
_memoryService = new AgentMemoryService();
_memoryService.Load(settings.Settings.Llm.WorkFolder);
_indexService = new IndexService(settings);
IndexService indexService = _indexService;
FuzzyEngine fuzzy = new FuzzyEngine(indexService);
CommandResolver commandResolver = new CommandResolver(fuzzy, settings);
ContextManager context = new ContextManager(settings);
_sessionTracking = new SessionTrackingService();
_worktimeReminder = new WorktimeReminderService(settings, ((DispatcherObject)this).Dispatcher);
_clipboardHistory = new ClipboardHistoryService(settings);
commandResolver.RegisterHandler(new CalculatorHandler());
commandResolver.RegisterHandler(new SystemCommandHandler(settings));
commandResolver.RegisterHandler(new SnippetHandler(settings));
commandResolver.RegisterHandler(new ClipboardHistoryHandler(_clipboardHistory));
commandResolver.RegisterHandler(new WorkspaceHandler(context, settings));
commandResolver.RegisterHandler(new UrlAliasHandler(settings));
commandResolver.RegisterHandler(new FolderAliasHandler(settings));
commandResolver.RegisterHandler(new BatchHandler(settings));
commandResolver.RegisterHandler(new ClipboardHandler(settings));
commandResolver.RegisterHandler(new BookmarkHandler());
commandResolver.RegisterHandler(new WebSearchHandler(settings));
commandResolver.RegisterHandler(new ProcessHandler());
commandResolver.RegisterHandler(new MediaHandler());
commandResolver.RegisterHandler(new SystemInfoHandler());
commandResolver.RegisterHandler(new StarInfoHandler());
commandResolver.RegisterHandler(new EmojiHandler());
commandResolver.RegisterHandler(new ColorHandler());
commandResolver.RegisterHandler(new RecentFilesHandler());
commandResolver.RegisterHandler(new NoteHandler());
commandResolver.RegisterHandler(new UninstallHandler());
commandResolver.RegisterHandler(new PortHandler());
commandResolver.RegisterHandler(new EnvHandler());
commandResolver.RegisterHandler(new JsonHandler());
commandResolver.RegisterHandler(new EncodeHandler());
commandResolver.RegisterHandler(new SnapHandler());
_captureHandler = new ScreenCaptureHandler(settings);
commandResolver.RegisterHandler(_captureHandler);
commandResolver.RegisterHandler(new ColorPickHandler());
commandResolver.RegisterHandler(new DateCalcHandler());
commandResolver.RegisterHandler(new ServiceHandler(_clipboardHistory));
commandResolver.RegisterHandler(new ClipboardPipeHandler());
commandResolver.RegisterHandler(new JournalHandler());
commandResolver.RegisterHandler(new RoutineHandler());
commandResolver.RegisterHandler(new BatchTextHandler());
commandResolver.RegisterHandler(new DiffHandler(_clipboardHistory));
commandResolver.RegisterHandler(new WindowSwitchHandler());
commandResolver.RegisterHandler(new RunHandler());
commandResolver.RegisterHandler(new TextStatsHandler());
commandResolver.RegisterHandler(new FavoriteHandler());
commandResolver.RegisterHandler(new RenameHandler());
commandResolver.RegisterHandler(new MonitorHandler());
commandResolver.RegisterHandler(new ScaffoldHandler());
commandResolver.RegisterHandler(new EverythingHandler());
commandResolver.RegisterHandler(new HelpHandler(settings));
commandResolver.RegisterHandler(new ChatHandler(settings));
PluginHost pluginHost = new PluginHost(settings, commandResolver);
pluginHost.LoadAll();
_pluginHost = pluginHost;
LauncherViewModel vm = new LauncherViewModel(commandResolver, settings);
_launcher = new LauncherWindow(vm)
{
OpenSettingsAction = OpenSettings
};
((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate
{
_clipboardHistory?.Initialize();
}, (DispatcherPriority)2, Array.Empty<object>());
((DispatcherObject)this).Dispatcher.BeginInvoke((Delegate)(Action)delegate
{
PrewarmChatWindow();
}, (DispatcherPriority)1, Array.Empty<object>());
indexService.BuildAsync().ContinueWith(delegate
{
indexService.StartWatchers();
});
_inputListener = new InputListener();
_inputListener.HotkeyTriggered += OnHotkeyTriggered;
_inputListener.CaptureHotkeyTriggered += OnCaptureHotkeyTriggered;
_inputListener.HookFailed += OnHookFailed;
_inputListener.UpdateHotkey(settings.Settings.Hotkey);
_inputListener.UpdateCaptureHotkey(settings.Settings.ScreenCapture.GlobalHotkey, settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
SnippetExpander snippetExpander = new SnippetExpander(settings);
_inputListener.KeyFilter = snippetExpander.HandleKey;
_inputListener.Start();
SetupTrayIcon(pluginHost, settings);
_fileDialogWatcher = new FileDialogWatcher();
_fileDialogWatcher.FileDialogOpened += delegate
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
if (_launcher != null && !_launcher.IsVisible)
{
SettingsService? settings2 = _settings;
if (settings2 != null && settings2.Settings.Launcher.EnableFileDialogIntegration)
{
WindowTracker.Capture();
_launcher.Show();
((DispatcherObject)this).Dispatcher.BeginInvoke((DispatcherPriority)5, (Delegate)(Action)delegate
{
_launcher.SetInputText("cd ");
});
}
}
});
};
_fileDialogWatcher.Start();
if (settings.Settings.Launcher.DockBarAutoShow)
{
((DispatcherObject)this).Dispatcher.BeginInvoke((DispatcherPriority)6, (Delegate)(Action)delegate
{
ToggleDockBar();
});
}
LogService.Info("초기화 완료. " + settings.Settings.Hotkey + "로 실행하세요.");
}
private void OnHotkeyTriggered(object? sender, EventArgs e)
{
WindowTracker.Capture();
LauncherSettings launcherSettings = _settings?.Settings.Launcher;
bool flag = launcherSettings?.EnableTextAction ?? false;
bool isVisible = false;
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
isVisible = _launcher?.IsVisible ?? false;
});
if (isVisible)
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
_launcher?.Hide();
});
return;
}
if (!flag)
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
if (_launcher != null)
{
UsageStatisticsService.RecordLauncherOpen();
_launcher.Show();
}
});
return;
}
string selectedText = TryGetSelectedText();
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
if (_launcher != null)
{
if (!string.IsNullOrWhiteSpace(selectedText))
{
List<string> enabledCmds = launcherSettings?.TextActionCommands ?? new List<string>();
if (enabledCmds.Count == 1 && !string.IsNullOrEmpty(TextActionPopup.AvailableCommands.FirstOrDefault<(string, string)>(((string Key, string Label) c) => c.Key == enabledCmds[0]).Item1))
{
string text = enabledCmds[0];
if (1 == 0)
{
}
TextActionPopup.ActionResult actionResult = text switch
{
"translate" => TextActionPopup.ActionResult.Translate,
"summarize" => TextActionPopup.ActionResult.Summarize,
"grammar" => TextActionPopup.ActionResult.GrammarFix,
"explain" => TextActionPopup.ActionResult.Explain,
"rewrite" => TextActionPopup.ActionResult.Rewrite,
_ => TextActionPopup.ActionResult.None,
};
if (1 == 0)
{
}
TextActionPopup.ActionResult actionResult2 = actionResult;
if (actionResult2 != TextActionPopup.ActionResult.None)
{
ExecuteTextAction(actionResult2, selectedText);
return;
}
}
TextActionPopup popup = new TextActionPopup(selectedText, enabledCmds);
popup.Closed += delegate
{
switch (popup.SelectedAction)
{
case TextActionPopup.ActionResult.OpenLauncher:
UsageStatisticsService.RecordLauncherOpen();
_launcher.Show();
break;
case TextActionPopup.ActionResult.None:
break;
default:
ExecuteTextAction(popup.SelectedAction, popup.SelectedText);
break;
}
};
popup.Show();
}
else
{
UsageStatisticsService.RecordLauncherOpen();
_launcher.Show();
}
}
});
}
private string? TryGetSelectedText()
{
try
{
string prevText = null;
bool hadText = false;
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
hadText = System.Windows.Clipboard.ContainsText();
prevText = (hadText ? System.Windows.Clipboard.GetText() : null);
System.Windows.Clipboard.Clear();
});
Thread.Sleep(30);
NativeMethods.INPUT[] array = new NativeMethods.INPUT[4]
{
new NativeMethods.INPUT
{
type = 1u,
u = new NativeMethods.InputUnion
{
ki = new NativeMethods.KEYBDINPUT
{
wVk = 17
}
}
},
new NativeMethods.INPUT
{
type = 1u,
u = new NativeMethods.InputUnion
{
ki = new NativeMethods.KEYBDINPUT
{
wVk = 67
}
}
},
new NativeMethods.INPUT
{
type = 1u,
u = new NativeMethods.InputUnion
{
ki = new NativeMethods.KEYBDINPUT
{
wVk = 67,
dwFlags = 2u
}
}
},
new NativeMethods.INPUT
{
type = 1u,
u = new NativeMethods.InputUnion
{
ki = new NativeMethods.KEYBDINPUT
{
wVk = 17,
dwFlags = 2u
}
}
}
};
NativeMethods.SendInput((uint)array.Length, array, Marshal.SizeOf<NativeMethods.INPUT>());
Thread.Sleep(80);
string selected = null;
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
if (System.Windows.Clipboard.ContainsText())
{
string text = System.Windows.Clipboard.GetText();
if (text != prevText && !string.IsNullOrWhiteSpace(text))
{
selected = text;
}
}
if (selected != null && prevText != null)
{
System.Windows.Clipboard.SetText(prevText);
}
else if (selected != null && !hadText)
{
System.Windows.Clipboard.Clear();
}
});
return selected;
}
catch
{
return null;
}
}
private void ExecuteTextAction(TextActionPopup.ActionResult action, string text)
{
if (1 == 0)
{
}
string text2 = action switch
{
TextActionPopup.ActionResult.Translate => "다음 텍스트를 번역해줘:\n\n" + text,
TextActionPopup.ActionResult.Summarize => "다음 텍스트를 요약해줘:\n\n" + text,
TextActionPopup.ActionResult.GrammarFix => "다음 텍스트의 문법을 교정해줘:\n\n" + text,
TextActionPopup.ActionResult.Explain => "다음 텍스트를 쉽게 설명해줘:\n\n" + text,
TextActionPopup.ActionResult.Rewrite => "다음 텍스트를 다시 작성해줘:\n\n" + text,
_ => text,
};
if (1 == 0)
{
}
string text3 = text2;
_launcher?.Show();
_launcher?.SetInputText("! " + text3);
}
private void OnCaptureHotkeyTriggered(object? sender, EventArgs e)
{
WindowTracker.Capture();
string mode = _settings?.Settings.ScreenCapture.GlobalHotkeyMode ?? "screen";
((DispatcherObject)this).Dispatcher.Invoke<Task>((Func<Task>)async delegate
{
if (_captureHandler == null)
{
return;
}
try
{
await _captureHandler.CaptureDirectAsync(mode);
}
catch (Exception ex)
{
LogService.Error("글로벌 캡처 실패: " + ex.Message);
}
});
}
private void OnHookFailed(object? sender, EventArgs e)
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
_trayIcon?.ShowBalloonTip(3000, "AX Copilot", "글로벌 단축키 등록에 실패했습니다.\n다른 앱과 " + (_settings?.Settings.Hotkey ?? "단축키") + "가 충돌하고 있을 수 있습니다.", ToolTipIcon.Warning);
});
}
private void SetupTrayIcon(PluginHost pluginHost, SettingsService settings)
{
_trayIcon = new NotifyIcon
{
Text = "AX Copilot",
Visible = true,
Icon = LoadAppIcon()
};
_trayMenu = new TrayMenuWindow();
_trayMenu.AddItem("\ue7c5", "AX Commander 호출하기", delegate
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
_launcher?.Show();
});
}).AddItem("\ue8bd", "AX Agent 대화하기", delegate
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
OpenAiChat();
});
}, out Border aiTrayItem).AddItem("\ue8a7", "독 바 표시", delegate
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
ToggleDockBar();
});
})
.AddSeparator()
.AddItem("\ue72c", "플러그인 재로드", delegate
{
pluginHost.Reload();
_trayIcon.ShowBalloonTip(2000, "AX Copilot", "플러그인이 재로드되었습니다.", ToolTipIcon.None);
})
.AddItem("\ue838", "로그 폴더 열기", delegate
{
string arguments = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "logs");
Process.Start(new ProcessStartInfo("explorer.exe", arguments)
{
UseShellExecute = true
});
})
.AddItem("\ue9d9", "사용 통계", delegate
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
new StatisticsWindow().Show();
});
})
.AddItem("\ue736", "사용 가이드 문서보기", delegate
{
try
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
new GuideViewerWindow().Show();
});
}
catch (Exception ex)
{
LogService.Error("사용 가이드 열기 실패: " + ex.Message);
}
})
.AddSeparator()
.AddToggleItem("\ue82f", "Windows 시작 시 자동 실행", IsAutoStartEnabled(), delegate(bool isChecked)
{
SetAutoStart(isChecked);
}, out Func<bool> _, out Action<string> _)
.AddSeparator()
.AddItem("\ue713", "설정", delegate
{
((DispatcherObject)this).Dispatcher.Invoke((Action)OpenSettings);
})
.AddItem("\ue946", "개발 정보", delegate
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
new AboutWindow().Show();
});
})
.AddItem("\ue711", "종료", delegate
{
_inputListener?.Dispose();
_trayIcon?.Dispose();
Shutdown();
});
_trayMenu.Opening += delegate
{
aiTrayItem.Visibility = ((!settings.Settings.AiEnabled) ? Visibility.Collapsed : Visibility.Visible);
};
_trayIcon.MouseClick += delegate(object? _, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
_launcher?.Show();
});
}
else if (e.Button == MouseButtons.Right)
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
_trayMenu?.ShowWithUpdate();
});
}
};
NotificationService.Initialize(delegate(string title, string msg)
{
((DispatcherObject)this).Dispatcher.Invoke((Action)delegate
{
_trayIcon?.ShowBalloonTip(4000, title, msg, ToolTipIcon.None);
});
});
}
public void OpenSettingsFromChat()
{
((DispatcherObject)this).Dispatcher.Invoke((Action)OpenSettings);
}
internal void PrewarmChatWindow()
{
if (_chatWindow == null && _settings != null)
{
_chatWindow = new ChatWindow(_settings);
}
}
private void OpenAiChat()
{
if (_settings != null)
{
if (_chatWindow == null)
{
_chatWindow = new ChatWindow(_settings);
}
_chatWindow.Show();
_chatWindow.Activate();
}
}
public void ToggleDockBar()
{
if (_dockBar != null && _dockBar.IsVisible)
{
_dockBar.Hide();
return;
}
if (_dockBar == null)
{
_dockBar = new DockBarWindow();
_dockBar.OnQuickSearch = delegate(string query)
{
if (_launcher != null)
{
_launcher.Show();
_launcher.Activate();
if (!string.IsNullOrEmpty(query))
{
((DispatcherObject)this).Dispatcher.BeginInvoke((DispatcherPriority)5, (Delegate)(Action)delegate
{
_launcher.SetInputText(query);
});
}
}
};
_dockBar.OnCapture = async delegate
{
WindowTracker.Capture();
if (_captureHandler != null)
{
await _captureHandler.CaptureDirectAsync("region");
}
};
_dockBar.OnOpenAgent = delegate
{
if (_launcher != null)
{
_launcher.Show();
((DispatcherObject)this).Dispatcher.BeginInvoke((DispatcherPriority)5, (Delegate)(Action)delegate
{
_launcher.SetInputText("!");
});
}
};
}
LauncherSettings launcherSettings = _settings?.Settings.Launcher;
List<string> itemKeys = launcherSettings?.DockBarItems ?? new List<string> { "launcher", "clipboard", "capture", "agent", "clock", "cpu" };
_dockBar.BuildFromSettings(itemKeys);
_dockBar.OnPositionChanged = delegate(double left, double top)
{
if (_settings != null)
{
_settings.Settings.Launcher.DockBarLeft = left;
_settings.Settings.Launcher.DockBarTop = top;
_settings.Save();
}
};
_dockBar.Show();
_dockBar.ApplySettings(launcherSettings?.DockBarOpacity ?? 0.92, launcherSettings?.DockBarLeft ?? (-1.0), launcherSettings?.DockBarTop ?? (-1.0), launcherSettings?.DockBarRainbowGlow ?? false);
}
public void RefreshDockBar()
{
if (_dockBar != null && _dockBar.IsVisible)
{
LauncherSettings launcherSettings = _settings?.Settings.Launcher;
List<string> itemKeys = launcherSettings?.DockBarItems ?? new List<string> { "launcher", "clipboard", "capture", "agent", "clock", "cpu" };
_dockBar.BuildFromSettings(itemKeys);
_dockBar.ApplySettings(launcherSettings?.DockBarOpacity ?? 0.92, launcherSettings?.DockBarLeft ?? (-1.0), launcherSettings?.DockBarTop ?? (-1.0), launcherSettings?.DockBarRainbowGlow ?? false);
}
}
private void OpenSettings()
{
SettingsViewModel vm;
if (_settingsWindow != null && _settingsWindow.IsVisible)
{
_settingsWindow.Activate();
}
else
{
if (_settings == null || _launcher == null)
{
return;
}
vm = new SettingsViewModel(_settings);
_settingsWindow = new SettingsWindow(vm, PreviewCallback, RevertCallback)
{
SuspendHotkeyCallback = delegate(bool suspend)
{
if (_inputListener != null)
{
_inputListener.SuspendHotkey = suspend;
}
}
};
vm.SaveCompleted += delegate
{
if (_inputListener != null && _settings != null)
{
_inputListener.UpdateHotkey(_settings.Settings.Hotkey);
_inputListener.UpdateCaptureHotkey(_settings.Settings.ScreenCapture.GlobalHotkey, _settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
}
_worktimeReminder?.RestartTimer();
};
_settingsWindow.Show();
}
void PreviewCallback(string themeKey)
{
if (themeKey == "custom")
{
CustomThemeColors customThemeColors = new CustomThemeColors();
foreach (ColorRowModel colorRow in vm.ColorRows)
{
typeof(CustomThemeColors).GetProperty(colorRow.Property)?.SetValue(customThemeColors, colorRow.Hex);
}
_launcher.ApplyTheme(themeKey, customThemeColors);
}
else
{
_launcher.ApplyTheme(themeKey, _settings.Settings.Launcher.CustomTheme);
}
}
void RevertCallback()
{
_launcher.ApplyTheme(_settings.Settings.Launcher.Theme ?? "system", _settings.Settings.Launcher.CustomTheme);
}
}
private static Icon LoadAppIcon()
{
Size smallIconSize = SystemInformation.SmallIconSize;
try
{
string path = Path.GetDirectoryName(Environment.ProcessPath) ?? AppContext.BaseDirectory;
string text = Path.Combine(path, "Assets", "icon.ico");
if (File.Exists(text))
{
return new Icon(text, smallIconSize);
}
}
catch
{
}
try
{
Uri uriResource = new Uri("pack://application:,,,/Assets/icon.ico");
Stream stream = System.Windows.Application.GetResourceStream(uriResource)?.Stream;
if (stream != null)
{
return new Icon(stream, smallIconSize);
}
}
catch
{
}
return SystemIcons.Application;
}
private static bool IsAutoStartEnabled()
{
try
{
using RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Run", writable: false);
return registryKey?.GetValue("AxCopilot") != null;
}
catch
{
return false;
}
}
private static void SetAutoStart(bool enable)
{
try
{
using RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Run", writable: true);
if (registryKey == null)
{
return;
}
if (enable)
{
string text = Environment.ProcessPath ?? Process.GetCurrentProcess().MainModule?.FileName ?? string.Empty;
if (!string.IsNullOrEmpty(text))
{
registryKey.SetValue("AxCopilot", "\"" + text + "\"");
}
}
else
{
registryKey.DeleteValue("AxCopilot", throwOnMissingValue: false);
}
}
catch (Exception ex)
{
LogService.Warn("자동 시작 레지스트리 설정 실패: " + ex.Message);
}
}
protected override void OnExit(ExitEventArgs e)
{
_chatWindow?.ForceClose();
_inputListener?.Dispose();
_clipboardHistory?.Dispose();
_indexService?.Dispose();
_sessionTracking?.Dispose();
_worktimeReminder?.Dispose();
_trayIcon?.Dispose();
try
{
_singleInstanceMutex?.ReleaseMutex();
}
catch
{
}
_singleInstanceMutex?.Dispose();
LogService.Info("=== AX Copilot 종료 ===");
base.OnExit(e);
}
[DebuggerNonUserCode]
[GeneratedCode("PresentationBuildTasks", "10.0.5.0")]
public void InitializeComponent()
{
if (!_contentLoaded)
{
_contentLoaded = true;
Uri resourceLocator = new Uri("/AxCopilot;component/app.xaml", UriKind.Relative);
System.Windows.Application.LoadComponent(this, resourceLocator);
}
}
[STAThread]
[DebuggerNonUserCode]
[GeneratedCode("PresentationBuildTasks", "10.0.5.0")]
public static void Main()
{
App app = new App();
app.InitializeComponent();
app.Run();
}
}

View File

@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace AxCopilot.Core;
public sealed class BulkObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnCollectionChanged(e);
}
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnPropertyChanged(e);
}
}
public void ReplaceAll(IEnumerable<T> items)
{
_suppressNotification = true;
try
{
base.Items.Clear();
foreach (T item in items)
{
base.Items.Add(item);
}
}
finally
{
_suppressNotification = false;
}
OnPropertyChanged(new PropertyChangedEventArgs("Count"));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}

View File

@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class CommandResolver
{
private readonly FuzzyEngine _fuzzy;
private readonly SettingsService _settings;
private readonly Dictionary<string, IActionHandler> _handlers = new Dictionary<string, IActionHandler>();
private readonly List<IActionHandler> _fuzzyHandlers = new List<IActionHandler>();
public IReadOnlyDictionary<string, IActionHandler> RegisteredHandlers => _handlers;
public CommandResolver(FuzzyEngine fuzzy, SettingsService settings)
{
_fuzzy = fuzzy;
_settings = settings;
}
public void RegisterHandler(IActionHandler handler)
{
if (handler.Prefix == null)
{
_fuzzyHandlers.Add(handler);
LogService.Info("FuzzyHandler 등록: name='" + handler.Metadata.Name + "'");
return;
}
if (_handlers.ContainsKey(handler.Prefix))
{
LogService.Warn($"Prefix '{handler.Prefix}' 중복 등록: '{handler.Metadata.Name}'이 기존 핸들러를 덮어씁니다.");
}
_handlers[handler.Prefix] = handler;
LogService.Info($"Handler 등록: prefix='{handler.Prefix}', name='{handler.Metadata.Name}'");
}
public async Task<IEnumerable<LauncherItem>> ResolveAsync(string input, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(input))
{
return Enumerable.Empty<LauncherItem>();
}
foreach (var (prefix, handler) in _handlers)
{
if (input.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
object obj;
if (input.Length <= prefix.Length)
{
obj = "";
}
else
{
string text2 = input;
int length = prefix.Length;
obj = text2.Substring(length, text2.Length - length).Trim();
}
string query = (string)obj;
try
{
return await handler.GetItemsAsync(query, ct);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex2)
{
LogService.Error("Handler '" + handler.Metadata.Name + "' 오류: " + ex2.Message);
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("오류: " + ex2.Message, handler.Metadata.Name, null, null));
}
}
}
int maxResults = _settings.Settings.Launcher.MaxResults;
HashSet<string> seenPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
List<LauncherItem> fuzzyItems = UsageRankingService.SortByUsage((from r in _fuzzy.Search(input, maxResults * 2)
where seenPaths.Add(r.Entry.Path)
select r).Take(maxResults).Select(delegate(FuzzyResult r)
{
string displayName = r.Entry.DisplayName;
string subtitle;
if (r.Entry.Type == IndexEntryType.Alias)
{
string aliasType = r.Entry.AliasType;
if (1 == 0)
{
}
string text3 = ((aliasType == "url") ? "URL 단축키" : ((!(aliasType == "batch")) ? r.Entry.Path : "명령 단축키"));
if (1 == 0)
{
}
subtitle = text3;
}
else
{
subtitle = r.Entry.Path + " ⇧ Shift+Enter: 폴더 열기";
}
object entry = r.Entry;
IndexEntryType type = r.Entry.Type;
if (1 == 0)
{
}
string symbol;
switch (type)
{
case IndexEntryType.App:
symbol = "\uecaa";
break;
case IndexEntryType.Folder:
symbol = "\ue8b7";
break;
case IndexEntryType.Alias:
{
string aliasType2 = r.Entry.AliasType;
if (1 == 0)
{
}
string text3 = ((aliasType2 == "url") ? "\ue774" : ((!(aliasType2 == "batch")) ? "\uecca" : "\ue756"));
if (1 == 0)
{
}
symbol = text3;
break;
}
default:
symbol = "\ue8a5";
break;
}
if (1 == 0)
{
}
return new LauncherItem(displayName, subtitle, null, entry, null, symbol);
}), (LauncherItem item) => (item.Data as IndexEntry)?.Path).ToList();
if (_fuzzyHandlers.Count > 0)
{
List<Task<IEnumerable<LauncherItem>>> extraTasks = _fuzzyHandlers.Select((IActionHandler h) => SafeGetItemsAsync(h, input, ct)).ToList();
await Task.WhenAll(extraTasks);
foreach (Task<IEnumerable<LauncherItem>> task in extraTasks)
{
if (task.IsCompletedSuccessfully)
{
fuzzyItems.AddRange(task.Result.Take(3));
}
}
}
return fuzzyItems;
}
public async Task ExecuteAsync(LauncherItem item, string lastInput, CancellationToken ct)
{
foreach (var (prefix, handler) in _handlers)
{
if (lastInput.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
object obj;
if (lastInput.Length <= prefix.Length)
{
obj = "";
}
else
{
int length = prefix.Length;
obj = lastInput.Substring(length, lastInput.Length - length).Trim().Split(' ')[0];
}
string q = (string)obj;
string cmdKey = (string.IsNullOrEmpty(q) ? prefix : (prefix + q));
UsageStatisticsService.RecordCommandUsage(cmdKey);
await handler.ExecuteAsync(item, ct);
return;
}
}
object data = item.Data;
if (data is string urlData && urlData.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
await ExecuteNullPrefixAsync(item, ct);
return;
}
data = item.Data;
IndexEntry entry = data as IndexEntry;
if (entry == null)
{
return;
}
string expanded = Environment.ExpandEnvironmentVariables(entry.Path);
try
{
await Task.Run(() => Process.Start(new ProcessStartInfo(expanded)
{
UseShellExecute = true
}));
}
catch (Exception ex)
{
Exception ex2 = ex;
LogService.Error("실행 실패: " + expanded + " - " + ex2.Message);
}
Task.Run(delegate
{
UsageRankingService.RecordExecution(entry.Path);
});
}
public async Task ExecuteNullPrefixAsync(LauncherItem item, CancellationToken ct)
{
foreach (IActionHandler handler in _fuzzyHandlers)
{
try
{
await handler.ExecuteAsync(item, ct);
return;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex2)
{
LogService.Error("FuzzyHandler '" + handler.Metadata.Name + "' 실행 오류: " + ex2.Message);
}
}
}
private static async Task<IEnumerable<LauncherItem>> SafeGetItemsAsync(IActionHandler handler, string query, CancellationToken ct)
{
try
{
return await handler.GetItemsAsync(query, ct);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex2)
{
Exception ex3 = ex2;
LogService.Error("FuzzyHandler '" + handler.Metadata.Name + "' 오류: " + ex3.Message);
return Enumerable.Empty<LauncherItem>();
}
}
}

View File

@@ -0,0 +1,384 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class ContextManager
{
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
private struct WINDOWPLACEMENT
{
public uint length;
public uint flags;
public uint showCmd;
public POINT ptMinPosition;
public POINT ptMaxPosition;
public RECT rcNormalPosition;
}
private struct POINT
{
public int x;
public int y;
}
private delegate bool EnumWindowsProc(nint hWnd, nint lParam);
private delegate bool MonitorEnumProc(nint hMonitor, nint hdcMonitor, ref RECT lprcMonitor, nint dwData);
private readonly SettingsService _settings;
private const uint SWP_NOZORDER = 4u;
private const uint SWP_NOACTIVATE = 16u;
private const uint MONITOR_DEFAULTTONEAREST = 2u;
public ContextManager(SettingsService settings)
{
_settings = settings;
}
public WorkspaceProfile CaptureProfile(string name)
{
List<WindowSnapshot> snapshots = new List<WindowSnapshot>();
Dictionary<nint, int> monitorMap = BuildMonitorMap();
EnumWindows(delegate(nint hWnd, nint _)
{
if (!IsWindowVisible(hWnd))
{
return true;
}
if (IsIconic(hWnd))
{
return true;
}
string windowTitle = GetWindowTitle(hWnd);
if (string.IsNullOrWhiteSpace(windowTitle))
{
return true;
}
if (IsSystemWindow(hWnd))
{
return true;
}
string processPath = GetProcessPath(hWnd);
if (string.IsNullOrEmpty(processPath))
{
return true;
}
GetWindowRect(hWnd, out var lpRect);
GetWindowPlacement(hWnd, out var lpwndpl);
uint showCmd = lpwndpl.showCmd;
if (1 == 0)
{
}
string text = showCmd switch
{
1u => "Normal",
2u => "Minimized",
3u => "Maximized",
_ => "Normal",
};
if (1 == 0)
{
}
string showCmd2 = text;
int monitorIndex = GetMonitorIndex(hWnd, monitorMap);
snapshots.Add(new WindowSnapshot
{
Exe = processPath,
Title = windowTitle,
Rect = new WindowRect
{
X = lpRect.Left,
Y = lpRect.Top,
Width = lpRect.Right - lpRect.Left,
Height = lpRect.Bottom - lpRect.Top
},
ShowCmd = showCmd2,
Monitor = monitorIndex
});
return true;
}, IntPtr.Zero);
WorkspaceProfile workspaceProfile = new WorkspaceProfile
{
Name = name,
Windows = snapshots,
CreatedAt = DateTime.Now
};
WorkspaceProfile workspaceProfile2 = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (workspaceProfile2 != null)
{
_settings.Settings.Profiles.Remove(workspaceProfile2);
}
_settings.Settings.Profiles.Add(workspaceProfile);
_settings.Save();
LogService.Info($"프로필 '{name}' 저장 완료: {snapshots.Count}개 창");
return workspaceProfile;
}
public async Task<RestoreResult> RestoreProfileAsync(string name, CancellationToken ct = default(CancellationToken))
{
WorkspaceProfile profile = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (profile == null)
{
return new RestoreResult(Success: false, "프로필 '" + name + "'을 찾을 수 없습니다.");
}
List<string> results = new List<string>();
int monitorCount = GetMonitorCount();
foreach (WindowSnapshot snapshot in profile.Windows)
{
ct.ThrowIfCancellationRequested();
nint hWnd = FindMatchingWindow(snapshot);
if (hWnd == IntPtr.Zero && File.Exists(snapshot.Exe))
{
try
{
Process.Start(new ProcessStartInfo(snapshot.Exe)
{
UseShellExecute = true
});
hWnd = await WaitForWindowAsync(snapshot.Exe, TimeSpan.FromSeconds(3.0), ct);
}
catch (Exception ex)
{
Exception ex2 = ex;
results.Add($"⚠ {snapshot.Title}: 실행 실패 ({ex2.Message})");
LogService.Warn("앱 실행 실패: " + snapshot.Exe + " - " + ex2.Message);
continue;
}
}
if (hWnd == IntPtr.Zero)
{
results.Add("⏭ " + snapshot.Title + ": 창 없음, 건너뜀");
continue;
}
if (snapshot.Monitor >= monitorCount)
{
string policy = _settings.Settings.MonitorMismatch;
if (policy == "skip")
{
results.Add("⏭ " + snapshot.Title + ": 모니터 불일치, 건너뜀");
continue;
}
}
try
{
nint hWnd2 = hWnd;
string showCmd = snapshot.ShowCmd;
if (1 == 0)
{
}
string text = showCmd;
int nCmdShow = ((text == "Maximized") ? 3 : ((!(text == "Minimized")) ? 9 : 2));
if (1 == 0)
{
}
ShowWindow(hWnd2, nCmdShow);
if (snapshot.ShowCmd == "Normal")
{
SetWindowPos(hWnd, IntPtr.Zero, snapshot.Rect.X, snapshot.Rect.Y, snapshot.Rect.Width, snapshot.Rect.Height, 20u);
}
results.Add("✓ " + snapshot.Title + ": 복원 완료");
}
catch (Exception ex3)
{
results.Add($"⚠ {snapshot.Title}: 복원 실패 ({ex3.Message})");
LogService.Warn("창 복원 실패 (권한 문제 가능): " + snapshot.Exe);
}
}
LogService.Info("프로필 '" + name + "' 복원: " + string.Join(", ", results));
return new RestoreResult(Success: true, string.Join("\n", results));
}
public bool DeleteProfile(string name)
{
WorkspaceProfile workspaceProfile = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (workspaceProfile == null)
{
return false;
}
_settings.Settings.Profiles.Remove(workspaceProfile);
_settings.Save();
return true;
}
public bool RenameProfile(string oldName, string newName)
{
WorkspaceProfile workspaceProfile = _settings.Settings.Profiles.FirstOrDefault((WorkspaceProfile p) => p.Name.Equals(oldName, StringComparison.OrdinalIgnoreCase));
if (workspaceProfile == null)
{
return false;
}
workspaceProfile.Name = newName;
_settings.Save();
return true;
}
private static nint FindMatchingWindow(WindowSnapshot snapshot)
{
nint found = IntPtr.Zero;
EnumWindows(delegate(nint hWnd, nint _)
{
string processPath = GetProcessPath(hWnd);
if (string.Equals(processPath, snapshot.Exe, StringComparison.OrdinalIgnoreCase))
{
found = hWnd;
return false;
}
return true;
}, IntPtr.Zero);
return found;
}
private static async Task<nint> WaitForWindowAsync(string exePath, TimeSpan timeout, CancellationToken ct)
{
DateTime deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
ct.ThrowIfCancellationRequested();
nint hWnd = FindMatchingWindow(new WindowSnapshot
{
Exe = exePath
});
if (hWnd != IntPtr.Zero)
{
return hWnd;
}
await Task.Delay(200, ct);
}
return IntPtr.Zero;
}
private static string GetWindowTitle(nint hWnd)
{
StringBuilder stringBuilder = new StringBuilder(256);
GetWindowText(hWnd, stringBuilder, stringBuilder.Capacity);
return stringBuilder.ToString();
}
private static string GetProcessPath(nint hWnd)
{
try
{
GetWindowThreadProcessId(hWnd, out var lpdwProcessId);
if (lpdwProcessId == 0)
{
return "";
}
Process processById = Process.GetProcessById((int)lpdwProcessId);
return processById.MainModule?.FileName ?? "";
}
catch
{
return "";
}
}
private static bool IsSystemWindow(nint hWnd)
{
StringBuilder stringBuilder = new StringBuilder(256);
GetClassName(hWnd, stringBuilder, stringBuilder.Capacity);
switch (stringBuilder.ToString())
{
case "Shell_TrayWnd":
case "Progman":
case "WorkerW":
case "DV2ControlHost":
return true;
default:
return false;
}
}
private static Dictionary<nint, int> BuildMonitorMap()
{
List<nint> monitors = new List<nint>();
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate(nint hMonitor, nint hdcMonitor, ref RECT lprc, nint dwData)
{
monitors.Add(hMonitor);
return true;
}, IntPtr.Zero);
return monitors.Select((nint hm, int idx) => (hm: hm, idx: idx)).ToDictionary(((nint hm, int idx) t) => t.hm, ((nint hm, int idx) t) => t.idx);
}
private static int GetMonitorIndex(nint hWnd, Dictionary<nint, int> map)
{
nint key = MonitorFromWindow(hWnd, 2u);
int value;
return map.TryGetValue(key, out value) ? value : 0;
}
private static int GetMonitorCount()
{
int count = 0;
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate
{
count++;
return true;
}, IntPtr.Zero);
return count;
}
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(nint hWnd);
[DllImport("user32.dll")]
private static extern bool IsIconic(nint hWnd);
[DllImport("user32.dll")]
private static extern int GetWindowText(nint hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
private static extern bool GetWindowRect(nint hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern bool GetWindowPlacement(nint hWnd, out WINDOWPLACEMENT lpwndpl);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll")]
private static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern bool ShowWindow(nint hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern nint MonitorFromWindow(nint hwnd, uint dwFlags);
[DllImport("user32.dll")]
private static extern bool EnumDisplayMonitors(nint hdc, nint lprcClip, MonitorEnumProc lpfnEnum, nint dwData);
}

View File

@@ -0,0 +1,468 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class FuzzyEngine
{
private readonly IndexService _index;
private static readonly char[] Chosungs = new char[19]
{
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
};
private static readonly HashSet<char> ChosungSet = new HashSet<char>(new _003C_003Ez__ReadOnlyArray<char>(new char[19]
{
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
}));
private static readonly char[] Jungsungs = new char[21]
{
'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ',
'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ',
'ㅣ'
};
private static readonly char[] Jongsungs = new char[28]
{
'\0', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ',
'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ',
'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
};
public FuzzyEngine(IndexService index)
{
_index = index;
}
public IEnumerable<FuzzyResult> Search(string query, int maxResults = 7)
{
if (string.IsNullOrWhiteSpace(query))
{
return Enumerable.Empty<FuzzyResult>();
}
string normalized = query.Trim().ToLowerInvariant();
IReadOnlyList<IndexEntry> entries = _index.Entries;
bool queryHasKorean = false;
string text = normalized;
foreach (char c in text)
{
if ((c >= '가' && c <= '힣') || ChosungSet.Contains(c))
{
queryHasKorean = true;
break;
}
}
if (entries.Count > 300)
{
return (from e in entries.AsParallel()
select new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)) into r
where r.Score > 0
orderby r.Score descending
select r).Take(maxResults);
}
return (from e in entries
select new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)) into r
where r.Score > 0
orderby r.Score descending
select r).Take(maxResults);
}
private static int CalculateScoreFast(string query, IndexEntry entry, bool queryHasKorean)
{
string text = (string.IsNullOrEmpty(entry.NameLower) ? entry.Name.ToLowerInvariant() : entry.NameLower);
if (query.Length == 0)
{
return 0;
}
if (text == query)
{
return 1000 + entry.Score;
}
if (text.StartsWith(query))
{
return 800 + entry.Score;
}
if (text.Contains(query))
{
return 600 + entry.Score;
}
if (!queryHasKorean)
{
int num = FuzzyMatch(query, text);
return (num > 0) ? (num + entry.Score) : 0;
}
int num2 = JamoContainsScoreFast(string.IsNullOrEmpty(entry.NameJamo) ? DecomposeToJamo(text) : entry.NameJamo, query);
if (num2 > 0)
{
return num2 + entry.Score;
}
int num3 = ChosungMatchScoreFast(string.IsNullOrEmpty(entry.NameChosung) ? null : entry.NameChosung, text, query);
if (num3 > 0)
{
return num3 + entry.Score;
}
int num4 = FuzzyMatch(query, text);
if (num4 > 0)
{
return num4 + entry.Score;
}
return 0;
}
internal static int CalculateScore(string query, string target, int baseScore)
{
if (query.Length == 0)
{
return 0;
}
if (target == query)
{
return 1000 + baseScore;
}
if (target.StartsWith(query))
{
return 800 + baseScore;
}
if (target.Contains(query))
{
return 600 + baseScore;
}
int num = JamoContainsScore(target, query);
if (num > 0)
{
return num + baseScore;
}
int num2 = ChosungMatchScore(target, query);
if (num2 > 0)
{
return num2 + baseScore;
}
int num3 = FuzzyMatch(query, target);
if (num3 > 0)
{
return num3 + baseScore;
}
return 0;
}
internal static int FuzzyMatch(string query, string target)
{
int num = 0;
int num2 = 0;
int num3 = 0;
int num4 = -1;
while (num < query.Length && num2 < target.Length)
{
if (query[num] == target[num2])
{
num3 = ((num4 != num2 - 1) ? (num3 + 10) : (num3 + 30));
if (num2 == 0)
{
num3 += 15;
}
num4 = num2;
num++;
}
num2++;
}
return (num == query.Length) ? Math.Max(num3, 50) : 0;
}
internal static string DecomposeToJamo(string text)
{
StringBuilder stringBuilder = new StringBuilder(text.Length * 3);
foreach (char c in text)
{
if (c >= '가' && c <= '힣')
{
int num = c - 44032;
int num2 = num / 588;
int num3 = num % 588 / 28;
int num4 = num % 28;
stringBuilder.Append(Chosungs[num2]);
stringBuilder.Append(Jungsungs[num3]);
if (num4 > 0)
{
stringBuilder.Append(Jongsungs[num4]);
}
}
else
{
stringBuilder.Append(c);
}
}
return stringBuilder.ToString();
}
internal static char GetChosung(char hangul)
{
if (hangul < '가' || hangul > '힣')
{
return '\0';
}
int num = hangul - 44032;
return Chosungs[num / 588];
}
internal static int JamoContainsScore(string target, string query)
{
if (!HasKorean(query))
{
return 0;
}
string text = DecomposeToJamo(target);
string text2 = DecomposeToJamo(query);
if (text2.Length == 0 || text.Length == 0)
{
return 0;
}
if (text.Contains(text2))
{
return (text.IndexOf(text2) == 0) ? 580 : 550;
}
int num = 0;
for (int i = 0; i < text.Length; i++)
{
if (num >= text2.Length)
{
break;
}
if (text2[num] == text[i])
{
num++;
}
}
if (num == text2.Length)
{
return 400;
}
return 0;
}
internal static bool HasChosung(string text)
{
return text.Any((char c) => ChosungSet.Contains(c));
}
internal static bool IsChosung(string text)
{
return text.Length > 0 && text.All((char c) => ChosungSet.Contains(c));
}
private static bool HasKorean(string text)
{
return text.Any((char c) => c >= '가' && c <= '힣');
}
internal static int ChosungMatchScore(string target, string query)
{
if (!HasChosung(query))
{
return 0;
}
List<char> list = new List<char>();
List<char> list2 = new List<char>();
foreach (char c in target)
{
char chosung = GetChosung(c);
if (chosung != 0)
{
list.Add(chosung);
list2.Add(c);
}
else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
{
list.Add(c);
list2.Add(c);
}
}
if (list.Count == 0)
{
return 0;
}
if (IsChosung(query))
{
if (ContainsChosungConsecutive(list, query))
{
return 520;
}
if (ContainsChosungSubsequence(list, query))
{
return 480;
}
return 0;
}
return MixedChosungMatch(list2, list, query);
}
private static bool ContainsChosungConsecutive(List<char> targetChosungs, string query)
{
for (int i = 0; i <= targetChosungs.Count - query.Length; i++)
{
bool flag = true;
for (int j = 0; j < query.Length; j++)
{
if (targetChosungs[i + j] != query[j])
{
flag = false;
break;
}
}
if (flag)
{
return true;
}
}
return false;
}
private static bool ContainsChosungSubsequence(List<char> targetChosungs, string query)
{
int num = 0;
for (int i = 0; i < targetChosungs.Count; i++)
{
if (num >= query.Length)
{
break;
}
if (targetChosungs[i] == query[num])
{
num++;
}
}
return num == query.Length;
}
private static int MixedChosungMatch(List<char> targetChars, List<char> targetChosungs, string query)
{
int num = 0;
int num2 = 0;
while (num < query.Length && num2 < targetChars.Count)
{
char c = query[num];
if (ChosungSet.Contains(c))
{
if (targetChosungs[num2] == c)
{
num++;
}
}
else if (targetChars[num2] == c)
{
num++;
}
num2++;
}
return (num == query.Length) ? 460 : 0;
}
private static int JamoContainsScoreFast(string targetJamo, string query)
{
if (!HasKorean(query))
{
return 0;
}
string text = DecomposeToJamo(query);
if (text.Length == 0 || targetJamo.Length == 0)
{
return 0;
}
if (targetJamo.Contains(text))
{
return (targetJamo.IndexOf(text, StringComparison.Ordinal) == 0) ? 580 : 550;
}
int num = 0;
for (int i = 0; i < targetJamo.Length; i++)
{
if (num >= text.Length)
{
break;
}
if (text[num] == targetJamo[i])
{
num++;
}
}
return (num == text.Length) ? 400 : 0;
}
private static int ChosungMatchScoreFast(string? targetChosung, string targetLower, string query)
{
if (!HasChosung(query))
{
return 0;
}
if (IsChosung(query))
{
if (string.IsNullOrEmpty(targetChosung))
{
return 0;
}
if (targetChosung.Contains(query, StringComparison.Ordinal))
{
return 520;
}
int num = 0;
for (int i = 0; i < targetChosung.Length; i++)
{
if (num >= query.Length)
{
break;
}
if (targetChosung[i] == query[num])
{
num++;
}
}
if (num == query.Length)
{
return 480;
}
return 0;
}
int num2 = 0;
int num3 = 0;
while (num2 < query.Length && num3 < targetLower.Length)
{
char c = query[num2];
char c2 = targetLower[num3];
if (ChosungSet.Contains(c))
{
char c3 = GetChosung(c2);
if (c3 == '\0' && ((c2 >= 'a' && c2 <= 'z') || (c2 >= '0' && c2 <= '9')))
{
c3 = c2;
}
if (c3 == c)
{
num2++;
}
}
else if (c2 == c)
{
num2++;
}
num3++;
}
return (num2 == query.Length) ? 460 : 0;
}
internal static bool ContainsChosung(string target, string chosungQuery)
{
List<char> list = (from c in target.Select(GetChosung)
where c != '\0'
select c).ToList();
if (list.Count < chosungQuery.Length)
{
return false;
}
return ContainsChosungConsecutive(list, chosungQuery) || ContainsChosungSubsequence(list, chosungQuery);
}
}

View File

@@ -0,0 +1,5 @@
using AxCopilot.Services;
namespace AxCopilot.Core;
public record FuzzyResult(IndexEntry Entry, int Score);

View File

@@ -0,0 +1,3 @@
namespace AxCopilot.Core;
public record struct HotkeyDefinition(int VkCode, bool Ctrl, bool Alt, bool Shift, bool Win);

View File

@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
namespace AxCopilot.Core;
public static class HotkeyParser
{
private static readonly Dictionary<string, int> _keyMap;
static HotkeyParser()
{
_keyMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["Space"] = 32,
["Enter"] = 13,
["Return"] = 13,
["Tab"] = 9,
["Esc"] = 27,
["Escape"] = 27,
["Backspace"] = 8,
["Back"] = 8,
["Delete"] = 46,
["Del"] = 46,
["Insert"] = 45,
["Ins"] = 45,
["Home"] = 36,
["End"] = 35,
["PageUp"] = 33,
["PgUp"] = 33,
["PageDown"] = 34,
["PgDn"] = 34,
["PrintScreen"] = 44,
["PrtSc"] = 44,
["Snapshot"] = 44,
["Pause"] = 19,
["Break"] = 19,
["ScrollLock"] = 145,
["Left"] = 37,
["Up"] = 38,
["Right"] = 39,
["Down"] = 40,
["`"] = 192,
["Grave"] = 192,
["-"] = 189,
["="] = 187,
["["] = 219,
["]"] = 221,
["\\"] = 220,
[";"] = 186,
["'"] = 222,
[","] = 188,
["."] = 190,
["/"] = 191
};
for (char c = 'A'; c <= 'Z'; c = (char)(c + 1))
{
_keyMap[c.ToString()] = c;
}
for (char c2 = '0'; c2 <= '9'; c2 = (char)(c2 + 1))
{
_keyMap[c2.ToString()] = c2;
}
for (int i = 1; i <= 24; i++)
{
_keyMap[$"F{i}"] = 111 + i;
}
for (int j = 0; j <= 9; j++)
{
_keyMap[$"Num{j}"] = 96 + j;
}
}
public static bool TryParse(string hotkey, out HotkeyDefinition result)
{
result = default(HotkeyDefinition);
if (string.IsNullOrWhiteSpace(hotkey))
{
return false;
}
string[] array = hotkey.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
bool ctrl = false;
bool alt = false;
bool shift = false;
bool win = false;
int? num = null;
string[] array2 = array;
foreach (string text in array2)
{
if (text.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || text.Equals("Control", StringComparison.OrdinalIgnoreCase))
{
ctrl = true;
continue;
}
if (text.Equals("Alt", StringComparison.OrdinalIgnoreCase))
{
alt = true;
continue;
}
if (text.Equals("Shift", StringComparison.OrdinalIgnoreCase))
{
shift = true;
continue;
}
if (text.Equals("Win", StringComparison.OrdinalIgnoreCase) || text.Equals("Windows", StringComparison.OrdinalIgnoreCase))
{
win = true;
continue;
}
if (_keyMap.TryGetValue(text, out var value))
{
num = value;
continue;
}
return false;
}
if (!num.HasValue)
{
return false;
}
result = new HotkeyDefinition(num.Value, ctrl, alt, shift, win);
return true;
}
public static string Format(HotkeyDefinition def)
{
List<string> list = new List<string>(5);
if (def.Ctrl)
{
list.Add("Ctrl");
}
if (def.Alt)
{
list.Add("Alt");
}
if (def.Shift)
{
list.Add("Shift");
}
if (def.Win)
{
list.Add("Win");
}
list.Add(VkToName(def.VkCode));
return string.Join("+", list);
}
private static string VkToName(int vk)
{
if (vk >= 65 && vk <= 90)
{
return ((char)vk).ToString();
}
if (vk >= 48 && vk <= 57)
{
return ((char)vk).ToString();
}
if (vk >= 112 && vk <= 135)
{
return $"F{vk - 111}";
}
if (vk >= 96 && vk <= 105)
{
return $"Num{vk - 96}";
}
string text = null;
foreach (var (text3, num2) in _keyMap)
{
if (num2 == vk && (text == null || text3.Length > text.Length))
{
text = text3;
}
}
return text ?? $"0x{vk:X2}";
}
}

View File

@@ -0,0 +1,249 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class InputListener : IDisposable
{
private delegate nint LowLevelKeyboardProc(int nCode, nint wParam, nint lParam);
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 256;
private const int WM_SYSKEYDOWN = 260;
private const int WM_KEYUP = 257;
private const int WM_SYSKEYUP = 261;
private const int VK_SHIFT = 16;
private const int VK_CONTROL = 17;
private const int VK_MENU = 18;
private const int VK_LWIN = 91;
private const int VK_RWIN = 92;
private nint _hookHandle = IntPtr.Zero;
private LowLevelKeyboardProc? _proc;
private int _retryCount = 0;
private const int MaxRetry = 3;
private volatile bool _suppressNextAltUp;
private volatile bool _suppressNextKeyUp;
private volatile int _suppressKeyUpVk;
private HotkeyDefinition _hotkey = new HotkeyDefinition(32, Ctrl: false, Alt: true, Shift: false, Win: false);
private HotkeyDefinition _captureHotkey;
private bool _captureHotkeyEnabled;
public bool SuspendHotkey { get; set; }
public Func<int, bool>? KeyFilter { get; set; }
public event EventHandler? HotkeyTriggered;
public event EventHandler? CaptureHotkeyTriggered;
public event EventHandler? HookFailed;
public void UpdateHotkey(string hotkeyStr)
{
if (HotkeyParser.TryParse(hotkeyStr, out var result))
{
_hotkey = result;
LogService.Info("핫키 변경: " + hotkeyStr);
}
else
{
LogService.Warn("핫키 파싱 실패: '" + hotkeyStr + "' — 기존 핫키 유지");
}
}
public void UpdateCaptureHotkey(string hotkeyStr, bool enabled)
{
_captureHotkeyEnabled = enabled;
if (enabled && HotkeyParser.TryParse(hotkeyStr, out var result))
{
_captureHotkey = result;
LogService.Info("캡처 단축키 활성화: " + hotkeyStr);
}
else if (!enabled)
{
LogService.Info("캡처 단축키 비활성화");
}
}
public void Start()
{
_proc = HookCallback;
Register();
}
private void Register()
{
using Process process = Process.GetCurrentProcess();
using ProcessModule processModule = process.MainModule;
_hookHandle = SetWindowsHookEx(13, _proc, GetModuleHandle(processModule.ModuleName), 0u);
if (_hookHandle == IntPtr.Zero)
{
int lastWin32Error = Marshal.GetLastWin32Error();
LogService.Error($"Global Hook 등록 실패 (에러 코드: {lastWin32Error})");
TryRetryRegister();
}
else
{
_retryCount = 0;
LogService.Info("Global Keyboard Hook 등록 완료 (" + HotkeyParser.Format(_hotkey) + ")");
}
}
private void TryRetryRegister()
{
if (_retryCount < 3)
{
_retryCount++;
LogService.Warn($"Hook 재등록 시도 {_retryCount}/{3}");
Task.Delay(1000).ContinueWith(delegate
{
Register();
});
}
else
{
LogService.Error("Hook 재등록 최대 횟수 초과");
this.HookFailed?.Invoke(this, EventArgs.Empty);
}
}
private static bool IsSuppressedForegroundWindow()
{
nint foregroundWindow = GetForegroundWindow();
if (foregroundWindow == IntPtr.Zero)
{
return false;
}
StringBuilder stringBuilder = new StringBuilder(64);
GetClassName(foregroundWindow, stringBuilder, 64);
string text = stringBuilder.ToString();
return text == "#32770" || text == "SunAwtDialog";
}
private nint HookCallback(int nCode, nint wParam, nint lParam)
{
if (nCode < 0)
{
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
int num = Marshal.ReadInt32(lParam);
if (wParam == 257 || wParam == 261)
{
if (_suppressNextAltUp && num == 18)
{
_suppressNextAltUp = false;
return 1;
}
if (_suppressNextKeyUp && num == _suppressKeyUpVk)
{
_suppressNextKeyUp = false;
return 1;
}
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
if (wParam != 256 && wParam != 260)
{
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
if (IsSuppressedForegroundWindow())
{
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
if (!SuspendHotkey && num == _hotkey.VkCode)
{
bool flag = !_hotkey.Ctrl || (GetAsyncKeyState(17) & 0x8000) != 0;
bool flag2 = !_hotkey.Alt || (GetAsyncKeyState(18) & 0x8000) != 0;
bool flag3 = !_hotkey.Shift || (GetAsyncKeyState(16) & 0x8000) != 0;
bool flag4 = !_hotkey.Win || (GetAsyncKeyState(91) & 0x8000) != 0 || (GetAsyncKeyState(92) & 0x8000) != 0;
if (flag && flag2 && flag3 && flag4)
{
this.HotkeyTriggered?.Invoke(this, EventArgs.Empty);
_suppressNextKeyUp = true;
_suppressKeyUpVk = num;
if (_hotkey.Alt)
{
_suppressNextAltUp = true;
}
return 1;
}
}
if (!SuspendHotkey && _captureHotkeyEnabled && num == _captureHotkey.VkCode)
{
bool flag5 = !_captureHotkey.Ctrl || (GetAsyncKeyState(17) & 0x8000) != 0;
bool flag6 = !_captureHotkey.Alt || (GetAsyncKeyState(18) & 0x8000) != 0;
bool flag7 = !_captureHotkey.Shift || (GetAsyncKeyState(16) & 0x8000) != 0;
bool flag8 = !_captureHotkey.Win || (GetAsyncKeyState(91) & 0x8000) != 0 || (GetAsyncKeyState(92) & 0x8000) != 0;
if (flag5 && flag6 && flag7 && flag8)
{
this.CaptureHotkeyTriggered?.Invoke(this, EventArgs.Empty);
_suppressNextKeyUp = true;
_suppressKeyUpVk = num;
if (_captureHotkey.Alt)
{
_suppressNextAltUp = true;
}
return 1;
}
}
Func<int, bool>? keyFilter = KeyFilter;
if (keyFilter != null && keyFilter(num))
{
return 1;
}
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
public void Dispose()
{
if (_hookHandle != IntPtr.Zero)
{
UnhookWindowsHookEx(_hookHandle);
_hookHandle = IntPtr.Zero;
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern nint SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, nint hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(nint hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern nint GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll")]
private static extern nint GetForegroundWindow();
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount);
}

View File

@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using AxCopilot.Handlers;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class PluginHost
{
private readonly SettingsService _settings;
private readonly CommandResolver _resolver;
private readonly List<IActionHandler> _loadedPlugins = new List<IActionHandler>();
public IReadOnlyList<IActionHandler> LoadedPlugins => _loadedPlugins;
public PluginHost(SettingsService settings, CommandResolver resolver)
{
_settings = settings;
_resolver = resolver;
}
public void LoadAll()
{
_loadedPlugins.Clear();
foreach (PluginEntry item in _settings.Settings.Plugins.Where((PluginEntry p) => p.Enabled))
{
LoadPlugin(item.Path);
}
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills");
if (!Directory.Exists(path))
{
return;
}
foreach (string item2 in Directory.EnumerateFiles(path, "*.skill.json"))
{
LoadJsonSkill(item2);
}
}
private void LoadPlugin(string dllPath)
{
if (!File.Exists(dllPath))
{
LogService.Warn("플러그인 파일 없음: " + dllPath);
return;
}
try
{
Assembly assembly = Assembly.LoadFrom(dllPath);
IEnumerable<Type> enumerable = from t in assembly.GetExportedTypes()
where typeof(IActionHandler).IsAssignableFrom(t) && !t.IsAbstract
select t;
foreach (Type item in enumerable)
{
if (Activator.CreateInstance(item) is IActionHandler actionHandler)
{
_resolver.RegisterHandler(actionHandler);
_loadedPlugins.Add(actionHandler);
LogService.Info("플러그인 로드: " + actionHandler.Metadata.Name + " v" + actionHandler.Metadata.Version);
}
}
}
catch (Exception ex)
{
LogService.Error("플러그인 로드 실패 (" + dllPath + "): " + ex.Message);
}
}
private void LoadJsonSkill(string skillPath)
{
try
{
IActionHandler actionHandler = JsonSkillLoader.Load(skillPath);
if (actionHandler != null)
{
_resolver.RegisterHandler(actionHandler);
_loadedPlugins.Add(actionHandler);
LogService.Info("JSON 스킬 로드: " + actionHandler.Metadata.Name);
}
}
catch (Exception ex)
{
LogService.Error("JSON 스킬 로드 실패 (" + skillPath + "): " + ex.Message);
}
}
public void Reload()
{
LogService.Info("플러그인 전체 재로드 시작");
LoadAll();
}
public int InstallFromZip(string zipPath)
{
if (!File.Exists(zipPath))
{
LogService.Warn("플러그인 zip 파일 없음: " + zipPath);
return 0;
}
string text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "plugins");
Directory.CreateDirectory(text);
int num = 0;
try
{
using ZipArchive zipArchive = ZipFile.OpenRead(zipPath);
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(zipPath);
string text2 = Path.Combine(text, fileNameWithoutExtension);
Directory.CreateDirectory(text2);
foreach (ZipArchiveEntry entry in zipArchive.Entries)
{
if (!string.IsNullOrEmpty(entry.Name))
{
string text3 = Path.Combine(text2, entry.Name);
if (!Path.GetFullPath(text3).StartsWith(Path.GetFullPath(text2)))
{
LogService.Warn("플러그인 zip 경로 위험: " + entry.FullName);
}
else
{
entry.ExtractToFile(text3, overwrite: true);
}
}
}
foreach (string dllFile in Directory.EnumerateFiles(text2, "*.dll"))
{
if (!_settings.Settings.Plugins.Any((PluginEntry p) => p.Path == dllFile))
{
_settings.Settings.Plugins.Add(new PluginEntry
{
Enabled = true,
Path = dllFile
});
LoadPlugin(dllFile);
num++;
}
}
string text4 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills");
Directory.CreateDirectory(text4);
foreach (string item in Directory.EnumerateFiles(text2, "*.skill.json"))
{
string text5 = Path.Combine(text4, Path.GetFileName(item));
File.Copy(item, text5, overwrite: true);
LoadJsonSkill(text5);
num++;
}
if (num > 0)
{
_settings.Save();
}
LogService.Info($"플러그인 설치 완료: {zipPath} → {num}개 핸들러");
}
catch (Exception ex)
{
LogService.Error("플러그인 zip 설치 실패: " + ex.Message);
}
return num;
}
public bool UninstallPlugin(string dllPath)
{
try
{
PluginEntry pluginEntry = _settings.Settings.Plugins.FirstOrDefault((PluginEntry p) => p.Path == dllPath);
if (pluginEntry != null)
{
_settings.Settings.Plugins.Remove(pluginEntry);
_settings.Save();
}
string directoryName = Path.GetDirectoryName(dllPath);
if (directoryName != null && Directory.Exists(directoryName))
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "plugins");
if (Path.GetFullPath(directoryName).StartsWith(Path.GetFullPath(path)))
{
Directory.Delete(directoryName, recursive: true);
}
}
LogService.Info("플러그인 제거 완료: " + dllPath);
return true;
}
catch (Exception ex)
{
LogService.Error("플러그인 제거 실패: " + ex.Message);
return false;
}
}
}

View File

@@ -0,0 +1,3 @@
namespace AxCopilot.Core;
public record RestoreResult(bool Success, string Message);

View File

@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Core;
public class SnippetExpander
{
private struct INPUT
{
public int type;
public InputUnion u;
}
[StructLayout(LayoutKind.Explicit)]
private struct InputUnion
{
[FieldOffset(0)]
public MOUSEINPUT mi;
[FieldOffset(0)]
public KEYBDINPUT ki;
[FieldOffset(0)]
public HARDWAREINPUT hi;
}
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public nint dwExtraInfo;
}
private struct MOUSEINPUT
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public nint dwExtraInfo;
}
private struct HARDWAREINPUT
{
public uint uMsg;
public ushort wParamL;
public ushort wParamH;
}
private readonly SettingsService _settings;
private readonly StringBuilder _buffer = new StringBuilder();
private bool _tracking;
private const ushort VK_BACK = 8;
private const int VK_ESCAPE = 27;
private const int VK_SPACE = 32;
private const int VK_RETURN = 13;
private const int VK_OEM_1 = 186;
private const int VK_SHIFT = 16;
private const int VK_CONTROL = 17;
private const int VK_MENU = 18;
private const ushort VK_CTRL_US = 17;
private static readonly HashSet<int> ClearKeys = new HashSet<int> { 33, 34, 35, 36, 37, 38, 39, 40, 46 };
public SnippetExpander(SettingsService settings)
{
_settings = settings;
}
public bool HandleKey(int vkCode)
{
if (!_settings.Settings.Launcher.SnippetAutoExpand)
{
return false;
}
if ((GetAsyncKeyState(17) & 0x8000) != 0)
{
_tracking = false;
_buffer.Clear();
return false;
}
if ((GetAsyncKeyState(18) & 0x8000) != 0)
{
_tracking = false;
_buffer.Clear();
return false;
}
if (vkCode == 186 && (GetAsyncKeyState(16) & 0x8000) == 0)
{
_tracking = true;
_buffer.Clear();
_buffer.Append(';');
return false;
}
if (!_tracking)
{
return false;
}
if ((vkCode >= 65 && vkCode <= 90) || (vkCode >= 48 && vkCode <= 57) || (vkCode >= 96 && vkCode <= 105) || vkCode == 189)
{
bool shifted = (GetAsyncKeyState(16) & 0x8000) != 0;
char c = VkToChar(vkCode, shifted);
if (c != 0)
{
_buffer.Append(char.ToLowerInvariant(c));
}
return false;
}
switch (vkCode)
{
case 8:
if (_buffer.Length > 1)
{
_buffer.Remove(_buffer.Length - 1, 1);
}
else
{
_tracking = false;
_buffer.Clear();
}
return false;
default:
if (vkCode != 13)
{
if (vkCode == 27 || ClearKeys.Contains(vkCode) || vkCode >= 112)
{
_tracking = false;
_buffer.Clear();
}
return false;
}
goto case 32;
case 32:
if (_buffer.Length > 1)
{
string keyword = _buffer.ToString(1, _buffer.Length - 1);
_tracking = false;
_buffer.Clear();
SnippetEntry snippetEntry = _settings.Settings.Snippets.FirstOrDefault((SnippetEntry s) => s.Key.Equals(keyword, StringComparison.OrdinalIgnoreCase));
if (snippetEntry != null)
{
string expanded = ExpandVariables(snippetEntry.Content);
int deleteCount = keyword.Length + 1;
((DispatcherObject)Application.Current).Dispatcher.BeginInvoke((Delegate)(Action)delegate
{
PasteExpansion(expanded, deleteCount);
}, Array.Empty<object>());
return true;
}
}
_tracking = false;
_buffer.Clear();
return false;
}
}
private static void PasteExpansion(string text, int deleteCount)
{
try
{
INPUT[] array = new INPUT[deleteCount * 2];
for (int i = 0; i < deleteCount; i++)
{
array[i * 2] = MakeKeyInput(8, keyUp: false);
array[i * 2 + 1] = MakeKeyInput(8, keyUp: true);
}
SendInput((uint)array.Length, array, Marshal.SizeOf<INPUT>());
Clipboard.SetText(text);
INPUT[] array2 = new INPUT[4]
{
MakeKeyInput(17, keyUp: false),
MakeKeyInput(86, keyUp: false),
MakeKeyInput(86, keyUp: true),
MakeKeyInput(17, keyUp: true)
};
SendInput((uint)array2.Length, array2, Marshal.SizeOf<INPUT>());
LogService.Info($"스니펫 확장 완료: {deleteCount}자 삭제 후 붙여넣기");
}
catch (Exception ex)
{
LogService.Warn("스니펫 확장 실패: " + ex.Message);
}
}
private static INPUT MakeKeyInput(ushort vk, bool keyUp)
{
INPUT result = new INPUT
{
type = 1
};
result.u.ki.wVk = vk;
result.u.ki.dwFlags = (keyUp ? 2u : 0u);
return result;
}
private static string ExpandVariables(string content)
{
DateTime now = DateTime.Now;
return content.Replace("{date}", now.ToString("yyyy-MM-dd")).Replace("{time}", now.ToString("HH:mm:ss")).Replace("{datetime}", now.ToString("yyyy-MM-dd HH:mm:ss"))
.Replace("{year}", now.Year.ToString())
.Replace("{month}", now.Month.ToString("D2"))
.Replace("{day}", now.Day.ToString("D2"));
}
private static char VkToChar(int vk, bool shifted)
{
if (vk >= 65 && vk <= 90)
{
return shifted ? ((char)vk) : char.ToLowerInvariant((char)vk);
}
if (vk >= 48 && vk <= 57)
{
return shifted ? ")!@#$%^&*("[vk - 48] : ((char)vk);
}
if (vk >= 96 && vk <= 105)
{
return (char)(48 + (vk - 96));
}
if (vk == 189)
{
return shifted ? '_' : '-';
}
return '\0';
}
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class BatchHandler : IActionHandler
{
private readonly SettingsService _settings;
public string? Prefix => ">";
public PluginMetadata Metadata => new PluginMetadata("batch", "명령 실행", "1.0", "AX");
public BatchHandler(SettingsService settings)
{
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
List<LauncherItem> list = new List<LauncherItem>();
IEnumerable<LauncherItem> collection = from a in _settings.Settings.Aliases
where a.Type == "batch" && (string.IsNullOrEmpty(query) || a.Key.Contains(query, StringComparison.OrdinalIgnoreCase))
select new LauncherItem(a.Key, a.Target, null, a, null, "\ue756");
list.AddRange(collection);
if (!string.IsNullOrEmpty(query))
{
string text = query.Replace("\"", "\\\"");
list.Insert(0, new LauncherItem("실행: " + query, "PowerShell에서 직접 실행", null, new AliasEntry
{
Type = "batch",
Target = "powershell -NoProfile -Command \"" + text + "\"",
ShowWindow = true
}, null, "\ue756"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is AliasEntry aliasEntry)
{
string text = Environment.ExpandEnvironmentVariables(aliasEntry.Target);
string[] array = text.Split(' ', 2);
ProcessStartInfo startInfo = new ProcessStartInfo(array[0])
{
Arguments = ((array.Length > 1) ? array[1] : ""),
UseShellExecute = aliasEntry.ShowWindow,
CreateNoWindow = !aliasEntry.ShowWindow,
WindowStyle = ((!aliasEntry.ShowWindow) ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal)
};
Process.Start(startInfo);
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class BatchTextHandler : IActionHandler
{
private static readonly (string Cmd, string Desc)[] Commands = new(string, string)[14]
{
("prefix [텍스트]", "각 줄 앞에 텍스트 추가"),
("suffix [텍스트]", "각 줄 뒤에 텍스트 추가"),
("wrap [문자]", "각 줄을 지정 문자로 감싸기 (예: wrap \")"),
("number", "줄번호 추가 (1. 2. 3. ...)"),
("sort", "줄 오름차순 정렬"),
("sortd", "줄 내림차순 정렬"),
("reverse", "줄 순서 뒤집기"),
("unique", "중복 줄 제거"),
("trim", "각 줄 앞뒤 공백 제거"),
("replace [A] [B]", "A를 B로 전체 치환"),
("csv", "줄들을 쉼표로 합쳐 한 줄로"),
("split [구분자]", "한 줄을 구분자로 분리하여 여러 줄로"),
("indent [N]", "각 줄 앞에 공백 N개 추가"),
("unindent", "각 줄의 선행 공백/탭 제거")
};
public string? Prefix => "batch";
public PluginMetadata Metadata => new PluginMetadata("BatchText", "텍스트 일괄 처리 — batch", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
if (string.IsNullOrWhiteSpace(text))
{
List<LauncherItem> list = Commands.Select(((string Cmd, string Desc) c) => new LauncherItem("batch " + c.Cmd, c.Desc, null, null, null, "\ue8d2")).ToList();
list.Insert(0, new LauncherItem("텍스트 일괄 처리", "클립보드 텍스트의 각 줄에 변환 적용 · 명령 입력", null, null, null, "\ue946"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
string text2 = null;
try
{
Application current = Application.Current;
if (current != null && ((DispatcherObject)current).Dispatcher.Invoke<bool>((Func<bool>)(() => Clipboard.ContainsText())))
{
text2 = ((DispatcherObject)Application.Current).Dispatcher.Invoke<string>((Func<string>)(() => Clipboard.GetText()));
}
}
catch
{
}
if (string.IsNullOrEmpty(text2))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("클립보드에 텍스트가 없습니다", "텍스트를 복사한 후 시도하세요", null, null, null, "\ue7ba")));
}
string[] array = (from l in text2.Split('\n')
select l.TrimEnd('\r')).ToArray();
string[] array2 = text.Split(' ', 2, StringSplitOptions.TrimEntries);
string text3 = array2[0].ToLowerInvariant();
string arg = ((array2.Length > 1) ? array2[1] : "");
string text4 = null;
string text5 = null;
try
{
switch (text3)
{
case "prefix":
text4 = string.Join("\n", array.Select((string l) => arg + l));
text5 = "각 줄 앞에 '" + arg + "' 추가";
break;
case "suffix":
text4 = string.Join("\n", array.Select((string l) => l + arg));
text5 = "각 줄 뒤에 '" + arg + "' 추가";
break;
case "wrap":
{
string w = (string.IsNullOrEmpty(arg) ? "\"" : arg);
text4 = string.Join("\n", array.Select((string l) => w + l + w));
text5 = "각 줄을 '" + w + "'로 감싸기";
break;
}
case "number":
text4 = string.Join("\n", array.Select((string l, int i) => $"{i + 1}. {l}"));
text5 = "줄번호 추가";
break;
case "sort":
text4 = string.Join("\n", array.Order());
text5 = "오름차순 정렬";
break;
case "sortd":
text4 = string.Join("\n", array.OrderDescending());
text5 = "내림차순 정렬";
break;
case "reverse":
text4 = string.Join("\n", array.Reverse());
text5 = "줄 순서 뒤집기";
break;
case "unique":
{
string[] array4 = array.Distinct().ToArray();
text4 = string.Join("\n", array4);
text5 = $"중복 제거: {array.Length}줄 → {array4.Length}줄";
break;
}
case "trim":
text4 = string.Join("\n", array.Select((string l) => l.Trim()));
text5 = "각 줄 공백 제거";
break;
case "replace":
{
string[] array3 = arg.Split(' ', 2, StringSplitOptions.TrimEntries);
if (array3.Length == 2)
{
text4 = text2.Replace(array3[0], array3[1]);
text5 = $"'{array3[0]}' → '{array3[1]}' 치환";
}
break;
}
case "csv":
text4 = string.Join(",", array.Select((string l) => l.Trim()));
text5 = $"{array.Length}줄 → CSV 한 줄";
break;
case "split":
{
string text6 = (string.IsNullOrEmpty(arg) ? "," : arg);
text4 = string.Join("\n", text2.Split(text6));
text5 = "'" + text6 + "' 기준 분리";
break;
}
case "indent":
{
int result;
int num = (int.TryParse(arg, out result) ? result : 4);
string pad = new string(' ', num);
text4 = string.Join("\n", array.Select((string l) => pad + l));
text5 = $"{num}칸 들여쓰기";
break;
}
case "unindent":
text4 = string.Join("\n", array.Select((string l) => l.TrimStart(' ', '\t')));
text5 = "선행 공백 제거";
break;
}
}
catch (Exception ex)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("처리 오류: " + ex.Message, "입력 데이터를 확인하세요", null, null, null, "\uea39")));
}
if (text4 == null)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("알 수 없는 명령: " + text3, "batch 만 입력하면 전체 명령 목록", null, null, null, "\ue7ba")));
}
string text7 = ((text4.Length > 120) ? (text4.Substring(0, 117) + "…") : text4);
text7 = text7.Replace("\n", "↵ ");
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("[" + text5 + "] → Enter로 클립보드 복사", text7, null, text4, null, "\ue8d2")));
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
string text = data as string;
if (text != null)
{
try
{
Application current = Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(text);
});
}
}
catch
{
}
NotificationService.Notify("일괄 처리 완료", "변환 결과가 클립보드에 복사되었습니다");
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class BookmarkHandler : IActionHandler
{
private record BookmarkEntry(string Name, string? Url);
private List<BookmarkEntry>? _cache;
private DateTime _cacheTime = DateTime.MinValue;
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(5.0);
public string? Prefix => null;
public PluginMetadata Metadata => new PluginMetadata("Bookmarks", "Chrome / Edge 북마크 검색", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(query))
{
return Task.FromResult((IEnumerable<LauncherItem>)Array.Empty<LauncherItem>());
}
RefreshCacheIfNeeded();
if (_cache == null || _cache.Count == 0)
{
return Task.FromResult((IEnumerable<LauncherItem>)Array.Empty<LauncherItem>());
}
string q = query.Trim().ToLowerInvariant();
List<LauncherItem> result = (from b in _cache.Where((BookmarkEntry b) => b.Name.ToLowerInvariant().Contains(q) || (b.Url?.ToLowerInvariant().Contains(q) ?? false)).Take(8)
select new LauncherItem(b.Name, b.Url ?? "", null, b.Url, null, "\ue774")).ToList();
return Task.FromResult((IEnumerable<LauncherItem>)result);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text && !string.IsNullOrWhiteSpace(text))
{
try
{
Process.Start(new ProcessStartInfo(text)
{
UseShellExecute = true
});
}
catch (Exception ex)
{
LogService.Warn("북마크 열기 실패: " + ex.Message);
}
}
return Task.CompletedTask;
}
private void RefreshCacheIfNeeded()
{
if (_cache != null && DateTime.Now - _cacheTime < CacheTtl)
{
return;
}
List<BookmarkEntry> list = new List<BookmarkEntry>();
foreach (string bookmarkFile in GetBookmarkFiles())
{
try
{
string json = File.ReadAllText(bookmarkFile);
ParseChromeBookmarks(json, list);
}
catch (Exception ex)
{
LogService.Warn("북마크 파일 읽기 실패: " + bookmarkFile + " — " + ex.Message);
}
}
_cache = list;
_cacheTime = DateTime.Now;
LogService.Info($"북마크 로드 완료: {list.Count}개");
}
private static IEnumerable<string> GetBookmarkFiles()
{
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string[] chromePaths = new string[4]
{
Path.Combine(localAppData, "Google", "Chrome", "User Data"),
Path.Combine(localAppData, "Google", "Chrome Beta", "User Data"),
Path.Combine(localAppData, "Google", "Chrome Dev", "User Data"),
Path.Combine(localAppData, "Google", "Chrome SxS", "User Data")
};
string[] edgePaths = new string[4]
{
Path.Combine(localAppData, "Microsoft", "Edge", "User Data"),
Path.Combine(localAppData, "Microsoft", "Edge Beta", "User Data"),
Path.Combine(localAppData, "Microsoft", "Edge Dev", "User Data"),
Path.Combine(localAppData, "Microsoft", "Edge Canary", "User Data")
};
foreach (string profileRoot in chromePaths.Concat(edgePaths))
{
if (!Directory.Exists(profileRoot))
{
continue;
}
string defaultBookmark = Path.Combine(profileRoot, "Default", "Bookmarks");
if (File.Exists(defaultBookmark))
{
yield return defaultBookmark;
}
string[] directories = Directory.GetDirectories(profileRoot, "Profile *");
foreach (string dir in directories)
{
string f = Path.Combine(dir, "Bookmarks");
if (File.Exists(f))
{
yield return f;
}
}
}
}
private static void ParseChromeBookmarks(string json, List<BookmarkEntry> result)
{
JsonNode jsonNode = JsonNode.Parse(json)?["roots"];
if (jsonNode == null)
{
return;
}
string[] array = new string[3] { "bookmark_bar", "other", "synced" };
foreach (string propertyName in array)
{
JsonNode jsonNode2 = jsonNode[propertyName];
if (jsonNode2 != null)
{
WalkNode(jsonNode2, result);
}
}
}
private static void WalkNode(JsonNode node, List<BookmarkEntry> result)
{
string text = node["type"]?.GetValue<string>();
if (text == "url")
{
string text2 = node["name"]?.GetValue<string>() ?? "";
string text3 = node["url"]?.GetValue<string>() ?? "";
if (!string.IsNullOrWhiteSpace(text2) && !string.IsNullOrWhiteSpace(text3))
{
result.Add(new BookmarkEntry(text2, text3));
}
}
else
{
if (!(text == "folder"))
{
return;
}
JsonArray jsonArray = node["children"]?.AsArray();
if (jsonArray == null)
{
return;
}
foreach (JsonNode item in jsonArray)
{
if (item != null)
{
WalkNode(item, result);
}
}
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class CalculatorHandler : IActionHandler
{
public string? Prefix => "=";
public PluginMetadata Metadata => new PluginMetadata("Calculator", "수식 계산기 — = 뒤에 수식 입력", "1.0", "AX");
public async Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(query))
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("수식을 입력하세요", "예: 1+2*3 · sqrt(16) · 100km in miles · 100 USD to KRW", null, null, null, "\ue8ef"));
}
string trimmed = query.Trim();
if (CurrencyConverter.IsCurrencyQuery(trimmed))
{
return await CurrencyConverter.ConvertAsync(trimmed, ct);
}
if (UnitConverter.TryConvert(trimmed, out string convertResult))
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem(convertResult, trimmed + " · Enter로 클립보드에 복사", null, convertResult, null, "\ue8ef"));
}
try
{
double value = MathEvaluator.Evaluate(trimmed);
string result = FormatResult(value);
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem(result, trimmed + " = " + result + " · Enter로 클립보드에 복사", null, result, null, "\ue8ef"));
}
catch (Exception ex)
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("계산할 수 없습니다", ex.Message, null, null, null, "\uea39"));
}
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try
{
Clipboard.SetText(text);
}
catch
{
}
}
return Task.CompletedTask;
}
private static string FormatResult(double value)
{
if (double.IsNaN(value))
{
return "NaN";
}
if (double.IsPositiveInfinity(value))
{
return "∞";
}
if (double.IsNegativeInfinity(value))
{
return "-∞";
}
if (value == Math.Floor(value) && Math.Abs(value) < 1000000000000000.0)
{
return ((long)value).ToString();
}
return value.ToString("G10", CultureInfo.InvariantCulture);
}
}

View File

@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
public class ChatHandler : IActionHandler
{
private const bool DEPLOY_STUB = false;
private readonly SettingsService _settings;
private readonly object _windowLock = new object();
private ChatWindow? _chatWindow;
public string? Prefix => "!";
public PluginMetadata Metadata => new PluginMetadata("ax.agent", "AX Agent", "1.0", "AX Agent — AI 어시스턴트");
public ChatHandler(SettingsService settings)
{
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
bool flag = false;
AppSettings appSettings = (Application.Current as App)?.SettingsService?.Settings;
if (appSettings != null && !appSettings.AiEnabled)
{
return Task.FromResult((IEnumerable<LauncherItem>)Array.Empty<LauncherItem>());
}
List<LauncherItem> list = new List<LauncherItem>();
string text = query.Trim();
if (string.IsNullOrEmpty(text))
{
list.Add(new LauncherItem("AX Agent 대화하기", "AI 비서와 대화를 시작합니다", null, "open_chat", null, "\ue8bd"));
try
{
ChatStorageService chatStorageService = new ChatStorageService();
List<ChatConversation> source = chatStorageService.LoadAllMeta();
foreach (ChatConversation item in source.Take(5))
{
string value = FormatTimeAgo(item.UpdatedAt);
string symbol = ChatCategory.GetSymbol(item.Category);
list.Add(new LauncherItem(item.Title, $"{value} · 메시지 {item.Messages.Count}개", null, "resume:" + item.Id, null, symbol));
}
if (source.Any())
{
list.Add(new LauncherItem("새 대화 시작", "이전 대화와 별개의 새 대화를 시작합니다", null, "new_chat", null, "\ue710"));
}
}
catch
{
}
}
else
{
list.Add(new LauncherItem("AI에게 물어보기: " + ((text.Length > 40) ? (text.Substring(0, 40) + "…") : text), "Enter를 누르면 AX Agent이 열리고 질문이 전송됩니다", null, "ask:" + text, null, "\ue8bd"));
list.Add(new LauncherItem("AX Agent 대화하기", "질문 없이 Agent 창만 엽니다", null, "open_chat", null, "\ue8bd"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
bool flag = false;
AppSettings appSettings = (Application.Current as App)?.SettingsService?.Settings;
if (appSettings != null && !appSettings.AiEnabled)
{
return Task.CompletedTask;
}
string data = (item.Data as string) ?? "open_chat";
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
{
EnsureChatWindow();
if (data.StartsWith("ask:"))
{
string text = data;
string message = text.Substring(4, text.Length - 4);
_chatWindow.Show();
_chatWindow.Activate();
_chatWindow.SendInitialMessage(message);
}
else if (data.StartsWith("resume:"))
{
string text = data;
string conversationId = text.Substring(7, text.Length - 7);
_chatWindow.Show();
_chatWindow.Activate();
_chatWindow.ResumeConversation(conversationId);
}
else if (data == "new_chat")
{
_chatWindow.Show();
_chatWindow.Activate();
_chatWindow.StartNewAndFocus();
}
else
{
_chatWindow.Show();
_chatWindow.Activate();
}
});
return Task.CompletedTask;
}
private void EnsureChatWindow()
{
lock (_windowLock)
{
if (_chatWindow != null && _chatWindow.IsLoaded)
{
return;
}
_chatWindow = new ChatWindow(_settings);
_chatWindow.Closed += delegate
{
lock (_windowLock)
{
_chatWindow = null;
}
};
}
}
private static string FormatTimeAgo(DateTime dt)
{
TimeSpan timeSpan = DateTime.Now - dt;
if (timeSpan.TotalMinutes < 1.0)
{
return "방금 전";
}
if (timeSpan.TotalHours < 1.0)
{
return $"{(int)timeSpan.TotalMinutes}분 전";
}
if (timeSpan.TotalDays < 1.0)
{
return $"{(int)timeSpan.TotalHours}시간 전";
}
if (timeSpan.TotalDays < 7.0)
{
return $"{(int)timeSpan.TotalDays}일 전";
}
return dt.ToString("yyyy-MM-dd");
}
}

View File

@@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class ClipboardHandler : IActionHandler
{
private readonly SettingsService _settings;
public string? Prefix => "$";
public PluginMetadata Metadata => new PluginMetadata("clipboard", "클립보드 변환", "1.0", "AX");
public ClipboardHandler(SettingsService settings)
{
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
List<LauncherItem> list = new List<LauncherItem>();
IEnumerable<ClipboardTransformer> enumerable = from t in GetBuiltinTransformers()
where string.IsNullOrEmpty(query) || t.Key.Contains(query, StringComparison.OrdinalIgnoreCase)
select t;
foreach (ClipboardTransformer item in enumerable)
{
list.Add(new LauncherItem(item.Key, item.Description ?? "", null, item, null, "\ue77f"));
}
IEnumerable<LauncherItem> collection = from t in _settings.Settings.ClipboardTransformers
where string.IsNullOrEmpty(query) || t.Key.Contains(query, StringComparison.OrdinalIgnoreCase)
select new LauncherItem(t.Key, t.Description ?? t.Type, null, t, null, "\ue77f");
list.AddRange(collection);
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
if (!(data is ClipboardTransformer transformer))
{
return;
}
string input = null;
((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate
{
input = (System.Windows.Clipboard.ContainsText() ? System.Windows.Clipboard.GetText() : null);
});
if (input == null)
{
return;
}
string result = await TransformAsync(transformer, input, ct);
if (result != null)
{
((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate
{
System.Windows.Clipboard.SetText(result);
});
nint prevHwnd = WindowTracker.PreviousWindow;
if (prevHwnd != IntPtr.Zero)
{
SetForegroundWindow(prevHwnd);
}
await Task.Delay(120, ct);
SendKeys.SendWait("^v");
LogService.Info("클립보드 변환: '" + transformer.Key + "' 적용");
}
}
private static async Task<string?> TransformAsync(ClipboardTransformer t, string input, CancellationToken ct)
{
try
{
string type = t.Type;
if (1 == 0)
{
}
string text = type;
string result;
if (!(text == "regex"))
{
if (!(text == "script") || t.Command == null)
{
goto IL_016f;
}
result = await RunScriptAsync(t.Command, input, t.Timeout, ct);
}
else
{
if (t.Pattern == null || t.Replace == null)
{
goto IL_016f;
}
result = Regex.Replace(input, t.Pattern, t.Replace, RegexOptions.None, TimeSpan.FromMilliseconds((t.Timeout > 0) ? t.Timeout : 5000));
}
goto IL_0188;
IL_016f:
result = ExecuteBuiltin(t.Key, input);
goto IL_0188;
IL_0188:
if (1 == 0)
{
}
return result;
}
catch (Exception ex)
{
Exception ex2 = ex;
LogService.Error("변환 실패 (" + t.Key + "): " + ex2.Message);
return null;
}
}
private static async Task<string> RunScriptAsync(string command, string input, int timeoutMs, CancellationToken ct)
{
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(timeoutMs);
string[] parts = command.Split(' ', 2);
ProcessStartInfo psi = new ProcessStartInfo(parts[0])
{
Arguments = ((parts.Length > 1) ? parts[1] : ""),
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardInputEncoding = Encoding.UTF8,
StandardOutputEncoding = Encoding.UTF8
};
using Process proc = Process.Start(psi);
await proc.StandardInput.WriteAsync(input);
proc.StandardInput.Close();
return await proc.StandardOutput.ReadToEndAsync(cts.Token);
}
internal static string? ExecuteBuiltin(string key, string input)
{
if (1 == 0)
{
}
string result = key switch
{
"$json" => FormatJson(input),
"$upper" => input.ToUpperInvariant(),
"$lower" => input.ToLowerInvariant(),
"$ts" => TryParseTimestamp(input),
"$epoch" => TryParseDate(input),
"$urle" => Uri.EscapeDataString(input),
"$urld" => Uri.UnescapeDataString(input),
"$b64e" => Convert.ToBase64String(Encoding.UTF8.GetBytes(input)),
"$b64d" => Encoding.UTF8.GetString(Convert.FromBase64String(input)),
"$md" => StripMarkdown(input),
"$trim" => input.Trim(),
"$lines" => string.Join(Environment.NewLine, from l in input.Split('\n')
select l.Trim() into l
where l.Length > 0
select l),
_ => null,
};
if (1 == 0)
{
}
return result;
}
private static string FormatJson(string input)
{
try
{
JsonDocument value = JsonDocument.Parse(input);
return JsonSerializer.Serialize(value, new JsonSerializerOptions
{
WriteIndented = true
});
}
catch
{
return input;
}
}
private static string? TryParseTimestamp(string input)
{
if (long.TryParse(input.Trim(), out var result))
{
return DateTimeOffset.FromUnixTimeSeconds(result).LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss");
}
return null;
}
private static string? TryParseDate(string input)
{
if (DateTime.TryParse(input.Trim(), out var result))
{
return new DateTimeOffset(result).ToUnixTimeSeconds().ToString();
}
return null;
}
private static string StripMarkdown(string input)
{
return Regex.Replace(input, "(\\*\\*|__)(.*?)\\1|(\\*|_)(.*?)\\3|`(.+?)`|#{1,6}\\s*", "$2$4$5");
}
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(nint hWnd);
private static IEnumerable<ClipboardTransformer> GetBuiltinTransformers()
{
return new _003C_003Ez__ReadOnlyArray<ClipboardTransformer>(new ClipboardTransformer[12]
{
new ClipboardTransformer
{
Key = "$json",
Type = "builtin",
Description = "JSON 포맷팅 (들여쓰기 적용)"
},
new ClipboardTransformer
{
Key = "$upper",
Type = "builtin",
Description = "대문자 변환"
},
new ClipboardTransformer
{
Key = "$lower",
Type = "builtin",
Description = "소문자 변환"
},
new ClipboardTransformer
{
Key = "$ts",
Type = "builtin",
Description = "유닉스 타임스탬프 → 날짜 문자열"
},
new ClipboardTransformer
{
Key = "$epoch",
Type = "builtin",
Description = "날짜 문자열 → 유닉스 타임스탬프"
},
new ClipboardTransformer
{
Key = "$urle",
Type = "builtin",
Description = "URL 인코딩"
},
new ClipboardTransformer
{
Key = "$urld",
Type = "builtin",
Description = "URL 디코딩"
},
new ClipboardTransformer
{
Key = "$b64e",
Type = "builtin",
Description = "Base64 인코딩"
},
new ClipboardTransformer
{
Key = "$b64d",
Type = "builtin",
Description = "Base64 디코딩"
},
new ClipboardTransformer
{
Key = "$md",
Type = "builtin",
Description = "마크다운 문법 제거"
},
new ClipboardTransformer
{
Key = "$trim",
Type = "builtin",
Description = "앞뒤 공백 제거"
},
new ClipboardTransformer
{
Key = "$lines",
Type = "builtin",
Description = "빈 줄 제거 및 각 줄 공백 정리"
}
});
}
}

View File

@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class ClipboardHistoryHandler : IActionHandler
{
[StructLayout(LayoutKind.Explicit, Size = 40)]
private struct INPUT
{
[FieldOffset(0)]
public uint Type;
[FieldOffset(8)]
public KEYBDINPUT ki;
}
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public nint dwExtraInfo;
}
private readonly ClipboardHistoryService _historyService;
private static readonly Dictionary<string, string> CategoryFilters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "url", "URL" },
{ "코드", "코드" },
{ "code", "코드" },
{ "경로", "경로" },
{ "path", "경로" },
{ "핀", "핀" },
{ "pin", "핀" }
};
public string? Prefix => "#";
public PluginMetadata Metadata => new PluginMetadata("ClipboardHistory", "클립보드 히스토리 — # 뒤에 검색어 (또는 빈 입력으로 전체 보기)", "1.0", "AX");
public ClipboardHistoryHandler(ClipboardHistoryService historyService)
{
_historyService = historyService;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
IReadOnlyList<ClipboardEntry> history = _historyService.History;
if (history.Count == 0)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("클립보드 히스토리가 없습니다", "텍스트를 복사하면 이 곳에 기록됩니다", null, null, null, "\ue81c")));
}
string q = query.Trim().ToLowerInvariant();
string catFilter = null;
foreach (var (text3, text4) in CategoryFilters)
{
if (q == text3 || q.StartsWith(text3 + " "))
{
catFilter = text4;
object obj;
if (q.Length <= text3.Length)
{
obj = "";
}
else
{
string text5 = q;
int num = text3.Length + 1;
obj = text5.Substring(num, text5.Length - num).Trim();
}
q = (string)obj;
break;
}
}
IEnumerable<ClipboardEntry> source = history.AsEnumerable();
if (catFilter == "핀")
{
source = source.Where((ClipboardEntry e) => e.IsPinned);
}
else if (catFilter != null)
{
source = source.Where((ClipboardEntry e) => e.Category == catFilter);
}
if (!string.IsNullOrEmpty(q))
{
source = source.Where((ClipboardEntry e) => e.Preview.ToLowerInvariant().Contains(q));
}
IOrderedEnumerable<ClipboardEntry> source2 = from e in source
orderby e.IsPinned descending, e.CopiedAt descending
select e;
List<LauncherItem> list = source2.Select(delegate(ClipboardEntry e)
{
string text6 = (e.IsPinned ? "\ud83d\udccc " : "");
string value = ((e.Category != "일반") ? ("[" + e.Category + "] ") : "");
return new LauncherItem(text6 + e.Preview, $"{value}{e.RelativeTime} · {e.CopiedAt:MM/dd HH:mm}", null, e, null, e.IsPinned ? "\ue728" : (e.IsText ? "\ue81c" : "\ueb9f"));
}).ToList();
if (list.Count == 0)
{
list.Add(new LauncherItem("'" + query + "'에 해당하는 항목 없음", "#pin #url #코드 #경로 로 필터링 가능", null, null, null, "\ue81c"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
if (!(data is ClipboardEntry entry))
{
return;
}
try
{
_historyService.SuppressNextCapture();
_historyService.PromoteEntry(entry);
if (!entry.IsText && entry.Image != null)
{
BitmapSource originalImg = ClipboardHistoryService.LoadOriginalImage(entry.OriginalImagePath);
Clipboard.SetImage(originalImg ?? entry.Image);
}
else if (!string.IsNullOrEmpty(entry.Text))
{
Clipboard.SetText(entry.Text);
nint prevWindow = WindowTracker.PreviousWindow;
if (prevWindow != IntPtr.Zero)
{
await Task.Delay(300, ct);
uint lpdwProcessId;
uint targetThread = GetWindowThreadProcessId(prevWindow, out lpdwProcessId);
uint currentThread = GetCurrentThreadId();
AttachThreadInput(currentThread, targetThread, fAttach: true);
SetForegroundWindow(prevWindow);
AttachThreadInput(currentThread, targetThread, fAttach: false);
await Task.Delay(100, ct);
SendCtrlV();
}
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex2)
{
LogService.Warn("클립보드 히스토리 붙여넣기 실패: " + ex2.Message);
}
}
private static void SendCtrlV()
{
INPUT[] array = new INPUT[4];
array[0].Type = 1u;
array[0].ki.wVk = 17;
array[1].Type = 1u;
array[1].ki.wVk = 86;
array[2].Type = 1u;
array[2].ki.wVk = 86;
array[2].ki.dwFlags = 2u;
array[3].Type = 1u;
array[3].ki.wVk = 17;
array[3].ki.dwFlags = 2u;
SendInput((uint)array.Length, array, Marshal.SizeOf<INPUT>());
}
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(nint hWnd);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")]
private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize);
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class ClipboardPipeHandler : IActionHandler
{
private static readonly Dictionary<string, (string Desc, Func<string, string> Fn)> Filters = new Dictionary<string, (string, Func<string, string>)>(StringComparer.OrdinalIgnoreCase)
{
["upper"] = ("대문자 변환", (string s) => s.ToUpperInvariant()),
["lower"] = ("소문자 변환", (string s) => s.ToLowerInvariant()),
["trim"] = ("앞뒤 공백 제거", (string s) => s.Trim()),
["trimall"] = ("모든 공백 제거", (string s) => Regex.Replace(s, "\\s+", "", RegexOptions.None, TimeSpan.FromSeconds(1.0))),
["sort"] = ("줄 정렬 (오름차순)", (string s) => string.Join("\n", s.Split('\n').Order())),
["sortd"] = ("줄 정렬 (내림차순)", (string s) => string.Join("\n", s.Split('\n').OrderDescending())),
["unique"] = ("중복 줄 제거", (string s) => string.Join("\n", s.Split('\n').Distinct())),
["reverse"] = ("줄 순서 뒤집기", (string s) => string.Join("\n", s.Split('\n').Reverse())),
["number"] = ("줄번호 추가", (string s) => string.Join("\n", s.Split('\n').Select((string l, int i) => $"{i + 1}. {l}"))),
["quote"] = ("각 줄 따옴표 감싸기", (string s) => string.Join("\n", from l in s.Split('\n')
select "\"" + l + "\"")),
["b64e"] = ("Base64 인코딩", (string s) => Convert.ToBase64String(Encoding.UTF8.GetBytes(s))),
["b64d"] = ("Base64 디코딩", (string s) => Encoding.UTF8.GetString(Convert.FromBase64String(s.Trim()))),
["urle"] = ("URL 인코딩", (string s) => Uri.EscapeDataString(s)),
["urld"] = ("URL 디코딩", (string s) => Uri.UnescapeDataString(s)),
["md"] = ("마크다운 제거", (string s) => Regex.Replace(s, "[#*_`~\\[\\]()]", "", RegexOptions.None, TimeSpan.FromSeconds(1.0))),
["lines"] = ("빈 줄 제거", (string s) => string.Join("\n", from l in s.Split('\n')
where !string.IsNullOrWhiteSpace(l)
select l)),
["count"] = ("글자/단어/줄 수", (string s) => $"글자: {s.Length} 단어: {s.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length} 줄: {s.Split('\n').Length}"),
["csv"] = ("CSV → 탭 변환", (string s) => s.Replace(',', '\t')),
["tab"] = ("탭 → CSV 변환", (string s) => s.Replace('\t', ','))
};
public string? Prefix => "pipe";
public PluginMetadata Metadata => new PluginMetadata("ClipboardPipe", "클립보드 파이프라인 — pipe", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
if (string.IsNullOrWhiteSpace(text))
{
List<LauncherItem> list = Filters.Select<KeyValuePair<string, (string, Func<string, string>)>, LauncherItem>((KeyValuePair<string, (string Desc, Func<string, string> Fn)> kv) => new LauncherItem(kv.Key, kv.Value.Desc, null, null, null, "\ue77f")).ToList();
list.Insert(0, new LauncherItem("클립보드 파이프라인", "필터를 > 로 연결: pipe upper > trim > b64e", null, null, null, "\ue946"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
string[] array = (from s in text.Split('>')
select s.Trim() into s
where !string.IsNullOrEmpty(s)
select s).ToArray();
string[] array2 = array.Where((string s) => !Filters.ContainsKey(s)).ToArray();
if (array2.Length != 0)
{
return Task.FromResult(new LauncherItem[1]
{
new LauncherItem("알 수 없는 필터: " + string.Join(", ", array2), "사용 가능: " + string.Join(", ", Filters.Keys.Take(10)) + " ...", null, null, null, "\ue7ba")
}.AsEnumerable());
}
string text2 = null;
try
{
Application current = Application.Current;
if (current != null && ((DispatcherObject)current).Dispatcher.Invoke<bool>((Func<bool>)(() => Clipboard.ContainsText())))
{
text2 = ((DispatcherObject)Application.Current).Dispatcher.Invoke<string>((Func<string>)(() => Clipboard.GetText()));
}
}
catch
{
}
if (string.IsNullOrEmpty(text2))
{
return Task.FromResult(new LauncherItem[1]
{
new LauncherItem("클립보드에 텍스트가 없습니다", "텍스트를 복사한 후 시도하세요", null, null, null, "\ue7ba")
}.AsEnumerable());
}
string text3 = text2;
List<string> list2 = new List<string>();
try
{
string[] array3 = array;
foreach (string key in array3)
{
text3 = Filters[key].Fn(text3);
list2.Add(Filters[key].Desc);
}
}
catch (Exception ex)
{
return Task.FromResult(new LauncherItem[1]
{
new LauncherItem("파이프라인 실행 오류: " + ex.Message, "입력 데이터를 확인하세요", null, null, null, "\uea39")
}.AsEnumerable());
}
string text4 = ((text3.Length > 100) ? (text3.Substring(0, 97) + "…") : text3);
text4 = text4.Replace("\r\n", "↵ ").Replace("\n", "↵ ");
return Task.FromResult(new LauncherItem[1]
{
new LauncherItem("[" + string.Join(" → ", array) + "] 결과 적용", text4 + " · Enter로 클립보드 복사", null, text3, null, "\ue77f")
}.AsEnumerable());
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
string text = data as string;
if (text != null)
{
try
{
Application current = Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(text);
});
}
}
catch
{
}
NotificationService.Notify("파이프라인 완료", "변환 결과가 클립보드에 복사되었습니다");
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,333 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class ColorHandler : IActionHandler
{
private static readonly Dictionary<string, string> _namedColors = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["red"] = "#FF0000",
["빨강"] = "#FF0000",
["빨간색"] = "#FF0000",
["green"] = "#008000",
["초록"] = "#008000",
["초록색"] = "#008000",
["blue"] = "#0000FF",
["파랑"] = "#0000FF",
["파란색"] = "#0000FF",
["white"] = "#FFFFFF",
["흰색"] = "#FFFFFF",
["하양"] = "#FFFFFF",
["black"] = "#000000",
["검정"] = "#000000",
["검은색"] = "#000000",
["yellow"] = "#FFFF00",
["노랑"] = "#FFFF00",
["노란색"] = "#FFFF00",
["orange"] = "#FFA500",
["주황"] = "#FFA500",
["주황색"] = "#FFA500",
["purple"] = "#800080",
["보라"] = "#800080",
["보라색"] = "#800080",
["pink"] = "#FFC0CB",
["분홍"] = "#FFC0CB",
["분홍색"] = "#FFC0CB",
["hotpink"] = "#FF69B4",
["핫핑크"] = "#FF69B4",
["cyan"] = "#00FFFF",
["시안"] = "#00FFFF",
["magenta"] = "#FF00FF",
["마젠타"] = "#FF00FF",
["brown"] = "#A52A2A",
["갈색"] = "#A52A2A",
["gray"] = "#808080",
["grey"] = "#808080",
["회색"] = "#808080",
["회"] = "#808080",
["silver"] = "#C0C0C0",
["은색"] = "#C0C0C0",
["gold"] = "#FFD700",
["금색"] = "#FFD700",
["navy"] = "#000080",
["남색"] = "#000080",
["teal"] = "#008080",
["틸"] = "#008080",
["lime"] = "#00FF00",
["라임"] = "#00FF00",
["maroon"] = "#800000",
["밤색"] = "#800000",
["olive"] = "#808000",
["올리브"] = "#808000",
["coral"] = "#FF7F50",
["코랄"] = "#FF7F50",
["salmon"] = "#FA8072",
["연어색"] = "#FA8072",
["skyblue"] = "#87CEEB",
["하늘색"] = "#87CEEB",
["lightblue"] = "#ADD8E6",
["연파랑"] = "#ADD8E6",
["darkblue"] = "#00008B",
["진파랑"] = "#00008B",
["darkgreen"] = "#006400",
["진초록"] = "#006400",
["lightgreen"] = "#90EE90",
["연초록"] = "#90EE90",
["indigo"] = "#4B0082",
["인디고"] = "#4B0082",
["violet"] = "#EE82EE",
["바이올렛"] = "#EE82EE",
["beige"] = "#F5F5DC",
["베이지"] = "#F5F5DC",
["ivory"] = "#FFFFF0",
["아이보리"] = "#FFFFF0",
["khaki"] = "#F0E68C",
["카키"] = "#F0E68C",
["lavender"] = "#E6E6FA",
["라벤더"] = "#E6E6FA",
["turquoise"] = "#40E0D0",
["터키옥"] = "#40E0D0",
["chocolate"] = "#D2691E",
["초콜릿"] = "#D2691E",
["crimson"] = "#DC143C",
["크림슨"] = "#DC143C",
["transparent"] = "#00000000"
};
private static readonly Regex _hexRe = new Regex("^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$");
private static readonly Regex _rgbRe = new Regex("^(?:rgb\\s*\\()?\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*\\)?$", RegexOptions.IgnoreCase);
private static readonly Regex _rgbaRe = new Regex("^rgba\\s*\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*([\\d.]+)\\s*\\)$", RegexOptions.IgnoreCase);
private static readonly Regex _hslRe = new Regex("^hsl\\s*\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)%?\\s*,\\s*([\\d.]+)%?\\s*\\)$", RegexOptions.IgnoreCase);
public string? Prefix => "color";
public PluginMetadata Metadata => new PluginMetadata("Color", "색상 변환기 — color #FF5500 · color 255,85,0 · color red", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(query))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("색상 코드를 입력하세요", "예: #FF5500 · 255,85,0 · red · hsl(20,100%,50%)", null, null, null, "\ue771")));
}
string text = query.Trim();
List<LauncherItem> list = new List<LauncherItem>();
if (_hexRe.IsMatch(text))
{
string text2 = text.TrimStart('#');
if (text2.Length == 3)
{
text2 = $"{text2[0]}{text2[0]}{text2[1]}{text2[1]}{text2[2]}{text2[2]}";
}
byte a = byte.MaxValue;
int r;
int g;
int b;
if (text2.Length == 8)
{
a = Convert.ToByte(text2.Substring(0, 2), 16);
r = Convert.ToInt32(text2.Substring(2, 2), 16);
g = Convert.ToInt32(text2.Substring(4, 2), 16);
b = Convert.ToInt32(text2.Substring(6, 2), 16);
}
else
{
r = Convert.ToInt32(text2.Substring(0, 2), 16);
g = Convert.ToInt32(text2.Substring(2, 2), 16);
b = Convert.ToInt32(text2.Substring(4, 2), 16);
}
AddColorItems(list, r, g, b, a, "#" + text2.ToUpperInvariant());
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
Match match = _rgbRe.Match(text);
if (match.Success)
{
int num = int.Parse(match.Groups[1].Value);
int num2 = int.Parse(match.Groups[2].Value);
int num3 = int.Parse(match.Groups[3].Value);
if (num <= 255 && num2 <= 255 && num3 <= 255)
{
AddColorItems(list, num, num2, num3, byte.MaxValue, $"rgb({num},{num2},{num3})");
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
}
Match match2 = _rgbaRe.Match(text);
if (match2.Success)
{
int num4 = int.Parse(match2.Groups[1].Value);
int num5 = int.Parse(match2.Groups[2].Value);
int num6 = int.Parse(match2.Groups[3].Value);
double num7 = double.Parse(match2.Groups[4].Value, CultureInfo.InvariantCulture);
byte a2 = (byte)Math.Clamp((int)(num7 * 255.0), 0, 255);
if (num4 <= 255 && num5 <= 255 && num6 <= 255)
{
AddColorItems(list, num4, num5, num6, a2, $"rgba({num4},{num5},{num6},{num7:G3})");
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
}
Match match3 = _hslRe.Match(text);
if (match3.Success)
{
double num8 = double.Parse(match3.Groups[1].Value, CultureInfo.InvariantCulture);
double num9 = double.Parse(match3.Groups[2].Value, CultureInfo.InvariantCulture) / 100.0;
double num10 = double.Parse(match3.Groups[3].Value, CultureInfo.InvariantCulture) / 100.0;
var (r2, g2, b2) = HslToRgb(num8, num9, num10);
AddColorItems(list, r2, g2, b2, byte.MaxValue, $"hsl({num8:G},{num9 * 100.0:G}%,{num10 * 100.0:G}%)");
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
if (_namedColors.TryGetValue(text, out string value))
{
string text3 = value.TrimStart('#');
int r3 = Convert.ToInt32(text3.Substring(0, 2), 16);
int g3 = Convert.ToInt32(text3.Substring(2, 2), 16);
int b3 = Convert.ToInt32(text3.Substring(4, 2), 16);
AddColorItems(list, r3, g3, b3, byte.MaxValue, value.ToUpperInvariant());
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
string searchQ = text.ToLowerInvariant();
IEnumerable<KeyValuePair<string, string>> enumerable = _namedColors.Where<KeyValuePair<string, string>>((KeyValuePair<string, string> kv) => kv.Key.Contains(searchQ, StringComparison.OrdinalIgnoreCase)).Take(8);
foreach (KeyValuePair<string, string> item in enumerable)
{
list.Add(new LauncherItem(item.Key + " → " + item.Value.ToUpperInvariant(), "Enter로 HEX 복사", null, item.Value.ToUpperInvariant(), null, "\ue771"));
}
if (!list.Any())
{
list.Add(new LauncherItem("인식할 수 없는 색상", "#RRGGBB · RGB(r,g,b) · hsl(h,s%,l%) · red 등 색상 이름", null, null, null, "\uea39"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try
{
Clipboard.SetText(text);
}
catch
{
}
}
return Task.CompletedTask;
}
private static void AddColorItems(List<LauncherItem> items, int r, int g, int b, byte a, string source)
{
string text = ((a == byte.MaxValue) ? $"#{r:X2}{g:X2}{b:X2}" : $"#{a:X2}{r:X2}{g:X2}{b:X2}");
string text2 = ((a == byte.MaxValue) ? $"rgb({r}, {g}, {b})" : $"rgba({r}, {g}, {b}, {(double)(int)a / 255.0:G3})");
(double h, double s, double l) tuple = RgbToHsl(r, g, b);
double item = tuple.h;
double item2 = tuple.s;
double item3 = tuple.l;
string text3 = $"hsl({item:G3}, {item2 * 100.0:G3}%, {item3 * 100.0:G3}%)";
(double h, double s, double v) tuple2 = RgbToHsv(r, g, b);
double item4 = tuple2.h;
double item5 = tuple2.s;
double item6 = tuple2.v;
string text4 = $"hsv({item4:G3}, {item5 * 100.0:G3}%, {item6 * 100.0:G3}%)";
items.Add(new LauncherItem("HEX → " + text.ToUpperInvariant(), source + " · Enter로 복사", null, text.ToUpperInvariant(), null, "\ue771"));
items.Add(new LauncherItem("RGB → " + text2, source + " · Enter로 복사", null, text2, null, "\ue771"));
items.Add(new LauncherItem("HSL → " + text3, source + " · Enter로 복사", null, text3, null, "\ue771"));
items.Add(new LauncherItem("HSV → " + text4, source + " · Enter로 복사", null, text4, null, "\ue771"));
}
private static (double h, double s, double l) RgbToHsl(int r, int g, int b)
{
double num = (double)r / 255.0;
double num2 = (double)g / 255.0;
double num3 = (double)b / 255.0;
double num4 = Math.Max(num, Math.Max(num2, num3));
double num5 = Math.Min(num, Math.Min(num2, num3));
double num6 = (num4 + num5) / 2.0;
double num7 = 0.0;
double value = 0.0;
if (num4 != num5)
{
double num8 = num4 - num5;
value = ((num6 > 0.5) ? (num8 / (2.0 - num4 - num5)) : (num8 / (num4 + num5)));
num7 = ((num4 == num) ? ((num2 - num3) / num8 + (double)((num2 < num3) ? 6 : 0)) : ((num4 == num2) ? ((num3 - num) / num8 + 2.0) : ((num - num2) / num8 + 4.0)));
num7 /= 6.0;
}
return (h: Math.Round(num7 * 360.0, 1), s: Math.Round(value, 4), l: Math.Round(num6, 4));
}
private static (double h, double s, double v) RgbToHsv(int r, int g, int b)
{
double num = (double)r / 255.0;
double num2 = (double)g / 255.0;
double num3 = (double)b / 255.0;
double num4 = Math.Max(num, Math.Max(num2, num3));
double num5 = Math.Min(num, Math.Min(num2, num3));
double value = num4;
double value2 = 0.0;
double num6 = 0.0;
double num7 = num4 - num5;
if (num4 != 0.0)
{
value2 = num7 / num4;
}
if (num7 != 0.0)
{
num6 = ((num4 == num) ? ((num2 - num3) / num7 + (double)((num2 < num3) ? 6 : 0)) : ((num4 == num2) ? ((num3 - num) / num7 + 2.0) : ((num - num2) / num7 + 4.0)));
num6 /= 6.0;
}
return (h: Math.Round(num6 * 360.0, 1), s: Math.Round(value2, 4), v: Math.Round(value, 4));
}
private static (int r, int g, int b) HslToRgb(double h, double s, double l)
{
double num;
double num2;
double num3;
if (s == 0.0)
{
num = (num2 = (num3 = l));
}
else
{
double num4 = ((l < 0.5) ? (l * (1.0 + s)) : (l + s - l * s));
double p = 2.0 * l - num4;
h /= 360.0;
num = Hue2Rgb(p, num4, h + 1.0 / 3.0);
num2 = Hue2Rgb(p, num4, h);
num3 = Hue2Rgb(p, num4, h - 1.0 / 3.0);
}
return (r: (int)Math.Round(num * 255.0), g: (int)Math.Round(num2 * 255.0), b: (int)Math.Round(num3 * 255.0));
}
private static double Hue2Rgb(double p, double q, double t)
{
if (t < 0.0)
{
t += 1.0;
}
if (t > 1.0)
{
t -= 1.0;
}
if (t < 1.0 / 6.0)
{
return p + (q - p) * 6.0 * t;
}
if (t < 0.5)
{
return q;
}
if (t < 2.0 / 3.0)
{
return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
}
return p;
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
public class ColorPickHandler : IActionHandler
{
public string? Prefix => "pick";
public PluginMetadata Metadata => new PluginMetadata("ColorPick", "스포이드 색상 추출 — pick", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("스포이드로 화면 색상 추출", "Enter → 스포이드 모드 진입 · 화면 아무 곳을 클릭하면 색상 코드 추출 · Esc 취소", null, "__PICK__", null, "\ue771")));
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
if (!(data is string s) || s != "__PICK__")
{
return;
}
await Task.Delay(200, ct);
Application current = Application.Current;
if (current == null)
{
return;
}
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
EyeDropperWindow eyeDropperWindow = new EyeDropperWindow();
eyeDropperWindow.ShowDialog();
if (eyeDropperWindow.PickedColor.HasValue)
{
ColorPickResultWindow colorPickResultWindow = new ColorPickResultWindow(eyeDropperWindow.PickedColor.Value, eyeDropperWindow.PickX, eyeDropperWindow.PickY);
colorPickResultWindow.Show();
}
});
}
}

View File

@@ -0,0 +1,135 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public static class CredentialManager
{
private struct FILETIME
{
public uint dwLowDateTime;
public uint dwHighDateTime;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct CREDENTIAL
{
public uint Flags;
public uint Type;
public nint TargetName;
public nint Comment;
public FILETIME LastWritten;
public uint CredentialBlobSize;
public nint CredentialBlob;
public uint Persist;
public uint AttributeCount;
public nint Attributes;
public nint TargetAlias;
public nint UserName;
}
private const uint CRED_TYPE_GENERIC = 1u;
private const uint CRED_PERSIST_LOCAL_MACHINE = 2u;
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CredRead(string target, uint type, uint flags, out nint credential);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CredWrite([In] ref CREDENTIAL userCredential, uint flags);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern void CredFree(nint cred);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CredDelete(string target, uint type, uint flags);
public static string? GetToken(string key)
{
if (string.IsNullOrEmpty(key))
{
return null;
}
try
{
if (CredRead(key, 1u, 0u, out var credential))
{
try
{
CREDENTIAL cREDENTIAL = Marshal.PtrToStructure<CREDENTIAL>(credential);
if (cREDENTIAL.CredentialBlobSize != 0 && cREDENTIAL.CredentialBlob != IntPtr.Zero)
{
return Marshal.PtrToStringUni(cREDENTIAL.CredentialBlob, (int)cREDENTIAL.CredentialBlobSize / 2);
}
}
finally
{
CredFree(credential);
}
}
}
catch (Exception ex)
{
LogService.Warn("Windows Credential Manager 읽기 실패 (" + key + "): " + ex.Message);
}
return Environment.GetEnvironmentVariable(key.ToUpperInvariant());
}
public static void SetToken(string key, string token)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(token))
{
return;
}
byte[] bytes = Encoding.Unicode.GetBytes(token);
nint num = Marshal.AllocHGlobal(bytes.Length);
nint num2 = Marshal.StringToCoTaskMemUni(key);
nint num3 = Marshal.StringToCoTaskMemUni(Environment.UserName);
try
{
Marshal.Copy(bytes, 0, num, bytes.Length);
CREDENTIAL userCredential = new CREDENTIAL
{
Type = 1u,
TargetName = num2,
UserName = num3,
CredentialBlobSize = (uint)bytes.Length,
CredentialBlob = num,
Persist = 2u
};
if (!CredWrite(ref userCredential, 0u))
{
LogService.Error($"토큰 저장 실패: {key}, 오류 코드: {Marshal.GetLastWin32Error()}");
}
else
{
LogService.Info("토큰 저장 완료: " + key);
}
}
finally
{
Marshal.FreeHGlobal(num);
Marshal.FreeCoTaskMem(num2);
Marshal.FreeCoTaskMem(num3);
}
}
public static bool DeleteToken(string key)
{
return !string.IsNullOrEmpty(key) && CredDelete(key, 1u, 0u);
}
}

View File

@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
internal static class CurrencyConverter
{
private static readonly Regex _pattern = new Regex("^(\\d+(?:\\.\\d+)?)\\s+([A-Za-z]{3})\\s+(?:to|in)\\s+([A-Za-z]{3})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Dictionary<string, (DateTime At, Dictionary<string, double> Rates)> _cache = new Dictionary<string, (DateTime, Dictionary<string, double>)>();
private static readonly HttpClient _http = new HttpClient
{
Timeout = TimeSpan.FromSeconds(5.0)
};
private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(1.0);
private static readonly Dictionary<string, string> _names = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["KRW"] = "원",
["USD"] = "달러",
["EUR"] = "유로",
["JPY"] = "엔",
["GBP"] = "파운드",
["CNY"] = "위안",
["HKD"] = "홍콩달러",
["SGD"] = "싱가포르달러",
["CAD"] = "캐나다달러",
["AUD"] = "호주달러",
["CHF"] = "스위스프랑",
["TWD"] = "대만달러",
["MXN"] = "멕시코페소",
["BRL"] = "브라질헤알",
["INR"] = "인도루피",
["RUB"] = "루블",
["THB"] = "바트",
["VND"] = "동",
["IDR"] = "루피아",
["MYR"] = "링깃",
["PHP"] = "페소",
["NZD"] = "뉴질랜드달러",
["SEK"] = "크로나",
["NOK"] = "크로나(노르)",
["DKK"] = "크로나(덴)",
["AED"] = "디르함",
["SAR"] = "리얄"
};
public static bool IsCurrencyQuery(string input)
{
return _pattern.IsMatch(input.Trim());
}
public static async Task<IEnumerable<LauncherItem>> ConvertAsync(string input, CancellationToken ct)
{
Match m = _pattern.Match(input.Trim());
if (!m.Success)
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("통화 형식 오류", "예: 100 USD to KRW", null, null, null, "\uea39"));
}
if (!double.TryParse(m.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var amount))
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("숫자 형식 오류", "", null, null, null, "\uea39"));
}
string from = m.Groups[2].Value.ToUpperInvariant();
string to = m.Groups[3].Value.ToUpperInvariant();
try
{
Dictionary<string, double> rates = await GetRatesAsync(from, ct);
if (rates == null)
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("환율 조회 실패", "네트워크 연결을 확인하세요", null, null, null, "\ue7ba"));
}
if (!rates.TryGetValue(to, out var rate))
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("지원하지 않는 통화: " + to, "3자리 ISO 4217 코드를 입력하세요", null, null, null, "\uea39"));
}
double result = amount * rate;
string fn;
string fromName = (_names.TryGetValue(from, out fn) ? fn : from);
string tn;
string toName = (_names.TryGetValue(to, out tn) ? tn : to);
string text;
switch (to)
{
default:
text = result.ToString("N2", CultureInfo.CurrentCulture);
break;
case "KRW":
case "JPY":
case "IDR":
case "VND":
text = result.ToString("N0", CultureInfo.CurrentCulture);
break;
}
string resultStr = text;
string rateStr = ((rate < 0.01) ? rate.ToString("G4", CultureInfo.InvariantCulture) : rate.ToString("N4", CultureInfo.CurrentCulture));
string display = resultStr + " " + to;
return new _003C_003Ez__ReadOnlyArray<LauncherItem>(new LauncherItem[2]
{
new LauncherItem($"{amount:N2} {from} = {display}", $"1 {from}({fromName}) = {rateStr} {to}({toName}) · Enter로 복사", null, display, null, "\ue8ef"),
new LauncherItem("숫자만: " + resultStr, "Enter로 숫자만 복사", null, resultStr, null, "\ue8ef")
});
}
catch (OperationCanceledException)
{
return Array.Empty<LauncherItem>();
}
catch (Exception ex2)
{
LogService.Warn("환율 조회 오류: " + ex2.Message);
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("환율 조회 실패", ex2.Message, null, null, null, "\ue7ba"));
}
}
private static async Task<Dictionary<string, double>?> GetRatesAsync(string baseCurrency, CancellationToken ct)
{
if (_cache.TryGetValue(baseCurrency, out var cached) && DateTime.Now - cached.At < CacheTtl)
{
return cached.Rates;
}
string url = "https://open.er-api.com/v6/latest/" + baseCurrency;
using JsonDocument doc = JsonDocument.Parse(await _http.GetStringAsync(url, ct));
JsonElement root = doc.RootElement;
if (!root.TryGetProperty("result", out var resultEl) || resultEl.GetString() != "success")
{
return null;
}
if (!root.TryGetProperty("rates", out var ratesEl))
{
return null;
}
Dictionary<string, double> rates = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
foreach (JsonProperty prop in ratesEl.EnumerateObject())
{
rates[prop.Name] = prop.Value.GetDouble();
}
_cache[baseCurrency] = (DateTime.Now, rates);
return rates;
}
}

View File

@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class DateCalcHandler : IActionHandler
{
private static readonly string[] DateFormats = new string[5] { "yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "MM/dd/yyyy", "dd-MM-yyyy" };
public string? Prefix => "date";
public PluginMetadata Metadata => new PluginMetadata("DateCalc", "날짜 계산 · D-day · 타임스탬프 변환", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
DateTime now = DateTime.Now;
List<LauncherItem> list = new List<LauncherItem>();
if (string.IsNullOrWhiteSpace(text))
{
string value = now.ToString("dddd", new CultureInfo("ko-KR"));
list.Add(Item($"{now:yyyy-MM-dd} ({value})", $"{now:HH:mm:ss} · {now:yyyy-MM-dd}"));
list.Add(Item($"유닉스 타임스탬프: {new DateTimeOffset(now).ToUnixTimeSeconds()}", "현재 시각의 Unix epoch"));
list.Add(Item($"올해 {now.DayOfYear}일째 / 남은 일: {(new DateTime(now.Year, 12, 31) - now).Days}일", $"ISO 주차: {ISOWeek.GetWeekOfYear(now)}주"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
Match match = Regex.Match(text, "^([+-])(\\d+)([dDwWmMyY])$");
if (match.Success)
{
int num = ((match.Groups[1].Value == "+") ? 1 : (-1));
int num2 = int.Parse(match.Groups[2].Value) * num;
string text2 = match.Groups[3].Value.ToLowerInvariant();
if (1 == 0)
{
}
DateTime dateTime = text2 switch
{
"d" => now.AddDays(num2),
"w" => now.AddDays(num2 * 7),
"m" => now.AddMonths(num2),
"y" => now.AddYears(num2),
_ => now,
};
if (1 == 0)
{
}
DateTime value2 = dateTime;
string value3 = value2.ToString("dddd", new CultureInfo("ko-KR"));
int days = (value2.Date - now.Date).Days;
string subtitle = ((days >= 0) ? $"오늘로부터 {days}일 후" : $"오늘로부터 {Math.Abs(days)}일 전");
list.Add(Item($"{value2:yyyy-MM-dd} ({value3})", subtitle));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
if (Regex.IsMatch(text, "^\\d{10,13}$") && long.TryParse(text, out var result))
{
long seconds = ((result > 9999999999L) ? (result / 1000) : result);
DateTime localDateTime = DateTimeOffset.FromUnixTimeSeconds(seconds).LocalDateTime;
string value4 = localDateTime.ToString("dddd", new CultureInfo("ko-KR"));
list.Add(Item($"{localDateTime:yyyy-MM-dd HH:mm:ss} ({value4})", $"Unix {result} → 로컬 시간"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
if (text.Equals("unix", StringComparison.OrdinalIgnoreCase) || text.Equals("to unix", StringComparison.OrdinalIgnoreCase))
{
long value5 = new DateTimeOffset(now).ToUnixTimeSeconds();
list.Add(Item($"{value5}", $"현재 시각 ({now:yyyy-MM-dd HH:mm:ss}) → Unix 타임스탬프"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
if (DateTime.TryParseExact(text, DateFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result2))
{
string value6 = result2.ToString("dddd", new CultureInfo("ko-KR"));
int days2 = (result2.Date - now.Date).Days;
if (1 == 0)
{
}
string text3 = ((days2 > 0) ? $"D-{days2} (앞으로 {days2}일)" : ((days2 != 0) ? $"D+{Math.Abs(days2)} ({Math.Abs(days2)}일 지남)" : "오늘"));
if (1 == 0)
{
}
string subtitle2 = text3;
list.Add(Item($"{result2:yyyy-MM-dd} ({value6})", subtitle2));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
list.Add(new LauncherItem("날짜 형식을 인식할 수 없습니다", "예: +30d, -100d, 2026-12-25, 1711584000, unix", null, null, null, "\ue7ba"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
private static LauncherItem Item(string title, string subtitle)
{
return new LauncherItem(title, subtitle + " · Enter로 복사", null, title, null, "\ue823");
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
string text = data as string;
if (text != null && !string.IsNullOrWhiteSpace(text))
{
try
{
Application current = Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(text);
});
}
}
catch
{
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Services;
using Microsoft.Win32;
namespace AxCopilot.Handlers;
public class DiffHandler : IActionHandler
{
private record DiffResult(string Text, int Added, int Removed, int Same);
private readonly ClipboardHistoryService? _clipHistory;
public string? Prefix => "diff";
public PluginMetadata Metadata => new PluginMetadata("Diff", "텍스트/파일 비교 — diff", "1.0", "AX");
public DiffHandler(ClipboardHistoryService? clipHistory = null)
{
_clipHistory = clipHistory;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
if (!string.IsNullOrWhiteSpace(text))
{
string[] array = text.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (array.Length == 2 && File.Exists(array[0]) && File.Exists(array[1]))
{
try
{
string textA = File.ReadAllText(array[0]);
string textB = File.ReadAllText(array[1]);
DiffResult diffResult = BuildDiff(textA, textB, Path.GetFileName(array[0]), Path.GetFileName(array[1]));
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("파일 비교: " + Path.GetFileName(array[0]) + " ↔ " + Path.GetFileName(array[1]), $"{diffResult.Added}줄 추가, {diffResult.Removed}줄 삭제, {diffResult.Same}줄 동일 · Enter로 결과 복사", null, diffResult.Text, null, "\ue8a5")));
}
catch (Exception ex)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("파일 읽기 실패: " + ex.Message, "", null, null, null, "\uea39")));
}
}
if (array.Length >= 1 && (File.Exists(array[0]) || Directory.Exists(Path.GetDirectoryName(array[0]) ?? "")))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("비교할 파일 2개의 경로를 입력하세요", "예: diff C:\\a.txt C:\\b.txt", null, null, null, "\ue946")));
}
}
IReadOnlyList<ClipboardEntry> readOnlyList = _clipHistory?.History;
if (readOnlyList == null || readOnlyList.Count < 2)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("비교할 텍스트가 부족합니다", "클립보드에 2개 이상의 텍스트를 복사하거나, diff [파일A] [파일B]로 파일 비교", null, null, null, "\ue946")));
}
List<ClipboardEntry> list = readOnlyList.Where((ClipboardEntry e) => e.IsText).Take(2).ToList();
if (list.Count < 2)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("텍스트 히스토리가 2개 미만입니다", "텍스트를 2번 이상 복사하세요", null, null, null, "\ue946")));
}
ClipboardEntry clipboardEntry = list[1];
ClipboardEntry clipboardEntry2 = list[0];
DiffResult diffResult2 = BuildDiff(clipboardEntry.Text, clipboardEntry2.Text, "이전 (" + clipboardEntry.RelativeTime + ")", "최근 (" + clipboardEntry2.RelativeTime + ")");
List<LauncherItem> list2 = new List<LauncherItem>
{
new LauncherItem($"클립보드 비교: +{diffResult2.Added} -{diffResult2.Removed} ={diffResult2.Same}", "이전 복사 ↔ 최근 복사 · Enter로 결과 복사", null, diffResult2.Text, null, "\ue81c")
};
IEnumerable<string> enumerable = (from l in diffResult2.Text.Split('\n')
where l.StartsWith("+ ") || l.StartsWith("- ")
select l).Take(5);
foreach (string item in enumerable)
{
string symbol = (item.StartsWith("+ ") ? "\ue710" : "\ue711");
list2.Add(new LauncherItem(item, "", null, null, null, symbol));
}
list2.Add(new LauncherItem("파일 선택하여 비교", "파일 선택 대화 상자에서 2개의 파일을 골라 비교합니다", null, "__FILE_DIALOG__", null, "\ue8a5"));
return Task.FromResult((IEnumerable<LauncherItem>)list2);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
if (data is string s && s == "__FILE_DIALOG__")
{
await Task.Delay(200, ct);
Application current = Application.Current;
if (current == null)
{
return;
}
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
OpenFileDialog openFileDialog = new OpenFileDialog
{
Title = "비교할 첫 번째 파일 선택",
Filter = "텍스트 파일|*.txt;*.cs;*.json;*.xml;*.md;*.csv;*.log|모든 파일|*.*"
};
if (openFileDialog.ShowDialog() == true)
{
string fileName = openFileDialog.FileName;
openFileDialog.Title = "비교할 두 번째 파일 선택";
if (openFileDialog.ShowDialog() == true)
{
string fileName2 = openFileDialog.FileName;
try
{
string textA = File.ReadAllText(fileName);
string textB = File.ReadAllText(fileName2);
DiffResult diffResult = BuildDiff(textA, textB, Path.GetFileName(fileName), Path.GetFileName(fileName2));
Clipboard.SetText(diffResult.Text);
NotificationService.Notify("파일 비교 완료", $"+{diffResult.Added} -{diffResult.Removed} ={diffResult.Same} · 결과 클립보드 복사됨");
}
catch (Exception ex)
{
NotificationService.Notify("비교 실패", ex.Message);
}
}
}
});
return;
}
data = item.Data;
string text = data as string;
if (text == null || string.IsNullOrWhiteSpace(text))
{
return;
}
try
{
Application current2 = Application.Current;
if (current2 != null)
{
((DispatcherObject)current2).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(text);
});
}
}
catch
{
}
NotificationService.Notify("비교 결과", "클립보드에 복사되었습니다");
}
private static DiffResult BuildDiff(string textA, string textB, string labelA, string labelB)
{
string[] array = (from l in textA.Split('\n')
select l.TrimEnd('\r')).ToArray();
string[] array2 = (from l in textB.Split('\n')
select l.TrimEnd('\r')).ToArray();
HashSet<string> hashSet = new HashSet<string>(array);
HashSet<string> hashSet2 = new HashSet<string>(array2);
StringBuilder stringBuilder = new StringBuilder();
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder2);
handler.AppendLiteral("--- ");
handler.AppendFormatted(labelA);
stringBuilder3.AppendLine(ref handler);
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(4, 1, stringBuilder2);
handler.AppendLiteral("+++ ");
handler.AppendFormatted(labelB);
stringBuilder4.AppendLine(ref handler);
stringBuilder.AppendLine();
int num = 0;
int num2 = 0;
int num3 = 0;
int num4 = Math.Max(array.Length, array2.Length);
for (int num5 = 0; num5 < num4; num5++)
{
string text = ((num5 < array.Length) ? array[num5] : null);
string text2 = ((num5 < array2.Length) ? array2[num5] : null);
if (text == text2)
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder5 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2);
handler.AppendLiteral(" ");
handler.AppendFormatted(text);
stringBuilder5.AppendLine(ref handler);
num3++;
continue;
}
if (text != null && !hashSet2.Contains(text))
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder6 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2);
handler.AppendLiteral("- ");
handler.AppendFormatted(text);
stringBuilder6.AppendLine(ref handler);
num2++;
}
if (text2 != null && !hashSet.Contains(text2))
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder7 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2);
handler.AppendLiteral("+ ");
handler.AppendFormatted(text2);
stringBuilder7.AppendLine(ref handler);
num++;
}
if (text != null && hashSet2.Contains(text) && text != text2)
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder8 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(2, 1, stringBuilder2);
handler.AppendLiteral(" ");
handler.AppendFormatted(text);
stringBuilder8.AppendLine(ref handler);
num3++;
}
}
return new DiffResult(stringBuilder.ToString(), num, num2, num3);
}
}

View File

@@ -0,0 +1,517 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class EmojiHandler : IActionHandler
{
private static readonly (string Emoji, string Name, string Tags)[] _emojis = new(string, string, string)[462]
{
("\ud83d\ude00", "크게 웃는 얼굴", "smile happy grin 웃음 행복"),
("\ud83d\ude03", "웃는 얼굴", "smile happy joy 웃음"),
("\ud83d\ude04", "눈 웃음", "smile laugh 웃음 기쁨"),
("\ud83d\ude01", "히죽 웃음", "grin beam 씩 웃다"),
("\ud83d\ude06", "크게 웃음", "laughing 폭소"),
("\ud83d\ude05", "식은땀 웃음", "sweat smile 안도"),
("\ud83e\udd23", "바닥 구르며 웃음", "rofl lol 빵 웃음"),
("\ud83d\ude02", "눈물 나게 웃음", "joy tears laugh 폭소"),
("\ud83d\ude42", "살짝 웃음", "slightly smiling 미소"),
("\ud83d\ude43", "거꾸로 웃음", "upside down 뒤집힌"),
("\ud83d\ude09", "윙크", "wink 윙크"),
("\ud83d\ude0a", "볼 빨개진 웃음", "blush 부끄러움 미소"),
("\ud83d\ude07", "천사", "angel halo 천사 선량"),
("\ud83e\udd70", "사랑스러운 얼굴", "love hearts 사랑 하트"),
("\ud83d\ude0d", "하트 눈", "heart eyes 사랑 반함"),
("\ud83e\udd29", "별 눈", "star struck 감동 황홀"),
("\ud83d\ude18", "뽀뽀", "kiss blow 키스 뽀뽀"),
("\ud83d\ude17", "오므린 입", "kiss whistle 키스"),
("\ud83d\ude1a", "눈 감고 뽀뽀", "kiss 키스"),
("\ud83d\ude19", "볼 뽀뽀", "kiss 키스"),
("\ud83d\ude0b", "맛있다", "yum delicious 맛 음식"),
("\ud83d\ude1b", "혀 내밀기", "tongue out 혀 놀림"),
("\ud83d\ude1c", "윙크하며 혀", "wink tongue 장난"),
("\ud83e\udd2a", "미친 표정", "zany crazy 정신없음"),
("\ud83d\ude1d", "눈 감고 혀", "tongue 혀"),
("\ud83e\udd11", "돈 눈", "money face 돈 부자"),
("\ud83e\udd17", "포옹", "hugging hug 안아줘 포옹"),
("\ud83e\udd2d", "입 가리고", "hand over mouth 헉 깜짝"),
("\ud83e\udd2b", "쉿", "shushing quiet 조용 쉿"),
("\ud83e\udd14", "생각 중", "thinking 고민 생각"),
("\ud83e\udd10", "입 막음", "zipper mouth 비밀"),
("\ud83e\udd28", "의심", "raised eyebrow 의심 의아"),
("\ud83d\ude10", "무표정", "neutral 무감각 무표정"),
("\ud83d\ude11", "표정 없음", "expressionless 냉담"),
("\ud83d\ude36", "입 없는 얼굴", "no mouth 침묵"),
("\ud83d\ude0f", "비웃음", "smirk 비웃 냉소"),
("\ud83d\ude12", "불만", "unamused 불만 짜증"),
("\ud83d\ude44", "눈 굴리기", "eye roll 어이없음"),
("\ud83d\ude2c", "이 드러냄", "grimace 으 민망"),
("\ud83e\udd25", "거짓말", "lying pinocchio 거짓말"),
("\ud83d\ude0c", "안도/평온", "relieved 안도 평온"),
("\ud83d\ude14", "슬픔", "pensive sad 슬픔 우울"),
("\ud83d\ude2a", "졸림", "sleepy 졸음"),
("\ud83e\udd24", "침 흘림", "drooling 군침 식욕"),
("\ud83d\ude34", "잠", "sleeping sleep 수면 잠"),
("\ud83d\ude37", "마스크", "mask sick 마스크 아픔"),
("\ud83e\udd12", "열 나는", "sick fever 열 아픔"),
("\ud83e\udd15", "머리 붕대", "injured hurt 부상"),
("\ud83e\udd22", "구역질", "nauseated sick 구역 메스꺼움"),
("\ud83e\udd2e", "토하는", "vomit 구토"),
("\ud83e\udd27", "재채기", "sneezing sick 재채기 감기"),
("\ud83e\udd75", "더운", "hot overheated 더움 열"),
("\ud83e\udd76", "추운", "cold freezing 추움 냉기"),
("\ud83e\udd74", "어지러운", "woozy 어지럼 취함"),
("\ud83d\ude35", "어질어질", "dizzy 어지럼 충격"),
("\ud83e\udd2f", "머리 폭발", "exploding head 충격 대박"),
("\ud83e\udd20", "카우보이", "cowboy hat 카우보이"),
("\ud83e\udd78", "변장", "disguise 변장 선글라스"),
("\ud83d\ude0e", "쿨한", "cool sunglasses 선글라스 쿨"),
("\ud83e\udd13", "공부벌레", "nerd glasses 공부 안경"),
("\ud83e\uddd0", "모노클", "monocle curious 고상 탐정"),
("\ud83d\ude15", "당황", "confused 당황 모호"),
("\ud83d\ude1f", "걱정", "worried concern 걱정"),
("\ud83d\ude41", "살짝 찡그림", "frown 슬픔"),
("☹\ufe0f", "찡그린 얼굴", "frown sad 슬픔"),
("\ud83d\ude2e", "입 벌림", "open mouth surprised 놀람"),
("\ud83d\ude2f", "놀람", "hushed surprised 깜짝"),
("\ud83d\ude32", "충격", "astonished 충격 놀람"),
("\ud83d\ude33", "얼굴 빨개짐", "flushed embarrassed 부끄럼 당황"),
("\ud83e\udd7a", "애원", "pleading eyes 부탁 눈빛"),
("\ud83d\ude26", "찡그리며 벌린 입", "frowning 불안"),
("\ud83d\ude27", "고통", "anguished 고통"),
("\ud83d\ude28", "무서움", "fearful scared 무서움 공포"),
("\ud83d\ude30", "식은땀", "anxious sweat 불안 걱정"),
("\ud83d\ude25", "눈물 조금", "sad disappointed 실망 눈물"),
("\ud83d\ude22", "울음", "cry sad 슬픔 눈물"),
("\ud83d\ude2d", "엉엉 울음", "loudly crying sob 통곡"),
("\ud83d\ude31", "공포에 질림", "screaming fear 비명 공포"),
("\ud83d\ude16", "혼란", "confounded 혼란"),
("\ud83d\ude23", "힘듦", "persevering 고생"),
("\ud83d\ude1e", "실망", "disappointed 실망"),
("\ud83d\ude13", "땀", "downcast sweat 땀 힘듦"),
("\ud83d\ude29", "피곤", "weary tired 지침 피곤"),
("\ud83d\ude2b", "극도로 지침", "tired exhausted 탈진"),
("\ud83e\udd71", "하품", "yawning bored 하품 지루함"),
("\ud83d\ude24", "콧김", "triumph snort 분노 콧김"),
("\ud83d\ude21", "화남", "angry mad 화남 분노"),
("\ud83d\ude20", "성남", "angry 화 성남"),
("\ud83e\udd2c", "욕", "cursing swearing 욕 분노"),
("\ud83d\ude08", "나쁜 미소", "smiling devil 악마 장난"),
("\ud83d\udc7f", "화난 악마", "angry devil 악마"),
("\ud83d\udc80", "해골", "skull death 해골 죽음"),
("☠\ufe0f", "해골 십자", "skull crossbones 독"),
("\ud83d\udca9", "응가", "poop 똥 응가"),
("\ud83e\udd21", "피에로", "clown 광대"),
("\ud83d\udc79", "도깨비", "ogre 도깨비 귀신"),
("\ud83d\udc7a", "텐구", "goblin 텐구"),
("\ud83d\udc7b", "유령", "ghost 유령 귀신"),
("\ud83d\udc7e", "우주인", "alien monster 외계인 게임"),
("\ud83e\udd16", "로봇", "robot 로봇"),
("\ud83d\udc4b", "손 흔들기", "wave waving hi bye 안녕"),
("\ud83e\udd1a", "손 뒤", "raised back hand 손"),
("\ud83d\udd90\ufe0f", "손바닥", "hand palm 다섯 손가락"),
("✋", "손 들기", "raised hand 손 들기 멈춤"),
("\ud83d\udd96", "스팍 손인사", "vulcan salute 스타트렉"),
("\ud83d\udc4c", "오케이", "ok perfect 오케이 좋아"),
("\ud83e\udd0c", "손가락 모아", "pinched fingers 이탈리아"),
("✌\ufe0f", "브이", "victory peace v 브이 평화"),
("\ud83e\udd1e", "행운 손가락", "crossed fingers lucky 행운 기도"),
("\ud83e\udd1f", "아이 러브 유", "love you 사랑해"),
("\ud83e\udd18", "록 손", "rock on metal 록"),
("\ud83e\udd19", "전화해", "call me shaka 전화 샤카"),
("\ud83d\udc48", "왼쪽 가리킴", "backhand left 왼쪽"),
("\ud83d\udc49", "오른쪽 가리킴", "backhand right 오른쪽"),
("\ud83d\udc46", "위 가리킴", "backhand up 위"),
("\ud83d\udd95", "욕", "middle finger 욕"),
("\ud83d\udc47", "아래 가리킴", "backhand down 아래"),
("☝\ufe0f", "검지 들기", "index pointing up 하나 포인트"),
("\ud83d\udc4d", "좋아요", "thumbs up like good 좋아 최고"),
("\ud83d\udc4e", "싫어요", "thumbs down dislike 싫어 별로"),
("✊", "주먹", "fist punch 주먹"),
("\ud83d\udc4a", "주먹 치기", "punch fist 주먹"),
("\ud83e\udd1b", "왼 주먹", "left fist 주먹"),
("\ud83e\udd1c", "오른 주먹", "right fist 주먹"),
("\ud83d\udc4f", "박수", "clapping applause 박수 응원"),
("\ud83d\ude4c", "만세", "raising hands celebrate 만세"),
("\ud83d\udc50", "양손 펼침", "open hands 환영"),
("\ud83e\udd32", "두 손 모음", "palms up together 기도 바람"),
("\ud83d\ude4f", "두 손 합장", "pray please thanks 감사 부탁 기도"),
("✍\ufe0f", "글쓰기", "writing pen 글쓰기"),
("\ud83d\udc85", "네일", "nail polish manicure 네일 손톱"),
("\ud83e\udd33", "셀카", "selfie 셀카"),
("\ud83d\udcaa", "근육", "muscle strong 근육 힘"),
("\ud83e\uddbe", "기계 팔", "mechanical arm 로봇 팔"),
("\ud83e\uddbf", "기계 다리", "mechanical leg 로봇 다리"),
("\ud83e\uddb5", "다리", "leg kick 다리"),
("\ud83e\uddb6", "발", "foot kick 발"),
("\ud83d\udc42", "귀", "ear hear 귀"),
("\ud83e\uddbb", "보청기 귀", "ear hearing aid 보청기"),
("\ud83d\udc43", "코", "nose smell 코"),
("\ud83e\udec0", "심장", "heart anatomical 심장"),
("\ud83e\udec1", "폐", "lungs 폐"),
("\ud83e\udde0", "뇌", "brain mind 뇌 지능"),
("\ud83e\uddb7", "치아", "tooth dental 치아"),
("\ud83e\uddb4", "뼈", "bone 뼈"),
("\ud83d\udc40", "눈", "eyes look see 눈 보기"),
("\ud83d\udc41\ufe0f", "한쪽 눈", "eye 눈"),
("\ud83d\udc45", "혀", "tongue 혀"),
("\ud83d\udc44", "입술", "lips mouth 입술"),
("\ud83d\udc8b", "입맞춤", "kiss lips 키스 입술"),
("\ud83e\ude78", "피", "blood drop 피 혈액"),
("❤\ufe0f", "빨간 하트", "red heart love 사랑 빨강"),
("\ud83e\udde1", "주황 하트", "orange heart 사랑"),
("\ud83d\udc9b", "노란 하트", "yellow heart 사랑"),
("\ud83d\udc9a", "초록 하트", "green heart 사랑"),
("\ud83d\udc99", "파란 하트", "blue heart 사랑"),
("\ud83d\udc9c", "보라 하트", "purple heart 사랑"),
("\ud83d\udda4", "검은 하트", "black heart 사랑 다크"),
("\ud83e\udd0d", "흰 하트", "white heart 사랑"),
("\ud83e\udd0e", "갈색 하트", "brown heart 사랑"),
("\ud83d\udc94", "깨진 하트", "broken heart 이별 상처"),
("❣\ufe0f", "느낌표 하트", "heart exclamation 사랑"),
("\ud83d\udc95", "두 하트", "two hearts 사랑"),
("\ud83d\udc9e", "회전 하트", "revolving hearts 사랑"),
("\ud83d\udc93", "뛰는 하트", "beating heart 설렘"),
("\ud83d\udc97", "성장 하트", "growing heart 사랑"),
("\ud83d\udc96", "반짝 하트", "sparkling heart 사랑"),
("\ud83d\udc98", "화살 하트", "heart arrow 큐피드"),
("\ud83d\udc9d", "리본 하트", "heart ribbon 선물 사랑"),
("\ud83d\udc9f", "하트 장식", "heart decoration 사랑"),
("☮\ufe0f", "평화", "peace 평화"),
("✝\ufe0f", "십자가", "cross 기독교"),
("☯\ufe0f", "음양", "yin yang 음양 균형"),
("\ud83d\udd2e", "수정구", "crystal ball magic 마법 점"),
("✨", "반짝임", "sparkles glitter 빛 반짝"),
("⭐", "별", "star 별"),
("\ud83c\udf1f", "빛나는 별", "glowing star 별빛"),
("\ud83d\udcab", "현기증", "dizzy star 빙글"),
("⚡", "번개", "lightning bolt 번개 전기"),
("\ud83d\udd25", "불", "fire hot 불 열정"),
("\ud83d\udca5", "폭발", "explosion boom 폭발"),
("❄\ufe0f", "눈송이", "snowflake cold 눈 추위"),
("\ud83c\udf08", "무지개", "rainbow 무지개"),
("☀\ufe0f", "태양", "sun sunny 태양 맑음"),
("\ud83c\udf19", "달", "moon crescent 달"),
("\ud83c\udf0a", "파도", "wave ocean 파도 바다"),
("\ud83d\udca8", "바람", "wind dash 바람"),
("\ud83d\udca6", "물방울", "sweat droplets water 물"),
("\ud83c\udf38", "벚꽃", "cherry blossom 벚꽃 봄"),
("\ud83c\udf39", "장미", "rose 장미 꽃"),
("\ud83c\udf3a", "히비스커스", "hibiscus 꽃"),
("\ud83c\udf3b", "해바라기", "sunflower 해바라기"),
("\ud83c\udf3c", "꽃", "blossom flower 꽃"),
("\ud83c\udf37", "튤립", "tulip 튤립"),
("\ud83d\udc90", "꽃다발", "bouquet flowers 꽃다발"),
("\ud83c\udf40", "네잎클로버", "four leaf clover lucky 행운"),
("\ud83c\udf3f", "허브", "herb green 풀 허브"),
("\ud83c\udf43", "잎사귀", "leaf 잎"),
("\ud83c\udf55", "피자", "pizza 피자"),
("\ud83c\udf54", "햄버거", "hamburger burger 버거"),
("\ud83c\udf2e", "타코", "taco 타코"),
("\ud83c\udf5c", "라면", "ramen noodles 라면 국수"),
("\ud83c\udf71", "도시락", "bento box 도시락"),
("\ud83c\udf63", "초밥", "sushi 초밥"),
("\ud83c\udf5a", "밥", "rice 밥"),
("\ud83c\udf5b", "카레", "curry rice 카레"),
("\ud83c\udf5d", "파스타", "pasta spaghetti 파스타"),
("\ud83c\udf66", "소프트 아이스크림", "ice cream soft serve 아이스크림"),
("\ud83c\udf82", "생일 케이크", "cake birthday 생일 케이크"),
("\ud83c\udf70", "케이크 조각", "cake slice 케이크"),
("\ud83e\uddc1", "컵케이크", "cupcake 컵케이크"),
("\ud83c\udf69", "도넛", "donut 도넛"),
("\ud83c\udf6a", "쿠키", "cookie 쿠키"),
("\ud83c\udf6b", "초콜릿", "chocolate bar 초콜릿"),
("\ud83c\udf6c", "사탕", "candy 사탕"),
("\ud83c\udf6d", "막대 사탕", "lollipop 막대사탕"),
("\ud83c\udf7a", "맥주", "beer mug 맥주"),
("\ud83c\udf7b", "건배", "clinking beer 건배"),
("\ud83e\udd42", "샴페인 건배", "champagne 샴페인 건배"),
("\ud83c\udf77", "와인", "wine 와인"),
("☕", "커피", "coffee hot 커피"),
("\ud83e\uddc3", "주스", "juice 주스"),
("\ud83e\udd64", "음료", "drink cup 음료 컵"),
("\ud83e\uddcb", "버블티", "bubble tea boba 버블티"),
("\ud83c\udf75", "녹차", "tea matcha 차 녹차"),
("\ud83d\udc36", "강아지", "dog puppy 강아지 개"),
("\ud83d\udc31", "고양이", "cat kitten 고양이"),
("\ud83d\udc2d", "쥐", "mouse 쥐"),
("\ud83d\udc39", "햄스터", "hamster 햄스터"),
("\ud83d\udc30", "토끼", "rabbit bunny 토끼"),
("\ud83e\udd8a", "여우", "fox 여우"),
("\ud83d\udc3b", "곰", "bear 곰"),
("\ud83d\udc3c", "판다", "panda 판다"),
("\ud83d\udc28", "코알라", "koala 코알라"),
("\ud83d\udc2f", "호랑이", "tiger 호랑이"),
("\ud83e\udd81", "사자", "lion 사자"),
("\ud83d\udc2e", "소", "cow 소"),
("\ud83d\udc37", "돼지", "pig 돼지"),
("\ud83d\udc38", "개구리", "frog 개구리"),
("\ud83d\udc35", "원숭이", "monkey 원숭이"),
("\ud83d\ude48", "눈 가린 원숭이", "see no evil monkey 안 봐"),
("\ud83d\ude49", "귀 가린 원숭이", "hear no evil monkey 안 들어"),
("\ud83d\ude4a", "입 가린 원숭이", "speak no evil monkey 안 말해"),
("\ud83d\udc14", "닭", "chicken 닭"),
("\ud83d\udc27", "펭귄", "penguin 펭귄"),
("\ud83d\udc26", "새", "bird 새"),
("\ud83e\udd86", "오리", "duck 오리"),
("\ud83e\udd85", "독수리", "eagle 독수리"),
("\ud83e\udd89", "부엉이", "owl 부엉이"),
("\ud83d\udc0d", "뱀", "snake 뱀"),
("\ud83d\udc22", "거북이", "turtle 거북이"),
("\ud83e\udd8b", "나비", "butterfly 나비"),
("\ud83d\udc0c", "달팽이", "snail 달팽이"),
("\ud83d\udc1b", "애벌레", "bug caterpillar 애벌레"),
("\ud83d\udc1d", "꿀벌", "bee honeybee 벌"),
("\ud83e\udd91", "오징어", "squid 오징어"),
("\ud83d\udc19", "문어", "octopus 문어"),
("\ud83d\udc20", "열대어", "tropical fish 열대어"),
("\ud83d\udc21", "복어", "blowfish puffer 복어"),
("\ud83e\udd88", "상어", "shark 상어"),
("\ud83d\udc2c", "돌고래", "dolphin 돌고래"),
("\ud83d\udc33", "고래", "whale 고래"),
("\ud83d\udc32", "용", "dragon 용"),
("\ud83e\udd84", "유니콘", "unicorn 유니콘"),
("\ud83d\udcf1", "스마트폰", "phone mobile smartphone 폰"),
("\ud83d\udcbb", "노트북", "laptop computer 노트북"),
("\ud83d\udda5\ufe0f", "데스크톱", "desktop computer 컴퓨터"),
("⌨\ufe0f", "키보드", "keyboard 키보드"),
("\ud83d\uddb1\ufe0f", "마우스", "mouse 마우스"),
("\ud83d\udda8\ufe0f", "프린터", "printer 프린터"),
("\ud83d\udcf7", "카메라", "camera 카메라"),
("\ud83d\udcf8", "플래시 카메라", "camera flash 사진"),
("\ud83d\udcf9", "비디오 카메라", "video camera 동영상"),
("\ud83c\udfa5", "영화 카메라", "movie camera film 영화"),
("\ud83d\udcfa", "TV", "television tv 텔레비전"),
("\ud83d\udcfb", "라디오", "radio 라디오"),
("\ud83c\udf99\ufe0f", "마이크", "microphone studio 마이크"),
("\ud83c\udfa4", "마이크 핸드헬드", "microphone karaoke 마이크"),
("\ud83c\udfa7", "헤드폰", "headphones 헤드폰"),
("\ud83d\udce1", "안테나", "satellite antenna 안테나"),
("\ud83d\udd0b", "배터리", "battery 배터리"),
("\ud83d\udd0c", "전원 플러그", "plug electric 플러그"),
("\ud83d\udca1", "전구", "bulb idea light 전구 아이디어"),
("\ud83d\udd26", "손전등", "flashlight torch 손전등"),
("\ud83d\udd6f\ufe0f", "양초", "candle 양초"),
("\ud83d\udcda", "책", "books stack 책"),
("\ud83d\udcd6", "열린 책", "open book read 독서"),
("\ud83d\udcdd", "메모", "memo note pencil 메모 노트"),
("✏\ufe0f", "연필", "pencil 연필"),
("\ud83d\udd8a\ufe0f", "펜", "pen 펜"),
("\ud83d\udccc", "압정", "pushpin pin 압정"),
("\ud83d\udcce", "클립", "paperclip 클립"),
("✂\ufe0f", "가위", "scissors cut 가위"),
("\ud83d\uddc2\ufe0f", "파일 폴더", "card index dividers folder 파일"),
("\ud83d\udcc1", "폴더", "folder 폴더"),
("\ud83d\udcc2", "열린 폴더", "open folder 폴더"),
("\ud83d\uddc3\ufe0f", "파일 박스", "card file box 서류함"),
("\ud83d\uddd1\ufe0f", "휴지통", "wastebasket trash 휴지통"),
("\ud83d\udd12", "잠금", "locked lock 잠금"),
("\ud83d\udd13", "열림", "unlocked 열림"),
("\ud83d\udd11", "열쇠", "key 열쇠"),
("\ud83d\udddd\ufe0f", "구식 열쇠", "old key 열쇠"),
("\ud83d\udd28", "망치", "hammer 망치"),
("\ud83d\udd27", "렌치", "wrench tool 렌치"),
("\ud83d\udd29", "나사", "nut bolt 나사"),
("⚙\ufe0f", "톱니바퀴", "gear settings 설정 톱니"),
("\ud83d\udee0\ufe0f", "도구", "tools hammer wrench 도구"),
("\ud83d\udc8a", "알약", "pill medicine 약 알약"),
("\ud83d\udc89", "주사기", "syringe injection 주사"),
("\ud83e\ude7a", "청진기", "stethoscope doctor 청진기"),
("\ud83c\udfc6", "트로피", "trophy award 트로피 우승"),
("\ud83e\udd47", "금메달", "first gold medal 금메달"),
("\ud83e\udd48", "은메달", "second silver 은메달"),
("\ud83e\udd49", "동메달", "third bronze 동메달"),
("\ud83c\udf96\ufe0f", "훈장", "medal military 훈장"),
("\ud83c\udf97\ufe0f", "리본", "ribbon awareness 리본"),
("\ud83c\udfab", "티켓", "ticket admission 티켓"),
("\ud83c\udf9f\ufe0f", "입장권", "admission tickets 티켓"),
("\ud83c\udfaa", "서커스", "circus tent 서커스"),
("\ud83c\udfa8", "팔레트", "art palette paint 그림 예술"),
("\ud83c\udfad", "연극", "performing arts theater 연극"),
("\ud83c\udfac", "클래퍼보드", "clapper film 영화 촬영"),
("\ud83c\udfae", "게임 컨트롤러", "video game controller 게임"),
("\ud83c\udfb2", "주사위", "dice game 주사위"),
("\ud83c\udfaf", "다트", "bullseye target dart 다트 목표"),
("\ud83c\udfb3", "볼링", "bowling 볼링"),
("⚽", "축구", "soccer football 축구"),
("\ud83c\udfc0", "농구", "basketball 농구"),
("\ud83c\udfc8", "미식축구", "american football 미식축구"),
("⚾", "야구", "baseball 야구"),
("\ud83c\udfbe", "테니스", "tennis 테니스"),
("\ud83c\udfd0", "배구", "volleyball 배구"),
("\ud83c\udfc9", "럭비", "rugby 럭비"),
("\ud83c\udfb1", "당구", "billiards pool 당구"),
("\ud83c\udfd3", "탁구", "ping pong table tennis 탁구"),
("\ud83c\udff8", "배드민턴", "badminton 배드민턴"),
("\ud83e\udd4a", "권투 장갑", "boxing glove 권투"),
("\ud83c\udfa3", "낚시", "fishing 낚시"),
("\ud83c\udfcb\ufe0f", "역도", "weightlifting gym 헬스 역도"),
("\ud83e\uddd8", "명상", "yoga meditation 명상 요가"),
("\ud83d\ude97", "자동차", "car automobile 자동차"),
("\ud83d\ude95", "택시", "taxi cab 택시"),
("\ud83d\ude99", "SUV", "suv car 차"),
("\ud83d\ude8c", "버스", "bus 버스"),
("\ud83d\ude8e", "무궤도 전차", "trolleybus 버스"),
("\ud83c\udfce\ufe0f", "레이싱카", "racing car 레이싱"),
("\ud83d\ude93", "경찰차", "police car 경찰"),
("\ud83d\ude91", "구급차", "ambulance 구급차"),
("\ud83d\ude92", "소방차", "fire truck 소방차"),
("\ud83d\ude90", "미니밴", "minibus van 밴"),
("\ud83d\ude9a", "트럭", "truck delivery 트럭"),
("✈\ufe0f", "비행기", "airplane flight plane 비행기"),
("\ud83d\ude80", "로켓", "rocket space launch 로켓"),
("\ud83d\udef8", "UFO", "flying saucer ufo 유에프오"),
("\ud83d\ude81", "헬리콥터", "helicopter 헬리콥터"),
("\ud83d\ude82", "기차", "train locomotive 기차"),
("\ud83d\ude86", "고속열차", "train 기차"),
("\ud83d\ude87", "지하철", "metro subway 지하철"),
("⛵", "돛단배", "sailboat 요트"),
("\ud83d\udea2", "배", "ship cruise 배"),
("\ud83d\udeb2", "자전거", "bicycle bike 자전거"),
("\ud83d\udef5", "스쿠터", "scooter moped 스쿠터"),
("\ud83c\udfcd\ufe0f", "오토바이", "motorcycle 오토바이"),
("\ud83c\udfe0", "집", "house home 집"),
("\ud83c\udfe1", "마당 있는 집", "house garden 집"),
("\ud83c\udfe2", "빌딩", "office building 빌딩"),
("\ud83c\udfe3", "우체국", "post office 우체국"),
("\ud83c\udfe5", "병원", "hospital 병원"),
("\ud83c\udfe6", "은행", "bank 은행"),
("\ud83c\udfe8", "호텔", "hotel 호텔"),
("\ud83c\udfeb", "학교", "school 학교"),
("\ud83c\udfea", "편의점", "convenience store shop 편의점"),
("\ud83c\udfec", "백화점", "department store 백화점"),
("\ud83c\udff0", "성", "castle 성"),
("⛪", "교회", "church 교회"),
("\ud83d\udd4c", "모스크", "mosque 모스크"),
("\ud83d\uddfc", "에펠탑", "eiffel tower paris 파리"),
("\ud83d\uddfd", "자유의 여신상", "statue of liberty new york 뉴욕"),
("\ud83c\udfd4\ufe0f", "산", "mountain snow 산"),
("\ud83c\udf0b", "화산", "volcano 화산"),
("\ud83d\uddfb", "후지산", "mount fuji japan 후지산"),
("\ud83c\udfd5\ufe0f", "캠핑", "camping tent 캠핑"),
("\ud83c\udfd6\ufe0f", "해변", "beach summer 해변 해수욕"),
("\ud83c\udf0f", "지구", "earth globe asia 지구"),
("\ud83d\udcaf", "100점", "hundred percent perfect 완벽 100"),
("\ud83d\udd22", "숫자", "numbers 숫자"),
("\ud83c\udd97", "OK", "ok button 오케이"),
("\ud83c\udd99", "업", "up button 업"),
("\ud83c\udd92", "쿨", "cool button 쿨"),
("\ud83c\udd95", "새것", "new button 새"),
("\ud83c\udd93", "무료", "free button 무료"),
("\ud83c\udd98", "SOS", "sos emergency 긴급 구조"),
("⚠\ufe0f", "경고", "warning caution 경고 주의"),
("\ud83d\udeab", "금지", "prohibited no 금지"),
("✅", "체크", "check mark done 완료 확인"),
("❌", "엑스", "x cross error 실패 오류"),
("❓", "물음표", "question mark 물음표"),
("❗", "느낌표", "exclamation mark 느낌표"),
("", "더하기", "plus add 더하기"),
("", "빼기", "minus subtract 빼기"),
("➗", "나누기", "divide 나누기"),
("✖\ufe0f", "곱하기", "multiply times 곱하기"),
("♾\ufe0f", "무한대", "infinity 무한"),
("\ud83d\udd01", "반복", "repeat loop 반복"),
("\ud83d\udd00", "셔플", "shuffle random 랜덤"),
("▶\ufe0f", "재생", "play 재생"),
("⏸\ufe0f", "일시정지", "pause 일시정지"),
("⏹\ufe0f", "정지", "stop 정지"),
("⏩", "빨리 감기", "fast forward 빨리감기"),
("⏪", "되감기", "rewind 되감기"),
("\ud83d\udd14", "알림", "bell notification 알림 벨"),
("\ud83d\udd15", "알림 끔", "bell off 알림끔"),
("\ud83d\udd0a", "볼륨 크게", "loud speaker volume up 볼륨"),
("\ud83d\udd07", "음소거", "muted speaker 음소거"),
("\ud83d\udce3", "메가폰", "megaphone loud 확성기"),
("\ud83d\udce2", "스피커", "loudspeaker 스피커"),
("\ud83d\udcac", "말풍선", "speech bubble chat 대화"),
("\ud83d\udcad", "생각 말풍선", "thought bubble thinking 생각"),
("\ud83d\udce7", "이메일", "email mail 이메일 메일"),
("\ud83d\udce8", "수신 봉투", "incoming envelope 수신"),
("\ud83d\udce9", "발신 봉투", "envelope outbox 발신"),
("\ud83d\udcec", "우편함", "mailbox 우편함"),
("\ud83d\udce6", "택배 박스", "package box parcel 택배 상자"),
("\ud83c\udf81", "선물", "gift present 선물"),
("\ud83c\udf80", "리본 묶음", "ribbon bow 리본"),
("\ud83c\udf8a", "색종이", "confetti 파티 축하"),
("\ud83c\udf89", "파티 폭죽", "party popper celebrate 파티 축하"),
("\ud83c\udf88", "풍선", "balloon party 풍선"),
("\ud83d\udd50", "1시", "one o'clock 1시 시간"),
("\ud83d\udd52", "3시", "three o'clock 3시 시간"),
("\ud83d\udd54", "4시", "four o'clock 4시 시간"),
("⏰", "알람 시계", "alarm clock 알람 시계"),
("⏱\ufe0f", "스톱워치", "stopwatch timer 스톱워치 타이머"),
("\ud83d\udcc5", "달력", "calendar date 달력 날짜"),
("\ud83d\udcc6", "찢는 달력", "tear-off calendar 달력"),
("\ud83d\udcb0", "돈 가방", "money bag 돈 부자"),
("\ud83d\udcb3", "신용카드", "credit card payment 카드 결제"),
("\ud83d\udcb5", "달러", "dollar banknote 달러"),
("\ud83d\udcb4", "엔화", "yen banknote 엔"),
("\ud83d\udcb6", "유로", "euro banknote 유로"),
("\ud83d\udcb7", "파운드", "pound banknote 파운드"),
("\ud83d\udcca", "막대 그래프", "bar chart graph 그래프"),
("\ud83d\udcc8", "상승 그래프", "chart increasing trend 상승 트렌드"),
("\ud83d\udcc9", "하락 그래프", "chart decreasing trend 하락"),
("\ud83d\udd0d", "돋보기", "magnifying glass search 검색 돋보기"),
("\ud83d\udd0e", "오른쪽 돋보기", "magnifying glass right search 검색"),
("\ud83c\udff3\ufe0f", "흰 깃발", "white flag 항복"),
("\ud83c\udff4", "검은 깃발", "black flag 해적"),
("\ud83d\udea9", "빨간 삼각기", "triangular flag 경고 깃발"),
("\ud83c\udfc1", "체크무늬 깃발", "chequered flag finish race 결승"),
("\ud83c\udf10", "지구본", "globe internet web 인터넷 웹"),
("⚓", "닻", "anchor 닻"),
("\ud83c\udfb5", "음표", "music note 음악 음표"),
("\ud83c\udfb6", "음표들", "musical notes 음악"),
("\ud83c\udfbc", "악보", "musical score 악보"),
("\ud83c\udfb9", "피아노", "piano keyboard 피아노"),
("\ud83c\udfb8", "기타", "guitar 기타"),
("\ud83e\udd41", "드럼", "drum 드럼"),
("\ud83e\ude97", "아코디언", "accordion 아코디언"),
("\ud83c\udfb7", "색소폰", "saxophone 색소폰"),
("\ud83c\udfba", "트럼펫", "trumpet 트럼펫"),
("\ud83c\udfbb", "바이올린", "violin 바이올린")
};
public string? Prefix => "emoji";
public PluginMetadata Metadata => new PluginMetadata("Emoji", "이모지 피커 — emoji 뒤에 이름 입력", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
IEnumerable<(string, string, string)> source;
if (string.IsNullOrWhiteSpace(query))
{
source = _emojis.Take(30);
}
else
{
string q = query.Trim().ToLowerInvariant();
source = _emojis.Where(((string Emoji, string Name, string Tags) e) => e.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Tags.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Emoji.Contains(q)).Take(20);
}
List<LauncherItem> list = source.Select<(string, string, string), LauncherItem>(((string Emoji, string Name, string Tags) e) => new LauncherItem(e.Emoji + " " + e.Name, "Enter로 클립보드에 복사", null, e.Emoji, null, "\ue76e")).ToList();
if (!list.Any() && !string.IsNullOrWhiteSpace(query))
{
list.Add(new LauncherItem("검색 결과 없음", "'" + query + "'에 해당하는 이모지가 없습니다", null, null, null, "\ue946"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try
{
Clipboard.SetText(text);
}
catch
{
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class EncodeHandler : IActionHandler
{
public string? Prefix => "encode ";
public PluginMetadata Metadata => new PluginMetadata("Encoder", "인코딩/해싱 — encode base64/url/hex/md5/sha256 텍스트", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
if (string.IsNullOrWhiteSpace(text))
{
return Task.FromResult(HelpItems());
}
string text2;
if (text.StartsWith("decode ", StringComparison.OrdinalIgnoreCase))
{
text2 = text;
string rest = text2.Substring(7, text2.Length - 7).Trim();
return Task.FromResult(HandleDecode(rest));
}
int num = text.IndexOf(' ');
if (num < 0)
{
return Task.FromResult(HelpItems());
}
string type = text.Substring(0, num).Trim().ToLowerInvariant();
text2 = text;
int num2 = num + 1;
string input = text2.Substring(num2, text2.Length - num2);
return Task.FromResult(HandleEncode(type, input));
}
private static IEnumerable<LauncherItem> HandleEncode(string type, string input)
{
try
{
if (1 == 0)
{
}
IEnumerable<LauncherItem> result;
switch (type)
{
case "base64":
case "b64":
result = SingleResult("Base64 인코딩", Convert.ToBase64String(Encoding.UTF8.GetBytes(input)));
break;
case "url":
result = SingleResult("URL 인코딩", Uri.EscapeDataString(input));
break;
case "hex":
result = SingleResult("HEX 인코딩", Convert.ToHexString(Encoding.UTF8.GetBytes(input)).ToLowerInvariant());
break;
case "md5":
result = SingleResult("MD5 해시", MD5Hash(input));
break;
case "sha1":
result = SingleResult("SHA-1 해시", SHA1Hash(input));
break;
case "sha256":
result = SingleResult("SHA-256 해시", SHA256Hash(input));
break;
case "sha512":
result = SingleResult("SHA-512 해시", SHA512Hash(input));
break;
case "html":
result = SingleResult("HTML 엔티티 인코딩", WebUtility.HtmlEncode(input));
break;
default:
result = new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("알 수 없는 타입: " + type, "base64, url, hex, md5, sha1, sha256, sha512, html 중 선택", null, null, null, "\ue7ba"));
break;
}
if (1 == 0)
{
}
return result;
}
catch (Exception ex)
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("변환 오류", ex.Message, null, null, null, "\uea39"));
}
}
private static IEnumerable<LauncherItem> HandleDecode(string rest)
{
int num = rest.IndexOf(' ');
if (num < 0)
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("사용법: encode decode <타입> <값>", "타입: base64, url, hex, html", null, null, null, "\ue946"));
}
string text = rest.Substring(0, num).Trim().ToLowerInvariant();
int num2 = num + 1;
string text2 = rest.Substring(num2, rest.Length - num2);
try
{
if (1 == 0)
{
}
IEnumerable<LauncherItem> result;
switch (text)
{
case "base64":
case "b64":
result = SingleResult("Base64 디코딩", Encoding.UTF8.GetString(Convert.FromBase64String(text2)));
break;
case "url":
result = SingleResult("URL 디코딩", Uri.UnescapeDataString(text2));
break;
case "hex":
result = SingleResult("HEX 디코딩", Encoding.UTF8.GetString(Convert.FromHexString(text2)));
break;
case "html":
result = SingleResult("HTML 엔티티 디코딩", WebUtility.HtmlDecode(text2));
break;
default:
result = new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("디코딩 불가 타입: " + text, "디코딩 지원: base64, url, hex, html", null, null, null, "\ue7ba"));
break;
}
if (1 == 0)
{
}
return result;
}
catch (Exception ex)
{
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("디코딩 오류", ex.Message, null, null, null, "\uea39"));
}
}
private static IEnumerable<LauncherItem> SingleResult(string title, string result)
{
string text = ((result.Length > 80) ? (result.Substring(0, 77) + "…") : result);
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem(title, text + " · Enter로 복사", null, result, null, "\ue8cb"));
}
private static IEnumerable<LauncherItem> HelpItems()
{
return new _003C_003Ez__ReadOnlyArray<LauncherItem>(new LauncherItem[7]
{
new LauncherItem("encode base64 <텍스트>", "Base64 인코딩/디코딩", null, null, null, "\ue8cb"),
new LauncherItem("encode url <텍스트>", "URL 퍼센트 인코딩/디코딩", null, null, null, "\ue8cb"),
new LauncherItem("encode hex <텍스트>", "16진수 인코딩/디코딩", null, null, null, "\ue8cb"),
new LauncherItem("encode md5 <텍스트>", "MD5 해시 (단방향)", null, null, null, "\ue8cb"),
new LauncherItem("encode sha256 <텍스트>", "SHA-256 해시 (단방향)", null, null, null, "\ue8cb"),
new LauncherItem("encode html <텍스트>", "HTML 엔티티 인코딩/디코딩", null, null, null, "\ue8cb"),
new LauncherItem("encode decode base64 <값>", "Base64 디코딩 예시", null, null, null, "\ue8cb")
});
}
private static string MD5Hash(string input)
{
byte[] inArray = MD5.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(inArray).ToLowerInvariant();
}
private static string SHA1Hash(string input)
{
byte[] inArray = SHA1.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(inArray).ToLowerInvariant();
}
private static string SHA256Hash(string input)
{
byte[] inArray = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(inArray).ToLowerInvariant();
}
private static string SHA512Hash(string input)
{
byte[] inArray = SHA512.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(inArray).ToLowerInvariant();
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try
{
Clipboard.SetText(text);
}
catch
{
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class EnvHandler : IActionHandler
{
private static readonly string[] _priorityKeys = new string[22]
{
"PATH", "JAVA_HOME", "PYTHON_HOME", "NODE_HOME", "GOPATH", "GOROOT", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP",
"TMP", "COMPUTERNAME", "USERNAME", "USERDOMAIN", "OS", "PROCESSOR_ARCHITECTURE", "SYSTEMROOT", "WINDIR", "PROGRAMFILES", "PROGRAMFILES(X86)",
"COMMONPROGRAMFILES", "NUMBER_OF_PROCESSORS"
};
public string? Prefix => "env";
public PluginMetadata Metadata => new PluginMetadata("EnvVars", "환경변수 조회 — env 뒤에 변수명 입력", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim();
List<(string Key, string Value)> allVars = (from DictionaryEntry e in Environment.GetEnvironmentVariables()
select (Key: e.Key?.ToString() ?? "", Value: e.Value?.ToString() ?? "") into x
where !string.IsNullOrEmpty(x.Key)
select x).ToList();
IEnumerable<(string, string)> source;
if (string.IsNullOrWhiteSpace(q))
{
HashSet<string> prioritySet = new HashSet<string>(_priorityKeys, StringComparer.OrdinalIgnoreCase);
IEnumerable<(string, string)> first = from k in _priorityKeys
where allVars.Any(((string Key, string Value) v) => string.Equals(v.Key, k, StringComparison.OrdinalIgnoreCase))
select allVars.First(((string Key, string Value) v) => string.Equals(v.Key, k, StringComparison.OrdinalIgnoreCase));
IOrderedEnumerable<(string, string)> second = from v in allVars
where !prioritySet.Contains(v.Key)
orderby v.Key
select v;
source = first.Concat(second).Take(20);
}
else
{
source = (from v in allVars
where v.Key.Contains(q, StringComparison.OrdinalIgnoreCase) || v.Value.Contains(q, StringComparison.OrdinalIgnoreCase)
orderby (!v.Key.StartsWith(q, StringComparison.OrdinalIgnoreCase)) ? 1 : 0, v.Key
select v).Take(20);
}
List<LauncherItem> list = source.Select<(string, string), LauncherItem>(delegate((string Key, string Value) v)
{
string text = ((v.Value.Length > 80) ? (v.Value.Substring(0, 77) + "…") : v.Value);
if (v.Key.Equals("PATH", StringComparison.OrdinalIgnoreCase) && v.Value.Contains(';'))
{
text = v.Value.Split(';')[0] + $" (외 {v.Value.Split(';').Length - 1}개)";
}
return new LauncherItem(v.Key, text + " · Enter로 값 복사", null, v.Value, null, "\ue8d7");
}).ToList();
if (!list.Any())
{
list.Add(new LauncherItem("'" + q + "' — 환경변수 없음", "해당 이름의 환경변수를 찾을 수 없습니다", null, null, null, "\ue7ba"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try
{
Clipboard.SetText(text);
}
catch
{
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,289 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class EverythingHandler : IActionHandler
{
private const int EVERYTHING_OK = 0;
private const int EVERYTHING_REQUEST_FILE_NAME = 1;
private const int EVERYTHING_REQUEST_PATH = 2;
private const int EVERYTHING_REQUEST_SIZE = 16;
private bool? _isAvailable;
public string? Prefix => "es";
public PluginMetadata Metadata => new PluginMetadata("EverythingSearch", "Everything 초고속 파일 검색 — es [키워드]", "1.0", "AX");
private bool IsAvailable
{
get
{
if (_isAvailable.HasValue)
{
return _isAvailable.Value;
}
try
{
Everything_GetMajorVersion();
_isAvailable = true;
}
catch
{
_isAvailable = false;
}
return _isAvailable.Value;
}
}
[DllImport("Everything64.dll", CharSet = CharSet.Unicode)]
private static extern uint Everything_SetSearchW(string lpSearchString);
[DllImport("Everything64.dll")]
private static extern void Everything_SetMax(uint dwMax);
[DllImport("Everything64.dll")]
private static extern void Everything_SetRequestFlags(uint dwRequestFlags);
[DllImport("Everything64.dll")]
private static extern bool Everything_QueryW(bool bWait);
[DllImport("Everything64.dll")]
private static extern uint Everything_GetNumResults();
[DllImport("Everything64.dll")]
private static extern uint Everything_GetLastError();
[DllImport("Everything64.dll", CharSet = CharSet.Unicode)]
private static extern nint Everything_GetResultFullPathNameW(uint nIndex, nint lpString, uint nMaxCount);
[DllImport("Everything64.dll", CharSet = CharSet.Unicode)]
private static extern void Everything_GetResultSize(uint nIndex, out long lpFileSize);
[DllImport("Everything64.dll")]
private static extern uint Everything_GetMajorVersion();
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (!IsAvailable)
{
return Task.FromResult((IEnumerable<LauncherItem>)new LauncherItem[1]
{
new LauncherItem("Everything이 설치되어 있지 않습니다", "voidtools.com에서 Everything을 설치하면 초고속 파일 검색을 사용할 수 있습니다", null, null, null, "\ue7ba")
});
}
string text = query.Trim();
if (string.IsNullOrWhiteSpace(text))
{
return Task.FromResult((IEnumerable<LauncherItem>)new LauncherItem[1]
{
new LauncherItem("Everything 파일 검색", "검색어를 입력하세요 — 파일명, 확장자(*.xlsx), 경로 일부 등", null, null, null, "\ue721")
});
}
try
{
Everything_SetSearchW(text);
Everything_SetMax(30u);
Everything_SetRequestFlags(19u);
if (!Everything_QueryW(bWait: true))
{
return Task.FromResult((IEnumerable<LauncherItem>)new LauncherItem[1]
{
new LauncherItem("Everything 검색 실패", $"오류 코드: {Everything_GetLastError()} — Everything 서비스가 실행 중인지 확인하세요", null, null, null, "\ue7ba")
});
}
uint num = Everything_GetNumResults();
if (num == 0)
{
return Task.FromResult((IEnumerable<LauncherItem>)new LauncherItem[1]
{
new LauncherItem("검색 결과 없음: " + text, "다른 키워드나 와일드카드(*.xlsx)를 시도해 보세요", null, null, null, "\ue946")
});
}
List<LauncherItem> list = new List<LauncherItem>();
nint num2 = Marshal.AllocHGlobal(1040);
try
{
for (uint num3 = 0u; num3 < num && num3 < 30; num3++)
{
Everything_GetResultFullPathNameW(num3, num2, 520u);
string text2 = Marshal.PtrToStringUni(num2) ?? "";
Everything_GetResultSize(num3, out var lpFileSize);
string fileName = Path.GetFileName(text2);
string text3 = Path.GetDirectoryName(text2) ?? "";
string text4 = ((lpFileSize > 0) ? FormatSize(lpFileSize) : "");
string subtitle = (string.IsNullOrEmpty(text4) ? text3 : (text4 + " · " + text3));
string symbol = (Directory.Exists(text2) ? "\ue8b7" : GetFileSymbol(text2));
list.Add(new LauncherItem(fileName, subtitle, null, text2, null, symbol));
}
}
finally
{
Marshal.FreeHGlobal(num2);
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
catch (Exception ex)
{
LogService.Warn("Everything 검색 오류: " + ex.Message);
return Task.FromResult((IEnumerable<LauncherItem>)new LauncherItem[1]
{
new LauncherItem("Everything 검색 오류", ex.Message, null, null, null, "\ue7ba")
});
}
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
string path = data as string;
if (path == null || string.IsNullOrEmpty(path))
{
return;
}
try
{
if (Directory.Exists(path))
{
Process.Start(new ProcessStartInfo
{
FileName = path,
UseShellExecute = true
});
}
else if (File.Exists(path))
{
Process.Start(new ProcessStartInfo
{
FileName = path,
UseShellExecute = true
});
}
}
catch (Exception ex)
{
Exception ex2 = ex;
LogService.Warn("Everything 결과 열기 실패: " + ex2.Message);
}
await Task.CompletedTask;
}
private static string FormatSize(long bytes)
{
if (bytes >= 1024)
{
if (bytes >= 1048576)
{
if (bytes >= 1073741824)
{
return $"{(double)bytes / 1073741824.0:F2} GB";
}
return $"{(double)bytes / 1048576.0:F1} MB";
}
return $"{(double)bytes / 1024.0:F1} KB";
}
return $"{bytes} B";
}
private static string GetFileSymbol(string path)
{
string text = Path.GetExtension(path).ToLowerInvariant();
if (1 == 0)
{
}
string result;
switch (text)
{
case ".xlsx":
case ".xls":
case ".csv":
result = "\ue9f9";
break;
case ".docx":
case ".doc":
result = "\ue8a5";
break;
case ".pptx":
case ".ppt":
result = "\uee71";
break;
case ".pdf":
result = "\uea90";
break;
case ".png":
case ".jpg":
case ".jpeg":
case ".gif":
case ".bmp":
case ".svg":
result = "\ueb9f";
break;
case ".mp4":
case ".avi":
case ".mkv":
case ".mov":
result = "\ue714";
break;
case ".mp3":
case ".wav":
case ".flac":
result = "\uec4f";
break;
case ".zip":
case ".rar":
case ".7z":
case ".tar":
case ".gz":
result = "\ue8c6";
break;
case ".exe":
case ".msi":
result = "\uecaa";
break;
case ".cs":
case ".py":
case ".js":
case ".ts":
case ".java":
case ".cpp":
case ".c":
case ".go":
result = "\ue943";
break;
case ".json":
case ".xml":
case ".yaml":
case ".yml":
result = "\ue713";
break;
case ".txt":
case ".md":
case ".log":
result = "\ue8d2";
break;
case ".html":
case ".htm":
case ".css":
result = "\ue774";
break;
default:
result = "\ue8a5";
break;
}
if (1 == 0)
{
}
return result;
}
}

View File

@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class FavoriteHandler : IActionHandler
{
private class FavEntry
{
[JsonPropertyName("name")]
public string Name { get; set; } = "";
[JsonPropertyName("path")]
public string Path { get; set; } = "";
}
private static readonly string FavFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favorites.json");
private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNameCaseInsensitive = true
};
private List<FavEntry> _cache = new List<FavEntry>();
private bool _loaded;
public string? Prefix => "fav";
public PluginMetadata Metadata => new PluginMetadata("Favorite", "즐겨찾기 관리 — fav", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
EnsureLoaded();
string q = query.Trim();
if (q.StartsWith("add ", StringComparison.OrdinalIgnoreCase))
{
string text = q;
string text2 = text.Substring(4, text.Length - 4).Trim();
int num = text2.IndexOf(' ');
if (num > 0)
{
string text3 = text2.Substring(0, num).Trim();
text = text2;
int num2 = num + 1;
string text4 = text.Substring(num2, text.Length - num2).Trim();
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("즐겨찾기 추가: " + text3, "경로: " + text4 + " · Enter로 추가", null, ValueTuple.Create("__ADD__", text3, text4), null, "\ue74e")));
}
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("사용법: fav add [이름] [경로]", "예: fav add 보고서 C:\\work\\report.xlsx", null, null, null, "\ue946")));
}
if (q.StartsWith("del ", StringComparison.OrdinalIgnoreCase))
{
string text = q;
string name = text.Substring(4, text.Length - 4).Trim();
FavEntry favEntry = _cache.FirstOrDefault((FavEntry b) => b.Name.Contains(name, StringComparison.OrdinalIgnoreCase));
if (favEntry != null)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("즐겨찾기 삭제: " + favEntry.Name, "경로: " + favEntry.Path + " · Enter로 삭제", null, ValueTuple.Create("__DEL__", favEntry.Name), null, "\ue74d")));
}
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("'" + name + "'에 해당하는 즐겨찾기 없음", "fav으로 전체 목록 확인", null, null, null, "\ue7ba")));
}
List<FavEntry> source = (string.IsNullOrWhiteSpace(q) ? _cache : _cache.Where((FavEntry b) => b.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || b.Path.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList());
if (!source.Any())
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem((_cache.Count == 0) ? "즐겨찾기가 없습니다" : ("'" + q + "'에 해당하는 즐겨찾기 없음"), "fav add [이름] [경로]로 추가하세요", null, null, null, "\ue946")));
}
List<LauncherItem> result = source.Select(delegate(FavEntry b)
{
bool flag = Directory.Exists(b.Path);
bool flag2 = File.Exists(b.Path);
string symbol = (flag ? "\ue8b7" : (flag2 ? "\ue8a5" : "\ue7ba"));
string text5 = (flag ? "폴더 열기" : (flag2 ? "파일 열기" : "경로를 찾을 수 없음"));
return new LauncherItem(b.Name, b.Path + " · " + text5, null, b.Path, null, symbol);
}).ToList();
return Task.FromResult((IEnumerable<LauncherItem>)result);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is (string, string, string) { Item1: "__ADD__" } tuple)
{
AddFav(tuple.Item2, tuple.Item3);
NotificationService.Notify("AX Copilot", "즐겨찾기 추가: " + tuple.Item2);
return Task.CompletedTask;
}
if (item.Data is (string, string) { Item1: "__DEL__" } tuple2)
{
RemoveFav(tuple2.Item2);
NotificationService.Notify("AX Copilot", "즐겨찾기 삭제: " + tuple2.Item2);
return Task.CompletedTask;
}
object data = item.Data;
string path = data as string;
if (path != null)
{
if (Directory.Exists(path) || File.Exists(path))
{
try
{
Process.Start(new ProcessStartInfo(path)
{
UseShellExecute = true
});
}
catch (Exception ex)
{
LogService.Warn("즐겨찾기 열기 실패: " + ex.Message);
}
}
else
{
try
{
Application current = Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(path);
});
}
}
catch
{
}
}
}
return Task.CompletedTask;
}
private void EnsureLoaded()
{
if (_loaded)
{
return;
}
_loaded = true;
try
{
if (File.Exists(FavFile))
{
_cache = JsonSerializer.Deserialize<List<FavEntry>>(File.ReadAllText(FavFile), JsonOpts) ?? new List<FavEntry>();
}
}
catch (Exception ex)
{
LogService.Warn("즐겨찾기 로드 실패: " + ex.Message);
}
}
private void AddFav(string name, string path)
{
EnsureLoaded();
_cache.RemoveAll((FavEntry b) => b.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
_cache.Insert(0, new FavEntry
{
Name = name,
Path = path
});
Save();
}
private void RemoveFav(string name)
{
EnsureLoaded();
_cache.RemoveAll((FavEntry b) => b.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
Save();
}
private void Save()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(FavFile));
File.WriteAllText(FavFile, JsonSerializer.Serialize(_cache, JsonOpts));
}
catch (Exception ex)
{
LogService.Warn("즐겨찾기 저장 실패: " + ex.Message);
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class FolderAliasHandler : IActionHandler
{
private readonly SettingsService _settings;
public string? Prefix => "cd";
public PluginMetadata Metadata => new PluginMetadata("folder-alias", "폴더 별칭", "1.0", "AX");
public FolderAliasHandler(SettingsService settings)
{
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
IEnumerable<LauncherItem> result = from a in _settings.Settings.Aliases
where a.Type == "folder" && (string.IsNullOrEmpty(query) || a.Key.Contains(query, StringComparison.OrdinalIgnoreCase))
select new LauncherItem(a.Key, Environment.ExpandEnvironmentVariables(a.Target), null, a, null, "\ue8b7");
return Task.FromResult(result);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is AliasEntry aliasEntry)
{
string arguments = Environment.ExpandEnvironmentVariables(aliasEntry.Target);
Process.Start(new ProcessStartInfo("explorer.exe", arguments)
{
UseShellExecute = true
});
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
public class HelpHandler : IActionHandler
{
private record HelpEntry(string Category, string Command, string Title, string Description, string Example, string Symbol, string ColorHex);
private readonly SettingsService? _settings;
private static readonly HelpEntry[] _baseEntries = new HelpEntry[47]
{
new HelpEntry("검색", "", "앱 · 파일 · 북마크 퍼지 검색", "앱 이름, 파일명, 한국어 초성, Chrome·Edge 북마크를 통합 검색", "chrome / 보고서 / ㅅㄷ (설정) / 북마크 제목", "\ue721", "#0078D4"),
new HelpEntry("계산", "=", "계산기", "수식 계산 · 단위 변환 · 실시간 환율", "= sqrt(144) / = 100 USD to KRW / = 15km to miles", "\ue8ef", "#4B5EFC"),
new HelpEntry("검색", "?", "웹 검색 (10개 엔진)", "?n 네이버 · ?g 구글 · ?y 유튜브 · ?gh 깃허브 · ?d DuckDuckGo · ?w 위키피디아 · ?nw 나무위키 · ?nm 네이버지도 · ?ni 네이버이미지 · ?gi 구글이미지", "? 오늘 날씨 / ?n 뉴스 / ?g python / ?nw 검색어 / ?y 음악 / ?gh axios", "\ue774", "#006EAF"),
new HelpEntry("클립보드", "#", "클립보드 히스토리", "복사 이력 검색 & 재사용 · Shift+↑↓로 여러 항목 병합", "# / # 회의록 / (Shift+↑↓ 선택 후 Shift+Enter 병합)", "\ue81c", "#B7791F"),
new HelpEntry("클립보드", "$", "클립보드 텍스트 변환 (12종)", "현재 클립보드 내용을 즉시 변환", "$json / $b64e / $upper / $url / $md5 / $trim", "\ue77f", "#8764B8"),
new HelpEntry("텍스트", ";", "텍스트 스니펫", "저장된 템플릿 불러오기 · 변수 치환 지원", ";addr / ;sig / ;greet", "\ue70b", "#0F6CBD"),
new HelpEntry("단축키", "@", "URL 단축키", "저장해둔 URL을 키워드로 바로 열기", "@gh / @notion / @jira", "\ue774", "#0078D4"),
new HelpEntry("단축키", "cd", "폴더 단축키", "저장해둔 폴더를 키워드로 바로 열기", "cd dl / cd work / cd desktop", "\ue8b7", "#107C10"),
new HelpEntry("단축키", ">", "터미널 명령 실행", "PowerShell 명령을 AX Commander에서 직접 실행", "> git status / > ipconfig / > cls", "\ue756", "#323130"),
new HelpEntry("창관리", "~", "워크스페이스 저장·복원", "현재 창 배치를 스냅샷으로 저장하고 언제든 복원", "~save 업무 / ~restore 업무 / ~list", "\ue8a1", "#C50F1F"),
new HelpEntry("창관리", "snap", "창 분할 레이아웃", "창을 화면의 특정 영역에 즉시 스냅", "snap left / snap right / snap tl / snap full", "\ue8a0", "#B45309"),
new HelpEntry("창관리", "cap", "스크린샷 캡처 (예약어 변경 가능)", "영역 선택 · 활성 창 · 스크롤 · 전체 화면. Shift+Enter로 지연 캡처(3/5/10초 타이머). 결과는 클립보드에 복사. 글로벌 단축키(PrintScreen 등)로 바로 캡처 가능. 설정 → 캡처 탭에서 예약어·단축키·스크롤 속도 변경", "cap region / cap window / cap scroll / cap screen / Shift+Enter → 지연 캡처", "\ue722", "#BE185D"),
new HelpEntry("시스템", "/", "시스템 명령", "잠금·절전·재시작·종료·타이머·알람", "/lock / /sleep / /shutdown / /timer 5m / /alarm 14:30", "\ue7e8", "#4A4A4A"),
new HelpEntry("시스템", "info · *", "시스템 정보", "IP · 배터리 · 볼륨 · 가동시간 · CPU · 디스크. `*` 입력으로도 동일하게 사용 가능", "info / info ip / info battery / * / * ip", "\ue7f4", "#5B4E7E"),
new HelpEntry("알림", "", "잠금 해제 사용시간 알림", "PC 잠금 해제 시 오늘 누적 사용 시간과 격려 문구·명언 팝업 표시. 설정 → 알림 탭에서 활성화", "설정 → 알림 탭: 활성화 토글 / 표시 위치(4방향) / 표시 간격(30분~4시간) / 자동 닫힘(5초~3분)", "\uea8f", "#EA8F00"),
new HelpEntry("시스템", "kill", "프로세스 종료", "프로세스 이름으로 검색 후 강제 종료", "kill chrome / kill node / kill teams", "\uea39", "#CC2222"),
new HelpEntry("시스템", "media", "미디어 제어", "재생·일시정지·이전·다음·볼륨 조절", "media play / media next / media vol+ / media mute", "\ue768", "#1A6B3C"),
new HelpEntry("개발", "json", "JSON 포맷 · 검증", "클립보드의 JSON을 정렬·압축·유효성 검사", "json → format / minify / validate", "\ue930", "#D97706"),
new HelpEntry("개발", "encode", "인코딩 · 해싱", "Base64 · URL · HTML · UTF-8 · MD5 · SHA256", "encode base64 / encode url / encode sha256", "\ue8cb", "#6366F1"),
new HelpEntry("개발", "color", "색상 변환", "HEX ↔ RGB ↔ HSL ↔ HSV 변환 및 색상명 지원", "color #FF5733 / color 255,87,51 / color red", "\ue771", "#EC4899"),
new HelpEntry("개발", "port", "포트 · 프로세스 조회", "포트 번호로 점유 프로세스 확인", "port 3000 / port 8080 / port 443", "\ue968", "#006699"),
new HelpEntry("개발", "env", "환경변수 조회", "시스템 환경변수 검색 및 클립보드 복사", "env / env PATH / env JAVA", "\ue8d7", "#0D9488"),
new HelpEntry("앱", "emoji", "이모지 피커", "300개+ 이모지 검색 · Enter로 클립보드 복사", "emoji / emoji 하트 / emoji wave / emoji 불꽃", "\ue76e", "#F59E0B"),
new HelpEntry("앱", "recent", "최근 파일", "Windows 최근 파일 목록 검색 & 바로 열기", "recent / recent 보고서 / recent xlsx", "\ue81c", "#059669"),
new HelpEntry("앱", "note", "빠른 메모", "간단한 메모를 저장하고 불러오기", "note 내일 회의 10시 / note", "\ue70b", "#7C3AED"),
new HelpEntry("앱", "uninstall", "앱 제거", "설치된 앱 검색 후 제거", "uninstall / uninstall kakao / uninstall zoom", "\ue74d", "#DC2626"),
new HelpEntry("유틸", "pick", "스포이드 색상 추출", "화면 아무 곳을 클릭하여 HEX 색상 코드 추출 · 돋보기로 실시간 미리보기 · 결과 반투명 창 5초 표시", "pick → 스포이드 모드 진입 → 클릭으로 색상 추출", "\ue771", "#EC4899"),
new HelpEntry("유틸", "date", "날짜 계산 · D-day · 타임스탬프", "날짜 가감(+30d), D-day 계산, Unix ↔ 날짜 변환, 요일·ISO 주차 조회", "date / date +30d / date 2026-12-25 / date 1711584000 / date unix", "\ue787", "#0EA5E9"),
new HelpEntry("시스템", "svc", "서비스 관리", "Windows 서비스 검색·시작·중지·재시작 + AX 클립보드 서비스 강제 재시작", "svc / svc spooler / svc restart clipboard", "\ue912", "#6366F1"),
new HelpEntry("유틸", "pipe", "클립보드 파이프라인", "변환을 > 로 체이닝: 대문자→공백제거→Base64 등 19종 필터 한 번에 적용", "pipe upper > trim > b64e / pipe sort > unique > number", "\ue8c8", "#8B5CF6"),
new HelpEntry("유틸", "journal", "업무 일지 자동 생성", "오늘 사용한 앱·명령어·활성 시간을 마크다운 요약으로 자동 생성. 스탠드업/일일 보고에 바로 사용", "journal / journal 2026-03-25", "\ue70b", "#0EA5E9"),
new HelpEntry("유틸", "routine", "루틴 자동화", "등록된 루틴(앱·폴더·URL 조합)을 한 번에 순서대로 실행. 출근/퇴근/회의 전 세팅", "routine / routine morning / routine endofday", "\ue82f", "#F59E0B"),
new HelpEntry("유틸", "batch", "텍스트 일괄 처리", "클립보드 각 줄에 동시 적용: 접두사·접미사·줄번호·정렬·중복제거·따옴표·치환·CSV 변환", "batch number / batch prefix >> / batch sort / batch replace A B", "\ue8c6", "#10B981"),
new HelpEntry("유틸", "diff", "텍스트/파일 비교", "클립보드 최근 2개 텍스트 또는 파일 2개를 줄 단위 비교. 추가·삭제·동일 줄 하이라이트", "diff / diff C:\\a.txt C:\\b.txt", "\ue89a", "#EF4444"),
new HelpEntry("유틸", "win", "윈도우 포커스 스위처", "열린 창을 타이틀·프로세스명으로 검색하여 Alt+Tab 없이 즉시 전환", "win / win chrome / win 보고서", "\ue8a7", "#6366F1"),
new HelpEntry("시스템", "^", "Windows 실행 명령", "Win+R 실행 창과 동일하게 명령어 실행. notepad, calc, cmd, control, mstsc 등 모든 Windows 실행 명령 지원", "^ notepad / ^ cmd / ^ calc / ^ control / ^ mspaint", "\ue7c4", "#E08850"),
new HelpEntry("유틸", "stats", "텍스트 통계 분석", "클립보드 텍스트 글자·단어·줄 수, 키워드 빈도, 읽기 시간 추정", "stats / stats 키워드 (클립보드 텍스트 분석)", "\ue8d2", "#6366F1"),
new HelpEntry("유틸", "fav", "즐겨찾기 (파일·폴더)", "자주 쓰는 경로를 등록하고 빠르게 열기", "fav / fav add 보고서 C:\\work\\report.xlsx / fav del 보고서", "\ue728", "#F59E0B"),
new HelpEntry("유틸", "rename", "파일 일괄 이름변경", "폴더 내 파일을 패턴·변수로 일괄 이름변경. {n}순번 {date}날짜 {orig}원본명", "rename C:\\work\\*.xlsx 보고서_{n}", "\ue8ac", "#8B5CF6"),
new HelpEntry("유틸", "monitor", "시스템 리소스 모니터", "CPU·메모리·디스크·프로세스 실시간 현황 조회", "monitor / monitor cpu / monitor mem / monitor disk", "\ue7f4", "#10B981"),
new HelpEntry("유틸", "scaffold", "프로젝트 스캐폴딩", "내장/사용자 템플릿으로 프로젝트 폴더 구조 일괄 생성", "scaffold / scaffold webapi / scaffold C:\\new-project webapi", "\ue8f1", "#0EA5E9"),
new HelpEntry("키보드", "Alt+Space", "AX Commander 열기 / 닫기", "어디서든 AX Commander를 토글", "설정 → 핫키에서 변경 가능", "\ue946", "#6B7280"),
new HelpEntry("키보드", "Enter", "실행", "선택된 항목 실행", "", "\ue946", "#6B7280"),
new HelpEntry("키보드", "Shift+Enter", "Large Type / 병합 실행 / 지연 캡처", "텍스트를 전체화면으로 표시 · 클립보드 항목 병합 · 캡처 모드에서는 지연 캡처(3초/5초/10초) 타이머 선택", "(#모드) Shift+↑↓ 선택 후 Shift+Enter 병합 / (cap모드) Shift+Enter → 타이머 선택", "\ue946", "#6B7280"),
new HelpEntry("키보드", "Tab", "자동완성", "선택 항목으로 입력 자동완성", "", "\ue946", "#6B7280"),
new HelpEntry("키보드", "→ (커서 끝)", "파일 액션 메뉴", "앱·파일 선택 후 → 키: 경로복사 · 탐색기 · 관리자 · 터미널", "", "\ue946", "#6B7280"),
new HelpEntry("키보드", "Ctrl+,", "설정 열기", "AX Copilot 설정 창", "", "\ue946", "#6B7280")
};
public string? Prefix => "help";
public PluginMetadata Metadata => new PluginMetadata("Help", "AX Commander 사용 설명서", "1.0", "AX");
public HelpHandler(SettingsService? settings = null)
{
_settings = settings;
}
private HelpEntry[] GetEntries()
{
string text = _settings?.Settings.ScreenCapture.Prefix ?? "cap";
if (string.IsNullOrWhiteSpace(text))
{
text = "cap";
}
string command = _settings?.Settings.Hotkey ?? "Alt+Space";
HelpEntry[] array = (HelpEntry[])_baseEntries.Clone();
for (int i = 0; i < array.Length; i++)
{
if (array[i].Title == "스크린샷 캡처 (예약어 변경 가능)")
{
array[i] = array[i]with
{
Command = text,
Example = $"{text} screen / {text} window / {text} region / {text} scroll / (설정 → 캡처 탭)"
};
}
if (array[i].Title == "AX Commander 열기 / 닫기")
{
array[i] = array[i]with
{
Command = command,
Example = "설정 → 핫키에서 변경 가능"
};
}
}
return array;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim();
HelpEntry[] entries = GetEntries();
if (string.IsNullOrEmpty(q))
{
int value = entries.Count((HelpEntry e) => e.Category != "키보드");
int value2 = entries.Count((HelpEntry e) => e.Category == "키보드");
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("AX Commander — 전체 기능 목록 보기", $"총 {value}개 명령어 · {value2}개 단축키 · Enter → 기능 설명 창 열기", null, "__HELP_OVERVIEW__", null, "\ue946")));
}
IEnumerable<HelpEntry> source = entries.Where((HelpEntry e) => e.Category.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Command.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Title.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
List<LauncherItem> list = source.Select((HelpEntry e) => new LauncherItem(FormatTitle(e), FormatSubtitle(e), null, e.Example, null, e.Symbol)).ToList();
if (list.Count == 0)
{
list.Add(new LauncherItem("'" + q + "'에 해당하는 명령어가 없습니다", "help만 입력하면 전체 기능 창을 열 수 있어요", null, null, null, "\ue946"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
string s = data as string;
if (s == null)
{
return Task.CompletedTask;
}
if (s == "__HELP_OVERVIEW__")
{
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
{
HelpEntry[] entries = GetEntries();
IEnumerable<HelpItemModel> items = entries.Select((HelpEntry e) => new HelpItemModel
{
Category = e.Category,
Command = (string.IsNullOrEmpty(e.Command) ? "(퍼지 검색)" : e.Command),
Title = e.Title,
Description = e.Description,
Example = e.Example,
Symbol = e.Symbol,
ColorBrush = ParseColor(e.ColorHex)
});
string globalHotkey = _settings?.Settings.Hotkey ?? "Alt+Space";
new HelpDetailWindow(items, entries.Count((HelpEntry e) => e.Category != "키보드"), globalHotkey).Show();
});
return Task.CompletedTask;
}
if (!string.IsNullOrWhiteSpace(s))
{
try
{
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(s);
});
}
catch
{
}
}
return Task.CompletedTask;
}
private static SolidColorBrush ParseColor(string hex)
{
try
{
Color color = (Color)ColorConverter.ConvertFromString(hex);
return new SolidColorBrush(color);
}
catch
{
return new SolidColorBrush(Colors.Gray);
}
}
private static string FormatTitle(HelpEntry e)
{
string text = (string.IsNullOrEmpty(e.Command) ? ("[" + e.Category + "]") : ("[" + e.Category + "] " + e.Command));
return text + " — " + e.Title;
}
private static string FormatSubtitle(HelpEntry e)
{
List<string> list = new List<string> { e.Description };
if (!string.IsNullOrEmpty(e.Example))
{
list.Add("예) " + e.Example);
}
return string.Join(" · ", list);
}
}

View File

@@ -0,0 +1,14 @@
namespace AxCopilot.Handlers;
internal sealed class InstalledApp
{
public string Name { get; init; } = "";
public string? UninstallString { get; init; }
public string? Publisher { get; init; }
public string? Version { get; init; }
public string? InstallDate { get; init; }
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class JournalHandler : IActionHandler
{
public string? Prefix => "journal";
public PluginMetadata Metadata => new PluginMetadata("Journal", "업무 일지 자동 생성 — journal", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
DateTime targetDate;
if (string.IsNullOrWhiteSpace(text))
{
targetDate = DateTime.Today;
}
else
{
if (!DateTime.TryParse(text, out var result))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("날짜 형식 오류", "예: journal 2026-03-25 또는 journal (오늘)", null, null, null, "\ue7ba")));
}
targetDate = result.Date;
}
int days = (DateTime.Today - targetDate).Days;
List<DailyUsageStats> stats = UsageStatisticsService.GetStats(Math.Max(days + 1, 1));
DailyUsageStats dailyUsageStats = stats.FirstOrDefault((DailyUsageStats s) => s.Date == targetDate.ToString("yyyy-MM-dd"));
List<LauncherItem> list = new List<LauncherItem>();
if (dailyUsageStats == null)
{
list.Add(new LauncherItem($"{targetDate:yyyy-MM-dd} — 기록 없음", "해당 날짜의 사용 기록이 없습니다", null, null, null, "\ue946"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
double value = (double)dailyUsageStats.ActiveSeconds / 3600.0;
StringBuilder stringBuilder = new StringBuilder();
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(14, 2, stringBuilder2);
handler.AppendLiteral("## 업무 일지 — ");
handler.AppendFormatted(targetDate, "yyyy-MM-dd");
handler.AppendLiteral(" (");
handler.AppendFormatted(targetDate, "dddd");
handler.AppendLiteral(")");
stringBuilder3.AppendLine(ref handler);
stringBuilder.AppendLine();
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(18, 1, stringBuilder2);
handler.AppendLiteral("- **PC 활성 시간**: ");
handler.AppendFormatted(value, "F1");
handler.AppendLiteral("시간");
stringBuilder4.AppendLine(ref handler);
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder5 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(14, 1, stringBuilder2);
handler.AppendLiteral("- **런처 호출**: ");
handler.AppendFormatted(dailyUsageStats.LauncherOpens);
handler.AppendLiteral("회");
stringBuilder5.AppendLine(ref handler);
if (dailyUsageStats.CommandUsage.Count > 0)
{
stringBuilder.AppendLine();
stringBuilder.AppendLine("### 사용한 명령어");
foreach (KeyValuePair<string, int> item in dailyUsageStats.CommandUsage.OrderByDescending<KeyValuePair<string, int>, int>((KeyValuePair<string, int> x) => x.Value).Take(10))
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder6 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(8, 2, stringBuilder2);
handler.AppendLiteral("- `");
handler.AppendFormatted(item.Key);
handler.AppendLiteral("` — ");
handler.AppendFormatted(item.Value);
handler.AppendLiteral("회");
stringBuilder6.AppendLine(ref handler);
}
}
stringBuilder.AppendLine();
stringBuilder.AppendLine("---");
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder7 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(21, 1, stringBuilder2);
handler.AppendLiteral("*AX Copilot 자동 생성 · ");
handler.AppendFormatted(DateTime.Now, "HH:mm");
handler.AppendLiteral("*");
stringBuilder7.AppendLine(ref handler);
string data = stringBuilder.ToString();
IEnumerable<string> values = from x in dailyUsageStats.CommandUsage.OrderByDescending<KeyValuePair<string, int>, int>((KeyValuePair<string, int> x) => x.Value).Take(3)
select x.Key;
string value2 = ((dailyUsageStats.CommandUsage.Count > 0) ? ("주요 명령: " + string.Join(", ", values)) : "명령어 사용 기록 없음");
list.Add(new LauncherItem($"{targetDate:yyyy-MM-dd} 업무 일지 — 클립보드로 복사", $"활성 {value:F1}h · 런처 {dailyUsageStats.LauncherOpens}회 · {value2}", null, data, null, "\ue70b"));
list.Add(new LauncherItem($"PC 활성 시간: {value:F1}시간", "잠금 해제 시간 기준 누적", null, $"PC 활성 시간: {value:F1}시간", null, "\ue823"));
list.Add(new LauncherItem($"런처 호출: {dailyUsageStats.LauncherOpens}회", "Alt+Space 또는 트레이 클릭", null, $"런처 호출: {dailyUsageStats.LauncherOpens}회", null, "\ue721"));
if (dailyUsageStats.CommandUsage.Count > 0)
{
foreach (KeyValuePair<string, int> item2 in dailyUsageStats.CommandUsage.OrderByDescending<KeyValuePair<string, int>, int>((KeyValuePair<string, int> x) => x.Value).Take(5))
{
list.Add(new LauncherItem($"{item2.Key} — {item2.Value}회", "Enter로 복사", null, $"{item2.Key}: {item2.Value}회", null, "\ue756"));
}
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
string text = data as string;
if (text != null && !string.IsNullOrWhiteSpace(text))
{
try
{
Application current = Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(text);
});
}
}
catch
{
}
NotificationService.Notify("업무 일지", "클립보드에 복사되었습니다");
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class JsonHandler : IActionHandler
{
private static readonly JsonSerializerOptions _prettyOpts = new JsonSerializerOptions
{
WriteIndented = true
};
private static readonly JsonSerializerOptions _compactOpts = new JsonSerializerOptions
{
WriteIndented = false
};
public string? Prefix => "json";
public PluginMetadata Metadata => new PluginMetadata("JsonFormatter", "JSON 검증/포맷 — json 뒤에 내용 또는 명령 입력", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
if (string.IsNullOrWhiteSpace(text) || text.Equals("format", StringComparison.OrdinalIgnoreCase) || text.Equals("minify", StringComparison.OrdinalIgnoreCase) || text.Equals("min", StringComparison.OrdinalIgnoreCase))
{
string text2 = "";
try
{
text2 = Clipboard.GetText();
}
catch
{
}
if (string.IsNullOrWhiteSpace(text2))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("클립보드가 비어 있습니다", "클립보드에 JSON 텍스트를 복사한 뒤 실행하세요", null, null, null, "\ue77f")));
}
return Task.FromResult(BuildItems(text2, text.StartsWith("min", StringComparison.OrdinalIgnoreCase)));
}
return Task.FromResult(BuildItems(text, isMinify: false));
}
private static IEnumerable<LauncherItem> BuildItems(string input, bool isMinify)
{
try
{
using JsonDocument jsonDocument = JsonDocument.Parse(input, new JsonDocumentOptions
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip
});
string text = JsonSerializer.Serialize(jsonDocument.RootElement, _prettyOpts);
string text2 = JsonSerializer.Serialize(jsonDocument.RootElement, _compactOpts);
JsonValueKind valueKind = jsonDocument.RootElement.ValueKind;
if (1 == 0)
{
}
string text3 = valueKind switch
{
JsonValueKind.Object => $"Object ({jsonDocument.RootElement.EnumerateObject().Count()}개 키)",
JsonValueKind.Array => $"Array ({jsonDocument.RootElement.GetArrayLength()}개 항목)",
_ => jsonDocument.RootElement.ValueKind.ToString(),
};
if (1 == 0)
{
}
string text4 = text3;
string data = (isMinify ? text2 : text);
string text5 = (isMinify ? "미니파이" : "포맷");
string text6 = ((text2.Length > 100) ? (text2.Substring(0, 97) + "…") : text2);
return new _003C_003Ez__ReadOnlyArray<LauncherItem>(new LauncherItem[3]
{
new LauncherItem("✅ 유효한 JSON — " + text4, text6 + " · Enter로 " + text5 + " 결과 클립보드 복사", null, data, null, "\ue930"),
new LauncherItem("포맷 (Pretty Print) 복사", $"{text.Length}자 · 들여쓰기 2스페이스", null, text, null, "\ue8a4"),
new LauncherItem("미니파이 (Minify) 복사", $"{text2.Length}자 · 공백 제거", null, text2, null, "\ue8c6")
});
}
catch (JsonException ex)
{
string subtitle = ((ex.Message.Length > 100) ? (ex.Message.Substring(0, 97) + "…") : ex.Message);
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("❌ JSON 오류", subtitle, null, null, null, "\uea39"));
}
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try
{
Clipboard.SetText(text);
}
catch
{
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,6 @@
namespace AxCopilot.Handlers;
public class JsonSkillCache
{
public int Ttl { get; set; } = 0;
}

View File

@@ -0,0 +1,8 @@
namespace AxCopilot.Handlers;
public class JsonSkillCredential
{
public string Type { get; set; } = "bearer_token";
public string CredentialKey { get; set; } = "";
}

View File

@@ -0,0 +1,20 @@
namespace AxCopilot.Handlers;
public class JsonSkillDefinition
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string Version { get; set; } = "1.0";
public string Prefix { get; set; } = "";
public JsonSkillCredential? Credential { get; set; }
public JsonSkillRequest? Request { get; set; }
public JsonSkillResponse? Response { get; set; }
public JsonSkillCache? Cache { get; set; }
}

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class JsonSkillHandler : IActionHandler
{
private readonly JsonSkillDefinition _def;
private readonly HttpClient _http = new HttpClient();
private List<LauncherItem>? _cache;
private DateTime _cacheExpiry = DateTime.MinValue;
public string? Prefix => _def.Prefix;
public PluginMetadata Metadata => new PluginMetadata(_def.Id, _def.Name, _def.Version, "JSON Skill");
public JsonSkillHandler(JsonSkillDefinition def)
{
_def = def;
_http.Timeout = TimeSpan.FromSeconds(3.0);
if (def.Credential?.Type == "bearer_token")
{
string token = CredentialManager.GetToken(def.Credential.CredentialKey);
if (!string.IsNullOrEmpty(token))
{
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
}
public async Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (_cache != null && DateTime.Now < _cacheExpiry)
{
return _cache;
}
if (_def.Request == null || _def.Response == null)
{
return Enumerable.Empty<LauncherItem>();
}
try
{
string url = _def.Request.Url.Replace("{{INPUT}}", Uri.EscapeDataString(query));
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri parsedUrl) || (parsedUrl.Scheme != Uri.UriSchemeHttp && parsedUrl.Scheme != Uri.UriSchemeHttps))
{
LogService.Error("[" + _def.Name + "] 유효하지 않은 URL: " + url);
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("설정 오류", "스킬 URL이 유효하지 않습니다 (http/https만 허용)", null, null));
}
string text = _def.Request.Method.ToUpper();
if (1 == 0)
{
}
HttpResponseMessage httpResponseMessage = ((!(text == "POST")) ? (await _http.GetAsync(url, ct)) : (await _http.PostAsync(url, BuildBody(query), ct)));
if (1 == 0)
{
}
HttpResponseMessage response = httpResponseMessage;
response.EnsureSuccessStatusCode();
List<LauncherItem> items = ParseResults(await response.Content.ReadAsStringAsync(ct));
JsonSkillCache? cache = _def.Cache;
if (cache != null && cache.Ttl > 0)
{
_cache = items;
_cacheExpiry = DateTime.Now.AddSeconds(_def.Cache.Ttl);
}
return items;
}
catch (TaskCanceledException)
{
if (_cache != null)
{
LogService.Warn("[" + _def.Name + "] API 타임아웃, 캐시 반환");
return _cache;
}
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("네트워크 오류", "연결을 확인하세요", null, null));
}
catch (Exception ex2)
{
LogService.Error("[" + _def.Name + "] API 호출 실패: " + ex2.Message);
return new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("오류: " + ex2.Message, _def.Name, null, null));
}
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.ActionUrl != null && Uri.TryCreate(item.ActionUrl, UriKind.Absolute, out Uri result) && (result.Scheme == Uri.UriSchemeHttp || result.Scheme == Uri.UriSchemeHttps))
{
Process.Start(new ProcessStartInfo(item.ActionUrl)
{
UseShellExecute = true
});
}
return Task.CompletedTask;
}
private HttpContent BuildBody(string query)
{
string content = JsonSerializer.Serialize(_def.Request?.Body).Replace("\"{{INPUT}}\"", "\"" + query + "\"");
return new StringContent(content, Encoding.UTF8, "application/json");
}
private List<LauncherItem> ParseResults(string json)
{
List<LauncherItem> list = new List<LauncherItem>();
try
{
JsonNode jsonNode = JsonNode.Parse(json);
if (jsonNode == null)
{
return list;
}
JsonNode jsonNode2 = NavigatePath(jsonNode, _def.Response.ResultsPath);
if (!(jsonNode2 is JsonArray source))
{
return list;
}
foreach (JsonNode item in source.Take(10))
{
if (item != null)
{
string title = NavigatePath(item, _def.Response.TitleField)?.ToString() ?? "(제목 없음)";
string subtitle = ((_def.Response.SubtitleField == null) ? "" : (NavigatePath(item, _def.Response.SubtitleField)?.ToString() ?? ""));
string actionUrl = ((_def.Response.ActionUrl == null) ? null : NavigatePath(item, _def.Response.ActionUrl)?.ToString());
list.Add(new LauncherItem(title, subtitle, null, item, actionUrl, "\ue82d"));
}
}
}
catch (Exception ex)
{
LogService.Error("[" + _def.Name + "] 응답 파싱 실패: " + ex.Message);
}
return list;
}
private static JsonNode? NavigatePath(JsonNode root, string path)
{
string[] array = path.Split('.');
JsonNode jsonNode = root;
string[] array2 = array;
foreach (string text in array2)
{
if (jsonNode == null)
{
return null;
}
int num = text.IndexOf('[');
if (num >= 0)
{
int num2 = text.IndexOf(']');
if (num2 < 0)
{
return null;
}
string propertyName = text.Substring(0, num);
int num3 = num + 1;
int index = int.Parse(text.Substring(num3, num2 - num3));
jsonNode = jsonNode[propertyName]?[index];
}
else
{
jsonNode = jsonNode[text];
}
}
return jsonNode;
}
}

View File

@@ -0,0 +1,22 @@
using System.IO;
using System.Text.Json;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public static class JsonSkillLoader
{
public static IActionHandler? Load(string filePath)
{
string json = File.ReadAllText(filePath);
JsonSkillDefinition jsonSkillDefinition = JsonSerializer.Deserialize<JsonSkillDefinition>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (jsonSkillDefinition == null)
{
return null;
}
return new JsonSkillHandler(jsonSkillDefinition);
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace AxCopilot.Handlers;
public class JsonSkillRequest
{
public string Method { get; set; } = "GET";
public string Url { get; set; } = "";
public Dictionary<string, string>? Headers { get; set; }
public object? Body { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace AxCopilot.Handlers;
public class JsonSkillResponse
{
public string ResultsPath { get; set; } = "results";
public string TitleField { get; set; } = "title";
public string? SubtitleField { get; set; }
public string? ActionUrl { get; set; }
}

View File

@@ -0,0 +1,256 @@
using System;
using System.Globalization;
namespace AxCopilot.Handlers;
internal static class MathEvaluator
{
private class Evaluator
{
private readonly string _s;
private int _i;
public Evaluator(string s)
{
_s = s;
_i = 0;
}
public double Parse()
{
double result = ParseExpr();
if (_i < _s.Length)
{
throw new InvalidOperationException($"예기치 않은 문자: '{_s[_i]}'");
}
return result;
}
private double ParseExpr()
{
double num = ParseTerm();
while (_i < _s.Length && (_s[_i] == '+' || _s[_i] == '-'))
{
char c = _s[_i++];
double num2 = ParseTerm();
num = ((c == '+') ? (num + num2) : (num - num2));
}
return num;
}
private double ParseTerm()
{
double num = ParsePower();
while (_i < _s.Length && (_s[_i] == '*' || _s[_i] == '/' || _s[_i] == '%'))
{
char c = _s[_i++];
double num2 = ParsePower();
num = c switch
{
'/' => num / num2,
'*' => num * num2,
_ => num % num2,
};
}
return num;
}
private double ParsePower()
{
double num = ParseUnary();
if (_i < _s.Length && _s[_i] == '^')
{
_i++;
double y = ParseUnary();
return Math.Pow(num, y);
}
return num;
}
private double ParseUnary()
{
if (_i < _s.Length && _s[_i] == '-')
{
_i++;
return 0.0 - ParsePrimary();
}
if (_i < _s.Length && _s[_i] == '+')
{
_i++;
return ParsePrimary();
}
return ParsePrimary();
}
private double ParsePrimary()
{
if (_i >= _s.Length)
{
throw new InvalidOperationException("수식이 불완전합니다.");
}
if (_i + 1 < _s.Length && _s[_i] == '0' && _s[_i + 1] == 'x')
{
_i += 2;
int i = _i;
while (_i < _s.Length && "0123456789abcdef".Contains(_s[_i]))
{
_i++;
}
string s = _s;
int num = i;
return Convert.ToInt64(s.Substring(num, _i - num), 16);
}
if (char.IsDigit(_s[_i]) || _s[_i] == '.')
{
int i2 = _i;
while (_i < _s.Length && (char.IsDigit(_s[_i]) || _s[_i] == '.'))
{
_i++;
}
if (_i < _s.Length && _s[_i] == 'e')
{
_i++;
if (_i < _s.Length && (_s[_i] == '+' || _s[_i] == '-'))
{
_i++;
}
while (_i < _s.Length && char.IsDigit(_s[_i]))
{
_i++;
}
}
string s2 = _s;
int num = i2;
return double.Parse(s2.Substring(num, _i - num), NumberStyles.Float, CultureInfo.InvariantCulture);
}
if (_s[_i] == '(')
{
_i++;
double result = ParseExpr();
if (_i < _s.Length && _s[_i] == ')')
{
_i++;
}
return result;
}
if (char.IsLetter(_s[_i]))
{
int i3 = _i;
while (_i < _s.Length && (char.IsLetterOrDigit(_s[_i]) || _s[_i] == '_'))
{
_i++;
}
string s3 = _s;
int num = i3;
string text = s3.Substring(num, _i - num);
switch (text)
{
case "pi":
return Math.PI;
case "e":
return Math.E;
case "inf":
return double.PositiveInfinity;
default:
if (_i < _s.Length && _s[_i] == '(')
{
_i++;
double num2 = ParseExpr();
double? num3 = null;
if (_i < _s.Length && _s[_i] == ',')
{
_i++;
num3 = ParseExpr();
}
if (_i < _s.Length && _s[_i] == ')')
{
_i++;
}
if (1 == 0)
{
}
double result2;
switch (text)
{
case "sqrt":
result2 = Math.Sqrt(num2);
break;
case "abs":
result2 = Math.Abs(num2);
break;
case "ceil":
result2 = Math.Ceiling(num2);
break;
case "floor":
result2 = Math.Floor(num2);
break;
case "round":
result2 = (num3.HasValue ? Math.Round(num2, (int)num3.Value) : Math.Round(num2));
break;
case "sin":
result2 = Math.Sin(num2 * Math.PI / 180.0);
break;
case "cos":
result2 = Math.Cos(num2 * Math.PI / 180.0);
break;
case "tan":
result2 = Math.Tan(num2 * Math.PI / 180.0);
break;
case "asin":
result2 = Math.Asin(num2) * 180.0 / Math.PI;
break;
case "acos":
result2 = Math.Acos(num2) * 180.0 / Math.PI;
break;
case "atan":
result2 = Math.Atan(num2) * 180.0 / Math.PI;
break;
case "log":
result2 = (num3.HasValue ? Math.Log(num2, num3.Value) : Math.Log10(num2));
break;
case "log2":
result2 = Math.Log2(num2);
break;
case "ln":
result2 = Math.Log(num2);
break;
case "exp":
result2 = Math.Exp(num2);
break;
case "pow":
if (!num3.HasValue)
{
throw new InvalidOperationException("pow(x,y) 형식으로 사용하세요.");
}
result2 = Math.Pow(num2, num3.Value);
break;
case "min":
result2 = (num3.HasValue ? Math.Min(num2, num3.Value) : num2);
break;
case "max":
result2 = (num3.HasValue ? Math.Max(num2, num3.Value) : num2);
break;
default:
throw new InvalidOperationException("알 수 없는 함수: " + text + "()");
}
if (1 == 0)
{
}
return result2;
}
throw new InvalidOperationException("알 수 없는 식별자: " + text);
}
}
throw new InvalidOperationException($"예기치 않은 문자: '{_s[_i]}'");
}
}
public static double Evaluate(string expr)
{
Evaluator evaluator = new Evaluator(expr.Replace(" ", "").Replace("×", "*").Replace("÷", "/")
.Replace("", ",")
.ToLowerInvariant());
return evaluator.Parse();
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class MediaHandler : IActionHandler
{
private record MediaKeyData(byte Vk);
private const byte VK_MEDIA_PLAY_PAUSE = 179;
private const byte VK_MEDIA_NEXT_TRACK = 176;
private const byte VK_MEDIA_PREV_TRACK = 177;
private const byte VK_VOLUME_UP = 175;
private const byte VK_VOLUME_DOWN = 174;
private const byte VK_VOLUME_MUTE = 173;
private const uint KEYEVENTF_EXTENDEDKEY = 1u;
private const uint KEYEVENTF_KEYUP = 2u;
private static readonly List<(string[] Keys, string Title, string Subtitle, byte Vk, string Symbol)> _commands;
public string? Prefix => "media";
public PluginMetadata Metadata => new PluginMetadata("MediaControl", "미디어 컨트롤 — media 뒤에 명령어 입력", "1.0", "AX");
[DllImport("user32.dll")]
private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, nuint dwExtraInfo);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim().ToLowerInvariant();
IEnumerable<(string[], string, string, byte, string)> source = ((!string.IsNullOrEmpty(q)) ? _commands.Where<(string[], string, string, byte, string)>(((string[] Keys, string Title, string Subtitle, byte Vk, string Symbol) c) => c.Keys.Any((string k) => k.StartsWith(q, StringComparison.OrdinalIgnoreCase)) || c.Title.Contains(q, StringComparison.OrdinalIgnoreCase)) : _commands);
List<LauncherItem> list = source.Select<(string[], string, string, byte, string), LauncherItem>(((string[] Keys, string Title, string Subtitle, byte Vk, string Symbol) c) => new LauncherItem(c.Title, c.Subtitle, null, new MediaKeyData(c.Vk), null, c.Symbol)).ToList();
if (list.Count == 0)
{
list.Add(new LauncherItem("알 수 없는 명령어", "play · next · prev · vol+ · vol- · mute 중 하나를 입력하세요", null, null, null, "\ue7ba"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (!(item.Data is MediaKeyData mediaKeyData))
{
return Task.CompletedTask;
}
try
{
keybd_event(mediaKeyData.Vk, 0, 1u, 0u);
keybd_event(mediaKeyData.Vk, 0, 3u, 0u);
LogService.Info($"미디어 키 전송: VK=0x{mediaKeyData.Vk:X2}");
}
catch (Exception ex)
{
LogService.Warn("미디어 키 전송 실패: " + ex.Message);
}
return Task.CompletedTask;
}
static MediaHandler()
{
int num = 6;
List<(string[], string, string, byte, string)> list = new List<(string[], string, string, byte, string)>(num);
CollectionsMarshal.SetCount(list, num);
Span<(string[], string, string, byte, string)> span = CollectionsMarshal.AsSpan(list);
span[0] = (new string[3] { "play", "pause", "pp" }, "재생 / 일시정지", "현재 미디어 재생 또는 일시정지", 179, "\ue768");
span[1] = (new string[2] { "next", ">>" }, "다음 트랙", "다음 곡으로 이동", 176, "\ue893");
span[2] = (new string[3] { "prev", "previous", "<<" }, "이전 트랙", "이전 곡으로 이동", 177, "\ue892");
span[3] = (new string[3] { "vol+", "volup", "up" }, "볼륨 올리기", "시스템 볼륨 증가", 175, "\ue995");
span[4] = (new string[3] { "vol-", "voldown", "down" }, "볼륨 낮추기", "시스템 볼륨 감소", 174, "\ue993");
span[5] = (new string[2] { "mute", "음소거" }, "음소거 토글", "볼륨 음소거 / 해제", 173, "\ue74f");
_commands = list;
}
}

View File

@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class MonitorHandler : IActionHandler
{
private struct MEMORYSTATUSEX
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
}
public string? Prefix => "monitor";
public PluginMetadata Metadata => new PluginMetadata("Monitor", "시스템 리소스 모니터 — monitor", "1.0", "AX");
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim().ToLowerInvariant();
List<LauncherItem> list = new List<LauncherItem>();
bool flag = string.IsNullOrWhiteSpace(text);
if (flag || text.Contains("cpu") || text.Contains("프로세서"))
{
int processorCount = Environment.ProcessorCount;
int value = Process.GetProcesses().Length;
int value2 = 0;
try
{
value2 = Process.GetProcesses().Sum(delegate(Process p)
{
try
{
return p.Threads.Count;
}
catch
{
return 0;
}
});
}
catch
{
}
list.Add(new LauncherItem($"CPU: {processorCount}코어 · 프로세스 {value}개 · 스레드 {value2:N0}개", "Enter로 클립보드 복사", null, $"CPU: {processorCount}코어, 프로세스 {value}개, 스레드 {value2:N0}개", null, "\ue950"));
}
if (flag || text.Contains("mem") || text.Contains("ram") || text.Contains("메모리"))
{
MEMORYSTATUSEX lpBuffer = new MEMORYSTATUSEX
{
dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>()
};
GlobalMemoryStatusEx(ref lpBuffer);
double value3 = (double)lpBuffer.ullTotalPhys / 1073741824.0;
double value4 = (double)(lpBuffer.ullTotalPhys - lpBuffer.ullAvailPhys) / 1073741824.0;
uint dwMemoryLoad = lpBuffer.dwMemoryLoad;
list.Add(new LauncherItem($"메모리: {value4:F1}GB / {value3:F1}GB ({dwMemoryLoad}% 사용)", $"사용 가능: {(double)lpBuffer.ullAvailPhys / 1073741824.0:F1}GB · Enter로 복사", null, $"메모리: {value4:F1}GB / {value3:F1}GB ({dwMemoryLoad}% 사용)", null, "\ue950"));
}
if (flag || text.Contains("disk") || text.Contains("디스크") || text.Contains("저장"))
{
DriveInfo[] drives = DriveInfo.GetDrives();
foreach (DriveInfo driveInfo in drives)
{
if (driveInfo.IsReady && driveInfo.DriveType == DriveType.Fixed)
{
double num2 = (double)driveInfo.TotalSize / 1073741824.0;
double num3 = (double)driveInfo.AvailableFreeSpace / 1073741824.0;
double num4 = num2 - num3;
int value5 = (int)(num4 / num2 * 100.0);
list.Add(new LauncherItem($"디스크 {driveInfo.Name.TrimEnd('\\')} {num4:F0}GB / {num2:F0}GB ({value5}%)", $"여유: {num3:F1}GB · {driveInfo.DriveFormat} · Enter로 복사", null, $"디스크 {driveInfo.Name}: {num4:F0}GB / {num2:F0}GB ({value5}%), 여유 {num3:F1}GB", null, "\ueda2"));
}
}
}
if (flag || text.Contains("uptime") || text.Contains("가동"))
{
TimeSpan timeSpan = TimeSpan.FromMilliseconds(Environment.TickCount64);
string text2 = ((timeSpan.Days > 0) ? $"{timeSpan.Days}일 {timeSpan.Hours}시간 {timeSpan.Minutes}분" : $"{timeSpan.Hours}시간 {timeSpan.Minutes}분");
list.Add(new LauncherItem("가동 시간: " + text2, "마지막 재시작 이후 경과 · Enter로 복사", null, "가동 시간: " + text2, null, "\ue823"));
}
if (flag || text.Contains("top") || text.Contains("프로세스"))
{
try
{
IEnumerable<string> values = Process.GetProcesses().Where(delegate(Process p)
{
try
{
return p.WorkingSet64 > 0;
}
catch
{
return false;
}
}).OrderByDescending(delegate(Process p)
{
try
{
return p.WorkingSet64;
}
catch
{
return 0L;
}
})
.Take(5)
.Select(delegate(Process p)
{
try
{
return $"{p.ProcessName} ({p.WorkingSet64 / 1048576}MB)";
}
catch
{
return p.ProcessName;
}
});
list.Add(new LauncherItem("메모리 상위 프로세스", string.Join(", ", values), null, "메모리 상위: " + string.Join(", ", values), null, "\ue7f4"));
}
catch
{
}
}
if (list.Count == 0)
{
list.Add(new LauncherItem("'" + text + "'에 해당하는 리소스 항목 없음", "cpu / mem / disk / uptime / top", null, null, null, "\ue7ba"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
string text = data as string;
if (text != null && !string.IsNullOrWhiteSpace(text))
{
try
{
Application current = Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(text);
});
}
}
catch
{
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,5 @@
using System;
namespace AxCopilot.Handlers;
internal record NoteEntry(DateTime SavedAt, string Content);

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class NoteHandler : IActionHandler
{
private static readonly string NotesFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "notes.txt");
public string? Prefix => "note";
public PluginMetadata Metadata => new PluginMetadata("Note", "빠른 메모 — note 뒤에 내용 입력", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
if (string.IsNullOrWhiteSpace(text))
{
List<NoteEntry> list = ReadNotes();
if (!list.Any())
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("메모가 없습니다", "note 뒤에 내용을 입력하면 저장됩니다", null, null, null, "\ue70b")));
}
List<LauncherItem> list2 = (from n in list.Take(10)
select new LauncherItem((n.Content.Length > 60) ? (n.Content.Substring(0, 57) + "…") : n.Content, $"{n.SavedAt:yyyy-MM-dd HH:mm} · Enter 복사 · Delete 삭제", null, n.Content, null, "\ue70b")).ToList();
list2.Add(new LauncherItem("전체 메모 삭제", $"총 {list.Count}개 메모 모두 삭제 · Enter로 실행", null, "__CLEAR__", null, "\ue74d"));
return Task.FromResult((IEnumerable<LauncherItem>)list2);
}
if (text.Equals("clear", StringComparison.OrdinalIgnoreCase))
{
int count = ReadNotes().Count;
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem($"전체 메모 삭제 ({count}개)", "모든 메모를 삭제합니다 · Enter로 실행", null, "__CLEAR__", null, "\ue74d")));
}
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("메모 저장: " + ((text.Length > 60) ? (text.Substring(0, 57) + "…") : text), $"{DateTime.Now:yyyy-MM-dd HH:mm} · Enter로 저장", null, ("__SAVE__", text), null, "\ue70b")));
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
object obj = data;
if (!(obj is (string, string) tuple))
{
if (obj is string text)
{
if (text == "__CLEAR__")
{
ClearNotes();
}
else
{
string text2 = text;
try
{
Clipboard.SetText(text2);
}
catch
{
}
}
}
}
else if (tuple.Item1 == "__SAVE__")
{
SaveNote(tuple.Item2);
NotificationService.Notify("AX Copilot", "메모 저장됨: " + ((tuple.Item2.Length > 30) ? (tuple.Item2.Substring(0, 27) + "…") : tuple.Item2));
}
return Task.CompletedTask;
}
private static void SaveNote(string content)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(NotesFile));
string contents = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {content}{Environment.NewLine}";
File.AppendAllText(NotesFile, contents, Encoding.UTF8);
}
catch (Exception ex)
{
LogService.Warn("메모 저장 실패: " + ex.Message);
}
}
private static List<NoteEntry> ReadNotes()
{
List<NoteEntry> list = new List<NoteEntry>();
if (!File.Exists(NotesFile))
{
return list;
}
try
{
string[] source = File.ReadAllLines(NotesFile, Encoding.UTF8);
foreach (string item in source.Reverse())
{
if (!string.IsNullOrWhiteSpace(item))
{
if (item.StartsWith('[') && item.Length > 21 && item[20] == ']' && DateTime.TryParse(item.Substring(1, 19), out var result))
{
DateTime savedAt = result;
string text = item;
list.Add(new NoteEntry(savedAt, text.Substring(22, text.Length - 22).Trim()));
}
else
{
list.Add(new NoteEntry(DateTime.MinValue, item.Trim()));
}
}
}
}
catch (Exception ex)
{
LogService.Warn("메모 읽기 실패: " + ex.Message);
}
return list;
}
private static void ClearNotes()
{
try
{
if (File.Exists(NotesFile))
{
File.Delete(NotesFile);
}
}
catch (Exception ex)
{
LogService.Warn("메모 삭제 실패: " + ex.Message);
}
}
public static bool DeleteNote(string content)
{
try
{
if (!File.Exists(NotesFile))
{
return false;
}
List<string> list = File.ReadAllLines(NotesFile, Encoding.UTF8).ToList();
for (int num = list.Count - 1; num >= 0; num--)
{
string text = list[num];
if (!string.IsNullOrWhiteSpace(text))
{
string text3;
if (text.StartsWith('[') && text.Length > 21 && text[20] == ']')
{
string text2 = text;
text3 = text2.Substring(22, text2.Length - 22).Trim();
}
else
{
text3 = text.Trim();
}
if (text3 == content)
{
list.RemoveAt(num);
File.WriteAllLines(NotesFile, list, Encoding.UTF8);
return true;
}
}
}
}
catch (Exception ex)
{
LogService.Warn("메모 개별 삭제 실패: " + ex.Message);
}
return false;
}
}

View File

@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class PortHandler : IActionHandler
{
private static readonly Dictionary<int, string> _procCache = new Dictionary<int, string>();
private static readonly Dictionary<int, int> _pidMap = new Dictionary<int, int>();
private static DateTime _cacheExpiry = DateTime.MinValue;
public string? Prefix => "port";
public PluginMetadata Metadata => new PluginMetadata("PortChecker", "포트 & 프로세스 점검 — port 뒤에 포트번호 또는 프로세스명", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
RefreshProcessCache();
TcpConnectionInformation[] activeTcpConnections;
try
{
IPGlobalProperties iPGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
activeTcpConnections = iPGlobalProperties.GetActiveTcpConnections();
}
catch (Exception ex)
{
LogService.Warn("포트 조회 실패: " + ex.Message);
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("네트워크 정보를 가져올 수 없습니다", ex.Message, null, null, null, "\ue7ba")));
}
string text = query.Trim();
if (string.IsNullOrWhiteSpace(text))
{
List<LauncherItem> list = (from c in activeTcpConnections
where c.State == TcpState.Established || c.State == TcpState.Listen
orderby c.LocalEndPoint.Port
select c).Take(20).Select(delegate(TcpConnectionInformation c)
{
string processNameForPort = GetProcessNameForPort(c.LocalEndPoint.Port);
string text2 = ((c.State == TcpState.Listen) ? "LISTEN" : "ESTABLISHED");
return new LauncherItem($":{c.LocalEndPoint.Port} → {c.RemoteEndPoint}", text2 + " · " + processNameForPort + " · Enter로 포트번호 복사", null, c.LocalEndPoint.Port.ToString(), null, "\ue968");
}).ToList();
if (!list.Any())
{
list.Add(new LauncherItem("활성 연결 없음", "TCP 연결이 감지되지 않았습니다", null, null, null, "\ue968"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
if (int.TryParse(text, out var portNum))
{
List<TcpConnectionInformation> source = activeTcpConnections.Where((TcpConnectionInformation c) => c.LocalEndPoint.Port == portNum || c.RemoteEndPoint.Port == portNum).ToList();
if (!source.Any())
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem($"포트 {portNum} — 사용 중 아님", "해당 포트를 사용하는 TCP 연결이 없습니다", null, portNum.ToString(), null, "\ue946")));
}
List<LauncherItem> result = source.Select(delegate(TcpConnectionInformation c)
{
string processNameForPort = GetProcessNameForPort(c.LocalEndPoint.Port);
int pidForPort = GetPidForPort(c.LocalEndPoint.Port);
return new LauncherItem($":{c.LocalEndPoint.Port} ←→ {c.RemoteEndPoint}", $"{c.State} · {processNameForPort} (PID {pidForPort}) · Enter로 PID 복사", null, (pidForPort > 0) ? pidForPort.ToString() : portNum.ToString(), null, "\ue968");
}).ToList();
return Task.FromResult((IEnumerable<LauncherItem>)result);
}
string procLower = text.ToLowerInvariant();
List<LauncherItem> list2 = activeTcpConnections.Where(delegate(TcpConnectionInformation c)
{
string text2 = GetProcessNameForPort(c.LocalEndPoint.Port).ToLowerInvariant();
return text2.Contains(procLower);
}).Take(15).Select(delegate(TcpConnectionInformation c)
{
string processNameForPort = GetProcessNameForPort(c.LocalEndPoint.Port);
return new LauncherItem($"{processNameForPort} : {c.LocalEndPoint.Port} → {c.RemoteEndPoint}", $"{c.State} · Enter로 포트번호 복사", null, c.LocalEndPoint.Port.ToString(), null, "\ue968");
})
.ToList();
if (!list2.Any())
{
list2.Add(new LauncherItem("'" + text + "' — 연결 없음", "해당 프로세스의 TCP 연결이 없습니다", null, null, null, "\ue7ba"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list2);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try
{
Clipboard.SetText(text);
}
catch
{
}
}
return Task.CompletedTask;
}
private static void RefreshProcessCache()
{
if (DateTime.Now < _cacheExpiry)
{
return;
}
_procCache.Clear();
_pidMap.Clear();
try
{
Process[] processes = Process.GetProcesses();
foreach (Process process in processes)
{
try
{
_procCache[process.Id] = process.ProcessName;
}
catch
{
}
}
}
catch (Exception ex)
{
LogService.Warn("프로세스 목록 갱신 실패: " + ex.Message);
}
try
{
ProcessStartInfo startInfo = new ProcessStartInfo("netstat", "-ano")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using Process process2 = Process.Start(startInfo);
if (process2 != null)
{
string text = process2.StandardOutput.ReadToEnd();
process2.WaitForExit(2000);
string[] array = text.Split('\n');
foreach (string text2 in array)
{
string[] array2 = text2.Trim().Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (array2.Length < 5 || !int.TryParse(array2[^1], out var result))
{
continue;
}
string text3 = array2[1];
int num = text3.LastIndexOf(':');
if (num >= 0)
{
string text4 = text3;
int num2 = num + 1;
if (int.TryParse(text4.Substring(num2, text4.Length - num2), out var result2))
{
_pidMap.TryAdd(result2, result);
}
}
}
}
}
catch (Exception ex2)
{
LogService.Warn("netstat 실행 실패: " + ex2.Message);
}
_cacheExpiry = DateTime.Now.AddSeconds(5.0);
}
private static string GetProcessNameForPort(int port)
{
int pidForPort = GetPidForPort(port);
string value;
return (pidForPort > 0 && _procCache.TryGetValue(pidForPort, out value)) ? value : "알 수 없음";
}
private static int GetPidForPort(int port)
{
int value;
return _pidMap.TryGetValue(port, out value) ? value : (-1);
}
}

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class ProcessHandler : IActionHandler
{
private record ProcessKillData(string Name, List<int> Pids);
private static readonly HashSet<string> ProtectedProcesses = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"system", "smss", "csrss", "wininit", "winlogon", "services", "lsass", "svchost", "explorer", "dwm",
"fontdrvhost", "spoolsv", "registry"
};
public string? Prefix => "kill ";
public PluginMetadata Metadata => new PluginMetadata("ProcessKiller", "프로세스 종료 — kill 뒤에 프로세스명 입력", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(query))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("종료할 프로세스명을 입력하세요", "예: kill chrome · kill notepad · kill explorer", null, null, null, "\ue7e8")));
}
string q = query.Trim().ToLowerInvariant();
try
{
List<Process> list = (from p in Process.GetProcesses()
where !ProtectedProcesses.Contains(p.ProcessName) && p.ProcessName.ToLowerInvariant().Contains(q)
orderby p.ProcessName
select p).Take(12).ToList();
if (list.Count == 0)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("'" + query + "' 프로세스를 찾을 수 없습니다", "실행 중인 프로세스가 없거나 이름이 다릅니다", null, null, null, "\ue7ba")));
}
List<LauncherItem> result = list.GroupBy<Process, string>((Process p) => p.ProcessName, StringComparer.OrdinalIgnoreCase).Select(delegate(IGrouping<string, Process> g)
{
List<int> list2 = g.Select((Process p) => p.Id).ToList();
long value = g.Sum(delegate(Process p)
{
try
{
return p.WorkingSet64 / 1024 / 1024;
}
catch
{
return 0L;
}
});
string title = ((g.Count() > 1) ? $"{g.Key} ({g.Count()}개 인스턴스)" : g.Key);
string subtitle = $"PID: {string.Join(", ", list2)} · 메모리: {value} MB · Enter로 종료";
return new LauncherItem(title, subtitle, null, new ProcessKillData(g.Key, list2), null, "\ue7e8");
}).ToList();
return Task.FromResult((IEnumerable<LauncherItem>)result);
}
catch (Exception ex)
{
LogService.Warn("프로세스 목록 조회 실패: " + ex.Message);
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("프로세스 목록 조회 실패", ex.Message, null, null, null, "\uea39")));
}
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (!(item.Data is ProcessKillData processKillData))
{
return Task.CompletedTask;
}
int num = 0;
int num2 = 0;
foreach (int pid in processKillData.Pids)
{
try
{
Process processById = Process.GetProcessById(pid);
processById.Kill(entireProcessTree: false);
num++;
}
catch
{
num2++;
}
}
LogService.Info($"프로세스 종료: {processKillData.Name} — {num}개 성공, {num2}개 실패");
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class RecentFilesHandler : IActionHandler
{
private static readonly string RecentFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft\\Windows\\Recent");
private static (DateTime At, List<(string Name, string LinkPath, DateTime Modified)> Files)? _cache;
private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(10.0);
public string? Prefix => "recent";
public PluginMetadata Metadata => new PluginMetadata("RecentFiles", "최근 파일 — recent 뒤에 검색어 입력", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
{
}
List<(string, string, DateTime)> recentFiles = GetRecentFiles();
IEnumerable<(string, string, DateTime)> source = recentFiles;
if (!string.IsNullOrWhiteSpace(q))
{
source = recentFiles.Where<(string, string, DateTime)>(((string Name, string LinkPath, DateTime Modified) f) => f.Name.Contains(q, StringComparison.OrdinalIgnoreCase));
}
List<LauncherItem> list = (from f in source.Take(20)
select new LauncherItem(f.Name, $"{f.Modified:yyyy-MM-dd HH:mm} · Enter로 열기", null, f.LinkPath, null, GetSymbol(f.Name))).ToList();
if (!list.Any())
{
list.Add(new LauncherItem(string.IsNullOrWhiteSpace(q) ? "최근 파일 없음" : "검색 결과 없음", string.IsNullOrWhiteSpace(q) ? "Windows Recent 폴더가 비어 있습니다" : ("'" + q + "' 파일을 최근 목록에서 찾을 수 없습니다"), null, null, null, "\ue946"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text && File.Exists(text))
{
try
{
Process.Start(new ProcessStartInfo(text)
{
UseShellExecute = true
});
}
catch (Exception ex)
{
LogService.Warn("최근 파일 열기 실패: " + ex.Message);
}
}
return Task.CompletedTask;
}
private static List<(string Name, string LinkPath, DateTime Modified)> GetRecentFiles()
{
if (_cache.HasValue && DateTime.Now - _cache.Value.At < CacheTtl)
{
return _cache.Value.Files;
}
List<(string, string, DateTime)> list = new List<(string, string, DateTime)>();
try
{
if (!Directory.Exists(RecentFolder))
{
return list;
}
List<(string, FileInfo)> list2 = (from p in Directory.GetFiles(RecentFolder, "*.lnk")
select (Path: p, Info: new FileInfo(p)) into f
orderby f.Info.LastWriteTime descending
select f).Take(100).ToList();
foreach (var item3 in list2)
{
string item = item3.Item1;
FileInfo item2 = item3.Item2;
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(item2.Name);
list.Add((fileNameWithoutExtension, item, item2.LastWriteTime));
}
}
catch (Exception ex)
{
LogService.Warn("최근 파일 목록 읽기 실패: " + ex.Message);
}
_cache = (DateTime.Now, list);
return list;
}
private static string GetSymbol(string name)
{
string text = Path.GetExtension(name).ToLowerInvariant();
if (1 == 0)
{
}
string result;
switch (text)
{
case ".exe":
case ".msi":
result = "\uecaa";
break;
case ".xlsx":
case ".xls":
case ".csv":
result = "\ue8a5";
break;
case ".docx":
case ".doc":
result = "\ue8a5";
break;
case ".pptx":
case ".ppt":
result = "\ue8a5";
break;
case ".pdf":
result = "\ue8a5";
break;
case ".txt":
case ".md":
case ".log":
result = "\ue8d2";
break;
case ".jpg":
case ".jpeg":
case ".png":
case ".gif":
case ".webp":
case ".bmp":
result = "\ueb9f";
break;
default:
result = "\ue8a5";
break;
}
if (1 == 0)
{
}
return result;
}
}

View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class RenameHandler : IActionHandler
{
public string? Prefix => "rename";
public PluginMetadata Metadata => new PluginMetadata("Rename", "파일 일괄 이름변경 — rename", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
if (string.IsNullOrWhiteSpace(text))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlyArray<LauncherItem>(new LauncherItem[3]
{
new LauncherItem("파일 일괄 이름변경", "rename [폴더\\패턴] [새이름 템플릿]", null, null, null, "\ue8ac"),
new LauncherItem("사용 예시", "rename C:\\work\\*.xlsx 보고서_{n} → 보고서_1.xlsx, 보고서_2.xlsx ...", null, null, null, "\ue946"),
new LauncherItem("변수: {n} 순번, {date} 날짜, {orig} 원본명", "rename D:\\photos\\*.jpg {date}_{n} → 2026-03-27_1.jpg ...", null, null, null, "\ue946")
}));
}
string[] array = text.Split(' ', 2, StringSplitOptions.TrimEntries);
string path = array[0];
string text2 = ((array.Length > 1) ? array[1] : null);
string dir = Path.GetDirectoryName(path);
string fileName = Path.GetFileName(path);
if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("폴더를 찾을 수 없습니다", "경로: " + (dir ?? "(비어 있음)"), null, null, null, "\ue7ba")));
}
string[] array2;
try
{
array2 = Directory.GetFiles(dir, fileName);
}
catch
{
array2 = Array.Empty<string>();
}
if (array2.Length == 0)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("일치하는 파일이 없습니다", "패턴: " + fileName + " · 폴더: " + dir, null, null, null, "\ue7ba")));
}
Array.Sort(array2);
if (string.IsNullOrWhiteSpace(text2))
{
List<LauncherItem> list = array2.Take(10).Select((string f, int i) => new LauncherItem(Path.GetFileName(f), dir, null, null, null, "\ue8a5")).ToList();
list.Insert(0, new LauncherItem($"총 {array2.Length}개 파일 발견", "뒤에 새 이름 템플릿을 추가하세요 (예: 보고서_{n})", null, null, null, "\ue946"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
string newValue = DateTime.Today.ToString("yyyy-MM-dd");
List<(string, string)> list2 = new List<(string, string)>();
for (int num = 0; num < array2.Length; num++)
{
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(array2[num]);
string extension = Path.GetExtension(array2[num]);
string text3 = text2.Replace("{n}", (num + 1).ToString()).Replace("{date}", newValue).Replace("{orig}", fileNameWithoutExtension);
if (!Path.HasExtension(text3))
{
text3 += extension;
}
list2.Add((Path.GetFileName(array2[num]), text3));
}
List<LauncherItem> list3 = new List<LauncherItem>();
list3.Add(new LauncherItem($"총 {array2.Length}개 파일 이름변경 실행", $"Enter로 실행 · {list2[0].Item1} → {list2[0].Item2} ...", null, ValueTuple.Create(dir, array2, list2.Select<(string, string), string>(((string From, string To) p) => p.To).ToArray()), null, "\ue8ac"));
foreach (var (text4, text5) in list2.Take(8))
{
list3.Add(new LauncherItem(text4 + " → " + text5, "미리보기", null, null, null, "\ue8a5"));
}
if (array2.Length > 8)
{
list3.Add(new LauncherItem($"... 외 {array2.Length - 8}개", "", null, null, null, "\ue946"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list3);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (!(item.Data is (string, string[], string[]) tuple) || 1 == 0)
{
return Task.CompletedTask;
}
(string, string[], string[]) tuple2 = tuple;
string item2 = tuple2.Item1;
string[] item3 = tuple2.Item2;
string[] item4 = tuple2.Item3;
int num = 0;
int num2 = 0;
for (int i = 0; i < item3.Length && i < item4.Length; i++)
{
try
{
string text = Path.Combine(item2, item4[i]);
if (File.Exists(text))
{
num2++;
continue;
}
File.Move(item3[i], text);
num++;
}
catch
{
num2++;
}
}
string message = ((num2 > 0) ? $"{num}개 이름변경 완료, {num2}개 실패 (이미 존재하거나 접근 불가)" : $"{num}개 파일 이름변경 완료");
NotificationService.Notify("AX Copilot", message);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class RoutineHandler : IActionHandler
{
internal record RoutineDefinition([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("description")] string Description, [property: JsonPropertyName("steps")] RoutineStep[] Steps);
internal record RoutineStep([property: JsonPropertyName("type")] string Type, [property: JsonPropertyName("target")] string Target, [property: JsonPropertyName("label")] string Label);
private static readonly string RoutineFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "routines.json");
private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNameCaseInsensitive = true
};
private static readonly RoutineDefinition[] BuiltInRoutines = new RoutineDefinition[2]
{
new RoutineDefinition("morning", "출근 루틴", new RoutineStep[2]
{
new RoutineStep("app", "explorer.exe", "파일 탐색기"),
new RoutineStep("info", "info", "시스템 정보 표시")
}),
new RoutineDefinition("endofday", "퇴근 루틴", new RoutineStep[1]
{
new RoutineStep("cmd", "journal", "오늘 업무 일지 생성")
})
};
public string? Prefix => "routine";
public PluginMetadata Metadata => new PluginMetadata("Routine", "루틴 자동화 — routine", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim();
List<RoutineDefinition> list = LoadRoutines();
if (string.IsNullOrWhiteSpace(q))
{
List<LauncherItem> list2 = new List<LauncherItem>
{
new LauncherItem("루틴 자동화", $"총 {list.Count}개 루틴 · 이름 입력 시 실행 · routines.json에서 편집", null, null, null, "\ue946")
};
foreach (RoutineDefinition item in list)
{
string value = string.Join(" → ", item.Steps.Select((RoutineStep s) => s.Label));
list2.Add(new LauncherItem("[" + item.Name + "] " + item.Description, $"{item.Steps.Length}단계: {value} · Enter로 실행", null, item, null, "\ue82f"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list2);
}
RoutineDefinition routineDefinition = list.FirstOrDefault((RoutineDefinition r) => r.Name.Equals(q, StringComparison.OrdinalIgnoreCase));
if (routineDefinition != null)
{
string text = string.Join(" → ", routineDefinition.Steps.Select((RoutineStep s) => s.Label));
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("[" + routineDefinition.Name + "] 루틴 실행", routineDefinition.Description + " · " + text, null, routineDefinition, null, "\ue82f")));
}
IEnumerable<RoutineDefinition> source = list.Where((RoutineDefinition r) => r.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || r.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
List<LauncherItem> list3 = source.Select((RoutineDefinition r) => new LauncherItem("[" + r.Name + "] " + r.Description, $"{r.Steps.Length}단계 · Enter로 실행", null, r, null, "\ue82f")).ToList();
if (!list3.Any())
{
list3.Add(new LauncherItem("'" + q + "' 루틴 없음", "routines.json에서 직접 추가하거나 routine으로 목록 확인", null, null, null, "\ue7ba"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list3);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
if (!(data is RoutineDefinition routine))
{
return;
}
int executed = 0;
RoutineStep[] steps = routine.Steps;
foreach (RoutineStep step in steps)
{
try
{
switch (step.Type.ToLowerInvariant())
{
case "app":
case "url":
case "folder":
Process.Start(new ProcessStartInfo(step.Target)
{
UseShellExecute = true
});
break;
case "cmd":
Process.Start(new ProcessStartInfo("powershell.exe", "-Command \"" + step.Target + "\"")
{
UseShellExecute = false,
CreateNoWindow = true
});
break;
case "info":
NotificationService.Notify("루틴", step.Label);
break;
}
executed++;
await Task.Delay(300, ct);
}
catch (Exception ex)
{
LogService.Warn("루틴 단계 실행 실패: " + step.Label + " — " + ex.Message);
}
}
NotificationService.Notify("루틴 완료", $"[{routine.Name}] {executed}/{routine.Steps.Length}단계 실행 완료");
}
private List<RoutineDefinition> LoadRoutines()
{
List<RoutineDefinition> list = new List<RoutineDefinition>(BuiltInRoutines);
try
{
if (File.Exists(RoutineFile))
{
string json = File.ReadAllText(RoutineFile);
List<RoutineDefinition> list2 = JsonSerializer.Deserialize<List<RoutineDefinition>>(json, JsonOpts);
if (list2 != null)
{
foreach (RoutineDefinition r in list2)
{
list.RemoveAll((RoutineDefinition x) => x.Name.Equals(r.Name, StringComparison.OrdinalIgnoreCase));
list.Add(r);
}
}
}
}
catch (Exception ex)
{
LogService.Warn("루틴 로드 실패: " + ex.Message);
}
return list;
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
public class RunHandler : IActionHandler
{
public string? Prefix => "^";
public PluginMetadata Metadata => new PluginMetadata("Run", "Windows 실행 명령", "1.0", "AX Copilot");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim();
if (string.IsNullOrEmpty(text))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlyArray<LauncherItem>(new LauncherItem[5]
{
new LauncherItem("Windows 실행 명령", "Win+R 실행 창과 동일 · 명령어 입력 후 Enter", null, null, null, "\ue7c4"),
new LauncherItem("^ notepad", "메모장 실행", null, null, null, "\ue946"),
new LauncherItem("^ cmd", "명령 프롬프트", null, null, null, "\ue946"),
new LauncherItem("^ control", "제어판", null, null, null, "\ue946"),
new LauncherItem("^ calc", "계산기", null, null, null, "\ue946")
}));
}
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("실행: " + text, "Enter → Windows 실행 명령으로 실행", null, "__RUN__" + text, null, "\ue7c4")));
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (!(item.Data is string text) || !text.StartsWith("__RUN__"))
{
return Task.CompletedTask;
}
string text2 = text;
int length = "__RUN__".Length;
string text3 = text2.Substring(length, text2.Length - length).Trim();
if (string.IsNullOrEmpty(text3))
{
return Task.CompletedTask;
}
try
{
Process.Start(new ProcessStartInfo(text3)
{
UseShellExecute = true
})?.Dispose();
}
catch (Exception ex)
{
CustomMessageBox.Show("실행 실패: " + ex.Message, "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Hand);
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class ScaffoldHandler : IActionHandler
{
internal record ScaffoldTemplate([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("description")] string Description, [property: JsonPropertyName("paths")] string[] Paths);
private static readonly string TemplateDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "templates");
private static readonly ScaffoldTemplate[] BuiltInTemplates = new ScaffoldTemplate[5]
{
new ScaffoldTemplate("webapi", "Web API 프로젝트", new string[8] { "src/Controllers/", "src/Models/", "src/Services/", "src/Middleware/", "tests/", "docs/", "README.md", ".gitignore" }),
new ScaffoldTemplate("console", "콘솔 애플리케이션", new string[5] { "src/", "src/Core/", "src/Services/", "tests/", "README.md" }),
new ScaffoldTemplate("wpf", "WPF 데스크톱 앱", new string[8] { "src/Views/", "src/ViewModels/", "src/Models/", "src/Services/", "src/Themes/", "src/Assets/", "tests/", "docs/" }),
new ScaffoldTemplate("data", "데이터 파이프라인", new string[9] { "src/Extractors/", "src/Transformers/", "src/Loaders/", "config/", "scripts/", "tests/", "data/input/", "data/output/", "README.md" }),
new ScaffoldTemplate("docs", "문서 프로젝트", new string[5] { "docs/", "images/", "templates/", "README.md", "CHANGELOG.md" })
};
public string? Prefix => "scaffold";
public PluginMetadata Metadata => new PluginMetadata("Scaffold", "프로젝트 스캐폴딩 — scaffold", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim();
IEnumerable<ScaffoldTemplate> second = LoadUserTemplates();
List<ScaffoldTemplate> list = BuiltInTemplates.Concat(second).ToList();
if (string.IsNullOrWhiteSpace(q))
{
List<LauncherItem> list2 = list.Select((ScaffoldTemplate t) => new LauncherItem("[" + t.Name + "] " + t.Description, $"{t.Paths.Length}개 폴더/파일 · Enter → 대상 경로 입력 후 생성", null, t, null, "\ue8b7")).ToList();
list2.Insert(0, new LauncherItem("프로젝트 스캐폴딩", $"총 {list.Count}개 템플릿 · 이름을 입력해 필터링", null, null, null, "\ue946"));
return Task.FromResult((IEnumerable<LauncherItem>)list2);
}
if (q.Contains('\\') || q.Contains('/'))
{
int num = q.LastIndexOf(' ');
if (num > 0)
{
string text = q.Substring(0, num).Trim();
string text2 = q;
int num2 = num + 1;
string templateName = text2.Substring(num2, text2.Length - num2).Trim();
ScaffoldTemplate scaffoldTemplate = list.FirstOrDefault((ScaffoldTemplate t) => t.Name.Equals(templateName, StringComparison.OrdinalIgnoreCase));
if (scaffoldTemplate != null)
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("[" + scaffoldTemplate.Name + "] → " + text, $"{scaffoldTemplate.Paths.Length}개 폴더/파일 생성 · Enter로 실행", null, ValueTuple.Create(text, scaffoldTemplate), null, "\ue74e")));
}
}
}
IEnumerable<ScaffoldTemplate> source = list.Where((ScaffoldTemplate t) => t.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || t.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
List<LauncherItem> list3 = source.Select(delegate(ScaffoldTemplate t)
{
string text3 = string.Join(", ", t.Paths.Take(4));
if (t.Paths.Length > 4)
{
text3 += $" ... (+{t.Paths.Length - 4})";
}
return new LauncherItem("[" + t.Name + "] " + t.Description, text3 + " · 사용법: scaffold [대상경로] " + t.Name, null, t, null, "\ue8b7");
}).ToList();
if (!list3.Any())
{
list3.Add(new LauncherItem("'" + q + "'에 해당하는 템플릿 없음", "scaffold 으로 전체 목록 확인", null, null, null, "\ue7ba"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list3);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (!(item.Data is (string, ScaffoldTemplate) tuple))
{
if (item.Data is ScaffoldTemplate scaffoldTemplate)
{
string usage = "scaffold [대상경로] " + scaffoldTemplate.Name;
try
{
Application current = Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(usage);
});
}
}
catch
{
}
}
return Task.CompletedTask;
}
var (basePath, template) = tuple;
return CreateStructure(basePath, template);
}
private static Task CreateStructure(string basePath, ScaffoldTemplate template)
{
try
{
int num = 0;
string[] paths = template.Paths;
foreach (string text in paths)
{
string path = Path.Combine(basePath, text.Replace('/', Path.DirectorySeparatorChar));
if (text.EndsWith('/') || text.EndsWith('\\') || !Path.HasExtension(text))
{
Directory.CreateDirectory(path);
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
if (!File.Exists(path))
{
File.WriteAllText(path, "");
}
}
num++;
}
NotificationService.Notify("스캐폴딩 완료", $"[{template.Name}] {num}개 항목 생성 → {basePath}");
}
catch (Exception ex)
{
LogService.Error("스캐폴딩 실패: " + ex.Message);
NotificationService.Notify("AX Copilot", "스캐폴딩 실패: " + ex.Message);
}
return Task.CompletedTask;
}
private static IEnumerable<ScaffoldTemplate> LoadUserTemplates()
{
if (!Directory.Exists(TemplateDir))
{
yield break;
}
string[] files = Directory.GetFiles(TemplateDir, "*.json");
foreach (string file in files)
{
ScaffoldTemplate tmpl = null;
try
{
string json = File.ReadAllText(file);
tmpl = JsonSerializer.Deserialize<ScaffoldTemplate>(json);
}
catch
{
}
if (tmpl != null)
{
yield return tmpl;
}
}
}
}

View File

@@ -0,0 +1,612 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
public class ScreenCaptureHandler : IActionHandler
{
private struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
private readonly SettingsService _settings;
private const int SW_RESTORE = 9;
private const byte VK_NEXT = 34;
private const uint WM_VSCROLL = 277u;
private const uint WM_KEYDOWN = 256u;
private const uint WM_KEYUP = 257u;
private const int SB_PAGEDOWN = 3;
private static readonly (string Key, string Label, string Desc)[] _options = new(string, string, string)[4]
{
("region", "영역 선택 캡처", "마우스로 드래그하여 원하는 영역만 캡처 · Shift+Enter: 타이머 캡처"),
("window", "활성 창 캡처", "런처 호출 전 활성 창만 캡처 · Shift+Enter: 타이머 캡처"),
("scroll", "스크롤 캡처", "활성 창을 끝까지 스크롤하며 페이지 전체 캡처 · Shift+Enter: 타이머 캡처"),
("screen", "전체 화면 캡처", "모든 모니터를 포함한 전체 화면 · Shift+Enter: 타이머 캡처")
};
public string? Prefix => string.IsNullOrWhiteSpace(_settings.Settings.ScreenCapture.Prefix) ? "cap" : _settings.Settings.ScreenCapture.Prefix.Trim();
public PluginMetadata Metadata => new PluginMetadata("ScreenCapture", "화면 캡처 — cap screen/window/scroll/region", "1.0", "AX");
internal int ScrollDelayMs => Math.Max(50, _settings.Settings.ScreenCapture.ScrollDelayMs);
public ScreenCaptureHandler(SettingsService settings)
{
_settings = settings;
}
[DllImport("user32.dll")]
private static extern bool GetWindowRect(nint hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern bool IsWindow(nint hWnd);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(nint hWnd);
[DllImport("user32.dll")]
private static extern bool ShowWindow(nint hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern bool PrintWindow(nint hwnd, nint hdcBlt, uint nFlags);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(nint hWnd);
[DllImport("user32.dll")]
private static extern nint SendMessage(nint hWnd, uint Msg, nint wParam, nint lParam);
[DllImport("user32.dll")]
private static extern nint FindWindowEx(nint parent, nint child, string? className, string? windowText);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim().ToLowerInvariant();
string saveHint = "클립보드에 복사";
IEnumerable<(string, string, string)> enumerable;
if (!string.IsNullOrWhiteSpace(q))
{
enumerable = _options.Where(((string Key, string Label, string Desc) o) => o.Key.StartsWith(q) || o.Label.Contains(q));
}
else
{
IEnumerable<(string, string, string)> options = _options;
enumerable = options;
}
IEnumerable<(string, string, string)> source = enumerable;
List<LauncherItem> list = source.Select<(string, string, string), LauncherItem>(((string Key, string Label, string Desc) o) => new LauncherItem(o.Label, o.Desc + " · " + saveHint, null, o.Key, null, "\ue722")).ToList();
if (!list.Any())
{
list.Add(new LauncherItem("알 수 없는 캡처 모드: " + q, "screen / window / scroll / region", null, null, null, "\ue7ba"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public IEnumerable<LauncherItem> GetDelayItems(string mode)
{
string text = _options.FirstOrDefault(((string Key, string Label, string Desc) o) => o.Key == mode).Label ?? mode;
return new LauncherItem[3]
{
new LauncherItem("3초 후 " + text, "Shift+Enter로 지연 캡처", null, "delay:" + mode + ":3", null, "\ue916"),
new LauncherItem("5초 후 " + text, "Shift+Enter로 지연 캡처", null, "delay:" + mode + ":5", null, "\ue916"),
new LauncherItem("10초 후 " + text, "Shift+Enter로 지연 캡처", null, "delay:" + mode + ":10", null, "\ue916")
};
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
if (!(data is string data2))
{
return;
}
if (data2.StartsWith("delay:"))
{
string[] parts = data2.Split(':');
if (parts.Length == 3 && int.TryParse(parts[2], out var delaySec))
{
await ExecuteDelayedCaptureAsync(parts[1], delaySec, ct);
return;
}
}
await Task.Delay(150, ct);
await CaptureDirectAsync(data2, ct);
}
private async Task ExecuteDelayedCaptureAsync(string mode, int delaySec, CancellationToken ct)
{
await Task.Delay(200, ct);
for (int i = delaySec; i > 0; i--)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(1000, ct);
}
await CaptureDirectAsync(mode, ct);
}
public async Task CaptureDirectAsync(string mode, CancellationToken ct = default(CancellationToken))
{
try
{
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
switch (mode)
{
case "screen":
await CaptureScreenAsync(timestamp);
break;
case "window":
await CaptureWindowAsync(timestamp);
break;
case "scroll":
await CaptureScrollAsync(timestamp, ct);
break;
case "region":
await CaptureRegionAsync(timestamp, ct);
break;
}
}
catch (Exception ex)
{
Exception ex2 = ex;
LogService.Error("캡처 실패: " + ex2.Message);
NotificationService.Notify("AX Copilot", "캡처 실패: " + ex2.Message);
}
}
private async Task CaptureScreenAsync(string timestamp)
{
Rectangle bounds = GetAllScreenBounds();
using Bitmap bmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
using Graphics g = Graphics.FromImage(bmp);
g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy);
CopyToClipboard(bmp);
await Task.Delay(10);
NotificationService.Notify("화면 캡처 완료", "클립보드에 복사되었습니다");
}
private async Task CaptureWindowAsync(string timestamp)
{
nint hwnd = WindowTracker.PreviousWindow;
if (hwnd == IntPtr.Zero || !IsWindow(hwnd))
{
NotificationService.Notify("AX Copilot", "캡처할 창이 없습니다. 런처 호출 전 창을 확인하세요.");
return;
}
nint launcherHwnd = GetLauncherHwnd();
if (launcherHwnd != IntPtr.Zero)
{
for (int i = 0; i < 10; i++)
{
if (!IsWindowVisible(launcherHwnd))
{
break;
}
await Task.Delay(50);
}
}
ShowWindow(hwnd, 9);
SetForegroundWindow(hwnd);
await Task.Delay(150);
if (!GetWindowRect(hwnd, out var rect))
{
return;
}
int w = rect.right - rect.left;
int h = rect.bottom - rect.top;
if (w <= 0 || h <= 0)
{
return;
}
using Bitmap bmp = CaptureWindow(hwnd, w, h, rect);
CopyToClipboard(bmp);
NotificationService.Notify("창 캡처 완료", "클립보드에 복사되었습니다");
}
private async Task CaptureScrollAsync(string timestamp, CancellationToken ct)
{
nint hwnd = WindowTracker.PreviousWindow;
if (hwnd == IntPtr.Zero || !IsWindow(hwnd))
{
NotificationService.Notify("AX Copilot", "캡처할 창이 없습니다.");
return;
}
nint launcherHwnd = GetLauncherHwnd();
if (launcherHwnd != IntPtr.Zero)
{
for (int i = 0; i < 10; i++)
{
if (!IsWindowVisible(launcherHwnd))
{
break;
}
await Task.Delay(50, ct);
}
}
ShowWindow(hwnd, 9);
SetForegroundWindow(hwnd);
await Task.Delay(200, ct);
if (!GetWindowRect(hwnd, out var rect))
{
return;
}
int w = rect.right - rect.left;
int h = rect.bottom - rect.top;
if (w <= 0 || h <= 0)
{
return;
}
nint scrollTarget = FindScrollableChild(hwnd);
List<Bitmap> frames = new List<Bitmap> { CaptureWindow(hwnd, w, h, rect) };
for (int j = 0; j < 14; j++)
{
ct.ThrowIfCancellationRequested();
if (scrollTarget != IntPtr.Zero)
{
SendMessage(scrollTarget, 277u, new IntPtr(3), IntPtr.Zero);
}
else
{
SendPageDown(hwnd);
}
await Task.Delay(ScrollDelayMs, ct);
if (!GetWindowRect(hwnd, out var newRect))
{
break;
}
Bitmap frame = CaptureWindow(hwnd, w, h, newRect);
int num;
if (frames.Count > 0)
{
num = (AreSimilar(frames[frames.Count - 1], frame) ? 1 : 0);
}
else
{
num = 0;
}
if (num != 0)
{
frame.Dispose();
break;
}
frames.Add(frame);
}
using Bitmap stitched = StitchFrames(frames, h);
foreach (Bitmap f in frames)
{
f.Dispose();
}
CopyToClipboard(stitched);
NotificationService.Notify("스크롤 캡처 완료", $"{stitched.Height}px · 클립보드에 복사되었습니다");
}
private static Bitmap CaptureWindow(nint hwnd, int w, int h, RECT rect)
{
Bitmap bitmap = new Bitmap(w, h, PixelFormat.Format32bppArgb);
using Graphics graphics = Graphics.FromImage(bitmap);
nint hdc = graphics.GetHdc();
bool flag = PrintWindow(hwnd, hdc, 2u);
graphics.ReleaseHdc(hdc);
if (!flag)
{
graphics.CopyFromScreen(rect.left, rect.top, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy);
}
return bitmap;
}
private static Rectangle GetAllScreenBounds()
{
Screen[] allScreens = Screen.AllScreens;
int num = allScreens.Min((Screen s) => s.Bounds.X);
int num2 = allScreens.Min((Screen s) => s.Bounds.Y);
int num3 = allScreens.Max((Screen s) => s.Bounds.Right);
int num4 = allScreens.Max((Screen s) => s.Bounds.Bottom);
return new Rectangle(num, num2, num3 - num, num4 - num2);
}
private static nint FindScrollableChild(nint hwnd)
{
string[] array = new string[7] { "Internet Explorer_Server", "Chrome_RenderWidgetHostHWND", "MozillaWindowClass", "RichEdit20W", "RICHEDIT50W", "TextBox", "EDIT" };
foreach (string className in array)
{
nint num = FindWindowEx(hwnd, IntPtr.Zero, className, null);
if (num != IntPtr.Zero)
{
return num;
}
}
return IntPtr.Zero;
}
private static void SendPageDown(nint hwnd)
{
SendMessage(hwnd, 256u, new IntPtr(34), IntPtr.Zero);
SendMessage(hwnd, 257u, new IntPtr(34), IntPtr.Zero);
}
private unsafe static bool AreSimilar(Bitmap a, Bitmap b)
{
if (a.Width != b.Width || a.Height != b.Height)
{
return false;
}
int num = (int)((double)a.Height * 0.8);
int width = a.Width;
int height = a.Height;
Rectangle rect = new Rectangle(0, num, width, height - num);
Rectangle rect2 = new Rectangle(0, num, width, height - num);
BitmapData bitmapData = a.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
BitmapData bitmapData2 = b.LockBits(rect2, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
int num2 = 0;
int num3 = 0;
int stride = bitmapData.Stride;
int num4 = width / 16 + 1;
int num5 = (height - num) / 8 + 1;
byte* ptr = (byte*)((IntPtr)bitmapData.Scan0).ToPointer();
byte* ptr2 = (byte*)((IntPtr)bitmapData2.Scan0).ToPointer();
for (int i = 0; i < num5; i++)
{
int num6 = i * 8;
if (num6 >= height - num)
{
break;
}
for (int j = 0; j < num4; j++)
{
int num7 = j * 16;
if (num7 >= width)
{
break;
}
int num8 = num6 * stride + num7 * 4;
if (Math.Abs(ptr[num8] - ptr2[num8]) < 5 && Math.Abs(ptr[num8 + 1] - ptr2[num8 + 1]) < 5 && Math.Abs(ptr[num8 + 2] - ptr2[num8 + 2]) < 5)
{
num2++;
}
num3++;
}
}
return num3 > 0 && (double)num2 / (double)num3 > 0.97;
}
finally
{
a.UnlockBits(bitmapData);
b.UnlockBits(bitmapData2);
}
}
private static Bitmap StitchFrames(List<Bitmap> frames, int windowHeight)
{
if (frames.Count == 0)
{
return new Bitmap(1, 1);
}
if (frames.Count == 1)
{
return new Bitmap(frames[0]);
}
int width = frames[0].Width;
List<int> list = new List<int>();
List<int> list2 = new List<int>();
int num = windowHeight;
for (int i = 1; i < frames.Count; i++)
{
int num2 = FindOverlap(frames[i - 1], frames[i]);
int num3 = ((num2 > 0) ? num2 : (windowHeight / 5));
int num4 = windowHeight - num3;
if (num4 <= 0)
{
num4 = windowHeight / 4;
num3 = windowHeight - num4;
}
list.Add(num3);
list2.Add(num4);
num += num4;
}
Bitmap bitmap = new Bitmap(width, num, PixelFormat.Format32bppArgb);
using Graphics graphics = Graphics.FromImage(bitmap);
graphics.DrawImage(frames[0], 0, 0, width, windowHeight);
int num5 = windowHeight;
for (int j = 1; j < frames.Count; j++)
{
int y = list[j - 1];
int num6 = list2[j - 1];
graphics.DrawImage(srcRect: new Rectangle(0, y, width, num6), destRect: new Rectangle(0, num5, width, num6), image: frames[j], srcUnit: GraphicsUnit.Pixel);
num5 += num6;
}
return bitmap;
}
private unsafe static int FindOverlap(Bitmap prev, Bitmap next)
{
int num = Math.Min(prev.Width, next.Width);
int height = prev.Height;
if (height < 16 || num < 16)
{
return 0;
}
int num2 = (int)((double)height * 0.7);
BitmapData bitmapData = prev.LockBits(new Rectangle(0, 0, prev.Width, prev.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
BitmapData bitmapData2 = next.LockBits(new Rectangle(0, 0, next.Width, next.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
int stride = bitmapData.Stride;
int stride2 = bitmapData2.Stride;
int result = 0;
byte* ptr = (byte*)((IntPtr)bitmapData.Scan0).ToPointer();
byte* ptr2 = (byte*)((IntPtr)bitmapData2.Scan0).ToPointer();
for (int num3 = num2; num3 > 8; num3 -= 2)
{
int num4 = height - num3;
if (num4 >= 0)
{
int num5 = 0;
int num6 = 0;
for (int i = 0; i < 8; i++)
{
int num7 = i * (num3 / 8);
int num8 = num4 + num7;
int num9 = num7;
if (num8 >= height || num9 >= next.Height)
{
continue;
}
for (int j = 4; j < num - 4; j += 12)
{
int num10 = num8 * stride + j * 4;
int num11 = num9 * stride2 + j * 4;
if (num10 + 2 < bitmapData.Height * stride && num11 + 2 < bitmapData2.Height * stride2)
{
if (Math.Abs(ptr[num10] - ptr2[num11]) < 10 && Math.Abs(ptr[num10 + 1] - ptr2[num11 + 1]) < 10 && Math.Abs(ptr[num10 + 2] - ptr2[num11 + 2]) < 10)
{
num5++;
}
num6++;
}
}
}
if (num6 > 0 && (double)num5 / (double)num6 > 0.8)
{
result = num3;
break;
}
}
}
return result;
}
finally
{
prev.UnlockBits(bitmapData);
next.UnlockBits(bitmapData2);
}
}
private async Task CaptureRegionAsync(string timestamp, CancellationToken ct)
{
Rectangle bounds = GetAllScreenBounds();
Bitmap fullBmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
try
{
using (Graphics g = Graphics.FromImage(fullBmp))
{
g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy);
}
Rectangle? selected = null;
System.Windows.Application current = System.Windows.Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
RegionSelectWindow regionSelectWindow = new RegionSelectWindow(fullBmp, bounds);
regionSelectWindow.ShowDialog();
selected = regionSelectWindow.SelectedRect;
});
}
if (!selected.HasValue || selected.Value.Width < 4 || selected.Value.Height < 4)
{
NotificationService.Notify("AX Copilot", "영역 선택이 취소되었습니다.");
return;
}
Rectangle r = selected.Value;
using Bitmap crop = new Bitmap(r.Width, r.Height, PixelFormat.Format32bppArgb);
using (Graphics g2 = Graphics.FromImage(crop))
{
g2.DrawImage(fullBmp, new Rectangle(0, 0, r.Width, r.Height), r, GraphicsUnit.Pixel);
}
CopyToClipboard(crop);
NotificationService.Notify("영역 캡처 완료", $"{r.Width}×{r.Height} · 클립보드에 복사되었습니다");
await Task.CompletedTask;
}
finally
{
if (fullBmp != null)
{
((IDisposable)fullBmp).Dispose();
}
}
}
private static void CopyToClipboard(Bitmap bmp)
{
try
{
System.Windows.Application current = System.Windows.Application.Current;
if (current == null)
{
return;
}
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
using MemoryStream memoryStream = new MemoryStream();
bmp.Save(memoryStream, ImageFormat.Bmp);
memoryStream.Position = 0L;
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memoryStream;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
((Freezable)bitmapImage).Freeze();
System.Windows.Clipboard.SetImage(bitmapImage);
});
}
catch (Exception ex)
{
LogService.Warn("클립보드 이미지 복사 실패: " + ex.Message);
}
}
private static nint GetLauncherHwnd()
{
try
{
nint hwnd = IntPtr.Zero;
System.Windows.Application current = System.Windows.Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Window window = System.Windows.Application.Current.Windows.OfType<Window>().FirstOrDefault((Window w) => ((object)w).GetType().Name == "LauncherWindow");
if (window != null)
{
hwnd = new WindowInteropHelper(window).Handle;
}
});
}
return hwnd;
}
catch
{
return IntPtr.Zero;
}
}
}

View File

@@ -0,0 +1,257 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class ServiceHandler : IActionHandler
{
private readonly ClipboardHistoryService? _clipboardService;
public string? Prefix => "svc";
public PluginMetadata Metadata => new PluginMetadata("Service", "Windows 서비스 관리 — svc", "1.0", "AX");
public ServiceHandler(ClipboardHistoryService? clipboardService = null)
{
_clipboardService = clipboardService;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim();
List<LauncherItem> list = new List<LauncherItem>();
if (q.Equals("restart clipboard", StringComparison.OrdinalIgnoreCase) || q.Equals("클립보드 재시작", StringComparison.OrdinalIgnoreCase))
{
list.Add(new LauncherItem("AX 클립보드 히스토리 서비스 재시작", "클립보드 감지가 작동하지 않을 때 사용 · Enter로 실행", null, "__RESTART_CLIPBOARD__", null, "\ue777"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
if (q.StartsWith("start ", StringComparison.OrdinalIgnoreCase) || q.StartsWith("stop ", StringComparison.OrdinalIgnoreCase) || q.StartsWith("restart ", StringComparison.OrdinalIgnoreCase))
{
string[] array = q.Split(' ', 2, StringSplitOptions.TrimEntries);
string text = array[0].ToLowerInvariant();
string svcName = ((array.Length > 1) ? array[1] : "");
if (!string.IsNullOrWhiteSpace(svcName))
{
try
{
ServiceController serviceController = ServiceController.GetServices().FirstOrDefault((ServiceController s) => s.ServiceName.Contains(svcName, StringComparison.OrdinalIgnoreCase) || s.DisplayName.Contains(svcName, StringComparison.OrdinalIgnoreCase));
if (serviceController != null)
{
if (1 == 0)
{
}
string text2 = text switch
{
"start" => "시작",
"stop" => "중지",
"restart" => "재시작",
_ => text,
};
if (1 == 0)
{
}
string text3 = text2;
list.Add(new LauncherItem("[" + text3 + "] " + serviceController.DisplayName, $"서비스명: {serviceController.ServiceName} · 현재 상태: {StatusText(serviceController.Status)} · Enter로 실행", null, ("__" + text.ToUpperInvariant() + "__", serviceController.ServiceName), null, (text == "stop") ? "\uea39" : "\ue777"));
}
else
{
list.Add(new LauncherItem("'" + svcName + "' 서비스를 찾을 수 없습니다", "서비스 이름 또는 표시 이름으로 검색", null, null, null, "\ue7ba"));
}
}
catch (Exception ex)
{
list.Add(new LauncherItem("서비스 조회 실패", ex.Message, null, null, null, "\uea39"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
}
try
{
ServiceController[] services = ServiceController.GetServices();
IEnumerable<ServiceController> enumerable = (string.IsNullOrWhiteSpace(q) ? (from s in services
where s.Status == ServiceControllerStatus.Running
orderby s.DisplayName
select s).Take(20) : (from s in services
where s.ServiceName.Contains(q, StringComparison.OrdinalIgnoreCase) || s.DisplayName.Contains(q, StringComparison.OrdinalIgnoreCase)
orderby s.DisplayName
select s).Take(20));
list.Add(new LauncherItem("AX 클립보드 히스토리 서비스 재시작", "svc restart clipboard · 클립보드 감지 문제 시 사용", null, "__RESTART_CLIPBOARD__", null, "\ue777"));
foreach (ServiceController item in enumerable)
{
string text4 = StatusText(item.Status);
string symbol = ((item.Status == ServiceControllerStatus.Running) ? "\ue73e" : "\ue711");
list.Add(new LauncherItem("[" + text4 + "] " + item.DisplayName, item.ServiceName + " · svc start/stop/restart " + item.ServiceName, null, item.ServiceName, null, symbol));
}
}
catch (Exception ex2)
{
list.Add(new LauncherItem("서비스 조회 실패", ex2.Message, null, null, null, "\uea39"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text && text == "__RESTART_CLIPBOARD__")
{
RestartClipboardService();
return Task.CompletedTask;
}
if (!(item.Data is (string, string) tuple))
{
object data = item.Data;
string name = data as string;
if (name != null)
{
try
{
Application current = Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(name);
});
}
}
catch
{
}
}
return Task.CompletedTask;
}
var (action, svcName) = tuple;
return ExecuteServiceAction(action, svcName);
}
private void RestartClipboardService()
{
try
{
if (_clipboardService == null)
{
NotificationService.Notify("AX Copilot", "클립보드 서비스 참조를 찾을 수 없습니다.");
return;
}
_clipboardService.Dispose();
Application current = Application.Current;
if (current == null)
{
return;
}
((DispatcherObject)current).Dispatcher.BeginInvoke((Delegate)(Action)delegate
{
try
{
_clipboardService.Reinitialize();
NotificationService.Notify("AX Copilot", "클립보드 히스토리 서비스가 재시작되었습니다.");
LogService.Info("클립보드 히스토리 서비스 강제 재시작 완료");
}
catch (Exception ex2)
{
NotificationService.Notify("AX Copilot", "클립보드 재시작 실패: " + ex2.Message);
LogService.Error("클립보드 재시작 실패: " + ex2.Message);
}
}, (DispatcherPriority)4, Array.Empty<object>());
}
catch (Exception ex)
{
NotificationService.Notify("AX Copilot", "클립보드 재시작 실패: " + ex.Message);
}
}
private static async Task ExecuteServiceAction(string action, string svcName)
{
try
{
if (1 == 0)
{
}
string text = action switch
{
"__START__" => "start",
"__STOP__" => "stop",
"__RESTART__" => "stop",
_ => null,
};
if (1 == 0)
{
}
string verb = text;
if (verb != null)
{
ProcessStartInfo psi = new ProcessStartInfo("sc.exe", verb + " \"" + svcName + "\"")
{
Verb = "runas",
UseShellExecute = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
Process.Start(psi)?.WaitForExit(5000);
if (action == "__RESTART__")
{
await Task.Delay(1000);
ProcessStartInfo startPsi = new ProcessStartInfo("sc.exe", "start \"" + svcName + "\"")
{
Verb = "runas",
UseShellExecute = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
Process.Start(startPsi)?.WaitForExit(5000);
}
if (1 == 0)
{
}
text = action switch
{
"__START__" => "시작",
"__STOP__" => "중지",
"__RESTART__" => "재시작",
_ => action,
};
if (1 == 0)
{
}
string label = text;
NotificationService.Notify("서비스 관리", svcName + " " + label + " 요청 완료");
}
}
catch (Exception ex)
{
Exception ex2 = ex;
NotificationService.Notify("AX Copilot", "서비스 제어 실패: " + ex2.Message);
}
}
private static string StatusText(ServiceControllerStatus status)
{
if (1 == 0)
{
}
string result = status switch
{
ServiceControllerStatus.Running => "실행 중",
ServiceControllerStatus.Stopped => "중지됨",
ServiceControllerStatus.StartPending => "시작 중",
ServiceControllerStatus.StopPending => "중지 중",
ServiceControllerStatus.Paused => "일시 중지",
ServiceControllerStatus.ContinuePending => "재개 중",
ServiceControllerStatus.PausePending => "일시 중지 중",
_ => status.ToString(),
};
if (1 == 0)
{
}
return result;
}
}

View File

@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class SnapHandler : IActionHandler
{
private struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
private struct MONITORINFO
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
private const uint SWP_SHOWWINDOW = 64u;
private const uint SWP_NOZORDER = 4u;
private const uint MONITOR_DEFAULTTONEAREST = 2u;
private const int SW_RESTORE = 9;
private const int SW_MAXIMIZE = 3;
private static readonly (string Key, string Label, string Desc)[] _snapOptions = new(string, string, string)[20]
{
("left", "왼쪽 절반", "화면 왼쪽 50% 영역에 배치"),
("right", "오른쪽 절반", "화면 오른쪽 50% 영역에 배치"),
("top", "위쪽 절반", "화면 위쪽 50% 영역에 배치"),
("bottom", "아래쪽 절반", "화면 아래쪽 50% 영역에 배치"),
("tl", "좌상단 1/4", "화면 좌상단 25% 영역에 배치"),
("tr", "우상단 1/4", "화면 우상단 25% 영역에 배치"),
("bl", "좌하단 1/4", "화면 좌하단 25% 영역에 배치"),
("br", "우하단 1/4", "화면 우하단 25% 영역에 배치"),
("l-rt", "좌반 + 우상", "왼쪽 50% + 오른쪽 상단 25% (2창용)"),
("l-rb", "좌반 + 우하", "왼쪽 50% + 오른쪽 하단 25% (2창용)"),
("r-lt", "우반 + 좌상", "오른쪽 50% + 왼쪽 상단 25% (2창용)"),
("r-lb", "우반 + 좌하", "오른쪽 50% + 왼쪽 하단 25% (2창용)"),
("third-l", "좌측 1/3", "화면 왼쪽 33% 영역에 배치"),
("third-c", "중앙 1/3", "화면 가운데 33% 영역에 배치"),
("third-r", "우측 1/3", "화면 오른쪽 33% 영역에 배치"),
("two3-l", "좌측 2/3", "화면 왼쪽 66% 영역에 배치"),
("two3-r", "우측 2/3", "화면 오른쪽 66% 영역에 배치"),
("full", "전체 화면", "최대화"),
("center", "화면 중앙", "화면 중앙 80% 크기로 배치"),
("restore", "원래 크기 복원", "창을 이전 크기로 복원")
};
public string? Prefix => "snap";
public PluginMetadata Metadata => new PluginMetadata("WindowSnap", "창 배치 — 2/3/4분할, 1/3·2/3, 전체화면, 중앙, 복원", "1.1", "AX");
[DllImport("user32.dll")]
private static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern bool ShowWindow(nint hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern nint MonitorFromWindow(nint hwnd, uint dwFlags);
[DllImport("user32.dll")]
private static extern bool GetMonitorInfo(nint hMonitor, ref MONITORINFO lpmi);
[DllImport("user32.dll")]
private static extern bool IsWindow(nint hWnd);
[DllImport("user32.dll")]
private static extern bool IsIconic(nint hWnd);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim().ToLowerInvariant();
nint previousWindow = WindowTracker.PreviousWindow;
bool flag = previousWindow != IntPtr.Zero && IsWindow(previousWindow);
string hint = (flag ? "Enter로 현재 활성 창에 적용" : "대상 창 없음 — 런처를 열기 전 창에 적용됩니다");
IEnumerable<(string, string, string)> enumerable;
if (!string.IsNullOrWhiteSpace(q))
{
enumerable = _snapOptions.Where(((string Key, string Label, string Desc) o) => o.Key.StartsWith(q) || o.Label.Contains(q));
}
else
{
IEnumerable<(string, string, string)> snapOptions = _snapOptions;
enumerable = snapOptions;
}
IEnumerable<(string, string, string)> source = enumerable;
List<LauncherItem> list = source.Select<(string, string, string), LauncherItem>(((string Key, string Label, string Desc) o) => new LauncherItem(o.Label, o.Desc + " · " + hint, null, o.Key, null, "\ue8a0")).ToList();
if (!list.Any())
{
list.Add(new LauncherItem("알 수 없는 스냅 방향: " + q, "left / right / tl / tr / bl / br / third-l/c/r / two3-l/r / full / center / restore", null, null, null, "\ue7ba"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
if (!(data is string snapKey))
{
return;
}
nint hwnd = WindowTracker.PreviousWindow;
if (hwnd != IntPtr.Zero && IsWindow(hwnd))
{
if (IsIconic(hwnd))
{
ShowWindow(hwnd, 9);
}
await Task.Delay(80, ct);
ApplySnap(hwnd, snapKey);
}
}
private static void ApplySnap(nint hwnd, string key)
{
nint hMonitor = MonitorFromWindow(hwnd, 2u);
MONITORINFO lpmi = new MONITORINFO
{
cbSize = Marshal.SizeOf<MONITORINFO>()
};
if (!GetMonitorInfo(hMonitor, ref lpmi))
{
return;
}
RECT rcWork = lpmi.rcWork;
int num = rcWork.right - rcWork.left;
int num2 = rcWork.bottom - rcWork.top;
int left = rcWork.left;
int top = rcWork.top;
if (key == "full")
{
ShowWindow(hwnd, 3);
return;
}
if (key == "restore")
{
ShowWindow(hwnd, 9);
return;
}
if (1 == 0)
{
}
(int, int, int, int) tuple = key switch
{
"left" => (left, top, num / 2, num2),
"right" => (left + num / 2, top, num / 2, num2),
"top" => (left, top, num, num2 / 2),
"bottom" => (left, top + num2 / 2, num, num2 / 2),
"tl" => (left, top, num / 2, num2 / 2),
"tr" => (left + num / 2, top, num / 2, num2 / 2),
"bl" => (left, top + num2 / 2, num / 2, num2 / 2),
"br" => (left + num / 2, top + num2 / 2, num / 2, num2 / 2),
"l-rt" => (left + num / 2, top, num / 2, num2 / 2),
"l-rb" => (left + num / 2, top + num2 / 2, num / 2, num2 / 2),
"r-lt" => (left, top, num / 2, num2 / 2),
"r-lb" => (left, top + num2 / 2, num / 2, num2 / 2),
"third-l" => (left, top, num / 3, num2),
"third-c" => (left + num / 3, top, num / 3, num2),
"third-r" => (left + num * 2 / 3, top, num / 3, num2),
"two3-l" => (left, top, num * 2 / 3, num2),
"two3-r" => (left + num / 3, top, num * 2 / 3, num2),
"center" => (left + num / 10, top + num2 / 10, num * 8 / 10, num2 * 8 / 10),
_ => (left, top, num, num2),
};
if (1 == 0)
{
}
var (x, y, cx, cy) = tuple;
ShowWindow(hwnd, 9);
SetWindowPos(hwnd, IntPtr.Zero, x, y, cx, cy, 68u);
}
}

View File

@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class SnippetHandler : IActionHandler
{
private struct INPUT
{
public uint type;
public InputUnion u;
}
[StructLayout(LayoutKind.Explicit)]
private struct InputUnion
{
[FieldOffset(0)]
public KEYBDINPUT ki;
}
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public nint dwExtraInfo;
}
private readonly SettingsService _settings;
public string? Prefix => ";";
public PluginMetadata Metadata => new PluginMetadata("Snippets", "텍스트 스니펫 — ; 뒤에 키워드 입력", "1.0", "AX");
public SnippetHandler(SettingsService settings)
{
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
List<SnippetEntry> snippets = _settings.Settings.Snippets;
if (!snippets.Any())
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("등록된 스니펫이 없습니다", "설정 → 스니펫 탭에서 추가하세요", null, null, null, "\ue70b")));
}
string q = query.Trim().ToLowerInvariant();
List<LauncherItem> list = (from s in snippets
where string.IsNullOrEmpty(q) || s.Key.ToLowerInvariant().Contains(q) || s.Name.ToLowerInvariant().Contains(q)
select new LauncherItem((s.Name.Length > 0) ? s.Name : s.Key, TruncatePreview(s.Content), null, s, null, "\ue70b")).ToList();
if (list.Count == 0)
{
list.Add(new LauncherItem("'" + query + "'에 해당하는 스니펫 없음", "설정에서 새 스니펫을 추가하세요", null, null, null, "\ue70b"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (!(item.Data is SnippetEntry snippetEntry))
{
return Task.CompletedTask;
}
try
{
string text = ExpandVariables(snippetEntry.Content);
Clipboard.SetText(text);
Task.Delay(100, ct).ContinueWith(delegate
{
((DispatcherObject)Application.Current).Dispatcher.Invoke((Action)delegate
{
INPUT[] array = new INPUT[4]
{
new INPUT
{
type = 1u,
u = new InputUnion
{
ki = new KEYBDINPUT
{
wVk = 17
}
}
},
new INPUT
{
type = 1u,
u = new InputUnion
{
ki = new KEYBDINPUT
{
wVk = 86
}
}
},
new INPUT
{
type = 1u,
u = new InputUnion
{
ki = new KEYBDINPUT
{
wVk = 86,
dwFlags = 2u
}
}
},
new INPUT
{
type = 1u,
u = new InputUnion
{
ki = new KEYBDINPUT
{
wVk = 17,
dwFlags = 2u
}
}
}
};
SendInput((uint)array.Length, array, Marshal.SizeOf<INPUT>());
});
}, ct, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
}
catch (Exception ex)
{
LogService.Warn("스니펫 실행 실패: " + ex.Message);
}
return Task.CompletedTask;
}
private static string ExpandVariables(string content)
{
DateTime now = DateTime.Now;
return content.Replace("{date}", now.ToString("yyyy-MM-dd")).Replace("{time}", now.ToString("HH:mm:ss")).Replace("{datetime}", now.ToString("yyyy-MM-dd HH:mm:ss"))
.Replace("{year}", now.Year.ToString())
.Replace("{month}", now.Month.ToString("D2"))
.Replace("{day}", now.Day.ToString("D2"));
}
private static string TruncatePreview(string content)
{
string text = content.Replace("\r\n", " ").Replace("\n", " ").Replace("\r", " ");
return (text.Length > 60) ? (text.Substring(0, 57) + "…") : text;
}
[DllImport("user32.dll")]
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class StarInfoHandler : IActionHandler
{
private readonly SystemInfoHandler _inner = new SystemInfoHandler();
public string? Prefix => "*";
public PluginMetadata Metadata => new PluginMetadata("StarInfo", "시스템 정보 빠른 조회 — * 단축키 (info와 동일)", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
return _inner.GetItemsAsync(query, ct);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
return _inner.ExecuteAsync(item, ct);
}
}

View File

@@ -0,0 +1,270 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
public class SystemCommandHandler : IActionHandler
{
private readonly SettingsService _settings;
private static readonly Regex _timerRe = new Regex("^(?:(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex _alarmRe = new Regex("^(\\d{1,2}):(\\d{2})$", RegexOptions.Compiled);
public string? Prefix => "/";
public PluginMetadata Metadata => new PluginMetadata("SystemCommands", "시스템 명령 — / 뒤에 명령 입력", "1.0", "AX");
public SystemCommandHandler(SettingsService settings)
{
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
SystemCommandSettings cfg = _settings.Settings.SystemCommands;
string q = query.Trim().ToLowerInvariant();
if (q.StartsWith("timer"))
{
string text = q;
string text2 = text.Substring(5, text.Length - 5).Trim();
List<LauncherItem> list = new List<LauncherItem>();
if (string.IsNullOrEmpty(text2))
{
list.Add(new LauncherItem("타이머 — 시간을 입력하세요", "예: /timer 5m · /timer 1h30m · /timer 30s 회의", null, null, null, "\ue916"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
string[] array = text2.Split(' ', 2);
string s = array[0];
string label = ((array.Length > 1) ? array[1].Trim() : "");
if (TryParseTimer(s, out var seconds) && seconds > 0)
{
string display = FormatDuration(seconds);
string title = (string.IsNullOrEmpty(label) ? ("타이머 " + display) : ("타이머 " + display + " — " + label));
list.Add(new LauncherItem(title, display + " 후 알림 · Enter로 시작", null, (Func<Task>)(() => StartTimerAsync(seconds, (label.Length > 0) ? label : display)), null, "\ue916"));
}
else
{
list.Add(new LauncherItem("형식 오류", "예: /timer 5m · /timer 1h30m · /timer 90s", null, null, null, "\uea39"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
if (q.StartsWith("alarm"))
{
string text = q;
string text3 = text.Substring(5, text.Length - 5).Trim();
List<LauncherItem> list2 = new List<LauncherItem>();
if (string.IsNullOrEmpty(text3))
{
list2.Add(new LauncherItem("알람 — 시각을 입력하세요", "예: /alarm 14:30 · /alarm 9:00", null, null, null, "\ue916"));
return Task.FromResult((IEnumerable<LauncherItem>)list2);
}
Match match = _alarmRe.Match(text3.Split(' ')[0]);
if (match.Success)
{
int num = int.Parse(match.Groups[1].Value);
int num2 = int.Parse(match.Groups[2].Value);
object obj;
if (!text3.Contains(' '))
{
obj = "";
}
else
{
text = text3;
int num3 = text3.IndexOf(' ') + 1;
obj = text.Substring(num3, text.Length - num3);
}
string label2 = (string)obj;
if (num >= 0 && num <= 23 && num2 >= 0 && num2 <= 59)
{
DateTime dateTime = DateTime.Today.AddHours(num).AddMinutes(num2);
if (dateTime <= DateTime.Now)
{
dateTime = dateTime.AddDays(1.0);
}
int diff = (int)(dateTime - DateTime.Now).TotalSeconds;
string timeStr = dateTime.ToString("HH:mm");
list2.Add(new LauncherItem("알람 " + timeStr + " " + ((dateTime.Date > DateTime.Today) ? "(내일)" : ""), (FormatDuration(diff) + " 후 알림 · Enter로 시작 " + ((label2.Length > 0) ? ("— " + label2) : "")).Trim(), null, (Func<Task>)(() => StartTimerAsync(diff, (label2.Length > 0) ? label2 : (timeStr + " 알람"))), null, "\ue916"));
}
else
{
list2.Add(new LauncherItem("시각 범위 오류", "00:00 ~ 23:59 범위로 입력하세요", null, null, null, "\uea39"));
}
}
else
{
list2.Add(new LauncherItem("형식 오류", "예: /alarm 14:30 · /alarm 09:00", null, null, null, "\uea39"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list2);
}
List<(string, string, string, string, bool, Func<Task>)> list3 = new List<(string, string, string, string, bool, Func<Task>)>();
list3.Add(("lock", "화면 잠금", "현재 세션을 잠급니다", "\ue72e", cfg.ShowLock, LockAsync));
list3.Add(("sleep", "절전 모드", "시스템을 절전 상태로 전환합니다", "\uec46", cfg.ShowSleep, SleepAsync));
list3.Add(("restart", "재시작", "컴퓨터를 재시작합니다", "\ue777", cfg.ShowRestart, RestartAsync));
list3.Add(("shutdown", "시스템 종료", "컴퓨터를 종료합니다", "\ue7e8", cfg.ShowShutdown, ShutdownAsync));
list3.Add(("hibernate", "최대 절전", "최대 절전 모드로 전환합니다", "\uec46", cfg.ShowHibernate, HibernateAsync));
list3.Add(("logout", "로그아웃", "현재 사용자 세션에서 로그아웃합니다", "\uf3b1", cfg.ShowLogout, LogoutAsync));
list3.Add(("recycle", "휴지통 비우기", "휴지통의 모든 파일을 영구 삭제합니다", "\ue74d", cfg.ShowRecycleBin, EmptyRecycleBinAsync));
list3.Add(("dock", "독 바 표시/숨기기", "화면 하단 독 바를 표시하거나 숨깁니다", "\ue8a0", true, ToggleDockAsync));
List<(string, string, string, string, bool, Func<Task>)> source = list3;
List<string> value;
IEnumerable<LauncherItem> source2 = from c in source
where c.Enabled
where string.IsNullOrEmpty(q) || c.Key.StartsWith(q) || c.Name.Contains(q) || (cfg.CommandAliases.TryGetValue(c.Key, out value) && value.Any((string a) => a.StartsWith(q, StringComparison.OrdinalIgnoreCase)))
select new LauncherItem(c.Name, c.Hint, null, c.Action, null, c.Symbol);
return Task.FromResult((IEnumerable<LauncherItem>)source2.ToList());
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
Func<Task> action = data as Func<Task>;
if (action != null)
{
await action();
}
}
private static async Task StartTimerAsync(int totalSeconds, string label)
{
await Task.Delay(TimeSpan.FromSeconds(totalSeconds));
NotificationService.Notify("⏰ 타이머 완료", label);
}
private static bool TryParseTimer(string s, out int totalSeconds)
{
totalSeconds = 0;
Match match = _timerRe.Match(s.ToLowerInvariant());
if (!match.Success)
{
return false;
}
int num = (match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 0);
int num2 = (match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 0);
int num3 = (match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0);
totalSeconds = num * 3600 + num2 * 60 + num3;
return true;
}
private static string FormatDuration(int totalSeconds)
{
int num = totalSeconds / 3600;
int num2 = totalSeconds % 3600 / 60;
int num3 = totalSeconds % 60;
if (num > 0 && num2 > 0)
{
return $"{num}시간 {num2}분";
}
if (num <= 0)
{
if (num2 > 0 && num3 > 0)
{
return $"{num2}분 {num3}초";
}
if (num2 <= 0)
{
return $"{num3}초";
}
return $"{num2}분";
}
return $"{num}시간";
}
private static Task LockAsync()
{
LockWorkStation();
return Task.CompletedTask;
}
private static Task SleepAsync()
{
System.Windows.Forms.Application.SetSuspendState(PowerState.Suspend, force: false, disableWakeEvent: false);
return Task.CompletedTask;
}
private static Task HibernateAsync()
{
System.Windows.Forms.Application.SetSuspendState(PowerState.Hibernate, force: false, disableWakeEvent: false);
return Task.CompletedTask;
}
private static Task RestartAsync()
{
MessageBoxResult messageBoxResult = CustomMessageBox.Show("컴퓨터를 재시작하시겠습니까?\n저장되지 않은 작업이 있으면 먼저 저장하세요.", "재시작 확인", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (messageBoxResult == MessageBoxResult.Yes)
{
Process.Start(new ProcessStartInfo("shutdown", "/r /t 5 /c \"AX Copilot에 의해 재시작됩니다.\"")
{
UseShellExecute = true,
CreateNoWindow = true
});
}
return Task.CompletedTask;
}
private static Task ShutdownAsync()
{
MessageBoxResult messageBoxResult = CustomMessageBox.Show("컴퓨터를 종료하시겠습니까?\n저장되지 않은 작업이 있으면 먼저 저장하세요.", "종료 확인", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (messageBoxResult == MessageBoxResult.Yes)
{
Process.Start(new ProcessStartInfo("shutdown", "/s /t 5 /c \"AX Copilot에 의해 종료됩니다.\"")
{
UseShellExecute = true,
CreateNoWindow = true
});
}
return Task.CompletedTask;
}
private static Task LogoutAsync()
{
MessageBoxResult messageBoxResult = CustomMessageBox.Show("로그아웃하시겠습니까?", "로그아웃 확인", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (messageBoxResult == MessageBoxResult.Yes)
{
ExitWindowsEx(0u, 0u);
}
return Task.CompletedTask;
}
private static Task EmptyRecycleBinAsync()
{
MessageBoxResult messageBoxResult = CustomMessageBox.Show("휴지통을 비우시겠습니까?\n삭제된 파일은 복구할 수 없습니다.", "휴지통 비우기", MessageBoxButton.YesNo, MessageBoxImage.Exclamation);
if (messageBoxResult == MessageBoxResult.Yes)
{
SHEmptyRecycleBin(IntPtr.Zero, null, 7u);
}
return Task.CompletedTask;
}
private static Task ToggleDockAsync()
{
((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate
{
(System.Windows.Application.Current as App)?.ToggleDockBar();
});
return Task.CompletedTask;
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool LockWorkStation();
[DllImport("user32.dll", SetLastError = true)]
private static extern bool ExitWindowsEx(uint uFlags, uint dwReason);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern uint SHEmptyRecycleBin(nint hwnd, string? pszRootPath, uint dwFlags);
}

View File

@@ -0,0 +1,462 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Threading;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Views;
using Microsoft.Win32;
namespace AxCopilot.Handlers;
public class SystemInfoHandler : IActionHandler
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct MEMORYSTATUSEX
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
}
internal sealed record InfoAction(string Type, string? Payload = null);
private static volatile PerformanceCounter? _cpuCounter;
private static float _cpuCached;
private static DateTime _cpuUpdated;
private static readonly object _cpuLock;
public string? Prefix => "info";
public PluginMetadata Metadata => new PluginMetadata("SystemInfo", "시스템 정보 — IP, 배터리, 볼륨, 가동시간 등", "1.0", "AX");
[DllImport("winmm.dll")]
private static extern int waveOutGetVolume(nint hwo, out uint dwVolume);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
static SystemInfoHandler()
{
_cpuUpdated = DateTime.MinValue;
_cpuLock = new object();
Task.Run(delegate
{
try
{
PerformanceCounter performanceCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
performanceCounter.NextValue();
_cpuCounter = performanceCounter;
}
catch
{
}
});
}
private static float GetCpuUsage()
{
try
{
PerformanceCounter cpuCounter = _cpuCounter;
if (cpuCounter == null)
{
return -1f;
}
lock (_cpuLock)
{
if ((DateTime.Now - _cpuUpdated).TotalMilliseconds > 800.0)
{
_cpuCached = cpuCounter.NextValue();
_cpuUpdated = DateTime.Now;
}
return _cpuCached;
}
}
catch
{
return -1f;
}
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = query.Trim().ToLowerInvariant();
List<LauncherItem> list = new List<LauncherItem>();
try
{
bool flag = string.IsNullOrEmpty(text);
bool flag2 = flag || text.Contains("ip") || text.Contains("네트워크") || text.Contains("network");
bool flag3 = flag || text.Contains("bat") || text.Contains("배터리") || text.Contains("battery");
bool flag4 = flag || text.Contains("vol") || text.Contains("볼륨") || text.Contains("volume");
bool flag5 = flag || text.Contains("up") || text.Contains("가동") || text.Contains("uptime");
bool flag6 = flag || text.Contains("sys") || text.Contains("시스템") || text.Contains("system") || text.Contains("host") || text.Contains("호스트") || text.Contains("os");
bool flag7 = flag || text.Contains("user") || text.Contains("사용자");
bool flag8 = flag || text.Contains("cpu") || text.Contains("프로세서") || text.Contains("processor");
bool flag9 = flag || text.Contains("ram") || text.Contains("메모리") || text.Contains("memory");
bool flag10 = flag || text.Contains("disk") || text.Contains("디스크") || text.Contains("storage") || text.Contains("저장");
bool flag11 = flag || text.Contains("screen") || text.Contains("화면") || text.Contains("resolution") || text.Contains("display");
if (flag6)
{
list.Add(new LauncherItem("컴퓨터: " + Environment.MachineName, "OS: " + GetOsVersion() + " · Enter로 시스템 정보 열기", null, new InfoAction("shell", "msinfo32"), null, "\ue7f4"));
}
if (flag7)
{
string text2 = Environment.UserDomainName + "\\" + Environment.UserName;
list.Add(new LauncherItem("사용자: " + Environment.UserName, text2 + " · Enter로 사용자 계정 설정 열기", null, new InfoAction("shell", "netplwiz"), null, "\ue77b"));
}
if (flag2)
{
string localIpAddress = GetLocalIpAddress();
if (localIpAddress != null)
{
list.Add(new LauncherItem("로컬 IP: " + localIpAddress, "LAN / Wi-Fi 주소 · Enter로 네트워크 설정 열기", null, new InfoAction("ms_settings", "ms-settings:network"), null, "\ue968"));
}
string defaultGateway = GetDefaultGateway();
if (defaultGateway != null)
{
list.Add(new LauncherItem("게이트웨이: " + defaultGateway, "기본 게이트웨이 · Enter로 네트워크 설정 열기", null, new InfoAction("ms_settings", "ms-settings:network"), null, "\ue968"));
}
}
if (flag3)
{
LauncherItem batteryItem = GetBatteryItem();
if (batteryItem != null)
{
list.Add(batteryItem);
}
}
if (flag4)
{
LauncherItem volumeItem = GetVolumeItem();
if (volumeItem != null)
{
list.Add(volumeItem);
}
}
if (flag5)
{
TimeSpan timeSpan = TimeSpan.FromMilliseconds(Environment.TickCount64);
string text3 = FormatUptime(timeSpan);
DateTime value = DateTime.Now - timeSpan;
list.Add(MakeItem("가동 시간: " + text3, $"마지막 재시작: {value:MM/dd HH:mm}", text3, "\ue823"));
}
if (flag8)
{
float cpuUsage = GetCpuUsage();
string title = ((cpuUsage < 0f) ? "CPU: 측정 중…" : $"CPU: {(int)cpuUsage}%");
string processorName = GetProcessorName();
list.Add(new LauncherItem(title, (string.IsNullOrEmpty(processorName) ? "전체 CPU 사용률" : processorName) + " · Enter로 리소스 모니터 열기", null, new InfoAction("resource_mon"), null, "\ue950"));
}
if (flag9)
{
MEMORYSTATUSEX lpBuffer = new MEMORYSTATUSEX
{
dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>()
};
if (GlobalMemoryStatusEx(ref lpBuffer))
{
double value2 = (double)lpBuffer.ullTotalPhys / 1024.0 / 1024.0 / 1024.0;
double value3 = (double)(lpBuffer.ullTotalPhys - lpBuffer.ullAvailPhys) / 1024.0 / 1024.0 / 1024.0;
uint dwMemoryLoad = lpBuffer.dwMemoryLoad;
list.Add(new LauncherItem($"RAM: {value3:F1} / {value2:F1} GB ({dwMemoryLoad}%)", $"사용 중: {value3:F1} GB · 여유: {(double)lpBuffer.ullAvailPhys / 1024.0 / 1024.0 / 1024.0:F1} GB · Enter로 리소스 모니터 열기", null, new InfoAction("resource_mon"), null, "\ue950"));
}
}
if (flag10)
{
foreach (DriveInfo item in from d in DriveInfo.GetDrives()
where d.IsReady && d.DriveType == DriveType.Fixed
select d)
{
double num = (double)item.TotalSize / 1024.0 / 1024.0 / 1024.0;
double num2 = (double)item.AvailableFreeSpace / 1024.0 / 1024.0 / 1024.0;
double num3 = num - num2;
int value4 = (int)(num3 / num * 100.0);
string value5 = (string.IsNullOrWhiteSpace(item.VolumeLabel) ? item.Name.TrimEnd('\\') : item.VolumeLabel);
list.Add(new LauncherItem($"드라이브 {item.Name.TrimEnd('\\')} ({value5}) — {num3:F0} / {num:F0} GB ({value4}%)", $"여유 공간: {num2:F1} GB · Enter로 탐색기 열기", null, new InfoAction("open_drive", item.RootDirectory.FullName), null, "\ueda2"));
}
}
if (flag11)
{
double primaryScreenWidth = SystemParameters.PrimaryScreenWidth;
double primaryScreenHeight = SystemParameters.PrimaryScreenHeight;
list.Add(new LauncherItem($"화면: {(int)primaryScreenWidth} × {(int)primaryScreenHeight}", "기본 모니터 해상도 · Enter로 디스플레이 설정 열기", null, new InfoAction("ms_settings", "ms-settings:display"), null, "\ue7f4"));
}
}
catch (Exception ex)
{
LogService.Warn("시스템 정보 조회 오류: " + ex.Message);
list.Add(new LauncherItem("시스템 정보 조회 실패", ex.Message, null, null, null, "\uea39"));
}
if (list.Count == 0)
{
list.Add(new LauncherItem("시스템 정보 없음", "ip · battery · volume · uptime · system 키워드로 검색하세요", null, null, null, "\ue946"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
InfoAction action = data as InfoAction;
if ((object)action == null)
{
return Task.CompletedTask;
}
switch (action.Type)
{
case "copy":
if (string.IsNullOrWhiteSpace(action.Payload))
{
break;
}
try
{
((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate
{
System.Windows.Clipboard.SetText(action.Payload);
});
LogService.Info("클립보드 복사: " + action.Payload);
}
catch (Exception ex2)
{
LogService.Warn("클립보드 복사 실패: " + ex2.Message);
}
break;
case "open_drive":
if (!string.IsNullOrWhiteSpace(action.Payload))
{
try
{
Process.Start(new ProcessStartInfo("explorer.exe", action.Payload)
{
UseShellExecute = true
});
}
catch (Exception ex4)
{
LogService.Warn("드라이브 열기 실패: " + ex4.Message);
}
}
break;
case "resource_mon":
((DispatcherObject)System.Windows.Application.Current).Dispatcher.Invoke((Action)delegate
{
ResourceMonitorWindow resourceMonitorWindow = System.Windows.Application.Current.Windows.OfType<ResourceMonitorWindow>().FirstOrDefault();
if (resourceMonitorWindow != null)
{
resourceMonitorWindow.Activate();
}
else
{
new ResourceMonitorWindow().Show();
}
});
break;
case "shell":
if (!string.IsNullOrWhiteSpace(action.Payload))
{
try
{
Process.Start(new ProcessStartInfo(action.Payload)
{
UseShellExecute = true
});
}
catch (Exception ex3)
{
LogService.Warn("셸 명령 실행 실패: " + ex3.Message);
}
}
break;
case "ms_settings":
if (!string.IsNullOrWhiteSpace(action.Payload))
{
try
{
Process.Start(new ProcessStartInfo(action.Payload)
{
UseShellExecute = true
});
}
catch (Exception ex)
{
LogService.Warn("설정 열기 실패: " + ex.Message);
}
}
break;
}
return Task.CompletedTask;
}
private static LauncherItem MakeItem(string title, string subtitle, string? copyValue, string symbol)
{
return new LauncherItem(title, subtitle, null, (copyValue != null) ? new InfoAction("copy", copyValue) : null, null, symbol);
}
private static string? GetLocalIpAddress()
{
try
{
NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
foreach (NetworkInterface networkInterface in allNetworkInterfaces)
{
if (networkInterface.OperationalStatus != OperationalStatus.Up)
{
continue;
}
NetworkInterfaceType networkInterfaceType = networkInterface.NetworkInterfaceType;
if ((networkInterfaceType == NetworkInterfaceType.Loopback || networkInterfaceType == NetworkInterfaceType.Tunnel) ? true : false)
{
continue;
}
foreach (UnicastIPAddressInformation unicastAddress in networkInterface.GetIPProperties().UnicastAddresses)
{
if (unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork)
{
return unicastAddress.Address.ToString();
}
}
}
}
catch
{
}
return null;
}
private static string? GetDefaultGateway()
{
try
{
NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
foreach (NetworkInterface networkInterface in allNetworkInterfaces)
{
if (networkInterface.OperationalStatus == OperationalStatus.Up)
{
GatewayIPAddressInformation gatewayIPAddressInformation = networkInterface.GetIPProperties().GatewayAddresses.FirstOrDefault((GatewayIPAddressInformation g) => g.Address.AddressFamily == AddressFamily.InterNetwork);
if (gatewayIPAddressInformation != null)
{
return gatewayIPAddressInformation.Address.ToString();
}
}
}
}
catch
{
}
return null;
}
private static LauncherItem? GetBatteryItem()
{
try
{
PowerStatus powerStatus = SystemInformation.PowerStatus;
float batteryLifePercent = powerStatus.BatteryLifePercent;
if (batteryLifePercent < 0f)
{
return new LauncherItem("배터리: 해당 없음", "데스크톱 PC 또는 항상 연결됨 · Enter로 전원 설정 열기", null, new InfoAction("ms_settings", "ms-settings:powersleep"), null, "\ue83f");
}
int num = (int)(batteryLifePercent * 100f);
bool flag = powerStatus.PowerLineStatus == System.Windows.Forms.PowerLineStatus.Online;
string text = ((powerStatus.BatteryLifeRemaining >= 0) ? (" · 잔여: " + FormatUptime(TimeSpan.FromSeconds(powerStatus.BatteryLifeRemaining))) : "");
string symbol = (flag ? "\ue83e" : ((num > 50) ? "\ue83f" : "\ueba0"));
return new LauncherItem($"배터리: {num}%{(flag ? " " : "")}", "전원: " + (flag ? "AC 연결됨" : "배터리 사용 중") + text + " · Enter로 전원 설정 열기", null, new InfoAction("ms_settings", "ms-settings:powersleep"), null, symbol);
}
catch
{
return null;
}
}
private static LauncherItem? GetVolumeItem()
{
try
{
if (waveOutGetVolume(IntPtr.Zero, out var dwVolume) == 0)
{
int num = (int)((double)(dwVolume & 0xFFFF) / 655.35);
int num2 = (int)((double)((dwVolume >> 16) & 0xFFFF) / 655.35);
int num3 = (num + num2) / 2;
return new LauncherItem($"볼륨: {num3}%", $"L: {num}% · R: {num2}% · Enter로 사운드 설정 열기", null, new InfoAction("ms_settings", "ms-settings:sound"), null, (num3 == 0) ? "\ue74f" : "\ue995");
}
}
catch
{
}
return null;
}
private static string GetOsVersion()
{
try
{
using RegistryKey registryKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
if (registryKey != null)
{
string text = (registryKey.GetValue("ProductName") as string) ?? "Windows";
string text2 = (registryKey.GetValue("CurrentBuildNumber") as string) ?? "";
object value = registryKey.GetValue("UBR");
return (value != null) ? $"{text} (빌드 {text2}.{value})" : (text + " (빌드 " + text2 + ")");
}
}
catch
{
}
return Environment.OSVersion.ToString();
}
private static string GetProcessorName()
{
try
{
using RegistryKey registryKey = Registry.LocalMachine.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0");
return (registryKey?.GetValue("ProcessorNameString") as string) ?? "";
}
catch
{
return "";
}
}
private static string FormatUptime(TimeSpan t)
{
if (!(t.TotalDays >= 1.0))
{
if (!(t.TotalHours >= 1.0))
{
return $"{t.Minutes}분 {t.Seconds}초";
}
return $"{t.Hours}시간 {t.Minutes}분";
}
return $"{(int)t.TotalDays}일 {t.Hours}시간 {t.Minutes}분";
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class TextStatsHandler : IActionHandler
{
public string? Prefix => "stats";
public PluginMetadata Metadata => new PluginMetadata("TextStats", "텍스트 통계 분석", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string text = null;
try
{
Application current = Application.Current;
if (current != null && ((DispatcherObject)current).Dispatcher.Invoke<bool>((Func<bool>)(() => Clipboard.ContainsText())))
{
text = ((DispatcherObject)Application.Current).Dispatcher.Invoke<string>((Func<string>)(() => Clipboard.GetText()));
}
}
catch
{
}
if (string.IsNullOrEmpty(text))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("클립보드에 텍스트가 없습니다", "텍스트를 복사한 후 다시 시도하세요", null, null, null, "\ue946")));
}
string text2 = query.Trim();
List<LauncherItem> list = new List<LauncherItem>();
int length = text.Length;
int length2 = text.Replace(" ", "").Replace("\t", "").Replace("\r", "")
.Replace("\n", "")
.Length;
string[] array = (from w in Regex.Split(text, "\\s+", RegexOptions.None, TimeSpan.FromSeconds(1.0))
where !string.IsNullOrWhiteSpace(w)
select w).ToArray();
int num = array.Length;
string[] array2 = text.Split('\n');
int value = array2.Length;
int value2 = array2.Count((string l) => !string.IsNullOrWhiteSpace(l));
int byteCount = Encoding.UTF8.GetByteCount(text);
string text3 = $"글자 {length:N0} · 공백 제외 {length2:N0} · 단어 {num:N0} · 줄 {value:N0}";
list.Add(new LauncherItem($"글자 수: {length:N0} ({length2:N0} 공백 제외)", $"UTF-8: {byteCount:N0} bytes · Enter로 결과 복사", null, $"글자 수: {length:N0} (공백 제외: {length2:N0})", null, "\ue8d2"));
list.Add(new LauncherItem($"단어 수: {num:N0}", "공백·줄바꿈 기준 분리 · Enter로 결과 복사", null, $"단어 수: {num:N0}", null, "\ue8d2"));
list.Add(new LauncherItem($"줄 수: {value:N0} (비어있지 않은 줄: {value2:N0})", "Enter로 결과 복사", null, $"줄 수: {value:N0} (비어있지 않은 줄: {value2:N0})", null, "\ue8d2"));
int num2 = text.Count((char c) => c >= '가' && c <= '\ud7af');
double num3 = ((num2 > length / 2) ? ((double)length2 / 500.0) : ((double)num / 200.0));
string text4 = ((num3 < 1.0) ? "1분 미만" : $"약 {Math.Ceiling(num3)}분");
list.Add(new LauncherItem("예상 읽기 시간: " + text4, (num2 > length / 2) ? "한국어 기준 (500자/분)" : "영어 기준 (200단어/분)", null, "예상 읽기 시간: " + text4, null, "\ue823"));
if (string.IsNullOrWhiteSpace(text2))
{
IEnumerable<string> values = from g in (from w in array
where w.Length >= 2
group w by w.ToLowerInvariant() into g
orderby g.Count() descending
select g).Take(5)
select $"{g.Key}({g.Count()})";
string text5 = string.Join(", ", values);
if (!string.IsNullOrEmpty(text5))
{
list.Add(new LauncherItem("상위 키워드", text5, null, "상위 키워드: " + text5, null, "\ue721"));
}
}
else
{
try
{
int count = Regex.Matches(text, Regex.Escape(text2), RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1.0)).Count;
list.Insert(0, new LauncherItem($"'{text2}' 출현 횟수: {count}회", "대소문자 무시 검색 · Enter로 결과 복사", null, $"'{text2}' 출현 횟수: {count}회", null, "\ue721"));
}
catch
{
}
}
list.Add(new LauncherItem("전체 요약 복사", text3, null, $"[텍스트 통계]\n{text3}\nUTF-8: {byteCount:N0} bytes\n읽기 시간: {text4}", null, "\ue77f"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
string text = data as string;
if (text != null && !string.IsNullOrWhiteSpace(text))
{
try
{
Application current = Application.Current;
if (current != null)
{
((DispatcherObject)current).Dispatcher.Invoke((Action)delegate
{
Clipboard.SetText(text);
});
}
}
catch
{
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
using Microsoft.Win32;
namespace AxCopilot.Handlers;
public class UninstallHandler : IActionHandler
{
private static readonly string[] _regPaths = new string[2] { "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall" };
private static (DateTime At, List<InstalledApp> Apps)? _cache;
private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30.0);
public string? Prefix => "uninstall";
public PluginMetadata Metadata => new PluginMetadata("Uninstall", "앱 제거 — uninstall 뒤에 앱 이름 입력", "1.0", "AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("제거할 앱 이름을 입력하세요", "예: uninstall chrome · uninstall office", null, null, null, "\ue74d")));
}
List<InstalledApp> installedApps = GetInstalledApps();
List<InstalledApp> source = (from a in installedApps
where a.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || (a.Publisher?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false)
orderby a.Name
select a).Take(12).ToList();
if (!source.Any())
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("검색 결과 없음", "'" + q + "'에 해당하는 설치된 앱이 없습니다", null, null, null, "\ue946")));
}
List<LauncherItem> result = source.Select((InstalledApp a) => new LauncherItem(a.Name, BuildSubtitle(a), null, a, null, "\ue74d")).ToList();
return Task.FromResult((IEnumerable<LauncherItem>)result);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (!(item.Data is InstalledApp installedApp))
{
return Task.CompletedTask;
}
if (string.IsNullOrWhiteSpace(installedApp.UninstallString))
{
LogService.Warn("제거 문자열 없음: " + installedApp.Name);
return Task.CompletedTask;
}
try
{
string text = installedApp.UninstallString.Trim();
if (text.StartsWith("msiexec", StringComparison.OrdinalIgnoreCase))
{
string arguments = text.Substring("msiexec".Length).Trim();
Process.Start(new ProcessStartInfo("msiexec.exe", arguments)
{
UseShellExecute = true
});
}
else
{
string fileName;
string arguments2;
if (text.StartsWith('"'))
{
int num = text.IndexOf('"', 1);
fileName = ((num > 0) ? text.Substring(1, num - 1) : text);
object obj;
if (num <= 0)
{
obj = "";
}
else
{
string text2 = text;
int num2 = num + 1;
obj = text2.Substring(num2, text2.Length - num2).Trim();
}
arguments2 = (string)obj;
}
else
{
int num3 = text.IndexOf(' ');
if (num3 > 0)
{
fileName = text.Substring(0, num3);
string text2 = text;
int num2 = num3 + 1;
arguments2 = text2.Substring(num2, text2.Length - num2);
}
else
{
fileName = text;
arguments2 = "";
}
}
ProcessStartInfo startInfo = new ProcessStartInfo(fileName, arguments2)
{
UseShellExecute = true
};
Process.Start(startInfo);
}
}
catch (Exception ex)
{
LogService.Warn("제거 실행 실패 [" + installedApp.Name + "]: " + ex.Message);
}
return Task.CompletedTask;
}
private static string BuildSubtitle(InstalledApp a)
{
List<string> list = new List<string>();
if (!string.IsNullOrWhiteSpace(a.Publisher))
{
list.Add(a.Publisher);
}
if (!string.IsNullOrWhiteSpace(a.Version))
{
list.Add("v" + a.Version);
}
if (!string.IsNullOrWhiteSpace(a.InstallDate))
{
list.Add(a.InstallDate);
}
list.Add("Enter로 제거");
return string.Join(" · ", list);
}
private static List<InstalledApp> GetInstalledApps()
{
if (_cache.HasValue && DateTime.Now - _cache.Value.At < CacheTtl)
{
return _cache.Value.Apps;
}
List<InstalledApp> list = new List<InstalledApp>();
HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
RegistryKey[] array = new RegistryKey[2]
{
Registry.LocalMachine,
Registry.CurrentUser
};
foreach (RegistryKey registryKey in array)
{
string[] regPaths = _regPaths;
foreach (string name in regPaths)
{
try
{
using RegistryKey registryKey2 = registryKey.OpenSubKey(name);
if (registryKey2 == null)
{
continue;
}
string[] subKeyNames = registryKey2.GetSubKeyNames();
foreach (string name2 in subKeyNames)
{
try
{
using RegistryKey registryKey3 = registryKey2.OpenSubKey(name2);
if (registryKey3 != null)
{
string text = registryKey3.GetValue("DisplayName") as string;
string text2 = registryKey3.GetValue("UninstallString") as string;
if (!string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(text2) && (!text.StartsWith("KB", StringComparison.OrdinalIgnoreCase) || text.Length >= 12) && hashSet.Add(text) && (!(registryKey3.GetValue("SystemComponent") is int num) || num != 1))
{
list.Add(new InstalledApp
{
Name = text,
UninstallString = text2,
Publisher = (registryKey3.GetValue("Publisher") as string),
Version = (registryKey3.GetValue("DisplayVersion") as string),
InstallDate = FormatDate(registryKey3.GetValue("InstallDate") as string)
});
}
}
}
catch
{
}
}
}
catch
{
}
}
}
list.Sort((InstalledApp a, InstalledApp b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
_cache = (DateTime.Now, list);
return list;
}
private static string? FormatDate(string? raw)
{
if (raw != null && raw.Length == 8 && int.TryParse(raw, out var _))
{
return $"{raw.Substring(0, 4)}-{raw.Substring(4, 2)}-{raw.Substring(6, 2)}";
}
return null;
}
}

View File

@@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
namespace AxCopilot.Handlers;
internal static class UnitConverter
{
private static readonly Regex Pattern = new Regex("^(-?\\d+(?:\\.\\d+)?)\\s*([a-z°/²³µ]+)\\s+(?:in|to)\\s+([a-z°/²³µ]+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Dictionary<string, double> _length = new Dictionary<string, double>
{
["km"] = 1000.0,
["m"] = 1.0,
["cm"] = 0.01,
["mm"] = 0.001,
["mi"] = 1609.344,
["mile"] = 1609.344,
["miles"] = 1609.344,
["ft"] = 0.3048,
["feet"] = 0.3048,
["foot"] = 0.3048,
["in"] = 0.0254,
["inch"] = 0.0254,
["inches"] = 0.0254,
["yd"] = 0.9144,
["yard"] = 0.9144,
["yards"] = 0.9144,
["nm"] = 1E-09
};
private static readonly Dictionary<string, double> _weight = new Dictionary<string, double>
{
["t"] = 1000.0,
["ton"] = 1000.0,
["tonnes"] = 1000.0,
["kg"] = 1.0,
["g"] = 0.001,
["mg"] = 1E-06,
["lb"] = 0.453592,
["lbs"] = 0.453592,
["pound"] = 0.453592,
["pounds"] = 0.453592,
["oz"] = 0.0283495,
["ounce"] = 0.0283495,
["ounces"] = 0.0283495
};
private static readonly Dictionary<string, double> _speed = new Dictionary<string, double>
{
["m/s"] = 1.0,
["mps"] = 1.0,
["km/h"] = 5.0 / 18.0,
["kmh"] = 5.0 / 18.0,
["kph"] = 5.0 / 18.0,
["mph"] = 0.44704,
["kn"] = 0.514444,
["knot"] = 0.514444,
["knots"] = 0.514444
};
private static readonly Dictionary<string, double> _data = new Dictionary<string, double>
{
["b"] = 1.0,
["byte"] = 1.0,
["bytes"] = 1.0,
["kb"] = 1024.0,
["kib"] = 1024.0,
["mb"] = 1048576.0,
["mib"] = 1048576.0,
["gb"] = 1073741824.0,
["gib"] = 1073741824.0,
["tb"] = 1099511627776.0,
["tib"] = 1099511627776.0,
["pb"] = 1125899906842624.0
};
private static readonly Dictionary<string, double> _area = new Dictionary<string, double>
{
["m²"] = 1.0,
["m2"] = 1.0,
["km²"] = 1000000.0,
["km2"] = 1000000.0,
["cm²"] = 0.0001,
["cm2"] = 0.0001,
["ha"] = 10000.0,
["acre"] = 4046.86,
["acres"] = 4046.86,
["ft²"] = 0.092903,
["ft2"] = 0.092903
};
private static readonly List<Dictionary<string, double>> _tables = new List<Dictionary<string, double>> { _length, _weight, _speed, _data, _area };
public static bool TryConvert(string input, out string? result)
{
result = null;
Match match = Pattern.Match(input.Trim());
if (!match.Success)
{
return false;
}
if (!double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2))
{
return false;
}
string text = match.Groups[2].Value.ToLowerInvariant();
string text2 = match.Groups[3].Value.ToLowerInvariant();
if (TryConvertTemperature(result2, text, text2, out var r))
{
result = FormatNum(r) + " " + TemperatureLabel(text2);
return true;
}
foreach (Dictionary<string, double> table in _tables)
{
if (table.TryGetValue(text, out var value) && table.TryGetValue(text2, out var value2))
{
double v = result2 * value / value2;
result = FormatNum(v) + " " + text2;
return true;
}
}
return false;
}
private static bool TryConvertTemperature(double v, string from, string to, out double r)
{
r = 0.0;
double num;
switch (from)
{
case "c":
case "°c":
case "celsius":
num = v;
break;
case "f":
case "°f":
case "fahrenheit":
num = (v - 32.0) * 5.0 / 9.0;
break;
case "k":
case "kelvin":
num = v - 273.15;
break;
default:
return false;
}
switch (to)
{
case "c":
case "°c":
case "celsius":
r = num;
break;
case "f":
case "°f":
case "fahrenheit":
r = num * 9.0 / 5.0 + 32.0;
break;
case "k":
case "kelvin":
r = num + 273.15;
break;
default:
return false;
}
return true;
}
private static string TemperatureLabel(string unit)
{
if (1 == 0)
{
}
string result;
switch (unit)
{
case "c":
case "°c":
case "celsius":
result = "°C";
break;
case "f":
case "°f":
case "fahrenheit":
result = "°F";
break;
case "k":
case "kelvin":
result = "K";
break;
default:
result = unit;
break;
}
if (1 == 0)
{
}
return result;
}
private static string FormatNum(double v)
{
if (v == Math.Floor(v) && Math.Abs(v) < 1000000000000.0)
{
return ((long)v).ToString("N0", CultureInfo.CurrentCulture);
}
return v.ToString("G6", CultureInfo.InvariantCulture);
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class UrlAliasHandler : IActionHandler
{
private readonly SettingsService _settings;
private static readonly HashSet<string> AllowedSchemes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "http", "https", "ftp", "ftps", "ms-settings", "mailto", "file" };
public string? Prefix => "@";
public PluginMetadata Metadata => new PluginMetadata("url-alias", "URL 별칭", "1.0", "AX");
public UrlAliasHandler(SettingsService settings)
{
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
IEnumerable<LauncherItem> result = _settings.Settings.Aliases.Where((AliasEntry a) => a.Type == "url" && (string.IsNullOrEmpty(query) || a.Key.Contains(query, StringComparison.OrdinalIgnoreCase))).Select(delegate(AliasEntry a)
{
string faviconPath = GetFaviconPath(a.Target);
return new LauncherItem(a.Key, a.Description ?? a.Target, faviconPath, a, a.Target, "\ue774");
});
return Task.FromResult(result);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is AliasEntry aliasEntry)
{
string text = Environment.ExpandEnvironmentVariables(aliasEntry.Target);
if (!IsAllowedUrl(text))
{
LogService.Warn("허용되지 않는 URL 스킴: " + text);
return Task.CompletedTask;
}
Process.Start(new ProcessStartInfo(text)
{
UseShellExecute = true
});
}
return Task.CompletedTask;
}
private static bool IsAllowedUrl(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri result))
{
return false;
}
return AllowedSchemes.Contains(result.Scheme);
}
private static string? GetFaviconPath(string url)
{
try
{
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
url = "https://" + url;
}
string text = new Uri(url).Host.ToLowerInvariant();
string text2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favicons", text + ".png");
if (File.Exists(text2))
{
return text2;
}
FaviconService.GetFavicon(url);
return null;
}
catch
{
return null;
}
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class WebSearchHandler : IActionHandler
{
private readonly SettingsService _settings;
private static readonly string _iconDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "Assets", "SearchEngines");
private static readonly Dictionary<string, (string Name, string UrlTemplate, string Icon)> _engines = new Dictionary<string, (string, string, string)>
{
["g"] = ("Google", "https://www.google.com/search?q={0}", "google"),
["n"] = ("Naver", "https://search.naver.com/search.naver?query={0}", "naver"),
["y"] = ("YouTube", "https://www.youtube.com/results?search_query={0}", "youtube"),
["gh"] = ("GitHub", "https://github.com/search?q={0}", "github"),
["d"] = ("DuckDuckGo", "https://duckduckgo.com/?q={0}", "duckduckgo"),
["w"] = ("Wikipedia", "https://ko.wikipedia.org/w/index.php?search={0}", "wikipedia"),
["nm"] = ("Naver Map", "https://map.naver.com/p/search/{0}", "navermap"),
["nw"] = ("나무위키", "https://namu.wiki/Special:Search?query={0}", "namuwiki"),
["ni"] = ("Naver 이미지", "https://search.naver.com/search.naver?where=image&query={0}", "naver"),
["gi"] = ("Google 이미지", "https://www.google.com/search?tbm=isch&q={0}", "google")
};
private const string SecurityWarn = "⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지";
public string? Prefix => "?";
public PluginMetadata Metadata => new PluginMetadata("WebSearch", "웹 검색 — ? 뒤에 검색어 또는 URL 입력", "1.0", "AX");
private string DefaultEngineKey => _settings.Settings.Launcher.WebSearchEngine ?? "g";
private (string Name, string UrlTemplate, string Icon) DefaultEngine
{
get
{
(string, string, string) value;
return _engines.TryGetValue(DefaultEngineKey, out value) ? value : _engines["g"];
}
}
public WebSearchHandler(SettingsService settings)
{
_settings = settings;
}
private static string? GetIconPath(string engineKey)
{
if (!_engines.TryGetValue(engineKey, out (string, string, string) value))
{
return null;
}
string text = Path.Combine(_iconDir, value.Item3 + ".png");
return File.Exists(text) ? text : null;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(query))
{
string iconPath = GetIconPath(DefaultEngineKey);
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem("검색어를 입력하세요", "예: ? 오늘 날씨 · ?g python · ?y 음악 · ?n 뉴스 | ⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", iconPath, null, null, "\ue774")));
}
List<LauncherItem> list = new List<LauncherItem>();
string text = query.Trim();
if (IsUrl(text))
{
string text2 = (text.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? text : ("https://" + text));
string faviconPathForUrl = GetFaviconPathForUrl(text2);
list.Add(new LauncherItem("열기: " + text, text2 + " | ⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", faviconPathForUrl, text2, null, "\ue774"));
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
string[] array = text.Split(' ', 2);
string text3 = array[0].ToLowerInvariant();
if (array.Length == 2 && _engines.TryGetValue(text3, out (string, string, string) value))
{
string text4 = array[1].Trim();
if (!string.IsNullOrWhiteSpace(text4))
{
string data = string.Format(value.Item2, Uri.EscapeDataString(text4));
list.Add(new LauncherItem(value.Item1 + "에서 '" + text4 + "' 검색", "⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", GetIconPath(text3), data, null, "\ue774"));
}
}
string defaultEngineKey = DefaultEngineKey;
(string, string, string) defaultEngine = DefaultEngine;
string data2 = string.Format(defaultEngine.Item2, Uri.EscapeDataString(text));
list.Add(new LauncherItem(defaultEngine.Item1 + "에서 '" + text + "' 검색", "⚠ 검색어가 외부로 전송됩니다 — 기밀 데이터 입력 금지", GetIconPath(defaultEngineKey), data2, null, "\ue774"));
if (text.Length <= 2)
{
foreach (var (text6, tuple2) in _engines)
{
list.Add(new LauncherItem("?" + text6 + " — " + tuple2.Item1, $"예: ?{text6} {text} | {" "}", GetIconPath(text6), null, null, "\ue774"));
}
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text && !string.IsNullOrWhiteSpace(text))
{
try
{
Process.Start(new ProcessStartInfo(text)
{
UseShellExecute = true
});
}
catch (Exception ex)
{
LogService.Warn("웹 검색 열기 실패: " + ex.Message);
}
}
return Task.CompletedTask;
}
private static bool IsUrl(string input)
{
if (input.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || input.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (!input.Contains(' ') && input.Contains('.') && input.Length > 3)
{
string text = input.Split('/')[0];
string[] array = text.Split('.');
return array.Length >= 2 && array[^1].Length >= 2 && array[^1].Length <= 6 && array[^1].All(char.IsLetter);
}
return false;
}
private static string? GetFaviconPathForUrl(string url)
{
try
{
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
url = "https://" + url;
}
string text = new Uri(url).Host.ToLowerInvariant();
string text2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favicons", text + ".png");
if (File.Exists(text2))
{
return text2;
}
FaviconService.GetFavicon(url);
return null;
}
catch
{
return null;
}
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.SDK;
namespace AxCopilot.Handlers;
public class WindowSwitchHandler : IActionHandler
{
private delegate bool EnumWindowsProc(nint hWnd, nint lParam);
private record WindowInfo(nint Handle, string Title, string ProcessName, bool IsMinimized);
private const int SW_RESTORE = 9;
public string? Prefix => "win";
public PluginMetadata Metadata => new PluginMetadata("WindowSwitch", "윈도우 포커스 스위처 — win", "1.0", "AX");
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(nint hWnd);
[DllImport("user32.dll")]
private static extern int GetWindowTextLength(nint hWnd);
[DllImport("user32.dll")]
private static extern int GetWindowText(nint hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(nint hWnd);
[DllImport("user32.dll")]
private static extern bool ShowWindow(nint hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern bool IsIconic(nint hWnd);
[DllImport("user32.dll")]
private static extern nint GetShellWindow();
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string q = query.Trim();
List<WindowInfo> list = GetOpenWindows();
if (!string.IsNullOrWhiteSpace(q))
{
list = list.Where((WindowInfo w) => w.Title.Contains(q, StringComparison.OrdinalIgnoreCase) || w.ProcessName.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
}
if (!list.Any())
{
return Task.FromResult((IEnumerable<LauncherItem>)new _003C_003Ez__ReadOnlySingleElementList<LauncherItem>(new LauncherItem(string.IsNullOrWhiteSpace(q) ? "열린 창이 없습니다" : ("'" + q + "'에 해당하는 창 없음"), "win 뒤에 프로세스명 또는 창 제목 입력", null, null, null, "\ue946")));
}
List<LauncherItem> list2 = (from w in list.Take(15)
select new LauncherItem((w.Title.Length > 60) ? (w.Title.Substring(0, 57) + "…") : w.Title, w.ProcessName + " · " + (w.IsMinimized ? "[최소화됨] · " : "") + "Enter → 전환", null, w.Handle, null, "\ue7f4")).ToList();
list2.Insert(0, new LauncherItem($"열린 창 {list.Count}개", "Enter → 해당 창으로 즉시 전환", null, null, null, "\ue946"));
return Task.FromResult((IEnumerable<LauncherItem>)list2);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is nint num && num != IntPtr.Zero)
{
if (IsIconic(num))
{
ShowWindow(num, 9);
}
SetForegroundWindow(num);
}
return Task.CompletedTask;
}
private static List<WindowInfo> GetOpenWindows()
{
List<WindowInfo> list = new List<WindowInfo>();
nint shellWnd = GetShellWindow();
string currentProcessName = Process.GetCurrentProcess().ProcessName;
EnumWindows(delegate(nint hWnd, nint _)
{
if (hWnd == shellWnd)
{
return true;
}
if (!IsWindowVisible(hWnd))
{
return true;
}
int windowTextLength = GetWindowTextLength(hWnd);
if (windowTextLength == 0)
{
return true;
}
StringBuilder stringBuilder = new StringBuilder(windowTextLength + 1);
GetWindowText(hWnd, stringBuilder, stringBuilder.Capacity);
string title = stringBuilder.ToString();
string text = "";
try
{
GetWindowThreadProcessId(hWnd, out var lpdwProcessId);
text = Process.GetProcessById((int)lpdwProcessId).ProcessName;
}
catch
{
}
if (text == currentProcessName)
{
return true;
}
bool flag;
switch (text)
{
case "ApplicationFrameHost":
case "ShellExperienceHost":
case "SearchHost":
case "StartMenuExperienceHost":
case "TextInputHost":
flag = true;
break;
default:
flag = false;
break;
}
if (flag)
{
return true;
}
list.Add(new WindowInfo(hWnd, title, text, IsIconic(hWnd)));
return true;
}, IntPtr.Zero);
return list;
}
}

View File

@@ -0,0 +1,3 @@
namespace AxCopilot.Handlers;
public record WorkspaceAction(WorkspaceActionType Type, string Name, string? NewName = null);

View File

@@ -0,0 +1,9 @@
namespace AxCopilot.Handlers;
public enum WorkspaceActionType
{
Restore,
Save,
Delete,
Rename
}

View File

@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AxCopilot.Core;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
public class WorkspaceHandler : IActionHandler
{
private readonly ContextManager _context;
private readonly SettingsService _settings;
public string? Prefix => "~";
public PluginMetadata Metadata => new PluginMetadata("workspace", "워크스페이스", "1.0", "AX");
public WorkspaceHandler(ContextManager context, SettingsService settings)
{
_context = context;
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
string[] array = query.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (array.Length == 0 || string.IsNullOrEmpty(query))
{
List<LauncherItem> list = _settings.Settings.Profiles.Select((WorkspaceProfile p) => new LauncherItem("~" + p.Name, $"{p.Windows.Count}개 창 | {p.CreatedAt:MM/dd HH:mm}", null, p, null, "\ue8a1")).ToList();
if (list.Count == 0)
{
list.Add(new LauncherItem("저장된 프로필 없음", "~save <이름> 으로 현재 배치를 저장하세요", null, null, null, "\ue946"));
}
return Task.FromResult((IEnumerable<LauncherItem>)list);
}
string text = array[0].ToLowerInvariant();
if (text == "save")
{
string text2 = ((array.Length > 1) ? array[1] : "default");
return Task.FromResult((IEnumerable<LauncherItem>)new LauncherItem[1]
{
new LauncherItem("현재 창 배치를 '" + text2 + "'으로 저장", "Enter로 확인", null, new WorkspaceAction(WorkspaceActionType.Save, text2), null, "\ue74e")
});
}
if (text == "delete" && array.Length > 1)
{
return Task.FromResult((IEnumerable<LauncherItem>)new LauncherItem[1]
{
new LauncherItem("프로필 '" + array[1] + "' 삭제", "Enter로 확인 (되돌릴 수 없습니다)", null, new WorkspaceAction(WorkspaceActionType.Delete, array[1]), null, "\ue74d")
});
}
if (text == "rename" && array.Length > 2)
{
return Task.FromResult((IEnumerable<LauncherItem>)new LauncherItem[1]
{
new LauncherItem($"프로필 '{array[1]}' → '{array[2]}'로 이름 변경", "Enter로 확인", null, new WorkspaceAction(WorkspaceActionType.Rename, array[1], array[2]), null, "\ue8ac")
});
}
IEnumerable<LauncherItem> result = from p in _settings.Settings.Profiles
where p.Name.Contains(query, StringComparison.OrdinalIgnoreCase)
select new LauncherItem("~" + p.Name + " 복원", $"{p.Windows.Count}개 창 | {p.CreatedAt:MM/dd HH:mm}", null, new WorkspaceAction(WorkspaceActionType.Restore, p.Name), null, "\ue72c");
return Task.FromResult(result);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
object data = item.Data;
if (!(data is WorkspaceAction action))
{
return;
}
switch (action.Type)
{
case WorkspaceActionType.Restore:
LogService.Info("복원 결과: " + (await _context.RestoreProfileAsync(action.Name, ct)).Message);
break;
case WorkspaceActionType.Save:
_context.CaptureProfile(action.Name);
break;
case WorkspaceActionType.Delete:
_context.DeleteProfile(action.Name);
break;
case WorkspaceActionType.Rename:
if (action.NewName != null)
{
_context.RenameProfile(action.Name, action.NewName);
}
break;
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace AxCopilot.Models;
public class AgentHookEntry
{
[JsonPropertyName("name")]
public string Name { get; set; } = "";
[JsonPropertyName("toolName")]
public string ToolName { get; set; } = "*";
[JsonPropertyName("timing")]
public string Timing { get; set; } = "post";
[JsonPropertyName("scriptPath")]
public string ScriptPath { get; set; } = "";
[JsonPropertyName("arguments")]
public string Arguments { get; set; } = "";
[JsonPropertyName("enabled")]
public bool Enabled { get; set; } = true;
}

View File

@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace AxCopilot.Models;
public class AliasEntry
{
[JsonPropertyName("key")]
public string Key { get; set; } = "";
[JsonPropertyName("type")]
public string Type { get; set; } = "url";
[JsonPropertyName("target")]
public string Target { get; set; } = "";
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("showWindow")]
public bool ShowWindow { get; set; } = false;
[JsonPropertyName("adapter")]
public string? Adapter { get; set; }
[JsonPropertyName("query")]
public string? Query { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace AxCopilot.Models;
public class ApiAdapter
{
[JsonPropertyName("id")]
public string Id { get; set; } = "";
[JsonPropertyName("baseUrl")]
public string BaseUrl { get; set; } = "";
[JsonPropertyName("credentialKey")]
public string CredentialKey { get; set; } = "";
}

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace AxCopilot.Models;
public class AppSettings
{
[JsonPropertyName("version")]
public string Version { get; set; } = "1.0";
[JsonPropertyName("ai_enabled")]
public bool AiEnabled { get; set; } = false;
[JsonPropertyName("hotkey")]
public string Hotkey { get; set; } = "Alt+Space";
[JsonPropertyName("launcher")]
public LauncherSettings Launcher { get; set; } = new LauncherSettings();
[JsonPropertyName("indexPaths")]
public List<string> IndexPaths { get; set; } = new List<string> { "%USERPROFILE%\\Desktop", "%APPDATA%\\Microsoft\\Windows\\Start Menu" };
[JsonPropertyName("indexExtensions")]
public List<string> IndexExtensions { get; set; } = new List<string>
{
".exe", ".lnk", ".bat", ".ps1", ".url", ".cmd", ".msi", ".pdf", ".doc", ".docx",
".xls", ".xlsx", ".ppt", ".pptx", ".hwp", ".hwpx", ".txt", ".md", ".csv", ".json",
".xml", ".yaml", ".yml", ".log", ".ini", ".cfg", ".conf", ".png", ".jpg", ".jpeg",
".gif", ".bmp", ".svg", ".webp", ".ico", ".tiff", ".zip", ".7z", ".rar"
};
[JsonPropertyName("indexSpeed")]
public string IndexSpeed { get; set; } = "normal";
[JsonPropertyName("monitorMismatch")]
public string MonitorMismatch { get; set; } = "warn";
[JsonPropertyName("profiles")]
public List<WorkspaceProfile> Profiles { get; set; } = new List<WorkspaceProfile>();
[JsonPropertyName("aliases")]
public List<AliasEntry> Aliases { get; set; } = new List<AliasEntry>();
[JsonPropertyName("clipboardTransformers")]
public List<ClipboardTransformer> ClipboardTransformers { get; set; } = new List<ClipboardTransformer>();
[JsonPropertyName("apiAdapters")]
public List<ApiAdapter> ApiAdapters { get; set; } = new List<ApiAdapter>();
[JsonPropertyName("plugins")]
public List<PluginEntry> Plugins { get; set; } = new List<PluginEntry>();
[JsonPropertyName("snippets")]
public List<SnippetEntry> Snippets { get; set; } = new List<SnippetEntry>();
[JsonPropertyName("clipboardHistory")]
public ClipboardHistorySettings ClipboardHistory { get; set; } = new ClipboardHistorySettings();
[JsonPropertyName("systemCommands")]
public SystemCommandSettings SystemCommands { get; set; } = new SystemCommandSettings();
[JsonPropertyName("screenCapture")]
public ScreenCaptureSettings ScreenCapture { get; set; } = new ScreenCaptureSettings();
[JsonPropertyName("reminder")]
public ReminderSettings Reminder { get; set; } = new ReminderSettings();
[JsonPropertyName("llm")]
public LlmSettings Llm { get; set; } = new LlmSettings();
}

View File

@@ -0,0 +1,71 @@
namespace AxCopilot.Models;
public static class ChatCategory
{
public const string General = "일반";
public const string Management = "경영";
public const string HR = "인사";
public const string Finance = "재무";
public const string RnD = "연구개발";
public const string Product = "제품분석";
public const string Yield = "수율분석";
public const string MfgTech = "제조기술";
public const string System = "시스템";
public static readonly (string Key, string Label, string Symbol, string Color)[] All = new(string, string, string, string)[9]
{
("일반", "일반", "\ue8bd", "#6B7280"),
("경영", "경영", "\ue902", "#8B5CF6"),
("인사", "인사", "\ue716", "#0EA5E9"),
("재무", "재무", "\ue8c7", "#D97706"),
("연구개발", "연구개발", "\ue9a8", "#3B82F6"),
("제품분석", "제품분석", "\ue9d9", "#EC4899"),
("수율분석", "수율분석", "\ue9f9", "#F59E0B"),
("제조기술", "제조기술", "\ue90f", "#10B981"),
("시스템", "시스템", "\ue770", "#EF4444")
};
public static string GetSymbol(string? category)
{
if (string.IsNullOrEmpty(category))
{
return "\ue8bd";
}
(string, string, string, string)[] all = All;
for (int i = 0; i < all.Length; i++)
{
var (text, _, result, _) = all[i];
if (text == category)
{
return result;
}
}
return "\ue8bd";
}
public static string GetColor(string? category)
{
if (string.IsNullOrEmpty(category))
{
return "#6B7280";
}
(string, string, string, string)[] all = All;
for (int i = 0; i < all.Length; i++)
{
var (text, _, _, result) = all[i];
if (text == category)
{
return result;
}
}
return "#6B7280";
}
}

View File

@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace AxCopilot.Models;
public class ChatConversation
{
[JsonPropertyName("id")]
public string Id { get; set; } = Guid.NewGuid().ToString("N");
[JsonPropertyName("title")]
public string Title { get; set; } = "새 대화";
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.Now;
[JsonPropertyName("updatedAt")]
public DateTime UpdatedAt { get; set; } = DateTime.Now;
[JsonPropertyName("pinned")]
public bool Pinned { get; set; } = false;
[JsonPropertyName("tab")]
public string Tab { get; set; } = "Chat";
[JsonPropertyName("category")]
public string Category { get; set; } = "일반";
[JsonPropertyName("systemCommand")]
public string SystemCommand { get; set; } = "";
[JsonPropertyName("workFolder")]
public string WorkFolder { get; set; } = "";
[JsonPropertyName("preview")]
public string Preview { get; set; } = "";
[JsonPropertyName("parentId")]
public string? ParentId { get; set; }
[JsonPropertyName("branchLabel")]
public string? BranchLabel { get; set; }
[JsonPropertyName("branchAtIndex")]
public int? BranchAtIndex { get; set; }
[JsonPropertyName("permission")]
public string? Permission { get; set; }
[JsonPropertyName("dataUsage")]
public string? DataUsage { get; set; }
[JsonPropertyName("outputFormat")]
public string? OutputFormat { get; set; }
[JsonPropertyName("mood")]
public string? Mood { get; set; }
[JsonPropertyName("messages")]
public List<ChatMessage> Messages { get; set; } = new List<ChatMessage>();
[JsonPropertyName("executionEvents")]
public List<ChatExecutionEvent> ExecutionEvents { get; set; } = new List<ChatExecutionEvent>();
}

Some files were not shown because too many files have changed in this diff Show More