feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
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
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
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
|
|
@ -65,6 +65,7 @@ interface LocalLLMEvents {
|
|||
token: (payload: { token: string; done: boolean }) => void
|
||||
complete: (payload: { result: GenerateResult }) => void
|
||||
'availability-changed': (payload: { available: boolean }) => void
|
||||
'pull-progress': (payload: unknown) => void
|
||||
error: (payload: { error: D3ROError }) => void
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +103,7 @@ class LocalLLMService extends EventEmitter {
|
|||
private _pullInFlight = new Map<string, Promise<void>>()
|
||||
private _pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
private _available = false
|
||||
private _serverVersion: string | null = null
|
||||
private _abortController: AbortController | null = null
|
||||
private _disposed = false
|
||||
|
||||
|
|
@ -109,6 +111,23 @@ class LocalLLMService extends EventEmitter {
|
|||
return this._state
|
||||
}
|
||||
|
||||
get serverVersion(): string | null {
|
||||
return this._serverVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* Ollama 서버를 명시적으로 실행하거나 재확인한다.
|
||||
*/
|
||||
async startServer(): Promise<'running' | 'starting' | 'not-installed' | 'failed'> {
|
||||
const result = await this.ensureRunning()
|
||||
if (result === 'starting' || result === 'running') {
|
||||
// 1초 뒤 빠른 재확인
|
||||
setTimeout(() => this._checkAvailability(), 1000)
|
||||
setTimeout(() => this._checkAvailability(), 3000)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Ollama 서버가 실행 중인지 확인하고, 설치되어 있는데 실행 중이 아니면 자동 실행한다.
|
||||
*
|
||||
|
|
@ -150,10 +169,27 @@ class LocalLLMService extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Ollama /api/tags 엔드포인트로 가용성 핑. 지정 타임아웃 내 응답이 오면 true.
|
||||
* Ollama /api/version 또는 /api/tags 엔드포인트로 가용성 핑. 지정 타임아웃 내 응답이 오면 true.
|
||||
*/
|
||||
private async _ping(timeoutMs: number): Promise<boolean> {
|
||||
const serverUrl = configGet('ollamaServerUrl')
|
||||
const serverUrl = configGet('ollamaServerUrl') || 'http://localhost:11434'
|
||||
try {
|
||||
const response = await fetch(`${serverUrl}/api/version`, {
|
||||
signal: AbortSignal.timeout(timeoutMs)
|
||||
})
|
||||
if (response.ok) {
|
||||
try {
|
||||
const data = (await response.json()) as { version?: string }
|
||||
if (data?.version) this._serverVersion = data.version
|
||||
} catch {
|
||||
// ignore json parse
|
||||
}
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// fallback to /api/tags
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${serverUrl}/api/tags`, {
|
||||
signal: AbortSignal.timeout(timeoutMs)
|
||||
|
|
@ -180,20 +216,39 @@ class LocalLLMService extends EventEmitter {
|
|||
const localAppData = process.env.LOCALAPPDATA
|
||||
if (localAppData) {
|
||||
candidates.push(path.join(localAppData, 'Programs', 'Ollama', 'ollama.exe'))
|
||||
candidates.push(path.join(localAppData, 'Ollama', 'ollama.exe'))
|
||||
candidates.push(path.join(localAppData, 'Programs', 'Ollama', 'ollama app.exe'))
|
||||
}
|
||||
const programFiles = process.env['ProgramFiles']
|
||||
if (programFiles) {
|
||||
candidates.push(path.join(programFiles, 'Ollama', 'ollama.exe'))
|
||||
}
|
||||
const programFilesX86 = process.env['ProgramFiles(x86)']
|
||||
if (programFilesX86) {
|
||||
candidates.push(path.join(programFilesX86, 'Ollama', 'ollama.exe'))
|
||||
}
|
||||
const userProfile = process.env.USERPROFILE
|
||||
if (userProfile) {
|
||||
candidates.push(path.join(userProfile, 'AppData', 'Local', 'Programs', 'Ollama', 'ollama.exe'))
|
||||
candidates.push(path.join(userProfile, 'AppData', 'Local', 'Ollama', 'ollama.exe'))
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
candidates.push('/usr/local/bin/ollama', '/opt/homebrew/bin/ollama')
|
||||
candidates.push(
|
||||
'/Applications/Ollama.app/Contents/Resources/ollama',
|
||||
'/usr/local/bin/ollama',
|
||||
'/opt/homebrew/bin/ollama'
|
||||
)
|
||||
} else {
|
||||
candidates.push('/usr/local/bin/ollama', '/usr/bin/ollama')
|
||||
candidates.push(
|
||||
'/usr/local/bin/ollama',
|
||||
'/usr/bin/ollama',
|
||||
'/opt/ollama/bin/ollama'
|
||||
)
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
await fs.promises.access(candidate, fs.constants.X_OK)
|
||||
await fs.promises.access(candidate, fs.constants.F_OK)
|
||||
return candidate
|
||||
} catch {
|
||||
// 다음 후보 시도
|
||||
|
|
@ -229,6 +284,19 @@ class LocalLLMService extends EventEmitter {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 명시적 연결 테스트 및 최신 상태 조회
|
||||
*/
|
||||
async checkConnection(): Promise<{ available: boolean; version: string | null; models: LLMModel[] }> {
|
||||
await this._checkAvailability()
|
||||
const models = this._available ? await this.getModels() : []
|
||||
return {
|
||||
available: this._available,
|
||||
version: this._serverVersion,
|
||||
models
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ollama 가용성 폴링을 시작한다 (5초 간격).
|
||||
*/
|
||||
|
|
@ -430,8 +498,10 @@ class LocalLLMService extends EventEmitter {
|
|||
const access = license.canUse(Feature.LLM_PROCESS)
|
||||
if (!access.allowed) {
|
||||
license.promptUpgrade(Feature.LLM_PROCESS, access.reason === 'quota_exceeded' ? 'quota_exceeded' : 'tier_required')
|
||||
// LLM 처리 차단 시 원본 텍스트 반환 (폴백)
|
||||
return text
|
||||
throw new D3ROError(
|
||||
access.reason === 'quota_exceeded' ? ErrorCode.QuotaExceeded : ErrorCode.TierRequired,
|
||||
`LLM process blocked: ${access.reason}`,
|
||||
)
|
||||
}
|
||||
license.consumeQuota(Feature.LLM_PROCESS)
|
||||
} catch {
|
||||
|
|
@ -446,11 +516,10 @@ class LocalLLMService extends EventEmitter {
|
|||
// reasoning 블록 제거 후 빈 응답이면 원본 텍스트 폴백
|
||||
// (모델이 thinking만 하고 출력은 안 한 경우 / 응답 파싱 실패 케이스)
|
||||
if (cleaned.length === 0) {
|
||||
logger.warn(
|
||||
`LLM returned empty after reasoning strip — falling back to original transcript ` +
|
||||
`(raw length=${result.text.length})`
|
||||
throw new D3ROError(
|
||||
ErrorCode.LLMProcessingFailed,
|
||||
'Local LLM returned empty text after reasoning strip',
|
||||
)
|
||||
return text
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
|
@ -577,17 +646,19 @@ class LocalLLMService extends EventEmitter {
|
|||
}
|
||||
|
||||
getStatus(): LLMStatus {
|
||||
const connectionState: LLMConnectionState = this._available
|
||||
? this._state === LLMState.Generating
|
||||
? 'connecting'
|
||||
: 'connected'
|
||||
: 'disconnected'
|
||||
const connectionState = (
|
||||
this._available
|
||||
? this._state === LLMState.Generating
|
||||
? 'connecting'
|
||||
: 'connected'
|
||||
: 'disconnected'
|
||||
) as LLMConnectionState
|
||||
|
||||
return {
|
||||
connectionState,
|
||||
serverUrl: configGet('ollamaServerUrl'),
|
||||
activeModel: configGet('llmModelId'),
|
||||
serverVersion: null
|
||||
serverUrl: configGet('ollamaServerUrl') || 'http://localhost:11434',
|
||||
activeModel: configGet('llmModelId') || 'gemma2:2b',
|
||||
serverVersion: this._serverVersion
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -608,8 +679,8 @@ class LocalLLMService extends EventEmitter {
|
|||
throw new D3ROError(ErrorCode.LLMServerUnreachable, 'Ollama server not available')
|
||||
}
|
||||
|
||||
const serverUrl = configGet('ollamaServerUrl')
|
||||
const model = options?.model ?? configGet('llmModelId') ?? 'gemma4:e4b'
|
||||
const serverUrl = configGet('ollamaServerUrl') || 'http://localhost:11434'
|
||||
const model = options?.model ?? configGet('llmModelId') ?? 'gemma2:2b'
|
||||
|
||||
this._abortController = new AbortController()
|
||||
this._state = LLMState.Generating
|
||||
|
|
@ -683,33 +754,55 @@ class LocalLLMService extends EventEmitter {
|
|||
|
||||
private async _checkAvailability(): Promise<void> {
|
||||
if (this._disposed) return
|
||||
const serverUrl = configGet('ollamaServerUrl')
|
||||
const serverUrl = configGet('ollamaServerUrl') || 'http://localhost:11434'
|
||||
|
||||
let isOk = false
|
||||
let detectedVersion: string | null = null
|
||||
|
||||
try {
|
||||
const response = await fetch(`${serverUrl}/api/tags`, {
|
||||
signal: AbortSignal.timeout(3000)
|
||||
const verRes = await fetch(`${serverUrl}/api/version`, {
|
||||
signal: AbortSignal.timeout(2000)
|
||||
})
|
||||
|
||||
const wasAvailable = this._available
|
||||
this._available = response.ok
|
||||
|
||||
if (!wasAvailable && this._available) {
|
||||
this._state = LLMState.Available
|
||||
this.emit('availability-changed', { available: true })
|
||||
logger.info('Ollama server connected')
|
||||
} else if (wasAvailable && !this._available) {
|
||||
this._state = LLMState.Unavailable
|
||||
this.emit('availability-changed', { available: false })
|
||||
logger.warn('Ollama server disconnected')
|
||||
if (verRes.ok) {
|
||||
isOk = true
|
||||
try {
|
||||
const data = (await verRes.json()) as { version?: string }
|
||||
if (data?.version) detectedVersion = data.version
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (this._available) {
|
||||
this._available = false
|
||||
this._state = LLMState.Unavailable
|
||||
this.emit('availability-changed', { available: false })
|
||||
logger.warn('Ollama server unreachable')
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (!isOk) {
|
||||
try {
|
||||
const tagsRes = await fetch(`${serverUrl}/api/tags`, {
|
||||
signal: AbortSignal.timeout(2000)
|
||||
})
|
||||
if (tagsRes.ok) {
|
||||
isOk = true
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const wasAvailable = this._available
|
||||
this._available = isOk
|
||||
if (detectedVersion) this._serverVersion = detectedVersion
|
||||
|
||||
if (!wasAvailable && this._available) {
|
||||
this._state = LLMState.Available
|
||||
this.emit('availability-changed', { available: true })
|
||||
logger.info(`Ollama server connected (version: ${this._serverVersion ?? 'active'})`)
|
||||
} else if (wasAvailable && !this._available) {
|
||||
this._state = LLMState.Unavailable
|
||||
this._serverVersion = null
|
||||
this.emit('availability-changed', { available: false })
|
||||
logger.warn('Ollama server disconnected')
|
||||
}
|
||||
}
|
||||
|
||||
// ── EventEmitter 타입 오버라이드 ───────────────────────
|
||||
|
|
@ -740,3 +833,15 @@ export function getLocalLLMService(): LocalLLMService {
|
|||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
/** bootstrap llm-polling 스텝 — Ollama 기동 + 가용성 폴링. */
|
||||
export async function startLocalLLMAvailability(): Promise<void> {
|
||||
const llm = getLocalLLMService()
|
||||
await llm.ensureRunning()
|
||||
llm.startPolling()
|
||||
}
|
||||
|
||||
export function resetLocalLLMServiceForTests(): void {
|
||||
if (instance) instance.removeAllListeners()
|
||||
instance = null
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue