diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b690812 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# mkdocs 构建产物。此前仅靠机器级 ~/.gitignore_global 屏蔽, +# 换机器或在 CI 中一次 `git add -A` 会提交上百个构建产物(ADR-023 §12)。 +/site/ + +# 本地与编辑器 +*.log +*.swp +.DS_Store +.idea/ +.vscode/ +__pycache__/ +.venv/ + +# 凭证载体一律不入库(ADR-021)。真实值只存在于被忽略的文件中, +# 占位样例走 *.sample / *.example(check-secrets.sh 的 DENY_NAME_OK)。 +.env +.env.* +!.env.sample +!.env.example +credentials* +*.pem +*.key diff --git a/docs/architecture/decisions.md b/docs/architecture/decisions.md index fd6e0c6..913b627 100644 --- a/docs/architecture/decisions.md +++ b/docs/architecture/decisions.md @@ -160,6 +160,8 @@ **决策**(2026-09-08):M3 MVP = 图片媒体上传闭环 + 帖子草稿/发布/删除 + 公共 Feed 游标分页 + 单层评论 + 点赞/收藏幂等 + Flutter 三页(home/create/post_detail)替换 demo 与乐观更新回滚。关注做最小数据接口(follow/unfollow + 数量);**话题首版剪出**;评论仅单层不做楼中楼。视频后置。 +> **⚠️ 「视频后置」一项已被 ADR-023 取代(2026-09-18)**:M4 视频与图片同批交付。本条其余裁剪(话题首版剪出、评论单层、区域系统属 M5)继续有效。 + ## ADR-019 写接口幂等形态按域选择 **决策**(2026-09-08):二元状态互动(点赞/收藏/关注)用 **PUT/DELETE 语义幂等**(重复调用同终态,无键管理);创建型写入(发帖/评论/媒体登记)用**表内幂等列(request_hash)**。M2 的 Idempotency-Key 键派生机制在 pets 域维持不变,不回改。 @@ -193,3 +195,117 @@ - **获赞总数走读侧实时聚合**(`SUM(like_count)` over 本人未删帖),**不引入新冗余列**:写侧维护成本高于读侧聚合收益,且数据量级远未到瓶颈。端点为新增的 `GET /api/v1/me/community-stats`,不并入既有 `follow-stats`(后者主体是「某用户的关注数」,混入「我的获赞」会造成主体歧义)。 **版本号**:本迭代交付按 **v0.4.0** 发布(新增端点与字段属功能增量,非纯补丁)。 + +## ADR-023 M4 AI 创作的架构、边界与关键决策 + +**决策**(2026-09-18,开工分析八份报告 + 与用户逐项讨论后拍板)。本 ADR 覆盖 M4 全部跨角色决策;单角色内的细节决策留在各自报告中。 + +### 1. 范围:视频与图片同批 + +- **视频与图片同批交付**,**取代 ADR-018 的「视频后置」**。数据库侧本无障碍:目标模型的 `creation` 三表自始按 `media_kind IN ('image','video')` 建模、目录按 `(id, media_kind)` 复合唯一键约束,`media.assets.kind` 的 CHECK 亦已含 `'video'`、`duration_ms` 已在。 +- 因此视频的成本集中在**契约与客户端**,不在 schema:契约 `kind` 枚举加 `video`、Flutter 需引入播放器依赖(现为零)、缩略图与时长呈现(`MediaType.video` 已备未启用)。`iteration-4/03` R13 评级 🟠 中高。 +- **供应商**:AI 视频 = 火山引擎 Seedance;AI 图片 = gpt-image-2。生产服务器为腾讯云轻量、**无 GPU**,自托管模型不可行,故一律走云 API。 +- **`patbond-ai` 先完成两类 provider 的接口适配**,客户端视频路径随契约扩展跟上。 +- `iteration-4/05` 的目录呈现为**数据驱动**(目录返回哪些 `media_kind` 就渲染哪些分组),故契约扩到 video 时 UI 自动长出第二段,无需重画。 + +### 2. 服务划分:patbond-ai 为独立 Python 仓,Java 持有队列 + +- **新建 Java 模块 `patbond-creation`(:8085)**,前缀 `/api/v1/creation/**`,承载客户端端点、Worker、Python 客户端与 `generation_jobs` 读写。沿 ADR-009 / ADR-017 两次先例(新模块 + 共库 + patbond-user 单迁移链)。 +- **`patbond-ai` 为独立仓、Python 实现**,只负责调用 AI 供应商 API。**不访问数据库**:任务状态的唯一事实来源是 Java 侧的 `creation.generation_jobs`。 +- **理由(为何不让 Python 直接消费队列)**:目标模型已把租约、退避、优先级、幂等、provider 去重全部设计在 Java 侧表中(`lease_owner` / `lease_expires_at` / `next_attempt_at` / `attempt_count` / `max_attempts` / `priority` 六列 + 两条与出队/回收语句逐列对齐的偏索引)。让 Python 也轮询该表等于用两种语言实现同一套租约逻辑,并使事实来源分裂。 +- **迁移链归属不变**:V6 仍放 `patbond-user`。这是既有约定而非新决策——V3(`pet_health`)与 V5(`community`)的业务代码在各自模块,迁移文件全部在 `patbond-user`。 +- **共享物**:`patbond-creation` 需复制 3 件(JWT 资源侧校验、`UuidV7`、`RequestHashes`)。**存储适配层不复制**,见 §4。是否上提 `patbond-common` 作为独立前置工单评估,退路是各抄一份。 + +### 3. 队列载体:Postgres `FOR UPDATE SKIP LOCKED` + 租约,不引入 broker + +- **不引入 Redis、不引入 MQ**,偏离 `development-plan.md:205` 的 RabbitMQ 设想。 +- 三条理由:① 目标模型已把租约队列设计完(含现成领取 SQL),换中间件等于让这批列作废、推翻已评审模型;② 实测 `docker-compose.yml` 仅 postgres/minio/auth/user/pet/community,**无 Redis 无 MQ**,引入即净增容器与运维面——与 ADR-002 移除 Nacos 同一判例;③ 任务状态真值保持单一(Postgres),无 dual-write,「Worker 重启不丢任务」由租约回收器天然满足。 +- 缺口:目标模型给了索引但没给回收器代码,**租约超时回收器为 M4 新增工单**。 + +### 4. Java↔Python 边界 + +- **统一异步,无论上游同步还是异步**。Seedance 类为异步(提交 + 查询),gpt-image 类为同步长连接;该差异**不得渗透到客户端契约与 job 表**——`generation_jobs` 状态机本就是异步形状,`iteration-4/05` 的等待态设计亦只有一条路径。Python 收到 submit 立即返回任务 id,同步 provider 在 Python 内部以后台任务承载长连接。 +- **收益**:Java 侧无长连接占线程(轻量服务器关键);`provider_request_id` 立即可得,`uq_generation_jobs_provider_request` 唯一索引才真正生效;超时不再产生「是否已扣费」的歧义——同 id 重新轮询即可,而非重发。 +- **双重扣费的残余风险如实登记**:Java 将自身 `idempotency_key` 透传 Python,Python 转发为上游幂等头。**上游不支持幂等头时风险窗口无法消除**,仅由 `max_attempts` 兜住并记录,不假装已解决。各供应商是否支持须逐一核对官方文档。 +- **传输为 REST(JSON),不用 gRPC**。理由不是性能——瓶颈是上游数秒至数分钟的出图出片,序列化差异无意义。理由是**契约体系**:本项目全部契约纪律建在 OpenAPI 上(快照、计数断言、契约矩阵、逐迭代冻结),引入 `.proto` 等于第二套无门禁契约体系;而本迭代开工审计恰好证明「没有门禁的契约必然漂移」(四模块快照锁实际不存在,白名单 42/41 之误四份文档一路不可见)。需要类型安全时**从 OpenAPI 生成 Python 客户端**,保持单一来源。 +- **内部契约独立成文**:`patbond-ai` 仓内单独一份 `openapi-internal.yaml`,独立版本号,**不进四模块快照锁**,也**不写入面向客户端的 `openapi.yaml`**——否则内部适配端点会污染公开契约的路径/操作计数。 +- **产物落盘走 presigned PUT**:Java 经 `patbond-user` 新增的 `/internal/media/**` 取得预签名上传凭证,交给 Python 直传对象存储,再由 Java 核实并建 `media.assets` 行。这正是客户端现用的两步上传,Python 站到客户端位置。 + - 由此**服务端无需新增 `putObject`/`getObject` 原语**:`ObjectStorage` 已有 `presignPut` 与 `presignGet`。开工报告中「服务端零对象写能力 = M4 缺失地基」的**推论据此更正**——缺 `putObject` 是事实,但服务端需要的是签发上传凭证,该能力早已存在。 + - 附带收益:**Python 零对象存储凭证**,少一处凭证面。`MediaProperties` 已区分 `endpoint`(容器内)与 `publicEndpoint`(客户端可达),预签名 URL 的 host 分发本就分离。 +- **轮询为保证,回调为优化**:绝不可只靠回调(网络抖动、任一侧重启、乱序都会使任务永久卡在 running)。**M4 只做轮询**:submit 请求体预留可选 `callbackUrl` 字段但不启用,将来启用无需契约变更。若日后启用,回调鉴权用 **Java 下发的一次性 job 绑定能力票**(Python 原样带回),使 Python 侧无需任何私钥。 +- **服务端轮询节律**(Java → Python,Docker 网络内):前 60 秒 3s、60s~5min 10s、5min 后 30s。**租约 TTL 必须大于单次轮询间隔**(设 60s,每次轮询顺带续租),否则会出现「任务仍在跑但租约过期被他人抢占」。`ix_generation_jobs_running` 即为「捞出待轮询的 running 任务」而建。 + +### 5. 鉴权:服务身份 JWT,独立密钥对 + +- Java→Python **签发服务身份 JWT,使用独立密钥对**;**禁止转发用户 JWT**(Worker 异步执行时用户 token 可能已过期,且不应让 Python 持有用户凭证)。 +- Claim 约定:`iss=patbond-api`、`aud=patbond-ai`、`sub=service:creation-worker`、`typ=service`、短 `exp`(5 分钟)、带 `jti`。Python 侧强制校验 `aud` 与 `typ`。 +- **选独立密钥对的事实依据**:现有 `JwtVerifier` **仅验签名,不校验 `aud` 也不校验 `iss`**,而 `JwtSigner` 签发的用户 token **不含 `aud` claim**(仅 `sub`/`iss`/`sessionId`/`exp`)。若服务票复用同一密钥对,user/pet/community(很快加 creation)四个模块都会把它当合法用户 token 接受——`sub` 非 UUID 大概会在后续解析时失败,但那是**意外挡住而非设计挡住**。独立密钥对无需改动任何已发布的验证器。 +- **两套内部鉴权并存的说明**:Java↔Java 的既有内部通道继续用 `InternalAuthFilter` 的共享 token(header 携带,未配置则 `/internal/**` 全拒;该 token 在 2026-09-11 安全事件中轮换过)。Python 那一跳改用 JWT 是有意选择更强机制(有效期、`aud`、`jti`,无长期共享秘密)。**本迭代不改造既有 Java↔Java 通道**,避免范围蔓延。 +- **数据最小化**:请求体不传 `userId`,除非 Python 确有需要。Python 完成工作只需 `jobId` + 生成参数 + 预签名上传 URL。 + +### 6. 数据模型:V6 两处偏离,V7 待供应商文档 + +- V6(`creation` schema 三表 + 14 索引 + 3 触发器 + 补回 `posts.generation_job_id` 外键)已落地,**零跨 schema 外键需裁剪**——首个不必砍外键的 baseline 迁移。 +- **偏离一:`input_asset_id` 改可空**(目标模型 NOT NULL)。参考图为**选填**,主流生图与生视频均同时支持文生与参考生,由用户是否上传参考图选择路径;原 NOT NULL 会使文生图无法表达。**未发明 `requires_input` 标记**(不超出已评审模型),「某模型是否必须有输入」由应用层查目录判断。 +- **偏离二:新增 `first_attempt_at`**(目标模型无此列)。`iteration-4/06` §4.2 的指标口径要求分离排队时长与纯执行耗时,而目标模型无法表达——其 `ck_generation_jobs_state` 强制 `queued` 分支 `started_at IS NULL`,故失败重排队必然擦除首次尝试时间。该列仅首次进入 `running` 时写入、永不覆盖,并**刻意排除在 `queued` 分支的置空要求之外**;`ck_generation_jobs_first_attempt` 钉住其与 `attempt_count` 的双向不变式。 +- **V7 目录种子暂缓**:种子需填 `provider_code` / `provider_model_name` / 版本快照等真实取值,须待 Seedance 与 gpt-image-2 官方文档到位后再写,否则即为编造数据。沿 V3/V4「结构与种子分离」先例。 +- **`posts.region_id` 仍为裸列**,区域系统属 M5(ADR-018 该项仍有效)。 + +### 7. 对象存储:决策挂起,现有实现不动 + +- **本地 MinIO 与云对象存储之间的选择本迭代不作决定**,理由是分布式、磁盘容量、备份恢复等考量尚未收敛。**现有本地存储代码一行不动、不删除。** +- 已核实的两点供决策参考: + - **`media.assets` 已同时支持两种模型**:`storage_type` 的 CHECK 为 `IN ('object','external')`,object 用 `bucket`+`object_key`,external 用 `external_url`,`ck_media_location` 强制二选一。**两种存储可共存于同一张表**,允许存量资产留 `object`、新资产走 `external` 的渐进迁移,**任一选择均无需 DDL 迁移**。 + - **现状是宿主机本地磁盘**:MinIO 数据落在 Docker 命名卷 `minio-data`,无任何云对象存储。且 `server-exposure.md` §2 显示生产服务器**只运行 gitea/nginx/文档站/act_runner/dockerd/sshd**,六容器栈(含 MinIO)当前**仅在开发机运行**。 +- **由此挂起两项**:① M4 是否将六容器栈部署到服务器(不部署则交付仍为「本机可验、真机不可达」);② MinIO 的 9000 端口既不在防火墙放行范围(仅 22/80/443)也无 nginx 路由,**预签名 URL 目前对真机不可达**——这构成客户端媒体链路的上线前置,也是「媒体上传弱网」真机验证长期挂起的深层原因之一。**注意 M4 的 AI 产物上传不受此影响**:Python 在容器网络内走内部 endpoint。 + +### 8. 埋点与实验 + +- **`eventVersion` 定型为「每个事件自身的 props schema 版本」**,否决「字典世代」读法。决定性依据是**可修复性不对称**:此读法下客户端现行硬编码 `1` 本就正确(零改动零回填);「字典世代」读法下全部历史行皆错且不可修复(`server_ts` 无法反推事件当初属于哪代字典)。配套 bump 规则:新增可选属性不 bump;删属性、改语义单位、改既有枚举含义、改触发时机必须 bump,新旧版本在字典中并列为两条。 +- 落地时白名单 key 改 `(name, version)` 二元组,并新增逐条拒绝原因 `unknown_event_version`——**必须置于逐条校验路径,不得放在 DTO 校验**,否则重犯整批连坐错误(见下条)。 +- **服务端生成侧不建第二条埋点管道**:以 `creation.generation_jobs` 事实表为服务端指标权威,经 `creationTaskId` 与客户端事件拼接。`platform.product_events` 的 `anonymous_id`/`session_id` NOT NULL 与 `platform IN ('android','ios')` CHECK 使 Worker 事件无法入表,放宽这两个 NOT NULL 是不可逆的反向操作。 +- **客户端埋点整批丢弃缺陷须修**:`TrackEventsRequest` 的 `@Valid` 级联至列表元素,任一元素校验失败即整请求 400,客户端进而判为永久拒绝并**整批删段**。这是**与平台无关的毒丸批次放大器**(任一条事件的任一字段可毒死整批 50 条),桌面 `platform=linux` 100% 丢事件只是其最显眼的表现。`analytics_service.dart` 注释所称「逐条 rejected、不影响客户端」与实现相反,且该错误表述已转述进 5 处文档。**枚举排除 linux 是设计意图,不动枚举;修客户端本地短路 + 调试覆盖开关。** +- **A/B 前置状态重置为「5 绿 3 半」**(原记「6 绿 1 半」):分流哈希、Flutter 曝光封装、feature flag **三者代码零实现**,须作为前置工单而非既有资产。按旧口径排期会在实验启动日才发现要先写三个组件。 +- **M4 首个「实验」为 A/A 基建验证空跑**(零产品风险,≥200 曝光即可暴露分流失衡与曝光漏报);真实 A/B 设计冻结存档,**放行条件挂流量而非挂日期**。 +- **北极星首次出数改为事件驱动触发条件**(原 2026-09-21 日期承诺作废,其 W37 入队窗口已于 09-13 关闭,无补救可能):`T0` = 首个满足「≥1 名真实移动端用户产生生命周期首条 `health_record_create_succeeded`」的 ISO 周,首次出数 = `T0 + 8 天`后的首个周一。**明确否决**用桌面 override 或 curl 造数凑读数。另须把北极星 SQL 落成仓库内可执行载体——它目前只存在于报告正文。 +- **补一个断言埋点白名单条数的测试**,列为 M4 前置:白名单实际 41 条而四份文档记 42(`experiment_exposed` 被重复计数),漏了整整一个迭代未被发现,根因正是 `EventDictionaryTest` 零条数断言。 + +### 9. 契约 + +- `openapi.yaml` **v1.4.0 → v1.5.0,纯增量**:零删除、零重命名、零必填收紧。「一键建草稿」复用既有 `POST /api/v1/posts`,**不新增路径**。 +- **429 与配额拆为两个错误码**,不共用一码:配额耗尽等的是次日(呈现为 sheet 并给出出口),频率过快等的是数秒(呈现为 SnackBar);共用一码必然使一半场景的文案与出口错误。契约当前**完全没有 429、`Retry-After` 或任何响应头机制**(`components.headers` 为空),M4 为首次引入,同时兑付客户端 `Retry-After` 死分支的历史挂账。 +- **两处配置扩展,零迁移**:`MediaProperties.allowedMimeTypes` 加视频类型(现仅 jpeg/png/webp)、`allowedPurposes` 加 AI 产物 purpose。`media.assets.purpose` 为裸 varchar 无 CHECK。 +- **须一并修正契约内部既有矛盾**:`openapi.yaml:1228` 称「purpose 仅 `post_image`」与 `:3460` 的三值枚举冲突(M3.5 加值时漏改端点描述)。M4 再加值若不同时修,即为第三次漂移。 +- **「AI 创作」标签不得由客户端声明或由 purpose 推断**:社区侧 `PostService.validateAssets` 只校验归属与 ready、不校验 purpose,故仅凭 purpose 或客户端标记可被任意图片伪造。AI 来源**须由服务端从 `generation_job_id` 派生**。 + +### 10. 客户端 + +- **进度反馈用自适应轮询**(1s×3 → 2s×5 → 3s,上限 5s),**不做 SSE/WebSocket**。决定性理由:现有网络层全部围绕「信封解包 + 401/40101 单飞刷新重放一次」构建,SSE 两者皆用不上,等于旁路整条既建鉴权链、形成第二套鉴权路径——而 M3.5 端口事故的根因正是「跨模块调用绕开既有接线纪律」。且任何推送方案都必须有「查状态」GET 兜底,有了它轮询已免费到手。 + - **AI 对话类若进 M5 需重新评估流式**,本条不构成对流式的一般性否决。 +- **硬超时按 `media_kind` 分档**,且**由服务端在任务响应中下发**(如 `estimatedTimeoutSec`)而非客户端硬编码:图片 5 分钟对视频不足,硬编码两个数会使换 provider 就要发版。 +- **绝不承诺推送**:客户端无任何推送能力(无 WS/SSE/FCM)。等待页写「完成后通知你」会使用户锁屏等待而永不到达;正确表述为「回到 App 就能看到结果」。 +- **不押注百分比进度**:`ck_generation_jobs_progress` 与状态机使 `queued` 时 progress 恒 0、`running` 时无下限约束,押注百分比的后果是最常见观感变成「0% 卡住」。主形态为阶段文字 + 不确定进度环,`progress>0` 才叠确定型数字;**展示已用时而非 ETA**(已用时是事实,ETA 是承诺,M4 给不出可信 ETA)。 +- 须定义服务端五个 status 之外的**第六个 UI 态:僵死/超时**(`running` 超阈值),且它不是错误态。 + +### 11. 遗留搭车与优先级调整 + +- **`widthPx`/`heightPx` 列 M4 必修**,且**必须两侧同批**——只修一侧用户零感知。该债的三段事实:详情页客户端**已正确消费**(有 clamp,服务端一给即生效);Feed 卡片是**客户端硬编码 4:3 缺陷**(维度可用却被忽略);客户端**根本无法提供维度**(`CreateMediaUploadRequest` 契约无该字段)。M4 严重度上升的实质不是「更严重」而是**「从看不见变成看得见」**:结果页按真实构图渲染、发到 Feed 被裁成 4:3,是同一次会话内的直接前后对照(9:16 竖图裁掉约 58%)。而 AI 输出路径**不需要图片解码**,Worker 写产物时维度就在手里。 +- **`SegmentedButton` 粉底降为 P2 品牌一致性债**(原记 P1 无障碍债):实测 `secondaryContainer #FFDAD2` + `onSecondaryContainer #5D4038` 对比度 **7.19:1 达标**。仍搭车修(根因与 M3.5-01 修 DatePicker 完全同构,主题层无 `segmentedButtonTheme` 亦无 `chipTheme`,一处补全修好 6 个使用点),但**不为它拖 M4 范围**。 +- **「我的收藏与草稿」两页搭车**:后端与客户端仓库方法均已就绪且在测试覆盖内,**只缺 2 个页面**,为本次盘点投入产出比最高项。 +- **必须与放开写侧同一提交修掉的数据损坏路径**:`post_compose_page.dart` 将恢复的 AI 草稿类目静默降级为 `general`。现无害(写侧被封),**M4 放开写侧后会把类目改错并写回服务端**。 +- **`ai_creation` 与话题无关**:它只是 category 枚举值,与 topics 表零字段关联,M3 所提「话题随 `ai_creation` 一起做」的搭车理由不成立,话题继续挂起。 + +### 12. 门禁、安全与发布 + +- **必需状态检查上下文改为 `(pull_request)`**(已执行并复核生效):原为 `CI / backend-test (push)`,而 `ci.yml` 的 push 触发器限定 `branches: [dev]`,造成双重后果——门禁实际由「推 dev」满足使 **PR 侧检查红也能合并**,且**任何非 dev 分支 → main 的 PR 永久无法合并**(其 head 永不产生 `(push)` 状态),使 hotfix 短命分支纪律实际失效。**改动为替换而非追加**(追加不解除死锁)。建议以一次空 PR 验证,勿以正式发布为试验场。 +- **M4 继续 dev 直推**:`push` 触发器只覆盖 dev,推 `feat/**` 不跑任何 CI,开分支等于自断反馈。仅 `spike/*`(探完不合并)与 `refactor/*`(rebase 后 ff-only 合回)开短命分支,且**均不得直接开 PR 到 main**。 +- **`check-secrets.sh` 已补 AI 服务商密钥形态**(三仓同构):新增 `AK-ANTHROPIC`(`sk-ant-`)与 `AK-OPENAI`(`sk-`/`sk-proj-`/`sk-svcacct-`),`KEY-ASSIGN` 扩至 `api[-_]?(key|secret|token)` 与 `auth[-_]?token`。修复前 5 种真实形态全部漏网。**须先于任何 AI 凭证落地**——现已完成。 +- **`patbond-ai` 纳入发布语义**:参与四仓 tag 同步、CI 门禁、防泄漏脚本与 E2E 回归。**它是唯一将存放 AI 凭证的仓,不得成为唯一没有防泄漏门禁的仓。** +- **契约跨仓漂移无 CI 兜底**:api 的 CI 只读 api 仓内文件、只验「快照↔守卫↔矩阵」内部自洽,**完全不校验与 doc 正典的字节一致性**。升版须按「doc 升正典单独提交 → 记录 md5 → api 侧一次性改完全部快照/常量/计数断言/矩阵 → 四道校验后单个原子提交」执行;最危险的反模式是手改快照而非从正典 `cp`——CI 不会发现。 +- **发布流程须固化进 `git-workflow.md`**:现正典是一份标题仍写「草案」的迭代报告,8 步中 4 步已过期,全部修正只以散文形式存在于 `releases.md`。**E2E 份数只在一处写死**,其余位置表述为「全份」并链接过去。M4 引入对外端点,故须新写第五份 E2E 脚本,发布回归变为五份。 +- **`patbond-doc` 须补 `.gitignore`**:`site/` 目前仅靠机器级全局忽略屏蔽,换机器或进 CI 时一次 `git add -A` 会提交上百个构建产物。 +- **三仓 `core.hooksPath` 均为 unset**,防泄漏第一层从未生效,须逐仓启用。 + +### 13. 版本号 + +本迭代交付按 **v0.5.0** 发布(新增模块、新增端点与新增 schema,属功能增量)。