mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 20:00:00 +08:00
冲突ipc
This commit is contained in:
parent
5b819221b3
commit
e7ae2bbe64
@ -146,5 +146,9 @@ export enum IpcChannel {
|
|||||||
MiniWindowReload = 'miniwindow-reload',
|
MiniWindowReload = 'miniwindow-reload',
|
||||||
|
|
||||||
ReduxStateChange = 'redux-state-change',
|
ReduxStateChange = 'redux-state-change',
|
||||||
ReduxStoreReady = 'redux-store-ready'
|
ReduxStoreReady = 'redux-store-ready',
|
||||||
|
|
||||||
|
// ASR Server
|
||||||
|
ASR_StartServer = 'start-asr-server',
|
||||||
|
ASR_StopServer = 'stop-asr-server'
|
||||||
}
|
}
|
||||||
|
|||||||
106
src/main/ipc.ts
106
src/main/ipc.ts
@ -1,6 +1,4 @@
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import { spawn, ChildProcess } from 'node:child_process'
|
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
import { isMac, isWin } from '@main/constant'
|
import { isMac, isWin } from '@main/constant'
|
||||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||||
@ -28,11 +26,11 @@ import { TrayService } from './services/TrayService'
|
|||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
import { getResourcePath } from './utils'
|
import { getResourcePath } from './utils'
|
||||||
import { decrypt, encrypt } from './utils/aes'
|
import { decrypt, encrypt } from './utils/aes'
|
||||||
|
import { registerASRServerIPC } from './services/ASRServerIPC'
|
||||||
import { getConfigDir, getFilesDir } from './utils/file'
|
import { getConfigDir, getFilesDir } from './utils/file'
|
||||||
import { compress, decompress } from './utils/zip'
|
import { compress, decompress } from './utils/zip'
|
||||||
|
|
||||||
// 存储ASR服务器进程
|
|
||||||
let asrServerProcess: ChildProcess | null = null
|
|
||||||
|
|
||||||
const fileManager = new FileStorage()
|
const fileManager = new FileStorage()
|
||||||
const backupManager = new BackupManager()
|
const backupManager = new BackupManager()
|
||||||
@ -297,102 +295,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
NutstoreService.getDirectoryContents(token, path)
|
NutstoreService.getDirectoryContents(token, path)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 启动ASR服务器
|
// 注册ASR服务器相关的IPC处理程序
|
||||||
ipcMain.handle('start-asr-server', async () => {
|
registerASRServerIPC(ipcMain, app)
|
||||||
try {
|
|
||||||
if (asrServerProcess) {
|
|
||||||
return { success: true, pid: asrServerProcess.pid }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取服务器文件路径
|
|
||||||
console.log('App path:', app.getAppPath())
|
|
||||||
// 在开发环境和生产环境中使用不同的路径
|
|
||||||
let serverPath = ''
|
|
||||||
let isExeFile = false
|
|
||||||
|
|
||||||
// 首先检查是否有打包后的exe文件
|
|
||||||
const exePath = path.join(app.getAppPath(), 'resources', 'cherry-asr-server.exe')
|
|
||||||
if (fs.existsSync(exePath)) {
|
|
||||||
serverPath = exePath
|
|
||||||
isExeFile = true
|
|
||||||
console.log('检测到打包后的exe文件:', serverPath)
|
|
||||||
} else if (process.env.NODE_ENV === 'development') {
|
|
||||||
// 开发环境
|
|
||||||
serverPath = path.join(app.getAppPath(), 'src', 'renderer', 'src', 'assets', 'asr-server', 'server.js')
|
|
||||||
} else {
|
|
||||||
// 生产环境
|
|
||||||
serverPath = path.join(app.getAppPath(), 'public', 'asr-server', 'server.js')
|
|
||||||
}
|
|
||||||
console.log('ASR服务器路径:', serverPath)
|
|
||||||
|
|
||||||
// 检查文件是否存在
|
|
||||||
if (!fs.existsSync(serverPath)) {
|
|
||||||
return { success: false, error: '服务器文件不存在' }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动服务器进程
|
|
||||||
if (isExeFile) {
|
|
||||||
// 如果是exe文件,直接启动
|
|
||||||
asrServerProcess = spawn(serverPath, [], {
|
|
||||||
stdio: 'pipe',
|
|
||||||
detached: false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 如果是js文件,使用node启动
|
|
||||||
asrServerProcess = spawn('node', [serverPath], {
|
|
||||||
stdio: 'pipe',
|
|
||||||
detached: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理服务器输出
|
|
||||||
asrServerProcess.stdout?.on('data', (data) => {
|
|
||||||
console.log(`[ASR Server] ${data.toString()}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
asrServerProcess.stderr?.on('data', (data) => {
|
|
||||||
console.error(`[ASR Server Error] ${data.toString()}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 处理服务器退出
|
|
||||||
asrServerProcess.on('close', (code) => {
|
|
||||||
console.log(`[ASR Server] 进程退出,退出码: ${code}`)
|
|
||||||
asrServerProcess = null
|
|
||||||
})
|
|
||||||
|
|
||||||
// 等待一段时间确保服务器启动
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
return { success: true, pid: asrServerProcess.pid }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('启动ASR服务器失败:', error)
|
|
||||||
return { success: false, error: (error as Error).message }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 停止ASR服务器
|
|
||||||
ipcMain.handle('stop-asr-server', async (_event, pid) => {
|
|
||||||
try {
|
|
||||||
if (!asrServerProcess) {
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查PID是否匹配
|
|
||||||
if (asrServerProcess.pid !== pid) {
|
|
||||||
console.warn(`请求停止的PID (${pid}) 与当前运行的ASR服务器PID (${asrServerProcess.pid}) 不匹配`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 杀死进程
|
|
||||||
asrServerProcess.kill()
|
|
||||||
|
|
||||||
// 等待一段时间确保进程已经退出
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
asrServerProcess = null
|
|
||||||
return { success: true }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('停止ASR服务器失败:', error)
|
|
||||||
return { success: false, error: (error as Error).message }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/main/services/ASRServerIPC.ts
Normal file
114
src/main/services/ASRServerIPC.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import { spawn, ChildProcess } from 'node:child_process'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { IpcMain, App } from 'electron'
|
||||||
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
|
|
||||||
|
// 存储ASR服务器进程
|
||||||
|
let asrServerProcess: ChildProcess | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册ASR服务器相关的IPC处理程序
|
||||||
|
* @param ipcMain IPC主进程对象
|
||||||
|
* @param app Electron应用对象
|
||||||
|
*/
|
||||||
|
export function registerASRServerIPC(ipcMain: IpcMain, app: App): void {
|
||||||
|
// 启动ASR服务器
|
||||||
|
ipcMain.handle(IpcChannel.ASR_StartServer, async () => {
|
||||||
|
try {
|
||||||
|
if (asrServerProcess) {
|
||||||
|
return { success: true, pid: asrServerProcess.pid }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取服务器文件路径
|
||||||
|
console.log('App path:', app.getAppPath())
|
||||||
|
// 在开发环境和生产环境中使用不同的路径
|
||||||
|
let serverPath = ''
|
||||||
|
let isExeFile = false
|
||||||
|
|
||||||
|
// 首先检查是否有打包后的exe文件
|
||||||
|
const exePath = path.join(app.getAppPath(), 'resources', 'cherry-asr-server.exe')
|
||||||
|
if (fs.existsSync(exePath)) {
|
||||||
|
serverPath = exePath
|
||||||
|
isExeFile = true
|
||||||
|
console.log('检测到打包后的exe文件:', serverPath)
|
||||||
|
} else if (process.env.NODE_ENV === 'development') {
|
||||||
|
// 开发环境
|
||||||
|
serverPath = path.join(app.getAppPath(), 'src', 'renderer', 'src', 'assets', 'asr-server', 'server.js')
|
||||||
|
} else {
|
||||||
|
// 生产环境
|
||||||
|
serverPath = path.join(app.getAppPath(), 'public', 'asr-server', 'server.js')
|
||||||
|
}
|
||||||
|
console.log('ASR服务器路径:', serverPath)
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(serverPath)) {
|
||||||
|
return { success: false, error: '服务器文件不存在' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务器进程
|
||||||
|
if (isExeFile) {
|
||||||
|
// 如果是exe文件,直接启动
|
||||||
|
asrServerProcess = spawn(serverPath, [], {
|
||||||
|
stdio: 'pipe',
|
||||||
|
detached: false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 如果是js文件,使用node启动
|
||||||
|
asrServerProcess = spawn('node', [serverPath], {
|
||||||
|
stdio: 'pipe',
|
||||||
|
detached: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理服务器输出
|
||||||
|
asrServerProcess.stdout?.on('data', (data) => {
|
||||||
|
console.log(`[ASR Server] ${data.toString()}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
asrServerProcess.stderr?.on('data', (data) => {
|
||||||
|
console.error(`[ASR Server Error] ${data.toString()}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理服务器退出
|
||||||
|
asrServerProcess.on('close', (code) => {
|
||||||
|
console.log(`[ASR Server] 进程退出,退出码: ${code}`)
|
||||||
|
asrServerProcess = null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 等待一段时间确保服务器启动
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
return { success: true, pid: asrServerProcess.pid }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启动ASR服务器失败:', error)
|
||||||
|
return { success: false, error: (error as Error).message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 停止ASR服务器
|
||||||
|
ipcMain.handle(IpcChannel.ASR_StopServer, async (_event, pid) => {
|
||||||
|
try {
|
||||||
|
if (!asrServerProcess) {
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查PID是否匹配
|
||||||
|
if (asrServerProcess.pid !== pid) {
|
||||||
|
console.warn(`请求停止的PID (${pid}) 与当前运行的ASR服务器PID (${asrServerProcess.pid}) 不匹配`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 杀死进程
|
||||||
|
asrServerProcess.kill()
|
||||||
|
|
||||||
|
// 等待一段时间确保进程已经退出
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
asrServerProcess = null
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('停止ASR服务器失败:', error)
|
||||||
|
return { success: false, error: (error as Error).message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,132 @@
|
|||||||
|
import { Readability } from '@mozilla/readability'
|
||||||
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
|
import { WebSearchProvider, WebSearchResponse, WebSearchResult } from '@renderer/types'
|
||||||
|
import TurndownService from 'turndown'
|
||||||
|
|
||||||
|
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||||
|
|
||||||
|
export interface SearchItem {
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const noContent = 'No content found'
|
||||||
|
|
||||||
|
export default class LocalSearchProvider extends BaseWebSearchProvider {
|
||||||
|
private turndownService: TurndownService = new TurndownService()
|
||||||
|
|
||||||
|
constructor(provider: WebSearchProvider) {
|
||||||
|
if (!provider || !provider.url) {
|
||||||
|
throw new Error('Provider URL is required')
|
||||||
|
}
|
||||||
|
super(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async search(
|
||||||
|
query: string,
|
||||||
|
maxResults: number = 15,
|
||||||
|
excludeDomains: string[] = []
|
||||||
|
): Promise<WebSearchResponse> {
|
||||||
|
const uid = nanoid()
|
||||||
|
try {
|
||||||
|
if (!query.trim()) {
|
||||||
|
throw new Error('Search query cannot be empty')
|
||||||
|
}
|
||||||
|
if (!this.provider.url) {
|
||||||
|
throw new Error('Provider URL is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedQuery = query.split('\r\n')[1] ?? query
|
||||||
|
const url = this.provider.url.replace('%s', encodeURIComponent(cleanedQuery))
|
||||||
|
const content = await window.api.searchService.openUrlInSearchWindow(uid, url)
|
||||||
|
|
||||||
|
// Parse the content to extract URLs and metadata
|
||||||
|
const searchItems = this.parseValidUrls(content).slice(0, maxResults)
|
||||||
|
console.log('Total search items:', searchItems)
|
||||||
|
|
||||||
|
const validItems = searchItems
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
(item.url.startsWith('http') || item.url.startsWith('https')) &&
|
||||||
|
excludeDomains.includes(new URL(item.url).host) === false
|
||||||
|
)
|
||||||
|
.slice(0, maxResults)
|
||||||
|
// console.log('Valid search items:', validItems)
|
||||||
|
|
||||||
|
// Fetch content for each URL concurrently
|
||||||
|
const fetchPromises = validItems.map(async (item) => {
|
||||||
|
// console.log(`Fetching content for ${item.url}...`)
|
||||||
|
const result = await this.fetchPageContent(item.url, this.provider.usingBrowser)
|
||||||
|
if (
|
||||||
|
this.provider.contentLimit &&
|
||||||
|
this.provider.contentLimit != -1 &&
|
||||||
|
result.content.length > this.provider.contentLimit
|
||||||
|
) {
|
||||||
|
result.content = result.content.slice(0, this.provider.contentLimit) + '...'
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for all fetches to complete
|
||||||
|
const results: WebSearchResult[] = await Promise.all(fetchPromises)
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: query,
|
||||||
|
results: results.filter((result) => result.content != noContent)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Local search failed:', error)
|
||||||
|
throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||||
|
} finally {
|
||||||
|
await window.api.searchService.closeSearchWindow(uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected parseValidUrls(_htmlContent: string): SearchItem[] {
|
||||||
|
throw new Error('Not implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchPageContent(url: string, usingBrowser: boolean = false): Promise<WebSearchResult> {
|
||||||
|
try {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 30000) // 30 second timeout
|
||||||
|
|
||||||
|
let html: string
|
||||||
|
if (usingBrowser) {
|
||||||
|
html = await window.api.searchService.openUrlInSearchWindow(`search-window-${nanoid()}`, url)
|
||||||
|
} else {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||||
|
},
|
||||||
|
signal: controller.signal
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error: ${response.status}`)
|
||||||
|
}
|
||||||
|
html = await response.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(timeoutId) // Clear the timeout if fetch completes successfully
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const doc = parser.parseFromString(html, 'text/html')
|
||||||
|
const article = new Readability(doc).parse()
|
||||||
|
// console.log('Parsed article:', article)
|
||||||
|
const markdown = this.turndownService.turndown(article?.content || '')
|
||||||
|
return {
|
||||||
|
title: article?.title || url,
|
||||||
|
url: url,
|
||||||
|
content: markdown || noContent
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.error(`Failed to fetch ${url}`, e)
|
||||||
|
return {
|
||||||
|
title: url,
|
||||||
|
url: url,
|
||||||
|
content: noContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
|
|
||||||
// 使用window.electron而不是直接导入electron模块
|
// 使用window.electron而不是直接导入electron模块
|
||||||
// 这样可以避免__dirname不可用的问题
|
// 这样可以避免__dirname不可用的问题
|
||||||
@ -23,7 +24,7 @@ class ASRServerService {
|
|||||||
window.message.loading({ content: i18n.t('settings.asr.server.starting'), key: 'asr-server' })
|
window.message.loading({ content: i18n.t('settings.asr.server.starting'), key: 'asr-server' })
|
||||||
|
|
||||||
// 使用IPC调用主进程启动服务器
|
// 使用IPC调用主进程启动服务器
|
||||||
const result = await window.electron.ipcRenderer.invoke('start-asr-server')
|
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ASR_StartServer)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.isServerRunning = true
|
this.isServerRunning = true
|
||||||
@ -65,7 +66,7 @@ class ASRServerService {
|
|||||||
window.message.loading({ content: i18n.t('settings.asr.server.stopping'), key: 'asr-server' })
|
window.message.loading({ content: i18n.t('settings.asr.server.stopping'), key: 'asr-server' })
|
||||||
|
|
||||||
// 使用IPC调用主进程停止服务器
|
// 使用IPC调用主进程停止服务器
|
||||||
const result = await window.electron.ipcRenderer.invoke('stop-asr-server', this.serverProcess)
|
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ASR_StopServer, this.serverProcess)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.isServerRunning = false
|
this.isServerRunning = false
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user