feat: update task routing and coze handling
This commit is contained in:
+88
-3
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.config.AppearancePatentProperties;
|
||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentResultRowDto;
|
||||
import com.nanri.aiimage.modules.brand.client.BrandCheckClient;
|
||||
import com.nanri.aiimage.modules.coze.service.CozeCredentialPoolService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -29,10 +30,14 @@ public class AppearancePatentCozeClient {
|
||||
|
||||
private static final String MODULE_TYPE = "APPEARANCE_PATENT";
|
||||
private static final MediaType APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
|
||||
private static final String INFRINGEMENT = "侵权";
|
||||
private static final String NO_INFRINGEMENT = "无侵权";
|
||||
private static final String BRAND_QUERY_FAILED = "商标查询失败";
|
||||
|
||||
private final AppearancePatentProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final CozeCredentialPoolService cozeCredentialPoolService;
|
||||
private final BrandCheckClient brandCheckClient;
|
||||
private final AtomicLong credentialCursor = new AtomicLong();
|
||||
|
||||
public List<AppearancePatentResultRowDto> inspect(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) {
|
||||
@@ -361,7 +366,7 @@ public class AppearancePatentCozeClient {
|
||||
List<String> rowIds = rows.stream().map(row -> nonBlank(row.getId(), "")).toList();
|
||||
List<String> asins = rows.stream().map(row -> nonBlank(row.getAsin(), "")).toList();
|
||||
List<String> countries = rows.stream().map(row -> nonBlank(row.getCountry(), "")).toList();
|
||||
List<String> skus = rows.stream().map(row -> nonBlank(row.getSku(), "")).toList();
|
||||
List<String> skus = rows.stream().map(row -> sanitizeSku(row.getSku())).toList();
|
||||
List<String> titles = rows.stream().map(row -> nonBlank(row.getTitle(), row.getAsin())).toList();
|
||||
List<String> urls = rows.stream().map(row -> nonBlank(row.getUrl(), "")).toList();
|
||||
|
||||
@@ -580,10 +585,15 @@ public class AppearancePatentCozeClient {
|
||||
if (row == null || result == null) {
|
||||
return;
|
||||
}
|
||||
row.setTitleRisk(result.title());
|
||||
if (isNoTitleRisk(result)) {
|
||||
row.setTitleRisk(NO_INFRINGEMENT);
|
||||
} else {
|
||||
BrandCheckClient.BrandCheckBatchResult brandCheck = brandCheckClient.checkTitleText(result.title(), "Terms");
|
||||
row.setTitleRisk(buildTitleRisk(result.title(), brandCheck));
|
||||
}
|
||||
row.setAppearanceRisk(result.appearance());
|
||||
row.setPatentRisk(result.patent());
|
||||
row.setConclusion(result.result());
|
||||
row.setConclusion(buildConclusion(row.getTitleRisk(), row.getAppearanceRisk(), result.result()));
|
||||
row.setStatus(result.status());
|
||||
row.setTitleReason(result.titleReason());
|
||||
row.setAppearanceReason(result.appearanceReason());
|
||||
@@ -591,6 +601,55 @@ public class AppearancePatentCozeClient {
|
||||
row.setScore(result.score());
|
||||
}
|
||||
|
||||
private boolean isNoTitleRisk(CozeResult result) {
|
||||
return result != null
|
||||
&& "无".equals(normalize(result.title()))
|
||||
&& "无".equals(normalize(result.titleReason()));
|
||||
}
|
||||
|
||||
String buildTitleRisk(String cozeTitle, BrandCheckClient.BrandCheckBatchResult brandCheck) {
|
||||
List<String> brands = brandCheck == null ? List.of() : brandCheck.brands();
|
||||
if (brands == null || brands.isEmpty()) {
|
||||
return firstNonBlank(cozeTitle, "");
|
||||
}
|
||||
if (brandCheck.hasFailedData()) {
|
||||
return INFRINGEMENT;
|
||||
}
|
||||
if (brandCheck.hasQueryFailedData()) {
|
||||
return BRAND_QUERY_FAILED;
|
||||
}
|
||||
return NO_INFRINGEMENT;
|
||||
}
|
||||
|
||||
String buildConclusion(String titleRisk, String appearanceRisk, String fallbackConclusion) {
|
||||
if (isInfringement(titleRisk) || isInfringement(appearanceRisk)) {
|
||||
return INFRINGEMENT;
|
||||
}
|
||||
if (isNoInfringement(titleRisk) && isNoInfringement(appearanceRisk)) {
|
||||
return NO_INFRINGEMENT;
|
||||
}
|
||||
if (isBrandQueryFailed(titleRisk)) {
|
||||
return BRAND_QUERY_FAILED;
|
||||
}
|
||||
return firstNonBlank(fallbackConclusion, "");
|
||||
}
|
||||
|
||||
private boolean isInfringement(String value) {
|
||||
String normalized = normalize(value);
|
||||
return normalized.contains("侵权")
|
||||
&& !normalized.contains("无侵权")
|
||||
&& !normalized.contains("未侵权");
|
||||
}
|
||||
|
||||
private boolean isNoInfringement(String value) {
|
||||
String normalized = normalize(value);
|
||||
return normalized.contains("无侵权") || normalized.contains("未侵权");
|
||||
}
|
||||
|
||||
private boolean isBrandQueryFailed(String value) {
|
||||
return normalize(value).contains(BRAND_QUERY_FAILED);
|
||||
}
|
||||
|
||||
private RestClient restClient() {
|
||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(properties.getCozeConnectTimeoutMillis());
|
||||
@@ -1012,6 +1071,32 @@ public class AppearancePatentCozeClient {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
private String sanitizeSku(String value) {
|
||||
String normalized = normalize(value);
|
||||
if (normalized.isBlank() || isProbablyDescriptionSku(normalized)) {
|
||||
return "";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean isProbablyDescriptionSku(String value) {
|
||||
String normalized = normalize(value);
|
||||
if (normalized.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String lower = normalized.toLowerCase(Locale.ROOT);
|
||||
return normalized.length() > 120
|
||||
|| normalized.contains("\n")
|
||||
|| lower.contains(" minimum tensile ")
|
||||
|| lower.contains(" multipurpose:")
|
||||
|| lower.contains(" strong, durable")
|
||||
|| lower.contains(" product includes ")
|
||||
|| lower.contains(" perfect gift")
|
||||
|| lower.contains(" high quality ")
|
||||
|| lower.contains(" easy to ")
|
||||
|| lower.contains(" capacity:");
|
||||
}
|
||||
|
||||
private String firstNonBlank(String preferred, String fallback) {
|
||||
return preferred == null || preferred.isBlank() ? fallback : preferred.trim();
|
||||
}
|
||||
|
||||
+132
-61
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.common.exception.TaskOwnerMismatchException;
|
||||
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
||||
import com.nanri.aiimage.common.util.CozeGroupResultPropagator;
|
||||
import com.nanri.aiimage.common.util.FailedStatusRowFilter;
|
||||
@@ -108,6 +109,7 @@ public class AppearancePatentTaskService {
|
||||
private static final String COZE_STATUS_FAILED = "FAILED";
|
||||
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
private static final String CONTENT_TYPE_ZIP = "application/zip";
|
||||
private static final int CHUNK_PAYLOAD_MERGE_RETRY_LIMIT = 3;
|
||||
private static final int RESULT_ROWS_READ_RETRY_LIMIT = 3;
|
||||
private static final long RESULT_ROWS_READ_RETRY_DELAY_MS = 500L;
|
||||
private static final Duration TASK_LOCK_TTL = Duration.ofMinutes(5);
|
||||
@@ -572,7 +574,8 @@ public class AppearancePatentTaskService {
|
||||
|
||||
public void finalizeStaleTasks() {
|
||||
if (transactionManager != null) {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime threshold = now.minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
List<FileTaskEntity> tasks = listStaleFinalizeCandidates(threshold);
|
||||
for (FileTaskEntity task : tasks) {
|
||||
@@ -596,7 +599,8 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
return;
|
||||
}
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime threshold = now.minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
List<FileTaskEntity> tasks = listStaleFinalizeCandidates(threshold);
|
||||
for (FileTaskEntity task : tasks) {
|
||||
@@ -647,8 +651,8 @@ public class AppearancePatentTaskService {
|
||||
return fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
|
||||
.lt(FileTaskEntity::getCreatedAt, threshold)
|
||||
.orderByAsc(FileTaskEntity::getCreatedAt)
|
||||
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
||||
.orderByAsc(FileTaskEntity::getUpdatedAt)
|
||||
.last("limit 200"));
|
||||
}
|
||||
|
||||
@@ -811,22 +815,6 @@ public class AppearancePatentTaskService {
|
||||
long activeAssembleJobs = taskFileJobService.countActiveAssembleJobs(taskId, MODULE_TYPE);
|
||||
log.info("[appearance-patent] stale recovery probe taskId={} uploadComplete={} pendingCozeStates={} activeAssembleJobs={} persistedRows={}",
|
||||
taskId, uploadComplete, pendingCozeStates, activeAssembleJobs, hasPersistedResultRows(taskId));
|
||||
if (isJavaSideProcessing(taskId)) {
|
||||
// 防止 Coze 永远 pending 时 stale-recovery 永久 defer:
|
||||
// 超过 stale-timeout-minutes × 4 仍未推进的 RUNNING 任务,强制走 finalize 链路。
|
||||
long deferCeilingMinutes = Math.max(1, properties.getStaleTimeoutMinutes()) * 4L;
|
||||
LocalDateTime updatedAt = task.getUpdatedAt();
|
||||
if (updatedAt != null
|
||||
&& Duration.between(updatedAt, LocalDateTime.now()).toMinutes() >= deferCeilingMinutes) {
|
||||
log.warn("[appearance-patent] stale recovery defer ceiling exceeded, forcing finalize taskId={} updatedAt={} ceilingMinutes={} pendingCozeStates={} activeAssembleJobs={}",
|
||||
taskId, updatedAt, deferCeilingMinutes, pendingCozeStates, activeAssembleJobs);
|
||||
return false;
|
||||
}
|
||||
touchJavaSideTaskActivity(taskId);
|
||||
log.info("[appearance-patent] stale recovery deferred because Java-side processing is still active taskId={} uploadComplete={} pendingCozeStates={} activeAssembleJobs={}",
|
||||
taskId, uploadComplete, pendingCozeStates, activeAssembleJobs);
|
||||
return true;
|
||||
}
|
||||
if (!hasPersistedResultRows(taskId)) {
|
||||
log.info("[appearance-patent] stale recovery aborted because no persisted rows taskId={}", taskId);
|
||||
return false;
|
||||
@@ -839,10 +827,10 @@ public class AppearancePatentTaskService {
|
||||
return false;
|
||||
}
|
||||
int pendingRows = collectPendingCozeCandidates(task, loadSubmittedChunks(taskId)).size();
|
||||
log.warn("[appearance-patent] python heartbeat timed out, forcing coze flush taskId={} pendingRows={} pendingCozeStates={} activeAssembleJobs={} forcedScopes={}",
|
||||
log.warn("[appearance-patent] Python 回传心跳超时,已封口上传并继续 Coze/文件收尾 taskId={} pendingRows={} pendingCozeStates={} activeAssembleJobs={} forcedScopes={}",
|
||||
taskId, pendingRows, pendingCozeStates, activeAssembleJobs, forcedScopes);
|
||||
} else {
|
||||
log.warn("[appearance-patent] stale running task resuming coze/file assembly after python timeout taskId={} pendingCozeStates={} activeAssembleJobs={}",
|
||||
log.warn("[appearance-patent] Python 超时恢复继续推进 Coze/文件收尾 taskId={} pendingCozeStates={} activeAssembleJobs={}",
|
||||
taskId, pendingCozeStates, activeAssembleJobs);
|
||||
}
|
||||
submitCozeForSubmittedChunk(new SubmitContext(task, null, null, 0, true, null));
|
||||
@@ -1029,32 +1017,52 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
|
||||
private void mergeChunkPayload(Long taskId, String scopeHash, Integer chunkIndex, List<AppearancePatentResultRowDto> rows) {
|
||||
TaskChunkEntity chunk = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskChunkEntity::getScopeHash, scopeHash)
|
||||
.eq(TaskChunkEntity::getChunkIndex, chunkIndex)
|
||||
.last("limit 1"));
|
||||
if (chunk == null || rows == null || rows.isEmpty()) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, AppearancePatentResultRowDto> persistedRows = readChunkRows(chunk);
|
||||
for (AppearancePatentResultRowDto row : rows) {
|
||||
persistedRows.put(rowKey(row), row);
|
||||
}
|
||||
String payloadJson = writeJson(rows == null ? List.of() : rows, "结果序列化失败");
|
||||
payloadJson = writeJson(new ArrayList<>(persistedRows.values()), "appearance patent chunk payload merge failed");
|
||||
String oldPayload = chunk.getPayloadJson();
|
||||
String storedPayload = storeSharedChunkPayloadVersioned(taskId, scopeHash, chunkIndex, payloadJson);
|
||||
chunk.setPayloadJson(storedPayload);
|
||||
chunk.setPayloadHash(DigestUtil.sha256Hex(payloadJson));
|
||||
chunk.setUpdatedAt(LocalDateTime.now());
|
||||
int updated = taskChunkMapper.updateById(chunk);
|
||||
if (updated <= 0) {
|
||||
for (int attempt = 1; attempt <= CHUNK_PAYLOAD_MERGE_RETRY_LIMIT; attempt++) {
|
||||
TaskChunkEntity chunk = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskChunkEntity::getScopeHash, scopeHash)
|
||||
.eq(TaskChunkEntity::getChunkIndex, chunkIndex)
|
||||
.last("limit 1"));
|
||||
if (chunk == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, AppearancePatentResultRowDto> persistedRows = readChunkRows(chunk);
|
||||
for (AppearancePatentResultRowDto row : rows) {
|
||||
persistedRows.put(rowKey(row), row);
|
||||
}
|
||||
String payloadJson = writeJson(new ArrayList<>(persistedRows.values()), "appearance patent chunk payload merge failed");
|
||||
String oldPayload = chunk.getPayloadJson();
|
||||
String oldPayloadHash = chunk.getPayloadHash();
|
||||
String newPayloadHash = DigestUtil.sha256Hex(payloadJson);
|
||||
String storedPayload = storeSharedChunkPayloadVersioned(taskId, scopeHash, chunkIndex, payloadJson);
|
||||
LambdaUpdateWrapper<TaskChunkEntity> updateWrapper = new LambdaUpdateWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getId, chunk.getId())
|
||||
.set(TaskChunkEntity::getPayloadJson, storedPayload)
|
||||
.set(TaskChunkEntity::getPayloadHash, newPayloadHash)
|
||||
.set(TaskChunkEntity::getUpdatedAt, LocalDateTime.now());
|
||||
if (oldPayloadHash == null) {
|
||||
updateWrapper.isNull(TaskChunkEntity::getPayloadHash);
|
||||
} else {
|
||||
updateWrapper.eq(TaskChunkEntity::getPayloadHash, oldPayloadHash);
|
||||
}
|
||||
int updated = taskChunkMapper.update(null, updateWrapper);
|
||||
if (updated > 0) {
|
||||
log.debug("[appearance-patent] chunk payload replaced taskId={} scopeHash={} chunk={} oldPayload={} newPayload={} attempt={}",
|
||||
taskId, scopeHash, chunkIndex, oldPayload, storedPayload, attempt);
|
||||
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(oldPayload, storedPayload);
|
||||
return;
|
||||
}
|
||||
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
||||
throw new IllegalStateException("appearance patent chunk payload update failed");
|
||||
if (attempt < CHUNK_PAYLOAD_MERGE_RETRY_LIMIT) {
|
||||
log.warn("[appearance-patent] chunk payload CAS conflict, retry merge taskId={} scopeHash={} chunk={} attempt={}/{}",
|
||||
taskId, scopeHash, chunkIndex, attempt, CHUNK_PAYLOAD_MERGE_RETRY_LIMIT);
|
||||
}
|
||||
}
|
||||
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(oldPayload, storedPayload);
|
||||
throw new IllegalStateException("appearance patent chunk payload update conflict");
|
||||
}
|
||||
|
||||
private List<AppearancePatentResultRowDto> expandRows(List<AppearancePatentResultRowDto> representatives,
|
||||
@@ -1438,6 +1446,9 @@ public class AppearancePatentTaskService {
|
||||
int cozeWorkUnits = countCozeWorkUnits(chunks, Math.max(1, properties.getCozeBatchSize()));
|
||||
int plannedCozeUnits = Math.max(cozeWorkUnits, countAllCozeStates(task.getId()));
|
||||
int totalProgressUnits = Math.max(3, plannedCozeUnits + 3);
|
||||
if (countPendingCozeStates(task.getId()) > 0) {
|
||||
finalizeTimedOutCozeStatesForTask(task.getId());
|
||||
}
|
||||
if (countPendingCozeStates(task.getId()) > 0) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
@@ -1605,6 +1616,16 @@ public class AppearancePatentTaskService {
|
||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||
List<CozeCandidate> candidates = collectPendingCozeCandidates(task, chunks);
|
||||
boolean flushRemainder = isResultSubmissionComplete(task.getId());
|
||||
if (!flushRemainder && !candidates.isEmpty()) {
|
||||
LocalDateTime jobUpdatedAt = job.getUpdatedAt();
|
||||
long pendingFlushMillis = cozeFlushPendingMillis();
|
||||
if (jobUpdatedAt != null
|
||||
&& Duration.between(jobUpdatedAt, LocalDateTime.now()).toMillis() >= pendingFlushMillis) {
|
||||
flushRemainder = true;
|
||||
log.warn("[appearance-patent] Coze 零头批次等待超时,强制提交 taskId={} jobId={} pendingRows={} batchSize={} jobUpdatedAt={} flushAfterMillis={}",
|
||||
task.getId(), job.getId(), candidates.size(), batchSize, jobUpdatedAt, pendingFlushMillis);
|
||||
}
|
||||
}
|
||||
int submitLimit = (candidates.size() / batchSize) * batchSize;
|
||||
if (flushRemainder && submitLimit < candidates.size()) {
|
||||
submitLimit = candidates.size();
|
||||
@@ -1869,7 +1890,7 @@ public class AppearancePatentTaskService {
|
||||
: "";
|
||||
if (!poll.hasPayload() && failureMessage.isBlank()) {
|
||||
failureMessage = isCozeStateTimedOut(state)
|
||||
? "Coze async workflow poll timeout"
|
||||
? "Coze 异步工作流轮询超时"
|
||||
: "Coze async workflow completed without output";
|
||||
}
|
||||
List<AppearancePatentResultRowDto> batchRows = readCozeBatchRows(state);
|
||||
@@ -1940,6 +1961,43 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private void finalizeTimedOutCozeStatesForTask(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return;
|
||||
}
|
||||
List<TaskScopeStateEntity> states = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getTaskId, taskId)
|
||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||
.in(TaskScopeStateEntity::getCozeStatus, List.of(COZE_STATUS_SUBMITTED, COZE_STATUS_RUNNING))
|
||||
.orderByAsc(TaskScopeStateEntity::getCozeSubmittedAt)
|
||||
.last("limit 50"));
|
||||
if (states == null || states.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (TaskScopeStateEntity state : states) {
|
||||
if (state == null || state.getId() == null || !isCozeStateTimedOut(state)) {
|
||||
continue;
|
||||
}
|
||||
CozeBatchContext context = readCozeBatchContext(state);
|
||||
if (context == null || context.resultId() == null) {
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze 批次上下文缺失");
|
||||
continue;
|
||||
}
|
||||
List<AppearancePatentResultRowDto> batchRows = readCozeBatchRows(state);
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task != null) {
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
mergeCozeRowsIntoSubmittedChunks(task,
|
||||
cozeClient.markRowsFailed(batchRows, "Coze 异步工作流轮询超时"),
|
||||
allRowsByBaseId);
|
||||
}
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze 异步工作流轮询超时");
|
||||
maybeFinalizeCozeJobLocked(taskId, context);
|
||||
log.warn("[appearance-patent] 文件任务超时兜底已将 Coze pending 批次置为失败 taskId={} stateId={} jobId={}",
|
||||
taskId, state.getId(), context.jobId());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean retryFailedCozeBatchState(TaskScopeStateEntity state,
|
||||
CozeBatchContext context,
|
||||
List<AppearancePatentResultRowDto> batchRows,
|
||||
@@ -2499,6 +2557,10 @@ public class AppearancePatentTaskService {
|
||||
return Duration.between(state.getCozeSubmittedAt(), LocalDateTime.now()).toMillis() >= timeoutMillis;
|
||||
}
|
||||
|
||||
private long cozeFlushPendingMillis() {
|
||||
return Math.max(1, properties.getCozeFlushPendingMinutes()) * 60_000L;
|
||||
}
|
||||
|
||||
private int cozeAttemptCount(TaskScopeStateEntity state) {
|
||||
return state == null || state.getCozeAttemptCount() == null ? 0 : state.getCozeAttemptCount();
|
||||
}
|
||||
@@ -2549,11 +2611,6 @@ public class AppearancePatentTaskService {
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
private boolean isJavaSideProcessing(Long taskId) {
|
||||
return countPendingCozeStates(taskId) > 0
|
||||
|| taskFileJobService.countActiveAssembleJobs(taskId, MODULE_TYPE) > 0;
|
||||
}
|
||||
|
||||
private void touchJavaSideTaskActivity(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return;
|
||||
@@ -2664,13 +2721,27 @@ public class AppearancePatentTaskService {
|
||||
log.warn("[appearance-patent] coze pending submit retry failed taskId={} stateId={} jobId={} rows={} err={}",
|
||||
state.getTaskId(), state.getId(), context.jobId(), batchRows.size(), message);
|
||||
if (isCozeThrottleLockTimeout(message)) {
|
||||
if (isCozeStateTimedOut(state)) {
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
String finalMessage = "等待 Coze 提交凭证超时:" + message;
|
||||
mergeCozeRowsIntoSubmittedChunks(task,
|
||||
cozeClient.markRowsFailed(batchRows, finalMessage),
|
||||
allRowsByBaseId);
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, finalMessage);
|
||||
maybeFinalizeCozeJobLocked(state.getTaskId(), context);
|
||||
log.warn("[appearance-patent] Coze pending 提交等待超时,已将批次置为失败 taskId={} stateId={} jobId={} rows={}",
|
||||
state.getTaskId(), state.getId(), context.jobId(), batchRows.size());
|
||||
return;
|
||||
}
|
||||
keepPendingCozeSubmitState(state, message);
|
||||
return;
|
||||
}
|
||||
int nextAttemptCount = cozeAttemptCount(state) + 1;
|
||||
if (nextAttemptCount >= MAX_COZE_SUBMIT_RETRY_COUNT) {
|
||||
if (nextAttemptCount >= MAX_COZE_SUBMIT_RETRY_COUNT || isCozeStateTimedOut(state)) {
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
String finalMessage = message + " after " + nextAttemptCount + " submit attempts";
|
||||
String finalMessage = isCozeStateTimedOut(state)
|
||||
? message + "(超过 " + properties.getCozePollTimeoutMillis() + "ms)"
|
||||
: message + ",已重试提交 " + nextAttemptCount + " 次";
|
||||
mergeCozeRowsIntoSubmittedChunks(task,
|
||||
cozeClient.markRowsFailed(batchRows, finalMessage),
|
||||
allRowsByBaseId);
|
||||
@@ -2711,7 +2782,11 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
log.warn("[appearance-patent] reject task operation because owner is another instance taskId={} operation={} owner={} current={}",
|
||||
task == null ? null : task.getId(), operation, owner, currentInstanceId());
|
||||
throw new BusinessException(40903, "该任务已绑定到另一台服务实例处理,请通过原实例继续处理");
|
||||
throw new TaskOwnerMismatchException(
|
||||
task == null ? null : task.getId(),
|
||||
operation,
|
||||
owner,
|
||||
currentInstanceId());
|
||||
}
|
||||
|
||||
private boolean isCozeStateOwnedByCurrentInstance(TaskScopeStateEntity state) {
|
||||
@@ -3909,6 +3984,7 @@ public class AppearancePatentTaskService {
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
ensureTaskOwnedByCurrentInstance(task, "parsed payload");
|
||||
return readParsedPayload(task);
|
||||
}
|
||||
|
||||
@@ -3917,6 +3993,7 @@ public class AppearancePatentTaskService {
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
ensureTaskOwnedByCurrentInstance(task, "queue payload");
|
||||
AppearancePatentParsedPayloadDto payload = readParsedPayload(task);
|
||||
AppearancePatentParsedPayloadDto queuePayload = new AppearancePatentParsedPayloadDto();
|
||||
queuePayload.setAiPrompt(payload.getAiPrompt());
|
||||
@@ -4004,6 +4081,8 @@ public class AppearancePatentTaskService {
|
||||
} catch (Exception ex) {
|
||||
log.warn("[appearance-patent] read chunk payload failed taskId={} chunk={} err={}",
|
||||
chunk.getTaskId(), chunk.getChunkIndex(), ex.getMessage());
|
||||
throw new BusinessException("appearance patent chunk payload read failed chunk="
|
||||
+ chunk.getChunkIndex() + ": " + ex.getMessage(), ex);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -4097,13 +4176,6 @@ public class AppearancePatentTaskService {
|
||||
return firstNonBlank(row.getConclusion(), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出最终 xlsx 时的状态展示:
|
||||
* - 如果 status 是 markFailed 合成的(failureSyntheticStatus=true),
|
||||
* 且 row 实际并没有任何风险维度结果,导出层展示错误信息(有则放入,无则留空),
|
||||
* 避免用户原样重新上传时该行被反复识别为失败行重新触发 Coze。
|
||||
* - 真正业务侧失败 / Python 真实回传 FAILED 仍按原值显示。
|
||||
*/
|
||||
private String userFacingStatus(AppearancePatentResultRowDto row) {
|
||||
if (row == null) {
|
||||
return "";
|
||||
@@ -4112,7 +4184,6 @@ public class AppearancePatentTaskService {
|
||||
if (!row.isFailureSyntheticStatus()) {
|
||||
return original;
|
||||
}
|
||||
// 合成状态:有错误信息则放入错误信息,没有则留空
|
||||
return firstNonBlank(row.getError(), "");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user