feat: update task routing and coze handling

This commit is contained in:
super
2026-06-03 17:14:50 +08:00
parent ea37d82d73
commit e98a1a1207
37 changed files with 1883 additions and 254 deletions

View File

@@ -0,0 +1,80 @@
package com.nanri.aiimage.common.util;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CozeGroupResultPropagatorTest {
@Test
void doesNotTreatNoInfringementAsInfringementHit() {
List<ParsedRow> parsedRows = List.of(
new ParsedRow("20_1"),
new ParsedRow("20_2")
);
List<ResultRow> resultRows = List.of(
new ResultRow("无侵权"),
new ResultRow("无侵权")
);
int updatedRows = CozeGroupResultPropagator.propagateByGroup(
parsedRows,
ParsedRow::displayId,
row -> resultRows.get(parsedRows.indexOf(row)),
ResultRow::conclusion,
ResultRow::setConclusion,
List.of("已侵权", "侵权")
);
assertEquals(0, updatedRows);
assertEquals("无侵权", resultRows.get(0).conclusion());
assertEquals("无侵权", resultRows.get(1).conclusion());
}
@Test
void propagatesWhenGroupHasRealInfringementHit() {
List<ParsedRow> parsedRows = List.of(
new ParsedRow("20_1"),
new ParsedRow("20_2")
);
List<ResultRow> resultRows = List.of(
new ResultRow("无侵权"),
new ResultRow("侵权")
);
int updatedRows = CozeGroupResultPropagator.propagateByGroup(
parsedRows,
ParsedRow::displayId,
row -> resultRows.get(parsedRows.indexOf(row)),
ResultRow::conclusion,
ResultRow::setConclusion,
List.of("已侵权", "侵权")
);
assertEquals(1, updatedRows);
assertEquals("侵权", resultRows.get(0).conclusion());
assertEquals("侵权", resultRows.get(1).conclusion());
}
private record ParsedRow(String displayId) {
}
private static final class ResultRow {
private String conclusion;
private ResultRow(String conclusion) {
this.conclusion = conclusion;
}
private String conclusion() {
return conclusion;
}
private void setConclusion(String conclusion) {
this.conclusion = conclusion;
}
}
}

View File

@@ -0,0 +1,64 @@
package com.nanri.aiimage.modules.appearancepatent.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.config.AppearancePatentProperties;
import com.nanri.aiimage.config.BrandCheckProperties;
import com.nanri.aiimage.modules.brand.client.BrandCheckClient;
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentResultRowDto;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
class AppearancePatentCozeClientTest {
private final AppearancePatentCozeClient client = new AppearancePatentCozeClient(
new AppearancePatentProperties(),
new ObjectMapper(),
null,
new BrandCheckClient(new BrandCheckProperties())
);
@Test
void titleRiskIsInfringementWhenBrandCheckHasFailedData() {
BrandCheckClient.BrandCheckBatchResult result =
new BrandCheckClient.BrandCheckBatchResult(List.of("阿凡达"), List.of("阿凡达"), List.of());
assertThat(client.buildTitleRisk("阿凡达", result)).isEqualTo("侵权");
}
@Test
void conclusionIsInfringementWhenTitleOrAppearanceIsInfringement() {
assertThat(client.buildConclusion("侵权", "无侵权", "")).isEqualTo("侵权");
assertThat(client.buildConclusion("无侵权", "侵权", "")).isEqualTo("侵权");
assertThat(client.buildConclusion("无侵权", "无侵权", "")).isEqualTo("无侵权");
}
@Test
void skipBrandCheckWhenCozeTitleAndReasonAreNone() throws Exception {
BrandCheckClient brandCheckClient = mock(BrandCheckClient.class);
AppearancePatentCozeClient cozeClient = new AppearancePatentCozeClient(
new AppearancePatentProperties(),
new ObjectMapper(),
null,
brandCheckClient
);
AppearancePatentResultRowDto row = new AppearancePatentResultRowDto();
row.setId("1");
String dataText = """
{"data":[{"row_id":"1","title":"","title_reason":"","appearance":"无侵权","result":"无侵权"}]}
""";
List<AppearancePatentResultRowDto> merged = cozeClient.mergeRowsFromDataText(List.of(row), dataText);
assertThat(merged).hasSize(1);
assertThat(merged.get(0).getTitleRisk()).isEqualTo("无侵权");
assertThat(merged.get(0).getConclusion()).isEqualTo("无侵权");
verify(brandCheckClient, never()).checkTitleText(anyString(), anyString());
}
}

View File

@@ -0,0 +1,29 @@
package com.nanri.aiimage.modules.brand.client;
import com.nanri.aiimage.config.BrandCheckProperties;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class BrandCheckClientTest {
@Test
void splitTitleTextSupportsCommonSeparatorsAndQuotes() {
BrandCheckClient client = new BrandCheckClient(new BrandCheckProperties());
List<String> brands = client.splitTitleText("'阿凡达,任天堂' Disney、LEGO\nSony");
assertThat(brands).containsExactly("阿凡达", "任天堂", "Disney", "LEGO", "Sony");
}
@Test
void splitTitleTextDeduplicatesBlankValues() {
BrandCheckClient client = new BrandCheckClient(new BrandCheckProperties());
List<String> brands = client.splitTitleText(" 任天堂, ,任天堂Sony ");
assertThat(brands).containsExactly("任天堂", "Sony");
}
}

View File

@@ -0,0 +1,86 @@
package com.nanri.aiimage.modules.similarasin.client;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.config.SimilarAsinProperties;
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SimilarAsinCozeClientTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
@SuppressWarnings("unchecked")
void buildParametersIncludesAlibabaPriceFromPythonPayload() throws Exception {
List<SimilarAsinResultRowDto> rows = objectMapper.readValue("""
[
{
"asin": "B0TEST123",
"url": "https://m.media-amazon.com/images/I/main.jpg",
"alibaba": [
{"url": "https://cbu01.alicdn.com/img/ibank/a.jpg", "price": 12.80},
{"url": "https://cbu01.alicdn.com/img/ibank/b.jpg", "price": 19.99}
],
"title": "Test title",
"sku": "SKU-1"
}
]
""", new TypeReference<>() {
});
SimilarAsinCozeClient client = new SimilarAsinCozeClient(new SimilarAsinProperties(), objectMapper, null);
Method method = SimilarAsinCozeClient.class.getDeclaredMethod(
"buildParameters", List.class, String.class, String.class, boolean.class);
method.setAccessible(true);
Map<String, Object> parameters = (Map<String, Object>) method.invoke(client, rows, "", "", true);
List<Map<String, Object>> items = (List<Map<String, Object>>) parameters.get("items");
Map<String, Object> item = items.getFirst();
List<Map<String, Object>> alibaba = (List<Map<String, Object>>) item.get("alibaba");
assertEquals("B0TEST123", item.get("asin"));
assertEquals("https://m.media-amazon.com/images/I/main.jpg", item.get("url"));
assertEquals("Test title", item.get("title"));
assertEquals("SKU-1", item.get("sku"));
assertEquals(2, alibaba.size());
assertEquals("https://cbu01.alicdn.com/img/ibank/a.jpg", alibaba.get(0).get("url"));
assertEquals(0, new BigDecimal("12.8").compareTo((BigDecimal) alibaba.get(0).get("price")));
assertEquals("https://cbu01.alicdn.com/img/ibank/b.jpg", alibaba.get(1).get("url"));
assertEquals(0, new BigDecimal("19.99").compareTo((BigDecimal) alibaba.get(1).get("price")));
assertTrue(rows.getFirst().hasImageUrl());
}
@Test
@SuppressWarnings("unchecked")
void buildParametersFallsBackAlibabaFromLegacyUrlsAndTopLevelPrice() throws Exception {
SimilarAsinResultRowDto row = new SimilarAsinResultRowDto();
row.setAsin("B0LEGACY1");
row.setUrl("https://m.media-amazon.com/images/I/main.jpg");
row.setUrls(List.of("https://cbu01.alicdn.com/img/ibank/legacy-a.jpg"));
row.setPrice("8.50");
row.setTitle("Legacy title");
row.setSku("SKU-LEGACY");
SimilarAsinCozeClient client = new SimilarAsinCozeClient(new SimilarAsinProperties(), objectMapper, null);
Method method = SimilarAsinCozeClient.class.getDeclaredMethod(
"buildParameters", List.class, String.class, String.class, boolean.class);
method.setAccessible(true);
Map<String, Object> parameters = (Map<String, Object>) method.invoke(client, List.of(row), "", "", false);
List<Map<String, Object>> items = (List<Map<String, Object>>) parameters.get("items");
List<Map<String, Object>> alibaba = (List<Map<String, Object>>) items.getFirst().get("alibaba");
assertEquals(1, alibaba.size());
assertEquals("https://cbu01.alicdn.com/img/ibank/legacy-a.jpg", alibaba.getFirst().get("url"));
assertEquals(0, new BigDecimal("8.5").compareTo((BigDecimal) alibaba.getFirst().get("price")));
}
}

View File

@@ -7,13 +7,16 @@ import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -154,4 +157,47 @@ class SimilarAsinImageEmbedderTest {
assertEquals(12345678L, ex.size());
assertTrue(ex.getMessage().contains("oversize"));
}
@Test
void httpClientKeepsImageDownloadResilienceEnabled() {
assertTrue(embedder.httpClient().retryOnConnectionFailure(), "OkHttp should retry transient connection failures");
assertTrue(embedder.httpClient().followRedirects(), "image CDN redirects should be followed");
assertTrue(embedder.httpClient().followSslRedirects(), "signed CDN downloads should follow browser-like SSL redirects");
}
@Test
void buildImageRequestUsesBrowserLikeHeaders() {
var req = SimilarAsinImageEmbedder.buildImageRequest("https://m.media-amazon.com/images/I/abc.jpg");
assertEquals("GET", req.method());
assertTrue(req.header("User-Agent").contains("Chrome"), "User-Agent should look browser-like");
assertTrue(req.header("Accept").contains("image/"), "Accept should prefer images");
assertEquals("https://www.amazon.com/", req.header("Referer"));
assertEquals("no-cache", req.header("Cache-Control"));
assertNotNull(req.header("Accept-Language"));
}
@Test
void buildImageRequestUsesDownloadHeadersForCozeSignedImages() {
var req = SimilarAsinImageEmbedder.buildImageRequest(
"https://lf9-bot-platform-tos-sign.coze.cn/bot-studio-bot-platform/bot_files/1/image/jpeg/2/merged_image.jpg?x-expires=1&x-signature=a");
assertEquals("GET", req.method());
assertTrue(req.header("User-Agent").contains("Chrome"), "User-Agent should look browser-like");
assertEquals("*/*", req.header("Accept"));
assertNull(req.header("Referer"), "Coze signed download links should not carry an Amazon referer");
assertEquals("no-cache", req.header("Cache-Control"));
assertTrue(SimilarAsinImageEmbedder.isCozeSignedImageUrl(req.url().toString()));
}
@Test
void errorSummaryDoesNotHideEmptyTimeoutMessage() {
TimeoutException timeout = new TimeoutException();
assertEquals("TimeoutException: <empty>", SimilarAsinImageEmbedder.errorSummary(timeout));
IOException io = new IOException("outer", new TimeoutException("inner"));
String summary = SimilarAsinImageEmbedder.errorSummary(io);
assertTrue(summary.contains("IOException: outer"));
assertTrue(summary.contains("cause=TimeoutException: inner"));
}
}