// src/main/services/OnlineLLMService.ts // C# .NET Backend API Server 연결 서비스. // 온라인 모드 사용 시 필수 인증(JWT Bearer)을 통해 서버로 AI 요청 전달. import { EventEmitter } from 'events' import { getLogger } from './LoggerService' import { configGet, configSet } from './ConfigService' import { D3ROError, ErrorCode } from '@d3ro/core/errors' import type { LLMAction } from '@d3ro/core/types' import { resolveSystemPrompt } from './llm-prompts' const logger = getLogger('OnlineLLMService') interface OnlineGenerateOptions { model?: string temperature?: number maxTokens?: number systemPrompt?: string } interface OnlineGenerateResponse { text: string model: string promptTokens: number completionTokens: number totalDurationMs: number cost: number } class OnlineLLMService extends EventEmitter { private _abortController: AbortController | null = null private _disposed = false isAvailable(): boolean { const token = configGet('authToken') return Boolean(token && token.length > 0) } private _ensureAuth(): string { if (this._disposed) { throw new D3ROError(ErrorCode.LLMProcessingFailed, 'OnlineLLMService disposed') } const token = configGet('authToken') if (!token || token.length === 0) { throw new D3ROError( ErrorCode.LLMServerUnreachable, '온라인 모드를 이용하기 위해서는 로그인이 꼭 필요합니다.' ) } return token } async processText( text: string, action: LLMAction, targetLanguage?: string, customPrompt?: string ): Promise { const token = this._ensureAuth() const apiUrl = configGet('onlineApiUrl') ?? 'http://localhost:5000' const systemPrompt = resolveSystemPrompt(action, targetLanguage, customPrompt) try { const response = await fetch(`${apiUrl}/api/llm/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ prompt: text, systemPrompt, model: configGet('llmModelId') ?? 'd3ro-gpt4o-mini', temperature: 0.7, maxTokens: 2048 }) }) if (response.status === 401) { configSet('authToken', null) throw new D3ROError( ErrorCode.LLMServerUnreachable, '인증 토큰이 만료되었습니다. 다시 로그인해주세요.' ) } if (!response.ok) { throw new D3ROError( ErrorCode.LLMProcessingFailed, `API Server responded with status ${response.status}` ) } const data = (await response.json()) as OnlineGenerateResponse if (!data.text || !data.text.trim()) { throw new D3ROError(ErrorCode.LLMProcessingFailed, 'Online LLM returned empty text') } return data.text.trim() } catch (err) { if (err instanceof D3ROError) throw err throw new D3ROError( ErrorCode.LLMProcessingFailed, `온라인 API 서버 연결 실패: ${err instanceof Error ? err.message : String(err)}` ) } } async *chatStream( messages: Array<{ role: string; content: string }>, options?: { model?: string; temperature?: number } ): AsyncGenerator { const token = this._ensureAuth() const apiUrl = configGet('onlineApiUrl') ?? 'http://localhost:5000' const response = await fetch(`${apiUrl}/api/llm/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ messages: messages.map((m) => ({ role: m.role, content: m.content })), model: options?.model ?? configGet('llmModelId') ?? 'd3ro-gpt4o-mini', temperature: options?.temperature ?? 0.7 }) }) if (!response.ok) { throw new D3ROError(ErrorCode.LLMProcessingFailed, `Online API Error: ${response.status}`) } const data = (await response.json()) as OnlineGenerateResponse yield data.text return data.text } cancelGeneration(): void { if (this._abortController) { this._abortController.abort() this._abortController = null } } dispose(): void { this._disposed = true this.cancelGeneration() this.removeAllListeners() } } let instance: OnlineLLMService | null = null export function resetOnlineLLMServiceForTests(): void { if (instance) instance.removeAllListeners() instance = null } export function getOnlineLLMService(): OnlineLLMService { if (!instance) { instance = new OnlineLLMService() } return instance }