Files

264 lines
22 KiB
Markdown
Raw Permalink Normal View History

---
name: dsh-instance-diagnose
description: DSH 多租户平台(ai1net.com / 47.77.182.89)单个用户实例的「故障诊断」技能,重点是内存 / OOM / 实例崩溃重启。当出现「实例打不开」「实例起不来」「实例挂了 / 崩了 / 一直重启」「报 503 / 502」「会话突然报错/中断」「跑着跑着断了」「响应慢」「怀疑内存不够」时触发。⚠️ 与 `dsh-desktop-dev-shell` 的边界:本技能管**服务器上的用户实例(含平台代理层)**;「本机 dsh 实例起不来」(在 Windows 上跑官方 dsh 源码)属对方。核心:先做第 0 层「跨机日志取证」(dshlog),再分「服务级 / 实例级 / 会话级」三层定位,再用 cgroup 内存三件套定量归因,最后在隔离 cgroup 里复现——**绝不在生产实例上做压力测试**。
version: 1.0.0
updated_at: 2026-09-22
last_change: 【2026-09-22 按要求统一版本号】frontmatter `version` → `1.0.0`(原 v1.2.0);正文与历史中的版本号为当时记录,未改动。此前 v1.2.0(2026-09-22):补触发词「实例起不来 / 实例挂了·崩了·一直重启 / 报 503·502」—— 实测盲区:用户说「实例起不来」时原描述命不中(只有「实例打不开」),会误落到 `dsh-desktop-dev-shell`;并显式写清与后者的边界(服务器用户实例 vs 本机源码实例)。此前 2026-09-15:死亡路径由 2 条扩到 4 条 —— 新增 C(bwrap 挂载点被嵌套 tmpfs 遮蔽(`Can't chdir to <正常路径>`))与 D(迁移复现不了启动参数(`Can't chdir to :` 空路径)),含分诊判据与「中间目录必须统一前置」的修法。
agent_created: true
---
# dsh-instance-diagnose — DSH 实例故障诊断
## 何时用
用户报「实例打不开 / 会话突然报错 / 聊到一半断了 / 很慢 / 内存不够」,或你看到 `crash-restart` 日志。**报障第一步永远是先读 journal 拿真实失败请求**,不要先猜。
## 平台事实(硬编码,勿猜)
| 项 | 值 |
|---|---|
| 服务器 | SSH:`ssh -p 22 -i ~/.ssh/id_ed25519 [email protected]`(⚠️ 2026-09-19 更正:别名 `bt-server` 里的 `Port 32022` 已失效 —— sshd 只监听 **22**,用它必 `Connection refused`) |
| 用户数据根 | `/var/lib/dshs/users/<uuid>/`(**不是** `/opt/dsh/users`,那里只有 main) |
| 已知账号 | guest = `4092b965-2f68-4977-9989-68b3966f7df0`(系统 uid **100002**)|admin = `cce6d1cd-b376-4304-80f0-0e1c58c9ffde`(uid 114801) |
| 实例配额 | ⚠️ **2026-09-14 起:基础 MIN、最多浮动到 MAX**(档案 **96**,用户要求:**不受插件开关影响**):cgroup `MemoryHigh = MIN_MEM_MB = 448 MiB`(**软限/基础**,超过即回收·限速)+ `MemoryMax = MAX_MEM_MB = 1024 MiB`(**硬限/上界**,越界 OOM);**不再读 profile 的 bundles**;`heapMbFor()` = 配额 − 96,**cap 256**。其余 `CPUQuota 150%` / `TasksMax 128` 未变。实测(2026-09-14):两 scope 均 `MemoryHigh=448M` + `MemoryMax=1024M`(与各自插件集合无关)。⚠️ **`dsh-univer-office` 这类重插件(gateway ≈390MB + 基座)512 装不下 ⇒ 先抬 MIN**。⚠️ `/etc/dshs.env` 里的 `DSH_INSTANCE_NODE_OPTIONS=--max-old-space-size=160` **已不是生效值**(代码侧 `withHeap()` 会摘掉 `--max-old-space-size` 再按配额补回),查看实际值请直接读 `/proc/<pid>/environ` 与 `systemctl show <scope> -p MemoryMax` |
| 宿主 | 物理内存仅 **1.83 GB**(1915896 kB);swap 1 GB |
| 会话文件 | `<用户根>/home/sessions/--var-lib-...-ws-<工作区>--/<session-id>/session.jsonl.zstd`(**多帧 zstd**,按 magic `28 b5 2f fd` 切分逐帧解压) |
> ⚠️ 会话目录名**不是** `home/sessions/<session>`,中间还有一层带名字的 workspace 转义目录。
## 三层定位(按顺序做,每层都能单独结案)
### 第 0 层 · 跨机日志取证(**先做这一层** —— 2026-09-18 加)
**遇到"线上跑着跑着不对"但说不清哪一层时,先用 `dshlog` 把 47 / 106 的 journald 拉回本机再判**:跨机、跨单元、带毫秒时间线,比逐台 `journalctl` 快一个量级。
```bash
N="E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe"
$N 07-scripts/dshlog.mjs collect --since 6h # 拉取(之后再用就不带 --since,走 cursor 增量)
$N 07-scripts/dshlog.mjs watch --since 1h # 巡检:8 条规则出判据表,有 FAIL ⇒ rc=2
$N 07-scripts/dshlog.mjs q "/EADDRINUSE|OOM/" --since 24h
$N 07-scripts/dshlog.mjs timeline --from 2026-09-18T20:00 --grep "overlay" --out /tmp/tl.log
```
⚠️ **三条必知**(细节见 `04-调整方案/129-日志采集与巡检-方案C实现.md`):
- 🔴 **`ssh -C` 是硬前提**:47 出方向未压缩实测 ~20 KB/s(1.6 MB 要 84 s),开压缩后 11 s。不加会**看起来像卡死**。
- 🔴 **实例层(`dsh --profile`)的 stdout 不进 journald**(`_SYSTEMD_UNIT=…scope` 与 `_PID=` 均为空,已双重取证)⇒ **`dshlog` 拿不到实例自己的日志**,只有 Worker 转发的部分。要实例层证据仍须按下面第 2 层**上机取 cgroup / proc**。
- 📌 归档在 `E:/dsh-logs/`(本机 E 盘,不在仓库内);`prune --keep N` 管保留。
### 第 1 层 · 服务级
```bash
systemctl status dshs --no-pager | head -20 # 重启过没
journalctl -u dshs --since today --no-pager | grep -iE "crash-restart|instance-restart|instance-stable"
```
- `instance-restart` 带 **`exitCode 137`** ⇒ **内核 SIGKILL(128+9),九成是 OOM**
- `exitCode 1` + `ModuleLoader.import` ⇒ 插件加载崩(如 `duplicate loader entry id`)
- 无 exitCode ⇒ 信号终止,看紧随其后的服务重启
### 第 2 层 · 实例级(内存三件套)
**先找 scope 名**(**每次重启 hash 会变**,别写死):
```bash
ls -d /sys/fs/cgroup/memory/system.slice/dsh-*.scope
```
> ⚠️ **本服务器是 cgroup v1**,路径是 `/sys/fs/cgroup/memory/system.slice/<scope>/`,
> 文件叫 `memory.usage_in_bytes` / `memory.max_usage_in_bytes` / `memory.limit_in_bytes`。
> cgroup v2 才是 `memory.current` / `memory.peak` —— **写 v2 路径会静默读到空**。
```bash
P=/sys/fs/cgroup/memory/system.slice/<scope>
cat $P/memory.usage_in_bytes # 当前(字节!不是 KB)
cat $P/memory.max_usage_in_bytes # 历史峰值 —— 等于 limit 就是「曾精确打满」
cat $P/memory.stat # ★ 关键:分 rss 与 cache
cat /proc/<pid>/smaps_rollup # Anonymous / Pss_Anon
```
**判据**:
- `rss` 占绝对多数、`cache` 很小 ⇒ **几乎全不可回收,内核 OOM killer 无路可退**
- `rss` 接近 `limit` 且 `max_usage == limit` ⇒ 配额**零余量**,任何波动就是终点
- `rss_huge` 大 ⇒ 透明大页放大占用
> ⚠️ **`ps` 的 RSS ≠ cgroup 计费**:实测 `ps` 报 437 MiB 而 cgroup 只用 381 MiB(共享页不计入)。
### 第 3 层 · 会话级(谁在吃内存)
```bash
find <用户根>/home/sessions -name session.jsonl.zstd -printf "%TY-%Tm-%Td %TH:%TM %10s %h\n" | sort -r | head -12
```
拉回本地用 `dsh-server-docs/07-scripts/sess-analyze.mjs` 解(**注意该脚本同目录的 `sess-list-presets.mjs` 首行曾缺 `/**` 起始符**,如报 `SyntaxError: Unexpected token '*'` 先补)。
**别忘了对照**:多实例时横向比 `dsh-instance-mem.log`(**时间戳是 UTC,+8 才是本地时间**)。无插件的实例峰值 vs 有插件的实例峰值 = 插件的净增量。
## ⚡ 快速分诊:用户看到的状态码决定查哪一层(2026-09-19 加)
| 用户看到 | 真实含义 | 第一动作 |
|---|---|---|
| **502** | 边缘 nginx 说"上游提前关闭连接" | **先看平台 journal 那条请求有没有 `request completed`**:没有 ⇒ **平台接了却没回响应**(档案 **141**:`proxyHttp` 在**实例冷启动窗口**裸断连接,现已改为 503+Retry-After)。⛔ 别先查 nginx 配置 / 域名 / 实例死活 |
| **431** | 请求头(Cookie)超限 | 陈旧 `dsh-auth-*` 堆积(档案 98)。⚠️ 平台 journal 里**查不到**该请求 |
| **500** 且正文含 `解析不出落点` | 控制面只读**单台**中继 | 见 `PLAYBOOK-实例与插件坑.md §18`(观测面绿而控制面红) |
| 白屏 / `Failed to load plugins` | 客户端 bundle 取不到 | 先问"**无痕窗口是否同样报错**"(区分浏览器侧 vs 服务器侧,PB §3.2) |
⚠️ **502 的典型时序**(实测):`POST /api/dsh/enter` 返回 200(耗时 11 s)→ scope `ActiveEnterTimestamp`
与 enter 同刻 → 用户 **10 余秒后**访问 `<user>.<baseDomain>` → 502。
⇒ 实例**就绪后自愈**(`curl 127.0.0.1:<实例端口>` 回 401);**事后同机 curl 复现不出 502** 是正常的,别因此否定结论。
## 四条死亡路径(症状不同,别混)
| 路径 | 触发 | 日志指纹 | systemd 结果 |
|---|---|---|---|
| **A · V8 堆限** | 堆冲到 `--max-old-space-size` | `FATAL ERROR: Reached heap limit` / `Ineffective mark-compacts` | `code=dumped/status=ABRT` |
| **B · cgroup 限** | RSS 打满 `MemoryMax` | `kernel: oom-kill:constraint=CONSTRAINT_MEMCG, task=node` | `code=killed/status=9/KILL` → 平台 `exitCode 137` |
| **C · bwrap 挂载点被遮蔽**(2026-09-15 新增) | bwrap 参数里**同时绑多个路径且它们嵌套在同一前缀下**(如"用户根" + "共享技能层")。后挂的 `--tmpfs <共同祖先>` 会把**已绑好的挂载点整个遮掉** | `bwrap: Can't chdir to <userRoot>/ws/xxx: No such file or directory` —— ⚠️ **路径看起来完全正常**,极易误判成"目录没建" | 子进程 `exitCode 1` → 平台按崩溃退避重启 → 5 次后**熔断**(10 min 冷却) |
| **D · cwd 为空**(2026-09-15 新增) | 启动参数里 `folder` 为空/`null`(**跨机迁移**时最容易:复现不了原启动参数) | `bwrap: Can't chdir to : No such file or directory` —— ⚠️ **路径是空的**(一眼可辨) | 同上 |
**A/B 拿不到可读的应用层日志**(这正是用户觉得"莫名其妙就断了"的原因);**C/D 反而有明确日志** ⇒ 见到 `Can't chdir` 直接分诊:
- **路径是空的** ⇒ **D**:查实例记录 / 迁移入参里的 `folder` 是不是 NULL。
📌 集群模式下实例行是 `claimInstance` 建的(local 模式不写库),**它必须把 `folder`/`patch` 一并落库**,否则迁移时无处取得启动参数。
- **路径正常却报不存在** ⇒ **C**:看 `orchestrator.ts` 的 bwrap 参数里,**中间目录是不是"就近创建"的**(例如插在 `--bind root root` 之后)。
✅ 正确做法:**所有挂载点的中间目录统一前置 + 去重 + 由外到内**,禁止插在任何一个 `--bind` 之后。
- 最小复现(**别拿生产实例试**):`bwrap <与平台等价的参数> -- /usr/bin/ls -ld <那个路径>`,再逐条增删参数二分。
⚠️ 顺带记住:需要给挂载点权限时只能用 `--tmpfs`(自带 0755),**不能用 `--perms`** —— 47 上 bwrap 是 0.4.0,不认该选项。
## 隔离复现(**唯一允许的压测方式**)
⛔ **压测走隔离环境**:在**生产实例**上压满配额 = 当场 SIGKILL,会连带把实例带进 crash-loop / 熔断(自找干扰)。⚠️ 注意 **2026-09-13 用户已明确「服务器是开发环境,不用担心中断用户」**(R8 已放宽)—— 但**别因此就去污染实例**:隔离复现能拿到同样的数据而**不留副作用**,仍是首选;只有在需要复现"平台侧联动"(如自动回滚、熔断计数)时才动真实例。
```bash
systemd-run --wait --pipe --collect --unit=memsim-x \
-p MemoryHigh=448M \
-p MemoryMax=1024M \
node /tmp/memsim/probe.mjs
```
- 独立 cgroup ⇒ **撞墙只杀模拟进程**,生产不受任何影响(宿主 available 需 > 400 MiB)
- 跑完 `rm -rf /tmp/memsim`,`systemctl list-units "memsim*"` 确认无残留
**复现路径 B(137)的配方**:必须用**纯堆外分配**才能走到 cgroup 限——
```js
const b = Buffer.allocUnsafe(4 * 1024 * 1024); b.fill(1); bufs.push(b)
```
⚠️ 若改用 JS 对象数组,会**先撞 V8 堆限(路径 A)**,永远复现不出 137。这是本轮踩到的坑。
**复现路径 A 的配方**:正常 push JS 对象即可,同时打印 `process.memoryUsage()` 看崩在哪个 `heapUsed`。
## 踩坑清单(全部实测)
1. **cgroup v1 vs v2 路径不同** —— 写错不报错,静默读到空值。
2. **`memory.*_bytes` 单位是字节**,不是 KB(差 1024 倍)。
3. **`dsh-instance-mem.log` 时间戳是 UTC**,+8 才对得上本地时间。
4. **scope 名带随机 hash**,每次重启都变,必须现查。
5. **`ps` RSS ≠ cgroup usage**(共享页不计入 cgroup)。
6. **会话目录多一层 workspace 转义目录**,别按 `home/sessions/<id>` 找。
7. **`systemd-run` 起服务不继承调用者 env** ⇒ 压测必须 `--setenv=NODE_OPTIONS=...`,否则堆限根本没生效。**自证手段**:脚本里打印 `require('node:v8').getHeapStatistics().heap_size_limit`。
8. **同质字符串会被引擎优化**:`'x'.repeat(N) + i` 走 cons string 惰性拼接(不复制前缀)→ 实测「投喂 23 GB 只涨 140 MB」的假数据。**内存类模拟实验极易骗人,优先用真实数据统计 + smaps 实测**。
9. **`.cjs` 不支持顶层 await** ⇒ 用 `await import()` 的探针脚本要存成 `.mjs`。
## 定量归因的经验值(2026-09-12 guest 实例实测)
| 组成 | 量 | 说明 |
|---|---|---|
| V8 老生代堆 | ≤`--max-old-space-size` | **这是「许可」不是「上限」**,V8 会主动把水位推满以减少 GC |
| RSS / heapUsed 膨胀 | **约 3.4×** | 实测 heapUsed 37 MiB 时 RSS 已 125 MiB(预留未用 + 回收未归还给 OS) |
| `dsh-univer-office` | **+65 MiB** | 磁盘 168 MB,含 29 MB + 10 MB 原生绑定 |
| 页缓存(node_modules mmap) | ~18 MiB | **唯一可回收的部分** |
| V8 不管但计入配额 | ~50 MiB | Buffer / 原生库 / 模块映射 |
### 内存去向实测(2026-09-12 · 空载实例 smaps 拆解)
**空载实例(零会话)已占 285 MiB**:`V8 堆/匿名 155.9` + `[heap] 58.1` + `文件映射 69.1`(其中 **node 本体 59.8**)+ `JIT 2.2`。
⚠️ **`[heap]`(malloc 区)不受 `--max-old-space-size` 约束** ⇒ 压堆限**不会**让总量线性下降。
### 会话数据几乎不占内存(重要反直觉)
实测:**整个会话(2546 事件 / 11 轮)的全部字符串只有 0.9 MB**(最大单串 40820 字符 ⇒ 工具有截断)。
⇒ **「减会话长度 / 少用 web_fetch」对内存几乎无用**;占用主体是**代码与插件加载**。
### 插件内存成本(逐个 `import` 实测)
| 插件 | rss 增量 | 备注 |
|---|---|---|
| `dsh-univer-office` | **+65.1 MiB** | 单文件 bundle 5400 行 / 6 MB,**顶层静态 import** 拉起重型依赖(连 `@puppeteer/browsers` 都在),全文件仅 2 处动态 import |
| `libsql` | +7.8 MiB | 原生绑定 |
| 平台自研 ×3(portal-entry / workspace-scoped-picker / business-plugins) | **0.0 MiB** | 自研插件写法是轻的 |
| `puppeteer-core` | **0.0 MiB** | 只 `import` 主入口、不启动浏览器 ⇒ **懒加载确实有效** |
⇒ **成本由「装了什么」决定,不是「装了几个」** —— 一个重型插件 ≈ 无穷多个轻量插件。
⇒ 治理优先级:**卸重型插件 > 让插件懒加载 > 折腾堆参数**。
⚠️ 插件的 `pnpm remove`(禁用)**必须重启实例**才真释放 —— Node 模块缓存不卸载。
**诊断结论的落地口径**:算「V8 堆上限 + 插件 + 堆外 + 缓存」总和是否 ≥ `MemoryMax`。若 ≥,就是**配额本身零余量**(配置问题,不是 bug)——此时可调项只有:① 压 `--max-old-space-size`(代价:更易撞路径 A)② 提高配额(受宿主物理内存限制)③ 减少单会话负载。
## 已知与本平台不兼容的插件
### `dsh-univer-office`(2026-09-12 定性:**架构级不兼容,配置不可修**)
两条**独立**的阻碍,缺一都打不开:
| 阻碍 | 机理 | 证据 |
|---|---|---|
| **Host → Gateway** | 插件启动 bundled Gateway(默认 `127.0.0.1:9080`),实例内 node 要连它做健康检查;而平台 nft `dsh_egress` 对 `skuid 100000-199999 → 127.0.0.0/8` 是 `reject with tcp reset` ⇒ 永远连不上 | `univer_new` 恒报 `bundled Gateway did not become ready within 10000ms`;实例内 `curl 127.0.0.1:908x` → `000` / Connection refused |
| **Browser → Viewer** | **源码硬编码** `gateway = http://127.0.0.1:${port}`、`viewerUrl = ${gateway}/?file=...`,client 拿它当 **iframe src** ⇒ 浏览器去**用户自己电脑**的 127.0.0.1 找 Viewer,那里没有服务 | `grep -o "viewerUrl: [^,]*" lib/index.js`;`grep -oE "http://127\.0\.0\.1:[^\`\"']*"` |
⇒ 它的 Viewer **假设「浏览器与实例同机」(本地部署场景)**,与托管多租户平台根本不兼容。**解封 loopback 也没用**——第二条拦在浏览器侧。
⇒ 只能改插件(viewerUrl 改走平台代理的相对路径)。**治理结论:候选池应下架 / 用户应禁用**(顺带省 **65 MiB**)。
**替代路径**:让 agent 用 python(openpyxl + python-docx + matplotlib)直接产出 xlsx/docx,已验证可行(2026-09-12)。
⇒ **评估标准已立档**:`04-调整方案/75`(**托管友好性 H1–H4** + **资源成本**;自研插件改造规范 **R-a~R-e**)。今后候选池导入与自研插件验收按该档的检查项走 —— 核心判据一句话:**「插件的一切对外交互,是否都能走平台已有的那一条入口」**(浏览器侧只用相对路径;实例侧不依赖 loopback 网络服务)。
## 注入层 / 浮层的**真机验证配方**(2026-09-13 实测,4 轮才摸清)
**为什么不能用 curl 验**:`src/supervisor/proxy.ts` 的注释写得很明白 —— **「curl 不带 Accept-Encoding,故此前验证是假阳性」**。注入只在「客户端接受 HTML → 代理把上游 `accept-encoding` 改写成 `identity` → 上游回未压缩 HTML」时发生;curl 一不留神就绕过这个判定,于是**"有标记"不代表脚本能在浏览器里跑**,而**"没标记"也可能是 curl 自己造成的**。⇒ **判定注入层是否活着,必须用真浏览器。**
**前置(R4 允许的临时会话,别用真实账号)**:
```bash
# 服务器上:建一个 10 分钟自过期的 guest 会话(ip=127.0.0.1 / ua=poc-curl2)
TOKEN=$(ssh bt-server 'cd /opt/dshs && /usr/local/bin/node mksess-guest.cjs')
# 用完立刻删(否则留下真实可用的会话行)
ssh bt-server "cd /opt/dshs && /usr/local/bin/node -e \"const c=require('crypto');const D=require('/opt/dshs/node_modules/better-sqlite3');const db=new D('/var/lib/dshs/dshs.db');console.log(db.prepare('DELETE FROM sessions WHERE token_hash=?').run(c.createHash('sha256').update('$TOKEN').digest('hex')).changes);db.close();\""
```
**工具**:`playwright-core` + `channel:'chrome'`(装在 `E:\ProgramData\.workbuddy\binaries\node\workspace`;须 `createRequire` 指向该目录,并在该目录内跑)。把 `sid` cookie 写进 context(`domain:'.ai1net.com'`, `secure:true`, `sameSite:'None'`),再 `goto https://<user>.ai1net.com/`。
**判定用的哨兵与元素(直接 `page.evaluate` 读)**:
| 判据 | 说明 |
|---|---|
| `window.__dshRecover === 1` | **recovery.js 全文执行完毕的哨兵**(脚本第 23 行设)—— **最强证据**,比找 DOM 元素可靠 |
| `window.fetch.toString()` 不含 `[native code]` | 证明监控层已装载(脚本包装了 `fetch` / `EventSource` / `WebSocket`) |
| `#__dshAssistBar` 存在 | assist.js 跑起来了(它 `mount()` 时建这个容器) |
| `#__dshRecover` + 文案 + `#__dshRetryBtn` | 浮层级 |
| `document.scripts` 里含 `__dsh` 的 `<script>` 数 = 2 | 两个注入脚本都下发了 |
**4 个坑(每个都让我误判过一次)**:
1. **`page.route` 拦不住 WebSocket** ⇒ 想用「拦 `/api/*`」或 `ctx.setOffline(true)` 造断流**逼不出浮层**(dsh 的主链路是 WS/SSE)。`setOffline` 也不会立即掐断已建立的 WS。
2. **`probe()` 有 15 秒节流**(`lastProbe`),而心跳每 25 秒跑一次 ⇒ **心跳会把节流窗口占掉**,你手动 `dispatchEvent(new Event('focus'))` 往往被**静默丢弃**。要么按心跳节奏等(`status` 恒 `running:false` → 连续两次软失败 ≈ 50 s),要么确保距上次探针 ≥15 s。
3. **`recover()` 成功后 0.7 秒内 `location.replace()` / `location.reload()`** ⇒ 浮层只亮 0.7 s。**任何 ≥2 秒的轮询都会全踩空**,然后你会得出"浮层没出现"的**错误结论**。
4. **`#__dshAssistBar` 本身是容器**,里面才是「📁 我的文件」「🧭 能力」两个 button —— **点容器无效**,要用 `page.click('#__dshAssistBar button:nth-child(1)')`。
**✅ 正解(纯客户端、零生产副作用)**:拦 `status` 仿真未运行 + **把 `/api/dsh/enter` 挂住不回**,浮层就会**停住不下跳**,从容观测:
```js
await page.route('**/api/dsh/status*', r => r.fulfill({ status:200, contentType:'application/json', body:'{"running":false}' }))
await page.route('**/api/dsh/enter*', () => { /* 故意永不回包 → 浮层停住 */ })
// 之后每 2 秒采样 #__dshRecover 的可见性与文案即可
```
2026-09-13 用这套验证拿到的实测结果:文案「工作区已休眠,正在唤醒…(**已等待 N 秒**)」**倒计时逐秒递增**、`DSH · AUTO RECOVERY`、AI 核心视觉 + 进度条;助手面板点开后正确列出工作区目录。**全程零页面错误。**
## 相关
- 排查完整案例:`04-调整方案/55`(会话档位)、`04-调整方案/16`(崩溃循环)
- 实例配额与内存优化:`04-调整方案/58`
- 会话档位是「会话创建时播种」的,老会话不跟随平台默认:`04-调整方案/33`