remove business logic
This commit is contained in:
parent
e043b85bcd
commit
d62dad6992
@ -1,5 +1,5 @@
|
|||||||
// src/bridge/featureBridge.js
|
// src/bridge/featureBridge.js
|
||||||
const { ipcMain, app, BrowserWindow } = require('electron');
|
const { ipcMain, app } = require('electron');
|
||||||
const settingsService = require('../features/settings/settingsService');
|
const settingsService = require('../features/settings/settingsService');
|
||||||
const authService = require('../features/common/services/authService');
|
const authService = require('../features/common/services/authService');
|
||||||
const whisperService = require('../features/common/services/whisperService');
|
const whisperService = require('../features/common/services/whisperService');
|
||||||
@ -13,12 +13,8 @@ const listenService = require('../features/listen/listenService');
|
|||||||
const permissionService = require('../features/common/services/permissionService');
|
const permissionService = require('../features/common/services/permissionService');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Renderer로부터의 요청을 수신
|
// Renderer로부터의 요청을 수신하고 서비스로 전달
|
||||||
initialize() {
|
initialize() {
|
||||||
|
|
||||||
// 서비스 이벤트 리스너 설정
|
|
||||||
this._setupServiceEventListeners();
|
|
||||||
|
|
||||||
// Settings Service
|
// Settings Service
|
||||||
ipcMain.handle('settings:getPresets', async () => await settingsService.getPresets());
|
ipcMain.handle('settings:getPresets', async () => await settingsService.getPresets());
|
||||||
ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
|
ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
|
||||||
@ -37,14 +33,12 @@ module.exports = {
|
|||||||
ipcMain.handle('get-default-shortcuts', async () => await shortcutsService.handleRestoreDefaults());
|
ipcMain.handle('get-default-shortcuts', async () => await shortcutsService.handleRestoreDefaults());
|
||||||
ipcMain.handle('save-shortcuts', async (event, newKeybinds) => await shortcutsService.handleSaveShortcuts(newKeybinds));
|
ipcMain.handle('save-shortcuts', async (event, newKeybinds) => await shortcutsService.handleSaveShortcuts(newKeybinds));
|
||||||
|
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
|
ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
|
||||||
ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
|
ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
|
||||||
ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
|
ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
|
||||||
ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted());
|
ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted());
|
||||||
ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted());
|
ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted());
|
||||||
|
|
||||||
|
|
||||||
// User/Auth
|
// User/Auth
|
||||||
ipcMain.handle('get-current-user', () => authService.getCurrentUser());
|
ipcMain.handle('get-current-user', () => authService.getCurrentUser());
|
||||||
@ -55,33 +49,7 @@ module.exports = {
|
|||||||
ipcMain.handle('quit-application', () => app.quit());
|
ipcMain.handle('quit-application', () => app.quit());
|
||||||
|
|
||||||
// Whisper
|
// Whisper
|
||||||
ipcMain.handle('whisper:download-model', async (event, modelId) => {
|
ipcMain.handle('whisper:download-model', async (event, modelId) => await whisperService.handleDownloadModel(modelId));
|
||||||
// 개별 진행률 이벤트 처리
|
|
||||||
const progressHandler = (data) => {
|
|
||||||
if (data.modelId === modelId) {
|
|
||||||
event.sender.send('whisper:download-progress', data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const completeHandler = (data) => {
|
|
||||||
if (data.modelId === modelId) {
|
|
||||||
event.sender.send('whisper:download-complete', data);
|
|
||||||
whisperService.removeListener('download-progress', progressHandler);
|
|
||||||
whisperService.removeListener('download-complete', completeHandler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
whisperService.on('download-progress', progressHandler);
|
|
||||||
whisperService.on('download-complete', completeHandler);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await whisperService.handleDownloadModel(modelId);
|
|
||||||
} catch (error) {
|
|
||||||
whisperService.removeListener('download-progress', progressHandler);
|
|
||||||
whisperService.removeListener('download-complete', completeHandler);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ipcMain.handle('whisper:get-installed-models', async () => await whisperService.handleGetInstalledModels());
|
ipcMain.handle('whisper:get-installed-models', async () => await whisperService.handleGetInstalledModels());
|
||||||
|
|
||||||
// General
|
// General
|
||||||
@ -90,86 +58,12 @@ module.exports = {
|
|||||||
|
|
||||||
// Ollama
|
// Ollama
|
||||||
ipcMain.handle('ollama:get-status', async () => await ollamaService.handleGetStatus());
|
ipcMain.handle('ollama:get-status', async () => await ollamaService.handleGetStatus());
|
||||||
ipcMain.handle('ollama:install', async (event) => {
|
ipcMain.handle('ollama:install', async () => await ollamaService.handleInstall());
|
||||||
// 개별 진행률 이벤트 처리
|
ipcMain.handle('ollama:start-service', async () => await ollamaService.handleStartService());
|
||||||
const progressHandler = (data) => {
|
|
||||||
event.sender.send('ollama:install-progress', data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const completeHandler = (data) => {
|
|
||||||
event.sender.send('ollama:install-complete', data);
|
|
||||||
ollamaService.removeListener('install-progress', progressHandler);
|
|
||||||
ollamaService.removeListener('install-complete', completeHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
ollamaService.on('install-progress', progressHandler);
|
|
||||||
ollamaService.on('install-complete', completeHandler);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await ollamaService.handleInstall();
|
|
||||||
} catch (error) {
|
|
||||||
ollamaService.removeListener('install-progress', progressHandler);
|
|
||||||
ollamaService.removeListener('install-complete', completeHandler);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ipcMain.handle('ollama:start-service', async (event) => {
|
|
||||||
// 개별 진행률 이벤트 처리
|
|
||||||
const completeHandler = (data) => {
|
|
||||||
event.sender.send('ollama:install-complete', data);
|
|
||||||
ollamaService.removeListener('install-complete', completeHandler);
|
|
||||||
};
|
|
||||||
|
|
||||||
ollamaService.on('install-complete', completeHandler);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await ollamaService.handleStartService();
|
|
||||||
} catch (error) {
|
|
||||||
ollamaService.removeListener('install-complete', completeHandler);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ipcMain.handle('ollama:ensure-ready', async () => await ollamaService.handleEnsureReady());
|
ipcMain.handle('ollama:ensure-ready', async () => await ollamaService.handleEnsureReady());
|
||||||
ipcMain.handle('ollama:get-models', async () => await ollamaService.handleGetModels());
|
ipcMain.handle('ollama:get-models', async () => await ollamaService.handleGetModels());
|
||||||
ipcMain.handle('ollama:get-model-suggestions', async () => await ollamaService.handleGetModelSuggestions());
|
ipcMain.handle('ollama:get-model-suggestions', async () => await ollamaService.handleGetModelSuggestions());
|
||||||
ipcMain.handle('ollama:pull-model', async (event, modelName) => {
|
ipcMain.handle('ollama:pull-model', async (event, modelName) => await ollamaService.handlePullModel(modelName));
|
||||||
// 개별 진행률 이벤트 처리
|
|
||||||
const progressHandler = (data) => {
|
|
||||||
if (data.model === modelName) {
|
|
||||||
event.sender.send('ollama:pull-progress', data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const completeHandler = (data) => {
|
|
||||||
if (data.model === modelName) {
|
|
||||||
event.sender.send('ollama:pull-complete', data);
|
|
||||||
ollamaService.removeListener('pull-progress', progressHandler);
|
|
||||||
ollamaService.removeListener('pull-complete', completeHandler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const errorHandler = (data) => {
|
|
||||||
if (data.model === modelName) {
|
|
||||||
event.sender.send('ollama:pull-error', data);
|
|
||||||
ollamaService.removeListener('pull-progress', progressHandler);
|
|
||||||
ollamaService.removeListener('pull-complete', completeHandler);
|
|
||||||
ollamaService.removeListener('pull-error', errorHandler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ollamaService.on('pull-progress', progressHandler);
|
|
||||||
ollamaService.on('pull-complete', completeHandler);
|
|
||||||
ollamaService.on('pull-error', errorHandler);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await ollamaService.handlePullModel(modelName);
|
|
||||||
} catch (error) {
|
|
||||||
ollamaService.removeListener('pull-progress', progressHandler);
|
|
||||||
ollamaService.removeListener('pull-complete', completeHandler);
|
|
||||||
ollamaService.removeListener('pull-error', errorHandler);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ipcMain.handle('ollama:is-model-installed', async (event, modelName) => await ollamaService.handleIsModelInstalled(modelName));
|
ipcMain.handle('ollama:is-model-installed', async (event, modelName) => await ollamaService.handleIsModelInstalled(modelName));
|
||||||
ipcMain.handle('ollama:warm-up-model', async (event, modelName) => await ollamaService.handleWarmUpModel(modelName));
|
ipcMain.handle('ollama:warm-up-model', async (event, modelName) => await ollamaService.handleWarmUpModel(modelName));
|
||||||
ipcMain.handle('ollama:auto-warm-up', async () => await ollamaService.handleAutoWarmUp());
|
ipcMain.handle('ollama:auto-warm-up', async () => await ollamaService.handleAutoWarmUp());
|
||||||
@ -204,9 +98,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ModelStateService
|
||||||
|
|
||||||
// ModelStateService
|
|
||||||
ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
|
ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
|
||||||
ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys());
|
ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys());
|
||||||
ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key));
|
ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key));
|
||||||
@ -217,80 +109,9 @@ module.exports = {
|
|||||||
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
|
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
|
||||||
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.log('[FeatureBridge] Initialized with all feature handlers.');
|
console.log('[FeatureBridge] Initialized with all feature handlers.');
|
||||||
},
|
},
|
||||||
|
|
||||||
// 서비스 이벤트 리스너 설정
|
|
||||||
_setupServiceEventListeners() {
|
|
||||||
// Ollama Service 이벤트 리스너
|
|
||||||
ollamaService.on('pull-progress', (data) => {
|
|
||||||
this._broadcastToAllWindows('ollama:pull-progress', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
ollamaService.on('pull-complete', (data) => {
|
|
||||||
this._broadcastToAllWindows('ollama:pull-complete', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
ollamaService.on('pull-error', (data) => {
|
|
||||||
this._broadcastToAllWindows('ollama:pull-error', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
ollamaService.on('download-progress', (data) => {
|
|
||||||
this._broadcastToAllWindows('ollama:download-progress', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
ollamaService.on('download-complete', (data) => {
|
|
||||||
this._broadcastToAllWindows('ollama:download-complete', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
ollamaService.on('download-error', (data) => {
|
|
||||||
this._broadcastToAllWindows('ollama:download-error', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Whisper Service 이벤트 리스너
|
|
||||||
whisperService.on('download-progress', (data) => {
|
|
||||||
this._broadcastToAllWindows('whisper:download-progress', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
whisperService.on('download-complete', (data) => {
|
|
||||||
this._broadcastToAllWindows('whisper:download-complete', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
whisperService.on('download-error', (data) => {
|
|
||||||
this._broadcastToAllWindows('whisper:download-error', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Model State Service 이벤트 리스너
|
|
||||||
modelStateService.on('state-changed', (data) => {
|
|
||||||
this._broadcastToAllWindows('model-state:updated', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelStateService.on('settings-updated', () => {
|
|
||||||
this._broadcastToAllWindows('settings-updated');
|
|
||||||
});
|
|
||||||
|
|
||||||
modelStateService.on('force-show-apikey-header', () => {
|
|
||||||
this._broadcastToAllWindows('force-show-apikey-header');
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[FeatureBridge] Service event listeners configured');
|
|
||||||
},
|
|
||||||
|
|
||||||
// 모든 창에 이벤트 방송
|
|
||||||
_broadcastToAllWindows(eventName, data = null) {
|
|
||||||
BrowserWindow.getAllWindows().forEach(win => {
|
|
||||||
if (win && !win.isDestroyed()) {
|
|
||||||
if (data !== null) {
|
|
||||||
win.webContents.send(eventName, data);
|
|
||||||
} else {
|
|
||||||
win.webContents.send(eventName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Renderer로 상태를 전송
|
// Renderer로 상태를 전송
|
||||||
sendAskProgress(win, progress) {
|
sendAskProgress(win, progress) {
|
||||||
win.webContents.send('feature:ask:progress', progress);
|
win.webContents.send('feature:ask:progress', progress);
|
||||||
|
@ -138,6 +138,8 @@ class AskService {
|
|||||||
console.log('[AskService] Service instance created.');
|
console.log('[AskService] Service instance created.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_broadcastState() {
|
_broadcastState() {
|
||||||
const askWindow = getWindowPool()?.get('ask');
|
const askWindow = getWindowPool()?.get('ask');
|
||||||
if (askWindow && !askWindow.isDestroyed()) {
|
if (askWindow && !askWindow.isDestroyed()) {
|
||||||
@ -381,6 +383,7 @@ class AskService {
|
|||||||
this._broadcastState();
|
this._broadcastState();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[AskService] Failed to parse stream data:', { line: data, error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,79 @@
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
// Request Queue System for Ollama API (only for non-streaming requests)
|
||||||
|
class RequestQueue {
|
||||||
|
constructor() {
|
||||||
|
this.queue = [];
|
||||||
|
this.processing = false;
|
||||||
|
this.streamingActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addStreamingRequest(requestFn) {
|
||||||
|
// Streaming requests have priority - wait for current processing to finish
|
||||||
|
while (this.processing) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.streamingActive = true;
|
||||||
|
console.log('[Ollama Queue] Starting streaming request (priority)');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestFn();
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
this.streamingActive = false;
|
||||||
|
console.log('[Ollama Queue] Streaming request completed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(requestFn) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.queue.push({ requestFn, resolve, reject });
|
||||||
|
this.process();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async process() {
|
||||||
|
if (this.processing || this.queue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait if streaming is active
|
||||||
|
if (this.streamingActive) {
|
||||||
|
setTimeout(() => this.process(), 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
while (this.queue.length > 0) {
|
||||||
|
// Check if streaming started while processing queue
|
||||||
|
if (this.streamingActive) {
|
||||||
|
this.processing = false;
|
||||||
|
setTimeout(() => this.process(), 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { requestFn, resolve, reject } = this.queue.shift();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[Ollama Queue] Processing queued request (${this.queue.length} remaining)`);
|
||||||
|
const result = await requestFn();
|
||||||
|
resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Ollama Queue] Request failed:', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global request queue instance
|
||||||
|
const requestQueue = new RequestQueue();
|
||||||
|
|
||||||
class OllamaProvider {
|
class OllamaProvider {
|
||||||
static async validateApiKey() {
|
static async validateApiKey() {
|
||||||
try {
|
try {
|
||||||
@ -79,71 +152,77 @@ function createLLM({
|
|||||||
}
|
}
|
||||||
messages.push({ role: 'user', content: userContent.join('\n') });
|
messages.push({ role: 'user', content: userContent.join('\n') });
|
||||||
|
|
||||||
try {
|
// Use request queue to prevent concurrent API calls
|
||||||
const response = await fetch(`${baseUrl}/api/chat`, {
|
return await requestQueue.add(async () => {
|
||||||
method: 'POST',
|
try {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const response = await fetch(`${baseUrl}/api/chat`, {
|
||||||
body: JSON.stringify({
|
method: 'POST',
|
||||||
model,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
messages,
|
body: JSON.stringify({
|
||||||
stream: false,
|
model,
|
||||||
options: {
|
messages,
|
||||||
temperature,
|
stream: false,
|
||||||
num_predict: maxTokens,
|
options: {
|
||||||
}
|
temperature,
|
||||||
})
|
num_predict: maxTokens,
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
|
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: {
|
||||||
|
text: () => result.message.content
|
||||||
|
},
|
||||||
|
raw: result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ollama LLM error:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
return {
|
|
||||||
response: {
|
|
||||||
text: () => result.message.content
|
|
||||||
},
|
|
||||||
raw: result
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ollama LLM error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
chat: async (messages) => {
|
chat: async (messages) => {
|
||||||
const ollamaMessages = convertMessagesToOllamaFormat(messages);
|
const ollamaMessages = convertMessagesToOllamaFormat(messages);
|
||||||
|
|
||||||
try {
|
// Use request queue to prevent concurrent API calls
|
||||||
const response = await fetch(`${baseUrl}/api/chat`, {
|
return await requestQueue.add(async () => {
|
||||||
method: 'POST',
|
try {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const response = await fetch(`${baseUrl}/api/chat`, {
|
||||||
body: JSON.stringify({
|
method: 'POST',
|
||||||
model,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
messages: ollamaMessages,
|
body: JSON.stringify({
|
||||||
stream: false,
|
model,
|
||||||
options: {
|
messages: ollamaMessages,
|
||||||
temperature,
|
stream: false,
|
||||||
num_predict: maxTokens,
|
options: {
|
||||||
}
|
temperature,
|
||||||
})
|
num_predict: maxTokens,
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
|
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: result.message.content,
|
||||||
|
raw: result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ollama chat error:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: result.message.content,
|
|
||||||
raw: result
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ollama chat error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -165,89 +244,92 @@ function createStreamingLLM({
|
|||||||
const ollamaMessages = convertMessagesToOllamaFormat(messages);
|
const ollamaMessages = convertMessagesToOllamaFormat(messages);
|
||||||
console.log('[Ollama Provider] Converted messages for Ollama:', ollamaMessages);
|
console.log('[Ollama Provider] Converted messages for Ollama:', ollamaMessages);
|
||||||
|
|
||||||
try {
|
// Streaming requests have priority over queued requests
|
||||||
const response = await fetch(`${baseUrl}/api/chat`, {
|
return await requestQueue.addStreamingRequest(async () => {
|
||||||
method: 'POST',
|
try {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
const response = await fetch(`${baseUrl}/api/chat`, {
|
||||||
body: JSON.stringify({
|
method: 'POST',
|
||||||
model,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
messages: ollamaMessages,
|
body: JSON.stringify({
|
||||||
stream: true,
|
model,
|
||||||
options: {
|
messages: ollamaMessages,
|
||||||
temperature,
|
stream: true,
|
||||||
num_predict: maxTokens,
|
options: {
|
||||||
}
|
temperature,
|
||||||
})
|
num_predict: maxTokens,
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
|
throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[Ollama Provider] Got streaming response');
|
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
|
||||||
async start(controller) {
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
response.body.on('data', (chunk) => {
|
|
||||||
buffer += chunk.toString();
|
|
||||||
const lines = buffer.split('\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim() === '') continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(line);
|
|
||||||
|
|
||||||
if (data.message?.content) {
|
|
||||||
const sseData = JSON.stringify({
|
|
||||||
choices: [{
|
|
||||||
delta: {
|
|
||||||
content: data.message.content
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
controller.enqueue(new TextEncoder().encode(`data: ${sseData}\n\n`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.done) {
|
|
||||||
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[Ollama Provider] Failed to parse chunk:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
response.body.on('end', () => {
|
|
||||||
controller.close();
|
|
||||||
console.log('[Ollama Provider] Streaming completed');
|
|
||||||
});
|
|
||||||
|
|
||||||
response.body.on('error', (error) => {
|
|
||||||
console.error('[Ollama Provider] Streaming error:', error);
|
|
||||||
controller.error(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Ollama Provider] Streaming setup error:', error);
|
|
||||||
controller.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
console.log('[Ollama Provider] Got streaming response');
|
||||||
ok: true,
|
|
||||||
body: stream
|
const stream = new ReadableStream({
|
||||||
};
|
async start(controller) {
|
||||||
|
let buffer = '';
|
||||||
} catch (error) {
|
|
||||||
console.error('[Ollama Provider] Request error:', error);
|
try {
|
||||||
throw error;
|
response.body.on('data', (chunk) => {
|
||||||
}
|
buffer += chunk.toString();
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim() === '') continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(line);
|
||||||
|
|
||||||
|
if (data.message?.content) {
|
||||||
|
const sseData = JSON.stringify({
|
||||||
|
choices: [{
|
||||||
|
delta: {
|
||||||
|
content: data.message.content
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
controller.enqueue(new TextEncoder().encode(`data: ${sseData}\n\n`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.done) {
|
||||||
|
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Ollama Provider] Failed to parse chunk:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
response.body.on('end', () => {
|
||||||
|
controller.close();
|
||||||
|
console.log('[Ollama Provider] Streaming completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
response.body.on('error', (error) => {
|
||||||
|
console.error('[Ollama Provider] Streaming error:', error);
|
||||||
|
controller.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Ollama Provider] Streaming setup error:', error);
|
||||||
|
controller.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
body: stream
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Ollama Provider] Request error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
|
const { BrowserWindow } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
@ -17,6 +18,19 @@ class LocalAIServiceBase extends EventEmitter {
|
|||||||
this.installationProgress = new Map();
|
this.installationProgress = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 모든 윈도우에 이벤트 브로드캐스트
|
||||||
|
_broadcastToAllWindows(eventName, data = null) {
|
||||||
|
BrowserWindow.getAllWindows().forEach(win => {
|
||||||
|
if (win && !win.isDestroyed()) {
|
||||||
|
if (data !== null) {
|
||||||
|
win.webContents.send(eventName, data);
|
||||||
|
} else {
|
||||||
|
win.webContents.send(eventName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getPlatform() {
|
getPlatform() {
|
||||||
return process.platform;
|
return process.platform;
|
||||||
}
|
}
|
||||||
@ -65,7 +79,7 @@ class LocalAIServiceBase extends EventEmitter {
|
|||||||
|
|
||||||
setInstallProgress(modelName, progress) {
|
setInstallProgress(modelName, progress) {
|
||||||
this.installationProgress.set(modelName, progress);
|
this.installationProgress.set(modelName, progress);
|
||||||
this.emit('install-progress', { model: modelName, progress });
|
// 각 서비스에서 직접 브로드캐스트하도록 변경
|
||||||
}
|
}
|
||||||
|
|
||||||
clearInstallProgress(modelName) {
|
clearInstallProgress(modelName) {
|
||||||
@ -194,15 +208,7 @@ class LocalAIServiceBase extends EventEmitter {
|
|||||||
if (totalSize > 0) {
|
if (totalSize > 0) {
|
||||||
const progress = Math.round((downloadedSize / totalSize) * 100);
|
const progress = Math.round((downloadedSize / totalSize) * 100);
|
||||||
|
|
||||||
// 이벤트 기반 진행률 보고
|
// 이벤트 기반 진행률 보고는 각 서비스에서 직접 처리
|
||||||
if (modelId) {
|
|
||||||
this.emit('download-progress', {
|
|
||||||
modelId,
|
|
||||||
progress,
|
|
||||||
downloadedSize,
|
|
||||||
totalSize
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기존 콜백 지원 (호환성 유지)
|
// 기존 콜백 지원 (호환성 유지)
|
||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
@ -215,7 +221,7 @@ class LocalAIServiceBase extends EventEmitter {
|
|||||||
|
|
||||||
file.on('finish', () => {
|
file.on('finish', () => {
|
||||||
file.close(() => {
|
file.close(() => {
|
||||||
this.emit('download-complete', { url, destination, size: downloadedSize, modelId });
|
// download-complete 이벤트는 각 서비스에서 직접 처리
|
||||||
resolve({ success: true, size: downloadedSize });
|
resolve({ success: true, size: downloadedSize });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -272,12 +278,7 @@ class LocalAIServiceBase extends EventEmitter {
|
|||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (attempt === maxRetries) {
|
if (attempt === maxRetries) {
|
||||||
this.emit('download-error', {
|
// download-error 이벤트는 각 서비스에서 직접 처리
|
||||||
url,
|
|
||||||
error: error.message,
|
|
||||||
modelId,
|
|
||||||
attempt: attempt
|
|
||||||
});
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,23 +288,6 @@ class LocalAIServiceBase extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모델 pull을 위한 이벤트 발생 메서드 추가
|
|
||||||
emitPullProgress(modelId, progress, status = 'pulling') {
|
|
||||||
this.emit('pull-progress', {
|
|
||||||
modelId,
|
|
||||||
progress,
|
|
||||||
status
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
emitPullComplete(modelId) {
|
|
||||||
this.emit('pull-complete', { modelId });
|
|
||||||
}
|
|
||||||
|
|
||||||
emitPullError(modelId, error) {
|
|
||||||
this.emit('pull-error', { modelId, error });
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyChecksum(filePath, expectedChecksum) {
|
async verifyChecksum(filePath, expectedChecksum) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
|
const { BrowserWindow } = require('electron');
|
||||||
const { PROVIDERS, getProviderClass } = require('../ai/factory');
|
const { PROVIDERS, getProviderClass } = require('../ai/factory');
|
||||||
const encryptionService = require('./encryptionService');
|
const encryptionService = require('./encryptionService');
|
||||||
const providerSettingsRepository = require('../repositories/providerSettings');
|
const providerSettingsRepository = require('../repositories/providerSettings');
|
||||||
@ -22,6 +23,19 @@ class ModelStateService extends EventEmitter {
|
|||||||
userModelSelectionsRepository.setAuthService(authService);
|
userModelSelectionsRepository.setAuthService(authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 모든 윈도우에 이벤트 브로드캐스트
|
||||||
|
_broadcastToAllWindows(eventName, data = null) {
|
||||||
|
BrowserWindow.getAllWindows().forEach(win => {
|
||||||
|
if (win && !win.isDestroyed()) {
|
||||||
|
if (data !== null) {
|
||||||
|
win.webContents.send(eventName, data);
|
||||||
|
} else {
|
||||||
|
win.webContents.send(eventName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
console.log('[ModelStateService] Initializing...');
|
console.log('[ModelStateService] Initializing...');
|
||||||
await this._loadStateForCurrentUser();
|
await this._loadStateForCurrentUser();
|
||||||
@ -352,8 +366,8 @@ class ModelStateService extends EventEmitter {
|
|||||||
|
|
||||||
this._autoSelectAvailableModels([]);
|
this._autoSelectAvailableModels([]);
|
||||||
|
|
||||||
this.emit('state-changed', this.state);
|
this._broadcastToAllWindows('model-state:updated', this.state);
|
||||||
this.emit('settings-updated');
|
this._broadcastToAllWindows('settings-updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
getApiKey(provider) {
|
getApiKey(provider) {
|
||||||
@ -372,8 +386,8 @@ class ModelStateService extends EventEmitter {
|
|||||||
|
|
||||||
this._autoSelectAvailableModels([]);
|
this._autoSelectAvailableModels([]);
|
||||||
|
|
||||||
this.emit('state-changed', this.state);
|
this._broadcastToAllWindows('model-state:updated', this.state);
|
||||||
this.emit('settings-updated');
|
this._broadcastToAllWindows('settings-updated');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -516,8 +530,8 @@ class ModelStateService extends EventEmitter {
|
|||||||
this._autoWarmUpOllamaModel(modelId, previousModelId);
|
this._autoWarmUpOllamaModel(modelId, previousModelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('state-changed', this.state);
|
this._broadcastToAllWindows('model-state:updated', this.state);
|
||||||
this.emit('settings-updated');
|
this._broadcastToAllWindows('settings-updated');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -529,7 +543,7 @@ class ModelStateService extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async _autoWarmUpOllamaModel(newModelId, previousModelId) {
|
async _autoWarmUpOllamaModel(newModelId, previousModelId) {
|
||||||
try {
|
try {
|
||||||
console.log(`[ModelStateService] 🔥 LLM model changed: ${previousModelId || 'None'} → ${newModelId}, triggering warm-up`);
|
console.log(`[ModelStateService] LLM model changed: ${previousModelId || 'None'} → ${newModelId}, triggering warm-up`);
|
||||||
|
|
||||||
// Get Ollama service if available
|
// Get Ollama service if available
|
||||||
const ollamaService = require('./ollamaService');
|
const ollamaService = require('./ollamaService');
|
||||||
@ -545,12 +559,12 @@ class ModelStateService extends EventEmitter {
|
|||||||
const success = await ollamaService.warmUpModel(newModelId);
|
const success = await ollamaService.warmUpModel(newModelId);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log(`[ModelStateService] ✅ Successfully warmed up model: ${newModelId}`);
|
console.log(`[ModelStateService] Successfully warmed up model: ${newModelId}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[ModelStateService] ⚠️ Failed to warm up model: ${newModelId}`);
|
console.log(`[ModelStateService] Failed to warm up model: ${newModelId}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`[ModelStateService] 🚫 Error during auto warm-up for ${newModelId}:`, error.message);
|
console.log(`[ModelStateService] Error during auto warm-up for ${newModelId}:`, error.message);
|
||||||
}
|
}
|
||||||
}, 500); // 500ms delay
|
}, 500); // 500ms delay
|
||||||
|
|
||||||
@ -584,7 +598,7 @@ class ModelStateService extends EventEmitter {
|
|||||||
if (success) {
|
if (success) {
|
||||||
const selectedModels = this.getSelectedModels();
|
const selectedModels = this.getSelectedModels();
|
||||||
if (!selectedModels.llm || !selectedModels.stt) {
|
if (!selectedModels.llm || !selectedModels.stt) {
|
||||||
this.emit('force-show-apikey-header');
|
this._broadcastToAllWindows('force-show-apikey-header');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
|
@ -3,7 +3,7 @@ const { promisify } = require('util');
|
|||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const { app } = require('electron');
|
const { app, BrowserWindow } = require('electron');
|
||||||
const LocalAIServiceBase = require('./localAIServiceBase');
|
const LocalAIServiceBase = require('./localAIServiceBase');
|
||||||
const { spawnAsync } = require('../utils/spawnHelper');
|
const { spawnAsync } = require('../utils/spawnHelper');
|
||||||
const { DOWNLOAD_CHECKSUMS } = require('../config/checksums');
|
const { DOWNLOAD_CHECKSUMS } = require('../config/checksums');
|
||||||
@ -27,8 +27,8 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
this.requestTimeout = 8000; // 8s for health checks
|
this.requestTimeout = 0; // Delete timeout
|
||||||
this.warmupTimeout = 60000; // 60s for model warmup (늘림)
|
this.warmupTimeout = 120000; // 120s for model warmup
|
||||||
this.healthCheckInterval = 60000; // 1min between health checks
|
this.healthCheckInterval = 60000; // 1min between health checks
|
||||||
this.circuitBreakerThreshold = 3;
|
this.circuitBreakerThreshold = 3;
|
||||||
this.circuitBreakerCooldown = 30000; // 30s
|
this.circuitBreakerCooldown = 30000; // 30s
|
||||||
@ -40,6 +40,19 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
this._startHealthMonitoring();
|
this._startHealthMonitoring();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 모든 윈도우에 이벤트 브로드캐스트
|
||||||
|
_broadcastToAllWindows(eventName, data = null) {
|
||||||
|
BrowserWindow.getAllWindows().forEach(win => {
|
||||||
|
if (win && !win.isDestroyed()) {
|
||||||
|
if (data !== null) {
|
||||||
|
win.webContents.send(eventName, data);
|
||||||
|
} else {
|
||||||
|
win.webContents.send(eventName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
try {
|
try {
|
||||||
const installed = await this.isInstalled();
|
const installed = await this.isInstalled();
|
||||||
@ -87,14 +100,17 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = options.timeout || this.requestTimeout;
|
const timeout = options.timeout || this.requestTimeout;
|
||||||
|
|
||||||
// Set up timeout mechanism
|
// Set up timeout mechanism only if timeout > 0
|
||||||
const timeoutId = setTimeout(() => {
|
let timeoutId = null;
|
||||||
controller.abort();
|
if (timeout > 0) {
|
||||||
this.activeRequests.delete(requestId);
|
timeoutId = setTimeout(() => {
|
||||||
this._recordFailure();
|
controller.abort();
|
||||||
}, timeout);
|
this.activeRequests.delete(requestId);
|
||||||
|
this._recordFailure();
|
||||||
this.requestTimeouts.set(requestId, timeoutId);
|
}, timeout);
|
||||||
|
|
||||||
|
this.requestTimeouts.set(requestId, timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
const requestPromise = this._executeRequest(url, {
|
const requestPromise = this._executeRequest(url, {
|
||||||
...options,
|
...options,
|
||||||
@ -115,8 +131,10 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
if (timeoutId !== null) {
|
||||||
this.requestTimeouts.delete(requestId);
|
clearTimeout(timeoutId);
|
||||||
|
this.requestTimeouts.delete(requestId);
|
||||||
|
}
|
||||||
this.activeRequests.delete(operationType === 'health' ? 'health' : requestId);
|
this.activeRequests.delete(operationType === 'health' ? 'health' : requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,7 +395,7 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
|
|
||||||
if (progress !== null) {
|
if (progress !== null) {
|
||||||
this.setInstallProgress(modelName, progress);
|
this.setInstallProgress(modelName, progress);
|
||||||
this.emit('pull-progress', {
|
this._broadcastToAllWindows('ollama:pull-progress', {
|
||||||
model: modelName,
|
model: modelName,
|
||||||
progress,
|
progress,
|
||||||
status: data.status || 'downloading'
|
status: data.status || 'downloading'
|
||||||
@ -388,7 +406,7 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
// Handle completion
|
// Handle completion
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
console.log(`[OllamaService] Successfully pulled model: ${modelName}`);
|
console.log(`[OllamaService] Successfully pulled model: ${modelName}`);
|
||||||
this.emit('pull-complete', { model: modelName });
|
this._broadcastToAllWindows('ollama:pull-complete', { model: modelName });
|
||||||
this.clearInstallProgress(modelName);
|
this.clearInstallProgress(modelName);
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
@ -406,7 +424,7 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
const data = JSON.parse(buffer);
|
const data = JSON.parse(buffer);
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
console.log(`[OllamaService] Successfully pulled model: ${modelName}`);
|
console.log(`[OllamaService] Successfully pulled model: ${modelName}`);
|
||||||
this.emit('pull-complete', { model: modelName });
|
this._broadcastToAllWindows('ollama:pull-complete', { model: modelName });
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.warn('[OllamaService] Failed to parse final buffer:', buffer);
|
console.warn('[OllamaService] Failed to parse final buffer:', buffer);
|
||||||
@ -881,7 +899,7 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
async handleInstall() {
|
async handleInstall() {
|
||||||
try {
|
try {
|
||||||
const onProgress = (data) => {
|
const onProgress = (data) => {
|
||||||
this.emit('install-progress', data);
|
this._broadcastToAllWindows('ollama:install-progress', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.autoInstall(onProgress);
|
await this.autoInstall(onProgress);
|
||||||
@ -891,11 +909,11 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
await this.startService();
|
await this.startService();
|
||||||
onProgress({ stage: 'starting', message: 'Ollama service started.', progress: 100 });
|
onProgress({ stage: 'starting', message: 'Ollama service started.', progress: 100 });
|
||||||
}
|
}
|
||||||
this.emit('install-complete', { success: true });
|
this._broadcastToAllWindows('ollama:install-complete', { success: true });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[OllamaService] Failed to install:', error);
|
console.error('[OllamaService] Failed to install:', error);
|
||||||
this.emit('install-complete', { success: false, error: error.message });
|
this._broadcastToAllWindows('ollama:install-complete', { success: false, error: error.message });
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -963,7 +981,7 @@ class OllamaService extends LocalAIServiceBase {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[OllamaService] Failed to pull model:', error);
|
console.error('[OllamaService] Failed to pull model:', error);
|
||||||
await ollamaModelRepository.updateInstallStatus(modelName, false, false);
|
await ollamaModelRepository.updateInstallStatus(modelName, false, false);
|
||||||
this.emit('pull-error', { model: modelName, error: error.message });
|
this._broadcastToAllWindows('ollama:pull-error', { model: modelName, error: error.message });
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ const { spawn } = require('child_process');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const { BrowserWindow } = require('electron');
|
||||||
const LocalAIServiceBase = require('./localAIServiceBase');
|
const LocalAIServiceBase = require('./localAIServiceBase');
|
||||||
const { spawnAsync } = require('../utils/spawnHelper');
|
const { spawnAsync } = require('../utils/spawnHelper');
|
||||||
const { DOWNLOAD_CHECKSUMS } = require('../config/checksums');
|
const { DOWNLOAD_CHECKSUMS } = require('../config/checksums');
|
||||||
@ -39,6 +40,19 @@ class WhisperService extends LocalAIServiceBase {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 모든 윈도우에 이벤트 브로드캐스트
|
||||||
|
_broadcastToAllWindows(eventName, data = null) {
|
||||||
|
BrowserWindow.getAllWindows().forEach(win => {
|
||||||
|
if (win && !win.isDestroyed()) {
|
||||||
|
if (data !== null) {
|
||||||
|
win.webContents.send(eventName, data);
|
||||||
|
} else {
|
||||||
|
win.webContents.send(eventName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
if (this.isInitialized) return;
|
if (this.isInitialized) return;
|
||||||
|
|
||||||
@ -157,18 +171,18 @@ class WhisperService extends LocalAIServiceBase {
|
|||||||
const modelPath = await this.getModelPath(modelId);
|
const modelPath = await this.getModelPath(modelId);
|
||||||
const checksumInfo = DOWNLOAD_CHECKSUMS.whisper.models[modelId];
|
const checksumInfo = DOWNLOAD_CHECKSUMS.whisper.models[modelId];
|
||||||
|
|
||||||
this.emit('download-progress', { modelId, progress: 0 });
|
this._broadcastToAllWindows('whisper:download-progress', { modelId, progress: 0 });
|
||||||
|
|
||||||
await this.downloadWithRetry(modelInfo.url, modelPath, {
|
await this.downloadWithRetry(modelInfo.url, modelPath, {
|
||||||
expectedChecksum: checksumInfo?.sha256,
|
expectedChecksum: checksumInfo?.sha256,
|
||||||
modelId, // modelId를 전달하여 LocalAIServiceBase에서 이벤트 발생 시 사용
|
modelId, // modelId를 전달하여 LocalAIServiceBase에서 이벤트 발생 시 사용
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
this.emit('download-progress', { modelId, progress });
|
this._broadcastToAllWindows('whisper:download-progress', { modelId, progress });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[WhisperService] Model ${modelId} downloaded successfully`);
|
console.log(`[WhisperService] Model ${modelId} downloaded successfully`);
|
||||||
this.emit('download-complete', { modelId });
|
this._broadcastToAllWindows('whisper:download-complete', { modelId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDownloadModel(modelId) {
|
async handleDownloadModel(modelId) {
|
||||||
|
@ -39,11 +39,12 @@ class ListenService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer(channel, data) {
|
sendToRenderer(channel, data) {
|
||||||
BrowserWindow.getAllWindows().forEach(win => {
|
const { windowPool } = require('../../window/windowManager');
|
||||||
if (!win.isDestroyed()) {
|
const listenWindow = windowPool?.get('listen');
|
||||||
win.webContents.send(channel, data);
|
|
||||||
}
|
if (listenWindow && !listenWindow.isDestroyed()) {
|
||||||
});
|
listenWindow.webContents.send(channel, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
|
@ -35,11 +35,13 @@ class SttService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer(channel, data) {
|
sendToRenderer(channel, data) {
|
||||||
BrowserWindow.getAllWindows().forEach(win => {
|
// Listen 관련 이벤트는 Listen 윈도우에만 전송 (Ask 윈도우 충돌 방지)
|
||||||
if (!win.isDestroyed()) {
|
const { windowPool } = require('../../../window/windowManager');
|
||||||
win.webContents.send(channel, data);
|
const listenWindow = windowPool?.get('listen');
|
||||||
}
|
|
||||||
});
|
if (listenWindow && !listenWindow.isDestroyed()) {
|
||||||
|
listenWindow.webContents.send(channel, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSendSystemAudioContent(data, mimeType) {
|
async handleSendSystemAudioContent(data, mimeType) {
|
||||||
|
@ -28,11 +28,12 @@ class SummaryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer(channel, data) {
|
sendToRenderer(channel, data) {
|
||||||
BrowserWindow.getAllWindows().forEach(win => {
|
const { windowPool } = require('../../../window/windowManager');
|
||||||
if (!win.isDestroyed()) {
|
const listenWindow = windowPool?.get('listen');
|
||||||
win.webContents.send(channel, data);
|
|
||||||
}
|
if (listenWindow && !listenWindow.isDestroyed()) {
|
||||||
});
|
listenWindow.webContents.send(channel, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addConversationTurn(speaker, text) {
|
addConversationTurn(speaker, text) {
|
||||||
@ -304,25 +305,20 @@ Keep all points concise and build upon previous analysis if provided.`,
|
|||||||
*/
|
*/
|
||||||
async triggerAnalysisIfNeeded() {
|
async triggerAnalysisIfNeeded() {
|
||||||
if (this.conversationHistory.length >= 5 && this.conversationHistory.length % 5 === 0) {
|
if (this.conversationHistory.length >= 5 && this.conversationHistory.length % 5 === 0) {
|
||||||
console.log(`🚀 Triggering analysis (non-blocking) - ${this.conversationHistory.length} conversation texts accumulated`);
|
console.log(`Triggering analysis - ${this.conversationHistory.length} conversation texts accumulated`);
|
||||||
|
|
||||||
this.makeOutlineAndRequests(this.conversationHistory)
|
const data = await this.makeOutlineAndRequests(this.conversationHistory);
|
||||||
.then(data => {
|
if (data) {
|
||||||
if (data) {
|
console.log('Sending structured data to renderer');
|
||||||
console.log('📤 Sending structured data to renderer');
|
this.sendToRenderer('summary-update', data);
|
||||||
this.sendToRenderer('summary-update', data);
|
|
||||||
|
// Notify callback
|
||||||
// Notify callback
|
if (this.onAnalysisComplete) {
|
||||||
if (this.onAnalysisComplete) {
|
this.onAnalysisComplete(data);
|
||||||
this.onAnalysisComplete(data);
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
console.log('No analysis data returned');
|
||||||
console.log('❌ No analysis data returned from non-blocking call');
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('❌ Error in non-blocking analysis:', error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +136,9 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// Listeners
|
// Listeners
|
||||||
onAskStateUpdate: (callback) => ipcRenderer.on('ask:stateUpdate', callback),
|
onAskStateUpdate: (callback) => ipcRenderer.on('ask:stateUpdate', callback),
|
||||||
removeOnAskStateUpdate: (callback) => ipcRenderer.removeListener('ask:stateUpdate', callback),
|
removeOnAskStateUpdate: (callback) => ipcRenderer.removeListener('ask:stateUpdate', callback),
|
||||||
|
|
||||||
|
onAskStreamError: (callback) => ipcRenderer.on('ask-response-stream-error', callback),
|
||||||
|
removeOnAskStreamError: (callback) => ipcRenderer.removeListener('ask-response-stream-error', callback),
|
||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
onShowTextInput: (callback) => ipcRenderer.on('ask:showTextInput', callback),
|
onShowTextInput: (callback) => ipcRenderer.on('ask:showTextInput', callback),
|
||||||
|
@ -331,14 +331,15 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
this.ipcTimeout = 10000; // 10s for IPC calls
|
this.ipcTimeout = 10000; // 10s for IPC calls
|
||||||
this.operationTimeout = 15000; // 15s for complex operations
|
this.operationTimeout = 15000; // 15s for complex operations
|
||||||
|
|
||||||
// Health monitoring system
|
// Health monitoring system
|
||||||
this.healthCheck = {
|
this.healthCheck = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
intervalId: null,
|
intervalId: null,
|
||||||
intervalMs: 30000, // 30s
|
intervalMs: 120000,
|
||||||
lastCheck: 0,
|
lastCheck: 0,
|
||||||
consecutiveFailures: 0,
|
consecutiveFailures: 0,
|
||||||
maxFailures: 3
|
maxFailures: 5,
|
||||||
|
skipDuringOperation: true // skip during operation
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load user model history from localStorage
|
// Load user model history from localStorage
|
||||||
@ -641,6 +642,17 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
if (this.llmProvider !== 'ollama' || this.connectionState === 'connecting') {
|
if (this.llmProvider !== 'ollama' || this.connectionState === 'connecting') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skip during operation
|
||||||
|
if (this.healthCheck.skipDuringOperation && (
|
||||||
|
this.operationQueue.length > 0 ||
|
||||||
|
this.connectionState === 'installing' ||
|
||||||
|
this.connectionState === 'starting' ||
|
||||||
|
Object.keys(this.operationMetrics.activeOperations || {}).length > 0
|
||||||
|
)) {
|
||||||
|
console.log('[ApiKeyHeader] Skipping health check - other operations in progress');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
this.healthCheck.lastCheck = now;
|
this.healthCheck.lastCheck = now;
|
||||||
|
@ -322,6 +322,12 @@ function createFeatureWindows(header, namesToCreate) {
|
|||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
ask.webContents.openDevTools({ mode: 'detach' });
|
ask.webContents.openDevTools({ mode: 'detach' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ask.on('closed', () => {
|
||||||
|
console.log('[WindowManager] Ask window closed, removing from pool.');
|
||||||
|
windowPool.delete('ask');
|
||||||
|
});
|
||||||
|
|
||||||
windowPool.set('ask', ask);
|
windowPool.set('ask', ask);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user