diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/AppearancePatentProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/AppearancePatentProperties.java index bb81f8ab..f06de610 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/AppearancePatentProperties.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/AppearancePatentProperties.java @@ -18,7 +18,7 @@ public class AppearancePatentProperties { /** * 外观检测模型(视觉) */ - private String appearanceModel = "gemini-3.7-flash"; + private String appearanceModel = "gemini-3.8-flash"; private int llmMaxTokens = 64000; private int llmConnectTimeoutMillis = 10000; private int llmReadTimeoutMillis = 180000; diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/SimilarAsinProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/SimilarAsinProperties.java index ec0683b5..6b255123 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/SimilarAsinProperties.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/SimilarAsinProperties.java @@ -150,13 +150,13 @@ public class SimilarAsinProperties { private String llmApiKey = ""; /** 类目匹配(一级/二级)使用的小模型。 */ - private String llmCategoryModel = "glm-5.3-flash"; + private String llmCategoryModel = "gemini-3.8-flash"; /** 合规检查(is_conform/reason/category)使用的小模型。 */ - private String llmConformModel = "glm-5.3-flash"; + private String llmConformModel = "gemini-3.8-flash"; /** 图片相似度对比(主图 vs 拼接图)使用的模型。 */ - private String llmImageCompareModel = "glm-5.3-flash"; + private String llmImageCompareModel = "gemini-3.8-flash"; private int llmMaxTokens = 64000; private int llmConnectTimeoutMillis = 10000; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java index ba133bff..9f7fe106 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java @@ -563,14 +563,22 @@ public class AppearancePatentTaskService { .atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); List tasks = listStaleFinalizeCandidates(); for (FileTaskEntity task : tasks) { - if (!isOwnerCurrent(ownerFromTask(task))) { - continue; - } + // P1-8(双实例 stale 判死盲区修复,与 similar-asin 同源):本方法由 + // DeleteBrandStaleTaskService 的 "delete-brand:stale-check" 分布式 job 锁保护, + // 单实例执行;历史上 owner 非本实例的任务直接跳过,若任务 owner 是另一实例则无人判死, + // 只能等 job 级 stuck 扫描误判。现改为全局判死:跨实例并发互斥由 task 分布式锁 + // (acquire 不等待、TTL 5 分钟)保证,owner 实例仍在正常推进的任务心跳不会 stale。 if (!isHeartbeatStale(task, thresholdMillis)) { continue; } + String owner = ownerFromTask(task); + if (!isOwnerCurrent(owner)) { + log.warn("[appearance-patent] 心跳超时任务归其他实例所有,由本实例(job 锁持有者)接管判死/恢复 taskId={} owner={} current={}", + task.getId(), owner, currentInstanceId()); + } TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(task.getId(), 0L); if (lockHandle == null) { + // 锁被 owner 实例/文件任务 worker 占用说明其仍活跃,静默跳过等下一轮 continue; } try (lockHandle) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java index fe043ca9..357a6caa 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java @@ -941,19 +941,33 @@ public class SimilarAsinTaskService { // "等 Python 上传" 分支每 30 分钟自我续命(resetStuckJobs → 重跑 processResultFileJob), // 导致 Python 已断线的 SIMILAR_ASIN 任务永远进不到候选集,30 分钟兜底失效。 // Redis heartbeat 仅在 Python 心跳/上传 result 时写入,不会被 Java 内部循环刷新。 + // P1-8(双实例 stale 判死盲区修复):本方法由 DeleteBrandStaleTaskService 的 + // "delete-brand:stale-check" 分布式 job 锁保护,任意时刻只有持有该锁的单个实例执行。 + // 历史上对 owner 非本实例的任务直接 continue:若任务 owner 是另一实例,owner 实例 + // 因拿不到 job 锁不会执行本方法,拿到 job 锁的实例又因 owner 过滤跳过该任务, + // 导致心跳超时任务无人判死,最终被 job 级 stuck 扫描(不分 owner)误判为 + // 「文件生成任务运行超时」。现改为全局判死: + // 1) 与 owner 实例内部操作互斥由 task 分布式锁保证(acquire 不等待、TTL 5 分钟自动续期, + // 拿不到锁说明对方正在持锁推进,跳过等下轮再判); + // 2) owner 实例仍在正常推进的任务其 Redis 心跳不会 stale,不会被误伤; + // 3) finalizeStaleTask 内部先走 tryRecoverTimedOutPythonTask(封口上传并继续 LLM 收尾), + // 不会粗暴杀任务,跨实例接管是安全的。 long thresholdMillis = LocalDateTime.now() .minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes())) .atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); List tasks = listStaleFinalizeCandidates(); for (FileTaskEntity task : tasks) { - if (!isOwnerCurrent(ownerFromTask(task))) { - continue; - } if (!isHeartbeatStale(task, thresholdMillis)) { continue; } + String owner = ownerFromTask(task); + if (!isOwnerCurrent(owner)) { + log.warn("[similar-asin] 心跳超时任务归其他实例所有,由本实例(job 锁持有者)接管判死/恢复 taskId={} owner={} current={}", + task.getId(), owner, currentInstanceId()); + } TaskDistributedLockService.LockHandle taskLockHandle = acquireTaskLock(task.getId(), 0L); if (taskLockHandle == null) { + // 锁被 owner 实例/文件任务 worker 占用说明其仍活跃,静默跳过等下一轮(2 分钟后再判) continue; } try (taskLockHandle) { diff --git a/backend-java/src/main/resources/application-local.example.yml b/backend-java/src/main/resources/application-local.example.yml index 5f6b9da2..64de7704 100644 --- a/backend-java/src/main/resources/application-local.example.yml +++ b/backend-java/src/main/resources/application-local.example.yml @@ -63,7 +63,7 @@ AIIMAGE_MODULE_CLEANUP_MODULE_TYPES=DEDUPE,SPLIT,CONVERT,DELETE_BRAND AIIMAGE_APPEARANCE_PATENT_LLM_HOST=https://ai.t8star.org AIIMAGE_APPEARANCE_PATENT_TITLE_MODEL=deepseek-v4-flash -AIIMAGE_APPEARANCE_PATENT_APPEARANCE_MODEL=gemini-3.7-flash +AIIMAGE_APPEARANCE_PATENT_APPEARANCE_MODEL=gemini-3.8-flash AIIMAGE_APPEARANCE_PATENT_LLM_MAX_TOKENS=64000 AIIMAGE_APPEARANCE_PATENT_LLM_BATCH_SIZE=10 AIIMAGE_APPEARANCE_PATENT_LLM_ROW_CONCURRENCY=10 diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index 916a2b04..3ee51a4c 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -242,7 +242,7 @@ aiimage: appearance-patent: llm-host: ${AIIMAGE_APPEARANCE_PATENT_LLM_HOST:https://ai.t8star.org} title-model: ${AIIMAGE_APPEARANCE_PATENT_TITLE_MODEL:deepseek-v4-flash} - appearance-model: ${AIIMAGE_APPEARANCE_PATENT_APPEARANCE_MODEL:gemini-3.7-flash} + appearance-model: ${AIIMAGE_APPEARANCE_PATENT_APPEARANCE_MODEL:gemini-3.8-flash} llm-max-tokens: ${AIIMAGE_APPEARANCE_PATENT_LLM_MAX_TOKENS:64000} llm-connect-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_LLM_CONNECT_TIMEOUT_MILLIS:10000} llm-read-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_LLM_READ_TIMEOUT_MILLIS:180000} @@ -272,9 +272,9 @@ aiimage: direct-llm-enabled: ${AIIMAGE_SIMILAR_ASIN_DIRECT_LLM_ENABLED:true} llm-host: ${AIIMAGE_SIMILAR_ASIN_LLM_HOST:https://ai.t8star.org} llm-api-key: ${AIIMAGE_SIMILAR_ASIN_LLM_API_KEY:} - llm-category-model: ${AIIMAGE_SIMILAR_ASIN_LLM_CATEGORY_MODEL:gemini-3.5-flash-lite} - llm-conform-model: ${AIIMAGE_SIMILAR_ASIN_LLM_CONFORM_MODEL:gemini-3.5-flash-lite} - llm-image-compare-model: ${AIIMAGE_SIMILAR_ASIN_LLM_IMAGE_COMPARE_MODEL:gemini-3.7-flash} + llm-category-model: ${AIIMAGE_SIMILAR_ASIN_LLM_CATEGORY_MODEL:gemini-3.8-flash} + llm-conform-model: ${AIIMAGE_SIMILAR_ASIN_LLM_CONFORM_MODEL:gemini-3.8-flash} + llm-image-compare-model: ${AIIMAGE_SIMILAR_ASIN_LLM_IMAGE_COMPARE_MODEL:gemini-3.8-flash} llm-max-tokens: ${AIIMAGE_SIMILAR_ASIN_LLM_MAX_TOKENS:64000} llm-connect-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_LLM_CONNECT_TIMEOUT_MILLIS:10000} llm-read-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_LLM_READ_TIMEOUT_MILLIS:180000} diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentLlmClientHttpTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentLlmClientHttpTest.java index 97ae365a..a405abac 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentLlmClientHttpTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentLlmClientHttpTest.java @@ -126,7 +126,7 @@ class AppearancePatentLlmClientHttpTest { assertThat(result.getAppearanceReason()).contains("视觉拆解"); assertThat(result.getTitleReason()).contains("Apple"); assertThat(callCounts.get("deepseek-v4-flash")).hasValue(1); - assertThat(callCounts.get("gemini-3.7-flash")).hasValue(1); + assertThat(callCounts.get("gemini-3.8-flash")).hasValue(1); } @Test @@ -162,7 +162,7 @@ class AppearancePatentLlmClientHttpTest { AppearancePatentResultRowDto result = rows.get(0); assertThat(result.getAppearanceRisk()).isEqualTo("外观识别异常"); assertThat(callCounts.get("deepseek-v4-flash")).isNull(); - assertThat(callCounts.get("gemini-3.7-flash")).isNull(); + assertThat(callCounts.get("gemini-3.8-flash")).isNull(); } @Test @@ -182,7 +182,7 @@ class AppearancePatentLlmClientHttpTest { client.inspectRows(List.of(row("6", "B006", "普通数据线", "", "https://img.example.com/6.jpg")), null, "test-key"); String appearanceBody = capturedBodies.stream() - .filter(b -> b.contains("gemini-3.7-flash")) + .filter(b -> b.contains("gemini-3.8-flash")) .findFirst() .orElseThrow(); assertThat(appearanceBody).contains("https://img.example.com/6.jpg"); diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/LlmGatewayTlsProbe.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/LlmGatewayTlsProbe.java index f2c3b23a..b388c336 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/LlmGatewayTlsProbe.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/LlmGatewayTlsProbe.java @@ -61,7 +61,7 @@ public class LlmGatewayTlsProbe { } HttpClient client = builder.build(); try { - String body = "{\"model\":\"glm-5.3-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":10}"; + String body = "{\"model\":\"gemini-3.8-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":10}"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://" + host + "/v1/chat/completions")) .timeout(Duration.ofSeconds(30))