diff --git a/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/sql/permission-fixture.sql b/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/sql/permission-fixture.sql
new file mode 100644
index 00000000..e8ccbac1
--- /dev/null
+++ b/docs/agent-specs/2026-06-09-agent-loop-v1/orchestrator/sql/permission-fixture.sql
@@ -0,0 +1,43 @@
+-- =============================================================================
+-- aigc:dify:callback 权限补丁(HJ-AGENT-LOOP-EXEC-001 §8.6,staging 专用)
+--
+-- 【执行前提(探活优先、不盲改库)】仅当 §12.3-① 权限探针返回 403/无权限时才执行本脚本;
+-- 探针返回 1-101-000-000(任务不存在)= 权限已通(yudao 超管租户角色对 @ss.hasPermission 全放行),无需执行。
+-- 【执行方式(utf8mb4 纪律,含中文必须显式字符集)】
+-- ssh mini-desktop 后:
+-- cd ~/game-staging/infra && set -a && . ./.env && set +a
+-- docker exec -i game-staging-mysql mysql -uroot -p"$MYSQL_ROOT_PASSWORD" \
+-- --default-character-set=utf8mb4 ruoyi-vue-pro < permission-fixture.sql
+-- 【回滚】按 permission='aigc:dify:callback' 反查 menu id,删除 system_role_menu 关联行与 system_menu 行即可。
+-- =============================================================================
+
+-- 第一步:建按钮型权限菜单(type=3 按钮;parent_id=0 顶层挂靠,仅作权限位,不在前端菜单树展示)
+INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name,
+ status, visible, keep_alive, always_show, creator, updater, deleted)
+SELECT 'Dify回调', 'aigc:dify:callback', 3, 99, 0, '', '', '', NULL,
+ 0, b'1', b'1', b'1', 'agent-loop', 'agent-loop', b'0'
+WHERE NOT EXISTS (
+ -- 幂等:已存在同 permission 菜单则不重复插入
+ SELECT 1 FROM system_menu WHERE permission = 'aigc:dify:callback' AND deleted = b'0'
+);
+
+-- 第二步:把该权限绑到测试 admin 所属角色
+-- role_id 取值说明:B1 验证用的 staging 测试 admin(Bearer test1 对应账号)所属角色——
+-- 先用下行查询确认 role_id 再替换(通常 yudao 种子库超管 role_id=1):
+-- SELECT ur.role_id FROM system_user_role ur
+-- JOIN system_users u ON u.id = ur.user_id WHERE u.username = 'admin' AND ur.deleted = b'0';
+INSERT INTO system_role_menu (role_id, menu_id, creator, updater, deleted, tenant_id)
+SELECT 1, m.id, 'agent-loop', 'agent-loop', b'0', 1
+FROM system_menu m
+WHERE m.permission = 'aigc:dify:callback' AND m.deleted = b'0'
+ AND NOT EXISTS (
+ -- 幂等:该角色已绑此菜单则不重复插入
+ SELECT 1 FROM system_role_menu rm
+ WHERE rm.role_id = 1 AND rm.menu_id = m.id AND rm.deleted = b'0'
+ );
+
+-- 验证:两行应各返回 1 条
+SELECT id, name, permission, type FROM system_menu WHERE permission = 'aigc:dify:callback' AND deleted = b'0';
+SELECT rm.id, rm.role_id, rm.menu_id FROM system_role_menu rm
+ JOIN system_menu m ON m.id = rm.menu_id
+WHERE m.permission = 'aigc:dify:callback' AND rm.deleted = b'0';
diff --git a/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/cn/wanxiang/game/module/aigc/enums/ErrorCodeConstants.java b/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/cn/wanxiang/game/module/aigc/enums/ErrorCodeConstants.java
index 4db0b9cf..1097125b 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/cn/wanxiang/game/module/aigc/enums/ErrorCodeConstants.java
+++ b/game-cloud/game-module-aigc/game-module-aigc-api/src/main/java/cn/wanxiang/game/module/aigc/enums/ErrorCodeConstants.java
@@ -7,7 +7,7 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
*
* aigc 模块,独占 1-101-***-*** 段(见 .agents/rules/engineering-conventions.md §1.3)。
* 约定:禁止与其它模块错误码段重叠;新增错误码在此登记。
- * 段内细分:000 任务基础校验 / 002 状态机非法流转(取值对齐契约 aigc.yaml)。
+ * 段内细分:000 任务基础校验 / 002 状态机非法流转(取值对齐契约 aigc.yaml)/ 003 Dify 回调(HJ-AGENT-LOOP-EXEC-001 §8)。
*
* @author 绘境AI
*/
@@ -29,4 +29,10 @@ public interface ErrorCodeConstants {
/** 重试非法:仅终态失败(failed/timed_out)可重试(契约 aigc.yaml retry:错误码 1-101-002-002) */
ErrorCode AIGC_TASK_CANNOT_RETRY = new ErrorCode(1_101_002_002, "仅失败或超时的任务可重试");
+ // ========== Dify 回调 1-101-003-***(HJ-AGENT-LOOP-EXEC-001 §8.4 回调状态机门禁)==========
+ /** 回调终态拒重入:任务已 failed/timed_out/canceled,回调拒绝再次驱动状态机(编排器对该任务走新任务重放,不复用旧 traceId) */
+ ErrorCode AIGC_CALLBACK_TASK_FINAL = new ErrorCode(1_101_003_001, "任务已终态,回调拒绝重入");
+ /** 回调参数非法:status 取值非 succeeded/failed,或 failed 分支 failureReason 不在契约#6 七值枚举内 */
+ ErrorCode AIGC_CALLBACK_STATUS_INVALID = new ErrorCode(1_101_003_002, "回调 status 取值非法");
+
}
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/pom.xml b/game-cloud/game-module-aigc/game-module-aigc-server/pom.xml
index 0c312fb9..f3df1b5f 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-server/pom.xml
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/pom.xml
@@ -32,6 +32,20 @@
${revision}
+
+
+ cn.iocoder.cloud
+ game-module-project-api
+ ${revision}
+
+
+
+
+ cn.iocoder.cloud
+ game-module-runtime-api
+ ${revision}
+
+
cn.iocoder.cloud
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/controller/admin/task/AdminAigcTaskController.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/controller/admin/task/AdminAigcTaskController.java
index 9ef4a93c..d1cce78f 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/controller/admin/task/AdminAigcTaskController.java
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/controller/admin/task/AdminAigcTaskController.java
@@ -5,6 +5,7 @@ import cn.wanxiang.game.module.aigc.controller.admin.task.vo.DifyCallbackReqVO;
import cn.wanxiang.game.module.aigc.controller.app.task.vo.AigcTaskRespVO;
import cn.wanxiang.game.module.aigc.convert.task.AigcTaskConvert;
import cn.wanxiang.game.module.aigc.dal.dataobject.task.AigcTaskDO;
+import cn.wanxiang.game.module.aigc.service.callback.DifyCallbackService;
import cn.wanxiang.game.module.aigc.service.task.AigcTaskService;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -37,6 +38,9 @@ public class AdminAigcTaskController {
@Resource
private AigcTaskService aigcTaskService;
+ @Resource
+ private DifyCallbackService difyCallbackService;
+
@GetMapping("/task/page")
@Operation(summary = "生成任务全量分页", description = "跨创作者排障,按状态/模板/traceId 检索(T-TEL-14)")
@PreAuthorize("@ss.hasPermission('aigc:task:query')")
@@ -46,18 +50,15 @@ public class AdminAigcTaskController {
}
@PostMapping("/dify/callback")
- @Operation(summary = "[对接点] Dify workflow 完成回调", description = "契约#6 output → 驱动任务状态机;不在 MVP 实现")
+ @Operation(summary = "Dify workflow 完成回调(实做)",
+ description = "契约#6 output → 驱动任务状态机:succeeded 走三表同事务落包链(version+package+task),"
+ + "failed 落 failure_reason 置 3;agent 闭环伪装 Dify 出参与未来真 Dify 共用(HJ-AGENT-LOOP-EXEC-001 §8.4)")
@PreAuthorize("@ss.hasPermission('aigc:dify:callback')")
public CommonResult difyCallback(@Valid @RequestBody DifyCallbackReqVO reqVO) {
- // TODO 外部依赖对接点(仅契约,不实现、不 mock):
- // Dify 6 节点工作流完成后回调本端点,以契约#6 output 形态驱动 game_aigc_task 状态机:
- // succeeded → 据 traceId 定位任务,置 status=2 + 回填 version_id 关联 + 生成元数据(gen_task_id);
- // 不写 game_version 的 package_url/checksum/bundle_size(权威写者=runtime 编译成功后回写,见 runtime V3.0.0);
- // failed → 落 failure_reason 置 status=3。
- // 幂等:同一 traceId 重复回调只生效一次(落幂等表 / 状态机终态拒重入)。
- // 正式实现需加服务间签名校验;LLM 实名充值为人工闸门,本端点不触发计费。
- log.warn("[difyCallback] 对接点未实现,仅受理占位 traceId={} status={}", reqVO.getTraceId(), reqVO.getStatus());
- return success(true);
+ // 实做(HJ-AGENT-LOOP-EXEC-001 §8.4):委托回调服务执行「定位/幂等→受理→薄校验→建版本→组包→落包→回填」;
+ // 事务边界与写链失败补偿在 Service 层承载(外层编排 + 内层独立事务 Bean,Spring 代理陷阱方案②)。
+ // TODO 对接点(M-b 真 Dify 接入时):补服务间签名校验;LLM 实名充值为人工闸门,本端点不触发计费。
+ return success(difyCallbackService.handleCallback(reqVO));
}
}
diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/controller/admin/task/vo/DifyCallbackReqVO.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/controller/admin/task/vo/DifyCallbackReqVO.java
index d278cba2..07f4ab11 100644
--- a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/controller/admin/task/vo/DifyCallbackReqVO.java
+++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/cn/wanxiang/game/module/aigc/controller/admin/task/vo/DifyCallbackReqVO.java
@@ -5,16 +5,20 @@ import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
/**
- * Dify workflow 完成回调 Request VO(对齐契约 aigc.yaml DifyCallbackReqVO / 契约#6 output)
+ * Dify workflow 完成回调 Request VO(对齐契约 aigc.yaml DifyCallbackReqVO / 契约#6 dify-workflow-io.json output)
*
- * 【外部依赖对接点,仅定义契约,不实现、不 mock】形态对齐契约#6 dify-workflow-io.json 的 output。
- * gameConfig/assets 等复杂结构在 MVP 骨架阶段不展开建模,正式对接时按契约#6 补全。
+ * 形态严格对齐契约#6 output(status/traceId/templateId/gameConfig/assets/qualityScore/failureReason)。
+ * 回调实做(HJ-AGENT-LOOP-EXEC-001 §8.4):succeeded 走「建版本→组包→落包→回填任务」三表同事务写入链;
+ * failed 走「定位→受理→置 failed + failure_reason」终态落库。深度 schema 校验权威在裁决引擎(编排器侧),
+ * 后端只做非空 + 模板一致薄校验(职责分界见 DifyCallbackTxService)。
*
* @author 绘境AI
*/
-@Schema(description = "管理后台 - Dify 完成回调 Request VO(对接点)")
+@Schema(description = "管理后台 - Dify 完成回调 Request VO(契约#6 output 形态)")
@Data
public class DifyCallbackReqVO {
@@ -31,10 +35,17 @@ public class DifyCallbackReqVO {
@Schema(description = "最终匹配/使用的模板 ID", example = "dodge")
private String templateId;
+ @Schema(description = "生成的可玩配置(契约#6 output.gameConfig → 注入 GamePackage.gameConfig;succeeded 必带,"
+ + "缺失视为业务性失败 config_invalid)", example = "{\"templateId\":\"clicker\",\"title\":\"点点乐\",\"target\":10}")
+ private Map gameConfig;
+
+ @Schema(description = "生成资源引用列表(契约#6 output.assets → 合入 GamePackage.assets;可空,空则落 [])")
+ private List