# CLAUDE.md — elec_rag_ui 项目约定与开发指南
本文件供 Claude Code 在协助开发此项目时参考。所有文档、注释、提交信息均使用中文。
## 项目概述
**elec_rag_ui** 是智能电子手册的开源前端工程,**仅支持 Linux(Kylin V10 / Ubuntu)**,**仅支持 ER160(揽月 M50 NPU)和 ER25(芯动力 RPP)硬件**。
服务进程管理(llama-server / embed-server 的启动停止)**完全委托给 er_backend**(HTTP 17700),前端不直接 spawn 任何推理进程。设置页面不再提供计算卡型号、嵌入模型、生成模型路径等配置入口,这些均由 er_backend Web 管理界面(端口 17700)配置。
本仓库从 monorepo(electronrag)通过 `scripts/sync-elec-rag-ui.sh` 单向同步而来,历史不保留。
## 技术栈
- **框架**:Electron + electron-vite + Vue3 + TypeScript
- **UI 组件库**:Naive UI(弹窗/通知/滑出容器)+ 自定义工业风 CSS
- **状态管理**:Pinia
- **向量数据库**:LanceDB(`@lancedb/lancedb`,嵌入式)
- **本地向量化**:`@huggingface/transformers`(底层 `onnxruntime-node`)
- **Markdown 渲染**:`marked`
- **打包**:electron-builder(Linux AppImage + deb)。**arm64 deb 在真机 Kylin 上走 electron-builder 自带的 fpm 路径必现 `tar failed (exit code 2)`**,实际可用的是 `scripts/package-kylin-arm64-deb.sh`(`npm run package:kylin:arm64:deb`),改用 `electron-builder --linux dir --arm64` 产出 unpacked 目录后手工 `dpkg-deb` 打包,已在真机验证
## 文件说明
```
src/
├── main/
│ ├── index.ts # 主进程:disableHardwareAcceleration, setFullScreen, Menu null
│ ├── config.ts # 读写 config.json,含 applyPlatformOverrides
│ └── ipc/
│ ├── index.ts # 注册所有 IPC 处理器
│ ├── indexHandler.ts # 文件选择、文档入库(分块→向量化→写入LanceDB)
│ ├── queryHandler.ts # 查询(向量化→检索→流式生成),query:done 含 GeneratorStats
│ ├── statusHandler.ts # 数据库状态、配置读写、测试连接(无 Ollama 分支)
│ ├── erBackendHandler.ts # er_backend HTTP 17700 代理 IPC(启动/停止/状态)
│ ├── hardwareHandler.ts # 硬件探测:hardware:get-detected / hardware:detected
│ └── npuHandler.ts # NPU 状态轮询(hm_smi),推送 npu:stats
│ └── rag/
│ ├── splitter.ts # 递归收集 .md/.txt,自实现递归字符分块
│ ├── embedder.ts # 向量化路由:local / llamacpp(无 Ollama 分支)
│ ├── localEmbedder.ts # 本地 CPU 向量化(Transformers.js 单例,懒加载)
│ ├── generator.ts # 流式生成:仅 llamacpp(接受 llamacppServer 配置)
│ ├── hardwareDetector.ts # 探测 hm_smi / rpp-info 判断 er160/er25/none
│ └── ollamaModels.ts # (保留文件,此项目不使用 Ollama)
├── preload/index.ts # contextBridge 暴露 electronAPI(无 llamaServer IPC)
└── renderer/src/
├── assets/main.css # CSS 变量(色板)、.i-btn 按钮体系
├── App.vue # 根布局:顶栏、主区;服务状态由 SSE 维护
├── components/
│ ├── IndexPanel.vue # 文档入库面板
│ ├── ChatPanel.vue # 问答气泡、Think 块、引用徽章、批量发送
│ ├── ContextDrawer.vue # 检索片段抽屉
│ ├── SettingsModal.vue # 设置抽屉(仅 RAG 参数 + 语音 + 批量测试)
│ └── ServiceControl.vue # 顶栏"启用/停止"按钮(走 er_backend HTTP)
├── stores/rag.ts # Pinia store
└── types/index.ts # TS 接口 + Window.electronAPI 类型声明
```
## AppConfig 结构
```typescript
interface AppConfig {
embed: {
provider: 'local' | 'llamacpp' // 无 Ollama 分支
localModelPath: string // 本地 onnx 模型目录
// baseUrl / model 由 applyPlatformOverrides 内部推导,不暴露给用户
}
rag: { chunkSize: number; chunkOverlap: number; topK: number; systemPrompt: string }
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 }
}
}
```
**注意**:本项目无 `chat` 字段(生成固定走 llamacppServer)。`embed.baseUrl` 字段存在但不对用户暴露,由 `applyPlatformOverrides` 固定推导。
## applyPlatformOverrides 逻辑
`getConfig()` 末尾调用。**自 `836d25c` 起,推理请求(生成 + 嵌入)统一走 er_backend 代理**(`ER_BACKEND_URL = 'http://127.0.0.1:17700'`),不再由前端按 cardType 自己推导 llama-server/embed-server 端口——服务实际监听哪个端口完全由 er_backend 后端配置决定,前端只认这一个代理入口:
- `config.embed.baseUrl` 恒等于 `ER_BACKEND_URL`,与 cardType 无关
- `generator.ts`、`statusHandler.ts` 的 `chat-test`、`erBackendHandler.ts` 全部请求 `ER_BACKEND_URL/v1/*`
`applyPlatformOverrides` 现在只负责按计算卡型号补默认启动参数(`extraArgs` 为空时才生效):
**ER160(揽月 M50)**:`embed.provider` 若不是 `local` 则改为 `llamacpp`;`llamacppServer.extraArgs` 默认 `'--port 8081 --presence_penalty 1.5'`;`llamacppEmbed.extraArgs` 默认走 `DEFAULT_CONFIG` 里的值
**ER25(芯动力 RPP)**:`embed.provider = 'local'`(强制,本地 CPU);`llamacppServer.extraArgs` 默认 `'--port 8080 --ctk bf16 --ctv bf16 --no-warmup -ub 128'`
## SettingsModal.vue 结构(精简后)
```
NDrawer (width=900)
├── .s-tabs 'index' | 'basic' | 'voice' | 'batch' | 'log'
├── 文档入库
├── 基本设置 检索参数(Top-K + 系统提示词)── 无计算卡/模型配置
├── 语音设置 TTS/ASR 参数
├── 批量测试 问题列表 + 等待时间滑块
└── 服务日志 嵌入/生成服务日志框(isLinux 可见)
```
计算卡型号、嵌入模型路径、生成模型路径等配置已从设置页移除,统一由 er_backend(http://:17700)管理。
## er_backend 集成
**ServiceControl.vue** 始终走 er_backend HTTP(`useErBackend = true`):
- 启动:`POST /api/generate/start`,`POST /api/embed/start`(ER25 跳过)
- 停止:`POST /api/generate/stop`,`POST /api/embed/stop`
- 状态 SSE:`GET /api/generate/logs`,`GET /api/embed/logs`
**erBackendHandler.ts** 提供对应 IPC,渲染进程通过 `window.electronAPI.erBackend.*` 调用。
## 关键技术约定
### LanceDB 余弦检索(lancedb.ts)
兆芯 KX-U6780A 无 AVX2,LanceDB 预编译包 SIMD 代码触发 `Illegal instruction`。`search()` 改为 `t.query().toArray()` + 纯 JS 余弦相似度:
```typescript
function toNumberArray(v: unknown): number[] | Float32Array {
if (v instanceof Float32Array || Array.isArray(v)) return v
if (typeof (v as any)?.toArray === 'function') return (v as any).toArray()
return Array.from(v as Iterable)
}
```
**Arrow Vector 陷阱**:`toArray()` 返回行中的 vector 字段是 Arrow `Vector` 对象,`b[i]` 返回 `undefined`,必须用 `toNumberArray()` 转换。
### 并发向量化(embedder.ts)
llamacpp 分支使用 `Promise.all` 并发发送,让服务端有机会攒批送 NPU:
```typescript
const vectors = await Promise.all(texts.map((t, i) => fetchOne(t, i)))
```
每条 60 秒超时,失败重试 2 次。
### Vue Proxy IPC 序列化
Pinia 响应式对象直接传给 `ipcRenderer.invoke()` 会触发结构化克隆错误。**所有传入 IPC 的响应式对象必须先序列化**:
```typescript
const plain = JSON.parse(JSON.stringify(reactiveObj))
await window.electronAPI.someIpc(plain)
```
### sharp stub
`stubs/sharp/` 作为 workspace 包(名称 `sharp`,版本 `0.34.5`)满足 `@huggingface/transformers` v4 的硬性 `require("sharp")`,避免打包后崩溃。
### 原生模块打包
```typescript
asarUnpack: ['node_modules/@lancedb/**', 'node_modules/apache-arrow/**', 'node_modules/onnxruntime-node/**']
```
### 版本号管理
版本号解析在三处保持一致:`electron-builder.config.ts`、`electron.vite.config.ts`、`scripts/package-kylin-arm64-deb.sh`。逻辑都是:`git describe --tags --abbrev=0`(去掉 `v` 前缀)→ 失败读项目根目录 `.app-version` 文件(同样去掉 `v` 前缀,已加入 `.gitignore`,PAD 部署等无 `.git` 环境下手动生成)→ 再失败分别 fallback 到 `'0.0.0'` / `'dev'` / `package.json` 版本号。**`.app-version` 缺失是 PAD 上界面显示版本号变成 `dev` 的常见原因**——rsync 部署到 PAD 时要记得同步这个文件。
### 应用图标打包踩坑
`resources/icon.png` 是图标源文件(1024×1024),`resources/icons/{16,24,32,48,64,128,256,512}x{同}.png` 是预生成的常规尺寸(`sips`/ImageMagick 生成,随图标一起更新)。两处坑:
1. **主进程窗口图标**:`src/main/index.ts` 用 `join(__dirname, '../../resources/icon.png')` 相对路径定位,`electron-builder.config.ts` 的 `files` 必须显式包含 `'resources/icon.png'`(默认只有 `out/**/*`,不会自动带上)。
2. **桌面菜单图标只装 1024×1024 不够**:不少桌面环境(如 UKUI)按常规小尺寸(48×48 等)查找图标,找不到就直接回退成缺省图标,不会从 1024 自动缩放。`scripts/package-kylin-arm64-deb.sh` 把 9 个尺寸全部装进 `hicolor/x/apps/`,另外在 `/usr/share/pixmaps/` 放一份兼容不认图标主题的老式菜单。装完新图标后需要**注销重登或重启**桌面 shell 才会刷新(图标缓存 `~/.cache/icon-cache.kcache`、`~/.cache/ksycoca5_*` 不会主动感知内容变化)。
### 打包体积精简(789M → 664M)
`electron-builder.config.ts` 的 `files` 排除了几个只在渲染层用、主进程从不 `require` 的包(`naive-ui`/`date-fns`/`highlight.js`/`lodash`/`lodash-es`,electron-vite build 时已被 Vite 静态打包进 `out/renderer/*.js`),以及 `onnxruntime-web`(`@huggingface/transformers` 的硬依赖,但项目强制走 `onnxruntime-node`,完全未用)。**排除 glob 必须写成 `'!**/node_modules/xxx/**'`,不能省 `**/` 前缀**——`'!node_modules/xxx/**'` 在真机验证时基本不生效(electron-builder 按完整相对路径匹配)。
`resources/tts/piper/` 只保留 `espeak-ng-data/` 子目录(131M → 18M):TTS 优先级第一位永远命中 VITS(小雅),Piper 自己的引擎(神经语音模型、二进制、自带 onnxruntime、phonemize 库)从未被实际调用,VITS 只是借用 Piper bundle 里的 `espeak-ng-data` 当 sherpa-onnx 校验用的 dataDir。`systemTts.ts` 的 `findPiperEspeakDataDir()` 只检查这个子目录是否存在,不要求 Piper 引擎的 bin/model/config 齐全。
`models/bge-base-zh-v1.5`、`models/bge-large-zh-v1.5`(只有 tokenizer 元数据无 onnx 权重,代码零引用)、`models/punct`(标点恢复模型,`extraResources` 从未把它纳入打包,功能一直是禁用状态)已从仓库删除。标点恢复功能代码保留(`punctuationModelDir` 为空时优雅跳过),需要的话自行准备模型再在设置里指定目录。
## NPU 状态监控
每 5 秒调用 `hm_smi --fields DDR_Memory_Free DDR_Memory_Total Core0 Core1 -a`,推送 `npu:stats` 事件:
```typescript
interface NpuStats { memUsed: number; memTotal: number; temp: number }
```
仅 `cardType === 'er160'` 时启动轮询;切换为 er25 时停止并推送 `null`。
## Git LFS 说明
`models/**/*.onnx` 通过 Git LFS 管理(见 `.gitattributes`)。克隆后必须:
```bash
git lfs install
GIT_SSL_NO_VERIFY=true git lfs pull # git.drumtm.com TLS 证书已过期,需绕过
```
推送含 LFS 对象的提交:
```bash
GIT_SSL_NO_VERIFY=true git lfs push origin main
git push origin main
```
## Kylin V10 / Ubuntu 适配要点
1. **禁用硬件加速**:兆芯集显 `MAX_FRAGMENT_UNIFORM_VECTORS=0` 导致 Skia 崩溃,`app.disableHardwareAcceleration()` 在 Linux 下强制启用
2. **全屏**:`ready-to-show` 后调用 `setFullScreen(true)`,覆盖任务栏
3. **菜单**:`Menu.setApplicationMenu(null)` 移除菜单栏
4. **onnxruntime-node**:必须在 `dependencies` 直接声明(不能为传递依赖),用 `require()` 而非 `await import()`
5. **npmrc**:`onnxruntime_node_install=skip` 跳过 CUDA EP 下载(NuGet 302 重定向无法跟随)
## 开发命令
```bash
npm install
npm run dev:linux # 开发模式(--no-sandbox)
npm run package:kylin # 打包 x64(AppImage + deb)
npm run package:kylin:arm64 # 打包 arm64(须在 ARM64 机器上;真机 Kylin 上 deb 目标会因 fpm bug 失败)
npm run package:kylin:arm64:deb # 打包 arm64 deb(推荐,已在真机验证;绕开 fpm bug,见上文"打包"条目)
```
## 同步来源
本仓库由 monorepo(electronrag)通过以下方式单向同步:
```bash
# 在 monorepo 根目录执行
bash scripts/sync-elec-rag-ui.sh "feat: 功能说明"
```
脚本同步 `src/elec_rag_ui/` → 独立仓库根目录,`models/` 和 `docs/` 同步至对应子目录,自动提交并推送(含 LFS 对象)。
**不要直接向本仓库提交代码**(除紧急修复外),应在 monorepo 的 `src/elec_rag_ui/` 修改后通过同步脚本推送。
## 注意事项
- 切换嵌入模型后必须重建索引(维度不同)
- Vue 响应式对象通过 IPC 传递前必须序列化
- PAD 部署使用 `--no-sandbox` 启动(`/usr/bin/智能电子手册` 脚本已内置)
- LFS push 须加 `GIT_SSL_NO_VERIFY=true`(git.drumtm.com 证书已过期)
- `libva error` 和 `GLSL warning` 为兆芯集显无害日志,可忽略