# 待办事项 按最能解除阻塞的顺序排列。`[ ]` = 未开始,`[~]` = 进行中。 ## P0 — 阻塞激活 - [x] **安装到当前激活的配置档 (profile) 中。** 完成于 2026-10-13 —— `dsh-renpy-skin: link:E:/ProjectFiles/dsh-renpy-skin` 已写入桌面端 配置档的 `package.json`,列在 `dsh.profile.bundles` 里,并已符号链接 到其 `node_modules`。这些路由 (route) 在运行中的宿主 (host) 上可以响应 (`GET /renpy-skin/manifest` → 200)。见 [STATUS.md](STATUS.md)。 ## P0.5 — 宿主重启(已完成) - [x] **重启宿主以加载当前 `lib/`。** 2026-10-14 已完成:桌面宿主带着 `--remote-debugging-port=9222` 重启(见 [tools/launch-dsh-debug.cmd](tools/launch-dsh-debug.cmd)), `GET /renpy-skin/manifest` 随后返回带 `audio` 与 `splash` 的新形态。 页眉图标、两个面板、播放列表、设置深链与开场图层都是在这一轮里 第一次被实机看到的。 - [x] **CDP 端口已重新绑定。** 2026-10-14 19:06 的重启成功抢到 9222: `DevToolsActivePort` 改写为 19:06:29,`http://127.0.0.1:9222/json/list` 列得出页面目标。P1.5 的三项修复因此得以用 CDP 复查。 ## P0.6 — Agent 加载链路重构(2026-10-17 调查,2026-10-18 执行) **背景(已核实)**:2026-10-16 的会话 `session-0a77af4b-8ba7-4a91-9545-1dfa7b1881e0` 使用 `standard` preset, 请求里 47 个工具,没有 `renpy_*`;31 个 step 全部只有 reasoning,最后 `max-tokens`。会话 cwd 也不是 VN 配置目录。结论:当前不是“提示词少写一句”, 而是工具注册、客户端服务、工作区/预设三条链路都可能断了。本节任务取代 「审计遗留」第 226-230、239-244 条的待验证描述;实现后一并勾掉。 - [x] **0. 先复现,不要先改。** 三项都在同一条链路上就地确认了: - `renpy_*` 确实一个都没注册——**我自己的这条会话里也一样**(用户当场指出 工具表里没有它们),说明不是那个会话独有的问题; - `systemPrompt` 同理:`lib/index.js:590` 与 `:624` 用的是同一个 `ctx.get` + 守卫模式,守卫静默跳过注册时没有任何日志; - 「切预设」这一步**不存在可行 API**:宿主端 `agentPresets` 只有 `resolve(id)` 与 `mount(agentCtx, id)`,且只在**创建** Agent 时通过 `CreateAgentOptions.meta.agentPreset` 生效;`agents.create(...)` 建出来的 会话不进入 Shell 的会话列表,用户看不见。客户端 `remote.session` 只有 `canOpenWorkspacePath` / `workspacePathApplications` / `openWorkspacePath`, 没有任何 preset 方法。**因此放弃切预设**(用户拍板):`renpy_*` 工具本来就是 全局注册的,persona 由用户自己在 composer 里选。 - [x] **1. 宿主端改为等待 `tools` / `systemPrompt`。** `lib/index.js` 的 `ctx.get("systemPrompt")` 与 `ctx.get("tools")` 都改成 `ctx.inject([...], (scoped) => ...)`,注册仍各自包在 `ctx.effect(...)` 里 (用 **scoped** 的 effect,随服务一起销毁)。顶层 `inject = []` 不变: 缺少服务的配置档仍要能加载 HTTP 路由。`webServer` 早在 2026-10-13 就是 这个写法,注释里还写着「两者不同时刻解析,因此工作正常」——那句话是错的, 三处有同一种失败模式。 - [x] **2. 客户端服务改为显式等待,禁止静默剪贴板兜底。** `apply` 里新增 `ctx.inject(["uiWorkspace","sessions"], ...)` 保存作用域服务, `workspaces` 用第二次 `ctx.inject` 补上。`loadWithAgent` 失败**一律 abort** 并抛出具原因的错误;`onFailure` 参数与整块 `navigator.clipboard.writeText` 兜底已删除。剪贴板只剩 `copyAgentPrompt()`,由失败后面板上的「复制提示词」 按钮显式触发。 - [x] **3. 按钮流程改成“准备 → 开会话 → 投递”(切预设已放弃)。** 新增 `POST /renpy-skin/agent/prepare`(入参 `{ session?, gameDir? }`): 校验存在 → 是目录 → 能扫出清单 → 读到游戏名 → **扫到 `.rpy`/`.rpyc` 或 `.rpa`**(只看目录是否存在会被任意空文件夹骗过:扫描器对任何目录都成功, 并把目录名当游戏名;脚本可能全在归档里,所以两者都为 0 才算不是)→ 持久化 `gameDir` + 清会话状态 + 把会话钉到这个游戏 → 生成两棵树 → 返回 `{ gameDir, root, profileDir, name, paths, stats }`。 **顺序上的坑**:必须先持久化并 `forgetSessionState()`,**再** `sessionGames.set()`——前者会清空 `sessionGames`,先 pin 就会被丢掉。 按钮拿到响应后:`workspaces.create({path: profileDir})` → `startSession` → 等新会话 id → `session.prompt(...)`;任何一步失败都 abort 并显示原因。 - [x] **4. 注入真实路径并精简提示词。** `agentLoadPrompt(facts)` 接收 `prepare` 的返回,除语言之外再追加一段事实块 (游戏名、游戏目录、配置目录、两棵树路径、各类文件计数、是否被截断)。 已准备时开头明说「游戏已由宿主加载,没有路径要问」;**没准备时用另一套措辞** (先问目录、再用 `renpy_configure`),否则会把模型指向宿主从未加载过的清单。 工具说明压到一两句,schema 交给工具自己。`renpy_configure` 只用于以后换游戏。 - [x] **5. 目录树两层:紧凑树进上下文,完整树落盘。** `lib/scantree.js`:`scan-tree.md` ≤400 行 / 约 12 KB(硬预算,按优先级分节, 装不下时逐块截断并在文件里写明剪到哪一行、完整清单在哪个文件); `scan-tree.full.txt` 完整遍历,上限 20000 条,超了写明 `[truncated]`; `.rpa` 只展开第一层并按顶层目录计数;跳过 `cache`、`saves`、`tl`、 `__pycache__`、`.git`、`renpy`。扩展名集合直接从 `lib/renpy.js` 导入, 不重复定义。真实游戏实测:2151 文件 → compact 184 行 / 5297 字节。 - [x] **6. 存储根适配 `DSH_HOME`。** 新增 `lib/home.js`:`DSH_RENPY_SKIN_HOME` > `$DSH_HOME/renpy-skin` > `~/.dsh-renpy-skin`,空白视为未设置;`lib/index.js` 里三处重复的路径表达式 合并成一处。旧位置在首次解析到新位置时**复制**一次(`config.json`、 `library.json`、`game/`,只复制不删)。不按 `DSH_PROFILE` 隔离(游戏库全局共享)。 - [x] **7. 验收与测试。** - [x] `node --check lib/index.js`、`node --check lib/client.js` 通过; - [x] `node --test "test/*.test.mjs"` = 205 tests / 199 pass / 0 fail / 6 skipped; - [x] 新增回归:`apply` 按 `systemPrompt`/`tools`/`webServer` 顺序等待且三处 各只注册一次;`lib/home.js` 的优先级与迁移;`scantree` 的预算截断与归档 聚合;`/agent/prepare` 三条(写两棵树 / 加载被指定的游戏而不是已设的 / 拒绝时不留下半成品);提示词两种形态;`prepareGame` 两种请求体; abort 时剪贴板没有被写。 - [x] **实机(CDP)已做**,2026-10-19 02:02–02:06。没有杀掉桌面宿主: 另起了一个一次性配置档 `verify`(`--from-default-profile web` + `plugin add link:E:/ProjectFiles/dsh-renpy-skin`),`dsh --profile verify --port 19487 --no-open` 跑同一份 `lib/`。`dsh web` 把带 token 的 URL 打印到 stdout (`Start-Process -RedirectStandardOutput` 即可读到),所以 SPA 也能开。 `node tools/verify-agent-load.mjs --port 19487` = 10/10 PASS; 浏览器里点「用 Agent 加载」后新会话: `cwd` = `C:\Users\\.dsh\renpy-skin\game\Remember the Flowers 0.17.9P`, `request/header` = **30 tools,其中 `renpy_*` 4 个**, `system/message` 里带 `## Ren'Py stage director` 段(即 `systemPrompt` 注册也被这次实机证实),user message 与 `agentLoadPrompt(facts)` 逐字一致, Agent 依次调用 `renpy_read_manifest` → 读两份清单 → `renpy_save_vn`, 写出 `tree.md` / `summary.md` / `vn.json`,`turn/end` = `{kind:"completed"}`。 **本次顺带确认的坑**:桌面宿主用的 `buddy` provider 只由 Electron 应用注册, 配置档里没有;`dsh-our-free-model` 的免费车道从本机出口 IP 一律回 `INVALID_CREDENTIAL`;第三方网关会**预扣**整个 `maxTokens` 额度, `defaultMaxTokens: 131072` 在余额不足时直接 403 —— 验收用 8192 即可。 `tools/launch-dsh-debug.cmd` 的 `taskkill /IM "DeepSeek Harness.exe" /T /F` 仍然只能由人手动运行(它会杀掉发起它的进程树)。 - [x] **8. 文档同步。** `docs/CONTRACTS.md`(三处 `ctx.inject`、客户端服务与 `/agent/prepare`、两层目录树、 存储根优先级)、`docs/ARCHITECTURE.md`(准备/扫描流程与新的不变式)、 `docs/STATUS.md`(第四个根因修复、已验证/未验证两节)、`CHANGELOG.md`、 本文件已同步。全部中文。 ## P0.7 — 全归档游戏的 `/agent/prepare` 崩溃(2026-10-19 修复,待重启复验) - [x] **已定位并修复。** 2026-10-19 21:49 用真实渲染的 CSE 复现:开着 CSE 时 `POST /renpy-skin/agent/prepare` 回 400,正文 `TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be of type string or an instance of Buffer or ArrayBuffer. Received an instance of Array`。 根因是 `lib/scantree.js:380-386` 的 `scripts.length === 0 ? [...] : []` 在归档分支又多套了一层数组,于是 `blocks[].lines` 里留下嵌套数组, `renderUnderBudget()` 用 `Buffer.byteLength(line, "utf8")` 量字节时抛错。 这个分支只在「没有松散脚本、脚本全在 `.rpa` 里」的游戏上走到——正是 CSE 这类 商业发行版,也是 `docs/questions/` 里那七篇记录的全部前提。 - [x] **回归测试已加。** `test/scantree.test.mjs` 的 `a game whose scripts all live inside an archive still produces a map` 用真实 RPA-3.0 归档(索引取自 `test/fixtures/rpa-indexes.json` 的 `simple`) 搭一个只有 `scripts.rpa` 的游戏,断言 `stats.scripts === 0`、 `stats.archiveEntries.scripts > 0`,且紧凑地图里出现 `no loose scripts; N compiled scripts live inside the archives`。 原有用例都带松散 `.rpy`,所以单测全绿而实机崩溃。 - [ ] **待用户重启宿主后复验。** 正在运行的宿主(2026-10-19 21:13 启动)内存里 还是旧 `lib/scantree.js`,`node tools/verify-agent-load.mjs --port 27194` 仍会在第 3 项回 400。重启后再跑一次,期望 `POST /renpy-skin/agent/prepare` → 200 且 `dirs[0].tree` 里有 `## scripts and config (0 loose)` 段。 注意:`--port` 必须用当前 GUI 的端口,宿主每次重启都会换。 ## 第八轮用户任务(名字/额外要求/记忆 + 文本框模块化 + 多对话框选择) 用户一次提出三件事(2026-10-19),已批准的范围与顺序如下。 - [x] **1. 全局名字、会话级名字覆盖、只给「在场」的额外要求,以及按角色分开的跨会话记忆。** 设置新增 `playerName` / `sessionNames` / `roleplayPrompt`;宿主新增只对预置 `renpy-scene` 生效的提示词段 `renpy-skin:roleplay`(`presetOfSession()` 优先读 `assembly.scope.header.agentPreset`,回退到 `session/event` 缓存); `lib/memory.js` 把记忆存成 `game/<扫描名>/memory.json`(纯 tag → 文本, 单角色 8000 字符上限、注入只带最近 1200 字符 × 最多 12 个角色); 新增 `renpy_memory` 工具与 `GET/POST /renpy-skin/memory` 路由; 设置页新增「Roleplay 「在场」」卡片,顶栏新增笔记本按钮与记忆面板。 模型主动记与用户手动记写的是同一份文件。测试 253 项 / 246 通过 / 7 跳过。 - [x] **2. 文本框模块化。** 清单新增 `dialogbox.modules`:扫描器把游戏自己的 `say` 屏翻译成扁平节点表(`box` / `image` / `text` / `button` / `bar`,画布像素 `rect` + `z`),底板之外的 `add`/`background` → 装饰图、`layout.name*` → 名字框、 `quick_menu` 里动作在白名单(`settings` / `music` / `memory` / `hide` / `skip`) 的按钮 → 可点按钮,其余按钮只进只读的 `quickMenu` 报告。客户端 `ModulesLayer` (`shell.overlay`,order −890)每 400 ms 量 composer 根元素,用与 `dialogboxRules()` 共用的 `composerGeometry()` 把画布坐标换成 `cqw`,只有白名单 按钮恢复 `pointer-events`;`dialogboxModules` 是总开关。`renpy_save_vn` 接受 `dialogbox.modules` 并整体替换扫描结果(逐节点校验,上限 40,`[]` 清空), `rederiveAssets()` 为 `image`/`hover.image` 派生 `imageUrl`。切游戏时清单重建, 模块层随 store 一起换掉,不需要重载页面。 - [x] **3. 多个对话框候选的选择器。** 一个 VN 有多张 textbox 时,清单给出 `dialogbox.candidates`(显式 `gui` 变量 → `window` 样式 → `say` 屏的 `add`/`background` → 常规路径 → 全库扫描兜底,排除手机/平板与 namebox, 上限 12 张;`candidates[0]` 恒等于当前所用的 `textboxPath`,由 `rederiveAssets()` 在覆盖层换盘后重排保证),设置页用缩略图选择器 (`PlatePicker`,候选 ≥ 2 时才出现)选,选中即 `POST /renpy-skin/dialogbox` 写进该游戏档案并重建清单;Agent 也能在清单摘要的 `plates` 里看到候选、 把路径写进 `dialogbox.textboxImage` 再用 `renpy_save_vn` 切换。 测试 253 项 / 246 通过 / 7 跳过。 ## P1 — 浏览器端 (browser half) 实机验证 以下各项均已在真实 GUI 中确认(2026-10-14,记住是 CDP 驱动 + 截图双重确认): - [x] 背景图层 (layer) 在对话背后渲染游戏背景。 - [x] 舞台 (stage) 图层在 `renpy_stage` 报告的位置渲染立绘 (sprite)。 - [x] 900 ms 轮询确实能捕获宿主驱动的舞台变化。 - [x] `theme.overrideTokens` 能生效,并在禁用时干净地回退。 - [x] 设置页面能往返:修改 `gameDir`,确认其持久化且清单 (manifest) 重新加载。 - [x] 对话框 (dialogue box) / 选项菜单 (choice menu) 的 CSS 皮肤 (`buildSkinCss`) 应用于用户气泡、输入框和问题卡片。 - [x] **页眉条**:Ren'Py 标记和音符出现在 `.Dc7zOa_headerUtilities` 中,且面板 (panel) 的控件可点击。 - [x] **开场 (splash) 图层**:在 Remember the Flowers 上确认 —— `kind: "video"`, 全帧绘制,`videoTime` 推进,跳过提示可见。GTST 的 开场是三屏文本 + imagebutton,其中只有美术资源 (asset) 被提取 —— 请确认 未被提取的屏幕是直接缺席,而不是显示空白帧。 - [x] **主菜单音乐**:GTST 将 `config.main_menu_music` 解析为 `audio.gotosnowtown`(主题 序章)/ `audio.ourfirststep`(硫石篇)。默认 分支会播放,且下拉列表列出另一项。 - [x] **音频**:`