import crypto from 'node:crypto'
import fs from 'node:fs'
import http from 'node:http'
import net from 'node:net'
import path from 'node:path'
import { IpcChannel } from '@shared/IpcChannel'
import { app, ipcMain } from 'electron'
import log from 'electron-log'
import { getResourcePath } from '../utils'
/**
* ASR服务器服务,用于管理ASR服务器进程
*/
export class ASRServerService {
// HTML内容
private INDEX_HTML_CONTENT: string = ''
// 服务器相关属性
private httpServer: http.Server | null = null
private wsClients: { browser: any | null; electron: any | null } = { browser: null, electron: null }
private serverPort: number = 34515 // 默认端口
private isServerRunning: boolean = false
/**
* 构造函数
*/
constructor() {
this.loadIndexHtml()
}
/**
* 加载index.html文件
*/
private loadIndexHtml(): void {
try {
// 在开发环境和生产环境中使用不同的路径
let htmlPath = ''
if (app.isPackaged) {
// 生产环境
const resourcePath = getResourcePath()
htmlPath = path.join(resourcePath, 'app', 'asr-server', 'index.html')
} else {
// 开发环境
htmlPath = path.join(app.getAppPath(), 'asr-server', 'index.html')
}
log.info(`加载index.html文件: ${htmlPath}`)
if (fs.existsSync(htmlPath)) {
this.INDEX_HTML_CONTENT = fs.readFileSync(htmlPath, 'utf8')
log.info(`成功加载index.html文件`)
} else {
log.error(`index.html文件不存在: ${htmlPath}`)
// 使用默认的HTML内容
this.INDEX_HTML_CONTENT = `
ASR Server Error
Error: index.html file not found
Please make sure the ASR server files are properly installed.
`
}
} catch (error) {
log.error(`加载index.html文件时出错:`, error)
// 使用默认的HTML内容
this.INDEX_HTML_CONTENT = `
ASR Server Error
Error loading index.html
An error occurred while loading the ASR server files.
`
}
}
/**
* 注册IPC处理程序
*/
public registerIpcHandlers(): void {
// 启动ASR服务器
ipcMain.handle(IpcChannel.Asr_StartServer, this.startServer.bind(this))
// 停止ASR服务器
ipcMain.handle(IpcChannel.Asr_StopServer, this.stopServer.bind(this))
}
/**
* 检查端口是否可用
* @param port 要检查的端口
* @returns 如果端口可用则返回true,否则返回false
*/
private isPortAvailable(port: number): Promise {
return new Promise((resolve) => {
const testServer = net.createServer()
testServer.once('error', (err: any) => {
if (err.code === 'EADDRINUSE') {
log.info(`端口 ${port} 已被占用,尝试其他端口...`)
resolve(false)
} else {
log.error(`检查端口 ${port} 时出错:`, err)
resolve(false)
}
})
testServer.once('listening', () => {
testServer.close()
resolve(true)
})
testServer.listen(port)
})
}
/**
* 找到可用的端口
* @param startPort 起始端口
* @returns 可用的端口
*/
private async findAvailablePort(startPort: number): Promise {
let port = startPort
const maxPort = startPort + 10 // 尝试最多10个端口
while (port < maxPort) {
if (await this.isPortAvailable(port)) {
return port
}
port++
}
throw new Error(`在 ${startPort} 和 ${maxPort - 1} 之间找不到可用的端口`)
}
/**
* 处理HTTP请求
* @param req HTTP请求
* @param res HTTP响应
*/
private handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
// 只处理根路径请求,返回index.html
if (req.url === '/' || req.url === '/index.html') {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(this.INDEX_HTML_CONTENT)
log.info(`返回index.html到客户端`)
} else {
// 其他路径返回404
res.writeHead(404, { 'Content-Type': 'text/plain' })
res.end('Not Found')
log.info(`请求的路径不存在: ${req.url}`)
}
}
/**
* 启动ASR服务器
* @returns Promise<{success: boolean, pid?: number, port?: number, error?: string}>
*/
private async startServer(): Promise<{ success: boolean; pid?: number; port?: number; error?: string }> {
try {
// 如果服务器已经运行,直接返回成功
if (this.isServerRunning && this.httpServer) {
return { success: true, port: this.serverPort }
}
// 尝试找到可用的端口
try {
this.serverPort = await this.findAvailablePort(this.serverPort)
} catch (error) {
log.error('找不到可用的端口:', error)
return { success: false, error: '找不到可用的端口' }
}
log.info(`使用端口: ${this.serverPort}`)
// 创建HTTP服务器
this.httpServer = http.createServer(this.handleHttpRequest.bind(this))
// 启动HTTP服务器
try {
await new Promise((resolve, reject) => {
if (!this.httpServer) {
reject(new Error('HTTP服务器创建失败'))
return
}
this.httpServer.on('error', (err) => {
log.error(`HTTP服务器错误:`, err)
reject(err)
})
this.httpServer.listen(this.serverPort, () => {
log.info(`HTTP服务器已启动,监听端口: ${this.serverPort}`)
resolve()
})
})
// 设置WebSocket处理
this.setupWebSocketServer()
// 标记服务器已启动
this.isServerRunning = true
log.info(`ASR服务器启动成功,端口: ${this.serverPort}`)
return { success: true, port: this.serverPort }
} catch (error) {
log.error('启动HTTP服务器失败:', error)
// 关闭HTTP服务器
if (this.httpServer) {
this.httpServer.close()
this.httpServer = null
}
return { success: false, error: `启动HTTP服务器失败: ${(error as Error).message}` }
}
} catch (error) {
log.error('启动ASR服务器失败:', error)
return { success: false, error: (error as Error).message }
}
}
/**
* 设置WebSocket服务器
*/
private setupWebSocketServer(): void {
if (!this.httpServer) {
log.error('HTTP服务器不存在,无法设置WebSocket')
return
}
// 处理WebSocket连接升级
this.httpServer.on('upgrade', (request, socket) => {
try {
log.info('[WebSocket] 收到连接升级请求')
// 解析WebSocket密钥
const key = request.headers['sec-websocket-key'] as string
const acceptKey = crypto
.createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary')
.digest('base64')
// 发送WebSocket握手响应
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${acceptKey}\r\n` +
'\r\n'
)
log.info('[WebSocket] 握手成功')
// 处理WebSocket数据
this.handleWebSocketConnection(socket)
} catch (error) {
log.error('[WebSocket] 处理升级错误:', error)
socket.destroy()
}
})
}
/**
* 处理WebSocket连接
* @param socket 套接字
*/
private handleWebSocketConnection(socket: any): void {
let buffer = Buffer.alloc(0)
const role: 'browser' | 'electron' | null = null
socket.on('data', (data: Buffer) => {
try {
buffer = Buffer.concat([buffer, data])
// 处理数据帧
while (buffer.length > 2) {
// 检查是否有完整的帧
const firstByte = buffer[0]
const secondByte = buffer[1]
// const isFinalFrame = Boolean((firstByte >>> 7) & 0x1); // 暂时不使用
const [opCode, maskFlag, payloadLength] = [firstByte & 0xf, (secondByte >>> 7) & 0x1, secondByte & 0x7f]
// 处理不同的负载长度
let payloadStartIndex = 2
let payloadLen = payloadLength
if (payloadLength === 126) {
payloadLen = buffer.readUInt16BE(2)
payloadStartIndex = 4
} else if (payloadLength === 127) {
// 处理大于16位的长度
payloadLen = Number(buffer.readBigUInt64BE(2))
payloadStartIndex = 10
}
// 处理掩码
let maskingKey: Buffer | undefined
if (maskFlag) {
maskingKey = buffer.slice(payloadStartIndex, payloadStartIndex + 4)
payloadStartIndex += 4
}
// 检查是否有足够的数据
const frameEnd = payloadStartIndex + payloadLen
if (buffer.length < frameEnd) {
// 需要更多数据
break
}
// 提取负载
const payload = buffer.slice(payloadStartIndex, frameEnd)
// 如果有掩码,解码负载
if (maskFlag && maskingKey) {
for (let i = 0; i < payload.length; i++) {
payload[i] = payload[i] ^ maskingKey[i % 4]
}
}
// 处理不同的操作码
if (opCode === 0x8) {
// 关闭帧
log.info('[WebSocket] 收到关闭帧')
socket.end()
return
} else if (opCode === 0x9) {
// Ping
this.sendPong(socket)
} else if (opCode === 0x1 || opCode === 0x2) {
// 文本或二进制数据
const message = opCode === 0x1 ? payload.toString('utf8') : payload
this.handleMessage(socket, message, role)
}
// 移除已处理的帧
buffer = buffer.slice(frameEnd)
}
} catch (error) {
log.error('[WebSocket] 处理数据错误:', error)
}
})
socket.on('close', () => {
const socketRole = (socket as any)._role || role
log.info(`[WebSocket] 连接关闭${socketRole ? ` (${socketRole})` : ''}`)
if (socketRole === 'browser') {
this.wsClients.browser = null
// 如果浏览器断开连接,通知Electron客户端
if (this.wsClients.electron) {
this.sendWebSocketFrame(
this.wsClients.electron,
JSON.stringify({
type: 'status',
message: 'Browser disconnected'
})
)
log.info('[WebSocket] 已向Electron发送Browser disconnected状态')
}
} else if (socketRole === 'electron') {
this.wsClients.electron = null
}
})
socket.on('error', (error: Error) => {
log.error(`[WebSocket] 套接字错误${role ? ` (${role})` : ''}:`, error)
})
}
/**
* 发送WebSocket数据
* @param socket 套接字
* @param data 数据
* @param opCode 操作码
*/
private sendWebSocketFrame(socket: any, data: string | object, opCode = 0x1): void {
try {
const payload = Buffer.from(typeof data === 'string' ? data : JSON.stringify(data))
const payloadLength = payload.length
let header: Buffer
if (payloadLength < 126) {
header = Buffer.from([0x80 | opCode, payloadLength])
} else if (payloadLength < 65536) {
header = Buffer.alloc(4)
header[0] = 0x80 | opCode
header[1] = 126
header.writeUInt16BE(payloadLength, 2)
} else {
header = Buffer.alloc(10)
header[0] = 0x80 | opCode
header[1] = 127
header.writeBigUInt64BE(BigInt(payloadLength), 2)
}
socket.write(Buffer.concat([header, payload]))
} catch (error) {
log.error('[WebSocket] 发送数据错误:', error)
}
}
/**
* 发送Pong响应
* @param socket 套接字
*/
private sendPong(socket: any): void {
const pongFrame = Buffer.from([0x8a, 0x00])
socket.write(pongFrame)
}
/**
* 处理消息
* @param socket 套接字
* @param message 消息
* @param currentRole 当前角色
*/
private handleMessage(socket: any, message: string | Buffer, currentRole: string | null): void {
try {
if (typeof message === 'string') {
const data = JSON.parse(message)
// 处理身份识别
if (data.type === 'identify') {
const role = data.role
if (role === 'browser' || role === 'electron') {
log.info(`[WebSocket] 客户端识别为: ${role}`)
// 存储客户端连接
this.wsClients[role] = socket
// 设置当前连接的角色
;(socket as any)._role = role
// 如果是浏览器连接,通知Electron客户端
if (role === 'browser' && this.wsClients.electron) {
// 发送browser_ready消息
this.sendWebSocketFrame(
this.wsClients.electron,
JSON.stringify({
type: 'status',
message: 'browser_ready'
})
)
log.info('[WebSocket] 已向Electron发送browser_ready状态')
// 发送Browser connected消息
this.sendWebSocketFrame(
this.wsClients.electron,
JSON.stringify({
type: 'status',
message: 'Browser connected'
})
)
log.info('[WebSocket] 已向Electron发送Browser connected状态')
}
return
}
}
// 获取当前连接的角色
const role = currentRole || (socket as any)._role
// 转发消息
if (role === 'browser') {
// 浏览器发送的消息转发给Electron
if (this.wsClients.electron) {
log.info(`[WebSocket] Browser -> Electron: ${JSON.stringify(data)}`)
this.sendWebSocketFrame(this.wsClients.electron, message)
} else {
log.info('[WebSocket] 无法转发消息: Electron客户端未连接')
}
} else if (role === 'electron') {
// Electron发送的消息转发给浏览器
if (this.wsClients.browser) {
log.info(`[WebSocket] Electron -> Browser: ${JSON.stringify(data)}`)
this.sendWebSocketFrame(this.wsClients.browser, message)
} else {
log.info('[WebSocket] 无法转发消息: 浏览器客户端未连接')
}
} else {
log.info(`[WebSocket] 收到来自未知角色的消息: ${message}`)
}
}
} catch (error) {
log.error('[WebSocket] 处理消息错误:', error, message)
}
}
/**
* 停止ASR服务器
* @param _event IPC事件
* @param pid 进程ID
* @returns Promise<{success: boolean, error?: string}>
*/
private async stopServer(): Promise<{ success: boolean; error?: string }> {
try {
// 关闭HTTP服务器
if (this.httpServer) {
this.httpServer.close()
this.httpServer = null
}
// 重置客户端连接
this.wsClients = { browser: null, electron: null }
// 重置服务器状态
this.isServerRunning = false
log.info('ASR服务器已停止')
return { success: true }
} catch (error) {
log.error('停止ASR服务器失败:', error)
return { success: false, error: (error as Error).message }
}
}
}
// 创建并导出单例
export const asrServerService = new ASRServerService()