按 docs/feature-pic-design.md 的方案实现: - AppConfig 新增 vision 字段(enabled 总开关、cameraDeviceId 记住上次 选用的摄像头),主进程/渲染层类型两处同步 - generator.ts 抽出共享的 streamChatCompletion(),新增 streamGenerateVision() 支持 OpenAI 视觉 API 的 image_url content-part, 文字问答路径不受影响 - 新增 visionHandler.ts(vision-query IPC,跳过向量检索,直接转发 图片+问题给 er_backend 代理),注册进 ipc/index.ts - 新增 CameraCapture.vue:getUserMedia 取景/拍照/重拍/多摄像头切换, 纯交互组件不掺业务逻辑 - ChatPanel.vue 接入拍照入口(仅 vision.enabled 时显示)、待发送图片 缩略图、问题气泡里的图片展示;抽出 beginQuery() 复用文字/拍照两条 路径共同的状态重置逻辑 - SettingsModal.vue 加拍照提问开关 - index.ts 权限处理器注释说明 getUserMedia 摄像头请求也走 'media' 不做检索、不做照片持久化、不做多摄像头预配置界面(MVP 范围见设计文档 §3)。 本地未跑 vue-tsc,接下来在真机上验证类型检查 + 构建 + 端到端联调。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
3.8 KiB
TypeScript
100 lines
3.8 KiB
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
|
||
|
||
console.log('[preload] 正在注册 electronAPI...')
|
||
|
||
contextBridge.exposeInMainWorld('electronAPI', {
|
||
// 文件选择
|
||
selectFiles: (): Promise<string[]> =>
|
||
ipcRenderer.invoke('select-files'),
|
||
selectDirectory: (): Promise<string | null> =>
|
||
ipcRenderer.invoke('select-directory'),
|
||
|
||
// 入库
|
||
indexDocuments: (payload: { paths: string[]; mode: 'append' | 'rebuild' }): Promise<void> =>
|
||
ipcRenderer.invoke('index-documents', payload),
|
||
|
||
// 查询
|
||
query: (payload: { question: string; systemPrompt: string; topK: number }): Promise<void> =>
|
||
ipcRenderer.invoke('query', payload),
|
||
|
||
// 拍照提问(不走检索,直接把图片+问题发给多模态生成服务)
|
||
visionQuery: (payload: { question: string; imageDataUrl: string }): Promise<void> =>
|
||
ipcRenderer.invoke('vision-query', payload),
|
||
|
||
// 状态 & 配置
|
||
getDbStatus: (): Promise<{ count: number; ready: boolean }> =>
|
||
ipcRenderer.invoke('get-db-status'),
|
||
getConfig: () => ipcRenderer.invoke('get-config'),
|
||
saveConfig: (config: unknown) => ipcRenderer.invoke('save-config', config),
|
||
testEmbedConnection: (embedConfig: unknown): Promise<{ ok: boolean; message: string }> =>
|
||
ipcRenderer.invoke('embed-test', embedConfig),
|
||
testChatConnection: (chatConfig: unknown): Promise<{ ok: boolean; message: string }> =>
|
||
ipcRenderer.invoke('chat-test', chatConfig),
|
||
|
||
// 本地嵌入模型
|
||
selectModelDir: (): Promise<string | null> =>
|
||
ipcRenderer.invoke('select-model-dir'),
|
||
checkModelPath: (dirPath: string): Promise<boolean> =>
|
||
ipcRenderer.invoke('check-model-path', dirPath),
|
||
|
||
// 退出程序
|
||
quitApp: (): Promise<void> =>
|
||
ipcRenderer.invoke('quit-app'),
|
||
|
||
// 批量问题导入导出
|
||
importBatchQuestions: (): Promise<string[] | null> =>
|
||
ipcRenderer.invoke('batch:import'),
|
||
exportBatchQuestions: (questions: string[]): Promise<boolean> =>
|
||
ipcRenderer.invoke('batch:export', questions),
|
||
|
||
// 硬件探测
|
||
hardwareGetDetected: (): Promise<'er160' | 'er25' | 'none' | null> =>
|
||
ipcRenderer.invoke('hardware:get-detected'),
|
||
|
||
// 平台信息
|
||
platform: process.platform,
|
||
|
||
// 语音输入(ASR)
|
||
asrSelectModelDir: (): Promise<string | null> =>
|
||
ipcRenderer.invoke('asr:select-model-dir'),
|
||
asrSelectExecutable: (): Promise<string | null> =>
|
||
ipcRenderer.invoke('asr:select-executable'),
|
||
asrCheckModelDir: (dirPath: string): Promise<boolean> =>
|
||
ipcRenderer.invoke('asr:check-model-dir', dirPath),
|
||
asrInitModel: (): Promise<boolean> =>
|
||
ipcRenderer.invoke('asr:init-model'),
|
||
asrStartStream: (): Promise<boolean> =>
|
||
ipcRenderer.invoke('asr:start-stream'),
|
||
asrFeedChunk: (samples: number[]): Promise<boolean> =>
|
||
ipcRenderer.invoke('asr:feed-chunk', samples),
|
||
asrStopStream: (): Promise<string> =>
|
||
ipcRenderer.invoke('asr:stop-stream'),
|
||
asrUnloadModel: (): Promise<boolean> =>
|
||
ipcRenderer.invoke('asr:unload-model'),
|
||
|
||
// 语音输出(系统 TTS 兜底)
|
||
ttsAvailable: (): Promise<boolean> =>
|
||
ipcRenderer.invoke('tts:available'),
|
||
ttsConfigure: (options: unknown): Promise<boolean> =>
|
||
ipcRenderer.invoke('tts:configure', options),
|
||
ttsSpeak: (text: string): Promise<boolean> =>
|
||
ipcRenderer.invoke('tts:speak', text),
|
||
ttsStop: (): Promise<boolean> =>
|
||
ipcRenderer.invoke('tts:stop'),
|
||
|
||
// er-backend 管理服务(Linux NPU)
|
||
erBackendEnsure: (): Promise<{ ok: boolean; message: string }> =>
|
||
ipcRenderer.invoke('er-backend:ensure'),
|
||
erBackendStatus: (): Promise<unknown> =>
|
||
ipcRenderer.invoke('er-backend:status'),
|
||
|
||
// 事件监听(ASR/TTS 推送事件)
|
||
on: (channel: string, callback: (...args: unknown[]) => void) => {
|
||
ipcRenderer.removeAllListeners(channel)
|
||
ipcRenderer.on(channel, (_event, ...args) => callback(...args))
|
||
},
|
||
off: (channel: string) => {
|
||
ipcRenderer.removeAllListeners(channel)
|
||
}
|
||
})
|