# 远程终端、端口转发、USB 透传 规划文档 > 目标:在 LocalNetMsg 中实现三类“远程使用对方设备”的能力——**对方机器的本地终端(shell)**、**TCP 端口转发**、**USB 设备透传**。本文先定整体架构、协议、库选型,再分阶段落地。 > > 适配范围:Windows / macOS / Linux 三端。被控端的 shell 按平台决定:Windows → PowerShell(默认 `pwsh.exe`,回退 `powershell.exe`),macOS → zsh(回退 bash),Linux → bash(按 `SHELL` 环境变量)。 --- ## 1. 设计目标与边界 | 项目 | MVP(v0.2.0) | v0.3.0 | 不做(明确放弃) | |---|---|---|---| | 远程终端 | 单 PTY、单 session、只读/可写 | 多 session、窗口大小同步、UTF-8 CJK、ANSI 颜色、剪贴板同步 | 远程录制/回放(交给 asciinema) | | 端口转发 | TCP `127.0.0.1` 监听、字节计数、TTL | 多并发、IPv6、UDP | SOCKS / HTTP CONNECT / 公网映射 | | USB 透传 | HID 设备名单 + “绑定本机、远程附加” | 串口、复合设备过滤 | Isochronous、复杂描述符自定义 | 不做 = 让用户走更合适的工具(AnyDesk/RustDesk 远控、Serveo/Cloudflared Tunnel、VirtualHere)。本地调试范围以**单局域网、单设备对、单设备**为限。 --- ## 2. 选型与依赖 ### 2.1 库与版本(2026-07 通过 `registry.npmmirror.com` 核实) | 用途 | 包 | 版本 | 备注 | |---|---|---|---| | PTY 后端(被控端) | `@lydell/node-pty` | `1.2.0-beta.12`(`beta=1.2.0-beta.14`,可选升级) | 自带六平台预编译:win32-x64/arm64、darwin-x64/arm64、linux-x64/arm64;`latest` 是 `1.2.0-beta.12`。**优先 `latest`**,只在 win11 ARM 用户出问题时再切 beta | | 终端前端(控制端) | `@xterm/xterm` | `6.0.0` | xterm.js | | 终端自适应 | `@xterm/addon-fit` | `0.11.0` | 让终端随容器尺寸变化 | | 终端链接 | `@xterm/addon-web-links` | `0.12.0` | 终端内 URL 自动可点击 | | 端口转发 / TCP 隧道 | Node 内置 `net` 模块 | — | 不引入额外依赖;与现有 `ws`、`file-server` 同风格 | | USB 后端(被控端) | `usb`(node-usb v3.0.1,Rust N-API 重写版) | `3.0.1` | 同时提供 `usb`(自由访问)和 `webusb`(需授权)两种 API | | HID 快速通道 | `node-hid`(v3.3.0) | `3.3.0` | 只针对 HID 类(键盘/鼠标/游戏手柄/自定义 HID)速度更快、API 更友好;非 HID 设备走 `usb` | | 串口 / USB-Serial | `serialport`(v13.0.0) | `13.0.0` | 需要 Node ≥20 | | 公网隧道(可选 v0.3+) | `localtunnel` | `2.0.2` | 公共中继;不推荐默认开,作为开发期选项 | > 镜像访问:`https://registry.npmmirror.com//latest` 可直接读 JSON,元数据字段含 `dist-tags.latest`、`engines`、`os`、`cpu`、`napi.targets`,比 GitHub 页面稳定。GitHub raw/README 多数代理 403/Cloudflare 拦截,npm 元数据是兜底。 ### 2.2 不选 / 避坑 - **`node-pty`** 上游 `microsoft/node-pty` `latest=1.1.0`,仅 `beta=1.2.0-beta.14`。`@lydell/node-pty` 是社区 fork,包更小、预编译更全。直接用社区版,等 `microsoft/node-pty` 进入 stable 再切。 - **`robotjs`**:维护停滞,对 Electron 32 ABI 兼容性差。 - **`usbipd-win`**:Windows 官方 USB/IP,原理正确,但与我们 Node 进程隔离;MVP 不集成,后续可作为被控端可选依赖以获得 Isochronous 能力。 - **FreeRDP / libvncserver / RustDesk**:都是完整远控项目,体积与许可(AGPL)不适合直接嵌入。 - **`@lydell/node-pty-*-*`** 这些平台包随主包自动拉,无需单独声明。 --- ## 3. 整体架构 ### 3.1 分层 ``` ┌──────────────────────────────────────────┐ │ Renderer (Vue 3) │ UI: TerminalPanel / PortForwardPanel / UsbPanel │ xterm.js + addon-fit + addon-web-links │ Pinia stores: terminal / forward / usb └───────────────▲──────────────────────────┘ │ IPC (contextBridge) ┌───────────────┴──────────────────────────┐ │ Preload (typed window.api) │ └───────────────▲──────────────────────────┘ │ ipcMain.handle / broadcastToRenderer ┌───────────────┴────────────────────────────────────────────────────────┐ │ Main (Node) │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ RemoteTerminal │ │ PortForward │ │ UsbForward │ │ │ │ (manager.ts) │ │ (manager.ts) │ │ (manager.ts) │ │ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ │ │ ┌────────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐ │ │ │ @lydell/node-pty│ │ net (TCP) │ │ usb / node-hid │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ │ │ │ └──────── WS 信令(复用 chat-client.ts:91)──┘ │ │ src/main/protocol.ts WsFrame 新增 terminal / forward / usb 控制帧 │ └───────────────────────────────────────────────────────────────────────────┘ ``` ### 3.2 与现有代码的衔接 | 现有 | 复用方式 | |---|---| | `src/main/protocol.ts:30` `WsFrame` union | 扩展三条新控制帧:`terminal`、`forward`、`usb` | | `src/main/chat-client.ts:91` `ws://${peer.address}:${peer.chatPort}` | 复用同一 outgoing WS;多路复用消息类型 | | `src/main/ipc.ts:59` `bindNetworkContext` | 在末尾给三个新 manager 各建一组 `c.chatServer.on('xxx', ...)` 与 IPC handler | | `src/main/discovery.ts` UDP 广播 | 不动;新功能依然走 LAN 局域网 | | `src/main/db.ts` | 新增 `audit_log` 表 + 写入 helper,记录 terminal/forward/usb 事件 | | `src/renderer/src/stores/*` | 新增 `useTerminalStore`、`useForwardStore`、`useUsbStore` | | `src/renderer/src/App.vue` | 在 sidebar 增加“工具”下拉,包含三个子入口 | | `src/renderer/src/components/SettingsView.vue` | 增加“远程”开关 + 默认 TTL/最大带宽/允许的设备名单 | --- ## 4. 协议扩展(WsFrame 新增) ```ts // src/main/protocol.ts(追加,不改 PROTOCOL_VERSION) export type TerminalFrame = | { type: 'terminal.open'; sessionId: string; rows: number; cols: number; shell?: string } | { type: 'terminal.input'; sessionId: string; data: string /* base64 of bytes */ } | { type: 'terminal.output'; sessionId: string; data: string /* base64 of bytes */ } | { type: 'terminal.resize'; sessionId: string; rows: number; cols: number } | { type: 'terminal.close'; sessionId: string; reason?: string } | { type: 'terminal.ack'; sessionId: string; ok: boolean; reason?: string } export type ForwardFrame = | { type: 'forward.open'; sessionId: string; listenPort: number; targetHost: string; targetPort: number; ttlSec: number } | { type: 'forward.data'; sessionId: string; dir: 'c2s' | 's2c'; data: string /* base64 */; fin?: boolean } | { type: 'forward.close'; sessionId: string; reason?: string; bytesIn: number; bytesOut: number } | { type: 'forward.ack'; sessionId: string; ok: boolean; reason?: string } export type UsbFrame = | { type: 'usb.list'; reqId: string } | { type: 'usb.devices'; reqId: string; devices: Array<{ busId: string; vid: number; pid: number; class: number; subclass: number; product?: string; manufacturer?: string; serial?: string }> } | { type: 'usb.attach'; sessionId: string; busId: string } | { type: 'usb.detach'; sessionId: string } | { type: 'usb.transfer'; sessionId: string; dir: 'host->dev' | 'dev->host'; data?: string; fin?: boolean; status?: string } | { type: 'usb.ack'; reqId?: string; sessionId?: string; ok: boolean; reason?: string } ``` `WsFrame` 联合类型追加上面三个分支。**信令用 JSON**,**数据帧也用 JSON 但 payload 用 base64**——避免把 WS 当二进制流用,简化现有 codec。 ### 4.1 控制权与角色 每条 `*.open` 帧必须包含以下 meta,由发送方填: - `fromDeviceId`(自动从握手记录填) - `appVersion`、`nonce`(每次 `open` 重新生成) 被控端收到 `open` 后: 1. 校验对端是否在 `usb/terminal/forward.allowPeers` 中(设置项)。 2. 弹窗确认(被控端的 window),10 秒倒计时。 3. 通过/拒绝,发送 ack。 --- ## 5. 模块设计 ### 5.1 Remote Terminal(`src/main/remote/terminal.ts`) ``` class TerminalManager { // 被控端: 接受 open 时新建 pty handleOpen(env: MessageEnvelope): void // 控制端: 写 stdin / 收 stdout sendInput(sessionId: string, bytes: Buffer): void onOutput(cb: (sessionId: string, bytes: Buffer) => void): void resize(sessionId: number, rows: number, cols: number): void close(sessionId: string, reason?: string): void } ``` 平台默认 shell: ```ts // src/main/remote/shell.ts import { platform } from 'node:process' import { execSync } from 'node:child_process' export function defaultShell(): { file: string; args: string[] } { switch (platform) { case 'win32': { const file = execSync('where pwsh.exe', { stdio: ['ignore', 'pipe', 'ignore'] }) .toString().trim() ? 'pwsh.exe' : 'powershell.exe' return { file, args: [] } } case 'darwin': case 'linux': { return { file: process.env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/bash'), args: [] } } } } ``` 被控端启动逻辑: ```ts import * as pty from '@lydell/node-pty' const env = { ...process.env, TERM: 'xterm-256color', LANG: process.env.LANG || 'en_US.UTF-8' } const proc = pty.spawn(shell.file, shell.args, { name: 'xterm-256color', cols: frame.cols, rows: frame.rows, cwd: os.homedir(), env, encoding: null /* bytes */, }) proc.onData((data: string) => send('terminal.output', { sessionId, data: Buffer.from(data, 'utf8').toString('base64') })) proc.onExit(({ exitCode }) => send('terminal.close', { sessionId, reason: `exit ${exitCode}` })) ``` 控制端写入: ```ts proc.write(Buffer.from(frame.data, 'base64').toString('utf8')) ``` > ConPTY 是 Windows 10+ 默认;xp/7 用 winpty(`@lydell/node-pty` 自动选)。`encoding: null` 让数据以 string 但保留字节流语义;解码统一在两端做。 ### 5.2 Port Forward(`src/main/remote/forward.ts`) ``` class ForwardManager { // 申请方(控制端) async request(peerDeviceId, { listenPort, targetHost, targetPort, ttlSec }): Promise // 数据流 onData(sessionId, dir, bytes) sendData(sessionId, dir, bytes, fin?) close(sessionId, reason?) } ``` 申请方作为 TCP server(`net.createServer`,`host: '127.0.0.1'`),被控端作为 TCP client: ``` 控制端浏览器/应用 -> 127.0.0.1:listenPort ↓ ForwardManager (net.Server) ↓ (base64 frame) chat-client.ts:91 (现有 WS) ↓ 对方 ForwardManager (net.Socket) ↓ 对方 127.0.0.1:targetPort ``` 要点: - 单 forward session 一个 TCP 连接,复用现有 WS 子协议;帧不超过 16 KB/帧,超出分片。 - 字节计数:双向分别累计;TTL 到期(默认 1 小时)自动关闭。 - 端口黑名单:禁止 `22, 23, 53, 80, 135, 139, 443, 445, 3389, 5900, 5985/5986`,被控端在 `forward.ack` 里拒。 - 不开 `0.0.0.0`;v0.3+ 再加 LAN 提示。 - 关闭时若仍有未消费字节,发送 `fin:true` 让对端 socket 半关闭,不丢数据。 ### 5.3 USB / 串口 共享(`src/main/remote/usb.ts`) **老实说在前**:纯 Node.js 在 Windows 上做不到"USB 设备完全透明地变成本机 USB"——那是商业软件 (VirtualHere / USB Network Gate / FlexiHub)或 Linux usbip + WSL 才有的能力。我们提供三种模式,按场景取用: | 模式 | 能力 | 平台 | 透明度 | |---|---|---|---| | **`serial`** | 真正的双向字节流转发:server 端 `SerialPort.open`,client 端发 hex 字符串 | 全平台 | ✓ 完整(OS 看来就是普通串口) | | **`usb`** | libusb 字节桥:server 端 `node-usb` open + claim interface,client 端发 controlTransfer/bulkTransfer 请求 | 全平台 | ✗ 不是透明 USB——是远程调设备 | | **`usbip`** | 调系统 `usbip` CLI,让 Linux 内核接管(vhci_hcd 模块) | Linux only | ✓ 真透明(设备出现在本机 lsusb) | #### 5.3.1 `serial` 模式(最实用) 覆盖所有 USB-串口适配器(CH340 / CP210x / FTDI / PL2303)和原生 COM/tty。**对端不用装任何驱动**——`serialport` 走 OS 标准串口 API,自动枚举。 对硬件开发者来说这就是"远程调试 Arduino / STM32 / ESP32 / 串口打印机 / 串口屏"。 ``` 控制端 被控端 usb.attach(busId, { kind: 'serial', baudRate: 115200 }) └→ chatClient.send(usb.attach) └→ 弹窗确认 └→ new SerialPort({ path, baudRate: ... }).open() └→ sp.on('data', buf) → usb.data { dir: 'dev->host' } usb.serialSend(sid, bytes) └→ chatClient.send(usb.data { dir: 'host->dev' }) └→ sp.write(buf) ``` #### 5.3.2 `usb` 模式(libusb 字节桥) 适合访问自定义 USB 设备(单片机编程器、调试器、JTAG、自定义硬件)。 **不是透明 USB**——是 `node-usb` 的 controlTransfer / bulkTransfer 调用搬到对端机器上。 要"像本机 USB 一样"必须走 usbip 或商业软件。 ``` 控制端 被控端 usb.attach(busId, { kind: 'usb', interfaceNumber: 0 }) └→ chatClient.send(usb.attach) └→ node-usb.open(vid,pid) └→ claimInterface(0) └→ 返回 endpoints 列表 usb.ctrlIn(sid, { requestType, request, value, index }, length) └→ chatClient.send(usb.ctrlIn { setup, length }) └→ dev.nativeControlTransferIn(setup, 5000, length) └→ usb.ctrlResult { ok, data, status } usb.bulkIn(sid, endpoint, length) 类似 usb.bulkOut(sid, endpoint, bytes) 类似 ``` #### 5.3.3 `usbip` 模式(Linux only) 真透明 USB 透传。`usbip attach -r -b ` 让本机内核接管,设备出现在 `lsusb`。 **前置条件**:用户机器装了 `usbip` 包 + 加载 `vhci_hcd` 内核模块。我们的应用首次用时自动检测 + 提示安装(`permissions.ts` 里的 `diagnoseAndFixUsbAttach`)。 **macOS/Windows 默认不支持**——UI 会显示提示但禁用该选项。 #### 5.3.4 协议帧 ```ts type UsbFrame = | { type: 'list'; reqId: string } | { type: 'devices'; reqId: string; devices: UsbDeviceInfo[] } | { type: 'attach'; sessionId: string; direction: UsbDirection; busId: string; config: UsbAttachConfig } | { type: 'attached'; sessionId: string; ok: boolean; reason?: string; info?: UsbAttachedInfo } | { type: 'detach'; sessionId: string; reason?: string } | { type: 'detached'; sessionId: string; ok: boolean; reason?: string } // 字节流 (serial 模式) | { type: 'data'; sessionId: string; dir: 'host->dev' | 'dev->host'; data: string; fin?: boolean } // USB 控制传输 (request-response, 用 reqId 配对) | { type: 'ctrlOut'; sessionId: string; reqId: string; setup: UsbControlSetup; data?: string } | { type: 'ctrlIn'; sessionId: string; reqId: string; setup: UsbControlSetup; length: number } | { type: 'ctrlResult'; sessionId: string; reqId: string; ok: boolean; data?: string; status?: number } // USB 批量/中断传输 (request-response, 用 reqId 配对) | { type: 'bulkOut'; sessionId: string; reqId: string; endpoint: number; data: string } | { type: 'bulkIn'; sessionId: string; reqId: string; endpoint: number; length: number; timeoutMs?: number } | { type: 'bulkResult'; sessionId: string; reqId: string; ok: boolean; data?: string; status?: number } | { type: 'ack'; ok: boolean; reason?: string } | { type: 'error'; sessionId: string; reason: string } ``` --- ## 6. 数据流与生命周期 ### 6.1 远程终端 ```text 控制端 被控端 ui-click "打开终端" └→ TerminalPanel.onOpen() └→ window.api.terminal.open(peerId, { rows, cols }) └→ ipcMain: terminal.open └→ chatClient.send(terminal.open frame) └→ ChatServer → TerminalManager.handleOpen ├→ 检查 peer 在 allowPeers ├→ 弹窗 (被控 UI) ├→ pty.spawn(defaultShell()) └→ send(terminal.ack { ok: true }) ui 显示 ack + xterm 开始渲染 ui-keypress └→ window.api.terminal.sendInput(sid, bytes) └→ chatClient.send(terminal.input frame) └→ TerminalManager → pty.write(bytes) pty.onData(bytes) └→ chatClient.send(terminal.output frame) ui: xterm.write(bytes) ``` ### 6.2 端口转发 ```text 控制端 被控端 window.api.forward.open(peerId, { listenPort, targetHost, targetPort, ttlSec }) └→ chatClient.send(forward.open frame) └→ ack { ok }(先校验端口黑名单、TTL) ui → curl 127.0.0.1:listenPort └→ net.Server connection └→ ForwardManager 维护 sessionId, socket map └→ chatClient.send(forward.data { dir: 'c2s', data: base64, fin }) └→ net.Socket.write(data) └→ target process response bytes → chatClient.send(forward.data { dir: 's2c', data }) ui ← net.Server socket.write(data) ``` ### 6.3 USB / 串口 共享 #### 6.3.1 serial 模式 (字节流) ```text 控制端 被控端 window.api.usb.list(peerId) └→ chatClient.send(usb.list) └→ SerialPort.list() + (libusb if any) + (usbip if linux) └→ usb.devices { kind: 'serial' | 'usb' | 'usbip', ... } ui 选 serial 设备 + 波特率 └→ window.api.usb.attach(peerId, busId, { config: { kind: 'serial', baudRate: 115200 } }) └→ chatClient.send(usb.attach { config }) └→ 弹窗确认 └→ new SerialPort(...).open() └→ sp.on('data', buf) → usb.data { dir: 'dev->host', data: base64 } └→ usb.attached { ok, info: { kind: 'serial', serialPath } } ui 写字节 └→ window.api.usbSerialSend(sid, bytes) └→ chatClient.send(usb.data { dir: 'host->dev', data }) └→ sp.write(buf) ui 关闭 └→ window.api.usb.detach(sid) → chatClient.send(usb.detach) └→ sp.close() → cleanupServerSession ``` #### 6.3.2 usb 模式 (libusb 字节桥) ```text 控制端 被控端 ui 选 usb 设备 └→ window.api.usb.attach(peerId, busId, { config: { kind: 'usb', interfaceNumber: 0 } }) └→ chatClient.send(usb.attach { config }) └→ 弹窗确认 └→ node-usb.findDeviceByIds(vid, pid) └→ dev.open() → selectConfiguration(1) → claimInterface(0) └→ 枚举 endpoints └→ usb.attached { ok, info: { kind: 'usb', endpoints: [...] } } ui 发 control IN (例: GET_DESCRIPTOR) └→ window.api.usbCtrlIn(sid, { requestType: 0x80, request: 0x06, value: 0x0100, index: 0 }, 18) └→ chatClient.send(usb.ctrlIn { setup, length: 18 }) └→ dev.nativeControlTransferIn(setup, 5000, 18) └→ usb.ctrlResult { ok, data, status: 18 } ui 发 bulk IN └→ window.api.usbBulkIn(sid, 0x81, 64) → 类似 ctrlIn ui 发 bulk OUT └→ window.api.usbBulkOut(sid, 0x01, hexData) → 走 ctrlOut-style request-response ``` #### 6.3.3 usbip 模式 (Linux only) ```text 控制端 (Linux 客户端) 被控端 (Linux 设备持有方) ui 选 usbip 设备 └→ window.api.usb.attach(peerId, busId, { config: { kind: 'usbip' } }) └→ chatClient.send(usb.attach { config: { kind: 'usbip' } }) └→ 弹窗确认 → 仅记录 (不直接 open) └→ usb.attached { ok } └→ permissions.diagnoseAndFixUsbAttach() — 检查 usbip + vhci_hcd └→ `usbip attach -r -b ` (spawn sudo-prompt) └─ → 内核 vhci_hcd 接收 → 设备出现在 `lsusb` ``` --- ## 7. UI 设计 ### 7.1 Sidebar 增加“工具”tab ``` 设备 文件 工具 设置 └─ 终端 └─ 端口转发 └─ USB 设备 ``` ### 7.2 TerminalPanel.vue ```vue ``` ### 7.3 ForwardPanel.vue - “+ 新建转发”按钮 → 弹表单(peer / 监听端口 / 目标主机 / 目标端口 / TTL) - 列表展示:`peerName, listen:127.0.0.1:PORT0 → peer:HOST:PORT1`,剩余 TTL,in/out 字节数 - 行内操作:停用、复制链接 ### 7.4 UsbPanel.vue 顶部方向切换 (我用对方的 / 对方用我的) + 模式切换 (串口 / USB / USB/IP) + 设备扫描按钮。 **串口模式**(最实用): - 设备表 + 波特率下拉 + Attach 按钮 - 附加后显示 hex 流(serial 字节流可视化) - "发送文本"按钮快速发 ASCII/UTF-8 字节 **USB 模式**(libusb 字节桥): - 设备表 + Interface # 输入 + "Linux detach 内核驱动" 复选 - 附加后显示: - **Endpoints 列表**(从 attach 帧的 `info.endpoints` 拿到,提示用户 EP 地址和方向) - **控制传输表单**:方向(IN/OUT)+ type(standard/class/vendor)+ recipient(device/interface/endpoint/other)+ request + wValue + wIndex + data(hex) / length - **批量/中断传输表单**:方向(IN/OUT)+ EP 地址 + length + timeout + data(hex) - **调用日志**:每次 ctrl/bulk 调用的时间、tag、status、hex dump **USB/IP 模式**(Linux): - 顶部显示提示:"USB/IP 是 Linux 内核自带的真透明 USB 透传...需要装 `usbip` + 加载 `vhci_hcd` 模块" - 设备表 + Attach 按钮 - 附加后由内核接管,本应用不再做数据转发 底部始终有"对方正在使用我的设备"横条(当 server-side session 存在时),可一键 stop。 --- ## 8. 安全模型 > 本项目原本 LAN 信任域 = 同一子网任意主机;启用终端/转发/USB 后需要更明确的边界。 ### 8.1 信任与确认 - 每条 `*.open` 必须在**被控端**弹窗(默认聚焦窗口、10 秒倒计时、不可超时自动通过)。 - 设置项 `remote.allowPeers: Record`,默认全 false。 - 同一对端 24 小时内只能弹一次允许(除非重新启动应用)。 - “仅只读终端”开关:禁用 `terminal.input`(被控端拒绝写入帧)。 - `forward.open` 必须显式选择目标端口范围 + TTL;被控端按白名单校验 host。 ### 8.2 端口安全 - 监听 host 永远 `127.0.0.1`,不开 `0.0.0.0`。 - 只挡自家端口 `SELF_PORTS = {47800, 47900, 47901}`(防把消息/文件端口转发造成死循环/桥接冲突),**其他端口一律不挡**(用户的 SSH/RDP/MySQL 等服务我们不去判断)。 - 默认 TTL 1 小时,最大 24 小时。 - 默认带宽 10 MB/s,最大 100 MB/s,按 session 计数。 ### 8.3 USB / 串口 安全 - 每条 `usb.list` / `usb.attach` 必须**被控端**显式授权(弹窗)。 - `serial` 模式:被控端打开串口前弹窗告知"对方将通过此串口发送/接收字节",用户勾"记住"后 24h 内不再弹。 - `usb` 模式:被控端 claim interface 前弹窗,告知"对方将通过本机的 USB 设备 X 发送控制/批量传输"。 - `usbip` 模式:弹窗告知"对方将 attach 你的 USB 设备 X 到他自己的机器上"(设备会从你机器上消失)。 - 设备列表本身(vendor/product/serial)属于"信息泄露"敏感字段,要求接收端在 `usb.list` 时显式授权一次。 - 被控端顶部横幅"USB/串口 设备已被 X 远程使用中"(chat header 同时显示)。 - session 30 秒无活动自动 detach。 ### 8.4 审计 新增表(`db.ts` 内 `initAudit()`): ```sql CREATE TABLE audit_log ( id INTEGER PRIMARY KEY, ts INTEGER NOT NULL, action TEXT NOT NULL, -- terminal.open / forward.open / usb.attach ... source_device_id TEXT, target_device_id TEXT, session_id TEXT, payload_json TEXT, result TEXT, -- ok / denied / error / closed bytes_in INTEGER DEFAULT 0, bytes_out INTEGER DEFAULT 0 ); CREATE INDEX idx_audit_ts ON audit_log(ts DESC); ``` 保留 30 天(设置项可改),手动“导出 audit.zip”。 --- ## 9. 与现有 pitfalls 的对齐 | Pitfall | 对策 | |---|---| | #1 `chat-client` 必带 `'error'` 监听 | 新增帧引入不影响;已有 on('error') 已稳 | | #2 drag-drop file path | 不涉及 | | #3 native rebuild | `@lydell/node-pty`、`usb`、`node-hid`、`serialport` 都是 native,必须走现有 `postinstall` 的 `electron-builder install-app-deps`;CI 加 `npm rebuild --runtime=electron` 兜底 | | #4 dist-Electron 路径 | 不动 | | #5 nsis oneClick false | 不动 | | #6 dragenter/dragleave 深度计数 | 不涉及 | | #7 渲染层只暴露 在线/离线 两种状态 | 新增的三个 UI 独立成 tab,不污染 ChatView/Sidebar 的现有状态机 | | #8 文件冲突命名 | 新增 `downloadDir` 复用 `file-server.ts` 既有目录,不改 | --- ## 10. 落地节奏 ### v0.2.0 — 单平台、单 session、只读终端 + 本机转发 1. 加 `@lydell/node-pty`、`@xterm/xterm`、`@xterm/addon-fit`、`@xterm/addon-web-links`、`usb`、`node-hid`、`serialport`。 2. `src/main/protocol.ts` 加 `WsFrame` 三类控制帧。 3. `src/main/remote/{terminal,forward,usb}.ts` 三个 manager。 4. `src/main/db.ts` 加 `audit_log` 表。 5. `src/main/ipc.ts` 增 `terminal:*`、`forward:*`、`usb:*` 共约 10 个 handler。 6. `src/renderer/src/components/remote/{TerminalPanel,ForwardPanel,UsbPanel}.vue`。 7. `src/renderer/src/stores/{terminal,forward,usb}.ts`。 8. `SettingsView.vue` 增加“远程”开关与 allowPeers 列表。 9. `npm run typecheck` 通过;手动两机对测。 ### v0.3.0 — 多 session、带宽/TTL、串口透传 1. `forward.maxBandwidth`、`forward.defaultTtl` 设置项。 2. `serialport` 路径完善(Linux udev 提示)。 3. xterm.js 同步主题、深色模式。 ### v0.4.0 — 公网可选 1. `localtunnel` 接入(仅转发;终端和 USB 不走公网)。 2. 文案与免责声明。 ### 不做 - 屏幕/桌面、鼠标键盘输入、剪贴板双向同步、音频、文件双向同步——都留给 RustDesk / AnyDesk。 - 公网中继自建(成本过高,Y 不如直接用现成 SaaS)。 - 远程重启、远程安装驱动、远程执行任意脚本。 --- ## 11. 验证清单(手动两机) | # | 场景 | 期望 | |---|---|---| | T1 | 控制端请求被控端终端 → 被控端允许 | xterm 渲染 PowerShell/zsh/bash 提示符 | | T2 | 控制端输入 `ls -la` | 双端能正确显示(中文文件名 OK) | | T3 | 控制端把窗口拉大 | 终端按比例刷新(fit + resize 帧) | | T4 | 控制端断开 | 被控端 PTY 收到 SIGHUP(win 下 ConPTY 退出) | | F1 | 控制端 127.0.0.1:5180 → 被控 127.0.0.1:22 | ack 被控拒(黑名单) | | F2 | 控制端 127.0.0.1:5180 → 被控 127.0.0.1:3000 | 浏览器开 5180 看到 3000 内容 | | F3 | TTL 到期 | listener 自动释放;`netstat` 看不到端口 | | U1 | 列出被控端 USB 设备 | 控制端看到 product/serial | | U2 | attach 串口 /dev/ttyUSB0 | 被控端 z串口被占;控制端能 write | | U3 | 被控端关闭应用 | 控制端收到 `usb.close { reason: 'peer-down' }` | | S1 | 控制端不在 allowPeers | 被控端收到 `open` 直接 ack false,写 audit | | S2 | 控制端伪造 `fromDeviceId` | 被控端按 WS 握手记录覆盖,伪造失败 | | S3 | 同时 5 个并发 forward | 全部独立,UI 列表正确 | --- ## 12. 参考 - `@lydell/node-pty` README & 平台包列表:`https://github.com/lydell/node-pty` - `@xterm/xterm` v6.0.0:`https://xtermjs.org/` - `usb@3.0.1`(Rust 重写版):`https://github.com/node-usb/node-usb-rs` - `node-hid@3.3.0`:`https://github.com/node-hid/node-hid` - `serialport@13.0.0`:`https://serialport.io/` - `localtunnel@2.0.2`:`https://github.com/localtunnel/localtunnel` - 现有项目入口:`AGENTS.md`、`src/main/protocol.ts`、`src/main/chat-client.ts:91`、`src/main/ipc.ts:59`