import { app } from 'electron' import { join } from 'path' import { existsSync, readFileSync, writeFileSync } from 'fs' // 推理请求统一走 er_backend 代理(管理面端口,/v1/* 由后端转发到其实际管理的 llama-server) export const ER_BACKEND_URL = 'http://127.0.0.1:17700' export interface AppConfig { embed: { provider: 'local' | 'llamacpp' localModelPath: string baseUrl: string // 由 applyPlatformOverrides 固定为 er_backend 代理地址,不对用户暴露 } rag: { chunkSize: number chunkOverlap: number topK: number systemPrompt: string } // 服务的启动参数/端口由 er_backend 统一管理,以下字段仅为历史配置保留,前端不再用于路由 llamacppServer: { executablePath: string modelPath: string port: number numCtx: number parallelSlots: number threads: number gpuLayers: number noWarmup: boolean extraArgs: string } llamacppEmbed: { modelPath: string port: number extraArgs: string } hardware: { cardType: 'er160' | 'er25' } batchQuestions: string[] batchWaitSeconds: number voice: { asr: { enabled: boolean executablePath: string modelDir: string numThreads: number vadSilenceTimeoutMs: number vadEnergyThreshold: number inputGain: number punctuationModelDir: string } tts: { enabled: boolean voiceURI: string pitch: number rate: number volume: number } } } export const DEFAULT_CONFIG: AppConfig = { embed: { provider: 'llamacpp', localModelPath: '', baseUrl: ER_BACKEND_URL }, llamacppServer: { executablePath: '', modelPath: '', port: 8081, numCtx: 8192, parallelSlots: 1, threads: -1, gpuLayers: 0, noWarmup: true, extraArgs: '' }, llamacppEmbed: { modelPath: '', port: 17701, extraArgs: '--embedding --pooling last -b 8096 -ub 8096 --parallel 10' }, hardware: { cardType: 'er160' }, batchQuestions: [], batchWaitSeconds: 10, voice: { asr: { enabled: false, executablePath: '', modelDir: '', numThreads: 2, vadSilenceTimeoutMs: 5000, vadEnergyThreshold: 0.005, inputGain: 10, punctuationModelDir: '' }, tts: { enabled: false, voiceURI: '', pitch: 1, rate: 1, volume: 1 } }, rag: { chunkSize: 480, chunkOverlap: 0, topK: 5, systemPrompt: `你是一个阅读理解助手,专门帮助用户基于给定的上下文内容回答问题。 你的任务是: - 只能依据用户提供的上下文内容进行回答; - 每个答案中如果有信息来源于上下文,必须标注对应的引用标号(例如:[1],也可以是多个标号,例如:[1][3]); - 如果问题的信息在上下文中找不到,请直接回复:"我不知道。" - 不得使用自己的知识库或常识来回答问题; - 回答应简洁明了,不添加额外解释或发挥。 请严格按照用户的指示进行判断和回答。` } } function getConfigPath(): string { return join(app.getPath('userData'), 'config.json') } export function getDbPath(): string { return join(app.getPath('userData'), 'lancedb') } export function getConfig(): AppConfig { const configPath = getConfigPath() if (!existsSync(configPath)) { const config = { ...DEFAULT_CONFIG } applyPlatformOverrides(config) return config } try { const raw = JSON.parse(readFileSync(configPath, 'utf-8')) const card: 'er160' | 'er25' = (raw.hardware?.cardType as 'er160' | 'er25') ?? 'er160' // embed.provider 兼容旧 'ollama' 值 → llamacpp let embedProvider = raw.embed?.provider ?? 'llamacpp' if (embedProvider === 'ollama') embedProvider = 'llamacpp' const config: AppConfig = { embed: { provider: embedProvider as 'local' | 'llamacpp', localModelPath: raw.embed?.localModelPath ?? '', baseUrl: raw.embed?.baseUrl ?? DEFAULT_CONFIG.embed.baseUrl }, rag: { ...DEFAULT_CONFIG.rag, ...raw.rag }, llamacppServer: { ...DEFAULT_CONFIG.llamacppServer, ...raw.llamacppServer, numCtx: (raw.llamacppServer?.numCtx ?? 0) <= 4096 ? DEFAULT_CONFIG.llamacppServer.numCtx : raw.llamacppServer.numCtx, parallelSlots: raw.llamacppServer?.parallelSlots ?? DEFAULT_CONFIG.llamacppServer.parallelSlots, noWarmup: raw.llamacppServer?.noWarmup ?? DEFAULT_CONFIG.llamacppServer.noWarmup }, llamacppEmbed: { ...DEFAULT_CONFIG.llamacppEmbed, ...raw.llamacppEmbed }, hardware: { cardType: card }, batchQuestions: Array.isArray(raw.batchQuestions) ? raw.batchQuestions.filter((q: unknown) => typeof q === 'string') : [], batchWaitSeconds: typeof raw.batchWaitSeconds === 'number' ? Math.min(30, Math.max(5, raw.batchWaitSeconds)) : DEFAULT_CONFIG.batchWaitSeconds, voice: { asr: { enabled: raw.voice?.asr?.enabled === true, executablePath: typeof raw.voice?.asr?.executablePath === 'string' ? raw.voice.asr.executablePath : '', modelDir: typeof raw.voice?.asr?.modelDir === 'string' ? raw.voice.asr.modelDir : '', numThreads: typeof raw.voice?.asr?.numThreads === 'number' ? raw.voice.asr.numThreads : DEFAULT_CONFIG.voice.asr.numThreads, vadSilenceTimeoutMs: typeof raw.voice?.asr?.vadSilenceTimeoutMs === 'number' ? Math.max(1000, Math.min(30000, raw.voice.asr.vadSilenceTimeoutMs)) : DEFAULT_CONFIG.voice.asr.vadSilenceTimeoutMs, vadEnergyThreshold: typeof raw.voice?.asr?.vadEnergyThreshold === 'number' ? Math.max(0.0001, Math.min(1, raw.voice.asr.vadEnergyThreshold)) : DEFAULT_CONFIG.voice.asr.vadEnergyThreshold, inputGain: typeof raw.voice?.asr?.inputGain === 'number' ? Math.max(0.1, Math.min(100, raw.voice.asr.inputGain)) : DEFAULT_CONFIG.voice.asr.inputGain, punctuationModelDir: typeof raw.voice?.asr?.punctuationModelDir === 'string' ? raw.voice.asr.punctuationModelDir : DEFAULT_CONFIG.voice.asr.punctuationModelDir }, tts: { enabled: raw.voice?.tts?.enabled === true, voiceURI: typeof raw.voice?.tts?.voiceURI === 'string' ? raw.voice.tts.voiceURI : '', pitch: typeof raw.voice?.tts?.pitch === 'number' ? raw.voice.tts.pitch : DEFAULT_CONFIG.voice.tts.pitch, rate: typeof raw.voice?.tts?.rate === 'number' ? raw.voice.tts.rate : DEFAULT_CONFIG.voice.tts.rate, volume: typeof raw.voice?.tts?.volume === 'number' ? raw.voice.tts.volume : DEFAULT_CONFIG.voice.tts.volume } } } applyPlatformOverrides(config) return config } catch { const config = { ...DEFAULT_CONFIG } applyPlatformOverrides(config) return config } } // 按计算卡型号覆盖 embed provider 和默认启动参数 // 推理请求统一走 er_backend 代理(ER_BACKEND_URL),服务端口以后端配置为准,前端不再维护 function applyPlatformOverrides(config: AppConfig): void { const card = config.hardware.cardType if (card === 'er25') { // ER25:嵌入固定本地 CPU,无嵌入服务进程 config.embed.provider = 'local' if (!config.llamacppServer.extraArgs.trim()) { config.llamacppServer.extraArgs = '--port 8080 --ctk bf16 --ctv bf16 --no-warmup -ub 128' } } else { // ER160:嵌入默认 llamacpp NPU(local 保持不变) if (config.embed.provider !== 'local') config.embed.provider = 'llamacpp' if (!config.llamacppServer.extraArgs.trim()) { config.llamacppServer.extraArgs = '--port 8081 --presence_penalty 1.5' } if (!config.llamacppEmbed.extraArgs.trim()) { config.llamacppEmbed.extraArgs = DEFAULT_CONFIG.llamacppEmbed.extraArgs } } // 嵌入/生成服务地址固定为 er_backend 代理 config.embed.baseUrl = ER_BACKEND_URL } export function saveConfig(config: AppConfig): void { writeFileSync(getConfigPath(), JSON.stringify(config, null, 2), 'utf-8') }