docs(资产/观测): workflow 双产出 fable 终审通过(资产面范围决策设计 + 阶段四后续波 plan)
workflow wdfr57899 异步产出、fable 终审: - 资产面范围决策设计(①源工程存储+②素材市场都要):opus 调研→主笔→对抗/完整性/材料三视角评审→修。fable 终审:对抗评审 3 硬伤真修(孤儿覆盖断言据代码纠正+修法二选一+清扫job抬必交/assetRefs据实化+跨线前置/验收改字节等价硬门sha256)、关键事实亲验坐实(BackendStore save:482/fetch:574 真实现非占位、assetRefs/lineage 契约零命中、TIER2_STORE 部署零命中)。四段拆:SRC最优先/MAT-VERIFY并行/MARKET-READONLY排期/MARKET-TRADE后置(对齐数据飞轮§4)。待创始人评审§7六项。 - 阶段四观测后续波 plan(②game-cloud挂agent+actuator/③Python两线接OTLP/④三跳traceparent/⑤埋点+看板+告警):照已签设计施工、不触发双评审门;工单六要素齐、碰生产窗口(game-cloud单体②⑤/生成线③④)全文分开、验收引设计§8六条+脱敏第7条。待创始人定窗口§7六项。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ab1b7693e
commit
960198bd20
294
docs/agent-specs/2026-07-05-游戏资产管理范围决策-设计.md
Normal file
294
docs/agent-specs/2026-07-05-游戏资产管理范围决策-设计.md
Normal file
@ -0,0 +1,294 @@
|
||||
---
|
||||
date: 2026-07-05
|
||||
topic: 资产管理-范围决策
|
||||
status: 草案(fable 主笔 2026-07-05 · 三视角评审已修订 · fable 终审通过)—— 范围拆线与先后已由创始人 2026-07-05 拍板(见 §5)。本档在既定范围内补 ① 源工程存储 与 ② 素材市场的工程设计,已按对抗 / 完整性 / 材料三视角评审逐条修订:半落库孤儿失败模式据代码纠正、`fetch` 消费方边界划清、重建验收改字节等价硬门、公开货架补审核门、`assetRefs` 跨线依赖据实化、分期号对齐数据飞轮 §4。**fable 终审:对抗评审 3 硬伤(孤儿覆盖断言/assetRefs 已预留/验收弱化)确认真修,关键事实亲验坐实(BackendStore save:482/fetch:574 真实现非占位、assetRefs/lineage 契约零命中、TIER2_STORE 部署零命中)。** 仍待执行计划或创始人拍板的点集中在 §7,待创始人评审。
|
||||
sot-impact: 不新建 canonical topic。本档在既定范围内细化 `架构/生成引擎/数据飞轮` §3.2 资产层与 §4 分期的落地设计,不改其 canonical 命题;分期编号以数据飞轮 §4(阶段一溯源链 / 二市场只读半 / 三市场流通半 / 四收益回流)为权威,本档 ①②③④ 是其下的执行拆段、不另立编号真相(对照见 §5)。真正落地时将牵动 `后端/数据模型`(game_source_project / game_material / tier2_source_project_version 的治理口径)与 `架构/契约总览`(studio.yaml 新增货架端点、trade.yaml source 加 ASSET),届时各走 contract-first 与迁移治理。素材归因用的 `lineage` / `assetRefs` 字段在数据飞轮 §3.1 目前只是提案、未落 source-project.schema.json,只读半对它有跨线前置(见 §4.1 / §5)。不改 55 P0 验收定义,不擅自把 P-MAT 升 P0。
|
||||
上级: docs/architecture/架构/生成引擎/数据飞轮.md
|
||||
关联: docs/mvp/MVP作战清单.md:37(W-ASSET 四段) · docs/mvp/MVP进度总账.md:59(studio 素材库 minimal-real / 前端 mock) · docs/mvp/MVP进度总账.md:150(tier2 BackendStore 孤儿风险) · docs/architecture/架构/生成引擎/数据飞轮.md §3.2/§4/§6 · contracts/api-schemas/studio.yaml(R-MAT 三端点) · contracts/api-schemas/trade.yaml(source enum 无 ASSET) · game-cloud/huijing-server/src/main/resources/db/migration/V18.0.0__create_game_source_project.sql · contracts/db-schemas/V24.0.0__create_game_material.sql · tier2/gen-worker/worker/store.py · tier2/gen-worker/worker/run.py:904
|
||||
图清单: [图1 四类资产与四段拆线 · 图2 源工程存储双面架构 · 图3 素材市场数据流(只读半/流通半) · 图4 四段优先级与闸门依赖]
|
||||
---
|
||||
|
||||
# 游戏资产管理范围决策 · 设计
|
||||
|
||||
W-ASSET 这条作战线原本用「游戏资产管理」一个宽词兜住了太多东西。创始人在一次范围复核里把它揪了出来:「资产」底下其实躺着四类完全不同的东西——玩家点开就玩的运行包、生成出来能改能重建的源工程、创作者上传自用的素材、以及跨创作者能买卖分成的资产市场。它们的负责人、卡点、验收门互不相同。焊在一个完成线里,结果就是本可以马上补稳的源工程可靠性,被支付牌照、授权、分成、法务这些外部闸门一起拖住。
|
||||
|
||||
2026-07-05 创始人已就范围拍板:拆线、先做「源工程长期存储 + 私有素材库真验」、并允许资产市场只读半在支付接通前先上(拍板全文见 §5)。本档承接这个已定的范围,把要动手的两件补成工程设计——数据结构、入口、使用路径、失败模式、验收线。两件的成熟度不一样,别混谈:① 源工程存储是一套可以直接切执行计划的完整设计;② 素材市场分三层,私有库真验(第一层)也可直接切,只读半(第二层)只给出契约增量清单与跨线前置——它要等素材中心 P-MAT 把素材结构化、要等溯源链阶段一把 `assetRefs` 归因字段落进契约,详细 schema 是素材中心模块的交付物、由承接它的执行计划做,流通半(第三层)只标边界与外部闸门依赖。范围决策本身不再重开;要评审的是 §3、§4 这两套做法。
|
||||
|
||||
## 1. 这份设计要解决的问题
|
||||
|
||||
要同时推进 ① 和 ②,不是因为它们像,恰恰因为它们各撑护城河的不同一面。
|
||||
|
||||
**源工程存储撑的是生成这条主线的可靠性。** 一款游戏发布之后,它的源工程还能不能被找回来改、能不能按版本重建出同一个包——这是便宜档 A11 修改回路、tier2 富游戏第二次装载、以及「游戏 = 长生命周期项目」这个范式共同的地基。它也是数据飞轮收益回流语料里那一份源工件(`source_json`)的来源。地基塌了,上面所有「改一改、做个变体、按版本回滚」的能力都是空中楼阁。
|
||||
|
||||
**素材市场撑的才是护城河四层里的「资产」层。** 把创作者各自攒的私有素材,长成一个可检索、可复用、可流通、权利链清晰的资产库——这正是数据飞轮 SoT §3.2 写的那条「资产沉淀」飞轮。它是竞品搬不走的东西之一:模型可以追平,越积越厚的素材库和权利链不行。
|
||||
|
||||
一个是地基,一个是护城河,谁也替不了谁,所以都要。但它们受的约束天差地别:源工程存储的验收不碰钱、不碰授权、不碰法务,是纯工程可靠性问题,靠自己就能验完;素材市场一旦要「流通」,就撞上真实支付收单和 IP 分成口径这两道外部闸门,快不了。这个差异决定了后面 §5 的排序——不是优先级偏好,是物理约束。要说清的是,「① 能先交付」讲的是它的验收自足,不是它和素材资产无关:① 存的那份源工程 manifest,正是收益回流语料的源工件来源,也是素材归因 `assetRefs` 将来要寄生的地方(见 §4.1)。① 其实是回流飞轮和资产飞轮共同的底座,先做它恰恰因为两条飞轮都踩在它上面,而它自己不欠钱、不欠授权、不欠法务。
|
||||
|
||||
四类资产的边界先摆清楚,后面只碰中间两类:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
R["运行包资产<br/>GamePackage / engineBundle / runtime package"]:::built
|
||||
S["源工程资产<br/>src 多文件工程 / sourceProject / versionId"]:::now
|
||||
M["私有素材 → 资产市场<br/>game_material / 六类 / 货架 / 授权 / 分成"]:::now
|
||||
X["(同上,市场化的流通半)"]:::later
|
||||
|
||||
R -->|已支撑 feed 真玩与发布链| Done["维持现状,不在本次范围"]
|
||||
S -->|"① 本次要动"| SRC["W-ASSET-SRC<br/>长期存储 + 版本寻址 + 取回重建"]
|
||||
M -->|"② 本次要动"| MAT["W-ASSET-MAT-VERIFY + MARKET-READONLY<br/>私有库真验 + 支付前只读货架"]
|
||||
M -->|"② 后置(外部闸门)"| TRADE["W-ASSET-MARKET-TRADE<br/>购买 / 授权 / 分成"]
|
||||
|
||||
classDef built fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20;
|
||||
classDef now fill:#fff7e6,stroke:#d97706,color:#7c2d12;
|
||||
classDef later fill:#f5f3ff,stroke:#7c3aed,color:#4c1d95,stroke-dasharray:5 4;
|
||||
```
|
||||
|
||||
运行包资产已经在 runtime / project / feed 主线里运转,玩家能打开能玩,不是本次缺口,维持现状。本档只设计中间两类:源工程资产(§3)和素材资产 → 资产市场(§4)。
|
||||
|
||||
## 2. 现状与地基
|
||||
|
||||
设计要落在现状上,先把两条线现在库里有什么、缺什么核清楚。
|
||||
|
||||
### 2.1 源工程存储:两套并存,各服务一档
|
||||
|
||||
源工程存储今天是两套并存的面,各服务一档,彼此独立、没有统一寻址。
|
||||
|
||||
**便宜档 / Tier0-1 这一档,走 Java 的 `game_source_project` 表**(V18.0.0,已在生产 Flyway 迁移里落地)。源工程 JSON 直接落 DB 的 `source_json` LONGTEXT 列(M0 存储);`source_hash` 是 sha256 幂等键;`version_id` 在构建成功后回填,反查产物版本;`status` 是状态机(0 草稿 / 1 已构建 / 2 孤儿 / 3 已发布),`status=2` 就是构建失败留下的孤儿态,这套机制本身已经把「落库了但没建成包」这种半成品显式建模了;`base_version_id` 记同一款游戏内 modify 的血缘。三个索引各管一件事:`idx_source_hash` 去重、`idx_game` 查历史、`idx_version` 反查源。它现在真实支撑着便宜档 A11 的 base 源注入。表里留了一个 `source_url` 列(第 33 行),打算切对象存储后用,但 `DEFAULT ''` 至今空着——现在 DB 里的 `source_json` 才是权威。
|
||||
|
||||
**tier2 富游戏这一档,走 `tier2/gen-worker/worker/store.py`。** 这里有一个抽象接口 `SourceProjectStore`(`save`/`fetch`),和两个实现。`LocalFsStore`(157–319 行)是真能跑的实现:内容哈希寻址、幂等、留版本历史,落在 `GEN_DIR/_store/<id>/<versionId>/`,spike 期一直真用,`default_store()` 默认返回的就是它。落库入口是 `run.persist_source_project`(run.py:904–941):它是收口处的**额外**一步,GEN_DIR 下的 workdir 源文件仍照常留盘(现有 build / run_gates / smoke 全链路不受影响),这一步只是**再**把交付的源工程经 store 持久化一份;而且是 best-effort——落库失败只告警、不中断生成主链。
|
||||
|
||||
### 2.2 纠正一个过时前提:BackendStore 不是「占位」,是「真实现但没接线」
|
||||
|
||||
早前的口径(包括本决策的初稿)把 tier2 的 `BackendStore` 说成「占位、`raise NotImplementedError`」。这在今天的代码里已经不成立,得据实改。
|
||||
|
||||
`store.py` 第 351 行起的 `BackendStore` 是一份写完的真实现:`save`(482 行)把 manifest 写进 MySQL、把源文件全文逐个 `put` 进 MinIO;`fetch`(574 行)按 `(game_id, versionId)` 从两边拼回一个可重建的源工程;幂等键是 `(game_id, source_hash)`,版本寻址键是 `(game_id, version_id)`;MinIO 的 object key = `<game_id>/<versionId>/<工程内相对路径>`,bucket 默认 `tier2-src`。它连惰性 import、连接失败响亮抛、路径安全校验都写了。真正陈旧的只有模块头部第 12 行那句注释,它还停在「占位 `raise NotImplementedError`」的旧话上,和下面 351 行起的真实现自相矛盾。
|
||||
|
||||
所以缺口不是「没写」,而是另外三件事,这三件才是 W-ASSET-SRC 要收的:
|
||||
|
||||
- **没接线。** `default_store()` 只有在环境变量 `TIER2_STORE=backend` 时才返回 `BackendStore`。全仓 grep 这个开关,命中的全是 store.py 自己的定义与文档、以及凭据档里一句「怎么切」的说明,**没有任何一个 yaml / sh / env 部署配置真把它打开**。生产默认仍然走本地 `LocalFsStore`。
|
||||
- **没真跑证据。** 进度总账第 150 行明记:BackendStore 落库只有 schema 形状、缺真跑证据、有孤儿风险。「按 versionId 取回 base 源 / 取回重建」这条端到端从来没在真 MySQL + 真 MinIO 上跑通过。
|
||||
- **建表游离在 Flyway 之外。** `tier2_source_project_version` 这张 manifest 表,是 store.py 里的 `CREATE TABLE IF NOT EXISTS`(334–348 行)在首次落库时自建的,不是一条 Flyway 迁移。它不受迁移治理,schema 变更没有版本、没有回滚补偿迁移的位置。
|
||||
|
||||
一句话:2026-07-05 决策稿说 BackendStore「占位」,在代码层不精确,但在「它不是已接生产的后端」这一点上,实质是对的。
|
||||
|
||||
### 2.3 素材侧:只有私有库,市场化能力为零
|
||||
|
||||
素材这条线,今天只有创作者私有素材库,资产市场(货架 / 授权 / 分成)一样没有。
|
||||
|
||||
`game_material` 表(V24.0.0,`contracts/db-schemas` 与 huijing-server 迁移同源,已生产落地)的列是:`creator_user_id`(归属隔离,创作者只见、只用自己的)、`category`(六类冻结:sprite / character / effect / scene / ui / music,由 `MaterialCategoryEnum` 四处同引)、`name` / `ref` / `url` / `provider`(默认 mmx-cli)/ `size_bytes` / `mime_type`,加 Yudao 审计列。只有一个索引 `idx_owner_cat(creator_user_id, category, id)` 供归属隔离浏览。字节存 infra 的 FileApi,本表只登记 ref/url。
|
||||
|
||||
Service 是 `StudioMaterialServiceImpl`:`register`(71 行,按引用登记,六类校验 + 声明 MIME 与类目族一致性自检,无字节所以单条 insert 不加事务)、`browse`(104 行,归属隔离,mapper 内 `creatorUserId` eq 硬过滤)、`selectIntoDraft`(116 行,悲观锁 `FOR UPDATE` 锁会话行 + DRAFT-only 守卫,把选中素材按 ref 去重合并进草稿 `assetContext`)。端点是 studio.yaml 的三个:`POST /app-api/studio/asset`、`GET /app-api/studio/asset/browse`、`POST /app-api/studio/asset/select`。单测 15/0/0。
|
||||
|
||||
缺的是整个市场化的那一半。`game_material` 没有货架、没有公开可见范围、没有授权类型、没有锁风标签、没有分成规则(SQL 列缺项,grep studio 模块 Java 的 shelf / 货架 / 授权 / 分成 / marketplace 全空)。trade 的收入来源枚举只有 `[1, 2]`(1 广告 / 2 打赏,trade.yaml:55/200/416 三处一致),没有 ASSET;没有采购单 / 授权单表;支付收单没接进 game 业务(只有 `PayWalletApi` 的余额加减,不是收单)。前端 game-studio 的素材中心页当前是 build/mock,没在 staging 真验过——这是「前端假绿」的风险:build 能过只证明代码能构建,不证明已接真后端。产品需求 P-MAT-01..06 全是 P1。所以素材市场目前只是设计立位,实现未建。
|
||||
|
||||
## 3. ① 源工程存储设计
|
||||
|
||||
源工程存储要解决的问题很具体:一款游戏发布之后,它的源工程还能不能被稳定找回来改、能不能按版本重建出同一个包。今天便宜档这一面(`game_source_project`)已经在生产真用,富游戏这一面(`BackendStore`)真实现已写好、却没接线也没真跑过。W-ASSET-SRC 就是把富游戏这一面从「写好但没通电」推到「接生产、有真跑证据、schema 受治理」,同时把两面的边界划清楚。
|
||||
|
||||
### 3.1 设计立场:两面各管一档,统一在寻址语义而非合表
|
||||
|
||||
一个容易犯的错是「既然有两套源工程存储,那就并成一张统一资产表」。不做。两面服务的档不同、存储模型也不同:便宜档源工程小、直接 DB LONGTEXT 就够,和 Java 事务、A11 血缘紧耦合;富游戏源工程是多文件工程,manifest 落库 + 全文落对象存储才合理,而且 tier2 是一个独立 Python service,`forbidden-import` 守着它不实连 game-cloud 后端。硬把两者并表,等于给一次性收益造一层长期抽象,违反「不为一次性代码建长期抽象」和「不留孤儿设计」两条红线。
|
||||
|
||||
真正需要统一的,是**寻址语义**,而不是物理表。两面其实已经在说同一套寻址三元组:便宜档有 `game_id / version_id / source_hash`,富游戏 manifest 表也有 `game_id / version_id / source_hash`。所以「统一」落在契约层——两面对外都用 `{ id(即 gameId), versionId, sourceHash }` 这个三元组做寻址键,谁调用谁都按这个三元组存取。这是最小改动、不制造孤儿抽象的做法。
|
||||
|
||||
这里要顺带把一层耦合讲透,别等溯源链落地时才发现撞车:两面的 manifest 都要能容忍将来 additive 进来的 `lineage` / `assetRefs` 字段(§4.1)。好在这层耦合是无害的——便宜档的 `source_json` 和富游戏的 `manifest_json` 都是不透明 JSON 列,新增一个可选字段不破存量;而按版本重建只读 `fileTree` 里的文件内容、根本不碰 `lineage`,所以字节等价重建这条核心保证不受溯源链改动影响。① 存的是骨架,`lineage` 是骨架上后贴的一张归因便签,两者互不干涉。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph CHEAP["便宜档 / Tier0-1(Java · game-cloud studio)"]
|
||||
J1["game_source_project 表(V18 · 已生产)<br/>source_json LONGTEXT · source_hash 幂等<br/>version_id 回填 · status 0草稿/1已构建/2孤儿/3已发布<br/>base_version_id 同款内 modify 血缘"]
|
||||
end
|
||||
subgraph RICH["tier2 富游戏(Python service · gen-worker)"]
|
||||
T1["run.persist_source_project 收口<br/>additive · best-effort"]
|
||||
T2["LocalFsStore(已生产默认)<br/>_store/id/versionId/ 落盘 · 内容哈希寻址"]
|
||||
T3["BackendStore(真实现 · 未接线)<br/>MySQL manifest = tier2_source_project_version<br/>+ MinIO bucket=tier2-src key=game_id/versionId/路径"]
|
||||
T1 --> T2
|
||||
T1 -. 只有 TIER2_STORE=backend 才走 .-> T3
|
||||
end
|
||||
ADDR["共同寻址语义:{ id=gameId, versionId, sourceHash }<br/>两面各自实现,不合表"]
|
||||
J1 --- ADDR
|
||||
T3 --- ADDR
|
||||
style T3 stroke-dasharray:5 4
|
||||
```
|
||||
|
||||
### 3.2 把 BackendStore 推到真接生产的三步
|
||||
|
||||
**第一步,接线激活。** 按 AGENTS §6 条款 9(内网决策进配置、不散落硬编码),把 `TIER2_STORE=backend` 放进 tier2 service 的部署配置(`tier2/config/infra.yaml` 的 store 段或部署 env 单点),让 `default_store()` 在生产返回 `BackendStore`,同时 MinIO / MySQL 连接参数从 `infra.yaml` 现取现连。切换只在 `default_store()` 这一处,主链和测试都经这个工厂取实现,不在别处硬编码具体类——这已经是现有代码的形态,只是把开关真打开。`LocalFsStore` 不动,6c6g 本地往返自检仍用它。
|
||||
|
||||
**第二步,给那张自建表定治理归属。** `tier2_source_project_version` 现在靠 `CREATE TABLE IF NOT EXISTS` 自建。这里有一个要在执行计划里拍死的选择:是把它抬成一条 Flyway 迁移,还是把这份 in-code DDL 正式登记成「tier2 自有、Flyway 之外」的受治理 schema。判断的关键是 tier2 用的是不是 huijing 那套 MySQL——`infra.yaml` 默认 `database='tier2'`,是一个独立逻辑库,而 game-cloud 的 Flyway 治的是 huijing 库。若确实是独立库,把它塞进 game-cloud 的 Flyway 是跨库的类别错误;更合适的是把 store.py 里那份 DDL 与「另有一份同源 `.sql`」(docstring 已提及 `config/schema/`)对齐成单一真相,登记为 tier2 owner 的受治理 schema,并在 doc↔code 之间立一个断言点(表名 `tier2_source_project_version`、幂等键、object key 前缀是契约点)。要说清对齐面在哪一侧:这张表和它的 object key 是 tier2 这个 Python service 自己的落库 / 取回口径(store.py 的 `save` / `fetch`),`forbidden-import` 守着 tier2 不实连 game-cloud、Java 也不跨读这张表(全仓 grep 确无 Java 引用它)。所以契约对齐的三处是凭据档、store.py 落库、store.py 取回,都在 Python 侧;便宜档 Java 那一面走的是它自己的 `game_source_project`,与这张表是两套并存的存储面、不交叉取回。无论走哪条,底线是:这张表不能继续游离——schema 变更要有版本、有回滚位置。**这一步的最终定夺留给执行计划**,因为它取决于「tier2 与 huijing 是否共库」这个部署事实,本档先标出选择与判据。
|
||||
|
||||
**第三步,补取回重建的真跑证据。** 这是 W-ASSET-SRC 的验收核心,也是今天最大的空白。要在真 MySQL + 真 MinIO(mini-infra / mini-desktop 窗口)上跑通一条端到端 harness,它同时就是本线几条核心断言的落点(下面 §3.3 提到的「缺源显式失败」「半落库不留隐形孤儿」都在这个 harness 里验,不悬空):
|
||||
|
||||
- **确定性重建**:`save` 一份富游戏源工程 → `fetch` 按 versionId 取回 → 把取回的 `fileTree[].content` 重新 esbuild 构建 → 断言重建产物与原 bundle 一致。一致的判据要硬:首选重建 bundle 的 sha256 与原 bundle 相等;若 esbuild 产物在某些环境下非字节稳定,则退到 `buildInputHash`(= sha256(sourceHash + buildProfile),契约里已有此字段)相等、且重跑九门 verdict 完全一致、无任何 `missingContent`。不接受「能进构建就算过」这类降级——那放得过一个缺文件的残缺取回,而残缺取回根本不是同一个包。
|
||||
- **版本寻址**:改一处源、重建,断言落一个**新 versionId**、旧版本仍可按旧 versionId 取回。
|
||||
- **幂等**:拿同一份源重投,断言幂等命中 `(game_id, source_hash)`、不落第二个版本。
|
||||
- **缺源显式失败**:人为让某文件在 MinIO 缺失(`fetch` 会把该文件项标 `missingContent`),断言重建步骤据此**显式失败**、报「无法重建此版本」,绝不静默退回全量重生成。
|
||||
- **半落库不留隐形孤儿**:模拟 MinIO 写成功而 MySQL 写失败,断言按 §3.3 选定的方案不产生「有对象、无 manifest 指向」的隐形残片,且清扫 job 能把它认出来回收。
|
||||
|
||||
跑通并留下日志,才算这条存储线真的「通电」了。
|
||||
|
||||
### 3.3 入口 / 使用路径 / 失败模式 / 验收
|
||||
|
||||
**入口。** 落库入口是 tier2 收口的 `run.persist_source_project`(已在,additive、best-effort),它经 `default_store()` 取实现,`save` 有真实生产接线。取回入口是 `BackendStore.fetch(game_id, versionId)`——这里要诚实:`fetch` 今天全仓没有一个生产调用点(grep `.fetch(` 零命中),它设计上的消费方「tier2 富游戏第二次装载(modify / 重建)」这个入口还没建。所以 W-ASSET-SRC 交付的是 `save` 接生产 + `fetch` 由第三步 harness 的 save→fetch→rebuild 闭环验证;`fetch` 的**生产消费方**(tier2 second-load)是另立的后续线,本档只声明它、不在本线接线。别把 `fetch` 当成已有生产消费的接口。便宜档这一面的入口是 studio 的 create/modify/extend 落 `game_source_project`,取回走 `base_version_id` / `version_id` 查历史,这套已在生产——注意它走的是便宜档自己的 Java 表,不经 `BackendStore`,和富游戏取回是两条独立路。
|
||||
|
||||
**使用路径。** 便宜档:A11 修改回路按 `base_version_id` 拿 base 源注入,已真用(Java 侧、`game_source_project`)。富游戏:一款游戏首次生成收口时 `save` 落 manifest + 全文;将来要改、要重建、要做变体时,由 tier2 second-load 入口按 `(game_id, versionId)` `fetch` 回来,拿到含各文件 content 的源工程重新构建或重跑九门——这条消费路是后续线,W-ASSET-SRC 先把 `save`/`fetch` 这对底座连通、用 harness 坐实取回可重建。两面都遵「改源不改产物」——源工程落源侧存储面,构建产物仍存 `game_version` + `game_runtime_package`,GamePackage 产物 schema 不动。
|
||||
|
||||
**失败模式。** 落库失败已经是 best-effort:`persist_source_project` try/except 兜住,产物仍在 GEN_DIR workdir,只告警不中断主链——生成这条主线不会因为归档失败而断。
|
||||
|
||||
真正要正视、且当前代码有真实隐患的是**半落库的孤儿**。`BackendStore.save` 的顺序是:按 `(game_id, source_hash)` 查幂等 → 未命中则派生 versionId → 先把文件 put 进 MinIO → 再写 MySQL 一行。这里有个不能忽略的事实:versionId = `v{秒级时间戳}-{hash12}`,时间戳是收口那一刻 `time.time()` 现取的(`studio.py` 收口处 `now_ts=time.time()`)。于是当 MinIO 写成功、MySQL insert 失败时,那一行从没落库,下次重投(重新生成)`now_ts` 变了 → versionId 变了 → MinIO object key 的前缀 `game_id/{新versionId}/` 也变了 → 上一次写进旧前缀的那批对象**不会被覆盖、永久留成没有 manifest 指向的隐形残片**。所以孤儿不是「只有永久失败才产生」,而是**每一次 MySQL-after-MinIO 的瞬时失败都产生**;初稿里「重投同 key 覆盖写、重试自愈」的说法与代码相反,据此纠正。
|
||||
|
||||
修法要在 W-ASSET-SRC 执行计划里定死一条,不能留在正文当推理。**推荐先写 manifest 再写对象**:`save` 改成先在 MySQL 落一行 `status=pending` 的 manifest、再 put MinIO、成功后把 `status` 翻 `committed`;幂等查询改成认 `(game_id, source_hash)` 不分状态——命中 `pending` 就复用该行已定的 versionId 续 put(同前缀真覆盖,才是真自愈)、命中 `committed` 直接返回。这样崩溃留下的是一条**可见**的 pending 行(`fetch` 本就靠 `missingContent` 容忍残缺),而不是无处可查的隐形对象;孤儿就等于 pending 超时行,清扫 job 按 `status` 认得出、按行里的 `(game_id, versionId)` 拼出前缀删得掉。代价是 manifest 表加一个 `status` 列。**备选**是让 versionId 只由 `source_hash` 派生(去掉时间戳,版本时序改靠 `created_at` + `idx_game_created` 兜),重投复用同前缀真覆盖——但它动的是 `derive_version_id` 这个和 `LocalFsStore` 共享的代码契约,改动面更大。无论走哪条,**孤儿清扫 job 都要抬成本线的必交项**:触发条件(pending 超时行,或有对象前缀却无 committed manifest 行)、调度、owner、一条可验收断言,一并进 §6,不再只在括号里带一句。方案 pick 留执行计划据代码影响面拍(见 §7)。
|
||||
|
||||
取回侧的失败要显式:`fetch` 命中 manifest 但某文件在 MinIO 缺失时,该文件项标 `missingContent`、不崩;但**重建必须据此显式失败**——缺源就报「无法重建此版本」,绝不静默退回全量重生成(静默重生成会悄悄丢掉这一版的真实源工程)。这条守卫的落点在上面第三步的重建 harness(它专门验一次缺文件重建显式失败),不是靠某个还没建的生产消费方兜。便宜档侧的孤儿由 `status=2` 显式建模,已在。
|
||||
|
||||
**验收线(W-ASSET-SRC)。**
|
||||
|
||||
- 生成成功后有源工程寻址结果,能关联到 `gameId / versionId / sourceHash`。
|
||||
- 按 `versionId` 能取回 base 源工程;改源重建落新 `versionId`,旧版本仍可取回;缺源时(某文件 `missingContent`)重建显式失败、不静默全量重生成。
|
||||
- 取回的源工程能确定性重建:重建 bundle 的 sha256 与原 bundle 相等(esbuild 非字节稳时退到 `buildInputHash` 相等 + 重跑九门 verdict 完全一致 + 无任何 `missingContent`)。不接受「能进构建/进九门就算过」这种降级。
|
||||
- 幂等重投不重复制造源版本:同 `game_id` + 同 `source_hash` 复用既有记录。
|
||||
- `TIER2_STORE=backend` 在生产真接线,真 MySQL + 真 MinIO 往返有跑通日志(不是本地 LocalFsStore 假通过)。
|
||||
- `tier2_source_project_version` 的 schema 治理归属已定(Flyway 迁移或登记为受治理的 tier2 自有 schema),不再游离。
|
||||
- 源落库失败不伪造成功,日志能追到 `gameId`、`versionId / sourceHash`、失败阶段。
|
||||
- 半落库不留隐形孤儿:按选定方案(manifest-first 的 pending 行 + 按 `status` 回收,或 versionId 复用同前缀真覆盖),模拟 MinIO 成功而 MySQL 失败后,没有「有对象、无 manifest 指向」的残片;孤儿清扫 job 有 owner、有触发条件、有一条真跑过的回收断言。
|
||||
|
||||
## 4. ② 素材市场设计
|
||||
|
||||
素材市场的终点,是数据飞轮 SoT §3.2 写的那句话:把创作者的私有素材库,升级成「可检索、可复用、可流通、权利链清晰的资产层」。这一节的设计严格落在那条 SoT 划的边界里,不越位替素材中心和 trade 做它们的活。
|
||||
|
||||
### 4.1 边界:生成侧只有两件薄活,市场横跨三模块
|
||||
|
||||
数据飞轮 SoT 把职责划得很清楚,照搬不改:**生成侧只做两件,都很薄。** 第一,把创作者选用的素材(无论私有库的还是市场买的)透传进生成上下文——复用 P-CRT-04 附件驱动创作那条已有通道(就是 `selectIntoDraft` 写进草稿 `assetContext`、create/modify/extend 接受 `assetContext` 输入的现成链路)。第二,在源项目血缘的 `assetRefs` 里记下本款游戏复用了哪些 `materialId`。生成侧**不做**素材的授权类型管理、不做货架检索、不做采购支付——那些是素材中心和 trade 的活。
|
||||
|
||||
这里有一个必须说清、否则阶段二会误判的点:`assetRefs` 不是「同款创作」的自然副产物。选不选素材,走的是 `assetContext` 这条正交通道,跟是不是同款无关。所以素材复用归因这份数据,得等创作者真的走了选素材通道才产生;溯源链攒下的是血缘本身,不是分成依据。别让流通半以为只读半会自动把「谁用了谁的素材」攒好。
|
||||
|
||||
还有一处契约归宿没定,得在切只读半前拍掉,否则字段无处安放:数据飞轮 §3.1 把 `assetRefs` 嵌在拟新增的 `lineage` 顶层对象里,而 `lineage` 按设计只在同款(remix)生成时才挂载、原创生成不带它。可 `assetRefs` 讲的是「本款用了哪些市场素材」,这件事和是不是同款无关——一个**原创但用了市场素材**的作品照样要记 `assetRefs`,它却没有 `lineage` 对象可挂。所以 `assetRefs` 到底是继续嵌在 `lineage` 里、还是抬成源 schema 的独立顶层字段(或独立关联表),是个悬着的契约选择,要和溯源链阶段一一并定(列入 §7 待拍板)。
|
||||
|
||||
整条线横跨三个模块加一道支付硬墙:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph STUDIO["studio 模块(登记 / 透传 / 归因)—— 生成侧只这两件薄活"]
|
||||
S1["game_material 私有六类库(V24 · 已生产)<br/>register / browse / selectIntoDraft"]
|
||||
S2["assetContext 透传生成上下文(复用 P-CRT-04)"]
|
||||
S3["源项目血缘 assetRefs 记 materialId<br/>(需选素材通道产生,非同款顺带)"]
|
||||
end
|
||||
subgraph MAT["素材中心 P-MAT(结构化 · 硬前置)"]
|
||||
M1["game_material additive 列<br/>+可见范围状态机(私有→待审→公开/驳回)<br/>+授权类型 +锁风标签 +分成规则"]
|
||||
M0["素材审核门(compliance/ip)<br/>锁风标签由审核写 · 只有过审进公开货架"]
|
||||
M2["跨创作者公开货架浏览 / 按 IP·类目检索<br/>服务端只列 可见范围=公开 且 锁风通过"]
|
||||
M1 --> M0 --> M2
|
||||
end
|
||||
subgraph TRADE["trade 模块(钱 · 流通半)"]
|
||||
R1["收入来源加 ASSET 枚举<br/>source_ref = 采购单 id · uk_source 幂等"]
|
||||
R2["采购单 / 授权单表 + 素材作者 share_rate 分成"]
|
||||
end
|
||||
PAY["真实支付收单(② 充值收单未接 · 日历闸门 / 二清)"]:::gate
|
||||
S1 --> S2 --> S3
|
||||
M2 -->|"只读半:免费 / 本人自有素材选用"| S2
|
||||
M2 -->|"流通半:付费采购 / 他人素材授权"| R1 --> R2
|
||||
PAY -. 硬前置 .-> R1
|
||||
classDef gate fill:#fee,stroke:#c33,color:#900;
|
||||
class PAY gate
|
||||
```
|
||||
|
||||
### 4.2 分三层落:私有库真验 → 只读半 → 流通半
|
||||
|
||||
**第一层,私有库 staging 真验(W-ASSET-MAT-VERIFY)。** 已有的 `game_material` 最小链路今天是 build/mock/单测,要把它推到 staging 真实可走:infra 上传字节 → studio `register` 登记引用 → `browse` 浏览本人素材 → `selectIntoDraft` 选用进草稿 → create/modify/extend 携带 `assetContext` → 生成侧能在 trace 或 sourceProject 里留下可追踪引用。这一层范围小、和 W-ASSET-SRC 并行、互不阻塞、不新增市场化能力;但它未必全是「跑通已有骨架」——最后一环「生成侧真消费 `assetContext` 并回填归因」当前没有证据,要先核实(见 §4.3 验收与 §5),核实为缺就补这段接线。
|
||||
|
||||
**第二层,资产市场只读半(W-ASSET-MARKET-READONLY,创始人已定支付前可先上)。** 这一层要动素材契约,但只动「能浏览、能选用、暂不能买卖」的那半。它有两条跨线前置,不是 studio 单独能闭合的(见 §5):素材结构化归素材中心 P-MAT,`assetRefs` 归因字段归溯源链阶段一。具体做法:
|
||||
|
||||
- **P-MAT 契约增量**,在 `game_material` 上加 additive 列:授权类型(原创自有 / UGC 孵化 / 授权 IP)、锁风标签、分成规则占位(素材作者分成比例,先留字段不结算),以及一个**可见范围状态机**——不是简单的私有 / 公开二值,而是「私有 → 待审 → 公开 / 驳回」,因为素材一旦要上跨创作者的公开货架,就得先过一道内容 / 版权审核(对比 `game_source_project` 有 `status`、`game_lineage_edge` 有 pending/active/voided,素材转公开同样要有审核态,不能一步从私有翻公开)。additive 不破存量私有库。这些列的详细 schema 是素材中心模块的活,但**不能悬空引用一个不存在的 spec**:在承接它的素材中心设计产物出现前,这份 additive 列 schema 由 W-ASSET-MARKET-READONLY 的执行计划作为 contract-first 交付物先落,owner 记素材中心。
|
||||
- **素材审核门**:公开货架是 UGC,不能无审核直接上。可见范围从「待审」到「公开」要过 compliance/ip 的审核门;锁风标签由审核流程写(不是随便哪个写入方能填的孤儿字段),服务端货架端点按「可见范围=公开 且 锁风通过」硬过滤,只有过审素材才进公开货架。审核门本身的流程与判据归 compliance/ip + 素材中心,是只读半的硬前置。
|
||||
- **跨创作者货架浏览**:新增一个不做归属隔离的公开货架浏览端点(区别于现有 `browse` 的 `creatorUserId` 硬过滤),按 category / IP 检索——但服务端只列「可见范围=公开 且 过审」的素材,归属隔离在这条路上换成「审核态过滤」,不是取消过滤。
|
||||
- **免费 / 本人自有素材选用**:`selectIntoDraft` 扩展成允许把公开货架里**免费的**、或**本人自己上传的**素材选进草稿透传生成。这里要收窄一个初稿含糊的口径:「对他人素材已购授权后再选用」依赖一条授权成交记录来判定,而那条记录(采购单 / 授权单)排在流通半,只读半没有数据能在服务端判「这个人对他人这份素材有没有授权」。所以只读半的选用只认「免费」和「本人自有」这两种服务端判得清的情形,「已购授权他人素材」明确划到流通半。
|
||||
- **assetRefs 归因(带跨线前置)**:创作者选用市场素材时,把 `materialId` 记进源项目血缘的 `assetRefs`。这里要据实说清:这个字段**契约里还没有**——`source-project.schema.json` 顶层目前没有 `lineage`、也没有 `assetRefs`(grep 零命中),`game_lineage_edge` 表也没建;它们是数据飞轮 §3.1「溯源链阶段一」的 contract-first 待办,不是「已预留」的既成事实。所以只读半要记 `assetRefs`,硬依赖溯源链阶段一先把 `lineage` / `assetRefs` 落进 schema 与落库链(或单独把 `assetRefs` 抬成顶层字段,见 §4.1 的归宿选择)。这条前置要在 §5 显式登记,别当成只读半自己范围内的活。
|
||||
- **边界拍死**(创始人 2026-07-05):这个中间态只能做「能浏览、能选用、暂不能买卖」,不接购买、授权支付、分成。
|
||||
|
||||
**第三层,流通半(W-ASSET-MARKET-TRADE,后置,被外部闸门物理卡住)。** 这一层带钱,前置齐了才能启动:
|
||||
|
||||
- **trade 收入来源加 ASSET 枚举**:`trade.yaml` 的 source enum 从 `[1, 2]` 扩到含 ASSET,`game_trade_income.source` 注释同步;`source_ref` 指采购单 id,素材作者按 `share_rate` 入账,`uk_source(source, source_ref, tenant)` 天然防重复分账。账户、提现整套现成,不重建。
|
||||
- **采购单 / 授权单表(新建)**:`game_material` 只是私有登记,没有「谁买了谁的素材、授权范围多大、单价多少」的成交记录。新建采购单表(买家 userId / 素材 materialId / 素材作者 / 成交价 / 授权范围 / 状态)作成交凭证,它的 id 就是上面入账的 `source_ref`;要有自己的幂等键(同一买家对同一素材的同一次采购不重复扣款)。授权关系并进采购单还是单列授权单表,由素材中心模块定。
|
||||
- **退款 / 撤权口径**:素材下架、版权争议、买家退款时,已入账的分成怎么冲、授权怎么撤,是带钱的逆向流程,口径要对齐 trade 的冲正机制(不是简单删行)。这条挂律所与 trade,本档只标出缺口。
|
||||
- **硬前置:真实支付收单,且要分清三条线别混。** ① 钱包余额 API(`PayWalletApi.addWalletBalance/getOrCreateWallet`,已有,但只是给钱包加减余额,不产生真实现金流入);② 支付订单充值收单 API(真实微信 / 支付宝把钱打进来,**当前没接进 game 业务**);③ trade 入账(分账记账,上面那组)。今天只有 ①③ 有地基,② 是断的,而流通半依赖 ②——② 受日历闸门(牌照 / 二清)约束,不是写几行代码能解决的。所以流通半**物理上排在支付真实化之后**,这是外部闸门决定的,不是工程优先级选择。
|
||||
|
||||
### 4.3 入口 / 使用路径 / 失败模式 / 验收
|
||||
|
||||
**入口。** 私有库:studio.yaml 现有三端点 `POST /app-api/studio/asset`(register)、`GET .../asset/browse`、`POST .../asset/select`。只读半:新增货架浏览端点、公开选用端点(additive,老调用不受影响)。流通半:新增采购端点,接 trade 入账。
|
||||
|
||||
**使用路径。** 创作者上传 → `register` 登记私有 →(只读半)提交转公开、进「待审」→ 过 compliance/ip 审核 → 「公开」上货架(未过审驳回,不进货架)→ 他人 `browse` 货架 → 免费 / 本人自有素材 `select` 进草稿 → `assetContext` 透传生成 → 血缘 `assetRefs` 归因(依赖溯源链阶段一字段就绪)。(流通半)付费采购 / 他人素材授权 → 采购单落成交凭证 → trade 按 ASSET 入账 → 素材作者 `share_rate` 分成到余额。
|
||||
|
||||
**失败模式。** `register` 无字节、单条 insert(不加事务),字节由共享 infra 上传;若 infra 上传成功而 register 失败,留孤儿文件,补偿口径已就近记录(进度总账:59)。归属隔离、货架可见范围、审核态一律服务端强制——前端不是可信边界。`selectIntoDraft` 用悲观锁 `FOR UPDATE` 串行化并发选用的「读既有→合并→回写」、DRAFT-only 守卫挡对已提交 / 已生成会话的污染,这两道已在。六类枚举、MIME 错族在服务端拒。
|
||||
|
||||
只读半引入一个私有库没有的**时序失败**要正视:市场素材是别人的,创作者把它 `select` 进草稿(草稿 `assetContext` 里存的是 ref 快照)之后、真正生成之前,原作者可能把素材下架、把可见范围翻回私有、或(流通半)撤掉授权。草稿于是攥着一个悬空或失权的 ref。口径:`assetContext` 存 ref 快照,但生成侧装配素材那一刻要对市场 ref **重解析**——复核可见范围仍为公开、审核仍有效、(流通半)授权仍在,失效的显式剔除并提示创作者(或整单拒绝),绝不拿一个已下架 / 已撤权的素材硬装进生成。私有库素材是本人的,不涉此时序缺口。
|
||||
|
||||
流通半:`uk_source` 幂等防重复分账,退款走冲正不删行,账户恒等式 `balance + frozen + total_withdraw = total_income` 不破。
|
||||
|
||||
**验收线。**
|
||||
|
||||
- **W-ASSET-MAT-VERIFY**:登录用户只能浏览、选用自己的素材(服务端归属校验是可信边界);infra 上传成功后 studio 只登记 ref/url/元数据、不重持字节;六类枚举只允许 sprite/character/effect/scene/ui/music,MIME 错族服务端拒;选用只能写本人 DRAFT 会话,非草稿态拒;`assetContext` 能随 create/modify/extend 进入生成侧,生成侧留下可追踪引用。整条 upload→register→browse→select→进生成上下文在 staging 跑通(不是 mock 页面假绿)。**开工前先核实一件事、别按「零新增能力」排期**:`assetContext` 目前只见于 Java 编排层 DTO,生成 worker(cheap-worker / tier2)侧 grep 无 `assetContext` / `materialId` 消费,Java 执行器自己也注明「assetContext 仍 transient(引擎线后用)」。也就是说素材大概率还没真流进生成、更没回填归因。若核实确实如此,「生成侧消费 `assetContext` + 把用到的 `materialId` 回填进 trace / sourceProject」要显式列入②的范围与验收,不能当作「跑通已有骨架」白捡(类比 A11 便宜档 cheap-worker 当初不认 modify 的同类缺口)。
|
||||
- **W-ASSET-MARKET-READONLY 进入条件**:P-MAT 契约补齐可见范围状态机(私有 / 待审 / 公开 / 驳回)、授权类型、锁风标签、分成规则占位;**素材审核门就绪**(compliance/ip 的内容 / 版权审核流程 + 锁风标签写入者 + 服务端「过审才进公开货架」的强制点);**溯源链阶段一的 `lineage` / `assetRefs` 契约已落 schema 与落库链**(这是 `assetRefs` 归因的硬前置,不落它这条验收无处记);产品接受「只读 / 免费选用」中间态、明确它不代表可交易市场。验收:跨创作者货架只列过审的公开素材、可按 category/IP 检索;免费 / 本人自有素材选用进草稿并透传;选用后素材失效时生成侧重解析剔除;血缘 `assetRefs` 正确记录(在其字段落地之后)。
|
||||
- **W-ASSET-MARKET-TRADE 进入条件**:真实支付收单接进 game 业务;trade 增加 ASSET 收入来源与采购 / 授权幂等凭据;法务确认素材授权、IP 派生、商用分成口径。验收:采购走 trade 入账、素材作者按分成比例收到余额、`uk_source` 幂等不重复分账、账户恒等式不破;退款 / 撤权走 trade 冲正、不删行、账户恒等式不破(详细逆向口径随 MARKET-TRADE 执行计划对齐律所 + trade,此处先立占位锚点,别在后续单里被「已标缺口」当成已交代而漏掉)。
|
||||
|
||||
## 5. 范围与优先级
|
||||
|
||||
创始人 2026-07-05 已就范围拍板三项:① 同意把 W-ASSET 拆成源工程存储与素材市场两条线;② 同意下一步做「源工程长期存储 + 私有素材库真验」;③ 同意支付接通前先做资产市场只读半。据此拆成四段执行:
|
||||
|
||||
| 段 | 内容 | 排序 | 卡点 |
|
||||
|---|---|---|---|
|
||||
| **W-ASSET-SRC** | 源工程长期存储 / 版本寻址 / 取回重建 | **最优先** | 无外部闸门,验收纯工程可自闭合 |
|
||||
| **W-ASSET-MAT-VERIFY** | 私有素材库 staging 真验 | 与 SRC 并行 | 范围小、可验;先核实生成侧是否真消费 assetContext(见 §4.3) |
|
||||
| **W-ASSET-MARKET-READONLY** | 支付前资产市场只读半 | 已拍方向,排期另切 | 前置 = P-MAT 契约增量 + 素材审核门 + **溯源链阶段一落 lineage/assetRefs 契约(跨线)** |
|
||||
| **W-ASSET-MARKET-TRADE** | 购买 / 授权 / 分成 | 后置 | 被支付收单 + trade ASSET + 法务口径物理卡住 |
|
||||
|
||||
排序原则是一句话:**能自己闭合的先闭合,受外部闸门卡住的不伪装成已完成。** 源工程存储的验收不碰钱、不碰授权、不碰法务,直接服务生成主线可靠性,所以最优先、能自足。私有素材库真验范围小、和 SRC 无依赖,并行做(唯一要先核实的是生成侧是否真消费 `assetContext`,见 §4.3)。资产市场只读半已拍方向,但压着三道前置:P-MAT 契约把素材结构化、素材审核门就绪、以及溯源链阶段一先把 `assetRefs` 归因字段落进契约——后者是跨线依赖,不是选素材通道顺带就有。所以它排在真验之后、单独切执行计划。流通半被真实支付收单和法务口径两道外部闸门物理卡住,不是工程量问题,后置。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
SRC["① W-ASSET-SRC<br/>源工程长期存储 / 版本寻址 / 取回重建"]:::now
|
||||
MV["② W-ASSET-MAT-VERIFY<br/>私有素材库 staging 真验"]:::now
|
||||
RO["③ W-ASSET-MARKET-READONLY<br/>资产市场只读半"]:::next
|
||||
TR["④ W-ASSET-MARKET-TRADE<br/>购买 / 授权 / 分成"]:::later
|
||||
SRC -->|并行·互不阻塞| MV
|
||||
MV -->|真链就绪| RO
|
||||
RO -->|真实支付收单 + 法务口径| TR
|
||||
PMAT["P-MAT 契约增量 + 素材审核门<br/>(素材中心 / compliance)"]:::dep -. 前置 .-> RO
|
||||
LIN["溯源链阶段一<br/>lineage / assetRefs 契约(数据飞轮 §4 阶段一)"]:::dep -. 跨线前置 .-> RO
|
||||
PAYG["支付收单 + 二清 / 牌照"]:::gate -. 前置 .-> TR
|
||||
LAW["法务 IP 派生 / 商用分成口径"]:::gate -. 前置 .-> TR
|
||||
classDef now fill:#e6ffe6,stroke:#3a3;
|
||||
classDef next fill:#fff7e6,stroke:#d90;
|
||||
classDef later fill:#f5f3ff,stroke:#7c3aed,stroke-dasharray:5 4;
|
||||
classDef dep fill:#eef2ff,stroke:#4f46e5;
|
||||
classDef gate fill:#fee,stroke:#c33,color:#900;
|
||||
```
|
||||
|
||||
**与数据飞轮资产层的一致性**,要把两类资产分清楚,否则容易把源工程和素材混成一件事。数据飞轮 §3.2 的「资产沉淀」飞轮讲的是**素材**——私有素材库升级成货架、授权、分成。W-ASSET 里对得上它的是 MAT-VERIFY / MARKET-READONLY / MARKET-TRADE 这三段,它们严格落在 §3.2 的边界、§6 开放问题 4(2026-07-05 只读半可先上)之内,不改那份 canonical 的任何命题,只补落地做法。而 **W-ASSET-SRC(源工程存储)不在数据飞轮的资产市场飞轮里**——它补的是「源工程资产」这一类,服务生成主线可靠性和「游戏 = 长生命周期项目」范式。这里按 §1 说清一层关系:① 不是与素材资产毫无瓜葛,它存的那份 manifest 是收益回流语料的源工件、也是 `assetRefs` 归因将来的落脚处,它其实是两条飞轮共同的底座;说它「能先做」讲的是它自身验收不依赖钱 / 授权 / 法务,不是说它和另一条飞轮无关。两者都叫「游戏资产管理」,但是两类资产、两套机制、两个 owner,这次一起做,是因为一个是地基、一个是护城河。
|
||||
|
||||
**分期编号对齐(避免和上级 canonical 双轨漂移)。** 数据飞轮 §4 的权威分期是「阶段一溯源链 / 阶段二市场只读半 / 阶段三市场流通半 / 阶段四收益回流」。本档为落地另起了 ①②③④ 的执行拆段,两套编号必须对得上、且以数据飞轮 §4 为准,不另立编号真相:本档 **③ MARKET-READONLY = 数据飞轮 §4 阶段二只读半**,**④ MARKET-TRADE = 阶段三流通半**;本档 **② MAT-VERIFY 是数据飞轮分期里没单列的一段**——它是 §3.2「资产沉淀第一块砖」私有库从 mock 到 staging 真链的兑现,是市场化之前的前验、不属于 §3.2 描述的市场化设计本身;本档 **① SRC 不在资产飞轮分期内**(它是源工程资产,见上一段)。数据飞轮 §4 阶段四(收益回流)不在本档范围。溯源链(阶段一)也不在本档范围,但本档 ③ 对它有跨线前置(`assetRefs` 字段),已在上表与 §4 登记。
|
||||
|
||||
## 6. 不留孤儿:新结构与接口的交付清单
|
||||
|
||||
项目硬约束:新数据结构 / 接口 / 领域模型,连同入口、使用路径、失败模式、验收一并交付。本次涉及的项集中列此,已在的标已在、要新增的标状态。
|
||||
|
||||
| 结构 / 接口 | 入口 | 使用路径 | 失败模式 | 验收 | 状态 |
|
||||
|---|---|---|---|---|---|
|
||||
| `game_source_project`(Java) | studio create/modify/extend 落库 | A11 按 `base_version_id` 取 base 源注入 | `status=2` 孤儿显式建模;落库失败不动 currentVersion | 已生产真用 | 已在 |
|
||||
| `tier2_source_project_version` + `BackendStore` | `save` = `run.persist_source_project` 收口(additive/best-effort,已接生产);`fetch` 生产消费方(tier2 第二装载)未建、本线只由 harness 验 | 首次生成 `save` 落 manifest + 全文;将来第二装载按 `(game_id, versionId)` fetch 重建(后续线) | **半落库孤儿会真产生**(versionId 含墙钟时间戳→重投换 MinIO 前缀、不自愈);修法二选一见 §3.3(manifest-first pending 或 source_hash 派生 versionId);fetch 缺源显式失败不静默重生成 | 接线 + 真 MySQL/MinIO 往返 + schema 治理归属 + **孤儿清扫 job(触发/调度/owner/回收断言)** | **真实现已写,待接线 / 真跑 / 治理 / 孤儿修法**(§3.2/§3.3) |
|
||||
| 寻址三元组 `{ id, versionId, sourceHash }` | 两面各自 `save/fetch` | 跨档取源统一按三元组寻址 | 三元组语义漂移由 doc↔code 断言点守 | 两面键名对齐、不合表 | 语义已在,契约点待立 |
|
||||
| `game_material` 市场化 additive 列 | `register` 扩展 | 可见范围状态机(私有→待审→公开/驳回)+ 授权类型 + 锁风标签 + 分成占位 | additive 不破存量私有库;素材转公开须过审、不能一步翻公开 | P-MAT 契约落地(素材中心 owner;详细 schema 由 READONLY 执行计划先落 contract-first) | **待建(只读半)** |
|
||||
| 素材审核门 | 待审→审核→公开/驳回 | 锁风标签由审核写;服务端「过审才进公开货架」强制 | 无审核门则公开 UGC 货架不上线 | compliance/ip 审核流程 + 服务端强制点就绪 | **待建(只读半硬前置,compliance/ip owner)** |
|
||||
| 货架浏览 / 公开选用端点 | studio.yaml 新增 market 端点 | 跨创作者货架检索(只列过审公开)→ 免费/本人自有素材选用进草稿 | 可见范围 / 审核态服务端强制,前端非边界;选用后失效生成侧重解析剔除 | 只读半 staging 真链 | **待建(只读半)** |
|
||||
| 血缘 `assetRefs` 归因 | 选素材通道(P-CRT-04) | 记 `materialId` 作流通半分成依据 | 非同款顺带产出,需选素材才有;字段归宿(嵌 lineage vs 顶层)未决(§4.1) | 选用市场素材时正确记录 | **契约未落地**:数据飞轮 §3.1 提案、随溯源链阶段一落 schema(跨线前置,非「已预留」) |
|
||||
| 采购单 / 授权单表 + trade `ASSET` 枚举 | 新增采购端点 → trade 入账 | 付费采购 → 素材作者 `share_rate` 分成 | `uk_source` 幂等防重复分账;退款走冲正不删行 | 支付接通 + trade ASSET + 法务口径 | **后置(流通半,外部闸门)** |
|
||||
|
||||
## 待拍板与后续动作
|
||||
|
||||
设计层面还有几处需要在切执行计划前定夺:
|
||||
|
||||
1. **半落库孤儿的修法二选一**(§3.3):manifest-first(先落 pending manifest 再 put 对象,+ `status` 列 + 按状态回收)vs versionId 只由 `source_hash` 派生(去时间戳)。前者更干净(隐形孤儿变可见 pending 行)、代价是加一列;后者动 `derive_version_id` 这个和 `LocalFsStore` 共享的代码契约、影响面更大。无论哪条,孤儿清扫 job 都是本线必交项。建议在 W-ASSET-SRC 执行计划里据代码影响面拍,倾向 manifest-first。
|
||||
2. **`tier2_source_project_version` 的 schema 治理路线**(§3.2 第二步):Flyway 迁移 vs 登记为 tier2 自有受治理 schema,取决于 tier2 与 huijing 是否共用同一 MySQL 实例。这是部署事实驱动的选择,建议在 W-ASSET-SRC 执行计划里据实拍。
|
||||
3. **`assetRefs` 的契约归宿**(§4.1):嵌在拟新增的 `lineage` 顶层对象里(但 `lineage` 只在同款时挂载,原创用市场素材就无处安放),还是抬成 `source-project.schema.json` 的独立顶层字段 / 独立关联表。这条与溯源链阶段一一并拍,是只读半 `assetRefs` 归因能不能落地的前提。
|
||||
4. **②开工前的 assetContext 消费核实**(§4.3):生成 worker(cheap-worker / tier2)当前 grep 无 `assetContext` / `materialId` 消费、Java 执行器注明它「仍 transient」。②执行计划开工前先核实到底流没流进生成;若没有,把「生成侧消费 + 归因回填」列进②范围,别按「零新增能力」排期。
|
||||
5. **寻址是否要做一层薄的跨档查询门面**:本档主张不合表、只统一寻址语义。若产品后续需要「一个游戏跨档查它所有源版本」的统一入口,再评估是否加一层只读查询门面——但不要提前造,避免孤儿抽象。
|
||||
6. 执行计划分别切:W-ASSET-SRC(接线 / 治理 / 取回重建真跑 / 孤儿修法)、W-ASSET-MAT-VERIFY(mock 换 staging 真链 + assetContext 消费核实)、W-ASSET-MARKET-READONLY(P-MAT 契约增量 + 素材审核门 + 货架 + assetRefs 归因,依赖溯源链阶段一,不接购买分成)。流通半(MARKET-TRADE)等支付与法务前置就绪再启动。
|
||||
285
docs/plans/2026-07-05-阶段四观测后续波实施-plan.md
Normal file
285
docs/plans/2026-07-05-阶段四观测后续波实施-plan.md
Normal file
@ -0,0 +1,285 @@
|
||||
---
|
||||
date: 2026-07-05
|
||||
topic: 观测体系-阶段四后续波实施
|
||||
status: 待实施 · 第一波(观测栈五件)已起 mini-infra;后续波 ②③④⑤ 按依赖串,碰 game-cloud 单体处(② 生产上线、⑤ 业务埋点)逐波开部署窗口
|
||||
上级: docs/agent-specs/2026-07-05-全链路可观测性-OTel三信号全栈-设计.md
|
||||
---
|
||||
|
||||
# 阶段四观测 · 后续波实施计划
|
||||
|
||||
## 1. 承接点
|
||||
|
||||
第一波已经把观测栈的后端五件起在 mini-infra:Collector 收 OTLP(`100.64.0.8:4317` gRPC / `4318` HTTP),Prometheus 存指标,Tempo 与 Loki 把块存进 MinIO 的 `obs-tempo` / `obs-loki` 桶,Grafana 预配好三数据源与三联下钻。进库前的脱敏 processor(手机号 / token / 密钥 / JWT 打码)已经挂在 Collector 的三条 pipeline 上,只是还没有真实信号流过它。这一波不碰生产,纯增量旁路。
|
||||
|
||||
剩下四波要做的,是把信号源一个个点亮再端到端串起来:给 game-cloud 单体挂 agent 让它出 span 和指标,给两条 Python 生成路接上 OTLP 让它们真发 span,把「用户点一下到模型返回」这条最会断的三跳 trace 打通,最后落业务埋点、看板、告警和运维入口。
|
||||
|
||||
这份文档是施工排布,不是新设计。阶段四设计已过双评审、创始人 2026-07-05 已拍六项(见上级设计档 §10),所以这里只做分波、工单、部署窗口与验收,不重开设计、不触发双评审门。所有埋点方式、取舍、验收判据都以上级设计为准,本文只给落地路径。
|
||||
|
||||
## 2. 分波总览
|
||||
|
||||
四波按依赖串,越往后越依赖前一波已经在生产就位。碰 game-cloud 单体的只有两处:波② 给单体挂 agent 加 actuator(要重构建、重启单体),波⑤ 在 aigc 里手埋两个业务指标(同样改单体代码)。Python 生成线的改动碰的是 mini-desktop 上的生成进程,走生成线重启窗口,不占 game-cloud 单体的部署窗口——这两类窗口全文分开写。
|
||||
|
||||
| 波 | 做什么 | 碰 game-cloud 单体 | 碰生成线(mini-desktop) | 依赖 |
|
||||
|---|---|---|---|---|
|
||||
| ② | 单体挂 OTel Java agent + actuator + micrometer;SAA observation 从 NOOP 活过来、`/actuator/prometheus` 成立 | 是(staging 验证 + 生产上线两点) | 否 | 第一波 Collector 就绪 |
|
||||
| ③ | 便宜档主路新接 OTLP span sink、tier2 studio_sink 改发 Collector;补 Python 侧 OTLP metrics 导出口 | 否 | 是(cheap Service/worker、tier2 Service 重启) | 第一波 |
|
||||
| ④ | 打通三跳 traceparent,让 ③ 已能读 context 的生成 span 挂到 Java trace 下 | 否(Java 侧靠 ② 的 agent 自动收发,零代码) | 是(worker_service / driver / Service 改动) | ②(agent 在生产)+ ③(span 已产、可读 context) |
|
||||
| ⑤ | 业务埋点(§6 清单)+ Grafana 看板 + 告警 + admin 一键进盘 + new-api 通道巡检告警 | 是(aigc 手埋 task_total / queue_depth) | 是(Python 埋点填值) | ②③④ |
|
||||
|
||||
一条贯穿全程的铁律:观测栈挂掉绝不能咬主链(设计 §8)。OTel agent 异步批量上报、Collector 不可达即本地丢弃而非阻塞,Python 侧 best-effort 是既有铁律,埋点开关默认关、经 profile 显式开。每一波的回滚都因此很干净。
|
||||
|
||||
## 3. 波② · game-cloud 接入
|
||||
|
||||
### 目标
|
||||
|
||||
给线上单体 huijing-server 点亮三件事:OTel Java agent 一个参数拿到 Spring MVC / JDBC / Redis / 出网 HTTP 的全链 span,并在出网(调 Python worker、调 new-api)时自动注入 traceparent;引 actuator 加 micrometer registry,让 `/actuator/prometheus` 端点成立、让已经挂好的 SAA 节点 observation 从 NOOP 活过来;把半死的 SkyWalking starter 共存收编,不删依赖、不启用、只把响应头 trace-id 的来源换成 OTel。
|
||||
|
||||
### 现状锚点(亲验)
|
||||
|
||||
- monitor starter(`huijing-spring-boot-starter-monitor`)经 system-server 和 infra-server 两个模块传递进了单体,在席但残缺。`HuijingTracerAutoConfiguration` 第 28–40 行 SkyWalking 的 `BizTraceAspect` 与 `Tracer` Bean 整段被注释(原作者留了 TODO「后续换 opentelemetry」),只剩 `TraceFilter` 往响应头写 trace-id,而这个 trace-id 取自 SkyWalking 上下文、agent 不在时是空串。
|
||||
- `micrometer-registry-prometheus` 与 `spring-boot-admin-starter-client` 在 starter 的 pom 里都是 `<optional>true</optional>`,没随传递装进来;`HuijingMetricsAutoConfiguration` 的 `@ConditionalOnClass(MeterRegistryCustomizer.class)` 要 actuator 在席才激活。
|
||||
- `AigcExecutorConfiguration` 用 `ObjectProvider<ObservationRegistry>.getIfAvailable(() -> ObservationRegistry.NOOP)` 软取 registry;`SaaStudioGraph` 里判 `registry != NOOP` 才挂 `GraphLifecycleListener`,拿到 NOOP 就跳过 = SAA 节点不埋点。装了 actuator/micrometer 这个 registry 才非 NOOP。
|
||||
- huijing-server 的 Dockerfile:`ENV JAVA_OPTS="-Xms512m -Xmx512m …"`,`CMD java ${JAVA_OPTS} -jar app.jar $ARGS`,JAVA_OPTS 可经 `-e` 覆盖。**推论**:agent jar 可以挂载进容器 + 改 JAVA_OPTS 追加 `-javaagent`,不必重构建镜像;但 actuator/micrometer 是编译期依赖,必须重构建 jar。所以波② 的生产上线是一次带重构建的完整重启。
|
||||
|
||||
### 步骤
|
||||
|
||||
1. 取 OpenTelemetry Java agent(pin 版本的 `opentelemetry-javaagent.jar`),放到部署机可挂载路径。
|
||||
2. 给单体聚合引入 actuator + micrometer registry,让指标端点成立、registry 非 NOOP;actuator 端点最小暴露 health 与 prometheus,management 端口与访问按内网口径收敛(不裸暴露到公网面)。
|
||||
3. 配 agent 环境:`OTEL_EXPORTER_OTLP_ENDPOINT=http://100.64.0.8:4318`(跨机经 Tailscale)、`OTEL_SERVICE_NAME=huijing-server`、采样交给 Collector 尾采样(agent 侧 `parentbased_always_on`)、补 resource 属性。agent 挂载与 actuator 一并经 profile 显式开、默认关(设计 §8 旁路铁律)。
|
||||
4. 把 `TraceFilter` 响应头 trace-id 的来源从 SkyWalking 上下文换成 OTel 当前 span 的 traceId,让前端拿到的 trace-id 真能在 Grafana 里查到;SkyWalking toolkit 依赖留着不删、profile 不启用(不动 yudao 框架层)。
|
||||
5. 取消 `prometheus.yml` 里 game-cloud 的 scrape 占位注释,`metrics_path=/actuator/prometheus`,target 指向生产单体的 Tailscale 地址:端口。
|
||||
6. 先在 staging(mini-desktop `/root/game-staging/repo`)挂 agent 压一轮,量启动耗时差、P95/P99 请求耗时差、生成任务耗时差,并确认 agent 的 queue/drop 指标可见(验收第 6 条),开销在阈内再上生产 profile。
|
||||
|
||||
### 工单
|
||||
|
||||
**T2-1 挂 agent 与配置**
|
||||
目标:单体启动带 `-javaagent`、把 span 发到 mini-infra Collector、出网自动注入 traceparent。输入:agent jar、Collector 端点、Dockerfile/JAVA_OPTS 注入位。产出:agent 挂载配置 + OTEL_* 环境 + profile 开关。验收:staging 启动后 Tempo 出现 huijing-server 的 Spring MVC/JDBC span。依赖:第一波 Collector。风险:agent 注入拖慢启动或加请求延迟——由 T2-4 量化兜。
|
||||
|
||||
**T2-2 引 actuator + micrometer,激活 registry**
|
||||
目标:`/actuator/prometheus` 出内容、ObservationRegistry 非 NOOP。输入:单体聚合 pom、actuator/micrometer 依赖。产出:actuator 最小端点 + micrometer registry(选型见 §7 待拍第 5 点)。验收:curl `/actuator/prometheus` 有 JVM/HTTP 指标;dispatcher=saa 路跑一次能见 SAA 节点 span。依赖:无(编译期)。风险:actuator 端点暴露面——按内网口径收敛、management 端口不外放。
|
||||
|
||||
**T2-3 SkyWalking 共存收编 + TraceFilter 换源**
|
||||
目标:半死 starter 不删不启用,响应头 trace-id 换成 OTel 当前 span。输入:`HuijingTracerAutoConfiguration`、`TraceFilter`。产出:TraceFilter 取 OTel traceId 的改动 + profile 不启用 SkyWalking。验收:响应头 trace-id 非空、且能在 Grafana 按它拉到 trace。依赖:T2-1(agent 在席才有当前 span)。风险:改动碰框架层——限定只改 trace-id 取值来源,不动 toolkit 依赖树。
|
||||
|
||||
**T2-4 staging 压测对比(验收第 6 条)**
|
||||
目标:量化 agent 开销到可接受判定。输入:staging 挂 agent 版本、基线版本。产出:启动/P95/P99/生成耗时差报告 + queue/drop 截图。验收:各差值在阈内、指标可见。依赖:T2-1/T2-2。风险:staging 与生产规格不一致致误判——报告注明规格差。
|
||||
|
||||
**T2-5 Prometheus scrape 接线**
|
||||
目标:Prometheus 抓到 game-cloud 指标。输入:`prometheus.yml` 占位、生产单体地址:端口。产出:启用 game-cloud job。验收:Prometheus target 状态 UP、`huijing_*` 与 JVM 指标可查。依赖:T2-2 端点成立、跨机连通。风险:跨机 scrape 走 Tailscale 抓不通——按跨机连通核对,不混成「都指 .8」。
|
||||
|
||||
### 碰生产 game-cloud 的部署窗口点
|
||||
|
||||
- **窗口 A(staging,不碰生产)**:mini-desktop staging 挂 agent 压一轮(T2-4)。
|
||||
- **窗口 B(生产上线,碰生产)**:生产单体重构建 jar(带 actuator/micrometer)+ 挂 agent + profile 显式开 → 重启单体。这是本波唯一碰生产的动作,建议低峰做;若生产是单实例,冷重启会有短暂不可用,提前公告。**前置须确认**:生产 huijing-server 的部署位置与启动方式(compose / systemd / docker run),它决定 agent 挂载与 JAVA_OPTS 注入手法(见 §7 待拍第 1 点)。
|
||||
|
||||
### 验收(引设计 §8)
|
||||
|
||||
- 第 6 条:staging 挂 agent 压一轮,启动与 P95/P99、生成任务耗时差在阈内,agent queue/drop 可见——冒烟绿不足以发现 agent 注入的启动变慢和批量上报内存积压,这条必须真量。
|
||||
- 第 4 条:actuator health + agent 出的 HTTP server 指标出得来,error budget 燃尽图与 health 探测对得上。
|
||||
- 第 5 条:停掉 Collector 后单体行为不变、冒烟门全绿(旁路铁律)。
|
||||
- 附:`/actuator/prometheus` 有内容并被 Prometheus 抓到;dispatcher=saa 路能见 SAA 节点 span(仅 saa 路;默认 http 路不跑 SAA 图,其 trace 来自 Python 线、归波④)。
|
||||
|
||||
### 回滚
|
||||
|
||||
摘掉 `-javaagent` 参数(JAVA_OPTS 改回)、关 actuator profile、还原 TraceFilter 取值来源、把 `prometheus.yml` 的 game-cloud job 注释回去。actuator/micrometer 是新增依赖,不启用即无副作用,主链业务代码一行不动。
|
||||
|
||||
## 4. 波③ · Python 两线接 OTLP
|
||||
|
||||
### 目标
|
||||
|
||||
两条生成路各接一处,让它们真产 OTLP span 发 Collector,并让生成 span 具备「读入站 W3C context 起子 span」的形态,为波④ 的贯穿铺好落点。这一波做完,两条线各自能在 Tempo 看到自产的 span(还没挂到 Java trace 下,是自成 trace);顺带把面五要的业务指标导出口在 Python 侧备齐。
|
||||
|
||||
### 现状锚点(亲验)
|
||||
|
||||
- 便宜档主路的 Service 入口(`cheap_service_app.py` 第 268–276 行)和 CLI 入口(`cheap_studio.py` 第 206–213 行)都用 `make_jsonl_sink` 把每步 trace 写进产物目录的 `trace.jsonl`,不产 OTLP span。真正的生成发生在 AgentScope Service 进程(`127.0.0.1:8300`),span 的插入点就在这两处的 `Tier2TraceMiddleware(sink=...)`。
|
||||
- otel 全套(api / sdk / exporter-otlp-proto-http,1.43.0)随 agentscope 2.0.2 装进了 cheap-worker 的 venv,依赖是齐的——但装依赖不等于在发 OTLP。
|
||||
- tier2 的 `studio_sink.py` 是全仓唯一的 OTLP exporter,可它自带私有 TracerProvider、每步自造 root span、靠 gen-ai conversation id 归组,结构上不读入站 context;端点取 `TIER2_STUDIO_URL` / `infra_config[studio].http_url`,取不到即 no-op、默认关。tier2 本地无独立 venv、studio_sink 惰性 import otel,运行 venv 的 otel 到位与否要在运行环境实测(设计 §3 明示)。
|
||||
- 代理坑:OTLP exporter 打 `100.64.0.8:4318` 走 Tailscale,必须绕过本机 fake-ip 代理(198.18.x),否则被拦。`cheap_service_driver` 已有 `install_proxy_bypass` 范式(把 host 并入 NO_PROXY),复用它。
|
||||
|
||||
### 步骤
|
||||
|
||||
1. 便宜档新增一个 OTLP span sink(与 `make_jsonl_sink` 同签名、吃 TraceStep 发 span),endpoint 指 Collector `4318`、`service.name=cheap-gen-worker`,best-effort(取不到 endpoint 或发送失败即降级 no-op、绝不抛,对齐 studio_sink 的 observe-only 铁律)。在 `cheap_service_app.py:268` 与 `cheap_studio.py:206` 两处把它与 jsonl sink 并接(Tier2TraceMiddleware 多 sink,或包一层 fan-out sink),jsonl 落盘保留不动。
|
||||
2. tier2 `studio_sink` 端点从 Studio 改成 Collector 的 OTLP/HTTP(`/v1/traces`),`service.name` 保持 `tier2-gen-worker`;把 `start_as_current_span` 从私有 root 改成读入站 context 起子 span(context 的提取归波④,本波先把「不自造 root、用传入 context」的形态改好,无 context 时退化为自身 root、不报错)。
|
||||
3. Python 侧建一个 OTLP MeterProvider(同 endpoint),备好 `llm_cost` / `gen_duration_seconds` / `llm_cache_hit_rate` / `gen_gate_fail_total` 的直发导出口(值来源见面五;本波只备口,填值随波⑤)。
|
||||
4. trace 与 metrics 两个 exporter 统一走 `install_proxy_bypass` / NO_PROXY 含 `100.64.0.8`,和既有 new-api / Service 旁路同源。
|
||||
5. 在扩容后的运行环境实测 tier2 venv 的 otel wheel 是否到位;缺则补 wheel,或确认惰性降级不影响主链。
|
||||
|
||||
### 工单
|
||||
|
||||
**T3-1 便宜档 OTLP span sink + 双入口并接**
|
||||
目标:便宜档主路真产 span 发 Collector。输入:Tier2TraceMiddleware 接口、Collector 端点。产出:OTLP sink 模块 + Service/CLI 两入口并接。验收:跑一局便宜档生成,Tempo 见 `cheap-gen-worker` 的 span;jsonl 仍照落。依赖:第一波。风险:多 sink 拖慢 ingest——BatchSpanProcessor 异步发、best-effort 降级兜。
|
||||
|
||||
**T3-2 tier2 studio_sink 改端点 + 去私有 root**
|
||||
目标:tier2 span 发 Collector、可读入站 context。输入:`studio_sink.py`。产出:端点改 Collector + 私有 root 改读入站 context。验收:tier2 跑一局,Tempo 见 `tier2-gen-worker` span。依赖:第一波。风险:私有 root 逻辑改动破坏 observe-only——保持 best-effort、失败降 no-op。
|
||||
|
||||
**T3-3 Python OTLP metrics 导出口**
|
||||
目标:成本/耗时/缓存/门结果有 metric 直发口。输入:Collector 端点、cost.compute/wallSec/cached token/verdict.guards 取值位。产出:MeterProvider + 四个 instrument 空口。验收:导出口能发出占位指标、Prometheus 经 Collector 出口抓到。依赖:第一波。风险:值口径与面五不一致——本波只建口,填值随⑤对齐。
|
||||
|
||||
**T3-4 OTLP 代理旁路统一**
|
||||
目标:两个 exporter 不被 fake-ip 代理拦。输入:`install_proxy_bypass` 范式。产出:trace/metrics exporter 前置装 NO_PROXY 含 `100.64.0.8`。验收:mini-desktop 上真发能到 Collector、无 502/连接关闭。依赖:无。风险:漏装某条路径——真 e2e 才暴露,须在生成线实机验(单测桩绕过真网络)。
|
||||
|
||||
**T3-5 tier2 运行 venv otel 实测**
|
||||
目标:确认 tier2 运行环境 otel 到位或安全降级。输入:mini-desktop tier2 venv。产出:实测结论 + 缺则补 wheel。验收:tier2 真跑能发 span 或惰性降级不崩主链。依赖:扩容后运行环境。风险:无 wheel 时静默 no-op——留一条告警日志、不当成功。
|
||||
|
||||
### 碰生产 game-cloud 的部署窗口点
|
||||
|
||||
不碰 game-cloud 单体。碰生成线:cheap Service(8300)、worker_service(9501)、cheap CLI、tier2 Service(8200)都在 mini-desktop,改完要重启生成线进程 = **生成线重启窗口**。择低峰、观察盲修空转率(既有头号观测)与九门过门率不回退;best-effort 铁律保证 OTLP 挂掉不咬生成。复现纪律:worktree/分支基线走 dev/2.0.0——默认分支 dev/1.0.0 无 game-cloud 与生成线新代码,派 worker 干活先 reset 到 dev/2.0.0 tip 或主树直改。
|
||||
|
||||
### 验收(引设计 §8)
|
||||
|
||||
- 第 1 条的半程:两条线各自能在 Tempo 看到自产的生成 span(此时尚未挂到 Java trace 下、是自成 trace,端到端挂接归波④)。
|
||||
- 第 5 条:停 Collector 后两条生成路行为不变、冒烟门全绿(best-effort 降级 no-op)。
|
||||
- 第 7 条预演:往一局便宜档生成的 brief/prompt 注入构造的手机号和 token,产出的 span 属性经 Collector redaction 后在 Tempo 查不到明文——脱敏 processor 第一波已在 Collector,这波第一次有真 span 流过它,正好把这条安全红线先验一遍。
|
||||
|
||||
### 回滚
|
||||
|
||||
关掉 OTLP endpoint 环境变量,两条 sink 降级 no-op、metrics 不发;jsonl sink 与生成主链一字不动。tier2 studio_sink 若要回 Studio,改回端点即可。
|
||||
|
||||
## 5. 波④ · 三跳 traceparent 打通
|
||||
|
||||
### 目标
|
||||
|
||||
把设计 §5 面四的「三跳不是两跳」接起来,让波③ 已能读 context 的生成 span 真挂到 Java 的 trace 下,端到端一条 trace 串到底(设计 §8 第 1 条,也是最会断的那条)。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
J["game-cloud<br/>WorkerDispatchClient<br/>(JDK HttpClient)"] -->|① agent 自动注入 traceparent| W["worker_service :9501<br/>(裸 http.server)<br/>② 手动 extract 建 server span"]
|
||||
W -->|③ driver httpx 手动 inject| S["AgentScope Service :8300<br/>④ 生成 span 接住 context<br/>(便宜档主路)"]
|
||||
W -->|⑤ urllib 手动 inject| CB["game-cloud 回调入口<br/>/dify/callback-internal<br/>Spring controller · agent 自动 extract"]
|
||||
```
|
||||
|
||||
三跳的真实拓扑:worker_service(9501)是收 Java job 的裸 http.server 派发壳,它经 `cheap_service_driver` 打本机 AgentScope Service(8300)跑生成,拿到结果后自己用 urllib 回调 game-cloud。所以一次生成的 span 树是:worker_service 的 server span 覆盖整个 job,driver 打 Service 那跳与回调那跳都是它的子 span,Service 里的生成 span 挂在 driver 那跳之下。
|
||||
|
||||
### 现状锚点(亲验,三个断点)
|
||||
|
||||
- 第一跳 Java → worker_service:Java 侧靠波② 的 agent 自动注入 traceparent(依赖 ② 已上生产);worker_service 是裸 http.server(`worker_service.py`),OTel 无自动埋点,`do_POST` 收到的 traceparent 没人提取。
|
||||
- 第二跳 worker_service → 本机 Service:driver 打 Service 现在 header 只有 `X-User-Id`(`cheap_service_driver.py:239`),trace 上下文在此断,而便宜档生成 span 恰恰产在 Service 进程。driver 已装 proxy-bypass,给 headers 加一个 traceparent 即可。
|
||||
- 第三跳 回调 game-cloud:worker_service `post_callback` 用 urllib(`_NO_PROXY_OPENER`,禁代理)POST 回 `/admin-api/aigc/dify/callback-internal`,要在这条 urllib 请求手动注入 traceparent;Java 回调入口是 Spring controller,agent 自动 extract,零 Java 代码。
|
||||
- tier2 线同构:其派发 worker 也是裸 http.server,按同款 extract/inject 接,studio_sink(③ 已改读入站 context)承接。
|
||||
|
||||
### 步骤
|
||||
|
||||
1. worker_service:`do_POST` 入口用 W3C `TraceContextTextMapPropagator` 从 `self.headers` extract context,起一个 server span 覆盖这次 job 处理,把 context 传进 `process_job`。
|
||||
2. `cheap_service_driver`:在 `drive_cheap_generation` 承重那跳(kick / SSE)的 httpx 请求里 inject traceparent 到 headers,与 `X-User-Id` 并列。
|
||||
3. `cheap_service_app`(Service 侧):在 HTTP/ASGI 入口 extract traceparent,让 `_cheap_middlewares_factory` 建的 `Tier2TraceMiddleware` span 挂在传入 context 下(③ 已把「用当前 context 起子 span」改好,这波把 context 真喂进来)。
|
||||
4. worker_service `post_callback`:在 server span 的 context 内,给 urllib Request 注入 traceparent;Java 入口靠 agent 自动 extract 收尾。
|
||||
5. 业务 traceId 并存关联:业务 traceId 作为 span 的一个属性保留(承担产物归位与回调关联),W3C traceparent 负责跨语言贯穿,不强升业务 traceId 的格式(设计 §10 第 5 项)。
|
||||
|
||||
### 工单
|
||||
|
||||
**T4-1 worker_service 入站 extract + server span**
|
||||
目标:Java 传来的 traceparent 被接住、起 server span。输入:`worker_service.py` `do_POST`、W3C propagator。产出:入站 extract + server span 覆盖 job。验收:Java 发一条带 traceparent 的 job,Tempo 见 worker_service server span 挂在 Java span 下。依赖:②。风险:extract 失败——降级为自身 root、不阻断。
|
||||
|
||||
**T4-2 driver httpx 出站 inject**
|
||||
目标:worker→Service 那跳带上 traceparent。输入:`cheap_service_driver.py:239` headers。产出:httpx 请求 inject traceparent。验收:Service 侧能收到 traceparent。依赖:T4-1。风险:漏在某个 sub-请求注入——承重跳(kick/SSE)必接,建 agent/session 跳可选。
|
||||
|
||||
**T4-3 Service 侧 ASGI 入口 extract + 生成 span 挂接**
|
||||
目标:生成 span 挂在传入 context 下。输入:`cheap_service_app` HTTP 入口、③ 的 OTLP sink。产出:ASGI 入口 extract + middleware span 起在该 context。验收:便宜档生成 span 在 Tempo 挂到 worker_service span 下。依赖:③ T3-1、T4-2。风险:AgentScope Service 是框架层,入站 header 到 middleware 的 context 传递需一处接线(instrument ASGI 层或从请求上下文取)——本波核心难点,先小 spike 打通再铺开。
|
||||
|
||||
**T4-4 回调 urllib inject**
|
||||
目标:回调那跳带 traceparent。输入:`post_callback` urllib Request。产出:server span context 内注入 traceparent。验收:game-cloud 回调入口(agent 自动 extract)把回调挂到同一 trace。依赖:T4-1、②。风险:urllib 禁代理路径上手动注 header——只加 header、不动禁代理逻辑。
|
||||
|
||||
**T4-5 tier2 同款接线 + 端到端串联验证**
|
||||
目标:tier2 三跳同款打通、端到端一条 trace。输入:tier2 派发 worker、studio_sink。产出:tier2 extract/inject 接线 + 一次真生成的贯穿 trace 证据。验收:两条线各拉出一条贯穿 Java→worker→Service→回调 的单 trace。依赖:T4-1..4、③ T3-2。风险:两条线拓扑细节差异——各自小 spike 坐实。
|
||||
|
||||
### 碰生产 game-cloud 的部署窗口点
|
||||
|
||||
Java 侧零代码(agent 自动 inject/extract),不新增 game-cloud 单体窗口——但依赖波② 的 agent 已在生产,故本波必须排在 ② 生产上线之后。Python 侧改 worker_service / driver / Service = **生成线重启窗口**,可与波③ 合并成一次重启,或紧随其后。
|
||||
|
||||
### 验收(引设计 §8)
|
||||
|
||||
- 第 1 条(端到端 trace_id 一致):从响应头拿到 trace-id,能在 Grafana 里拉出贯穿 Java → worker_service → 本机 Service 生成 → 回调 的单条 trace(便宜档主路;tier2 同验)。这是本波的硬命门。
|
||||
- 第 5 条复验:贯穿接线全是 best-effort,停 Collector 后生成不变、冒烟门全绿。
|
||||
|
||||
### 回滚
|
||||
|
||||
propagator 接线是纯读 header + 起 span,失败即 best-effort 降级(extract 不到就自身 root、inject 失败就不带 header);关掉 OTLP endpoint 就整体回到波③ 前的自成 trace 态,主链不受影响。
|
||||
|
||||
## 6. 波⑤ · 业务埋点 + 看板 + 告警 + admin 进盘 + 通道巡检
|
||||
|
||||
### 目标
|
||||
|
||||
把 §6 埋点清单落全,建 Grafana 看板与告警规则、接飞书/钉钉 webhook、admin 一键进盘、new-api 通道巡检探活加健康告警。自动冻结阀本期不做,作为已登记的 follow-up 排下一期(设计 §10 第 4 项;2026-06 通道整批作废的教训不丢)。
|
||||
|
||||
### 埋点落点(对着设计 §6)
|
||||
|
||||
| 指标 | 埋在哪 | 碰生产 |
|
||||
|---|---|---|
|
||||
| `gen_task_total{status}` counter | game-cloud aigc 任务终态回填处(手埋) | game-cloud 单体 |
|
||||
| `gen_queue_depth` gauge | game-cloud 控制平面背压判定处(手埋) | game-cloud 单体 |
|
||||
| `gen_duration_seconds` histogram | Python worker(已算 wallSec,填进 ③ 备好的口) | 生成线 |
|
||||
| `gen_gate_fail_total{gate}` | Python 侧聚合 verdict.guards / trace 字段 | 生成线 |
|
||||
| `llm_cost` | Python worker cost.compute 导出,**标降级**(worker 自估值、非 new-api 权威;告警阈值留余量;`logs.quota` 权威对账薄片作 follow-up) | 生成线 |
|
||||
| `llm_cache_hit_rate` | Python worker cached token 导出 | 生成线 |
|
||||
| 服务可用性 | ② 的 agent HTTP server 指标 + actuator health | 已在 ② |
|
||||
| SAA 图节点 span | ② 装 micrometer 即活,仅 dispatcher=saa 路 | 已在 ② |
|
||||
| new-api 多通道健康 | 新建后台巡检 job:每通道探活 + 查 key 额度 → 健康指标 + 告警(冻结阀排下一期) | 落点待拍 |
|
||||
| 全链路 trace | ④ 已打通 | — |
|
||||
| 集中日志(脱敏后) | logback OTLP 或 Collector filelog,只收 WARN/ERROR,trace_id 注入 MDC,进库经 Collector 脱敏 | 采集方式待拍 |
|
||||
| admin 一键进盘 | game-admin 前端加运维入口跳 Grafana | 前端发布 |
|
||||
|
||||
### 步骤
|
||||
|
||||
1. game-cloud aigc 手埋 `gen_task_total`(终态回填处)与 `gen_queue_depth`(背压判定处),用 micrometer(② 已引);SAA span 随 ② 自动活。
|
||||
2. Python 侧把 `gen_duration_seconds` / `llm_cost` / `llm_cache_hit_rate` / `gen_gate_fail_total` 填进波③ 备好的导出口,值取 cost.compute / wallSec / cached token / verdict.guards;`llm_cost` 明确标「估值非权威」。
|
||||
3. 集中日志:logback OTLP(或 Collector filelog)只收 WARN/ERROR,MDC 注入 trace_id(用 ② 的 OTel 当前 span traceId),进库经 Collector 脱敏。
|
||||
4. Grafana 看板:生成成功率 / 耗时 P50·P95 / 队列深度 / 九门过门率 / 单次成本 / 缓存命中 / 可用性,配指标→trace→日志三联下钻;沿用第一波已 provisioning 的三数据源。
|
||||
5. 告警规则:成功率跌破 80% 达标线与预警线、队列越限、可用性 error budget 燃尽、通道可用数掉下限;接飞书/钉钉 webhook。成功率告警阈值口径回写时与《观测体系》统一(设计 sot-impact 已声明)。
|
||||
6. admin 一键进盘:game-admin 前端加运维入口跳 Grafana(《观测体系》§2/§3.5 硬目标,本期做)。
|
||||
7. new-api 通道巡检 job:探活每通道 + 查 key 额度,出通道健康指标 + 告警;自动冻结阀留 follow-up。
|
||||
|
||||
### 工单
|
||||
|
||||
**T5-1 game-cloud 业务埋点(task_total / queue_depth)**
|
||||
目标:两个业务指标从 aigc 直出。输入:终态回填处、背压判定处、micrometer registry(② 已引)。产出:两个 instrument + profile 开关。验收:Prometheus 抓到 `huijing_gen_task_total` / `huijing_gen_queue_depth`。依赖:②。风险:手埋碰单体代码——限定终态与背压两处、开关默认关。
|
||||
|
||||
**T5-2 Python 埋点填值**
|
||||
目标:③ 的四个导出口填真值。输入:cost.compute / wallSec / cached token / verdict.guards。产出:四个 metric 填值 + `llm_cost` 降级标注。产收:看板见非零值。依赖:③ T3-3、④。风险:cost 自估偏差——阈值留余量、注明非权威。
|
||||
|
||||
**T5-3 集中日志 OTLP + MDC + 脱敏对账**
|
||||
目标:WARN/ERROR 日志进 Loki、带 trace_id、脱敏。输入:logback/filelog、MDC、Collector 脱敏。产出:日志接入 + trace_id 关联。验收:Loki 见脱敏后日志、可从 trace 下钻到日志。依赖:②(traceId 源)。风险:采集方式选型(见 §7 待拍第 4 点)。
|
||||
|
||||
**T5-4 Grafana 看板**
|
||||
目标:一块盘看到底。输入:三数据源、指标名。产出:概览盘 + 三联下钻。验收:七类指标在盘、可下钻。依赖:T5-1/T5-2、④。风险:指标名漂移——与埋点工单对名。
|
||||
|
||||
**T5-5 告警规则 + webhook**
|
||||
目标:阈值越线真送达。输入:Grafana 告警、飞书/钉钉 webhook。产出:告警规则 + 通知渠道。验收:造一条越线,群里收到(验收第 2 条)。依赖:T5-4。风险:换了告警件(Grafana 替夜莺)——更要真验送达。
|
||||
|
||||
**T5-6 admin 一键进盘**
|
||||
目标:运营从 admin 一键进 Grafana。输入:game-admin 前端、Grafana 地址。产出:运维入口。验收:admin 点入口进盘即见数据源与看板。依赖:第一波 Grafana。风险:前端发布窗口独立于单体。
|
||||
|
||||
**T5-7 new-api 通道巡检 + 健康告警**
|
||||
目标:通道可用性有眼睛。输入:new-api 通道配置、key 额度查询。产出:巡检 job + 健康指标 + 告警;冻结阀留 follow-up。验收:关一个通道,健康指标掉、告警送达。依赖:T5-5。风险:job 落点(见 §7 待拍第 3 点);自动冻结阀本期不做、勿顺手加。
|
||||
|
||||
### 碰生产 game-cloud 的部署窗口点
|
||||
|
||||
- **game-cloud 手埋(T5-1)**= 改单体代码 = 生产部署窗口(重构建 + 重启)。是否与波② 生产上线合并成一次窗口,交主控拍(见 §7 待拍第 2 点)。
|
||||
- **集中日志(T5-3)**:走 logback 改动则落 game-cloud 生产窗口;走 Collector filelog 抓容器 stdout 则不改单体(见 §7 待拍第 4 点)。
|
||||
- **通道巡检(T5-7)**:落 game-cloud 后台 job 则占生产窗口;落独立小 job(mini-infra)则不碰单体(见 §7 待拍第 3 点)。
|
||||
- **Python 埋点(T5-2)**= 生成线重启窗口。
|
||||
- **Grafana 看板/告警/webhook(T5-4/T5-5)与 admin 进盘(T5-6)**= 观测栈配置 + 前端发布,不碰 game-cloud 单体。
|
||||
|
||||
### 验收(引设计 §8 全六条 + 第 7 条)
|
||||
|
||||
本波收口做全量:
|
||||
|
||||
1. 端到端 trace_id 一致(④ 已打通,这波在看板上复看)。
|
||||
2. 告警真送达:造一条阈值越线(成功率跌破 80% 或队列越限),Grafana 告警在约定时间内落到飞书/钉钉群——换了告警件更要验这条。
|
||||
3. 成本抽样对账:`llm_cost` 指标与一笔 new-api 实际扣费抽样比对,偏差在余量内(认它是自估值)。
|
||||
4. 可用性 error budget 燃尽图与 health 探测对得上。
|
||||
5. 停 Collector 主链不变、冒烟门全绿。
|
||||
6. agent 性能量化(② 已做,收口复看指标在盘)。
|
||||
7. 脱敏真打码:往一次生成的 brief / prompt 注入构造的手机号和 token,跑完在 Tempo 的 span 属性和 Loki 的日志正文里都查不到明文、只见掩码——漏配即安全回退,这条必须能真跑。
|
||||
|
||||
### 回滚
|
||||
|
||||
埋点、日志、看板、告警都是增量:埋点开关默认关经 profile 开、看板告警删规则即止、巡检 job 停即止;game-cloud 手埋回滚 = 关 profile 或还原埋点代码,主链不受影响。
|
||||
|
||||
## 7. 需主控拍板的点
|
||||
|
||||
1. **生产 huijing-server 的部署位置与启动方式**(compose / systemd / docker run)。它决定波② 的 agent 挂载与 JAVA_OPTS 注入手法,以及窗口 B 的重启方式(单实例冷重启需公告短暂不可用)。
|
||||
2. **波② 生产上线与波⑤ game-cloud 业务埋点是否合并成一次生产窗口**。合并少一次重启;但设计倾向先验 agent 开销(第 6 条)稳定再上埋点。建议 ② 单独先上、⑤ 随后,请拍。
|
||||
3. **new-api 通道巡检 job 的落点**:game-cloud 后台 job(复用鉴权与 channel 配置、但碰生产)vs 独立小 job(落 mini-infra、不碰单体)。
|
||||
4. **集中日志采集方式**:logback OTLP appender(改单体、trace_id 注入 MDC 最干净)vs Collector filelog 抓容器 stdout(不改单体但 trace 关联弱)。
|
||||
5. **actuator/micrometer registry 选型**:OTLP registry(直发 Collector、与 trace 同管道)vs prometheus registry(暴露 `/actuator/prometheus` 让 Prometheus scrape)。设计两者都提;scrape 占位已在 `prometheus.yml`,倾向 prometheus registry 少一条出网,请拍。
|
||||
6. **生成线重启与 game-cloud 生产窗口的先后节拍**:波④ 依赖波② 的 agent 已在生产(agent 未上生产前,Python 侧注入的 traceparent 在 Java 侧无 agent 承接就是断)。建议 ② 生产上线后再排 ③④ 生成线重启;③ 可先上(自成 trace 无害),④ 紧跟 ②。
|
||||
Loading…
x
Reference in New Issue
Block a user