# 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 前台开机自启?」，默认 **y**（关闭）；选 **n** 时 MS 自启项保持原样，norua 自启照常开启。两种选择都不执行完整停用旧栈，不自动清理旧重启 cron；旧应答文件没有这一项时按关闭处理。菜单 **17**也使用同一条前台配置路径，保留 MS 桌面手动入口、触屏 `set_touch` 和后台服务。
修改前保存原文件、权限和属主，终端打印 `sudo bash …/restore.sh` 恢复命令；回退会把新增项移入备份归档，不删除业务文件。
已有有效 norua 启动链优先复用；发现重复 CEF 入口会只禁用重复项。只配置下次桌面登录，不启动/停止当前程序，也不为了验收而注销或重启。

“装完重启”默认 **n**；只有本次明确同意中断营业才选 **y**。旧应答文件/`--resume` 不沿用此前的完整停用、cron 清理或重启选择；续跑默认不重启，并重新核对前台自启动。
菜单 4 / `--disable-menusifu --yes` 仍是另行明确要求完整停用后台旧栈时的工具，菜单 19 仍是显式清理旧重启 cron 的操作；两者均不在自动装机默认流程中。
需要临时回到 MenuSifu 时用菜单 30 / `--restore-menusifu --yes [--start] [--disable-openpos-autostart]`：从菜单 4 的备份恢复，cron 合并不覆盖、unit 还原到停用前状态，可选立即启动并可回滚地停用 OpenPOS 自启。

## 触屏双指缩放

`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 账户密码不变。
整机重启默认 `n`，只有明确取得本次整机重启授权后才选择重启。低配套装需先在所选下载站发布，目前先用于前沿版试点。

```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 --channel edge
# 门店版
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 | 1280 为试点档；已有标准 VM 的改动下次启动生效 |
| 副屏显示 CDS? | y | 检测到第二块屏时部署独立 CDS 实例；n 使用 POS 副实例 |
| 补足总 swap 到 4GB? | y，仅低配问 | 保留活动 swap，不执行 swapoff，不改 swappiness |
| MS 前台自启动 | 问卷选择，默认关闭（选 n = 保持原样） | 不停用后台旧栈；软件、数据、手动桌面入口和现有 cron 保留 |
| 锁定内核? | y | 与菜单 23 同一实现;已锁过 / 体检未过只警告不拦 |
| 装完重启整机? | n | 只有明确取得本次重启授权才选 y；续跑不会继承此前的 y |
| 安装 CL3 读卡 VM? | y，标准方案 | 未插 Moby5500 也可预装；不需要读卡 VM 时选 n |

安装摘要会显示 Moby5500 是否已插入。标准方案选 `n` 时跳过 VM 安装；续跑沿用原选择。
低配套装固定包含自己的 VM。双屏自动部署第二实例，主屏 client-id 为 PC0，副屏为 PC1。
打印队列 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(norua 开启；MS 前台按问卷关闭或保持原样) → 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`

## 双屏触摸错位

症状：双屏扩展（如主屏 eDP-1 在左、副屏 DP-1 在右，合成 3840×1080 桌面）时，点哪里都偏，
越往右偏得越多。原因是触摸设备的 `Coordinate Transformation Matrix` 仍是单位矩阵，
每块触摸屏都被拉伸到整个桌面。

1. **先查 MS 自带的触屏开机项还在不在**。MS 机器靠 `~/.config/autostart/Menusifu touchScreen.desktop`
   （`Exec=set_touch` → `/opt/POS/touch onStart`，同时开触摸提示音）开机映射触摸。菜单 17 / `--auto` 会保留它，
   但菜单 4 / `--disable-menusifu` 目前会把它一起挪进 `~/menusifu-disable-backups/<时间>/`（Issue #15236）。
   被挪走时，优先把这一个文件从备份放回原处（别执行整份 `restore.sh`，它会把 MS 前台自启也恢复回来）。
2. **没有 MS 触屏配置的机器**，按设备名映射：先确认触摸屏和显示器的对应关系，
   用物理尺寸比对，不要猜：
   ```bash
   export DISPLAY=:0
   xrandr --query | grep connected          # 显示器名及 mm 尺寸
   xinput list                              # 触摸设备名(常见 eGalax / ILITEK)
   udevadm info /dev/input/eventN | grep -E 'NAME=|WIDTH_MM|HEIGHT_MM'   # 设备节点见 xinput list-props <id>
   xinput map-to-output <id> <显示器名>     # 立即生效；同一触摸屏的多个 id(如 "... UNKNOWN")都要映射
   ```
   现场点四个角确认后，写成开机自启（id 每次开机会变，脚本按名字匹配）：
   `~/.local/bin/touch-map.sh` 内容如下（显示器名按实机替换），
   再建 `~/.config/autostart/touch-map.desktop`（`Exec=/home/menu/.local/bin/touch-map.sh`）：
   ```sh
   #!/bin/sh
   export DISPLAY=${DISPLAY:-:0}
   sleep 5
   for id in $(xinput list --id-only); do
     case "$(xinput list --name-only "$id")" in
       *eGalax*) xinput map-to-output "$id" eDP-1 ;;
       *ILITEK*) xinput map-to-output "$id" DP-1 ;;
     esac
   done
   ```
   `~/.config/autostart` 若是 root 属主（早期用 sudo 建过），先 `sudo chown menu:menu` 再写。
   两种方式只留一个，避免两个开机项一起写映射。

映射在重启或重插触摸 USB 后会丢，重新跑一次脚本即可恢复。

## 场景 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**下载约 10 GB 镜像。新装会配置 CL3、libvirt 开机启动、仅本机转发及 USB 自动重连。允许读卡器未插入。
3. 已有 VM 可通过菜单 **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. 在验收台架先拔掉 Moby，再冷启动。用 `sudo virsh -c qemu:///system dominfo rua_server` 确认 VM 已运行且启用 Autostart；用 `sudo ss -ltnp 'sport = :15678'` 确认只监听 `127.0.0.1:15678`。插入和重新拔插后检查 `/var/log/openpos-reattach-reader.log` 的新挂载记录，以及 `/var/log/libvirt/rua_app.log` 的 CL3 provisioning `Success` 和设备能力读取。

## 来电识别(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。

## 读卡器每晚定时复位(菜单 29,Issue #14094)

Moby5500 固件约每 24 小时自己重启一次,时刻由读卡器上电(或复位)那一刻决定,每天再晚约 22 秒,
很多店落在午市、晚市:重启那一两分钟刷不了卡,旧版 rua_server 还可能卡死更久。2026-10-31 龙门湾
实测:向 rua_server 发 `resetdevice` 等于给读卡器重新上电,自重启时刻随之挪到复位那一刻。所以每晚
固定时刻复位一次,读卡器就只在凌晨重启。

- 有读卡器的机器,一键装机自动装;存量机跑菜单 29(问几点,直接回车 = 03:00),或非交互:
  `bash openpos_installer.sh --install-reader-nightly-reset 03:00`。重跑即改时刻。
- 时刻按本机时区;时区还是 UTC 时菜单 29 拒装(先 `sudo timedatectl set-timezone <门店时区>`)。
  凌晨避开店里自己的定时任务(如 tomcat 重启、MenuSifu 遗留的定时重启)。
- 很老的 rua_server.exe 不写电量心跳,脚本每晚只记一行 SKIP;先跑菜单 13 升级。
- 读卡器不在宿主 USB 上时绝不关 VM(VM 开机挂着读卡器,关了就起不来),留给读卡器重新插上后的自动重连。
- 行为:最近 10 分钟有人刷卡 → 当晚跳过;RUA 卡死或读卡器卡住(`CardReaderBusy` / 会话卡锁)→ 先重启
  读卡器 VM 再复位;复位后读卡器没断开 → 重启 VM 再试一次(每晚最多一次);断开后 4 分钟内没拿回序列号
  和一次成功心跳 → 重启 VM 兜底。VM 关不掉就留给人处理,**绝不 destroy**,并挂一个 30 分钟后的检查(那时关掉了就自动 start);本脚本关掉的 VM 退出前一定拉起。
- 夜里关机的店当晚不复位,白天开机也**不补跑**(补跑就是在营业时段断读卡器);下一晚自动接上。
- 看结果:`sudo tail /var/log/openpos-reader-nightly-reset.log`,每晚一段,`RESULT: OK` 为成功。
  下一次触发时间:`systemctl list-timers openpos-reader-nightly-reset.timer`。
- 停用:`sudo systemctl disable --now openpos-reader-nightly-reset.timer`。
- 最好配合新版 rua_server(菜单 13):新版复位后 20–35 秒恢复,旧版要 1–2 分钟且可能卡死(要靠上面的兜底)。

## 配置门店路由器(菜单 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** 修。

### 交付前三项验收(2026-11-11 双屏 MX 现场经验)

后台配置齐全不代表本机能出票、能刷卡。三项都要在门店机上逐项核实，不能只看界面上的「成功」：

| 项 | 本机判据 | 常见卡点 |
|---|---|---|
| 触摸 | 两块屏四个角都点得准;`xinput list-props <设备> \| grep Coordinate` 不是单位矩阵 | 见「双屏触摸错位」 |
| 打印 | `lpstat -v` 有指向打印机 IP 的队列;主实例 `config.jsonc` 有对应 `printerN-name`;下单后 `/var/log/cups/access_log` 出现 `Send-Document` | 只在 POS 里填了 IP |
| 刷卡 | `dominfo rua_server` 为 running + Autostart enable;`/var/log/libvirt/rua_app.log` 有 `PerformJsonProvisioningString response: {"ResponseCode":"Success"}` 和 `status -> 1 client(s): {"ev":"connected"…}` | VM 没装完;主实例缺 `usb-swiper` |

- **POS 里填了打印机 IP、「测试打印」也成功，就是不出纸**:Linux CEF 壳只按 `printerN-name`
  往本机 CUPS 队列发，不用云端的 IP。补 raw 队列
  `sudo lpadmin -p <队列名> -E -v socket://<打印机IP>:9100 -m raw` 并在主实例 `config.jsonc` 写
  `printerN-name`(槽位号与 POS 打印机列表一致)，重开主屏 POS 生效。「测试打印成功」只代表送达主机。
- **收据打哪台**:看设备 License 的收据工位；它为空时走店级回退工位(`fallback_print_station_key`)。
  店级 `receipt_printer_slot` 已退役，值为 0 不代表不打小票。
- **刷卡处理商显示为空**:`swipe_processor` 为空时前端按已配凭据探测，不是没配。核对 WorldPay
  看店铺的 WorldPay 配置是否启用、Acceptor ID 是否已填。
- **菜单 9 中途失败后，读卡器每次插上都报 `failed to get domain 'rua_server'`**:VM 没定义成功。
  常见于装完 libvirt(菜单 5)还没重启就跑菜单 9。重启后重跑菜单 9 即可，已下载的镜像会保留；
  旧版脚本下载中断会留下半截镜像，续跑前先比对 `rua_server.qcow2` 大小与下载站一致，并跑
  `qemu-img check`。
- **菜单 9 跑完要逐项核对**:`Autostart` 是否 enable(否则重启后不能刷卡，`virsh -c qemu:///system autostart rua_server`)、
  15678 是否只监听 `127.0.0.1`、VM 定义里有没有把读卡器设成启动必需(开机时读卡器没插，VM 就起不来)。
  门店站发布的脚本在这几项上可能落后于主干，以实机结果为准。
- **读卡服务日志一直是 `status -> 0 client(s)`**:VM 和读卡器都正常，是主屏 POS 没连上来。
  确认主实例 `config.jsonc` 有 `"usb-swiper": "MOBY5500_net"`,改完重开主屏 POS。旧版 `run.sh`
  每次开 POS 都会重启一次读卡 VM，等约 30 秒再刷卡。
- **远程突然连不上**:`tailscale ping` 能通但 `tailscale ping --icmp` 和 SSH 都超时，说明机器系统已经
  卡死，Tailscale 只是还报在线。远程处理不了，请现场强制重启。

## 装不下去怎么办

**先跑菜单 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`。Windows guest 的 `--anyip` 供 QEMU 转发使用，宿主只监听 `127.0.0.1:15678`。完成后由 libvirt 独立开机启动，打开 POS 不再重启此 VM。若 VM 处于关机状态（开机自启失败或被人关过），双击桌面 OpenPOS POS 会把它启动；正在运行的 VM 不受影响。
- 需暂时关闭新 VM 的开机启动时，执行 `sudo virsh -c qemu:///system autostart --disable rua_server`。恢复时去掉 `--disable`。两条命令均只改变下次开机行为。
- 维护窗口内需正常重启新 VM 时，执行 `~/Downloads/cef_ubuntu/Release/restart_rua.sh --restart-managed`。
- 已有 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 配置。已存在的副屏配置仍保留，历史注释配置需按前面的恢复步骤手动启用。
