a79086e4e8
新增网盘功能 - 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
4.0 KiB
4.0 KiB
连接状态模型 (在线 / 通道可用)
聊天框顶部那个小圆点 + 旁边的文字,到底表示什么?这份文档说清楚, 以及为什么状态必须拉取式同步而不是只靠事件推送。
1. 两种"在线",别混
| 概念 | 含义 | 数据来源 | 表现 |
|---|---|---|---|
| 设备在线 | 对方机器还在局域网里活着 | UDP 广播发现 (discovery.ts),15s 没心跳算 lost |
侧边栏"在线"绿标 |
| 通道可用 | 我们和对方之间有活的 WS,能收发消息 | outgoing (我们连过去的) 或 inbound (对方连过来的) 任一活着 |
聊天框顶部绿点 |
- 设备在线 ≠ 通道可用。对方可能开着防火墙挡住 WS,UDP 还在广播。
- 通道可用 不等于 设备在线:对端进程刚起、还没被 UDP 发现到,但已经连上来了。
通道可用的判定(src/main/ws-state.ts 的 computeWsState):
alive = clientStatus[id] === 'open' || server.isConnected(id)
// ↑ outgoing: 我们主动连的 ↑ inbound: 对方连过来的
两个方向是独立的两条腿。早期实现只看 outgoing,结果对方只连进来、 我们没连上时,UI 会误报"等待重连"——而消息其实收发正常。
2. 状态必须能"随时拉取"
启动顺序里有一处天然竞态(src/main/index.ts):
discovery.start() ← 先启动
chatClient.connectTo(...) ← 已入库设备, 立刻发起 WS 连接 (setTimeout 0)
createMainWindow() ← 之后才建窗口
WS 通常几十毫秒内就握手成功 → 主进程广播 device:wsState →
可渲染层还没加载完、还没注册监听 → 这条事件被整个丢掉。
之后再没有 'open' 事件了(连接一直保持着),UI 就永久停在"等待重连…"。
结论:状态事件只管"变化通知",不能当"当前状态"的唯一来源。 任何时候想知道现在的状态,都得能主动查一次。
| 时机 | 谁来拉 | 拉什么 |
|---|---|---|
| 渲染层 mount | 渲染层 | ipcRenderer.invoke('app:state') → { ws: {...} } |
主进程 boot:ready |
主进程推 | 带上同一份 ws 快照 |
| 连接断开 | 主进程推 | 全量快照 { ws: {...} } |
device:wsState 事件带的是全量快照而不是"某个设备变了"的增量:
增量事件会漏(订阅晚了就没了),全量快照任何时刻都能自愈。
渲染层还有两条纪律(src/renderer/src/App.vue):
- 先注册监听,再做 await。任何
await都会让出事件循环,期间推来的事件就丢了。 - 快照里的设备 = "没连上"(按 false 处理),不做增量合并,避免留下过期状态。
3. 文案
| 条件 | 显示 |
|---|---|
| 通道可用 | 在线 · 192.168.0.7(绿点) |
| 设备在线但通道不通 | 在线 · 192.168.0.7 (连接中…)(黄点,后台在 backoff 重连) |
| 都不可用 | 离线 · 12:05(灰点) |
远程功能(终端 / 转发 / USB / 网盘)都走 WS 通道,所以入口按钮在
设备离线 && 通道不通 时才禁用——只看设备在线会把"能连但没连上"的情况误判成可用。
4. 回归测试
npm run selftest 里的 [通道状态] 组(W1–W3):
- W1 通道状态可随时拉取(连接早已建好,现在才查也查得到)
- W2 只有 inbound 通(对方连我,我没主动连)也算通道可用
- W3 两条腿都断了 → 状态回落 false
5. 相关文件
| 文件 | 作用 |
|---|---|
src/main/ws-state.ts |
computeWsState 纯函数(无 electron 依赖,方便自测) |
src/main/chat-server.ts |
isConnected(deviceId) — 查 inbound 连接 |
src/main/chat-client.ts |
getStatus() — 各对端的 outgoing 状态 |
src/main/ipc.ts |
collectWsState() / broadcastWsState() / app:state IPC |
src/renderer/src/stores/device.ts |
wsOpen 映射 + setWsStates() 全量替换 |
src/renderer/src/components/ChatView.vue |
顶部的点与文案 |