package com.nanri.aiimage.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; @Data @ConfigurationProperties(prefix = "aiimage.similar-asin") public class SimilarAsinProperties { /** * P0-1:单次提交 LLM 批次的 row 数量。 * 历史值 10,在含 puzzle 多图行的场景下频繁触发 720712008 * "node executed out of limit: 1000"。降到 3 以避免节点上限被打爆。 * 出现持续 720712008 时还会被 P1-1 滑窗自适应再降到 1。 */ private int llmBatchSize = 3; /** * img_switch=false 时单次提交 LLM 的 row 数。 * 不走图片检测时压力小,恢复到 10 行一批以提高吞吐;开启图片检测时仍使用 llmBatchSize。 */ private int llmTextOnlyBatchSize = 10; private long dbTaskTouchIntervalMillis = 120000L; private long dbJobTouchIntervalMillis = 60000L; private int staleTimeoutMinutes = 30; private String staleFinalizeCron = "0 */2 * * * *"; /** * 末尾零头 batch 的强制 flush 阈值(分钟):当不足 llmBatchSize 的零头 row * 长时间挂着(Python 慢回传)时触发提交。 * 任务级实测:345 行 / 4h 总耗时中,约 2-3 小时是 batch 永远凑不满 batchSize 在等下一波回传, * 把阈值从 15 调到 1:最多 60s 后 1-2 行也强制提交,让 LLM 提交侧持续进票, * 总耗时降到与 Python 回传节奏接近。 */ private int llmFlushPendingMinutes = 1; /** * 图片下载、解码和缩放共享该池;4 核生产机默认 2,避免图片任务占满整机 CPU。 */ private int imageDownloadPoolSize = 2; /** * 单张图片下载超时(秒)。 * P2-10:放宽到 8 + retry=1 在快源(aiproxy/m.media-amazon)下没问题, * 但慢源(cbu01.alicdn)会一直挂 8s 才进入 retry,整体串行时间放大。 * 调到 5s + retry=2,让慢源更早重试新连接,单图最坏耗时 ≈ 5s * (1+2) = 15s。 */ private int imageDownloadTimeoutSeconds = 5; /** 单个结果文件整批图片预取预算,耗尽后缺图单元格降级为 URL。 */ private int imagePrefetchTimeoutSeconds = 1800; /** 多源结果文件组装的单任务硬上限;运行期间由文件任务 heartbeat 保活。 */ private int resultFileTimeoutMinutes = 90; /** * Assemble each source workbook from chunk-scoped result maps. Keeping a * switch makes rollback possible for an existing deployment while the new * bounded path is observed in production. */ private boolean boundedResultAssemblyEnabled = true; /** * assemble 阶段 taskImageCache 的字节上限。 * 默认 256MB:5000 行 × 3 列 × 平均 100KB = 1.5GB 远超 2GB 堆, * 用 BoundedImageCache 按字节累计 LRU 淘汰避免爆堆。 * 由于 embed() 写完即 remove(),活跃图片字节通常 ≤ 100MB,仅在极端 prefetch 领先场景才会触发淘汰。 * 出现淘汰过频影响命中率时可上调到 512MB;2GB 堆约束下不建议超过 768MB。 */ private long imageCacheMaxBytes = 256L * 1024L * 1024L; private String imageLocalCacheDir = ""; private boolean imageDbCacheEnabled = false; /** * 是否启用 merge 增量缓冲:每个 batch DONE 时仅缓冲 llmRows, * 不立即合并到 chunk;finalize 前一次性按 chunkScopeHash 分组合并, * 把 OSS chunk 读写从 1000+ 次降到 chunk 数量级。 */ private boolean llmResultBufferEnabled = true; /** * 解析接口返回的预览行/预览组数量上限。 * 响应体只携带预览行(默认 100),全量行仅写入后端任务载荷。 * 有效范围 [1, 1000];0/负值回退默认,超上限 clamp 到 1000。 */ private int parseResponsePreviewLimit = 100; /** * Task 7:单个源文件大小上限(字节)。超过则拒绝解析,防止无界文件增长。 */ private long maxSourceFileBytes = 50L * 1024L * 1024L; /** * Task 7:单次解析最大有效行数。超过则拒绝解析,防止任务无界增长。 */ private int maxParseRows = 50000; /** * Task 7:单字段最大长度(字符)。超过的字段值截断到该上限,防止内存无界增长。 */ private int maxFieldLength = 2000; /** * Task 8:xlsx(zip) 最大条目数。受控读取在 WorkBookFactory 打开前探测, * 超过则拒绝,防止 zip bomb / 超大工作簿拖垮内存。 */ private int maxWorkbookZipEntries = 20000; /** * Task 8:xlsx(zip) 解压后总字节数上限。同样在打开前探测,超过则拒绝。 */ private long maxWorkbookUncompressedBytes = 512L * 1024L * 1024L; /** * Task 13:单次 chunk 合并后的最大行数。mergeChunkPayload 合并后总行数超过该上限时, * 从最旧行开始降级到 orphan 兜底(assemble 阶段 putIfAbsent 合并回结果),chunk 不无界增长。 * 默认与 maxParseRows 一致(50000)。 */ private int chunkMergeMaxRows = 50000; /** * Task 13:单次 chunk 合并后 payload 的字节上限。合并后序列化字节超过该上限时, * 从最旧行开始降级到 orphan 兜底;单行本身超过该上限时抛异常拒绝合并。 * 默认 16MB:50000 行 × 平均 300B/行 ≈ 15MB,留余量。 */ private long chunkMergePayloadMaxBytes = 16L * 1024L * 1024L; /** * Task 15:图片缓存 last_used_at 异步批量刷新的缓冲阈值。 * lookup 命中先入内存缓冲(按 url_hash 去重),达到该阈值时立即批量 touch; * 其余由定时 flush 兜底,把逐图 UPDATE 合并为批量 UPDATE。 */ private int imageCacheTouchFlushThreshold = 1000; /** * Task 16:图片预取短预算(秒)。assemble 阶段预取在预算内 best-effort * 尽力完成,预算耗尽即取消在途任务并回退 URL,避免整批预取拖垮结果组装。 * 该值仅作用于 assemble 阶段预取;后台预热仍使用 imagePrefetchTimeoutSeconds。 */ private int imagePrefetchBudgetSeconds = 60; /** * 货源查询直连 LLM 模式开关(默认 true:新任务与存量 PENDING 批次都走直连 LLM, * 不再经过工作流中转)。 */ private boolean directLlmEnabled = true; /** 直连 LLM 的 base url(OpenAI 兼容 /v1/chat/completions)。 */ private String llmHost = "https://ai.t8star.org"; /** 直连 LLM 的 Bearer token;前端未传 api_key 时兜底使用。 */ private String llmApiKey = ""; /** 类目匹配(一级/二级)使用的小模型。 */ private String llmCategoryModel = "gemini-3.5-flash-lite"; /** 合规检查(is_conform/reason/category)使用的小模型。 */ private String llmConformModel = "gemini-3.5-flash-lite"; /** 图片相似度对比(主图 vs 拼接图)使用的模型。 */ private String llmImageCompareModel = "gemini-3.7-flash"; private int llmMaxTokens = 64000; private int llmConnectTimeoutMillis = 10000; private int llmReadTimeoutMillis = 180000; private int llmRetryTimes = 3; /** 批内行级并发上限:每行最多 2 次图片对比 + 1 次合规 + 3 次类目匹配。 */ private int llmRowConcurrency = 5; /** 拼接图/主图下载超时(秒),慢源图片较多时放大该值。 */ private int llmImageDownloadTimeoutSeconds = 10; }