docs(gen): W-G1 组A execution v2——两轮对抗评审(Codex+opus)修订定稿

v1 判 NO-GO(3 P0 未穿透)→ v2 修订:
- 砍门③(promptHash 全局缓存=跨用户复用/跨模块孤儿API/sha256口径雷,非开闸闸;防刷由配额门兜)
- GP9 safety 前置到 submitGenerate 服务层(避执行器内卡串行 tick=DoS;独立客户端短超时8s+1重试)
- retry 走共享 enqueueWithControlPlane(堵 retry 绕过 GP9/配额)
- fail-closed 用中性 LLM_ERROR(不误标正常用户违规)
- P1:pom 改 -server/补 infra-api 依赖/验收断业务码非HTTP429/背压>=/level加分档测试/删CAS错误背书
范围=2门+降级+GP9前置;待创始人 nod 实施

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-16 18:45:08 +08:00
parent a344ccff04
commit 689d86ed87

View File

@ -0,0 +1,389 @@
# W-G1 开闸验收门 · 组AD12 控制平面 v0 + GP9 合规先行段)· execution 版
> **状态**execution 版 v2 · 2026-06-16 · 经两轮对抗评审Codex+opus修订
> **底稿(已读·与之一致勿偏)**`docs/agent-specs/2026-06-16-W-G1开闸验收门-review.md`§3 组A 路径 / §5 爆炸半径 / §6 验收)。
> **接地基准**:本文所有 `file:line` 已对 2026-06-16 dev/2.0.0 工作树 Read 核实;标注「已核实」= 亲读确认,「需确认」= 未直读到、留实现期校验。
> **纪律**契约先行DDL/接口先于实现描述);幂等/超时/失败/补偿对每个外部交互交代;不预写大段不可验证代码。
---
## 修订记录v2
> v1 经 Codex可行性+ opus前提两轮对抗评审判 **NO-GO**:方向对、省力,但有 P0 未穿透。主代理合并三方(含自审)后定盘修订,要点如下:
1. **砍门③promptHash 缓存命中复用)**v0 不做——它是省钱优化非开闸闸,且带全局跨用户复用(所有权/隐私/白嫖真扣费)+ 依赖不存在的跨模块产物状态查询 API孤儿+ 返回契约缺 versionId + sha256/MD5 口径雷,风险不值当塞 v0。「3 道前置门」整体改为「**2 道前置门 + 降级门**」。
2. **GP9 safety 前置到 `submitGenerate`(服务层),不放执行器**:满足底稿"违规不入队",避开 opus P0-3执行器内 safety 卡串行 tick 线程 = DoS。safety 用**服务层独立 LLM 客户端**(执行器 LLM 通道被 `aigc.executor.enabled` 门控、服务层注入不到),**短超时 8s + 至多 1 次重试**。
3. **retry 也走控制平面**:抽共享私有方法 `enqueueWithControlPlane(...)``submitGenerate``retryTask` 共用Codex P0-2retry 直 insert 会绕过配额/背压/降级/GP9
4. **fail-closed 用中性提示,不新增枚举**:真违规 → `UNSAFE_PROMPT`safety 超时/失败 → 复用既有 `LLM_ERROR` + 中性提示"安全检查暂不可用,请稍后重试"`FailureReasonEnum` 跨契约共享、禁单边扩展)。
5. **各 P1/P2 修正**pom 路径改 `game-module-aigc-server/pom.xml`aigc-server 补 `huijing-module-infra-api` 依赖(否则注入 `ConfigApi` 编译失败);验收断言断**业务码 `1_101_001_*`**(不产生 HTTP 429/503背压判定用 `>=` 上限;并发 race 删"CAS 硬保证"错误背书改真理由level 加"手塞 L2 读 L2 档"单测。
---
## ① 目标与范围边界
### 1.1 目标
把"一句话生成"对真实创作者放开前,焊死 **2 道硬阻塞闸**,使开闸后单用户压不垮全局串行 worker、违规 prompt 进不了生成与 feed
- **D12 控制平面 v0**:在生成任务唯一入口 `AigcTaskServiceImpl.submitGenerate` 入队前加 **2 道前置门 + 降级门**(降级开关 / 配额+并发 / 队列深度背压)+ `game_aigc_task``level` 列;计费扣退只做**额度记账骨架**(非真扣费)。**`submitGenerate``retryTask` 共用同一套控制平面**(抽 `enqueueWithControlPlane`)。
- **GP9 合规先行段**:补 **10 条违规 prompt 负例集** + **在 `submitGenerate` 提交侧、入队前**接 `safety.prompt-check`(服务层独立 LLM 客户端)→ `safe=false` 即**同步拒绝、不入队**(抛 `UNSAFE_PROMPT` 业务异常)。
> **promptHash 仍由现有链算 + 落 provenance不变v0 只是不读它做缓存。** 原"门③ 缓存命中复用"推迟(见 §1.2 非范围)。
### 1.2 范围边界(非范围 · 勿展开 · 写此防越界)
| 非范围项 | 归属 | 不在本规格的理由 |
|---|---|---|
| **D12 真实计费扣退**(过门交付成功才扣 / 系统失败自动退) | 拆 M4 | 网关 new-api `AddToken` 硬编码 UserId 阻塞(普查 §newapi:21-22MVP 无真支付v0 只做额度记账骨架 |
| **promptHash 全局去重缓存(原门③)** | 推迟 | 跨用户归属/跨模块产物状态查询/sha256 口径需专门设计v0 防刷由配额门兜(同 prompt 重复也吃配额) |
| **组B 落库组**D11 就绪评分 / 9d trace 账本 / D9 反同质化) | 另排期 | 评审定为"放了要能管/追溯"v0 并行不卡开闸 |
| **审核台可见入口**D11 评分页 / 合规复核页) | 产品轨 | game-admin 现零 aigc 入口,属前端产品轨 |
| **会员档位数值**L1/L2/L3 各档日配额/并发数) | 待创始人 | 本规格只落 `level` 列与门结构,**具体数值取配置默认值 + 留待创始人拍**(见 §5.4 配额数值占位) |
> **本规格只交付组A 两块。** 上表五项任一被实现 = 越界,须打回。
---
## ② 前置条件
1. **代码基线**dev/2.0.0`game-module-aigc` 编译通过;执行器 `aigc.executor.enabled=true` 时 staging 可真跑M2 已收口,见 memory `generation-spike-c1-result`)。
2. **Flyway 现状**`huijing-server/.../db/migration/` 现有最大版本 = **V14.0.0**`V14.0.0__m4_trade_withdraw_transfer_ref.sql`,已核实)。本规格 `level` 迁移 **接 V15.0.0****不得与 M4 的 V14.0.0 撞号**。
3. **prompt 契约现状**`safety.prompt-check` 契约已就绪(`contracts/prompts/01-safety/prompt-safety-check.md`,输出 `{safe:boolean, reason}`已核实registry 已声明其 `eval: eval/safety.prompt-check/`(已核实),但 **eval 目录尚不存在**`find contracts/prompts/eval``safety.prompt-check/`,已核实)。
4. **关键架构事实(已核实,强约束下列设计)**
- 生成任务**唯一服务入口** = `AigcTaskServiceImpl.submitGenerate(reqVO, userId)`App 端 `AppAigcTaskController.submitGenerate` 与 studio 端 `AigcApi.submitGenerate``AigcApiImpl` → service**两臂共用此一处**(已核实 controller + AigcApi。→ 前置门 + GP9 接此单点 = 双臂同覆盖,无分叉(对齐评审 §5「后端单一入口」
- **`retryTask`:122 当前直接 `aigcTaskMapper.insert(retry)`:140 入队(已核实),绕过任何前置控制**。本规格抽共享 `enqueueWithControlPlane``submitGenerate``retryTask` 同走决策D
- LLM 通道 `ExecutorLlmClient` + prompt 装载 `PromptResourceLoader` **只在执行器内**、且经 `@ConditionalOnProperty(aigc.executor.enabled=true)` 装配(已核实 `AigcExecutorConfiguration`:45 + ExecutorLlmClient:35「本类不标 @Component,只经 @Bean 注册」)→ **`submitGenerate`(服务层)触达不到执行器 LLM 通道**。此事实决定 GP9 须**服务层另起独立 LLM 客户端**(见 §4.2 + §6.2)。
- maven-resources 当前只把 `prompts/04-config/*.md` + `templates/*.schema.json` 复制进 `classpath:wanxiang-contracts/`(已核实 **`game-module-aigc-server/pom.xml`:172-175**resources plugin 在 server 模块、非 parent `game-module-aigc/pom.xml`)→ **`01-safety/prompt-safety-check.md` 未进 classpath**GP9 需扩 copy-glob。
- aigc-server pom 现**仅依赖 `huijing-module-system-api`**:87已核实**无 `huijing-module-infra-api`** → 若降级开关走 infra `ConfigApi`,须先加该依赖(见 §6.1),否则注入编译失败。
---
## ③ 涉及模块与文件路径file:line · 已核实)
### 3.1 D12 — 改动/新增
| 文件 | 现状(已核实) | 本规格动作 |
|---|---|---|
| `game-module-aigc-server/.../service/task/AigcTaskServiceImpl.java` | `submitGenerate`:68入队前**无任何前置检查**:69-76 仅白名单+prompt非空+模板存在,:89 insert`retryTask`:122 **直 insert**:140 绕过控制(已核实);`sha256`:238 落 promptHash:85`cancelTask`:105 状态机已就位 | 抽私有 `enqueueWithControlPlane(...)`降级门→配额并发→背压→GP9 safety→insert`submitGenerate`+`retryTask` 同调§6.1 |
| `game-module-aigc-server/.../dal/mysql/task/AigcTaskMapper.java` | `BaseMapperX` 派生(含 MyBatis-Plus `selectCount`);已有 `selectClaimableQueued`/`claimQueuedTask`/`selectStaleRunning` | 加 2 个计数查询(配额计数 / 队列深度计数走既有索引§5.3 |
| `game-module-aigc-api/.../enums/ErrorCodeConstants.java` | aigc 段 `1-101-***-***`,子段 000 基础 / 002 状态机 / 003 回调(已核实)| 新增 **001 控制平面**子段错误码(配额超限 / 背压拒绝 / 降级暂停 / UNSAFE_PROMPT§5.5 |
| `huijing-server/.../db/migration/V15.0.0__aigc_task_add_level.sql` | 不存在 | 新建:`game_aigc_task``level`§5.1 DDL |
| `game-module-aigc-server/.../dal/dataobject/task/AigcTaskDO.java` | `level` 字段缺V2.0.0 无 level 列,已核实)| 加 `level` 字段(映射新列)|
| `game-module-aigc-server/.../*Properties.java`(新建 `AigcControlPlaneProperties` | 执行器配置绑 `aigc.executor`(已核实)| 配额/并发/背压阈值 + 降级开关键 + safety 短超时,**配置驱动**§5.4|
| `game-module-aigc-server/pom.xml` | 仅 `huijing-module-system-api`:87已核实| 加 `huijing-module-infra-api` 依赖(降级开关走 `ConfigApi`§6.1|
### 3.2 GP9 — 改动/新增
| 文件 | 现状(已核实) | 本规格动作 |
|---|---|---|
| `contracts/prompts/eval/safety.prompt-check/inputs.jsonl` + `labels.jsonl` + `README.md` | 目录不存在(已核实)| 新建10 条违规负例§5.6 + §6.2|
| **`game-module-aigc-server/pom.xml`**:172-175 | copy-glob 只含 `04-config/*.md`+`templates/*.schema.json`已核实resources plugin 在 server 模块)| 加 `<include>prompts/01-safety/*.md</include>` |
| `game-module-aigc-server/.../service/task/`**新建服务层 safety 客户端** | 执行器 `ExecutorLlmClient`/`PromptResourceLoader``aigc.executor.enabled` 门控、服务层注入不到(已核实)| 新建服务层独立 safety LLM 客户端OpenAI 兼容调 new-api+ 装载 `01-safety` prompt§6.2|
| `game-module-aigc-server/.../service/task/AigcTaskServiceImpl.java` | `submitGenerate`:68 既有校验:69-76 → 入队 | 在既有校验后、入队前调 safety§6.1 门序 ④);`safe=false``UNSAFE_PROMPT`safety 故障抛中性 `LLM_ERROR`|
| `FailureReasonEnum.java`:18 | `UNSAFE_PROMPT("unsafe_prompt",...)`:18 / `LLM_ERROR("llm_error",...)`:22 已声明;注释:10 明文"**不可单边扩展(跨契约共享枚举)**"(已核实)| 复用既有两枚举不新增UNSAFE_PROMPT 现为**提交时同步拒绝**业务码,非执行器终态 |
> **DifyCallbackTxService.java**:302 `promptHash` 已在组包 provenance 落库链已核实本规格不改回调写链promptHash 仍照算照落、**v0 不读它做缓存**(门③推迟)。
---
## ④ 数据流与依赖
### 4.1 D12 控制平面数据流(共享 `enqueueWithControlPlane` · submitGenerate + retryTask 同入)
```mermaid
flowchart TD
A1[submitGenerate<br/>App/studio 两臂] --> EQ
A2[retryTask<br/>重试入口] --> EQ
EQ[enqueueWithControlPlane 共享方法] --> V0[既有: validateCreator + prompt非空 + 模板存在]
V0 --> G0{降级门<br/>new-api 暂停开关?<br/>最便宜·无 LLM}
G0 -->|暂停中| R0[拒: AIGC_GENERATE_PAUSED 业务码]
G0 -->|放行| G1{门① 配额+并发<br/>per-creator×level 当日计数 + 并发?}
G1 -->|超限| R1[拒: AIGC_QUOTA_EXCEEDED 业务码 + 排队位]
G1 -->|通过| G2{门② 队列深度背压<br/>全局 queued+running 计数 >= 上限?}
G2 -->|超限| R2[拒: AIGC_BACKPRESSURE_REJECTED 业务码]
G2 -->|通过| G9{GP9 safety<br/>service 层独立 LLM·最贵·放最后<br/>safe?}
G9 -->|safe=false 违规| RB[拒: UNSAFE_PROMPT 业务码 不入队]
G9 -->|超时/失败| RF[拒: LLM_ERROR 中性提示 fail-closed 不入队]
G9 -->|safe=true| INS[落库 status=0 queued + level + 记账骨架]
INS --> OK[返回 taskId现行行为]
```
> **门顺序设计依据决策D**:① 降级门最先(最便宜、无 LLMnew-api 挂了直接挡)→ ② 配额+并发(纯计数)→ ③ 背压(纯计数)→ ④ GP9 safety**LLM、最贵、放最后**,免得给"超配额/超背压必被拒"的请求白烧一次 safety 调用)→ ⑤ insert。
> **safety 在配额门之后的语义**:超配额/超背压请求**不付 safety**;违规请求**付一次 safety 但在 insert 前被拒 = 不入队、不占配额、0 生成成本**。满足底稿"违规不入队"。
### 4.2 GP9 数据流(服务层提交侧 · 入队前 · 独立 LLM 客户端)
```mermaid
flowchart TD
S[submitGenerate/retryTask 经控制平面门①②通过] --> CK{GP9: safety.prompt-checktask.prompt}
CK -->|service 层独立 LLM 客户端<br/>OpenAI 兼容 → new-api| LLM[渲染 01-safety prompt<br/>短超时 8s + 至多 1 次重试]
LLM --> P{解析 safe?}
P -->|safe=false 真违规| BLOCK[抛 UNSAFE_PROMPT 业务异常<br/>同步拒绝·不入队·0 生成成本]
P -->|safe=true| INS[继续 insert 入队]
P -->|超时/失败·1 次重试后仍判不出| FC[fail-closed: 抛 LLM_ERROR<br/>中性提示 安全检查暂不可用请稍后重试<br/>不入队·用户可重试]
```
> **GP9 接线点 = 服务层 `submitGenerate` 提交侧、入队前决策B不接执行器**。理由:① 满足底稿验收"违规不入队";② 避开 opus P0-3——执行器内做 safety 会让 LLM 调用卡在串行 tick 线程上(最长 270s = 90s×3单条违规即拖死整条生成流水 = DoS。
> **不复用执行器 `ExecutorLlmClient`**:它经 `@ConditionalOnProperty(aigc.executor.enabled)` 门控,服务层注入不到(已核实)→ 服务层**另起独立 safety LLM 客户端**OpenAI 兼容直调 new-api**短超时(建议 8s+ 至多 1 次重试****不是**生成的 90s×3
> **成本核算(诚实)**:每次提交都付**一次便宜模型 safety 调用**(小额,独立于 worker 的 ¥0.15 预算),单独配置/记成本日志(见 §4.3 / §7。**不再有"执行器预检 0 生成成本即挡"的论证**——本规格在提交侧付一次 safety 成本,换取"违规不入队"。
### 4.3 依赖
- **外部**new-api 网关——
- **GP9 safety**:服务层独立 LLM 客户端OpenAI 兼容),**短超时 8s + 至多 1 次重试**(不复用执行器配方);**成本单独配置/记日志**,不混入 worker 的 ¥0.15 生成预算决策E / Codex P1-3
- 生成本身:执行器既有 `ExecutorLlmClient`(不受本规格改动)。
- **内部**`AigcTaskMapper`(计数查询)、服务层 safety prompt 装载、降级开关载体 infra `ConfigApi`(需补 `huijing-module-infra-api` 依赖§6.1)。
---
## ⑤ 接口/数据契约(契约先行)
### 5.1 `game_aigc_task``level` 列 — Flyway DDL 草案
文件:`huijing-server/src/main/resources/db/migration/V15.0.0__aigc_task_add_level.sql`
```sql
-- =============================================================================
-- 契约 #2 DB 迁移 | 模块aigc | W-G1 组A · D12 控制平面 v0
-- 文件V15.0.0__aigc_task_add_level.sqlFlyway 只新增;接 V14.0.0 之后,勿与 M4 撞号)
-- 内容game_aigc_task 加 level 列 —— per-creator×level 配额门的分档维度L1/L2/L3 会员档)
-- 默认值1= L1存量任务回填 L1与"白名单仅 A2"现状一致档位数值由配置驱动DDL 只承载维度)
-- =============================================================================
ALTER TABLE `game_aigc_task`
ADD COLUMN `level` TINYINT NOT NULL DEFAULT 1
COMMENT '创作者会员档位配额分档维度1=L1 2=L2 3=L3存量回填 L1档位额度数值见 aigc 控制平面配置'
AFTER `creator_user_id`;
-- 配额计数走 (creator_user_id, level, status, create_time) 维度;现有 idx_creator_status 已覆盖
-- (creator_user_id, status) 前缀,按日计数追加 create_time 范围条件即可(评估期若慢查再补复合索引,
-- v0 量级小不预建——遵循"量大再独立表/索引"取舍,评审版 §4
```
> **level 取值来源(需确认 · 附录 B3**submitGenerate 入参 `AigcGenerateReqVO` 当前无 level 字段已核实。v0 实现取值有两条路:(a) 从创作者身份/会员档查(最干净,但会员档数据模型未建);(b) 入队时**统一落 L1**(与"白名单仅 A2"现状一致,配额按 L1 档生效)。**建议 v0 走 (b) 落 L1 占位**,会员档真实化后再接 (a),避免为未建的会员体系造孤儿字段。**注**level 列虽 v0 统一落 L1但分档逻辑须 v0 即可用——§8 加"手塞 level=2 读 L2 档"单测覆盖免会员档接入时才首跑决策E
### 5.2 `safety.prompt-check` 调用契约(已就绪 · 本规格只接线)
输入:`{{input.prompt}}` = `task.getPrompt()`(创作者一句话)。
输出(契约固定,已核实 `prompt-safety-check.md`:11-16
```json
{"safe": true | false, "reason": "<简短中文理由>"}
```
接线消费(**服务层提交侧**):服务层独立 safety LLM 客户端渲染该 prompt → 调 new-api短超时 8s + 1 重试)→ 解析 `safe`
- `safe==false` → 抛 `ServiceException(UNSAFE_PROMPT)`(同步拒绝、不入队)。
- 超时/失败1 重试后仍判不出)→ fail-closed`ServiceException(LLM_ERROR, 中性提示)`决策C
**幂等**safety 检查是只读判定、无副作用;同 prompt 重复检查结果一致(除模型抖动);拒绝在 insert 之前 = 任务根本未落库,无终态写入、无幂等风险。
### 5.3 配额/背压查询 — SQL 形状(走既有索引)
```text
门① 配额计数per-creator×level 当日非取消计数):
SELECT COUNT(*) FROM game_aigc_task
WHERE creator_user_id = ? AND level = ? AND deleted = 0
AND status != 5 (canceled) -- 取消的不占配额
AND create_time >= <当日 00:00>;
→ >= 日配额上限 → 拒 AIGC_QUOTA_EXCEEDED。走 idx_creator_status 前缀 + create_time 过滤。
门① 并发上限L1=1该用户在飞任务数
SELECT COUNT(*) FROM game_aigc_task
WHERE creator_user_id = ? AND deleted = 0
AND status IN (0 queued, 1 running); -- 未终态 = 在飞
→ >= level 并发上限L1=1→ 拒 AIGC_QUOTA_EXCEEDED并发位满
门② 队列深度背压(全局在飞计数):
SELECT COUNT(*) FROM game_aigc_task
WHERE deleted = 0 AND status IN (0 queued, 1 running);
→ >= 全局队列深度上限queue-depth-limit→ 拒 AIGC_BACKPRESSURE_REJECTED。走 idx_status_create。
```
> **背压/配额判定一律用 `>= 上限`**决策E / Codex P2若用 `>` 则"等于上限"时仍放第 N+1 个,实际超额一位。配额、并发、背压三处统一 `>=`
> **MyBatis-Plus 实现**:用 `BaseMapperX``selectCount(LambdaQueryWrapperX)`,对齐 `AigcTaskMapper` 既有 default 方法风格(已核实),零原生 SQL、零 DDL 索引新增v0
### 5.4 控制平面配置(配置驱动 · 数值占位)
绑定前缀建议 `aigc.control-plane`(与执行器 `aigc.executor` 平级,新建 `AigcControlPlaneProperties` 隔离职责):
```yaml
aigc:
control-plane:
enabled: true # 控制平面总开关false = 全门旁路feature-flag 回滚§10
queue-depth-limit: 50 # 门② 全局在飞上限(占位值,按 worker 串行吞吐定,需确认)
safety:
timeout-ms: 8000 # GP9 safety 短超时决策B非生成 90s×3
max-retry: 1 # GP9 safety 至多 1 次重试
fail-closed: true # safety 判不出 → fail-closed 拒绝(中性 LLM_ERROR决策C
quota: # 门① per-level 日配额 + 并发(数值占位·待创始人 §1.2
L1: { daily: 10, concurrency: 1 }
L2: { daily: 50, concurrency: 2 }
L3: { daily: 200, concurrency: 5 }
# 降级开关不放此处静态配置,走 infra ConfigApi 热改§6.1):键 aigc.generate.paused
```
> **数值全为占位**(评审版定"会员档位数值待创始人")。本规格落"门结构 + 配置键 + 安全默认"**具体额度不在本规格拍板**。`enabled=false` 默认值的选择见 §10开闸前置 true但提交评审时默认 false 以零行为变更)。
### 5.5 新增错误码aigc 段 001 子段)
`ErrorCodeConstants.java` 追加(不与 000/002/003 重叠):
```java
// ========== 生成控制平面 1-101-001-***W-G1 组A · D12 + GP9==========
/** 配额超限per-creator×level 日配额或并发上限已满 */
ErrorCode AIGC_QUOTA_EXCEEDED = new ErrorCode(1_101_001_001, "今日生成额度已用完或并发已满,请稍后再试");
/** 背压拒绝:全局生成队列深度达上限(保护全局串行 worker */
ErrorCode AIGC_BACKPRESSURE_REJECTED = new ErrorCode(1_101_001_002, "生成排队过多,请稍后再试");
/** 生成暂停降级开关开启new-api 故障等),暂停受理新生成 */
ErrorCode AIGC_GENERATE_PAUSED = new ErrorCode(1_101_001_003, "生成服务繁忙维护中,请稍后再试");
// 注GP9 真违规复用 FailureReasonEnum.UNSAFE_PROMPT 业务码(提交侧同步拒绝);
// safety 故障 fail-closed 复用 FailureReasonEnum.LLM_ERROR + 中性提示决策C禁单边扩展枚举
```
> **口径**`ServiceException` 经全局异常 handler 统一转 `CommonResult.error(业务码, msg)`**不产生 HTTP 429/503**决策E。上列码均为业务码HTTP 仍 200body.code 非 0。验收断言断业务码、非 HTTP 状态§8
### 5.6 GP9 拒绝回包形状(提交侧同步拒绝)
GP9 在**服务层提交侧**阻断(入队前),故对用户的呈现 = `submitGenerate` 调用**当场同步报错**(非任务终态轮询):
```text
真违规safe=false
submitGenerate 抛 ServiceException(UNSAFE_PROMPT) → 全局 handler → CommonResult.error(unsafe_prompt 码,"Prompt 不安全")
→ 任务未落库(不入队);前端当场提示用户改 prompt。
safety 故障 fail-closed超时/失败1 重试后仍判不出):
submitGenerate 抛 ServiceException(LLM_ERROR,"安全检查暂不可用,请稍后重试") → CommonResult.error
→ 任务未落库;前端提示"稍后重试"用户可重试new-api 恢复后过门)。
```
> **统一口径决策C**真违规→UNSAFE_PROMPTsafety 故障→中性 LLM_ERROR**不复用 UNSAFE_PROMPT、不新增枚举**),避免 new-api 抖动时正常用户被判"内容不安全"而流失。两态均为提交侧同步业务异常。
---
## ⑥ 关键实现步骤
### 6.1 D12 — 共享 `enqueueWithControlPlane` + 降级 + level 迁移
抽私有方法 `enqueueWithControlPlane(AigcTaskDO task, int level)`**`submitGenerate``retryTask` 都调它**决策Dretry 也重跑 safety+配额,违规/超配额请求无法靠 retry 绕开)。门序固定(最便宜→最贵):
1. **降级门**(最先,无 LLM读降级开关 → 暂停中抛 `AIGC_GENERATE_PAUSED`
2. **配额门①**(纯计数):查 §5.3 日配额计数 + 并发计数 → 任一 `>= aigc.control-plane.quota.{level}` 上限 → 抛 `AIGC_QUOTA_EXCEEDED`
3. **背压门②**(纯计数):查 §5.3 全局在飞计数 `>= queue-depth-limit` → 抛 `AIGC_BACKPRESSURE_REJECTED`
4. **GP9 safety**LLM最贵放最后调服务层独立 safety 客户端§6.2)→ `safe=false``UNSAFE_PROMPT`;超时/失败抛中性 `LLM_ERROR`fail-closed。**放最后**=超配额/超背压必被拒的请求不付 safety违规请求付一次 safety 但在 insert 前被拒(不入队、不占配额)。
5. **insert**:四门全过 → 既有落库(`aigcTaskMapper.insert`+ 落 `level`v0=L1§5.1+ **额度记账骨架**(仅 log/占位字段,非真扣费——记账=记录"本次拟扣额度"于日志/可选字段,真扣退随 M4
接线收口:
- `submitGenerate`:68——把现有 :69-89 的「校验→insert」改为「既有校验 → 组装 task → `enqueueWithControlPlane(task, level)`」。
- `retryTask`:122——把 :135-140 的「组装 retry → 直 insert」改为「组装 retry → `enqueueWithControlPlane(retry, level)`retry 重跑全门,正确)。
- **Flyway V15.0.0**:按 §5.1 DDL 加 `level` 列;`AigcTaskDO``level` 字段。
**降级开关载体 = infra `ConfigApi`决策E**
- aigc-server **现无 `huijing-module-infra-api` 依赖**(仅 `huijing-module-system-api`:87已核实**须先在 `game-module-aigc-server/pom.xml` 加 `huijing-module-infra-api` 依赖**,否则注入 `ConfigApi` 编译失败。
- 配置 key 种子:`aigc.generate.paused`infra `infra_config`admin 后台可改、热生效、无需重启,契合"new-api 故障→暂停入队"运维诉求)。
- 缺省语义:`getConfigValueByKey("aigc.generate.paused")` 读不到 / 非 "true" = **视为未暂停**fail-open on switch read避免配置中心抖动→全站停服
### 6.2 GP9 — 10 负例 + 服务层独立 safety 客户端接线
1. **建负例集** `contracts/prompts/eval/safety.prompt-check/`(格式照既有 eval 套件,已核实 `config.clicker-designer/` 结构):
- `inputs.jsonl`10 行,每行 `{"evalKey":"safety:neg:N","prompt":"<违规 prompt>","category":"<类目>"}`,覆盖红线类目:**政治敏感 / 色情低俗 / 暴恐血腥 / 侵权仿冒 / 赌博诈骗 / 毒品 / 未成年人不适宜 / 注入攻击("忽略上述规则"类)/ 自我伤害 / 违法交易**10 条各 1 类,确保类目覆盖广而非同质)。
- `labels.jsonl`10 行,每行 `{"evalKey":"safety:neg:N","expectedSafe":false,"category":"<同上>"}`(全部 `expectedSafe:false` = 负例,门禁断言 100% 判 unsafe
- `README.md`:照既有 eval README 范式写来源/格式/变更纪律(改 prompt 须升 version 过四道闸)。
2. **扩 copy-glob****`game-module-aigc-server/pom.xml`**:172 区块加 `<include>prompts/01-safety/*.md</include>`(使 safety prompt 进 `classpath:wanxiang-contracts/`resources plugin 在 server 模块决策E
3. **新建服务层独立 safety LLM 客户端**`game-module-aigc-server/.../service/task/` 下):
- **不复用** 执行器 `ExecutorLlmClient`/`PromptResourceLoader`(被 `aigc.executor.enabled` 门控、服务层注入不到,已核实)。
- 自装载 `wanxiang-contracts/prompts/01-safety/prompt-safety-check.md`safety 无模板 schema渲染只需 `{{input.prompt}}` 单变量)。
- OpenAI 兼容调 new-api**短超时 8s + 至多 1 次重试**§5.4 配置成本单独记日志§4.3)。
- 装配建议随 `aigc.control-plane.enabled` 总开关(关 = GP9 旁路§10不被执行器开关连带。
4. **接线 `submitGenerate` 提交侧**(经 `enqueueWithControlPlane` 门序 ④§6.1
- 渲染 safety prompt`prompt`=task.getPrompt())→ 调服务层 safety 客户端 → 解析 `safe`
- `safe==false` → 抛 `ServiceException(UNSAFE_PROMPT)`(同步拒绝、不入队)。
- 超时/失败1 重试后仍判不出) → fail-closed`ServiceException(LLM_ERROR, "安全检查暂不可用,请稍后重试")`§5.6 / §7
- `safe==true` → 继续 §6.1 步骤 5 insert。
> **复用既有枚举、不改 `FailureReasonEnum`**`UNSAFE_PROMPT`:18 / `LLM_ERROR`:22 已声明,注释:10 明文"不可单边扩展(跨契约共享枚举)"已核实。GP9 只是让 UNSAFE_PROMPT 从"执行器终态占位"变为"提交侧同步拒绝业务码",并复用 LLM_ERROR 承载 fail-closed 中性态——零枚举改动。
---
## ⑦ 边界失败路径(逐条交代幂等/超时/失败/补偿)
| 失败场景 | 处置 | 幂等/补偿 |
|---|---|---|
| **配额超限**(门①) | 抛 `AIGC_QUOTA_EXCEEDED`(业务码),消息含"今日额度已满/并发已满"。前端展示排队位 = 当前在飞数 / 上限 | 纯读判定,无副作用;用户重试仍被挡,幂等 |
| **背压拒绝**(门②) | 抛 `AIGC_BACKPRESSURE_REJECTED`(业务码)。建议前端"稍后重试",不自动重试(防雪崩) | 同上,纯读 |
| **真违规**GP9 safety `safe=false` | 抛 `UNSAFE_PROMPT` 业务异常,**同步拒绝、不入队、0 生成成本**。提交侧当场报错(非任务终态) | 拒绝在 insert 前 = 任务未落库,无终态写入、无补偿需求 |
| **safety 服务超时/失败**GP9 LLM 挂) | **fail-closed 中性拒绝**决策C短超时 8s + **至多 1 次重试** 后仍判不出 → 抛 `LLM_ERROR` + 中性提示"安全检查暂不可用,请稍后重试"**不复用 UNSAFE_PROMPT、不新增枚举**),同步拒绝、不入队。**理由**:合规是法务红线,"判不出就放过"会让违规在 safety 不可用窗口直入生成与 feed风险不可逆用中性提示避免 new-api 抖动时正常用户被误标"内容不安全"而流失。**大面积故障兜底**降级开关§6.1)可整体暂停入队,避免逐条 fail-closed 刷错误 | 提交侧同步拒绝、任务未落库用户可直接重试new-api 恢复后过门)= 天然补偿,无额外补偿逻辑 |
| **并发 race两请求同时过配额门**(门①查时都未超,落库后双超) | v0 **承认软超**:配额门是"读计数→判→落库"非原子,高并发下可能略微超额(如 L1 并发=1 时两请求同瞬到、都读到 0 在飞、都入队 = 实际 2 在飞)。**v0 容忍**(量级小、超额有限);**严格化留待后续(附录 B6**:可加 (creator_user_id, level) 维度唯一/乐观锁或 Redis 原子计数,但 v0 不引入(避免为边际场景上分布式锁,对齐评审"量大再独立表"取舍)。**真理由决策E删错误的"CAS 硬保证"背书)**:全局只有**单作业串行 worker**`AigcGenerateExecutor` `@Scheduled` fixedDelay 进程单飞,:171/:47 已核实)兜吞吐——同一时刻至多一任务在跑,故"实际多入队几个 queued"只是排队多、压不垮 worker。**per-creator 并发=1 是软限**(配额门唯一实现、**无 CAS 兜底**——CAS 认领只保证"同一任务不被双实例认领",不保证"同一 creator 不超并发")。开闸期 creator 少,软超有限、可接受 | 软超可接受;单作业串行 worker 保证不会真把全局压垮CAS 保证同一任务不双跑 |
| **降级开关读取失败**infra config 不可达) | fail-open on switch读不到开关值 = 视为未暂停(正常受理),避免"配置中心抖动→全站停止生成" | 无副作用 |
| **level 列迁移失败**V15.0.0 执行错) | Flyway 失败即 app 启动失败(既有 Flyway 行为);回滚写新补偿迁移(不改 V15.0.0,遵 V2.0.0 头注铁律) | DDL 幂等由 Flyway 版本表保证 |
| **取消信号在途 worker**(用户在生成中途取消) | 复用执行器既有语义(已核实):`cancelTask` 置 status=5CAS 认领 `claimQueuedTask` 仅 status=0 可认领,已取消任务认领落空跳过;回调链终态拒重入丢弃在途结果 | 既有幂等,本规格零改 |
> **safety 成本Codex P1-3**GP9 safety 每次提交付一次便宜模型调用,**成本单独配置/记日志**§4.3 / §5.4**不混入 worker 的 ¥0.15 生成预算**——避免把"提交侧合规成本"错算进"单款生成成本"台账。
---
## ⑧ 验证方法(对齐评审 §6每门一条自动化用例
| 门 | 自动化用例(断言) | 层级 |
|---|---|---|
| **D12 门①配额** | 同 creator 当日提交达 L1 日配额上限 → 下一次 `submitGenerate``AIGC_QUOTA_EXCEEDED`(断**业务码 `1_101_001_001`**,非 HTTP 429另一 creator 不受影响 | service 单测mock mapper 计数)|
| **D12 门①并发** | L1 并发=1该 creator 有 1 个在飞status∈{0,1})时再提交 → 抛 `AIGC_QUOTA_EXCEEDED`(并发位满);该任务终态后可再提交 | service 单测 |
| **D12 门②背压** | 全局在飞数 **达** `queue-depth-limit``>=`,断"等于上限即拒")→ `submitGenerate``AIGC_BACKPRESSURE_REJECTED`(断业务码 `1_101_001_002` | service 单测 |
| **D12 降级** | 降级开关 `aigc.generate.paused=true``submitGenerate``AIGC_GENERATE_PAUSED`(业务码 `1_101_001_003`);置回恢复 | service 单测mock ConfigApi|
| **D12 level 分档** | **手动塞 `level=2` → 读 L2 配额档生效**L2 daily/concurrency 与 L1 不同,断分档逻辑 v0 即可用非会员档接入才首跑决策E | service 单测 |
| **D12 retry 走控制平面** | `retryTask` 在配额已满 / 降级开启 / 违规 prompt 下 → **同样被拒**(断 retry 不绕过控制平面决策D | service 单测 |
| **GP9 负例 100%** | 10 条负例逐条经 safety 判定 → 100% `safe=false``submitGenerate` 100% 抛 `UNSAFE_PROMPT`、**0 条落库入队**(断 insert 未被调用 / 不派发 worker | service 单测mock safety 客户端返回 safe=false+ 可选 staging 真 LLM 抽测 |
| **GP9 fail-closed 中性** | safety LLM 抛超时mock→ 1 重试后仍失败 → `submitGenerate`**`LLM_ERROR`(中性提示)**、不入队(断**不复用 UNSAFE_PROMPT**、不新增枚举决策C | service 单测 |
> **断言口径决策E**:所有验收断言断 `ServiceException` 的**业务码 `1_101_001_*`**(经全局 handler 转 `CommonResult.error`HTTP 仍 200**不断 HTTP 429/503**。
> **GP9 负例 100% 阻断的"真判定"建议**:单测 mock safety 客户端返回固定 `safe=false` 验**接线正确**safe=false→抛 UNSAFE_PROMPT另在 staging 用真 new-api 跑 10 负例验**模型真能判违规**(防 prompt 本身判不准)——两层分开,前者门禁必跑、后者上线前抽验。
---
## ⑨ 完成条件
1. **D12**:抽 `enqueueWithControlPlane``submitGenerate` + `retryTask` 同走;降级门 + 配额并发门 + 背压门接入V15.0.0 `level` 迁移落库、`AigcTaskDO` 加字段aigc-server 补 `huijing-module-infra-api` 依赖;新增 001 段 3 个错误码控制平面配置键就位数值占位§8 D12 六条用例绿(含 level 分档、retry 走控制平面)。
2. **GP9**`safety.prompt-check` 10 负例集落 `contracts/prompts/eval/safety.prompt-check/`**`game-module-aigc-server/pom.xml`** copy-glob 扩 01-safety新建服务层独立 safety 客户端(短超时 8s + 1 重试);`submitGenerate` 提交侧接 safety → `safe=false``UNSAFE_PROMPT`、故障 fail-closed 抛中性 `LLM_ERROR`§8 两条 GP9 用例绿(含 10 负例 100% 阻断、fail-closed 中性)。
3. **编译/构建**`game-module-aigc` 编译通过;`huijing-server` Flyway 迁移在 staging 隔离库执行通过(去 mini-desktop 验,对齐 memory `m1-runtime-bringup-state`)。
4. **零越界**§1.2 五非范围项均未实现;**门③promptHash 缓存命中复用)未做**非范围的回调写链、组B 落库、审核台、会员档数值均未触碰。
5. **行为回滚验证**`aigc.control-plane.enabled=false`(或开关关)时 `submitGenerate`/`retryTask` 行为**与现行逐字一致**全门旁路、GP9 旁路),单测断言。
---
## ⑩ 回滚策略feature-flag 默认关 = 现行逐字不变)
- **D12 + GP9 回滚 = `aigc.control-plane.enabled=false`**(或控制平面 Bean `@ConditionalOnProperty` 关):全门 + GP9 旁路,`enqueueWithControlPlane` 退化为"仅既有校验→insert"的现行行为(`submitGenerate`/`retryTask` 逐字不变)。**提交评审时该开关默认 false**(零行为变更进主干),开闸前由创始人显式置 true对齐评审版"前置门 feature-flag 默认关 = 现行行为不变"§5
- **GP9 旁路**safety 客户端随 `aigc.control-plane.enabled` 装配;关闭 = 跳过 safety现行行为。负例集 / pom glob / safety 客户端装载均为 additive不开关也不影响存量。
- **level 列**additive 列、`DEFAULT 1`,存量行自动回填 L1不读不影响真要回滚写 V16 补偿迁移 drop不改 V15.0.0)。
- **infra-api 依赖**additive 依赖,不影响现有编译/运行(仅为 `ConfigApi` 注入)。
- **错误码**:新增 001 段,与现有段无重叠,纯 additive。
---
## 附:核实清单 + 两轮评审处置
### A. 已核实 file:line亲读确认
- `submitGenerate`:68、入队前无前置门:69-76、insert:89、`retryTask`:122 **直 insert**:140决策D 共享入队的改造点、promptHash 落库:85、`sha256`:238、`cancelTask`:105 —— `AigcTaskServiceImpl.java`**路径 `game-module-aigc-server`,非底稿提示的 `-biz`**)。
- 执行器现成件:`@Scheduled` fixedDelay 进程单飞:171/:47、CAS 认领 `claimQueuedTask`:327、watchdog 收尸、job `budget={maxYuan:0.15,maxLlmCalls:8}`、预检段 `runGenerationPipeline`:354、`callbackFailed`:588 —— `AigcGenerateExecutor.java`(核实)。**注**:全局并发兜底 = 单作业串行 workerfixedDelay 单飞),**非 SemaphoreCAS 只保证同一任务不双认领,不保证 per-creator 并发**决策E 真理由依据)。
- `FailureReasonEnum.UNSAFE_PROMPT`:18 / `LLM_ERROR`:22注释:10 明文"**取值与 Dify output.failureReason 严格一致,不可单边扩展(跨契约共享枚举)**"(核实)→ 决策C 复用既有枚举的硬依据。
- LLM 通道 `ExecutorLlmClient`:35「不标 @Component,只经 @Bean 注册」+ `AigcExecutorConfiguration`:45 `@ConditionalOnProperty(prefix="aigc.executor",name="enabled",havingValue="true")`(核实)→ **服务层注入不到执行器 LLM 通道**决策B 须服务层另起独立客户端的硬依据。
- `game-module-aigc-server/pom.xml`:152 maven-resources-plugin、:172-175 copy-glob 仅 `04-config/*.md`+`templates/*.schema.json`(核实,**resources plugin 在 server 模块、非 parent `game-module-aigc/pom.xml`**)→ 决策E pom 路径修正依据01-safety 未进 classpath。
- aigc-server pom 仅 `huijing-module-system-api`:87、**无 `huijing-module-infra-api`**(核实)→ 决策E 补依赖依据。
- `game_aigc_task` 全列status 0-5 含 canceled、retry_of、finish_time、trace_id、prompt_hash、**无 level 列**(核实 V2.0.0)。
- Flyway 现有最大 = **V14.0.0**M4→ level 迁移定 **V15.0.0**(核实,不撞号)。
- safety 契约 `{safe,reason}`:11-16、registry 声明 `eval:`(核实);**eval/safety.prompt-check/ 目录不存在**(核实)。
- 两臂共用单入口App controller + `AigcApi.submitGenerate`→service核实
### B. 两轮评审Codex 可行性 + opus 前提)处置结论
| # | 评审存疑 | 处置(主代理定盘) |
|---|---|---|
| B1 | GP9 接线点(执行器 vs 提交侧) | **safety 前置到 `submitGenerate` 提交侧、入队前**决策B——满足"违规不入队"、避开 opus P0-3执行器内 safety 卡串行 tick = DoS服务层独立 LLM 客户端 + 短超时 8s + 1 重试。 |
| B2 | safety 失败 fail-closed vs fail-open | **fail-closed 中性**决策C——真违规→`UNSAFE_PROMPT`;超时/失败→复用既有 `LLM_ERROR` + 中性提示"安全检查暂不可用,请稍后重试"(不新增枚举、不误标用户违规)。 |
| B3 | level 取值来源 | **level 列保留**v0 统一落 L1 占位(会员档未建,避孤儿);**§8 加"手塞 level=2 读 L2 档"单测**,给分档逻辑 v0 覆盖决策E。 |
| B4 | 降级开关载体 | **infra `ConfigApi`**admin 热改、无需重启fail-open on read——但 **aigc-server 须先补 `huijing-module-infra-api` 依赖**否则注入编译失败决策E`aigc.generate.paused`,读不到=不暂停。 |
| B5 | 门③ promptHash 命中复用 | **随门③砍而 moot**决策A——v0 不做全局去重缓存(跨用户归属/产物状态查询/sha256 口径需专门设计,孤儿+隐私+白嫖风险);防刷由配额门兜(同 prompt 重复也吃配额。promptHash 仍照算照落 provenancev0 只是不读它做缓存。 |
| B6 | 并发 race 软超是否可接受 | **删"CAS 认领层有硬保证"错误背书,改真理由**决策E——全局单作业串行 workerfixedDelay 单飞兜吞吐per-creator 并发=1 是软限(配额门唯一实现、无 CAS 兜底);开闸期 creator 少、软超有限、v0 可接受。 |
| B7 | 配额/背压数值 | **数值占位**(评审版已定"待创始人",非存疑)——本规格只落门结构 + 配置键 + 安全默认,具体额度不在本规格拍板。 |
| B8 | `enabled` 默认值 | **采纳默认 false**——提交评审默认 false零行为变更进主干开闸前创始人显式置 true。 |
> **新增评审项落地**retry 绕过控制平面Codex P0-2→ 决策D 抽 `enqueueWithControlPlane` 共享方法;背压 `>` vs `>=`Codex P2→ 决策E 统一 `>=`safety 成本混入 worker 预算Codex P1-3→ 决策E safety 成本单独配置/记日志;验收断 HTTP 429/503口径雷→ 决策E 改断业务码 `1_101_001_*`