task-98: 移除 similar-asin/appearance-patent 模块 Coze,状态机与共享组件改名 LLM
Build Backend JAR / build (push) Has been cancelled

- similarasin/appearancepatent 模块全部 Coze 工作流调用改走 direct-LLM(已确认唯一运行路径)
- 共享组件改名:CozeTaskQueueGate→TaskQueueGate、CozeGroupResultPropagator→GroupResultPropagator
- 状态机改名:biz_task_scope_state 的 coze_* 列→llm_*、stateJson coze 键→llm(V100 迁移已应用生产)
- 删除 biz_coze_credential 表、CozeCredential* 类、SimilarAsinCozeClient、AppearancePatentCozeClient→LlmClient
- 前端 brand 页 Coze 文案→LLM;Python 后端删除 cozepy 依赖与死配置
- 修复 TaskResultFileJobWorker 启动失败:TaskFileJobConfig 注册 ResultFileJobHandlerRegistry 与 13 个 handler bean(含 validateCoverage 启动校验)
This commit is contained in:
2026-09-01 02:19:45 +08:00
parent 0cc7380205
commit e2607ab723
109 changed files with 6903 additions and 4582 deletions
@@ -0,0 +1,237 @@
package com.nanri.aiimage.common.service;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.common.exception.TaskOwnerMismatchException;
import com.nanri.aiimage.config.InstanceRoutingProperties;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.DelegatingServletInputStream;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.util.ContentCachingRequestWrapper;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* TaskOwnerForwardService 转发安全测试(任务 54)。
* 反射调用私有静态方法 requestBody / copyForwardHeaders / hasAlreadyForwarded
* 验证 multipart 大 body 转发路径与 hop-by-hop 头剔除。
*/
class TaskOwnerForwardServiceTest {
private static final byte[] MULTIPART_BODY = (
"-----b\r\nContent-Disposition: form-data; name=\"f\"; filename=\"a.bin\"\r\n\r\n"
+ "x".repeat(2048) + "\r\n-----b--\r\n").getBytes(StandardCharsets.UTF_8);
private static Object invoke(String name, Class<?>[] paramTypes, Object... args) throws Exception {
Method method = TaskOwnerForwardService.class.getDeclaredMethod(name, paramTypes);
method.setAccessible(true);
try {
return method.invoke(null, args);
} catch (InvocationTargetException e) {
throw (Exception) e.getCause();
}
}
private static byte[] requestBody(HttpServletRequest request) throws Exception {
return (byte[]) invoke("requestBody", new Class<?>[]{HttpServletRequest.class}, request);
}
private static HttpHeaders forwardHeaders(HttpServletRequest request, String currentInstanceId) throws Exception {
return (HttpHeaders) invoke("copyForwardHeaders",
new Class<?>[]{HttpServletRequest.class, String.class}, request, currentInstanceId);
}
private static boolean alreadyForwarded(HttpServletRequest request) throws Exception {
return (boolean) invoke("hasAlreadyForwarded", new Class<?>[]{HttpServletRequest.class}, request);
}
@Test
void nonWrapperReadsInputStream() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
request.setContent(MULTIPART_BODY);
assertArrayEquals(MULTIPART_BODY, requestBody(request), "非 wrapper 走 inputStream 路径读完整 body");
}
@Test
void wrapperReadsCache() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
request.setContent(MULTIPART_BODY);
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request);
byte[] body = requestBody(wrapper);
assertArrayEquals(new byte[0], body, "wrapper 路径读缓存:body 未被消费时缓存为空数组");
byte[] content = wrapper.getContentAsByteArray();
assertTrue(content.length == 0, "未消费则缓存仍为空");
wrapper.getInputStream().readAllBytes();
assertArrayEquals(MULTIPART_BODY, requestBody(wrapper), "消费后缓存与原始 body 一致");
}
@Test
void multipartBodyForwardOk() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/upload");
request.setContentType("multipart/form-data; boundary=----b");
request.setContent(MULTIPART_BODY);
assertArrayEquals(MULTIPART_BODY, requestBody(request), "multipart 转发 body 与原请求字节一致");
}
@Test
void ioErrorHasMessage() {
// 伪造读流即抛 IOException 的 requestread() 抛 IOExceptionStreamUtils 原样上抛
java.io.InputStream failing = new java.io.InputStream() {
@Override
public int read() throws java.io.IOException {
throw new java.io.IOException("stream closed");
}
};
MockHttpServletRequest broken = new MockHttpServletRequest("POST", "/api/x") {
@Override
public jakarta.servlet.ServletInputStream getInputStream() {
return new DelegatingServletInputStream(failing);
}
};
broken.setContent("x".getBytes(StandardCharsets.UTF_8));
BusinessException ex = assertThrows(BusinessException.class,
() -> invoke("requestBody", new Class<?>[]{HttpServletRequest.class}, broken),
"IO 异常包装为 BusinessException");
assertTrue(ex.getMessage().contains("读取转发请求体失败"), "异常消息携带上下文:" + ex.getMessage());
}
@Test
void hopByHopHeadersRemoved() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
request.addHeader("Connection", "keep-alive");
request.addHeader("Transfer-Encoding", "chunked");
request.addHeader("Host", "example.com");
request.addHeader("Content-Length", "123");
request.addHeader("X-Custom", "keep-me");
HttpHeaders headers = forwardHeaders(request, "server-110");
assertFalse(headers.containsKey("Connection"), "connection 剔除");
assertFalse(headers.containsKey("Transfer-Encoding"), "transfer-encoding 剔除");
assertFalse(headers.containsKey("Host"), "host 剔除");
assertFalse(headers.containsKey("Content-Length"), "content-length 剔除");
assertEquals("keep-me", headers.getFirst("X-Custom"), "普通头保留");
}
@Test
void forwardedHeaderSet() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
HttpHeaders headers = forwardHeaders(request, "server-121");
assertEquals("server-121", headers.getFirst(TaskOwnerForwardService.FORWARDED_HEADER),
"转发头标记当前实例");
}
@Test
void contentTypeKept() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
request.setContentType("multipart/form-data; boundary=----b");
HttpHeaders headers = forwardHeaders(request, "server-110");
assertEquals("multipart/form-data; boundary=----b", headers.getFirst(HttpHeaders.CONTENT_TYPE),
"content-type 保留");
}
@Test
void loopDetected() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
request.addHeader(TaskOwnerForwardService.FORWARDED_HEADER, "server-110");
assertTrue(alreadyForwarded(request), "已带转发头判定为循环");
MockHttpServletRequest fresh = new MockHttpServletRequest("POST", "/api/forward");
assertFalse(alreadyForwarded(fresh), "无转发头不判定循环");
}
@Test
void forwardExceptionCarriesLoopMessage() {
TaskOwnerMismatchException ex = new TaskOwnerMismatchException(
1L, "op", "server-110", "server-121");
InstanceRoutingProperties properties = new InstanceRoutingProperties();
properties.setRoutes(Map.of("server-110", "http://10.0.0.1:18080"));
TaskOwnerForwardService service = new TaskOwnerForwardService(properties);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/forward");
request.addHeader(TaskOwnerForwardService.FORWARDED_HEADER, "server-110");
BusinessException loopEx = assertThrows(BusinessException.class,
() -> service.forwardCurrentRequest(ex, request),
"循环转发拒绝并抛 BusinessException");
assertTrue(loopEx.getMessage().contains("循环"), "循环消息:" + loopEx.getMessage());
}
private static byte[] forwardBodyThroughWrapper(byte[] content, String contentType) throws Exception {
return forwardBodyThroughWrapper(content, contentType, 1024 * 1024);
}
private static byte[] forwardBodyThroughWrapper(byte[] content, String contentType, int cacheLimitBytes) throws Exception {
// 模拟真实链路:过滤器包装 → 业务读取(填充缓存)→ 转发读缓存
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/upload");
request.setContentType(contentType);
request.setContent(content);
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request, cacheLimitBytes);
wrapper.getInputStream().readAllBytes();
return requestBody(wrapper);
}
@Test
void forwardJsonBytesEqual() throws Exception {
byte[] body = "{\"taskId\":100,\"status\":\"RUNNING\"}".getBytes(StandardCharsets.UTF_8);
assertArrayEquals(body, forwardBodyThroughWrapper(body, "application/json"), "json 字节级一致");
}
@Test
void forwardMultipartBytesEqual() throws Exception {
assertArrayEquals(MULTIPART_BODY, forwardBodyThroughWrapper(MULTIPART_BODY, "multipart/form-data; boundary=----b"),
"multipart 字节级一致");
}
@Test
void forwardEmptyBodySafe() throws Exception {
byte[] empty = new byte[0];
assertArrayEquals(empty, forwardBodyThroughWrapper(empty, "application/json"), "空 body 安全");
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/empty");
assertArrayEquals(empty, requestBody(request), "非包装空 body 返回空数组");
}
@Test
void forwardLargeBodyEqual() throws Exception {
byte[] large = ("{\"payload\":\"" + "x".repeat(1024 * 1024 + 100) + "\"}").getBytes(StandardCharsets.UTF_8);
assertArrayEquals(large, forwardBodyThroughWrapper(large, "application/json", 2 * 1024 * 1024), ">1MB 大 body 一致");
}
@Test
void forwardUtf8ChineseEqual() throws Exception {
byte[] body = "{\"name\":\"任务进度查询\",\"备注\":\"成功\"}".getBytes(StandardCharsets.UTF_8);
assertArrayEquals(body, forwardBodyThroughWrapper(body, "application/json; charset=utf-8"), "中文 UTF-8 一致");
}
@Test
void forwardSpecialCharsEqual() throws Exception {
byte[] body = "{\"s\":\"a\\n\\t\\\"b\\\\céü中\"}".getBytes(StandardCharsets.UTF_8);
assertArrayEquals(body, forwardBodyThroughWrapper(body, "application/json"), "转义与特殊字符一致");
}
@Test
void forwardMultipleChunksEqual() throws Exception {
StringBuilder sb = new StringBuilder("{\"chunks\":[");
for (int i = 0; i < 50; i++) {
if (i > 0) sb.append(',');
sb.append('"').append("chunk-").append(i).append('"');
}
sb.append("]}");
byte[] body = sb.toString().getBytes(StandardCharsets.UTF_8);
assertArrayEquals(body, forwardBodyThroughWrapper(body, "application/json"), "多段内容一致");
}
@Test
void forwardIdentityRepeated() throws Exception {
byte[] body = "{\"repeat\":\"yes\"}".getBytes(StandardCharsets.UTF_8);
byte[] first = forwardBodyThroughWrapper(body, "application/json");
byte[] second = forwardBodyThroughWrapper(body, "application/json");
assertArrayEquals(first, second, "两次转发结果一致");
assertArrayEquals(body, second, "与原始一致");
}
}
@@ -6,7 +6,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CozeGroupResultPropagatorTest {
class GroupResultPropagatorTest {
@Test
void doesNotTreatNoInfringementAsInfringementHit() {
@@ -19,7 +19,7 @@ class CozeGroupResultPropagatorTest {
new ResultRow("无侵权")
);
int updatedRows = CozeGroupResultPropagator.propagateByGroup(
int updatedRows = GroupResultPropagator.propagateByGroup(
parsedRows,
ParsedRow::displayId,
row -> resultRows.get(parsedRows.indexOf(row)),
@@ -44,7 +44,7 @@ class CozeGroupResultPropagatorTest {
new ResultRow("侵权")
);
int updatedRows = CozeGroupResultPropagator.propagateByGroup(
int updatedRows = GroupResultPropagator.propagateByGroup(
parsedRows,
ParsedRow::displayId,
row -> resultRows.get(parsedRows.indexOf(row)),