diff --git a/CLAUDE.md b/CLAUDE.md index 6b1223c..35f6a25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -208,7 +208,7 @@ Telegram Webhook → Security Validation → Command/Message Router | `index.ts` | Worker 진입점, Email Handler | `fetch()`, `email()` | | `openai-service.ts` | AI 응답 + Function Calling | `generateResponse()`, `executeFunctionCall()` | | `summary-service.ts` | 프로필 시스템 | `updateSummary()`, `getConversationContext()` | -| `deposit-agent.ts` | 예치금 에이전트 (Assistants API) | `callDepositAgent()`, `executeDepositFunction()` | +| `deposit-agent.ts` | 예치금 함수 (코드 직접 처리) | `executeDepositFunction()` | | `security.ts` | Webhook 보안 | `validateWebhook()`, `checkRateLimit()` | | `commands.ts` | 봇 명령어 | `handleCommand()` | | `telegram.ts` | Telegram API | `sendMessage()`, `sendTypingAction()` | @@ -223,7 +223,7 @@ Telegram Webhook → Security Validation → Command/Message Router | 문서 | `lookup_docs` | Context7 | 문서, 사용법, API | | 도메인 | `manage_domain` | 코드 직접 처리 → Namecheap | 도메인, 네임서버, WHOIS | | 도메인 추천 | `suggest_domains` | GPT + Namecheap | **도메인 추천, 도메인 제안, 도메인 아이디어** | -| 예치금 | `manage_deposit` | Deposit Agent → D1 | **입금, 충전, 잔액, 계좌, 송금** | +| 예치금 | `manage_deposit` | 코드 직접 처리 → D1 | **입금, 충전, 잔액, 계좌, 송금** | **Data Layer (D1 SQLite):** | 테이블 | 용도 | 주요 컬럼 | @@ -373,7 +373,6 @@ curl -X POST 'https://api.openai.com/v1/assistants/asst_XMoVGU7ZwRpUPI6PHGvRNm8E | `SUMMARY_THRESHOLD` | 20 | 프로필 업데이트 주기 (메시지 수) | | `MAX_SUMMARIES_PER_USER` | 3 | 유지할 프로필 버전 수 | | `DOMAIN_OWNER_ID` | - | 도메인 관리 권한 Telegram ID | -| `DEPOSIT_AGENT_ID` | - | 예치금 관리 Assistant ID | | `DEPOSIT_ADMIN_ID` | - | 예치금 관리 권한 Telegram ID | | `WEBHOOK_SECRET` | - | Telegram Webhook 인증 (wrangler secret, Vault: telegram-bot) | | `BANK_API_SECRET` | - | 입금 알림 API 인증 키 (wrangler secret) | @@ -388,7 +387,6 @@ curl -X POST 'https://api.openai.com/v1/assistants/asst_XMoVGU7ZwRpUPI6PHGvRNm8E |--------|------|-----------|----------| | **AI Gateway** | OpenAI 프록시 | gateway.ai.cloudflare.com | 지역 제한 우회, 로그/캐시 | | Context7 | 문서 조회 | context7.com API | - | -| Deposit Agent | 예치금 관리 | OpenAI Assistants (직접) | `asst_XMoVGU7ZwRpUPI6PHGvRNm8E` | | Namecheap API | 도메인 백엔드 | namecheap-api.anvil.it.com | 날짜: MM/DD/YYYY → ISO 변환 | | WHOIS API | WHOIS 조회 | whois-api-eight.vercel.app | ccSLD 미지원 | | wttr.in | 날씨 | wttr.in | - | @@ -408,8 +406,9 @@ URL: gateway.ai.cloudflare.com/v1/{account_id}/telegram-bot/openai/... ``` **적용 범위:** -- ✅ Chat Completions (openai-service.ts) - AI Gateway 경유 -- ❌ Assistants API (deposit-agent.ts) - 직접 호출 (Gateway 미지원) +- ✅ Chat Completions - AI Gateway 경유 +- ✅ 예치금 관리 - 코드 직접 처리 (Assistants API 제거) +- ✅ 도메인 관리 - 코드 직접 처리 **대시보드:** https://dash.cloudflare.com → AI → AI Gateway → telegram-bot @@ -463,26 +462,35 @@ Content-Type: application/json **입금 계좌:** 하나은행 427-910018-27104 (주식회사 아이언클래드) - Vault 경로: `secret/companies/ironclad-corp` -**Deposit Agent 핵심 규칙:** -``` -1. 금액 제한 없음: 1원도 입금 가능 -2. 입금 신고 시 반드시 입금자명 + 금액 확인 (빠지면 물어보기) -3. "계좌번호 주세요" → get_account_info 호출 -4. 자연어 금액 인식: "만원"→10000, "5천원"→5000, "삼만오천원"→35000 -5. 즉시 실행: 입금자명+금액 있으면 확인 없이 바로 request_deposit 호출 -6. 간편 취소: "취소해줘" → 최근 pending 자동 선택 -7. 동시 요청 허용: 기존 pending 있어도 새 입금 신고 가능 +**아키텍처 변경 (2026-01):** Assistants API → 코드 직접 처리 + +| 구분 | 이전 (Agent) | 현재 (코드) | +|------|-------------|-------------| +| 의도 파악 | Deposit Agent | 메인 AI (action 파라미터) | +| API 호출 | Agent Function Calling | `executeDepositFunction()` | +| 응답 형식 | Agent 생성 (불안정) | 코드 고정 (100% 일관성) | +| 지역 제한 | ❌ Assistants API 403 | ✅ AI Gateway 경유 | + +**manage_deposit 도구 파라미터:** +```typescript +{ + action: 'balance' | 'account' | 'request' | 'history' | 'cancel' | 'pending' | 'confirm' | 'reject', + depositor_name?: string, // request용 + amount?: number, // request용 (자연어→숫자 변환) + transaction_id?: number, // cancel, confirm, reject용 + limit?: number // history용 (기본 10) +} ``` -**Deposit Agent 응답 포맷:** +**응답 포맷 (고정):** ``` -잔액 조회: "현재 잔액: 10,000원" -입금 성공: "입금 확인! 5,000원 충전. 잔액: 15,000원" -입금 대기: "입금 요청 등록! 은행 확인 후 자동 충전됩니다." +잔액 조회: "💰 현재 잔액: 10,000원" +입금 성공: "✅ 입금 확인 완료! • 입금액: 5,000원 • 현재 잔액: 15,000원" +입금 대기: "📋 입금 요청 등록 (#123) • 입금액: 5,000원" 거래 내역: "#5: 입금 10원 ✓ (01/17)" (✓확인, ⏳대기, ✗취소) ``` -**Deposit Agent 도구:** +**action별 처리:** | 함수 | 설명 | 권한 | |------|------|------| | `get_balance` | 잔액 조회 | 모든 사용자 | diff --git a/src/deposit-agent.ts b/src/deposit-agent.ts index 11c3c81..f92594d 100644 --- a/src/deposit-agent.ts +++ b/src/deposit-agent.ts @@ -9,15 +9,15 @@ * - [관리자] 대기 목록, 입금 확인/거절 */ -interface DepositContext { +export interface DepositContext { userId: number; telegramUserId: string; isAdmin: boolean; db: D1Database; } -// 예치금 API 함수 실행 -async function executeDepositFunction( +// 예치금 API 함수 실행 (export for direct use without Agent) +export async function executeDepositFunction( funcName: string, funcArgs: Record, context: DepositContext diff --git a/src/openai-service.ts b/src/openai-service.ts index ebc01ad..804229a 100644 --- a/src/openai-service.ts +++ b/src/openai-service.ts @@ -1,5 +1,5 @@ import type { Env } from './types'; -import { callDepositAgent } from './deposit-agent'; +import { executeDepositFunction, type DepositContext } from './deposit-agent'; // Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회) const OPENAI_API_URL = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions'; @@ -153,16 +153,33 @@ const tools = [ type: 'function', function: { name: 'manage_deposit', - description: '예치금을 관리합니다. 잔액 조회, 입금 계좌 안내, 입금 신고(충전 요청), 거래 내역 조회 등을 수행합니다. "입금", "충전", "잔액", "계좌", "계좌번호", "송금" 등의 키워드가 포함되면 반드시 이 도구를 사용하세요.', + description: '예치금을 관리합니다. "입금", "충전", "잔액", "계좌", "계좌번호", "송금", "거래내역" 등의 키워드가 포함되면 반드시 이 도구를 사용하세요.', parameters: { type: 'object', properties: { - query: { + action: { type: 'string', - description: '예치금 관련 요청 (예: 잔액 확인, 홍길동 10000원 입금, 거래 내역, 입금 취소 #123)', + enum: ['balance', 'account', 'request', 'history', 'cancel', 'pending', 'confirm', 'reject'], + description: 'balance: 잔액 조회, account: 입금 계좌 안내, request: 입금 신고(충전 요청), history: 거래 내역, cancel: 입금 취소, pending: 대기 목록(관리자), confirm: 입금 확인(관리자), reject: 입금 거절(관리자)', + }, + depositor_name: { + type: 'string', + description: '입금자명. request action에서 필수', + }, + amount: { + type: 'number', + description: '금액. request action에서 필수. 자연어 금액은 숫자로 변환 (만원→10000, 5천원→5000)', + }, + transaction_id: { + type: 'number', + description: '거래 ID. cancel, confirm, reject action에서 필수', + }, + limit: { + type: 'number', + description: '조회 개수. history action에서 사용 (기본 10)', }, }, - required: ['query'], + required: ['action'], }, }, }, @@ -804,6 +821,84 @@ async function executeDomainAction( } } +// 예치금 결과 포맷팅 (고정 형식) +function formatDepositResult(action: string, result: any): string { + if (result.error) { + return `🚫 ${result.error}`; + } + + switch (action) { + case 'balance': + return `💰 현재 잔액: ${result.formatted}`; + + case 'account': + return `💳 입금 계좌 안내 + +• 은행: ${result.bank} +• 계좌번호: ${result.account} +• 예금주: ${result.holder} + +📌 ${result.instruction}`; + + case 'request': + if (result.auto_matched) { + return `✅ 입금 확인 완료! + +• 입금액: ${result.amount.toLocaleString()}원 +• 입금자: ${result.depositor_name} +• 현재 잔액: ${result.new_balance.toLocaleString()}원 + +${result.message}`; + } else { + return `📋 입금 요청 등록 (#${result.transaction_id}) + +• 입금액: ${result.amount.toLocaleString()}원 +• 입금자: ${result.depositor_name} + +💳 입금 계좌 +${result.account_info.bank} ${result.account_info.account} +(${result.account_info.holder}) + +📌 ${result.message}`; + } + + case 'history': { + if (result.message && !result.transactions?.length) { + return `📋 ${result.message}`; + } + const statusIcon = (s: string) => s === 'confirmed' ? '✓' : s === 'pending' ? '⏳' : '✗'; + const txList = result.transactions.map((tx: any) => { + const date = tx.confirmed_at || tx.created_at; + const dateStr = date ? new Date(date).toLocaleDateString('ko-KR', { month: '2-digit', day: '2-digit' }) : ''; + return `#${tx.id}: ${tx.type === 'deposit' ? '입금' : tx.type} ${tx.amount.toLocaleString()}원 ${statusIcon(tx.status)} (${dateStr})`; + }).join('\n'); + return `📋 거래 내역\n\n${txList}`; + } + + case 'cancel': + return `✅ 거래 #${result.transaction_id} 취소 완료`; + + case 'pending': { + if (result.message && !result.pending?.length) { + return `📋 ${result.message}`; + } + const pendingList = result.pending.map((p: any) => + `#${p.id}: ${p.depositor_name} ${p.amount.toLocaleString()}원 (${p.user})` + ).join('\n'); + return `📋 대기 중인 입금 요청\n\n${pendingList}`; + } + + case 'confirm': + return `✅ 입금 확인 완료 (#${result.transaction_id}, ${result.amount.toLocaleString()}원)`; + + case 'reject': + return `❌ 입금 거절 완료 (#${result.transaction_id})`; + + default: + return `💰 ${JSON.stringify(result)}`; + } +} + // 도구 실행 async function executeTool(name: string, args: Record, env?: Env, telegramUserId?: string, db?: D1Database): Promise { switch (name) { @@ -963,8 +1058,8 @@ async function executeTool(name: string, args: Record, env?: Env } case 'manage_deposit': { - const query = args.query; - console.log('[manage_deposit] 시작:', { query, telegramUserId, hasDb: !!db }); + const { action, depositor_name, amount, transaction_id, limit } = args; + console.log('[manage_deposit] 시작:', { action, depositor_name, amount, telegramUserId }); if (!telegramUserId || !db) { return '🚫 예치금 기능을 사용할 수 없습니다.'; @@ -980,36 +1075,46 @@ async function executeTool(name: string, args: Record, env?: Env } const isAdmin = telegramUserId === env?.DEPOSIT_ADMIN_ID; + const context: DepositContext = { + userId: user.id, + telegramUserId, + isAdmin, + db, + }; - if (!env?.OPENAI_API_KEY || !env?.DEPOSIT_AGENT_ID) { - console.log('[manage_deposit] DEPOSIT_AGENT_ID 미설정, 기본 응답'); - return '💰 예치금 에이전트가 설정되지 않았습니다. 관리자에게 문의하세요.'; + // action → executeDepositFunction 매핑 + const actionMap: Record = { + balance: 'get_balance', + account: 'get_account_info', + request: 'request_deposit', + history: 'get_transactions', + cancel: 'cancel_transaction', + pending: 'get_pending_list', + confirm: 'confirm_deposit', + reject: 'reject_deposit', + }; + + const funcName = actionMap[action]; + if (!funcName) { + return `🚫 알 수 없는 작업: ${action}`; } try { - console.log('[manage_deposit] callDepositAgent 호출'); - const result = await callDepositAgent( - env.OPENAI_API_KEY, - env.DEPOSIT_AGENT_ID, - query, - { - userId: user.id, - telegramUserId, - isAdmin, - db, - } - ); - console.log('[manage_deposit] callDepositAgent 완료:', result?.slice(0, 100)); + const funcArgs: Record = {}; + if (depositor_name) funcArgs.depositor_name = depositor_name; + if (amount) funcArgs.amount = Number(amount); + if (transaction_id) funcArgs.transaction_id = Number(transaction_id); + if (limit) funcArgs.limit = Number(limit); - // Markdown → HTML 변환 - const htmlResult = result - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/\*(.+?)\*/g, '$1') - .replace(/`(.+?)`/g, '$1'); - return `💰 ${htmlResult}`; + console.log('[manage_deposit] executeDepositFunction 호출:', funcName, funcArgs); + const result = await executeDepositFunction(funcName, funcArgs, context); + console.log('[manage_deposit] 결과:', JSON.stringify(result).slice(0, 200)); + + // 결과 포맷팅 (고정 형식) + return formatDepositResult(action, result); } catch (error) { console.error('[manage_deposit] 오류:', error); - return `💰 예치금 처리 오류: ${String(error)}`; + return `🚫 예치금 처리 오류: ${String(error)}`; } }