# 已知取舍 - **`net` / `diskio` 是速率指标,首个样本必然是 `null`**,要等第二次采集才有读数。 这是刻意的:没有基线时「0 B/s」会画成一条贴底的平线,读起来像「这台设备的网络闲着」, 而那是比「还不知道」强得多的断言。 - **`publicSample()` 会剥掉 `netRows` / `dioRows`**(跨样本基线)。它们只服务于下一轮差分, 不下发浏览器;所以「网络有没有在动」要看 `net.items`,而不是这些内部字段。 - **编辑页的「测试连接」用表单里的凭据试连**;密码框留空时,Host 会**补上这台设备已保存的密码** (与保存的合并语义一致),所以「只改了个描述、顺手点一下测试」不会再失败。 想验证「新密码对不对」,把密码**填进去**再测——填了就以填的为准。 - **终端不是终端模拟器**:没有引入 xterm,而是一个文本窗格 + 控制键条(与 device-manager 一致)。 手写的客户端模块拿不到 DSH 内置终端用的私有 `require.async`,xterm 在那个上下文里不可达。 后果是全屏类程序(`vim`、`top -b` 之外的全屏模式)显示不会正确;日常诊断命令不受影响。 - **文本窗格剥掉 ANSI,而不是解释它**:颜色、光标定位、模式切换(`[?25h` / `[?2004h`)、 窗口标题与超链接都被删除,`\r` 归一成 `\n`,退格当删除。代价是**看不到任何颜色**—— 这是刻意的:一个不画 ANSI 的 `
` 里,保留序列的唯一效果就是让用户读到
  `[0m[01;34m` 这种原文(m08259)。跨帧的半截序列会被扣住等下一块(上限 512 字节)。
- **「扩大」是填满面板所在的那一栏,不是浏览器全屏**(m09014 / m09258)。
  实现是一个 class(`.ddm-term-panel-full`,四个边归零 + `max-height:none` + 显式
  `position:absolute`),而面板本身是 `absolute`、尺寸用百分比,参照物是**本插件自己的
  `.ddm-view`**(`position:relative`)——所以挂在侧边栏就填满那一栏,挂在主区就填满主区,
  **永远不依赖别的插件给的祖先结构**。**刻意不用元素级 Fullscreen API**:它会把面板提到
  浏览器 top layer 覆盖整个窗口,把面板旁边要读的对话盖住,而那正是这个功能存在的理由。
  (第一版曾以为「侧边栏携带 `contain: layout style` 会把面板困在栏内」——那是**没量过**的
  假设,实测宿主是 `[data-dsh-panel-host]{position:fixed;inset:0}` 全视口层,
  `inset:0` 因此解析到视口,面板会吞掉窗口且退不出来。见坑 9。)
- **按钮上写的是「扩大 / 还原」,不是「全屏」**(m09258)。行为从来不是全屏,
  而一个说「全屏」的按钮正是前两次缺陷难以描述的原因。状态字段名仍是 `fullscreen`
  (存储形状兼容),但它是只写字段——真正渲染的是 `publish` 进 store 的那一份。
- **新开的终端总是从右下角开始**。`openTerminal` 的 `publish` 里 `fullscreen: false`
  是唯一被渲染的那份种子;上一次会话的展开状态**不会**被带进下一个会话。
  (这个结论由一个**存活两次**的变异体换来:对一棵「终端已关闭所以不存在」的树做断言,
  无论代码怎么写都成立——见坑 9 的补记。)
- **终端能不能用,只有联机才算数**:162 条 `terminal-smoke` 断言全跑在 `openShell` 替身上,
  它们证明不了「浏览器拿到的 socket 路径就是 Host 真正服务的那个」。
  `tools\live-terminal-e2e.mjs` 补上这一段(连真实升级路由、开真 shell、跑一条只读命令等回显)。
  另一个后果是:**替身返回的形状会悄悄变成我以为的形状**——它返回 `session` 对象,
  我第一版却断言它是字符串,于是把一次真实的绿写成了红。
- **终端服务端不做命令检查**(照抄 device-manager 的取向):补偿手段是默认关闭 + 回环/允许列表 +
  上限与空闲超时 + 审计,而不是解析输入。这是「默认关闭」必须是真的默认关闭的原因。
- **升级握手现在会留痕,但只留判定所需的头部**:`terminal.upgrade` 记
  `from`/`host`/`origin`/`site`/`url`,拒绝时追加 `terminal.upgradeDenied`。
  它**不记 cookie、不记凭据、不记请求体**——代价是审计回答不了「那次连接带了什么 Cookie」,
  收益是它永远不会变成一份凭据副本。
- **服务端的拒绝无法用来复现浏览器的问题**:所有回环形态的升级请求实测都返回 `101`,
  因此「浏览器连不上」这件事**不可能只在服务端一侧查清**。这轮补上留痕之后,
  下一次失败至少能在日志里看到它的 `Origin` 和 `Host`,而不是一片空白。
  事实证明这个判断是对的:根因最后落在客户端(见坑 8)。
- **受信任的页面来源现在包含 `dsh-app://app`**(桌面壳自己的页面)。它只在 socket 地址与
  `Host` 头**都已证明回环**之后才被求值,所以放宽的是「可接受的页面来源」,
  不是「可接受的调用方」。`dsh-app://shell`(壳的**另一个**文档)被故意排除:
  它不是这个插件的 UI,没有理由能开设备终端。
- **插件的 Host 地址取自 `__DSH_TRANSPORT__.streamBaseUrl`,不再取自 `window.location`**。
  代价是它依赖一个 DSH 内部约定(壳在桌面 boot 握手后把真 Host 源写进这个全局);
  收益是 GUI 里的终端终于连得上。回退链是 `streamBaseUrl` → `document.baseURI`,
  后者正是纯浏览器标签页里两者一致的情形。
- **子进程管道在受限沙箱下会 `EPERM`**:所以变异驱动器由 PowerShell **每个变异起一个独立子进程**,
  绝不在 Node 里 `spawnSync` 抓输出。
- **负载只以数值形式出现,并且必须带上核数**(m09671)。负载是 `/proc/loadavg` 的
  1/5/15 分钟平均进程数,**不是百分比**——`1.0` 在单核是满载、在四核只是四分之一。
  所以「1.0 / 0.9 / 0.8」这句话本身不说明机器忙不忙,核数是读数的一部分。
  核数由 `load` 采集命令**顺带**数一次 `grep -c '^cpu[0-9]' /proc/stat` 得到,
  落在 `sample.load.cores`,**不再向 `cpu` 指标借**;采了 `cpu` 时若那行没回来,
  才退回 `/proc/stat`。
- **核数未知时不印核数,绝不当成 1**(m09671)。旧代码是
  `Math.max(1, numOrNull(sample?.cpu?.cores) ?? 1)`,而 `cpu.cores` 只有采了 `cpu`
  指标才存在——于是只采 `uptime/load/mem` 的设备(示例 `nas-b`,4 核)
  把 `0.54` 画成了 `54%` 的条长,**四倍长**,并被 70/90 阈值误判。
  `coresOf()` 原本的 `Math.max(1, n)` 是同一处伪造的第二份。
  **一个从未被测量的分母,是一个伪造的读数。** 见坑 13。
- **长度条整体删掉了**(m09671):负载原先靠 gauge 显示(因为它没有波形列),
  现在它和其他无波形列的指标一样进数值条,`load` 单元格印
  `0.54 / 0.53 / 0.54 · 4 核`。去重规则仍是「有波形列的指标不出现在数值条」,
  只是 `load` 从「例外」变成了「必须出现」的那一项——M7 因此改写成
  「删掉 load 的单元格」,与 M25 的配对关系保留。