feat: 网盘浏览下载 + 远程功能修复 + 连接状态同步 + Linux 图标打包

新增网盘功能
- FileServer 增加 GET /drive/meta|list|download, 每机长期 token 校验 + 目录穿越防护
- 新增 drive.ts 客户端: listDrive / getDriveRoot / downloadDriveFile / collectDriveFiles
- ipc 暴露 drive:list|root|download|openFolder|pickDir, DB 增 drive_token 列 + ALTER 迁移
- 新增 DrivePanel.vue, 聊天框加"网盘" tab, 设置页加网盘分区
- 支持整目录递归下载并保持层级

修复远程功能
- 终端: 控制端收到的 output 帧被直接丢弃 (case 'output': return), 导致 xterm 全空白
- ChatServer: ws 构造即 listen, 'listening' 可能早于 start() 挂监听, 导致永久挂起
- USB: serverAttach 自行生成 sessionId 与控制端不一致 (数据帧查表落空);
  libusb 枚举误用 getDevices() (WebUSB 语义, 只返回已授权设备) 改 loadDevices();
  control transfer 传数字 bmRequestType (node-usb v3 要字符串枚举);
  detachKernelDriver 挪到 selectConfiguration 之前 (否则 EBUSY)
- 转发 UI: store 的 onOpened 是 no-op 致对方发起的转发不显示、无轮询致流量/TTL 不刷新、
  对方发起的会话关不掉 (新增 closeSession, server 侧关闭需通知对端否则端口泄漏)、
  入口/目标补归属标注、方向标签语义统一、监听端口输入框加宽

修复"在线却显示等待重连"
- 根因: 启动时主进程先连上对端 (connectTo 早于 createMainWindow), device:wsState
  在渲染层订阅前发出去被丢弃; 之后连接一直保持, 再无 open 事件 -> UI 永久停在等待重连
- 改为推全量快照 + 渲染层 mount 时主动拉取 (app:state); 渲染层先注册监听再 await
- 通道语义改为 outgoing 或 inbound 任一 WS 活着即可用 (原来只看 outgoing)

修复 Linux 安装后没图标
- linux.icon 指向单张 PNG 时 app-builder 解析不出尺寸 (size=0), 图标被装进
  hicolor/0x0/ 非法目录, 桌面环境找不到; 改为指向 resources/icons 目录 (8 个标准尺寸)
- StartupWMClass 填的是 productName, 与窗口真实 WM_CLASS (可执行文件名) 对不上,
  任务栏找不到启动器; 改为 local-net-msg

其他
- 新增 scripts/selftest 自测框架 (两个虚拟节点 + 真实 ChatServer/ChatClient), 30 条用例全过
- 重写 scripts/make-icon.js 生成多尺寸图标集 (原脚本引用的源图已不存在, sharp 依赖也缺)
- 新增 docs/drive.md, docs/connection-status.md
This commit is contained in:
2026-10-20 16:43:56 +08:00
parent ec9f854136
commit a79086e4e8
42 changed files with 2258 additions and 144 deletions
+48
View File
@@ -0,0 +1,48 @@
// 无头测试用的 electron 桩: 只提供 src/main 里真正用到的 API
// (app.getPath / app.getVersion / BrowserWindow / ipcMain / shell ...)
const os = require('node:os')
const path = require('node:path')
const fs = require('node:fs')
const root = process.env.LNM_SELFTEST_HOME || path.join(os.tmpdir(), 'lnm-selftest')
fs.mkdirSync(root, { recursive: true })
function ensure(p) {
fs.mkdirSync(p, { recursive: true })
return p
}
module.exports = {
app: {
isPackaged: false,
getPath(name) {
if (name === 'userData') return ensure(path.join(root, 'userData'))
if (name === 'documents') return ensure(path.join(root, 'documents'))
if (name === 'downloads') return ensure(path.join(root, 'downloads'))
if (name === 'home') return os.homedir()
return ensure(path.join(root, name))
},
getVersion: () => '0.1.0',
getName: () => 'local-net-msg',
getAppPath: () => process.cwd(),
on: () => {},
once: () => {},
whenReady: async () => {},
quit: () => {},
},
BrowserWindow: class BrowserWindow {
static getAllWindows() { return [] }
static getFocusedWindow() { return null }
isMinimized() { return false }
restore() {}
show() {}
focus() {}
webContents = { send: () => {} }
},
// 注意: 故意不提供 ipcRenderer —— electron-store 见到它会以为是渲染进程
ipcMain: { on: () => {}, once: () => {}, handle: () => {}, removeHandler: () => {} },
contextBridge: { exposeInMainWorld: () => {} },
dialog: { showErrorBox: () => {}, showOpenDialog: async () => ({ canceled: true, filePaths: [] }) },
shell: { openPath: async () => '', showItemInFolder: () => {} },
Notification: class { static isSupported() { return false } constructor() {} show() {} },
}