mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
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.
This commit is contained in:
@@ -2605,8 +2605,9 @@ function registerIpcHandlers() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// HTTP API 服务
|
// HTTP API 服务
|
||||||
ipcMain.handle('http:start', async (_, port?: number) => {
|
ipcMain.handle('http:start', async (_, port?: number, host?: string) => {
|
||||||
return httpService.start(port || 5031)
|
const bindHost = typeof host === 'string' && host.trim() ? host.trim() : '127.0.0.1'
|
||||||
|
return httpService.start(port || 5031, bindHost)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('http:stop', async () => {
|
ipcMain.handle('http:stop', async () => {
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
|
|
||||||
// HTTP API 服务
|
// HTTP API 服务
|
||||||
http: {
|
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'),
|
stop: () => ipcRenderer.invoke('http:stop'),
|
||||||
status: () => ipcRenderer.invoke('http:status')
|
status: () => ipcRenderer.invoke('http:status')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ interface ConfigSchema {
|
|||||||
messagePushEnabled: boolean
|
messagePushEnabled: boolean
|
||||||
httpApiEnabled: boolean
|
httpApiEnabled: boolean
|
||||||
httpApiPort: number
|
httpApiPort: number
|
||||||
|
httpApiHost: string
|
||||||
httpApiToken: string
|
httpApiToken: string
|
||||||
windowCloseBehavior: 'ask' | 'tray' | 'quit'
|
windowCloseBehavior: 'ask' | 'tray' | 'quit'
|
||||||
quoteLayout: 'quote-top' | 'quote-bottom'
|
quoteLayout: 'quote-top' | 'quote-bottom'
|
||||||
@@ -125,6 +126,7 @@ export class ConfigService {
|
|||||||
httpApiToken: '',
|
httpApiToken: '',
|
||||||
httpApiEnabled: false,
|
httpApiEnabled: false,
|
||||||
httpApiPort: 5031,
|
httpApiPort: 5031,
|
||||||
|
httpApiHost: '127.0.0.1',
|
||||||
messagePushEnabled: false,
|
messagePushEnabled: false,
|
||||||
windowCloseBehavior: 'ask',
|
windowCloseBehavior: 'ask',
|
||||||
quoteLayout: 'quote-top',
|
quoteLayout: 'quote-top',
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class HttpService {
|
|||||||
private server: http.Server | null = null
|
private server: http.Server | null = null
|
||||||
private configService: ConfigService
|
private configService: ConfigService
|
||||||
private port: number = 5031
|
private port: number = 5031
|
||||||
|
private host: string = '127.0.0.1'
|
||||||
private running: boolean = false
|
private running: boolean = false
|
||||||
private connections: Set<import('net').Socket> = new Set()
|
private connections: Set<import('net').Socket> = new Set()
|
||||||
private messagePushClients: Set<http.ServerResponse> = new Set()
|
private messagePushClients: Set<http.ServerResponse> = new Set()
|
||||||
@@ -114,12 +115,13 @@ class HttpService {
|
|||||||
/**
|
/**
|
||||||
* 启动 HTTP 服务
|
* 启动 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) {
|
if (this.running && this.server) {
|
||||||
return { success: true, port: this.port }
|
return { success: true, port: this.port }
|
||||||
}
|
}
|
||||||
|
|
||||||
this.port = port
|
this.port = port
|
||||||
|
this.host = host
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.server = http.createServer((req, res) => this.handleRequest(req, res))
|
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.running = true
|
||||||
this.startMessagePushHeartbeat()
|
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 })
|
resolve({ success: true, port: this.port })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -225,7 +227,7 @@ class HttpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMessagePushStreamUrl(): string {
|
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<string, unknown>): void {
|
broadcastMessagePush(payload: Record<string, unknown>): void {
|
||||||
@@ -250,8 +252,9 @@ class HttpService {
|
|||||||
const enabled = this.configService.get('httpApiEnabled')
|
const enabled = this.configService.get('httpApiEnabled')
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
const port = Number(this.configService.get('httpApiPort')) || 5031
|
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 {
|
try {
|
||||||
await this.start(port)
|
await this.start(port, host)
|
||||||
console.log(`[HttpService] Auto-started on port ${port}`)
|
console.log(`[HttpService] Auto-started on port ${port}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[HttpService] Auto-start failed:', err)
|
console.error('[HttpService] Auto-start failed:', err)
|
||||||
@@ -314,7 +317,7 @@ class HttpService {
|
|||||||
return
|
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
|
const pathname = url.pathname
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -961,7 +964,7 @@ class HttpService {
|
|||||||
parsedContent: msg.parsedContent,
|
parsedContent: msg.parsedContent,
|
||||||
mediaType: media?.kind,
|
mediaType: media?.kind,
|
||||||
mediaFileName: media?.fileName,
|
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
|
mediaLocalPath: media?.fullPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1231,7 +1234,7 @@ class HttpService {
|
|||||||
type: this.mapMessageType(msg.localType, msg),
|
type: this.mapMessageType(msg.localType, msg),
|
||||||
content: this.getMessageContent(msg),
|
content: this.getMessageContent(msg),
|
||||||
platformMessageId: msg.serverId ? String(msg.serverId) : undefined,
|
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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ export class VoiceTranscribeService {
|
|||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
console.warn(`[VoiceTranscribe] 未找到 ${platformPkg} 目录,可能导致语音引擎加载失败`)
|
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
|
return env
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
// HTTP API 设置 state
|
// HTTP API 设置 state
|
||||||
const [httpApiEnabled, setHttpApiEnabled] = useState(false)
|
const [httpApiEnabled, setHttpApiEnabled] = useState(false)
|
||||||
const [httpApiPort, setHttpApiPort] = useState(5031)
|
const [httpApiPort, setHttpApiPort] = useState(5031)
|
||||||
|
const [httpApiHost, setHttpApiHost] = useState('127.0.0.1')
|
||||||
const [httpApiRunning, setHttpApiRunning] = useState(false)
|
const [httpApiRunning, setHttpApiRunning] = useState(false)
|
||||||
const [httpApiMediaExportPath, setHttpApiMediaExportPath] = useState('')
|
const [httpApiMediaExportPath, setHttpApiMediaExportPath] = useState('')
|
||||||
const [isTogglingApi, setIsTogglingApi] = useState(false)
|
const [isTogglingApi, setIsTogglingApi] = useState(false)
|
||||||
@@ -349,6 +350,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
const savedApiPort = await configService.getHttpApiPort()
|
const savedApiPort = await configService.getHttpApiPort()
|
||||||
if (savedApiPort) setHttpApiPort(savedApiPort)
|
if (savedApiPort) setHttpApiPort(savedApiPort)
|
||||||
|
|
||||||
|
const savedApiHost = await configService.getHttpApiHost()
|
||||||
|
if (savedApiHost) setHttpApiHost(savedApiHost)
|
||||||
|
|
||||||
setAuthEnabled(savedAuthEnabled)
|
setAuthEnabled(savedAuthEnabled)
|
||||||
setAuthUseHello(savedAuthUseHello)
|
setAuthUseHello(savedAuthUseHello)
|
||||||
setIsLockMode(savedIsLockMode)
|
setIsLockMode(savedIsLockMode)
|
||||||
@@ -1871,7 +1875,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
setShowApiWarning(false)
|
setShowApiWarning(false)
|
||||||
setIsTogglingApi(true)
|
setIsTogglingApi(true)
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.http.start(httpApiPort)
|
const result = await window.electronAPI.http.start(httpApiPort, httpApiHost)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setHttpApiRunning(true)
|
setHttpApiRunning(true)
|
||||||
if (result.port) setHttpApiPort(result.port)
|
if (result.port) setHttpApiPort(result.port)
|
||||||
@@ -1891,7 +1895,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCopyApiUrl = () => {
|
const handleCopyApiUrl = () => {
|
||||||
const url = `http://127.0.0.1:${httpApiPort}`
|
const url = `http://${httpApiHost}:${httpApiPort}`
|
||||||
navigator.clipboard.writeText(url)
|
navigator.clipboard.writeText(url)
|
||||||
showMessage('已复制 API 地址', true)
|
showMessage('已复制 API 地址', true)
|
||||||
}
|
}
|
||||||
@@ -1923,6 +1927,26 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>监听地址</label>
|
||||||
|
<span className="form-hint">
|
||||||
|
API 服务绑定的主机地址。默认 <code>127.0.0.1</code> 仅本机访问;Docker/N8N 等容器场景请改为 <code>0.0.0.0</code> 以允许外部访问(注意配合 Token 鉴权)
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="field-input"
|
||||||
|
value={httpApiHost}
|
||||||
|
placeholder="127.0.0.1"
|
||||||
|
onChange={(e) => {
|
||||||
|
const host = e.target.value.trim() || '127.0.0.1'
|
||||||
|
setHttpApiHost(host)
|
||||||
|
scheduleConfigSave('httpApiHost', () => configService.setHttpApiHost(host))
|
||||||
|
}}
|
||||||
|
disabled={httpApiRunning}
|
||||||
|
style={{ width: 180, fontFamily: 'monospace' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>服务端口</label>
|
<label>服务端口</label>
|
||||||
<span className="form-hint">API 服务监听的端口号(1024-65535)</span>
|
<span className="form-hint">API 服务监听的端口号(1024-65535)</span>
|
||||||
@@ -1980,7 +2004,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="field-input"
|
className="field-input"
|
||||||
value={`http://127.0.0.1:${httpApiPort}`}
|
value={`http://${httpApiHost}:${httpApiPort}`}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<button className="btn btn-secondary" onClick={handleCopyApiUrl} title="复制">
|
<button className="btn btn-secondary" onClick={handleCopyApiUrl} title="复制">
|
||||||
@@ -2029,13 +2053,13 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="field-input"
|
className="field-input"
|
||||||
value={`http://127.0.0.1:${httpApiPort}/api/v1/push/messages${httpApiToken ? `?access_token=${httpApiToken}` : ''}`}
|
value={`http://${httpApiHost}:${httpApiPort}/api/v1/push/messages${httpApiToken ? `?access_token=${httpApiToken}` : ''}`}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(`http://127.0.0.1:${httpApiPort}/api/v1/push/messages${httpApiToken ? `?access_token=${httpApiToken}` : ''}`)
|
navigator.clipboard.writeText(`http://${httpApiHost}:${httpApiPort}/api/v1/push/messages${httpApiToken ? `?access_token=${httpApiToken}` : ''}`)
|
||||||
showMessage('已复制推送地址', true)
|
showMessage('已复制推送地址', true)
|
||||||
}}
|
}}
|
||||||
title="复制"
|
title="复制"
|
||||||
@@ -2052,7 +2076,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
<div className="api-item">
|
<div className="api-item">
|
||||||
<div className="api-endpoint">
|
<div className="api-endpoint">
|
||||||
<span className="method get">GET</span>
|
<span className="method get">GET</span>
|
||||||
<code>{`http://127.0.0.1:${httpApiPort}/api/v1/push/messages`}</code>
|
<code>{`http://${httpApiHost}:${httpApiPort}/api/v1/push/messages`}</code>
|
||||||
</div>
|
</div>
|
||||||
<p className="api-desc">通过 SSE 长连接接收消息事件,建议接收端按 `messageKey` 去重。</p>
|
<p className="api-desc">通过 SSE 长连接接收消息事件,建议接收端按 `messageKey` 去重。</p>
|
||||||
<div className="api-params">
|
<div className="api-params">
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const CONFIG_KEYS = {
|
|||||||
HTTP_API_TOKEN: 'httpApiToken',
|
HTTP_API_TOKEN: 'httpApiToken',
|
||||||
HTTP_API_ENABLED: 'httpApiEnabled',
|
HTTP_API_ENABLED: 'httpApiEnabled',
|
||||||
HTTP_API_PORT: 'httpApiPort',
|
HTTP_API_PORT: 'httpApiPort',
|
||||||
|
HTTP_API_HOST: 'httpApiHost',
|
||||||
MESSAGE_PUSH_ENABLED: 'messagePushEnabled',
|
MESSAGE_PUSH_ENABLED: 'messagePushEnabled',
|
||||||
WINDOW_CLOSE_BEHAVIOR: 'windowCloseBehavior',
|
WINDOW_CLOSE_BEHAVIOR: 'windowCloseBehavior',
|
||||||
QUOTE_LAYOUT: 'quoteLayout',
|
QUOTE_LAYOUT: 'quoteLayout',
|
||||||
@@ -1509,3 +1510,12 @@ export async function getHttpApiPort(): Promise<number> {
|
|||||||
export async function setHttpApiPort(port: number): Promise<void> {
|
export async function setHttpApiPort(port: number): Promise<void> {
|
||||||
await config.set(CONFIG_KEYS.HTTP_API_PORT, port)
|
await config.set(CONFIG_KEYS.HTTP_API_PORT, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getHttpApiHost(): Promise<string> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.HTTP_API_HOST)
|
||||||
|
return typeof value === 'string' && value.trim() ? value.trim() : '127.0.0.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setHttpApiHost(host: string): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.HTTP_API_HOST, host)
|
||||||
|
}
|
||||||
|
|||||||
2
src/types/electron.d.ts
vendored
2
src/types/electron.d.ts
vendored
@@ -838,7 +838,7 @@ export interface ElectronAPI {
|
|||||||
getLogs: () => Promise<string[]>
|
getLogs: () => Promise<string[]>
|
||||||
}
|
}
|
||||||
http: {
|
http: {
|
||||||
start: (port?: number) => Promise<{ success: boolean; port?: number; error?: string }>
|
start: (port?: number, host?: string) => Promise<{ success: boolean; port?: number; error?: string }>
|
||||||
stop: () => Promise<{ success: boolean }>
|
stop: () => Promise<{ success: boolean }>
|
||||||
status: () => Promise<{ running: boolean; port: number; mediaExportPath: string }>
|
status: () => Promise<{ running: boolean; port: number; mediaExportPath: string }>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user