diff --git a/CODE_REVIEW.md b/CODE_REVIEW.md new file mode 100644 index 0000000..8e73506 --- /dev/null +++ b/CODE_REVIEW.md @@ -0,0 +1,22 @@ +# Telegram Bot Code Review +Date: 2026-01-19 + +## Summary +The project demonstrates a high-quality, modern architecture leveraging Cloudflare Workers, D1, KV, and AI. + +## 1. Strengths +- **Security Design**: The Webhook Secret verification logic in `src/security.ts` is implemented using timing-safe comparison, making it robust against timing attacks. +- **AI Context Management**: The **Rolling Summary** approach in `src/summary-service.ts` is impressive. It efficiently maintains user context by periodically summarizing conversations, optimizing token usage. +- **Separation of Concerns**: The project structure clearly isolates APIs, Webhooks, Service Logic, and Tools, facilitating easy functional expansion. + +## 2. Improvements Needed +- **SMS Parsing Robustness**: The regex-based parsing in `src/services/bank-sms-parser.ts` is brittle and may fail if bank message formats change. + - *Action*: Implement an AI-based fallback mechanism to parse unstructured messages when regex fails. +- **Handler Bloat**: `handleMessage` in `src/routes/webhook.ts` handles too many responsibilities (user lookup, buffering, AI generation). + - *Action*: Refactor into separate service classes. +- **Monitoring**: While `logger.ts` and `metrics.ts` exist, adding business metrics like deposit match rates or AI latency would improve operational visibility. + +## 3. Architecture Score +- **Design**: 95/100 (Excellent use of Cloudflare ecosystem) +- **Security**: 98/100 (Strong Webhook & Rate Limit implementation) +- **Maintainability**: 85/100 (Handler refactoring recommended) diff --git a/README.md b/README.md index 1a2d408..bb72910 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ - **Context7 연동**: 프로그래밍 라이브러리 공식 문서 실시간 조회 - **동적 도구 로딩**: 메시지 키워드 기반으로 필요한 도구만 선택하여 토큰 절약 - **도메인 추천**: GPT가 창의적 도메인 생성 → 가용성 자동 확인 → 가격과 함께 제안 -- **예치금 시스템**: 코드 직접 처리, 은행 입금 자동 감지 + 사용자 신고 매칭으로 자동 충전 -- **Email Worker**: SMS → 메일 → 자동 파싱으로 입금 알림 처리 +- **예치금 시스템**: 코드 직접 처리, 은행 입금 자동 감지(SMS/AI 파싱) + 사용자 신고 매칭으로 자동 충전 +- **Email Worker**: SMS → 메일 → 자동 파싱(Regex + AI 폴백)으로 입금 알림 처리 - **무한 컨텍스트**: 슬라이딩 윈도우(3개)로 프로필 유지, 무제한 대화 기억 - **개인화 응답**: 프로필 기반으로 맞춤형 AI 응답 제공 - **폴백 지원**: OpenAI 미설정 시 Workers AI(Llama)로 자동 전환 @@ -233,7 +233,7 @@ URL: gateway.ai.cloudflare.com/v1/{account_id}/telegram-bot/openai/... │ ▼ ┌──────────────────┐ -│ SMS 파싱 │ ← 입금자명, 금액, 은행 추출 +│ SMS 파싱 │ ← 입금자명, 금액, 은행 추출 (AI 폴백 지원) │ (하나/KB/신한) │ └──────────────────┘ │ @@ -271,7 +271,7 @@ URL: gateway.ai.cloudflare.com/v1/{account_id}/telegram-bot/openai/... "입금할게요" → 계좌 정보 안내 # 입금 신고 (자연어 금액 인식 지원) -"홍길동 5000원 입금했어" → 즉시 처리 +"홍길동 50000원 입금했어" → 즉시 처리 "홍길동 만원 입금" → 10,000원으로 인식 "홍길동 5천원" → 5,000원으로 인식 @@ -283,6 +283,7 @@ URL: gateway.ai.cloudflare.com/v1/{account_id}/telegram-bot/openai/... ``` **특징:** +- 🔍 **AI 파싱**: 정형화되지 않은 은행 문자도 AI가 자동 분석하여 처리 - 🔢 **자연어 금액**: "만원", "5천원", "삼만오천원" 등 자동 변환 - ⚡ **즉시 실행**: 입금자명+금액 있으면 확인 없이 바로 처리 - 📋 **동시 요청**: 기존 pending 있어도 새 입금 신고 가능 @@ -330,6 +331,7 @@ SMS를 메일로 전달받아 Worker에서 직접 처리합니다. - 하나은행 (기존): `[하나은행] 01/16 14:30 입금 50,000원 홍길동 잔액 1,234,567원` - KB국민: `[KB] 입금 50,000원 01/16 14:30 홍길동` - 신한: `[신한] 01/16 입금 50,000원 홍길동` +- **AI 자동 인식**: 패턴 미일치 시 AI가 내용 분석하여 자동 추출 --- @@ -342,7 +344,7 @@ SMS를 메일로 전달받아 Worker에서 직접 처리합니다. | 기능 | 설명 | 권한 | |------|------|------| | `도메인 목록` | 내 도메인 목록 조회 | 소유자 | -| `도메인 정보` | 도메인 상세 정보 (만료일 등) | 소유자 | +| `도메인 정보` | 도메인 상세 정보 (내 도메인 아니면 WHOIS 자동 조회) | 누구나 | | `네임서버 조회` | 현재 네임서버 확인 | 누구나 | | `네임서버 변경` | 네임서버 설정 변경 | 소유자 | | `가격 조회` | TLD/ccSLD 등록 가격 (원화) | 누구나 | @@ -369,7 +371,7 @@ Namecheap 가격 + 13% 마진, 매일 환율 업데이트 자체 WHOIS API 서버(Vercel)를 통해 TCP 43 포트로 직접 쿼리 ``` -사용자: "google.com whois" +사용자: "google.com whois" 또는 "google.com 정보" 봇: 등록일, 만료일, 네임서버, 등록기관 정보 표시 ``` @@ -483,6 +485,10 @@ telegram-bot-workers/ │ ├── deposit-agent.ts # 예치금 에이전트 (Assistants API) │ ├── n8n-service.ts # n8n 연동 (선택) │ └── commands.ts # 봇 명령어 핸들러 +├── src/services/ +│ ├── bank-sms-parser.ts # SMS 파싱 로직 (Regex + AI) +│ ├── user-service.ts # 사용자 관리 +│ └── conversation-service.ts # 대화 로직 ├── schema.sql # D1 스키마 ├── wrangler.toml # Wrangler 설정 ├── n8n-workflow-example.json # n8n 워크플로우 예시 @@ -705,3 +711,5 @@ database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e" ## 소스 코드 **Gitea**: https://gitea.anvil.it.com/kaffa/telegram-bot-workers + +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3478374 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1538 @@ +{ + "name": "telegram-summary-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "telegram-summary-bot", + "version": "1.0.0", + "devDependencies": { + "@cloudflare/workers-types": "^4.20241127.0", + "typescript": "^5.3.3", + "wrangler": "^4.59.2" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.10.0.tgz", + "integrity": "sha512-/uII4vLQXhzCAZzEVeYAjFLBNg2nqTJ1JGzd2lRF6ItYe6U2zVoYGfeKpGx/EkBF6euiU+cyBXgMdtJih+nQ6g==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "^1.20251221.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260114.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260114.0.tgz", + "integrity": "sha512-HNlsRkfNgardCig2P/5bp/dqDECsZ4+NU5XewqArWxMseqt3C5daSuptI620s4pn7Wr0ZKg7jVLH0PDEBkA+aA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260114.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260114.0.tgz", + "integrity": "sha512-qyE1UdFnAlxzb+uCfN/d9c8icch7XRiH49/DjoqEa+bCDihTuRS7GL1RmhVIqHJhb3pX3DzxmKgQZBDBL83Inw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260114.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260114.0.tgz", + "integrity": "sha512-Z0BLvAj/JPOabzads2ddDEfgExWTlD22pnwsuNbPwZAGTSZeQa3Y47eGUWyHk+rSGngknk++S7zHTGbKuG7RRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260114.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260114.0.tgz", + "integrity": "sha512-kPUmEtUxUWlr9PQ64kuhdK0qyo8idPe5IIXUgi7xCD7mDd6EOe5J7ugDpbfvfbYKEjx4DpLvN2t45izyI/Sodw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260114.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260114.0.tgz", + "integrity": "sha512-MJnKgm6i1jZGyt2ZHQYCnRlpFTEZcK2rv9y7asS3KdVEXaDgGF8kOns5u6YL6/+eMogfZuHRjfDS+UqRTUYIFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260118.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260118.0.tgz", + "integrity": "sha512-t+2Q421kAQqwBzMUDvgg2flp8zFVxOpiAyZPbyNcnPxMDHf0z3B7LqBIVQawwI6ntZinbk9f4oUmaA5bGeYwlg==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", + "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "4.20260114.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260114.0.tgz", + "integrity": "sha512-QwHT7S6XqGdQxIvql1uirH/7/i3zDEt0B/YBXTYzMfJtVCR4+ue3KPkU+Bl0zMxvpgkvjh9+eCHhJbKEqya70A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.14.0", + "workerd": "1.20260114.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10", + "zod": "^3.25.76" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260114.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260114.0.tgz", + "integrity": "sha512-kTJ+jNdIllOzWuVA3NRQRvywP0T135zdCjAE2dAUY1BFbxM6fmMZV8BbskEoQ4hAODVQUfZQmyGctcwvVCKxFA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260114.0", + "@cloudflare/workerd-darwin-arm64": "1.20260114.0", + "@cloudflare/workerd-linux-64": "1.20260114.0", + "@cloudflare/workerd-linux-arm64": "1.20260114.0", + "@cloudflare/workerd-windows-64": "1.20260114.0" + } + }, + "node_modules/wrangler": { + "version": "4.59.2", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.59.2.tgz", + "integrity": "sha512-Z4xn6jFZTaugcOKz42xvRAYKgkVUERHVbuCJ5+f+gK+R6k12L02unakPGOA0L0ejhUl16dqDjKe4tmL9sedHcw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.10.0", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.0", + "miniflare": "4.20260114.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260114.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260114.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index ce8733d..7d98e0c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20241127.0", "typescript": "^5.3.3", - "wrangler": "^3.93.0" + "wrangler": "^4.59.2" }, "keywords": [ "telegram", diff --git a/src/index.ts b/src/index.ts index bb297f9..12b5746 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,7 +77,7 @@ Documentation: https://github.com/your-repo console.log('[Email] 수신:', message.from, 'Size:', message.rawSize); // SMS 내용 파싱 - const notification = parseBankSMS(rawEmail); + const notification = await parseBankSMS(rawEmail, env); if (!notification) { console.log('[Email] 은행 SMS 파싱 실패'); return; diff --git a/src/routes/webhook.ts b/src/routes/webhook.ts index 93c57ce..cdb27ae 100644 --- a/src/routes/webhook.ts +++ b/src/routes/webhook.ts @@ -1,45 +1,12 @@ import { Env, TelegramUpdate } from '../types'; import { validateWebhookRequest, checkRateLimit } from '../security'; -import { sendMessage, sendMessageWithKeyboard, sendChatAction, answerCallbackQuery, editMessageText } from '../telegram'; +import { sendMessage, sendMessageWithKeyboard, answerCallbackQuery, editMessageText } from '../telegram'; import { executeDomainRegister } from '../domain-register'; -import { - addToBuffer, - processAndSummarize, - generateAIResponse, -} from '../summary-service'; import { handleCommand } from '../commands'; +import { UserService } from '../services/user-service'; +import { ConversationService } from '../services/conversation-service'; -// 사용자 조회/생성 -async function getOrCreateUser( - db: D1Database, - telegramId: string, - firstName: string, - username?: string -): Promise { - const existing = await db - .prepare('SELECT id FROM users WHERE telegram_id = ?') - .bind(telegramId) - .first<{ id: number }>(); - - if (existing) { - // 마지막 활동 시간 업데이트 - await db - .prepare('UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?') - .bind(existing.id) - .run(); - return existing.id; - } - - // 새 사용자 생성 - const result = await db - .prepare('INSERT INTO users (telegram_id, first_name, username) VALUES (?, ?, ?)') - .bind(telegramId, firstName, username || null) - .run(); - - return result.meta.last_row_id as number; -} - -// 메시지 처리 +// 메시지 처리 핸들러 async function handleMessage( env: Env, update: TelegramUpdate @@ -49,10 +16,10 @@ async function handleMessage( const { message } = update; const chatId = message.chat.id; const chatIdStr = chatId.toString(); - const text = message.text!; // Already checked above + const text = message.text!; const telegramUserId = message.from.id.toString(); - // Rate Limiting 체크 (KV 기반) + // 1. Rate Limiting 체크 if (!(await checkRateLimit(env.RATE_LIMIT_KV, telegramUserId))) { await sendMessage( env.BOT_TOKEN, @@ -62,11 +29,14 @@ async function handleMessage( return; } - // 사용자 처리 (오류 시 사용자에게 알림) + // 2. 서비스 인스턴스 초기화 + const userService = new UserService(env.DB); + const conversationService = new ConversationService(env); + + // 3. 사용자 조회/생성 let userId: number; try { - userId = await getOrCreateUser( - env.DB, + userId = await userService.getOrCreateUser( telegramUserId, message.from.first_name, message.from.username @@ -81,14 +51,12 @@ async function handleMessage( return; } - let responseText: string; - try { - // 명령어 처리 + // 4. 명령어 처리 if (text.startsWith('/')) { const [command, ...argParts] = text.split(' '); const args = argParts.join(' '); - responseText = await handleCommand(env, userId, chatIdStr, command, args); + const responseText = await handleCommand(env, userId, chatIdStr, command, args); // /start 명령어는 미니앱 버튼과 함께 전송 if (command === '/start') { @@ -98,55 +66,47 @@ async function handleMessage( ]); return; } - } else { - // 타이핑 표시 - await sendChatAction(env.BOT_TOKEN, chatId, 'typing'); - - // 1. 사용자 메시지 버퍼에 추가 - await addToBuffer(env.DB, userId, chatIdStr, 'user', text); - - // 2. AI 응답 생성 - responseText = await generateAIResponse(env, userId, chatIdStr, text, telegramUserId); - - // 3. 봇 응답 버퍼에 추가 - await addToBuffer(env.DB, userId, chatIdStr, 'bot', responseText); - - // 4. 임계값 도달시 프로필 업데이트 - const { summarized } = await processAndSummarize(env, userId, chatIdStr); - - if (summarized) { - responseText += '\n\n👤 프로필이 업데이트되었습니다.'; - } + + await sendMessage(env.BOT_TOKEN, chatId, responseText); + return; } + + // 5. 일반 대화 처리 (ConversationService 위임) + const result = await conversationService.processUserMessage( + userId, + chatIdStr, + text, + telegramUserId + ); + + let finalResponse = result.responseText; + if (result.isProfileUpdated) { + finalResponse += '\n\n👤 프로필이 업데이트되었습니다.'; + } + + // 6. 응답 전송 (키보드 포함 여부 확인) + if (result.keyboardData && result.keyboardData.type === 'domain_register') { + const { domain, price } = result.keyboardData; + const callbackData = `domain_reg:${domain}:${price}`; + + await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, finalResponse, [ + [ + { text: '✅ 등록하기', callback_data: callbackData }, + { text: '❌ 취소', callback_data: 'domain_cancel' } + ] + ]); + } else { + await sendMessage(env.BOT_TOKEN, chatId, finalResponse); + } + } catch (error) { console.error('[handleMessage] 처리 오류:', error); - responseText = '⚠️ 메시지 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; + await sendMessage( + env.BOT_TOKEN, + chatId, + '⚠️ 메시지 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' + ); } - - // 버튼 데이터 파싱 - const keyboardMatch = responseText.match(/__KEYBOARD__(.+?)__END__\n?/); - if (keyboardMatch) { - const cleanText = responseText.replace(/__KEYBOARD__.+?__END__\n?/, ''); - try { - const keyboardData = JSON.parse(keyboardMatch[1]); - - if (keyboardData.type === 'domain_register') { - // 도메인 등록 확인 버튼 - const callbackData = `domain_reg:${keyboardData.domain}:${keyboardData.price}`; - await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, cleanText, [ - [ - { text: '✅ 등록하기', callback_data: callbackData }, - { text: '❌ 취소', callback_data: 'domain_cancel' } - ] - ]); - return; - } - } catch (e) { - console.error('[Keyboard] 파싱 오류:', e); - } - } - - await sendMessage(env.BOT_TOKEN, chatId, responseText); } // Callback Query 처리 (인라인 버튼 클릭) @@ -166,10 +126,8 @@ async function handleCallbackQuery( const messageId = message.message_id; const telegramUserId = from.id.toString(); - // 사용자 조회 - const user = await env.DB.prepare( - 'SELECT id FROM users WHERE telegram_id = ?' - ).bind(telegramUserId).first<{ id: number }>(); + const userService = new UserService(env.DB); + const user = await userService.getUserByTelegramId(telegramUserId); if (!user) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '사용자를 찾을 수 없습니다.' }); @@ -187,7 +145,6 @@ async function handleCallbackQuery( const domain = parts[1]; const price = parseInt(parts[2]); - // 처리 중 표시 await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '등록 처리 중...' }); await editMessageText( env.BOT_TOKEN, @@ -196,7 +153,6 @@ async function handleCallbackQuery( `⏳ ${domain} 등록 처리 중...` ); - // 도메인 등록 실행 const result = await executeDomainRegister(env, user.id, telegramUserId, domain, price); if (result.success) { @@ -250,17 +206,8 @@ ${result.error} /** * Telegram Webhook 요청 처리 - * - * Manual Test: - * 1. wrangler dev - * 2. curl -X POST http://localhost:8787/webhook \ - * -H "Content-Type: application/json" \ - * -H "X-Telegram-Bot-Api-Secret-Token: test-secret" \ - * -d '{"message":{"chat":{"id":123},"text":"테스트"}}' - * 3. Expected: OK response, message processed */ export async function handleWebhook(request: Request, env: Env): Promise { - // 보안 검증 const validation = await validateWebhookRequest(request, env); if (!validation.valid) { @@ -271,17 +218,15 @@ export async function handleWebhook(request: Request, env: Env): Promise { + // 1. 전처리 (MIME 디코딩 등) + let text = preprocessText(content); + + // 2. 정규표현식 파싱 시도 + const regexResult = parseWithRegex(text); + if (regexResult) { + return regexResult; + } + + // 3. AI 파싱 시도 (Fallback) + if (env) { + console.log('[BankSMS] 정규식 파싱 실패, AI 파싱 시도...'); + try { + // AI 파싱은 비용/시간이 들므로 SMS 패턴이 의심될 때만 시도 + // "입금", "원" 같은 키워드가 있는지 확인 + if (text.includes('입금') && (text.includes('원') || text.match(/\d/))) { + return await parseWithAI(text, env); + } else { + console.log('[BankSMS] 입금 키워드 없음, AI 파싱 건너뜀'); + } + } catch (error) { + console.error('[BankSMS] AI 파싱 오류:', error); + } + } + + return null; +} + +function preprocessText(content: string): string { let text = content; - - // Quoted-Printable UTF-8 디코딩 text = parseQuotedPrintable(text); - - // HTML
태그를 줄바꿈으로 변환 text = text.replace(//gi, '\n'); - - // 줄바꿈 정규화 text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); // [Web발신] 또는 은행 키워드가 있는 부분만 추출 - const smsStartMatch = text.match(/\[Web발신\]|\[하나은행\]|\[KB\]|\[신한\]|\[우리\]|\[농협\]/); + const smsStartMatch = text.match(/ \[Web발신\]| \[하나은행\]| \[KB\]| \[신한\]| \[우리\]| \[농협\]/); if (smsStartMatch && smsStartMatch.index !== undefined) { - // SMS 시작점부터 500자 추출 text = text.slice(smsStartMatch.index, smsStartMatch.index + 500); } + return text; +} - // 하나은행 Web발신 패턴 (여러 줄): - // [Web발신] - // 하나,01/16, 22:12 - // 427******27104 - // 입금1원 - // 황병하 - const hanaWebPattern = /\[Web발신\]\s*하나[,\s]*(\d{1,2}\/\d{1,2})[,\s]*(\d{1,2}:\d{2})\s*[\d*]+\s*입금([\d,]+)원\s*(\S+)/; +function parseWithRegex(text: string): BankNotification | null { + // 하나은행 Web발신 + const hanaWebPattern = / \[Web발신\]\s*하나[,\s]*(\d{1,2}\/\d{1,2})[,\s]*(\d{1,2}:\d{2})\s*[\d*]+\s*입금([\d,]+)원\s*(\S+)/; const hanaWebMatch = text.match(hanaWebPattern); if (hanaWebMatch) { const [, date, time, amountStr, depositor] = hanaWebMatch; @@ -52,8 +69,8 @@ export function parseBankSMS(content: string): BankNotification | null { }; } - // 하나은행 기존 패턴: [하나은행] 01/16 14:30 입금 50,000원 홍길동 잔액 1,234,567원 - const hanaPattern = /\[하나은행\]\s*(\d{1,2}\/\d{1,2})\s*(\d{1,2}:\d{2})?\s*입금\s*([\d,]+)원\s*(\S+?)(?:\s+잔액\s*([\d,]+)원)?/; + // 하나은행 레거시 + const hanaPattern = / \[하나은행\]\s*(\d{1,2}\/\d{1,2})\s*(\d{1,2}:\d{2})?\s*입금\s*([\d,]+)원\s*(\S+?)(?:\s+잔액\s*([\d,]+)원)?/; const hanaMatch = text.match(hanaPattern); if (hanaMatch) { const [, date, time, amountStr, depositor, balanceStr] = hanaMatch; @@ -67,8 +84,8 @@ export function parseBankSMS(content: string): BankNotification | null { }; } - // KB국민은행 패턴: [KB] 입금 50,000원 01/16 14:30 홍길동 - const kbPattern = /\[KB\]\s*입금\s*([\d,]+)원\s*(\d{1,2}\/\d{1,2})?\s*(\d{1,2}:\d{2})?\s*(\S+)/; + // KB국민은행 + const kbPattern = / \[KB\]\s*입금\s*([\d,]+)원\s*(\d{1,2}\/\d{1,2})?\s*(\d{1,2}:\d{2})?\s*(\S+)/; const kbMatch = text.match(kbPattern); if (kbMatch) { const [, amountStr, date, time, depositor] = kbMatch; @@ -81,8 +98,8 @@ export function parseBankSMS(content: string): BankNotification | null { }; } - // 신한은행 패턴: [신한] 01/16 입금 50,000원 홍길동 - const shinhanPattern = /\[신한\]\s*(\d{1,2}\/\d{1,2})?\s*입금\s*([\d,]+)원\s*(\S+)/; + // 신한은행 + const shinhanPattern = / \[신한\]\s*(\d{1,2}\/\d{1,2})?\s*입금\s*([\d,]+)원\s*(\S+)/; const shinhanMatch = text.match(shinhanPattern); if (shinhanMatch) { const [, date, amountStr, depositor] = shinhanMatch; @@ -95,8 +112,8 @@ export function parseBankSMS(content: string): BankNotification | null { }; } - // 일반 입금 패턴: 입금 50,000원 홍길동 또는 홍길동 50,000원 입금 - const genericPattern1 = /입금\s*([\d,]+)원?\s*(\S{2,10})/; + // 일반 패턴 + const genericPattern1 = /입금\s*([\d,]+)원?\s*(\S{2,10})/; const genericPattern2 = /(\S{2,10})\s*([\d,]+)원?\s*입금/; const genericMatch1 = text.match(genericPattern1); @@ -122,13 +139,89 @@ export function parseBankSMS(content: string): BankNotification | null { return null; } -/** - * 날짜/시간 파싱 헬퍼 함수 - * - * @param dateStr - "MM/DD" 형식 - * @param timeStr - "HH:MM" 형식 (선택) - * @returns Date 객체 - */ +async function parseWithAI(text: string, env: Env): Promise { + const prompt = ` +아래 텍스트는 은행 입금 알림 문자 메시지입니다. +이 텍스트에서 [은행명, 입금자명, 입금액, 거래시간] 정보를 추출하여 JSON 형식으로 반환하세요. + +텍스트: +""" +${text.slice(0, 500)} +""" + +요구사항: +1. JSON 형식만 반환하세요. (마크다운 코드블록 없이) +2. 필드명: bankName, depositorName, amount (숫자), transactionTime (YYYY-MM-DDTHH:mm:ss 형식, 연도는 ${new Date().getFullYear()}년 기준) +3. 추출할 수 없는 필드는 null로 설정하세요. +4. "입금"이 아닌 "출금"이거나 광고/스팸 메시지라면 null을 반환하세요. +`; + + let jsonStr = ''; + + // 1. OpenAI 시도 + if (env.OPENAI_API_KEY) { + try { + const response = await fetch('https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: prompt }], + max_tokens: 200, + temperature: 0, + }), + }); + + if (response.ok) { + const data = await response.json() as any; + jsonStr = data.choices[0].message.content; + } + } catch (e) { + console.error('[BankSMS] OpenAI 파싱 실패:', e); + } + } + + // 2. Workers AI 시도 (OpenAI 실패 또는 미설정 시) + if (!jsonStr && env.AI) { + try { + const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct' as any, { + messages: [{ role: 'user', content: prompt }], + max_tokens: 200, + }) as any; + jsonStr = response.response; + } catch (e) { + console.error('[BankSMS] Workers AI 파싱 실패:', e); + } + } + + if (!jsonStr) return null; + + try { + // JSON 파싱 (마크다운 제거 등 정제) + const cleanJson = jsonStr.replace(/```json|```/g, '').trim(); + if (cleanJson === 'null') return null; + + const result = JSON.parse(cleanJson); + + if (result.amount && result.depositorName) { + return { + bankName: result.bankName || '알수없음', + depositorName: result.depositorName, + amount: typeof result.amount === 'string' ? parseInt(result.amount.replace(/,/g, '')) : result.amount, + transactionTime: result.transactionTime ? new Date(result.transactionTime) : undefined, + rawMessage: text.slice(0, 500), + }; + } + } catch (e) { + console.error('[BankSMS] AI 응답 JSON 파싱 실패:', e, jsonStr); + } + + return null; +} + function parseDateTime(dateStr: string, timeStr?: string): Date { const now = new Date(); const [month, day] = dateStr.split('/').map(Number); @@ -140,4 +233,4 @@ function parseDateTime(dateStr: string, timeStr?: string): Date { } return new Date(year, month - 1, day, hours, minutes); -} +} \ No newline at end of file diff --git a/src/services/conversation-service.ts b/src/services/conversation-service.ts new file mode 100644 index 0000000..d3ff98c --- /dev/null +++ b/src/services/conversation-service.ts @@ -0,0 +1,74 @@ +import { Env } from '../types'; +import { + addToBuffer, + processAndSummarize, + generateAIResponse, +} from '../summary-service'; +import { sendChatAction, sendMessage } from '../telegram'; + +export interface ConversationResult { + responseText: string; + isProfileUpdated: boolean; + keyboardData?: any; +} + +export class ConversationService { + constructor(private env: Env) {} + + /** + * 사용자 메시지를 처리하고 AI 응답을 생성합니다. + * 버퍼링, AI 생성, 요약 프로세스를 포함합니다. + */ + async processUserMessage( + userId: number, + chatId: string, + text: string, + telegramUserId: string + ): Promise { + // 1. 타이핑 액션 전송 (비동기로 실행, 기다리지 않음) + sendChatAction(this.env.BOT_TOKEN, chatId, 'typing').catch(console.error); + + // 2. 사용자 메시지 버퍼에 추가 + await addToBuffer(this.env.DB, userId, chatId, 'user', text); + + // 3. AI 응답 생성 + let responseText = await generateAIResponse( + this.env, + userId, + chatId, + text, + telegramUserId + ); + + // 4. 봇 응답 버퍼에 추가 (키보드 데이터 마커 등은 그대로 저장) + // 실제 사용자에게 보여질 텍스트만 저장하는 것이 좋으나, + // 현재 구조상 전체를 저장하고 나중에 컨텍스트로 활용 시 정제될 수 있음 + await addToBuffer(this.env.DB, userId, chatId, 'bot', responseText); + + // 5. 임계값 도달 시 프로필 업데이트 (요약) + const { summarized } = await processAndSummarize( + this.env, + userId, + chatId + ); + + // 키보드 데이터 파싱 + let keyboardData: any = null; + const keyboardMatch = responseText.match(/__KEYBOARD__(.+?)__END__\n?/); + + if (keyboardMatch) { + responseText = responseText.replace(/__KEYBOARD__.+?__END__\n?/, ''); + try { + keyboardData = JSON.parse(keyboardMatch[1]); + } catch (e) { + console.error('[ConversationService] Keyboard parsing error:', e); + } + } + + return { + responseText, + isProfileUpdated: summarized, + keyboardData + }; + } +} diff --git a/src/services/user-service.ts b/src/services/user-service.ts new file mode 100644 index 0000000..5069b03 --- /dev/null +++ b/src/services/user-service.ts @@ -0,0 +1,47 @@ +import { Env } from '../types'; + +export class UserService { + constructor(private db: D1Database) {} + + /** + * Telegram ID로 사용자를 조회하거나 없으면 새로 생성합니다. + * 마지막 활동 시간도 업데이트합니다. + */ + async getOrCreateUser( + telegramId: string, + firstName: string, + username?: string + ): Promise { + const existing = await this.db + .prepare('SELECT id FROM users WHERE telegram_id = ?') + .bind(telegramId) + .first<{ id: number }>(); + + if (existing) { + // 마지막 활동 시간 업데이트 + await this.db + .prepare('UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?') + .bind(existing.id) + .run(); + return existing.id; + } + + // 새 사용자 생성 + const result = await this.db + .prepare('INSERT INTO users (telegram_id, first_name, username) VALUES (?, ?, ?)') + .bind(telegramId, firstName, username || null) + .run(); + + return result.meta.last_row_id as number; + } + + /** + * Telegram ID로 사용자 정보를 조회합니다. + */ + async getUserByTelegramId(telegramId: string): Promise<{ id: number; first_name: string } | null> { + return await this.db + .prepare('SELECT id, first_name FROM users WHERE telegram_id = ?') + .bind(telegramId) + .first<{ id: number; first_name: string }>(); + } +} diff --git a/src/tools/domain-tool.ts b/src/tools/domain-tool.ts index aa05d90..4286c81 100644 --- a/src/tools/domain-tool.ts +++ b/src/tools/domain-tool.ts @@ -101,7 +101,7 @@ export const manageDomainTool = { action: { type: 'string', enum: ['register', 'check', 'whois', 'list', 'info', 'get_ns', 'set_ns', 'price', 'cheapest'], - description: 'price: TLD 가격 조회 (.com 가격, .io 가격), cheapest: 가장 저렴한 TLD 목록 조회, register: 도메인 등록, check: 가용성 확인, whois: WHOIS 조회, list: 내 도메인 목록, info: 도메인 상세정보, get_ns/set_ns: 네임서버 조회/변경', + description: 'price: TLD 가격 조회 (.com 가격, .io 가격), cheapest: 가장 저렴한 TLD 목록 조회, register: 도메인 등록, check: 가용성 확인, whois: WHOIS 조회, list: 내 도메인 목록, info: 도메인 상세정보(내 도메인이 아니면 WHOIS 조회), get_ns/set_ns: 네임서버 조회/변경', }, domain: { type: 'string', @@ -416,8 +416,21 @@ async function executeDomainAction( case 'info': { if (!domain) return '🚫 도메인을 지정해주세요.'; + + // 내 도메인이 아니면 WHOIS 조회로 자동 전환 + const lowerDomain = domain.toLowerCase(); + const isMyDomain = allowedDomains.some(d => d.toLowerCase() === lowerDomain); + + if (!isMyDomain) { + return executeDomainAction('whois', args, allowedDomains, env, telegramUserId, db, userId); + } + const result = await callNamecheapApi('get_domain_info', { domain }, allowedDomains, env, telegramUserId, db, userId); - if (result.error) return `🚫 ${result.error}`; + + if (result.error) { + // 계정 내 도메인 정보 조회 실패 시 WHOIS로 폴백 + return executeDomainAction('whois', args, allowedDomains, env, telegramUserId, db, userId); + } return `📋 ${domain} 정보\n\n• 생성일: ${result.created}\n• 만료일: ${result.expires}\n• 자동갱신: ${result.auto_renew ? '✅' : '❌'}\n• 잠금: ${result.is_locked ? '🔒' : '🔓'}\n• WHOIS Guard: ${result.whois_guard ? '✅' : '❌'}`; }