elec_rag_ui/CLAUDE.md
ssurui 6faf48fff4 docs: 补充打包体积精简的技术记录
记录 789M→664M 精简过程中的三个点:node_modules 排除 glob 必须带
**/ 前缀(真机踩过的坑)、Piper 引擎从未被实际调用只需保留
espeak-ng-data、bge-base/bge-large/punct 模型已从仓库删除的原因。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 00:56:19 +08:00

251 lines
14 KiB
Markdown
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.

# 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'
├── 文档入库 <IndexPanel />
├── 基本设置 检索参数(Top-K + 系统提示词)── 无计算卡/模型配置
├── 语音设置 TTS/ASR 参数
├── 批量测试 问题列表 + 等待时间滑块
└── 服务日志 嵌入/生成服务日志框(isLinux 可见)
```
计算卡型号、嵌入模型路径、生成模型路径等配置已从设置页移除,统一由 er_backend(http://<PAD>: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<number>)
}
```
**Arrow Vector 陷阱**:`toArray()` 返回行中的 vector 字段是 Arrow `Vector<FixedSizeList>` 对象,`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/<size>x<size>/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` 为兆芯集显无害日志,可忽略