# 更新日志

本项目遵循[语义化版本](https://semver.org/lang/zh-CN/)。

## [0.1.7] - 2026-09-20

> 这一版在 0.1.6 之上修了三个**真机问题**（都是用户/其他使用者实测报上来的），并订正了几处"状态在撒谎"。
> 0.1.6 的内容（修皮肤站登录 400）与 0.1.5 的全部内容也都在这一版里，装 0.1.7 就都有了。

### 🔴 修复（断线后状态不同步：`mc_status` / 顶部状态条 / AI 都以为还连着）

- **现象**（用户）："连接断开了状态没有同步。`mc_status` 和顶部状态条都不知道连接断开了，AI 也以为连接还在。"
- **三处根因**（都在，缺一不可）：
  1. **core 从来不播报"断线"**：`b.on('end')` 里只写一行日志，不 `emit` 任何东西 ⇒ 上层根本没机会知道；
  2. **会话不转发**：`McSession` 只把 `spawn/death/reconnect/chat/system/damage` 转进事件队列，
     没有断线这一类 ⇒ `mc_events` 里看不到、AI 也不会被叫醒；
  3. **看门狗没有这条唤醒项**，断线后它**照旧挂着**（`watch.armed` 还说"在监听"），
     前端 `active` 也因此继续为真 —— 三处一起撒谎。
- **修法**：
  · core 在连接结束时 `emit('offline', { reason, willReconnect, sub })`（原因取 `lastError`，并判断还要不要自动重连）；
  · 会话把它转进事件队列（`mc_events` 看得见）；
  · 看门狗新增唤醒项 **`wakeOn.disconnect`（默认开）**：
    **会重连** → 叫醒 AI 说"连接断了（原因）——正在自动重连，先别再操作角色"，
    **不会重连** → 直接关掉看门狗（关闭通知本来就会告诉 AI"你已不在 MC 里、要用 mc_connect 重新进服"）；
  · 前端：`/api/mc/status` 的 `active` 把"正在自动重连"也算上（否则刚断线状态条整个消失，用户以为插件把状态忘了），
    状态条新增显示 **"重连中…"**（配 `data-mc-reconnecting`），不再假装"在游戏中"；
    并用 **`reconnectPending`**（掉线置位 → 真重连成功 / 手动 `mc_connect` 成功才清）覆盖"正在尝试连接"那一段，
    否则那几十秒里状态条会从"重连中…"退回"未上线"，看着像插件放弃了；
  · 顺手：断线时不再发心跳唤醒（否则会说"我还在游戏里"，那是撒谎）。
- **真实场景验收**（本地官方 1.21.1 服务端，连上进服后**把服务端进程杀掉**）：
  `offline` 事件 `{reason:"read ECONNRESET", willReconnect:true}` → `status()` 回
  `online:false / ghost:true / reconnecting:true` → 看门狗留档有 offline，并把
  「【MC 看门狗｜disconnect】连接断了（read ECONNRESET）——插件正在自动重连…」**真投给了 AI**。
- **自检**：新增 10 条断言（唤醒矩阵有 disconnect / 会重连则排队唤醒 / 不会重连则自动关闭 /
  断线进留档 / **真 socket 断开时 core 确实发出 offline** / 会话转队列 / `modeView` 与
  `/api/mc/status` 带 `reconnecting` / 状态条显示"重连中…"）。

### 🔴 修复（`mc_events {waitSec}` 把唤醒**堵住**了：等到消息、提及了也不醒，用户空等很久）

- **现象**（用户实测）："等待有时候并不能唤醒！比如等到消息，但是发了消息，就算提及它也没有唤醒，
  让用户空等特别久。反倒是等完之后看门狗注入提示词。"
- **根因**：宿主的 `agent.steer` 语义是「**运行中**在下一次 **step 边界**消费」，
  而 step 边界要等**当前这一步的工具调用返回**才算到。
  于是 `mc_events {waitSec:120}` 一堵，看门狗 2 秒前就组好的唤醒文案被压在等待后面，
  直到等待超时才投出去 —— **等待堵住了它正在等的那件事**。
- **修法**：
  · 等待逻辑抽成 `src/wait.mjs`（`waitForEvents`），多一条收工条件：**被唤醒打断**；
  · 看门狗在**所有注入之前**（唤醒 / 关闭通知 / 补提醒 / 心跳——都走 `#inject` 这一个口子）
    调 `sess.interruptWait(kind)` 置个标记；等待循环每 250ms 看一眼，见到**立刻收工**：
    工具一返回 → step 结束 → 唤醒文案当场投出去；
  · 工具返回里明确告诉 AI：`interrupted: true` + `interruptReason` +
    「看门狗命中唤醒条件…**先别急着再 wait**，等那条提示进来」。
- **实测**：被打断时等待 **126ms** 就返回（原来会把 30 秒/120 秒干等满）。
- **顺带改掉描述里的一句谎**：原描述写"看门狗记录的**一切**：…被传送、捡物、上下线"，
  但那三类**不在**这个队列里（只在看门狗留档，用 `mc_watch {action:"log"}` 看）。
  现在描述写清了，并提醒 `waitSec` 只是兜底（看门狗会主动叫你）、建议 ≤30。
- **自检**：新增 8 条断言（等到事件立刻回 / **被唤醒打断立刻回** / 用户停止立刻回 / 到点才回 /
  **看门狗注入前真的会打断**（stub 会话记录调用）/ 工具侧接线 / 注入口接线 / 描述不再谎称）。

### 🔴 修复（连 **1.21/1.21.1** 进服成功、1 秒后被踢：`Failed to decode packet 'accept_teleportation'`）

- **现象**（其他用户反馈，已本地用**官方 1.21.1 服务端**完整复现）：进服完全成功（服务端日志
  `logged in with entity id …` + `joined the game`），紧接着就掉线，服务端报

  ```
  Internal Exception: io.netty.handler.codec.DecoderException:
      Failed to decode packet 'serverbound/minecraft:accept_teleportation'
  ```

  换端口 / 换地址（localhost、回环、局域网 IP）/ 换版本字符串都一样；而**连 26.2 却完全正常**。
- **根因**：我们的**按键上报兼容层**（为 26.2 加的、每 50ms 发一次 `player_input`）是**无条件**发的，
  但 **1.21/1.21.1（协议 767）根本没有 `player_input` 这个包**（1.21.3+ 才有）。
  而 `minecraft-protocol` 的 protodef **遇到未知包名不报错**——实测它写出的字节是

  ```
  write('player_input')            → 02 00 00        ← id=0x00、body 为空
  write('definitely_not_a_packet') → 02 00 00        ← 和一个瞎编的包名一模一样
  write('teleport_confirm')        → 03 00 00 07     ← 正常包：id=0x00 + VarInt(7)
  ```

  服务端于是把 id `0x00` 当成它自己注册表里的 **`accept_teleportation`**（1.21.x 里确认传送就是 0x00），
  去读 `teleportId` 时**没有字节可读** ⇒ 抛 `DecoderException` ⇒ 立刻踢人。
  所以错误名与"我们真正发的包"毫无关系，极具误导性。
- **修法**：给 core 加一道"**发之前先查这个版本的协议数据里有没有这个包**"的护栏（`#supportsPacket`，
  带缓存），`player_input` 只在该版本真有它时才发；顺带在每次 tick 也复查一遍（重连可能换版本）。
  查不到的版本会记一行日志说明，而不是硬发。
- **顺带修掉「幽灵在线」**：被踢之后 mineflayer 的 `bot.entity` 会残留，而 `online` 只看 `entity`，
  于是死连接被报成"在线"（工具对着死 socket 干等、世界时间冻结、`/list` 零响应）。
  现在 `online` 要求**连接没结束**，`status()` 会明确回 `ghost: true` + 一句"要回游戏里请重新 mc_connect"，
  `requireBot()` 也直接说"连接已结束（bot.entity 是残留）"。
- **回归测试**（自检 + 真机）：
  · 自检断言：1.21.1 上**一个包都不发**、26.2 上**照常发**、源码里必须留着这道护栏，
    以及"幽灵在线"的三条（`online` 变 false / `status()` 标 ghost / `requireBot()` 明确报错）。
  · 真机复现与验收（本地官方 1.21.1 服务端）：**修前 1 秒被踢**（错误与反馈逐字一致）；
    **修后稳坐 8 秒不掉线**，`player_input` 发出 0 次、服务端日志再无 `DecoderException`。

## [0.1.6] - 2026-09-19

> 这一版在 0.1.5 之上只加了一个修复。0.1.5 的全部内容（P0 崩溃修复 / `mc_lan` 只留广播 /
> 新增 `mc_ping` / 默认行事准则第五版）也都在这一版里，装 0.1.6 就都有了。

### 🔴 修复（某个皮肤站账号登录一直 **400**：authenticate 少了 Yggdrasil 必填的 `agent` 字段）

- **现象**（用户实测）：某个皮肤站的账户怎么都登不上，一直"认证失败 HTTP 400"；
  **另一个皮肤站**（自建、老实现）同一个插件、同一个流程却没事。
- **根因**：`/authserver/authenticate` 的请求体里少了 **`agent`** ——
  它是 **Yggdrasil 官方协议里 authenticate 的必填字段**（authlib-injector 与官方客户端都会发）。
  我们一直没发，**老实现容忍缺省**，但**换成新实现 `Yggdrasil Connect` 的皮肤站不容忍**：
  缺字段直接回 **400**（而不是 401/403），于是错误码落在"认证失败 HTTP 400"上，
  看起来像"密码错"，其实请求根本没被当成一次登录。
- **实测对照**（2026-09-19，真发请求，同一地址只差这一个字段）：

  | 认证服 | 缺 `agent` | 带 `agent`（假账号） |
  | --- | --- | --- |
  | 出问题的那个（`Yggdrasil Connect`） | **400** | **403**（正常走完凭据校验） |
  | 一直正常的那个（老实现） | 403 | 401 |

  即：**"通不通"的差别就在这一个字段上**。
- **修法**：`authenticate` 请求体补上 `agent: { name: 'Minecraft', version: 1 }`（标准值，官方客户端同款），
  并给 **400** 单独补了一句人话提示（区分"请求不合规"与"账号密码不对"）。
  另外在代码里留了这条事故的说明，防止后人又把 `agent` 删掉。
- **回归测试**：自检里那个"桩掉 `fetch`、抓真实请求体"的用例加了两条断言 ——
  请求体必须带 `agent（name=Minecraft / version=1）`、且源码里保留"缺 agent 会 400"的说明。
  另外真机端到端验过：走插件自己的认证代码打那个皮肤站，现在拿到的是 **403**（凭据校验），不再是 400。

## [0.1.5] - 2026-09-18

### 🔴🔴 修复（P0：连一个不存在的服务器会把**整个 DSH 进程搞崩**）

- **现象**（用户实测）：对 AI 说连一个根本没开的服务器（`127.0.0.1:61631`），宿主进程直接死：

  ```
  Error: connect ECONNREFUSED 127.0.0.1:61631
  Emitted 'error' event on McBot instance at: src/core.mjs:671
  node:events:486   throw er; // Unhandled 'error' event
  ```

  ——**工具报错可以，但绝不能把宿主带走**。
- **根因**：`McBot extends EventEmitter`，而 **EventEmitter 的语义是「`emit('error')` 时若没有监听者就 throw」**。
  原来那行把 bot 的网络错误**原样转发**到 McBot 上：

  ```js
  b.on('error', (e) => { this.lastError = e.message; this.emit('error', e) })   // ❌
  ```

  而 McBot 通常**没有 `'error'` 监听者**（工具层是 `await` 抛错返回，不订阅 error）
  ⇒ 一个"服务器没开"这种再普通不过的错误，就把整个进程带走了。
  另一处（认证阶段）写的是 `try { this.emit('error', err) } catch {}` —— **假保护**：
  抛出来的就是这次 `emit` 自己，`try/catch` 拦不住它。
  （与 2026-09-17 那次"未处理 Promise 拒绝 → 宿主 fail-loud `exit(1)`"是同一族：**插件里的错误绝不能漏到宿主**。）
- **修法**：把"上报错误"收敛成**唯一一个口子** `#reportError(e)`，它保证：
  1. 记 `lastError`（工具层据此给用户一句人话）；
  2. 记一行日志（`mc_diag` / 日志能看到）；
  3. **只在真有监听者时才 `emit`**（`listenerCount('error') > 0`）—— 没人听就到此为止，**绝不 throw**；
  4. 自己再兜一层 `try/catch`（监听者回调里抛错也不该带走进程）。
  两处调用点（bot 的 `error` 转发、认证阶段 `connect()` 的 rejection）都改走它。
- **回归测试**（自检里**真连**一个刚关掉的端口，且**故意不订阅 `'error'`** —— 真实工具路径就是这样）：
  断言"以普通错误结束"、`lastError` 带得上 `ECONNREFUSED`、并且**走到后面的断言 = 进程没被带走**；
  另加静态断言：代码里每一处 `this.emit('error')` 都必须带"有监听者才发"的保护
  （注释里的事故引用不计）。

### 默认行事准则（`.whale-craft/RULES.md`）第五版：新增「较长思考」整节

- **内容换成用户给的全文**（第四版 2318 字 → 第五版 5911 字节 / 87 行，十个分节）：
  `宗旨` · `称呼` · `记忆` · `边界信息` · `登录游戏` · `看门狗` · `聊天` · `建筑须知` · **`较长思考`** · `硬规矩`。
- **新加的整节「较长思考」**：长时间思考不给响应会让用户以为你卡住了；
  建议**偶尔简短汇报**想法——一方面是给用户交代，另一方面**这样事件提示词才有注入时机**（否则错过新情况）。
- 「建筑须知」补三条：先把**大体结构**做完再抠细节 · **每阶段完成要给用户响应**并说明还在继续 ·
  建造完成后**必须复盘**（完整性 / 美观 / 与环境融合），发现问题先修再认定任务完成。
- 自检同步：断言"第五版十个分节都在"、"建筑须知补的三条都在"、"较长思考讲到
  『长思考要偶尔汇报』『这样事件提示词才有注入时机』"。
- ⚠️ 想立刻用上这一版：本次发版会带着 **0.1.5** 的版本号，"随版本更新"（默认开）会自动替换你工作区里那份；
  也可以在「MC设置 → 提示词」里点**恢复默认**。

### 🔴 `mc_lan` 砍掉扫网段：只保留原版那一件事（听局域网公告）

- **起因**（用户）："原版都没有，我们凭什么加！一个工具调用半天，用户又说慢！"
- **删掉的行为**：曾用"① 听多播公告 + ② 扫本机 /24 的 25565-25569 + 手写 STATUS ping"两条路。
  第 ② 条是一个 **8.2 秒**（实测一个 /24）起步的动作，最坏情况（网段里 TCP 通、但不回 MC 协议）
  单个 /24 可到 100 秒级、多网段可到分钟级——而 **`defineTool` 的 `timeoutMs` 在宿主里只是元数据、
  没有任何地方真正拿它包超时**，所以慢就是真慢。
- **为什么删得起**：它多给的"版本 / MOTD / 人数"，在 `mc_connect` 进服时本来就会拿到
  （mineflayer 传 `version:false` → `minecraft-protocol` 的 `autoVersion` 内部就是同一条 STATUS ping）；
  唯一不可替代的"没开公告的服务器"，原版客户端自己也找不到 —— 让用户报地址即可。
- **现在的 `mc_lan`**：只听 `224.0.2.60:4445` 上"对局域网开放"的公告
  （`[MOTD]…[/MOTD][AD]端口[/AD]`，原版/复刻实现的重发周期 1.5 秒），**不扫端口、不主动发包**，
  参数只剩 `mode`（只有 `broadcast`）与 `seconds`（默认 3、上限 15）——
  **恒定在 seconds 秒内返回**，不再是网络扫描器，也再没有分钟级的调用。
- **顺带**：`src/lan.mjs` 从 313 行缩到 ~80 行，连 `node:net` 都不再 import（只剩 `node:dgram`）；
  默认行事准则里那句"先扫描局域网服务器"改成"可以先听一下公告，找不到就直接问用户地址"。
- **回归测试**：断言 7 个扫描相关导出**全部消失**、`lan.mjs` 里不再出现 TCP/扫描字样、
  工具参数只剩 `mode`/`seconds`、返回里没有 `scanned` 字段；
  并在回环上真发三条公告（合法 / 垃圾 / 重复）验证"听得到 + 垃圾被丢 + 同 host:port 去重"，
  以及 `seconds=1` 到点就返回、`seconds=999` 被夹到 15 秒。

### ✨ 新增 `mc_ping`：**已知地址**时的单地址探测（通不通 / 版本 / MOTD / 人数 / 延迟）

- **起因**（用户）："有没有 ping 的工具？已知地址，测试通透性和 motd 等信息，单地址。"
  —— 砍掉扫段之后，"知道地址、但想先看一眼再进服"这件事确实没有工具可用。
- **做什么**：发一次 Minecraft **STATUS ping**（握手 `nextState=1` + `ping_start`），拿到：
  **通不通**（能不能拿到状态响应）/ **版本** / **协议号** / **MOTD**（颜色码已去）/ **在线与上限人数** /
  **延迟**（写 `ping` 到收 `pong` 的往返）。🔴 **不登录、不用账户、不进服**，拿到就断。
- **和别的工具的分工**：`mc_lan` = 不知道地址时听局域网公告；`mc_ping` = 知道地址时主动探一次；
  `mc_connect` = 真进服（要账户、会掉线重连）。默认行事准则里加了「有明确地址时先 `mc_ping` 探一下再进服」。
- **为什么不直接用上游的 `mc.ping()`**（踩过的坑，写下来免得后人再踩）：
  ① 它**不暴露 client**，想在超时时真掐掉连接就得覆盖 `options.connect` ——
  而 `ping.js` 结尾正是 `options.connect(client)`，覆盖掉它 = **谁都不建 socket**：
  实测四种场景（通的 / 端口没人听 / 只 accept 不回包 / 域名错）全都只能干等自己的硬超时；
  ② 它的超时是 `closeTimeout`，默认 **120 秒**，对一次工具调用太长。
  所以改成用**同一套底层**（`Client` + `states` + `tcpDns`，锚在解析到的 mineflayer 上）自己走一遍包序，
  **超时与清理自己掌握**（默认 5 秒、上限 30 秒；无论成功/失败/超时都 `end()` + `destroy()`）。
- **错误说人话**：`ECONNREFUSED`=端口没人听 · `ENOTFOUND`=域名解析不了 · `ETIMEDOUT`=连上了没回应 ·
  `ECONNRESET`=被服务端重置……认不出的原样回。**任何失败都收敛成 `{ok:false, error, hint}`，
  绝不把异常冒泡**（工具报错可以，带走宿主不行——就是 0.1.5 那条 P0 的教训）。
- **回归测试**（自检里真起一个 MC 服务端）：起一个 `minecraft-protocol` 服务端（离线模式、`1.21.4`、
  MOTD 带颜色码、`max-players=7`）在回环上，断言 ping 到版本 `1.21.4`／协议 `769`／人数 `0/7`／
  MOTD 颜色码已去；另外断言"端口没人听 → 立刻 `ECONNREFUSED`"、"黑洞连接 → 1.2 秒被硬超时掐掉
  （不是等上游 120 秒）"、"域名解析不了 → 指向 DNS"；以及地址解析的合法/非法各 4 种写法。

### 🧹 「MC设置 → 提示词」页去掉两条多余提示

- **起因**（用户）："提示词页『还没到投递时机（提示词在**发起请求之前**现场判定并投递；这个会话还没发过消息）｜
  「注入工作区 AGENTS.md」是关的』这个提示框多余了，删掉。"
- 那行判据里，`本会话注入：MC模式 ✓/✗ ｜ 本提示词 ✓/✗ ｜ 工作区 AGENTS.md ✓/✗` 已经把答案摆出来了；
  再说一遍"还没到投递时机 / 开关是关的"是重复信息。
- **留下的**（这些是**看不出来、又真的影响投递**的原因，不能删）：没有 agent 上下文 · 这个会话没选工作区 ·
  不是 MC 模式（连 preset 名一起说）· 工作区根目录里根本没有 `AGENTS.md` 这个文件 ·
  preset 的 persona 压掉了宿主运行期上下文（带怎么修）。
- 于是**正常情况那一行整行不再渲染**（前端对 `notes.length === 0` 本来就不渲染）。

## [0.1.4] - 2026-09-18

### 默认行事准则（`.whale-craft/RULES.md`）第二版：新增「建筑须知」整节

- 默认内容换成新一版（1821 → 2318 字）：新增整节 **「建筑须知」** ——
  与环境相互影响/避让改造 · 方块属性与 NBT（楼梯方向、台阶、火把灯笼附着）· 多部分方块（门窗床）·
  受连接影响的方块（栅栏栏杆墙）· 细节与功能连通性 · 建成后复盘。
- 「聊天」补明：游戏中用户在聊天时**第一时间回复**，之后再做深度思考与行动。
- ⚠️ **版本号没变 ⇒ 插件自带的「随版本更新」不会自动替换你工作区里那份**。
  想立刻用上这一版：在「MC设置 → 提示词」里点**恢复默认**（或手动覆盖 `.whale-craft/RULES.md`）。

### 🔴 修复（提示词**晚一步**才注入：跟用户那句不在同一批）

- **现象**（真机）：会话里能看到提示词，但**是 LLM 已经跑起来之后才出现的**——
  用户的原话是"两个全都是 LLM 运行了一半才后知后觉地注入，根本不是用户发送第一条信息后注入"。
- **根因**：上一版把投递挂在 `agent/pre-step`，但**塞进了 `inbox.nextStep` 队列**。
  而宿主的顺序是 `preStep()` 里**先 `inbox.claim()`**（把这一步的消息领走）**再**跑
  `agent/pre-step` 瀑布（`core/agent-loop/src/agent.ts:244` → `:249`）——
  所以那一步塞进队列的提示行**这一步用不上**，要等**下一步**才被领走。会话日志实测：
  `step/start step=1` → 用户那句 → `next-step -3` → `step/start step=2` → 三条提示行。
- **修法**：照宿主官方插件 `agent-instructions` 的写法，**改写 `decision.messages`**
  （`context/agent-instructions/src/index.ts:313-338`）：交棒拿回 decision 后，把提示行
  **并进本步消息的最前面**。宿主紧接着在 `agent.ts:373-377` 把 `decision.messages`
  逐条落成会话日志的 `user/message` 并作为请求里的那批消息 —— 既是正规通道（可持久化、可重放），
  又能**当场生效**：提示行与用户那句**同一批**送给模型，模型第一眼就看到规矩。
- 顺带修掉两个自己埋的 bug（都会被 `catch` 吞掉、表现成"静默不注入"，极难查）：
  - `reconcileNotices` 的几处提前返回漏了 `todo` 字段 ⇒ `enterWithNotices` 读 `.length` 抛 TypeError；
  - 用了没导入的 `versionPromptHash` ⇒ ReferenceError。
- 另修：切出 MC 模式时**无条件清投递台账**（老代码按"队列非空"判断，而现在提示行在本步就送出、
  队列通常是空的 ⇒ 漏清 ⇒ 切回来**再也不投**）；并给会话日志划一条"切出时刻"的线，
  这样"切出 → 再切回"能正常重投，不会被日志里的旧投递记录误判成"已经投过"。

### 改进（「MC设置」入口：选中 MC 模式就显示，工作区改到**点击时**检查）

- **以前**：MC 模式但**没选工作区**时，插件会把「MC设置」入口**藏起来**——用户只看到"没有按钮"，
  不知道是为什么。起因是更早的规则"没有工作区就拒绝 MC 模式会话与设置"。
- **现在**：**只要选中了 MC 模式就显示按钮**（有没有工作区都显示），工作区改到**点按钮那一刻**再检查：
  - 有工作区 → 正常打开设置；
  - 没工作区 → 弹一句**原生提示**，告诉用户"请先在这个对话里选择一个工作区"，
    并说明原因（记忆与提示词都放在工作区的 `.whale-craft` 目录里，没有工作区就没地方放）。
- 两个入口（新对话页模式芯片右边、已有会话标题条）**口径统一**；顺带去掉了一条
  "服务端说不显示就不显示"的网络依赖。
- 服务端行为**没变**：没工作区仍然不套工具白名单、不注入提示词、不建 `.whale-craft/`；
  设置接口在没有工作区时仍然**拒绝**（`400` + 明确原因）。变的只是**入口可见性**。
- `/api/mc/mode` 现在额外报 `hasWorkspace`，并且**不再**因为"没工作区"把 `mcMode` 压成 `false`
  （那正是前端藏入口的旧依据）；诊断里仍写 `reason: 'no-workspace'`。


### 🔴 修复（P0：装了这个版本，**所有模式的所有会话每一轮都失败**）

- **现象**：随便发一句话，界面显示 `本轮运行失败 Cannot read properties of undefined (reading 'kind')`；
  标准模式、MC 模式一视同仁，等于**装了插件就没法对话**。会话日志里 `turn/start` 之后直接 `turn/end{kind:"error"}`，
  **连 `step/start` 都没有**。
- **根因（0.1.4 引入）**：0.1.4 把提示词注入挂到了 `agent/pre-step`，而这条事件是 **cordis 的 waterfall**，
  监听器**必须调用 `next()` 交棒**。0.1.4 的写法只声明了一个形参、函数体也没有 `return next()`：

  ```js
  // ❌ 0.1.4（事故写法）
  ctx.on('agent/pre-step', ({ agent } = {}) => { reconcileNotices(agent) })
  ```

  cordis 的契约是明确的（`@deepseek-ai/cordis` 的 `waterfall()` 注释原话）：
  **"a listener that does not call `next()` vetoes the rest of the chain, including the built-in behavior"**
  —— 不交棒 ⇒ 否决后面所有监听器**和宿主内置行为**，且 waterfall 的返回值就是那个监听器的返回值（`undefined`）。
  宿主拿到 `undefined` 后紧接着读 `decision.kind` ⇒ `TypeError` ⇒ 整轮失败。
- **修法**：接第二个形参并交棒，且**交棒不受对账结果影响**（对账挂在 `next()` 返回值的 `finally` 上，
  内层无论成功失败都保证先交过棒；`next` 不接受参数、载荷沿用原 args，所以原样返回内层结果即可）：

  ```js
  // ✅ 0.1.5
  ctx.on('agent/pre-step', ({ agent } = {}, next) => {
    const inner = next()
    ...
    return inner.finally(() => { try { reconcileNotices(agent) } catch { ... } })
  })
  ```
- **为什么自检没拦住**（补上了）：原来的自检全部在插件自己的进程里直接调函数，**绕开了宿主的编排**，
  所以"契约违规"测不出来。现在补了三层：
  1. **替身按宿主契约行事** —— `agent/pre-step` 的替身在监听器没交棒时**当场抛错**，
     任何一项用到它的断言都会红（旧写法必挂）；
  2. **横切静态检查** —— 扫描源码里所有 `ctx.on('<事件>', …)`，凡是**宿主已登记的 waterfall 事件**
     （`agent/pre-step`、`agent/request`、`tools/execute`、`system-prompt/assemble` 等 14 个）
     都必须接 `next` 并调用它；**并带负向验证**（把事故写法喂给同一判据，必须判违规）；
  3. **真 cordis 集成探针** —— 用宿主实际装的那份 cordis 起一个真 `Context`，注册插件的监听器，
     走一遍 `waterfall()`，断言内层被执行、返回值不是 `undefined` 且带 `kind`。
- 教训：**挂到 waterfall 之前先读那个 dispatch 模式的契约**，别只看事件签名；"抛错"和"不交棒"一样致命。


### 修复（提示词注入的**挂载点**搬到了正确的地方）

- **背景**：DSH 的「模式」（agent preset）**只允许在对话开始前切换**——第一轮一旦跑过，
  宿主就拒绝换（`agent-preset/locked`）。也就是说，模式的最终值是在**你发出第一句话、马上要送给模型**
  的那一刻才定下来的。而插件此前是在**会话刚建立 / 你点模式芯片的那一瞬间**就把提示词排进了投递队列，
  比"定下来"早了一步。
- **后果**：在空白会话里先点「MC模式」、又说一句话都没发就点回「普通模式」时，
  那三条提示词已经排在队列里等着了 —— 你随后第一次开口，它们就**注进了一个普通模式会话**
  （"MC 行事准则 + 版本提示 + 记忆索引"出现在标准模式的对话里，而且会一直留在历史里）。
- **现在**：提示词改在 **`agent/pre-step`**（宿主那条"消息已被取走、系统提示词已装好、这一步马上发给模型"
  的流水线）**现场对账**后投递 —— 投出去的每一份都对应**发起这次请求时这个会话真正在用的模式**，
  串模式从根上不再可能。
  - 不是 MC 模式 ⇒ 一条都不投；是 MC 模式且这个模式还没投过 ⇒ 投三条。
  - 幂等：**按模式记账**（同一个模式只投一次）＋**回读会话日志**（进程重启 / 插件重载后，台账没了，
    也不会把已经注过的再注一遍）＋队列里已有同名提示行就不重复入队；正文变了（改了行事准则 / 记忆索引更新 /
    换了插件版本）则**就地替换**队列里那一条，不会新旧两份同时出现。
  - 顺带删掉了旧实现里那段"已经注过的补一条**作废声明**"的圆场逻辑 —— 不再需要了。

### 修复（切出模式时漏清投递台账，会导致提示词再也不投）

- 自检里逮到的真 bug：切出 MC 模式时**清掉了待投递队列，却没清投递台账** ⇒ 再切回 MC 模式时，
  插件误判"这个模式已经投过了"，**一个字都不再投**。现在两者一起清。

### 修复（🔴 **提示词一条都没注入**：依赖里少了 `@deepseek-ai/dsh-llm`）

- **现象**（真机报的）：`npm` 装到**另一台机器**上之后，工具一切正常，但提示词注入了没有 ——
  AI 自己都说"没看到"，对话窗口里也没有那几行。
- **原因**：插件用宿主提供的 `createUserMessage()` 来构造那几条"插件提示行"，而这个函数来自
  `@deepseek-ai/dsh-llm` —— 它**当时根本没写进依赖声明**。在开发机上碰巧能解析到（那份
  `node_modules` 里有指向宿主源码的链接），别人正常 `npm i` 装出来的插件目录里没有这个包、
  往上也找不到 ⇒ 函数拿不到 ⇒ **提示行一条都建不出来**。
  而"工具白名单/文件边界"那两道只用插件上下文、不碰任何宿主包 ——
  所以症状精确地是"**工具都在、提示词全无**"。更糟的是旧代码在拿不到函数时**静默返回**，
  只在日志里留一行，界面上完全看不出来。
- **修法（两条腿）**：
  1. **不再依赖宿主包**：消息构造统一收到 `src/user-message.mjs` —— **优先用宿主实现**，
     拿不到就用插件**自带的等价实现**（字段与宿主 `UserMessage` 逐个对齐：
     `role` / `content` / `source` / `id`）。提示词注入从此与环境无关；拿不到时**明确记一行日志**。
  2. 依赖声明补上 `@deepseek-ai/dsh-llm`（**optional** peer + devDependency，范围 `*`），
     让有宿主包的环境走宿主实现。
- **同一次事故还有第二处**：`mc_watch`（看门狗）用同一招构造"有人喊你/你被打了一下"的提示行，
  拿不到宿主包时它会**退化去调 `sessionController.prompt`** —— 那是**用户来源**消息，
  会在对话里冒充你说话。现在它也走 `user-message.mjs`，不再有那条退化路径。
- 自检新增断言：共用模块必须在 · 自带等价实现必须在 · 形状对齐 · 仍优先宿主实现 ·
  看门狗也用同一个模块 · 并**报出本次跑的是宿主实现还是自带实现**。
- 验证方式：造了一个"**只有 `dsh-llm` 缺失**"的环境（正是那台机器的情形）跑完整自检 ——
  全绿（修之前那里有 9 条红：看门狗退化成用户消息），关键投递断言照样通过。

### 改进（投递失败不再静默）

- 拿不到宿主的消息构造函数时，以前只是**静默返回**（表现成"工具都好使、提示词一条都没有"，
  还查不出原因）；现在会明确记一行日志说明原因。


## [0.1.3] - 2026-09-17

### 默认行事准则（`.whale-craft/RULES.md`）更新

- 「称呼」不再写死：**默认叫 Master，用户指定了就按用户指定的来**（"以记忆为准"那条保留）。
- 对 **DSH** 这个称呼/缩写同样敏感（以前只写"AI 相关称呼"）。
- 「看门狗」里的错字改正（"一遍在用户提到你时…" → "**以便**在用户提到你时…"）。
- 「聊天」新增一条：**用户在游戏里跟你说话时，第一时间先回一句**，别让他干等；
  之后再做深度思考和行动，必要时干完再回一次。
- 其余不变（记忆、边界信息、登录流程、看门狗单通道、硬规矩）。

### 修复（部分 DSH 版本上「切不进 MC 模式」）

- **插件自动创建的「MC模式」在部分 DSH 版本上加载失败**，切换时报：
  `无法切换到「MC 模式」：… persona (@deepseek-ai/dsh-persona): invalid config: - $text missing required value`。
  原因：persona 的"人设正文"字段名**跨 DSH 版本变过** —— 老版是 `text`，新版是 `prefix`，
  而此前我们**写死了 `prefix`**。
- 现在**两种都支持**：键名**跟着你那份 DSH 自带的源 preset** 走（源用 `text` 就用 `text`，源用 `prefix`
  就用 `prefix`），我们只替换它的正文值；**绝不新增对方 schema 里没有的键**；认不出结构就一行都不动并记日志。
- **启动自检 + 自动修复**：每次启动都检查**插件自己建的**那份 preset，发现 persona 键名与本版本不符
  （或 `complete: true` 还在）就自动改对 —— **升级插件即可修好老环境**，不必手工编辑 preset 文件。
  （自建规格 `MC_PRESET_SPEC` 升到 6：升级时会把旧规格建的 preset 重建一遍。）
- 边界不变：**只有插件自己建的 preset 会被动**；你手写/改过的那份（无自建标记或组成被改过）一律不碰。

### 修复（切出 MC 模式后，工具白名单不会撤销）

- **从「MC模式」切回普通模式后，那个会话的工具面仍然停留在 MC 白名单上** —— 表现是普通模式的会话
  连 shell 都没有（没有 `pwsh`，也没有 `subagent` / `workflow` 这些），看起来像"标准模式坏了、
  命令执行不了"。原因：`tools.restrict()` 造成的可见性限制是**黏**的（挂在会话自己的 scope 上，
  会话不死就不消失），而插件此前**只"套用"、从不"撤销"**（`restrict()` 返回的撤销手柄被丢掉了）。
- 现在**双向**：切进 MC模式 → 套白名单；**切出 → 撤销**，并把该会话从"MC 模式"兜底名单里移出；
  **再切回来还能重新套上**（不会一次撤销就永久不管了）。
- 「管理工具硬锁」与「文件只能读写 `.whale-craft/`」那两道本来就在**每次调用现场判定**，
  切出模式即刻失效；只有工具白名单是黏的，这次补上了与它对称的撤销。

### 修复（切出 MC 模式后，MC 提示词与游戏事件仍会灌进普通会话）

- **切到「MC模式」再切回普通模式，MC 的行事准则 / 版本提示 / 记忆索引照样会被注入**。
  原因：这几条是**排队投递**的（放进会话的"待投递"队列，**到下一条消息进来时才真正投递**）。
  "切到 MC模式 → 一句话没发 → 切回普通模式"的会话，那几条还躺在队列里等着，你一开口就投进了一个
  **普通模式**的会话。
- 现在切出 MC 模式时**撤回**它们：还在队列里的**直接删掉**；已经进过对话（摘不掉）的，
  补一条**一行作废声明**（"本会话已退出 MC 模式，上面那条行事准则自此刻起作废"），
  免得模型继续按 MC 的规矩办事。**切回 MC 模式会重新投递**（不是一次撤销就永久不管）。
- **看门狗不再往已退出 MC 模式的会话里灌游戏事件**：狗是"黏"的（挂载状态在会话上，切模式不会自动关），
  以前切回普通模式后它仍会把"有人叫你 / 你被打了一下"注进会话。现在加了一道**模式闸门**：
  切出即闭嘴、**切回自动恢复**（不必重新挂载）。
- 说明：**bot 本身不会被自动踢下线**（避免切一下模式就断开你正在玩的游戏）。
  退出 MC 模式后想下线，说一声或调 `mc_disconnect`。

## [0.1.2] - 2026-09-17

### 修复（重要：0.1.1 里有一条会把**整个 DSH 带崩**的路径）

- **一条没人接的 Promise 拒绝会让 DSH 进程直接退出**（控制台表现：`dsh: fatal load failure:
  Error: ForbiddenOperationException`，然后进程没了）。两个成因都堵掉了：
  - **离线账户被当成"有凭据"**：连接时会把假令牌递给会话服务器，被认证服拒绝。现在按账户类型给开关：
    **离线账户不报备**（`haveCredentials=false`），只有皮肤站账户才带令牌去会话服务器。
  - **第三方库的回调 / Promise 不兼容**：皮肤站令牌失效时，`minecraft-protocol` 用老式回调调
    yggdrasil 的会话 join，而新版 `join` 是 async 且**不调那个回调** ⇒ 拒绝无人接管。
    插件侧补了一层兼容：替它把回调调起来，并保证不再有悬空拒绝。
  - 同类风险一并处理：登录后回写账户信息、启动刷凭据索引、preset 自建自检这三处"顺带做的事"
    现在都有兜底，失败只记日志。
- **认证被拒时给出可执行的指引**：不再是天书异常，而是"这个账户的登录状态已失效 ——
  请到「MC设置」重新登录它（或点「刷新」）"。
- 新增**"遗言"记录**：万一宿主仍因未处理拒绝退出，插件会在它退出前把那条拒绝的调用栈写进插件日志，
  下次不必再靠猜。

### 其它

- 仓库新增本机发版工具（`npm run publish:npm`）与 `RELEASING.md`；CI 的发布工作流**不再碰 npm**，
  产物改 **zip**、Release 正文取自本文件（不再自动生成 diff 链接）。

## [0.1.1] - 2026-09-17

### 新增

**「文件分享」：发布区里的文件怎么给你**

- 「MC设置」多了一页「文件分享」，两种模式（默认**关闭**）：
  - **关闭**：AI 调 `mc_kit_express` 时只会得到一句提示，于是把文件的**绝对路径**告诉你，你自己打开；
  - **在线**：AI 得到**完整 URL**，图片可以直接在对话里显示。
- 分享地址改为 `<base>/api/whale-craft/express/<工作区 uuid>/<相对 .express 的路径>`。
  用 DSH 工作区注册表里的稳定 id，而不再是目录名 —— 不同父目录下的同名工作区不会撞，
  **目录改名后链接也不失效**，URL 里也不暴露目录名。
- 在线模式需要填 `base`（你访问这台 DSH 用的地址）。base 那一行有「**获取当前**」：
  一键用你现在访问页面的地址填好并保存；切到「在线」而 base 还空着时也会**自动**填一次。
  不切模式、不点按钮，则不会去动它。
- 「**清除分享数据**」：二次确认后清空当前工作区 `.express/` 里的所有文件（目录本身重建）。与分享模式无关，随时可点。
- 只有「在线」模式会开启那条访问路由；关闭模式下访问一律 404。

**「随版本更新」：提示词跟着插件版本走**

- 「MC设置 → 提示词」新增开关，**默认开启**：插件升级后，用新版本的默认行事准则替换
  `<工作区>/.whale-craft/RULES.md`（**会覆盖你的修改**）。判定靠记忆目录里的 `.rules-version` 标记。
- 想长期保留自己那份，把它**关掉**即可：关掉后插件永不动它，而且**不会"攒着"**——
  以后再打开也不会突然覆盖。
- 从 0.1.0 升级到 0.1.1 这一次**不会**替换你的文件（第一次遇到这个开关只记下版本号）。

### 修复

- **`GET /api/mc/presets` 在真机上一直返回 404**（漏在了设置接口的分派名单之外），
  导致界面只能靠内置兜底名单判断哪些 preset 算 MC 模式。现在可达。
- `mc_map` 的工具说明不再声称"返回值里带现成的 markdown"——那个字段早已移除，
  取交付一律走 `mc_kit_express`。

### 变更（不兼容）

- 「文件分享」的旧地址 `/api/mc/whale-craft/<工作区目录名>/…` **已停用**（一律 404），
  旧链接请让 AI 重新取一次。
- 原先的「Windows 本地」模式**已移除**（它只是把绝对路径原样回给 AI，而前端不认本地路径，
  点不开也内联不了）——只剩「关闭」和「在线」。
- AI 拿到的交付内容只来自 `mc_kit_express` 那**一行**；别的工具返回值里不再有任何分享字段。

### 自检

- 离线断言 512 → **594** 条：新增文件分享两种模式、工作区 uuid 解析与查找、
  「随版本更新」的四种情形、发布区新路由与信任栅栏、`clientOrigin` 精准取值等。

## [0.1.0] - 2026-09-16

首个发布。

- 每会话一个独立的 Minecraft 机器人（mineflayer 直接作为依赖），28 个工具：
  `mc_*` 24 个 · `mc_kit_*` 3 个 · `mc_admin_config` 1 个。
- 单通道看门狗：事件只走 `mc_watch`，空闲唤醒、生成中插话（提示词注入，不冒充用户发言）。
- 长期记忆：工作区 `.whale-craft/` 文档树，索引由 AI 维护，会话开始时作为插件提示注入。
- 「MC设置」：账户（离线 / 第三方皮肤站；密码只进宿主凭据库）、指令白名单、提示词（`RULES.md`）。
- MC 模式权限隔离：无条件白名单 + 文件工具限制在 `.whale-craft/` 内。
- 发布区 `.whale-craft/.express/`：插件自带的文件交付通道（目录即白名单、不用 token、防穿透）。
- 工作区 `.whale-craft/RULES.md` 行事准则 + 随版本发布的**版本硬提示词**。
