API 版本管理
EnerOS 采用语义化版本 + URL 路径版本的双重策略,确保 API 演进过程中客户端无需频繁升级,同时允许引入破坏性变更。版本管理涵盖 REST、GraphQL、WebSocket、SSE 四种 API,每种协议的版本协商方式略有不同,但共享同一套兼容性保证与废弃流程。
版本策略
1. URL 路径版本(REST / SSE)
每个主版本通过 URL 路径区分,路径中的版本号与 EnerOS 大版本对齐:
| 版本路径 | 状态 | 发布时间 | 废弃时间 | EnerOS 对应版本 |
|---|---|---|---|---|
/api/v1/... | 稳定 | 2026-01 | — | v0.40+ |
/api/v2/... | 预览 | 2026-09(计划) | — | v0.50+ |
/api/v3/... | 规划中 | 2027(计划) | — | v1.0+ |
http://localhost:8080/api/v1/networks # 当前稳定版
http://localhost:8080/api/v2/networks # 下一主版本(开发中)
规则:
- 主版本号变更 = 破坏性变更(端点删除、字段语义变更、行为变更)
- 次版本与补丁版本不改变 URL 路径
- 同一主版本下,新端点/新字段通过追加方式引入,向后兼容
- 多个主版本可同时在线,过渡期内并行支持
2. 语义化版本(SemVer)
EnerOS 整体版本遵循 MAJOR.MINOR.PATCH 规则:
| 变更类型 | 示例 | 兼容性 | URL 版本号 | 说明 |
|---|---|---|---|---|
| PATCH | bug 修复、性能优化、文档更新 | 完全兼容 | 不变 | 0.47.0 → 0.47.1 |
| MINOR | 新增端点、新增字段、新增枚举值 | 向后兼容 | 不变 | 0.47.0 → 0.48.0 |
| MAJOR | 删除端点、字段语义变更、行为变更 | 破坏性 | +1 | 0.47.x → 1.0.0 |
3. Header 版本协商(GraphQL)
GraphQL 不使用 URL 版本,通过 Header 协商 API 版本与特性集:
POST /graphql HTTP/1.1
Accept: application/graphql; version=2026-07
Content-Type: application/json
| Header | 取值 | 说明 |
|---|---|---|
Accept | application/graphql; version=YYYY-MM | 指定 API 月份版本 |
Accept | application/graphql+json; version=2026-07 | 显式指定 JSON 响应 |
Schema-Version | 2026-07 | Schema 版本(与 Accept 等价) |
GraphQL 版本命名采用 YYYY-MM 格式,每季度发布一个新版本:
| 版本 | 状态 | 发布时间 | 废弃时间 |
|---|---|---|---|
2026-01 | 已废弃 | 2026-01 | 2026-07 |
2026-04 | 已废弃 | 2026-04 | 2026-10 |
2026-07 | 当前稳定 | 2026-07 | 2027-01 |
2026-10 | 计划 | 2026-10 | 2027-04 |
4. WebSocket 子协议版本
WebSocket 通过子协议(Sec-WebSocket-Protocol)协商版本:
Sec-WebSocket-Protocol: eneros.v1
| 子协议 | 状态 | 发布时间 | 废弃时间 |
|---|---|---|---|
eneros.v1 | 稳定 | 2026-01 | — |
eneros.v2 | 规划 | 2027 | — |
客户端可在握手时声明多个子协议,服务端选择最高支持的版本:
const ws = new WebSocket('ws://localhost:8080/ws', ['eneros.v1', 'eneros.v2']);
console.log(ws.protocol); // 服务端选定的版本,如 "eneros.v1"
兼容性保证
兼容性级别
| 级别 | 说明 | 保证期 |
|---|---|---|
| 稳定(Stable) | 接口已固化,仅接受向后兼容变更 | 至下一主版本发布后 12 个月 |
| 预览(Preview) | 接口可能变更,不建议生产使用 | 无保证,随时可能调整 |
| 实验(Experimental) | 实验性功能,可能被移除 | 无保证 |
| 废弃(Deprecated) | 即将下线,应尽快迁移 | 公告后 6-9 个月 |
兼容性规则
以下变更视为向后兼容,不改变版本号:
| 变更 | 兼容性 | 示例 |
|---|---|---|
| 新增端点 | ✅ | 新增 GET /api/v1/topology/{id} |
| 新增可选请求字段 | ✅ | 新增 description 可选字段 |
| 新增响应字段 | ✅ | 响应中新增 updated_at |
| 新增枚举值 | ✅ | status 新增 archived |
| 字段从可选改为必填 | ❌ | 破坏性,需新版本 |
| 字段语义变更 | ❌ | v 从标幺值改为有名值 |
| 删除端点 | ❌ | 删除 GET /api/v1/old_endpoint |
| 删除字段 | ❌ | 删除 legacy_field |
| 改变默认值 | ⚠️ | 视影响范围,可能需新版本 |
字段废弃标注
废弃字段通过响应头与响应体同时标注:
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sun, 06 Jul 2027 00:00:00 GMT
Link: </api/v2/networks>; rel="successor-version"
Warning: 299 - "The 'bus_count' field is deprecated, use 'buses.count' instead"
| 响应头 | 说明 |
|---|---|
Deprecation | true 表示响应包含废弃字段 |
Sunset | 端点/字段的预计下线日期 |
Link: rel="successor-version" | 新版本端点位置 |
Warning | 299 状态码的人类可读警告 |
GraphQL 字段废弃通过 @deprecated 标注:
type Network {
busCount: Int! @deprecated(reason: "Use 'buses.count' instead")
buses: [Bus!]!
}
API 版本生命周期
每个主版本经历完整的生命周期:
规划 → 预览 → 稳定 → 维护 → 废弃 → 下线
│ │ │ │ │ │
│ │ │ │ │ └─ 返回 410 Gone
│ │ │ │ └─ 仅安全修复,文档标注
│ │ │ └─ 不接受新特性,仅 bug 修复
│ │ └─ 主推版本,完整支持
│ └─ 可用但可能变更,文档标注 "Preview"
└─ RFC / ADR 讨论阶段
| 阶段 | 持续时间 | 支持 | 文档标注 |
|---|---|---|---|
| 规划 | 1-3 个月 | 无 | RFC |
| 预览 | 3-6 个月 | best-effort | Preview |
| 稳定 | 12-24 个月 | 完整 | 无 |
| 维护 | 6-12 个月 | 仅安全修复 | Maintenance |
| 废弃 | 6 个月 | 仅安全修复 | Deprecated |
| 下线 | — | 无 | 410 Gone |
当前版本状态
| 版本 | 状态 | 发布 | 进入维护 | 废弃公告 | 下线 |
|---|---|---|---|---|---|
| v1 | 稳定 | 2026-01 | — | — | — |
| v2 | 预览 | 2026-09 | — | — | — |
| GraphQL 2026-01 | 废弃 | 2026-01 | 2026-07 | 2026-07 | 2027-01 |
| GraphQL 2026-04 | 废弃 | 2026-04 | 2026-10 | 2026-10 | 2027-04 |
| GraphQL 2026-07 | 稳定 | 2026-07 | — | — | — |
废弃流程
废弃阶段
每个端点或字段的废弃遵循标准化流程:
| 阶段 | 持续时间 | 动作 | 客户端感知 |
|---|---|---|---|
| 1. 公告期 | 6 个月 | 文档与响应头标注 deprecated | 响应头 Deprecation: true |
| 2. 告警期 | 3 个月 | 调用废弃端点返回警告日志 | 响应头 Warning: 299 |
| 3. 限流期 | 1 个月 | 废弃端点限流收紧至 10% | 频繁收到 429 |
| 4. 下线 | — | 移除端点,返回 410 Gone | 调用失败 |
废弃响应示例
第 1 阶段:公告期
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sun, 06 Jul 2027 00:00:00 GMT
Link: </api/v2/networks/net_8f3a2b>; rel="successor-version"
Warning: 299 - "GET /api/v1/networks/{id}/legacy_powerflow is deprecated, use POST /api/v1/networks/{id}/powerflow instead"
Content-Type: application/json
{"data": {"id": "net_8f3a2b", "bus_count": 14}}
第 4 阶段:下线
{
"error": {
"code": "GONE",
"message": "Endpoint GET /api/v1/networks/{id}/legacy_powerflow has been removed. Use POST /api/v1/networks/{id}/powerflow instead.",
"details": {
"removed_at": "2027-07-06T00:00:00Z",
"successor": "/api/v1/networks/{id}/powerflow",
"doc_url": "https://docs.openeneros.com/api-reference/versioning#迁移指南"
}
},
"request_id": "req_20260706_001"
}
GraphQL 废弃
GraphQL 字段废弃通过 @deprecated 指令,客户端在 Introspection 时可获取:
query {
__type(name: "Network") {
fields {
name
isDeprecated
deprecationReason
}
}
}
响应:
{
"data": {
"__type": {
"fields": [
{"name": "busCount", "isDeprecated": true, "deprecationReason": "Use 'buses.count' instead"},
{"name": "buses", "isDeprecated": false, "deprecationReason": null}
]
}
}
}
迁移指南
v1 → v2 迁移示例
字段重命名
v1 响应:
curl http://localhost:8080/api/v1/networks/net_8f3a2b
{
"id": "net_8f3a2b",
"name": "IEEE-14",
"bus_count": 14,
"branch_count": 20,
"created_at": "2026-07-06T08:30:00Z"
}
v2 响应(字段语义化,使用嵌套结构):
curl http://localhost:8080/api/v2/networks/net_8f3a2b
{
"id": "net_8f3a2b",
"name": "IEEE-14",
"topology": {
"buses": {"count": 14},
"branches": {"count": 20}
},
"metadata": {
"created_at": "2026-07-06T08:30:00Z"
}
}
迁移映射表:
| v1 字段 | v2 字段 | 说明 |
|---|---|---|
bus_count | topology.buses.count | 嵌套化 |
branch_count | topology.branches.count | 嵌套化 |
created_at | metadata.created_at | 元信息分组 |
v | voltage.magnitude | 语义化 |
theta | voltage.angle | 语义化 |
端点路径变更
| v1 端点 | v2 端点 | 说明 |
|---|---|---|
POST /networks/{id}/powerflow | POST /networks/{id}:runPowerFlow | 改用 Google API 命名规范 |
GET /agents/{id}/status | GET /agents/{id}:getStatus | 动作类端点使用冒号 |
POST /timeseries/batch | POST /timeseries:batchWrite | 动作类端点使用冒号 |
行为变更
| 行为 | v1 | v2 | 说明 |
|---|---|---|---|
| 潮流默认方法 | newton_raphson | newton_raphson | 不变 |
| 潮流默认超时 | 30000ms | 60000ms | 增大超时 |
| 时序聚合时区 | UTC | UTC(支持 tz 参数) | 增强 |
| 分页默认大小 | 20 | 50 | 增大默认值 |
| 错误码命名 | INVALID_REQUEST | INVALID_REQUEST | 不变 |
迁移步骤
- 审计当前 API 使用:通过审计日志统计在用端点
- 阅读迁移指南:对照字段映射表评估改动量
- 使用版本感知 SDK:SDK 自动处理版本差异
- 逐步切换:先在测试环境切换至 v2,验证后逐步推进生产
- 监控废弃告警:观察
Warning: 299响应头,识别遗漏的废弃端点 - 删除 v1 调用:v1 下线前完成全部迁移
SDK 版本协商
eneros-sdk 自动处理版本协商与字段映射:
use eneros_sdk::ApiClient;
let client = ApiClient::builder()
.endpoint("http://localhost:8080")
.api_version("v2") // 指定版本
.token("<bearer-token>")
.auto_migrate(true) // 启用自动迁移(v1 → v2 字段映射)
.build()?;
let network = client.networks().get("net_8f3a2b").await?;
// v2 SDK 直接返回嵌套结构
println!("{:?}", network.topology.buses.count);
from eneros_sdk import ApiClient
client = ApiClient(
endpoint="http://localhost:8080",
api_version="v2",
token="<bearer-token>",
auto_migrate=True,
)
network = client.networks.get("net_8f3a2b")
print(network.topology.buses.count)
import { ApiClient } from '@eneros/sdk';
const client = new ApiClient({
endpoint: 'http://localhost:8080',
apiVersion: 'v2',
token: '<bearer-token>',
autoMigrate: true,
});
const network = await client.networks.get('net_8f3a2b');
console.log(network.topology.buses.count);
变更日志
每次 MINOR / MAJOR 发布同步更新 CHANGELOG.md 与 API 参考文档。重大变更发布 ADR(架构决策记录)说明动机与迁移路径。
变更日志格式
# CHANGELOG
## [0.48.0] - 2026-07-06
### Added
- 新增 `GET /api/v1/topology/{id}` 端点,查询电网拓扑
- 新增 `POST /api/v1/networks/{id}:validate` 端点
- `NetworkInput` 新增 `tags` 字段
- `AgentStatus` 枚举新增 `paused` 值
### Changed
- `POST /networks/{id}/powerflow` 默认超时从 30s 调整为 60s
- 分页默认 `page_size` 从 20 调整为 50
### Deprecated
- `Network.bus_count` 字段废弃,使用 `Network.buses.count` 替代
- `GET /agents/{id}/status` 端点废弃,使用 `GET /agents/{id}` 替代
### Fixed
- 修复潮流计算在孤岛场景下 panic 的问题
- 修复 WebSocket 重连后事件丢失的问题
### Security
- 升级 `rustls` 至 0.23.x,修复 CVE-2026-1234
ADR(架构决策记录)
重大变更发布 ADR,记录决策背景、方案对比与最终选择:
docs/adr/
├── 0001-url-path-versioning.md
├── 0002-graphql-header-versioning.md
├── 0003-websocket-subprotocol-versioning.md
├── 0004-v2-field-nesting.md
└── 0005-deprecation-process.md
ADR 模板:
# ADR-0004: v2 字段嵌套化
- 状态:已接受
- 日期:2026-06-15
- 决策者:API 工作组
## 背景
v1 响应字段扁平化,导致字段数量增长后难以理解字段归属...
## 方案对比
- 方案 A:保持扁平 + 前缀(`network_bus_count`)
- 方案 B:嵌套分组(`topology.buses.count`)✅
- 方案 C:GraphQL 化(全部走 GraphQL)
## 决策
采用方案 B,理由...
## 影响
- 客户端需更新字段访问路径
- SDK 提供 auto_migrate 自动映射
兼容性测试
EnerOS 维护兼容性测试套件,确保版本演进不破坏既有契约:
| 测试类型 | 说明 | 工具 |
|---|---|---|
| 契约测试 | 验证响应结构符合 Schema | schemathesis |
| 快照测试 | 对比响应 JSON 快照 | insta |
| 集成测试 | 端到端调用验证 | testcontainers |
| 回归测试 | 旧版本端点不被破坏 | cargo test |
兼容性测试在 CI 中自动运行,任何破坏性变更都会被检测并阻止合并。
多版本共存
EnerOS 支持多主版本并行运行,过渡期内 v1 与 v2 同时可用:
http://localhost:8080/api/v1/networks # v1 端点
http://localhost:8080/api/v2/networks # v2 端点
数据层共享,仅接口层不同。同一资源在 v1 与 v2 中的 ID 一致,可跨版本引用。
多版本配置
[api]
enabled_versions = ["v1", "v2"]
default_version = "v1"
v1_sunset_at = "2027-07-06" # v1 计划下线日期
v2_preview = true # v2 是否为预览版
版本协商最佳实践
客户端固定版本
生产环境建议固定 API 版本,避免自动升级带来的意外:
let client = ApiClient::builder()
.endpoint("http://localhost:8080")
.api_version("v1") // 固定 v1
.pin_minor_version("0.47") // 固定 minor 版本
.build()?;
监控废弃告警
客户端应监控 Warning 与 Deprecation 响应头,提前发现需迁移的端点:
import logging
response = client.get("/networks/net_8f3a2b")
if response.headers.get("Deprecation") == "true":
sunset = response.headers.get("Sunset", "unknown")
warning = response.headers.get("Warning", "")
logging.warning(f"调用了废弃端点,下线时间: {sunset}, 详情: {warning}")
渐进式迁移
1. 全量使用 v1
2. 新功能使用 v2(混合阶段)
3. 逐步将 v1 调用迁移至 v2
4. v1 下线后,全部使用 v2