Only in One — 增量 PRD v2(差距分析与需求补全)
文档类型:增量 PRD。本文只描述「相对 v1.0 的变更与补全」,不重复 PRD.md / architecture.md 全文。
基线:docs/PRD.md(v1.0 定稿)、docs/architecture.md(v1.0 定稿)、docs/packaging.md、docs/phase0-validation.md、README.md
代码基线:server/{server.js,db.js,test/api.test.mjs,smoke.mjs}、launcher/src/*.ts、launcher/test/core.test.ts、scripts/*、config/*
定位不变:团队内部工具,≤10 人。不引入企业级复杂度。
最后更新:2026-10-21
0. 摘要(给决策者的一段话)
现有代码的架构分层与方向性决策是正确的(控制面/数据面分离、证书同步方向已按 architecture.md §5 修正),主要问题集中在三处:
- 安全落地缺口——信任模型写在文档里,但代码未兑现:客户端未校验证书指纹、证书文件名可被路径穿越、外部命令存在注入面、JWT 密钥有弱默认值、登录无限流。
- 核心闭环未闭合——只上传不上报(缺本机证书自动发现与上传)、心跳只打一次(在线状态必然失真)、凭据不持久化、服务器挂了就全瘫(无离线降级)。结果是「登录 → 看见在线设备 → 一键连上」这条主路径目前跑不通,必须手工操作。
- 多端与工程卫生——macOS 优先实现,Windows 半支持,Linux 完全未纳入;无日志、无集中配置、无迁移、无 CI、无
.gitignore(数据库与打包产物会进版本库)、文档缺 API 契约与故障排查。
本轮共识别 50 条差距(P0 = 8 / P1 = 30 / P2 = 12),对应 50 条需求(P0 = 8 / P1 = 29 / P2 = 13)。建议按 P0 → P1 → P2 三批落地,P0 单独成批并优先合入。
1. 现状与差距分析
严重度定义:P0 = 正确性 / 安全 / 数据丢失;P1 = 关键功能缺失;P2 = 体验与工程优化。
A. 账户服务器 — 认证与鉴权(G-01 ~ G-09)
| # |
现状(代码事实) |
问题 |
影响 |
严重度 |
| G-01 |
server.js:22 verifyPassword 直接 stored.split(':') + timingSafeEqual,无 try/catch |
畸形 password_hash(无冒号 / 长度不符 / 空串)会抛 TypeError/RangeError,冒泡成 500 并可能回显堆栈 |
单个脏数据即可让该账号永久无法登录;异常面暴露 |
P0 |
| G-02 |
server.js:13 JWT_SECRET = process.env.JWT_SECRET || 'dev-only-change-me' |
生产环境漏配密钥时静默使用已知弱密钥,且无长度/随机性校验 |
任何人可伪造任意用户 token,控制面完全失守(可拉取全部证书、冒充设备) |
P0 |
| G-03 |
/api/auth/login 无限流、无失败锁定、无审计日志 |
可无限次暴力破解密码 |
弱口令账号必然被破;无追溯手段 |
P0 |
| G-04 |
登录签发 expiresIn: '7d' 的单一 JWT,无 refresh、无 jti、无吊销表 |
token 泄露后 7 天内无法失效;无「登出」语义 |
丢设备 = 丢 7 天控制权 |
P1 |
| G-05 |
register 仅判空(测试里 pw12345 可通过),username 未做格式校验 |
弱口令与非法用户名可入库;username 后续参与路径拼接 |
口令易破;为路径穿越提供输入源 |
P1 |
| G-06 |
server.js:99 const fp = fingerprint ?? 服务端计算 |
服务端信任客户端提交的指纹,架构 §6.3「服务器算指纹、客户端下载后校验」被架空 |
指纹可被伪造,客户端校验失去保护意义 |
P0 |
| G-07 |
cert_pem 只判空,无 PEM 格式/大小/可解析性校验 |
任意字符串(含 8MB 垃圾)可入库并下发到所有对端 |
污染全部设备的 Sunshine credentials 目录;存储膨胀 |
P1 |
| G-08 |
无全局错误处理;无 Helmet/安全响应头;express.json() 无 body 上限;无 CORS 策略 |
未捕获异常返回 Express 默认 HTML 500;大 body 可打爆内存 |
客户端解析失败、报错不可读;DoS 面 |
P1 |
| G-09 |
响应格式混用:{ok:true} / {error:'...'} / 裸数组;无 API 版本前缀;错误无码 |
客户端只能靠 HTTP 状态码猜语义(api.ts:30 直接抛 path -> status) |
前端无法给出准确提示;后续接口演进困难 |
P1 |
B. 证书同步与免 PIN(G-10 ~ G-14)
| # |
现状 |
问题 |
影响 |
严重度 |
| G-10 |
connect.ts:29-34 下载后直接 fs.writeFileSync,未比对 fingerprint(CertEntry.fingerprint 字段已定义但从未使用) |
架构 §6.3 明确要求「客户端下载后校验」,代码未实现 |
服务端(或中间人)替换证书无法被发现 = 免 PIN 信任链形同虚设 |
P0 |
| G-11 |
connect.ts:31 path.join(dir, \moonlight-${c.device_name}.pem`),device_name` 完全外部可控且无校验 |
路径穿越:device_name = '../../../.ssh/authorized_keys' 可向任意目录写入可控内容 |
同账户任一成员(或凭证泄露者)可在其他成员机器上任意写文件 |
P0 |
| G-12 |
fs.mkdirSync + fs.writeFileSync 直写,无 tmp+rename、无 fsync、无 EACCES/ENOENT 处理 |
进程中断留下半截证书;ProgramData 权限不足时抛原始异常 |
Sunshine 拒绝信任该文件;GUI 只显示原始 errno |
P1 |
| G-13 |
从不清理本地已失效设备的证书文件 |
credentials 目录随改名/重装无限增长 |
陈旧证书堆积,排查困难 |
P2 |
| G-14 |
没有任何代码上传本机 Moonlight 公钥证书(api.ts 有 uploadCert 但无任何调用方) |
PRD 5.2 [State-driven]「首次手动配对并自动上传证书」未实现,同步链路只有「下载」半边 |
免 PIN 闭环断裂:新设备必须人工拷贝证书,PRD 核心卖点失效 |
P1 |
C. 外部命令执行(G-15 ~ G-17)
| # |
现状 |
问题 |
影响 |
严重度 |
| G-15 |
connect.ts:57、usb.ts:28/34/42/48、share.ts:7/9/11 全部用 exec 拼接字符串;可控变量含 ip、app、busid、host、shareName |
命令注入:ip 来自设备列表(可伪造),busid/host/shareName 来自 GUI 输入 |
任意命令以启动器权限执行 |
P0 |
| G-16 |
所有 exec 无 timeout、无 maxBuffer 约束;attachUsb 内含 sudo,无 TTY 时静默挂起 |
卡死的进程永久占用 Promise;GUI 无响应且无法取消 |
启动器假死;用户只能强杀 |
P1 |
| G-17 |
无外部依赖预检(moonlight / usbipd / usbip / Sunshine / SMB 服务是否存在、版本是否兼容) |
缺依赖时只抛 command not found |
用户不知道该装什么、去哪装 |
P1 |
D. 多端适配(G-18 ~ G-23)
| # |
现状 |
问题 |
影响 |
严重度 |
| G-18 |
discover.ts:22 if (process.platform !== 'darwin') return [] |
mDNS 仅 macOS;Windows/Linux 直接回落到全子网扫描 |
非 Mac 端发现慢、依赖扫描,与架构 §6.2「mDNS 优先」不符 |
P1 |
| G-19 |
usb.ts 仅 Windows usbipd + macOS usbip attach;无 Linux 分支;detachUsb 要求用户手输 port,无「已挂载列表」查询 |
Linux 完全不可用;macOS 卸载体验需人工记忆端口号 |
Linux 无法使用 USB 共享;卸载易错、易残留 |
P1 |
| G-20 |
discover.ts:55-65 对每个 IPv4 网卡生成 254 个 IP,未去重、含 .0/.255;scanLan 对全部候选并发建 socket,无并发上限 |
多网卡 / 虚拟机 / VPN / Docker 网桥环境下瞬间创建 500~2000 个 socket |
fd 耗尽、扫描耗时不可控、触发网络设备限流;candidateIps 单测只断言格式,未覆盖该风险 |
P1 |
| G-21 |
share.ts 只有「打开对端共享」;scripts/setup-smb.ps1(创建加密 SMB 共享)未接入启动器,且仅 Windows |
PRD 5.4 [Event-driven]「首次开启文件共享时创建加密 SMB 共享」未实现 |
文件共享只剩「打开」半边,且需手工跑 PowerShell |
P1 |
| G-22 |
connect.ts:10-15 路径硬编码(ProgramData/~/.config),不校验 Sunshine 是否安装、目录是否存在 |
装了非默认路径 Sunshine 或未安装时静默写错目录 |
证书写了但 Sunshine 不认,问题极难排查 |
P2 |
| G-23 |
scripts/ 仅 3 个 Windows PowerShell + 1 个 macOS sh;无 Linux 脚本 |
Linux 无任何自动化 |
若纳入 Linux,需从零补脚本 |
P2 |
E. 状态、在线与离线(G-24 ~ G-28)
| # |
现状 |
问题 |
影响 |
严重度 |
| G-24 |
agent.ts:13-18 心跳只在调用时执行一次;GUI 需手动点「上线」;无定时器 |
无周期性心跳,last_seen 立刻过期 |
「在线设备列表」必然失真,一键连接的目标 IP 多为过期值 |
P1 |
| G-25 |
/api/devices 返回 last_seen 但服务端无判定;GUI .dot 恒为绿色,只把 last_seen 当时间字符串显示 |
无在线/离线状态 |
用户无法判断能否连上,只能盲试 |
P1 |
| G-26 |
无任何离线降级:服务器不可达时 startAgent 直接抛错,登录也失败 |
PRD 5.5 [Unwanted]「服务器不可用仍可离线连接」未实现 |
云服务器一挂,内网直连也用不了——违背「数据面不依赖云」的设计初衷 |
P1 |
| G-27 |
main.ts:19 let token: string | null = null,仅内存;重启必须重登 |
PRD 5.5「敏感信息存于系统钥匙串(Keychain / Credential Manager)」未实现 |
每次重启都要重输服务器地址+账密,与「连接方便」目标冲突 |
P1 |
| G-28 |
无托盘常驻、无开机自启;Windows 上关窗即 app.quit() |
启动器关闭 = 心跳停止 = 设备显示离线 |
被控端无法被发现 |
P2 |
F. 可维护性与工程卫生(G-29 ~ G-36)
| # |
现状 |
问题 |
影响 |
严重度 |
| G-29 |
server 仅一行启动 console.log;launcher 完全没有日志 |
无统一日志模块、无分级、无落盘、无脱敏 |
现场故障无法追溯;USB/证书问题只能靠猜 |
P1 |
| G-30 |
配置散落:JWT_SECRET/PORT/OIO_DB_PATH 各处直接读 env,无集中校验;db.js:9 用 process.cwd() 定位数据库 |
换目录启动会新建空库;配错不报错 |
表现为「数据全部丢失」;运维踩坑率高 |
P1 |
| G-31 |
settings.ts:12 用 require('electron') + try/catch 嗅探运行环境 |
测试与运行时耦合,纯逻辑模块无法独立实例化 |
难以对 settings 做单测;Electron 升级易碎 |
P2 |
| G-32 |
main.ts 无单实例锁、无窗口位置/尺寸持久化、无 uncaughtException/崩溃恢复 |
可重复启动多份;崩溃后静默退出 |
重复心跳、状态错乱 |
P2 |
| G-33 |
db.js 仅一次性 CREATE TABLE IF NOT EXISTS,无迁移机制、无备份 |
后续任何字段变更都会让老库静默不兼容 |
升级即故障 |
P1 |
| G-34 |
仓库无 .gitignore;server/data/onlyinone.db(含密码哈希)、launcher/release/(数百 MB 二进制与签名产物)、dist/、node_modules/、.DS_Store 全在工作区 |
数据库与打包产物会被提交进版本库 |
密码哈希与历史泄露风险;仓库急剧膨胀 |
P0 |
| G-35 |
无 ESLint / Prettier;package.json 里 typecheck:core 手工罗列 9 个文件名,与 tsconfig.json 重复 |
风格不统一;新增文件易漏检 |
代码质量靠人肉 |
P2 |
| G-36 |
launcher/dist/(编译产物)与 launcher/release/ 与源码同库 |
产物与 src/ 可能不同步,误导排查 |
调试时看到过期代码 |
P2 |
G. 测试与 CI(G-37 ~ G-40)
| # |
现状 |
问题 |
影响 |
严重度 |
| G-37 |
server/test/api.test.mjs 共 8 个用例(health / 心跳+列表 / 证书上传+拉取 / 重复注册 / 错误密码 / 无 token / 缺 device_name / 缺 cert_pem) |
无覆盖:密码校验异常、限流、token 过期与吊销、指纹强制计算、PEM 校验、统一错误格式、DB 迁移 |
安全修复无法回归验证 |
P1 |
| G-38 |
launcher/test/core.test.ts 共 4 个用例(sunshineCredDir / syncPeerCerts / candidateIps / SHARE_NAME) |
usb.ts、share.ts、settings.ts、agent.ts、discover 的扫描逻辑、main.ts IPC 全部零覆盖;syncPeerCerts 未覆盖指纹与路径穿越 |
本轮 P0 修复(注入、穿越、指纹)无测试护栏 |
P1 |
| G-39 |
无 GitHub Actions / 无 pre-commit / 无覆盖率门禁;npm test 未被任何自动化强制执行 |
质量完全依赖人工自觉 |
回归随时可能引入 |
P1 |
| G-40 |
server/smoke.mjs 需手工先起服务再跑,未纳入 npm scripts |
冒烟未自动化 |
发布前验证易遗漏 |
P2 |
H. 文档(G-41 ~ G-44)
| # |
现状 |
问题 |
影响 |
严重度 |
| G-41 |
无 API 契约文档(字段、错误码、鉴权、版本、示例) |
启动器与服务端的契约只存在于代码里 |
联调靠读码;P0 统一响应格式无法对齐 |
P1 |
| G-42 |
无故障排查手册 |
证书同步失败 / USB 挂载失败 / 发现不到设备 / SMB 打不开均无处置指引 |
每个问题都要重新排查一遍 |
P1 |
| G-43 |
无安全说明(信任模型如何落地、私钥边界、密钥管理、日志脱敏、漏洞报告渠道) |
安全设计只在架构文档里,使用者看不到 |
误操作风险(如误传私钥) |
P1 |
| G-44 |
README 阶段表 3 处 ⏳ TODO 未落地;无「如何新增平台分支 / 如何贡献」指引 |
文档与实际进度脱节 |
新人上手成本高 |
P2 |
I. 启动器 GUI 与交互(G-45 ~ G-50)
| # |
现状 |
问题 |
影响 |
严重度 |
| G-45 |
connect.ts:58 exec 的 Promise 在进程退出时才 resolve;renderer.js:91 await api.connect(ip) |
点「连接」后 IPC 一直挂起,直到串流结束 |
UI 显示「连接中」数小时;期间整个界面像卡死 |
P1 |
| G-46 |
ConnectOptions(分辨率/帧率/码率/编码器/全屏)能力已实现,但 GUI 无任何入口 |
参数写死 1920x1080@60/40Mbps/HEVC |
弱网或高分辨率屏无法调整 |
P2 |
| G-47 |
错误直接拼 e.message(mainStatus.textContent = '连接失败:' + e.message),实际显示为 /api/auth/login -> 401 |
api.ts:30 丢弃了服务端错误信息 |
用户完全看不懂 |
P1 |
| G-48 |
renderer.js:82/114 用 innerHTML 拼接 device_name、usb name、busid |
存在 DOM 注入面(当前仅被 index.html 的 CSP 单点拦住) |
CSP 一旦被放宽即成 XSS |
P1 |
| G-49 |
无「退出登录」入口 |
换账号只能重启应用 |
共享机器上无法切换身份 |
P2 |
| G-50 |
USB 面板无「已挂载设备」列表,卸载需手输 port(同 G-19 的 UI 侧) |
用户不知道挂载了什么、端口是多少 |
设备残留占用,重复挂载失败 |
P1 |
差距统计:50 条。P0 = 8(G-01/02/03/06/10/11/15/34);P1 = 30;P2 = 12。
2. 产品目标与非目标
2.1 本轮目标(3 条,正交)
| # |
目标 |
可验证的达成标志 |
| T1 安全可信 |
让「云服务器是盲协调器、被攻破也不泄露私钥与数据」这一信任模型在代码层面成立,而不只是停留在架构文档里。 |
8 条 P0 全部关闭;注入/穿越/指纹三类攻击载荷在自动化测试中 100% 被拒。 |
| T2 功能闭环 |
补完 PRD v1 已承诺但未落地的核心能力,使「登录 → 看见在线设备 → 一键连上 → 挂载 USB / 打开共享」成为默认路径,无需手工拷贝证书或跑脚本。 |
新装设备从零到首次免 PIN 连接成功,全程无手工拷贝文件;心跳与在线状态自动维持。 |
| T3 多端可用与可维护 |
让 Windows / macOS / Linux 在各项能力上有明确的可达状态与降级路径(要么能用,要么明确告知不支持),并补齐日志、测试、CI、文档,使单人可持续维护。 |
三端能力矩阵 100% 有明确结论(✅/⚠️/➖/❌);CI 全绿;核心模块行覆盖 ≥70%。 |
2.2 非目标(本轮明确不做)
- 不做公网中继与虚拟组网(Tailscale 引导仍属 V2)。
- 不做 macOS 作为被控端(虚拟显示器无原生方案,仍属 V2)。
- 不做双向 USB / 双向文件共享(仍属 V2)。
- 不做 Web 控制台、移动端 App、多租户 / 组织 / 角色权限体系。
- 不做企业级合规(等保、SOC2、审计留痕平台),不引入 K8s、微服务、消息队列、Redis。
- 不重写架构:保持三层架构与控制面/数据面边界;不替换 Express / Electron / SQLite 技术栈。
- 不做自动更新服务与遥测上报(日志仅落本地)。
- 不做「不兼容 USB 设备库」这类需要长期运营的数据资产(本轮只做「可读错误 + 本地日志」)。
3. 用户故事
3.1 团队成员(使用者)
- 作为团队成员,我希望在新设备上装好启动器、登录一次后就能自动完成证书同步,以便我不必手动拷贝证书文件或去问同事要。
- 作为团队成员,我希望设备列表能明确显示哪些设备在线、多久前活跃,且连不上时给出「设备离线 / IP 过期 / 服务器不可达」的具体原因,以便我不再盲试浪费时间。
- 作为团队成员,我希望云服务器挂掉或我不在团队网络时,仍能连接最近用过的设备,以便内网直连的能力不被一个旁路的认证服务拖垮。
- 作为团队成员,我希望挂载/卸载 USB 时能直接看到已共享和已挂载的列表并一键操作,以便我不必记住 busid 和端口号,也不会留下残留占用。
3.2 运维 / 管理员
- 作为运维,我希望服务器在密钥未配置或过弱时拒绝启动并给出明确指引,以便我不会因漏配而让整个控制面临险。
- 作为运维,我希望有统一的接口契约、错误码与故障排查手册,以便我能在同事报告「连不上」时按图索骥,而不是去读源码。
- 作为运维,我希望所有日志结构化落盘且不含密码/token,以便我能定位问题又不制造新的泄露面。
4. 需求池(EARS)
格式:[Ubiquitous] 常态 / [Event-driven] 事件触发 / [State-driven] 状态触发 / [Unwanted] 异常约束。
每条给出验收方式。编号 R-Px-nn 与第 1 节差距编号在括号内交叉引用。
4.1 P0 — 共 8 条(正确性 / 安全 / 数据丢失)
| # |
类型 |
需求 |
验收方式 |
| R-P0-01 |
[Event-driven] |
当收到登录请求且目标账号的 password_hash 格式非法时,系统应捕获异常、记录一条脱敏 warn 日志,并返回与「凭据错误」完全一致的 401 响应,不得抛出未捕获异常、不得回显原始哈希内容。 |
单测覆盖 4 种畸形值(空串 / 无冒号 / 长度不符 / 含非法 hex),全部返回 401 且 process 无 uncaughtException;响应体与错误密码响应逐字节一致。(G-01) |
| R-P0-02 |
[Ubiquitous] |
服务器启动时必须校验 JWT_SECRET:长度 ≥32 字符、非内置默认值(dev-only-change-me、change-me-to-a-long-random-string 等)、非全同字符;不满足则拒绝启动(exit code ≠ 0)并打印可执行的修复指引。仅当 NODE_ENV !== 'production' 且显式设置 ALLOW_WEAK_SECRET=1 时允许放宽(并打印醒目告警)。 |
3 组弱密钥启动测试均 exit ≠ 0 且日志含原因;.env.example 同步更新为「请生成 ≥32 位随机串」。(G-02) |
| R-P0-03 |
[Event-driven] |
当同一 IP 或同一用户名在 60 秒内登录失败 ≥5 次时,系统应返回 HTTP 429(错误码 RATE_LIMITED)并在 15 分钟内拒绝该主体的登录尝试;计数须记录在内存或 DB 中,且日志禁止记录密码原文。 |
单测连续 6 次失败,第 6 次返回 429、第 7 次仍为 429;等待窗口超时后恢复 401/200;日志断言不含密码串。(G-03) |
| R-P0-04 |
[Ubiquitous] |
服务器必须始终自行计算证书 SHA-256 指纹(sha256(cert_pem) 十六进制小写),完全忽略并覆盖请求体中的 fingerprint 字段。 |
上传 cert_pem=A, fingerprint='deadbeef' 后 GET 返回真实 sha256;单测断言返回值与 crypto 计算结果一致。(G-06;细化架构 §6.3,架构未规定冲突处理) |
| R-P0-05 |
[Event-driven] |
当启动器下载对端证书时,系统应本地重算 SHA-256(cert_pem) 并与服务端返回的 fingerprint 逐字符比对;不一致时跳过写入、记录 error 日志,并向 UI 返回 { wrote:false, reason:'fingerprint_mismatch' }。指纹为空或缺失时按不一致处理。 |
单测注入 3 组不一致数据(内容被改 / 指纹为空 / 指纹长度异常),断言 wrote=false、目标文件未被创建、无异常抛出;一致时正常写入。(G-10) |
| R-P0-06 |
[Ubiquitous] |
系统构造证书文件名时,必须对 device_name 做白名单校验:仅允许 [A-Za-z0-9._-]、长度 1–64、不以 . 开头;并且在 path.resolve() 后校验最终路径仍位于 credentials 目录内(startsWith(dir + path.sep))。任一校验失败则跳过该设备并记录 error。 |
单测覆盖 6 类载荷:../x、/etc/x、..\\x、空串、含 NUL、超长(>64),全部拒绝写入且文件树无任何越界产物。(G-11) |
| R-P0-07 |
[Ubiquitous] |
所有外部进程调用必须使用参数数组形式(execFile / spawn,shell:false),禁止将外部可控字符串拼进 shell 命令行。同时:IP 须匹配 IPv4 正则、busid 须匹配 ^\d{1,3}-\d{1,3}$、共享名须匹配 ^[A-Za-z0-9_$-]{1,64}$、应用名与设备名须匹配白名单字符集。 |
单测对上述 5 类字段各注入 4 种载荷(; rm -rf /、$(whoami)、&& ...、换行符),断言调用被拒、返回结构化错误,且 child_process mock 无任何 spawn 记录。(G-15) |
| R-P0-08 |
[Ubiquitous] |
仓库根目录必须提供 .gitignore,至少忽略:node_modules/、**/dist/、launcher/release/、server/data/*.db*(含 -wal/-shm)、.env、.DS_Store、*.log。 |
git check-ignore 对上述路径全部命中;git status --porcelain 输出不含以上路径;已存在于工作区的 server/data/onlyinone.db 与 launcher/release/** 从跟踪中移除(git rm --cached)并在 README 说明。(G-34) |
4.2 P1 — 共 29 条(关键功能缺失)
认证与服务器(R-P1-01 ~ R-P1-05)
| # |
类型 |
需求 |
验收方式 |
| R-P1-01 |
[Event-driven] |
当用户调用登出时,系统应将当前 token 的 jti 写入吊销表并在后续请求中返回 401(TOKEN_REVOKED);同时签发策略改为 access token ≤2h + refresh token ≤30d,并提供 refresh 端点。 |
单测:登出后用同一 token 请求 /api/devices 返回 401;refresh 成功换新;过期 access 返回 401。(G-04)覆盖 PRD 5.1 第 3 条「签发 JWT 供后续请求使用」,理由:原条款未定义有效期与吊销,token 泄露后无法失效。 |
| R-P1-02 |
[Ubiquitous] |
注册时用户名须 3–32 字符、匹配 ^[a-zA-Z0-9_-]+$ 且大小写不敏感去重;密码须 ≥10 字符,且不得为纯数字、纯字母或内置弱口令表成员。 |
单测:6 组非法输入均返回 400 并带具体错误码;合规输入通过。(G-05) |
| R-P1-03 |
[Ubiquitous] |
上传 cert_pem 必须:以 -----BEGIN CERTIFICATE----- 起始、-----END CERTIFICATE----- 结尾、大小 1B–8KB、且可被 crypto.X509Certificate 成功解析;否则返回 400(INVALID_CERT)。 |
单测:截断 PEM / 超长 / 含私钥块(BEGIN PRIVATE KEY)三类均被拒;合法 PEM 通过。含私钥块必须被显式拒绝并提示「请只上传公钥证书」(落实架构 §2 信任模型)。(G-07) |
| R-P1-04 |
[Ubiquitous] |
所有 API 响应统一为 { ok:boolean, data?:any, error?:{ code:string, message:string } };路径统一为 /api/v1/*,旧 /api/* 返回 308 跳转;HTTP 状态码语义固定:400 参数错误 / 401 未鉴权或已吊销 / 403 无权限 / 404 / 409 冲突 / 429 限流 / 500 内部错误。 |
新增契约测试遍历全部端点,断言响应形状与状态码矩阵。(G-09) |
| R-P1-05 |
[Ubiquitous] |
服务器须注册全局错误处理中间件与 uncaughtException / unhandledRejection 兜底:记录结构化日志并返回统一 500 JSON(INTERNAL),不回显堆栈;同时启用基础安全响应头(CSP / X-Content-Type-Options / Referrer-Policy),express.json({ limit:'64kb' }),非本机来源 CORS 默认拒绝。 |
单测:路由内主动抛错 → 返回 500 JSON 且响应体不含 at 堆栈关键字;>64KB body → 413。(G-08) |
证书同步(R-P1-06 ~ R-P1-07)
| # |
类型 |
需求 |
验收方式 |
| R-P1-06 |
[Ubiquitous] |
证书写入必须采用原子方式:写入同目录临时文件 → fsync → rename 覆盖;mkdir 失败 / EACCES / ENOENT / ENOSPC 均需捕获并返回带 code 的结构化错误(如 CRED_DIR_NO_PERMISSION),不得抛出原始异常。 |
单测:mock fs 抛 EACCES,断言返回结构化错误;模拟写入中途失败,断言目标文件保持原内容(原子性)。(G-12) |
| R-P1-07 |
[State-driven] |
当启动器已登录且本机尚未上传证书时,系统应在标准 Moonlight 证书位置自动发现公钥证书并上传;未发现时,UI 应展示「首次配对引导」(说明需在 Moonlight 中手动配对一次,提供刷新按钮)。发现到多个候选项时,取最新修改的一个并提示用户确认。 |
单测:mock 证书目录 0/1/2 个文件三种情形,断言上传行为与提示文案;人工验收:全新设备登录后自动完成上传,无需手工拷文件。(G-14;落实 PRD 5.2 State-driven) |
外部命令(R-P1-08 ~ R-P1-09)
| # |
类型 |
需求 |
验收方式 |
| R-P1-08 |
[Ubiquitous] |
每个外部进程调用必须带 timeout(连接 10s / USB 20s / 单主机探测 2s),超时即 kill 子进程并返回可读错误(TIMEOUT);需提权的命令(usbip attach)必须走显式提权入口(macOS osascript / Windows UAC / Linux pkexec),且无交互 TTY 时直接失败返回 ELEVATION_REQUIRED,禁止静默等待密码输入。 |
单测注入永不退出的 mock 命令,断言 10s 内返回 TIMEOUT 且子进程被 kill;断言所有调用均带 shell:false。(G-16) |
| R-P1-09 |
[State-driven] |
当功能入口所需的外部依赖(Sunshine / Moonlight / usbipd / usbip / SMB 服务)缺失,或版本低于最低要求时,系统应在该入口展示「未安装 / 版本过低 + 对应平台的安装指引链接」,并禁用操作按钮,而不是抛出原始命令错误。 |
单测:mock 依赖检测返回 missing / outdated,断言 UI 状态与文案;各平台最低版本写入 docs/troubleshooting.md。(G-17) |
多端适配(R-P1-10 ~ R-P1-13)
| # |
类型 |
需求 |
验收方式 |
| R-P1-10 |
[Ubiquitous] |
设备发现须实现三端 mDNS:macOS dns-sd、Linux avahi-browse、Windows 采用待确认方案(见 Q3);任一实现不可用时回落子网扫描,且扫描失败不得阻断登录与其他功能。 |
单测 mock 各平台命令输出,断言解析正确;Windows 端至少保证「回落扫描可用」。(G-18) |
| R-P1-11 |
[Ubiquitous] |
usb.ts 须按平台分派:win32 → usbipd;darwin / linux → usbip。并新增 listAttached() 查询本机已挂载设备(usbip port 输出解析),替代手工输入端口。所有平台不支持的操作须返回 UNSUPPORTED_ON_PLATFORM 并在 UI 隐藏对应按钮。 |
单测:三平台 process.platform mock 下断言调用的命令与参数;listAttached 解析 3 种真实输出格式(含空列表、中文设备名)。(G-19、G-50) |
| R-P1-12 |
[Ubiquitous] |
子网扫描须:对候选 IP 去重、剔除网络号(.0)与广播地址(.255)、并发上限默认 64(可配)、单主机超时 2s;candidateIps() 对同一网段只生成一次。 |
单测:mock 出 3 个同网段网卡 + 1 个不同网段,断言结果为 253 + 253 = 506 条、无重复、不含 .0/.255;扫描测试断言并发峰值 ≤64、总耗时 ≤8s(mock)。(G-20) |
| R-P1-13 |
[Event-driven] |
当用户在被控端点击「开启本机文件共享」时,系统应调用平台脚本创建共享并返回可直接复制的 smb:// 地址;Windows 使用 New-SmbShare -EncryptData $true,macOS / Linux 方案见 Q4。共享目录与名称须持久化到设置,重启后状态可恢复。 |
单测:mock 脚本调用断言参数正确;人工验收:Windows 端一键开启后 macOS 可挂载并读写。(G-21;落实 PRD 5.4 Event-driven) |
状态与在线(R-P1-14 ~ R-P1-17)
| # |
类型 |
需求 |
验收方式 |
| R-P1-14 |
[State-driven] |
当启动器已登录且处于运行态时,系统应每 30 秒(可配 15–300s,带 ±10% 抖动)自动上报心跳,携带设备名、平台、局域网 IP;连续 2 次失败则进入「离线降级」状态。手动「上线」按钮降级为「立即刷新」。 |
单测:使用 fake timer 推进 90s,断言心跳调用 3 次且间隔含抖动;连续失败 2 次后断言状态转为 offline_degraded。(G-24)覆盖 PRD 5.2 第 1 条,理由:原文「上线时上报」过于宽松,导致在线状态必然失真。 |
| R-P1-15 |
[Ubiquitous] |
设备列表须基于 last_seen 判定状态:≤90s 在线(绿点)、≤7d 近期(灰点 + 相对时间)、其余离线(空心点 + 绝对日期);离线设备的「连接」按钮禁用并提示原因。 |
单测:3 个时间边界值断言状态映射正确;UI 快照检查点样式与提示文案。(G-25) |
| R-P1-16 |
[Unwanted] |
若账户服务器不可达(网络错误 / 5xx / 超时),系统不得阻断已有功能:应继续使用本地缓存的证书与最近一次成功拉取的设备列表发起连接,并在界面顶部常驻展示「⚠️ 离线模式(服务器不可达),功能可能不是最新」。缓存须加密校验完整性。 |
单测:断网 mock 下断言连接流程可完成且状态为 offline_degraded;人工验收:kill 掉服务器后仍能连接 5 分钟前在线的设备。(G-26;落实 PRD 5.5 Unwanted) |
| R-P1-17 |
[Ubiquitous] |
登录凭据(token)须持久化到系统钥匙串:macOS Keychain、Windows Credential Manager、Linux 方案见 Q5。启动时自动恢复并静默续期;登出 / 令牌失效时清除。在 Q5 确认前,Linux 端维持「仅内存、重启重登」,不引入自制加密文件。 |
单测:抽象 CredentialStore 接口,用内存实现覆盖「保存-读取-清除-不存在」四态;人工验收:macOS 与 Windows 重启后无需重登。(G-27)部分覆盖 PRD 5.5 第 2 条,理由:Linux 端方案未定,宁可不存也不做错。 |
可维护性(R-P1-18 ~ R-P1-20)
| # |
类型 |
需求 |
验收方式 |
| R-P1-18 |
[Ubiquitous] |
server 与 launcher 均须使用统一日志模块:结构化 JSON、分级(debug/info/warn/error)、带 ISO 时间戳与模块名;server 输出 stdout + 文件轮转(单文件 ≤5MB,保留 3 份),launcher 输出 userData/logs/(同样轮转)。日志中禁止出现密码、token、私钥、完整证书内容(证书只记指纹前 8 位)。 |
单测:构造含 password/token/BEGIN PRIVATE KEY 的输入,断言日志输出经脱敏(出现 *** 且不出现原文);断言轮转文件数量上限。(G-29) |
| R-P1-19 |
[Ubiquitous] |
配置须集中到单一模块并在启动时校验:server(PORT / JWT_SECRET / DB_PATH / LOG_LEVEL / RATE_LIMIT_*)、launcher(serverUrl / heartbeatInterval / scanConcurrency / logLevel)。数据库路径须基于模块文件所在目录解析,不得使用 process.cwd()。 |
单测:在临时目录以不同 cwd 启动两次,断言解析到同一 DB 路径;缺必填项时启动失败并打印缺失项清单。(G-30) |
| R-P1-20 |
[Ubiquitous] |
数据库须支持版本化迁移:引入 schema_migrations(version, applied_at) 表与 migrations/ 脚本目录,启动时按序执行未应用的迁移;执行前自动备份 db 文件(保留最近 3 份),迁移失败则回滚备份并拒绝启动。 |
单测:从空库连续应用 3 个迁移,断言版本记录与最终表结构;注入失败迁移,断言回滚且数据未丢。(G-33) |
测试与 CI(R-P1-21 ~ R-P1-23)
| # |
类型 |
需求 |
验收方式 |
| R-P1-21 |
[Ubiquitous] |
server 测试须新增覆盖:密码校验异常、登录限流、token 吊销与过期、指纹强制计算、PEM 校验(含私钥块拒绝)、统一响应与错误码、DB 迁移。用例总数 ≥24,全部通过。 |
cd server && npm test 输出用例数 ≥24、失败 0;覆盖率报告生成。(G-37) |
| R-P1-22 |
[Ubiquitous] |
launcher 测试须新增:usb(平台分派 + 注入 + listAttached 解析)、share(注入 + 平台分支)、settings(依赖注入后可测)、discover(去重 + 并发上限 + 三端 mDNS 解析)、agent(心跳周期 + 降级)、connect(指纹校验 + 路径穿越 + 注入)、CredentialStore 抽象。用例总数 ≥30,connect/discover/usb/agent 行覆盖 ≥70%。 |
cd launcher && npm test 输出用例数 ≥30、失败 0;--coverage 报告显示上述四模块 ≥70%。(G-38) |
| R-P1-23 |
[Ubiquitous] |
仓库须提供 CI 工作流:每次 push / PR 执行 npm ci、类型检查、npm test(server + launcher)、npm run build;生成覆盖率报告;低于门禁(server 60% / launcher 核心四模块 70%)时流水线失败。 |
新建分支推一次触发 CI,断言全绿;人为降低覆盖率后断言流水线失败。(G-39) |
文档(R-P1-24 ~ R-P1-26)
| # |
类型 |
需求 |
验收方式 |
| R-P1-24 |
[Ubiquitous] |
须新增 docs/api.md:覆盖 100% 端点的路径、方法、鉴权、请求字段(类型/必填/约束)、响应示例、错误码表、版本策略。 |
逐端点比对代码与文档,端点数为 N 则文档条目数为 N。(G-41) |
| R-P1-25 |
[Ubiquitous] |
须新增 docs/troubleshooting.md:≥10 条常见故障,每条含「现象 → 可能原因(按概率排序)→ 逐步处置 → 仍不行怎么办(如何导出日志)」。须覆盖:证书同步失败、指纹不一致、USB 挂载失败、发现不到设备、SMB 打不开、心跳失败、服务器起不来、DB 锁定。 |
条目数 ≥10;抽查 3 条由未参与开发的人按文档操作能定位问题。(G-42) |
| R-P1-26 |
[Ubiquitous] |
须新增 docs/security.md:信任模型落地说明(哪些数据上云、哪些永不上云)、私钥边界、JWT 密钥生成与轮换步骤、日志脱敏规则、已知限制、漏洞报告渠道。 |
由非作者审阅一遍能回答「私钥会不会上云」。须明确写出:「任何情况下都不要上传 device.key,只传 .pem/.crt 公钥证书。」(G-43) |
GUI(R-P1-27 ~ R-P1-29)
| # |
类型 |
需求 |
验收方式 |
| R-P1-27 |
[Event-driven] |
当用户点击「连接」时,系统应以 spawn + detached 方式拉起 Moonlight 并在 ≤3 秒内向 UI 返回「已发起连接」(基于进程成功 spawn 或 3s 存活判定),不得等待串流进程退出。 |
单测:mock spawn 后断言 Promise 在 3s 内 resolve;人工验收:点击后界面立即可继续操作,串流窗口独立存在。(G-45) |
| R-P1-28 |
[Ubiquitous] |
所有面向用户的错误须经统一映射表转换为「人话文案 + 错误码 + 处置建议」三段式(例:RATE_LIMITED →「尝试过于频繁,请 15 分钟后再试(错误码 RATE_LIMITED)」)。禁止直接展示 e.message 原始内容(如 /api/auth/login -> 401)。 |
单测:遍历错误码表断言每个码都有映射文案(覆盖率 100%);代码搜索断言渲染层无 e.message 直接拼接。(G-47) |
| R-P1-29 |
[Ubiquitous] |
渲染层禁止用 innerHTML 插入任何来自网络或外部进程的数据;改用 textContent / createElement,或经显式 HTML 转义函数处理。device_name、usb name、busid、platform 一律视为不可信输入。 |
代码搜索断言渲染层无 innerHTML = 拼接外部变量;单测对转义函数注入 <img onerror> 等 4 种载荷断言被转义。(G-48) |
4.3 P2 — 共 13 条(体验与工程优化)
| # |
类型 |
需求 |
验收方式 |
| R-P2-01 |
[State-driven] |
当服务端证书集合中已不含某设备、而本地存在 moonlight-<device>.pem 时,系统应删除该陈旧文件(仅删除符合本工具命名规则的文件)。 |
单测:本地 3 个文件、服务端返回 1 个,断言删除 2 个且命名不规则的同名文件未被误删。(G-13) |
| R-P2-02 |
[Ubiquitous] |
sunshineCredDir() 应改为「平台默认路径 + 已知候选路径探测 + 设置项覆盖 + 未检出时明确提示 Sunshine 未安装」,而非硬编码。 |
单测:mock 三种文件系统布局断言探测结果;未检出时返回 null 并给出安装指引。(G-22) |
| R-P2-03 |
[Ubiquitous] |
若 Q1 确认纳入 Linux,须补充 scripts/linux-setup.sh,覆盖 Sunshine 安装引导、usbip 内核模块加载、Samba 引导。 |
脚本在 Ubuntu 22.04 / 24.04 各跑通一次(--dry-run 亦可)。(G-23) |
| R-P2-04 |
[Ubiquitous] |
启动器应支持托盘常驻与开机自启(默认关闭,设置项开启);关闭窗口时最小化到托盘而非退出(Windows/Linux)。 |
人工验收:三端设置项可开关,重启后生效。(G-28) |
| R-P2-05 |
[Ubiquitous] |
settings.ts 应改为依赖注入(构造时传入路径解析器),移除 require('electron') + try/catch 环境嗅探。 |
单测可在无 Electron 环境下直接构造并测试读写。(G-31) |
| R-P2-06 |
[Ubiquitous] |
main.ts 应实现:单实例锁(requestSingleInstanceLock)、窗口位置与尺寸持久化、uncaughtException / unhandledRejection 兜底提示与日志。 |
人工验收:二次启动时聚焦已有窗口;重启后窗口位置保持。(G-32) |
| R-P2-07 |
[Ubiquitous] |
引入 ESLint + Prettier 与统一配置;删除 package.json 中手工罗列文件的 typecheck:core,改为复用 tsconfig.json。 |
npm run lint 零 error;npm run typecheck 覆盖 src/**/*。(G-35) |
| R-P2-08 |
[Ubiquitous] |
launcher/dist/ 与 launcher/release/ 不再纳入版本库(依赖 R-P0-08 的 .gitignore),产物仅由 CI 与本地构建生成。 |
git status --porcelain 中不含 launcher/dist、launcher/release。(G-36) |
| R-P2-09 |
[Ubiquitous] |
将 server/smoke.mjs 脚本化为 npm run smoke,自动拉起临时端口服务、跑完整链路、自动清理退出。 |
npm run smoke 一键通过且结束后无残留进程。(G-40) |
| R-P2-10 |
[Ubiquitous] |
README 阶段映射表须与 v2 增量范围同步:移除已落地项、把本轮新增项归入对应阶段、补齐「如何新增平台分支」的贡献指引。 |
审阅:README 无与代码现状矛盾的陈述。(G-44) |
| R-P2-11 |
[Ubiquitous] |
设置页须暴露连接参数(分辨率 / 帧率 / 码率 / 编码器 / 全屏)并持久化,连接时作为默认值传入。 |
单测:设置写入后 connectToDevice 接收的参数与设置一致。(G-46) |
| R-P2-12 |
[Event-driven] |
当用户在主界面点击「退出登录」或收到 TOKEN_REVOKED / 401 时,系统应清除本地凭据与缓存、停止心跳,并跳回登录页。 |
单测:调用登出后断言 CredentialStore 为空、心跳定时器已清除。(G-49) |
| R-P2-13 |
[Ubiquitous] |
主界面须展示本机当前角色(主控 / 被控 / 双向预留)与依赖就绪状态灯(Sunshine / Moonlight / 证书 / 心跳),仅展示,不含切换功能(切换属 V2)。 |
UI 检查:4 个状态灯随检测结果变化。(为 V2 双向控制预留骨架) |
5. 多端适配矩阵
图例:✅ 已支持且可用 · ⚠️ 部分支持 / 实验性 / 需验证 · ❌ 不可用 · ➖ 本轮明确不做(非缺陷)
5.1 现状(v1.0 代码实际状态)
| 能力 |
Windows |
macOS |
Linux |
| 串流被控(Sunshine) |
✅ 脚本 + NVENC 预设 |
➖ V2(虚拟显示器未解) |
❌ 未纳入 |
| 串流主控(Moonlight) |
✅ moonlight stream |
✅ |
❌ 未纳入 |
| USB 共享(导出) |
✅ usbipd bind |
❌ 无 usbipd |
❌ |
| USB 挂载(导入) |
❌ |
⚠️ usbip attach,需 sudo、无已挂载列表 |
❌ |
| 文件共享 导出(开启本机 SMB) |
❌ scripts/setup-smb.ps1 有但未接入启动器 |
❌ |
❌ |
| 文件共享 导入(打开对端 SMB) |
✅ explorer |
✅ open smb:// |
⚠️ 仅 xdg-open,未验证 |
| 局域网发现(mDNS) |
❌ 直接返回空 |
✅ dns-sd |
❌ 直接返回空 |
| 子网扫描兜底(47990) |
⚠️ 可用但有并发/去重缺陷 |
⚠️ 同上 |
⚠️ 同上 |
| 凭据持久化(钥匙串) |
❌ |
❌ |
❌ |
| 外部命令安全执行 |
❌ 全部 exec 拼接 |
❌ 同左 |
❌ 同左 |
| 启动器安装包 |
✅ NSIS exe |
✅ dmg(未公证) |
❌ |
5.2 本轮目标
前提:Linux 是否纳入本轮取决于 Q1。下表「目标(纳入 Linux)」与「目标(不纳入 Linux)」两栏并列,请皇上裁决。
| 能力 |
Windows 目标 |
macOS 目标 |
Linux 目标(若纳入) |
Linux 目标(若不纳入) |
| 串流被控 |
✅ 保持 + 预设可切换 |
➖ V2 不变 |
⚠️ 实验性:Sunshine Linux 需实测(VAAPI/NVENC),无虚拟显示器 |
❌ 启动器提示「暂不支持」 |
| 串流主控 |
✅ 保持 |
✅ 保持 |
⚠️ 实验性:Moonlight Linux 客户端 |
❌ 同上 |
| USB 共享(导出) |
✅ 保持 + 参数化 |
➖ V2 |
⚠️ usbip bind(需加载 usbip-core/vhci_hcd) |
❌ |
| USB 挂载(导入) |
➖ 本轮不做(无 usbip 客户端场景) |
✅ 平台分派 + 提权入口 + 已挂载列表 |
⚠️ usbip attach + 已挂载列表 |
❌ |
| 文件共享 导出 |
✅ 接入 New-SmbShare -EncryptData |
⚠️ 方案待定(Q4) |
⚠️ Samba 引导(Q4) |
❌ |
| 文件共享 导入 |
✅ |
✅ |
✅ xdg-open(需实测) |
✅(同左,作为「能打开共享」的最低支持) |
| 局域网发现 mDNS |
⚠️ 方案待定(Q3),未定前保持扫描兜底 |
✅ 保持 |
⚠️ avahi-browse(需 avahi-utils) |
➖ |
| 子网扫描兜底 |
✅ 去重 + 并发 ≤64 |
✅ 同左 |
✅ 同左 |
✅ 同左(扫描与平台无关) |
| 凭据持久化 |
✅ Credential Manager |
✅ Keychain |
⚠️ 方案待定(Q5);未定前仅内存 |
➖ 仅内存 |
| 外部命令安全执行 |
✅ 参数数组 + 超时 |
✅ 同左 |
✅ 同左 |
✅ 同左(跨端统一改造) |
| 依赖预检与指引 |
✅ |
✅ |
✅ |
✅(至少给出「不支持」的明确提示) |
| 启动器安装包 |
✅ 保持 |
✅ 保持 |
⚠️ AppImage/deb 待定(Q2) |
❌ |
跨端统一要求(不分平台):所有外部命令参数化(R-P0-07)、统一错误映射(R-P1-28)、统一日志(R-P1-18)、平台不支持时返回 UNSUPPORTED_ON_PLATFORM 并隐藏入口,而非抛原始错误。
6. UI / 交互改进建议
现状:单页两视图(登录 / 主界面),主界面 5 张卡片 —— 本机状态、我的设备、USB 设备、文件共享、底部状态行。整体结构可用,问题集中在「状态不自动、错误不可读、操作需手工记忆」。
6.1 登录视图
| 现状 |
建议 |
对应需求 |
| 服务器地址已持久化 ✅ |
保留;新增地址格式校验与「检测到使用 http,控制面未加密」的显式提示 |
R-P1-04、Q7 |
| 无自动登录 |
新增「记住我」勾选(默认开),凭据存钥匙串后启动时静默恢复 |
R-P1-17 |
错误文案为 /api/auth/login -> 401 |
改为三段式:人话文案 + 错误码 + 处置建议 |
R-P1-28 |
| 注册无强度反馈 |
实时显示密码强度与规则(≥10 位、非纯数字/纯字母) |
R-P1-02 |
| 无离线入口 |
服务器不可达且本地有缓存凭据时,提供「以离线模式继续」按钮 |
R-P1-16 |
6.2 本机状态卡片
| 现状 |
建议 |
对应需求 |
| 需手动点「上线」 |
登录后自动开始心跳,按钮改为「立即刷新」;显示最近一次心跳时间与结果 |
R-P1-14 |
| 只显示设备名 |
增加:本机角色(主控/被控/双向预留)、依赖就绪状态灯(Sunshine / Moonlight / 证书 / 心跳) |
R-P2-13 |
| 无退出登录 |
右上角增加「退出登录」 |
R-P2-12 |
| 无离线标识 |
服务器不可达时卡片变黄 + 顶部常驻「⚠️ 离线模式」条 |
R-P1-16 |
6.3 设备卡片
| 现状 |
建议 |
对应需求 |
绿点恒亮、只显示 last_seen 时间 |
在线/近期/离线三态点 + 相对时间(「3 分钟前」) |
R-P1-15 |
| 无 IP 时点击才提示 |
无 IP 或离线时「连接」按钮直接禁用并 tooltip 说明原因 |
R-P1-15 |
平台显示原始字符串 win32 |
映射为中文 + 图标(Windows / macOS / Linux) |
R-P1-29 |
| 连接参数写死 |
设备卡片保持「一键连接」(用默认参数),参数调整放入设置页 |
R-P2-11 |
| 无「打开该设备共享」入口 |
设备卡片增加「共享」按钮,自动带出 IP 与默认共享名 |
R-P1-13 |
6.4 USB 面板
| 现状 |
建议 |
对应需求 |
| 刷新失败提示「本机非 Windows 被控端?」 |
按平台显示可用操作;不支持的平台直接隐藏对应区块并显示说明 |
R-P1-11 |
| 只有「共享」按钮,无已共享列表 |
拆为两栏:本机 USB 设备(可共享/已共享状态 + 取消共享)、已挂载设备(来自 listAttached(),一键卸载) |
R-P1-11、G-50 |
| 挂载/卸载需手输 host + busid + port |
保留手工输入(跨平台场景仍需要),但增加输入格式校验与错误提示;同账户设备可从下拉框选择自动带出 IP |
R-P0-07 |
| sudo 无反馈 |
提权前明确提示「将请求管理员权限」,提权失败给出 ELEVATION_REQUIRED 的可读说明 |
R-P1-08 |
6.5 文件共享面板
| 现状 |
建议 |
对应需求 |
| 只有「打开对端共享」 |
拆为两栏:本机共享(开启/关闭、目录选择、复制 smb:// 地址)、打开对端共享(可从设备列表带出 IP) |
R-P1-13 |
| 共享名手输,无校验 |
共享名格式校验 + 默认值 OnlyInOne 预填 |
R-P0-07 |
| 无状态显示 |
显示本机共享是否启用、共享目录路径、对端连接提示 |
R-P1-13 |
6.6 全局
- 状态提示:底部单行状态改为可关闭的 Toast(成功 3s 自动消失,错误常驻至手动关闭或下次操作)。
- 错误规范:统一「文案 + 错误码 + 处置建议」三段式,错误码可一键复制(便于在群里求助)。
- 设置页:服务器地址、心跳间隔、扫描并发、连接参数(分辨率/帧率/码率/编码器/全屏)、日志级别与「导出日志」按钮、托盘与开机自启开关。
- 无障碍与适配:最小窗口 900×640;设备/USB 列表区超 6 条时内部滚动而非撑高窗口;保持当前深色主题,暂不做浅色(P2,未列入需求池)。
7. 验收标准(可量化)
7.1 安全(P0 硬性门槛,任一不达标则本轮不通过)
| 指标 |
目标 |
| 命令注入载荷拦截率 |
100%(5 类字段 × 4 种载荷 = 20 组,全部被拒且无子进程产生) |
| 证书文件名路径穿越拦截率 |
100%(6 类载荷全部拒绝,无越界文件产生) |
| 指纹不一致证书写入率 |
0%(3 组不一致数据全部跳过写入) |
畸形 password_hash 登录 |
100% 返回 401,无未捕获异常,响应体与「凭据错误」一致 |
弱 / 默认 JWT_SECRET 启动 |
100% 拒绝启动(exit ≠ 0) |
| 登录限流 |
第 6 次失败返回 429,且日志中密码原文出现次数 = 0 |
含私钥块(BEGIN PRIVATE KEY)的证书上传 |
100% 拒绝并返回 INVALID_CERT |
git status --porcelain 中的敏感/产物路径 |
0 条(*.db、release/、dist/、.env、node_modules/) |
7.2 功能
| 指标 |
目标 |
| 心跳周期 |
30s ± 10%(可配 15–300s),fake timer 验证 90s 内 3 次 |
| 服务器不可达 → 进入离线降级 |
≤60s,且离线模式下仍能对 5 分钟前在线的设备发起连接 |
| 在线判定阈值 |
last_seen ≤ 90s 判在线,误差 ≤1 个心跳周期 |
| 证书同步耗时(10 台设备) |
≤2s;首次登录到证书上传完成 ≤10s |
| 本机证书自动发现上传成功率 |
100%(证书存在于标准路径时) |
| 新设备从安装到首次免 PIN 连接成功 |
全程无需手工拷贝任何文件(人工验收 1 次) |
| 点击「连接」到 UI 返回「已发起连接」 |
≤3s,且期间界面可继续操作 |
| 一键连接成功率(已同步证书 + 目标在线) |
100%(人工验收 Windows 被控 ⇄ macOS 主控 各 1 次) |
7.3 多端
| 指标 |
目标 |
| 三端 mDNS 或扫描兜底可用率 |
每端至少有一种发现方式可用 |
| 子网扫描并发峰值 |
≤64(可配) |
| 单网段扫描完成耗时 |
≤8s(含去重后 253 个地址) |
| Linux(若纳入) |
启动器可安装、可登录、可连接;USB/被控标注为实验性 |
| 不支持的平台/操作 |
100% 返回 UNSUPPORTED_ON_PLATFORM 并隐藏入口,不抛原始错误 |
7.4 质量与工程
| 指标 |
目标 |
| server 测试用例数 |
≥24,通过率 100% |
| launcher 测试用例数 |
≥30,通过率 100% |
核心模块行覆盖(connect/discover/usb/agent) |
≥70% |
| server 整体行覆盖 |
≥60% |
| CI 流水线 |
push / PR 全绿;人为降低覆盖率后必须失败 |
npm run build 与类型检查 |
零错误 |
| ESLint(新增代码) |
零 error |
| 原有 8 + 4 个用例 |
全部保持通过(非回归) |
7.5 文档
| 指标 |
目标 |
docs/api.md 端点覆盖 |
100%(端点数 = 文档条目数) |
docs/troubleshooting.md 条目 |
≥10 条,含「现象→原因→处置→导出日志」四段 |
docs/security.md |
1 份,非作者审阅后能回答「私钥会不会上云」 |
| 错误码映射表覆盖 |
100%(每个错误码都有人话文案) |
8. 待确认问题
以下需皇上裁决,PM 不自行拍板技术选型。每项给出 PM 建议与不确认时的默认处理。
| # |
问题 |
选项 |
PM 建议 |
不确认时的默认处理 |
| Q1 |
Linux 是否纳入本轮支持范围? |
A. 纳入(实验性)/ B. 本轮不纳入,仅保证「明确提示不支持」 |
B。团队 ≤10 人,实际设备是 Windows 小主机 + MacBook;Linux 三端全验证成本高,容易稀释 P0 安全修复的精力 |
按 B 执行 |
| Q2 |
若纳入 Linux,是否产出安装包(AppImage / deb)? |
A. 产出 AppImage/B. 仅源码运行 |
A(仅当 Q1=A) |
跳过 |
| Q3 |
Windows 端 mDNS 发现如何实现? |
A. 引入 Bonjour 原生 SDK/B. 使用 npm 的 mDNS 库(如 bonjour-service)/C. 不做 mDNS,Windows 只用子网扫描 |
C。跨网段不适用,且引入原生依赖会显著增加打包复杂度;扫描兜底已能满足局域网场景 |
按 C 执行 |
| Q4 |
macOS / Linux「开启本机 SMB 共享」如何实现? |
A. 调用 sharing -a 等命令行/B. 直接改写 /etc/smb.conf(需提权)/C. 仅引导用户到系统设置并给出步骤 |
C。写入系统 SMB 配置风险高且易与系统更新冲突,团队内部场景下引导一次即可 |
按 C 执行(仅 Windows 一键开启) |
| Q5 |
Linux 端凭据持久化方案? |
A. libsecret(Secret Service)/B. 加密文件(密钥派生自用户口令)/C. 不持久化 |
C。自制加密容易做错;宁可重启重登 |
按 C 执行 |
| Q6 |
心跳 30s / 在线阈值 90s 是否合适?笔记本合盖或休眠时是否放宽? |
A. 保持 30s/90s/B. 放宽到 60s/180s/C. 按电源状态自适应 |
A。团队场景多为常驻小主机;若 Battery 场景多则改 C。阈值做成可配即可 |
按 A 执行,参数可配 |
| Q7 |
是否允许启动器通过明文 HTTP 访问控制面? |
A. 强制 HTTPS(需定自签证书分发方案)/B. 允许 HTTP 但显式提示风险/C. loopback 允许、跨机强制 HTTPS |
C。本地调试必须走 localhost;跨机一律 HTTPS,部署文档给出自签或 Let's Encrypt 步骤 |
按 C 执行。覆盖 PRD 5.1 第 2 条,理由:原文「所有控制面 API 强制 HTTPS」会阻塞本地调试,且 README 已指导用户填 http://localhost:3000;用「显式风险提示 + 部署要求 TLS」平衡 |
| Q8 |
外部依赖(Sunshine / Moonlight / usbipd / usbip)安装策略? |
A. 启动器自动安装(winget/brew/apt)/B. 仅给指引链接,用户自行安装/C. 检测 + 一键跳转下载页 |
C。自动安装涉及管理员权限与静默参数稳定性,性价比低 |
按 C 执行(R-P1-09 只做检测与指引) |
| Q9 |
是否需要「托盘常驻 + 开机自启」? |
A. 需要(被控端必须常驻心跳)/B. 不需要 |
A,但默认关闭、由用户开启。被控端不常驻会导致设备永远显示离线 |
按 A 执行(列为 P2) |
| Q10 |
启动器 GUI 技术栈是否维持原生 HTML/JS? |
A. 维持原生/B. 引入 React/Vue + 构建链 |
A。当前 GUI 逻辑简单,引入框架会增加构建与打包复杂度 |
按 A 执行 |
| Q11 |
本轮是否做代码签名与公证(macOS 公证 / Windows 代码签名证书)? |
A. 做(需付费证书 + 开发者账号)/B. 不做,维持 identity: null + 右键打开 |
B。团队内部分发,签名不阻塞功能;但不签名的安全提示会让新同事以为是病毒,建议至少在 README 写清 |
按 B 执行,README 补充说明 |
| Q12 |
日志是否需要集中收集? |
A. 仅本地落盘/B. 上报到内部服务 |
A。≤10 人团队,出问题让用户「导出日志」发群里即可;集中收集会引入新的数据面与隐私面 |
按 A 执行 |
| Q13 |
设备记录清理策略? |
A. 不清理/B. 自动清理 30 天未活跃设备 |
A。≤10 人,数据量可忽略;自动清理容易误删在用设备 |
按 A 执行 |
| Q14 |
V2 的「Mac 被控 / 双向控制」是否提前排期? |
A. 保持 V2/B. 提前到本轮 |
A。macOS 虚拟显示器无原生方案,属硬阻塞,不应挤占本轮安全与闭环的精力 |
按 A 执行 |
9. 与旧版 PRD / 架构文档的冲突与覆盖声明
| # |
旧版条款 |
本轮处理 |
理由 |
| 1 |
PRD 5.1:「所有控制面 API 强制 HTTPS」 |
覆盖。改为:loopback(127.0.0.1/localhost)允许 HTTP;跨机一律 HTTPS;任何 HTTP 连接须在 UI 显式提示「控制面未加密」 |
原文会阻塞本地调试,且与 README「本机调试填 http://localhost:3000」矛盾;安全上通过「显式风险提示 + 部署文档要求 TLS」平衡(见 Q7) |
| 2 |
PRD 5.1:「注册/登录时签发 JWT 供后续请求使用」 |
细化覆盖。改为 access token ≤2h + refresh token ≤30d + jti 吊销表 + 登出端点 |
原文未定义有效期与吊销,token 泄露后 7 天内无法失效(R-P1-01) |
| 3 |
PRD 5.2:「当设备启动器上线时,向账户服务器心跳上报在线状态与 IP」 |
细化覆盖。改为「登录成功后自动开始周期心跳(30s±10%),手动『上线』降级为『立即刷新』」 |
原文「上线时上报」过于宽松,单次上报导致在线状态必然失真(R-P1-14) |
| 4 |
PRD 5.5:「敏感信息(token/证书)存储于系统钥匙串(Keychain / Credential Manager)」 |
部分覆盖。macOS Keychain、Windows Credential Manager 按原文实现;Linux 端在 Q5 确认前维持「仅内存、重启重登」,不引入自制加密文件 |
自制加密容易做错,宁可不存也不做错(R-P1-17) |
| 5 |
PRD 4:「V1 设备矩阵:仅 Windows 小主机 + MacBook」 |
待裁决。本轮把 Linux 列为「可选目标」,是否纳入取决于 Q1;不纳入时仅需保证「明确提示不支持」而非静默失败 |
甲方本次提出「增加多端适配」诉求,与原文冲突,需皇上裁决 |
| 6 |
架构 §6.3:「服务器对证书算 SHA256 指纹并返回,客户端下载后校验」 |
确认并强化。补充:服务端必须忽略并覆盖客户端提交的 fingerprint 字段(原文未规定冲突处理);客户端校验失败时跳过写入并告警,而非「下载后校验」这种可选项 |
原文缺少冲突处理与失败处置,导致该条款在代码中被完全绕过(R-P0-04、R-P0-05) |
| 7 |
PRD 5.3:「若设备挂载失败,系统给出明确错误并记录不兼容设备」 |
部分覆盖。本轮只做「明确错误 + 结构化日志」,不做「不兼容设备库」这类需长期运营的数据资产 |
团队 ≤10 人,维护不兼容设备库的收益低于成本;避免企业级过度设计(见 §2.2 非目标) |
| 8 |
架构 §6.4:「V1 只同步客户端证书(单向免 PIN)」 |
确认不变。本轮不引入服务端证书同步 |
维持 V1 范围 |
| 9 |
架构 §6.5:「文件共享 V1 仅 Win→Mac 单向」 |
确认不变。本轮只补「Windows 一键开启共享」这一半,不引入反向共享 |
维持 V1 范围 |
10. 建议落地顺序
| 批次 |
内容 |
建议节奏 |
| 批次 1(安全) |
R-P0-01 ~ R-P0-08,配套 R-P1-21 / R-P1-22 的注入、穿越、指纹用例 |
单独一个 PR 合入,优先于任何功能开发 |
| 批次 2(闭环) |
R-P1-06 / R-P1-07 / R-P1-14 ~ R-P1-17 / R-P1-27 / R-P1-28 |
使主路径「登录 → 在线设备 → 一键连接」真正跑通 |
| 批次 3(多端与工程) |
R-P1-08 ~ R-P1-13、R-P1-18 ~ R-P1-20、R-P1-23 |
平台分派、日志、配置、迁移、CI |
| 批次 4(文档与体验) |
R-P1-24 ~ R-P1-26、R-P1-29、R-P2-* |
文档与体验收尾 |
每批次合入前须满足:本批次涉及的测试全绿、CI 通过、相关文档已更新。