Files
nodeagent/docs/VERSIONING.md
T
jkooe e23788d717 refactor(versioning): 统一版本号 —— semver 为唯一版本号,开发批次代号并入其归属版本
## 问题(皇上指出)
项目里两套编号并行,无法判断哪个是"当前版本":
- **发布版本**(semver):v1.1.0 ~ v1.6.0、v2.0.0
- **开发批次代号**:v1 ~ v26(README 路线表 / 代码注释 / 文档)
时间线证据:v1.1.0 首发(09-29)时批次已推进到 v12.4 —— **二者从来不是一对一**(26 批次只发过 10 版)。

## 规则(新增 docs/VERSIONING.md §0 版本号唯一性规范)
- **唯一版本号 = semver `vX.Y.Z`**,两个来源必须永远一致:git tag 与 package.json(构建期注入 agent)
- **批次代号不是版本号**,不再单独出现;需要区分同版内多批改动时用**功能名**
- 附「批次 → 归属发布版本」映射表(按 tag 时间线与功能内容推定,并注明以 CHANGELOG 为准)

## 统一动作
1. **批次 v23 ~ v26 → v2.0.0**(22 文件 / 53 处)—— 它们确实属于 v2.0.0
2. **臣的批次号 v1.7 / v1.8 → v2.0.0**(27 文件)—— 未单独发版
3. **简写补全三段式**:v1.5 → v1.5.0、v1.6 → v1.6.0
4. **README 路线表**:`**v1**` → `**批次 1**`;表头加「批次 ≠ 版本号」说明并链到 §0;
   补齐批次 16~29 与 **v2.0.0 发布行**;删掉过时的「待办」行(提到的分支早已删除)
5. `docs/VERSIONING.md` 里**故意保留** 3 处批次号 —— 那是规范说明本身

## 保留项(真实发布版本,不动)
v1.1.0 / v1.2.1 / v1.3.0 / v1.4.0~v1.4.3 / v1.5.0 / v1.6.0 / v2.0.0(10 个 tag)

## 验证
- 版本号一致性:package.json = tag = 8 个 workspace = tauri.conf.json = **2.0.0** ✓
- v1.7/v1.8/v23~v26 全仓残留 **3 处**(均在 VERSIONING.md 规范说明中,属预期)
- 7 包构建 · 单测 273 · e2e 30 · ps1-encoding 24 全绿
2026-10-10 14:27:12 +08:00

154 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 版本与兼容性契约
> 为什么需要这份文档:agent 与 CLI/MCP 是**分开更新的**(CLI 随控制端装,agent 推/拉到被控端),
> 新旧混用是常态而非例外。没有明文契约,升级顺序、什么算破坏性改动、出问题怎么定位全靠猜。
## 0. 版本号唯一性规范(2026-10-10 确立)
> **背景**:项目早期同时存在两套编号 —— 发布版本(`v1.1.0`/`v2.0.0`)与开发批次代号(`v1`~`v26`)。
> 二者**从来不是一对一**(例:`v1.1.0` 首发时批次已推进到 `v12.4`),并列出现时无法判断哪个是"当前版本"。
> 自 v2.0.0 起统一为下列规则。
### 唯一版本号
**`vX.Y.Z`(semver)是项目唯一的版本标识**,两个来源必须永远一致:
| 来源 | 位置 | 谁在读 |
|---|---|---|
| git tag | `v2.0.0` | 人 / GitHub Release |
| `package.json` 的 `version` | 全部 workspace + `tauri.conf.json` | 构建期经 esbuild `--define` 注入 agent |
```bash
nodeagent info # 被控端自报的 version 即来自这里
git tag -l # 发布历史,唯一权威
```
### 开发批次代号(已并入发布版本)
`v1`~`v26` 是**开发阶段的内部代号**(一批相关改动的集合),**不是版本号,也不再单独出现**:
| 批次 | 内容 | **归属发布版本** |
|---|---|---|
| v1 ~ v12.4 | 命令级接管 → 事件订阅 / GUI 宏 / Hub 并发(首发前的全部积累) | **v1.1.0** |
| v12.2 ~ v15 | 等待语义 / macOS GUI 对齐 / 常驻助手 / 图像模板 | v1.2.1 ~ v1.4.x |
| v16 ~ v18 | 网络两阶段提交 / 音频 / 构建指纹 | v1.4.x |
| v19 ~ v22 | 拉取式自更新 / 证书钉住 / 每客户端 PSK / 发现最小化 | v1.5.0 ~ v1.6.0 |
| v23 ~ v26、语义属性化、状态采样 | 连接层三防护 / 零信任安装 / 审计锚定 / UIA 属性 / monitor | **v2.0.0** |
> 上表按 tag 时间线与功能内容推定,**近似**而非精确(批次与发布本无严格边界)。
> 判断"某个改动属于哪个版本"时,**以 CHANGELOG 的版本段落为准**,不以批次号为准。
### 书写规则(新代码 / 新文档)
| 场景 | 写法 | 例 |
|---|---|---|
| 引用发布版本 | `vX.Y.Z` | `v2.0.0 新增 gui.await` |
| 需要区分同一版本内的多批改动 | 用**功能名**,不用批次号 | `审计锚定(v2.0.0)` |
| 历史注释中的批次号 | 保留但**不得**再新增;见上表归属 | `v16:网络两阶段提交` |
## 1. 版本号
- 单一来源:**根 `package.json` 的 `version`**,构建时经 esbuild `--define` 注入 agent。
- 运行时可通过 `system.info` 的 `build` 字段读到:`version` / `commit`(git sha)/ `built_at` /
`hash`(运行中脚本的 sha256 前 12 位 —— 部署校验用)。
- 控制端在握手(`auth_ok`)即拿到被控端版本,`nodeagent info` 会显示:
```
被控端版本 : v1.0.0 @ ef9ad97 构建 740d51953fd0
能力对齐 : 42 项,两端一致 ✓
```
## 2. 语义化版本的含义
| 变更类型 | 版本位 | 例子 |
|---|---|---|
| **破坏性**:删除能力、给既有能力**新增必填参数**、改变既有能力的返回结构、修改认证/握手流程 | MAJOR | 删掉 `screen.find`;把 `fs.read` 的 `path` 改成必填 `remote_path` |
| **新增**:新能力、既有能力的**可选**参数、新增返回字段、新增审计事件类型 | MINOR | 加 `system.audio.*`、给 `deploy` 加 `--check` |
| **修复/内部**:bug 修复、性能、文档、不影响协议的重构 | PATCH | 修备份编码、拆分文件 |
## 3. 兼容性契约(硬规则)
1. **只增不改**:既有能力的**参数名与语义不得变更**;要改就新增能力(如 `screen.find` 与未来的
`screen.find2` 并存一代),旧能力至少在下一个 MAJOR 才移除。
2. **不新增必填参数**:既有能力只能加**可选**参数 —— 否则旧控制端一调就 `E_PARAM_INVALID`。
3. **返回结构只加不减**:新增字段必须可选,旧控制端忽略即可。
4. **`auth_ok` 的字段只增不减**:v20 新增的 `agent_version`/`build`/`protocol` 均为可选,
旧控制端不认也不报错。
5. **协议主版本**(`PROTOCOL_VERSION` 的主号)不一致时被控端直接拒绝连接并返回
`E_PROTOCOL_MISMATCH` —— 这是唯一的硬闸门,用于承载**无法兼容**的协议变更。
6. **能力清单是唯一事实源**:控制端不比版本号,而是比**能力集合**
(`capabilityDiff`)—— 版本号可能相同而内容不同(本地 dev 构建),能力集合不会骗人。
## 3.1 v21 新增字段(均为**可选**,向后兼容)
| 位置 | 字段 | 含义 |
|---|---|---|
| 被控端 `agent.json` | `allow_from: string[]` | 来源网段白名单;**未配置 = 放行全部**(与旧行为一致) |
| 被控端 `system.info` | `build.cert_sha256` / `network.allow_from` | 证书指纹与白名单状态(供控制端展示与巡检) |
| 被控端 `auth_ok` | `build.cert_sha256` | 同上(握手期即可得) |
| 控制端 `nodes.<名>` | `cert_sha256: string` | **钉住**的被控端证书指纹;不一致即 `E_CERT_MISMATCH` 拒连 |
| 发现报文 | `cert_sha256` | 未认证广播,**仅作参考**,不可当身份凭据 |
新增错误码:`E_CERT_MISMATCH (-32422)`、`E_NET_DENY` 类事件走审计 `net.deny`。
## 4. 升级顺序建议
**先升级被控端,再升级控制端**:
| 组合 | 结果 |
|---|---|
| 新被控端 + 旧控制端 | ✅ 安全。被控端只做加法,旧控制端不认识的新能力会被忽略 |
| 旧被控端 + 新控制端 | ⚠️ 可用,但新功能不可用。`nodeagent info` 会列出**远端缺少**的能力并提示升级 |
> 若顺序反了也不会坏:控制端调用缺失能力时返回 `E_CAPABILITY_NOT_FOUND`,
> 而 `nodeagent info` / `connect` 会提前把它变成一句人话提示。
## 5. 更新方式(两条路,按可达性选)
| 方式 | 命令 | 前提 |
|---|---|---|
| 推送式 | `pnpm update:win <设备名>` | 控制端↔被控端**可达**;自动打包含校验与指纹复核 |
| **拉取式** | `nodeagent update --url <地址> --sha256 <哈希>` | **只要被控端能上网**(含 GitHub Release) |
先预检再更新(不动任何文件):
```bash
nodeagent --node win update --url <地址> --sha256 <哈希> --dry-run
```
`--sha256` 是**安全底线**:不校验哈希等于开放远程代码执行。
## 6. 发版流程
```bash
pnpm release patch|minor|major|<x.y.z> # 改版本 → 构建 → 打包 → 校验和/清单 → 提交打标签 → GitHub Release
pnpm release 1.0.0 --dry-run # 演练(无任何副作用)
```
产物(GitHub Release 资产):
| 资产 | 用途 |
|---|---|
| `agent.mjs` | **自更新载荷** —— 被控端 `system.agent.update` 直接用它(配 `--sha256`) |
| `nodeagent-win-x64.zip` | 人工安装包(自包含 `node.exe`,免装 Node.js) |
| `latest.json` | 版本清单:version/commit/built_at/capabilities + 两个资产的 url 与 sha256 |
| `SHA256SUMS` | 校验和 |
> ⚠️ **首次仍需一次性安装**:自更新能力本身得先在机器上(鸡生蛋无解)。
> ⚠️ **`node.exe`(Node 运行时)不在自更新范围**:只换 `agent.mjs`;需升级 Node 时重跑 `install.ps1`。
> ⚠️ 本仓库的 `.github/` 改动需要 `workflow` 权限才能推送(`gh auth refresh -h github.com -s workflow`),
> 发版脚本刻意跳过 `.github/`,避免发版流程被它卡住。
## 7. 出问题怎么排查
```bash
nodeagent --node win info # 版本 + 能力对齐情况(先看这个)
nodeagent --node win invoke system.info --args '{"fields":["build"]}' # 构建指纹
nodeagent --node win deploy <包> --check # 本地包 vs 远端是否一致(不改动)
```
- **远端过旧**:`info` 会列出缺失能力 → `nodeagent update ...`
- **本控制端过旧**:`info` 会列出"不认识的新能力" → 更新控制端(`git pull` → `pack:mac` → `install-macos.sh`)
- **同版本但指纹不同**:属正常(内容相同才叫同版本);用 `--check` 对比实际内容
- **部署后没生效**:先查杀软是否处置了被替换的 `agent.mjs`(见 `REMOTE-LIMITS.md` 的「杀软误拦」)