games-development-ai/docs/agent-specs/2026-06-09-王蓝莓小卖部黄金闭环-execution.md
zizi 867c4523ae docs(exec): 黄金闭环执行 spec(HJ-EXEC-006 v2) 定稿 — 双模型两轮评审 Go
- B0 产出:化解 autoplan 5 个 critical,主 agent Opus 子代理独立核验真实代码(file:line)
- Codex 首轮发现 11 项(4C+5H+2M)全采纳,推翻 v1 的 C5 幂等简化(恢复 eventId+uk_event_id)
- Codex 第二轮确认轮:Go,11 项 spec 层全闭合、无新设计矛盾,留 4 个 B1 实现守门
- D-PUB 已拍板 α(审核通过即自动发布);完成线=数据回路 A/B 可证

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 13:35:26 +00:00

21 KiB
Raw Blame History

王蓝莓小卖部黄金闭环 — 执行 spec(HJ-EXEC-006 · v2 经 Codex 复审硬化)

文档号:HJ-EXEC-006 | 版本:v2 | 日期:2026-06-09 | 分支:dev/2.0.0 | 基线:dev/1.0.0 上游:设计 王蓝莓小卖部黄金闭环-design.md(v2)、路线 下一阶段路线-plan.md(B 轨)。 执行视图:MVP作战清单.md 的 B0→B1→B2→B3。状态以 MVP进度总账.md 为准。 双模型评审:主 agent Opus 子代理独立核验真实代码(§3 file:line)+ Codex Eng 二审(4C+5H+2M 全采纳,对账见 §9)。v1→v2 关键反转:C5 恢复 eventId(v1 简化被证不成立)。


0. 目标与范围边界

目标:1 条垂直闭环做真,完成线 = 数据回路 A/B 可证(B 上报有效事件后排到 A 前 + 关 mock 仍通过 + 重放不重复计数)。

范围内:通用 clicker-plus 数据驱动 runtime + 确定性 PackageFactory;发布编排 审核通过→PUBLISHED→写 feed;最小数据回路(同步写 telemetry→算 quality_score→回灌 feed rank);小卖部皮肤 + ≥10 参数化种子。

非目标(YAGNI):真实 Dify/LLM(用模板/兜底)、真实 OSS(package 存 DB,退场契约 §3.4)、MQ producer/consumer/DLQ(同步写替代 §3.5)、创作者自助/定时发布(auto-publish §1)、真实鉴权/多租户(mock token + tenant_id=1)、3D/Cocos、为单品类扩 runtime(经营=配置皮肤)。


1. 决策点 D-PUB(✅ 已拍板 α,2026-06-09):审核通过即自动发布

Codex 二审指出 α 会改 APPROVED 常驻语义(admin UI 把 APPROVED 当可下架态,game-admin/src/views/wanxiang/review/index.vue:88)。用户已拍板 α(2026-06-09);连带改动(本节下方)纳入 B1 实现。

现状:submitPublish(创作者)已声明发布意图→项目进 REVIEWING(1);reviewProject(APPROVE) 止于 APPROVED(2)(ProjectServiceImpl.java:291);APPROVED→PUBLISHED 无代码。

推荐 α(审核通过即发布):人工 reviewProject(APPROVE) 在同一本地事务内:记 APPROVE 审核记录 → 项目状态直接 REVIEWING(1)→PUBLISHED(4)(跳过常驻 APPROVED)→ 编排发布(§3.2)。

  • α 连带改动(Codex M10):① ProjectStatusEnum 注释标注 auto-publish 下 APPROVED(2) 仅作审核记录值、项目态不停留;② contracts/api-schemas/project.yaml 审核响应说明;③ admin review/index.vue 的状态筛选/「可下架」操作改挂 PUBLISHED(4)(UNLIST 作用于已发布);④ 单测更新。
  • compliance gate 与 α 的关系(Codex M10 澄清):submitPublish 的锁风门 verdict——block→不写库(admitted=false);review/pass→进 REVIEWING(1)。compliance「pass」不自动发布,只是放进人工审核队列;只有人工 APPROVE 才触发 α 自动发布。

若选 β(审核通过→APPROVED 常驻,再单独「发布」动作):§3.2 触发点从 reviewProject(APPROVE) 改为独立 publishProject 端点,§1 连带改动里 ③ 反向(APPROVED 保留可下架),其余 §3 不变。localized。


2. 前置条件(开工即查,不补则静默假成功 —— Eng 双审实证)

后端/环境:

  1. 契约裁决:以 contracts/*.yaml / -api Java 接口为准;不破坏契约、不改已合入 Flyway V1-V9,允许新增 V10 + 最小后端实现。
  2. staging:BASE=http://100.64.0.7:48080,单体在 mini-desktop 隔离容器。重型构建/启动一律 mini-desktop,勿本机。

前端联调(Codex C3/C4/H8 —— 不补则试玩取包失败退 demo、like/share 漏采): 3. 环境变量真名 VITE_API_BASE(game-studio/src/api/request.ts:35);mock 中间件加门控(VITE_API_BASE 非空即关),启动打印 baseURL+mock 状态。 4. host 取包统一走 src/api/runtime.ts(经 request.ts),删除/改造裸 axios 同源取包(src/host/runtimeApi.ts:37);Play/Preview 显式注入 fetchPackage(Play.vue:196 现未注入);request 拦截器补 tenant-id:1 + token=test1。 5. fetchAndVerifyManifest 的相对 manifestUrl resolve 到 API base(inject.ts:174 现裸 fetch 同源)。 6. Play.vue 的 onTelemetry 白名单原样转发契约事件名(Play.vue:112 现把所有事件改写成 game_impression,吞掉 like/share);Feed 互动补有效 traceId(Feed.vue:151,168)。 7. sendBeacon 写死同源 /app-api(telemetry/index.ts:24-25,142,204)→ 改走 VITE_API_BASE 或常规 axios 通道;所有 staging 种子行统一 tenant_id=1。 8. 前端补 eventId(UUID)+ 真 traceId(buildEnvelope 现无 eventId、traceId 默认 '',telemetry/index.ts:101)。


3. 5 个 critical 的确定性解法(现状 file:line → 解法 → 契约/文件 → AC)

3.1 C1 — 「可见态」精确定义 + feed 读路径 enforce(Codex H9)

现状(已核验):ProjectStatusEnum DRAFT0/REVIEWING1/APPROVED2/REJECTED3/PUBLISHED4/UNLISTED5/BANNED6(game-module-project-api/.../ProjectStatusEnum.java:21-27);game_project.current_version_id 在(V1.0.0:24);game_runtime_package.status 0/1已发布/2(PackageStatusEnum);feed 默认查 zone_id=0(FeedServiceImpl.java:47)。Codex H9:feed 读只过 rank.zone/status/sort(FeedRankMapper.java:29),发布门禁 isGamePublished() 默认 true(FeedServiceImpl.java:285)——写错一条 rank 就出流。

解法 —— 可见态四条件谓词 + 读写双侧 enforce:

可见态 = project.status==PUBLISHED(4) AND project.current_version_id==versionId
         AND game_runtime_package(versionId).status==PUBLISHED(1) AND game_feed_rank(game_id,zone_id).status==1
  • 写侧:FeedApi.upsertRank 写 status=1 前校验前三条件(不满足拒写)。
  • 读侧:去掉 isGamePublished() 默认放行;buildStream 对 rank 结果批查 project 二次过滤(或 join),漏发/降级游戏不出流。

AC:仅走完 §3.2 编排的游戏进 feed;手工写错的 rank 行不出流。

3.2 C2 — 发布编排(核心;本地事务硬约束 Codex H5/H6)

现状(已核验):reviewProject APPROVE 落 APPROVED(2)、全仓无 setStatus(PUBLISHED);submitPublish 止于 REVIEWING + compliance gate 已注入(ProjectServiceImpl.java:140);feed-api 无 FeedApi、getZones() 返空;可复用孤儿端点 RuntimePackageServiceImpl.publishPackage(:57-82)翻包 0→1,但 version 回写是 TODO(:77)、不写 feed、无调用方。Codex H5:跨模块 -api 是 @FeignClient 风格(ProjectApi.java:22),本地事务仅靠 @Primary 本地实现(ProjectApiImpl.java:21)。

解法 —— 新增 PublishOrchestrationService(project-server),α 在 reviewProject(APPROVE) 同一 @Transactional 内串联:

  1. 记 APPROVE 审核记录。
  2. 校验 game_runtime_package(currentVersionId).status==0预览就绪,否则抛 PROJECT_PUBLISH_PACKAGE_NOT_READY(新错误码),事务回滚、审核不落。
  3. RuntimePackageApi.publish(currentVersionId):包 0→1(复用 publishPackage 逻辑)。
  4. game_version(currentVersionId).status=3已发布 由本编排显式写(不依赖 runtime TODO,Codex H6)。
  5. project.status=PUBLISHED(4)(首个 PUBLISHED 写入点)。
  6. FeedApi.upsertRank(...):写 game_feed_rank(game_id, zone_id=launch_zone_id||0, version_id, quality_score=0, boost=0, sort_score=0, status=1, pinned=0)(发布基线 upsert,与 §3.5 算分 upsert 分流)。

事务原子硬约束(Codex H5,B1 必守):FeedApiImpl/RuntimePackageApiImpl 必须为 @RestController @Primary 本地委托 Service 的实现;PublishOrchestrationService 注入这些本地 bean;事务内禁止 Feign/HTTP/@Async/REQUIRES_NEW。加故障注入测试:feed upsert 抛错 → project/package/version 全回滚。

契约/文件:新增 game-module-feed-api:FeedApi+FeedRankUpsertReqDTO(含 mode:PUBLISH_BASELINE / QUALITY_REFRESH 分流,Codex M11);game-module-runtime-api:RuntimePackageApi.publish(versionId);project-server 加依赖 feed-api/runtime-api;新增 FeedApiImpl/RuntimePackageApiImpl(@Primary 本地);改 ProjectServiceImpl.reviewProject APPROVE 分支调编排。

AC:人工 APPROVE 后 /app-api/feed/stream(zone 0)含该游戏,顺序由 sort_score 真实驱动。

3.3 C3 — version 创建 + 完成回填闭环(Codex H7)

现状(已核验):game_version 表在(V1.0.0:44-61),全仓无 insert;AigcTaskDO.versionId 字段在(:43)靠未实现 difyCallback 回填;studio 轮询完成态直接取 task.getVersionId()(StudioServiceImpl.java:181)——只在 PackageFactory 插 version 不够。

解法 —— 落包步骤建版本 + 回填 aigc 任务,按 taskId 幂等:PackageFactory 成功落包后:① ProjectVersionApi.createForPackage(...) 创建/复用 game_version(status=2预览就绪);② 写 game_runtime_package(status=0);③ AigcTaskService.completeWithVersion(taskId, versionId) 回填 game_aigc_task.version_id + status=succeeded → studio 轮询据此拿到 versionId。current_version_id 仍由创作者 submitPublish(versionId) 写入。

契约/文件:新增 project-server GameVersionDO/GameVersionMapper/GameVersionService + ProjectVersionApi.createForPackage(-api);aigc 新增 AigcTaskService.completeWithVersion(幂等)。不改 V1 表结构。

AC:每个 clicker-plus 包对应真实 game_version 行;studio 轮询能拿到 versionId;重复回填幂等。

3.4 C4 — package 落地 + manifest 原样服务(Codex C4)

现状(已核验):game_runtime_package 无 package_json 列,manifestUrl 由 package_url 纯派生(RuntimeConvert.java:44、runtime.yaml:223);前端 fetchAndVerifyManifest 对原始字节算 sha256、checksum 缺省则跳过(inject.ts:169-186);当前最大 Flyway=V9。

解法(MVP 无 OSS,包存 DB,端点原样服务,抽象隔离):

  • Flyway V10:ALTER TABLE game_runtime_package ADD COLUMN package_json LONGTEXT NULL COMMENT 'MVP 临时整包;M3 接 OSS 后下线(退场契约)'。三处同步(contracts/db-schemas、模块、yudao-server)。
  • 新增 PackageStore 接口(runtime-server):putManifest(versionId,json)/getManifest(versionId):String;MVP impl=DB(读写 package_json),M3 换 OSS impl 调用方不变(退场契约)。
  • 新增端点 GET /app-api/runtime/package/{versionId}/manifest:返回原始 JSON 文本,不包 CommonResult、不 parse/re-serialize、不加换行(Codex C4);checksum=sha256(该响应文本),PackageFactory 写入时按相同字节计算并存 game_runtime_package.checksum。
  • 前端:相对 manifestUrl resolve 到 API base(§2.5);取包统一走 request(§2.4)。

AC:GamePlayer 取真包→manifest 来自 DB→sha256 校验通过→runtime 加载试玩,不退 demo 兜底(staging 须真包)。

3.5 C5 — eventId 幂等(v1 简化被推翻,恢复 design v2)+ process_status(Codex C1/C2)

现状(已核验):game_telemetry_event 唯一键 uk_dedup(trace_id,event,ts)(V5.0.0:38)、无 eventId;信封无 eventId、事件名 game_play_start/game_play_end(props duration_ms,completed);ingest 仅 dispatchToMq() 空壳不落库(EventIngestServiceImpl.java:120-124);process_status 语义 0待聚合/1已聚合/2失败(V5.0.0:13,30);前端无 eventId、traceId 默认 ''。

解法(Codex C1:恢复 eventId;C2:定义 process_status 事务):

  • 恢复 eventId(每事件实例 UUID,幂等真身):events.schema.json + telemetry.yaml 信封加 eventId(必填);Flyway V10 给 game_telemetry_event 加 event_id VARCHAR(64) + 以 uk_event_id(event_id) 替换 uk_dedup(UUID 全局唯一,彻底解决"同毫秒同名误吞 + 重放重计数");前端 buildEnvelope 生成 eventId=crypto.randomUUID() + 真 traceId(每 play 会话一个)。
  • 同步写本地事务(替代 MQ):ingestBatch 在一个 @Transactional 内:逐信封 INSERT(uk_event_id 幂等)→仅"真插入"(affected>0)才累加 game_telemetry_game_stat(按 (game_id,stat_date) upsert)→ 算 quality_score → FeedApi.upsertRank(mode=QUALITY_REFRESH) 回灌 → 把该事件 process_status=1已聚合;任一步失败整体回滚(事件不计)。MQ producer/consumer 只留契约+TODO。

quality_score 口径:clamp(60×完玩率+20×like率+20×share率,0,100),完玩率=completed_count/play_end_count,写 game_telemetry_game_stat.quality_score(DECIMAL5,2);回灌 game_feed_rank.quality_score(DECIMAL10,4)=当日 stat(精度转换),sort_score=quality_score+boost。

AC:B 上报 game_play_start+game_play_end(duration_ms,completed=true)+like+share(带 eventId+真 traceId)→ game_telemetry_event 有原始行 → stat.quality_score 变大 → rank.sort_score 同步变大;重放同批次(同 eventId)不重复计数。


4. 三批次执行(涉及模块/文件/步骤/失败路径)

4.1 批次①契约/状态机校正(B1)

落 §2 全部前置项(含 host 取包统一走 request、Play.onTelemetry 白名单、eventId/traceId);落 §3.1 可见态读写 enforce、§3.2 发布编排(@Primary 本地事务 + 故障注入测试)、§3.3 version 创建+回填、§3.4 V10+PackageStore+manifest 端点、§3.5 V10 eventId+同步写。

  • 失败路径:包未就绪→拒发布并回滚;compliance block→不写库;feed 本体回填查不到 project→空态;编排任一步失败→全回滚。
  • done:前端真连 staging;submit→人工 APPROVE→项目 PUBLISHED(4)+包 1+版本 3+feed rank(status=1);feed/stream 读到它。

4.2 批次②最小数据闭环(B2)← 护城河种子,A/B AC 在此锁定

PackageFactory(aigc 桩→确定性产 clicker-plus 包)→ 落包(建 version+runtime_package+PackageStore 存 package_json,回填 aigc 任务)→ §4.1 发布编排入 feed → 通用 clicker-plus runtime 试玩。

  • runtime 必须真吐 game_play_start/game_play_end(duration_ms,completed)/like/share(带 eventId),经 Play.onTelemetry 白名单原样上报 → §3.5 同步写 → 算分 → 回灌。
  • 失败路径:取包失败→不静默退 demo 兜底掩盖(demo 兜底仅 mock 链路)。
  • done = 数据回路 A/B 验收协议通过(§6)。

4.3 批次③小卖部皮肤 + ≥10 种子(B3,可降级)

clicker-plus 配置出小卖部皮(点击售卖+极简库存)+ ≥10 参数化种子;撞 <15KB 预算先退纯 clicker-plus,数据回路 AC 不动。done:feed 有内容池(≥10)。


5. 接口/数据契约清单(contract-first,先改后写 —— 补全 Codex M11)

契约/产物 动作 位置
FeedApi + FeedRankUpsertReqDTO(含 mode:PUBLISH_BASELINE/QUALITY_REFRESH) + FeedApiImpl(@Primary 本地) 新增 game-module-feed-{api,server}
RuntimePackageApi.publish(versionId) + RuntimePackageApiImpl(@Primary 本地) 新增 game-module-runtime-{api,server}
ProjectVersionApi.createForPackage + GameVersionDO/Mapper/Service 新增 game-module-project-{api,server}
AigcTaskService.completeWithVersion(taskId,versionId)(幂等) 新增 game-module-aigc-server
PublishOrchestrationService 新增 game-module-project-server
pom 依赖:project-server→feed-api/runtime-api;telemetry-server→feed-api 新增 各 server pom.xml
错误码 PROJECT_PUBLISH_PACKAGE_NOT_READY(1-100 段) 新增 project-api ErrorCodeConstants
V10:game_runtime_package.package_json LONGTEXT + game_telemetry_event.event_id+uk_event_id(替换 uk_dedup) 新增(三处同步) contracts/db-schemas/V10.0.0__*.sql + 模块 + yudao-server
events.schema.json / telemetry.yaml 信封加 eventId(必填) 改 contracts/
runtime.yaml 新增 /manifest 端点(原样 JSON)+ manifestUrl 语义说明 改 contracts/api-schemas/runtime.yaml
Mapper 自定义 upsert SQL(stat、feed_rank 两套 mode) 新增 各 server Mapper
project.yaml α 审核响应说明 + admin review/index.vue 状态筛选/操作 改 contracts + game-admin
game-package.schema.json 不改(clicker-plus gameConfig 子 schema 作 templates/ 后续批) —

B1 实现守门(第二轮 Codex 确认轮残留,必守):① event_id VARCHAR(64) NOT NULL 再加 uk_event_id(MySQL 唯一键允许多 NULL,必须非空,否则幂等失效);② V10 只能有一个被 Flyway 实际执行的副本(yudao-server/.../db/migration/),勿在多个 classpath jar 放同版本 V10(一个 V10 文件同时 ALTER runtime+telemetry 两表 OK);③ FeedRankUpsertReqDTO.mode=QUALITY_REFRESH 只更 quality_score/sort_score,保留 boost/pinned/status;④ 新增 -api 包不得反向依赖 -server 或互嵌对方 DTO(防 Maven 成环;已核实现有方向 server→对方 api 无环)。


6. 验证(A/B 协议 + 通用判定 + 负路径)

A/B 协议(可执行):备同 zone 同 boost 的 A/B 两个已发布游戏(tenant_id=1)→记初始 feed 顺序→向 B 上报有效 game_play_start+game_play_end(duration_ms,completed=true)+like+share(带 eventId+真 traceId)→断言 game_telemetry_event 有原始行、stat.quality_score 变大、feed_rank.sort_score 同步变大→再请求 feed B 排到 A 前→重放同批次(同 eventId)不重复计数→关 mock 仍通过。 通用判定:不只看 200,须 code==0+结构符 yaml+非 mock 特征值;主 agent 独立复跑,不采信子代理自报。 负路径:错 token→401;停后端→不白屏;空数据→空态。故障注入:feed upsert 抛错→发布事务全回滚(§3.2)。 数据清理:固定 traceId/eventId 前缀 + 固定 gameId 清单 + cleanup SQL,验证前后表快照。

7. 回滚(4 独立单元,各自 commit)

① 前端联调(env/mock/token/tenant/beacon/取包统一/onTelemetry/eventId)|② 发布编排(PublishOrchestration+FeedApi/RuntimePackageApi @Primary+GameVersion+ProjectVersionApi+V10 package_json+PackageStore+manifest 端点)|③ telemetry 同步写+算分+回灌+V10 event_id |④ clicker-plus runtime+种子脚本。

8. 完成条件(B0→B3)

§6 A/B 协议通过(关 mock)= 本轮完成线;4 链路烟测登记总账;缺口 T+1 修复机制(标 owner/严重度/是否阻断/最晚日)。

9. 决策审计 + 双模型对账

项 v1 Codex 二审 v2 处置
C1 可见态 四条件谓词 H9:读路径未 enforce 读写双侧 enforce + 去 isGamePublished 默认放行
C2 编排 复用孤儿端点 + @Transactional H5:仅 @Primary 本地实现成立;H6:version 由 project 写 @Primary 硬约束 + 故障注入测试 + version 显式写
C3 version 落包建版本 H7:缺 aigc 回填闭环(studio 依赖) + completeWithVersion 回填,taskId 幂等
C4 package V10 package_json + PackageStore C4:manifest 端点须原样 JSON + 前端 baseURL resolve 端点不包 CommonResult + 前端 resolve + 取包统一走 request
C5 幂等 砍 eventId 复用 (trace_id,event,ts) C1:不成立(误吞/重计);C2:process_status 未定义 恢复 eventId+uk_event_id(推翻 v1)+ 同步写本地事务定义 process_status
前端联调 6 前置项 C3:host 取包绕 request;H8:Play 吞 like/share + 取包统一走 request + onTelemetry 白名单 + Feed traceId
D-PUB α 待点头 M10:改 APPROVED 语义,admin 连带 已拍板 α(2026-06-09)+ 连带改 admin/enum/project.yaml/单测
契约清单 §5 M11:缺实现类/错误码/pom/upsert 分流 §5 补全

评审轮次:① 主 agent Opus 子代理独立核验真实代码(5 critical 全实,C2/C5 有简化空间);② Codex 首轮(11 项 4C+5H+2M,全采纳,推翻 v1 C5 简化);③ Codex 第二轮确认轮(2026-06-09):Go,11 项 spec 层全闭合、无新设计矛盾,残留 4 守门见 §5 末。