- Add full conversation flow with session management - Handle tool call execution - Support __PASSTHROUGH__ and __SESSION_END__ markers - Add hasDomainSession helper for routing - Export executeDomainAction from domain-tool.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1094 lines
29 KiB
Markdown
1094 lines
29 KiB
Markdown
# 도메인/예치금 에이전트 리팩토링 구현 계획
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** domain-tool.ts와 deposit-agent.ts를 server-agent 패턴의 세션 기반 AI 에이전트로 리팩토링
|
|
|
|
**Architecture:** 모든 도메인/예치금 요청이 에이전트를 통과하고 AI가 의도를 파악. 도메인은 작업 단위 세션, 예치금은 입금 신고만 세션 유지.
|
|
|
|
**Tech Stack:** TypeScript, Cloudflare Workers, D1, OpenAI GPT-4o-mini
|
|
|
|
---
|
|
|
|
## Phase 1: 기반 작업
|
|
|
|
### Task 1.1: 마이그레이션 SQL 작성
|
|
|
|
**Files:**
|
|
- Create: `migrations/006_add_agent_sessions.sql`
|
|
|
|
**Step 1: 마이그레이션 파일 작성**
|
|
|
|
```sql
|
|
-- 006_add_agent_sessions.sql
|
|
-- Domain Agent Sessions
|
|
CREATE TABLE IF NOT EXISTS domain_sessions (
|
|
user_id TEXT PRIMARY KEY,
|
|
status TEXT NOT NULL CHECK(status IN ('gathering', 'suggesting', 'confirming', 'setting_ns', 'completed')),
|
|
collected_info TEXT, -- JSON: {keywords, purpose, suggestions[]}
|
|
target_domain TEXT, -- 등록/NS변경 대상 도메인
|
|
messages TEXT, -- JSON: 대화 히스토리
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
expires_at INTEGER NOT NULL -- TTL 1시간
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_domain_sessions_expires ON domain_sessions(expires_at);
|
|
|
|
-- Deposit Agent Sessions
|
|
CREATE TABLE IF NOT EXISTS deposit_sessions (
|
|
user_id TEXT PRIMARY KEY,
|
|
status TEXT NOT NULL CHECK(status IN ('collecting_amount', 'collecting_name', 'confirming', 'completed')),
|
|
collected_info TEXT, -- JSON: {amount, depositor_name}
|
|
messages TEXT, -- JSON: 대화 히스토리
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
expires_at INTEGER NOT NULL -- TTL 30분
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_deposit_sessions_expires ON deposit_sessions(expires_at);
|
|
```
|
|
|
|
**Step 2: 로컬 D1에 적용 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && wrangler d1 execute telegram-conversations --local --file=migrations/006_add_agent_sessions.sql`
|
|
Expected: 테이블 생성 성공
|
|
|
|
**Step 3: 커밋**
|
|
|
|
```bash
|
|
git add migrations/006_add_agent_sessions.sql
|
|
git commit -m "chore: add domain/deposit agent session tables migration"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.2: agents 디렉토리 생성 및 server-agent 이동
|
|
|
|
**Files:**
|
|
- Create: `src/agents/` 디렉토리
|
|
- Move: `src/server-agent.ts` → `src/agents/server-agent.ts`
|
|
- Modify: `src/openai-service.ts` (import 경로 수정)
|
|
- Modify: `src/tools/server-tool.ts` (import 경로 수정)
|
|
- Modify: `src/index.ts` (import 경로 수정)
|
|
|
|
**Step 1: 디렉토리 생성 및 파일 이동**
|
|
|
|
```bash
|
|
mkdir -p src/agents
|
|
mv src/server-agent.ts src/agents/server-agent.ts
|
|
```
|
|
|
|
**Step 2: openai-service.ts import 수정**
|
|
|
|
기존:
|
|
```typescript
|
|
import { getServerSession, processServerConsultation } from './server-agent';
|
|
```
|
|
|
|
변경:
|
|
```typescript
|
|
import { getServerSession, processServerConsultation } from './agents/server-agent';
|
|
```
|
|
|
|
**Step 3: tools/server-tool.ts import 수정**
|
|
|
|
기존:
|
|
```typescript
|
|
import { getServerSession, saveServerSession, deleteServerSession } from '../server-agent';
|
|
```
|
|
|
|
변경:
|
|
```typescript
|
|
import { getServerSession, saveServerSession, deleteServerSession } from '../agents/server-agent';
|
|
```
|
|
|
|
**Step 4: index.ts import 수정 (있다면)**
|
|
|
|
필요 시 import 경로 수정
|
|
|
|
**Step 5: 빌드 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npm run dev`
|
|
Expected: 에러 없이 실행
|
|
|
|
**Step 6: 커밋**
|
|
|
|
```bash
|
|
git add src/agents/ src/openai-service.ts src/tools/server-tool.ts
|
|
git commit -m "refactor: move server-agent to agents directory"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.3: troubleshoot-agent도 agents로 이동
|
|
|
|
**Files:**
|
|
- Move: `src/troubleshoot-agent.ts` → `src/agents/troubleshoot-agent.ts`
|
|
- Modify: `src/openai-service.ts` (import 경로 수정)
|
|
- Modify: `src/tools/troubleshoot-tool.ts` (import 경로 수정)
|
|
|
|
**Step 1: 파일 이동**
|
|
|
|
```bash
|
|
mv src/troubleshoot-agent.ts src/agents/troubleshoot-agent.ts
|
|
```
|
|
|
|
**Step 2: openai-service.ts import 수정**
|
|
|
|
기존:
|
|
```typescript
|
|
import { getTroubleshootSession, processTroubleshoot } from './troubleshoot-agent';
|
|
```
|
|
|
|
변경:
|
|
```typescript
|
|
import { getTroubleshootSession, processTroubleshoot } from './agents/troubleshoot-agent';
|
|
```
|
|
|
|
**Step 3: tools/troubleshoot-tool.ts import 수정**
|
|
|
|
필요 시 import 경로 수정
|
|
|
|
**Step 4: 빌드 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npm run dev`
|
|
Expected: 에러 없이 실행
|
|
|
|
**Step 5: 커밋**
|
|
|
|
```bash
|
|
git add src/agents/troubleshoot-agent.ts src/openai-service.ts src/tools/troubleshoot-tool.ts
|
|
git commit -m "refactor: move troubleshoot-agent to agents directory"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 1.4: 타입 정의 추가 (types.ts)
|
|
|
|
**Files:**
|
|
- Modify: `src/types.ts`
|
|
|
|
**Step 1: DomainSession 타입 추가**
|
|
|
|
```typescript
|
|
// Domain Agent Session
|
|
export interface DomainSession {
|
|
telegramUserId: string;
|
|
status: 'gathering' | 'suggesting' | 'confirming' | 'setting_ns' | 'completed';
|
|
collectedInfo: {
|
|
keywords?: string;
|
|
purpose?: string; // 블로그, 쇼핑몰, 브랜드 등
|
|
suggestions?: Array<{
|
|
domain: string;
|
|
price: number;
|
|
available: boolean;
|
|
}>;
|
|
};
|
|
targetDomain?: string; // 등록/NS변경 대상
|
|
targetNameservers?: string[]; // NS변경 시 새 네임서버
|
|
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
// Deposit Agent Session
|
|
export interface DepositSession {
|
|
telegramUserId: string;
|
|
status: 'collecting_amount' | 'collecting_name' | 'confirming' | 'completed';
|
|
collectedInfo: {
|
|
amount?: number;
|
|
depositorName?: string;
|
|
};
|
|
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
```
|
|
|
|
**Step 2: 빌드 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npx tsc --noEmit`
|
|
Expected: 타입 에러 없음
|
|
|
|
**Step 3: 커밋**
|
|
|
|
```bash
|
|
git add src/types.ts
|
|
git commit -m "feat: add DomainSession and DepositSession types"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: 도메인 에이전트
|
|
|
|
### Task 2.1: domain-agent.ts 기본 구조 생성
|
|
|
|
**Files:**
|
|
- Create: `src/agents/domain-agent.ts`
|
|
|
|
**Step 1: 세션 CRUD 함수 작성**
|
|
|
|
```typescript
|
|
/**
|
|
* Domain Agent - 도메인 관리 AI 에이전트
|
|
*
|
|
* 기능:
|
|
* - 대화형 도메인 추천 상담
|
|
* - 도메인 등록 확인 흐름
|
|
* - 네임서버 변경 확인 흐름
|
|
* - 단순 조회는 즉시 응답 (세션 없음)
|
|
*/
|
|
|
|
import type { Env, DomainSession } from '../types';
|
|
import { createLogger } from '../utils/logger';
|
|
|
|
const logger = createLogger('domain-agent');
|
|
|
|
const SESSION_TTL_MS = 3600 * 1000; // 1 hour
|
|
|
|
// Session CRUD
|
|
export async function getDomainSession(
|
|
db: D1Database,
|
|
userId: string
|
|
): Promise<DomainSession | null> {
|
|
try {
|
|
const now = Date.now();
|
|
const result = await db.prepare(
|
|
'SELECT * FROM domain_sessions WHERE user_id = ? AND expires_at > ?'
|
|
).bind(userId, now).first<{
|
|
user_id: string;
|
|
status: string;
|
|
collected_info: string | null;
|
|
target_domain: string | null;
|
|
messages: string | null;
|
|
created_at: number;
|
|
updated_at: number;
|
|
}>();
|
|
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
telegramUserId: result.user_id,
|
|
status: result.status as DomainSession['status'],
|
|
collectedInfo: result.collected_info ? JSON.parse(result.collected_info) : {},
|
|
targetDomain: result.target_domain || undefined,
|
|
messages: result.messages ? JSON.parse(result.messages) : [],
|
|
createdAt: result.created_at,
|
|
updatedAt: result.updated_at,
|
|
};
|
|
} catch (error) {
|
|
logger.error('세션 조회 실패', error as Error, { userId });
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function saveDomainSession(
|
|
db: D1Database,
|
|
userId: string,
|
|
session: DomainSession
|
|
): Promise<void> {
|
|
try {
|
|
const now = Date.now();
|
|
const expiresAt = now + SESSION_TTL_MS;
|
|
|
|
await db.prepare(`
|
|
INSERT INTO domain_sessions
|
|
(user_id, status, collected_info, target_domain, messages, created_at, updated_at, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
status = excluded.status,
|
|
collected_info = excluded.collected_info,
|
|
target_domain = excluded.target_domain,
|
|
messages = excluded.messages,
|
|
updated_at = excluded.updated_at,
|
|
expires_at = excluded.expires_at
|
|
`).bind(
|
|
userId,
|
|
session.status,
|
|
JSON.stringify(session.collectedInfo || {}),
|
|
session.targetDomain || null,
|
|
JSON.stringify(session.messages || []),
|
|
session.createdAt || now,
|
|
now,
|
|
expiresAt
|
|
).run();
|
|
|
|
logger.info('세션 저장 성공', { userId, status: session.status });
|
|
} catch (error) {
|
|
logger.error('세션 저장 실패', error as Error, { userId });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function deleteDomainSession(
|
|
db: D1Database,
|
|
userId: string
|
|
): Promise<void> {
|
|
try {
|
|
await db.prepare('DELETE FROM domain_sessions WHERE user_id = ?')
|
|
.bind(userId)
|
|
.run();
|
|
logger.info('세션 삭제 성공', { userId });
|
|
} catch (error) {
|
|
logger.error('세션 삭제 실패', error as Error, { userId });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function cleanupExpiredDomainSessions(db: D1Database): Promise<number> {
|
|
try {
|
|
const result = await db.prepare(
|
|
'DELETE FROM domain_sessions WHERE expires_at < ?'
|
|
).bind(Date.now()).run();
|
|
|
|
const deleted = result.meta.changes || 0;
|
|
if (deleted > 0) {
|
|
logger.info('만료 세션 정리', { deleted });
|
|
}
|
|
return deleted;
|
|
} catch (error) {
|
|
logger.error('만료 세션 정리 실패', error as Error);
|
|
return 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 빌드 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npx tsc --noEmit`
|
|
Expected: 타입 에러 없음
|
|
|
|
**Step 3: 커밋**
|
|
|
|
```bash
|
|
git add src/agents/domain-agent.ts
|
|
git commit -m "feat: add domain-agent session CRUD functions"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.2: 도메인 에이전트 AI 호출 함수 작성
|
|
|
|
**Files:**
|
|
- Modify: `src/agents/domain-agent.ts`
|
|
|
|
**Step 1: AI 도구 정의 및 호출 함수 추가**
|
|
|
|
```typescript
|
|
// Domain Expert AI Tools
|
|
const domainAgentTools = [
|
|
{
|
|
type: 'function' as const,
|
|
function: {
|
|
name: 'check_domain',
|
|
description: '도메인 등록 가능 여부와 가격을 확인합니다.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
domain: {
|
|
type: 'string',
|
|
description: '확인할 도메인 (예: example.com)',
|
|
},
|
|
},
|
|
required: ['domain'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function' as const,
|
|
function: {
|
|
name: 'search_suggestions',
|
|
description: '키워드 기반으로 도메인을 추천합니다.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
keywords: {
|
|
type: 'string',
|
|
description: '도메인 추천을 위한 키워드',
|
|
},
|
|
},
|
|
required: ['keywords'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function' as const,
|
|
function: {
|
|
name: 'get_whois',
|
|
description: '도메인 WHOIS 정보를 조회합니다.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
domain: {
|
|
type: 'string',
|
|
description: '조회할 도메인',
|
|
},
|
|
},
|
|
required: ['domain'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function' as const,
|
|
function: {
|
|
name: 'get_tld_price',
|
|
description: 'TLD 가격을 조회합니다.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
tld: {
|
|
type: 'string',
|
|
description: 'TLD (예: com, io, net)',
|
|
},
|
|
},
|
|
required: ['tld'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function' as const,
|
|
function: {
|
|
name: 'list_my_domains',
|
|
description: '사용자의 도메인 목록을 조회합니다.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'function' as const,
|
|
function: {
|
|
name: 'get_nameservers',
|
|
description: '도메인의 네임서버를 조회합니다.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
domain: {
|
|
type: 'string',
|
|
description: '조회할 도메인',
|
|
},
|
|
},
|
|
required: ['domain'],
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
// AI 호출 함수
|
|
async function callDomainExpertAI(
|
|
env: Env,
|
|
session: DomainSession,
|
|
userMessage: string
|
|
): Promise<{
|
|
action: 'question' | 'suggest' | 'register' | 'set_ns' | 'immediate';
|
|
message: string;
|
|
collectedInfo?: DomainSession['collectedInfo'];
|
|
targetDomain?: string;
|
|
targetNameservers?: string[];
|
|
toolCalls?: Array<{ name: string; args: Record<string, unknown> }>;
|
|
}> {
|
|
if (!env.OPENAI_API_KEY) {
|
|
throw new Error('OPENAI_API_KEY not configured');
|
|
}
|
|
|
|
const { getOpenAIUrl } = await import('../utils/api-urls');
|
|
|
|
const conversationHistory = session.messages.map(m => ({
|
|
role: m.role === 'user' ? 'user' as const : 'assistant' as const,
|
|
content: m.content,
|
|
}));
|
|
|
|
const systemPrompt = `당신은 10년 경력의 도메인 컨설턴트입니다.
|
|
|
|
## 전문성
|
|
- 브랜딩, SEO, 가격 대비 가치를 고려한 조언 제공
|
|
- 불필요한 프리미엄 도메인 추천 자제
|
|
- 실용적이고 기억하기 쉬운 도메인 추천
|
|
|
|
## 성격
|
|
- 따뜻하고 친근하지만 전문적인 어조
|
|
- 비기술자도 이해하기 쉽게 설명
|
|
|
|
## 작업 분류
|
|
|
|
### 세션 필요 작업 (action으로 반환)
|
|
- "도메인 추천" → action="suggest", 키워드/용도 수집
|
|
- "도메인 등록" → action="register", 도메인명 확인
|
|
- "네임서버 변경" → action="set_ns", 변경할 NS 확인
|
|
|
|
### 즉시 응답 작업 (도구 호출 후 action="immediate")
|
|
- 가격 조회 → get_tld_price 도구 호출
|
|
- WHOIS 조회 → get_whois 도구 호출
|
|
- 내 도메인 목록 → list_my_domains 도구 호출
|
|
- 네임서버 조회 → get_nameservers 도구 호출
|
|
- 도메인 가용성 확인 → check_domain 도구 호출
|
|
|
|
## 현재 수집된 정보
|
|
${JSON.stringify(session.collectedInfo, null, 2)}
|
|
|
|
## 응답 형식 (반드시 JSON만 반환)
|
|
{
|
|
"action": "question" | "suggest" | "register" | "set_ns" | "immediate",
|
|
"message": "사용자에게 보여줄 메시지",
|
|
"collectedInfo": { ... }, // 수집된 정보 업데이트
|
|
"targetDomain": "example.com", // register, set_ns 시
|
|
"targetNameservers": ["ns1.example.com", "ns2.example.com"] // set_ns 시
|
|
}`;
|
|
|
|
try {
|
|
const response = await fetch(getOpenAIUrl(env), {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${env.OPENAI_API_KEY}`,
|
|
},
|
|
body: JSON.stringify({
|
|
model: 'gpt-4o-mini',
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
...conversationHistory,
|
|
{ role: 'user', content: userMessage },
|
|
],
|
|
tools: domainAgentTools,
|
|
tool_choice: 'auto',
|
|
response_format: { type: 'json_object' },
|
|
max_tokens: 800,
|
|
temperature: 0.7,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
|
|
}
|
|
|
|
const data = await response.json() as {
|
|
choices: Array<{
|
|
message: {
|
|
content: string | null;
|
|
tool_calls?: Array<{
|
|
id: string;
|
|
function: { name: string; arguments: string };
|
|
}>;
|
|
};
|
|
}>;
|
|
};
|
|
|
|
const assistantMessage = data.choices[0].message;
|
|
|
|
// Tool calls가 있으면 처리
|
|
if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
|
|
const toolCalls = assistantMessage.tool_calls.map(tc => ({
|
|
name: tc.function.name,
|
|
args: JSON.parse(tc.function.arguments),
|
|
}));
|
|
|
|
return {
|
|
action: 'immediate',
|
|
message: '',
|
|
toolCalls,
|
|
};
|
|
}
|
|
|
|
// JSON 응답 파싱
|
|
const aiResponse = assistantMessage.content || '';
|
|
const parsed = JSON.parse(aiResponse);
|
|
|
|
return {
|
|
action: parsed.action,
|
|
message: parsed.message,
|
|
collectedInfo: parsed.collectedInfo,
|
|
targetDomain: parsed.targetDomain,
|
|
targetNameservers: parsed.targetNameservers,
|
|
};
|
|
} catch (error) {
|
|
logger.error('Domain Expert AI 호출 실패', error as Error);
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 빌드 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npx tsc --noEmit`
|
|
Expected: 타입 에러 없음
|
|
|
|
**Step 3: 커밋**
|
|
|
|
```bash
|
|
git add src/agents/domain-agent.ts
|
|
git commit -m "feat: add domain-agent AI tools and callDomainExpertAI function"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.3: 도메인 에이전트 도구 실행 함수 이관
|
|
|
|
**Files:**
|
|
- Modify: `src/agents/domain-agent.ts`
|
|
- Reference: `src/tools/domain-tool.ts` (로직 복사)
|
|
|
|
**Step 1: domain-tool.ts에서 핵심 로직 복사**
|
|
|
|
기존 `domain-tool.ts`의 `callNamecheapApi`, `executeDomainAction` 함수들을
|
|
`domain-agent.ts`로 이관. 단, 함수명 변경:
|
|
|
|
- `executeDomainAction` → `executeDomainTool`
|
|
|
|
```typescript
|
|
// domain-agent.ts에 추가
|
|
|
|
// Namecheap API 호출 함수 (domain-tool.ts에서 복사)
|
|
async function callNamecheapApi(
|
|
funcName: string,
|
|
funcArgs: Record<string, unknown>,
|
|
allowedDomains: string[],
|
|
env?: Env,
|
|
telegramUserId?: string,
|
|
db?: D1Database,
|
|
userId?: number
|
|
): Promise<unknown> {
|
|
// ... domain-tool.ts의 callNamecheapApi 함수 전체 복사
|
|
}
|
|
|
|
// 도구 실행 함수
|
|
async function executeDomainTool(
|
|
toolName: string,
|
|
args: Record<string, unknown>,
|
|
env: Env,
|
|
telegramUserId: string,
|
|
db: D1Database
|
|
): Promise<string> {
|
|
// 사용자 도메인 목록 조회
|
|
const user = await db.prepare(
|
|
'SELECT id FROM users WHERE telegram_id = ?'
|
|
).bind(telegramUserId).first<{ id: number }>();
|
|
|
|
if (!user) {
|
|
return '🚫 사용자 정보를 찾을 수 없습니다.';
|
|
}
|
|
|
|
const domains = await db.prepare(
|
|
'SELECT domain FROM user_domains WHERE user_id = ? AND verified = 1'
|
|
).bind(user.id).all<{ domain: string }>();
|
|
const userDomains = domains.results?.map(d => d.domain) || [];
|
|
|
|
switch (toolName) {
|
|
case 'check_domain': {
|
|
// domain-tool.ts의 'check' action 로직
|
|
const domain = args.domain as string;
|
|
// ... 구현
|
|
}
|
|
case 'search_suggestions': {
|
|
// domain-tool.ts의 executeSuggestDomains 로직
|
|
// ... 구현
|
|
}
|
|
case 'get_whois': {
|
|
// domain-tool.ts의 'whois' action 로직
|
|
// ... 구현
|
|
}
|
|
case 'get_tld_price': {
|
|
// domain-tool.ts의 'price' action 로직
|
|
// ... 구현
|
|
}
|
|
case 'list_my_domains': {
|
|
// domain-tool.ts의 'list' action 로직
|
|
// ... 구현
|
|
}
|
|
case 'get_nameservers': {
|
|
// domain-tool.ts의 'get_ns' action 로직
|
|
// ... 구현
|
|
}
|
|
default:
|
|
return `알 수 없는 도구: ${toolName}`;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 빌드 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npx tsc --noEmit`
|
|
Expected: 타입 에러 없음
|
|
|
|
**Step 3: 커밋**
|
|
|
|
```bash
|
|
git add src/agents/domain-agent.ts
|
|
git commit -m "feat: add domain tool execution functions to domain-agent"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.4: 도메인 에이전트 메인 처리 함수 작성
|
|
|
|
**Files:**
|
|
- Modify: `src/agents/domain-agent.ts`
|
|
|
|
**Step 1: processDomainConsultation 함수 작성**
|
|
|
|
```typescript
|
|
export async function processDomainConsultation(
|
|
userMessage: string,
|
|
session: DomainSession,
|
|
env: Env
|
|
): Promise<string> {
|
|
try {
|
|
logger.info('도메인 상담 처리 시작', {
|
|
userId: session.telegramUserId,
|
|
message: userMessage.slice(0, 50),
|
|
status: session.status
|
|
});
|
|
|
|
// 취소 키워드 처리
|
|
if (/^(취소|다시|처음|리셋)/.test(userMessage.trim())) {
|
|
await deleteDomainSession(env.DB, session.telegramUserId);
|
|
return '도메인 상담이 취소되었습니다. 다시 시작하려면 "도메인 추천"이라고 말씀해주세요.';
|
|
}
|
|
|
|
// 무관한 메시지 감지
|
|
const unrelatedPatterns = /날씨|시간|계산|서버|입금|충전|잔액/;
|
|
if (unrelatedPatterns.test(userMessage)) {
|
|
return '__PASSTHROUGH__';
|
|
}
|
|
|
|
// 세션에 메시지 추가
|
|
session.messages.push({ role: 'user', content: userMessage });
|
|
|
|
// AI 호출
|
|
const aiResult = await callDomainExpertAI(env, session, userMessage);
|
|
|
|
// 도구 호출이 있으면 실행
|
|
if (aiResult.action === 'immediate' && aiResult.toolCalls) {
|
|
const results: string[] = [];
|
|
for (const toolCall of aiResult.toolCalls) {
|
|
const result = await executeDomainTool(
|
|
toolCall.name,
|
|
toolCall.args,
|
|
env,
|
|
session.telegramUserId,
|
|
env.DB
|
|
);
|
|
results.push(result);
|
|
}
|
|
|
|
// 세션 삭제 (즉시 응답은 세션 불필요)
|
|
await deleteDomainSession(env.DB, session.telegramUserId);
|
|
return results.join('\n\n');
|
|
}
|
|
|
|
// 수집된 정보 업데이트
|
|
if (aiResult.collectedInfo) {
|
|
session.collectedInfo = { ...session.collectedInfo, ...aiResult.collectedInfo };
|
|
}
|
|
if (aiResult.targetDomain) {
|
|
session.targetDomain = aiResult.targetDomain;
|
|
}
|
|
if (aiResult.targetNameservers) {
|
|
session.targetNameservers = aiResult.targetNameservers;
|
|
}
|
|
|
|
// 세션에 AI 응답 추가
|
|
session.messages.push({ role: 'assistant', content: aiResult.message });
|
|
|
|
// 상태 전환
|
|
switch (aiResult.action) {
|
|
case 'question':
|
|
session.status = 'gathering';
|
|
break;
|
|
case 'suggest':
|
|
session.status = 'suggesting';
|
|
// TODO: 도메인 추천 실행
|
|
break;
|
|
case 'register':
|
|
session.status = 'confirming';
|
|
// TODO: 등록 확인 흐름
|
|
break;
|
|
case 'set_ns':
|
|
session.status = 'setting_ns';
|
|
// TODO: NS 변경 확인 흐름
|
|
break;
|
|
}
|
|
|
|
await saveDomainSession(env.DB, session.telegramUserId, session);
|
|
return aiResult.message;
|
|
|
|
} catch (error) {
|
|
logger.error('도메인 상담 처리 실패', error as Error);
|
|
await deleteDomainSession(env.DB, session.telegramUserId);
|
|
return '죄송합니다. 도메인 상담 중 오류가 발생했습니다.\n다시 시도하려면 "도메인 추천"이라고 말씀해주세요.';
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: 빌드 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npx tsc --noEmit`
|
|
Expected: 타입 에러 없음
|
|
|
|
**Step 3: 커밋**
|
|
|
|
```bash
|
|
git add src/agents/domain-agent.ts
|
|
git commit -m "feat: add processDomainConsultation main handler"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.5: openai-service.ts에 도메인 세션 체크 추가
|
|
|
|
**Files:**
|
|
- Modify: `src/openai-service.ts`
|
|
|
|
**Step 1: import 추가**
|
|
|
|
```typescript
|
|
import { getDomainSession, processDomainConsultation } from './agents/domain-agent';
|
|
```
|
|
|
|
**Step 2: generateOpenAIResponse에 도메인 세션 체크 추가**
|
|
|
|
서버 세션 체크 전에 도메인 세션 체크 추가:
|
|
|
|
```typescript
|
|
export async function generateOpenAIResponse(
|
|
env: Env,
|
|
userMessage: string,
|
|
systemPrompt: string,
|
|
recentContext: { role: 'user' | 'assistant'; content: string }[],
|
|
telegramUserId?: string,
|
|
db?: D1Database,
|
|
chatIdStr?: string
|
|
): Promise<string> {
|
|
// Check if domain session is active
|
|
if (telegramUserId && env.DB) {
|
|
try {
|
|
const domainSession = await getDomainSession(env.DB, telegramUserId);
|
|
|
|
if (domainSession && domainSession.status !== 'completed') {
|
|
logger.info('Active domain session detected', {
|
|
userId: telegramUserId,
|
|
status: domainSession.status
|
|
});
|
|
|
|
const result = await processDomainConsultation(userMessage, domainSession, env);
|
|
|
|
if (result !== '__PASSTHROUGH__') {
|
|
return result;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('Domain session check failed', error as Error);
|
|
}
|
|
}
|
|
|
|
// ... 기존 서버 세션 체크 코드 ...
|
|
```
|
|
|
|
**Step 3: 빌드 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npx tsc --noEmit`
|
|
Expected: 타입 에러 없음
|
|
|
|
**Step 4: 커밋**
|
|
|
|
```bash
|
|
git add src/openai-service.ts
|
|
git commit -m "feat: add domain session check to openai-service"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2.6: domain-tool.ts를 에이전트 시작 트리거로 변경
|
|
|
|
**Files:**
|
|
- Modify: `src/tools/domain-tool.ts`
|
|
|
|
**Step 1: manage_domain 도구를 세션 시작 트리거로 변경**
|
|
|
|
```typescript
|
|
// domain-tool.ts 전체 리팩토링
|
|
|
|
import type { Env } from '../types';
|
|
import { createLogger } from '../utils/logger';
|
|
import { getDomainSession, saveDomainSession } from '../agents/domain-agent';
|
|
|
|
const logger = createLogger('domain-tool');
|
|
|
|
export const manageDomainTool = {
|
|
type: 'function',
|
|
function: {
|
|
name: 'manage_domain',
|
|
description: '도메인 관련 모든 작업을 처리합니다. 도메인 추천, 등록, 가격 조회, WHOIS, 네임서버 관리 등.',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
request: {
|
|
type: 'string',
|
|
description: '사용자의 도메인 관련 요청 (자연어)',
|
|
},
|
|
},
|
|
required: ['request'],
|
|
},
|
|
},
|
|
};
|
|
|
|
// suggest_domains 도구는 manage_domain으로 통합, 제거 예정
|
|
export const suggestDomainsTool = manageDomainTool;
|
|
|
|
export async function executeManageDomain(
|
|
args: { request: string },
|
|
env?: Env,
|
|
telegramUserId?: string,
|
|
db?: D1Database
|
|
): Promise<string> {
|
|
if (!env || !telegramUserId || !db) {
|
|
return '🚫 도메인 관리 권한이 없습니다.';
|
|
}
|
|
|
|
// 세션 시작
|
|
const existingSession = await getDomainSession(db, telegramUserId);
|
|
|
|
if (!existingSession) {
|
|
// 새 세션 생성
|
|
const newSession = {
|
|
telegramUserId,
|
|
status: 'gathering' as const,
|
|
collectedInfo: {},
|
|
messages: [],
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
};
|
|
await saveDomainSession(db, telegramUserId, newSession);
|
|
|
|
logger.info('도메인 세션 시작', { userId: telegramUserId });
|
|
return '__START_DOMAIN_SESSION__'; // 특수 마커로 세션 시작 알림
|
|
}
|
|
|
|
return '__DOMAIN_SESSION_ACTIVE__'; // 이미 세션 활성화 중
|
|
}
|
|
|
|
export async function executeSuggestDomains(
|
|
args: { keywords: string },
|
|
env?: Env,
|
|
telegramUserId?: string,
|
|
db?: D1Database
|
|
): Promise<string> {
|
|
// manage_domain으로 리다이렉트
|
|
return executeManageDomain({ request: `도메인 추천: ${args.keywords}` }, env, telegramUserId, db);
|
|
}
|
|
```
|
|
|
|
**Step 2: tools/index.ts 수정**
|
|
|
|
도구 목록에서 suggest_domains 제거 또는 manage_domain과 통합
|
|
|
|
**Step 3: 빌드 테스트**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npx tsc --noEmit`
|
|
Expected: 타입 에러 없음
|
|
|
|
**Step 4: 커밋**
|
|
|
|
```bash
|
|
git add src/tools/domain-tool.ts src/tools/index.ts
|
|
git commit -m "refactor: convert domain-tool to session start trigger"
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 3: 예치금 에이전트 (Task 3.1 - 3.5)
|
|
|
|
Phase 2와 동일한 패턴으로:
|
|
|
|
### Task 3.1: deposit-agent.ts를 agents로 이동 및 세션 로직 추가
|
|
### Task 3.2: 예치금 에이전트 AI 호출 함수 작성
|
|
### Task 3.3: 예치금 에이전트 도구 실행 함수 정리
|
|
### Task 3.4: processDepositConsultation 함수 작성
|
|
### Task 3.5: openai-service.ts에 예치금 세션 체크 추가
|
|
|
|
(상세 내용은 Phase 2 패턴과 동일하게 진행)
|
|
|
|
---
|
|
|
|
## Phase 4: 정리 및 테스트
|
|
|
|
### Task 4.1: 사용하지 않는 코드 정리
|
|
|
|
**Files:**
|
|
- Delete: `src/tools/deposit-tool.ts` (deposit-agent로 통합됨)
|
|
- Modify: `src/tools/index.ts` (import 정리)
|
|
|
|
### Task 4.2: 로컬 테스트
|
|
|
|
**Step 1: 로컬 서버 실행**
|
|
|
|
Run: `cd /Users/kaffa/Projects/bots/telegram-bot-workers && npm run dev`
|
|
|
|
**Step 2: 도메인 에이전트 테스트**
|
|
|
|
```bash
|
|
# 도메인 추천 테스트
|
|
curl -X POST http://localhost:8787/webhook \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Telegram-Bot-Api-Secret-Token: test" \
|
|
-d '{"message":{"chat":{"id":123},"from":{"id":123},"text":"도메인 추천해줘"}}'
|
|
|
|
# .com 가격 조회 테스트 (즉시 응답)
|
|
curl -X POST http://localhost:8787/webhook \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Telegram-Bot-Api-Secret-Token: test" \
|
|
-d '{"message":{"chat":{"id":123},"from":{"id":123},"text":".com 가격"}}'
|
|
```
|
|
|
|
**Step 3: 예치금 에이전트 테스트**
|
|
|
|
```bash
|
|
# 충전 테스트 (세션 시작)
|
|
curl -X POST http://localhost:8787/webhook \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Telegram-Bot-Api-Secret-Token: test" \
|
|
-d '{"message":{"chat":{"id":123},"from":{"id":123},"text":"충전할게"}}'
|
|
|
|
# 잔액 조회 테스트 (즉시 응답)
|
|
curl -X POST http://localhost:8787/webhook \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Telegram-Bot-Api-Secret-Token: test" \
|
|
-d '{"message":{"chat":{"id":123},"from":{"id":123},"text":"잔액"}}'
|
|
```
|
|
|
|
### Task 4.3: 문서 업데이트
|
|
|
|
**Files:**
|
|
- Modify: `CLAUDE.md`
|
|
- Modify: `README.md`
|
|
|
|
파일 구조, 에이전트 설명 업데이트
|
|
|
|
### Task 4.4: 최종 커밋 및 배포
|
|
|
|
```bash
|
|
git add .
|
|
git commit -m "feat: complete domain and deposit agent refactoring"
|
|
|
|
# 프로덕션 마이그레이션 (주의!)
|
|
wrangler d1 execute telegram-conversations --file=migrations/006_add_agent_sessions.sql
|
|
|
|
# 배포
|
|
npm run deploy
|
|
```
|
|
|
|
---
|
|
|
|
## 위험 요소 체크리스트
|
|
|
|
- [ ] 마이그레이션 전 D1 백업
|
|
- [ ] 기존 domain-tool.ts 테스트가 통과하는지 확인
|
|
- [ ] 기존 deposit-agent.ts 테스트가 통과하는지 확인
|
|
- [ ] 세션 TTL 만료 후 정상 동작 확인
|
|
- [ ] `__PASSTHROUGH__` 동작 확인
|
|
- [ ] Circuit Breaker 동작 확인
|