From 83f50cbaee7f71e9ab63f08bd6d4dc1c189396f1 Mon Sep 17 00:00:00 2001 From: hicccc77 <98377878+hicccc77@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:10:16 +0800 Subject: [PATCH] fix: support configurable bind host for HTTP API and fix Windows sherpa-onnx PATH - fix(#547): HTTP API server now supports configurable bind host (default 127.0.0.1) Docker/N8N users can set host to 0.0.0.0 in settings to allow container access. Adds httpApiHost config key, UI input in settings, and passes host through IPC chain (preload -> main -> httpService). - fix(#546): Add Windows PATH injection for sherpa-onnx native module buildTranscribeWorkerEnv() now adds the sherpa-onnx-win-x64 directory to PATH on Windows, fixing 'Could not find sherpa-onnx-node' errors caused by missing DLL search path in forked worker processes. --- electron/main.ts | 5 +-- electron/preload.ts | 2 +- electron/services/config.ts | 2 ++ electron/services/httpService.ts | 19 ++++++----- electron/services/voiceTranscribeService.ts | 8 +++++ src/pages/SettingsPage.tsx | 36 +++++++++++++++++---- src/services/config.ts | 10 ++++++ src/types/electron.d.ts | 2 +- 8 files changed, 66 insertions(+), 18 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index c0a6aa6..8321601 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -2605,8 +2605,9 @@ function registerIpcHandlers() { }) // HTTP API 服务 - ipcMain.handle('http:start', async (_, port?: number) => { - return httpService.start(port || 5031) + ipcMain.handle('http:start', async (_, port?: number, host?: string) => { + const bindHost = typeof host === 'string' && host.trim() ? host.trim() : '127.0.0.1' + return httpService.start(port || 5031, bindHost) }) ipcMain.handle('http:stop', async () => { diff --git a/electron/preload.ts b/electron/preload.ts index 41d8246..54e68fe 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -422,7 +422,7 @@ contextBridge.exposeInMainWorld('electronAPI', { // HTTP API 服务 http: { - start: (port?: number) => ipcRenderer.invoke('http:start', port), + start: (port?: number, host?: string) => ipcRenderer.invoke('http:start', port, host), stop: () => ipcRenderer.invoke('http:stop'), status: () => ipcRenderer.invoke('http:status') } diff --git a/electron/services/config.ts b/electron/services/config.ts index 47939bf..5fdd609 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -54,6 +54,7 @@ interface ConfigSchema { messagePushEnabled: boolean httpApiEnabled: boolean httpApiPort: number + httpApiHost: string httpApiToken: string windowCloseBehavior: 'ask' | 'tray' | 'quit' quoteLayout: 'quote-top' | 'quote-bottom' @@ -125,6 +126,7 @@ export class ConfigService { httpApiToken: '', httpApiEnabled: false, httpApiPort: 5031, + httpApiHost: '127.0.0.1', messagePushEnabled: false, windowCloseBehavior: 'ask', quoteLayout: 'quote-top', diff --git a/electron/services/httpService.ts b/electron/services/httpService.ts index c67770e..02fa030 100644 --- a/electron/services/httpService.ts +++ b/electron/services/httpService.ts @@ -101,6 +101,7 @@ class HttpService { private server: http.Server | null = null private configService: ConfigService private port: number = 5031 + private host: string = '127.0.0.1' private running: boolean = false private connections: Set = new Set() private messagePushClients: Set = new Set() @@ -114,12 +115,13 @@ class HttpService { /** * 启动 HTTP 服务 */ - async start(port: number = 5031): Promise<{ success: boolean; port?: number; error?: string }> { + async start(port: number = 5031, host: string = '127.0.0.1'): Promise<{ success: boolean; port?: number; error?: string }> { if (this.running && this.server) { return { success: true, port: this.port } } this.port = port + this.host = host return new Promise((resolve) => { this.server = http.createServer((req, res) => this.handleRequest(req, res)) @@ -153,10 +155,10 @@ class HttpService { } }) - this.server.listen(this.port, '127.0.0.1', () => { + this.server.listen(this.port, this.host, () => { this.running = true this.startMessagePushHeartbeat() - console.log(`[HttpService] HTTP API server started on http://127.0.0.1:${this.port}`) + console.log(`[HttpService] HTTP API server started on http://${this.host}:${this.port}`) resolve({ success: true, port: this.port }) }) }) @@ -225,7 +227,7 @@ class HttpService { } getMessagePushStreamUrl(): string { - return `http://127.0.0.1:${this.port}/api/v1/push/messages` + return `http://${this.host}:${this.port}/api/v1/push/messages` } broadcastMessagePush(payload: Record): void { @@ -250,8 +252,9 @@ class HttpService { const enabled = this.configService.get('httpApiEnabled') if (enabled) { const port = Number(this.configService.get('httpApiPort')) || 5031 + const host = String(this.configService.get('httpApiHost') || '127.0.0.1').trim() || '127.0.0.1' try { - await this.start(port) + await this.start(port, host) console.log(`[HttpService] Auto-started on port ${port}`) } catch (err) { console.error('[HttpService] Auto-start failed:', err) @@ -314,7 +317,7 @@ class HttpService { return } - const url = new URL(req.url || '/', `http://127.0.0.1:${this.port}`) + const url = new URL(req.url || '/', `http://${this.host}:${this.port}`) const pathname = url.pathname try { @@ -961,7 +964,7 @@ class HttpService { parsedContent: msg.parsedContent, mediaType: media?.kind, mediaFileName: media?.fileName, - mediaUrl: media ? `http://127.0.0.1:${this.port}/api/v1/media/${media.relativePath}` : undefined, + mediaUrl: media ? `http://${this.host}:${this.port}/api/v1/media/${media.relativePath}` : undefined, mediaLocalPath: media?.fullPath } } @@ -1231,7 +1234,7 @@ class HttpService { type: this.mapMessageType(msg.localType, msg), content: this.getMessageContent(msg), platformMessageId: msg.serverId ? String(msg.serverId) : undefined, - mediaPath: mediaMap.get(msg.localId) ? `http://127.0.0.1:${this.port}/api/v1/media/${mediaMap.get(msg.localId)!.relativePath}` : undefined + mediaPath: mediaMap.get(msg.localId) ? `http://${this.host}:${this.port}/api/v1/media/${mediaMap.get(msg.localId)!.relativePath}` : undefined } }) diff --git a/electron/services/voiceTranscribeService.ts b/electron/services/voiceTranscribeService.ts index 107cbc5..952bac9 100644 --- a/electron/services/voiceTranscribeService.ts +++ b/electron/services/voiceTranscribeService.ts @@ -75,6 +75,14 @@ export class VoiceTranscribeService { if (candidates.length === 0) { console.warn(`[VoiceTranscribe] 未找到 ${platformPkg} 目录,可能导致语音引擎加载失败`) } + } else if (process.platform === 'win32') { + // Windows: 把 sherpa-onnx DLL 所在目录加到 PATH,否则 native module 找不到依赖 + const existing = env['PATH'] || '' + const merged = [...candidates, ...existing.split(';').filter(Boolean)] + env['PATH'] = Array.from(new Set(merged)).join(';') + if (candidates.length === 0) { + console.warn(`[VoiceTranscribe] 未找到 ${platformPkg} 目录,可能导致语音引擎加载失败`) + } } return env diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index de8d6f7..36e1776 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -190,6 +190,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { // HTTP API 设置 state const [httpApiEnabled, setHttpApiEnabled] = useState(false) const [httpApiPort, setHttpApiPort] = useState(5031) + const [httpApiHost, setHttpApiHost] = useState('127.0.0.1') const [httpApiRunning, setHttpApiRunning] = useState(false) const [httpApiMediaExportPath, setHttpApiMediaExportPath] = useState('') const [isTogglingApi, setIsTogglingApi] = useState(false) @@ -349,6 +350,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedApiPort = await configService.getHttpApiPort() if (savedApiPort) setHttpApiPort(savedApiPort) + const savedApiHost = await configService.getHttpApiHost() + if (savedApiHost) setHttpApiHost(savedApiHost) + setAuthEnabled(savedAuthEnabled) setAuthUseHello(savedAuthUseHello) setIsLockMode(savedIsLockMode) @@ -1871,7 +1875,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setShowApiWarning(false) setIsTogglingApi(true) try { - const result = await window.electronAPI.http.start(httpApiPort) + const result = await window.electronAPI.http.start(httpApiPort, httpApiHost) if (result.success) { setHttpApiRunning(true) if (result.port) setHttpApiPort(result.port) @@ -1891,7 +1895,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { } const handleCopyApiUrl = () => { - const url = `http://127.0.0.1:${httpApiPort}` + const url = `http://${httpApiHost}:${httpApiPort}` navigator.clipboard.writeText(url) showMessage('已复制 API 地址', true) } @@ -1923,6 +1927,26 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { +
+ + + API 服务绑定的主机地址。默认 127.0.0.1 仅本机访问;Docker/N8N 等容器场景请改为 0.0.0.0 以允许外部访问(注意配合 Token 鉴权) + + { + const host = e.target.value.trim() || '127.0.0.1' + setHttpApiHost(host) + scheduleConfigSave('httpApiHost', () => configService.setHttpApiHost(host)) + }} + disabled={httpApiRunning} + style={{ width: 180, fontFamily: 'monospace' }} + /> +
+
API 服务监听的端口号(1024-65535) @@ -1980,7 +2004,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {