fix: Email Routing MIME 파싱 개선 + 레거시 코드 정리
- Email Routing에서 수신한 이메일 파싱 수정 - Quoted-Printable UTF-8 디코딩 함수 추가 - HTML <br/> 태그를 줄바꿈으로 변환 - SMS 키워드 위치 기반 본문 추출 - 레거시 코드 삭제 - /api/bank-notification 엔드포인트 제거 (Email Routing으로 대체) - BANK_API_SECRET 관련 코드 및 문서 제거 - DEPOSIT_AGENT_ID 제거 (Assistants API → 코드 직접 처리) - CLI 테스트 클라이언트 개선 - .env 파일 자동 로드 지원 - WEBHOOK_SECRET 환경변수 불필요 - 문서 업데이트 - NAMECHEAP_API_KEY 설명 명확화 (래퍼 인증 키) - CLI 테스트 섹션 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
51
CLAUDE.md
51
CLAUDE.md
@@ -1,5 +1,8 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
|
> 🔧 **개발자용 문서**: 기술 상세, 코드 패턴, 트러블슈팅
|
||||||
|
> 📖 **[README.md](./README.md)**: 기능 소개, 배포 가이드, 사용법
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## Auto-Read on Start
|
## Auto-Read on Start
|
||||||
@@ -69,8 +72,8 @@ npm run chat # CLI 테스트 클라이언트
|
|||||||
|
|
||||||
**CLI 테스트 클라이언트:**
|
**CLI 테스트 클라이언트:**
|
||||||
```bash
|
```bash
|
||||||
# 환경변수 설정
|
# .env 파일 생성 (최초 1회)
|
||||||
export WEBHOOK_SECRET="..." # Vault: secret/data/telegram-bot
|
echo 'WEBHOOK_SECRET=...' > .env # Vault: secret/data/telegram-bot
|
||||||
|
|
||||||
# 대화형 모드
|
# 대화형 모드
|
||||||
npm run chat
|
npm run chat
|
||||||
@@ -84,6 +87,7 @@ npm run chat "날씨 알려줘"
|
|||||||
wrangler secret put BOT_TOKEN # Telegram Bot Token
|
wrangler secret put BOT_TOKEN # Telegram Bot Token
|
||||||
wrangler secret put WEBHOOK_SECRET # Webhook 검증용
|
wrangler secret put WEBHOOK_SECRET # Webhook 검증용
|
||||||
wrangler secret put OPENAI_API_KEY # OpenAI API 키
|
wrangler secret put OPENAI_API_KEY # OpenAI API 키
|
||||||
|
wrangler secret put NAMECHEAP_API_KEY # namecheap-api 래퍼 인증 키
|
||||||
```
|
```
|
||||||
|
|
||||||
**Webhook 설정:**
|
**Webhook 설정:**
|
||||||
@@ -223,7 +227,7 @@ Telegram Webhook → Security Validation → Command/Message Router
|
|||||||
| 문서 | `lookup_docs` | Context7 | 문서, 사용법, API |
|
| 문서 | `lookup_docs` | Context7 | 문서, 사용법, API |
|
||||||
| 도메인 | `manage_domain` | 코드 직접 처리 → Namecheap | 도메인, 네임서버, WHOIS |
|
| 도메인 | `manage_domain` | 코드 직접 처리 → Namecheap | 도메인, 네임서버, WHOIS |
|
||||||
| 도메인 추천 | `suggest_domains` | GPT + Namecheap | **도메인 추천, 도메인 제안, 도메인 아이디어** |
|
| 도메인 추천 | `suggest_domains` | GPT + Namecheap | **도메인 추천, 도메인 제안, 도메인 아이디어** |
|
||||||
| 예치금 | `manage_deposit` | 코드 직접 처리 → D1 | **입금, 충전, 잔액, 계좌, 송금** |
|
| 예치금 | `manage_deposit` | 코드 직접 처리 | **입금, 충전, 잔액, 계좌, 송금** |
|
||||||
|
|
||||||
**Data Layer (D1 SQLite):**
|
**Data Layer (D1 SQLite):**
|
||||||
| 테이블 | 용도 | 주요 컬럼 |
|
| 테이블 | 용도 | 주요 컬럼 |
|
||||||
@@ -351,18 +355,6 @@ case 'new_tool':
|
|||||||
|
|
||||||
**로그:** `[search_web] 번역: "판골린 VPN" → "Pangolin VPN"`
|
**로그:** `[search_web] 번역: "판골린 VPN" → "Pangolin VPN"`
|
||||||
|
|
||||||
### Deposit Agent 프롬프트 수정 방법
|
|
||||||
```bash
|
|
||||||
# Vault에서 API 키 조회
|
|
||||||
curl -s -H "X-Vault-Token: hvs.xxx" https://vault.anvil.it.com/v1/secret/data/openai
|
|
||||||
|
|
||||||
# Assistant 프롬프트 업데이트
|
|
||||||
curl -X POST 'https://api.openai.com/v1/assistants/asst_XMoVGU7ZwRpUPI6PHGvRNm8E' \
|
|
||||||
-H 'Authorization: Bearer sk-xxx' \
|
|
||||||
-H 'OpenAI-Beta: assistants=v2' \
|
|
||||||
-d @update-agent.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -375,7 +367,6 @@ curl -X POST 'https://api.openai.com/v1/assistants/asst_XMoVGU7ZwRpUPI6PHGvRNm8E
|
|||||||
| `DOMAIN_OWNER_ID` | - | 도메인 관리 권한 Telegram ID |
|
| `DOMAIN_OWNER_ID` | - | 도메인 관리 권한 Telegram ID |
|
||||||
| `DEPOSIT_ADMIN_ID` | - | 예치금 관리 권한 Telegram ID |
|
| `DEPOSIT_ADMIN_ID` | - | 예치금 관리 권한 Telegram ID |
|
||||||
| `WEBHOOK_SECRET` | - | Telegram Webhook 인증 (wrangler secret, Vault: telegram-bot) |
|
| `WEBHOOK_SECRET` | - | Telegram Webhook 인증 (wrangler secret, Vault: telegram-bot) |
|
||||||
| `BANK_API_SECRET` | - | 입금 알림 API 인증 키 (wrangler secret) |
|
|
||||||
| `BRAVE_API_KEY` | - | Brave Search API 키 (wrangler secret) |
|
| `BRAVE_API_KEY` | - | Brave Search API 키 (wrangler secret) |
|
||||||
| `DEPOSIT_API_SECRET` | - | Deposit API 인증 키 (namecheap-api용, wrangler secret) |
|
| `DEPOSIT_API_SECRET` | - | Deposit API 인증 키 (namecheap-api용, wrangler secret) |
|
||||||
|
|
||||||
@@ -392,8 +383,7 @@ curl -X POST 'https://api.openai.com/v1/assistants/asst_XMoVGU7ZwRpUPI6PHGvRNm8E
|
|||||||
| wttr.in | 날씨 | wttr.in | - |
|
| wttr.in | 날씨 | wttr.in | - |
|
||||||
| Brave Search | 검색 | api.search.brave.com | Free AI 플랜 (2,000/월) |
|
| Brave Search | 검색 | api.search.brave.com | Free AI 플랜 (2,000/월) |
|
||||||
| Vault | API 키 관리 | vault.anvil.it.com | - |
|
| Vault | API 키 관리 | vault.anvil.it.com | - |
|
||||||
| Gmail | 입금 SMS 수신 | deposit.anvil@gmail.com | Apps Script 연동 |
|
| Email Routing | 입금 SMS 수신 | Cloudflare Email Routing | Worker email handler로 직접 처리 |
|
||||||
| Apps Script | Gmail → Worker 연동 | script.google.com | 1분마다 실행, message_id 중복 방지 |
|
|
||||||
|
|
||||||
### Cloudflare AI Gateway
|
### Cloudflare AI Gateway
|
||||||
|
|
||||||
@@ -423,10 +413,8 @@ URL: gateway.ai.cloudflare.com/v1/{account_id}/telegram-bot/openai/...
|
|||||||
↓
|
↓
|
||||||
매칭 O → confirmed + 잔액↑ | 매칭 X → pending
|
매칭 O → confirmed + 잔액↑ | 매칭 X → pending
|
||||||
|
|
||||||
[시나리오 2: SMS 먼저 - Gmail → Apps Script → Worker]
|
[시나리오 2: SMS 먼저 - Email Routing]
|
||||||
은행 SMS → Gmail(deposit.anvil@gmail.com) → Apps Script (1분마다)
|
은행 SMS → 메일 전달 → Cloudflare Email Routing → Worker (email handler)
|
||||||
↓
|
|
||||||
POST /api/bank-notification
|
|
||||||
↓
|
↓
|
||||||
파싱 → bank_notifications 저장
|
파싱 → bank_notifications 저장
|
||||||
↓
|
↓
|
||||||
@@ -442,22 +430,9 @@ URL: gateway.ai.cloudflare.com/v1/{account_id}/telegram-bot/openai/...
|
|||||||
| 자동 매칭 성공 | ✅ 입금액 + 현재 잔액 | ✅ 입금 정보 + 매칭 완료 |
|
| 자동 매칭 성공 | ✅ 입금액 + 현재 잔액 | ✅ 입금 정보 + 매칭 완료 |
|
||||||
| 매칭 대기 (SMS만) | - | ✅ 입금 정보 + 대기 상태 |
|
| 매칭 대기 (SMS만) | - | ✅ 입금 정보 + 대기 상태 |
|
||||||
|
|
||||||
**Gmail → Worker 연동:**
|
**Email Routing 설정:**
|
||||||
- Gmail 계정: `deposit.anvil@gmail.com`
|
- Cloudflare Dashboard → Email → Email Routing → Routes
|
||||||
- Apps Script: 1분마다 `is:unread 입금` 검색 → Worker API 호출
|
- 수신 주소 → Worker: `telegram-summary-bot` 라우팅
|
||||||
- 중복 방지: Gmail message_id 기반
|
|
||||||
|
|
||||||
**API 엔드포인트:**
|
|
||||||
```
|
|
||||||
POST /api/bank-notification
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"content": "[Web발신]\n하나,01/16, 23:30\n427******27104\n입금5원\n황병하",
|
|
||||||
"messageId": "19bc737b3415596a",
|
|
||||||
"secret": "BANK_API_SECRET 값"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**입금 계좌:** 하나은행 427-910018-27104 (주식회사 아이언클래드)
|
**입금 계좌:** 하나은행 427-910018-27104 (주식회사 아이언클래드)
|
||||||
- Vault 경로: `secret/companies/ironclad-corp`
|
- Vault 경로: `secret/companies/ironclad-corp`
|
||||||
|
|||||||
108
README.md
108
README.md
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
> Cloudflare Workers + D1 + OpenAI를 활용한 사용자 프로필 기반 텔레그램 봇
|
> Cloudflare Workers + D1 + OpenAI를 활용한 사용자 프로필 기반 텔레그램 봇
|
||||||
|
|
||||||
|
**📖 이 문서**: 기능 소개, 배포 가이드, 사용법 (사용자/운영자용)
|
||||||
|
**🔧 [CLAUDE.md](./CLAUDE.md)**: 기술 상세, 코드 패턴, 트러블슈팅 (개발자용)
|
||||||
|
|
||||||
## 목차
|
## 목차
|
||||||
|
|
||||||
1. [개요](#개요)
|
1. [개요](#개요)
|
||||||
@@ -26,8 +29,7 @@
|
|||||||
- **Context7 연동**: 프로그래밍 라이브러리 공식 문서 실시간 조회
|
- **Context7 연동**: 프로그래밍 라이브러리 공식 문서 실시간 조회
|
||||||
- **동적 도구 로딩**: 메시지 키워드 기반으로 필요한 도구만 선택하여 토큰 절약
|
- **동적 도구 로딩**: 메시지 키워드 기반으로 필요한 도구만 선택하여 토큰 절약
|
||||||
- **도메인 추천**: GPT가 창의적 도메인 생성 → 가용성 자동 확인 → 가격과 함께 제안
|
- **도메인 추천**: GPT가 창의적 도메인 생성 → 가용성 자동 확인 → 가격과 함께 제안
|
||||||
- **Deposit Agent**: OpenAI Assistants API 기반 예치금 관리 에이전트 연동
|
- **예치금 시스템**: 코드 직접 처리, 은행 입금 자동 감지 + 사용자 신고 매칭으로 자동 충전
|
||||||
- **예치금 시스템**: 은행 입금 자동 감지 + 사용자 신고 매칭으로 자동 충전
|
|
||||||
- **Email Worker**: SMS → 메일 → 자동 파싱으로 입금 알림 처리
|
- **Email Worker**: SMS → 메일 → 자동 파싱으로 입금 알림 처리
|
||||||
- **무한 컨텍스트**: 슬라이딩 윈도우(3개)로 프로필 유지, 무제한 대화 기억
|
- **무한 컨텍스트**: 슬라이딩 윈도우(3개)로 프로필 유지, 무제한 대화 기억
|
||||||
- **개인화 응답**: 프로필 기반으로 맞춤형 AI 응답 제공
|
- **개인화 응답**: 프로필 기반으로 맞춤형 AI 응답 제공
|
||||||
@@ -43,7 +45,7 @@
|
|||||||
| **Context7** | 라이브러리 문서 조회 API |
|
| **Context7** | 라이브러리 문서 조회 API |
|
||||||
| **도메인 관리** | 코드 직접 처리 → Namecheap API |
|
| **도메인 관리** | 코드 직접 처리 → Namecheap API |
|
||||||
| **도메인 추천** | GPT + Namecheap API (코드 레벨) |
|
| **도메인 추천** | GPT + Namecheap API (코드 레벨) |
|
||||||
| **Deposit Agent** | 예치금 관리 (OpenAI Assistants) |
|
| **예치금 관리** | 코드 직접 처리 → D1 |
|
||||||
| **Namecheap API** | 도메인 조회/가용성/가격 백엔드 |
|
| **Namecheap API** | 도메인 조회/가용성/가격 백엔드 |
|
||||||
| **Email Workers** | SMS → 메일 파싱 (입금 알림) |
|
| **Email Workers** | SMS → 메일 파싱 (입금 알림) |
|
||||||
| **Workers AI** | 폴백용 (Llama 3.1 8B) |
|
| **Workers AI** | 폴백용 (Llama 3.1 8B) |
|
||||||
@@ -148,7 +150,7 @@ OpenAI Function Calling을 통해 AI가 자동으로 필요한 도구를 호출
|
|||||||
| **문서** | "React hooks 사용법", "OpenAI API 예제" | Context7 |
|
| **문서** | "React hooks 사용법", "OpenAI API 예제" | Context7 |
|
||||||
| **도메인** | "도메인 목록", "anvil.it.com 네임서버", ".com 가격", "google.com whois" | 코드 직접 처리 + WHOIS API |
|
| **도메인** | "도메인 목록", "anvil.it.com 네임서버", ".com 가격", "google.com whois" | 코드 직접 처리 + WHOIS API |
|
||||||
| **도메인 추천** | "커피숍 도메인 추천해줘", "스타트업 도메인 아이디어" | GPT + Namecheap |
|
| **도메인 추천** | "커피숍 도메인 추천해줘", "스타트업 도메인 아이디어" | GPT + Namecheap |
|
||||||
| **예치금** | "잔액 확인", "충전하고 싶어", "10000원 입금했어" | D1 + Email Worker |
|
| **예치금** | "잔액 확인", "충전하고 싶어", "10000원 입금했어" | 코드 직접 처리 |
|
||||||
|
|
||||||
### 동작 방식
|
### 동작 방식
|
||||||
|
|
||||||
@@ -168,6 +170,31 @@ OpenAI: 날씨 데이터를 자연어로 응답 생성
|
|||||||
응답: "🌤 서울 날씨\n온도: 5°C\n습도: 45%..."
|
응답: "🌤 서울 날씨\n온도: 5°C\n습도: 45%..."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 동적 도구 로딩
|
||||||
|
|
||||||
|
메시지 키워드를 분석하여 필요한 도구만 선택적으로 로딩합니다. (토큰 40% 절약)
|
||||||
|
|
||||||
|
| 카테고리 | 도구 | 감지 패턴 |
|
||||||
|
|----------|------|-----------|
|
||||||
|
| domain | manage_domain, suggest_domains | 도메인, 네임서버, whois, .com |
|
||||||
|
| deposit | manage_deposit | 입금, 충전, 잔액, 계좌 |
|
||||||
|
| weather | get_weather | 날씨, 기온, 비, 눈 |
|
||||||
|
| search | search_web, lookup_docs | 검색, 찾아, 뭐야, 가격 |
|
||||||
|
| utility | get_current_time, calculate | (항상 포함) |
|
||||||
|
|
||||||
|
패턴 매칭 없으면 전체 도구 사용 (폴백)
|
||||||
|
|
||||||
|
### AI Gateway
|
||||||
|
|
||||||
|
OpenAI API 호출을 Cloudflare AI Gateway를 통해 프록시하여 지역 제한을 우회합니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
Gateway ID: telegram-bot
|
||||||
|
URL: gateway.ai.cloudflare.com/v1/{account_id}/telegram-bot/openai/...
|
||||||
|
```
|
||||||
|
|
||||||
|
**대시보드**: Cloudflare Dashboard → AI → AI Gateway → telegram-bot
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 예치금 시스템
|
## 예치금 시스템
|
||||||
@@ -202,7 +229,7 @@ OpenAI: 날씨 데이터를 자연어로 응답 생성
|
|||||||
```
|
```
|
||||||
[시나리오 2: 은행 SMS가 먼저 도착]
|
[시나리오 2: 은행 SMS가 먼저 도착]
|
||||||
|
|
||||||
은행 SMS → Gmail → Apps Script
|
은행 SMS → 메일 전달 → Cloudflare Email Routing → Worker (email handler)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌──────────────────┐
|
┌──────────────────┐
|
||||||
@@ -280,57 +307,23 @@ OpenAI: 날씨 데이터를 자연어로 응답 생성
|
|||||||
감사합니다! 🎉
|
감사합니다! 🎉
|
||||||
```
|
```
|
||||||
|
|
||||||
### Gmail → Apps Script → Worker 연동
|
### Cloudflare Email Routing
|
||||||
|
|
||||||
SMS를 Gmail로 전달받아 Apps Script에서 Worker API를 호출합니다.
|
SMS를 메일로 전달받아 Worker에서 직접 처리합니다.
|
||||||
|
|
||||||
**흐름:**
|
**흐름:**
|
||||||
```
|
```
|
||||||
은행 SMS → Gmail(deposit.anvil@gmail.com) → Apps Script (1분마다)
|
은행 SMS → 메일 전달 → Cloudflare Email Routing → Worker (email handler)
|
||||||
↓
|
↓
|
||||||
POST /api/bank-notification → DB 저장 → 자동 매칭
|
SMS 파싱 → DB 저장 → 자동 매칭
|
||||||
↓
|
↓
|
||||||
매칭 성공 → 사용자/관리자 Telegram 알림
|
매칭 성공 → 사용자/관리자 Telegram 알림
|
||||||
```
|
```
|
||||||
|
|
||||||
**Apps Script 코드:**
|
**설정 방법:**
|
||||||
```javascript
|
1. Cloudflare Dashboard → Email → Email Routing → Routes
|
||||||
function checkBankEmails() {
|
2. 수신 주소 설정 (예: `deposit@your-domain.com`)
|
||||||
var threads = GmailApp.search('is:unread 입금', 0, 10);
|
3. Worker로 라우팅: `telegram-summary-bot`
|
||||||
|
|
||||||
for (var i = 0; i < threads.length; i++) {
|
|
||||||
var messages = threads[i].getMessages();
|
|
||||||
|
|
||||||
for (var j = 0; j < messages.length; j++) {
|
|
||||||
var message = messages[j];
|
|
||||||
if (!message.isUnread()) continue;
|
|
||||||
|
|
||||||
var messageId = message.getId();
|
|
||||||
var body = message.getPlainBody();
|
|
||||||
|
|
||||||
try {
|
|
||||||
UrlFetchApp.fetch(
|
|
||||||
'https://telegram-summary-bot.kappa-d8e.workers.dev/api/bank-notification',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
payload: JSON.stringify({
|
|
||||||
content: body,
|
|
||||||
messageId: messageId,
|
|
||||||
secret: 'BANK_API_SECRET 값'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Error: ' + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
threads[i].markRead();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**트리거 설정:** 시간 기반 → 분 타이머 → 1분마다
|
|
||||||
|
|
||||||
**지원 은행 SMS 패턴:**
|
**지원 은행 SMS 패턴:**
|
||||||
- 하나은행 (Web발신): `[Web발신] 하나,01/16, 23:30 427******27104 입금5원 황병하`
|
- 하나은행 (Web발신): `[Web발신] 하나,01/16, 23:30 427******27104 입금5원 황병하`
|
||||||
@@ -540,14 +533,14 @@ wrangler secret put WEBHOOK_SECRET
|
|||||||
# OpenAI API Key (필수)
|
# OpenAI API Key (필수)
|
||||||
wrangler secret put OPENAI_API_KEY
|
wrangler secret put OPENAI_API_KEY
|
||||||
|
|
||||||
# 입금 알림 API Secret (Apps Script 연동용)
|
|
||||||
wrangler secret put BANK_API_SECRET
|
|
||||||
|
|
||||||
# Brave Search API Key
|
# Brave Search API Key
|
||||||
wrangler secret put BRAVE_API_KEY
|
wrangler secret put BRAVE_API_KEY
|
||||||
|
|
||||||
# Deposit API Secret (namecheap-api 연동용)
|
# Deposit API Secret (namecheap-api 연동용)
|
||||||
wrangler secret put DEPOSIT_API_SECRET
|
wrangler secret put DEPOSIT_API_SECRET
|
||||||
|
|
||||||
|
# namecheap-api 래퍼 인증 키 (도메인 추천용)
|
||||||
|
wrangler secret put NAMECHEAP_API_KEY
|
||||||
```
|
```
|
||||||
|
|
||||||
### Vault 연동 (선택)
|
### Vault 연동 (선택)
|
||||||
@@ -582,6 +575,19 @@ curl https://telegram-summary-bot.kappa-d8e.workers.dev/setup-webhook
|
|||||||
curl https://telegram-summary-bot.kappa-d8e.workers.dev/webhook-info
|
curl https://telegram-summary-bot.kappa-d8e.workers.dev/webhook-info
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 6. CLI 테스트 (선택)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env 파일 생성 (최초 1회)
|
||||||
|
echo 'WEBHOOK_SECRET=...' > .env # Vault: secret/telegram-bot
|
||||||
|
|
||||||
|
# 대화형 모드
|
||||||
|
npm run chat
|
||||||
|
|
||||||
|
# 단일 메시지 모드
|
||||||
|
npm run chat "안녕"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 보안 설정
|
## 보안 설정
|
||||||
@@ -680,7 +686,7 @@ database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e"
|
|||||||
| `/webhook-info` | GET | Webhook 상태 |
|
| `/webhook-info` | GET | Webhook 상태 |
|
||||||
| `/setup-webhook` | GET | Webhook 설정 |
|
| `/setup-webhook` | GET | Webhook 설정 |
|
||||||
| `/webhook` | POST | Telegram Webhook |
|
| `/webhook` | POST | Telegram Webhook |
|
||||||
| `/api/bank-notification` | POST | 입금 알림 API (Apps Script 연동) |
|
| `/api/bank-notification` | POST | 입금 알림 API (레거시, Email Routing으로 대체) |
|
||||||
| `/api/deposit/balance` | GET | 예치금 잔액 조회 (namecheap-api용) |
|
| `/api/deposit/balance` | GET | 예치금 잔액 조회 (namecheap-api용) |
|
||||||
| `/api/deposit/deduct` | POST | 예치금 차감 (namecheap-api용) |
|
| `/api/deposit/deduct` | POST | 예치금 차감 (namecheap-api용) |
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,25 @@
|
|||||||
/**
|
/**
|
||||||
* Telegram Bot CLI Chat Client
|
* Telegram Bot CLI Chat Client
|
||||||
* - Worker의 /api/test 엔드포인트를 통해 직접 대화
|
* - Worker의 /api/test 엔드포인트를 통해 직접 대화
|
||||||
* - 사용법: npx tsx scripts/chat.ts
|
* - 사용법: npm run chat
|
||||||
* 또는: npx tsx scripts/chat.ts "메시지"
|
* 또는: npm run chat "메시지"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// .env 파일 로드
|
||||||
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
const envContent = fs.readFileSync(envPath, 'utf-8');
|
||||||
|
for (const line of envContent.split('\n')) {
|
||||||
|
const [key, ...valueParts] = line.split('=');
|
||||||
|
if (key && valueParts.length > 0) {
|
||||||
|
process.env[key.trim()] = valueParts.join('=').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const WORKER_URL = process.env.WORKER_URL || 'https://telegram-summary-bot.kappa-d8e.workers.dev';
|
const WORKER_URL = process.env.WORKER_URL || 'https://telegram-summary-bot.kappa-d8e.workers.dev';
|
||||||
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
|
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Deposit Agent - 예치금 관리 에이전트 (OpenAI Assistants API)
|
* Deposit Agent - 예치금 관리 (코드 직접 처리)
|
||||||
|
*
|
||||||
|
* 변경 이력:
|
||||||
|
* - 2026-01: Assistants API → 코드 직접 처리로 변경 (지역 제한 우회, 응답 일관성)
|
||||||
*
|
*
|
||||||
* 기능:
|
* 기능:
|
||||||
* - 잔액 조회
|
* - 잔액 조회
|
||||||
|
|||||||
157
src/index.ts
157
src/index.ts
@@ -169,112 +169,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bank Notification API (Gmail → Apps Script → Worker)
|
|
||||||
if (url.pathname === '/api/bank-notification' && request.method === 'POST') {
|
|
||||||
try {
|
|
||||||
const body = await request.json() as { content: string; secret?: string; messageId?: string };
|
|
||||||
|
|
||||||
// 간단한 인증 (BANK_API_SECRET 또는 WEBHOOK_SECRET 사용)
|
|
||||||
const apiSecret = (env as any).BANK_API_SECRET || env.WEBHOOK_SECRET;
|
|
||||||
if (apiSecret && body.secret !== apiSecret) {
|
|
||||||
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[API] Bank notification received:', body.content?.slice(0, 100));
|
|
||||||
|
|
||||||
// 메일 ID로 중복 체크
|
|
||||||
if (body.messageId) {
|
|
||||||
const existing = await env.DB.prepare(
|
|
||||||
'SELECT id FROM bank_notifications WHERE message_id = ?'
|
|
||||||
).bind(body.messageId).first();
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
console.log('[API] 중복 메일 무시:', body.messageId);
|
|
||||||
return Response.json({ success: true, duplicate: true, messageId: body.messageId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SMS 파싱
|
|
||||||
const notification = parseBankSMS(body.content || '');
|
|
||||||
if (!notification) {
|
|
||||||
console.log('[API] 파싱 실패:', body.content);
|
|
||||||
return Response.json({ error: 'Parse failed', content: body.content }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[API] 파싱 결과:', notification);
|
|
||||||
|
|
||||||
// DB에 저장
|
|
||||||
const insertResult = await env.DB.prepare(
|
|
||||||
`INSERT INTO bank_notifications (bank_name, depositor_name, amount, balance_after, transaction_time, raw_message, message_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
||||||
).bind(
|
|
||||||
notification.bankName,
|
|
||||||
notification.depositorName,
|
|
||||||
notification.amount,
|
|
||||||
notification.balanceAfter || null,
|
|
||||||
notification.transactionTime?.toISOString() || null,
|
|
||||||
notification.rawMessage,
|
|
||||||
body.messageId || null
|
|
||||||
).run();
|
|
||||||
|
|
||||||
const notificationId = insertResult.meta.last_row_id;
|
|
||||||
console.log('[API] 알림 저장 완료, ID:', notificationId);
|
|
||||||
|
|
||||||
// 자동 매칭 시도
|
|
||||||
const matched = await tryAutoMatch(env.DB, notificationId as number, notification);
|
|
||||||
|
|
||||||
// 매칭 성공 시 사용자에게 알림
|
|
||||||
if (matched && env.BOT_TOKEN) {
|
|
||||||
const user = await env.DB.prepare(
|
|
||||||
'SELECT telegram_id FROM users WHERE id = ?'
|
|
||||||
).bind(matched.userId).first<{ telegram_id: string }>();
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
// 업데이트된 잔액 조회
|
|
||||||
const deposit = await env.DB.prepare(
|
|
||||||
'SELECT balance FROM user_deposits WHERE user_id = ?'
|
|
||||||
).bind(matched.userId).first<{ balance: number }>();
|
|
||||||
|
|
||||||
await sendMessage(
|
|
||||||
env.BOT_TOKEN,
|
|
||||||
parseInt(user.telegram_id),
|
|
||||||
`✅ <b>입금 확인 완료!</b>\n\n` +
|
|
||||||
`입금액: ${matched.amount.toLocaleString()}원\n` +
|
|
||||||
`현재 잔액: ${(deposit?.balance || 0).toLocaleString()}원\n\n` +
|
|
||||||
`감사합니다! 🎉`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 관리자에게 알림
|
|
||||||
if (env.BOT_TOKEN && env.DEPOSIT_ADMIN_ID) {
|
|
||||||
const statusMsg = matched
|
|
||||||
? `✅ 자동 매칭 완료! (거래 #${matched.transactionId})`
|
|
||||||
: '⏳ 매칭 대기 중 (사용자 입금 신고 필요)';
|
|
||||||
|
|
||||||
await sendMessage(
|
|
||||||
env.BOT_TOKEN,
|
|
||||||
parseInt(env.DEPOSIT_ADMIN_ID),
|
|
||||||
`🏦 <b>입금 알림</b>\n\n` +
|
|
||||||
`은행: ${notification.bankName}\n` +
|
|
||||||
`입금자: ${notification.depositorName}\n` +
|
|
||||||
`금액: ${notification.amount.toLocaleString()}원\n` +
|
|
||||||
`${notification.balanceAfter ? `잔액: ${notification.balanceAfter.toLocaleString()}원\n` : ''}` +
|
|
||||||
`\n${statusMsg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({
|
|
||||||
success: true,
|
|
||||||
notification,
|
|
||||||
matched: !!matched
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[API] Bank notification error:', error);
|
|
||||||
return Response.json({ error: String(error) }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deposit API - 잔액 조회 (namecheap-api 전용)
|
// Deposit API - 잔액 조회 (namecheap-api 전용)
|
||||||
if (url.pathname === '/api/deposit/balance' && request.method === 'GET') {
|
if (url.pathname === '/api/deposit/balance' && request.method === 'GET') {
|
||||||
try {
|
try {
|
||||||
@@ -497,7 +391,7 @@ Documentation: https://github.com/your-repo
|
|||||||
// SMS 내용 파싱
|
// SMS 내용 파싱
|
||||||
const notification = parseBankSMS(rawEmail);
|
const notification = parseBankSMS(rawEmail);
|
||||||
if (!notification) {
|
if (!notification) {
|
||||||
console.log('[Email] 은행 SMS 파싱 실패:', rawEmail.slice(0, 200));
|
console.log('[Email] 은행 SMS 파싱 실패');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,10 +462,55 @@ Documentation: https://github.com/your-repo
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Quoted-Printable UTF-8 디코딩
|
||||||
|
function decodeQuotedPrintableUTF8(str: string): string {
|
||||||
|
// 줄 연속 문자 제거
|
||||||
|
str = str.replace(/=\r?\n/g, '');
|
||||||
|
|
||||||
|
// =XX 패턴을 바이트로 변환
|
||||||
|
const bytes: number[] = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < str.length) {
|
||||||
|
if (str[i] === '=' && i + 2 < str.length) {
|
||||||
|
const hex = str.slice(i + 1, i + 3);
|
||||||
|
if (/^[0-9A-Fa-f]{2}$/.test(hex)) {
|
||||||
|
bytes.push(parseInt(hex, 16));
|
||||||
|
i += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes.push(str.charCodeAt(i));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF-8 바이트를 문자열로 변환
|
||||||
|
try {
|
||||||
|
return new TextDecoder('utf-8').decode(new Uint8Array(bytes));
|
||||||
|
} catch {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 은행 SMS 파싱 함수
|
// 은행 SMS 파싱 함수
|
||||||
function parseBankSMS(content: string): BankNotification | null {
|
function parseBankSMS(content: string): BankNotification | null {
|
||||||
// 이메일에서 SMS 본문 추출 (여러 줄에 걸쳐 있을 수 있음)
|
// MIME 이메일 전처리
|
||||||
const text = content.replace(/\r\n/g, '\n').replace(/=\n/g, '');
|
let text = content;
|
||||||
|
|
||||||
|
// Quoted-Printable UTF-8 디코딩
|
||||||
|
text = decodeQuotedPrintableUTF8(text);
|
||||||
|
|
||||||
|
// HTML <br/> 태그를 줄바꿈으로 변환
|
||||||
|
text = text.replace(/<br\s*\/?>/gi, '\n');
|
||||||
|
|
||||||
|
// 줄바꿈 정규화
|
||||||
|
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
|
||||||
|
// [Web발신] 또는 은행 키워드가 있는 부분만 추출
|
||||||
|
const smsStartMatch = text.match(/\[Web발신\]|\[하나은행\]|\[KB\]|\[신한\]|\[우리\]|\[농협\]/);
|
||||||
|
if (smsStartMatch && smsStartMatch.index !== undefined) {
|
||||||
|
// SMS 시작점부터 500자 추출
|
||||||
|
text = text.slice(smsStartMatch.index, smsStartMatch.index + 500);
|
||||||
|
}
|
||||||
|
|
||||||
// 하나은행 Web발신 패턴 (여러 줄):
|
// 하나은행 Web발신 패턴 (여러 줄):
|
||||||
// [Web발신]
|
// [Web발신]
|
||||||
|
|||||||
@@ -253,8 +253,7 @@ function selectToolsForMessage(message: string): typeof tools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 도메인 추천 함수
|
// 도메인 추천 함수
|
||||||
async function suggestDomains(keywords: string, apiKey: string): Promise<string> {
|
async function suggestDomains(keywords: string, apiKey: string, namecheapApiKey: string): Promise<string> {
|
||||||
const namecheapApiKey = '05426957210b42e752950f565ea82a3f48df9cccfdce9d82cd9817011968076e';
|
|
||||||
const namecheapApiUrl = 'https://namecheap-api.anvil.it.com';
|
const namecheapApiUrl = 'https://namecheap-api.anvil.it.com';
|
||||||
const TARGET_COUNT = 10;
|
const TARGET_COUNT = 10;
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
@@ -1123,11 +1122,15 @@ async function executeTool(name: string, args: Record<string, string>, env?: Env
|
|||||||
console.log('[suggest_domains] 시작:', { keywords });
|
console.log('[suggest_domains] 시작:', { keywords });
|
||||||
|
|
||||||
if (!env?.OPENAI_API_KEY) {
|
if (!env?.OPENAI_API_KEY) {
|
||||||
return '🚫 도메인 추천 기능이 설정되지 않았습니다.';
|
return '🚫 도메인 추천 기능이 설정되지 않았습니다. (OPENAI_API_KEY 미설정)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!env?.NAMECHEAP_API_KEY) {
|
||||||
|
return '🚫 도메인 추천 기능이 설정되지 않았습니다. (NAMECHEAP_API_KEY 미설정)';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await suggestDomains(keywords, env.OPENAI_API_KEY);
|
const result = await suggestDomains(keywords, env.OPENAI_API_KEY, env.NAMECHEAP_API_KEY);
|
||||||
console.log('[suggest_domains] 완료:', result?.slice(0, 100));
|
console.log('[suggest_domains] 완료:', result?.slice(0, 100));
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export interface Env {
|
|||||||
OPENAI_API_KEY?: string;
|
OPENAI_API_KEY?: string;
|
||||||
NAMECHEAP_API_KEY?: string;
|
NAMECHEAP_API_KEY?: string;
|
||||||
DOMAIN_OWNER_ID?: string;
|
DOMAIN_OWNER_ID?: string;
|
||||||
DEPOSIT_AGENT_ID?: string;
|
|
||||||
DEPOSIT_ADMIN_ID?: string;
|
DEPOSIT_ADMIN_ID?: string;
|
||||||
BRAVE_API_KEY?: string;
|
BRAVE_API_KEY?: string;
|
||||||
DEPOSIT_API_SECRET?: string;
|
DEPOSIT_API_SECRET?: string;
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ compatibility_date = "2024-01-01"
|
|||||||
binding = "AI"
|
binding = "AI"
|
||||||
|
|
||||||
[vars]
|
[vars]
|
||||||
SUMMARY_THRESHOLD = "20"
|
SUMMARY_THRESHOLD = "20" # 프로필 업데이트 주기 (메시지 수)
|
||||||
MAX_SUMMARIES_PER_USER = "3"
|
MAX_SUMMARIES_PER_USER = "3" # 유지할 프로필 버전 수 (슬라이딩 윈도우)
|
||||||
N8N_WEBHOOK_URL = "https://n8n.anvil.it.com"
|
N8N_WEBHOOK_URL = "https://n8n.anvil.it.com" # n8n 연동 (선택)
|
||||||
DOMAIN_OWNER_ID = "821596605"
|
DOMAIN_OWNER_ID = "821596605" # 도메인 관리 권한 Telegram ID
|
||||||
DEPOSIT_AGENT_ID = "asst_XMoVGU7ZwRpUPI6PHGvRNm8E"
|
DEPOSIT_ADMIN_ID = "821596605" # 예치금 관리 권한 Telegram ID
|
||||||
DEPOSIT_ADMIN_ID = "821596605"
|
|
||||||
|
|
||||||
[[d1_databases]]
|
[[d1_databases]]
|
||||||
binding = "DB"
|
binding = "DB"
|
||||||
@@ -27,3 +26,6 @@ database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e"
|
|||||||
# - BOT_TOKEN: Telegram Bot Token
|
# - BOT_TOKEN: Telegram Bot Token
|
||||||
# - WEBHOOK_SECRET: Webhook 검증용 시크릿
|
# - WEBHOOK_SECRET: Webhook 검증용 시크릿
|
||||||
# - OPENAI_API_KEY: OpenAI API 키
|
# - OPENAI_API_KEY: OpenAI API 키
|
||||||
|
# - NAMECHEAP_API_KEY: namecheap-api 래퍼 인증 키 (도메인 추천용)
|
||||||
|
# - BRAVE_API_KEY: Brave Search API 키
|
||||||
|
# - DEPOSIT_API_SECRET: Deposit API 인증 키 (namecheap-api 연동)
|
||||||
|
|||||||
Reference in New Issue
Block a user