feat(aigc): D11 质量评分参数外置进配置集(W-CFG-EXT E1-E3)
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled

背景/目的:D11 就绪评分的四维权重与效率预算阈值原本硬编码在 ReadinessScorer
里,放量校准要改代码换 jar;本提交把它们收进阶段二配置集治理,改一版即生效、可
回滚、可归因(SoT §10「校准走配置、不在现场拍脑袋」的配置载体,不改任何命题)。

范围/做法(E1-E3):
- E1 content_json 加第三块 readiness(十字段,值=ReadinessScorer 常量)+ 档位枚举
  加跨档特例值 READINESS(无 Nacos dataId、进程内消费);codec 加 readiness 值域
  sanity(四维权重和≈1.0±0.001/各∈[0,1]/预算>0/repairs_good≤ok/子项分∈[0,1])
  与 routeA/routeB 互斥;NO_ROUTE 判据扩为 !hasA&&!hasB&&!hasReadiness(空串/{}
  仍红)。
- E2 新 ReadinessConfigProvider(读激活版 readiness、短 TTL 缓存、逐字段回落、读失
  败不抛)+ GenConfigService.getActiveVersionBySetKey(按 uk_set_key 定位、读不到返
  null);ReadinessScorer 从 provider 取权重阈值、硬编码常量降为回落默认(一个不删)
  、@Autowired(required=false) 可选注入不破坏既有 new ReadinessScorer() 单测;顺修
  :19/:151 陈旧「预算<=0.15」注释(实际 0.6)。
- E3 激活编排一行不改:readiness-only 经 codec 产空载荷,现有 dispatch/reconcile
  在空载荷下天然短路成「值域 sanity + 账本收口、无投影、恒一致」。

建模:按计划倾向用枚举值 READINESS(§7 待拍点,创始人未明拍,可 revise 为与 tier
正交的 kind 维度)。契约先落(content_json.readiness schema/javadoc、
AigcConfigTierEnum、GenConfigService 接口、NO_ROUTE 错误码文案)再改实现。

验证:整 game-module-aigc-server 单测 385/0/0(7 skipped=需真基建 RealIT);api
经 -am 从源码重建绿;既有反射测试(CheapWorkerResultOutContractTest)与 score==95
锚点(SaaGraphDispatcherTraceTest)未破坏。真 Spring 上下文 wiring + 真后端 e2e
排部署窗口。

边界:W-CFG-KB(知识件外置)不在本提交(gated 别的依赖);admin 端 readiness 数字
字段编辑表单为 follow-up(本提交只做后端)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-05 18:06:05 -07:00
parent 8200aa08d0
commit 8724501f32
14 changed files with 930 additions and 29 deletions

View File

@ -23,7 +23,16 @@ public enum AigcConfigTierEnum {
/** 便宜档AI 轻参与的高质轻量游戏cheap-workerMiniMax-M3 路,软停线 cheap_rmb_hard_limit=¥10 */
CHEAP("cheap", "便宜档"),
/** 富游戏档AI 深参与的自治富游戏tier2 gen-workerPhaser软停线 rmb_hard_limit=¥50 */
TIER2("tier2", "富游戏档");
TIER2("tier2", "富游戏档"),
/**
* 质量评分档(W-CFG-EXT · 跨档特例):承载 D11 就绪评分参数(四维权重 + 效率预算阈值 + 稳定性两级阈值
* + 中性子项分),同时给便宜档/富游戏档的游戏打分,不属任何生成档 Nacos dataId无路 A/ B 外投影
* 消费方 {@link com.wanxiang.huijing.game.module.aigc.service.task.ReadinessScorer} 就在 game-cloud与配置集账本
* 同库同进程, ReadinessConfigProvider 进程内读激活版本行的 content_json.readiness;激活退化成值域 sanity +
* 账本指针前移的纯账本收口故其配置集内容与 routeA/routeB 互斥(codec 激活前置校验),也不参与按 cheap/tier2
* Nacos dataId 的下发逻辑
*/
READINESS("readiness", "质量评分");
/** 档位码(落库 tier 列) */
private final String tier;

View File

@ -91,7 +91,7 @@ public interface ErrorCodeConstants {
/** 值域 sanity 校验不通过:负预算/空模型名/阈值出域/max_tokens≤thinking/chat_model_config 不完整等(设计 §3.6,激活前置,拒绝不发任何一路)。占位符带明细。 */
ErrorCode AIGC_CONFIG_ACTIVATE_SANITY_FAILED = new ErrorCode(1_101_004_009, "配置值域校验不通过:{}");
/** 激活内容缺路由content_json 未声明可激活的 routeA/routeB无落点可下发。 */
ErrorCode AIGC_CONFIG_ACTIVATE_NO_ROUTE = new ErrorCode(1_101_004_010, "配置内容未声明任何可激活的下发路由routeA/routeB 均缺)");
ErrorCode AIGC_CONFIG_ACTIVATE_NO_ROUTE = new ErrorCode(1_101_004_010, "配置内容未声明任何可激活的下发路由routeA/routeB/readiness 均缺)");
/** 双路下发失败:某路 PATCH/publish 失败 → 整批判未生效、已把成功路补偿重推回上一激活版、指针不前移、可重试(设计 §3.6)。占位符带明细。 */
ErrorCode AIGC_CONFIG_ACTIVATE_DISPATCH_FAILED = new ErrorCode(1_101_004_011, "配置激活下发失败(已判未生效并补偿回上一激活版,可重试):{}");

View File

@ -10,8 +10,10 @@ package com.wanxiang.huijing.game.module.aigc.service.config;
* <li><b> B</b>{@link #pathBJson}可空publish Nacos 生效 dataId {@code area.key} 扁平 JSON预算/门阈值</li>
* </ul>
*
* <p>两路都可空但不能都空那样没有可激活的落点纯参数档可能只有路 B prompt/模型档可能只有路 A是否下发某路
* 由该路是否为空决定{@link #hasPathA()} / {@link #hasPathB()}
* <p>两路可各自为空:纯参数档可能只有路 B, prompt/模型档可能只有路 A是否下发某路由该路是否为空决定
* ({@link #hasPathA()} / {@link #hasPathB()})<b>两路皆空 = 空载荷</b>:这是 W-CFG-EXT 质量评分配置集的合法形态
* (readiness 块进程内消费无外投影),激活时现有 dispatch/reconcile 天然短路成纯账本收口;生成配置集(routeA/routeB)
* 则至少一路非空(codec 对既无路由又无 readiness 的空内容抛 NO_ROUTE)
*
* @param pathA A 载荷PATCH /agent+/session 的目标与请求体null = 本版本不下发路 A
* @param pathBJson B 载荷area.key 扁平 JSON 文本整内容 publishnull = 本版本不下发路 B

View File

@ -44,6 +44,23 @@ import static com.wanxiang.huijing.framework.common.exception.util.ServiceExcept
* 预算为正且落合理区间超时有下限system_prompt 非空deeper type credential type 匹配 Service PATCH
* {@code _ensure_credential_exists} 运行时兜凭据在 Service 存储game-cloud 侧看不到失败即路 A 下发失败补偿
*
* <p><b>第三块 readiness(W-CFG-EXT · D11 质量评分参数, routeA/routeB 互斥)</b>:除按下发路由分的 routeA/routeB ,
* content_json 可含一个平级的 {@code readiness} ,承载 D11 就绪评分的十个标量(四维权重 + 中性分 + 效率预算阈值
* + 稳定性两级阈值与两级分)它是<b>跨档进程内消费无外投影</b>的特例不下发路 A/ B, game-cloud 进程内的
* {@code ReadinessConfigProvider} 直读激活版本行故本编解码器对只含 readiness 的配置集产出<b>空载荷</b>
* (pathA=pathB=null),现有 dispatch/reconcile 在空载荷下天然短路成值域 sanity + 账本收口无投影恒一致
* 字段名与默认起步值 = {@link com.wanxiang.huijing.game.module.aigc.service.task.ReadinessScorer} 的回落常量,契约:
* <pre>
* { "readiness": {
* "playability_weight": 0.50, "first_play_weight": 0.25,
* "stability_weight": 0.15, "efficiency_weight": 0.10,
* "neutral_score": 0.5, "efficiency_budget_rmb": 0.6,
* "stability_repairs_good_max": 0, "stability_repairs_ok_max": 2,
* "stability_ok_score": 0.7, "stability_bad_score": 0.4 } }
* </pre>
* 激活前置 readiness 值域 sanity(不过即拒):四维权重各 [0,1] 且和 1.0(±0.001)中性/稳定性两级分 [0,1]
* 效率预算 > 0修复轮 good_max ok_max;并与 routeA/routeB 互斥(readiness 存在时禁二者,反之亦然)
*
* @author 绘境AI
*/
@Slf4j
@ -70,18 +87,96 @@ public final class GenConfigContentCodec {
JsonNode root = parseRoot(contentJson);
JsonNode routeA = root.get("routeA");
JsonNode routeB = root.get("routeB");
JsonNode readiness = root.get(ReadinessConfigKeys.BLOCK);
boolean hasA = routeA != null && routeA.isObject();
boolean hasB = routeB != null && routeB.isObject() && routeB.size() > 0;
if (!hasA && !hasB) {
boolean hasReadiness = readiness != null && readiness.isObject();
if (!hasA && !hasB && !hasReadiness) {
// 无任何可下发路由没有落点设计 §4 步骤3 交付以双路下发为核心至少一路
throw exception(AIGC_CONFIG_ACTIVATE_NO_ROUTE);
}
// 互斥(W-CFG-EXT):质量评分块是跨档进程内消费无外投影,不与生成下发路(routeA/routeB)共存于同一配置集
// 若一个质量评分配置集误混 routeB,激活会按 tier=readiness 拼出伪 dataId(resolveDataId 不认档但仍拼) publish;
// 误混 routeA resolveServiceUrl null 下发失败故激活前置拒, enum-value 建模对既有按档下发逻辑安全
if (hasReadiness && (hasA || hasB)) {
throw sanity("readiness 块与 routeA/routeB 互斥:质量评分配置集不下发 prompt/模型/预算/门阈值,不得混配生成路");
}
if (hasReadiness) {
// 质量评分配置集:只做值域 sanity,不产任何外投影载荷空载荷(pathA=pathB=null) 现有 dispatch/reconcile
// 天然短路成sanity + 账本收口无投影恒一致(激活编排一行不改, GenConfigActivationServiceImpl.dispatch)
validateReadiness(readiness);
return new GenConfigActivationPayload(null, null);
}
GenConfigActivationPayload.PathA pathA = hasA ? buildPathA(prompt, routeA) : null;
String pathBJson = hasB ? buildPathB(routeB) : null;
return new GenConfigActivationPayload(pathA, pathBJson);
}
// ==================== readiness:D11 质量评分参数 值域 sanity(W-CFG-EXT) ====================
/**
* 校验 content_json.readiness 块值域(激活前置,不过即拒):四维权重各 [0,1] 且和 1.0(容差 ±0.001)
* 中性分与稳定性两级分 [0,1]效率预算 > 0修复轮 good_max ok_max字段全须齐(缺即拒)一版完整的 D11
* 参数集才准激活;运行期个别字段缺失的逐字段回落是 ReadinessConfigProvider 对陈旧/异常激活版的防御,不放宽本处
* 新激活须完整的闸(两个时刻:新激活须完整过闸,已激活版遇 schema 增长靠 provider 逐字段回落存活)
* 字段值域与默认起步值语义见 ReadinessScorer 各回落常量
*/
private void validateReadiness(JsonNode r) {
double wPlay = requireUnit(r, ReadinessConfigKeys.PLAYABILITY_WEIGHT);
double wFirst = requireUnit(r, ReadinessConfigKeys.FIRST_PLAY_WEIGHT);
double wStab = requireUnit(r, ReadinessConfigKeys.STABILITY_WEIGHT);
double wEff = requireUnit(r, ReadinessConfigKeys.EFFICIENCY_WEIGHT);
double sum = wPlay + wFirst + wStab + wEff;
if (Math.abs(sum - 1.0) > 0.001) {
throw sanity(String.format("readiness 四维权重和=%.4f 偏离 1.0(容差 ±0.001)", sum));
}
// 中性分 + 稳定性两级分:[0,1]
requireUnit(r, ReadinessConfigKeys.NEUTRAL_SCORE);
requireUnit(r, ReadinessConfigKeys.STABILITY_OK_SCORE);
requireUnit(r, ReadinessConfigKeys.STABILITY_BAD_SCORE);
// 效率维预算阈值:须为正
double budget = requireNumber(r, ReadinessConfigKeys.EFFICIENCY_BUDGET_RMB);
if (budget <= 0) {
throw sanity("readiness." + ReadinessConfigKeys.EFFICIENCY_BUDGET_RMB + "=" + budget + " 须为正");
}
// 稳定性修复轮阈值:good_max ok_max(整数)
int goodMax = requireInt(r, ReadinessConfigKeys.STABILITY_REPAIRS_GOOD_MAX);
int okMax = requireInt(r, ReadinessConfigKeys.STABILITY_REPAIRS_OK_MAX);
if (goodMax > okMax) {
throw sanity(String.format("readiness.%s(%d) 须 <= %s(%d)",
ReadinessConfigKeys.STABILITY_REPAIRS_GOOD_MAX, goodMax,
ReadinessConfigKeys.STABILITY_REPAIRS_OK_MAX, okMax));
}
}
/** 取必填数值字段(缺失/非数值即 sanity 拒)。 */
private double requireNumber(JsonNode r, String field) {
JsonNode v = r.get(field);
if (v == null || !v.isNumber()) {
throw sanity("readiness." + field + " 缺失或非数值");
}
return v.asDouble();
}
/** 取必填单位区间 [0,1] 字段(缺失/非数值/越界即拒)。 */
private double requireUnit(JsonNode r, String field) {
double v = requireNumber(r, field);
if (v < 0 || v > 1) {
throw sanity("readiness." + field + "=" + v + " 越界 [0,1]");
}
return v;
}
/** 取必填整数字段(缺失/非整数即拒;浮点如 2.5 也拒——修复轮阈值是整数)。 */
private int requireInt(JsonNode r, String field) {
JsonNode v = r.get(field);
if (v == null || !v.isIntegralNumber()) {
throw sanity("readiness." + field + " 缺失或非整数");
}
return v.asInt();
}
// ==================== A/agent + /session 请求体组装 + 值域校验 ====================
private GenConfigActivationPayload.PathA buildPathA(String prompt, JsonNode routeA) {

View File

@ -96,6 +96,22 @@ public interface GenConfigService {
*/
GenConfigVersionDO getVersion(Long configSetId, Long versionId);
/**
* 按业务键取当前激活版本行(供进程内消费方直读激活配置, D11 质量评分参数 W-CFG-EXT)
*
* <p> {@link #getVersion} 的差别:getVersion versionId 精确取任一历史版不存在即抛;本方法按配置集业务键
* 定位其当前激活版(active_version_id 指向的那一行),<b>读不到不抛返回 null</b>配置集不存在从未激活
* (active_version_id NULL)或激活版行缺失,都返 null,交调用方回落默认(消费侧非阻断:D11 打分读不到激活版
* 就用硬编码默认,绝不因配置缺失打挂回调)
*
* <p> setKey(而非 tier)定位:uk_set_key 保证至多一条,寻址无歧义;跨档的质量评分配置集是单例(约定业务键
* {@link ReadinessConfigKeys#READINESS_SET_KEY}), tier 取则一档可有多集有歧义,故走业务键
*
* @param setKey 配置集业务键
* @return 当前激活版本行 DO;配置集不存在/从未激活/激活版缺失 null
*/
GenConfigVersionDO getActiveVersionBySetKey(String setKey);
/**
* 两版 diff应用层做prompt 是否变更 + 两侧字节数结构化旋钮拍平成点路径逐项比对给增//
*

View File

@ -210,6 +210,18 @@ public class GenConfigServiceImpl implements GenConfigService {
return version;
}
@Override
public GenConfigVersionDO getActiveVersionBySetKey(String setKey) {
// 按业务键定位配置集(uk_set_key 至多一条);不存在 null(消费侧回落,不抛)
GenConfigSetDO set = configSetMapper.selectBySetKey(setKey);
if (set == null || set.getActiveVersionId() == null) {
// 配置集未建 / 从未激活(active_version_id NULL) 无当前激活版
return null;
}
// 取当前激活版指针指向的版本行(理论上必归属本集;缺失则 null,交调用方回落)
return configVersionMapper.selectByIdAndSetId(set.getActiveVersionId(), set.getId());
}
@Override
public GenConfigVersionDiffRespVO diffVersions(Long configSetId, Long versionAId, Long versionBId) {
GenConfigVersionDO a = getVersion(configSetId, versionAId);

View File

@ -0,0 +1,57 @@
package com.wanxiang.huijing.game.module.aigc.service.config;
/**
* D11 质量评分配置集(W-CFG-EXT)的内容契约常量:content_json {@code readiness} 块的块名与十个字段名,
* 外加质量评分配置集的约定业务键
*
* <p><b>为何单独成类</b>:这批字段名是配置集内容(doc)读取消费(code)之间的唯一约定
* {@link GenConfigContentCodec} 激活前置的 readiness 值域 sanity {@code ReadinessConfigProvider} 的逐字段解析
* 都引用它,把字段名收进一处避免两边各写一份字符串字面量漂移无人对账字段值(默认起步值)不在这里,
* {@link com.wanxiang.huijing.game.module.aigc.service.task.ReadinessScorer} 的回落默认常量里那里才是
* 热源缺失时用哪个数的权威,本类只管用哪个键名读
*
* <p>字段值域与默认值语义见 {@code ReadinessScorer} 各常量的 Javadoc;字段名采直白 snake_case,与打分器四维/
* 稳定性两级/中性分/效率预算一一对应
*
* @author 绘境AI(W-CFG-EXT · D11 参数外置)
*/
public final class ReadinessConfigKeys {
private ReadinessConfigKeys() {
}
/** content_json 里承载 D11 参数的块名(平级于 routeA/routeB,互斥)。 */
public static final String BLOCK = "readiness";
/**
* 质量评分配置集的约定业务键(set_key)跨档单例:全租户内质量评分配置集用这一个业务键,
* {@code ReadinessConfigProvider} 据它定位当前激活版(uk_set_key 保证至多一条,寻址无歧义)
* 运营在配置中心新建质量评分配置集时须用此业务键,否则 provider 找不到激活版打分器回落硬编码默认
*/
public static final String READINESS_SET_KEY = "readiness-default";
// ---- 四维权重(1.0)----
/** 可玩性权重。 */
public static final String PLAYABILITY_WEIGHT = "playability_weight";
/** 首局权重。 */
public static final String FIRST_PLAY_WEIGHT = "first_play_weight";
/** 稳定性权重。 */
public static final String STABILITY_WEIGHT = "stability_weight";
/** 效率权重。 */
public static final String EFFICIENCY_WEIGHT = "efficiency_weight";
// ---- 子项参数 ----
/** 数据缺失时的中性子项分。 */
public static final String NEUTRAL_SCORE = "neutral_score";
/** 效率维预算阈值(成本 ≤ 此值算满分)。 */
public static final String EFFICIENCY_BUDGET_RMB = "efficiency_budget_rmb";
/** 稳定性:修复轮 ≤ 此值算满分(1.0)。 */
public static final String STABILITY_REPAIRS_GOOD_MAX = "stability_repairs_good_max";
/** 稳定性:修复轮 ≤ 此值算次优(ok 分),超则劣(bad 分)。 */
public static final String STABILITY_REPAIRS_OK_MAX = "stability_repairs_ok_max";
/** 稳定性次优分(good < 修复轮 ≤ ok 时取值)。 */
public static final String STABILITY_OK_SCORE = "stability_ok_score";
/** 稳定性劣分(修复轮 > ok 时取值)。 */
public static final String STABILITY_BAD_SCORE = "stability_bad_score";
}

View File

@ -0,0 +1,142 @@
package com.wanxiang.huijing.game.module.aigc.service.config;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wanxiang.huijing.game.module.aigc.dal.dataobject.config.GenConfigVersionDO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* D11 质量评分参数的进程内读取面(W-CFG-EXT):当前激活的质量评分配置集version 行里的 content_json.readiness
* 解析成 {@link ReadinessView}, {@link com.wanxiang.huijing.game.module.aigc.service.task.ReadinessScorer} 打分时取用
*
* <p><b>为何进程内直读账本不走投影</b>:D11 参数的消费方就是同库同进程的打分器,不像 prompt/模型( A PATCH Service)
* 或预算/门阈值( B publish Nacos)要投影到外部系统所以这里直接读 MySQL 账本的激活版本行,是配置控制面阶段二
* 消费两路之外补齐的第三条消费口(game-cloud 进程内读激活版)
*
* <p><b> TTL 缓存,不接激活失效信号</b>:打分发生在回调落库这条本就 DB-heavy 的路上,一次缓存读可忽略;用短 TTL
* 失效(而非让激活链发进程内失效信号)把配置中心与打分器解耦激活一版权重后,最迟一个 TTL 内下一次打分即读到新值,
* 不必把 provider 耦合进激活编排TTL 很短,改一版即生效的体感仍在秒级
*
* <p><b>永不抛逐字段回落</b>:未建质量评分配置集 / 未激活 / DB 读异常 / content_json 非法 / 某字段缺失或类型不符
* 任一情形都不抛异常打挂回调,退化成该字段用打分器硬编码默认整份读不到 {@link ReadinessView#empty()}
* (全字段回落);读到但缺某字段 仅该字段为 null(仅它回落)回落方向永远收敛到硬编码默认,保证配置集出任何
* 问题,打分退化成现状绝不更差
*
* @author 绘境AI(W-CFG-EXT · D11 参数外置)
*/
@Slf4j
@Component
public class ReadinessConfigProvider {
/** content_json 解析器(无共享可变状态,线程安全)。 */
private static final ObjectMapper JSON = new ObjectMapper();
/** 缓存短 TTL 默认值(毫秒):激活一版后最迟这么久下一次打分读到新值。 */
private static final long DEFAULT_TTL_MS = 5_000L;
@Resource
private GenConfigService genConfigService;
/** 缓存 TTL(毫秒);测试可经包内构造器设 0 强制每次重读。 */
private final long ttlMs;
/** 缓存视图(volatile 读写;短窗内并发多读一次刷新只多几次 DB,可接受,不加锁保简单)。 */
private volatile ReadinessView cachedView;
/** 缓存过期时刻(epoch millis);now ≥ 它则重读。 */
private volatile long cacheExpireAtMs;
/** Spring 用:默认短 TTL。 */
public ReadinessConfigProvider() {
this.ttlMs = DEFAULT_TTL_MS;
}
/** 测试用(包可见):自定义 TTL(设 0 = 每次重读,便于验解析/回落而不受缓存干扰);genConfigService 由测试注入。 */
ReadinessConfigProvider(GenConfigService genConfigService, long ttlMs) {
this.genConfigService = genConfigService;
this.ttlMs = ttlMs;
}
/**
* 取当前激活质量评分配置集的 D11 参数视图( TTL 缓存;/失败逐字段回落)
*
* @return 只读视图(永不为 null;读不到则 {@link ReadinessView#empty()})
*/
public ReadinessView get() {
long now = System.currentTimeMillis();
ReadinessView cached = cachedView;
if (cached != null && now < cacheExpireAtMs) {
return cached;
}
ReadinessView fresh = load();
cachedView = fresh;
cacheExpireAtMs = now + ttlMs;
return fresh;
}
/** 读激活版 + 解析成视图;任何异常/缺失都收敛到全回落(empty),绝不抛。 */
private ReadinessView load() {
try {
GenConfigVersionDO version =
genConfigService.getActiveVersionBySetKey(ReadinessConfigKeys.READINESS_SET_KEY);
if (version == null) {
// 未建质量评分配置集 / 未激活过 全字段回落硬编码默认
return ReadinessView.empty();
}
JsonNode readiness = extractReadiness(version.getContentJson());
if (readiness == null) {
log.warn("[readiness-config] 激活版无 content_json.readiness 块,全回落硬编码默认 versionId={}", version.getId());
return ReadinessView.empty();
}
return parse(readiness);
} catch (Exception e) {
// DB 读异常等:退化成全回落,不打挂回调(非阻断哲学)
log.warn("[readiness-config] 读激活质量评分配置集失败,全回落硬编码默认", e);
return ReadinessView.empty();
}
}
/** 从 content_json 取 readiness 块(非法 JSON / 无该块 → null)。 */
private JsonNode extractReadiness(String contentJson) {
if (contentJson == null || contentJson.isBlank()) {
return null;
}
try {
JsonNode root = JSON.readTree(contentJson);
JsonNode readiness = root == null ? null : root.get(ReadinessConfigKeys.BLOCK);
return (readiness != null && readiness.isObject()) ? readiness : null;
} catch (Exception e) {
log.warn("[readiness-config] content_json 非法 JSON,全回落 len={}", contentJson.length());
return null;
}
}
/** 逐字段解析成视图:字段缺失/类型不符 → 该字段 null(仅它回落),不影响其余字段。 */
private ReadinessView parse(JsonNode r) {
return new ReadinessView(
doubleOrNull(r, ReadinessConfigKeys.PLAYABILITY_WEIGHT),
doubleOrNull(r, ReadinessConfigKeys.FIRST_PLAY_WEIGHT),
doubleOrNull(r, ReadinessConfigKeys.STABILITY_WEIGHT),
doubleOrNull(r, ReadinessConfigKeys.EFFICIENCY_WEIGHT),
doubleOrNull(r, ReadinessConfigKeys.NEUTRAL_SCORE),
doubleOrNull(r, ReadinessConfigKeys.EFFICIENCY_BUDGET_RMB),
intOrNull(r, ReadinessConfigKeys.STABILITY_REPAIRS_GOOD_MAX),
intOrNull(r, ReadinessConfigKeys.STABILITY_REPAIRS_OK_MAX),
doubleOrNull(r, ReadinessConfigKeys.STABILITY_OK_SCORE),
doubleOrNull(r, ReadinessConfigKeys.STABILITY_BAD_SCORE));
}
/** 取数值字段;缺失或非数值 → null(该字段回落)。 */
private static Double doubleOrNull(JsonNode node, String field) {
JsonNode v = node.get(field);
return (v != null && v.isNumber()) ? v.asDouble() : null;
}
/** 取整数字段;缺失或非整数(含浮点)→ null(该字段回落)。 */
private static Integer intOrNull(JsonNode node, String field) {
JsonNode v = node.get(field);
return (v != null && v.isIntegralNumber()) ? v.asInt() : null;
}
}

View File

@ -0,0 +1,92 @@
package com.wanxiang.huijing.game.module.aigc.service.config;
/**
* D11 质量评分参数的只读视图(W-CFG-EXT):当前激活质量评分配置集里 content_json.readiness 各字段的进程内快照
*
* <p><b>逐字段回落语义</b>:每个字段可空{@code null} 表示激活版里没有该字段(或未激活/读取异常整份为空)
* 取值 getter 都要求传入一个回落默认(打分器的硬编码常量),字段非空取激活版值字段空取回落默认
* 这样某个字段缺失只回落那一个,不因一个字段缺就整份弃用激活版在一处兑现,调用方(打分器)只管把自己的
* 常量作为回落默认传进来{@link #empty()} 是全空视图,任何字段都取回落默认等价于无激活版全用硬编码,
* 用于 provider 缺席(如未接配置中心的单测)或读取失败
*
* <p><b>只读无副作用</b>:构造后不可变;provider TTL 缓存的就是本视图实例,多线程打分共享读安全
*
* @author 绘境AI(W-CFG-EXT · D11 参数外置)
*/
public final class ReadinessView {
// 四维权重(null=激活版缺该字段回落)
private final Double playabilityWeight;
private final Double firstPlayWeight;
private final Double stabilityWeight;
private final Double efficiencyWeight;
// 子项参数
private final Double neutralScore;
private final Double efficiencyBudgetRmb;
private final Integer stabilityRepairsGoodMax;
private final Integer stabilityRepairsOkMax;
private final Double stabilityOkScore;
private final Double stabilityBadScore;
public ReadinessView(Double playabilityWeight, Double firstPlayWeight, Double stabilityWeight,
Double efficiencyWeight, Double neutralScore, Double efficiencyBudgetRmb,
Integer stabilityRepairsGoodMax, Integer stabilityRepairsOkMax,
Double stabilityOkScore, Double stabilityBadScore) {
this.playabilityWeight = playabilityWeight;
this.firstPlayWeight = firstPlayWeight;
this.stabilityWeight = stabilityWeight;
this.efficiencyWeight = efficiencyWeight;
this.neutralScore = neutralScore;
this.efficiencyBudgetRmb = efficiencyBudgetRmb;
this.stabilityRepairsGoodMax = stabilityRepairsGoodMax;
this.stabilityRepairsOkMax = stabilityRepairsOkMax;
this.stabilityOkScore = stabilityOkScore;
this.stabilityBadScore = stabilityBadScore;
}
/** 全空视图:任何字段取回落默认(等价「无激活版、全用硬编码常量」)。 */
public static ReadinessView empty() {
return new ReadinessView(null, null, null, null, null, null, null, null, null, null);
}
public double playabilityWeight(double fallback) {
return playabilityWeight != null ? playabilityWeight : fallback;
}
public double firstPlayWeight(double fallback) {
return firstPlayWeight != null ? firstPlayWeight : fallback;
}
public double stabilityWeight(double fallback) {
return stabilityWeight != null ? stabilityWeight : fallback;
}
public double efficiencyWeight(double fallback) {
return efficiencyWeight != null ? efficiencyWeight : fallback;
}
public double neutralScore(double fallback) {
return neutralScore != null ? neutralScore : fallback;
}
public double efficiencyBudgetRmb(double fallback) {
return efficiencyBudgetRmb != null ? efficiencyBudgetRmb : fallback;
}
public int stabilityRepairsGoodMax(int fallback) {
return stabilityRepairsGoodMax != null ? stabilityRepairsGoodMax : fallback;
}
public int stabilityRepairsOkMax(int fallback) {
return stabilityRepairsOkMax != null ? stabilityRepairsOkMax : fallback;
}
public double stabilityOkScore(double fallback) {
return stabilityOkScore != null ? stabilityOkScore : fallback;
}
public double stabilityBadScore(double fallback) {
return stabilityBadScore != null ? stabilityBadScore : fallback;
}
}

View File

@ -1,6 +1,9 @@
package com.wanxiang.huijing.game.module.aigc.service.task;
import com.wanxiang.huijing.game.module.aigc.service.config.ReadinessConfigProvider;
import com.wanxiang.huijing.game.module.aigc.service.config.ReadinessView;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
@ -16,7 +19,7 @@ import java.util.Map;
* - playability 占半壁九门全过 = 就绪基线trace.pass==true 1.0否则 0九门是硬地板
* - firstPlay首局trace.gatespec driver sevenGateVerdict.guards.H_progress==true 1.0否则 0.5
* - stability稳定性repairs==0 1.0repairs<=2 0.7否则 0.4修复轮越少越就绪
* - efficiency效率cost.totalRmb 在预算(<=0.15) 1.0超则线性降到 0成本可控度
* - efficiency效率cost.totalRmb 在预算阈值内(起步 ¥0.6,W-CFG-EXT 可热配) 1.0超则线性降到 0成本可控度
* - 任一子项数据缺失 cost 取价失败 该子项取中性 0.5不让缺数据把分打到 0非阻断哲学
*
* 权重占位执行版 §9-2 待校准playability 0.50 / firstPlay 0.25 / stability 0.15 / efficiency 0.10
@ -43,12 +46,35 @@ public class ReadinessScorer {
/**
* 效率子项的预算阈值成本 totalRmb <= 此值算满分超则线性降
* 起步值 ¥0.6创始人 2026-07-04 决策5按推荐 0.15 与便宜档真实成本 ¥0.41.2 错配
* 效率维恒 0 零区分力取便宜档成本中位纠正当前为硬编码常量改要动代码 + jar
* 待随 D11 就绪评分参数本阈值 + 四维权重外置进配置中心决策6进配置集涟漪届时
* 本常量降为热源缺失时的回落默认真值由激活版本行下发
* 效率维恒 0 零区分力取便宜档成本中位纠正已随 D11 就绪评分参数(本阈值 + 四维权重)外置进配置中心
* (W-CFG-EXT,决策6进配置集涟漪)本常量降为热源缺失时的回落默认,真值由激活的质量评分配置集版本行
* (content_json.readiness.efficiency_budget_rmb) ReadinessConfigProvider 下发,改一版即生效不换 jar
*/
private static final double BUDGET_RMB = 0.6;
/** 稳定性:修复轮 <= 此值算满分 1.0(回落默认,原内联字面量 r<=0)。 */
private static final int REPAIRS_GOOD_MAX = 0;
/** 稳定性:修复轮 <= 此值算次优 ok 分,超则劣 bad 分(回落默认,原内联字面量 r<=2)。 */
private static final int REPAIRS_OK_MAX = 2;
/** 稳定性次优分(good < 修复轮 <= ok 时取值;回落默认,原内联字面量 0.7)。 */
private static final double STABILITY_OK_SCORE = 0.7;
/** 稳定性劣分(修复轮 > ok 时取值;回落默认,原内联字面量 0.4)。 */
private static final double STABILITY_BAD_SCORE = 0.4;
/**
* D11 质量评分参数的进程内读取面(W-CFG-EXT):打分时取当前激活质量评分配置集的四维权重与阈值,读不到逐字段
* 回落到上面的 static final 常量<b>可选注入</b>(required=false):Spring 上下文里由 {@link ReadinessConfigProvider}
* bean 注入;单测 {@code new ReadinessScorer()} 时为 null {@link #view()} 返回 {@link ReadinessView#empty()}
* 全字段回落硬编码常量(等价外置前行为,故既有单测不受影响)
*/
@Autowired(required = false)
private ReadinessConfigProvider configProvider;
/** 取当前 D11 参数视图:provider 缺席(单测)/为空 → 全回落硬编码常量。 */
private ReadinessView view() {
return configProvider != null ? configProvider.get() : ReadinessView.empty();
}
/**
* trace 算就绪度分0-100 整数
*
@ -61,14 +87,16 @@ public class ReadinessScorer {
return null;
}
try {
double playability = scorePlayability(trace);
double firstPlay = scoreFirstPlay(trace);
double stability = scoreStability(trace);
double efficiency = scoreEfficiency(trace);
double weighted = W_PLAYABILITY * playability
+ W_FIRST_PLAY * firstPlay
+ W_STABILITY * stability
+ W_EFFICIENCY * efficiency;
// 一次解析当前激活的 D11 参数视图( TTL 缓存);读不到逐字段回落硬编码常量(W-CFG-EXT)
ReadinessView v = view();
double playability = scorePlayability(trace, v);
double firstPlay = scoreFirstPlay(trace, v);
double stability = scoreStability(trace, v);
double efficiency = scoreEfficiency(trace, v);
double weighted = v.playabilityWeight(W_PLAYABILITY) * playability
+ v.firstPlayWeight(W_FIRST_PLAY) * firstPlay
+ v.stabilityWeight(W_STABILITY) * stability
+ v.efficiencyWeight(W_EFFICIENCY) * efficiency;
// 夹取 [0,1] 后映射 0-100防权重/子项越界稳健兜底
int score = (int) Math.round(100.0 * clamp01(weighted));
return Math.max(0, Math.min(100, score));
@ -81,9 +109,13 @@ public class ReadinessScorer {
/** 可玩性子项trace.pass==true → 1.0;明确 false → 0缺失 → 中性 0.5。 */
private double scorePlayability(Map<String, Object> trace) {
return scorePlayability(trace, view());
}
private double scorePlayability(Map<String, Object> trace, ReadinessView v) {
Object pass = trace.get("pass");
if (pass == null) {
return NEUTRAL;
return v.neutralScore(NEUTRAL);
}
return asBool(pass) ? 1.0 : 0.0;
}
@ -98,8 +130,12 @@ public class ReadinessScorer {
* firstPlay 永卡中性 0.5永不到 1.0HTTP/SAA 两路同病修后H_progress Map 时读其 {@code pass} 布尔字段
* 真实形态同时<b>保留对裸 bool 的向后兼容</b>老格式/测试夹具
*/
@SuppressWarnings("unchecked")
private double scoreFirstPlay(Map<String, Object> trace) {
return scoreFirstPlay(trace, view());
}
@SuppressWarnings("unchecked")
private double scoreFirstPlay(Map<String, Object> trace, ReadinessView v) {
Object gatespec = trace.get("gatespec");
boolean hasDriver = gatespec instanceof Map && ((Map<String, Object>) gatespec).get("driver") != null;
Object verdict = trace.get("sevenGateVerdict");
@ -111,7 +147,7 @@ public class ReadinessScorer {
hProgress = guardPass(((Map<String, Object>) guards).get("H_progress"));
}
}
return (hasDriver && hProgress) ? 1.0 : NEUTRAL;
return (hasDriver && hProgress) ? 1.0 : v.neutralScore(NEUTRAL);
}
/**
@ -136,37 +172,50 @@ public class ReadinessScorer {
/** 稳定性子项repairs==0 → 1.0<=2 → 0.7;否则 0.4;缺失 → 中性 0.5。 */
private double scoreStability(Map<String, Object> trace) {
return scoreStability(trace, view());
}
private double scoreStability(Map<String, Object> trace, ReadinessView v) {
Object repairs = trace.get("repairs");
if (!(repairs instanceof Number)) {
return NEUTRAL;
return v.neutralScore(NEUTRAL);
}
int r = ((Number) repairs).intValue();
if (r <= 0) {
if (r <= v.stabilityRepairsGoodMax(REPAIRS_GOOD_MAX)) {
return 1.0;
}
return r <= 2 ? 0.7 : 0.4;
return r <= v.stabilityRepairsOkMax(REPAIRS_OK_MAX)
? v.stabilityOkScore(STABILITY_OK_SCORE)
: v.stabilityBadScore(STABILITY_BAD_SCORE);
}
/**
* 效率子项cost.totalRmb <= 预算(0.15) 1.0超则线性降到 0totalRmb>=2×预算 0
* cost 缺失/取价失败 totalRmb 中性 0.5worker new-api 取价失败时 cost 段降级不应把分打到 0
* 效率子项cost.totalRmb <= 预算阈值 1.0超则线性降到 0totalRmb>=2×预算 0
* cost 缺失/取价失败 totalRmb 中性分worker new-api 取价失败时 cost 段降级不应把分打到 0
* 预算阈值取激活的质量评分配置集 {@code efficiency_budget_rmb}W-CFG-EXT读不到回落 {@link #BUDGET_RMB}起步 ¥0.6
*/
@SuppressWarnings("unchecked")
private double scoreEfficiency(Map<String, Object> trace) {
return scoreEfficiency(trace, view());
}
@SuppressWarnings("unchecked")
private double scoreEfficiency(Map<String, Object> trace, ReadinessView v) {
double budget = v.efficiencyBudgetRmb(BUDGET_RMB);
Object cost = trace.get("cost");
if (!(cost instanceof Map)) {
return NEUTRAL;
return v.neutralScore(NEUTRAL);
}
Object totalRmb = ((Map<String, Object>) cost).get("totalRmb");
if (!(totalRmb instanceof Number)) {
return NEUTRAL; // 取价失败降级cost 只剩 error+byModel 中性
return v.neutralScore(NEUTRAL); // 取价失败降级cost 只剩 error+byModel 中性
}
double rmb = ((Number) totalRmb).doubleValue();
if (rmb <= BUDGET_RMB) {
if (rmb <= budget) {
return 1.0;
}
// 线性降预算2×预算 区间从 1.0 降到 0 2×预算 钳到 0
double ratio = (rmb - BUDGET_RMB) / BUDGET_RMB;
double ratio = (rmb - budget) / budget;
return clamp01(1.0 - ratio);
}

View File

@ -58,6 +58,13 @@ class GenConfigActivationServiceImplTest extends BaseMockitoUnitTest {
+ "\"routeB\":{\"budget.rmb_hard_limit\":40.0}}";
/** 值域非法内容(负预算)。 */
private static final String BAD_CONTENT = "{\"routeB\":{\"budget.rmb_hard_limit\":-5.0}}";
/** 质量评分配置集内容readiness-onlyW-CFG-EXTcodec 产空载荷、激活天然短路成纯账本收口。 */
private static final String READINESS_CONTENT =
"{\"readiness\":{\"playability_weight\":0.50,\"first_play_weight\":0.25,"
+ "\"stability_weight\":0.15,\"efficiency_weight\":0.10,"
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4}}";
@BeforeEach
void setUp() {
@ -208,6 +215,30 @@ class GenConfigActivationServiceImplTest extends BaseMockitoUnitTest {
verify(genConfigService, never()).abortActivating(any(), any());
}
// ==================== 质量评分配置集readiness-onlyW-CFG-EXT E3空载荷天然短路 ====================
/**
* E3质量评分配置集readiness-only激活 codec 产空载荷 现有 dispatch 天然短路
* 账本正常收口markVersionActivated但两处投影全程零触碰Agent Service/Redis Nacos dataId 都不写纯账本收口无外投影
* 激活编排一行未改复用现成 dispatch/收口本例坐实空载荷短路成立
*/
@Test
void testActivate_readinessOnly_pureLedgerNoDispatch() {
stubSet(1L, AigcConfigStatusEnum.PENDING_REVIEW, null, "readiness"); // 跨档质量评分档
stubVersion(1L, 3000L, null, READINESS_CONTENT); // prompt只有 readiness
when(genConfigService.beginActivating(1L, 3000L))
.thenReturn(new GenConfigActivationSnapshot(AigcConfigStatusEnum.PENDING_REVIEW.getStatus(), null));
service.activate(1L, 3000L);
// 纯账本收口 ACTIVE + 前移指针markVersionActivated不失败复位
verify(genConfigService).markVersionActivated(1L, 3000L);
verify(genConfigService, never()).abortActivating(any(), any());
// 两处投影零写入Agent Service路A/Redis Nacos路B/dataId全程不被触碰
verify(agentClient, never()).patch(anyString(), any());
verify(nacosClient, never()).publish(anyString(), any());
}
// ==================== 前置拒绝不写账本不下发 ====================
/** 值域非法(负预算)→ 激活前置拒:不落 ACTIVATING、不发任何一路。 */

View File

@ -201,4 +201,108 @@ class GenConfigContentCodecTest {
assertTrue(p.hasPathB());
}
// ==================== readinessD11 质量评分配置集W-CFG-EXT ====================
/** 合法 readiness 起步值(十字段,值 = ReadinessScorer 常量)。 */
private static final String VALID_READINESS =
"{\"readiness\":{\"playability_weight\":0.50,\"first_play_weight\":0.25,"
+ "\"stability_weight\":0.15,\"efficiency_weight\":0.10,"
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4}}";
/** 只有 readiness 块(起步值合法)→ 空载荷(两路皆空):现有 dispatch/reconcile 天然短路成纯账本收口。 */
@Test
void testBuild_readinessOnly_emptyPayload() {
GenConfigActivationPayload p = codec().build(null, VALID_READINESS);
assertFalse(p.hasPathA(), "质量评分配置集不下发路 A");
assertFalse(p.hasPathB(), "质量评分配置集不下发路 B");
assertNull(p.pathA());
assertNull(p.pathBJson());
}
/** readiness 四维权重和≠1.0(如 0.5/0.5/0.5/0.5)→ sanity 拒。 */
@Test
void testReadinessSanity_weightSumNotOne() {
String c = "{\"readiness\":{\"playability_weight\":0.5,\"first_play_weight\":0.5,"
+ "\"stability_weight\":0.5,\"efficiency_weight\":0.5,"
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4}}";
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
}
/** readiness 单权重越界 [0,1](如 1.5)→ sanity 拒。 */
@Test
void testReadinessSanity_weightOutOfUnit() {
String c = "{\"readiness\":{\"playability_weight\":1.5,\"first_play_weight\":-0.5,"
+ "\"stability_weight\":0.0,\"efficiency_weight\":0.0,"
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4}}";
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
}
/** readiness 预算非正0→ sanity 拒。 */
@Test
void testReadinessSanity_budgetNotPositive() {
String c = VALID_READINESS.replace("\"efficiency_budget_rmb\":0.6", "\"efficiency_budget_rmb\":0.0");
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
}
/** readiness 修复轮 good_max > ok_max → sanity 拒。 */
@Test
void testReadinessSanity_repairsGoodGtOk() {
String c = VALID_READINESS
.replace("\"stability_repairs_good_max\":0", "\"stability_repairs_good_max\":5")
.replace("\"stability_repairs_ok_max\":2", "\"stability_repairs_ok_max\":2");
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
}
/** readiness 子项分越界 [0,1]stability_ok_score=1.7)→ sanity 拒。 */
@Test
void testReadinessSanity_subScoreOutOfUnit() {
String c = VALID_READINESS.replace("\"stability_ok_score\":0.7", "\"stability_ok_score\":1.7");
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
}
/** readiness 缺字段(缺 efficiency_budget_rmb→ sanity 拒(新激活须完整;运行期缺字段回落是 provider 的事)。 */
@Test
void testReadinessSanity_missingField() {
String c = VALID_READINESS.replace(",\"efficiency_budget_rmb\":0.6", "");
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
}
/** readiness 修复轮阈值非整数2.5)→ sanity 拒。 */
@Test
void testReadinessSanity_repairsNotInteger() {
String c = VALID_READINESS.replace("\"stability_repairs_ok_max\":2", "\"stability_repairs_ok_max\":2.5");
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
}
/** 互斥readiness 与 routeB 共存 → sanity 拒(质量评分块不得混配生成下发路)。 */
@Test
void testReadiness_mutexWithRouteB() {
String c = "{\"readiness\":{\"playability_weight\":0.50,\"first_play_weight\":0.25,"
+ "\"stability_weight\":0.15,\"efficiency_weight\":0.10,"
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4},"
+ "\"routeB\":{\"budget.cheap_rmb_hard_limit\":10.0}}";
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf(null, c).getCode());
}
/** 互斥readiness 与 routeA 共存 → sanity 拒。 */
@Test
void testReadiness_mutexWithRouteA() {
String c = "{\"readiness\":{\"playability_weight\":0.50,\"first_play_weight\":0.25,"
+ "\"stability_weight\":0.15,\"efficiency_weight\":0.10,"
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4},"
+ "\"routeA\":{\"agentId\":\"a1\",\"sessionId\":\"s1\",\"session\":{\"chat_model_config\":"
+ "{\"type\":\"t\",\"credential_id\":\"c\",\"model\":\"m\",\"parameters\":{}}}}}";
assertEquals(AIGC_CONFIG_ACTIVATE_SANITY_FAILED.getCode(), sanityOf("sys", c).getCode());
}
}

View File

@ -60,6 +60,13 @@ class GenConfigReconcileServiceImplTest extends BaseMockitoUnitTest {
private static final String DATAID = "gen-hot-params-cheap";
private static final String PB_IN_SYNC = "{\"budget.cheap_rmb_hard_limit\":10.0}";
private static final String PB_DRIFT = "{\"budget.cheap_rmb_hard_limit\":999.0}";
/** 质量评分配置集内容readiness-onlyW-CFG-EXT空载荷、无投影可对账。 */
private static final String READINESS_CONTENT =
"{\"readiness\":{\"playability_weight\":0.50,\"first_play_weight\":0.25,"
+ "\"stability_weight\":0.15,\"efficiency_weight\":0.10,"
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4}}";
@BeforeEach
void setUp() {
@ -257,6 +264,33 @@ class GenConfigReconcileServiceImplTest extends BaseMockitoUnitTest {
verify(nacosClient, never()).probeDataId(anyString());
}
/**
* E3质量评分配置集readiness-only对账 codec 产空载荷 两路 applicable=false IN_SYNC零漂移
* 且两路投影零探测无路A/路B 可对账坐实漂移对账对质量评分配置集报无投影无漂移
*/
@Test
void testReconcile_readinessOnly_inSyncNoProjection() {
GenConfigSetDO set = new GenConfigSetDO();
set.setId(1L);
set.setTier("readiness"); // 跨档质量评分档
set.setStatus(AigcConfigStatusEnum.ACTIVE.getStatus());
set.setActiveVersionId(300L);
when(genConfigService.getConfigSet(1L)).thenReturn(set);
GenConfigVersionDO v = new GenConfigVersionDO();
v.setId(300L);
v.setConfigSetId(1L);
v.setContentJson(READINESS_CONTENT); // 只有 readiness 空载荷
when(genConfigService.getVersion(1L, 300L)).thenReturn(v);
GenConfigDriftReportRespVO report = service.reconcile(1L);
assertEquals("IN_SYNC", report.getVerdict(), "质量评分配置集无投影 → 恒一致");
assertTrue(report.getDriftItems().isEmpty(), "无投影、零漂移项");
// 两路投影零探测无路A/路B 可对账
verify(agentClient, never()).probeAgentSystemPrompt(anyString(), anyString());
verify(nacosClient, never()).probeDataId(anyString());
}
// ==================== 最终一致容忍窗 ====================
/** 路B 首读旧值(漂移)、窗内再读已收敛 → 容忍窗吸收传播延迟、IN_SYNC 不误报。 */

View File

@ -0,0 +1,258 @@
package com.wanxiang.huijing.game.module.aigc.service.config;
import com.wanxiang.huijing.game.module.aigc.dal.dataobject.config.GenConfigVersionDO;
import com.wanxiang.huijing.game.module.aigc.service.task.ReadinessScorer;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* {@link ReadinessConfigProvider} {@link ReadinessScorer} 外置读线的单元测试(W-CFG-EXT):把守
* 未激活/读失败逐字段回落硬编码默认读到激活版按字段生效单字段缺失只回落该字段
* 起步值 = ReadinessScorer 常量的 doccode 对账机器门 TTL 缓存 Mockito,不依赖 DB/Spring
*
* @author 绘境AI(W-CFG-EXT · D11 参数外置)
*/
class ReadinessConfigProviderTest {
/** 起步值 content_json(字面量 = 契约文档里的起步值;doc↔code 断言据此对齐 ReadinessScorer 常量)。 */
private static final String STARTER_CONTENT =
"{\"readiness\":{\"playability_weight\":0.50,\"first_play_weight\":0.25,"
+ "\"stability_weight\":0.15,\"efficiency_weight\":0.10,"
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4}}";
private static final double SENTINEL = -999.0;
private static final int INT_SENTINEL = -999;
private GenConfigVersionDO versionWith(String contentJson) {
GenConfigVersionDO v = new GenConfigVersionDO();
v.setId(1L);
v.setContentJson(contentJson);
return v;
}
/** ttl=0 的 provider每次重读验解析/回落不受缓存干扰)。 */
private ReadinessConfigProvider providerOver(GenConfigService svc) {
return new ReadinessConfigProvider(svc, 0L);
}
// ==================== 未激活 / 读失败 全回落 ====================
/** 未建/未激活质量评分配置集getActiveVersionBySetKey 返 null→ 全字段回落硬编码默认sentinel 全部原样返回)。 */
@Test
void get_noActiveVersion_allFallback() {
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY))).thenReturn(null);
ReadinessView v = providerOver(svc).get();
assertEquals(SENTINEL, v.playabilityWeight(SENTINEL), 1e-9, "未激活应回落(返回 sentinel");
assertEquals(SENTINEL, v.efficiencyBudgetRmb(SENTINEL), 1e-9);
assertEquals(INT_SENTINEL, v.stabilityRepairsOkMax(INT_SENTINEL));
}
/** DB 读抛异常 → 不抛穿、退化成全回落(非阻断哲学)。 */
@Test
void get_dbThrows_allFallback() {
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY)))
.thenThrow(new RuntimeException("DB 不可达"));
ReadinessView v = providerOver(svc).get();
assertEquals(SENTINEL, v.playabilityWeight(SENTINEL), 1e-9, "读异常应回落、不抛");
}
/** 激活版无 content_json.readiness 块(如生成配置集误挂)→ 全回落。 */
@Test
void get_noReadinessBlock_allFallback() {
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY)))
.thenReturn(versionWith("{\"routeB\":{\"budget.cheap_rmb_hard_limit\":10.0}}"));
ReadinessView v = providerOver(svc).get();
assertEquals(SENTINEL, v.stabilityOkScore(SENTINEL), 1e-9);
}
/** content_json 非法 JSON → 全回落、不抛。 */
@Test
void get_invalidJson_allFallback() {
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY)))
.thenReturn(versionWith("{不是合法json"));
ReadinessView v = providerOver(svc).get();
assertEquals(SENTINEL, v.neutralScore(SENTINEL), 1e-9);
}
// ==================== 读到激活版 按字段生效 ====================
/** 激活版给非默认权重 → view 逐字段返回激活值(非回落)。 */
@Test
void get_activeVersion_fieldsTakeEffect() {
String content = "{\"readiness\":{\"playability_weight\":0.60,\"first_play_weight\":0.20,"
+ "\"stability_weight\":0.10,\"efficiency_weight\":0.10,"
+ "\"neutral_score\":0.4,\"efficiency_budget_rmb\":1.2,"
+ "\"stability_repairs_good_max\":1,\"stability_repairs_ok_max\":3,"
+ "\"stability_ok_score\":0.8,\"stability_bad_score\":0.3}}";
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY)))
.thenReturn(versionWith(content));
ReadinessView v = providerOver(svc).get();
assertEquals(0.60, v.playabilityWeight(SENTINEL), 1e-9);
assertEquals(1.2, v.efficiencyBudgetRmb(SENTINEL), 1e-9);
assertEquals(1, v.stabilityRepairsGoodMax(INT_SENTINEL));
assertEquals(3, v.stabilityRepairsOkMax(INT_SENTINEL));
assertEquals(0.8, v.stabilityOkScore(SENTINEL), 1e-9);
}
// ==================== 单字段缺失 只回落该字段 ====================
/** 激活版缺 efficiency_budget_rmb只该字段回落 sentinel其余仍取激活版值。 */
@Test
void get_missingOneField_onlyThatFieldFallsBack() {
String content = STARTER_CONTENT.replace(",\"efficiency_budget_rmb\":0.6", "");
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY)))
.thenReturn(versionWith(content));
ReadinessView v = providerOver(svc).get();
assertEquals(SENTINEL, v.efficiencyBudgetRmb(SENTINEL), 1e-9, "缺失字段回落 sentinel");
assertEquals(0.50, v.playabilityWeight(SENTINEL), 1e-9, "其余字段仍取激活版值、不受牵连");
assertEquals(2, v.stabilityRepairsOkMax(INT_SENTINEL), "其余字段仍取激活版值");
}
// ==================== doccode起步值 = ReadinessScorer 常量机器门 ====================
/**
* 契约文档里的起步值STARTER_CONTENT 字面量逐项 = ReadinessScorer 的回落常量
* 谁改了打分器常量却没同步契约起步值或反之此断言即红字段名与默认值 = ReadinessScorer 常量钉成机器门
*/
@Test
void docCode_starterValues_equalScorerConstants() throws Exception {
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY)))
.thenReturn(versionWith(STARTER_CONTENT));
ReadinessView v = providerOver(svc).get();
assertEquals(constD("W_PLAYABILITY"), v.playabilityWeight(SENTINEL), 1e-9);
assertEquals(constD("W_FIRST_PLAY"), v.firstPlayWeight(SENTINEL), 1e-9);
assertEquals(constD("W_STABILITY"), v.stabilityWeight(SENTINEL), 1e-9);
assertEquals(constD("W_EFFICIENCY"), v.efficiencyWeight(SENTINEL), 1e-9);
assertEquals(constD("NEUTRAL"), v.neutralScore(SENTINEL), 1e-9);
assertEquals(constD("BUDGET_RMB"), v.efficiencyBudgetRmb(SENTINEL), 1e-9);
assertEquals(constI("REPAIRS_GOOD_MAX"), v.stabilityRepairsGoodMax(INT_SENTINEL));
assertEquals(constI("REPAIRS_OK_MAX"), v.stabilityRepairsOkMax(INT_SENTINEL));
assertEquals(constD("STABILITY_OK_SCORE"), v.stabilityOkScore(SENTINEL), 1e-9);
assertEquals(constD("STABILITY_BAD_SCORE"), v.stabilityBadScore(SENTINEL), 1e-9);
}
/**
* 端到端等价行为版 doccode起步值激活版喂进带 provider 的打分器与无 provider纯常量打分器对同一 trace 同分
* 若起步值 常量两者分数会偏离
*/
@Test
void docCode_starterConfig_scoresSameAsHardcoded() {
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY)))
.thenReturn(versionWith(STARTER_CONTENT));
ReadinessScorer withProvider = new ReadinessScorer();
ReflectionTestUtils.setField(withProvider, "configProvider", providerOver(svc));
ReadinessScorer hardcoded = new ReadinessScorer(); // provider=null 全回落常量
Map<String, Object> trace = fullTrace();
assertEquals(hardcoded.score(trace), withProvider.score(trace),
"起步值激活版与硬编码常量对同一 trace 应同分(起步值=常量)");
}
/** 端到端生效:激活版把 playability 权重从 0.50 抬到 0.90,同一 trace 就绪分随之变化(不重编 jar。 */
@Test
void endToEnd_activatedWeightChangesScore() {
// 提高可玩性权重压其余到和为 1.0 playability=1.0 而其余非满的 trace playability 权重会改变合成分
String content = "{\"readiness\":{\"playability_weight\":0.90,\"first_play_weight\":0.04,"
+ "\"stability_weight\":0.03,\"efficiency_weight\":0.03,"
+ "\"neutral_score\":0.5,\"efficiency_budget_rmb\":0.6,"
+ "\"stability_repairs_good_max\":0,\"stability_repairs_ok_max\":2,"
+ "\"stability_ok_score\":0.7,\"stability_bad_score\":0.4}}";
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY)))
.thenReturn(versionWith(content));
ReadinessScorer withProvider = new ReadinessScorer();
ReflectionTestUtils.setField(withProvider, "configProvider", providerOver(svc));
ReadinessScorer hardcoded = new ReadinessScorer();
// traceplayability=1.0pass=truefirstPlay=0.5 H stability=1.0repairs=0efficiency 中性 0.5
Map<String, Object> trace = new HashMap<>();
trace.put("pass", true);
trace.put("repairs", 0);
// 不给 gatespec/verdict firstPlay 走中性不给 cost efficiency 中性
assertNotEquals(hardcoded.score(trace), withProvider.score(trace),
"抬高 playability 权重后同一 trace 就绪分应变化");
}
// ==================== TTL 缓存 ====================
/** TTL 内多次 get 只读一次 DB缓存命中。 */
@Test
void get_withinTtl_cachesOneDbRead() {
GenConfigService svc = mock(GenConfigService.class);
when(svc.getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY)))
.thenReturn(versionWith(STARTER_CONTENT));
ReadinessConfigProvider provider = new ReadinessConfigProvider(svc, 60_000L); // TTL
provider.get();
provider.get();
provider.get();
verify(svc, times(1)).getActiveVersionBySetKey(eq(ReadinessConfigKeys.READINESS_SET_KEY));
}
// ==================== 工具 ====================
/** 反射读 ReadinessScorer 的 private static final double 常量。 */
private static double constD(String name) throws Exception {
Field f = ReadinessScorer.class.getDeclaredField(name);
f.setAccessible(true);
return f.getDouble(null);
}
/** 反射读 ReadinessScorer 的 private static final int 常量。 */
private static int constI(String name) throws Exception {
Field f = ReadinessScorer.class.getDeclaredField(name);
f.setAccessible(true);
return f.getInt(null);
}
/** 九门全过的满信号 traceplayability/firstPlay/stability=1.0、efficiency 满分)。 */
private static Map<String, Object> fullTrace() {
Map<String, Object> trace = new HashMap<>();
trace.put("pass", true);
trace.put("repairs", 0);
Map<String, Object> gatespec = new HashMap<>();
gatespec.put("driver", "click");
trace.put("gatespec", gatespec);
Map<String, Object> hProgress = new HashMap<>();
hProgress.put("pass", true);
Map<String, Object> guards = new HashMap<>();
guards.put("H_progress", hProgress);
Map<String, Object> verdict = new HashMap<>();
verdict.put("guards", guards);
trace.put("sevenGateVerdict", verdict);
Map<String, Object> cost = new HashMap<>();
cost.put("totalRmb", 0.3); // <= budget 0.6 efficiency 1.0
trace.put("cost", cost);
return trace;
}
}