refactor: migrate Deposit Agent from Assistants API to direct function calling
- Replace OpenAI Assistants API with direct function calling (AI Gateway) - Add action-based parameters to manage_deposit tool (like manage_domain) - Export executeDepositFunction for direct use in openai-service.ts - Add formatDepositResult function for consistent response formatting - Remove DEPOSIT_AGENT_ID dependency (no longer needed) - Update CLAUDE.md documentation Benefits: - Bypasses regional restrictions via AI Gateway - 100% consistent response formatting - No Assistants API costs - Faster execution (no thread creation) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
48
CLAUDE.md
48
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` | 잔액 조회 | 모든 사용자 |
|
||||
|
||||
@@ -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<string, any>,
|
||||
context: DepositContext
|
||||
|
||||
@@ -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<string, string>, env?: Env, telegramUserId?: string, db?: D1Database): Promise<string> {
|
||||
switch (name) {
|
||||
@@ -963,8 +1058,8 @@ async function executeTool(name: string, args: Record<string, string>, 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<string, string>, 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<string, string> = {
|
||||
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<string, any> = {};
|
||||
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, '<b>$1</b>')
|
||||
.replace(/\*(.+?)\*/g, '<i>$1</i>')
|
||||
.replace(/`(.+?)`/g, '<code>$1</code>');
|
||||
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)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user