# LocalNetMsg Agent Guide LAN instant-messaging desktop app. Stack: **Electron 32 + Vue 3 + TypeScript + Vite + Pinia + Element Plus + SQLite (better-sqlite3)**. Three independent processes built by `electron-vite`. ## Commands | Task | Command | |---|---| | Dev (Electron + HMR) | `npm run dev` | | Build (no installer) | `npm run build` | | Typecheck main+preload | `npm run typecheck:node` | | Typecheck renderer | `npm run typecheck:web` | | Typecheck both | `npm run typecheck` | | Remote feature self-test (terminal / forward / USB) | `npm run selftest` | | Packaged-artifact smoke check (asar unpack + real pty spawn) | `npm run selftest:package` (run after `npm run package:*`) | | Windows installer | `npm run package:win` → `dist-Electron\LocalNetMsg-0.1.0-Setup.exe` | | macOS / Linux | `npm run package:mac` / `npm run package:linux` | | Unpacked dir only | `npm run package:dir` | All `package:*` scripts bake in `ELECTRON_MIRROR` + `ELECTRON_BUILDER_BINARIES_MIRROR` (npmmirror.com). Do not remove unless on the open internet. `npm run selftest` runs the remote (terminal / port-forward / USB) self-test: one process, two virtual nodes, real `ChatServer` + `ChatClient` + remote managers. See `scripts/selftest/`. Verify changes via `npm run typecheck` + `npm run selftest` + manual `npm run dev`. ## Layout ``` src/ main/ Electron main process (Node) — entry: index.ts discovery.ts UDP broadcast beacon (multi-NIC, per-interface subnet bcast) chat-server.ts WebSocketServer on 0.0.0.0:47900 chat-client.ts Outgoing WS with backoff reconnect file-server.ts HTTP /upload + /file, default cap 100GB db.ts SQLite via better-sqlite3 (data/app.db) settings.ts electron-store wrapper (~/AppData/Roaming/local-net-msg/config.json) notify.ts System Notification + Windows overlay badge ipc.ts All ipcMain.handle() wiring protocol.ts MessageEnvelope, WsFrame, DeviceInfo types preload/ contextBridge exposing typed API to renderer renderer/ Vue 3 SPA (Vite root) src/ main.ts createApp + Pinia + ElementPlus (full import) + zhCn App.vue Global event listeners (device:*, message:*, settings:*) components/ Sidebar, ChatView, MessageInput, MessageItem, SettingsView, MarkdownView stores/ Pinia: device.ts, message.ts, session.ts api.ts IPC typings (mirrors preload) utils/ format.ts (colorFor, initialsOf, formatSize, ...) electron.vite.config.ts Three builds → out/{main,preload,renderer}/ ``` Renderer alias: `@` → `src/renderer/src`. ## Network protocol UDP discovery on **47800**, chat WS on **47900**, file HTTP on **47901**. All three bind `0.0.0.0` and auto-shift on conflict. `DeviceInfo` carries `address` (chosen from the first non-internal IPv4 of the responding NIC) plus `chatPort`/`filePort`. The chat server **sends no broadcast** of its own — discovery comes purely from UDP. Receiver uses `rinfo.address` (real source IP), never the payload's `self.address`. ## Hard-won pitfalls (don't repeat these) 1. **`chat-client` MUST have an `on('error')` listener.** Node EventEmitter throws `ERR_UNHANDLED_ERROR` and kills the main process if `'error'` fires with no listener. The WS `'error'` event is followed by `'close'` which triggers reconnect. Don't try to "fix" by emitting on the client; just `console.warn` and move on. 2. **Drag-drop file path.** Electron 32 removed `File.path`. Use `webUtils.getPathForFile(file)` from `electron` — exposed in `src/preload/index.ts` as `getPathForFile(file)`. Callers must read it via `window.api.getPathForFile(f)` BEFORE the await boundary. 3. **Native rebuild.** `postinstall` runs `electron-builder install-app-deps` to rebuild `better-sqlite3` against the bundled Electron ABI. After `npm i` or upgrading Electron, this must run. If you forget, sqlite open will throw on launch. 4. **`mkdir` for Windows reserved names.** Output dir is `dist-Electron\` (escaped; `dist\electron` chokes on Windows). Don't rename without verifying the build still produces a valid path. 5. **`nsis` is `oneClick: false`.** Installer asks for install path + creates Desktop/Start-Menu shortcuts. Permissions are user-scoped. 6. **Renderer global drag highlight.** Track `dragenter`/`dragleave` with a depth counter (not a boolean) — child elements fire leave/enter pairs. See `MessageInput.vue`'s `dragDepth`. 7. **WS 通道状态要"拉取式"同步, 不能只靠事件推送.** 聊天框顶部显示的是"通道可用"(outgoing 或 inbound 任一 WS 活着),跟侧边栏的"设备在线"(UDP 发现)是两回事。启动时主进程往往先连上对端(`index.ts` 里 `connectTo` 早于 `createMainWindow`),那条 `device:wsState` 事件会在渲染层订阅之前就发出去被丢掉 —— 早期实现只有事件推送,UI 因此永久停在"等待重连…",而连接其实一直是好的。规矩:(1) `device:wsState` 推**全量快照**而不是增量;(2) 渲染层 mount 时主动 `invoke('app:state')` 拉一次;(3) 渲染层**先注册监听再 await**,否则 await 期间的事件也会丢。详见 `docs/connection-status.md`。 8. **File conflict naming.** Append `_N` before the first `.`: `archive.tar.gz` → `archive_1.tar.gz`, `.bashrc` → `.bashrc_1`. Logic in `file-server.ts` `targetPath()`. ## Configuration knobs - `LNM_MAX_FILE_SIZE=` env var overrides the 100 GB default in `file-server.ts`. Useful for testing the limit path. - `deviceName`, `downloadDir`, `notifications`, `autoStart` live in `~/AppData/Roaming/local-net-msg/config.json` via `electron-store`. Edit the file directly while dev is stopped. - `data/app.db` — SQLite DB. DELETE to reset all devices/messages. Auto-migrates on next launch. ## Packaging gotcha `release/` contains an `app.asar` Windows cannot delete while the previous app instance holds a file handle. Build now writes to `dist-Electron\` to dodge this. If you ever switch back, kill the running app first or you'll get EPERM. ## Linux packaging (icon + taskbar) Ubuntu 装完"没图标"是个坑,有两个独立原因: 1. **`linux.icon` 必须指向目录,不能是单张 PNG。** electron-builder 把图标装进 `/usr/share/icons/hicolor/x/apps/.png`,而单张 PNG 交给 `app-builder` 时它解析不出尺寸(返回 `size: 0`),于是装进 `hicolor/0x0/` 这种非法目录,桌面环境根本找不到 → 启动器没图标。正确做法是给一个**目录**,里面按 `x.png` 命名:`resources/icons/{16,24,32,48,64,128,256,512}x{...}.png`,然后 `linux.icon = "resources/icons"`。图标集用 `node scripts/make-icon.js` 生成(需要 ImageMagick 的 `convert`;生成结果已提交,平时不用跑)。 2. **`StartupWMClass` 要跟窗口真实的 WM_CLASS 一致。** electron-builder 默认填 `productName`(`LocalNetMsg`),但 Electron 实际设的是**可执行文件名**(`local-net-msg`)—— 对不上,任务栏就找不到对应启动器,显示成通用图标。用 `xprop -id WM_CLASS` 确认真实值,然后在 `build.linux.desktop.StartupWMClass` 里写它。 Windows / macOS 仍用单张 `resources/icon.png`(那里不按尺寸分目录)。 ## Windows 兼容性 完整的分析、已修问题和待验证清单见 `docs/windows-compat.md`。最容易踩的两个: 1. **`build.asarUnpack` 里的原生模块规则必须递归(`**/*`)。** node-pty 1.2 是"主包 + 平台包"拆包架构,平台包 `@lydell/node-pty--` 里除了 `.node` 还有 `conpty.dll` / `OpenConsole.exe`,而 node-pty 的 ConPTY worker 用 `worker_threads.Worker` 起脚本——都必须是磁盘上的真实文件。规则写成 `.../node-pty-*/*`(单层)会把 `prebuilds//` 下的东西留在 asar 里,Windows 终端直接起不来(Linux 上看不出来,因为只需要一个 `.node`)。自测 S7 守着这条规则。 2. **Windows 保留设备名**:`CON` `PRN` `AUX` `NUL` `COM1-9` `LPT1-9`(带扩展名也算)不能直接当文件名——对它们 `open()` 打开的是设备不是文件。落盘前统一走 `src/main/safe-name.ts` 的 `safeFileName()`(按**本机**平台决定要不要改写,Linux 上 `CON` 是合法文件名,不该动)。