initial: LAN IM desktop app (Electron 32 + Vue 3 + TS + SQLite)
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# 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` |
|
||||
| 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.
|
||||
|
||||
No test suite. Verify changes via `npm run typecheck` + 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. **Try not to leak Electron internals to UI.** Chat-client WS state stays in main; renderer only sees discovery online/offline. Adding a "chat not connected" pill in the UI was reverted for a reason — keep the surface area to two states (在线/离线).
|
||||
|
||||
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=<bytes>` 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.
|
||||
Reference in New Issue
Block a user