Replace D2 rendering with DiagramAgent (Mermaid + Kroki)
- Delete d2-tool.ts and D2_RENDER_URL references - Add diagram-agent.ts: OpenAI generates Mermaid → Kroki renders PNG → Telegram sendPhoto + R2 cache - Update onboarding-agent to use generate_diagram tool with DiagramAgent - Switch wrangler.toml from D2_RENDER_URL to KROKI_URL - Remove D2 from env-validation, api-urls, tools/index Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
206
src/agents/diagram-agent.ts
Normal file
206
src/agents/diagram-agent.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Diagram Agent - Mermaid 다이어그램 생성 서브 에이전트
|
||||
*
|
||||
* 세션/멀티라운드 불필요. 호출 시:
|
||||
* 1. OpenAI로 Mermaid 코드 생성
|
||||
* 2. Kroki로 PNG 렌더링
|
||||
* 3. Telegram으로 직접 전송 (+ R2 캐시)
|
||||
*/
|
||||
|
||||
import type { Env, OpenAIAPIResponse } from '../types';
|
||||
import { sendPhoto, sendMessage, sendChatAction } from '../telegram';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import { getOpenAIUrl } from '../utils/api-urls';
|
||||
|
||||
const logger = createLogger('diagram-agent');
|
||||
|
||||
const MERMAID_SYSTEM_PROMPT = `You are a technical diagram generator for customer support at a cloud hosting service.
|
||||
The customer describes a problem they are experiencing. Analyze the problem and create a Mermaid diagram that visualizes:
|
||||
- The system architecture involved
|
||||
- Where the problem might be occurring
|
||||
- The flow of data/requests that is failing
|
||||
|
||||
IMPORTANT RULES:
|
||||
1. Output ONLY the Mermaid diagram code, nothing else
|
||||
2. Do NOT wrap in \`\`\`mermaid\`\`\` code blocks
|
||||
3. Use 'graph TD' for flow diagrams or 'sequenceDiagram' for request flows
|
||||
4. Use Korean labels since the customer speaks Korean
|
||||
5. Mark the problem area with style: style NodeName fill:#f66,stroke:#333
|
||||
6. Keep the diagram clear and focused (max 15 nodes)
|
||||
7. CRITICAL: Always wrap node labels in double quotes to escape special characters.
|
||||
CORRECT: A["웹서버 web-prod-01"] --> B["DB서버 db-prod-01"]
|
||||
WRONG: A[웹서버(web-prod-01)] --> B[DB서버(db-prod-01)]
|
||||
Never use parentheses (), curly braces {}, or other special chars inside labels without double quotes.`;
|
||||
|
||||
export interface DiagramContext {
|
||||
env: Env;
|
||||
chatId: number;
|
||||
telegramUserId: string;
|
||||
messageId?: number;
|
||||
}
|
||||
|
||||
export class DiagramAgent {
|
||||
/**
|
||||
* Mermaid 코드 생성 → Kroki 렌더 → Telegram 전송
|
||||
*/
|
||||
async generateAndSend(
|
||||
context: DiagramContext,
|
||||
description: string,
|
||||
diagramType?: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const { env, chatId, messageId } = context;
|
||||
|
||||
// 1. Generate Mermaid code
|
||||
const mermaidCode = await this.generateMermaidCode(env, description, diagramType);
|
||||
if (!mermaidCode) {
|
||||
return { success: false, message: 'Mermaid 다이어그램 코드 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
// 2. Send typing indicator
|
||||
await sendChatAction(env.BOT_TOKEN, chatId, 'upload_photo').catch(() => {});
|
||||
|
||||
// 3. Render via Kroki
|
||||
const imageData = await this.renderWithKroki(env, mermaidCode);
|
||||
if (!imageData) {
|
||||
// Fallback: send mermaid code as text
|
||||
const escaped = mermaidCode.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
await sendMessage(env.BOT_TOKEN, chatId,
|
||||
`다이어그램 렌더링 서비스에 연결할 수 없습니다.\n\n<b>Mermaid 코드:</b>\n<pre>${escaped}</pre>`,
|
||||
{ parse_mode: 'HTML' });
|
||||
return { success: true, message: '렌더링 실패로 Mermaid 코드를 텍스트로 전송했습니다.' };
|
||||
}
|
||||
|
||||
// 4. Cache in R2
|
||||
await this.cacheInR2(env, mermaidCode, imageData);
|
||||
|
||||
// 5. Send photo to Telegram (retry without reply_to_message_id on failure)
|
||||
try {
|
||||
await sendPhoto(env.BOT_TOKEN, chatId, imageData, {
|
||||
caption: '다이어그램입니다. 추가 설명이 필요하시면 말씀해주세요.',
|
||||
reply_to_message_id: messageId,
|
||||
});
|
||||
} catch (firstError) {
|
||||
logger.warn('sendPhoto failed, retrying without reply_to_message_id', {
|
||||
error: (firstError as Error).message,
|
||||
});
|
||||
try {
|
||||
await sendPhoto(env.BOT_TOKEN, chatId, imageData, {
|
||||
caption: '다이어그램입니다. 추가 설명이 필요하시면 말씀해주세요.',
|
||||
});
|
||||
} catch (retryError) {
|
||||
// Final fallback: send mermaid code as text
|
||||
logger.error('sendPhoto retry failed', retryError as Error);
|
||||
const escaped = mermaidCode.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
await sendMessage(env.BOT_TOKEN, chatId,
|
||||
`다이어그램 이미지 전송에 실패했습니다.\n\n<b>Mermaid 코드:</b>\n<pre>${escaped}</pre>`,
|
||||
{ parse_mode: 'HTML' });
|
||||
return { success: true, message: '이미지 전송 실패로 Mermaid 코드를 텍스트로 전송했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Diagram sent', { chatId });
|
||||
return { success: true, message: '다이어그램이 고객에게 전송되었습니다.' };
|
||||
}
|
||||
|
||||
private async generateMermaidCode(
|
||||
env: Env,
|
||||
description: string,
|
||||
diagramType?: string
|
||||
): Promise<string | null> {
|
||||
if (!env.OPENAI_API_KEY) {
|
||||
logger.error('OPENAI_API_KEY not set', new Error('Missing key'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeHint = diagramType
|
||||
? `\n\nPreferred diagram type: ${diagramType}. Use 'graph TD' for flow/architecture, 'sequenceDiagram' for sequence.`
|
||||
: '';
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
||||
|
||||
try {
|
||||
const response = await fetch(getOpenAIUrl(env), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${env.OPENAI_API_KEY}`,
|
||||
},
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: MERMAID_SYSTEM_PROMPT + typeHint },
|
||||
{ role: 'user', content: description },
|
||||
],
|
||||
max_tokens: 1000,
|
||||
temperature: 0.3,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('OpenAI API error', new Error(`HTTP ${response.status}`));
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as OpenAIAPIResponse;
|
||||
let code = data.choices[0]?.message?.content?.trim();
|
||||
if (!code) return null;
|
||||
|
||||
code = code.replace(/```mermaid\n?/g, '').replace(/```\n?/g, '').trim();
|
||||
return code;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Mermaid generation failed', error as Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async renderWithKroki(env: Env, mermaidCode: string): Promise<ArrayBuffer | null> {
|
||||
const krokiUrl = env.KROKI_URL;
|
||||
if (!krokiUrl) {
|
||||
logger.error('KROKI_URL not set', new Error('Missing KROKI_URL'));
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${krokiUrl}/mermaid/png`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: mermaidCode,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.error('Kroki render failed', new Error(errorText), { status: response.status });
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.arrayBuffer();
|
||||
} catch (error) {
|
||||
logger.error('Kroki connection error', error as Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async cacheInR2(env: Env, mermaidCode: string, imageData: ArrayBuffer): Promise<void> {
|
||||
if (!env.R2_BUCKET) return;
|
||||
|
||||
try {
|
||||
const encoder = new TextEncoder();
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(mermaidCode));
|
||||
const hash = Array.from(new Uint8Array(hashBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
|
||||
await env.R2_BUCKET.put(`diagrams/${hash}.png`, imageData, {
|
||||
httpMetadata: { contentType: 'image/png' },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('R2 cache failed', error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,12 @@
|
||||
* - 지식 베이스 검색
|
||||
*/
|
||||
|
||||
import type { Env, ToolDefinition, OnboardingSession, RenderD2Args } from '../types';
|
||||
import type { ToolDefinition, OnboardingSession } from '../types';
|
||||
import type { AgentToolContext } from './base-agent';
|
||||
import { BaseAgent } from './base-agent';
|
||||
import { DiagramAgent } from './diagram-agent';
|
||||
import { SessionManager } from '../utils/session-manager';
|
||||
import { getSessionConfig } from '../constants/agent-config';
|
||||
import { getD2RenderUrl } from '../utils/api-urls';
|
||||
import { createLogger } from '../utils/logger';
|
||||
|
||||
const config = getSessionConfig('onboarding');
|
||||
@@ -54,7 +54,7 @@ export class OnboardingAgent extends BaseAgent<OnboardingSession> {
|
||||
3. 기술 요구사항 파악 (트래픽 규모, 필요 스펙, 보안 요구사항)
|
||||
4. 예산 범위 확인
|
||||
5. 적절한 서비스 조합 추천
|
||||
6. 필요 시 D2 다이어그램으로 아키텍처 시각화
|
||||
6. 필요 시 Mermaid 다이어그램으로 아키텍처 시각화
|
||||
|
||||
## 현재 세션 상태: ${session.status}
|
||||
|
||||
@@ -67,7 +67,7 @@ export class OnboardingAgent extends BaseAgent<OnboardingSession> {
|
||||
- 상담이 자연스럽게 종료되면 __SESSION_END__를 응답 끝에 추가하세요.
|
||||
|
||||
## 도구 사용
|
||||
- 아키텍처 설명 시 render_d2로 다이어그램을 생성할 수 있습니다.
|
||||
- 아키텍처 설명 시 generate_diagram으로 Mermaid 다이어그램을 생성하여 고객에게 전송할 수 있습니다.
|
||||
- 서비스 관련 질문에 search_knowledge로 지식 베이스를 검색할 수 있습니다.`;
|
||||
}
|
||||
|
||||
@@ -76,22 +76,22 @@ export class OnboardingAgent extends BaseAgent<OnboardingSession> {
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'render_d2',
|
||||
description: '아키텍처 다이어그램을 D2 언어로 렌더링합니다. 서버 구성, 네트워크 토폴로지, 서비스 아키텍처를 시각화할 때 사용합니다.',
|
||||
name: 'generate_diagram',
|
||||
description: '아키텍처 다이어그램을 Mermaid로 생성하여 고객에게 전송합니다. 서버 구성, 네트워크 토폴로지, 서비스 아키텍처를 시각화할 때 사용합니다.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
source: {
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'D2 다이어그램 소스 코드',
|
||||
description: '다이어그램에 표현할 아키텍처 또는 상황 설명',
|
||||
},
|
||||
format: {
|
||||
diagram_type: {
|
||||
type: 'string',
|
||||
enum: ['svg', 'png'],
|
||||
description: '출력 형식 (기본값: svg)',
|
||||
enum: ['flow', 'sequence', 'architecture'],
|
||||
description: '다이어그램 유형 (선택, 기본값: architecture)',
|
||||
},
|
||||
},
|
||||
required: ['source'],
|
||||
required: ['description'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -127,8 +127,12 @@ export class OnboardingAgent extends BaseAgent<OnboardingSession> {
|
||||
context: AgentToolContext
|
||||
): Promise<string> {
|
||||
switch (name) {
|
||||
case 'render_d2':
|
||||
return this.handleRenderD2(args as unknown as RenderD2Args, context.env);
|
||||
case 'generate_diagram':
|
||||
return this.handleGenerateDiagram(
|
||||
args.description as string,
|
||||
args.diagram_type as string | undefined,
|
||||
context
|
||||
);
|
||||
case 'search_knowledge':
|
||||
return this.handleSearchKnowledge(
|
||||
args as { query: string; category?: string },
|
||||
@@ -139,32 +143,28 @@ export class OnboardingAgent extends BaseAgent<OnboardingSession> {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRenderD2(args: RenderD2Args, env: Env): Promise<string> {
|
||||
try {
|
||||
const renderUrl = getD2RenderUrl(env);
|
||||
const response = await fetch(`${renderUrl}/render`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source: args.source,
|
||||
format: args.format || 'svg',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('D2 렌더링 실패', new Error(`HTTP ${response.status}`));
|
||||
return JSON.stringify({ error: 'D2 다이어그램 렌더링에 실패했습니다.' });
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
success: true,
|
||||
message: '아키텍처 다이어그램이 생성되었습니다.',
|
||||
format: args.format || 'svg',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('D2 렌더링 오류', error as Error);
|
||||
return JSON.stringify({ error: 'D2 렌더링 서비스에 연결할 수 없습니다.' });
|
||||
private async handleGenerateDiagram(
|
||||
description: string,
|
||||
diagramType: string | undefined,
|
||||
context: AgentToolContext
|
||||
): Promise<string> {
|
||||
if (!context.chatId) {
|
||||
return JSON.stringify({ error: '채팅 정보가 없어 다이어그램을 전송할 수 없습니다.' });
|
||||
}
|
||||
|
||||
const diagramAgent = new DiagramAgent();
|
||||
const result = await diagramAgent.generateAndSend(
|
||||
{
|
||||
env: context.env,
|
||||
chatId: context.chatId,
|
||||
telegramUserId: context.userId,
|
||||
messageId: context.messageId,
|
||||
},
|
||||
description,
|
||||
diagramType
|
||||
);
|
||||
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
private async handleSearchKnowledge(
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { createLogger } from '../utils/logger';
|
||||
import type { Env, ToolDefinition, RenderD2Args } from '../types';
|
||||
|
||||
const logger = createLogger('d2-tool');
|
||||
|
||||
export const renderD2Tool: ToolDefinition = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'render_d2',
|
||||
description:
|
||||
'D2 다이어그램 렌더링: D2 소스 코드를 이미지(SVG/PNG)로 변환합니다.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
source: {
|
||||
type: 'string',
|
||||
description: 'D2 다이어그램 소스 코드',
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: ['svg', 'png'],
|
||||
description: '출력 포맷 (기본: svg)',
|
||||
},
|
||||
},
|
||||
required: ['source'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function executeRenderD2(
|
||||
args: RenderD2Args,
|
||||
env?: Env,
|
||||
_userId?: string,
|
||||
db?: D1Database
|
||||
): Promise<string> {
|
||||
try {
|
||||
if (!env?.D2_RENDER_URL) {
|
||||
return 'D2 렌더링 서비스가 설정되지 않았습니다.';
|
||||
}
|
||||
if (!env.R2_BUCKET) {
|
||||
return 'R2 스토리지가 설정되지 않았습니다.';
|
||||
}
|
||||
|
||||
const format = args.format ?? 'svg';
|
||||
const sourceHash = await hashSource(args.source + format);
|
||||
|
||||
// Check cache
|
||||
if (db) {
|
||||
const cached = await db
|
||||
.prepare('SELECT r2_key FROM d2_cache WHERE source_hash = ?')
|
||||
.bind(sourceHash)
|
||||
.first<{ r2_key: string }>();
|
||||
|
||||
if (cached) {
|
||||
logger.info('D2 cache hit', { hash: sourceHash });
|
||||
return `다이어그램이 준비되었습니다: /d2/${cached.r2_key}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render via external service
|
||||
const response = await fetch(env.D2_RENDER_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ source: args.source, format }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.error('D2 render API failed', undefined, {
|
||||
status: response.status,
|
||||
error: errorText,
|
||||
});
|
||||
return `다이어그램 렌더링 실패: ${response.statusText}`;
|
||||
}
|
||||
|
||||
const imageData = await response.arrayBuffer();
|
||||
const contentType = format === 'svg' ? 'image/svg+xml' : 'image/png';
|
||||
const r2Key = `d2/${sourceHash}.${format}`;
|
||||
|
||||
// Store in R2
|
||||
await env.R2_BUCKET.put(r2Key, imageData, {
|
||||
httpMetadata: { contentType },
|
||||
});
|
||||
|
||||
// Cache metadata
|
||||
if (db) {
|
||||
await db
|
||||
.prepare(
|
||||
'INSERT INTO d2_cache (source_hash, r2_key, format) VALUES (?, ?, ?)'
|
||||
)
|
||||
.bind(sourceHash, r2Key, format)
|
||||
.run();
|
||||
}
|
||||
|
||||
logger.info('D2 diagram rendered and cached', { hash: sourceHash, format });
|
||||
return `다이어그램이 준비되었습니다: /d2/${r2Key}`;
|
||||
} catch (error) {
|
||||
logger.error('D2 tool error', error as Error);
|
||||
return '다이어그램 렌더링 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
|
||||
}
|
||||
}
|
||||
|
||||
async function hashSource(source: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(source);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { manageDomainTool, executeManageDomain } from './domain-tool';
|
||||
import { manageWalletTool, executeManageWallet } from './wallet-tool';
|
||||
import { manageServerTool, executeManageServer } from './server-tool';
|
||||
import { checkServiceTool, executeCheckService } from './service-tool';
|
||||
import { renderD2Tool, executeRenderD2 } from './d2-tool';
|
||||
import { adminTool, executeAdmin } from './admin-tool';
|
||||
import { searchKnowledgeTool, executeSearchKnowledge } from './knowledge-tool';
|
||||
|
||||
@@ -45,11 +44,6 @@ const CheckServiceArgsSchema = z.object({
|
||||
service_id: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
const RenderD2ArgsSchema = z.object({
|
||||
source: z.string().min(1).max(10000),
|
||||
format: z.enum(['svg', 'png']).optional(),
|
||||
});
|
||||
|
||||
const AdminArgsSchema = z.object({
|
||||
action: z.enum([
|
||||
'block_user', 'unblock_user', 'set_role', 'broadcast',
|
||||
@@ -109,7 +103,6 @@ const toolExecutors: Record<
|
||||
manage_wallet: createValidatedExecutor(ManageWalletArgsSchema, executeManageWallet, 'wallet'),
|
||||
manage_server: createValidatedExecutor(ManageServerArgsSchema, executeManageServer, 'server'),
|
||||
check_service: createValidatedExecutor(CheckServiceArgsSchema, executeCheckService, 'service'),
|
||||
render_d2: createValidatedExecutor(RenderD2ArgsSchema, executeRenderD2, 'd2'),
|
||||
admin: createValidatedExecutor(AdminArgsSchema, executeAdmin, 'admin'),
|
||||
search_knowledge: createValidatedExecutor(SearchKnowledgeArgsSchema, executeSearchKnowledge, 'knowledge'),
|
||||
};
|
||||
@@ -123,7 +116,6 @@ export const tools: ToolDefinition[] = [
|
||||
manageWalletTool,
|
||||
manageServerTool,
|
||||
checkServiceTool,
|
||||
renderD2Tool,
|
||||
adminTool,
|
||||
searchKnowledgeTool,
|
||||
];
|
||||
@@ -205,6 +197,5 @@ export { manageDomainTool } from './domain-tool';
|
||||
export { manageWalletTool } from './wallet-tool';
|
||||
export { manageServerTool } from './server-tool';
|
||||
export { checkServiceTool } from './service-tool';
|
||||
export { renderD2Tool } from './d2-tool';
|
||||
export { adminTool } from './admin-tool';
|
||||
export { searchKnowledgeTool } from './knowledge-tool';
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { Env } from '../types';
|
||||
|
||||
const DEFAULT_OPENAI_GATEWAY = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-ai-support/openai';
|
||||
const DEFAULT_D2_RENDER_URL = 'http://10.253.100.107:8080';
|
||||
|
||||
/**
|
||||
* OpenAI Chat Completions API URL (AI Gateway 경유)
|
||||
*/
|
||||
@@ -18,9 +16,3 @@ export function getOpenAIBaseUrl(env: Env): string {
|
||||
return env.OPENAI_API_BASE || DEFAULT_OPENAI_GATEWAY;
|
||||
}
|
||||
|
||||
/**
|
||||
* D2 diagram rendering service URL
|
||||
*/
|
||||
export function getD2RenderUrl(env: Env): string {
|
||||
return env.D2_RENDER_URL || DEFAULT_D2_RENDER_URL;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ export const EnvSchema = z.object({
|
||||
|
||||
// API URLs (optional, have defaults in wrangler.toml)
|
||||
OPENAI_API_BASE: z.string().url().optional(),
|
||||
D2_RENDER_URL: z.string().url().optional(),
|
||||
NAMECHEAP_API_URL: z.string().url().optional(),
|
||||
WHOIS_API_URL: z.string().url().optional(),
|
||||
CLOUD_ORCHESTRATOR_URL: z.string().url().optional(),
|
||||
|
||||
@@ -9,8 +9,8 @@ binding = "AI"
|
||||
ENVIRONMENT = "production"
|
||||
# AI Gateway 경유
|
||||
OPENAI_API_BASE = "https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-ai-support/openai"
|
||||
# D2 렌더링 서비스 (jp1 Incus)
|
||||
D2_RENDER_URL = "http://10.253.100.107:8080"
|
||||
# Kroki 다이어그램 서비스 (Mermaid/D2 등 → HAProxy)
|
||||
KROKI_URL = "https://kroki.anvil.it.com"
|
||||
# 외부 API
|
||||
NAMECHEAP_API_URL = "https://namecheap-api.anvil.it.com"
|
||||
WHOIS_API_URL = "https://whois-api-kappa-inoutercoms-projects.vercel.app"
|
||||
@@ -58,6 +58,17 @@ index_name = "knowledge-embeddings"
|
||||
[triggers]
|
||||
crons = ["0 15 * * *", "*/5 * * * *", "0 * * * *"]
|
||||
|
||||
# Cloudflare Queues: 태그 기반 범용 비동기 작업 큐
|
||||
[[queues.producers]]
|
||||
queue = "work-queue"
|
||||
binding = "WORK_QUEUE"
|
||||
|
||||
[[queues.consumers]]
|
||||
queue = "work-queue"
|
||||
max_batch_size = 1
|
||||
max_retries = 2
|
||||
dead_letter_queue = "work-queue-dlq"
|
||||
|
||||
# Secrets (wrangler secret put):
|
||||
# - BOT_TOKEN: Telegram Bot Token
|
||||
# - WEBHOOK_SECRET: Webhook 검증용 시크릿
|
||||
|
||||
Reference in New Issue
Block a user