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 type { AgentToolContext } from './base-agent';
|
||||||
import { BaseAgent } from './base-agent';
|
import { BaseAgent } from './base-agent';
|
||||||
|
import { DiagramAgent } from './diagram-agent';
|
||||||
import { SessionManager } from '../utils/session-manager';
|
import { SessionManager } from '../utils/session-manager';
|
||||||
import { getSessionConfig } from '../constants/agent-config';
|
import { getSessionConfig } from '../constants/agent-config';
|
||||||
import { getD2RenderUrl } from '../utils/api-urls';
|
|
||||||
import { createLogger } from '../utils/logger';
|
import { createLogger } from '../utils/logger';
|
||||||
|
|
||||||
const config = getSessionConfig('onboarding');
|
const config = getSessionConfig('onboarding');
|
||||||
@@ -54,7 +54,7 @@ export class OnboardingAgent extends BaseAgent<OnboardingSession> {
|
|||||||
3. 기술 요구사항 파악 (트래픽 규모, 필요 스펙, 보안 요구사항)
|
3. 기술 요구사항 파악 (트래픽 규모, 필요 스펙, 보안 요구사항)
|
||||||
4. 예산 범위 확인
|
4. 예산 범위 확인
|
||||||
5. 적절한 서비스 조합 추천
|
5. 적절한 서비스 조합 추천
|
||||||
6. 필요 시 D2 다이어그램으로 아키텍처 시각화
|
6. 필요 시 Mermaid 다이어그램으로 아키텍처 시각화
|
||||||
|
|
||||||
## 현재 세션 상태: ${session.status}
|
## 현재 세션 상태: ${session.status}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export class OnboardingAgent extends BaseAgent<OnboardingSession> {
|
|||||||
- 상담이 자연스럽게 종료되면 __SESSION_END__를 응답 끝에 추가하세요.
|
- 상담이 자연스럽게 종료되면 __SESSION_END__를 응답 끝에 추가하세요.
|
||||||
|
|
||||||
## 도구 사용
|
## 도구 사용
|
||||||
- 아키텍처 설명 시 render_d2로 다이어그램을 생성할 수 있습니다.
|
- 아키텍처 설명 시 generate_diagram으로 Mermaid 다이어그램을 생성하여 고객에게 전송할 수 있습니다.
|
||||||
- 서비스 관련 질문에 search_knowledge로 지식 베이스를 검색할 수 있습니다.`;
|
- 서비스 관련 질문에 search_knowledge로 지식 베이스를 검색할 수 있습니다.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,22 +76,22 @@ export class OnboardingAgent extends BaseAgent<OnboardingSession> {
|
|||||||
{
|
{
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: 'render_d2',
|
name: 'generate_diagram',
|
||||||
description: '아키텍처 다이어그램을 D2 언어로 렌더링합니다. 서버 구성, 네트워크 토폴로지, 서비스 아키텍처를 시각화할 때 사용합니다.',
|
description: '아키텍처 다이어그램을 Mermaid로 생성하여 고객에게 전송합니다. 서버 구성, 네트워크 토폴로지, 서비스 아키텍처를 시각화할 때 사용합니다.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
source: {
|
description: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'D2 다이어그램 소스 코드',
|
description: '다이어그램에 표현할 아키텍처 또는 상황 설명',
|
||||||
},
|
},
|
||||||
format: {
|
diagram_type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['svg', 'png'],
|
enum: ['flow', 'sequence', 'architecture'],
|
||||||
description: '출력 형식 (기본값: svg)',
|
description: '다이어그램 유형 (선택, 기본값: architecture)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['source'],
|
required: ['description'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -127,8 +127,12 @@ export class OnboardingAgent extends BaseAgent<OnboardingSession> {
|
|||||||
context: AgentToolContext
|
context: AgentToolContext
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'render_d2':
|
case 'generate_diagram':
|
||||||
return this.handleRenderD2(args as unknown as RenderD2Args, context.env);
|
return this.handleGenerateDiagram(
|
||||||
|
args.description as string,
|
||||||
|
args.diagram_type as string | undefined,
|
||||||
|
context
|
||||||
|
);
|
||||||
case 'search_knowledge':
|
case 'search_knowledge':
|
||||||
return this.handleSearchKnowledge(
|
return this.handleSearchKnowledge(
|
||||||
args as { query: string; category?: string },
|
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> {
|
private async handleGenerateDiagram(
|
||||||
try {
|
description: string,
|
||||||
const renderUrl = getD2RenderUrl(env);
|
diagramType: string | undefined,
|
||||||
const response = await fetch(`${renderUrl}/render`, {
|
context: AgentToolContext
|
||||||
method: 'POST',
|
): Promise<string> {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
if (!context.chatId) {
|
||||||
body: JSON.stringify({
|
return JSON.stringify({ error: '채팅 정보가 없어 다이어그램을 전송할 수 없습니다.' });
|
||||||
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({
|
const diagramAgent = new DiagramAgent();
|
||||||
success: true,
|
const result = await diagramAgent.generateAndSend(
|
||||||
message: '아키텍처 다이어그램이 생성되었습니다.',
|
{
|
||||||
format: args.format || 'svg',
|
env: context.env,
|
||||||
});
|
chatId: context.chatId,
|
||||||
} catch (error) {
|
telegramUserId: context.userId,
|
||||||
logger.error('D2 렌더링 오류', error as Error);
|
messageId: context.messageId,
|
||||||
return JSON.stringify({ error: 'D2 렌더링 서비스에 연결할 수 없습니다.' });
|
},
|
||||||
}
|
description,
|
||||||
|
diagramType
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSON.stringify(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleSearchKnowledge(
|
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 { manageWalletTool, executeManageWallet } from './wallet-tool';
|
||||||
import { manageServerTool, executeManageServer } from './server-tool';
|
import { manageServerTool, executeManageServer } from './server-tool';
|
||||||
import { checkServiceTool, executeCheckService } from './service-tool';
|
import { checkServiceTool, executeCheckService } from './service-tool';
|
||||||
import { renderD2Tool, executeRenderD2 } from './d2-tool';
|
|
||||||
import { adminTool, executeAdmin } from './admin-tool';
|
import { adminTool, executeAdmin } from './admin-tool';
|
||||||
import { searchKnowledgeTool, executeSearchKnowledge } from './knowledge-tool';
|
import { searchKnowledgeTool, executeSearchKnowledge } from './knowledge-tool';
|
||||||
|
|
||||||
@@ -45,11 +44,6 @@ const CheckServiceArgsSchema = z.object({
|
|||||||
service_id: z.number().int().positive().optional(),
|
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({
|
const AdminArgsSchema = z.object({
|
||||||
action: z.enum([
|
action: z.enum([
|
||||||
'block_user', 'unblock_user', 'set_role', 'broadcast',
|
'block_user', 'unblock_user', 'set_role', 'broadcast',
|
||||||
@@ -109,7 +103,6 @@ const toolExecutors: Record<
|
|||||||
manage_wallet: createValidatedExecutor(ManageWalletArgsSchema, executeManageWallet, 'wallet'),
|
manage_wallet: createValidatedExecutor(ManageWalletArgsSchema, executeManageWallet, 'wallet'),
|
||||||
manage_server: createValidatedExecutor(ManageServerArgsSchema, executeManageServer, 'server'),
|
manage_server: createValidatedExecutor(ManageServerArgsSchema, executeManageServer, 'server'),
|
||||||
check_service: createValidatedExecutor(CheckServiceArgsSchema, executeCheckService, 'service'),
|
check_service: createValidatedExecutor(CheckServiceArgsSchema, executeCheckService, 'service'),
|
||||||
render_d2: createValidatedExecutor(RenderD2ArgsSchema, executeRenderD2, 'd2'),
|
|
||||||
admin: createValidatedExecutor(AdminArgsSchema, executeAdmin, 'admin'),
|
admin: createValidatedExecutor(AdminArgsSchema, executeAdmin, 'admin'),
|
||||||
search_knowledge: createValidatedExecutor(SearchKnowledgeArgsSchema, executeSearchKnowledge, 'knowledge'),
|
search_knowledge: createValidatedExecutor(SearchKnowledgeArgsSchema, executeSearchKnowledge, 'knowledge'),
|
||||||
};
|
};
|
||||||
@@ -123,7 +116,6 @@ export const tools: ToolDefinition[] = [
|
|||||||
manageWalletTool,
|
manageWalletTool,
|
||||||
manageServerTool,
|
manageServerTool,
|
||||||
checkServiceTool,
|
checkServiceTool,
|
||||||
renderD2Tool,
|
|
||||||
adminTool,
|
adminTool,
|
||||||
searchKnowledgeTool,
|
searchKnowledgeTool,
|
||||||
];
|
];
|
||||||
@@ -205,6 +197,5 @@ export { manageDomainTool } from './domain-tool';
|
|||||||
export { manageWalletTool } from './wallet-tool';
|
export { manageWalletTool } from './wallet-tool';
|
||||||
export { manageServerTool } from './server-tool';
|
export { manageServerTool } from './server-tool';
|
||||||
export { checkServiceTool } from './service-tool';
|
export { checkServiceTool } from './service-tool';
|
||||||
export { renderD2Tool } from './d2-tool';
|
|
||||||
export { adminTool } from './admin-tool';
|
export { adminTool } from './admin-tool';
|
||||||
export { searchKnowledgeTool } from './knowledge-tool';
|
export { searchKnowledgeTool } from './knowledge-tool';
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { Env } from '../types';
|
import type { Env } from '../types';
|
||||||
|
|
||||||
const DEFAULT_OPENAI_GATEWAY = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-ai-support/openai';
|
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 경유)
|
* 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;
|
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)
|
// API URLs (optional, have defaults in wrangler.toml)
|
||||||
OPENAI_API_BASE: z.string().url().optional(),
|
OPENAI_API_BASE: z.string().url().optional(),
|
||||||
D2_RENDER_URL: z.string().url().optional(),
|
|
||||||
NAMECHEAP_API_URL: z.string().url().optional(),
|
NAMECHEAP_API_URL: z.string().url().optional(),
|
||||||
WHOIS_API_URL: z.string().url().optional(),
|
WHOIS_API_URL: z.string().url().optional(),
|
||||||
CLOUD_ORCHESTRATOR_URL: z.string().url().optional(),
|
CLOUD_ORCHESTRATOR_URL: z.string().url().optional(),
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ binding = "AI"
|
|||||||
ENVIRONMENT = "production"
|
ENVIRONMENT = "production"
|
||||||
# AI Gateway 경유
|
# AI Gateway 경유
|
||||||
OPENAI_API_BASE = "https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-ai-support/openai"
|
OPENAI_API_BASE = "https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-ai-support/openai"
|
||||||
# D2 렌더링 서비스 (jp1 Incus)
|
# Kroki 다이어그램 서비스 (Mermaid/D2 등 → HAProxy)
|
||||||
D2_RENDER_URL = "http://10.253.100.107:8080"
|
KROKI_URL = "https://kroki.anvil.it.com"
|
||||||
# 외부 API
|
# 외부 API
|
||||||
NAMECHEAP_API_URL = "https://namecheap-api.anvil.it.com"
|
NAMECHEAP_API_URL = "https://namecheap-api.anvil.it.com"
|
||||||
WHOIS_API_URL = "https://whois-api-kappa-inoutercoms-projects.vercel.app"
|
WHOIS_API_URL = "https://whois-api-kappa-inoutercoms-projects.vercel.app"
|
||||||
@@ -58,6 +58,17 @@ index_name = "knowledge-embeddings"
|
|||||||
[triggers]
|
[triggers]
|
||||||
crons = ["0 15 * * *", "*/5 * * * *", "0 * * * *"]
|
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):
|
# Secrets (wrangler secret put):
|
||||||
# - BOT_TOKEN: Telegram Bot Token
|
# - BOT_TOKEN: Telegram Bot Token
|
||||||
# - WEBHOOK_SECRET: Webhook 검증용 시크릿
|
# - WEBHOOK_SECRET: Webhook 검증용 시크릿
|
||||||
|
|||||||
Reference in New Issue
Block a user