diff --git a/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/AigcConfigTierEnum.java b/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/AigcConfigTierEnum.java index f632ef61..37e262d2 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/AigcConfigTierEnum.java +++ b/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/AigcConfigTierEnum.java @@ -23,7 +23,16 @@ public enum AigcConfigTierEnum { /** 便宜档:AI 轻参与的高质轻量游戏(cheap-worker,MiniMax-M3 路,软停线 cheap_rmb_hard_limit=¥10) */ CHEAP("cheap", "便宜档"), /** 富游戏档:AI 深参与的自治富游戏(tier2 gen-worker,Phaser,软停线 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; diff --git a/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/ErrorCodeConstants.java b/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/ErrorCodeConstants.java index e4f95ebc..545d07b9 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/ErrorCodeConstants.java +++ b/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/ErrorCodeConstants.java @@ -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, "配置激活下发失败(已判未生效并补偿回上一激活版,可重试):{}"); diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigActivationPayload.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigActivationPayload.java index 232c0fb5..21804bd2 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigActivationPayload.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigActivationPayload.java @@ -10,8 +10,10 @@ package com.wanxiang.huijing.game.module.aigc.service.config; *
两路都可空(但不能都空——那样没有可激活的落点):纯参数档可能只有路 B,纯 prompt/模型档可能只有路 A。是否下发某路 - * 由该路是否为空决定({@link #hasPathA()} / {@link #hasPathB()})。 + *
两路可各自为空:纯参数档可能只有路 B,纯 prompt/模型档可能只有路 A。是否下发某路由该路是否为空决定 + * ({@link #hasPathA()} / {@link #hasPathB()})。两路皆空 = 空载荷:这是 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 文本,整内容 publish);null = 本版本不下发路 B diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigContentCodec.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigContentCodec.java index 56728afe..8fc06126 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigContentCodec.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigContentCodec.java @@ -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 下发失败→补偿。 * + *
第三块 readiness(W-CFG-EXT · D11 质量评分参数,与 routeA/routeB 互斥):除按下发路由分的 routeA/routeB 外, + * content_json 可含一个平级的 {@code readiness} 块,承载 D11 就绪评分的十个标量(四维权重 + 中性分 + 效率预算阈值 + * + 稳定性两级阈值与两级分)。它是跨档、进程内消费、无外投影的特例——不下发路 A/路 B,由 game-cloud 进程内的 + * {@code ReadinessConfigProvider} 直读激活版本行。故本编解码器对只含 readiness 的配置集产出空载荷 + * (pathA=pathB=null),现有 dispatch/reconcile 在空载荷下天然短路成「值域 sanity + 账本收口、无投影、恒一致」。 + * 字段名与默认起步值 = {@link com.wanxiang.huijing.game.module.aigc.service.task.ReadinessScorer} 的回落常量,契约: + *
+ * { "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 值域 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) {
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigService.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigService.java
index 586e30a6..365707e4 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigService.java
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigService.java
@@ -96,6 +96,22 @@ public interface GenConfigService {
*/
GenConfigVersionDO getVersion(Long configSetId, Long versionId);
+ /**
+ * 按业务键取「当前激活版本行」(供进程内消费方直读激活配置,如 D11 质量评分参数 W-CFG-EXT)。
+ *
+ * 与 {@link #getVersion} 的差别:getVersion 按 versionId 精确取任一历史版、不存在即抛;本方法按配置集业务键 + * 定位其当前激活版(active_version_id 指向的那一行),且读不到不抛、返回 null——配置集不存在、从未激活 + * (active_version_id 为 NULL)、或激活版行缺失,都返 null,交调用方回落默认(消费侧非阻断:D11 打分读不到激活版 + * 就用硬编码默认,绝不因配置缺失打挂回调)。 + * + *
按 setKey(而非 tier)定位:uk_set_key 保证至多一条,寻址无歧义;跨档的质量评分配置集是单例(约定业务键 + * {@link ReadinessConfigKeys#READINESS_SET_KEY}),按 tier 取则一档可有多集、有歧义,故走业务键。 + * + * @param setKey 配置集业务键 + * @return 当前激活版本行 DO;配置集不存在/从未激活/激活版缺失 → null + */ + GenConfigVersionDO getActiveVersionBySetKey(String setKey); + /** * 两版 diff(应用层做):prompt 给「是否变更 + 两侧字节数」,结构化旋钮拍平成点路径逐项比对给增/删/改。 * diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigServiceImpl.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigServiceImpl.java index 37401f94..6ae3f30e 100644 --- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigServiceImpl.java +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/GenConfigServiceImpl.java @@ -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); diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/ReadinessConfigKeys.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/ReadinessConfigKeys.java new file mode 100644 index 00000000..45cae6e1 --- /dev/null +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/ReadinessConfigKeys.java @@ -0,0 +1,57 @@ +package com.wanxiang.huijing.game.module.aigc.service.config; + +/** + * D11 质量评分配置集(W-CFG-EXT)的内容契约常量:content_json 里 {@code readiness} 块的块名与十个字段名, + * 外加质量评分配置集的约定业务键。 + * + *
为何单独成类:这批字段名是「配置集内容(doc)」与「读取消费(code)」之间的唯一约定—— + * {@link GenConfigContentCodec} 激活前置的 readiness 值域 sanity 与 {@code ReadinessConfigProvider} 的逐字段解析 + * 都引用它,把字段名收进一处避免两边各写一份字符串字面量、漂移无人对账。字段值(默认起步值)不在这里, + * 在 {@link com.wanxiang.huijing.game.module.aigc.service.task.ReadinessScorer} 的回落默认常量里——那里才是 + * 「热源缺失时用哪个数」的权威,本类只管「用哪个键名读」。 + * + *
字段值域与默认值语义见 {@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"; + +} diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/ReadinessConfigProvider.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/ReadinessConfigProvider.java new file mode 100644 index 00000000..630973af --- /dev/null +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/ReadinessConfigProvider.java @@ -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} 打分时取用。 + * + *
为何进程内直读账本、不走投影:D11 参数的消费方就是同库同进程的打分器,不像 prompt/模型(路 A PATCH Service) + * 或预算/门阈值(路 B publish Nacos)要投影到外部系统。所以这里直接读 MySQL 账本的激活版本行,是配置控制面阶段二 + * 「消费两路」之外补齐的第三条消费口(game-cloud 进程内读激活版)。 + * + *
短 TTL 缓存,不接激活失效信号:打分发生在回调落库这条本就 DB-heavy 的路上,一次缓存读可忽略;用短 TTL + * 失效(而非让激活链发进程内失效信号)把配置中心与打分器解耦——激活一版权重后,最迟一个 TTL 内下一次打分即读到新值, + * 不必把 provider 耦合进激活编排。TTL 很短,「改一版即生效」的体感仍在秒级。 + * + *
永不抛、逐字段回落:未建质量评分配置集 / 未激活 / 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; + } + +} diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/ReadinessView.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/ReadinessView.java new file mode 100644 index 00000000..9d832f5b --- /dev/null +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/config/ReadinessView.java @@ -0,0 +1,92 @@ +package com.wanxiang.huijing.game.module.aigc.service.config; + +/** + * D11 质量评分参数的只读视图(W-CFG-EXT):当前激活质量评分配置集里 content_json.readiness 各字段的进程内快照。 + * + *
逐字段回落语义:每个字段可空——{@code null} 表示激活版里没有该字段(或未激活/读取异常整份为空)。 + * 取值 getter 都要求传入一个回落默认(打分器的硬编码常量),字段非空取激活版值、字段空取回落默认。 + * 这样「某个字段缺失只回落那一个,不因一个字段缺就整份弃用激活版」在一处兑现,调用方(打分器)只管把自己的 + * 常量作为回落默认传进来。{@link #empty()} 是全空视图,任何字段都取回落默认——等价于「无激活版、全用硬编码」, + * 用于 provider 缺席(如未接配置中心的单测)或读取失败。 + * + *
只读、无副作用:构造后不可变;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;
+ }
+
+}
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/task/ReadinessScorer.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/task/ReadinessScorer.java
index 1cf2b443..7461a8eb 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/task/ReadinessScorer.java
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/service/task/ReadinessScorer.java
@@ -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.0;repairs<=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.4–1.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 常量。可选注入(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