d3ro-voice/apps/desktop/src/main/services/OnlineLLMService.ts
Yun Chan 708e20f747
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
2026-08-20 11:12:05 +09:00

163 lines
4.6 KiB
TypeScript

// 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<string> {
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<string, string> {
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
}