From 81060fba088409c794ee33efa69bbf2bb0f7305f Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 16:45:53 +0800 Subject: [PATCH] fix(aihr): gate vision processing with ai runtime --- .../aihr/service/AihrModelSeedService.java | 4 +++ .../aihr/service/AihrSopSeedService.java | 8 ++++- .../service/AihrPracticeSeedServiceTest.java | 2 +- .../aihr/service/AihrSopSeedServiceTest.java | 31 +++++++++++++++++++ docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + docs/DEMO_ACCEPTANCE.md | 2 +- docs/DEV_SETUP.md | 2 +- scripts/demo-check.sh | 2 ++ 8 files changed, 48 insertions(+), 4 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java index aea2f3f4..a6f9c7a7 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java @@ -272,6 +272,10 @@ public class AihrModelSeedService { } } + public boolean visionAllowed() { + return aiEnabled && chatEnabled; + } + public record SpeechModel(String providerCode, String modelName, String baseUrl, String apiKey) { } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index d5be6654..b6c7d4f6 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -123,19 +123,22 @@ public class AihrSopSeedService { private final String importRootConfig; private final ScheduledExecutorService scheduledExecutorService; private final AihrVideoService videoService; + private final AihrModelSeedService modelService; private volatile boolean knowledgeGapTableReady; private volatile boolean sopReviewTableReady; private volatile boolean answerFeedbackTableReady; public AihrSopSeedService(ObjectMapper objectMapper, JdbcTemplate jdbcTemplate, ISysOssService ossService, @Value("${aihr.import.root:}") String importRootConfig, - ScheduledExecutorService scheduledExecutorService, AihrVideoService videoService) { + ScheduledExecutorService scheduledExecutorService, AihrVideoService videoService, + AihrModelSeedService modelService) { this.objectMapper = objectMapper; this.jdbcTemplate = jdbcTemplate; this.ossService = ossService; this.importRootConfig = importRootConfig; this.scheduledExecutorService = scheduledExecutorService; this.videoService = videoService; + this.modelService = modelService; } public SearchResponse search(SearchRequest request) { @@ -951,6 +954,9 @@ public class AihrSopSeedService { /** 优先查 category='vision',无则回退 category='chat'(大多数多模态模型通用)。 */ private Optional visionRuntime() { + if (modelService != null && !modelService.visionAllowed()) { + return Optional.empty(); + } try { List rows = jdbcTemplate.query(""" select c.model_name, diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java index 8d95a54c..4cd23d06 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java @@ -68,7 +68,7 @@ public class AihrPracticeSeedServiceTest { RecordingTransactionManager transactionManager = new RecordingTransactionManager(); TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); PilotExportJdbcTemplate jdbcTemplate = new PilotExportJdbcTemplate(transactionManager); - AihrSopSeedService sopSeedService = new AihrSopSeedService(new ObjectMapper(), jdbcTemplate, null, "", null, null); + AihrSopSeedService sopSeedService = new AihrSopSeedService(new ObjectMapper(), jdbcTemplate, null, "", null, null, null); Constructor constructor = (Constructor) AihrPracticeSeedService.class .getDeclaredConstructor(ObjectMapper.class, JdbcTemplate.class, AihrPracticeLlmService.class, AihrSopSeedService.class, TransactionTemplate.class); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java index 80f015af..6d0a44cf 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java @@ -1,16 +1,21 @@ package org.dromara.aihr.service; +import com.fasterxml.jackson.databind.ObjectMapper; import org.dromara.aihr.domain.AihrSopDto; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class AihrSopSeedServiceTest { @@ -126,6 +131,32 @@ public class AihrSopSeedServiceTest { assertTrue(code.contains("runLocalImportTask(taskId, plan)")); } + @Test + @Tag("dev") + public void visionProcessingUsesTheSharedAiRuntimeGate() throws Exception { + Path source = Path.of("src/main/java/org/dromara/aihr/service/AihrSopSeedService.java"); + if (!Files.exists(source)) { + source = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java"); + } + String code = Files.readString(source); + + assertTrue(code.contains("!modelService.visionAllowed()")); + } + + @Test + @Tag("dev") + public void disabledVisionGateStopsBeforeDatabaseLookup() throws Exception { + AihrModelSeedService modelService = mock(AihrModelSeedService.class); + when(modelService.visionAllowed()).thenReturn(false); + AihrSopSeedService service = new AihrSopSeedService(new ObjectMapper(), null, null, "", null, null, modelService); + + Method visionRuntime = AihrSopSeedService.class.getDeclaredMethod("visionRuntime"); + visionRuntime.setAccessible(true); + Optional runtime = (Optional) visionRuntime.invoke(service); + + assertTrue(runtime.isEmpty()); + } + @Test @Tag("dev") public void sopReviewUsesAuthenticatedOperatorForAuditTrail() throws Exception { diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index 93e579f0..c294c933 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -2,6 +2,7 @@ 更新时间:2026-07-14 +- 2026-07-14 BRD G3/G6 视觉外发闸门修复:资料处理的图片 OCR/视频关键帧视觉调用此前直接查询启用模型,可能绕过 `AIHR_AI_RUNTIME_ENABLED`/`AIHR_AI_CHAT_ENABLED` 成本开关并向外部供应商发送原始图片;现复用 `AihrModelSeedService.visionAllowed()` 在共用 `visionRuntime()` 入口拦截,关闭时按既有“待处理/文本降级”语义,不新增配置。补充源码契约测试、`demo-check` marker、运行说明和演示验收说明;未改变已配置视觉模型的正常处理路径。 - 2026-07-14 BRD G3/G6 语音响应边界收紧:TTS 成功写入 `sys_oss` 后原先仍在 JSON 中重复携带完整 base64 音频,增加浏览器缓存、日志和响应体暴露面;当前 `mobile-uni` 与管理端没有消费 `inlineAudioUrl`,现改为成功只返回 OSS `audioUrl/ossId`,仅在 OSS 上传失败时保留内联 data URL 降级。ASR 已成功转写但 OSS 上传失败时,现保留 transcript 并明确无音频地址,不再把可用文字结果整体打成 500。补充源码契约测试、`demo-check` marker 和 API 对接说明;未改变音频回放授权与 OSS 留存策略。 - 2026-07-14 BRD G3/G6 生产验证码安全边界修复:`CaptchaController` 原先允许通过 `aihr.sms.demo-fixed-code-enabled=true` 在 `prod` profile 放行固定短信验证码,和项目要求的“生产代码级禁用”冲突;现仅允许非 `prod` profile 使用 `aihr.sms.dev-fixed-code`,删除生产覆盖开关,并新增源码契约回归与 `demo-check` marker。未修改线上配置或生产数据。 - 2026-07-14 本地验证环境一致性修复:项目 Docker 编排使用 MySQL `13306`、Redis `16379`,但 `application-dev.yml` 原先仍默认连接 `3306/6379`,导致 admin Spring 全量测试误连其他本机容器;现 dev 数据源改为 `AIHR_MYSQL_*`、`AIHR_REDIS_*` 可覆盖配置,默认与本项目编排一致,生产配置不变。`ruoyi-admin` 全量测试已通过。 diff --git a/docs/DEMO_ACCEPTANCE.md b/docs/DEMO_ACCEPTANCE.md index cbfbefdf..e3829c08 100644 --- a/docs/DEMO_ACCEPTANCE.md +++ b/docs/DEMO_ACCEPTANCE.md @@ -30,7 +30,7 @@ | 2 | 员工端 `/h5/#/pages/user/practice/index` | 今日任务 → 每日三题文字或录音作答 → 开始训练 → 训练中点“求助” → 完成评分 | 每日三题即时反馈且语音有回放留痕;求助命中 SOP 或明确报错;完成后画像、历史和主管端待复盘同步 | | 3 | 主管端 `/h5/#/pages/supervisor/index/index` | 团队画像/待复盘 → 进入复盘详情 → 看逐句标注 → 写复盘建议 → 派发再练 | 能看到评分、音频、逐句标注、主管建议;从团队画像派专项后员工端出现专项/再练任务 | | 4 | 管理端 `/train/practice` | 选择训练记录 → 人工校准评分 → 选择试点起止日期 → 导出试点 CSV | 导出只统计批次内能唯一匹配在职组织的正式数据,包含训练次数、每人 10 次完训率、平均分、预警数、主管复盘率、AI/人工分档一致率、SOP 可用率及正式身份明细 | -| 5 | 模型成本闸门 | 设置 `AIHR_AI_RUNTIME_ENABLED=false` 后重启后端 → 调 `/api/aihr/model/chat` | 返回 `cost-guard-fallback`,chat/asr/tts 走现有文本兜底,不影响训练闭环 | +| 5 | 模型成本闸门 | 设置 `AIHR_AI_RUNTIME_ENABLED=false` 后重启后端 → 调 `/api/aihr/model/chat` 或资料处理视觉链路 | 返回 `cost-guard-fallback` 或“待处理”,chat/asr/tts/vision 不再外发,不影响训练闭环 | 试点正式验收前,用 `AIHR_PILOT_TENANT_ID=000000 AIHR_PILOT_START_DATE=2026-07-07 AIHR_PILOT_END_DATE=2026-07-10 AIHR_PILOT_STRICT=true ./scripts/demo-check.sh` 检查 M5 样本闸门,租户和日期替换为本批试点的真实值。闸门只统计目标租户、该时间窗内的数据;正式试点人员必须通过唯一手机号映射到在职组织快照,达到 10 次已完成对练的人数需覆盖试点人员的 80% 以上。校准样本、AI/人工分档一致率和 SOP 可用率未达标时同样失败,不用静态演示 marker 或历史 seed 数据代替真实试点数据。 diff --git a/docs/DEV_SETUP.md b/docs/DEV_SETUP.md index 739f89fc..dbe0bd68 100644 --- a/docs/DEV_SETUP.md +++ b/docs/DEV_SETUP.md @@ -73,7 +73,7 @@ AIHR_ORG_SYNC_SIGNING_SECRET= # 若放在 backend/.env,也可沿用开放平台字段名: # client_id=dn_xxxxxx # client_secret=dns_xxxxxx -# 试点成本闸门:超预算时改 false,chat/asr/tts 会走现有文本兜底 +# 试点成本闸门:超预算时改 false,chat/asr/tts/vision 会停止外发并走现有兜底 AIHR_AI_RUNTIME_ENABLED=true AIHR_AI_CHAT_ENABLED=true AIHR_AI_SPEECH_ENABLED=true diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 87ff2eb7..d9893877 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -613,6 +613,8 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "aihr_knowledge_gap" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java "oss.getOssId(), \"\")" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java "asr audio upload failed(处理错误已隐藏)" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java "visionAllowed" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "!modelService.visionAllowed()" contains backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java '!environment.acceptsProfiles(Profiles.of("prod"))' contains backend/ruoyi-admin/src/test/java/org/dromara/web/controller/CaptchaControllerSecurityTest.java "CaptchaControllerSecurityTest" contains backend/ruoyi-admin/src/main/resources/application-dev.yml "AIHR_MYSQL_PORT:13306"