ssurui 21f9c7e453 feat: 实现拍照提问模式 MVP(阶段1+2)
按 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>
2026-09-03 14:50:52 +08:00

100 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
})