规则:每完成一个阶段,先审核 + 跑测试验证无问题,再进入下一阶段。 依据:dev_docs/design-modules/14-dev-plan.md(WBS/验收)
交付物
server/Cargo workspace:12 个 crate(dsh-core 有真实实现,其余为占位)server/crates/dsh-core:数据模型(Value 自定义 serde / Structure / BranchState / VersionRecord / PublishEvent / Ciphertext)、ErrorKind 错误体系、KV 键构造、Validator(类型/必填/结构约束/限额)、compute_diff + apply_diff、limits 常量- 契约三件套:
proto/config.v1.proto、api/openapi.v1.yaml、schema/storage.v1.schema.json(Value 增加 ciphertext 字段) scripts/check-contracts.sh:proto/openapi/schema 三方 lint.github/workflows/ci.yml:lint / unit / contract 三 job(后续阶段逐步补 stage 4~9)rust-toolchain.toml、.gitignore
验证结果(全部通过)
| 检查 | 结果 |
|---|---|
| cargo fmt --check | ✅ 干净 |
| cargo clippy --all-targets --all-features -- -D warnings | ✅ 无警告 |
| cargo test --workspace | ✅ 24 个测试全绿(core 13 单测 + 5 集成 + 11 占位) |
| scripts/check-contracts.sh | ✅ proto OK / openapi 25 paths / schema 16 $defs |
M0 验收标准对照:□ 契约三方 lint ✓ □ 决策记录(D1–D16,见 design-v2 §16)✓ □ 工作区脚手架 ✓ □ CI 骨架 ✓
交付物
- dsh-core 状态机(Command apply / 幂等 I10 / 必填校验 / GetConfig / 版本快照 / dump·restore)
- dsh-storage:RocksDB(4 列族 + Store + checkpoint)
- dsh-raft:openraft 0.9(LogStore/StateMachineStore/JSON 快照)+ 进程内直连网络(测试)+ HTTP 网络(多进程生产,reqwest+axum)
- dsh-cli:--dev-single + 集群模式(--node-id/--bootstrap/--join),Raft RPC 服务、集群管理端点(members/join/promote)
- watch SSE(dev-single 发布事件广播)
- 脚本:dev-single-demo.sh(11 步)、cluster-demo.sh(7 步)
M1 验收结果(全部通过)
| 验收项 | 结果 |
|---|---|
| 建项目/分支→草稿→发布→GetConfig 新版本 | ✅ dev-single + 集群 |
| 3 节点 join(learner→voter)→ 写→全节点复制 | ✅ 进程内测试 + 多进程 e2e |
| kill 任一节点后多数派继续读写 | ✅ 节点2 被杀后仍可写,存活节点可见 |
| watch 收到发布事件 | ✅ dev-single SSE |
| cargo fmt / clippy -D warnings | ✅ |
| cargo test --workspace | ✅ 40 passed / 0 failed |
工程记录
- HTTP 网络:openraft RPC 经 axum /raft/* + reqwest JSON;错误以 500 返回(客户端按网络错误重试);快照分块由 openraft 默认 full_snapshot 处理
- 集群写:经 raft.client_write(幂等键在命令内);读:本地状态机(M2.5 加线性一致读)
- 演示脚本坑:--http-addr 不能带 http:// 前缀(bind 会 DNS 失败)
交付物
- dsh-core 发布引擎完整语义:Rollback(历史不可变 I6/I9)、共享库(SharedDraft/SharedPublish 原子级联 D7/D15)、RefBind/RefUnbind(item 级引用 + 索引反查)、发布时引用物化、prune_versions(版本裁剪)
- dsh-crypto:AEAD 信封加密(AES-256-GCM,KEK+每项 DEK)、主密钥加载(env/文件)、脱敏、wire 格式对齐 core::Ciphertext
- dsh-render:YAML/TOML/JSON 渲染(Value→普通 JSON 树;secret 掩码/解密)、TOML 约束错误处理、格式等价性测试
- dsh-jobs:JobScheduler(仅 leader)+ VersionRetention 版本裁剪
- dsh-cli:secret 提交前加密(I8,保持 Raft apply 确定性)、读取解密、渲染端点 /v1/.../config?format=、rollback 端点、版本裁剪任务接线(--version-retention)
M2 验收结果(全部通过)
| 验收项 | 结果 |
|---|---|
| 回滚=新版本(rollback_of),历史不可变 | ✅ core 测试 + API e2e(v3 回滚到 v1 内容) |
| 共享库发布自动级联(原子) | ✅ core 测试(引用分支版本推进+值更新) |
| 引用物化:草稿无值时取共享值 | ✅ core 测试 |
| secret 加密存储→解密输出(I8) | ✅ crypto 测试 + API e2e |
| YAML/TOML/JSON 渲染 + 等价性 | ✅ render 测试 + API e2e |
| 版本裁剪保留活动版本+最近 N | ✅ jobs 测试 |
| cargo fmt / clippy -D warnings | ✅ |
| cargo test --workspace | ✅ 49 passed / 0 failed |
| 回归:dev-single / cluster 演示 | ✅ 全过 |
工程记录
- 加密在 API 层(提交命令前)执行,状态机只存密文 —— 保证 Raft apply 确定性(随机 nonce 不跨节点发散)
- 确定性时间:apply 用日志序号作 now_ms(D16)
- 组级引用(item_key=None)M2 暂不支持(validation 拒绝),后续迭代
交付物
- 集群 watch:StateMachineStore 在 Raft apply 时广播 PublishEvent(各节点本地)→ cli 转发到统一 broadcast → SSE;dev-single 与集群共用同一 watch 通道
- 可观测性:/readyz(集群就绪检查)、/metrics(Prometheus 文本:dsh_projects/dsh_versions)、审计(内存环形缓冲 + GET /api/v1/audit)
- 单管理员会话(I7):login/logout/heartbeat + 鉴权中间件(/api/v1/* 除 login/healthz/readyz/cluster-join 外需 Bearer);第二登录 409 ERR_SESSION_IN_USE
- 内嵌 Admin UI(模块 09):rust-embed 内嵌 /admin 静态页(项目列表/配置查看/watch 事件流),单二进制交付
M2.5 验收结果(全部通过)
| 验收项 | 结果 |
|---|---|
| 集群 watch:follower 收到 leader 发布事件 | ✅ SSE 事件(structure_publish + value_publish) |
| 单会话:第二登录 409 / 无 token 401 | ✅ |
| /metrics /readyz /audit | ✅ |
| Admin UI /admin | ✅ 内嵌 HTML |
| 回归:dev-single + cluster 演示(含鉴权) | ✅ 全过 |
| cargo fmt / clippy / 49 tests | ✅ |
工程记录
- 集群 watch 语义:每个节点本地 apply 时广播(openraft 复制日志到所有节点,事件天然一致),SSE 订阅任一节点即可
- /api/v1/cluster/join 豁免鉴权(节点加入前的引导调用);login 前无会话
- 演示脚本鉴权改造:dev-single 用 AUTH 头;cluster 用按主机 token 文件(api() 助手)
交付物(sdk/ 目录)
- TS SDK(sdk/ts):ConfigClient(端点池 failover + 退避)、get/getItem(/snapshot 数据面端点)、watch(SSE + 断线重连退避)、ConfigError;Node --experimental-strip-types 直接运行
- Go SDK(sdk/go):configclient 包(Get/GetItem/Watch),bufio SSE 解析,断线重连
- Python SDK(sdk/python):ConfigClient(urllib 标准库),get/get_item/watch(threading 停止)
- 数据面快照端点 /v1/projects/{p}/branches/{b}/snapshot(无鉴权,纯值输出,含版本号)
- scripts/sdk-contract-test.sh:三语言对同一 dev-single 的契约测试(get + watch)
M3 验收结果(全部通过)
| 验收项 | 结果 |
|---|---|
| TS:get + watch | ✅(get v3 → watch v4) |
| Go:get + watch | ✅(get v12 → watch v13) |
| Python:get + watch | ✅(get v15 → watch v16) |
| 契约一致(同一服务/同端点) | ✅ 三语言对拍 |
| 回归:fmt / clippy / 49 tests / dev-single / cluster | ✅ |
工程记录
- 环境坑:~/.cache 只读 → GOCACHE=/tmp/dsh-gocache;GO 无外部依赖(stdlib)
- 契约测试时序:循环发布消除"测试启动编译耗时"竞态;publish 用唯一 request_id(幂等去重)
- Node 22 --experimental-strip-types 不支持 parameter property(TS SDK 避开该语法)
交付物
- scripts/chaos-test.sh:leader SIGKILL → 重新选举 → 继续写入 → 重启(同 data-dir 自动恢复)→ 追赶一致(含宕机期间写入);follower 击杀重启追赶一致
- 重启自动恢复(auto-rejoin):raft-meta 非空时无需 --bootstrap/--join,openraft 从持久化状态 resume(学习更高 term 后让位并追赶)
- 安全加固:安全响应头中间件(X-Content-Type-Options / X-Frame-Options / CSP)、JoinReq 宽松反序列化、tracing 日志接入
- 发布资产:deploy/Dockerfile(多阶段)、deploy/docker-compose.yml(3 节点)、根 README.md(快速开始/能力/构建测试/文档索引)
- 基准冒烟:读路径 GET /snapshot 6279 QPS(200 并发 × 4 万请求全成功)
M4 验收结果(全部通过)
| 验收项 | 结果 |
|---|---|
| 混沌:leader 击杀 → 选举 → 写入 | ✅ |
| 混沌:节点重启 → 追赶一致(含宕机期间写入) | ✅ |
| 安全加固(CSP/安全头) | ✅ |
| 发布清单(Docker/compose/README) | ✅ |
| 基准冒烟(读 6279 QPS) | ✅ |
| 最终回归:fmt/clippy/49 tests/契约 lint/4 个 e2e 脚本 | ✅ 全绿 |
| 里程碑 | 交付 | 验证 |
|---|---|---|
| M0 契约+脚手架 | proto/openapi/schema 契约、Cargo workspace 12 crates、dsh-core 数据模型 | lint + 29 tests |
| M1 单节点+集群 | 状态机/发布/RocksDB/openraft 集成/HTTP 网络/--dev-single/--bootstrap/--join | 49 tests + 2 e2e |
| M2 发布引擎+加密+渲染+任务 | 回滚/共享库级联/引用物化/AEAD 加密/三格式渲染/版本裁剪 | core/crypto/render/jobs tests + e2e |
| M2.5 集群 watch+可观测+会话+UI | raft apply 事件广播 SSE/metrics/readyz/审计/单管理员会话/内嵌 /admin | M2.5 e2e |
| M3 三语言 SDK | TS/Go/Python(get/getItem/watch/failover)+ 契约测试 | 三语言对拍全过 |
| M4 混沌+加固+发布 | leader 击杀/重启追赶混沌、安全头、Docker/compose/README、基准 | 全部脚本 + 49 tests |
| M5 会话/审计/快照/集群watch | 会话落 Raft(I7 集群级单会话+leader 转发)、审计落库、快照持久化、集群 watch 自动化、Raft 错误传播 | 59 tests + 跨节点 409 e2e |
| M6 模块化+gRPC | 4 crate 归位(cli 仅组装)、tonic ConfigService(GetConfig/GetItem/Watch/ListMembers :8383)+ 鉴权 | 65 tests + gRPC 集成测试 |
| M7 组级引用+密钥轮换 | 整组绑定共享组(物化/级联/解绑)、KeyRing 轮换(rotate API/CLI/重包任务/CRY-002) | 74 tests + 轮换 HTTP e2e |
| M8 CI/基准/SBOM | 8 jobs 全绿流水线、bench.sh 基准、SPDX SBOM、release 产物 | CI 全绿 + 实测达标 |
最终工程指标(M8 终态)
- cargo test --workspace:74 passed / 0 failed;clippy -D warnings 零警告;fmt 干净
- GitHub Actions 8 jobs 全绿:lint / unit / contract / raft / sdk / e2e / bench / release
- 基准实测:读 35016 QPS(本机)/ 9419(CI);写 1620 QPS(本机,单写者串行);watch 12ms;内存 41MB ≤128MB;release 二进制 8.68MB ≤50MB
- SBOM:Defing-release.spdx.json(SPDX,44KB)
- 端到端脚本全部通过:dev-single-demo、cluster-demo、chaos-test、sdk-contract-test、check-contracts
- 仓库:https://github.com/ipconfiger/Defing(main,含完整设计文档 dev_docs/design-* 与 progress/remaining-work 进度)
交付物(依据 dev_docs/remaining-work.md R1–R4)
- B2 会话落 Raft(I7 集群级单会话):
AdminSession{token_hash,issued_at,expires_at}入状态机(sess/admin键);Command::SessionLogin/Logout/Heartbeat;token 仅存 SHA-256 哈希(明文不落库/日志);auth_middleware校验状态机会话;login 非 leader 自动跟随leader_hint转发到 leader 公开端点(跨节点唯一);--session-ttl(默认 24h) - Raft 错误传播修复(使能项):
TypeConfig::R = u64→Result<u64, Error>,状态机 apply 错误随客户端响应返回(之前被吞掉返回 0);client_write拆出单次版try_client_write+WriteError::ForwardToLeader{leader_hint}+leader_http_addr兜底解析 - B4 审计持久化:
Command::AuditAppend→ 状态机写audit/{seq:020}(seq 单调,audit/seq计数键);get_audit(action/since/limit)读库;GET /api/v1/audit实现 openapi 参数;管理写操作全量审计(project_create/branch_create/draft_update/publish/structure_publish/rollback/cluster_join/cluster_promote/login/logout);AuditRetention任务(--audit-retention默认 100k)+ 版本裁剪任务接线(--version-retention,此前未接线) - B5 快照持久化:build/install 快照落盘 snapshots 列族(meta+data),
get_current_snapshot内存为空时读盘(重启不再从 leader 重拉全量) - B7 集群 watch 自动化:
tests/cluster.rs新增 follower 订阅sm_store.subscribe()收到 leader 发布事件断言;新增forward_hint契约测试(learner ForwardToLeader 携带 leader http_addr)
M5 验收结果(全部通过)
| 验收项 | 结果 |
|---|---|
| 二次登录 409(跨节点,含 HTTP e2e) | ✅ 核心测试 + cluster-demo 断言 |
| kill leader 后会话保持(日志复制) | ✅ chaos 重启后旧 token 仍有效 |
| 审计落库可查(含 action/since/limit 过滤) | ✅ core 测试 + HTTP 冒烟 |
| 审计/会话跨重启持久化(RocksDB) | ✅ 重启后条目仍在、旧 token 有效 |
| 快照跨重启恢复(get_current_snapshot 非 None) | ✅ snapshot_persist 测试 |
| 集群 watch 自动化测试 | ✅ cluster_watch_events_reach_subscribers |
| cargo fmt / clippy -D warnings | ✅ 零警告 |
| cargo test --workspace | ✅ 59 passed / 0 failed(原 49 + 10) |
| 回归:dev-single / cluster / chaos / sdk-contract / check-contracts | ✅ 全过 |
工程记录
- 会话 TTL 语义:状态机存 API 层注入的墙钟 expires_at(仅数据),过期判定在 auth_middleware 用墙钟比较;确定性由"命令内携带时间戳"保证
- leader 转发:login 是唯一"无会话"写命令,转发到 leader 的公开
/api/v1/login(同密码,免新增内部端点,不引入未鉴权 apply 面);其余写命令按 design 返回ERR_LEADER_REDIRECT+leader_hint - e2e 脚本适配集群级单会话:cluster-demo/chaos/restart-test 改为登录一次、token 全集群共享;重启后复用旧 token(会话持久化)
- 坑:
NodeInfo.http_addr无 scheme → 转发 URL 需补http://;MutexGuard非 Send,handler 内须作用域化(不能跨 await)
交付物(依据 dev_docs/remaining-work.md P1 两项)
- B1 模块化归位(4 个占位 crate 全部填充,cli 仅组装):
dsh-api(模块 05/09):ApiState + 全部 HTTP handler + 鉴权中间件 + 错误映射 + Admin UI(rust-embed)+build_router;HTTP 路由与行为与迁移前完全一致dsh-publish(模块 04):PublishService(提交前 secret 加密 I8 / publish / rollback / publish_structure / update_draft 编排)dsh-observability(模块 10):AuditLog(审计落库)+ metrics_text + is_ready + cluster_members_jsondsh-watch(模块 06):WatchHub(广播 + sender + raft apply 转发)+ watch_ssedsh-raft:通用写路径下沉为write_command(dev-single 直 apply / 集群 client_write + leader 转发提示);main.rs从 1469 行降至 ~300 行组装器
- A1 gRPC 数据面(proto/config.v1.proto 落地):
- tonic 0.14 + tonic-prost-build 生成(build.rs,protoc 3.19.6);
ConfigService(GetConfig/GetItem/Watch/ListMembers)挂--grpc-addr(默认 :8383) - proto ↔ 内部模型转换(Value oneof / 事件 / diff→Change);secret 脱敏 + masked 标记(数据面不解密)
- Watch:after_version 历史重放(相邻快照 diff 合成事件)+ 实时事件去重
- 鉴权:metadata
authorization: Bearer <token>(--data-plane-token,静态表 MVP,未配置开放);HTTP 保留为降级通道
- tonic 0.14 + tonic-prost-build 生成(build.rs,protoc 3.19.6);
M6 验收结果(全部通过)
| 验收项 | 结果 |
|---|---|
| 迁移后 HTTP 行为不变 | ✅ dev-single / cluster / chaos / sdk-contract / check-contracts 全过 |
| gRPC GetConfig/GetItem(含 secret 脱敏、NotFound) | ✅ grpc_data_plane 集成测试(真实 TCP + 生成客户端) |
| gRPC Watch(after_version 续传 + 实时事件) | ✅ |
| gRPC 鉴权(无 token 401 / 正确 token 通过) | ✅ |
| gRPC ListMembers(dev-single → FailedPrecondition) | ✅ |
| 新 crate 单测(publish 加密 / watch 广播) | ✅ 2+2+2 |
| cargo fmt / clippy -D warnings | ✅ 零警告 |
| cargo test --workspace | ✅ 65 passed / 0 failed |
| 集群脚本每节点独立 gRPC 端口(88xx) | ✅ 避免 3 节点争用 8383 |
工程记录
- tonic-build 0.14 API 变更:prost 编译移至
tonic-prost-build(compile_protos);生成代码依赖tonic-prostcrate - 生成类型名(Value/EventType/ChangeKind)与 dsh-core 冲突 → grpc.rs 内 core 类型别名(CoreValue 等)
- gRPC 服务经
Server::builder().serve(addr)挂载于独立 task;bind 失败仅告警不影响 HTTP - e2e 脚本无需改动(HTTP 面行为不变;仅 cluster 脚本补充 --grpc-addr 独立端口)
交付物(依据 dev_docs/remaining-work.md P2 两项)
- B3 组级引用(R6/AC6.2 完整语义):
RefBinding.item_key=None= 整组绑定共享组- bind 校验:结构组存在 + 共享组内有 ≥1 个与结构组 item key 匹配的已发布共享项(否则 Validation)
- 发布物化:结构组内 item 按 key 取共享值填充草稿缺失项(草稿显式值优先)
- 共享发布级联:新增
idx/refg/{shared_group}/{project}/{group}组级索引,共享项发布时按结构组 key 匹配推进引用项目分支版本 - 解绑:组级 ref + 组级索引删除
- B6 密钥轮换(design-v2 §7.5 / CRY-002)
- dsh-crypto:
KeyRing(旧 KEK 列表 + 当前 KEK,dek_v代际对齐)、rotate_master_key、rewrap_dek(edek 重包,数据不重加密)、环文件持久化({key-file}.ring.json,重启可解旧数据) POST /api/v1/admin/rotate-master-key(鉴权 + 审计 rotate_master_key + 环文件持久化)- CLI 客户端:
dsh --gen-master-key、dsh --rotate-master-key <b64> --admin-endpoint <url> --admin-token <t>(单会话下 login 会 409,故支持直接传 token) RewrapDeks后台任务(jobs,5min,leader-local 扫描快照/共享项/草稿重包代际 < 当前的密文,幂等)- 启动加载:master key 文件 + 环文件 → KeyRing(轮换后重启数据仍可解)
- dsh-crypto:
M7 验收结果(全部通过)
| 验收项 | 结果 |
|---|---|
| 组级绑定校验(无匹配共享项被拒) | ✅ core 测试 |
| 组级物化(草稿值优先 + 匹配项取共享值) | ✅ core 测试 |
| 组级级联(共享发布推进引用分支) | ✅ core 测试(含多分支) |
| 组级解绑停止物化 | ✅ core 测试 |
| rewrap_deks 扫描快照/共享/草稿重写 | ✅ core + jobs 测试 |
| 轮换后旧数据可解(CRY-002) | ✅ crypto 测试 + HTTP e2e(轮换后 my-secret-1 仍可解) |
| rewrap_dek 数据不变、edek 更新 | ✅ crypto 测试 |
| 轮换 API + 审计 + 环文件 | ✅ HTTP e2e(generation=2、ring 2 entries、审计 1 条) |
| 重启后数据可解(环文件加载) | ✅ HTTP e2e |
| 轮换后新写入可解 | ✅ HTTP e2e |
| cargo fmt / clippy -D warnings | ✅ 零警告 |
| cargo test --workspace | ✅ 74 passed / 0 failed(原 65 + 9) |
| 回归:dev-single / cluster / chaos / sdk-contract / check-contracts | ✅ 全过 |
工程记录
- 组级引用语义:整组绑定共享组 SG,物化/级联按"项目结构组 item key ∈ 共享组已发布项"匹配(key 集合对齐),与 item 级引用并存(各自索引前缀 idx/ref、idx/refg)
- 轮换流程:operator 准备新 KEK(--gen-master-key)→
dsh --rotate-master-key --admin-token <t>调 API → 节点 KeyRing 追加 + 环文件持久化 → RewrapDeks 任务(leader)逐节点重包;旧 KEK 常驻环(可解旧数据)直到 operator 手动清理 - 坑:单会话(I7)下 CLI 客户端 login 会 409 → 增加
--admin-token直接复用会话;e2e 中发布需满足必填项(草稿漏 host 导致 422,非缺陷) - rewrap 为 leader-local 写(与 VersionRetention 同模型):状态机与 raft 日志的既有关系下,快照安装/重启持久化保持一致;旧 KEK 兜底可解,最终一致
交付物
- A2 CI 全量流水线(GitHub Actions,stage 1~9 全落地):
.github/workflows/ci.yml8 jobs- lint(fmt/clippy -D warnings/deny)、unit(cargo test --workspace)、contract(三方契约 lint)
- raft(集群/快照持久化/转发契约测试)、sdk(三语言契约对拍)、e2e(dev-single + 3 节点集群 + 混沌)
- bench(基准冒烟归档)、release(--release 构建 + SBOM + 产物上传)
- A3 正式基准:
scripts/bench.sh+ Go 基准客户端(读 QPS / 写 QPS / watch 延迟 / 二进制 / 内存) - A4 SBOM:anchore/sbom-action(SPDX JSON,随 release 产物归档)
A3 基准实测(design-v2 §12 目标对照)
| 指标 | 本机(debug) | CI runner | 设计目标 |
|---|---|---|---|
| 读 QPS(GET /snapshot,200 并发) | 35016 | 9419 | 未设读上限 |
| 写 QPS(草稿+发布,单写者串行 apply) | 1620 | 1048 | ≥10k(design 注:写路径单写者串行) |
| watch 延迟(发布→SSE) | 12ms | 22ms | ≤1s ✓ |
| 内存 RSS | 40MB | 41MB | ≤128MB ✓ |
| release 二进制 | — | 8.68MB | ≤50MB ✓ |
CI 修复记录(首次上云踩坑)
- protoc 缺失:dsh-api/build.rs(tonic-prost-build)需要 protoc → 所有编译 job 补
arduino/setup-protoc - sdk 脚本硬编码 /home/alex 路径 → 改
$REPO(脚本相对仓库根解析) - sdk watch 首编窗口竞态:Go 首编 ~15s 错过 5 秒发布窗口 → 改为持续发布直到测试进程退出
- 修复后:8 jobs 全绿(unit 74 tests / e2e 含 chaos / lint 零警告 / release+SBOM)
里程碑总览(M0–M8 全部达成):契约 → 单节点/集群 → 发布引擎/加密/渲染/任务 → watch/可观测/会话/UI → 三语言 SDK → 混沌/加固/发布 → 会话落Raft/审计/快照/集群watch → 模块化/gRPC → 组级引用/密钥轮换 → CI 全量/基准/SBOM
依据 dev_docs/remaining-work.md(M8 终态后重新审计)+ 上轮发现清单;每项实现→测试→验证闭环。
- 11 个缺失 HTTP 端点(openapi 25→37 路径): 项目详情/删除(force)、分支详情/删除、分支对比 diff、值提升 promote、 共享库 CRUD + 发布(级联)、共享引用绑定/解绑、cluster/remove(voter RemoveVoters / learner RemoveNodes)
- core 新增公共访问器:list_shared_published / list_shared_drafts / list_refs
- /api/v1/cluster/members 补全成员表(对齐 openapi Member schema)
- P0-b 安全修复:管理面/渲染/数据面默认掩码 secret;reveal=true 需会话+审计 (render 端点新增 reveal/version 参数;与 gRPC 数据面 masked 语义一致)
- 验证:scripts/api-surface-test.sh(13 组断言全过);cluster-demo 增 remove-node 步骤
- gRPC 数据面客户端(TS/Go/Python):GetConfig/GetItem/Watch(断线 after_version 续传)/ ListMembers;Endpoint{grpc?,http?} 优先 gRPC,纯字符串降级 HTTP/SSE; TS 用 @grpc/proto-loader 动态加载;Go/Python 提交生成 stubs(configv1/、config/)
- scripts/sdk-grpc-contract-test.sh:三语言对同一 :8383 契约对拍;CI sdk job 接入(补 setup-go 1.22)
- Admin UI 管理控制台(单文件内嵌,替代原 72 行占位页):登录/登出、项目 CRUD、 结构草稿+发布、分支草稿表单编辑(含 secret)+发布、版本历史+回滚、分支对比+promote、 共享库 CRUD+发布+引用绑定、审计查询、SSE watch;prompt/confirm 改内嵌 modal; 切 tab 实时刷新。浏览器自动化全流程验证通过(含回滚 v3)
- 移除重复的 dsh-cli/admin(实际嵌入源为 dsh-api/admin)
dsh admin {gen-master-key,rotate-master-key,force-logout,set-password,promote, remove-node,snapshot,retention-status}(全局 --admin-endpoint/--admin-token/--admin-password)- core 新增 Command::AdminSetPassword(哈希落状态机,集群一致;login 优先校验,回退节点配置); HTTP /api/v1/admin/{force-logout,set-password,snapshot,retention-status}(鉴权+审计)
- watch 增强:SSE ?after_version=N 重放历史再转实时(与 gRPC 重放一致); 广播溢出(慢消费者)→ gRPC 发 snapshot_required=true 关流 / SSE 结束流(客户端续传)
- 三语言 SDK HTTP watch 断线带 after_version 续传
- 指标 2→9 项:dsh_{projects,branches,versions,shared_items,shared_drafts, audit_entries,session_active,master_key_ok,raft_role,raft_term,raft_committed_index}
- 测试补全:LIM-001 限额拒绝、AdminSetPassword 落库/读取;dsh-testkit 由占位改为真实夹具库 (demo_structure/seed_demo_project,grpc_data_plane 集成测试接入使用)
- 删除 dsh-cli/src/lib.rs 占位;cargo-deny(server/deny.toml 许可白名单)+ CI deny step 去 no-op 兜底
- cargo test --workspace:76 passed / 0 failed;clippy -D warnings 零警告;fmt 干净
- 全部 e2e 脚本通过:dev-single-demo / cluster-demo(含 remove-node)/ api-surface-test / sdk-contract-test(HTTP)/ sdk-grpc-contract-test(三语言 gRPC)/ check-contracts(37 paths)
- Admin UI 浏览器自动化全流程验证(登录→建项目→结构→草稿→发布→对比→提升→回滚→共享→审计)
依据:dev_docs/remaining-work.md(review 检出 5 项未收口)逐项落地并验证。
- 灰度 SDK 三语言适配(端到端最后一环):TS/Go/Python
ConfigClient增加instance/labels身份上报 (gRPCinstance_id/labels+ HTTPX-Dsh-Instance/X-Dsh-Labels)、响应/事件读取gray/resolved_version、 watch「gray 事件永不按版本过滤」+ 断线 snapshot 拉取契约;Go/Python stubs 重新生成对齐 proto G3 字段。 - gray-snap/ 回收:apply 路径(gray publish/promote/abort/结构发布 bump)删除旧序号灰度快照,
消除「随灰度发布累积」已知限制;新增
gray_snapshot_recycled_on_lifecycle测试钉死(core 53 用例)。 - 灰度 e2e 入 CI:
g1-policy-demo.sh/gray-demo.sh/gray-obs-demo.sh三脚本加入 e2e job。 - D1 全落地:
--watch-event-retain(进程内广播缓冲容量,默认 10000)+--allow-no-master-key(启动强制 + 逃生阀;全部演示脚本/README 补 flag;api-surface-test/docker-compose 已自带主密钥无需改)。 - 文档收口:remaining-work D1 闭环、g1-policy 默认 Stale 修订一致、gray-release §6 表回填、本 progress 段。
交付物
- join 重启/崩溃恢复幂等(对应踩坑 C3/C4,详见 dev_docs/defing-cluster.md):
- dsh-cli:有持久化状态(raft-meta 非空)→ 忽略
--bootstrap/--join/--bootstrap-peers,直接 resume; join 收到 409(已在集群)视为幂等成功;join 命中 follower 跟随 428leader_hint切换目标; - dsh-api:
/cluster/join对已存在 learner 幂等成功(openraft add_learner 幂等 re-add), 已是 voter 保留 409(防劫持);非 leader 返回 428 + leader_hint(与写路径同约定); - 根因:join 端点对"已在成员表"的 node_id 返回 409 → 客户端 300ms 重试 30s → 崩溃循环。
- dsh-cli:有持久化状态(raft-meta 非空)→ 忽略
- 静态成员表建群
--bootstrap-peers(研究报告 dev_docs/research-cluster-bootstrap.md):- 三节点传完全相同三段式成员表(
node_id@raft_addr@http_addr必填),并行启动直接选举, 全员 voter,无需 join/promote;openraft 同 map 并发 initialize 安全(先到者首写, 其余收到良性 NotAllowed 后经复制追平); - 校验(启动即失败):三段式必填、raft/http 地址各自查重、拒绝 0.0.0.0/:: 通配、端口 1-65535、 本节点在 map 中且地址与本地参数一致;
- A2:有状态且 seed 与持久化成员表不一致 → WARN 差异明细(不覆盖不阻断——成员表是共识复制数据, 单节点覆盖会分叉;运行期成员变更走 API,推倒重建先清卷);
- B1:长时间无 leader(quorum 未达成)→ 15s 后每 10s 周期提示,消除静默空转。
- 三节点传完全相同三段式成员表(
- 测试与回归:dsh-cli 解析/校验/diff 单测 5 项;dsh-raft 新增三节点静态 map 自举测试;
dsh-api 新增 join 幂等集成测试(learner 重复 join 200 / voter 409 / 地址校验);
scripts/seed-demo.sh(6 段全场景)入 CI e2e。
验证结果(全部通过)
| 检查 | 结果 |
|---|---|
| cargo fmt --check | ✅ 干净 |
| cargo clippy --all-targets --all-features -- -D warnings | ✅ 无警告 |
| cargo test --workspace | ✅ 全绿 |
| scripts/seed-demo.sh(A1/A3 拒绝、B1 提示、建群、复制、同/异 seed 重启) | ✅ 全过 |
| scripts/cluster-demo.sh / chaos-test.sh / dev-single-demo.sh(回归) | ✅ 全过 |
| 真实二进制冒烟:kill -9 后同命令重启 resume;seed 建群写 leader 三节点复制一致 | ✅ |
文档:defing-cluster.md(C3 重写/C4 新增/两种建群方式/校验清单)、docker-compose.yml.demo 与 deploy/docker-compose.yml(seed 主推 + 坑 C1/C2/C3/C4 全部修正)、README(集群方式一 seed)、 research-cluster-bootstrap.md(设计 + 实施状态 + 实测修正)。