task-98: 移除 similar-asin/appearance-patent 模块 Coze,状态机与共享组件改名 LLM
Build Backend JAR / build (push) Has been cancelled
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:
+52
-98
@@ -6,9 +6,7 @@ import com.nanri.aiimage.config.SimilarAsinProperties;
|
||||
import com.nanri.aiimage.config.ZiniaoProperties;
|
||||
import com.nanri.aiimage.metrics.ExternalCallMetricsRecorder;
|
||||
import com.nanri.aiimage.modules.brand.client.BrandCheckClient;
|
||||
import com.nanri.aiimage.modules.coze.service.CozeCredentialPoolService;
|
||||
import com.nanri.aiimage.modules.similarasin.client.SimilarAsinCozeClient;
|
||||
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
|
||||
import com.nanri.aiimage.modules.similarasin.client.SimilarAsinLlmClient;
|
||||
import com.nanri.aiimage.modules.ziniao.client.ZiniaoClientImpl;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
@@ -35,13 +33,9 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Task 78:为所有外部调用(Coze / 品牌检查 / 紫鸟)统一增加耗时、重试、
|
||||
* Task 78:为所有外部调用(LLM / 品牌检查 / 紫鸟)统一增加耗时、重试、
|
||||
* 失败率和 payload 字节指标。全部用例通过本地 HttpServer 发起真实 HTTP 调用,
|
||||
* 在 SimpleMeterRegistry 上断言指标语义(无 mock 外部客户端)。
|
||||
*/
|
||||
@@ -52,26 +46,19 @@ class ExternalCallMetricsRecorderTest {
|
||||
private HttpServer server;
|
||||
private int port;
|
||||
private final ExecutorService serverExecutor = Executors.newCachedThreadPool();
|
||||
private final AtomicInteger cozeSubmitCount = new AtomicInteger();
|
||||
private final AtomicInteger llmSubmitCount = new AtomicInteger();
|
||||
private final AtomicInteger ziniaoCount = new AtomicInteger();
|
||||
private final AtomicInteger brandCount = new AtomicInteger();
|
||||
private final AtomicBoolean cozeFailNext = new AtomicBoolean();
|
||||
|
||||
/** 可复用 Coze 凭据池:返回一个固定凭据,避免真实 HTTP 调用被凭据检查拦截。 */
|
||||
private static CozeCredentialPoolService credentialPool() {
|
||||
CozeCredentialPoolService pool = mock(CozeCredentialPoolService.class);
|
||||
when(pool.listEnabled(anyString())).thenReturn(List.of(
|
||||
new CozeCredentialPoolService.CozeCredential("test", "wf-1", "token", Integer.MAX_VALUE)));
|
||||
return pool;
|
||||
}
|
||||
private final AtomicBoolean llmFailNext = new AtomicBoolean();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||
// backlog 128:并发 20 请求时 accept 线程偶发停顿会被 OS 队列吸收,避免连接拒绝
|
||||
server = HttpServer.create(new InetSocketAddress(0), 128);
|
||||
server.setExecutor(serverExecutor);
|
||||
server.createContext("/brand_check", this::handleBrandCheck);
|
||||
server.createContext("/app/builtin/company", this::handleZiniaoCompany);
|
||||
server.createContext("/v1/workflow/run", this::handleCozeSubmit);
|
||||
server.createContext("/v1/chat/completions", this::handleLlmSubmit);
|
||||
server.start();
|
||||
port = server.getAddress().getPort();
|
||||
}
|
||||
@@ -94,17 +81,14 @@ class ExternalCallMetricsRecorderTest {
|
||||
"{\"code\":\"0\",\"data\":{\"companyId\":1001}}".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private void handleCozeSubmit(HttpExchange exchange) throws IOException {
|
||||
int count = cozeSubmitCount.incrementAndGet();
|
||||
if (cozeFailNext.getAndSet(false)) {
|
||||
private void handleLlmSubmit(HttpExchange exchange) throws IOException {
|
||||
int count = llmSubmitCount.incrementAndGet();
|
||||
if (llmFailNext.getAndSet(false)) {
|
||||
exchange.sendResponseHeaders(500, 0);
|
||||
exchange.close();
|
||||
return;
|
||||
}
|
||||
String executeId = "exec-" + count;
|
||||
String payload = "{\"data\":[{\"asin\":\"B0TEST78\",\"country\":\"US\",\"result\":\"ok\",\"conclusion\":\"ok\"}]}";
|
||||
String response = "{\"code\":0,\"data\":{\"execute_id\":\"" + executeId
|
||||
+ "\",\"status\":\"Success\",\"data\":" + payload + "}}";
|
||||
String response = "{\"choices\":[{\"message\":{\"content\":\"{\\\"result\\\":\\\"ok\\\"}\"}}]}";
|
||||
sendJson(exchange, 200, response.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@@ -168,26 +152,21 @@ class ExternalCallMetricsRecorderTest {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 1. 正常默认路径:Coze 批量检查走本地服务,耗时/payload 字节全部记录 ----
|
||||
// ---- 1. 正常默认路径:LLM 对话走本地服务,耗时/payload 字节全部记录 ----
|
||||
|
||||
@Test
|
||||
void test_task_078_payload_metrics_normal_default_path() throws Exception {
|
||||
SimilarAsinProperties props = cozeProps();
|
||||
SimilarAsinCozeClient client =
|
||||
new SimilarAsinCozeClient(props, objectMapper, credentialPool(), new ExternalCallMetricsRecorder(registry));
|
||||
SimilarAsinResultRowDto row = new SimilarAsinResultRowDto();
|
||||
row.setAsin("B0TEST78");
|
||||
row.setTitle("Test");
|
||||
row.setSku("SKU-1");
|
||||
row.setCountry("US");
|
||||
SimilarAsinProperties props = llmProps();
|
||||
SimilarAsinLlmClient client =
|
||||
new SimilarAsinLlmClient(props, objectMapper, new ExternalCallMetricsRecorder(registry));
|
||||
|
||||
List<SimilarAsinResultRowDto> result = client.inspect(List.of(row), "", "test-key");
|
||||
String content = client.invokeChat("test-model", "system", "hello", "test-key");
|
||||
|
||||
assertEquals(1, result.size(), "默认成功路径必须返回完整结果");
|
||||
assertEquals("ok", result.getFirst().getConclusion(), "主输出必须解析到 Coze 结果");
|
||||
assertNotNull(findTimer("aiimage.external-call.duration", "client", "coze"),
|
||||
"必须记录 Coze 调用耗时");
|
||||
assertTrue(findSummary("aiimage.external-call.payload.bytes", "client", "coze").totalAmount() > 0,
|
||||
assertNotNull(content, "默认成功路径必须返回结果");
|
||||
assertTrue(content.contains("ok"), "主输出必须解析到 LLM 结果");
|
||||
assertNotNull(findTimer("aiimage.external-call.duration", "client", "llm"),
|
||||
"必须记录 LLM 调用耗时");
|
||||
assertTrue(findSummary("aiimage.external-call.payload.bytes", "client", "llm").totalAmount() > 0,
|
||||
"必须记录 payload 字节指标");
|
||||
}
|
||||
|
||||
@@ -195,27 +174,19 @@ class ExternalCallMetricsRecorderTest {
|
||||
|
||||
@Test
|
||||
void test_task_078_payload_metrics_normal_multiple_items() throws Exception {
|
||||
SimilarAsinProperties props = cozeProps();
|
||||
SimilarAsinCozeClient client =
|
||||
new SimilarAsinCozeClient(props, objectMapper, credentialPool(), new ExternalCallMetricsRecorder(registry));
|
||||
List<SimilarAsinResultRowDto> rows = new ArrayList<>();
|
||||
SimilarAsinProperties props = llmProps();
|
||||
SimilarAsinLlmClient client =
|
||||
new SimilarAsinLlmClient(props, objectMapper, new ExternalCallMetricsRecorder(registry));
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
SimilarAsinResultRowDto row = new SimilarAsinResultRowDto();
|
||||
row.setAsin("B0BATCH" + i);
|
||||
row.setCountry("US");
|
||||
row.setTitle("Batch " + i);
|
||||
row.setSku("SKU-" + i);
|
||||
rows.add(row);
|
||||
client.invokeChat("test-model", "system", "prompt-" + i, "test-key");
|
||||
}
|
||||
awaitMetric("aiimage.external-call.duration", "client", "llm");
|
||||
assertEquals(3, llmSubmitCount.get(), "三次请求全部真实发出");
|
||||
assertEquals(3.0, counterCount("aiimage.external-call.total",
|
||||
"client", "llm", "result", "success"), "三次成功全部记录");
|
||||
|
||||
List<SimilarAsinResultRowDto> result = client.inspect(rows, "", "test-key");
|
||||
|
||||
assertEquals(3, result.size(), "批量结果不丢失");
|
||||
assertEquals("B0BATCH0", result.get(0).getAsin(), "顺序稳定");
|
||||
assertEquals("B0BATCH2", result.get(2).getAsin(), "顺序稳定");
|
||||
awaitMetric("aiimage.external-call.duration", "client", "coze");
|
||||
|
||||
// 全部外部客户端(Coze / 品牌检查 / 紫鸟)在同一次批量中各自记录指标
|
||||
// 全部外部客户端(LLM / 品牌检查 / 紫鸟)在同一次批量中各自记录指标
|
||||
BrandCheckClient brand = new BrandCheckClient(brandProps(), new ExternalCallMetricsRecorder(registry));
|
||||
brand.checkAll(List.of("Nintendo", "LEGO", "Sony"), "Terms");
|
||||
ZiniaoClientImpl ziniao = new ZiniaoClientImpl(ziniaoProps(), objectMapper, new ExternalCallMetricsRecorder(registry));
|
||||
@@ -280,22 +251,16 @@ class ExternalCallMetricsRecorderTest {
|
||||
|
||||
@Test
|
||||
void test_task_078_payload_metrics_boundary_limit_and_overflow() throws Exception {
|
||||
SimilarAsinProperties props = cozeProps();
|
||||
props.setCozeReadTimeoutMillis(5000);
|
||||
SimilarAsinProperties props = llmProps();
|
||||
ExternalCallMetricsRecorder recorder = new ExternalCallMetricsRecorder(registry);
|
||||
ExecutorService pool = Executors.newFixedThreadPool(4);
|
||||
try {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
int index = i;
|
||||
pool.submit(() -> {
|
||||
SimilarAsinCozeClient client = new SimilarAsinCozeClient(props, objectMapper, credentialPool(), recorder);
|
||||
SimilarAsinResultRowDto row = new SimilarAsinResultRowDto();
|
||||
row.setAsin("B0LIMIT" + index);
|
||||
row.setCountry("US");
|
||||
row.setTitle("Limit " + index);
|
||||
row.setSku("SKU-" + index);
|
||||
SimilarAsinLlmClient client = new SimilarAsinLlmClient(props, objectMapper, recorder);
|
||||
try {
|
||||
client.inspect(List.of(row), "", "test-key");
|
||||
client.invokeChat("test-model", "system", "prompt-" + index, "test-key");
|
||||
} catch (Exception ignored) {
|
||||
// 并发下结果失败也视为已处理
|
||||
}
|
||||
@@ -305,13 +270,13 @@ class ExternalCallMetricsRecorderTest {
|
||||
pool.shutdown();
|
||||
}
|
||||
|
||||
awaitMetric("aiimage.external-call.total", "client", "coze", "result", "success");
|
||||
for (int i = 0; i < 2000 && cozeSubmitCount.get() < 20; i++) {
|
||||
awaitMetric("aiimage.external-call.total", "client", "llm", "result", "success");
|
||||
for (int i = 0; i < 2000 && llmSubmitCount.get() < 20; i++) {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
assertEquals(20, cozeSubmitCount.get(), "并发 20 请求全部真实发出");
|
||||
assertEquals(20, llmSubmitCount.get(), "并发 20 请求全部真实发出");
|
||||
assertEquals(20.0, counterCount("aiimage.external-call.total",
|
||||
"client", "coze", "result", "success"), "20 次成功全部记录,无重复");
|
||||
"client", "llm", "result", "success"), "20 次成功全部记录,无重复");
|
||||
}
|
||||
|
||||
// ---- 7. 非法参数:空列表拒绝,不发起请求,无指标 ----
|
||||
@@ -334,40 +299,29 @@ class ExternalCallMetricsRecorderTest {
|
||||
@Test
|
||||
void test_task_078_payload_metrics_dependency_failure_releases_resources() throws Exception {
|
||||
ExternalCallMetricsRecorder recorder = new ExternalCallMetricsRecorder(registry);
|
||||
SimilarAsinProperties props = cozeProps();
|
||||
props.setCozeReadTimeoutMillis(5000);
|
||||
SimilarAsinCozeClient client =
|
||||
new SimilarAsinCozeClient(props, objectMapper, credentialPool(), recorder);
|
||||
List<SimilarAsinResultRowDto> rows = new ArrayList<>();
|
||||
SimilarAsinResultRowDto row = new SimilarAsinResultRowDto();
|
||||
row.setAsin("B0FAIL78");
|
||||
row.setCountry("US");
|
||||
row.setTitle("Fail");
|
||||
row.setSku("SKU-FAIL");
|
||||
rows.add(row);
|
||||
SimilarAsinProperties props = llmProps();
|
||||
SimilarAsinLlmClient client =
|
||||
new SimilarAsinLlmClient(props, objectMapper, recorder);
|
||||
|
||||
// 第一次调用走 500 失败路径,第二次调用恢复成功:错误可恢复
|
||||
cozeFailNext.set(true);
|
||||
client.inspect(rows, "", "test-key");
|
||||
client.inspect(rows, "", "test-key");
|
||||
llmFailNext.set(true);
|
||||
client.invokeChat("test-model", "system", "hello", "test-key");
|
||||
client.invokeChat("test-model", "system", "hello", "test-key");
|
||||
|
||||
awaitMetric("aiimage.external-call.total", "client", "coze", "result", "failure");
|
||||
assertNotNull(findTimer("aiimage.external-call.duration", "client", "coze"),
|
||||
awaitMetric("aiimage.external-call.total", "client", "llm", "result", "failure");
|
||||
assertNotNull(findTimer("aiimage.external-call.duration", "client", "llm"),
|
||||
"失败调用同样记录耗时");
|
||||
awaitMetric("aiimage.external-call.total", "client", "coze", "result", "success");
|
||||
assertNotNull(findTimer("aiimage.external-call.duration", "client", "coze"),
|
||||
awaitMetric("aiimage.external-call.total", "client", "llm", "result", "success");
|
||||
assertNotNull(findTimer("aiimage.external-call.duration", "client", "llm"),
|
||||
"恢复后的成功调用也记录耗时");
|
||||
assertEquals(1.0, counterCount("aiimage.external-call.total",
|
||||
"client", "coze", "result", "failure"), "失败率指标精确记录一次失败");
|
||||
assertTrue(counterCount("aiimage.external-call.retry.total", "client", "coze") >= 1.0,
|
||||
"客户端重试循环记录重试次数指标");
|
||||
"client", "llm", "result", "failure"), "失败率指标精确记录一次失败");
|
||||
}
|
||||
|
||||
private SimilarAsinProperties cozeProps() {
|
||||
private SimilarAsinProperties llmProps() {
|
||||
SimilarAsinProperties props = new SimilarAsinProperties();
|
||||
props.setCozeBaseUrl("http://127.0.0.1:" + port);
|
||||
props.setCozeWorkflowPath("/v1/workflow/run");
|
||||
props.setCozeWorkflowHistoryPath("/v1/workflows/{workflow_id}/run_histories/{execute_id}");
|
||||
props.setLlmHost("http://127.0.0.1:" + port);
|
||||
props.setLlmRetryTimes(2);
|
||||
return props;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user