# Linux POS 装机操作指南(给装机人员)

> 全程只需要一个入口命令:
>
> **门店版(稳定版)**:
> ```bash
> cd ~/Downloads && wget -O openpos_installer.sh https://downloads.openpos.site/linux/openpos_installer.sh && bash openpos_installer.sh
> ```
>
> **前沿版(内部测试机 / 试点店)—— 注意脚本名不一样**:
> ```bash
> cd ~/Downloads && wget -O openpos_installer_edge.sh https://downloads.staging.openpos.site/linux/openpos_installer_edge.sh && bash openpos_installer_edge.sh
> ```
>
> 之后按菜单编号操作。菜单顶部会常显**当前下载源**,非门店版会带 ⚠️。
>
> 主菜单按「一键装机 / 更新 / 补装单项 / 诊断与修复 / 专用设备」分组显示,编号不变。
>
> 两个脚本**同版本内容相同、名字不同**,前沿版可先发布新功能；脚本按自己的文件名决定默认通道(`*_edge*` → 前沿版)。
> 菜单顶部会常显当前下载源和它是谁定的(文件名 / 菜单18 / 环境变量 / 默认)。
>
> 通道优先级:**环境变量 > 菜单 18 记的配置(`~/.openpos-installer.conf`)> 脚本文件名 > stable**。
> 所以同一台机器上想临时换通道,进菜单选 **18) 切换下载通道** 即可,不用重下脚本。
>
> ⚠️ **历史坑(2026-07-30 已修)**:此前两站脚本连名字都一样,「从 staging 取脚本」不影响
> 「去哪取包」,用户按前沿版链接取回来跑,菜单 2/3 静默从**生产站**装了旧壳。现在前沿站上
> `openpos_installer.sh` 这个名字被一个只会指路的提示脚本占住,踩不进去。

## 标准门店装机默认规则

标准装机和点名补装默认执行：**关闭 MS 前台自启动、开启 norua 自启动，保留 MS 软件、数据和手动打开入口；整机重启另行授权。**
只调整已授权目标机，不自动批量转换旧版 restmesh 门店。MS 后台服务、设备配置和正在运行的程序保持原状；只有自启动调整时不执行 POS 启动脚本、不注销、不重启验收。

完整 skill 随仓库共享：[store-pos-install](https://github.com/401040579/openPOSv2/blob/main/.agents/skills/store-pos-install/SKILL.md)；
有效 XDG 覆盖、systemd 入口去重、备份及恢复步骤见 [前台自启动调整](https://github.com/401040579/openPOSv2/blob/main/.agents/skills/store-pos-install/references/autostart.md)。
Codex 可从仓库的 `.agents/skills` 发现该 skill；Claude Code 及其他 AI 按根 `CLAUDE.md` / `AGENTS.md` 的装机路由读取。
需要个人 slash skill 时将整个目录（含 `references/`）安装到对应客户端的 skill 目录；旧个人副本也须同步，已有会话需重新加载，GitHub 合并不等于其他电脑已更新。

`--auto` 默认配置 MS 前台自启关闭、norua 自启开启，不再询问或执行完整停用旧栈，不再自动清理旧重启 cron。菜单 **17**也使用同一条前台配置路径，保留 MS 桌面手动入口、触屏 `set_touch` 和后台服务。
修改前保存原文件、权限和属主，终端打印 `sudo bash …/restore.sh` 恢复命令；回退会把新增项移入备份归档，不删除业务文件。
已有有效 norua 启动链优先复用；发现重复 CEF 入口会只禁用重复项。只配置下次桌面登录，不启动/停止当前程序，也不为了验收而注销或重启。

“装完重启”默认 **n**；只有本次明确同意中断营业才选 **y**。旧应答文件/`--resume` 不沿用此前的完整停用、cron 清理或重启选择；续跑默认不重启，并重新核对前台自启动。
菜单 4 / `--disable-menusifu --yes` 仍是另行明确要求完整停用后台旧栈时的工具，菜单 19 仍是显式清理旧重启 cron 的操作；两者均不在自动装机默认流程中。

## 触屏双指缩放

`run.sh` 默认给 CEF 加 `--disable-pinch`，主屏 POS 和副屏 CDS 都禁止双指放大整个页面，
正常单指滚动、签字及页面按屏幕尺寸适配保持原有行为。网页的 `user-scalable=no`
不能替代桌面 CEF 的启动参数。

新装机由菜单 11 下载启动脚本、菜单 12 复制到副实例。已有双屏机器更新此限制时，
先确认所选下载站已发布新版 `run.sh`，再运行菜单 11、12；菜单 3 仅更新 CEF 二进制，
会保留旧启动脚本。安排空闲时段重新打开 POS 后生效，运行中的进程不会自动改变。

## ★ 一键装机(`--auto`,2026-09-11 起,Issue #6391)

新机器不用记菜单编号:取脚本后加 `--auto`,先回答问卷、查看安装摘要，再开始配置。
Linux CEF 默认使用 basic；只有选择或保留 keyring 备用模式时，迁移才可能另需一次
隐藏输入的旧 keyring 密码。Linux 账户密码不变。
只有明确取得本次整机重启授权后才选择重启；安装器问卷的默认 `y` 不能代替授权。低配套装需先在所选下载站发布，目前先用于前沿版试点。

```bash
# 前沿版(内部 / 试点店)
cd ~/Downloads && wget -O openpos_installer_edge.sh https://downloads.staging.openpos.site/linux/openpos_installer_edge.sh && bash openpos_installer_edge.sh --auto
# 门店版
cd ~/Downloads && wget -O openpos_installer.sh https://downloads.openpos.site/linux/openpos_installer.sh && bash openpos_installer.sh --auto
```

也可以进菜单后输入 `auto`。`--auto --channel stable|edge` 显式换通道(与菜单 18 同一落点)。
⚠️ **不要写成 `wget -O- … | bash`**:脚本靠自己的文件名判通道,管道模式会静默落到门店版。

**问卷**(店名、VM 内存档位之外为 y/n，直接回车取默认):

| 问题 | 默认 | 说明 |
|---|---|---|
| 店名(英文/拼音) | 必填 | Tailscale 机器名与装机记录靠它 |
| 加入远程支持(Tailscale + SSH)? | y | 失败只警告且不记为完成；装完跑菜单 0 补。已入网机器保留原账户与网络，管理员核对是否正确 |
| POS 页面走测试站? | n | **门店一律 n**。通道只决定壳包 / 刷卡服务 / 启动脚本从哪个站取,不决定页面地址 |
| 使用低配套装? | 前沿版约 4GB RAM 默认 y；稳定版默认 n | 检测实际 glibc/KVM/磁盘等；不满足就停下，不升级系统或 libc |
| VM 内存 MiB | 4GB 机器 1536，8GB 起 2048 | 仅接受 1536 / 2048；已有标准 VM 的改动下次启动生效 |
| 副屏显示 CDS? | y | 检测到第二块屏时部署独立 CDS 实例；n 使用 POS 副实例 |
| 补足总 swap 到 4GB? | y，仅低配问 | 保留活动 swap，不执行 swapoff，不改 swappiness |
| MS 前台自启动 | 固定关闭 | 不停用后台旧栈；软件、数据、手动桌面入口和现有 cron 保留 |
| 锁定内核? | y | 与菜单 23 同一实现;已锁过 / 体检未过只警告不拦 |
| 装完重启整机? | n | 只有明确取得本次重启授权才选 y；续跑不会继承此前的 y |

**自动判**:双屏(`xrandr` 数到 ≥2 台就装第二实例,client-id PC1)；`lsusb` 检查
Moby5500。低配套装固定预装匹配 VM，即使读卡器尚未插入；标准方案未检测到设备时另问
是否预装 VM。镜像大小以实际制品为准。主屏 client-id 固定 PC0。
打印队列 helper 的环境跟**最终生效的** `config.jsonc` url 走(通常就是「POS 页面走测试站?」的结果;
config 早被人改过时以 config 为准):staging router → `staging`,生产 router → `production`,其他地址
不猜环境、只警告跳过(按需跑菜单 27 选环境)。装成后重启壳,在
POS 设置 → 打印 → 本机打印机(Linux) 扫描 / 一键新建 CUPS 队列;没装成只警告,装完跑菜单 27 补
(补装前也可手工建队列后在 `config.jsonc` 填 `printerN-name`)。

**固定会做**:菜单 0(选了才做)→ 标准依赖/CEF 或低配体检/校验/安装 → 存储模式预检 → 写 config.jsonc → 11 →(双屏)12 → 17(MS 前台关闭 / norua 开启) → 27(打印队列 helper,失败只警告)→ 25(/dev/shm watchdog)→(选了)23 → 标准方案的 VM/USB 配置或低配 RUA 就绪检查 →(选了)重启。
**不再中途重启**:装 libvirt 后 `virsh` 走 sudo,不依赖重新登录。

**失败了怎么办**:必要步骤失败就停下并打印「从这一步重跑」与步骤清单(✅ 已完成 / ❌ 失败步 /
⏸ 未执行 / ⚠️ 只警告;装完成功时同样打印)；远程支持、打印 helper 等可选步骤失败
会警告，且不会记为已完成。答案与已完成的步骤记在
`~/.openpos-installer-auto.env`(0600);重跑 `bash openpos_installer_edge.sh --auto --resume`
(或再跑 `--auto`,它会问「接着上次?」)跳过已完成的步骤。各步本就幂等,重跑安全。
旧记录里的远程支持“已完成”还会重新核对在线状态与服务；新版存储预检与主、副屏
启动脚本步骤也不能被旧记录跳过。装完文件自动删除，剩余警告按菜单提示补装。

**默认 basic 与 keyring 备用模式**：标准安装、低配安装、菜单 11 先检查存储模式。
默认 basic 不访问系统密钥环，不需要解锁 Default Keyring。basic 的 Cookie 使用公开固定
密钥，不具备每机独立密钥的保密性；完整取舍见 [LOWMEM.md](LOWMEM.md#默认-basic-与-keyring-备用模式)。
升级时若标准 profile 中存在旧 v11 Cookie，且没有明确选择 basic，安装器会保留
keyring 模式，避免切换后旧 Cookie 无法读取。异常数据库会停止安装，需先核查。
不要只替换 run.sh 绕过预检，也不要删除 profile、Cookie 或 keyring 来绕过。

需要显式启用 keyring 备用时，用菜单 **26** 或
`bash openpos_installer_edge.sh --prepare-keyring --migrate`（门店版换成 `openpos_installer.sh`）。
准备成功后写入桌面用户 `~/.config/openpos/cef-password-store` 的 `keyring` 策略，并启用
开机自动解锁。新机生成每机独立、仅 root 可读的随机解锁密钥，不保存 Linux 密码。
已有 keyring 先备份，再验证旧密码并保留凭据迁移；密码可能不同于 Linux 密码。
验证或解锁失败时保留原数据并停止，不会自动降级为 basic。

**打印队列 helper(POS 页面「新建队列」用,ADR 0564 §2.13)**:菜单 **27** 或
`bash openpos_installer.sh --install-printer-helper production`(试点 / 测试机按实际服务端换成
`staging` / `beta`;**环境必须显式给,不跟下载通道走**)。存量机单独补装,不必重装壳。装完会打印
本机 `machine` 值;镜像克隆的机器要各自重新生成 machine-id,**D-Bus 那份要一起删**(只删
`/etc/machine-id` 时 `systemd-machine-id-setup` 会把克隆带来的 `/var/lib/dbus/machine-id` 抄回来,身份不变):
`sudo rm -f /etc/machine-id /var/lib/dbus/machine-id && sudo systemd-machine-id-setup && sudo ln -sf /etc/machine-id /var/lib/dbus/machine-id`,
重启后用 `/usr/local/libexec/openpos/openpos-printer-helper version` 确认 `machine` 已变,再重跑。若报「directory … is not searchable by other users」,说明该目录(常见是他人预建的 0700 `/usr/local/libexec` 或其子目录)桌面用户进不去:核对目录里没有不该公开的内容后 `sudo chmod 0711 <目录>` 再重跑;安装器不会替你改已存在目录的权限。撤销:`sudo rm /etc/sudoers.d/openpos-printer-helper`。细节见
`shells/tools/printer-helper/README.md`。

下载站更新不会自动更新旧门店本机脚本。更新前先备份并关闭主、副屏 CEF，再通过新版
安装器菜单 11（双屏另跑菜单 12）安装；保留 keyring 的旧店可能仍需完成备用模式准备。

## 独立工具：阻止已知问题内核 `6.8.0-136-generic`

这不是 MenuSifu 清理的一部分，也不会由菜单 4 自动执行。先只读检查：

```bash
cd ~/Downloads
wget -O kernel_guard.sh https://downloads.staging.openpos.site/linux/kernel_guard.sh
bash kernel_guard.sh --check
```

确认输出的目标内核与 GRUB 判断正确后，维护窗口再执行：

```bash
sudo bash kernel_guard.sh --apply
```

如果 `--check` 显示 `Legacy broad freeze detected: yes`，说明机器已经执行过旧版“关闭全部
自动更新 + hold HWE/MySQL”的脚本。普通 `--apply` 会拒绝继续，必须先核对输出，再显式迁移：

```bash
sudo bash kernel_guard.sh --apply --migrate-legacy-freeze
```

迁移会删除旧 apt drop-in、解除旧脚本加的 HWE meta/MySQL broad holds，并恢复两个 apt timer；
若仍有独立理由冻结 MySQL，可同时加 `--hold-mysql`。`--rollback` 会把迁移前的旧 drop-in、holds
和 timer 状态原样恢复，不会把旧冻结悄悄丢掉。

- 136 尚未安装：只写精确 apt pin，**不改 GRUB、不要求重启**；137+ 仍可更新。
- 136 已安装且当前不高于 136：选择最高的可用低版本并固定下次启动；只有当前正跑 136
  才提示需要人工重启，脚本自身绝不 reboot。固定期间只 hold 目标内核的具体 image/modules/
  headers，防 autoremove 删掉 saved entry 指向的 boot assets；不 hold HWE meta。
- 不关闭 `apt-daily`/unattended security updates；默认不 hold MySQL。确有独立理由才加
  `--hold-mysql`，它只记录并回滚本脚本新增的 hold。
- 回滚：`sudo bash kernel_guard.sh --rollback`。状态保存在
  `/var/lib/openpos/kernel-guard/active/`，回滚后移入 `history/` 留痕。

营业机器只跑 `--check`；`--apply` 和人工重启必须另约维护窗口。

## 场景 A:新装(单屏)

1. 菜单 **1**(装依赖)
2. 菜单 **2**(装 CEF 壳全量包,约 390MB)—— 装完会自动放一份 `config.jsonc` 模板
3. **编辑配置**:`~/Downloads/cef_ubuntu/Release/config.jsonc`
   - `url`:模板默认 router 统一登录(生产 `https://router.openpos.site`);要开机直落某个
     app 才改成该 app 子域(如 `https://pos.openpos.site`)
   - `client-id`:本机编号(主机一般 PC0)
   - `printerN-name`:打印机(CUPS 名,`lpstat -p` 查)
   - 要刷卡的机器:打开 `"usb-swiper": "MOBY5500_net"`
   - ⚠️ 注释只能整行写,行尾注释会让壳子解析失败;最后一个键别留尾逗号
4. 菜单 **11**(装启动脚本 + 桌面快捷方式)
5. 菜单 **17**(开机自启)—— 门店机重启后能自己把 POS 打开,不用店员操作
6. 按[前台自启动调整](https://github.com/401040579/openPOSv2/blob/main/.agents/skills/store-pos-install/references/autostart.md)关闭 MS 前台自启，保留软件、数据、后台服务和触屏配置；核对 norua 只有一条有效启动链。
   菜单 **4**不是默认步骤；只有另行明确要求完整停用旧栈才使用。只读检查可运行
   `bash openpos_installer.sh --audit-menusifu`，但报告中的后台残留不代表必须清理。
7. 触摸屏默认使用 **norua 应用键盘**，不需要安装 Onboard。一键装机会配置此模式；
   手动装机或已有机器运行菜单 **14**，选择默认项 **1**。
   脚本关闭当前桌面用户的 Onboard 自动弹出、自启动，以及 GNOME 屏幕键盘开关；
   不卸载 Onboard，不修改 CEF 启动参数或其他无障碍设置。已打开的 Onboard 关闭一次即可，
   也可注销后重新登录。修改前备份到 `~/.local/state/openpos-installer/app-keyboard-*/`，
   屏幕上会打印 `bash …/restore.sh`，执行即可恢复修改前的配置。
   POS 设置 → 设备 → **应用键盘** 可选跟随设备、自动弹出、手动打开或关闭；
   偏好只保存于本机当前浏览器，iPhone/iPad/Android 始终使用系统键盘。
   Linux 桌面及其他应用需要实体键盘。确实需要系统屏幕键盘时，菜单 14 选 **2**，
   安装原有 OpenPOS Midnight 皮肤的 Onboard，并在 POS 设置里选择关闭应用键盘。
8. 验证:双击桌面「OpenPOS POS」,应先**全屏播 2 秒品牌动画**(浅灰底、无声,菜单 11
   会一并下发视频并补装 `mpv` + `xdotool`;没视频直接起 CEF 也算正常),视频结束后
   后面应该**已经是**打开的 CEF(CEF 在视频后面并行启动),然后**全屏**打开 router
   统一登录页(壳子默认全屏,见 `main_context_impl.cc` 的 `g_bFullWindow`;要窗口模式在
   config.jsonc 写 `"full-window": false`)。**不是 CEF 示例页** —— 看到示例页说明
   `config.jsonc` 没生效,回第 3 步。读卡器 VM 重启不再弹终端窗口,要看进度
   `tail -f /tmp/openpos-restart-rua.log`

## 场景 B:新装(双屏)

1. 先按场景 A 装好主实例并验证
2. 菜单 **12**(装第二实例):
   - client-id 直接回车用默认 PC1(或按店里规划输入)
   - 自动完成:复制第二实例、主实例钉屏 0、副实例钉屏 1
3. 副屏要显示不同页面(如自助点单 / 客显):编辑 `~/Downloads/cef_ubuntu2/Release/config.jsonc` 的 `url`
4. 验证:双击桌面「OpenPOS POS」(**只有这一个图标**)→ 主屏出 POS、副屏出第二实例。
   只接了一块屏时,同一个图标只启动主实例。

## 场景 C:老机器升级

1. 重新下载 installer(见顶部入口命令,直接覆盖旧的)
2. 菜单 **3**(更新 CEF 壳)——**会保留 `config.jsonc` 和启动脚本**,机器上已有第二实例会一起更新
3. 菜单 **11**(更新启动脚本 / 快捷方式)
4. 复核 MS 前台自启已关闭、norua 自启已开启且没有重复入口；保留软件、数据及后台服务，不默认跑菜单 **4**
5. **刷卡的机器:菜单 3 之后必须跑菜单 13**(读卡器保持插着)。新壳配旧的 VM 刷卡服务
   = 读卡器读到卡、页面却每笔都拒(2026-09-03 Seven Tea 真实事故)。菜单 3 跑完会自动检查,
   看到红色「刷不了卡」提示直接回车就是跑 13;想单独查用菜单 **24**
6. 验证:双击桌面图标,POS 正常起来;刷卡机器再真刷一笔


### ⚠️ 只有「手工整个换目录」才会丢登录态 —— 走菜单 3 是安全的(2026-08-04 核实)

壳子的浏览器 profile 在 **`Release/cef/`**(登录 token、选中门店、设备 License 都在里面)。

**菜单 3 不会碰它** —— 它是 `tar -xzf … -C Release`(解压覆盖,**不删旧目录**),
而 CI 的包里**根本没有 `cef/`**(实测条目数 0),所以 profile、`config.jsonc`、
启动脚本都原样留着。**日常升级请走菜单 3。**

⚠️ 但如果你**手工把整个 `Release/` 换掉**(rename 旧目录、解开新包),那就会丢:
新包解开是一个不含 `cef/` 的目录,那台机器要重新登录、**重新激活设备(烧一个 License 槽)**。
症状很像「新包坏了」(壳能起、页面能开,就是要求重新登录),容易被误判成升级失败而回滚。

真要手工换,先备份再搬回来:

```bash
cd ~/cef_ubuntu                      # 或壳子所在目录
cp -a Release/cef  /tmp/cef-backup
cp -a Release/config.jsonc /tmp/     # 新包不含它,但手工换目录一样会丢
#   …换新包…
cp -a /tmp/cef-backup Release/cef && cp -a /tmp/config.jsonc Release/
chown -R "$USER:$USER" Release
```

## 场景 D:无头打印主机(不接显示器的专职打印服务器,2026-08-13)

> 治「iPad/安卓平板当打印主机,熄屏就断打印」:一台不接显示器的 Linux 盒子常驻打印。
> 与门店收银机互不相干(独立目录 ~/openpos-printhost,不碰 cef_ubuntu),同机可共存但
> 产品上建议专机专用。

0. 菜单 21/22 进门先确认设备角色:须输入 `PRINTHOST` 才继续,其它输入一律取消、不动机器。
   **收银机上要新建打印队列走菜单 27**,不是这里。
1. 菜单 **21**(一键装:依赖 + 壳 + 配置 + systemd 常驻;url 按当前下载通道自动指
   staging / 生产的 printhost 站点)
2. 建 CUPS 队列并回填 `~/openpos-printhost/Release/config.jsonc` 的 `printerN-name`
   (队列建法同「标签打印机」节的 lpadmin;改完 `sudo systemctl restart openpos-printhost`)
3. 一次性上岗:菜单 **22**(SSH 免 VNC,全在终端问答)——
   推荐**配对码**:老板在掌中宝「设备配对」页生成 6 位码给装机人,admin 密码不经过盒子,
   码即绑店;脚本自动激活 License(普通员工的码/账号会 403,要店级 admin 的)+
   引导换专职低权限账号常驻。备选:x11vnc 看虚拟屏人肉点(见下载站 `PRINT_HOST_INSTALL.md` 方式 B)
4. **切主机顺序不能反**:先停旧主机(iPad 退出 pos)→ 后台 → 设备 切到新设备 →
   状态面板横幅 60s 内变绿 ✅ → 逐槽「测试打印」验真出纸
5. 排障:`journalctl -u openpos-printhost -f`;完整手册 = 下载站 `PRINT_HOST_INSTALL.md`

## ⚠️ 换壳会丢登录态和设备激活(2026-08-04 实测)

壳子的浏览器 profile 在 **`Release/cef/`**(登录 token、选中的门店、设备 License 都在里面)。
新包解开是一个**空的** `cef/`,直接覆盖 = 那台机器要重新登录、重新激活设备(**会烧一个 License 槽**)。

升级前先备份、升级后搬回来:

```bash
cd ~/cef_ubuntu            # 或壳子所在目录
cp -a Release/cef /tmp/cef-profile-backup
#   …换新包…
rm -rf Release/cef && cp -a /tmp/cef-profile-backup Release/cef
chown -R "$USER:$USER" Release/cef
```

`config.jsonc` 同理(新包里那份是模板,不含本店的 `client-id` / 打印机名 / `usb-swiper`),
一并备份还原。

## 标签打印机(Zebra ZD411)额外步骤

标签机和小票机一样都走 CUPS,只是**必须建成 raw 队列**(打印机自己认 ZPL 指令,不能让
CUPS 再套一层驱动去转换)。打印机先在路由器上拿到固定 IP,然后:

```bash
# 队列名随便起,但要和 config.jsonc 里填的一致;IP 换成打印机实际地址
sudo lpadmin -p zd411 -E -v socket://192.168.1.50:9100 -m raw
lpstat -p zd411          # 应显示 idle / enabled
```

然后 `config.jsonc` 里把这个队列挂到对应槽位,例如标签槽是 3 号:

```
  "printer3-name": "zd411",
```

pos 侧还要把该槽位的**类型选成 Zebra ZPL**(设置 → 打印机),否则发过去的是小票用的
ESC/POS 位图,标签机会打出乱码。

**装纸后必须做一次介质校准(SmartCal)**:同时按住 **PAUSE + CANCEL 约 2 秒**,打印机会走几张
纸学会标签间距。不校准的现象是出空白标签或内容偏移半张 —— 这是打印机安装步骤,不是软件问题。

> ⚠️ 手上没有 ZD411 实机验证过按键组合(2026-07-25):这里写的是 Zebra ZD 系列文档的 SmartCal
> 组合。**issue #1973 验收清单里写的 `FEED+CANCEL` 是错的**,别照那条做。装机时如果 PAUSE+CANCEL
> 没反应,以随机附的 Quick Start 卡片为准,并回来改这一行。

验证:pos 设置页对该槽点「测试打印」,应出一张带店名和时间的标签。没反应就看壳子
终端日志里有没有 `printraw:` 开头的错误行(队列名写错会打印 `CUPS queue not found`)。

## 刷卡机器(Moby5500)额外步骤

Linux 上的读卡服务跑在一个 Windows 虚拟机里(`rua_server`,壳子通过 `127.0.0.1:15678` 连它)。

1. 菜单 **5**(装 libvirt)→ 菜单 **6**(重启)→ 重新打开 installer
2. 菜单 **9**(下载约 10GB 镜像 + 定义 VM + 修自启)——**读卡器要插在机器上**
3. 菜单 **10**(USB 拔插自动重连)。装两样东西,都幂等、重跑覆盖:
   - **udev 规则**:插回读卡器时先 detach 域里的幽灵 hostdev 条目再 attach
     (旧版只 attach,拔插一次后永远失败,2026-08-08 实锤);
   - **libvirt qemu hook**:`rua_server` started 后 ~10s 补挂读卡器 —— udev 只在
     ADD 触发,覆盖不了「VM 重启后 live 挂载丢失」的盲区(2026-08-10 实锤)。已有
     hook 文件(比如来电识别那节装的)会**追加合并**不覆盖;VM XML 里带开机 hostdev
     的机器(设备已挂好)脚本会自动跳过,不会白白弹跳设备。
   - 老机器升级:**重跑一次菜单 10 即可**拿到两个修复,不用重装。
4. 验证:`sudo tail -f /var/log/libvirt/rua_app.log`,应先后出现 `started on port` 和
   `InterfaceDeviceSerialNumber`;拔插重连和 VM 重启补挂看
   `/var/log/openpos-reattach-reader.log`,应有 `attach ok`(或门店机形态的
   `already attached and healthy`)

## 来电识别(Caller ID)额外步骤

店里插一个 USB 电话调制解调器(HiRO V.92 一类),来电时收银屏弹出号码 + 认出老客 + 一键开外带单。
**modem 只监听,不摘机不拨号** —— 座机照常响、照常接。

接线:**电话线 → modem 的 `LINE` 口,座机接 modem 的 `PHONE` 口**(盒子上那两个 RJ-11 就是干这个的,
不用另买分线器)。

### 前提:先做完「刷卡机器」那节

来电识别的服务和读卡服务是**同一个** `rua_server`(跑在那台 Windows VM 里)。所以菜单 5/6/9/10
必须先做完 —— 没有那台 VM 就没有来电识别。

### 步骤

1. **先验硬件**(不需要 VM,两分钟):

   ```bash
   cd ~/Downloads && wget -O cid-probe.sh https://downloads.openpos.site/linux/cid-probe.sh
   sudo bash cid-probe.sh
   ```

   敲完 `ATZ` / `AT+VCID=1` 都回 `OK` 后打个电话进来,看到 `NMBR = ...` 就是通了。
   两条 AT 都回 `ERROR` = 这块 modem 不支持来电识别,换一块。

2. **把 modem 透传进 VM**:

   ```bash
   lsusb | grep -i modem            # 记下 VID:PID,例如 0572:1349
   wget -O setup-cid-modem.sh https://downloads.openpos.site/linux/setup-cid-modem.sh
   sudo bash setup-cid-modem.sh 0572:1349
   ```

   脚本会装 udev 规则 + libvirt hook + **重启 libvirtd**(不重启的话 hook 等于没装,而且不报错)。

3. **验证**:

   ```bash
   sudo tail -f /var/log/libvirt/rua_app.log
   ```

   应出现 `CID: modem found via WMI: "..." on COM3` 和 `CID armed on COM3 via AT+VCID=1`。
   然后打个电话,应出现 `CID: incoming call: number="..."`。

4. 重启 VM 后 modem 会**自动挂回去**(hook 那层),不用手动做什么。

### ⚠️ 装完之后别碰菜单 13(除非知道自己在做什么)

菜单 13「更新 VM 内刷卡服务」会**优先从我们自己的下载站**取 `rua_server.exe`(含来电识别),
下载站上没有才**落回上游版本 —— 那份不含来电识别**,更新完来电弹窗就消失了
(读卡器照常工作,不报错)。落回上游时脚本会打印警告并要求输入 `yes` 确认,
**看到那段警告就停下来问一句**。

> ✅ 2026-08-06 起下载站上确实有我们的版本了(此前脚本去错了目录,一直落回上游)。
> 跑菜单 13 会看到 `源:…/win/rua_server.exe(本仓构建,含来电识别)` —— 看到这行就对了。
> 看到「落回上游」那段警告才需要停下来问。

## /dev/shm watchdog(CEF 白屏自愈,2026-08-14 起每台必装)

CEF135 在 Intel iGPU(J6412/N95/N100 均已复现)+ Ubuntu 22.04 上有光栅 tile 泄漏:renderer 以
~2MB/min 累积「已删除仍 mmap」的 /dev/shm 段,约 30 小时吃满 3.8G 后 GPU 分不出命令缓冲,
**白屏但进程不死**(与壳版本/页面/参数无关,详见 `docs/known-issues.md`「/dev/shm 100%
而目录零文件」条)。在有根治(升 Mesa / 升 CEF)之前,每台机装这个 watchdog 兜底:

```bash
cp shm_watchdog.sh /home/menu/shm_watchdog.sh && chmod +x /home/menu/shm_watchdog.sh
(crontab -l 2>/dev/null | grep -v shm_watchdog; echo "*/5 * * * * /home/menu/shm_watchdog.sh") | crontab -
```

行为:每 5 分钟查一次,`/dev/shm ≥95%` 时按实例目录逐个走 `run.sh` 重启 CEF(单/双屏
自适应),30 分钟冷却防抖;平时只采样。`~/shm_usage.log` 会同时记录 renderer RSS、
deleted-shm/全部 2MB map 数、GPU 实际加载的 iris/libGL/libgbm 路径与 Mesa 旁路环境；触发时
`~/shm_watchdog_evidence/` 另存压缩的完整 maps、smaps_rollup 和段大小直方图，便于区分
tile 资源池泄漏、普通 RSS 增长和驱动 A/B 没有真正生效。恢复日志在
`~/shm_watchdog.log`。office-mx 与 Seven Tea 已于 2026-08-14 手动装过,新装机器跑上面
两行即可；旧机更新脚本不需要改 cron。

## 配置门店路由器(菜单 28,ADR 0542)

把门店路由器(GL-SFT1200)的网络状态每分钟上报到 suadmin「门店网络」。门店机接在路由器 LAN 上,
菜单 28 经 SSH 把采集器推进路由器、在路由器上核销绑定码、装 cron 和开机重建,装完当场验证。

> ⚠️ **目前只在前沿版提供**:staging 下载站的 `openpos_installer_edge.sh` 才有菜单 28,门店版
> 脚本不显示它。这是「功能在哪个站开放」;上报到哪个服务端仍在菜单里**显式选**(门店一律 production)。

1. 先在 suadmin「门店网络」给这家店**发一枚绑定码**:30 分钟有效、只能用一次。换路由器要发
   replace(替换)码,首绑码会被拒(「这家店已经有一台在用的路由器」)。
2. 门店机跑菜单 **28**:选环境 → 路由器地址(默认是本机网关,回车即可)→ 用户(默认 root)→
   粘贴绑定码(不回显)。全程最多问一次路由器 root 密码;第一次连接会自动记下路由器主机指纹。
   没装 ssh 客户端时先 `sudo apt-get install -y openssh-client`。
3. 菜单会同步跑一轮上报(先错峰等 0~15 秒),再等最多约 2 分钟,确认 cron 触发的那一轮退出码是 0(记在路由器 `/tmp/openpos-netreport.rc`)。
4. 最终判据:suadmin「门店网络」里这家店的「最近上报时间」**每分钟刷新**。

已经装过、只想修复(重装采集器 / cron / 开机段)时,绑定码**留空**回车即可,原凭据保留。
凭据(token)只存在路由器 `/etc/openpos-netreport.conf`(600),门店机上不落盘。
手工装法与每条坑的原理见 `deploy/router/openpos-netreport` 文件头与 `deploy/router/OPAL-BASELINE.md` §5f。

## 常见问题

- **菜单 2/3/21 或 `--auto` 一开头就报「下载站上没有这个文件」**:当前通道的下载站还没发布壳包
  (门店站曾缺 `openpos-cef-linux-x64.tar.gz`)。脚本已在动机器之前停下,按提示改用前沿版脚本
  `--auto --channel edge`;报「连不上下载站」则是网络问题,不是缺包。
- **打开是 CEF 示例页不是 POS**:`config.jsonc` 没被读到。确认它和 `cefclient` 同目录、
  文件名没写成 `config.json`、没有行尾注释、没有尾逗号。
- **双屏没分开、都挤在主屏**:确认两份 `config.jsonc` 里分别有 `"preferred_monitor": 0` 和 `1`
  (新建副实例时菜单 12 会自动写，`//` 注释行不生效；已有副实例重跑会保留配置，需备份后手动启用对应键);确认壳子是 2026-07-20 之后的版本(更早的 Linux 壳没实现 preferred_monitor)。
- **点图标只出一个实例**:`xrandr | grep " connected"` 确认系统认到两块屏;确认 `cef_ubuntu2` 目录存在。
- **菜单 13 报 hibernation / NTFS 拒绝写入**:VM 里的 Windows 开着快速启动。进 VM
  (virt-manager)以管理员运行 `powercfg /h off`,彻底关机后重试。
- **菜单 13 / 16 报 `VM did not shut off within 2 minutes`**:VM 里的 Windows 熄屏了不理会关机
  信号。脚本现在会先「按一下键盘」唤醒再关(两次机会);还不行就按提示截屏看 VM 卡在哪,
  **不要 `virsh destroy` 硬断电**(会把盘弄脏,接下来的写入全报 Read-only)。⚠️ 这个报错之后
  **exe 没有换**,不算跑过 13。
- **升壳(菜单 3)之后刷卡机读到卡但页面每笔都失败 / 报 empty callback**:VM 里的刷卡服务
  没跟着升。跑菜单 **24** 确认,再跑菜单 **13**。
- **VM 起不来报 `Did not find USB device 26f1:56b0`**:读卡器没插。插上后
  `virsh -c qemu:///system start rua_server`。
- **刷卡不工作、日志显示 `accepting connections from localhost only`**:VM 自启少了 `--anyip`,
  跑菜单 **16** 修。

## 装不下去怎么办

**先跑菜单 0「远程支持」** —— 它把这台机器加进 Tailscale 网络并打开 SSH,技术支持就能
直接连进来看,不用你在电话里念报错。中途会问**店名**(用英文或拼音,比如 `seven-tea`),
填了后台就能直接认出是哪家店;不填按回车也能继续。跑完屏幕会打印一个 `100.x.x.x` 的
地址和机器名,**把那两行发给技术支持**即可。

> 需要联网;装完之后这台机器对技术支持长期可达。不想留这个入口时,跑
> `sudo tailscale logout`,并从 `~/.ssh/authorized_keys` 里删掉 `restmesh-remote-support`
> 那一行(**两步都要做**)。

菜单 0 也连不上时,再把屏幕上的报错、`~/Downloads` 下的日志、以及
`sudo tail -50 /var/log/libvirt/rua_app.log`(刷卡问题)发给技术支持。

## 已验证的读卡服务版本与旧机升级

标准安装器固定使用 source `659198634711ca8f34ad3c4f7b37b9e37b2665fb` 的原生 Windows x86 RUA，exe SHA-256 为 `9df1aa937f378516ffde2936b13fa74bf5c1948bb07782e32b4cd8aec4f7badf`。按所选通道从 `win/rua/<source>/rua_server.exe` 下载，关 VM 前先核对指纹；下载失败或指纹不同直接停止，不回落到其他构建。

- 新装标准 VM 默认写入 `--anyip --cl3-provisioning COM2`。
- 已有 VM 的菜单 9 / 13 升级沿用实际 Startup bat 指向的 exe 路径，保留 bat 全部字节，包括自定义参数、COM 口和 legacy/CL3 档位。不会因升级而切换初始化档位。
- 已有 bat 必须只有一条有效的 RUA 启动命令，并含 `--anyip`。缺失时停止升级，先核对菜单 16 的启动项修复方案；菜单 16 是显式修复操作，可能重建 bat。
- 旧 exe 备份成功后才写入，写入后回读指纹。失败先恢复旧 exe；恢复也失败时保留 VM 关闭，不继续启动损坏文件。
- DLL 不随菜单 13 更换。其他 SDK 版本须先核对兼容性；低配套装仍使用独立 manifest 与镜像合同，不混用标准 VM 升级入口。

双屏新装使用有效的 `preferred_monitor` 0 / 1 配置。已存在的副屏配置仍保留，历史注释配置需按前面的恢复步骤手动启用。
