# Changelog

本文件记录 bohr CLI 对用户可见的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)，版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。

## 编写约定

- **每个改动用户面行为的 MR 必须在 `[Unreleased]` 下补一条**，发版时整体移到新版本号下。
- 分类固定为：`Breaking`（破坏性变更）/ `Added`（新增）/ `Changed`（变更）/ `Fixed`（修复）/ `Deprecated`（废弃）。
- **每条不超过 100 字，`Breaking` 也一样**，只写用户可感知的变化和必要用法；实现细节、设计理由、故障复盘与验收证据留在 commit message、MR 或设计文档中。「1–2 句话」是软的——四百字的长句字面上也算两句，2.6.34/2.6.38/2.6.39 的多条都是这么跑偏的（全库中位数 215 字）。条目还会整条进发布通知卡片，而飞书自定义机器人有 20 KB 上限，超了退化成只显示标题。字数按卡片上显示的文字数（加粗与反引号不算），由 `go test ./test/changelog` 检查 `[Unreleased]` 与 2.6.101 之后发布的小节。
- **不写登记行号、MR 号等内部标识**。CHANGELOG 面向用户，登记表是内部工具——用户不需要知道它的存在；追溯信息本来就在 commit 与 MR 里。
- `Breaking` 一条至少写清三件事：**改前行为、改后行为、调用方要怎么改**。历史上 2.5.1 废除 `job download/log` 位置参数、2.5.6 把 `node create --disk_size` 由可选改必填，两次都没有公告，评测侧与 skill 首跑即挂——这一节就是为了不再发生第三次。
- **发版时必须把 `[Unreleased]` 下的条目整体搬到新版本号下**。这一步由 CI 强制：打 tag 后 `scripts/verify-changelog.sh` 会要求 `## [<版本号>]` 一节存在且非空、且 `[Unreleased]` 已清空，不满足则 build 阶段直接失败，发不出去。本地可先跑 `sh scripts/verify-changelog.sh` 自查。
- **发版只走打 tag 这一条路**。手动 `npm publish` 绕开上面整套门禁——2.5.8 就是这么发出去的：构建自 2.5.7 的 commit、`vcs.modified=true`、不带本文件，而本文件当时把五条 batchjob 改动记在了它名下，实际一条都不在包里。一个版本号写进 CHANGELOG，就必须能在该版本的发布物里探到对应改动。

## 已知的归属错误

下面这条**不订正**：已发布小节一旦发出去就冻在 npm 包里，改仓库里这份只会让两份文件对同一个版本给出两种说法，而用户手上那份改不了。所以真相记在这里，历史留在原处。

- `## [2.6.44]` 里的「**配套扩展可完整管理**：CLI 或 npm 安装的 trisol/wenyon 现可被 list/info/verify/pin/remove 识别」实际随 **2.6.46** 发出，不在 2.6.44 里。成因：2.6.45 从未打 tag，2.6.46 发版时不可变门禁的基线仍是 2.6.44，而当时的规则只查「上一版条目有没有丢」、不查「有没有多」，这条便落进了基线自己那节并被此后的逐字保护冻住。同类情况已由 2.6.94 起的规则 E 在门禁层挡住。

---

## [Unreleased]

## [2.7.7] - 2026-09-22

### Breaking

- **`widgets versions publish` 去掉 `--version`**：平台已改为自动分配版本号并拒收指定版本号的请求，原写法每次都失败；删去该参数即可。

### Added

- **Widget 发布说明**：`widgets versions publish --release-notes` 随版本附带发布说明，最多 10000 字符。

### Fixed

- **Widget 校验与宿主对齐**：`validate` 不再放行 `behaviors`（宿主尚不接受，上传会被拒），脚手架退回 `widget-sdk@0.0.3`，撤回 2.7.5 的对应改动。
- **Widget 版本发布与读取恢复**：`versions publish` 按平台分配的 `versionNumber` 核对回执；`versions get` 能读新发布的版本（其 `version` 为 null）。
- **`widgets bindings set` 不再误报失败**：提交与现状相同的绑定时平台不涨 revision，CLI 原先把这次成功报成回执无效。

## [2.7.6] - 2026-09-21

### Added

- **`lkm search` 增加论文过滤**：论文 ID/DOI/标题、发表日期、可见性，以及可选的论文富化元数据。
- **`lkm graph` 四选一标识**：`--package-id`/`--paper-id`/`--doi`/`--title`，标题可配 `--title-resolve-limit`。
- **`lkm reasoning` 对齐检索过滤**：`--keywords`/`--mode`/`--sort`/`--offset`，以及同一套论文/日期过滤（不含 visibility）。

### Fixed

- **`lkm search --mode` / `--sort` 现在会生效**：此前改通道或排序没有效果。

### Changed

- **超限参数改为本地失败**：`--keywords`/`--offset`/`--top-k`/`--scopes` 与 `variables batch` 超限不再请求服务端。
- **`lkm search` / `reasoning` 的 `--mode` / `--sort` help 只列可选值**：不再写出接口字段名。
- **`lkm graph --paper-id` 不再单独必填**：改为与 `--package-id` / `--doi` / `--title` 四选一。
- **`lkm --help` 补了 LKM 是什么**：把论文变成可溯源推理图；本地 PDF 用 `extract`。
- **`lkm extract` help 收紧选命令**：`--format graph` 只是同类字段、不是 `lkm graph` 信封；cache hit 仍看 `status`。
- **`lkm extract` / `submit` 写清单价**：成功抽取一篇 1.00 元，命中缓存 0.10 元。
- **`lkm claim reasoning --format` 只接受 `graph`**：不再转发任意旧格式。

### Deprecated

- **`lkm parse` 请改用 `lkm extract`**：旧入口仍可用并打告警，公示自 2026-09-22 起 14 天；脚本和 skill 请改用 `bohr lkm extract`。

## [2.7.5] - 2026-09-21

### Added

- **Widget 契约升到公网 0.0.4**：脚手架改用 `widget-sdk@0.0.4`，manifest 可声明 `behaviors`（仅 `panel.open`，限 panel slot）。

## [2.7.4] - 2026-09-20

### Fixed

- **沙盒与 Batch Job 统一支付参数**：`--payment-source photon|balance` 选择订单渠道；默认仍扣余额，个人光子无需项目 ID，不自动切换渠道。
- **支付参数兼容与校验**：旧 `--payment-type 0|1` 继续可用；新旧参数冲突在请求前拒绝，沙盒项目预算与光子仍互斥。
- **Widget 脚手架可从公网安装**：三个 slot 改用公开的 `@dp-bohrium/widget-sdk`，无需内部 npm 凭据即可安装和构建；同步离线契约与开发指引。

## [2.7.3] - 2026-09-20

### Added

- **`whoami` 与 `login` 标出个人或 Team/企业账户**：企业带 `bizCode`，并警告两套账户不通。`status --verify` 同口径。

- **沙盒支持光子支付**：默认仍用余额；`sandbox create --payment-type 1` 选择光子。非法值及项目预算与光子冲突在创建前拒绝，需后端支持。

### Changed

- **Team/企业账户隐藏身份查询中的余额**：`whoami` 和 `status --verify` 的人读与机器输出均省略余额及余额充足状态，个人账户保持原有展示。
- **`whoami` 不再原样回声 ak/get**：只保留 `user_id`、`orgId`、`account` 和 Team/企业的 `bizCode`。

- **沙盒费用区分支付单位**：列表按渠道标注 CNY 或 photons，光子优先使用 `photonCost`；保留缺失费用与零费用的区别，JSON 保留原值。

## [2.7.2] - 2026-09-20

### Breaking

- **参数值错误统一分类**：支付来源、批量作业支付类型、结构检索参数值不合法时，`COMMAND_FAILED` 改为 `VALIDATION_FAILED`；调用方按新码修改参数。
- **交付物空列表统一为数组**：`agents artifacts list` 无结果时由 `artifacts: null` 改为 `artifacts: []`（含 `--all-files`）；调用方按数组长度判空。

### Fixed

- **停止批量作业的确认等待遵守时限**：`batchjob kill --confirm-timeout` 也约束挂住的状态查询；到期报告已受理但未确认，提前取消则明确提示确认中断。

## [2.7.1] - 2026-09-19

### Changed

- **中断后的退出码**：自行处理中断的命令（如 structure search 下载）被 Ctrl-C 后返回自身的退出码，不再是 130；未处理中断的命令不变。

### Fixed

- **只停启动器也能停下 CLI**：`timeout`、Agent 框架超时只给 `bohr` 进程发信号时，CLI 不再成为孤儿继续运行、占着输出，而是照常处理中断后退出。
- **结构下载被挂断时不留临时文件**：关掉终端或收到 SIGHUP 时，`structure search --download` 同样清理临时文件并报下载已取消。

## [2.7.0] - 2026-09-18

### Breaking

- **`bohr file list -o json` 改为标准列表**：`objects` 改名 `items`，翻页改读 `pagination.has_more`、`next_token`。
- **`sandbox`/`batchjob machine list -o json` 合成一张表**：`cpu`、`gpu` 并入 `items`，按每行新增的 `class` 区分。
- **`bohr project list -o json` 去掉顶层 `totalPage`**：判断有无下一页改读 `data.pagination.has_more`。
- **移除 15 条已过公示期的弃用项**（3 条命令、11 处 flag 旧名、`-o pretty`）：此前带告警仍可用，现在直接报错；改法见下表。

本版移除下列已过公示期的弃用项。旧名 → 新名的映射只在这里，运行时的报错不会说新名字。

**命令**

| 移除 | 改用 |
|-|-|
| `bohr database list` | `bohr database polymer list` |
| `bohr mentor` | `bohr agents mentor` |
| `bohr sandbox image` | `bohr image` |

**flag 与格式常量**

| 移除 | 改用 |
|-|-|
| `--space` （bohr file list） | pass the space as a path prefix instead: --space personal with <path> becomes personal/<path>, and --space share becomes share/<path> |
| `--space` （bohr file upload） | pass the space as a path prefix instead: --space personal with <path> becomes personal/<path>, and --space share becomes share/<path> |
| `--projectId` （bohr dataset list） | `--project-id` |
| `--project_id` （bohr job submit） | `--project-id` |
| `--project_id` （bohr job_group create） | `--project-id` |
| `--projectId` （bohr job_group list） | `--project-id` |
| `--project_id` （bohr node create） | `--project-id` |
| `--project_id` （bohr project delete） | `--project-id` |
| `--datasetId` （bohr dataset delete） | `--dataset-id` |
| `--imageId` （bohr image delete） | `--image-id` |
| `--chooseType` （bohr machine list） | `--choose-type` |
| `-o pretty` | `-o json` |

- **`bohr file upload` 的远端路径须带 `personal/` 或 `share/` 前缀**：此前无前缀会被补全并告警，现在报 `VALIDATION_FAILED`。

### Added

- **`bohr file list --next-token`**：把上一页的 `data.pagination.next_token` 传回来取下一页；人读输出在还有下一页时直接给出这条命令。

## [2.6.103] - 2026-09-18

### Fixed

- **人读输出展开嵌套对象**：`auth status --verify` 的 identity 不再打成 `map[orgId:4.56e+06]`，ID 显示为整数。

## [2.6.102] - 2026-09-18

### Breaking

- **`database structure search` 部分成功不再报 `UNKNOWN`**：有结构即成功并警告缺失；全无报 `UPSTREAM_ERROR`。按码分支的脚本需改。
- **`database structure search --download` 下载失败不再报 `COMMAND_FAILED`**：改按原因报超时、网络或存储端错误并带重跑命令；脚本需改判据。
- **`database structure search --download` 没拿到可用的归档链接时不再报成功**：改报 `UPSTREAM_INVALID_RESPONSE`；脚本需改。

### Fixed

- **`bohr auth whoami` / `auth status --verify` 的 `user_id` / `orgId` 一律显示为整数**：上游打成数字字符串时 JSON 也不再带引号。
- **`database structure search --download` 限时 120 秒**：不回包不再挂着；失败或被中断都不留半个文件。
- **`database structure search --download` 中断时报 `STRUCTURE_ARCHIVE_DOWNLOAD_CANCELLED`**，带重跑命令。
- **`database structure search` 人读输出补上网关下发的弃用提示**。

## [2.6.101] - 2026-09-16

### Added

- **Widget 开发与上传**：`agents dev widgets` 支持创建项目、离线校验、打包、上传 ZIP 和查询检查；内置指引见 `skills read widget`。
- **Widget 文件与运行态**：新增 file-preview 和 workspace-files panel 脚手架，分段读取、缺权限展示、取消与销毁；内置事件订阅和动作幂等说明，固定 contracts alpha.5 / SDK alpha.3。
- **Widget 发布与绑定**：新增受预期 digest 保护的版本发布、带草稿 revision 的 Agent 完整绑定读写、owner 样例预览深链与 `--open`。
- **Widget 资源闭环**：新增资源/可选版本查询、发布检查、精确版本查询、上传会话 commit 和样例预览准备；校验服务端 `sha256:` 摘要格式，拒绝缺失或错配的成功回执。预览准备不代表浏览器渲染通过。
- **Batch Job 支持光子支付**：`batchjob submit --payment-type 1` 选择光子，默认 `0` 现金；余额不足不回退现金，非法值在创建前拒绝。

### Changed

- **Batch Job 费用区分支付单位**：详情和列表的人读输出按渠道标注光子或元，缺失费用显示未知；技能文档同步说明支付参数与失败处理。

### Breaking

- **Widget 预览版列表输出**：`widgets list`、`widgets available` 和 `widgets versions list` 的 `data.widgets` / `data.versions` 改为标准 `data.items` 并增加 `data.pagination`；使用旧预览版字段的脚本需同步修改。正式版首次提供此功能。

### Fixed

- **Widget 预览兼容性**：预览请求携带目标平台 Origin，并支持当前 Runtime 契约；自定义网关需指定 `--platform-origin`。

## [2.6.100] - 2026-09-14

### Breaking

- **`file download` 路径不存在改报 `RESOURCE_NOT_FOUND`（2.6.87 遗漏补记）**：原 `UPSTREAM_ERROR`，退出码 1 → 3；脚本需改判据。

### Fixed

- **同一种超时不再时而报成 `NETWORK_ERROR`**：改按错误类型判，一律报 `UPSTREAM_TIMEOUT`（退出码 11）；URL 含 timeout 字样的连不上也不再误报超时。
- **`file download` 连不上时改为建议重试**：`NETWORK_ERROR` 的 `retryable` 改为 true；递归下载会按文件重试，两次重试前分别等 1、2 秒。
- **`file download` 遇网关 5xx 或 429 改为建议重试**：`UPSTREAM_ERROR` 此时 `retryable` 为 true，递归下载会按文件重试。
- **`file download` 失败提示的重跑命令带上原 `--project-id`、`--dest`**：share 路径照做不再报错，递归重跑会跳过已下完的文件。
- **`file stat`/`download` 路径不存在、`file delete` 目录非空的提示命令可原样执行**：带上原 `--project-id`，路径按需加引号；前者此前是占位符。

## [2.6.99] - 2026-09-13

### Breaking

- **flag 写错不再报 `COMMAND_FAILED`**：不存在或缺值报 `INVALID_ARGUMENTS`，值类型不对报 `VALIDATION_FAILED`；都带该命令 `--help` 的 `hint`，退出码不变。按旧码分支的脚本需改判据。

## [2.6.98] - 2026-09-12

### Fixed

- **`bohr tools info` 传了工具名而不是 key 时会指路了**：报错里补上「去 `bohr tools search` 结果的 `Key:` 一栏取 key」，此前这句提示在生产上从未出现。
- **`bohr tools info` 人读输出里的镜像 id 不再带引号**：原先显示成 `(id "110331")`，照着复制出去是错的。

## [2.6.97] - 2026-09-12

### Fixed

- **`bohr doctor` 的 `connectivity` 改查网关健康接口**：此前打的是一条不存在的路由，任何 HTTP 应答都算连通；现在认出是 Bohrium 网关才报 pass。
- **`bohr doctor` 的 `extension_health` 与 `bohr extension list` 一致**：此前漏报 trisol/wenyon；缺二进制记 `warn`。
- **遮蔽补救改名为 `.legacy` 后缀**：原先的 `bohr-legacy` 会被当成扩展（PATH 上 `bohr-*` 都算）。已按旧写法改过的，再改一次即可。

## [2.6.96] - 2026-09-12

### Breaking

- **`bohr sandbox doctor` 没就绪时改为退出 0**：原先退出 1，与命令失败同码。结论改读 `data.ready`；`&&` 短路的脚本要改判它。

### Added

- **`bohr doctor` 新增 `path_integrity` 检查**：独立安装遮住 npm 那一份时记 `warn`，点名两个路径并给出改法；`--offline` 下也跑。

### Fixed

- **`bohr doctor` 不再把 AccessKey 拼进 URL**：此前两项检查把它放进查询串，会进网关与代理日志，断网时还会打在输出里。现在走请求头。
- **`bohr doctor` 的 `cli_version` 真去查最新版了**：此前恒报 pass。落后时给出升级命令；落后或查不到记 `warn`，不影响 `data.healthy`。
- **`bohr update` 更新独立安装时，会点名 npm 管着的那一份**：此前它原地更新完就返回成功，版本号对了、遮蔽还在，调用方从此没有理由再敲 `npm install -g`——而那是唯一会提醒遮蔽的地方。现在 stderr 给出两个路径与 `mv` 改法，`-o json` 的 `data.other_install` 给出同一件事。问不出 npm 根目录、或 npm 没装本包时保持沉默。
- **`~/.vouch/state.json` 记全凭据覆盖的受众**：token 本就替 wenyon 与 trisol 签发，此前只写 wenyon。重新登录即更新，不改变 trisol 连通性。

### Changed（内部，无用户面行为变化）

- 删掉 `scripts/install.sh` / `install.ps1` 两个独立安装脚本。它们从未随 npm 包分发，自身文档教的分发 URL 已失效（405 / 404），安装路径仍然只有 `npm install -g @dptech-corp/bohr-cli`。过去跑过它的机器上 `~/.bohr/bin/bohr` 的残留不受影响，处理方法见 README 的「升级后 `bohr version` 显示的还是旧版本号」一节。

### Changed

- **同名命令在本组更深处也指得出来**：`bohr notebook restart` 补一句 `"bohr notebook server restart" exists`；原先只有顶层或全树唯一才说。

## [2.6.95] - 2026-09-11

### Breaking

- **`bohr skills list` 的 `-o json` 换成标准列表形状**：`data.skills` 改名为 `data.items`，`data.count` 由 `data.pagination.total` 取代；`bohr skills list <名字>` 的 `data.entries` 同样改名 `data.items`。读这两个键的脚本要改字段名。

- **`bohr sandbox list` 与 `bohr sandbox template list` 的 `-o json` 换成标准列表形状**：`data.list` 改名 `data.items`；`data.page` / `data.page_size` / `data.total` 移进 `data.pagination`；`data.total_pages` 去掉，改读 `data.pagination.has_more` 判断还有没有下一页。行本身不变。

### Added

- **八条列表命令的 `-o json` 补上 `data.pagination`**：image / sandbox image / project / dataset / node / machine / extension / skills list 现在都给 `{page, page_size, total, has_more}`，与 `job list` 一致。`has_more` 直接回答「还要不要再发一次请求」。行本身不变。

### Changed

- **`bohr tools info` 的人类输出把镜像放到了前面**：`-o human` 此前只精选介绍文案（Profile/Overview/Summary…），镜像地址、MCP 地址、Dockerfile 都落在末尾一个字母序的兜底段里，而 Dockerfile 是全文（实测 76 行，占输出 37%）。现在 Image / MCP 紧跟 Name，Dockerfile 只给一行摘要并指向 `--output json`。`-o json` 不变，仍是全量。

- **`bohr tools info` 人类输出里的长字段一律压成一行**：不只 Dockerfile——兜底段的任何多行或超长取值都只给首行加行数，并指向 `--output json`。同时修掉 id 被打成科学计数法（`2.0251220175459e+13`）。`-o json` 不变。

## [2.6.94] - 2026-09-10

### Added

- **`bohr extension install` 有了机读回执**：`-o json` 下 stdout 给一份信封，逐个列出工具的名称、动作（installed/kept）、版本、安装路径与哈希。此前 stdout 一个字节都没有。进度仍在 stderr，人类输出不变。

- **CHANGELOG 已发布小节多出一行会被门禁挡住了**：此前只查「上一版条目有没有丢」、不查「有没有多」，新条目被 rebase 静默填进已发布的小节时全绿，到下一次发版就被逐字保护永久冻死。本条不含 CLI 行为面改动。

### Fixed

- **`bohr extension install` 拿不到可用 SHA256 就拒装**（与 npm 安装同一口径）：此前少给哈希会跳过校验、假二进制照装还返回成功，trisol 空校验文件更会 panic。空/空白/HTML/截断/缺字段五种都在下载前拒。代价：发布方不给哈希时安装会失败。

- **扩展下载不再撞 60 秒总时限**：此前元数据请求与二进制下载共用一个 60 秒预算，而 Go 的 Timeout 是含读完响应体的总时限，等于在赌带宽——14MB 的 wenyon-cli 实测一趟 82 秒。下载现在有自己的预算。

- **续期不再因 issuer 少一个尾斜杠打错地址**：登录态里存的 issuer 不带尾斜杠时，token 续期会打到 `<issuer>token`，报成瞬时故障、重试永远不会成功。现在带不带斜杠都打到 `<issuer>/token`。

## [2.6.93] - 2026-09-10

### Added

- 新增 `--payment-source photon|balance`，普通命令与 `bohr api` 均可用；不指定仍先光子后余额，显式渠道不足时提示且不切换扣费来源。

- **`bohr extension unpin <name>`**：解掉 `bohr extension pin` 钉下的哈希，恢复该扩展的升级。此前解 pin 只能手改 `~/.bohr/tools/<name>/manifest.yaml` 或整个删掉重装。

### Fixed

- **`bohr extension install` 不再留下用不上的 `.bak`**：此前每装一次都把旧二进制改名留在 `~/.bohr/tools/<name>/current/` 下，没有任何东西会读它或清它，一个工具因此长期占双份磁盘（实测 wenyon 29M vs 12M）。

- **pin 住的扩展不会再被静默解开**：此前每次升级伴生工具都把 `pinned_hash` 抹成空串，`bohr extension verify` 随之不再检查 pin，而全过程一声不吭。现在升级前先查 pin：发布方给的哈希与 pin 不一致就保留原二进制并说明怎么解，一致时照常安装且 pin 原样保留。

### Changed

- **pin 住的扩展不再被升级**（`bohr extension install` 与 npm postinstall 同一口径）。改前：照装、并清掉 pin。改后：不点名时打一行 `Kept <name>: …` 跳过、退出码不变；**点名**装一个 pin 住的工具则报错并非零退出。调用方要恢复升级，先 `bohr extension unpin <name>` 再 install。这包括安全更新。



## [2.6.92] - 2026-09-10

### Fixed

- **安装失败的原因现在真的看得见**：npm 默认把 postinstall 的输出整个收走，此前拒装/失败的理由一个字都到不了用户眼前；改走终端后正常 `npm install` 下就能看到。
- **两个安装同时跑不再互相踩**：临时文件名此前固定，并发安装会让一个装好的工具被报成失败。
- **服务端不出这个平台的包时会说清楚**：此前报的是「取哈希失败 HTTP 400」，读起来像服务端故障。
- **postinstall 下载不再拖垮 `npm install`**：写盘失败、跨协议跳转、重定向环此前会让安装崩掉或卡死，现在都变成一次可见的失败并跳过该工具。
- **伴生工具的哈希校验改为必须**：发布方给不出可用 SHA256（空响应、缺字段、格式非法）时，此前会静默跳过校验照装 trisol / wenyon-cli，现在改为不装并说清原因。
- **npmmirror 显式同步真的会发出去了**：发版流水线里那一步此前用的是 `wget --method=PUT`，而镜像内是 BusyBox 的 wget、不认这个选项（v2.6.91 日志逐字为 `wget: unrecognized option: method=PUT`），失败又被 `|| true` 咽掉——「显式同步」看起来一直在做、实际一次都没做成，镜像站只能靠自己的机制慢慢追。现改用 node 发 PUT 并拆成独立 job，失败会显示为流水线 warning 而不是埋在日志里。

### Added

- **装完之后会告诉你 `bohr` 是不是刚装的那个**：`npm install -g` 结束时，若 PATH 上解析到的 `bohr` 不是本次安装的那一个（已知两个来源：早期版本留在 `~/.bohrium` 的可执行文件，以及 `scripts/install.sh` 装在 `~/.bohr/bin` 的那一个，两者都会把自己的目录前置进 PATH），会打印一段提示，并按**实际挡路的那个路径**给出改名命令。判不准时保持沉默：Windows 上不判（npm 生成的是 `.cmd` 垫片，比对必然不等）、PATH 上没有 `bohr` 时不判。它不改变安装的退出码。
- **npm 包 README 补了「升级后 `bohr version` 还是旧版本号」的排障步骤**：含两个来源的对照表，以及一条容易踩的——改名旧二进制之后必须 `hash -r`（bash）或 `rehash`（zsh），否则下一条命令会报 `No such file or directory` 指着刚改名的文件。

### Changed

- **拒绝 https→http 的降级跳转**：清单与哈希旁挂文件是整条校验链的信任根，被降级到明文等于校验一起失效。
- **伴生工具的复用判定只看哈希**：不再先跑一次 `<tool> version`——那等于在校验之前执行一个还没验证的文件；发布方没给版本号时现在也能复用。
- **`--space` 的弃用告警改写**：直接给出改法（`--space personal` 加 `<path>` 写成 `personal/<path>`），不再只说「将来会移除」。

## [2.6.91] - 2026-09-09

### Added

- **`bohr file delete` 有回执了**：`-o json` 回 `{path, space, project_id, recursive, objects, size}`。递归删除先列举一遍来数对象，列举失败照删并带一条 warning。
- **递归 `file download` 机读也打进度**：stderr 每行一个 `{"event":"progress",…}`，结束再打一条最终值；stdout 仍只有一个信封。
- **递归 `file download -o json` 新增 `downloaded`**：真传了字节的文件数，与 `files`（处理过的总数）分开。

### Fixed

- **递归 `file download` 人读汇总行分得清下载与跳过**：全部跳过时不再与真下载了 N 个长成同一句话。
- **`file stat -o json` 目录的 `lastModified` 不再是「公元 1 年」**：上游的零值时间改回 `null`；人读一直打 `-`。

## [2.6.90] - 2026-09-09

### Changed

- **`file` 命令不再多打一次身份查询**：网关本就从 accessKey 识别调用方，每条 `file` 命令少一次往返；传别人的 userId 本来也会被拒。
- **命令名不存在时，会说出同名命令在哪**：敲 `bohr sandbox image`（image 已搬到顶层）这类搬过家的路径，报错会补一句 `"bohr image" exists`。同名命令有多处时不猜。

## [2.6.89] - 2026-09-08

### Added

- **递归 `file download` 边列边下**：第一页 iterate 返回就开始下，不再先静默列完整棵树。
- **递归列举每页 1000 条**：原先写死 100，大目录会空等数分钟才有第一个文件。
- **递归 `file download` 在 stderr 打进度**：human 输出 listed/downloaded/skipped；json 不打。
- **递归 `file download` 单个失败不再整单作废**：其余文件继续；JSON 为 `PARTIAL_FAILURE` 并带 `failed`。
- **递归 `file download` 跳过已下完的文件**：本地大小与 listing 一致则不再请求；JSON 有 `skipped`。
- **递归 `file download` 按文件重试可恢复的传输失败**：超时或中断最多 3 次；再跑会跳过已完成文件。

## [2.6.88] - 2026-09-07

### Fixed

- **`sandbox machine list` 的 GPU 列不再是空的**：它读的字段名不是生产在发的那两个，于是这一列在生产上恒为空；`batchjob machine list` 一直是对的。
- **`sandbox machine list` 遇到形状不对的上游响应会报错**：此前 data 是数组、是字符串、是 null 都照样 `ok=true`，人读只见一张空表，调用方无从知道自己选不出机器。
- **`sandbox machine list` 目录为空时说明原因**：此前只打一句 `No SKUs found.`，机读那一侧连 warning 都没有。

### Changed

- **两条 machine list 合成一份实现**：`sandbox` 与 `batchjob` 打的本就是同一个端点。两条命令的用法、输出与 flag 都不变，`SKU_ID` 仍只在 `batchjob` 那张表上。
- **`sandbox machine list -o json` 的字段清单订正**：此前登记的 `sandbox_id`、`jobId` 等来自测试桩，真实端点从未返回；现在登记的是 `cpu[]` / `gpu[]` 两组。

## [2.6.87] - 2026-09-07

### Added

- **`bohr file download --recursive`**：目录按相对路径逐文件下载；不加则报 `FILE_IS_DIRECTORY`。
- **`bohr file download --concurrency`**：目录下载并行拉文件，默认 5，范围 1–16。
- **错误码 `FILE_IS_DIRECTORY`、`DIRECTORY_NOT_EMPTY`**：目录下载未递归、非空目录删除未递归。

### Fixed

- **`bohr file stat` human 不再把 Size/时间打成 0、目录打成 File**：改读 `contentLength`/`fileType`，Path 用用户路径。
- **非空目录 `file delete` 不再报 `UNKNOWN`**：改为 `DIRECTORY_NOT_EMPTY`，并提示 `--recursive`。

## [2.6.86] - 2026-09-04

### Fixed

- **不写 `-o` 时的失败在管道里也给信封**：默认就是 `-o auto`，管道里该解析成 JSON。此前只有显式 `-o json` 才有，默认路径 stdout 为空、原因只在 stderr。终端上不变。
- **`sandbox machine list --choose-type` 真的只返回该类**：网关收下但不照做，CLI 此前也没补，指定 `gpu` 会连 CPU 机型一起给；`batchjob` 那条一直是对的。
- **`batchjob machine list` 目录为空时不再劝人「去掉过滤词再试」**：没带 `--choose-type` 时本就没有过滤词可去，此前两种「空」说的是同一句话。

## [2.6.85] - 2026-09-04

### Changed

- **`--choose-type` 三条 machine list 统一只收 `cpu`/`gpu`/`all`**：`sandbox` 那条此前对任何值放行，`banana` 也退 0 并返回全部机型。

### Fixed

- **资源 ID 不合法在发请求前报 `VALIDATION_FAILED`**：此前位置参数那一侧不查正负，`bohr node get 0` 会真去请求那个 ID。
- **`-o json` 的失败不再同时往 stderr 打一份散文**：把两条流合起来读的调用方此前拿到解析不了的输出。
- **`dataset delete --dataset-id` 与 `image delete --image-id` 可以单独使用**：此前只给 flag 会报缺位置参数。
- **`batchjob machine list --choose-type all` 不再被拒**：`all` 正是这条 flag 帮助文案自己写的默认值。
- **`batchjob machine list --dry-run` 把过滤词报在 `params` 而非 `body`**：真调用发的是 query 参数，此前预览说错了地方。

### Deprecated

- **三个驼峰 flag 名改用连字符**：`--choose-type` / `--dataset-id` / `--image-id`；旧名仍可用但不再出现在 `--help`。

## [2.6.84] - 2026-09-04

### Fixed

- **`machine list` 的 `--sort` 在机读格式下不再无效**：`sandbox`/`batchjob` 两条此前只在 `-o human` 排序，而自带示例写的是 `--sort price -o json`。
- **`sandbox machine list --sort price` 不再按字典序排**：`144.00 RMB/h` 此前排在 `15.00 RMB/h` 前面，挑最便宜机型会挑错。
- **`-o yaml` 的键名与省略规则改为跟随 `-o json`**：此前键名取 Go 字段名小写（`requestid`），且 `omitempty` 失效、空字段全打出来。

### Deprecated

- **`-o pretty` 已废弃**：它与 `-o json` 输出逐字节相同，从来不是另一种格式。仍可用，但不再出现在 `--help` 与文档里，请改用 `-o json`。

## [2.6.83] - 2026-09-04

### Breaking

- **`job list --quiet` 改吐 Bohr ID**：原吐的 LBG ID 喂给下游 job 命令一律报不存在；现吐 `bohrId`。要旧值请读 `-o json` 的 `data.items[].id`。

### Changed

- **`job list -o human` 首列改为 `BOHR_ID`**：原首列是任何命令都不收的 LBG 调度 ID，现改名 `LBG_JOB_ID` 挪到末列。
- **`job list` / `job describe` 的 `bohrId` 移到首列**：`-o csv` 第一列此前是任何命令都不收的 LBG `id`。

### Fixed

- **`-o table` / `-o csv` 的列序不再随机**：同一命令同一数据此前每次运行列序都不同，按列位置取值的脚本会随机取错字段。
- **`-o ndjson` 对归一化后的列表命令不再整页一行**：`job list` 等此前 3 条数据只吐 1 行，现一行一条。

## [2.6.82] - 2026-09-03

### Breaking

- **`project get` 的 `data` 由透传改为固定字段**：原随上游给 30 个键，现只给文档列出的 10 个（与 `project list` 一致）；读 `totalCost` 等的脚本需改。
- **`--dry-run` 的输出改为信封**：85 条命令原先打 `DRY RUN: …` 纯文本、无视 `-o`，现统一为 `data.dry_run` 与 `data.operations[]`；按文本抓的脚本要改。
- **`bohr api --dry-run` 的 `data` 改形**：由 `{method, path, params, body}` 改为与其它命令同形的 `{dry_run, operations[]}`；脚本要改。
- **`job describe -j` 的 `data` 不再是裸数组**：任务移到 `data.items[]`，组内总数在 `data.pagination.total`；解析数组的脚本要改。

### Added

- **`job_group download` 新增 `data.selection`**：给出本次 `-n`、实下数、组内总数与 `has_more`，此前无从知道还剩多少没下。

### Fixed

- **`-o human` 不再把命令自己的输出结构体打成 Go 转储**：改为按 JSON 字段名逐行输出。
- **`-o human` 的大数字不再变成科学计数**：`4657258` 此前打成 `4.657258e+06`，ID 最容易撞上。
- **`--jq` 的数组下标能用了**：`data.items[0]` 此前对每条命令都报 `expected array`，只有 `[]` 全取是好的。
- **`--dry-run` 在 `-o human` 下不再打 Go 结构体**：改为逐行列出方法、路径、body 与备注。
- **`job describe -j` 只列了一部分时会说**：组内超过 100 个任务时多出来的原先静默丢弃，现在 stderr 说明少了多少、怎么拿全。
- **`job_group download` 只下了一部分时会说**：`-n` 默认 10，此前与「整组下完」输出完全同形，现在 stderr 说明还剩多少。

## [2.6.81] - 2026-09-03

### Breaking

- **`job_group download` 的 `data` 再次改形**：`jobId`/`downloaded`/`files[]` 改为 `jobs`/`files`/`failures`；按 2.6.80 字段写的脚本需改。
- **移除告警码 `JOB_NO_RESULT_FILES`**：2.6.80 引入，现按 `JOB_FILES_EMPTY` 记进 `data.failures`（同 `job download`）；按该码分流的脚本需改。

### Fixed

- **`job_group download` 能下载了**：它问的 `/openapi/v4/job/<id>` 网关上没有这条路由、传的还是内部 ID 而非 BOHR_ID，2.6.80 及更早一个文件都下不下来。
- **`job_group download` 取回每个任务的全部产物文件**：原先只取第一个，其余静默丢掉。
- **`job_group download` 列表翻到一半失败时不再空手退出**：已列到的作业照样下完，整条命令仍报失败并交出台账。
- **订正 2.6.80 里 `job_group download` 的说法**：该版写「机读格式下真的会下载了……现在每个任务都下」，实测任何格式下都下不下来，本版才修好。

## [2.6.80] - 2026-09-03

### Breaking

- 无模板的 `sandbox create --image` 改用 `dynamic-image-replace`（1C/2GiB），不再默认 `sdbxagent`；保留旧行为请显式选后者。
- **`job describe` 缺参数时改报 `INVALID_ARGUMENTS`**：原报 `COMMAND_FAILED`，现点名 `id` 与 `job_group_id`，退出码仍是 1；按错误码分支的脚本需改。
- **`job_group download` 改吐下载台账**：`data` 原为上游作业详情，现为 `jobId`/`downloaded`/`files[]`；失败改退非 0（原恒 0）。读旧字段或依赖 0 的脚本需改。
- **`job describe` 多个 ID 的机读 `data` 改为信封数组**：原为详情数组，现每项形如 `{ok,data}`；单个 ID 形状不变。`.data[].x` 需改 `.data[].data.x`。
- **`job delete` 人读下解析失败改按错误码退出**：原恒退 1，现与 `-o json` 一致（如查不到退 3）；按 `$? == 1` 分支的脚本需改。

### Added

- **新增 `bohr lkm reference`**：按 paper ID 或 DOI 查参考文献/被引；`--paper-id` 与 `--doi` 至少给一侧，按次计费与 graph 同档。
- `sandbox create --image` 新增 `--cpu`，可选择 `1c2g` 至 `16c32g` 共六档 CPU profile。
- **`bohr job describe 20143720` 支持位置参数**，与 `-i` 等价、可混用，别名 `detail`/`info`/`get`/`show` 同样支持；`-i`、`-j` 原写法不受影响。
- `job_group download` 新增告警码 `JOB_GROUP_EMPTY`（组内无任务）与 `JOB_NO_RESULT_FILES`（任务无产物文件）。
- Wenyon 模板/沙箱创建支持数据集挂载和用户 JWT；仅接受 `volcengine-openkruise` Provider。
- Wenyon write mount 从空目录开始；当前无 read lease 或 discard/end-only，终态发布新的 current version。

### Changed

- `sandbox exec` 补充复杂命令的单参数示例；混用 `--command` 与位置命令时改为直接报错，不再静默忽略。

### Fixed

- **`job describe` 多 ID 时不再首个失败就放弃**：原先第一个查不到就收场、后面的一个都不问；现在每个都问，失败的逐条报出来，退出码非 0。
- **`job_group download` 在机读格式下真的会下载了**：原先 `-o json`（管道里的默认）取到第一个任务的详情就返回，一个文件都不下、还退 0；现在每个任务都下。
- **`job_group download` 下载失败不再静默退 0**：取不到详情或文件传输失败的任务，会在台账里记为失败并让退出码非 0。
- **`lkm reference` 人读按 v4 读 `data.items`**：此前只认上游 `papers`，网关收成标准列表面后会误报没有结果。
- **`lkm reference` 人读展开被引/参考文献**：每条给出标题、ID、DOI；未查询仍标 `not queried`。

## [2.6.78] - 2026-09-03

### Added

- `bohr job describe` 新增别名 `detail`，`bohr dataset versions` 新增别名 `version`；原写法照常可用。

### Fixed

- `bohr job delete` 收到 `0` 或负数 ID 时直接报错，不再先发一次解析请求；与 `bohr job kill` 的校验口径一致。

## [2.6.77] - 2026-09-02

### Breaking

- **`watermark upload` 的 `--file` 改为必填**：原报 `COMMAND_FAILED` 加 `lstat :`，现报 `INVALID_ARGUMENTS` 并点名 `file`，退出码仍是 1；按错误码分支的脚本需改。

### Fixed

- **`bohr kb delete` 此前从未成功过**：所打的 v4 路由下游不存在、恒回 404。现已改打可用端点；多个 ID 不再只删第一个。
- `bohr kb delete` 删不存在的知识库现在报 `RESOURCE_NOT_FOUND`（退出码 3）并提示先跑 `bohr kb list`。

## [2.6.76] - 2026-09-02

### Fixed

- `bohr dataset share` / `unshare` 在写之前先判明两种失败：数据集不存在返回 `RESOURCE_NOT_FOUND`（退出码 3），无权改授权返回 `PERMISSION_DENIED`（退出码 2）。此前两者都是 `UNKNOWN` + 退出码 1，且要发出一次写请求才知道。
- `bohr job group <子命令>` 的提示不再丢掉你敲的 flag：`bohr job group list --number 5` 现在提示 `bohr job_group list --number 5`。
- `bohr job_group list --help` 补上「怎么一次拿到总数」的例子，与 `job list --help` 一致。

## [2.6.75] - 2026-09-02

### Added

- 新增 `bohr dataset permission`（查看谁能访问）、`bohr dataset share`（授权给用户或整个项目，角色 member/manager）、`bohr dataset unshare`（收回访问权，destructive，需 `--yes`）。输出不含成员手机号与邮箱。
- `bohr dataset share` / `unshare` 写完会重新读回名单核对；与提交的不一致（同一时刻有别人也在改）时返回 `RESOURCE_CONFLICT` 并交出网关此刻的名单，而不是静默按成功返回。

### Fixed

- `bohr job_group list -o json` 的 `data.pagination.total` 修正为上游按当前过滤条件报的匹配总数；此前返回的是本次取回的行数（`-n 300` 报 300，真值 15 万余）。人读模式没拿全时也会像 `job list` 一样在 stderr 报出总数与字段名。
- `bohr job_group download -n <大于 200>` 此前只会取第一页、静默漏掉其余 job，现已能翻完。

## [2.6.74] - 2026-09-01

### Added

- **常用别名**：`job describe` 可写作 `job info` / `job get` / `job show`，`job kill` 可写作 `job cancel`，`file` 可写作 `files`，`job_group` 可写作 `jobgroup` / `job-group`。原写法不变。
- **`bohr job group` 从死路改成路牌**：此前报 `unknown command "group" for "bohr job"`、不含正确写法；现返回 `UNKNOWN_COMMAND`（退出码仍为 1），hint 里给出可直接复制的 `bohr job_group <子命令与位置参数>`（flag 不带过去，需自己补回）。

### Changed

- **`bohr file delete` 与 `bohr dataset` 的 help 把「没有这个能力」说出来**：`file delete --help` 写明删除不可撤销、没有 restore 命令也没有回收站；`dataset --help` 写明共享与权限不在本模块内。此前要靠逐个 `--help` 或猜端点才能得知。
- **`job list` / `job_group list` 会说出总数在哪**：人读模式下没拿全时补一行 `(showing 10 of 528098; -n fetches more, data.pagination.total in -o json)`，`--help` 也写明该字段是上游按当前过滤条件报的匹配总数、一次请求即得，不必翻完再数。

### Fixed

- **`job list` / `job_group list` 翻页中途失败不再丢弃已取回的数据**：此前任一页失败即整条命令失败、输出为空；现照常交出已取回的行，`ok=false`、退出码仍随上游错误码，`data.pagination.has_more` 为 `true`。调用方判「是否拿全」看 `ok` 与 `has_more`，不要只看有没有 `data.items`。

## [2.6.73] - 2026-09-01

### Breaking

- **`job list` / `job_group list` 的 `data` 由裸数组改为 `{items, pagination}`**：行在 `data.items`，`data.pagination` 含 `page`/`page_size`/`total`/`has_more`，其中 `total` 是上游报的匹配总数而非本次条数。脚本把 `.data[]` 改成 `.data.items[]`。

### Fixed

- **`job list` 无命中时 `-o json` 仍返回信封**：此前先打印 `No jobs found ...` 就退出，输出既不是 JSON 也不是错误；现 `ok=true` 且 `data.items` 为 `[]`。
- **`-o table` / `-o csv` 不再对归一化后的列表退化成打 JSON**：渲染器此前只认切片形状的类型化 data。
- **订正 2.6.72 里 `job list` 的提速说法**：该版写的是「由约 220 秒降到约 35 秒」。确定的部分是上游请求数由 **700 次降到 35 次**（每页 10 条 → 200 条）；秒数不该那样写——同一命令在同一台机器上六次实测为 14/59/14/49/14/23 秒，随网关负载波动约 4 倍，任何单一秒数都不代表典型情况。

## [2.6.72] - 2026-09-01

### Breaking

- **`file download` 传输失败改用传输层错误码**：此前一律 `COMMAND_FAILED`/400/rc=1/不可重试；现超时为 `UPSTREAM_TIMEOUT`/504/rc=11、内容不完整为 `UPSTREAM_INVALID_RESPONSE`/502/rc=1（均 `retryable=true`），连不上为 `NETWORK_ERROR`/503/rc=1。按 code 或 rc 分支的调用方需加这三支。

### Fixed

- **`file download` 不再受 30 秒总超时限制**：改为流式写盘、按传输停顿判超时，GB 级文件不再必然失败；等待响应头仍有 30 秒上限，中途走 `.part` 临时名。
- **`file download` 落盘前核对长度**：与 `Content-Length` 对不上即判失败并丢弃临时文件，不再可能交出一个被判成功的截断文件。
- **列表命令的分页参数名写错，请求量是应有的十几倍**：网关只认 `pageSize`，而 `job list`、`job_group download` 发的是 `perPage`、`job_group list` 发的是 `pagesize`，一律被静默忽略、每页固定 10 条。现已修正并按需增大页大小，`bohr job list -n 7000` 由约 220 秒降到约 35 秒。

## [2.6.71] - 2026-09-01

### Added

- **`file upload` 新增 `--resume` 与 `--upload-concurrency`**：大文件可续传；并发参数是上限，File 分片按序写入。
- **`billing ledger` 新增 `--page` / `--page-size`**：此前只能拿到第 1 页 20 条，响应里的 `has_more` 无法据以翻页；默认值与服务端一致，行为不变。

### Fixed

- **人读模式的错误输出补上 `Request ID`**：此前终端用户手上没有可交给上游的追溯锚点；有上游 trace 时另起一行给出。
- **`docs/zh/job.md` 的 `job submit` 示例补上 `-m`**：不用 `-i` 时缺镜像，照抄必被服务端拒绝。
- **大文件上传不再整份单次 POST**：达到 50 MiB 后按序分片；远端进度冲突会重建会话并安全重传。

## [2.6.70] - 2026-08-31

### Breaking

- **`config list` 不再返回命令 flag**：此前 `data` 混入提交和通用参数，现仅含配置；调用方请停止读取这些误入字段。

## [2.6.69] - 2026-08-31

### Breaking

- **`job kill` 未确认终态改判为失败**：原 rc=0 加警告，现 `ok=false`/`JOB_STOP_WAIT_TIMEOUT`/rc=1；要旧行为传 `--no-wait`。

## [2.6.68] - 2026-08-31

### Breaking

- **项目 ID 参数统一为 `--project-id`**：此前六组命令使用 `--project_id`/`--projectId`；旧名保留为隐藏弃用别名并提示迁移，调用方请改用新名。

### Added

- **`config list -o json` 新增 `data.project-id`**：规范 flag 进入配置视图；旧 `data.project_id` 继续保留。

### Fixed

- **销毁中的沙箱恢复为可重试**：相关命令再次返回 `subtype=sandbox_destroying`、`retryable=true`；轮询至 `RESOURCE_NOT_FOUND`。

## [2.6.67] - 2026-08-29

### Fixed

- **`dataset list`、`job_group create`、`node create` 的示例 `-p 123` 改为长选项**：`-p` 实为 `--profile`。

## [2.6.66] - 2026-08-28

### Fixed

- **`ok:false` 不再带 2xx 的 `error.http`**：上游用 200 承载的失败（多条 `get` 的 not-found）现按错误码给 404，非 2xx 不变。
- **批量删除/停止的第 2 条起，失败项的 `error.code` 与 `error.http` 现在也归一**：此前只有第 1 条被收敛，同一数组里两条相邻失败长得不一样。

## [2.6.65] - 2026-08-28

### Breaking

- **`project get` 改为只返回请求的那个项目**：原先任何 id 都返回全部项目且 `ok=true`，现改读 `data`，查不到报 `RESOURCE_NOT_FOUND`。

### Changed（内部，无用户面行为变化）

- 新增生产契约巡检：只读打生产，断言过滤参数真在过滤、空集与非空同型、不存在的目标必须报 not-found；并让 CI 保证带 tag 的测试文件编得过。

### Fixed

- **`job_group list` 命中为空时 `data` 由 `null` 改为 `[]`**：与非空时同型，`.data[]`、`.data | length` 不再在空结果上失败。

## [2.6.64] - 2026-08-28

### Added

- **新增 `sandbox create --inherit-auth`**：经数据面将当前登录注入 tmpfs；内层须为可核验的 npm bohr-cli 2.6.45+。失败后勿盲目重试。

### Changed（内部，无用户面行为变化）

- 实测确认三条命令两端的响应形状一致，端点切换账上最后三条 `unknown` 已定性；同时补上这三条此前缺失的回归测试。
- 新增「零文档命令」门禁：现存 31 条一个字文档都没有的命令已逐条登记，新命令不写文档必须显式登记，补了文档必须删行。

### Fixed

- **文档补齐 `file` 三条写命令的 `--recursive`**：它是模式开关；`copy`/`move` 源类型用错会报错并在目标留残留，`delete` 不会；成功不返回 `data`。

## [2.6.63] - 2026-08-28

### Breaking

- **`sandbox template list` 的 `data` 形状不再随分页参数变**：原为裸数组、传 `--page` 才是对象，现一律是分页对象，改读 `data.list[]`。
- **`sandbox template list` 拒绝越界分页参数**：`--page` < 1、`--page-size` 不在 1–100 现在报错，此前上游默默收下并压回 100。

### Fixed

- `sandbox list` 的 `-o table`/`-o csv` 恢复表格渲染，`-o ndjson` 恢复一行一条。此前三者都退化成打印整个分页对象。

## [2.6.62] - 2026-08-27

### Breaking

- **`sandbox list` 的 `data` 形状不再随 `--query` 变**：active 原为裸数组，现一律是带分页元数据的对象，改读 `data.list[]`。

### Added

- **新增 `sandbox storage inspect`**：只读查看 rootfs 总量、用量和余量；未知值返回 `null`。
- **新增 `sandbox storage resize`**：运行中只增不减地扩充 rootfs，`--target` 表示总容量，并等待控制面与 `df /` 容量就绪。

### Fixed

- **订正 `sandbox list` 的文档字段表**：原写 `data.items[]`，且列着早已不返回的 `id`、`projectId`、`createdAt`；现按实际形状重写。

## [2.6.61] - 2026-08-27

### Changed（内部，无用户面行为变化）

- 发版对账门禁不再把「另一次发版的流水线还在跑」的 tag 判成悬空 tag；跑完仍未上 npm 的，下一次照常报出。

## [2.6.60] - 2026-08-27

### Breaking

- **遗留配置搬不过来时不再静默继续**：`~/.bohrium/`、`~/.lbg/` 里的文件坏了，此前退出码 0、凭据凭空消失；现在报 `CONFIG_UNREADABLE`、退出码 1。
  - 只影响新路径 `~/.bohr-cli/config.yaml` 还不存在的升级用户；已迁移过的不受影响，遗留目录里的陈年坏文件不会被读。
- **迁移时写新配置失败不再静默**：此前退出码 0、什么都没搬也不出声；现在报 `CONFIG_WRITE_FAILED`、退出码 1。

## [2.6.59] - 2026-08-27

### Breaking

- **配置文件读不动时不再静默继续**：此前退出码 0、`ok=true`、配置里的值凭空消失；现在报 `CONFIG_UNREADABLE`、退出码 1，先修好或删掉该文件。
- **写配置失败统一报 `CONFIG_WRITE_FAILED`**：改前 auth logout 报 `INTERNAL`、config set 报 `COMMAND_FAILED`，退出码不变。

### Added

- **新增错误码 `CONFIG_UNREADABLE` 与 `CONFIG_WRITE_FAILED`**：system 类，HTTP 422/500，退出码均为 1，hint 给出配置文件路径。

## [2.6.58] - 2026-08-27

### Breaking

- **`bohr config set` 成功改输出信封**：此前是 `Set k = v` 纯文本，现在只放信封，`data` 带 `key` 与 `value`。按文本解析的脚本需改。
- **关闭计费确认后的三句中文提醒改走 stderr**：此前在 stdout。要拿「怎么改回去」的调用方请读 `data.restore_command`。
- **缺参数时不再先要确认**：计费/破坏性命令此前报 `CONFIRMATION_REQUIRED`，现改报 `INVALID_ARGUMENTS`，退出码 10 → 1。按旧码分支的脚本需改。
- **`bohr node create` 不给任何输入时改为报错**：此前打帮助并退出码 0，现在报「分组里至少给一个」、退出码 1。按退出码判成功的脚本需改。

### Added

- **`notebook cell add/update/delete` 成功时输出信封**：此前无任何输出。`data` 带 `file`、`operation`、`cells` 与 `index`。

## [2.6.57] - 2026-08-26

### Breaking

- **七个命令族共 54 个 flag 声明必填**：缺参数报错由各命令自定义文案统一为 `required flag(s) "x" not set`，并提前到执行前。按文案匹配的脚本需改。
- **9 处「二选一」声明为分组**：都没给时报错统一为 `at least one of the flags in the group [a b] is required`，按文案匹配的脚本需改。
- **补记：`sandbox list` 自 2.6.49 起不再返回 `data.items[].id`**：它是内部数据库行号，喂不回任何命令；请改读 `sandbox_id`。当时漏发了公告。
- **调用错误改报 `INVALID_ARGUMENTS`**：元数不对、缺必填 flag、二选一没给、ID 与替代 flag 都没给这四类。按旧码匹配的调用方需改判据；退出码仍是 1。
- **13 条 job/node/project 命令缺 ID 的报错统一**：改为 `requires <bohr_id> as an argument, or --id`，四种旧文案不再出现。
- **给了零值的 ID flag 不再报「你必须提供」**：`--node_id 0` 这类改报 `--node_id must be a positive node ID`，拒绝的输入不变。

### Added

- **新增错误码 `INVALID_ARGUMENTS`**：params 类，HTTP 400，退出码 1；hint 带该命令的 `--help` 与 `missing_params`。

### Changed

- **`job +cancel`、`job +watch` 的 Usage 行补上 `<bohr_id>...`**：此前只写 `+cancel [flags]`，看不出它收 Bohr ID。
- **`--help` 标出必填与二选一**：此前 93 条必填里只有 16 个手写过 `(required)`，现在全部标出；分组加 `(one of: --a, --b)`。

### Fixed

- **`bohr --help` 不再示范已下掉的 `config initialize`**：该命令早已移除，根命令示例仍列着，改为 `bohr config list`。
- **两条 `--help` 示例跑不通**：`job_group download 123 654` 改为 `-j 123`；远端命令带 flag 写 `exec <id> -- ls -la`。

## [2.6.56] - 2026-08-26

### Breaking

- **移除全局 `--page-all`**：改前机型/Agent 列表会扩类或自动拉全；改后参数报错。机型用 `-c all`，其余逐页查询。

### Fixed

- **`bohr doctor -o json` 的 `duration_ms` 不再时有时无**：改前耗时不足 1 毫秒的检查不带这个字段，机读方分不清是没测量还是为零；改后每条都带，快的记 0。

## [2.6.55] - 2026-08-26（未发布——版本号跳过）

v2.6.55 的 tag 流水线因 response-fields golden file 未更新而失败，受保护 tag 无法删除。改动实际随 2.6.56 发出。

### Breaking

- **移除全局 `--page-all`**：改前机型/Agent 列表会扩类或自动拉全；改后参数报错。机型用 `-c all`，其余逐页查询。

### Fixed

- **`bohr doctor -o json` 的 `duration_ms` 不再时有时无**：改前耗时不足 1 毫秒的检查不带这个字段，机读方分不清是没测量还是为零；改后每条都带，快的记 0。

## [2.6.54] - 2026-08-26

### Fixed

- **`image list` 支持翻页**：新增 `--page` 参数（默认 1），`--limit` 更名为 `--size`（默认 100，`--limit` 保留为隐藏别名），此前只能看到前 100 条。

## [2.6.53] - 2026-08-26（未发布——版本号跳过）

v2.6.53 的 tag 流水线因测试未随改名更新而失败，受保护 tag 无法删除。改动实际随 2.6.54 发出。

### Fixed

- **`image list` 支持翻页**：新增 `--page` 参数（默认 1），`--limit` 更名为 `--size`（默认 100，`--limit` 保留为隐藏别名），此前只能看到前 100 条。

## [2.6.52] - 2026-08-26

### Breaking

- **63 条命令不再静默忽略多余的位置参数**：改前 `job submit x.json` 丢掉文件名、交空作业并计费，ok=true、rc=0；改后报错 rc=1 并点名多余的词。值请改用 flag。

### Fixed

- **数据集不存在不再显示 UNKNOWN**：识别 `100003/100004` 为权限不足或资源不存在。
- **不收位置参数的命令报错不再说 `unknown command`**：这些命令没有子命令，那句话把人往「子命令拼错了」引。现在明说不收位置参数、多给了哪几个词，并指向该命令的 `--help`。

## [2.6.51] - 2026-08-25

### Added

- **`bohr schemas errors` 新增 `exit_code` 字段**：98 个错误码对应的进程退出码首次可机读，此前只有文档里一张散文表格。
- **`notice.warnings[].code` 首次公开枚举**：17 个警告码写进 `notice.v1`，`bohr schemas get notice.v1` 可查；此前只声明为任意字符串。

### Fixed

- **`file download`、`job delete` 上游失败的错误码修正**：改前 `error.code` 为 `UNKNOWN`、真码藏在 `subtype`；改后为新增的 `UPSTREAM_ERROR`，退出码不变。
- **订正文档里的退出码表**：`AUTH_ERROR`、`AUTH_CANCELLED`、`AUTH_TIMEOUT` 原记作 2 实为 1，三个 `UPSTREAM_*` 原记作 11 实为 1。退出码本身未变。

## [2.6.50] - 2026-08-25

### Added

- **`bohr update` 一并更新 companion tools**：已安装的 wenyon 和 trisol 随 bohr 自更新一起升级到最新版本。

## [2.6.49] - 2026-08-25

### Breaking

- **`sandbox image list` 改列制品目录**：九个筛选/分页 flag 移除，只剩 `--type`；单个构建仍可 `image get`/`buildlog` 查，按条件筛构建任务暂无替代。**随 2.6.47 已发出**，当时漏记。

### Added

- **新增内嵌 `bohr-mentor` Skill**：提供与当前 CLI 一致的科学问答、计费确认和失败重试边界指引。
- **新增内嵌 `bohr-pdf-parse` Skill**：提供 PDF 提交、按页计费、异步结果与结构化输出指引。
- **补齐独立 Bohrium Skill 的 CLI 内嵌覆盖**：新增其余 12 个命令域的版本锁定指引。
- **新增内嵌 `bohr-scimaster` Skill**：覆盖论文深搜、稿件评审、异步等待与报告下载，当前共 20 个 Skill。

### Changed

- **`lkm parse result` 新增结果格式选择**：支持 `--format local|graph`，首次成功按 1 元/缓存 0.1 元确认计费。
- **公开模板不再与额外临时存储互斥**：`--visibility 1` 也能带 `--extra-ephemeral-storage-gb`，只按配额判。**随 2.6.47 已发出**，当时漏记。

### Fixed

- **上游报错时不再丢掉限流/计费/废弃提示**：`{"code":非零}` 形态的失败此前把响应头带的 notice 整体丢弃，429 也看不到限流提示。另修 notice 无期限时渲染成 `(deadline: )`。
- **修复 KB 与 Scholar 分页参数**：`--page/--size` 现使用上游真实字段，不再静默回退默认分页。
- **补齐 SciMaster wait 与 Image list 输出上限**：新增本地 `--limit` 并保证机读结果不超限。
- **澄清 `--jq` 能力**：帮助明确其为字段/数组路径过滤器，不再暗示支持完整 jq 语言。

### Deprecated

- **`bohr sandbox image` 整组标记废弃**：人读模式 stderr 告警，`-o json` 在 `notice.deprecation` 里给出替代命令；功能不变，后续版本移除。

## [2.6.48] - 2026-08-25

### Added

- **`bohr image` 一级命令**（`bohr sandbox image` 仍是别名）：build/commit/pull 等九条。**随 2.6.47 已发出**，当时漏记。

### Fixed

- **数据集大文件分片上传复用 HTTP 连接**：减少重复建连与 TLS 握手，提升 `dataset create` 的实际上传吞吐。
- **`bohr agents dev` 及其下三个子组打错子命令时也给 `UNKNOWN_COMMAND`**：此前回 `COMMAND_FAILED` 且没有提示，现在同其余命令一样点名错词并指向该组 `--help`。
- **命令打错时，人读输出也给出路**：此前 `--help` 提示只进 `-o json` 的信封，终端里只有一句 `unknown command`；现在多一行 `run '<组> --help' to see the available commands`。

## [2.6.47] - 2026-08-24

### Breaking

- **下掉 `bohr config initialize`**（含别名 `i`）：它一直是空实现，什么也不做。配置目录由 `auth login`、`config set` 自动创建。

- **Agent 执行目录。** `bohr agents run` 仅执行 `bohr agents list` 中的 7 个目标；其他命令和 `bohr api` 不变。

### Added

- **新增内嵌 `bohr-notebook` Skill**：可读取与当前版本一致的实例、Cell、异步运行、发布与协作指引。
- **新增内嵌 `bohr-lkm` 与 `bohr-wiki` Skill**：提供知识检索、推理、PDF 抽取和科学百科指引。
- **沙盒父子生命周期**：`create --parent-sandbox-id` 建立多级关系，列表默认树形展示，删除父沙盒会级联清理后代。
- **模板资源与共享控制**：创建支持 `--shm-size`、`--visibility`；新增 `share/unshare/grants` 与项目筛选。

### Changed

- **Sandbox Skill 减少网络盘扫描**：反复检索 `/share`、`/personal` 时建立窄范围路径索引，并按任务选择额外临时盘。

### Fixed

- **`sandbox doctor -o json` 不再输出两份 JSON**：环境未就绪时 stdout 会追加第二份错误信封，调用方解析直接失败。
- **子命令名打错时 `-o json` 也给信封**：`bohr config nosuchsub` 此前 stdout 为空，现给 `UNKNOWN_COMMAND` 并提示该组 `--help`。

## [2.6.46] - 2026-08-24

### Added

- **`bohr config set openapi_host` 支持 UAT 环境**：白名单新增 `https://open.uat.bohrium.com`。

### Fixed

- **companion 扩展生命周期修复**：配套扩展的发现与管理在特定条件下失效，现已修正。
- **CI 发布构建 strip 符号表**：六平台二进制缩小约 30%，解决 artifact 超限。

## [2.6.45] - 未发布（版本号跳过）

CI artifact 超限导致发布失败，变更随 2.6.46 发出。

## [2.6.44] - 2026-08-24

### Fixed

- **`job list`、`job_group list` 与 notebook 六条命令补回机读输出的 `meta.request_id`**：此前重建信封时丢失，出问题无 ID 可追。
- **不发请求的命令不再编造 `meta.request_id`**：2.6.43 只把字段改可选，六条命令仍在编；dry run 也不再报。
- **`billing recharge`、`dataset create`、`sandbox template delete` 的机读输出补上 `meta`**：此前完全没有。
- **配套扩展可完整管理**：CLI 或 npm 安装的 trisol/wenyon 现可被 list/info/verify/pin/remove 识别；remove 需 `--yes`。


## [2.6.43] - 2026-08-23

### Breaking

- **`meta.request_id` 与 `meta.duration_ms` 改为可选**：不发请求的命令不再输出空串与 0（实测要几十毫秒，报 0 是错的）。有真实值时照常输出。
- **`sandbox delete` 传单个 ID 时不再把结果包成数组**。改前：单元素数组；改后：与其余五条批量命令一致，直接给该条结果。按数组解析的调用方需改回按对象读。

## [2.6.42] - 2026-08-22

### Fixed

- **到不了输出层的错误在机读格式下也给信封**：缺必填 flag 等此前只往 stderr 打纯文本、stdout 为空。新增中性错误码 `COMMAND_FAILED`。human 文案不变。
- **`extension` 七个子命令在机读格式下输出信封**：此前 `-o json` 吐的是人读表格，尽管 `--help` 里写着支持。human 文案不变。
- **`bohr update` 与 `update --check` 输出信封**：此前吐人读句子；更新过程的进度改写 stderr，stdout 只留结果。

## [2.6.41] - 2026-08-22

### Breaking

- **移除 `bohr event consume`**。改前：`events/stream` 恒回 501；改后：`event` 不再注册。上游无后端服务，无调用曾成功，无需迁移。


### Fixed

- **`-o ndjson` 不再丢失错误信息**：失败时整条信封输出为一行，此前只打印 `null`。成功仍逐行输出裸数据。
- **`version`、`config get`、`config list` 在 ndjson/yaml/table/csv 下不再输出 human 文案**，改为与其他命令一致的信封。
- **未知命令在 `-o json` 下也给信封**：新增错误码 `UNKNOWN_COMMAND`，此前 stdout 完全为空。同时去掉重复打印的第二行错误；human 文案不变。

## [2.6.40] - 2026-08-21

### Added

- **信封 `meta` 新增 `cli_commit` 与 `cli_modified`**：区分同一版本号下的不同构建，取不到时不出现；既有字段取值不变。


- **`bohr version` 新增 `commit`、`modified`、`built`**：标识具体构建而非仅版本号，脏工作区构建带 `-dirty` 后缀。

## [2.6.39] - 2026-08-21

### Added

- **`bohr job_group terminate` / `delete` 也接受 `bohrJobGroupId`**：一个任务组有两个标识（`job_group list` 同时透出 `id` 与 `bid`），此前这两条命令只收 `id`，而 `job submit -g` 与 `job submit` 返回的 `bohrJobGroupId` 给的是 `bid`——同一个组做不同的事要拿不同的号，拿错就报 `record not found`，看起来像组不存在。现在传 `bid` 会被自动换成 `id` 再发出；传 `id` 的行为与调用次数均不变。

- **上一条换不出来时会说明该用哪个 ID**：换算依赖任务组下的作业已被索引，而刚 `job submit` 完的组要等几秒才可查，恰好是最常见的「提交完就停整组」场景。此时报错的 code、HTTP 状态与文案一律保持上游原样，只补一条 hint 说明该改用 submit 响应里的 `jobGroupId`——那个值调用方手上本来就有。

- **`bohr job submit` 输出可直接回传的任务组 ID**：新增 `bohrJobGroupId`（`-o json`）/ `BohrJobGroupId`（human），即 `-g` 真正接受的那个值。此前返回体里的 `jobGroupId` 来自平台 ID 空间，拿它回传 `-g` 必然报 `JobGroup(<id>) not find`，而可用的那个只能另外从 `bohr job_group list` 的 `bid` 或 `bohr job_group create` 的 `groupId` 取；现在提交完即可直接用返回值把后续作业提进同一个组。原有字段一律保留、取值不变，`-g` 的帮助文案同时写明了该收哪个值。

### Fixed

- **多个 ID 时不再只处理第一个**：`job_group terminate`、`job_group delete`、`image delete`、`job delete` 这四条命令在 `-o json` 等机读输出下，处理完第一个 ID 就返回了——`bohr job_group terminate A B -o json` 只停 A，B 从未被请求，且退出码为 0。human 输出下同理，第一个失败的 ID 会中断其后全部 ID；`job delete` 尤其严重：首个 ID 解析失败时，后面本可删除的作业一个都不会被删。现在四条命令都会走完全部 ID，失败的一个不影响其后的，退出码如实反映任何一次失败。机读输出沿用仓库既有的批量约定（`envelope.Batch`，与 `dataset delete`、`sandbox delete`、`job stop` 一致）：单个 ID 的输出形态完全不变，多个 ID 汇总为一个 batch envelope，每个 ID 一条结果。

- **`bohr job submit` 的响应异常不再把两种失败写成同一句话**：建组、建任务、提交三个阶段此前对任何异常响应都只报 `invalid <阶段> response`，并丢掉底层的解码错误——「响应结构变了」和「结构对但字段缺失/为 0」这两种归属完全不同的失败，读到的文案一模一样。现在解码失败会带上原始错误，字段异常会点名是哪个字段、实得什么值（如 `job creation response carries no upload token`）。

## [2.6.38] - 2026-08-19

### Fixed

- **`bohr trisol` 在登录态过期后自动续期，不再把过期 token 注入子进程**：此前 trisol 启动时把磁盘登录态里的 access token 原样注入 `TRISOL_TOKEN`，token 一过期 trisol 全部命令收到无归因的裸 `401`，而 refresh token 明明有效（wenyon 有续期路径、trisol 没有）。现在注入前统一走「过期即用 refresh token 换新」的判定，wenyon 与 trisol 同口径。续期失败分两级：vouch 服务端明确拒绝（refresh token 已失效）时拒绝启动并提示 `bohr auth login`；瞬时失败（网络错、5xx）保留登录态、告警后照常启动，一次网络抖动不再毁掉有效会话。wenyon 自有 `~/.vouch/state.json` 仍有效或完全没有登录态时照常启动。
- **过期的注入 vouch token 在所有出口一致地被拒**：2.6.37 把校验加在了 wenyon/trisol 启动与托管会话的 `auth status`，但 API 命令的 HTTP 客户端仍会把过期的 `BOHR_VOUCH_TOKEN` 设为 Bearer 发往网关，`auth whoami` 仍会打印看似正常的身份，`auth token` 把它折叠成「未配置凭据」，非托管 `auth status` 完全不判。现在 API 命令在请求发出前以 `AUTH_EXPIRED` 信封失败并点名要更换的变量（凭据判死不标记 retryable；瞬时取不到凭据仍按可重试报告），`auth whoami` / `auth token` 同码同因，非托管 `auth status` 报 `vouch (environment, expired)`。非托管会话配置了 accessKey 时以 accessKey 优先（与请求链路同口径），遗留的过期注入 token 不再挡住 `auth whoami`。`ALLOW_EXPIRED_VOUCH_TOKEN=1` 逃生口对以上路径同样生效。
- **凭据故障的错误码与建议不再因命令而异**：同一个刷新故障此前在 API 命令是可重试的网络错误、在 `auth token` / `auth whoami` 却变成「未配置凭据 — 请重新登录」，会让用户为一个其实完好的会话重新登录；vouch 服务端明确拒绝续期（如 `invalid_grant`）时这两个命令也丢掉服务端给出的原因。现在两类失败在所有出口同码：凭据判死报不可重试的 `AUTH_EXPIRED` 并保留归因原文，瞬时故障报可重试的网络错误并保留登录态。「登录态已过期且没有 refresh token」也归入前者（此前被当成瞬时故障，提示重试一个不可能成功的续期）。

## [2.6.37] - 2026-08-19

### Fixed

- **注入的 vouch token 过期时不再被静默发往下游**：`BOHR_VOUCH_TOKEN` / `VOUCH_ACCESS_TOKEN` 此前在任何校验之前就被采用，而磁盘登录态那条路径会检查过期并自动刷新。结果是过期的注入 token 被原样发出、下游回一个没有上下文的 `401`，同时 `bohr auth status` 还报「已登录」。现在 CLI 会读 token 的 `exp` 声明，已过期则直接失败并指出是哪个变量、何时过期、以及换新 token 或 `unset` 改用登录态两条出路；`bohr auth status` 在托管会话里返回 `logged_in=false` 与 `auth_method=managed_vouch_expired`；`bohr wenyon` / `bohr trisol` 在启动子进程之前完成同一判定。环境注入没有 refresh token，故不做自动续期。不透明 token 与不含 `exp` 的 JWT 无从判断，原样放行；签名不验，验签是资源服务器的职责。时钟偏移或排障需要时用 `ALLOW_EXPIRED_VOUCH_TOKEN=1` 把失败降为告警。该环境变量此前无任何用户文档，现补入 `docs/zh/auth.md`。
- **`bohr auth whoami` 不再显示 `auth_method` 字段**：AK 和 vouch token 登录后互补获取，区分认证路径无意义且容易误导。输出改为统一的 `"authenticated": true`。
- **登录时不再刷新无用的 entitlements 缓存**：`refreshEntitlements` 每次登录都尝试拉取但从无消费者，仅产生 "Warning: could not refresh entitlements" 噪音，已移除。

## [2.6.36] - 2026-08-19

### Fixed

- **CHANGELOG 不再声明两个装不到的版本**：`## [2.6.23]` 与 `## [2.6.30]` 从未发布到 npm，但两节此前照常写着发布日期，随包发到用户手里后会让人按不存在的版本安装或回溯。两节的日期改为「未发布（版本号跳过）」并在节首说明改动实际随哪一版发出（2.6.24、2.6.31）；同时订正 `## [2.6.30]` 里 `bohr lkm parse` 的子命令名，实际为 `submit / status / result / wait`，此前误写为 `submit / query / result`，其中 `query` 从未存在。

## [2.6.35] - 2026-08-19

### Fixed

- **浏览器 SSO 登录后自动获取 vouch token**：修复 `bohr auth login`（浏览器方式）完成后不会执行 token-exchange 获取 vouch token 的问题，导致 `bohr trisol` / `bohr wenyon` 等依赖 vouch 的命令无法认证。现在浏览器登录与 `--ak` 和 device flow 行为一致。

## [2.6.34] - 2026-08-19

### Added

- **三道新发版门禁 + Tag/npm 双向对账**：`verify-release-commit-shape.sh` 要求发版提交相对第一 parent 只改动 `CHANGELOG.md`、`internal/version/version.go`、`npm/package.json`（v2.6.29 把版本 bump 揉进特性 MR、16 个文件照样发布，既有门禁只数 parent 个数挡不住）；`verify-release-tag-type.sh` 要求发版 Tag 为 annotated（该要求此前只是报告建议，其后 v2.6.28–v2.6.33 六个版本全部回退成轻量 Tag）；新增 `verify-release-parity` job 做 Tag 与 npm 版本双向对账，覆盖「npm 有版本无 Tag」（2.5.8 手动脏构建发布）与「有 Tag 无 npm 版本」（v2.5.13、v2.6.0、v2.6.23、v2.6.30 四个悬空 Tag）两个方向，已知历史偏差声明在 `scripts/release-parity-exceptions.txt`。三道各有逃生口，均需显式写在流水线里。对应登记行 recvqS8mqShaXo。本版不含任何 CLI 行为面改动。

## [2.6.33] - 2026-08-18

### Changed

- **`bohr trisol` 不再需要 entitlement 授权**：移除客户端 entitlement 检查，所有已认证用户均可使用 `bohr trisol`，与 `bohr wenyon` 一致。

## [2.6.32] - 2026-08-18

### Added

- **AK 登录自动兑换 vouch token**：`bohr auth login --ak` 成功后自动通过 RFC 8693 token-exchange 向 vouch 换取 access+refresh token（双 audience：wenyon-svc + trisol-svc），wenyon/trisol 无需再单独认证。兑换失败仅输出 warning，不阻塞 AK 登录。

## [2.6.31] - 2026-08-18

### Added

- **Vouch 登录支持双 audience（wenyon-svc + trisol-svc）**：device flow 和 token refresh 均请求双 audience，一次登录 wenyon 和 trisol 都可验签。存量单 audience 会话不受影响，升级后重新登录即可获得双 aud token。`BOHR_VOUCH_AUDIENCE` 支持逗号分隔覆盖。

### Fixed

- **`bohr file download` 现在输出信封，进度提示不再污染 stdout**：此前该命令无论成功或失败都只打印纯文本，`-o json` 也一样，机读调用方连成功都无法解析；进度行 `Downloading ... to ...` 还固定打在 stdout。现在成功返回含 `remote_path`、`dest_path`、`space`、`project_id`、`size` 的信封，上游非 2xx 返回携带上游 HTTP 状态的错误信封，进度提示仅在 human 输出下打到 stderr。上游状态原样透传，不在 CLI 侧把 400 改写成 not-found。
- **`bohr job delete` 与 `bohr kb document list` 的失败路径补出信封**：此前二者失败时 stdout 为空，错误只以自由文本出现在 stderr，机读调用方拿不到可判别的 `error.code`。现在 `job delete` 在非 human 输出下透出上游失败信封（上游错误码原样保留），`kb document list` 对不存在的知识库返回 `RESOURCE_NOT_FOUND`、`http=404`、退出码 3 并给出 `bohr kb list` 指引。`job delete` 同一错误此前会重复输出两次，现已消除。
- **`bohr project get` 传不存在的项目 ID 不再报成功**：该命令由项目列表接口按 ID 过滤实现，此前查不到项目时会返回 `ok:true` 与退出码 0，只是 `data.items` 为空，调用方无法用退出码或 `ok` 区分「项目不存在」与「查到了」。现在返回 `error.code=RESOURCE_NOT_FOUND`、`error.http=404`、退出码 3，与 `file stat`、`sandbox get`、`batchjob describe` 等命令的 not-found 形态一致，并给出 `bohr project list` 的指引。项目存在时的返回结构不变。

## [2.6.30] - 未发布（版本号跳过）

> **该版本号从未发布到 npm**，不要按它安装或回溯。tag `v2.6.30` 指向直推 main 的提交 `de65136`，其 tag 流水线的 build 阶段被 `verify-release-commit.sh` 判定为未经 MR review 而拦下，发布未完成。下列改动**实际随 2.6.31 发出**（`v2.6.30` 是 `v2.6.31` 的祖先），因此功能没有丢失；本节保留是为了让这个版本号不至于从变更史里凭空消失。见 `scripts/release-parity-exceptions.txt`。

### Added

- **npm postinstall 自动下载 trisol 外部版**：`npm install -g @dptech-corp/bohr-cli` 后自动拉取 `?variant=bohrium` 的 trisol（与 wenyon 一致），无需手动 `bohr extension install trisol`；用 `BOHR_TRISOL_SERVER` / `BOHR_TRISOL_CHANNEL` 可指向非 prod 环境。
- **`bohr trisol` 支持 `BOHR_TRISOL_API_URL` 指定 trisol 环境**：dev 构建设 `BOHR_TRISOL_API_URL=https://trisol-dev.dp.tech` 即连 dev；不设则用 trisol CLI 自身配置（prod 默认）。显式可逆，不焊死安装环境——迁移 prod 只需取消该变量。
- **`bohr lkm parse` 异步 PDF 知识抽取**：支持 submit / status / result / wait 子命令，通过 open-platform 调用 LKM 的论文解析服务。

## [2.6.29] - 2026-08-18

### Breaking

- **`bohr tools install` 移至 `bohr extension install`**：安装 trisol/wenyon 等配套扩展 CLI 的命令归入扩展管理命令组（与 `extension add/list/info/remove/pin/verify` 并列），`bohr tools` 回归纯科学工具发现（list/search/info/domains/subdomains）。此前 `bohr tools install <tool>` 的调用请改为 `bohr extension install <tool>`，参数与行为不变（`--dry-run`/`--silent`/`--server`/`--channel` 及 `BOHR_TOOLS_SERVER` 环境变量照旧）。

## [2.6.28] - 2026-08-18

### Added

- **新增 SciX 托管运行支持**：SciX 本地 Sandbox 内的 bohr CLI 仅使用当前会话注入的 Vouch 或 AccessKey，不读取宿主机持久凭证，并限制登录、凭证导出、自更新和扩展等有状态命令。
- **`bohr tools install trisol` 改为拉取 Bohr 版 CLI**：trisol 下载走 `?variant=bohrium`，安装的是裁剪过 admin 命令的 `bohr-trisol`（Bohr 平台分发）产物，与内部版分发分离。
- **`bohr tools install --server <url>` 环境覆盖**：指向非 prod 的 trisol 环境（如 dev/fastlane）做联调下载；也支持 `BOHR_TOOLS_SERVER` 环境变量。
- **`bohr tools install --channel <stable|dev>` 通道选择**：非 prod 服务器的默认通道是 stable（没有外部版二进制），dev/fastlane 联调需 `--channel dev` 拉取含外部版的 dev 通道。

## [2.6.27] - 2026-08-18

### Breaking

- **Mentor 的规范入口调整为 `bohr agents mentor`**：与其他托管科学 Agent 服务统一归入 `agents`。原顶层 `bohr mentor` 已隐藏并废弃，兼容期内仍可使用且会提示迁移，旧入口至少保留一个 minor 版本；参数、流式输出和 `mentor.session` 计费行为保持不变。

## [2.6.26] - 2026-08-17

### Breaking

- **`bohr skills` 改为只读、随 CLI 内嵌的命令指引入口**：此前该命令会代理 `bohrium-skills-cli`，并同时提供 `install` / `update` / `status` / `sync` / `diff`、`--managed` 与 `--lang`，`bohr sandbox skill` / `bohr batchjob skill` 还会导出或覆盖本地副本；现在只保留顶层 `list [skill[/path]]` 和 `read <skill>[/path] [path]`，不访问 Skill Hub，也不再写入用户 Skill 目录。通用 Skill Hub 用户请直接使用 `bohrium-skills-cli`，原先读取 CLI 指引的调用方改用 `bohr skills list/read`。内嵌 Skill 的规范名同时由 `bohrium-sandbox` / `bohrium-batchjob` 改为 `bohr-sandbox` / `bohr-batchjob`。

## [2.6.25] - 2026-08-17

### Fixed

- **多 AccessKey 账号重新登录不再静默换绑**：Vouch/设备码登录会优先保留当前 profile 已绑定的 Key，并在结果中说明选择原因与候选数量；首次登录遇到多把 Key、或原绑定已不在列表中时拒绝自动选择，提示使用 `bohr auth login --ak <key>` 显式绑定，避免计费与审计归属漂移。

## [2.6.24] - 2026-08-17

### Added

- **Sandbox 镜像新增 Dockerfile 回查命令**：`bohr sandbox image dockerfile <id>` 默认逐字输出解码后的构建源码，`-o json` 保留服务端 base64 字段；commit 或导入类镜像没有保存 Dockerfile 时会返回结构化 `RESOURCE_NOT_FOUND`，不再以空内容成功。

## [2.6.23] - 未发布（版本号跳过）

> **该版本号从未发布到 npm**，不要按它安装或回溯。tag `v2.6.23` 是打在特性合并提交 `7e71796` 上的轻量 tag，该提交自称的版本号是 2.6.22；其 tag 流水线的 build / publish / notify-release 三个 job 被人工取消，发布未完成。下列改动**实际随 2.6.24 发出**（`v2.6.23` 是 `v2.6.24` 的祖先），因此功能没有丢失；本节保留是为了让这个版本号不至于从变更史里凭空消失。见 `scripts/release-parity-exceptions.txt`。

### Added

- `bohr agents run/watch` 新增 `--no-reasoning` 与 `--out <file>`：前者只保留正文，后者按终端相同分段把完整模型文本原子保存到文件。

### Changed

- `bohr agents run/watch` 的 human 输出默认以 `[agents] reasoning:` / `[agents] content:` 分段展示思考与正文；JSON/YAML 默认返回 `data.reasoning`，使用 `--no-reasoning` 时省略。

## [2.6.22] - 2026-08-14

### Added

- **知识库文档新增按稳定 ID 管理的命令面**：`bohr kb document list` 可按知识库或目录列出 `document_id`，`get` 返回不泄露临时下载 URL 的安全元数据，`delete` 按 `document_id` 删除文档及附件。当前 OpenAPI 尚未提供解析/索引状态时，列表会明确返回 `index_state=unavailable` 和告警，不再把上传受理误当成索引完成。

## [2.6.21] - 2026-08-13

### Fixed

- `bohr job log` 与 `bohr job download` 恢复接受 Bohr ID 位置参数；位置参数可重复，也可与现有 `--id/-i` 混用，旧脚本无需改写即可继续下载日志和结果文件。`job log/download/kill/terminate` 共用的 ID 解析现在会在请求前拒绝 `0` 和负数。

## [2.6.20] - 2026-08-13

### Added

- `bohr wenyon` 子命令对所有用户开放，不再需要 entitlements 权限。
- `npm install -g @dptech-corp/bohr-cli` 现在通过 postinstall 自动下载对应平台的 wenyon-cli（external 变体），无需额外操作。
- `bohr auth login` 成功后自动写入 `~/.vouch/state.json`，wenyon-cli 认证无缝衔接。
- `bohr auth logout` 同时清理 wenyon 认证状态。
- `bohr wenyon` 调用前自动检测并刷新过期的 wenyon token。
- wenyon-cli 下载新增 SHA256 校验。
- 新增独立安装脚本 `scripts/install.sh`（Linux/macOS）和 `scripts/install.ps1`（Windows）。

### Changed

- wenyon-cli 下载改用 `variant=external` 参数获取外部版产物。
- wenyon 平台检测扩展支持 windows-amd64 和 windows-arm64。

## [2.6.19] - 2026-08-13

### Breaking

- **`bohr file` 的远端路径改为由 `personal/` 或 `share/` 前缀唯一决定空间**。改前：多数子命令会把任意路径直接发给后端，并可能用 `--project-id` 或 `--space` 推断空间；改后：个人盘只接受 `--project-id 0`，共享盘要求正数 `--project-id`，非法前缀、除 `file list` 外的根目录操作和跨空间复制/移动会在请求前报错，省略 `file list` 路径或传 `/` 均表示个人盘根目录。调用方需给路径补上对应空间前缀，并为 `share/...` 传正整数 `--project-id`。

### Deprecated

- `bohr file list/upload --space` 已废弃，请改用路径前缀；兼容期内该参数只校验显式路径前缀且必须与其匹配，不能再为 `file list` 补全无前缀路径。`file upload` 的无前缀远端路径会暂时补全并输出迁移告警。

## [2.6.18] - 2026-08-13

### Added

- **`bohr agents` 新增完整科学 Agent 命令面**：用户可通过 `run/watch/sessions/artifacts` 运行 Agent、查看实时进度并下载交付物；Agent、Skill、Model、Team、Workspace、Trace、Sandbox、MCP、Memory 等管理能力统一放在 `bohr agents dev` 下。业务请求经 OpenPlatform `/openapi/v4/shrimp/*path` 透明转发到 Shrimp，CLI 不提供通用 request 透传命令。
- **未发布 Agent Draft 可直接运行验证**：`bohr agents run --agent <id> --agent-target draft` 使用当前 Draft，只允许有编辑权限的用户，不需要绕过 local Agent 的发布计费门禁。
- **会话 Workspace 输入文件可显式附加到任务**：`artifacts upload` 返回 `attachmentId` 和文件路径；在已有会话的 `run` 中通过可重复的 `--workspace-file /bohr-workspace/...` 让 Agent 读取对应输入文件。
- **Workspace 新增纯 CLI cloud 目录引导**：支持浏览、创建当前用户 Drive 目录，注册目录身份并创建或更新 Workspace，无需先打开网页端。
- **自定义 Model 管理命令保持可发现并由服务端严格鉴权**：管理员可执行 custom model CRUD/test，普通用户只能查看自身可选模型，不能通过 CLI 注入自定义 provider。

### Changed

- **Agent 命令面仅发布当前稳定上游能力**：不包含高级 queue 编辑与 session queue-control；保留任务自动排队等待、取消已排队任务、SSE、终态校验及交付物下载。

### Fixed

- **Agent Platform 的稳定失败码已纳入 CLI 统一错误目录**：Queue、Workflow、Artifact、Skill、Model、Workspace 与权限错误不再被新版统一输出层降级为 `UNKNOWN`；`bohr schemas errors` 和 `error.v1` 会同步发布这些错误码及退出语义。
- **Sandbox 交付物的两个单文件下载命令改走真实可用的 Session Workspace 原始字节接口**：`bohr agents artifacts download` 会先按 Artifact 元数据选择下载路由，`bohr agents dev sandbox download-file` 会先解析 Sandbox 绑定的 Session；不再分别返回 HTTP 400 和 404，非 Sandbox Artifact 与虚拟 KB 文件的既有下载协议保持不变。
- **Skill 路径统一使用稳定名称，workflow 空 body 取消不再触发上游 500**。
- **local/remote Session 继续执行补齐客户端身份参数**，cloud Session 行为不变。
- **Agent 命令完整接入统一计费门禁**：`run`、Batch/Cron 触发、Sandbox 创建/批准和 Compute 批准按下游实际用量提示并进入 `bohr billing pricing --resource agents`；其余命令显式标记为免费，不影响既有 SciMaster 策略。
- **补齐 Agent 读取、观察和下载命令的 `--dry-run`**：284 个叶子命令及 3 个可执行分组短写均可在不联网、不等待、不创建或覆盖文件的情况下预览；使用文档中的全部命令已做零网络 dry-run 扫描。
- **`bohr agents watch --output-dir` 兼容 Shrimp 轻量 Workflow 状态响应**：当状态接口不返回 `sessionId` 时，CLI 会按需从 Trace 详情解析会话，再收集并下载交付物；显式 `--session` 和 `run --output-dir` 的既有行为不变。

## [2.6.17] - 2026-08-13

### Fixed

- Job、BatchJob、Node、Notebook 与 Sandbox 的资源计费确认现在统一按实际的余额口径提示，不再误称优先使用光子；对应的 `+verb` 快捷命令同样生效。

## [2.6.16] - 2026-08-13

### Fixed

- `bohr agents scimaster review-paper submit` 对已移除的 `--type` 参数现在会在计费确认、文件读取和网络请求前返回结构化 `VALIDATION_FAILED` 与迁移提示；`--type full` 提示直接删除参数，`--type light` 明确该能力已下线且没有替代用法。

## [2.6.15] - 2026-08-13

### Breaking

- **`bohr agents scimaster review-paper submit` 移除 `--type` 参数**。改前：CLI 暴露 `full`/`light` 两种审稿类型，但线上已关闭 `light`，传入 `--type light` 只会返回 HTTP 422 且不创建任务。改后：命令不再暴露审稿类型选择，并在内部继续按现有后端协议提交受支持的审稿任务；调用方需删除 `--type full`，`--type light` 因功能下线没有替代用法。

### Changed

- SciMaster 文档补充 `bohr paper search`、`bohr agents scimaster search-paper` 与 `review-paper` 的选择说明，区分普通论文库筛选、异步智能检索和论文审稿。

## [2.6.14] - 2026-08-12

### Fixed

- `bohr batchjob download` 在缺少 `--dest` 或目标目录已存在时现在会返回标准错误信封和非零退出码；目录冲突使用稳定错误码 `BATCH_JOB_RESULT_DESTINATION_EXISTS`，两种错误都会给出指向可用目录且可安全复制执行的命令提示，不再退化为纯文本错误。

## [2.6.13] - 2026-08-12

### Fixed

- `bohr schemas errors` 与 `bohr schemas get error.v1` 现在会发布 CLI 实际返回的完整稳定错误码集合，并与进程退出码共用同一注册表；未注册的外部错误码会统一返回 `UNKNOWN`，原码在没有既有 subtype 时保留于 `error.subtype`。

## [2.6.12] - 2026-08-12

### Breaking

- **`bohr kb search` 移除无效的 `--top-k` 参数，并对齐当前知识库搜索返回结构**。改前：CLI 接受 `--top-k` 但不会把它传给后端，human 输出还按已废弃的 `items/content/score/documentId/title` 结构解析结果。改后：命令直接渲染平台返回的 `data.Files` 与 `data.total`，不再伪造旧结构；调用方需删除 `--top-k`，需要稳定字段时改用 `-o json` 消费 `data.Files` 和 `data.total`。

### Changed

- CI 现在会把文档中的命令路径、参数和 `--jq` 表达式与真实 Cobra 命令树及代表性输出进行一致性校验；README 与中英文使用文档中的失效示例已同步修正。

### Fixed

- `bohr billing pricing` 的 JSON `aliases` 字段和 human `ALIASES` 列现在会展示收费 `+verb` 快捷命令与 canonical 命令的对应关系（例如 `bohr sandbox +run` 对应 `bohr sandbox create`），且不会为别名复制价格行。
- 无效的嵌套命令路径现在会返回明确错误和非零退出码，不再错误展示父命令帮助并退出 0；保留用于兼容的 deprecated skill 子命令也会继续出现在父命令帮助中。

## [2.6.11] - 2026-08-12

### Fixed

- `bohr pdf result` 现在将生产 UniParser 返回的 `status=error` 与 `status=failed` 一并映射为 `PARSE_FAILED`，保留失败描述并返回非零退出码，不再把加密或不受支持 PDF 的解析失败报告为成功。
- `bohr sandbox create`、`bohr sandbox +run`、`bohr sandbox image build` 和 `bohr sandbox image commit` 的计费确认现在按真实的余额扣费口径提示，不再误称优先使用光子；其他资源和按次命令的既有提示保持不变。

## [2.6.10] - 2026-08-12

### Added

- `bohr pdf result` 新增 `--objects`、`--pages-dict`、`--pages-tree` 结构化结果开关和 `--formatted plain|markup|markdown|latex|html` 格式化输出；图表、图例、坐标及层级结构保持 UniParser 原始字段，不二次推断数值。

### Fixed

- `bohr pdf result` 的 human 输出改用 UniParser 实际返回的 `token/status/content/proc_page/total_page` 并汇总结构化块；上游 `status=failed` 现在返回 `PARSE_FAILED` 和非零退出码。

## [2.6.9] - 2026-08-12

### Fixed

- queued Job 执行 `bohr job +cancel` 时默认在取消受理后立即返回，避免因遗留后端状态同步延迟无意义等待；需要等待展示状态收敛时可显式传 `--wait`。queued Job 执行 `terminate` 会在本地返回 `RESOURCE_CONFLICT` 和可执行的 `+cancel` 提示，不再请求后端的 running-only 接口；`--wait` 与 `--no-wait` 同时使用时会明确报错。

## [2.6.8] - 2026-08-11

### Changed

- `bohr job kill` 和 `bohr job terminate` 默认等待最多 60 秒确认任务终态，并在结果中返回 `accepted`、`confirmed`、`terminal` 和 `phase`；使用 `--no-wait` 可立即返回。

### Fixed

- `bohr sandbox delete` 的非 `--force` 预检失败现在返回结构化错误；本地无法确认安全删除时返回 `PRECONDITION_FAILED` 和可执行的 `--force` 提示，上游鉴权及资源不存在错误保持原样。
- `bohr auth login --resume` 支持跨命令续接设备码登录；待完成状态按 profile 保存，过期、拒绝和上游失败均返回可判读的结构化结果。
- `bohr node restart` 会携带实际设备类型和自动关机时间，新增 `--turnoff_after/-t` 参数，修复容器和 VM 重启配置错误。
- `bohr node stop` 和 `bohr node delete` 现在发送合法 JSON 请求体，不再因空请求体返回 EOF。
- `bohr agents scimaster search-paper` 对齐 `mid/high` 模式的默认参数和结果数量语义，并在提交及等待期间展示有效请求、任务 ID、进度和恢复命令。

## [2.6.7] - 2026-08-11

### Changed

- **正式 release tag 现在必须指向经 MR 合入产生的 merge commit**。改前：tag build 只校验 CHANGELOG、版本号和平台包 pin 等发布物内容，像 v2.6.6 这样把功能、CHANGELOG 与版本 bump 直接推到 `main` 后打 tag，仍能绕过 review 正常发布。改后：`vX.Y.Z` tag 指向的 commit 必须包含至少两个 parent；直推或 cherry-pick 形成的单 parent commit 会在构建发布物前被 CI 拒绝。项目若将来明确改用 fast-forward merge，必须在流水线中显式设置可审计的逃生开关。

### Fixed

- **`bohr dataset delete` 与 `bohr sandbox delete` 不再吞掉删除失败或把批量失败包装成成功**。改前：子请求返回 `ok:false` 时，`dataset delete` 仍可能退出 0；`sandbox delete --force` 还会返回外层 `ok:true`、退出 0，批量调用方无法根据外层信封或退出码判断是否真的删掉。改后：单条与批量删除都会保留每项结果，任一子项失败即令外层信封 `ok:false`，沿用首个失败的结构化错误并返回对应非零退出码；CLI 内部请求错误同样不再只打印后继续。

## [2.6.6] - 2026-08-10

### Added

- **新增 `BOHR_VOUCH_TOKEN` 环境变量支持**：vouch token 此前只能从 `~/.bohr-cli/vouch.json` 读取，CI/容器等无法执行 device-flow 的环境下无法使用 vouch 鉴权。现在设置 `BOHR_VOUCH_TOKEN` 环境变量即可注入 token，优先级高于文件；trisol/wenyon 的 token 注入同样支持该变量。

### Fixed

- **preview npm 发布现在同步发布主包与六个平台二进制包**。改前：preview 流水线只修改并发布 `@dptech-corp/bohr-cli` 主包，六个 `optionalDependencies` 仍指向旧正式版，平台 preview 包也不存在，导致安装 `@preview` 后实际运行旧二进制。改后：CI 使用同一个下一 patch preview 版本暂存七个包，先发布六个平台包并将主包 pin 精确更新到该版本，最后发布主包；手动撤回也按流水线冻结版本清理整套七包。

## [2.6.5] - 2026-08-10

### Breaking

- **Notebook 计算命令现在进入统一计费确认门禁**。改前：`bohr notebook server start`、`server restart` 和 `notebook run` 会启动按资源及时长计费的 Notebook Node 或 BatchJob，但没有 billing annotation，非交互调用无需确认即可请求或上传，`bohr billing pricing` 也查不到它们。改后：三条命令均按 `billing_resource` 处理，未带 `--yes/-y` 的 Agent/CI 会在任何远端请求或上传前收到 `CONFIRMATION_REQUIRED`；价目目录新增 `--resource notebook`，其中 Server 价格依据 Core 的 Notebook 场景 Node SKU，异步运行依据 BatchJob 机型。调用方要怎么改：非交互调用显式加 `--yes`，或在获得授权后运行 `bohr config set billing_confirmation false --yes`。
- **原始 `bohr api` 请求现在按下游计量进入统一确认门禁**。改前：该命令被标成免费，即使调用收费端点也能绕过根级计费确认。改后：CLI 不再根据任意且持续演进的路径猜测免费或收费，所有实际发送的原始请求均以 `billing_downstream` 保守确认，价目表新增 `api.raw`；`--dry-run` 仍只预览且不确认。调用方要怎么改：已获授权的非交互请求显式加 `--yes/-y`，或优先使用具有精确计费策略的类型化命令。
- **SciMaster 提交命令的计费 subtype 从 `billing_per_call` 校正为 `billing_downstream`**。改前：CLI 已在价目表声明下游按实际用量计量，却把两条 `submit` 命令标成按次计费，结构化拒绝信封会给出未经证实的 `billing_per_call`。改后：`search-paper submit` 与 `review-paper submit` 仍在执行前要求确认，但明确表示费率和计费单位由下游决定，不再猜测按次扣费，结构化价目表省略未知的 `unit`。调用方要怎么改：若按 `error.subtype` 分支处理，将这两条命令识别为 `billing_downstream`；不要再依赖未经证实的 `unit=usage`，`--yes/-y` 用法不变。

### Changed

- **CI 现在要求每个可执行命令显式声明 `free`、`per_call`、`resource` 或 `downstream`**，并根据实际完整价目表双向校验全部收费命令的精确计费类型。新增功能若忘记计费判断、确认门禁、价目条目，或用可收费标识掩盖了错误的计费形态，都会在 billing conformance 步骤直接失败；重复 ID、缺失字段、同命令冲突策略和不完整价格形态同样失败。hidden 独立命令受检，shortcut 继承并校验目标策略；根级 gate 启用 Cobra hook 遍历，子命令自己的 hook 不再能覆盖计费确认。

## [2.6.4] - 2026-08-09

### Added

- **发版门禁新增「CHANGELOG 已发布节不可变」检查**。2.6.3 发版曾把已发布的 `## [2.6.2]` 节标题直接改名成 `## [2.6.3]`——2.6.2 从变更史里消失、七条条目被错误归属，而 `verify-changelog.sh` 只断言「新版本节存在非空 + [Unreleased] 已清空」，改名恰好同时满足两条。新增 `scripts/verify-changelog-history.sh` 在 tag build 阶段以上一个正式 tag 为基线断言：基线里的每个版本节标题必须仍然存在；比基线更老的节逐字不变；基线自己那节的条目行允许搬进别的节（归属更正）但不允许消失。拿基线拿不到（克隆无 tag）且文件里有历史节时响亮失败，不 fail-open。
- **发版门禁新增「平台子包 pin 一致性」检查**。一次 bump 要手工同步 9 处版本号，其中主包 `optionalDependencies` 里六个平台子包的 pin 此前无任何检查——漏改任意一个，现行三项版本校验照样全绿，发出去就是「主包版本 X、该平台实装旧版二进制」的错位发布物。新增 `scripts/verify-package-pins.sh` 在 tag build 阶段逐个断言六个 pin == 主版本，且平台清单与发布循环一致（多出 CI 不发布的平台、或少一个平台，同样硬失败）。

## [2.6.3] - 2026-08-08

### Added

- **npm 包新增 README，并公告存量用户升级路径**。npm 页面此前没有任何说明文字，而 2.6.2 修复的自更新缺陷有一个无法由代码解决的死角：2.6.1 及以下版本跑的是各自内置的旧更新器，`bohr update` 在这些版本上必然失败，修复永远送不到他们手里。README 置顶写明：存量用户需手动执行一次 `npm install -g @dptech-corp/bohr-cli@latest`，此后即可恢复 `bohr update` 自更新。同时附安装说明、快速开始与文档入口。

## [2.6.2] - 2026-08-08

### Breaking

- **`bohr image pull` 不再接受非 Bohrium registry 的镜像地址**。改前：地址改写是一句无校验的 `strings.Replace(addr, "registry.dp.tech", …)`，地址里没有这段就原样放行——于是 `bohr image pull ubuntu:22.04` 会先登录 Bohrium registry、再从 **Docker Hub** 把镜像拉下来，并报告成功；命令做的事和它的名字不是一回事。改后：只接受 `registry.dp.tech/`、`registry.bohrium.dp.tech/`、`registry-test.bohrium.dp.tech/` 开头的地址，其余返回 `VALIDATION_FAILED` 并在 `hint.command` 里给出对应的 `docker pull` 命令。调用方要怎么改：拉非 Bohrium 镜像直接用 `docker pull`，本命令只负责 Bohrium 镜像。

### Fixed

- **`bohr image pull` 缺参数时不再返回退出码 0**。改前：不带地址时打印两行提示后 `return nil`，退出码 0、无信封——Agent 会把「忘了传参」读成「镜像已经在本地了」。现在返回 `VALIDATION_FAILED` 信封、非零退出码，并在 `hint` 里给出完整示例命令。
- **`bohr image pull` 全链路改为信封输出**。改前：成功、无 AccessKey、docker 失败、超时四个出口全是 `fmt.Println` 纯文本，`-o json` 完全无效；无 AccessKey 时是一句裸文本加退出码 1，而同一目录下的 `bohr image list` 对同一种情况返回 `AUTH_REQUIRED` 信封加退出码 2。现在四个出口都是信封：成功回 `{image, requested, registry}`；无 AccessKey 回 `AUTH_REQUIRED`（退出码 2，与其余命令一致）；本机没有 docker 回 `PRECONDITION_FAILED` 并说明本命令依赖本地 Docker daemon；超时回 `UPSTREAM_TIMEOUT`、`retryable=true`，保留原有的「先查 `docker images` 和 `df -h`」说明。Docker 自身的进度输出改走 stderr，因此 `-o json` 时 stdout 只有一个信封，不再被进度条污染。
- **修复 test 环境用户被静默指向生产 registry**。改前：registry 选择比对的是 `host == "https://openapi.test.dp.tech"`，而 `config.GetHost()` 是白名单，只可能返回 `open.bohrium.com` / `open.test.bohrium.com` / localhost / 生产默认值，`internal/migration` 还专门把那个旧 host 改写掉——该分支不可达。后果是 test 环境用户拿到生产 registry，用 test AccessKey 登录必然失败且看不出原因。现在按实际可返回的 host 选择 `registry-test.bohrium.dp.tech`。
- **`bohr image pull` 补齐文档**。此前全仓 `grep "image pull"` 只命中它自己的源文件，任何文档、SKILL、reference 里都没有，用户与 Agent 无从知道这条路存在。`docs/zh/image.md` 新增完整章节（用途、前置条件、参数、失败码与退出码对照），中英文 usage-guide 的镜像段同步补上一行。文档写明了两条容易踩的前提：需要本机 Docker daemon（因此不适用于 Agent / 沙箱 / CI），以及拉取是同步的、全局 `--wait` 对它不起作用。
- **`bohr schemas errors` 与 `bohr schemas get error.v1` 补上 `UPSTREAM_TIMEOUT` 与 `PRECONDITION_FAILED`，并新增两者的一致性校验**。两个码此前已经在发、也已经各自有退出码映射，却不在这份清单里——一个查不到的错误码，调用方只能靠猜。更要紧的是这两份清单各自手工维护：只补其中一份，CLI 就会自相矛盾——发出一个「自己的错误目录说合法、自己的已发布 schema 判非法」的码，拿真实失败信封去校验会挂在一个正确的信封上。现在两份都补齐，并加了双向校验测试（该包此前零测试）。注：`envelope.Error.ExitCode` 是同一批事实的第三份手工副本，把三者收敛成一份注册表需要改动已发布产物与所有发码的命令，留作后续。
- **`bohr image pull --dry-run --timeout 0` 不再静默通过**。`--timeout` 的校验此前紧挨着它的使用点，位于 dry-run 分支之后，于是 dry-run 会正常打印计划、对非法的时限只字不提。校验已移到其余入参校验处。
- **`bohr image pull` 在本机缺少 docker 时补 `error.subtype = docker_not_found`**。`PRECONDITION_FAILED` 命名的是一类失败，subtype 才说清是哪个前置条件，调用方不必解析文案。
- **`bohr update` 在 2.6.0 分包之后恢复可用**。2.6.0 把六个平台二进制从主包拆进按平台分的子包，`internal/update` 没有跟着改：暂存校验无条件要求主包里存在 `bin/bohr-<goos>-<goarch>`，而 2.6.x 主包已经没有 `bin/` 了，于是升级在暂存阶段就以一句裸 Go 错误失败；装了 2.6.x 之后反方向也断——运行中的二进制位于子包，包名比对必然不等于主包，于是掉进 `installStandalone`，那条路用的是同一个暂存校验。现在所有需要二进制的地方都**先查再用**，胖包与分包两种形态都认。
  一并修掉的是分包给版本库带来的第二处断裂，它不会自己暴露：`npm install` 会把平台子包**提升**为主包的兄弟目录（实测 npm 10.9.4；评测机上则是嵌套——由 npm 决定，不由发布物决定），而版本库只搬主包，子包随暂存目录一起被删。只放宽暂存校验的话，升级会照常改完软链，再由激活校验发现跑起来的是**全局残留的上一个版本**的二进制，然后回滚——失败点从「任何改动之前」挪到「改动之后」。现在子包会先被移入主包内部，每个版本目录自包含。
- **`bohr update` 失败改出信封**。改前：失败是一条裸 Go 错误字符串写 stderr，`-o json` 下同样没有信封（无 `error.code`、无 `hint`），而自更新恰恰是调用方最需要能判读的一条路。现在统一为 `PRECONDITION_FAILED` 信封（Windows 上的「不支持事务式自更新」为 `NOT_IMPLEMENTED`），带 `error.subtype` 区分具体前置条件，`hint.command` 一律给出可执行的 `npm install -g @dptech-corp/bohr-cli@<版本>`。
- **`bohr update --check` 不再引导用户去跑一条在本平台跑不通的命令**。改前：只要有新版就无条件打印 `Run 'bohr update' to install.`，包括 Windows——那里的自更新从来就没实现过。现在 `--check` 与 `update` 结论一致：能自更新才推荐 `bohr update`，否则直接给出对应的 `npm install -g` 命令。
- **分包后取不到平台子包时 `bohr` 改出信封**。npm 跳过可选依赖时退出码是 0，所以「装完了但跑不起来」此前只表现为 `npm/run.js` 打的三行纯文本；`--omit=optional`、旧版 npm、pnpm 全局装都会走到这里，而这是调用方能碰到的最早一个出口。现在两个出口（子包缺失、平台架构不受支持）都产出 `PRECONDITION_FAILED` 信封并写 stdout，`error.subtype` 分别为 `platform_package_missing` 与 `platform_unsupported`，仍保留「npm 没装上当前平台的可选依赖」这句归因。同时修正退出码透传：子进程被信号杀死时 `e.status` 为 `null`，旧写法 `e.status || 1` 会把它退成 1；现在有退出码就原样透出（实测 `CONFIRMATION_REQUIRED` 的 10 正确透传），被信号杀死则退 `128+signal`。

## [2.6.1] - 2026-08-07

### Changed

- **npm 包分发改为按平台拆包**：主包 `@dptech-corp/bohr-cli` 不再内含 6 个平台二进制（~195MB），改为通过 `optionalDependencies` 只安装当前平台对应的子包（~30MB）。安装方式不变（`npm install -g @dptech-corp/bohr-cli`），安装速度提升约 5 倍。

## [2.5.25] - 2026-08-07

### Added

- **新增完整的 `bohr notebook` 工作流**：支持 `server` 管理 Notebook 实例的资源查询、创建、状态、用量、列表、停止、重启和删除；支持 `cell` 在本地原子编辑 `.ipynb`；支持 `run` 通过隔离的异步 BatchJob 执行并等待结果；支持历史、搜索、发布、转私有、协作者与评论管理。异步执行会产出 `executed.ipynb` 和 `notebook.log`，不连接实时 Jupyter Kernel，也不继承其内存、临时包或容器根盘文件。
- **新增 `bohr batchjob download <job_id> --dest <目录>`**：从终态 BatchJob 下载并安全解压结果包；仅接受受信 HTTPS 存储地址，限制下载体积、解压体积和条目数，拒绝路径穿越、符号链接及特殊 ZIP 条目，并原子写入一个尚不存在的目标目录。预签名 URL 始终只在进程内使用，不进入命令输出。
- **正式 npm 发布成功后新增飞书群通知**：语义化版本 Tag 流水线发布 `@dptech-corp/bohr-cli` 后，由群自定义机器人发送一张 Card 2.0 卡片，分为发布信息、本版本全部公开变更和相关链接三段。Webhook 与签名密钥只从 Masked + Protected CI 变量读取；通知失败只将流水线标为 warning，不会把已经完成的 npm 发布改判为失败。

### Fixed

- **修复 `bohr notebook server start --dataset` 无法兼容分页式数据集版本响应**：版本预检现在同时接受数组和 `{items:[...]}` 两种上游结构；`null` 或缺少条目的异常响应会在创建 Notebook 前明确失败，避免跳过预检或把合法版本误判为不存在。
- **修复 debug 模式可能把 AccessKey、上传凭据或预签名 URL 写入终端日志**：OpenAPI 与存储客户端的 debug 行为改为只保留请求耗时等元数据，不再启用原始请求/响应 dump；BatchJob 下载仍在进程内使用完整签名地址，对外展示时会移除 query 与 fragment。

## [2.5.24] - 2026-08-07

### Breaking

- **`bohr agents scimaster search-paper submit` 与 `review-paper submit` 现在受计费确认门禁约束**。改前：`search-paper submit` 完全不经门禁，带真实 AccessKey 的 Agent 会无确认直接提交计量任务；`review-paper submit` 有一道写在命令内部的确认，非交互下返回退出码 1 加一行纯文本（无信封、无 `error.code`、无 `hint`），且不认 `billing_confirmation` 配置——照官方文档配好的 Agent 仍会卡在这里，卡住的理由在信封里查不到。改后：两条命令与其余 23 条计费命令行为一致，非交互下返回 `CONFIRMATION_REQUIRED`、退出码 10、带 `hint.command`，`bohr config set billing_confirmation false` 对它们同样生效。调用方要怎么改：脚本与 Agent 在这两条命令上补 `--yes`，或按文档配置 `bohr config set billing_confirmation false --yes`。另：`review-paper submit` 原先用 `os.Stdin.Stat()` 判字符设备，会把 `/dev/null` 当成交互终端，该判断随之移除。

### Added

- **`bohr paper patent` 新增 `--type` 档位参数**：`0`=普通 / `1`=加强 / `2`=专业。价目表一直有这三档，而命令此前只能发出最便宜的那一档。
- **`bohr image pull` 新增 `--timeout`**（默认 30 分钟）：`docker login` 与 `docker pull` 此前不受任何一层的时限约束——命令没有、`--wait` 没实现、docker 自己会对停滞的层无限重试。评测机上有一个 pull 在拉起它的脚本早已结束后仍挂了 15 天。超时会终止子进程并返回结构化说明：哪一步超时、本地镜像可能处于什么状态、用 `docker images <目标>` 复核、以及重跑时调大 `--timeout`。
- **`bohr billing pricing` 补齐 `bohr agents scimaster` 两个条目**（`--resource scimaster` 可单独查看）。SciMaster 由下游按实际用量自行计量扣费，费率与计费单位都不由本 CLI 发布，因此这两项的 `price` 为 `downstream`、`unit` 为 `usage`（human 输出显示「下游计量 / 按实际用量」），而不是复用表示按机型时长计费的 `dynamic`，也不声称按次计费；`search-paper` 附 `details_command` 指向 `bohr agents scimaster search-paper quota`，human 输出标注为「额度查询」而非「价格查询」——那条命令返回的是账号剩余免费次数，不是单价。此前这两条命令收费却不在价目表里，Agent 事前查价只能得到「免费」这个错误结论。

### Fixed

- **修复 `bohr image delete` 恒失败：`invalid param EOF`**。上游 `DELETE /openapi/v4/image/private/:id` 会绑定一个必须携带 `device` 字段的 JSON body，而 CLI 把 `device` 发成了 query 参数——`internal/client` 的 `Delete` 是唯一一个第二个位置参数表示 query 而非 body 的方法（`Post`/`Put`/`Patch` 都是 body），当初补 `device` 的改动因此写进了错误的位置，代码读起来像是发了、实际从未发出。现在 `Delete` 与其余动词签名一致（`Delete(path, body, queryParams...)`），`image delete` 发送 `{"device":"container"}`。同一根因还修好了 `bohr api DELETE <path> --data '<json>'`：`--data` 此前会被 `--dry-run` 原样回显、然后在真正发出时丢弃，于是「用裸 API 绕过去」这个变通手段失败得和它要绕的命令一模一样。
- **修复 dataset / job 上传中等大小文件必定超时**：小于 50 MiB 的文件走单发 POST，该路径的客户端总超时硬编码为 20 秒。总超时施加在上传请求上等于对带宽下注——按单连接实测约 0.4–1 MB/s，20 秒只够约 8 MB，而选择这条路径的阈值是 50 MiB，中间整段区间稳定失败三次后报网络错误。现在单发路径与分片路径共用同一个数据面上限（原本分片是 2 小时、单发是 20 秒，两者做的是同一件事却各有各的数字），结束上传的是 Ctrl-C 或调用方自己的 deadline，而不是编译期写死的带宽猜测。
- **修复 `bohr config set` 把全部全局 flag 快照写进配置文件**：全局 viper 绑定了所有通用 flag，而 `WriteConfig` 序列化的正是 viper 知道的一切，于是 `bohr config set billing_confirmation false --yes`——文档里唯一那条 Agent/CI 配置命令——写出的文件里还有 `yes: true`、被钉死的 `output` 与 `openapi_host` 等十余个键（实测隔离 HOME 下 27 个）。那个 `yes` 从未真的绕过删除确认（`GetYes` 读 flag 不读 viper），但一份读起来像永久授权的配置文件，麻烦程度和真的授权了一样。现在只写用户显式设置的键，已有内容保持不变。
- **修复多给位置参数时报错第一行指错对象**：`bohr sandbox image build extra-arg` 的 stderr 首行是 `unknown command "sandbox" (not a registered command or installed extension)`——扩展分发的兜底路径只按子串匹配 "unknown command"，然后拿命令链的**头**去找扩展。按首行归因的调用方（含 Agent）会被告知一个存在的命令不存在、且可能需要安装扩展。现在仅当 Cobra 拒绝的正是 `os.Args[1]` 本身（即 `unknown command "<arg>" for "bohr"`）才走扩展兜底，真正的未知顶层命令行为不变。
- **`bohr tools info` 的错误与默认输出不再让人自己猜**：①传工具名而非 key 时（新用户第一反应，因为 search 最醒目的就是名字），失败信封现在带 `hint.action=lookup_tool_key` 与 `bohr tools search <名字> --output json`，message 明说这里要的是 tool unique key 不是显示名——该命令按次计费，靠试错找参数是要花钱的；缺参数时的报错同样说明 key 从 `tools search` 结果的 `Key:` 字段来。②human 输出此前按固定白名单渲染、把其余字段（含容器镜像）静默丢弃，于是"看不到镜像"被读成"没有镜像"——实际 `--output json` 里一直都有。现在未渲染的标量字段直接打印、结构化字段列出名称，并提示用 `--output json` 取全量；没有被丢弃的字段时不打印这段。
- **`bohr sandbox describe` 不再把正常的销毁过渡态报成故障**：沙箱删除后约 15 秒内上游回 409 `sandbox is not available for operation`，此前透出为 `INVALID_REQUEST` 且带上诊断**失败**沙箱的 hint（`sandbox list --query failed`），轮询方无法区分"还在关，继续等"和"出事了"。现在该形态标注 `error.subtype=sandbox_destroying`、`retryable=true`，hint 改为「正在销毁，轮询至 RESOURCE_NOT_FOUND」。判定收得很窄（仅 `INVALID_REQUEST` + 该消息），因为 409 也用于真正的冲突。`sandbox list` 的 `--query active` 同步说明：它是「平台尚未回收的」而非「可用的」，包含 `status_name=destroying`，判断可用性应读 `status_name` 而不是「在不在列表里」。
- **`bohr paper search --type` 补文档与校验**：原帮助只写 `Search type filter`，既不说取值也不说不同档位价格不同——加强版不是独立端点、而是 keyword 接口的一个参数，于是它可达却不可见。现在帮助写明 `0`=普通 / `1`=加强并指向 `bohr billing pricing --resource paper`；超出范围的档位在请求发出前即被拒绝（避免按未知价格计费），`patent` 同理（0/1/2）。默认档位行为不变，不传 `--type` 时请求体依旧不带该字段。
- **`dataset create` 失败时不再一律建议 `--resume`**：此前只要会话目录存在就打印该建议，而会话目录从每次运行的第一刻起就存在，于是它在 `--resume` 根本帮不上忙的失败上也照打不误。小于分片大小两倍的文件走单发 POST、按构造不留任何可续状态，对这类数据集重跑 `--resume` 只会原样重发那个刚失败的请求。现在仅在确有已完成文件或已完成分片时给出该建议并附上数量，否则明说「没有可续状态，`--resume` 会从头重传」。

### Changed

- **计费门禁新增完整性检查（CI 强制）**：`bohr billing pricing` 目录里出现的每一条命令，都必须能在命令树里解析到、且带有计费标注。此前门禁靠逐条手工登记，`bohr agents scimaster` 在 2.5.20 随新命令族整体落在门外而 CI 全绿——因为没人把它加进名单，所以「名单里没有缺失项」。现在检查从价目目录反推，新增命令族只要进了价目表就必须过门禁，漏登记会在 CI 直接失败。

## [2.5.23] - 2026-08-06

### Added

- **`bohr sandbox list` 的 human 输出新增 `COST(CNY)` 列**：费用来自列表接口每条沙盒记录的可选 `cost` 字段，按人民币元显示两位小数；明确的零显示为 `0.00`，历史数据缺少订单或费用尚未返回时显示 `-`，不会把未知误报为零。`--query failed` 的诊断表同样保留费用列。

### Changed

- **Sandbox 的 `-o table` 不再因首条历史记录缺少 `cost` 而隐藏后续记录的费用**：仅在表格渲染阶段补齐缺失键，JSON/YAML 继续保持接口原始的缺失与显式零语义。内置 Sandbox 与 Batch Job Skill 同步说明列表费用字段、CNY 单位、结算延迟以及 `notice.billing.balance` 是账户余额而非资源费用。

## [2.5.22] - 2026-08-06

### Fixed

- **修复 `dataset create --resume` 撞上已失效的分片会话时整个上传失败**：续传缓存里保存着上一次中断时的 multipart 上传 id，若那次上传从未完成，对象存储会对写入该 id 的每个分片回 `NoSuchUpload`——确定性的、每次重试都一样。此前这个回答在分片路径上没有被识别（只有 complete 阶段认得它），于是重试三次后整个文件上传失败，而用户要的恰恰是「接着传」。现在分片路径同样识别该形态：不再浪费重试，并在**确实是续传**的情况下丢弃失效的缓存 id、重开一个 multipart 会话把该文件重传一遍，同时给出 `DATASET_UPLOAD_SESSION_REBUILT` 提醒（该文件是整份重传、不是增量）。非续传场景下会话失效仍按错误抛出、不做重建，避免把另一类故障吞成静默循环。

## [2.5.21] - 2026-08-06

### Added

- **`bohr sandbox template create` 新增 `--visibility` 控制模板可见性**：`1`=公开（平台所有人可见、可用于创建沙箱），`2`=私有（仅自己，除非显式分享）；不传 `--visibility`（或 `--file` 中的 `visibility` 字段）时沿用后端默认的私有，不会误公开。取值仅接受 `1` 或 `2`，其余值在请求发出前即被 CLI 拒绝。内置 Sandbox Skill 同步补充可见性说明（顶层 SKILL.md 命令表与 `references/sandbox/templates.md` 的 Visibility 小节），含「公开模板不支持自定义额外临时存储」的约束，便于 Agent 感知这一点。

### Fixed

- **修复分片上传遇到服务端错误时整体失败且不给原因的问题**：`RespUploadByPart` 把 `error` 字段声明为字符串，而存储服务返回的是对象（本文件里其余响应类型一律用 `*ErrInfo`，只有这一处例外）。由于失败响应体恰恰只在分片被拒时才解码，一旦某个分片遇到服务端错误，解码就在那里失败并直接返回 `json: cannot unmarshal object into Go struct field RespUploadByPart.error of type string`——真实原因（配额、鉴权、分片校验）全部丢失，且这个返回发生在构造带 HTTP 状态的分片拒绝错误**之前**，重试逻辑因此无法分类，三次重试后整个上传失败。现在 `error` 兼容对象、字符串与 null 三种形态，无法识别的形态保留原文而不是让解码失败。实测（75 MiB 文件、中断后 `--resume`）：发布版三次里两次以该类型错误告终，修复后同场景恢复正常续传。

- **计费确认被拒时给出可执行的恢复路径**：非交互环境（Agent、CI，或任何管道调用——stdin 不是终端就算）下计费命令返回的 `CONFIRMATION_REQUIRED` 此前只有一句 `billable operation cancelled; pass --yes to confirm`，没有 `hint`，`billing_confirmation` 这个出口在信封里完全不可见，调用方除了重试同一条命令无从判断下一步。现在信封带 `hint.action=confirm` 与 `hint.command`（原命令加 `--yes`），`error.subtype` 区分 `billing_per_call` 与 `billing_resource`，message 明说命令未执行、未产生扣费，并同时给出 `bohr config set billing_confirmation false --yes` 与 `bohr billing pricing`。`retryable` 保持 `false`——重跑同一条命令只会得到同一个拒绝，可执行的是 hint 里那条不同的命令。
- **内置 Skill 补齐计费门禁说明**：`bohr sandbox skill` 与 `bohr batchjob skill` 装出的资产此前教 Agent 写不带 `--yes` 的 `sandbox create` / `image build` / `image commit` / `batchjob submit`，照做即 `CONFIRMATION_REQUIRED`、退出码 10、资源未创建。现在顶层 SKILL.md 各有一节说明门禁、退出码、两个出口与「未获授权不要自行加 --yes」，示例与 lifecycle/images 两份 reference 同步更新。

## [2.5.20] - 2026-08-06

### Added

- **新增 `bohr agents scimaster` 论文工作流**：支持论文检索的配额查询、提交、状态查询和等待，以及论文审稿的列表、提交、状态查询和等待；检索支持语言偏好、模式、结果数量和等待选项，审稿支持全文/作者模式与 `--dry-run` 预览。

## [2.5.19] - 2026-08-05

### Added

- **计费确认新增首次使用引导（MR !55）**：第一次在交互式终端确认计费命令时，CLI 会额外提示 `bohr billing pricing`、单次 `--yes` 和 Agent/CI 关闭逐次确认的配置命令；引导使用独立版本号记录，普通 CLI 升级不会反复显示，未来仅在引导或计费规则明显变化时重新提示。相关命令示例同时加入 `bohr billing pricing --help` 与 `bohr config set --help`。

### Fixed

- **Dataset 并行传输与断点续传指引更准确、可直接执行（MR !56）**：修正 `dataset create --help` 把全局 `-p` 误写成数据集路径参数的问题，补齐并行上传和 `--resume` 的可复制示例；上传恢复会只读汇总真正可复用的文件与 multipart 分片，并在配额、建数据集、取 token、上传或提交失败后说明缓存已保留及恢复参数。`dataset download` 会显示 `.part` 的恢复偏移，明确重复相同命令与 `--dest` 即可自动续传；只有真正可恢复的中断才给续传建议，使用 `--force` 时会提示重试需去掉该参数，协议错误、404 或目标冲突不再误报为可自动恢复。

## [2.5.18] - 2026-08-05

### Added

- **新增计费价格目录与 Agent 可控确认**：`bohr billing pricing` 无需 AccessKey 或网络即可列出 19 个固定人民币计费项和 7 个按机型、时长动态计费的资源项，并提供 Wiki/Tool、LKM 的个人月度免费额度政策及后续机型价格查询命令；所有已识别计费命令在执行前统一显示 `[y/N]`，说明优先使用光子、光子不足时使用余额。自动化可单次传入 `--yes/-y`，或通过全局/profile 的 `billing_confirmation=false` 关闭逐次计费确认；关闭设置需要完整输入 `yes` 或显式 `--yes`，且不会绕过删除等高风险操作原有确认。
- **Dataset 新增并发、断点续传与可靠下载**：`bohr dataset create` 支持命令级全局上传并发、文件与分片续传、源文件/manifest 兼容性校验，并在结果中返回真实 HTTP 峰值 `peak_upload_concurrency`；新增 `bohr dataset download`，支持版本选择、HTTP Range 续传、自动重试与签名刷新、ZIP/CRC 校验，以及跨平台 no-replace 安全发布。

### Changed

- **Dataset 续传状态与敏感信息处理收紧**：上传和下载缓存只保存恢复所需的非敏感状态，不写 AccessKey、上传 token 或签名 URL；敏感版本响应禁用 debug dump。下载完成前始终保留 `.part`，校验通过后才原子发布，避免覆盖已存在文件或把不完整产物当作成功。

## [2.5.17] - 2026-08-04

### Breaking

- **`bohr sandbox files read` 不再把 `--output` 当作本地下载路径**：改前：子命令用 `--output <path>` 作为 `--destination` 的别名，与全局输出格式参数同名，导致该命令的 `-o json` 在请求发出前报 `unknown shorthand flag`，而 `--output json` 会被误读成下载到本地文件 `json`；改后：`--output/-o` 在所有命令中只表示输出格式，文件落盘统一使用 `--destination/-D`，未知输出格式也会在执行前明确失败、不再静默回退为 JSON；调用方需把 `files read ... --output <path>` 替换为 `files read ... --destination <path>` 或 `-D <path>`。

### Added

- **新增沙盒失败原因主动诊断**：`bohr sandbox list --query failed -o json` 通过分页状态查询返回最近失败记录并保留 `failed_reason` / `status_reason`；sandbox 创建、连接及数据面访问失败时，控制面结构化错误与原始 SDK 错误都会提示 Agent 先执行该命令、读取原因后再重试。`--query active|all|running|failed` 同时覆盖旧 active 列表和新的分页状态视图。

### Changed

- **沙盒命令和新建 PTY 默认以 root 执行**：前台/后台 `sandbox exec`、`terminal create` 及未带 `--pid` 的新 `terminal attach` 统一使用 `root`；显式 `--user <user>` 仍可覆盖，新连接已有 PTY 时保留该 PTY 原来的用户。内置 Bohrium Sandbox Skill 同步要求 Agent 默认使用 root。
- **沙盒默认超时统一为 90 秒**：sandbox 控制面请求、数据面连接及前台 `exec` 从旧的 30/60 秒默认值统一为 90 秒；后台 `exec` 未显式传 `--timeout` 时默认 `0`（无限制），显式有限值仍会在到期后终止远端命令。

### Fixed

- **修复 login 与其它命令的 OpenAPI host 解析不同源、AK 获取可能打错主机的问题**：`auth login` 的 AK 获取与 entitlements 刷新此前自行读取全局配置的 `OPENAPI_HOST` 键——受 AutomaticEnv 影响，shell 里无关的裸 `OPENAPI_HOST` 环境变量会静默劫持它（开发环境常见 `OPENAPI_HOST=https://openapi.dp.tech`），ak/list 与 ak/add 被打到不承载这些路由的主机、报裸文本 `404 page not found`，而同一 shell 里 `bohr api` 一切正常。现在 login 与其它命令走同一 host 解析权威（配置文件 / profile / `BOHR_OPENAPI_HOST` / 主机白名单，legacy 主机静默替换回生产默认），**含 `--ak` 路径的 key 校验**——此前它被劫持到错误主机时，非 JSON 响应会让校验静默变成免检、未经校验的 key 照样落盘；AK 获取的错误信息同时带上实际请求的完整 URL，不再只印路径把肇事主机藏起来。

## [2.5.16] - 2026-08-03

### Breaking

- **移除顶层 `bohr mrdice` 命令（2.5.14 首发即迁移）**：改前：晶体结构检索挂在产品代号下（`bohr mrdice struct-search`），`--help` 的读者无从得知 MrDice 是什么；改后：命令迁至 `bohr database structure search`，flag 与行为完全不变（`--query`/`--n-results`/`--output-format`/`--download`/`--out`），网关路由亦不变；调用方把命令路径整体替换即可。该命令仅在 2.5.14 存在过一版，尽早移除以免引用扩散。

### Changed

- **`bohr database` 重组为两组命令面**：领域库（无需 ak、返回领域对象）＝`polymer list`（聚合物文献记录）与 `structure search`（晶体结构检索）；通用表格（需 db_ak、返回行）＝`tables`/`schema`/`query`。`--help` 分区展示，平台有哪些科学数据库一眼可见。原 `bohr database list` 名不副实（名为列数据库、实为列聚合物记录），正名为 `bohr database polymer list`；旧路径保留一个版本的隐藏兼容别名，执行时提示迁移。

### Fixed

- **database 的 tables/schema/query 撞上「invalid database path」时给出可行动的错误**：数据网关目前只开通了聚合物记录列表一条路由，这三个子命令传任意 db_ak（含字面量 `polymer`）都会得到 500 INTERNAL『Request transformation failed: invalid database path』和一个 contact_support 提示——按提示开工单解决不了一个尚未建成的功能。现在识别该响应形态并改写为说明现状的错误（`subtype=db_route_unavailable`、`retryable=false`、hint 指向 `bohr database polymer list`），上游路由开通后改写自动失效、真实响应原样透传；三个子命令的帮助文本同步注明现状。db_ak 发现端点与路由开通本身仍需上游提供。
- **修复设备码登录后拿不到 AccessKey 的问题**：网关的 `ak/list`（vouch Bearer 路径）返回裸数字响应体时（2026-08-01 用户现场实测；该形态是否为无 AK 账号特有未定论），登录流程此前在列举一步即中止、根本走不到创建，报 `could not obtain access key (failed to parse gateway response ...)`，账号停留在仅 vouch 态（billing 受限、doctor 不过）。现在非对象响应体按「未列出可用键」处理并继续走创建（注意：若账号实际有键，此路径会多建一把——平台允许多键并存且暂无删除接口，取「能完成登录」优先）；创建响应异常时回查一次列表兜底；仍失败时警告附带可行动的手动步骤。鉴权拒绝与显式错误码仍按硬失败处理。

## [2.5.15] - 2026-08-03

### Changed

- **LKM、Wiki 与 Tools 的内置帮助和使用文档与生产接口保持一致**：更新真实字段、参数语义、输出示例与安全的 `--dry-run` 示例，覆盖中英文快速使用指南。

### Fixed

- **修复 LKM 命令按旧响应结构解析而输出空白或错误信息的问题**：`reasoning`、`claim reasoning`、`graph`、`variables batch` 现在按生产 schema 展示推理链、论文、节点、关系与变量；`search --keywords` 使用上游实际接受的逗号分隔关键词，并能从来源元数据中可靠提取论文 ID。
- **修复 Wiki 命令请求字段、接口和渲染结构不匹配的问题**：`article` 使用 `entry_id` 并读取文档内容，`levels`/`disciplines` 读取 `majors`，`graph` 展示 domains、nodes 和 relationships，`course` 改用 field ID 调用真实课程索引接口，keyword 列表保留语言变体。
- **修复 Tools 详情、搜索和子领域列表丢失字段或漏页的问题**：`info` 展示 overview、key points、tutorial、usage 与仓库地址，`search` 展示真实返回字段，`list`/`subdomains` 统一请求并自动聚合全部分页；`tools install --dry-run` 只预览下载和安装目标，不写入本地。

## [2.5.14] - 2026-08-03

### Added

- **新增 `bohr mrdice struct-search` 晶体结构检索命令**：支持按化学式、元素、空间群或 MOF/COF 框架名称检索结构，可设置返回数量与 CIF/POSCAR 输出格式，并可将结果归档下载到本地。

## [2.5.12] - 2026-08-01

### Breaking

- **网关拦截改为结构化错误**：WAF 或边缘规则拒绝请求时统一返回 `GATEWAY_BLOCKED`、退出码 1，并通过 `error.subtype`、`error.upstream_trace_id` 和 `hint.action` 给出拦截来源、追踪 ID 与处理建议；HTML 页面不再原样塞进 `error.message`。调用方若按 `PERMISSION_DENIED`、退出码 2 或 HTML 文本判断此类错误，应改读 `error.code` 和结构化字段。
- **`bohr file` 的前置用户查询失败改为遵循标准错误契约**：`list`、`stat`、`download`、`copy`、`move`、`delete`、`mkdir` 会输出完整错误信封，并按 `error.code` 返回退出码，不再固定为 1。依赖 stderr 文本或固定退出码的脚本应改读 `error.code`。
- **过期的分片上传会话不再被当作成功**：`bohr dataset create` 会检查目标对象是否实际存在；`bohr job submit --input` 无法确认上传完成时会失败并提示重试，同时清理失效的本地续传状态，避免提交缺少输入文件的作业。

### Added

- **新增 `bohr wiki search`**：SciencePedia 科学百科检索，支持 `--lang` 与 JSON 输出。
- **新增 `bohr lkm variables batch` 与 `bohr lkm claim reasoning`**：按变量 ID 批量取值、按声明 ID 取推理链（支持 `--format graph`）。
- **新增 `bohr exp nmr`**：NMR 谱预测（`--task predict`）、按位移检索（`--task search`）与反向推定（`--task reverse-predict`）。
- **新增 `bohr exp ems`**：上传单张 SEM/TEM 图像做颗粒分析。

### Fixed

- **修复 `bohr dataset create` 在部分网络中连接过期默认存储域名而无法上传的问题**：现在优先使用 API 返回的存储地址。
- **修复 `bohr dataset create` 参数错误却返回成功退出码的问题**：缺少名称、路径、项目 ID、本地文件，或路径格式非法时，均返回非零退出码并给出具体提示。
- **提升 `bohr dataset create` 与 `bohr job submit --input` 的分片上传可靠性**：不再遗漏大小恰为 25 MiB 整数倍文件的最后一片；网络错误、5xx、408 和 429 会自动重试最多 3 次；最终失败时保留真实进度并展示上游原因。
- **`bohr file` 在用户查询成功但缺少 `user_id` 时立即报 `UPSTREAM_INVALID_RESPONSE`**，并提示使用 `bohr auth status --verify` 排查，避免带着无效 ID 继续请求。

## [2.5.11] - 2026-07-31

### Fixed

- **V4 计费回显只展示可确认的信息**：Finance 光子消费响应不返回实际扣除数量，CLI
  因此只保留 `channel=photon`，human 输出不再显示无法确认的光子金额；余额渠道继续
  显示准确的 CNY 扣费金额，Wiki、Tool 与 LKM 只显示对应 plan 的剩余免费次数。

## [2.5.10] - 2026-07-31

### Added

- **新增 `bohr billing photon`**：可查询当前光子余额及光子消费记录，并支持 human、JSON 与 YAML 输出。
- **V4 付费命令新增本次请求的计费回显**：human 输出会显示实际扣除的光子或余额；Wiki、Tool 与 LKM 使用免费额度时，会显示免费额度来源及剩余次数。JSON/YAML 输出保留完整的 `notice.billing` 结构，便于程序读取。

### Changed

- **付费命令不再额外展示账户余额**：只展示本次请求的扣费结果；`bohr auth whoami` 等仅返回账户余额的命令仍保持原有展示。

## [2.5.9] - 2026-07-30

> 下面的 batchjob 各条最初被误记在 `[2.5.8]` 名下。2.5.8 是一次未经 tag 触发的手动发布，其发布物构建自 2.5.7 的 commit（`vcs.revision=18b9e4e`、`vcs.modified=true`），**不含任何 batchjob 改动**——它们首次发布是在 2.5.9。按原文安装 2.5.8 的调用方并不具备下列行为。

### Breaking

- **`bohr batchjob kill` 不再把成功的停止报成失败**。改前：上游的 kill 是异步的（只往 RocketMQ 投一条消息就返回），响应体是 `{"code":0,"isShowMsg":false}`、不含作业对象；而 CLI 要求响应里有 `jobId` 相符的作业对象，拿不到就翻成 `ok:false` / `UPSTREAM_INVALID_RESPONSE` / 退出码 1，文案还称"可能还在跑"——于是每一次真实生效的停止都被报成失败（实测：CLI 报失败，10 秒后作业已是 `deleted`）。这个要求原理上不可满足：没有任何同步响应能确认一次异步停止。改后：`code=0` 读作**停止已受理**，随后轮询 `describe` 到终态并返回真实状态，`ok:true`、退出码 0。调用方若原先靠"退出码非零"判断停止失败，需改为读 `data.status_name` / `data.terminal`；若原先靠 `--no-wait` 之外的方式期望立即返回，注意本命令现在默认会确认（见 `--confirm-timeout`）。
- **`bohr batchjob` 各命令的 `exitCode` 只在作业成功时给出数值**。改前：`pending` / `running` 下上游恒回 0（作业还没退出），`failed` / `deleted` 下同样恒回 0（作业没有正常退出，例如被 kill 或排队超时未启动）——两种情况透出 `0` 都会被读成"正常成功退出"。改后：只有 `succeeded` 保留 `exitCode`，其余为 `null`；失败原因请读 `errorCode` / `errorMessage`。调用方若原先用 `exitCode == 0` 判断成功，必须改判 `status_name == "succeeded"` 或 `terminal`。响应中本就没有 `exitCode` 字段时不会新增。
- **`bohr batchjob kill` 被上游拒绝时不再标记为可重试**。改前：`retryable: true`；改后：`false`。远端状态未知时重试一个破坏性操作意味着可能重复执行一次不可逆动作，而 `retryable: true` 恰恰是给自动化的"再试一次"指示。

### Added

- **`bohr batchjob` 的作业对象新增 `terminal` 布尔字段**，与 `status_name` 一同由 CLI 注入，单个作业与 `list` 的每一项同规则。消费方判终态不必再自行维护数值集合（终态为 `succeeded` / `failed` / `deleted`；未知状态一律按非终态处理，以免上游新增状态后提前停止轮询）。
- **`bohr batchjob kill --confirm-timeout`**（默认 30s）：停止受理后等待作业进入终态的时长，取值必须大于零。超时未进终态时命令仍为 `ok:true`（停止确实已受理），并带 `notice.warnings[].code = KILL_NOT_CONFIRMED`；`--no-wait` 则跳过确认，直接返回受理结果并带同一个警告。两种未确认的情况下 `data` 也带 `status_name`（最后观测到的状态）、`terminal: false` 与 `confirmed: false`——正是需要继续轮询时才用得上这两个字段，不能只在确认成功时才有。确认期间 Ctrl-C 立即生效。

### Changed

- **`bohr batchjob wait` 的失败文案不再打印 worker exit code**。`failed` 时上游恒回 0，印出来会被读成"退出码 0 却失败了"。失败原因改由 `errorMessage` 与 `backend detail`（`errorCode`）承载。

### Changed（内部，无用户面行为变化）

- 新增 `internal/batchjobstatus`，作为 batchjob 状态语义的唯一权威（数值→`status_name`/`terminal` 映射、`exitCode` 门控）。与 job 侧的 `internal/jobstatus` 同构但**不共用**：两套系统的 status 值域互不相通，混用会把一方的数值按另一方的语义解读。包注释记录了 2026-07-30 的生产实测结论，含"`endTime` 无需门控"的依据（batchjob 在非终态本就回 `null`，与 job 侧会漂移的 `endTime` 不同）。

## [2.5.8] - 2026-07-30

> **手动发布，未经 tag 触发**：发布物的 `vcs.revision` 是 2.5.7 的 commit `18b9e4e`、`vcs.modified=true`（脏构建，构建自带未提交改动的工作区），`vcs.time` 也仍停在 2.5.7 的 `2026-07-29T15:39:12Z`，因此无法由二进制自身判断它构建自什么内容；本文件当时也未随包发布。这条路径绕开了 2.5.7 轮新加的 CHANGELOG 门禁与干净构建基线。其内容仅为下面两条 file 改动。

### Changed

- **`--dry-run` 打印的 file 请求行形态随之改变**：由 `GET /openapi/v4/file/stat/<path>?projectId=N` 变为 `GET /openapi/v4/file/stat?path=<path>&projectId=N`（`delete` / `deleter` / `download` 同）。仅影响按字面解析 dry-run 输出的调用方，命令用法不变。

### Fixed

- **`bohr file stat` / `download` / `delete` 不再因路径内容被入口网关 403 拦下**。改前：路径被拼进 URL 路径段（`GET /openapi/v4/file/stat/<path>`），自建 openresty 对 `.tar` / `.bak` / `.php` / `.asp` 等后缀有 `location ~* \.(…)$ { return 403; }` 型加固规则，命中即 403；含中文或空格的路径还会因转义不全而损坏（`download` 手写的 `url.PathEscape` 不转义点号，URL 仍以 `.tar` 结尾，照样被拦）。改后：路径改走 `?path=` 查询参数，由 HTTP 客户端统一编码，后缀不再出现在 URL 路径中。四个端点的服务端 query 形式由 open-platform `b5366f0` 提供，旧路径形式仍保留兼容，因此本次改动不需要服务端配合发布。生产实测：`GET /file/stat/personal/probe.tar` 仍 403，`GET /file/stat?path=/personal/probe.tar` 与含中文空格的 `?path=/personal/测试 目录/a.txt` 均正常。
  - 顺带修掉一处拼接缺陷：改前 `"/openapi/v4/file/stat/" + filePath` 在 `filePath` 自带前导斜杠时会出线双斜杠（`/file/stat//personal/...`），`delete` 同形态。

## [2.5.7] - 2026-07-29

> `job submit` 一条为事后补录：该改动（`c73e0c0`）合入时本文件尚未建立，随 2.5.7 发布但未被记录——正是本文件要杜绝的情形。

### Breaking

- **`file stat` 对不存在的路径改为报错**。改前：网关对缺失路径回 HTTP 200，CLI 原样输出 `ok:true`、退出码 0，真实存在性只藏在 `data.exist` 里；改后：`exist=false` 时返回 `ok:false`、`error.code=RESOURCE_NOT_FOUND`、退出码 3。调用方若原先靠读 `data.exist` 判断，可继续工作（错误路径也带 hint）；若原先靠退出码 0 认定"命令成功即路径存在"，该假设本就不成立，现在会正确失败。响应中不含 `exist` 字段时行为不变，仍按成功处理。
- **`bohr machine list --chooseType` 取值非法时改为提前失败**。改前：任意值原样透传给网关，得到一份语义不明的列表；改后：只接受 `cpu` / `gpu` / `all`，其余在发请求前 `rc=1` 拦下并列出合法取值。
- **`bohr skills update` / `bohr skills status` 会在 provider 报告 `success=false` 时失败**。改前：`update` 完全忽略该字段，照常打印"已是最新"；改后：返回结构化错误 `SKILLS_PROVIDER_ERROR` 并以非零码退出。
- **`bohr job submit` 不再默认打包当前工作目录**。改前：`--input_directory` 默认 `./`，未显式指定时静默把 cwd 整个 zip 上传（在 `/root` 下提交会撞到 uv 缓存的悬空符号链接而失败）；改后：只有显式设置 `--input_directory` 或配置文件里的 `input_directory` 才读取本地目录，两者都没有时使用**空输入归档**、完全不读 cwd，并在 JSON / dry-run 输出中带 `notice.warnings[].code = EMPTY_JOB_INPUT`。调用方若原先依赖"在输入目录里执行即可自动上传"，需要显式加 `--input_directory .`。

### Added

- **`bohr machine list -c all`**：一次列出 CPU 与 GPU 两类机型，每条 SKU 带 `chooseType` 字段标明来源。
- **`bohr node get` 补齐可自明的 ID 与机型字段**：JSON 输出新增 `requestedNodeId`（调用方传入的 id）、`machineId`（等于上游 `nodeId`）、`machineType`（上游缺失时由 `spec` 推导）；human 表格新增 `MACHINE_ID`、`MACHINE_TYPE` 两列。上游原有字段一律保留不动。
- **`bohr skills` 系列的结构化错误码**：`SKILLS_CLI_NOT_FOUND`（外部二进制 `bohrium-skills-cli` 不在 PATH）、`SKILLS_CLI_FAILED`（外部二进制非零退出）、`SKILLS_CLI_INVALID_OUTPUT`（输出非 JSON）、`SKILLS_PROVIDER_ERROR`（provider 报告失败）。非 human 输出下以标准信封返回，并附 `hint`。覆盖 `list` / `install` / `update` / `status` / `read`。

### Changed

- **`bohr machine list --page-all` 不再只翻 CPU**。该全局旗标此前在本命令内未被读取，叠加 `--chooseType` 默认 `cpu`，导致"列出全部机型"实际只返回 CPU SKU，曾被误读为"平台没有可提交的 GPU 机型"。现在 `--page-all` 会同时查询 cpu 与 gpu 并合并结果。
- **单一类别的机型列表会声明自己不完整**：只查了一类时，输出带 `notice.warnings[].code = PARTIAL_MACHINE_LIST`，说明哪一类被排除、以及如何拿到完整列表。`-c all` / `--page-all` 下不再出现该警告。
- **`bohr job submit` 先在本地校验并打包输入，再创建远端资源**。改前：先建 Job Group、再建 Job，最后才打包上传，打包失败会留下已创建但永远不会运行的空 Job Group 与 Job；改后：本地校验与 zip 在第一步完成，失败则一个远端对象都不创建。输入目录中的符号链接与非普通文件不受支持，会在这一步报错。

### Changed（内部，无用户面行为变化）

- `internal/envelope` 新增 `Envelope.Warn` / `AddWarnings`，`internal/middleware` 新增 `ResolvedOutput`，`internal/output` 新增 `PrintTableEnvelope`。前两个收拢了各命令里手抄的"懒初始化 Notice 再 append"与 `"" / "auto"` 格式判断；`PrintTableEnvelope` 修掉一个结构性缺口：走表格渲染的命令绕过 `output.Print`，因此 `notice` 从来不会被打印——警告对读表格的人是不可见的，而他们正是警告的目标读者。

### Fixed

- **`node get` 的 ID 契约破裂现在可被机器识别**：`node create` 返回 `id`，而对同一节点 `node get <id>` 回的 `nodeId` 实为 machineId，两者不等。CLI 侧无法统一上游 ID 空间，但会在响应中同时透出两者，并附 `notice.warnings[].code = NODE_ID_MISMATCH`。上游改为返回同一 ID 后，该警告自动消失。
- **`node get` 回读的 `imageName` 被削去 registry/namespace 前缀**时，附 `notice.warnings[].code = NODE_IMAGE_NAME_UNQUALIFIED`，提示按 `repository:tag` 尾段比对，而不要与建机时传入的完整地址逐字比较。
