Category: Uncategorized

  • 版本 1.0.392 开发日志

    构建 1.0.392(code 392)· 2026-10-03 21:12:14 · sha256 2b2b5ac777b04f3a…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-03 21:08:17 | 本包:kushan-1.0.392.apk(1.0.392 / code 392,sha256 2b2b5ac777b04f3a…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-CINE-PACK  (2026-10-03 01:39:43)
    
    # B-078 资源包化:过场片移出 APK、按需下载(用户令 2026-10-02「视频丢到资源包」)
    
    ## 现状摸底(本轮实测)
    - 包内片源:`assets/cine/*.ogv`(15 个文件,仓内 41MB;APK 内约 **20MB**:`21_first.ogv` 4.5MB、`25_first` 4.4MB、`23_first` 3.7MB、`1_first` 3.1MB、`37_first` 2.8MB…)。
    - 取件机制**已具备一半**:`scripts/cine_lib.gd` 与立绘同款 —— `user://cine/` 缓存 + 在线清单 `user://cine/index.json` + **包内目录兜底**(`resolve()`:本地缓存 → 包内;网络归 net 层,本模块不碰网络)。
    - 导出排除表**已有先例**:`export_presets.cfg` 的 `exclude_filter` 里已排除 `assets/portraits/*`,**但没排 `assets/cine/*`** ⇒ 这是「移出 APK」那一步的落点。
    - **缺的那一半**:立绘有下载器(`scripts/room/portrait_fetch.gd`、`profile_view.gd`、`npc_panel.gd` 调 `PL.save_disk`),**过场片没有下载器**(`cine_player.gd` 只调 `CL.resolve()`)。
    - 托管落点现成:nw 站的 `/files/`(203 站点目录 `/mnt/data2/www/wwwroot/kushan-nw/files/` → 公网 `https://nw.tripee.cn/files/`、Tailscale `http://100.127.177.38:8088/files/`、局域网 `http://192.168.50.203:8088/files/`),镜像到云机 `/srv/kushan-files/`。
    - 相关参数:`cine.online_base`(**当前为空**)、`cine.ext=.ogv`、`cine.cache_max_mb=64`。
    
    ## 实施方案(按 107 号 S0→S8,一功能一文件)
    1. 新增 `scripts/room/cine_fetch.gd`(对照 `room/portrait_fetch.gd`):按 `cine.online_base` 拉 `index.json` → 按名拉 `.ogv` → `CL.save_index()`/`CL.save_disk()`;三基址候选(公网/Tailscale/局域网)照 `net_endpoints` 口径。
    2. `cine_player.gd` 在 `CL.resolve()` 返空且 `CL.index_has()` 为真时调取件(**只补取件调用,不改播放逻辑**)。
    3. 上传:`assets/cine/*.ogv` → `/files/cine/`(203 本地 + 云机镜像),并生成 `/files/cine/index.json`(名→大小/sha256)。
    4. `data/params.json` 置 `cine.online_base`(三链取一,仍受「外网口径待人拍板」约束 ⇒ 先写内网基址,公网基址随更新通道一并拍板)。
    5. `export_presets.cfg` 在 `exclude_filter` 追加 `assets/cine/*`。
    6. 出包(预期 APK ≈131.8MB → **≈112MB**,即减掉片源那约 20MB)+真机验:**无缓存时能下载并播放**、**断网时优雅降级**(不卡死、不报脚本错)、再次进入离线可播。
    7. 护栏:新增 `tools/verify_cine_pack.py` —— ① 排除表必须含 `assets/cine/*`;② 仓内片源与远端清单逐条对齐(数量/大小);③ `cine.online_base` 非空且与 `tools/hosts.sh` 口径一致。
    
    ## 本轮边界
    - **未动任何片源与排除表**:在「下载器」落地前把包内兜底删掉=过场直接失播 ⇒ 先补取件、再拆包,顺序不能反(本轮只立项+摸底,未见红)。
    
    ## 1.0.389 重出(排除表改走参数)
    
    ## 1.0.389 出包与真机(本轮收尾)
    - APK:**112721647 字节**(改前 1.0.388=131846522);包内 `\.ogv` 条目=**0**、`assets/cine/` 条目=0
    - 资源包:203 站点 5 片+index.json(18.6MB),云机镜像 6 文件;三链回读 200(公网/Tailscale/局域网)
    - 真机 `[Cine] got` 命中=0(清端上 `files/cine` 缓存后跑)
    - 护栏:`python3 tools/verify_cine_pack.py` ⇒ PASS 4 FAIL 0;全量 `run_verify_all.sh` ⇒ PASS 96 FAIL 0
    - **真因**:[1/6] 备份 preset+params、把签名库路径替换成本机路径、注入排除过滤表(版本=standard)
      已把 pack.edition 设为 standard,排除表 assets/portraits/*,evidence/*,docs/*,tasks/*,tools/*,data/private/*,edition/*,dist/*,addons/gut/*,kushan_db/*,backup_archive/*,tests/*,release/*,instances/*,_cl_/*,.venv/*,*.md,*.py,*.sh,*.sql,*.log,assets/cine/*
      已替换 keystore/release 与 keystore/debug
      exclude_filter=assets/portraits/*,evidence/*,docs/*,tasks/*,tools/*,data/private/*,edition/*,dist/*,addons/gut/*,kushan_db/*,backup_archive/*,tests/*,release/*,instances/*,_cl_/*,.venv/*,*.md,*.py,*.sh,*.sql,*.log,assets/cine/*(22 条)
      package/unique_name=com.kushan.dynasty
      应用名=贵霜帝国(出包后还原 project.godot)
    [2/6] 装配原生插件(真源 android_plugins/ → 构建模板 android/build/;幂等)
      [assemble] kushanmic/KushanMicPlugin.java → java/org/godotengine/plugin/kushan/KushanMicPlugin.java
      [assemble] manifest 已有注册 org.godotengine.plugin.v2.KushanMic(不动)
      [assemble] 装配完成:java 1 个,meta-data 1 条
      [assemble] wrapper 已是镜像源(不动)
    
    ### TASK-PKG-SIZE  (2026-10-02 21:55:55)
    
    # 包体构成与可裁项(2026-10-02·深化轮)
    
    ## 实测(1.0.388)
    - APK 总大小:**131846522 字节**(≈131.8MB)
    - sha256:`53a9c0cfc29ee1814d50ac48510f4eb3…`
    
    ## APK 内最大条目(unzip -l 实测)
    ```
         178.4 MB  
          67.8 MB  lib/arm64-v8a/libgodot_android.so
           4.9 MB  classes.dex
           4.5 MB  assets/assets/cine/21_first.ogv
           4.4 MB  assets/assets/cine/25_first.ogv
           3.7 MB  assets/assets/cine/23_first.ogv
           3.1 MB  assets/assets/cine/1_first.ogv
           2.8 MB  assets/assets/cine/37_first.ogv
           1.6 MB  assets/data/world.json
           1.5 MB  assets/.godot/imported/game_serif_sc.otf-fee6e8dbe9021132ab5d30e59a28dc70.fontdata
           1.3 MB  lib/arm64-v8a/libc++_shared.so
           1.2 MB  assets/.godot/imported/orchard_gate.png-2913d0a41c09cf2923d4b4c21b829fd3.ctex
           1.2 MB  assets/.godot/imported/bazaar_day.png-e23e46e519d2ead442183eab5a88ff55.ctex
           1.1 MB  assets/.godot/imported/bazaar_day.png-0c55d8c07ebe6adf1265f6e250f76503.ctex
    ```
    
    ## 仓内 assets 各大类(真源,非 APK 内)
    ```
    1.9G	assets/portraits
    59M	assets/avatars
    41M	assets/cine
    30M	assets/scenes
    14M	assets/refcards
    6.9M	assets/audio
    2.0M	assets/fonts
    1.7M	assets/companion
    1.7M	assets/branding
    28K	assets/textures
    12K	assets/ui
    assets 合计: 2.1G
    ```
    
    
    ### TASK-DEEPEN-20261002  (2026-10-02 21:39:54)
    
    # 深化轮 2026-10-02:技术债/一致性清点(不依赖用户拍板的部分)
    
    ## ① `verify_track_wire` 偶发红 → 实测已不复现(本项收口)
    历史记录(memory/待人否清单)称「`verify_track_wire` 套件偶发红、单跑绿」。本轮实测:
    ```
    run1: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    run2: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    run3: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    ```
    外加全量 `run_verify_all.sh` ⇒ **PASS=95 FAIL=0**(该件在回归里同样绿)。⇒ 该项**已不复现**,从待人否清单移除。
    
    ## ② 68 号状态列读取的坑(自记,防再踩)
    用「按 `|` 切列取最后一格」的脚本读 68 号状态**不可靠**:① 部分行以 `|` 结尾 ⇒ 末格为空;② 单元格内含**字面 `|`**(如 B-039 判据里的 `买 …|把 … 卖出去`)⇒ 切列错位。
    后果:本轮先误判 B-038/39/40、B-066、B-046 为「未验」。**正解**:直接在该行里找状态标记的**成对形态**(`✅ **已` / `⚠️ **` / `🚧 **`),或读主行的第 3 格(状态列)而非末格。
    
    ## ③ 本轮已收口的相邻项
    B-046(运维默认值不再指 52+新护栏,见 `evidence/TASK-B046-HOSTS/`)、B-049/B-050/B-041(见 `evidence/TASK-TUTORIAL-WALLET-STEP/` 与 `evidence/TASK-TUTORIAL-QA/`)。
    
    ### TASK-B046-HOSTS  (2026-10-02 21:30:19)
    
    # B-046 收口(地址单一真源)
    
    ## 判据
    「各脚本的 host 取单一真源、默认即云机」——迁移后网关/库真源在云机,运维脚本再跑不得把代码发回已降级的 52。
    
    ## 本轮改动(7 处有效默认值 → 真源)
    | 文件 | 改前默认 | 改后 |
    |---|---|---|
    | `tools/verify_m1_params.py` | `czdcc@192.168.50.52` | `czdcc@192.168.50.203`(真源仓在 203) |
    | `tools/snapshot.sh` | `${SSH_HOST:-czdcc@192.168.50.52}` | `czdcc@192.168.50.203` |
    | `tools/backup_series.py` | `czdcc@192.168.50.52` | `czdcc@192.168.50.203` |
    | `tools/smoke_newbie_story.py` | `KUSHAN_MUD_HOST=192.168.50.52` | `100.123.201.98`(=`hosts.sh` 的 `KUSHAN_GW_HOST`) |
    | `tools/smoke_roomgraph_walk.py` | 同上 | 同上 |
    | `tools/smoke_mud_login.py` | 同上 | 同上 |
    | `tools/publish_external.py` | 写死 `czdcc@192.168.50.52` 与 `http://192.168.50.52:8000` | 外置 `KUSHAN_PUB_HOST`/`KUSHAN_PUB_BASE`(默认云机/公网前门)+**退役机拒发闸** |
    
    
  • 版本 1.0.391 开发日志

    构建 1.0.391(code 391)· 2026-10-03 20:54:26 · sha256 feabe4772e477d99…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-03 20:50:27 | 本包:kushan-1.0.391.apk(1.0.391 / code 391,sha256 feabe4772e477d99…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-CINE-PACK  (2026-10-03 01:39:43)
    
    # B-078 资源包化:过场片移出 APK、按需下载(用户令 2026-10-02「视频丢到资源包」)
    
    ## 现状摸底(本轮实测)
    - 包内片源:`assets/cine/*.ogv`(15 个文件,仓内 41MB;APK 内约 **20MB**:`21_first.ogv` 4.5MB、`25_first` 4.4MB、`23_first` 3.7MB、`1_first` 3.1MB、`37_first` 2.8MB…)。
    - 取件机制**已具备一半**:`scripts/cine_lib.gd` 与立绘同款 —— `user://cine/` 缓存 + 在线清单 `user://cine/index.json` + **包内目录兜底**(`resolve()`:本地缓存 → 包内;网络归 net 层,本模块不碰网络)。
    - 导出排除表**已有先例**:`export_presets.cfg` 的 `exclude_filter` 里已排除 `assets/portraits/*`,**但没排 `assets/cine/*`** ⇒ 这是「移出 APK」那一步的落点。
    - **缺的那一半**:立绘有下载器(`scripts/room/portrait_fetch.gd`、`profile_view.gd`、`npc_panel.gd` 调 `PL.save_disk`),**过场片没有下载器**(`cine_player.gd` 只调 `CL.resolve()`)。
    - 托管落点现成:nw 站的 `/files/`(203 站点目录 `/mnt/data2/www/wwwroot/kushan-nw/files/` → 公网 `https://nw.tripee.cn/files/`、Tailscale `http://100.127.177.38:8088/files/`、局域网 `http://192.168.50.203:8088/files/`),镜像到云机 `/srv/kushan-files/`。
    - 相关参数:`cine.online_base`(**当前为空**)、`cine.ext=.ogv`、`cine.cache_max_mb=64`。
    
    ## 实施方案(按 107 号 S0→S8,一功能一文件)
    1. 新增 `scripts/room/cine_fetch.gd`(对照 `room/portrait_fetch.gd`):按 `cine.online_base` 拉 `index.json` → 按名拉 `.ogv` → `CL.save_index()`/`CL.save_disk()`;三基址候选(公网/Tailscale/局域网)照 `net_endpoints` 口径。
    2. `cine_player.gd` 在 `CL.resolve()` 返空且 `CL.index_has()` 为真时调取件(**只补取件调用,不改播放逻辑**)。
    3. 上传:`assets/cine/*.ogv` → `/files/cine/`(203 本地 + 云机镜像),并生成 `/files/cine/index.json`(名→大小/sha256)。
    4. `data/params.json` 置 `cine.online_base`(三链取一,仍受「外网口径待人拍板」约束 ⇒ 先写内网基址,公网基址随更新通道一并拍板)。
    5. `export_presets.cfg` 在 `exclude_filter` 追加 `assets/cine/*`。
    6. 出包(预期 APK ≈131.8MB → **≈112MB**,即减掉片源那约 20MB)+真机验:**无缓存时能下载并播放**、**断网时优雅降级**(不卡死、不报脚本错)、再次进入离线可播。
    7. 护栏:新增 `tools/verify_cine_pack.py` —— ① 排除表必须含 `assets/cine/*`;② 仓内片源与远端清单逐条对齐(数量/大小);③ `cine.online_base` 非空且与 `tools/hosts.sh` 口径一致。
    
    ## 本轮边界
    - **未动任何片源与排除表**:在「下载器」落地前把包内兜底删掉=过场直接失播 ⇒ 先补取件、再拆包,顺序不能反(本轮只立项+摸底,未见红)。
    
    ## 1.0.389 重出(排除表改走参数)
    
    ## 1.0.389 出包与真机(本轮收尾)
    - APK:**112721647 字节**(改前 1.0.388=131846522);包内 `\.ogv` 条目=**0**、`assets/cine/` 条目=0
    - 资源包:203 站点 5 片+index.json(18.6MB),云机镜像 6 文件;三链回读 200(公网/Tailscale/局域网)
    - 真机 `[Cine] got` 命中=0(清端上 `files/cine` 缓存后跑)
    - 护栏:`python3 tools/verify_cine_pack.py` ⇒ PASS 4 FAIL 0;全量 `run_verify_all.sh` ⇒ PASS 96 FAIL 0
    - **真因**:[1/6] 备份 preset+params、把签名库路径替换成本机路径、注入排除过滤表(版本=standard)
      已把 pack.edition 设为 standard,排除表 assets/portraits/*,evidence/*,docs/*,tasks/*,tools/*,data/private/*,edition/*,dist/*,addons/gut/*,kushan_db/*,backup_archive/*,tests/*,release/*,instances/*,_cl_/*,.venv/*,*.md,*.py,*.sh,*.sql,*.log,assets/cine/*
      已替换 keystore/release 与 keystore/debug
      exclude_filter=assets/portraits/*,evidence/*,docs/*,tasks/*,tools/*,data/private/*,edition/*,dist/*,addons/gut/*,kushan_db/*,backup_archive/*,tests/*,release/*,instances/*,_cl_/*,.venv/*,*.md,*.py,*.sh,*.sql,*.log,assets/cine/*(22 条)
      package/unique_name=com.kushan.dynasty
      应用名=贵霜帝国(出包后还原 project.godot)
    [2/6] 装配原生插件(真源 android_plugins/ → 构建模板 android/build/;幂等)
      [assemble] kushanmic/KushanMicPlugin.java → java/org/godotengine/plugin/kushan/KushanMicPlugin.java
      [assemble] manifest 已有注册 org.godotengine.plugin.v2.KushanMic(不动)
      [assemble] 装配完成:java 1 个,meta-data 1 条
      [assemble] wrapper 已是镜像源(不动)
    
    ### TASK-PKG-SIZE  (2026-10-02 21:55:55)
    
    # 包体构成与可裁项(2026-10-02·深化轮)
    
    ## 实测(1.0.388)
    - APK 总大小:**131846522 字节**(≈131.8MB)
    - sha256:`53a9c0cfc29ee1814d50ac48510f4eb3…`
    
    ## APK 内最大条目(unzip -l 实测)
    ```
         178.4 MB  
          67.8 MB  lib/arm64-v8a/libgodot_android.so
           4.9 MB  classes.dex
           4.5 MB  assets/assets/cine/21_first.ogv
           4.4 MB  assets/assets/cine/25_first.ogv
           3.7 MB  assets/assets/cine/23_first.ogv
           3.1 MB  assets/assets/cine/1_first.ogv
           2.8 MB  assets/assets/cine/37_first.ogv
           1.6 MB  assets/data/world.json
           1.5 MB  assets/.godot/imported/game_serif_sc.otf-fee6e8dbe9021132ab5d30e59a28dc70.fontdata
           1.3 MB  lib/arm64-v8a/libc++_shared.so
           1.2 MB  assets/.godot/imported/orchard_gate.png-2913d0a41c09cf2923d4b4c21b829fd3.ctex
           1.2 MB  assets/.godot/imported/bazaar_day.png-e23e46e519d2ead442183eab5a88ff55.ctex
           1.1 MB  assets/.godot/imported/bazaar_day.png-0c55d8c07ebe6adf1265f6e250f76503.ctex
    ```
    
    ## 仓内 assets 各大类(真源,非 APK 内)
    ```
    1.9G	assets/portraits
    59M	assets/avatars
    41M	assets/cine
    30M	assets/scenes
    14M	assets/refcards
    6.9M	assets/audio
    2.0M	assets/fonts
    1.7M	assets/companion
    1.7M	assets/branding
    28K	assets/textures
    12K	assets/ui
    assets 合计: 2.1G
    ```
    
    
    ### TASK-DEEPEN-20261002  (2026-10-02 21:39:54)
    
    # 深化轮 2026-10-02:技术债/一致性清点(不依赖用户拍板的部分)
    
    ## ① `verify_track_wire` 偶发红 → 实测已不复现(本项收口)
    历史记录(memory/待人否清单)称「`verify_track_wire` 套件偶发红、单跑绿」。本轮实测:
    ```
    run1: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    run2: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    run3: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    ```
    外加全量 `run_verify_all.sh` ⇒ **PASS=95 FAIL=0**(该件在回归里同样绿)。⇒ 该项**已不复现**,从待人否清单移除。
    
    ## ② 68 号状态列读取的坑(自记,防再踩)
    用「按 `|` 切列取最后一格」的脚本读 68 号状态**不可靠**:① 部分行以 `|` 结尾 ⇒ 末格为空;② 单元格内含**字面 `|`**(如 B-039 判据里的 `买 …|把 … 卖出去`)⇒ 切列错位。
    后果:本轮先误判 B-038/39/40、B-066、B-046 为「未验」。**正解**:直接在该行里找状态标记的**成对形态**(`✅ **已` / `⚠️ **` / `🚧 **`),或读主行的第 3 格(状态列)而非末格。
    
    ## ③ 本轮已收口的相邻项
    B-046(运维默认值不再指 52+新护栏,见 `evidence/TASK-B046-HOSTS/`)、B-049/B-050/B-041(见 `evidence/TASK-TUTORIAL-WALLET-STEP/` 与 `evidence/TASK-TUTORIAL-QA/`)。
    
    ### TASK-B046-HOSTS  (2026-10-02 21:30:19)
    
    # B-046 收口(地址单一真源)
    
    ## 判据
    「各脚本的 host 取单一真源、默认即云机」——迁移后网关/库真源在云机,运维脚本再跑不得把代码发回已降级的 52。
    
    ## 本轮改动(7 处有效默认值 → 真源)
    | 文件 | 改前默认 | 改后 |
    |---|---|---|
    | `tools/verify_m1_params.py` | `czdcc@192.168.50.52` | `czdcc@192.168.50.203`(真源仓在 203) |
    | `tools/snapshot.sh` | `${SSH_HOST:-czdcc@192.168.50.52}` | `czdcc@192.168.50.203` |
    | `tools/backup_series.py` | `czdcc@192.168.50.52` | `czdcc@192.168.50.203` |
    | `tools/smoke_newbie_story.py` | `KUSHAN_MUD_HOST=192.168.50.52` | `100.123.201.98`(=`hosts.sh` 的 `KUSHAN_GW_HOST`) |
    | `tools/smoke_roomgraph_walk.py` | 同上 | 同上 |
    | `tools/smoke_mud_login.py` | 同上 | 同上 |
    | `tools/publish_external.py` | 写死 `czdcc@192.168.50.52` 与 `http://192.168.50.52:8000` | 外置 `KUSHAN_PUB_HOST`/`KUSHAN_PUB_BASE`(默认云机/公网前门)+**退役机拒发闸** |
    
    
  • 版本 1.0.390 开发日志

    构建 1.0.390(code 390)· 2026-10-03 04:43:05 · sha256 a5d50df0cbd98423…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-03 04:38:53 | 本包:kushan-1.0.390.apk(1.0.390 / code 390,sha256 a5d50df0cbd98423…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-CINE-PACK  (2026-10-03 01:39:43)
    
    # B-078 资源包化:过场片移出 APK、按需下载(用户令 2026-10-02「视频丢到资源包」)
    
    ## 现状摸底(本轮实测)
    - 包内片源:`assets/cine/*.ogv`(15 个文件,仓内 41MB;APK 内约 **20MB**:`21_first.ogv` 4.5MB、`25_first` 4.4MB、`23_first` 3.7MB、`1_first` 3.1MB、`37_first` 2.8MB…)。
    - 取件机制**已具备一半**:`scripts/cine_lib.gd` 与立绘同款 —— `user://cine/` 缓存 + 在线清单 `user://cine/index.json` + **包内目录兜底**(`resolve()`:本地缓存 → 包内;网络归 net 层,本模块不碰网络)。
    - 导出排除表**已有先例**:`export_presets.cfg` 的 `exclude_filter` 里已排除 `assets/portraits/*`,**但没排 `assets/cine/*`** ⇒ 这是「移出 APK」那一步的落点。
    - **缺的那一半**:立绘有下载器(`scripts/room/portrait_fetch.gd`、`profile_view.gd`、`npc_panel.gd` 调 `PL.save_disk`),**过场片没有下载器**(`cine_player.gd` 只调 `CL.resolve()`)。
    - 托管落点现成:nw 站的 `/files/`(203 站点目录 `/mnt/data2/www/wwwroot/kushan-nw/files/` → 公网 `https://nw.tripee.cn/files/`、Tailscale `http://100.127.177.38:8088/files/`、局域网 `http://192.168.50.203:8088/files/`),镜像到云机 `/srv/kushan-files/`。
    - 相关参数:`cine.online_base`(**当前为空**)、`cine.ext=.ogv`、`cine.cache_max_mb=64`。
    
    ## 实施方案(按 107 号 S0→S8,一功能一文件)
    1. 新增 `scripts/room/cine_fetch.gd`(对照 `room/portrait_fetch.gd`):按 `cine.online_base` 拉 `index.json` → 按名拉 `.ogv` → `CL.save_index()`/`CL.save_disk()`;三基址候选(公网/Tailscale/局域网)照 `net_endpoints` 口径。
    2. `cine_player.gd` 在 `CL.resolve()` 返空且 `CL.index_has()` 为真时调取件(**只补取件调用,不改播放逻辑**)。
    3. 上传:`assets/cine/*.ogv` → `/files/cine/`(203 本地 + 云机镜像),并生成 `/files/cine/index.json`(名→大小/sha256)。
    4. `data/params.json` 置 `cine.online_base`(三链取一,仍受「外网口径待人拍板」约束 ⇒ 先写内网基址,公网基址随更新通道一并拍板)。
    5. `export_presets.cfg` 在 `exclude_filter` 追加 `assets/cine/*`。
    6. 出包(预期 APK ≈131.8MB → **≈112MB**,即减掉片源那约 20MB)+真机验:**无缓存时能下载并播放**、**断网时优雅降级**(不卡死、不报脚本错)、再次进入离线可播。
    7. 护栏:新增 `tools/verify_cine_pack.py` —— ① 排除表必须含 `assets/cine/*`;② 仓内片源与远端清单逐条对齐(数量/大小);③ `cine.online_base` 非空且与 `tools/hosts.sh` 口径一致。
    
    ## 本轮边界
    - **未动任何片源与排除表**:在「下载器」落地前把包内兜底删掉=过场直接失播 ⇒ 先补取件、再拆包,顺序不能反(本轮只立项+摸底,未见红)。
    
    ## 1.0.389 重出(排除表改走参数)
    
    ## 1.0.389 出包与真机(本轮收尾)
    - APK:**112721647 字节**(改前 1.0.388=131846522);包内 `\.ogv` 条目=**0**、`assets/cine/` 条目=0
    - 资源包:203 站点 5 片+index.json(18.6MB),云机镜像 6 文件;三链回读 200(公网/Tailscale/局域网)
    - 真机 `[Cine] got` 命中=0(清端上 `files/cine` 缓存后跑)
    - 护栏:`python3 tools/verify_cine_pack.py` ⇒ PASS 4 FAIL 0;全量 `run_verify_all.sh` ⇒ PASS 96 FAIL 0
    - **真因**:[1/6] 备份 preset+params、把签名库路径替换成本机路径、注入排除过滤表(版本=standard)
      已把 pack.edition 设为 standard,排除表 assets/portraits/*,evidence/*,docs/*,tasks/*,tools/*,data/private/*,edition/*,dist/*,addons/gut/*,kushan_db/*,backup_archive/*,tests/*,release/*,instances/*,_cl_/*,.venv/*,*.md,*.py,*.sh,*.sql,*.log,assets/cine/*
      已替换 keystore/release 与 keystore/debug
      exclude_filter=assets/portraits/*,evidence/*,docs/*,tasks/*,tools/*,data/private/*,edition/*,dist/*,addons/gut/*,kushan_db/*,backup_archive/*,tests/*,release/*,instances/*,_cl_/*,.venv/*,*.md,*.py,*.sh,*.sql,*.log,assets/cine/*(22 条)
      package/unique_name=com.kushan.dynasty
      应用名=贵霜帝国(出包后还原 project.godot)
    [2/6] 装配原生插件(真源 android_plugins/ → 构建模板 android/build/;幂等)
      [assemble] kushanmic/KushanMicPlugin.java → java/org/godotengine/plugin/kushan/KushanMicPlugin.java
      [assemble] manifest 已有注册 org.godotengine.plugin.v2.KushanMic(不动)
      [assemble] 装配完成:java 1 个,meta-data 1 条
      [assemble] wrapper 已是镜像源(不动)
    
    ### TASK-PKG-SIZE  (2026-10-02 21:55:55)
    
    # 包体构成与可裁项(2026-10-02·深化轮)
    
    ## 实测(1.0.388)
    - APK 总大小:**131846522 字节**(≈131.8MB)
    - sha256:`53a9c0cfc29ee1814d50ac48510f4eb3…`
    
    ## APK 内最大条目(unzip -l 实测)
    ```
         178.4 MB  
          67.8 MB  lib/arm64-v8a/libgodot_android.so
           4.9 MB  classes.dex
           4.5 MB  assets/assets/cine/21_first.ogv
           4.4 MB  assets/assets/cine/25_first.ogv
           3.7 MB  assets/assets/cine/23_first.ogv
           3.1 MB  assets/assets/cine/1_first.ogv
           2.8 MB  assets/assets/cine/37_first.ogv
           1.6 MB  assets/data/world.json
           1.5 MB  assets/.godot/imported/game_serif_sc.otf-fee6e8dbe9021132ab5d30e59a28dc70.fontdata
           1.3 MB  lib/arm64-v8a/libc++_shared.so
           1.2 MB  assets/.godot/imported/orchard_gate.png-2913d0a41c09cf2923d4b4c21b829fd3.ctex
           1.2 MB  assets/.godot/imported/bazaar_day.png-e23e46e519d2ead442183eab5a88ff55.ctex
           1.1 MB  assets/.godot/imported/bazaar_day.png-0c55d8c07ebe6adf1265f6e250f76503.ctex
    ```
    
    ## 仓内 assets 各大类(真源,非 APK 内)
    ```
    1.9G	assets/portraits
    59M	assets/avatars
    41M	assets/cine
    30M	assets/scenes
    14M	assets/refcards
    6.9M	assets/audio
    2.0M	assets/fonts
    1.7M	assets/companion
    1.7M	assets/branding
    28K	assets/textures
    12K	assets/ui
    assets 合计: 2.1G
    ```
    
    
    ### TASK-DEEPEN-20261002  (2026-10-02 21:39:54)
    
    # 深化轮 2026-10-02:技术债/一致性清点(不依赖用户拍板的部分)
    
    ## ① `verify_track_wire` 偶发红 → 实测已不复现(本项收口)
    历史记录(memory/待人否清单)称「`verify_track_wire` 套件偶发红、单跑绿」。本轮实测:
    ```
    run1: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    run2: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    run3: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    ```
    外加全量 `run_verify_all.sh` ⇒ **PASS=95 FAIL=0**(该件在回归里同样绿)。⇒ 该项**已不复现**,从待人否清单移除。
    
    ## ② 68 号状态列读取的坑(自记,防再踩)
    用「按 `|` 切列取最后一格」的脚本读 68 号状态**不可靠**:① 部分行以 `|` 结尾 ⇒ 末格为空;② 单元格内含**字面 `|`**(如 B-039 判据里的 `买 …|把 … 卖出去`)⇒ 切列错位。
    后果:本轮先误判 B-038/39/40、B-066、B-046 为「未验」。**正解**:直接在该行里找状态标记的**成对形态**(`✅ **已` / `⚠️ **` / `🚧 **`),或读主行的第 3 格(状态列)而非末格。
    
    ## ③ 本轮已收口的相邻项
    B-046(运维默认值不再指 52+新护栏,见 `evidence/TASK-B046-HOSTS/`)、B-049/B-050/B-041(见 `evidence/TASK-TUTORIAL-WALLET-STEP/` 与 `evidence/TASK-TUTORIAL-QA/`)。
    
    ### TASK-B046-HOSTS  (2026-10-02 21:30:19)
    
    # B-046 收口(地址单一真源)
    
    ## 判据
    「各脚本的 host 取单一真源、默认即云机」——迁移后网关/库真源在云机,运维脚本再跑不得把代码发回已降级的 52。
    
    ## 本轮改动(7 处有效默认值 → 真源)
    | 文件 | 改前默认 | 改后 |
    |---|---|---|
    | `tools/verify_m1_params.py` | `czdcc@192.168.50.52` | `czdcc@192.168.50.203`(真源仓在 203) |
    | `tools/snapshot.sh` | `${SSH_HOST:-czdcc@192.168.50.52}` | `czdcc@192.168.50.203` |
    | `tools/backup_series.py` | `czdcc@192.168.50.52` | `czdcc@192.168.50.203` |
    | `tools/smoke_newbie_story.py` | `KUSHAN_MUD_HOST=192.168.50.52` | `100.123.201.98`(=`hosts.sh` 的 `KUSHAN_GW_HOST`) |
    | `tools/smoke_roomgraph_walk.py` | 同上 | 同上 |
    | `tools/smoke_mud_login.py` | 同上 | 同上 |
    | `tools/publish_external.py` | 写死 `czdcc@192.168.50.52` 与 `http://192.168.50.52:8000` | 外置 `KUSHAN_PUB_HOST`/`KUSHAN_PUB_BASE`(默认云机/公网前门)+**退役机拒发闸** |
    
    
  • 版本 1.0.389 开发日志

    构建 1.0.389(code 389)· 2026-10-03 01:38:43 · sha256 c98a72dd2b065281…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-03 01:29:37 | 本包:kushan-1.0.389.apk(1.0.389 / code 389,sha256 c98a72dd2b065281…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-CINE-RESOURCE-PACK  (2026-10-03 01:29:35)
    
    # B-051 资源包化:过场片移出 APK、按需下载(用户令 2026-10-02「视频丢到资源包」)
    
    ## 现状摸底(本轮实测)
    - 包内片源:`assets/cine/*.ogv`(15 个文件,仓内 41MB;APK 内约 **20MB**:`21_first.ogv` 4.5MB、`25_first` 4.4MB、`23_first` 3.7MB、`1_first` 3.1MB、`37_first` 2.8MB…)。
    - 取件机制**已具备一半**:`scripts/cine_lib.gd` 与立绘同款 —— `user://cine/` 缓存 + 在线清单 `user://cine/index.json` + **包内目录兜底**(`resolve()`:本地缓存 → 包内;网络归 net 层,本模块不碰网络)。
    - 导出排除表**已有先例**:`export_presets.cfg` 的 `exclude_filter` 里已排除 `assets/portraits/*`,**但没排 `assets/cine/*`** ⇒ 这是「移出 APK」那一步的落点。
    - **缺的那一半**:立绘有下载器(`scripts/room/portrait_fetch.gd`、`profile_view.gd`、`npc_panel.gd` 调 `PL.save_disk`),**过场片没有下载器**(`cine_player.gd` 只调 `CL.resolve()`)。
    - 托管落点现成:nw 站的 `/files/`(203 站点目录 `/mnt/data2/www/wwwroot/kushan-nw/files/` → 公网 `https://nw.tripee.cn/files/`、Tailscale `http://100.127.177.38:8088/files/`、局域网 `http://192.168.50.203:8088/files/`),镜像到云机 `/srv/kushan-files/`。
    - 相关参数:`cine.online_base`(**当前为空**)、`cine.ext=.ogv`、`cine.cache_max_mb=64`。
    
    ## 实施方案(按 107 号 S0→S8,一功能一文件)
    1. 新增 `scripts/room/cine_fetch.gd`(对照 `room/portrait_fetch.gd`):按 `cine.online_base` 拉 `index.json` → 按名拉 `.ogv` → `CL.save_index()`/`CL.save_disk()`;三基址候选(公网/Tailscale/局域网)照 `net_endpoints` 口径。
    2. `cine_player.gd` 在 `CL.resolve()` 返空且 `CL.index_has()` 为真时调取件(**只补取件调用,不改播放逻辑**)。
    3. 上传:`assets/cine/*.ogv` → `/files/cine/`(203 本地 + 云机镜像),并生成 `/files/cine/index.json`(名→大小/sha256)。
    4. `data/params.json` 置 `cine.online_base`(三链取一,仍受「外网口径待人拍板」约束 ⇒ 先写内网基址,公网基址随更新通道一并拍板)。
    5. `export_presets.cfg` 在 `exclude_filter` 追加 `assets/cine/*`。
    6. 出包(预期 APK ≈131.8MB → **≈112MB**,即减掉片源那约 20MB)+真机验:**无缓存时能下载并播放**、**断网时优雅降级**(不卡死、不报脚本错)、再次进入离线可播。
    7. 护栏:新增 `tools/verify_cine_pack.py` —— ① 排除表必须含 `assets/cine/*`;② 仓内片源与远端清单逐条对齐(数量/大小);③ `cine.online_base` 非空且与 `tools/hosts.sh` 口径一致。
    
    ## 本轮边界
    - **未动任何片源与排除表**:在「下载器」落地前把包内兜底删掉=过场直接失播 ⇒ 先补取件、再拆包,顺序不能反(本轮只立项+摸底,未见红)。
    
    ## 1.0.389 重出(排除表改走参数)
    
    ## 1.0.389 出包与真机(本轮收尾)
    - APK:**112721647 字节**(改前 1.0.388=131846522);包内 `\.ogv` 条目=**0**、`assets/cine/` 条目=0
    - 资源包:203 站点 5 片+index.json(18.6MB),云机镜像 6 文件;三链回读 200(公网/Tailscale/局域网)
    - 真机 `[Cine] got` 命中=0(清端上 `files/cine` 缓存后跑)
    - 护栏:`python3 tools/verify_cine_pack.py` ⇒ PASS 4 FAIL 0;全量 `run_verify_all.sh` ⇒ PASS 96 FAIL 0
    
    ### TASK-PKG-SIZE  (2026-10-02 21:55:55)
    
    # 包体构成与可裁项(2026-10-02·深化轮)
    
    ## 实测(1.0.388)
    - APK 总大小:**131846522 字节**(≈131.8MB)
    - sha256:`53a9c0cfc29ee1814d50ac48510f4eb3…`
    
    ## APK 内最大条目(unzip -l 实测)
    ```
         178.4 MB  
          67.8 MB  lib/arm64-v8a/libgodot_android.so
           4.9 MB  classes.dex
           4.5 MB  assets/assets/cine/21_first.ogv
           4.4 MB  assets/assets/cine/25_first.ogv
           3.7 MB  assets/assets/cine/23_first.ogv
           3.1 MB  assets/assets/cine/1_first.ogv
           2.8 MB  assets/assets/cine/37_first.ogv
           1.6 MB  assets/data/world.json
           1.5 MB  assets/.godot/imported/game_serif_sc.otf-fee6e8dbe9021132ab5d30e59a28dc70.fontdata
           1.3 MB  lib/arm64-v8a/libc++_shared.so
           1.2 MB  assets/.godot/imported/orchard_gate.png-2913d0a41c09cf2923d4b4c21b829fd3.ctex
           1.2 MB  assets/.godot/imported/bazaar_day.png-e23e46e519d2ead442183eab5a88ff55.ctex
           1.1 MB  assets/.godot/imported/bazaar_day.png-0c55d8c07ebe6adf1265f6e250f76503.ctex
    ```
    
    ## 仓内 assets 各大类(真源,非 APK 内)
    ```
    1.9G	assets/portraits
    59M	assets/avatars
    41M	assets/cine
    30M	assets/scenes
    14M	assets/refcards
    6.9M	assets/audio
    2.0M	assets/fonts
    1.7M	assets/companion
    1.7M	assets/branding
    28K	assets/textures
    12K	assets/ui
    assets 合计: 2.1G
    ```
    
    
    ### TASK-DEEPEN-20261002  (2026-10-02 21:39:54)
    
    # 深化轮 2026-10-02:技术债/一致性清点(不依赖用户拍板的部分)
    
    ## ① `verify_track_wire` 偶发红 → 实测已不复现(本项收口)
    历史记录(memory/待人否清单)称「`verify_track_wire` 套件偶发红、单跑绿」。本轮实测:
    ```
    run1: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    run2: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    run3: rc=0 | === 埋点护栏:PASS=5 FAIL=0 ===
    ```
    外加全量 `run_verify_all.sh` ⇒ **PASS=95 FAIL=0**(该件在回归里同样绿)。⇒ 该项**已不复现**,从待人否清单移除。
    
    ## ② 68 号状态列读取的坑(自记,防再踩)
    用「按 `|` 切列取最后一格」的脚本读 68 号状态**不可靠**:① 部分行以 `|` 结尾 ⇒ 末格为空;② 单元格内含**字面 `|`**(如 B-039 判据里的 `买 …|把 … 卖出去`)⇒ 切列错位。
    后果:本轮先误判 B-038/39/40、B-066、B-046 为「未验」。**正解**:直接在该行里找状态标记的**成对形态**(`✅ **已` / `⚠️ **` / `🚧 **`),或读主行的第 3 格(状态列)而非末格。
    
    ## ③ 本轮已收口的相邻项
    B-046(运维默认值不再指 52+新护栏,见 `evidence/TASK-B046-HOSTS/`)、B-049/B-050/B-041(见 `evidence/TASK-TUTORIAL-WALLET-STEP/` 与 `evidence/TASK-TUTORIAL-QA/`)。
    
    ### TASK-B046-HOSTS  (2026-10-02 21:30:19)
    
    # B-046 收口(地址单一真源)
    
    ## 判据
    「各脚本的 host 取单一真源、默认即云机」——迁移后网关/库真源在云机,运维脚本再跑不得把代码发回已降级的 52。
    
    ## 本轮改动(7 处有效默认值 → 真源)
    | 文件 | 改前默认 | 改后 |
    |---|---|---|
    | `tools/verify_m1_params.py` | `czdcc@192.168.50.52` | `czdcc@192.168.50.203`(真源仓在 203) |
    | `tools/snapshot.sh` | `${SSH_HOST:-czdcc@192.168.50.52}` | `czdcc@192.168.50.203` |
    | `tools/backup_series.py` | `czdcc@192.168.50.52` | `czdcc@192.168.50.203` |
    | `tools/smoke_newbie_story.py` | `KUSHAN_MUD_HOST=192.168.50.52` | `100.123.201.98`(=`hosts.sh` 的 `KUSHAN_GW_HOST`) |
    | `tools/smoke_roomgraph_walk.py` | 同上 | 同上 |
    | `tools/smoke_mud_login.py` | 同上 | 同上 |
    | `tools/publish_external.py` | 写死 `czdcc@192.168.50.52` 与 `http://192.168.50.52:8000` | 外置 `KUSHAN_PUB_HOST`/`KUSHAN_PUB_BASE`(默认云机/公网前门)+**退役机拒发闸** |
    
    注释/文档串里的「回落 52 用 `KUSHAN_DB_HOST=192.168.50.52`」**保留**(那是给人看的回落用法,不是默认值);`tools/hosts.sh`、`tools/db_creds.py`、`dbq.py`、`room_exits_restore.py`、`show_rooms.py`、`plan_room_exits.py`、`gen_update_07.py`、`player_event_tracker.py`、`tools/sudoers/*` 进白名单。
    
    ## 新护栏
    `tools/verify_hosts_single_source.py`(进全量回归):扫 `tools/**` 的 .py/.sh(排除白名单与 `__pycache__`),
    **注释与文档字符串之外**出现 `192.168.50.52`/`100.80.163.51` 即 FAIL(含文件:行:原文),红项=0 才算过。
    
    ### TASK-TUTORIAL-WALLET-STEP  (2026-10-02 21:19:08)
    
    # TASK-TUTORIAL-WALLET-STEP —— 引导「钱包基础」步两处真问题(2026-10-02,为取 B-041 画面级证据而查出来)
    
    ## 怎么查出来的
    
  • 版本 1.0.388 开发日志

    构建 1.0.388(code 388)· 2026-10-02 20:40:24 · sha256 53a9c0cfc29ee181…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 20:35:28 | 本包:kushan-1.0.388.apk(1.0.388 / code 388,sha256 53a9c0cfc29ee181…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-TUTORIAL-WALLET-STEP  (2026-10-02 20:07:35)
    
    # TASK-TUTORIAL-WALLET-STEP —— 引导「钱包基础」步两处真问题(2026-10-02,为取 B-041 画面级证据而查出来)
    
    ## 怎么查出来的
    B-041 要画面级证据「动作栏背包照常/无交易面板入口」,但真机两次都停在**新手引导层**(`[2/6]`)。
    按纪律不当通过、继续攻这道墙,于是:
    ① `TUTORIAL_AUTOPLAY=0` 无效(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 即 return ⇒ **只关自动播放,不禁引导**)
    ② OCR 行级 bbox 找「一键跳过全部」⇒ 分词把整行并成一句,**无可点词框** ⇒ 点不掉
    ③ 本地缓存 `user://tutorial_<wallet16>.json` —— 设备上**不存在**(状态在服务端)
    ④ 服务端状态表 `player_tutorial_state`:`local_0` step_idx=1 已有 finished_at;**`local_1`/`local_59` = step_idx 1、finished_at NULL** ⇒ 卡住的就是这两个
    ⑤ 开 `TUTORIAL_AUTOPLAY=1` 让它自己走:从 `[2/6]` 推进到 **`[5/6]`「钱包基础」** 后**卡死** ——
       `KUSHAN_TUTORIAL_AUTOPLAY step=wallet(idx=4) act=no_keeper_here idx=4->4 ok=false` **每 6 秒重试一次、连续 5 次不前进**
    
    ## 由此查出的两处真问题(都已立项)
    ### 1. 引导文案还指着**已退场的「商铺」面板**(用户令 2026-10-01 交易一律靠对话)
    `data/i18n.json` `ui.tutorial.t014` 原文:
    > 点击右下角 [b]轨迹球[/b] → 「商铺」\n用 [b]KSC[/b] 货币购买商品,完成交易。\n这是西域最常见…
    
    按 B-041 口径(面板已退场、交易一律靠与掌柜对话),这句在**教玩家点一个已经不存在的入口**。
    ### 2. 引导 autoplay 的 wallet 步**走不过去**(`no_keeper_here` 循环)
    `scripts/tutorial_autoplay.gd` 里 wallet 步的真实流程注释写的是「先移到掌柜房 → 再说话买」,
    实际是卡在找不到掌柜、也不换房(`idx=4->4` 原地重试)⇒ **引导 6/6 走不完** ⇒ 直接影响:
    - 新手第一次进游戏推不动(真机体验)
    - 所有「要画面级证据」的真机取证都被引导层挡在外面(B-041 的缺口就是这么来的)
    
    ## 本轮对 B-041 的处置
    B-041 保持 **⚠️ 半验**(代码级判据全过、画面级缺口未补)——**不因「查到别的问题」就把它记成已验**。
    本文件夹是这两处新问题的**证据与立项依据**,与 B-041 的缺口互为因果、分开登记。
    
    ## 原始读数
    - `logcat_b041e.txt`:`KUSHAN_TUTORIAL_AUTOPLAY step=wallet … no_keeper_here` 连续 5 条
    - `ocr_room.txt`:`[5/6] 钱包基础 点击右下角轨迹球—「商铺」用KSC货币购买商品…`
    
    ## 本轮修法与真机复现口径(2026-10-02)
    - **B-048 已改**:`data/i18n.json` 与 `scripts/tutorial_steps.gd`(同一句双真源、护栏要求逐字一致)⇒「点击右下角轨迹球 → 找 [b]掌柜[/b] 说话 / 用 [b]KSC[/b] 货币买下商品,完成交易。」
      基线键表 `evidence/TASK-E20/keymap.tsv` 的「改前字面量」按 C1.3 口径同步(只动含 `t014` 的行)⇒ `verify_client_i18n` 652 条全一致。
    - **B-049 已改**:`scripts/tutorial_autoplay.gd` wallet 步把 `no_keeper_here` 拆成 **`no_keepers_data(sk=0,items=N)`**(店主/商品数据为空)/**`no_keeper_hop(sk=N,items=N,from=房间)`**(数据在、从本房走不到店主房),真机日志一次读出病因。
    
    ## 真机复现(1.0.384,三星 R3CR704Q72V)
    - **B-048 ✅ 目视成立**:装上 384 后该步画面 OCR 读出「…[5/6] 钱包基础 点击右下角轨迹球—**找掌柜说话** 用 KSC 货币**买下商品**,完成交易。」
      ⇒ 包内与屏上都是新文案(旧文案写「→「商铺」用 KSC 货币购买商品」)。证据 `device_384_t014.png`/`ocr_384_t014.txt`(OCR 把「掌柜」认成「学柜」,属识别噪声,句式为新版可辨)。
    
    ### TASK-B041-RETIRED  (2026-10-02 18:45:39)
    
    # TASK-B041-RETIRED —— 面板路线退场 收口取证(2026-10-02)
    
    ## 对账(先查实况,不认状态列)
    68 号 B-041 状态列原写「⬜ 未做(2026-10-01)」,且带一条 2026-10-01 21:44 的订正(说 `scripts/shop_price.gd` 仍在、19 处站点仍在)。
    **本轮实查实况(2026-10-02)**:
    - `scripts/shop_price.gd` **不在磁盘、不在 git 跟踪**
    - `scenes/generated/shops/*.tscn` **0 个**
    - `project.godot` **无 ShopPrice autoload**
    - `data/i18n.json` **无 `ui.shop_price.*` 键**(原文记 5 键)
    - `data/ui_config.json` **无指向 `shop_price.gd` 的站点**(原文记 19 处)
    ⇒ (a)(b)(c)(d) 四步在码里已成;状态列过时。
    
    ## 判据核对(68 号 B-041 原文)
    | 判据 | 实证 |
    |---|---|
    | 商店/交易面板从代码里退场 | `room_view._show_room_shop()` 函数体=`_keeper_in_room()` → `_on_npc_pressed(k)`(找掌柜说话),无面板调用;`main.gd` 旧路渲染自检自报 `KUSHAN_TRADE RETIRED old panel path gone` 后正常退出 |
    | 背包面板必须保住 | `scripts/room/bag_panel.gd` 在册,`room_view.gd` preload 它、`_show_inventory()` 调 `show_panel`/`show_inventory()` |
    | 动作栏「背包」照常、无交易面板入口 | 见 `menu_items.txt`(真机 `MENU_DUMP=1` 自报)+ `device_room.png` |
    | 全量护栏红项=0 | `bash tools/run_verify_all.sh` ⇒ 见提交信息 |
    
    ## 真机读数(三星 R3CR704Q72V,1.0.383)
    见 `menu_items.txt`/`logcat_full.txt`/`device_room.png`。
    
    ## 新增常驻护栏
    `tools/verify_trade_panel_retired.py`(R.1-R.8):防「面板路线复活」——删件类改动最怕后续悄悄加回入口/autoload,
    而复活不报错,只会让玩家又看到一条已被用户令否定的路。
    
    ## ⚠️ 订正(2026-10-02 二次取证):真机「动作栏自报」**没取到** —— 记为半验,不当通过
    第一次取证(`logcat_full.txt`/`device_room.png`)拍到的其实是**新手引导层**(画面 OCR=「场景移动…[2/6]」+「下一步/跳过此步/一键跳过全部」),
    所以 `KUSHAN_MENUITEM` 自报 **0 条** —— 房间的动作栏那一步根本没走到。这不是「查过了没有商店入口」,而是**没查成**。
    第二次尝试(`logcat_b041b.txt`/`step1_after_launch.png`):
    - `TUTORIAL_AUTOPLAY=0` 只关掉**自动播放**(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 就 return),**引导本身还在**;
    - 想用 OCR 定位「一键跳过全部」再点掉 ⇒ `image_to_data` 分词把整行并成 `下一步跳过此步一键跳过全部`,**没有可点的词框命中** ⇒ 没点成。
    ⇒ 结论:**「无交易面板入口」的代码级判据(护栏 R.6/`main.gd` RETIRED)成立;真机画面级自报仍是缺口**,
    需要先让引导走完(或找到引导态的落盘标记清掉)再取一次。**这一条按「未验不许当通过」记 ⚠️ 半验**。
    
    ## 真机读数(第二轮,仍为引导态)
    - `MENUITEM` 条数 0(引导层挡住)/`shop_price` 0/`SCRIPT ERROR` 0/`Parse Error` 0
    - 引导层 OCR 原文:`ocr_room.txt`
    
    ### TASK-PANEL-SWEEP  (2026-10-02 17:42:22)
    
    # TASK-PANEL-SWEEP —— NPC 卡片「分节单拍」批量目视巡检(用户令:评测必走目视)
    
    ## 做法
    ```
    KUSHAN_RENDER_SECTION=<节名> xvfb-run -a <godot> --path . \
      --render-panel npccard:<cid> --render-output /绝对/路径.png
    ```
    节名(真名,取自 `scripts/npc_panel.gd` 的 `_find("<名>")`):**Dignity / Dress / Life / Story / Wishes**
    (页眉控件 AreaLine / NameLine 不是节、不在可单拍之列)。
    
    ## 本轮实拍(shots/,2 人 × 4 节 = 8 张,全部 `hidden=4 err=0`)
    `c17_*`=丫鬟阿娜(房 5 内院,官户)|`c21_*`=掌柜(巴扎馔摊,商户)
    
    OCR 回读(`ocr_sweep.txt`,多预处理并集;识别噪声保留原样):
    - `c17_Dignity` ⇒ 称呼 / 折算 **×2.00**  `c21_Dignity` ⇒ 称呼「柜上」/ 折算 **×1.00** ⇒ 两行当名分不同
    - `c17_Dress` ⇒ 装束:金莲/手链/默认…  `c21_Dress` ⇒ 装束:金莲/手链/贞操/世界/默认… ⇒ **装束节首次有目视证据**(此前只有自报原文)
    - `c17_Life` ⇒ 1582 启蒙 / 1576 / 富商 …  `c21_Life` ⇒ 1578 启蒙 / 1572 / 广场 … ⇒ 经历节
    - `*_Wishes` ⇒ 心愿(c17 有「西行」一条)
    
    ## 顺带修的护栏
    `tools/verify_panel_sections.py`(进全量回归,PASS 90→**91**):
    P.1 面板有 `isolate_section`;P.2 `main.gd` 在**截图之前**按 `KUSHAN_RENDER_SECTION` 调用;
    P.3 每个 `_find("<名>")` 都有对应 `name = "<名>"` 赋值;P.4 挂了 `_section` 且已命名的节都能被 `_find` 取到。
    —— 写完当场抓到两件事:① 我把心愿节名写成 `Wish`(真名 `Wishes`)⇒ 那一张图根本没单拍;② P.4 初版判据把页眉控件也算节 ⇒ 假阳性。两条都已改。
    
    ### TASK-B15-ESCORT  (2026-10-02 16:23:23)
    
    # TASK-B15-ESCORT —— 68 号 B-15「着装拦截(女性独自离镇被拦下并给提示)」落地 + 真机自检取证
    
    ## 判据(68 号 B-15)
    > 用户令 2026-09-23(L-01/L-02):女性角色独自离镇被巡查拦下并给提示 | 玩家手点路径:女性角色单独出门 → 截图
    
    ## 落地(全参数化、一功能一文件)
    - **新模块** `scripts/law_escort_gate.gd`(纯算不碰 UI):出门前判「律令是否拦」——
      · 性别判据走参数 `dress.apply_gender`(与装扮线同源)
      · 场合判据复用 `scripts/dress_tier.gd` 的 `mode_for_room`(内室名单走 `dress.inner_room_types`)
      · 律令 → 效果 走参数 `laws.escort_effects`(形如 `L-02:street.block_unaccompanied=1`),只与 `law.enabled_ids` 相交时生效
    
  • 版本 1.0.387 开发日志

    构建 1.0.387(code 387)· 2026-10-02 20:23:36 · sha256 5dd8ea2efbfa29a6…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 20:19:04 | 本包:kushan-1.0.387.apk(1.0.387 / code 387,sha256 5dd8ea2efbfa29a6…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-TUTORIAL-WALLET-STEP  (2026-10-02 20:07:35)
    
    # TASK-TUTORIAL-WALLET-STEP —— 引导「钱包基础」步两处真问题(2026-10-02,为取 B-041 画面级证据而查出来)
    
    ## 怎么查出来的
    B-041 要画面级证据「动作栏背包照常/无交易面板入口」,但真机两次都停在**新手引导层**(`[2/6]`)。
    按纪律不当通过、继续攻这道墙,于是:
    ① `TUTORIAL_AUTOPLAY=0` 无效(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 即 return ⇒ **只关自动播放,不禁引导**)
    ② OCR 行级 bbox 找「一键跳过全部」⇒ 分词把整行并成一句,**无可点词框** ⇒ 点不掉
    ③ 本地缓存 `user://tutorial_<wallet16>.json` —— 设备上**不存在**(状态在服务端)
    ④ 服务端状态表 `player_tutorial_state`:`local_0` step_idx=1 已有 finished_at;**`local_1`/`local_59` = step_idx 1、finished_at NULL** ⇒ 卡住的就是这两个
    ⑤ 开 `TUTORIAL_AUTOPLAY=1` 让它自己走:从 `[2/6]` 推进到 **`[5/6]`「钱包基础」** 后**卡死** ——
       `KUSHAN_TUTORIAL_AUTOPLAY step=wallet(idx=4) act=no_keeper_here idx=4->4 ok=false` **每 6 秒重试一次、连续 5 次不前进**
    
    ## 由此查出的两处真问题(都已立项)
    ### 1. 引导文案还指着**已退场的「商铺」面板**(用户令 2026-10-01 交易一律靠对话)
    `data/i18n.json` `ui.tutorial.t014` 原文:
    > 点击右下角 [b]轨迹球[/b] → 「商铺」\n用 [b]KSC[/b] 货币购买商品,完成交易。\n这是西域最常见…
    
    按 B-041 口径(面板已退场、交易一律靠与掌柜对话),这句在**教玩家点一个已经不存在的入口**。
    ### 2. 引导 autoplay 的 wallet 步**走不过去**(`no_keeper_here` 循环)
    `scripts/tutorial_autoplay.gd` 里 wallet 步的真实流程注释写的是「先移到掌柜房 → 再说话买」,
    实际是卡在找不到掌柜、也不换房(`idx=4->4` 原地重试)⇒ **引导 6/6 走不完** ⇒ 直接影响:
    - 新手第一次进游戏推不动(真机体验)
    - 所有「要画面级证据」的真机取证都被引导层挡在外面(B-041 的缺口就是这么来的)
    
    ## 本轮对 B-041 的处置
    B-041 保持 **⚠️ 半验**(代码级判据全过、画面级缺口未补)——**不因「查到别的问题」就把它记成已验**。
    本文件夹是这两处新问题的**证据与立项依据**,与 B-041 的缺口互为因果、分开登记。
    
    ## 原始读数
    - `logcat_b041e.txt`:`KUSHAN_TUTORIAL_AUTOPLAY step=wallet … no_keeper_here` 连续 5 条
    - `ocr_room.txt`:`[5/6] 钱包基础 点击右下角轨迹球—「商铺」用KSC货币购买商品…`
    
    ## 本轮修法与真机复现口径(2026-10-02)
    - **B-048 已改**:`data/i18n.json` 与 `scripts/tutorial_steps.gd`(同一句双真源、护栏要求逐字一致)⇒「点击右下角轨迹球 → 找 [b]掌柜[/b] 说话 / 用 [b]KSC[/b] 货币买下商品,完成交易。」
      基线键表 `evidence/TASK-E20/keymap.tsv` 的「改前字面量」按 C1.3 口径同步(只动含 `t014` 的行)⇒ `verify_client_i18n` 652 条全一致。
    - **B-049 已改**:`scripts/tutorial_autoplay.gd` wallet 步把 `no_keeper_here` 拆成 **`no_keepers_data(sk=0,items=N)`**(店主/商品数据为空)/**`no_keeper_hop(sk=N,items=N,from=房间)`**(数据在、从本房走不到店主房),真机日志一次读出病因。
    
    ## 真机复现(1.0.384,三星 R3CR704Q72V)
    - **B-048 ✅ 目视成立**:装上 384 后该步画面 OCR 读出「…[5/6] 钱包基础 点击右下角轨迹球—**找掌柜说话** 用 KSC 货币**买下商品**,完成交易。」
      ⇒ 包内与屏上都是新文案(旧文案写「→「商铺」用 KSC 货币购买商品」)。证据 `device_384_t014.png`/`ocr_384_t014.txt`(OCR 把「掌柜」认成「学柜」,属识别噪声,句式为新版可辨)。
    
    ### TASK-B041-RETIRED  (2026-10-02 18:45:39)
    
    # TASK-B041-RETIRED —— 面板路线退场 收口取证(2026-10-02)
    
    ## 对账(先查实况,不认状态列)
    68 号 B-041 状态列原写「⬜ 未做(2026-10-01)」,且带一条 2026-10-01 21:44 的订正(说 `scripts/shop_price.gd` 仍在、19 处站点仍在)。
    **本轮实查实况(2026-10-02)**:
    - `scripts/shop_price.gd` **不在磁盘、不在 git 跟踪**
    - `scenes/generated/shops/*.tscn` **0 个**
    - `project.godot` **无 ShopPrice autoload**
    - `data/i18n.json` **无 `ui.shop_price.*` 键**(原文记 5 键)
    - `data/ui_config.json` **无指向 `shop_price.gd` 的站点**(原文记 19 处)
    ⇒ (a)(b)(c)(d) 四步在码里已成;状态列过时。
    
    ## 判据核对(68 号 B-041 原文)
    | 判据 | 实证 |
    |---|---|
    | 商店/交易面板从代码里退场 | `room_view._show_room_shop()` 函数体=`_keeper_in_room()` → `_on_npc_pressed(k)`(找掌柜说话),无面板调用;`main.gd` 旧路渲染自检自报 `KUSHAN_TRADE RETIRED old panel path gone` 后正常退出 |
    | 背包面板必须保住 | `scripts/room/bag_panel.gd` 在册,`room_view.gd` preload 它、`_show_inventory()` 调 `show_panel`/`show_inventory()` |
    | 动作栏「背包」照常、无交易面板入口 | 见 `menu_items.txt`(真机 `MENU_DUMP=1` 自报)+ `device_room.png` |
    | 全量护栏红项=0 | `bash tools/run_verify_all.sh` ⇒ 见提交信息 |
    
    ## 真机读数(三星 R3CR704Q72V,1.0.383)
    见 `menu_items.txt`/`logcat_full.txt`/`device_room.png`。
    
    ## 新增常驻护栏
    `tools/verify_trade_panel_retired.py`(R.1-R.8):防「面板路线复活」——删件类改动最怕后续悄悄加回入口/autoload,
    而复活不报错,只会让玩家又看到一条已被用户令否定的路。
    
    ## ⚠️ 订正(2026-10-02 二次取证):真机「动作栏自报」**没取到** —— 记为半验,不当通过
    第一次取证(`logcat_full.txt`/`device_room.png`)拍到的其实是**新手引导层**(画面 OCR=「场景移动…[2/6]」+「下一步/跳过此步/一键跳过全部」),
    所以 `KUSHAN_MENUITEM` 自报 **0 条** —— 房间的动作栏那一步根本没走到。这不是「查过了没有商店入口」,而是**没查成**。
    第二次尝试(`logcat_b041b.txt`/`step1_after_launch.png`):
    - `TUTORIAL_AUTOPLAY=0` 只关掉**自动播放**(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 就 return),**引导本身还在**;
    - 想用 OCR 定位「一键跳过全部」再点掉 ⇒ `image_to_data` 分词把整行并成 `下一步跳过此步一键跳过全部`,**没有可点的词框命中** ⇒ 没点成。
    ⇒ 结论:**「无交易面板入口」的代码级判据(护栏 R.6/`main.gd` RETIRED)成立;真机画面级自报仍是缺口**,
    需要先让引导走完(或找到引导态的落盘标记清掉)再取一次。**这一条按「未验不许当通过」记 ⚠️ 半验**。
    
    ## 真机读数(第二轮,仍为引导态)
    - `MENUITEM` 条数 0(引导层挡住)/`shop_price` 0/`SCRIPT ERROR` 0/`Parse Error` 0
    - 引导层 OCR 原文:`ocr_room.txt`
    
    ### TASK-PANEL-SWEEP  (2026-10-02 17:42:22)
    
    # TASK-PANEL-SWEEP —— NPC 卡片「分节单拍」批量目视巡检(用户令:评测必走目视)
    
    ## 做法
    ```
    KUSHAN_RENDER_SECTION=<节名> xvfb-run -a <godot> --path . \
      --render-panel npccard:<cid> --render-output /绝对/路径.png
    ```
    节名(真名,取自 `scripts/npc_panel.gd` 的 `_find("<名>")`):**Dignity / Dress / Life / Story / Wishes**
    (页眉控件 AreaLine / NameLine 不是节、不在可单拍之列)。
    
    ## 本轮实拍(shots/,2 人 × 4 节 = 8 张,全部 `hidden=4 err=0`)
    `c17_*`=丫鬟阿娜(房 5 内院,官户)|`c21_*`=掌柜(巴扎馔摊,商户)
    
    OCR 回读(`ocr_sweep.txt`,多预处理并集;识别噪声保留原样):
    - `c17_Dignity` ⇒ 称呼 / 折算 **×2.00**  `c21_Dignity` ⇒ 称呼「柜上」/ 折算 **×1.00** ⇒ 两行当名分不同
    - `c17_Dress` ⇒ 装束:金莲/手链/默认…  `c21_Dress` ⇒ 装束:金莲/手链/贞操/世界/默认… ⇒ **装束节首次有目视证据**(此前只有自报原文)
    - `c17_Life` ⇒ 1582 启蒙 / 1576 / 富商 …  `c21_Life` ⇒ 1578 启蒙 / 1572 / 广场 … ⇒ 经历节
    - `*_Wishes` ⇒ 心愿(c17 有「西行」一条)
    
    ## 顺带修的护栏
    `tools/verify_panel_sections.py`(进全量回归,PASS 90→**91**):
    P.1 面板有 `isolate_section`;P.2 `main.gd` 在**截图之前**按 `KUSHAN_RENDER_SECTION` 调用;
    P.3 每个 `_find("<名>")` 都有对应 `name = "<名>"` 赋值;P.4 挂了 `_section` 且已命名的节都能被 `_find` 取到。
    —— 写完当场抓到两件事:① 我把心愿节名写成 `Wish`(真名 `Wishes`)⇒ 那一张图根本没单拍;② P.4 初版判据把页眉控件也算节 ⇒ 假阳性。两条都已改。
    
    ### TASK-B15-ESCORT  (2026-10-02 16:23:23)
    
    # TASK-B15-ESCORT —— 68 号 B-15「着装拦截(女性独自离镇被拦下并给提示)」落地 + 真机自检取证
    
    ## 判据(68 号 B-15)
    > 用户令 2026-09-23(L-01/L-02):女性角色独自离镇被巡查拦下并给提示 | 玩家手点路径:女性角色单独出门 → 截图
    
    ## 落地(全参数化、一功能一文件)
    - **新模块** `scripts/law_escort_gate.gd`(纯算不碰 UI):出门前判「律令是否拦」——
      · 性别判据走参数 `dress.apply_gender`(与装扮线同源)
      · 场合判据复用 `scripts/dress_tier.gd` 的 `mode_for_room`(内室名单走 `dress.inner_room_types`)
      · 律令 → 效果 走参数 `laws.escort_effects`(形如 `L-02:street.block_unaccompanied=1`),只与 `law.enabled_ids` 相交时生效
    
  • 版本 1.0.386 开发日志

    构建 1.0.386(code 386)· 2026-10-02 19:56:09 · sha256 4f967002a70b56a4…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 19:51:21 | 本包:kushan-1.0.386.apk(1.0.386 / code 386,sha256 4f967002a70b56a4…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-TUTORIAL-WALLET-STEP  (2026-10-02 19:39:54)
    
    # TASK-TUTORIAL-WALLET-STEP —— 引导「钱包基础」步两处真问题(2026-10-02,为取 B-041 画面级证据而查出来)
    
    ## 怎么查出来的
    B-041 要画面级证据「动作栏背包照常/无交易面板入口」,但真机两次都停在**新手引导层**(`[2/6]`)。
    按纪律不当通过、继续攻这道墙,于是:
    ① `TUTORIAL_AUTOPLAY=0` 无效(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 即 return ⇒ **只关自动播放,不禁引导**)
    ② OCR 行级 bbox 找「一键跳过全部」⇒ 分词把整行并成一句,**无可点词框** ⇒ 点不掉
    ③ 本地缓存 `user://tutorial_<wallet16>.json` —— 设备上**不存在**(状态在服务端)
    ④ 服务端状态表 `player_tutorial_state`:`local_0` step_idx=1 已有 finished_at;**`local_1`/`local_59` = step_idx 1、finished_at NULL** ⇒ 卡住的就是这两个
    ⑤ 开 `TUTORIAL_AUTOPLAY=1` 让它自己走:从 `[2/6]` 推进到 **`[5/6]`「钱包基础」** 后**卡死** ——
       `KUSHAN_TUTORIAL_AUTOPLAY step=wallet(idx=4) act=no_keeper_here idx=4->4 ok=false` **每 6 秒重试一次、连续 5 次不前进**
    
    ## 由此查出的两处真问题(都已立项)
    ### 1. 引导文案还指着**已退场的「商铺」面板**(用户令 2026-10-01 交易一律靠对话)
    `data/i18n.json` `ui.tutorial.t014` 原文:
    > 点击右下角 [b]轨迹球[/b] → 「商铺」\n用 [b]KSC[/b] 货币购买商品,完成交易。\n这是西域最常见…
    
    按 B-041 口径(面板已退场、交易一律靠与掌柜对话),这句在**教玩家点一个已经不存在的入口**。
    ### 2. 引导 autoplay 的 wallet 步**走不过去**(`no_keeper_here` 循环)
    `scripts/tutorial_autoplay.gd` 里 wallet 步的真实流程注释写的是「先移到掌柜房 → 再说话买」,
    实际是卡在找不到掌柜、也不换房(`idx=4->4` 原地重试)⇒ **引导 6/6 走不完** ⇒ 直接影响:
    - 新手第一次进游戏推不动(真机体验)
    - 所有「要画面级证据」的真机取证都被引导层挡在外面(B-041 的缺口就是这么来的)
    
    ## 本轮对 B-041 的处置
    B-041 保持 **⚠️ 半验**(代码级判据全过、画面级缺口未补)——**不因「查到别的问题」就把它记成已验**。
    本文件夹是这两处新问题的**证据与立项依据**,与 B-041 的缺口互为因果、分开登记。
    
    ## 原始读数
    - `logcat_b041e.txt`:`KUSHAN_TUTORIAL_AUTOPLAY step=wallet … no_keeper_here` 连续 5 条
    - `ocr_room.txt`:`[5/6] 钱包基础 点击右下角轨迹球—「商铺」用KSC货币购买商品…`
    
    ## 本轮修法与真机复现口径(2026-10-02)
    - **B-048 已改**:`data/i18n.json` 与 `scripts/tutorial_steps.gd`(同一句双真源、护栏要求逐字一致)⇒「点击右下角轨迹球 → 找 [b]掌柜[/b] 说话 / 用 [b]KSC[/b] 货币买下商品,完成交易。」
      基线键表 `evidence/TASK-E20/keymap.tsv` 的「改前字面量」按 C1.3 口径同步(只动含 `t014` 的行)⇒ `verify_client_i18n` 652 条全一致。
    - **B-049 已改**:`scripts/tutorial_autoplay.gd` wallet 步把 `no_keeper_here` 拆成 **`no_keepers_data(sk=0,items=N)`**(店主/商品数据为空)/**`no_keeper_hop(sk=N,items=N,from=房间)`**(数据在、从本房走不到店主房),真机日志一次读出病因。
    
    ## 真机复现(1.0.384,三星 R3CR704Q72V)
    - **B-048 ✅ 目视成立**:装上 384 后该步画面 OCR 读出「…[5/6] 钱包基础 点击右下角轨迹球—**找掌柜说话** 用 KSC 货币**买下商品**,完成交易。」
      ⇒ 包内与屏上都是新文案(旧文案写「→「商铺」用 KSC 货币购买商品」)。证据 `device_384_t014.png`/`ocr_384_t014.txt`(OCR 把「掌柜」认成「学柜」,属识别噪声,句式为新版可辨)。
    
    ### TASK-B041-RETIRED  (2026-10-02 18:45:39)
    
    # TASK-B041-RETIRED —— 面板路线退场 收口取证(2026-10-02)
    
    ## 对账(先查实况,不认状态列)
    68 号 B-041 状态列原写「⬜ 未做(2026-10-01)」,且带一条 2026-10-01 21:44 的订正(说 `scripts/shop_price.gd` 仍在、19 处站点仍在)。
    **本轮实查实况(2026-10-02)**:
    - `scripts/shop_price.gd` **不在磁盘、不在 git 跟踪**
    - `scenes/generated/shops/*.tscn` **0 个**
    - `project.godot` **无 ShopPrice autoload**
    - `data/i18n.json` **无 `ui.shop_price.*` 键**(原文记 5 键)
    - `data/ui_config.json` **无指向 `shop_price.gd` 的站点**(原文记 19 处)
    ⇒ (a)(b)(c)(d) 四步在码里已成;状态列过时。
    
    ## 判据核对(68 号 B-041 原文)
    | 判据 | 实证 |
    |---|---|
    | 商店/交易面板从代码里退场 | `room_view._show_room_shop()` 函数体=`_keeper_in_room()` → `_on_npc_pressed(k)`(找掌柜说话),无面板调用;`main.gd` 旧路渲染自检自报 `KUSHAN_TRADE RETIRED old panel path gone` 后正常退出 |
    | 背包面板必须保住 | `scripts/room/bag_panel.gd` 在册,`room_view.gd` preload 它、`_show_inventory()` 调 `show_panel`/`show_inventory()` |
    | 动作栏「背包」照常、无交易面板入口 | 见 `menu_items.txt`(真机 `MENU_DUMP=1` 自报)+ `device_room.png` |
    | 全量护栏红项=0 | `bash tools/run_verify_all.sh` ⇒ 见提交信息 |
    
    ## 真机读数(三星 R3CR704Q72V,1.0.383)
    见 `menu_items.txt`/`logcat_full.txt`/`device_room.png`。
    
    ## 新增常驻护栏
    `tools/verify_trade_panel_retired.py`(R.1-R.8):防「面板路线复活」——删件类改动最怕后续悄悄加回入口/autoload,
    而复活不报错,只会让玩家又看到一条已被用户令否定的路。
    
    ## ⚠️ 订正(2026-10-02 二次取证):真机「动作栏自报」**没取到** —— 记为半验,不当通过
    第一次取证(`logcat_full.txt`/`device_room.png`)拍到的其实是**新手引导层**(画面 OCR=「场景移动…[2/6]」+「下一步/跳过此步/一键跳过全部」),
    所以 `KUSHAN_MENUITEM` 自报 **0 条** —— 房间的动作栏那一步根本没走到。这不是「查过了没有商店入口」,而是**没查成**。
    第二次尝试(`logcat_b041b.txt`/`step1_after_launch.png`):
    - `TUTORIAL_AUTOPLAY=0` 只关掉**自动播放**(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 就 return),**引导本身还在**;
    - 想用 OCR 定位「一键跳过全部」再点掉 ⇒ `image_to_data` 分词把整行并成 `下一步跳过此步一键跳过全部`,**没有可点的词框命中** ⇒ 没点成。
    ⇒ 结论:**「无交易面板入口」的代码级判据(护栏 R.6/`main.gd` RETIRED)成立;真机画面级自报仍是缺口**,
    需要先让引导走完(或找到引导态的落盘标记清掉)再取一次。**这一条按「未验不许当通过」记 ⚠️ 半验**。
    
    ## 真机读数(第二轮,仍为引导态)
    - `MENUITEM` 条数 0(引导层挡住)/`shop_price` 0/`SCRIPT ERROR` 0/`Parse Error` 0
    - 引导层 OCR 原文:`ocr_room.txt`
    
    ### TASK-PANEL-SWEEP  (2026-10-02 17:42:22)
    
    # TASK-PANEL-SWEEP —— NPC 卡片「分节单拍」批量目视巡检(用户令:评测必走目视)
    
    ## 做法
    ```
    KUSHAN_RENDER_SECTION=<节名> xvfb-run -a <godot> --path . \
      --render-panel npccard:<cid> --render-output /绝对/路径.png
    ```
    节名(真名,取自 `scripts/npc_panel.gd` 的 `_find("<名>")`):**Dignity / Dress / Life / Story / Wishes**
    (页眉控件 AreaLine / NameLine 不是节、不在可单拍之列)。
    
    ## 本轮实拍(shots/,2 人 × 4 节 = 8 张,全部 `hidden=4 err=0`)
    `c17_*`=丫鬟阿娜(房 5 内院,官户)|`c21_*`=掌柜(巴扎馔摊,商户)
    
    OCR 回读(`ocr_sweep.txt`,多预处理并集;识别噪声保留原样):
    - `c17_Dignity` ⇒ 称呼 / 折算 **×2.00**  `c21_Dignity` ⇒ 称呼「柜上」/ 折算 **×1.00** ⇒ 两行当名分不同
    - `c17_Dress` ⇒ 装束:金莲/手链/默认…  `c21_Dress` ⇒ 装束:金莲/手链/贞操/世界/默认… ⇒ **装束节首次有目视证据**(此前只有自报原文)
    - `c17_Life` ⇒ 1582 启蒙 / 1576 / 富商 …  `c21_Life` ⇒ 1578 启蒙 / 1572 / 广场 … ⇒ 经历节
    - `*_Wishes` ⇒ 心愿(c17 有「西行」一条)
    
    ## 顺带修的护栏
    `tools/verify_panel_sections.py`(进全量回归,PASS 90→**91**):
    P.1 面板有 `isolate_section`;P.2 `main.gd` 在**截图之前**按 `KUSHAN_RENDER_SECTION` 调用;
    P.3 每个 `_find("<名>")` 都有对应 `name = "<名>"` 赋值;P.4 挂了 `_section` 且已命名的节都能被 `_find` 取到。
    —— 写完当场抓到两件事:① 我把心愿节名写成 `Wish`(真名 `Wishes`)⇒ 那一张图根本没单拍;② P.4 初版判据把页眉控件也算节 ⇒ 假阳性。两条都已改。
    
    ### TASK-B15-ESCORT  (2026-10-02 16:23:23)
    
    # TASK-B15-ESCORT —— 68 号 B-15「着装拦截(女性独自离镇被拦下并给提示)」落地 + 真机自检取证
    
    ## 判据(68 号 B-15)
    > 用户令 2026-09-23(L-01/L-02):女性角色独自离镇被巡查拦下并给提示 | 玩家手点路径:女性角色单独出门 → 截图
    
    ## 落地(全参数化、一功能一文件)
    - **新模块** `scripts/law_escort_gate.gd`(纯算不碰 UI):出门前判「律令是否拦」——
      · 性别判据走参数 `dress.apply_gender`(与装扮线同源)
      · 场合判据复用 `scripts/dress_tier.gd` 的 `mode_for_room`(内室名单走 `dress.inner_room_types`)
      · 律令 → 效果 走参数 `laws.escort_effects`(形如 `L-02:street.block_unaccompanied=1`),只与 `law.enabled_ids` 相交时生效
    
  • 版本 1.0.385 开发日志

    构建 1.0.385(code 385)· 2026-10-02 19:37:26 · sha256 91000ab0369bf314…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 19:32:50 | 本包:kushan-1.0.385.apk(1.0.385 / code 385,sha256 91000ab0369bf314…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-TUTORIAL-WALLET-STEP  (2026-10-02 19:20:10)
    
    # TASK-TUTORIAL-WALLET-STEP —— 引导「钱包基础」步两处真问题(2026-10-02,为取 B-041 画面级证据而查出来)
    
    ## 怎么查出来的
    B-041 要画面级证据「动作栏背包照常/无交易面板入口」,但真机两次都停在**新手引导层**(`[2/6]`)。
    按纪律不当通过、继续攻这道墙,于是:
    ① `TUTORIAL_AUTOPLAY=0` 无效(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 即 return ⇒ **只关自动播放,不禁引导**)
    ② OCR 行级 bbox 找「一键跳过全部」⇒ 分词把整行并成一句,**无可点词框** ⇒ 点不掉
    ③ 本地缓存 `user://tutorial_<wallet16>.json` —— 设备上**不存在**(状态在服务端)
    ④ 服务端状态表 `player_tutorial_state`:`local_0` step_idx=1 已有 finished_at;**`local_1`/`local_59` = step_idx 1、finished_at NULL** ⇒ 卡住的就是这两个
    ⑤ 开 `TUTORIAL_AUTOPLAY=1` 让它自己走:从 `[2/6]` 推进到 **`[5/6]`「钱包基础」** 后**卡死** ——
       `KUSHAN_TUTORIAL_AUTOPLAY step=wallet(idx=4) act=no_keeper_here idx=4->4 ok=false` **每 6 秒重试一次、连续 5 次不前进**
    
    ## 由此查出的两处真问题(都已立项)
    ### 1. 引导文案还指着**已退场的「商铺」面板**(用户令 2026-10-01 交易一律靠对话)
    `data/i18n.json` `ui.tutorial.t014` 原文:
    > 点击右下角 [b]轨迹球[/b] → 「商铺」\n用 [b]KSC[/b] 货币购买商品,完成交易。\n这是西域最常见…
    
    按 B-041 口径(面板已退场、交易一律靠与掌柜对话),这句在**教玩家点一个已经不存在的入口**。
    ### 2. 引导 autoplay 的 wallet 步**走不过去**(`no_keeper_here` 循环)
    `scripts/tutorial_autoplay.gd` 里 wallet 步的真实流程注释写的是「先移到掌柜房 → 再说话买」,
    实际是卡在找不到掌柜、也不换房(`idx=4->4` 原地重试)⇒ **引导 6/6 走不完** ⇒ 直接影响:
    - 新手第一次进游戏推不动(真机体验)
    - 所有「要画面级证据」的真机取证都被引导层挡在外面(B-041 的缺口就是这么来的)
    
    ## 本轮对 B-041 的处置
    B-041 保持 **⚠️ 半验**(代码级判据全过、画面级缺口未补)——**不因「查到别的问题」就把它记成已验**。
    本文件夹是这两处新问题的**证据与立项依据**,与 B-041 的缺口互为因果、分开登记。
    
    ## 原始读数
    - `logcat_b041e.txt`:`KUSHAN_TUTORIAL_AUTOPLAY step=wallet … no_keeper_here` 连续 5 条
    - `ocr_room.txt`:`[5/6] 钱包基础 点击右下角轨迹球—「商铺」用KSC货币购买商品…`
    
    ## 本轮修法与真机复现口径(2026-10-02)
    - **B-048 已改**:`data/i18n.json` 与 `scripts/tutorial_steps.gd`(同一句双真源、护栏要求逐字一致)⇒「点击右下角轨迹球 → 找 [b]掌柜[/b] 说话 / 用 [b]KSC[/b] 货币买下商品,完成交易。」
      基线键表 `evidence/TASK-E20/keymap.tsv` 的「改前字面量」按 C1.3 口径同步(只动含 `t014` 的行)⇒ `verify_client_i18n` 652 条全一致。
    - **B-049 已改**:`scripts/tutorial_autoplay.gd` wallet 步把 `no_keeper_here` 拆成 **`no_keepers_data(sk=0,items=N)`**(店主/商品数据为空)/**`no_keeper_hop(sk=N,items=N,from=房间)`**(数据在、从本房走不到店主房),真机日志一次读出病因。
    
    ## 真机复现(1.0.384,三星 R3CR704Q72V)
    - **B-048 ✅ 目视成立**:装上 384 后该步画面 OCR 读出「…[5/6] 钱包基础 点击右下角轨迹球—**找掌柜说话** 用 KSC 货币**买下商品**,完成交易。」
      ⇒ 包内与屏上都是新文案(旧文案写「→「商铺」用 KSC 货币购买商品」)。证据 `device_384_t014.png`/`ocr_384_t014.txt`(OCR 把「掌柜」认成「学柜」,属识别噪声,句式为新版可辨)。
    
    ### TASK-B041-RETIRED  (2026-10-02 18:45:39)
    
    # TASK-B041-RETIRED —— 面板路线退场 收口取证(2026-10-02)
    
    ## 对账(先查实况,不认状态列)
    68 号 B-041 状态列原写「⬜ 未做(2026-10-01)」,且带一条 2026-10-01 21:44 的订正(说 `scripts/shop_price.gd` 仍在、19 处站点仍在)。
    **本轮实查实况(2026-10-02)**:
    - `scripts/shop_price.gd` **不在磁盘、不在 git 跟踪**
    - `scenes/generated/shops/*.tscn` **0 个**
    - `project.godot` **无 ShopPrice autoload**
    - `data/i18n.json` **无 `ui.shop_price.*` 键**(原文记 5 键)
    - `data/ui_config.json` **无指向 `shop_price.gd` 的站点**(原文记 19 处)
    ⇒ (a)(b)(c)(d) 四步在码里已成;状态列过时。
    
    ## 判据核对(68 号 B-041 原文)
    | 判据 | 实证 |
    |---|---|
    | 商店/交易面板从代码里退场 | `room_view._show_room_shop()` 函数体=`_keeper_in_room()` → `_on_npc_pressed(k)`(找掌柜说话),无面板调用;`main.gd` 旧路渲染自检自报 `KUSHAN_TRADE RETIRED old panel path gone` 后正常退出 |
    | 背包面板必须保住 | `scripts/room/bag_panel.gd` 在册,`room_view.gd` preload 它、`_show_inventory()` 调 `show_panel`/`show_inventory()` |
    | 动作栏「背包」照常、无交易面板入口 | 见 `menu_items.txt`(真机 `MENU_DUMP=1` 自报)+ `device_room.png` |
    | 全量护栏红项=0 | `bash tools/run_verify_all.sh` ⇒ 见提交信息 |
    
    ## 真机读数(三星 R3CR704Q72V,1.0.383)
    见 `menu_items.txt`/`logcat_full.txt`/`device_room.png`。
    
    ## 新增常驻护栏
    `tools/verify_trade_panel_retired.py`(R.1-R.8):防「面板路线复活」——删件类改动最怕后续悄悄加回入口/autoload,
    而复活不报错,只会让玩家又看到一条已被用户令否定的路。
    
    ## ⚠️ 订正(2026-10-02 二次取证):真机「动作栏自报」**没取到** —— 记为半验,不当通过
    第一次取证(`logcat_full.txt`/`device_room.png`)拍到的其实是**新手引导层**(画面 OCR=「场景移动…[2/6]」+「下一步/跳过此步/一键跳过全部」),
    所以 `KUSHAN_MENUITEM` 自报 **0 条** —— 房间的动作栏那一步根本没走到。这不是「查过了没有商店入口」,而是**没查成**。
    第二次尝试(`logcat_b041b.txt`/`step1_after_launch.png`):
    - `TUTORIAL_AUTOPLAY=0` 只关掉**自动播放**(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 就 return),**引导本身还在**;
    - 想用 OCR 定位「一键跳过全部」再点掉 ⇒ `image_to_data` 分词把整行并成 `下一步跳过此步一键跳过全部`,**没有可点的词框命中** ⇒ 没点成。
    ⇒ 结论:**「无交易面板入口」的代码级判据(护栏 R.6/`main.gd` RETIRED)成立;真机画面级自报仍是缺口**,
    需要先让引导走完(或找到引导态的落盘标记清掉)再取一次。**这一条按「未验不许当通过」记 ⚠️ 半验**。
    
    ## 真机读数(第二轮,仍为引导态)
    - `MENUITEM` 条数 0(引导层挡住)/`shop_price` 0/`SCRIPT ERROR` 0/`Parse Error` 0
    - 引导层 OCR 原文:`ocr_room.txt`
    
    ### TASK-PANEL-SWEEP  (2026-10-02 17:42:22)
    
    # TASK-PANEL-SWEEP —— NPC 卡片「分节单拍」批量目视巡检(用户令:评测必走目视)
    
    ## 做法
    ```
    KUSHAN_RENDER_SECTION=<节名> xvfb-run -a <godot> --path . \
      --render-panel npccard:<cid> --render-output /绝对/路径.png
    ```
    节名(真名,取自 `scripts/npc_panel.gd` 的 `_find("<名>")`):**Dignity / Dress / Life / Story / Wishes**
    (页眉控件 AreaLine / NameLine 不是节、不在可单拍之列)。
    
    ## 本轮实拍(shots/,2 人 × 4 节 = 8 张,全部 `hidden=4 err=0`)
    `c17_*`=丫鬟阿娜(房 5 内院,官户)|`c21_*`=掌柜(巴扎馔摊,商户)
    
    OCR 回读(`ocr_sweep.txt`,多预处理并集;识别噪声保留原样):
    - `c17_Dignity` ⇒ 称呼 / 折算 **×2.00**  `c21_Dignity` ⇒ 称呼「柜上」/ 折算 **×1.00** ⇒ 两行当名分不同
    - `c17_Dress` ⇒ 装束:金莲/手链/默认…  `c21_Dress` ⇒ 装束:金莲/手链/贞操/世界/默认… ⇒ **装束节首次有目视证据**(此前只有自报原文)
    - `c17_Life` ⇒ 1582 启蒙 / 1576 / 富商 …  `c21_Life` ⇒ 1578 启蒙 / 1572 / 广场 … ⇒ 经历节
    - `*_Wishes` ⇒ 心愿(c17 有「西行」一条)
    
    ## 顺带修的护栏
    `tools/verify_panel_sections.py`(进全量回归,PASS 90→**91**):
    P.1 面板有 `isolate_section`;P.2 `main.gd` 在**截图之前**按 `KUSHAN_RENDER_SECTION` 调用;
    P.3 每个 `_find("<名>")` 都有对应 `name = "<名>"` 赋值;P.4 挂了 `_section` 且已命名的节都能被 `_find` 取到。
    —— 写完当场抓到两件事:① 我把心愿节名写成 `Wish`(真名 `Wishes`)⇒ 那一张图根本没单拍;② P.4 初版判据把页眉控件也算节 ⇒ 假阳性。两条都已改。
    
    ### TASK-B15-ESCORT  (2026-10-02 16:23:23)
    
    # TASK-B15-ESCORT —— 68 号 B-15「着装拦截(女性独自离镇被拦下并给提示)」落地 + 真机自检取证
    
    ## 判据(68 号 B-15)
    > 用户令 2026-09-23(L-01/L-02):女性角色独自离镇被巡查拦下并给提示 | 玩家手点路径:女性角色单独出门 → 截图
    
    ## 落地(全参数化、一功能一文件)
    - **新模块** `scripts/law_escort_gate.gd`(纯算不碰 UI):出门前判「律令是否拦」——
      · 性别判据走参数 `dress.apply_gender`(与装扮线同源)
      · 场合判据复用 `scripts/dress_tier.gd` 的 `mode_for_room`(内室名单走 `dress.inner_room_types`)
      · 律令 → 效果 走参数 `laws.escort_effects`(形如 `L-02:street.block_unaccompanied=1`),只与 `law.enabled_ids` 相交时生效
    
  • 版本 1.0.384 开发日志

    构建 1.0.384(code 384)· 2026-10-02 19:15:28 · sha256 ffefd21b349ec249…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 19:11:00 | 本包:kushan-1.0.384.apk(1.0.384 / code 384,sha256 ffefd21b349ec249…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-TUTORIAL-WALLET-STEP  (2026-10-02 19:05:05)
    
    # TASK-TUTORIAL-WALLET-STEP —— 引导「钱包基础」步两处真问题(2026-10-02,为取 B-041 画面级证据而查出来)
    
    ## 怎么查出来的
    B-041 要画面级证据「动作栏背包照常/无交易面板入口」,但真机两次都停在**新手引导层**(`[2/6]`)。
    按纪律不当通过、继续攻这道墙,于是:
    ① `TUTORIAL_AUTOPLAY=0` 无效(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 即 return ⇒ **只关自动播放,不禁引导**)
    ② OCR 行级 bbox 找「一键跳过全部」⇒ 分词把整行并成一句,**无可点词框** ⇒ 点不掉
    ③ 本地缓存 `user://tutorial_<wallet16>.json` —— 设备上**不存在**(状态在服务端)
    ④ 服务端状态表 `player_tutorial_state`:`local_0` step_idx=1 已有 finished_at;**`local_1`/`local_59` = step_idx 1、finished_at NULL** ⇒ 卡住的就是这两个
    ⑤ 开 `TUTORIAL_AUTOPLAY=1` 让它自己走:从 `[2/6]` 推进到 **`[5/6]`「钱包基础」** 后**卡死** ——
       `KUSHAN_TUTORIAL_AUTOPLAY step=wallet(idx=4) act=no_keeper_here idx=4->4 ok=false` **每 6 秒重试一次、连续 5 次不前进**
    
    ## 由此查出的两处真问题(都已立项)
    ### 1. 引导文案还指着**已退场的「商铺」面板**(用户令 2026-10-01 交易一律靠对话)
    `data/i18n.json` `ui.tutorial.t014` 原文:
    > 点击右下角 [b]轨迹球[/b] → 「商铺」\n用 [b]KSC[/b] 货币购买商品,完成交易。\n这是西域最常见…
    
    按 B-041 口径(面板已退场、交易一律靠与掌柜对话),这句在**教玩家点一个已经不存在的入口**。
    ### 2. 引导 autoplay 的 wallet 步**走不过去**(`no_keeper_here` 循环)
    `scripts/tutorial_autoplay.gd` 里 wallet 步的真实流程注释写的是「先移到掌柜房 → 再说话买」,
    实际是卡在找不到掌柜、也不换房(`idx=4->4` 原地重试)⇒ **引导 6/6 走不完** ⇒ 直接影响:
    - 新手第一次进游戏推不动(真机体验)
    - 所有「要画面级证据」的真机取证都被引导层挡在外面(B-041 的缺口就是这么来的)
    
    ## 本轮对 B-041 的处置
    B-041 保持 **⚠️ 半验**(代码级判据全过、画面级缺口未补)——**不因「查到别的问题」就把它记成已验**。
    本文件夹是这两处新问题的**证据与立项依据**,与 B-041 的缺口互为因果、分开登记。
    
    ## 原始读数
    - `logcat_b041e.txt`:`KUSHAN_TUTORIAL_AUTOPLAY step=wallet … no_keeper_here` 连续 5 条
    - `ocr_room.txt`:`[5/6] 钱包基础 点击右下角轨迹球—「商铺」用KSC货币购买商品…`
    
    ## 本轮修法与真机复现口径(2026-10-02)
    - **B-048 已改**:`data/i18n.json` 与 `scripts/tutorial_steps.gd`(同一句双真源、护栏要求逐字一致)⇒「点击右下角轨迹球 → 找 [b]掌柜[/b] 说话 / 用 [b]KSC[/b] 货币买下商品,完成交易。」
      基线键表 `evidence/TASK-E20/keymap.tsv` 的「改前字面量」按 C1.3 口径同步(只动含 `t014` 的行)⇒ `verify_client_i18n` 652 条全一致。
    - **B-049 已改**:`scripts/tutorial_autoplay.gd` wallet 步把 `no_keeper_here` 拆成 **`no_keepers_data(sk=0,items=N)`**(店主/商品数据为空)/**`no_keeper_hop(sk=N,items=N,from=房间)`**(数据在、从本房走不到店主房),真机日志一次读出病因。
    
    ### TASK-B041-RETIRED  (2026-10-02 18:45:39)
    
    # TASK-B041-RETIRED —— 面板路线退场 收口取证(2026-10-02)
    
    ## 对账(先查实况,不认状态列)
    68 号 B-041 状态列原写「⬜ 未做(2026-10-01)」,且带一条 2026-10-01 21:44 的订正(说 `scripts/shop_price.gd` 仍在、19 处站点仍在)。
    **本轮实查实况(2026-10-02)**:
    - `scripts/shop_price.gd` **不在磁盘、不在 git 跟踪**
    - `scenes/generated/shops/*.tscn` **0 个**
    - `project.godot` **无 ShopPrice autoload**
    - `data/i18n.json` **无 `ui.shop_price.*` 键**(原文记 5 键)
    - `data/ui_config.json` **无指向 `shop_price.gd` 的站点**(原文记 19 处)
    ⇒ (a)(b)(c)(d) 四步在码里已成;状态列过时。
    
    ## 判据核对(68 号 B-041 原文)
    | 判据 | 实证 |
    |---|---|
    | 商店/交易面板从代码里退场 | `room_view._show_room_shop()` 函数体=`_keeper_in_room()` → `_on_npc_pressed(k)`(找掌柜说话),无面板调用;`main.gd` 旧路渲染自检自报 `KUSHAN_TRADE RETIRED old panel path gone` 后正常退出 |
    | 背包面板必须保住 | `scripts/room/bag_panel.gd` 在册,`room_view.gd` preload 它、`_show_inventory()` 调 `show_panel`/`show_inventory()` |
    | 动作栏「背包」照常、无交易面板入口 | 见 `menu_items.txt`(真机 `MENU_DUMP=1` 自报)+ `device_room.png` |
    | 全量护栏红项=0 | `bash tools/run_verify_all.sh` ⇒ 见提交信息 |
    
    ## 真机读数(三星 R3CR704Q72V,1.0.383)
    见 `menu_items.txt`/`logcat_full.txt`/`device_room.png`。
    
    ## 新增常驻护栏
    `tools/verify_trade_panel_retired.py`(R.1-R.8):防「面板路线复活」——删件类改动最怕后续悄悄加回入口/autoload,
    而复活不报错,只会让玩家又看到一条已被用户令否定的路。
    
    ## ⚠️ 订正(2026-10-02 二次取证):真机「动作栏自报」**没取到** —— 记为半验,不当通过
    第一次取证(`logcat_full.txt`/`device_room.png`)拍到的其实是**新手引导层**(画面 OCR=「场景移动…[2/6]」+「下一步/跳过此步/一键跳过全部」),
    所以 `KUSHAN_MENUITEM` 自报 **0 条** —— 房间的动作栏那一步根本没走到。这不是「查过了没有商店入口」,而是**没查成**。
    第二次尝试(`logcat_b041b.txt`/`step1_after_launch.png`):
    - `TUTORIAL_AUTOPLAY=0` 只关掉**自动播放**(`tutorial_autoplay.gd:_ready()` 里 `cfg_value("TUTORIAL_AUTOPLAY") != "1"` 就 return),**引导本身还在**;
    - 想用 OCR 定位「一键跳过全部」再点掉 ⇒ `image_to_data` 分词把整行并成 `下一步跳过此步一键跳过全部`,**没有可点的词框命中** ⇒ 没点成。
    ⇒ 结论:**「无交易面板入口」的代码级判据(护栏 R.6/`main.gd` RETIRED)成立;真机画面级自报仍是缺口**,
    需要先让引导走完(或找到引导态的落盘标记清掉)再取一次。**这一条按「未验不许当通过」记 ⚠️ 半验**。
    
    ## 真机读数(第二轮,仍为引导态)
    - `MENUITEM` 条数 0(引导层挡住)/`shop_price` 0/`SCRIPT ERROR` 0/`Parse Error` 0
    - 引导层 OCR 原文:`ocr_room.txt`
    
    ### TASK-PANEL-SWEEP  (2026-10-02 17:42:22)
    
    # TASK-PANEL-SWEEP —— NPC 卡片「分节单拍」批量目视巡检(用户令:评测必走目视)
    
    ## 做法
    ```
    KUSHAN_RENDER_SECTION=<节名> xvfb-run -a <godot> --path . \
      --render-panel npccard:<cid> --render-output /绝对/路径.png
    ```
    节名(真名,取自 `scripts/npc_panel.gd` 的 `_find("<名>")`):**Dignity / Dress / Life / Story / Wishes**
    (页眉控件 AreaLine / NameLine 不是节、不在可单拍之列)。
    
    ## 本轮实拍(shots/,2 人 × 4 节 = 8 张,全部 `hidden=4 err=0`)
    `c17_*`=丫鬟阿娜(房 5 内院,官户)|`c21_*`=掌柜(巴扎馔摊,商户)
    
    OCR 回读(`ocr_sweep.txt`,多预处理并集;识别噪声保留原样):
    - `c17_Dignity` ⇒ 称呼 / 折算 **×2.00**  `c21_Dignity` ⇒ 称呼「柜上」/ 折算 **×1.00** ⇒ 两行当名分不同
    - `c17_Dress` ⇒ 装束:金莲/手链/默认…  `c21_Dress` ⇒ 装束:金莲/手链/贞操/世界/默认… ⇒ **装束节首次有目视证据**(此前只有自报原文)
    - `c17_Life` ⇒ 1582 启蒙 / 1576 / 富商 …  `c21_Life` ⇒ 1578 启蒙 / 1572 / 广场 … ⇒ 经历节
    - `*_Wishes` ⇒ 心愿(c17 有「西行」一条)
    
    ## 顺带修的护栏
    `tools/verify_panel_sections.py`(进全量回归,PASS 90→**91**):
    P.1 面板有 `isolate_section`;P.2 `main.gd` 在**截图之前**按 `KUSHAN_RENDER_SECTION` 调用;
    P.3 每个 `_find("<名>")` 都有对应 `name = "<名>"` 赋值;P.4 挂了 `_section` 且已命名的节都能被 `_find` 取到。
    —— 写完当场抓到两件事:① 我把心愿节名写成 `Wish`(真名 `Wishes`)⇒ 那一张图根本没单拍;② P.4 初版判据把页眉控件也算节 ⇒ 假阳性。两条都已改。
    
    ### TASK-B15-ESCORT  (2026-10-02 16:23:23)
    
    # TASK-B15-ESCORT —— 68 号 B-15「着装拦截(女性独自离镇被拦下并给提示)」落地 + 真机自检取证
    
    ## 判据(68 号 B-15)
    > 用户令 2026-09-23(L-01/L-02):女性角色独自离镇被巡查拦下并给提示 | 玩家手点路径:女性角色单独出门 → 截图
    
    ## 落地(全参数化、一功能一文件)
    - **新模块** `scripts/law_escort_gate.gd`(纯算不碰 UI):出门前判「律令是否拦」——
      · 性别判据走参数 `dress.apply_gender`(与装扮线同源)
      · 场合判据复用 `scripts/dress_tier.gd` 的 `mode_for_room`(内室名单走 `dress.inner_room_types`)
      · 律令 → 效果 走参数 `laws.escort_effects`(形如 `L-02:street.block_unaccompanied=1`),只与 `law.enabled_ids` 相交时生效
      · 同行判据=同房有无男性(`DataStore.npcs_in_room`)
      · 律令名读 `data/laws_xiangfei.json`(真源,不编)
    - **接线**:`scripts/room_view.gd` 的 `_move_to`(房间页唯一出门回调)——被拦则**原地不动**+弹提示;
      自报 `KUSHAN_ESCORT blocked=… law=… why=…`(ASCII)
    
  • 版本 1.0.383 开发日志

    构建 1.0.383(code 383)· 2026-10-02 18:03:54 · sha256 db227e9405379aba…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 17:58:55 | 本包:kushan-1.0.383.apk(1.0.383 / code 383,sha256 db227e9405379aba…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-PANEL-SWEEP  (2026-10-02 17:42:22)
    
    # TASK-PANEL-SWEEP —— NPC 卡片「分节单拍」批量目视巡检(用户令:评测必走目视)
    
    ## 做法
    ```
    KUSHAN_RENDER_SECTION=<节名> xvfb-run -a <godot> --path . \
      --render-panel npccard:<cid> --render-output /绝对/路径.png
    ```
    节名(真名,取自 `scripts/npc_panel.gd` 的 `_find("<名>")`):**Dignity / Dress / Life / Story / Wishes**
    (页眉控件 AreaLine / NameLine 不是节、不在可单拍之列)。
    
    ## 本轮实拍(shots/,2 人 × 4 节 = 8 张,全部 `hidden=4 err=0`)
    `c17_*`=丫鬟阿娜(房 5 内院,官户)|`c21_*`=掌柜(巴扎馔摊,商户)
    
    OCR 回读(`ocr_sweep.txt`,多预处理并集;识别噪声保留原样):
    - `c17_Dignity` ⇒ 称呼 / 折算 **×2.00**  `c21_Dignity` ⇒ 称呼「柜上」/ 折算 **×1.00** ⇒ 两行当名分不同
    - `c17_Dress` ⇒ 装束:金莲/手链/默认…  `c21_Dress` ⇒ 装束:金莲/手链/贞操/世界/默认… ⇒ **装束节首次有目视证据**(此前只有自报原文)
    - `c17_Life` ⇒ 1582 启蒙 / 1576 / 富商 …  `c21_Life` ⇒ 1578 启蒙 / 1572 / 广场 … ⇒ 经历节
    - `*_Wishes` ⇒ 心愿(c17 有「西行」一条)
    
    ## 顺带修的护栏
    `tools/verify_panel_sections.py`(进全量回归,PASS 90→**91**):
    P.1 面板有 `isolate_section`;P.2 `main.gd` 在**截图之前**按 `KUSHAN_RENDER_SECTION` 调用;
    P.3 每个 `_find("<名>")` 都有对应 `name = "<名>"` 赋值;P.4 挂了 `_section` 且已命名的节都能被 `_find` 取到。
    —— 写完当场抓到两件事:① 我把心愿节名写成 `Wish`(真名 `Wishes`)⇒ 那一张图根本没单拍;② P.4 初版判据把页眉控件也算节 ⇒ 假阳性。两条都已改。
    
    ### TASK-B15-ESCORT  (2026-10-02 16:23:23)
    
    # TASK-B15-ESCORT —— 68 号 B-15「着装拦截(女性独自离镇被拦下并给提示)」落地 + 真机自检取证
    
    ## 判据(68 号 B-15)
    > 用户令 2026-09-23(L-01/L-02):女性角色独自离镇被巡查拦下并给提示 | 玩家手点路径:女性角色单独出门 → 截图
    
    ## 落地(全参数化、一功能一文件)
    - **新模块** `scripts/law_escort_gate.gd`(纯算不碰 UI):出门前判「律令是否拦」——
      · 性别判据走参数 `dress.apply_gender`(与装扮线同源)
      · 场合判据复用 `scripts/dress_tier.gd` 的 `mode_for_room`(内室名单走 `dress.inner_room_types`)
      · 律令 → 效果 走参数 `laws.escort_effects`(形如 `L-02:street.block_unaccompanied=1`),只与 `law.enabled_ids` 相交时生效
      · 同行判据=同房有无男性(`DataStore.npcs_in_room`)
      · 律令名读 `data/laws_xiangfei.json`(真源,不编)
    - **接线**:`scripts/room_view.gd` 的 `_move_to`(房间页唯一出门回调)——被拦则**原地不动**+弹提示;
      自报 `KUSHAN_ESCORT blocked=… law=… why=…`(ASCII)
    - **文案**:`ui.room.escort_blocked`「巡查拦下:%s」/`ui.room.escort_by`「依 %s %s」(I18n,键已入 `_meta.keymap`)
    - **受控自检**(真机可得证):cfg `ESCORT_QA=selftest` + `ESCORT_QA_GENDER=<male|female>` + `ESCORT_QA_ROOM=<房号>`
      ⇒ 进房后逐个出口跑同一个 `check()` 并自报;被拦的那条会真弹提示(同一 UI 路径)。
      为什么要它:**真机登录身份来自端上存档/服务端 wallet→player 映射**,端上建女号之前无法手点验证;
      自检只注入「判据输入」(性别/房间),走的是同一份代码与真数据。
    
    ## 真机实证(1.0.380 · 三星 R3CR704Q72V · 原文见 selftest_female.txt / selftest_male.txt)
    - **女眷口径**(`ESCORT_QA_ROOM=113` 村口水井,房内无男):
      `to=583 type=street mode=street blocked=true law=L-02` / `to=660 alley blocked=true` / `to=440 road blocked=true`
    - **男口径对照**:`to=583/660/440 blocked=false why=gender=male`
    ⇒ 拦与不拦**只由性别×场合×同行×律令决定**,三条街面/巷/路出口全拦,同房有男性时不拦(参数 `escort.qa_male_present` 可对照)。
    - 冒烟:进房 1 条、SCRIPT ERROR=0、缺参=0;出包 1.0.380 三链已同步 nw。
    
    ## 边界(不谎报)
    1. **「玩家手点路径」这一半未验**:真机登录身份来自端上存档(`GameState.setup_from_world` 取世界 players/本地存档),
       我建的 QA 女号(characters id=347「验身女」+钱包)未被登录采纳 ⇒ 未跑成「女号手点出门」。
       闸已挂在真实出门回调上,**端上建女号(主菜单建号表单支持 female)后即可手点复现**。
    2. **提示语的截图未抓到**:`_toast_msg` 是瞬时提示(约 2–3 秒),我按 6 秒/45 秒采样都错过 ⇒
       本项目视只到「门没开出去 + 房间页正常」;提示文案本身由 I18n 键+同一调用路径保证。
       (若要抓到:把提示驻留时间做成参数,或进门后立刻连拍。)
    3. **QA 女号未清**:`characters id=347`+其钱包仍在库里(INSERT-only 纪律,未删);若要清待人一句。
    
    ### TASK-D14-AUDIT  (2026-10-02 15:12:02)
    
    # TASK-D14-AUDIT —— 68 号 D-14「改动留痕可回滚」落地 + 回滚演练
    
    ## 判据(68 号 D-14)
    > 每次改动有审计记录(谁/何时/哪场所/改前改后),可回滚 | 审计回读 + 回滚演练
    
    ## 落地(写库仍只走 `tools/wdb.sh`,闸门=系列备份)
    - **审计表**:`docs/migrations/20261002a_domain_rule_audit.sql` → `domain_rule_audit`
      (只追加:scope_kind / scope_id / actor / action / reason / before_json / after_json / src_audit_id / created_at)
    - **唯一写入端**:`tools/domain_rule_write.py`
      - `set`:读改前 → 同事务 upsert `domain_authority` + **追加审计行**(不会出现「改了没留痕」)
      - `rollback --audit-id K`:把 K 的 `before_json` 写回,并**再追加一条** action=rollback 的审计行(回滚本身留痕)
      - `history`:按场所回读全部改动(谁/何时/改前改后)
    - **常驻护栏**:`tools/verify_domain_audit.py`(D.1 迁移件在册 · D.2 写 `domain_authority` 只许本写入端 ·
      D.3 规则写与审计 INSERT 同文件 · D.4 审计列面含「谁/哪场所/改前改后」)⇒ 已进 `run_verify_all.sh` 自动集
    
    ## 演练实证(原文见 `drill.txt`;真库真跑,地点=room 670 巴扎馕摊,主人=掌柜 21)
    1. **改前**:无规则行、审计 0 行
    2. **改①**(内室档 3):`[审计] id=1 action=set actor=QA-D14 before=None after={"inner":{"xiangfei_tier":3}}`
    3. **改②**(内室档 5 + 禁用乳环):`[审计] id=2 … before={"inner":{"xiangfei_tier":3}} after={…5…}`
       ⇒ **改前改后都留下**,两笔改动可逐笔追溯
    4. **回滚①**(回到改②之前):`[回滚后] rules={"inner":{"xiangfei_tier":3}}`=**字节级还原改①的值**;
       `[审计] id=3 action=rollback src_audit_id=2`
    5. **回滚②**(回到「本无规则」):`rules=None`(置空、**不做 DELETE**);`[审计] id=4 action=rollback src_audit_id=1`
    6. **审计回读**:4 行齐全(谁=actor / 何时=created_at / 哪场所=scope_kind+scope_id / 改前改后=before/after_json)
    7. **护栏**:`VERIFY domain_audit: PASS`(D.1/D.2/D.3/D.4 全绿)
    
    ## 边界与口径(不谎报)
    - **不做 DELETE**:回滚到「本无规则」时把 `rules_json` 置空(语义=无规则),避免删数据;表内留一行空规则。
    - 演练留在 `domain_rule_audit` 的 4 行是**真实操作痕迹**(actor=QA-D14,reason=D-14 演练①②/回滚①②),
      按「只追加」纪律**不删**;`domain_authority` 中 room 670 现为 `rules_json=NULL`(=无规则,玩家不可见)。
    - **本轮无客户端改动** ⇒ 不重出包(1.0.374 仍为最新)。
    - 依赖模型链的「对话改世界」入口(B-19/B-26)仍挂外部额度;一旦通电,写入端就是本工具,**留痕/回滚自动生效**。
    
    ### TASK-B18-PRECEDENCE  (2026-10-02 14:56:53)
    
    # TASK-B18-PRECEDENCE —— 68 号 B-18「三级优先级逐层兜底」实现 + 组合实证
    
    ## 判据(68 号 B-18)
    > 主人规则 > 地方律令 > 世界默认,逐层兜底 | 三种组合各截一张图对照
    
    ## 实现(真源=`data/domain_rules.json` 的 `precedence` 原文,一级没规定才落下一级)
    | 层 | 口径 | 本轮落地 |
    |---|---|---|
    | **L1 场所主人自定义规则** | `domain_rules.json`:伯克/店主人/一家之主/团队头目对自己地盘说了算(含女性着装) | 读**房间 `attrs.dress`**(客户端可得的口袋);`xiangfei_tier` 可覆盖档位、`ban_pieces`/`add_pieces` 出附加说明 |
    | **L2 地方律令** | `data/laws_xiangfei.json`(L-01 着装令 / L-02 同行令…) | 生效编号 × 参数 `laws.dress_effects`(结构化映射,形如 `L-01:street.enforce_veil=1`)⇒ 只在与 `law.enabled_ids` 相交时生效 |
    | **L3 世界默认** | `data/dress_spec.json` 的 modes/tiers | 前两层都没规定时的兜底 |
    
    面板「装束」行末尾会写明**依据哪一层**(例如「(依 L-01 着装令)」「(依 世界默认)」),玩家看得见规矩从哪来。
    
    ## 组合实证(`KUSHAN_DRESS` 自报原文,`combos.txt`)
    - **① L2 生效**:`cid=120 rtype=street tier=1 source=law by=L-01 着装令` ⇒ 街面罩袍强制靠地方律令
    - **② L2 失效落 L3**(`KUSHAN_QA_CHAIN=l2off`):`source=world by=世界默认`
    - **③ L1 最高**(QA 注入主人规矩「内室档位 5 + 禁用乳环」):`cid=10 source=owner tier=5`(**覆盖**由地位推得的 4 档)`…|本场所禁用:乳环(依 本场所主人规矩)`
    - **④ 内室无律令命中落 L3**:`cid=10 source=world tier=4`
    ⇒ **逐层兜底成立**:L1 > L2 > L3 各有独立组合为证,且「依据」在文案里明写。
    
    ## 边界(未做,不谎报)