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
163 lines
4.6 KiB
TypeScript
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
|
|
}
|