## 测试体系

- 新增 `npm test`（`node --test`，零第三方依赖，Node ≥ 20 内置测试框架）
- `test/` 目录 6 个测试文件、52 个用例：
  - `validation.test.mjs` — session 结构/sessionKey/参考名清洗校验
  - `routing.test.mjs` — ref_idx 精确路由、署名兜底、#to 解析（纯函数，依赖注入）
  - `lock.test.mjs` — 文件锁互斥/死亡接管/同 PID 恢复/心跳（临时目录 + 子进程）
  - `registry.test.mjs` — registry 读写/认领唯一性/leader 记录/pruneDead/认领展示信息（`QQ_INTEGRATION_DATA_DIR` 隔离）
  - `ipc.test.mjs` — IPC 真实 socket 收发/同 id 重连接管/非法注册断开
  - `command.test.mjs` — 命令分发（#create/#close 多 PID/#sessions 分页/#instances 认领展示/移除命令引导）
- 纯逻辑抽离到独立模块：`validation.ts`（校验）、`routing.ts`（路由），index.ts 直接 import（不再测试逻辑副本）

## 0.5.3

- 修复（Windows）：命名管道路径转义不足导致启动报 `EACCES: permission denied \.pipepi-qq-<pid>`、QQ Bot 连接失败（issue #3）；修正转义后 Windows 下 IPC 监听与 QQ 连接恢复正常

## 0.5.2

- 新增 `#create`：leader 从 QQ 侧 spawn 新的 pi 实例（follower）——`#create <序号/名称>` 复用现有 session（`--session` + session 头部真实 cwd），`#create new [--dir <目录>]` 全新 session（缺省当前实例 cwd）。rpc mode headless 长驻，`tail -f /dev/null |` 保活 stdin（不随 leader 生命周期），实例 ID = PID
- 新增 `#close <实例ID>`：关闭指定实例（registry 匹配 + `/proc/<pid>/cmdline` 校验防误杀，SIGTERM → 超时 SIGKILL）；可关闭 leader（先回复再退出，剩余实例自动重新选举）
- `#sessions` 改为显示全部 session（不再按实例目录过滤），按最近使用时间降序，支持分页（每页 10 条，`#sessions <页码>`，全局序号跨页连续，直接用于 `#create`）
- 移除 `#resume` 后，`#create <序号/名称>` 复用 session 时与 `#sessions` 全局序号保持一致
- session-manager 新增 `formatSessionListPage`（分页）、`getSessionCwd`（读 session 头部 cwd，零歧义）、`unencodeProjectDir`（项目目录名反解，existsSync 消歧）
- `#close` 支持多 PID：`#close 123 456` 空格分隔逐个关闭并汇总回复（✅ 已关闭 / ❌ 失败原因）；多 PID 含自己时先关其他、自己最后退出
- `#instances` 展示每个实例的认领会话（缩进列表）：名字优先（QQ 会话名），无名字用最近消息摘要，60 字符截断 + 相对时间；claim 时把会话名字/最近消息写入 registry（`claimedSessionInfo`，向后兼容旧数据）
- `#sessions` / `#instances` 摘要截断统一为 60 字符（`SESSION_PREVIEW_LEN`，#history 仍为 300）
- 修复：`#create`/`#close` 命令分发缺失（case 未编译进 switch，导致 `#create new` 提示未知命令）；新增 `command.test.mjs`（11 用例）覆盖命令分发/spawn 参数/分页/移除命令引导
- 修复（审查）：follower claim / reroute 不再清空认领展示信息（`setClaim` 未提供 info 时保留现有；IPC `claim` 信封透传 `info`，leader 侧清洗后落库）
- 修复（审查）：`#sessions` 页码越界时 clamp（页脚显示实际页，不再出现「第 99/2 页」）；spawn 补 `child.on('error')` 监听防 uncaughtException 拖崩宿主
- 版本号对齐：package.json / package-lock.json bump 至 0.5.2（此前 lock 文件滞后在 0.2.0，直接发版会导致 npm publish 发布错误版本）

- 修复：`#history` 在多实例下展示错误实例的消息——原实现取「全局最近修改」的 session 文件（可能属于其他实例，导致看到的不是当前实例的对话）；改为通过当前实例的 `sessionManager.getSessionFile()` 定向读取本实例的 session 历史（session 切换后自动指向新文件）；拿不到时回退旧逻辑
- 移除 `#resume`/`#new`/`#clear`：这三个命令基于不完整的 hack（绕过命令流程，上下文不清空/不加载），已被 `#create` 取代——复用 session 或全新 session 都通过 spawn 新实例实现，行为完整

## 0.5.1

- 新增 GitHub Actions 自动发布 workflow（`.github/workflows/release.yml`）：push `v*` tag 后自动执行 typecheck + test、从 CHANGELOG 提取对应版本段创建 **GitHub Release**、`npm publish` 发布到 npm（首次需在仓库 Secrets 配置 `NPM_TOKEN`；也可手动 `workflow_dispatch` 触发）
- 新增 `scripts/extract-changelog-notes.mjs`：从 CHANGELOG 提取版本段落（Release 正文来源）

## 0.5.0

**新功能：多实例消息来源标识 + 定向回复路由**

### 新增
- **出站消息统一署名**：所有发往 QQ 的消息自动加 `> 【session名-PID】` 引用块署名（署名行与正文以空行分隔），用户可一眼辨别消息来自哪个 pi 实例
- **标识统一**：实例内部唯一标识默认 = PID（`#to <PID>` 切换，去掉了 hostname）；**出站署名显示 `> 【session名-PID】`**（未命名 session 时 `> 【PID】`），引用路由按署名尾部 PID 兜底匹配
- **leader 故障转移**：follower 重连循环中定期尝试接管锁（`lock.acquire`，检测旧 leader PID 死亡/锁释放），成功后自动升级为新 leader，避免 leader 退出后 follower 无限重连；配置 `role: follower` 强制跟随时不升级
- **follower 静默重连**：与 leader 断开后仅写日志、不刷 UI 提示（与 leader 侧 QQ WS 静默重连一致），连接成功才通知；重连前先尝试接管锁升级
- **实例显示名 = 当前活跃 pi session 名**：自动取 pi session 名（`/rename` 可自定义）；session 切换或重命名时自动同步到注册表（未命名 session 时署名回退为 `> 【PID】`，无 hostname 回退）
- **引用消息定向路由**：用户在 QQ 中「引用」某条消息回复时，按被引用消息的来源实例精确路由（基于 `ref_idx` 映射，60 分钟 TTL）；映射未命中时按消息署名唯一匹配兜底
- **`#to <实例> [内容]` 命令**：查看当前会话绑定实例 / 切换会话到指定实例 / 定向发送内容；支持含空格的实例名（最长前缀匹配），重名时提示使用 instanceId
- **`#instances` 命令**：列出所有在线实例（显示名/角色/认领会话数）
- 群聊（GROUP_AT_MESSAGE_CREATE）同样支持引用消息解析（需实测确认）

### 安全加固（审计后）
- 修复：`#to` 确认回复不再回夺会话认领（follower 场景下切换失效的严重 bug）
- 修复：异步/延时回调（IPC onConnect/onClose、WS onAuthFailed/onFatalError、锁接管）捕获的 ctx 在 `/reload` 或 session 替换后失效，访问 `ctx.ui` 抛 uncaughtException 会拖崩宿主 pi；改为 `safeNotify` 包装捕获，仅跳过 UI 通知（日志不受影响）
- 安全：IPC 新增端点（inject/reroute/instance_update）全量结构校验 + 权限约束（inject 仅合法实例、reroute 仅当前 claimer）
- 安全：register 连接级唯一性校验（防伪造实例条目/身份接管）
- 安全：引用署名兜底改为唯一匹配 + 校验被引用消息确为机器人所发（防消息定向劫持）
- 安全：session 名重名时路由拒绝（唯一匹配才命中，防定向歧义；实例内部标识为 PID，天然唯一）
- 安全：isAllowed 未知会话类型一律拒绝（防伪造类型绕过白名单）
- 安全：`#instances` 展示 instanceId（默认即 PID，供 `#to` 定向），不再暴露主机名
- 加固：显示名清洗（去控制字符/限长）、markdown 转义增强、refIdxMap 硬上限驱逐、leader 命令回复也记录 ref_idx

### 已知限制
- registry 多进程写入为无锁 read-modify-write（原子写防损坏，但跨进程存在极小丢失更新窗口；依赖 30s pruneDead 收敛）
- 引用路由的精确匹配依赖 QQ API 返回 `ext_info.ref_idx` 与引用事件 `ref_msg_idx`，需真实环境实测；未返回时自动降级为署名唯一匹配
- `#to <名> <内容>` 中实例名与内容存在前缀歧义时（如 `web` 与 `web dev`），优先匹配更长实例名，可用 `#to` 切换后单独发消息规避

# Changelog

## 0.4.4

- 安全加固：新增 QQ 消息白名单（`allowedUsers`/`allowedGroups`）防远程提示词注入
- 安全加固：配置/日志/registry/lock 文件权限收紧为 0600/0700
- 安全加固：IPC 消息大小限制（防 OOM）、session.id 校验（防路径穿越）、auth 错误响应体截断
- 修复：API 401 时真正强制刷新 token（此前未绕过本地缓存）
- 修复：IPC settings_update 增加 schema 校验与互斥归一
- 修复：Windows 下 IPC 改用命名管道路径（`\\.\\pipe\\...`），server 监听失败通过 ready Promise 冒泡而非 uncaughtException 拖崩宿主进程（修复思路来自 @illusionlie 的 PR #2）
- 文档：README 中英文拆分为独立文件（README.md + README.zh-CN.md），新增贡献者展示
- 文档：修正 12 处文档与代码不一致（#settings 示例、日志截断说明、REST 端点、/qq-target 用法等）

## 0.4.3

- 更新 npm 包描述为中英双语。

## 0.4.2

- README: 重写为中英双版，英文在前默认。

## 0.4.1

- 修复 `agent_settled` 事件处理器中访问 stale `ctx.sessionManager` 导致的崩溃（`lastMessageOnly=true` 时触发）。改为在 `message_end` 时缓存 assistant 消息内容，`agent_settled` 直接使用缓存，不再依赖可能过期的 ctx。

## 0.4.0

**重大重构：消除全部硬编码 + 修复多实例/网络/并发场景下的 22 个问题。**

### 新增
- 新增 `constants.ts`：集中管理所有常量（路径、API 端点、超时值），支持环境变量 `QQ_INTEGRATION_DATA_DIR`、`QQ_API_BASE`、`QQ_TOKEN_API` 覆盖。
- Settings IPC 同步机制：`#settings` 变更通过 leader 统一执行并广播给所有 follower，保持多实例内存状态一致。
- Auth 致命错误机制：Token 连续刷新失败 3 次后自动断开连接并通知用户，避免静默退化为僵尸状态。
- WS 鉴权失败回调 `onAuthFailed`：InvalidSession 时通知上层并 teardown。
- IPC `broadcast()` 方法：leader 可向所有 follower 广播消息。

### 修复
- **锁 TOCTOU 竞态**：`lock.ts` 改用 `openSync(path, "wx")` 原子创建（O_EXCL），消除两进程同时抢到锁的可能。
- **配置文件并发写入**：`config.ts` 和 `registry.ts` 改用 `atomicWrite`（写临时文件 + rename），消除 truncate 窗口导致配置损毁的风险。
- **`saveSettings` JSON.parse 失败保护**：解析失败时放弃写入，保护 appId/appSecret 不被覆盖为空。
- **WS `connect()` 过早 resolve**：延迟到 READY/RESUMED 事件后 resolve，确保 UI "已连接" 与实际鉴权状态一致。
- **WS 重连无限循环**：增加指数退避（1s → 2s → ... → 60s 上限）。
- **WS 超时未 terminate**：超时时 `ws.terminate()` 强制关闭半连接。
- **WS close handler 竞争**：`if (_ws === ws)` 守卫防止旧 close 清空新连接引用。
- **Follower 退避重试**：指数退避（2s → 4s → ... → 30s 上限），避免对死 socket 的固定间隔轮询。
- **IPC server close**：主动 `destroy()` 所有已有连接，而非仅停监听。
- **Follower teardown 自清 registry**：`removeInstance` 移到角色判断外，任何角色退出都清理自己的注册表条目。
- **Leader 启动立即 pruneDead**：`becomeLeader` 后立即执行一次清理，消除 30 秒延迟窗口。

### 重构
- 全部硬编码路径、URL、超时值、数值常量收口到 `constants.ts`，支持环境变量覆盖。
- 消除 `session-manager.ts` 中的 `"nullsky"` 用户名硬编码，改用 `userInfo().username`。
- 所有文件移除 `homedir()` 直接调用，统一引用 `PATHS`/`DEFAULTS`。

## 0.3.6

- 新增多实例支持（方案 A：文件锁选举 + 本地 Unix socket IPC 委派）：
  - 多个 pi 实例共享一个 QQ Bot 连接——抢到文件锁的为 leader（持有 QQ WebSocket），其余为 follower（经本地 IPC 把 QQ 收发委托给 leader），避免多实例各自连接被踢。
  - 新增 `registry.ts`（实例注册表）与 `ipc.ts`（IPC 服务/客户端）。
  - `config.role` 可强制 `auto`/`leader`/`follower`；`config.instanceId` 可固定实例 ID。
  - QQ 入站按会话认领（claim）路由到对应 follower；出站经 IPC 转 leader 发送。
- 新增 `autoConnect` 配置项（默认 `true`）：pi 启动时自动连接 QQ Bot；设为 `false` 则需手动 `/qq-connect`（撤销了 0.3.5 的"不自动连接"行为）。
- 更新 README：新增完整「配置项」章节，列出全部可配置字段（appId/appSecret/instanceId/role/autoConnect 及 settings 各项）。

## 0.3.5

- 修正 README 中"pi 启动时自动连接 QQ Bot"的错误描述，实际需手动输入 `/qq-connect` 连接。

## 0.3.4

- 修复 `lastMessageOnly` 转发重复问题：原实现监听 `turn_end` 事件，但 `turn_end` 每轮（turn）触发一次，agentic 模式下多步工具调用任务会在 QQ 中产生多条转发。
- 改用 `agent_settled` 事件（整次 agent 运行仅触发一次），从 `sessionManager.getEntries()` 取最后一条 assistant 消息转发，确保 QQ 只收到一条最终回复。
