修改完善这个专利部分
This commit is contained in:
+123
-18
@@ -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.coze.service.CozeCredentialPoolService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -19,22 +20,26 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
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 final AppearancePatentProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final CozeCredentialPoolService cozeCredentialPoolService;
|
||||
private final AtomicLong credentialCursor = new AtomicLong();
|
||||
|
||||
public List<AppearancePatentResultRowDto> inspect(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
if (properties.getCozeToken() == null || properties.getCozeToken().isBlank()) {
|
||||
if (!hasConfiguredCredential()) {
|
||||
log.warn("[appearance-patent] coze token not configured, keep raw rows size={}", rows.size());
|
||||
return rows.stream().map(this::copy).toList();
|
||||
}
|
||||
@@ -48,17 +53,31 @@ public class AppearancePatentCozeClient {
|
||||
}
|
||||
|
||||
public CozeSubmitResponse submitWorkflow(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey));
|
||||
return submitWorkflow(rows, prompt, apiKey, nextCredential());
|
||||
}
|
||||
|
||||
public CozeSubmitResponse submitWorkflow(List<AppearancePatentResultRowDto> rows,
|
||||
String prompt,
|
||||
String apiKey,
|
||||
CozeCredentialRef credential) throws Exception {
|
||||
CozeCredentialRef resolvedCredential = resolveCredential(credential);
|
||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, resolvedCredential));
|
||||
ensureSuccess(submitRoot);
|
||||
return new CozeSubmitResponse(
|
||||
extractExecuteId(submitRoot),
|
||||
extractResultDataText(submitRoot),
|
||||
writeJson(submitRoot)
|
||||
writeJson(submitRoot),
|
||||
resolvedCredential.name()
|
||||
);
|
||||
}
|
||||
|
||||
public CozePollResponse pollWorkflow(String executeId) throws Exception {
|
||||
JsonNode pollRoot = objectMapper.readTree(getWorkflowHistory(executeId));
|
||||
return pollWorkflow(executeId, null);
|
||||
}
|
||||
|
||||
public CozePollResponse pollWorkflow(String executeId, CozeCredentialRef credential) throws Exception {
|
||||
CozeCredentialRef resolvedCredential = resolveCredential(credential);
|
||||
JsonNode pollRoot = objectMapper.readTree(getWorkflowHistory(executeId, resolvedCredential));
|
||||
ensureSuccess(pollRoot);
|
||||
String status = normalize(resolveWorkflowStatus(pollRoot)).toUpperCase(Locale.ROOT);
|
||||
String dataText = extractResultDataText(pollRoot);
|
||||
@@ -66,7 +85,8 @@ public class AppearancePatentCozeClient {
|
||||
String failureMessage = isFailedWorkflowStatus(status)
|
||||
? firstNonBlank(resolveFailureMessage(pollRoot), "Coze async workflow failed")
|
||||
: "";
|
||||
return new CozePollResponse(executeId, status, dataText, outputText, failureMessage, writeJson(pollRoot));
|
||||
return new CozePollResponse(executeId, status, dataText, outputText, failureMessage, writeJson(pollRoot),
|
||||
resolvedCredential.name());
|
||||
}
|
||||
|
||||
public List<AppearancePatentResultRowDto> mergeRowsFromDataText(List<AppearancePatentResultRowDto> rows, String dataText) throws Exception {
|
||||
@@ -164,7 +184,8 @@ public class AppearancePatentCozeClient {
|
||||
}
|
||||
|
||||
private String runWorkflowAsyncAndWait(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey));
|
||||
CozeCredentialRef credential = nextCredential();
|
||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, credential));
|
||||
ensureSuccess(submitRoot);
|
||||
|
||||
String immediateData = extractResultDataText(submitRoot);
|
||||
@@ -180,7 +201,7 @@ public class AppearancePatentCozeClient {
|
||||
long deadline = System.currentTimeMillis() + Math.max(10000, properties.getCozePollTimeoutMillis());
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
ensureNotInterrupted();
|
||||
JsonNode pollRoot = objectMapper.readTree(getWorkflowHistory(executeId));
|
||||
JsonNode pollRoot = objectMapper.readTree(getWorkflowHistory(executeId, credential));
|
||||
ensureSuccess(pollRoot);
|
||||
|
||||
String dataText = extractResultDataText(pollRoot);
|
||||
@@ -205,20 +226,24 @@ public class AppearancePatentCozeClient {
|
||||
throw new IllegalStateException("Coze async workflow poll timeout");
|
||||
}
|
||||
|
||||
private String postWorkflow(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) {
|
||||
private String postWorkflow(List<AppearancePatentResultRowDto> rows,
|
||||
String prompt,
|
||||
String apiKey,
|
||||
CozeCredentialRef credential) {
|
||||
Map<String, Object> parameters = buildParameters(rows, prompt, apiKey);
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("workflow_id", properties.getCozeWorkflowId());
|
||||
body.put("workflow_id", credential.workflowId());
|
||||
body.put("parameters", parameters);
|
||||
body.put("is_async", Boolean.TRUE);
|
||||
log.info("[appearance-patent] coze request url={} body={}",
|
||||
log.info("[appearance-patent] coze request credential={} url={} body={}",
|
||||
credential.name(),
|
||||
joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()),
|
||||
writeJson(maskCozeRequestBody(body)));
|
||||
|
||||
RestClient.RequestBodySpec request = restClient().post()
|
||||
.uri(joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()))
|
||||
.headers(headers -> {
|
||||
headers.setBearerAuth(stripBearer(properties.getCozeToken()));
|
||||
headers.setBearerAuth(stripBearer(credential.token()));
|
||||
headers.setContentType(APPLICATION_JSON_UTF8);
|
||||
headers.set(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
|
||||
});
|
||||
@@ -233,28 +258,98 @@ public class AppearancePatentCozeClient {
|
||||
});
|
||||
}
|
||||
|
||||
private String getWorkflowHistory(String executeId) {
|
||||
private String getWorkflowHistory(String executeId, CozeCredentialRef credential) {
|
||||
String path = properties.getCozeWorkflowHistoryPath()
|
||||
.replace("{workflow_id}", properties.getCozeWorkflowId())
|
||||
.replace("{workflow_id}", credential.workflowId())
|
||||
.replace("{execute_id}", executeId);
|
||||
return restClient().get()
|
||||
.uri(joinUrl(properties.getCozeBaseUrl(), path))
|
||||
.headers(headers -> {
|
||||
headers.setBearerAuth(stripBearer(properties.getCozeToken()));
|
||||
headers.setBearerAuth(stripBearer(credential.token()));
|
||||
headers.setContentType(APPLICATION_JSON_UTF8);
|
||||
headers.set(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
|
||||
})
|
||||
.exchange((clientRequest, clientResponse) -> {
|
||||
byte[] responseBytes = StreamUtils.copyToByteArray(clientResponse.getBody());
|
||||
String responseText = responseBytes.length == 0 ? "" : new String(responseBytes, StandardCharsets.UTF_8);
|
||||
log.info("[appearance-patent] coze history response executeId={} status={} body={}",
|
||||
executeId,
|
||||
log.info("[appearance-patent] coze history response credential={} executeId={} status={} body={}",
|
||||
credential.name(), executeId,
|
||||
clientResponse.getStatusCode(),
|
||||
responseText);
|
||||
return responseText;
|
||||
});
|
||||
}
|
||||
|
||||
public CozeCredentialRef nextCredential() {
|
||||
List<CozeCredentialPoolService.CozeCredential> pooledCredentials = cozeCredentialPoolService.listEnabled(MODULE_TYPE);
|
||||
CozeCredentialPoolService.CozeCredential pooledCredential =
|
||||
cozeCredentialPoolService.chooseRoundRobin(MODULE_TYPE, pooledCredentials, properties.getCozeCredentialStripeSize());
|
||||
if (pooledCredential != null) {
|
||||
return new CozeCredentialRef(pooledCredential.name(), pooledCredential.workflowId(), pooledCredential.token(),
|
||||
pooledCredential.maxConcurrent());
|
||||
}
|
||||
List<CozeCredentialRef> credentials = configuredCredentials();
|
||||
int stripeSize = Math.max(1, properties.getCozeCredentialStripeSize());
|
||||
long cursor = Math.max(0L, credentialCursor.getAndIncrement());
|
||||
int index = (int) ((cursor / stripeSize) % credentials.size());
|
||||
return credentials.get(index);
|
||||
}
|
||||
|
||||
public CozeCredentialRef credentialByName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return nextCredential();
|
||||
}
|
||||
String normalizedName = normalize(name);
|
||||
for (CozeCredentialRef credential : configuredCredentials()) {
|
||||
if (normalize(credential.name()).equals(normalizedName)) {
|
||||
return credential;
|
||||
}
|
||||
}
|
||||
return nextCredential();
|
||||
}
|
||||
|
||||
public boolean hasConfiguredCredential() {
|
||||
return !configuredCredentials().isEmpty();
|
||||
}
|
||||
|
||||
public int configuredCredentialCount() {
|
||||
return configuredCredentials().size();
|
||||
}
|
||||
|
||||
private CozeCredentialRef resolveCredential(CozeCredentialRef credential) {
|
||||
return credential == null ? nextCredential() : credential;
|
||||
}
|
||||
|
||||
private List<CozeCredentialRef> configuredCredentials() {
|
||||
List<CozeCredentialRef> credentials = new ArrayList<>();
|
||||
for (CozeCredentialPoolService.CozeCredential credential : cozeCredentialPoolService.listEnabled(MODULE_TYPE)) {
|
||||
credentials.add(new CozeCredentialRef(credential.name(), credential.workflowId(), credential.token(),
|
||||
credential.maxConcurrent()));
|
||||
}
|
||||
if (!credentials.isEmpty()) {
|
||||
return credentials;
|
||||
}
|
||||
if (properties.getCozeCredentials() != null) {
|
||||
int index = 1;
|
||||
for (AppearancePatentProperties.CozeCredential credential : properties.getCozeCredentials()) {
|
||||
if (credential == null
|
||||
|| normalize(credential.getWorkflowId()).isBlank()
|
||||
|| normalize(credential.getToken()).isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String name = firstNonBlank(credential.getName(), "credential-" + index);
|
||||
credentials.add(new CozeCredentialRef(name, credential.getWorkflowId(), credential.getToken(), Integer.MAX_VALUE));
|
||||
index++;
|
||||
}
|
||||
}
|
||||
if (credentials.isEmpty()
|
||||
&& properties.getCozeWorkflowId() != null && !properties.getCozeWorkflowId().isBlank()
|
||||
&& properties.getCozeToken() != null && !properties.getCozeToken().isBlank()) {
|
||||
credentials.add(new CozeCredentialRef("default", properties.getCozeWorkflowId(), properties.getCozeToken(), Integer.MAX_VALUE));
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildParameters(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) {
|
||||
List<String> groupKeys = rows.stream().map(row -> nonBlank(row.getGroupKey(), rowKey(row))).toList();
|
||||
List<String> rowIds = rows.stream().map(row -> nonBlank(row.getId(), "")).toList();
|
||||
@@ -965,7 +1060,8 @@ public class AppearancePatentCozeClient {
|
||||
public record CozeSubmitResponse(
|
||||
String executeId,
|
||||
String immediateData,
|
||||
String rawResponse
|
||||
String rawResponse,
|
||||
String credentialName
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -975,7 +1071,8 @@ public class AppearancePatentCozeClient {
|
||||
String dataText,
|
||||
String outputText,
|
||||
String failureMessage,
|
||||
String rawResponse
|
||||
String rawResponse,
|
||||
String credentialName
|
||||
) {
|
||||
public boolean hasPayload() {
|
||||
return dataText != null && !dataText.isBlank() || outputText != null && !outputText.isBlank();
|
||||
@@ -1001,6 +1098,14 @@ public class AppearancePatentCozeClient {
|
||||
}
|
||||
}
|
||||
|
||||
public record CozeCredentialRef(
|
||||
String name,
|
||||
String workflowId,
|
||||
String token,
|
||||
int maxConcurrent
|
||||
) {
|
||||
}
|
||||
|
||||
private static final class PartialCozeResultException extends RuntimeException {
|
||||
|
||||
private final int resolvedCount;
|
||||
|
||||
+562
-74
@@ -28,6 +28,7 @@ import com.nanri.aiimage.modules.appearancepatent.model.vo.AppearancePatentParse
|
||||
import com.nanri.aiimage.modules.appearancepatent.model.vo.AppearancePatentTaskBatchVo;
|
||||
import com.nanri.aiimage.modules.appearancepatent.model.vo.AppearancePatentTaskDetailVo;
|
||||
import com.nanri.aiimage.modules.appearancepatent.model.vo.AppearancePatentTaskItemVo;
|
||||
import com.nanri.aiimage.modules.coze.service.CozeCredentialPoolService;
|
||||
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
|
||||
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
||||
@@ -69,6 +70,7 @@ import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -84,6 +86,8 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -100,15 +104,17 @@ public class AppearancePatentTaskService {
|
||||
private static final String COZE_STATUS_DONE = "DONE";
|
||||
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 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);
|
||||
private static final long TASK_LOCK_WAIT_MILLIS = 10000L;
|
||||
private static final long TASK_LOCK_RETRY_DELAY_MILLIS = 200L;
|
||||
private static final Duration COZE_SUBMIT_LOCK_TTL = Duration.ofMinutes(2);
|
||||
private static final long COZE_SUBMIT_LOCK_WAIT_MILLIS = 180000L;
|
||||
private static final long COZE_SUBMIT_LOCK_WAIT_MILLIS = 1000L;
|
||||
private static final long COZE_SUBMIT_LOCK_RETRY_DELAY_MILLIS = 500L;
|
||||
private static final long COZE_SUBMIT_MIN_INTERVAL_MILLIS = 30000L;
|
||||
private static final long PENDING_COZE_RETRY_INTERVAL_MILLIS = 30000L;
|
||||
private static final int MAX_COZE_SUBMIT_RETRY_COUNT = 5;
|
||||
private static final List<String> RESULT_HEADERS = List.of(
|
||||
"id",
|
||||
@@ -119,7 +125,7 @@ public class AppearancePatentTaskService {
|
||||
"外观维度(外观设计专利)",
|
||||
"专利维度(发明/实用新型专利)",
|
||||
"结论",
|
||||
"status"
|
||||
"状态"
|
||||
);
|
||||
|
||||
private final LocalFileStorageService localFileStorageService;
|
||||
@@ -140,6 +146,7 @@ public class AppearancePatentTaskService {
|
||||
private final DistributedJobLockService distributedJobLockService;
|
||||
private final TaskDistributedLockService taskDistributedLockService;
|
||||
private final InstanceMetadata instanceMetadata;
|
||||
private final CozeCredentialPoolService cozeCredentialPoolService;
|
||||
@Autowired
|
||||
@Qualifier("cozeTaskExecutor")
|
||||
private TaskExecutor cozeTaskExecutor;
|
||||
@@ -165,12 +172,12 @@ public class AppearancePatentTaskService {
|
||||
long parseStartedAt = System.nanoTime();
|
||||
for (AppearancePatentSourceFileDto source : sourceFiles) {
|
||||
if (source.getFileKey() == null || source.getFileKey().isBlank()) {
|
||||
throw new BusinessException("fileKey 不能为空");
|
||||
}
|
||||
File input = localFileStorageService.findLocalSourceFile(source.getFileKey());
|
||||
if (input == null || !input.exists()) {
|
||||
throw new BusinessException("源文件不存在");
|
||||
}
|
||||
throw new BusinessException("fileKey 不能为空");
|
||||
}
|
||||
File input = localFileStorageService.findLocalSourceFile(source.getFileKey());
|
||||
if (input == null || !input.exists()) {
|
||||
throw new BusinessException("源文件不存在");
|
||||
}
|
||||
|
||||
ParsedWorkbook parsed = parseWorkbook(input, source);
|
||||
totalRows += parsed.totalRows();
|
||||
@@ -748,7 +755,7 @@ public class AppearancePatentTaskService {
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
} else if (isResultSubmissionComplete(task.getId())) {
|
||||
maybeFinalizeCozeJobLocked(task.getId(), new CozeBatchContext(
|
||||
job.getId(), result.getId(), null, null, 1, 1, currentInstanceId(), 0));
|
||||
job.getId(), result.getId(), null, null, 1, 1, currentInstanceId(), 0, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1429,18 +1436,7 @@ public class AppearancePatentTaskService {
|
||||
|
||||
@Scheduled(fixedDelayString = "${aiimage.appearance-patent.coze-poll-delay-ms:30000}")
|
||||
public void pollPendingCozeJobs() {
|
||||
List<TaskScopeStateEntity> states = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||
.in(TaskScopeStateEntity::getCozeStatus, List.of(COZE_STATUS_SUBMITTED, COZE_STATUS_RUNNING))
|
||||
.isNotNull(TaskScopeStateEntity::getCozeExecuteId)
|
||||
.and(wrapper -> wrapper
|
||||
.apply("JSON_UNQUOTE(JSON_EXTRACT(state_json, '$.ownerInstanceId')) IS NULL")
|
||||
.or()
|
||||
.apply("JSON_UNQUOTE(JSON_EXTRACT(state_json, '$.ownerInstanceId')) = ''")
|
||||
.or()
|
||||
.apply("JSON_UNQUOTE(JSON_EXTRACT(state_json, '$.ownerInstanceId')) = {0}", currentInstanceId()))
|
||||
.orderByDesc(TaskScopeStateEntity::getUpdatedAt)
|
||||
.last("limit 50"));
|
||||
List<TaskScopeStateEntity> states = listOwnedPendingCozeStates();
|
||||
if (states == null || states.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -1464,6 +1460,41 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private List<TaskScopeStateEntity> listOwnedPendingCozeStates() {
|
||||
Map<Long, TaskScopeStateEntity> merged = new LinkedHashMap<>();
|
||||
for (TaskScopeStateEntity state : queryOwnedPendingCozeStates(false, 50)) {
|
||||
if (state != null && state.getId() != null) {
|
||||
merged.put(state.getId(), state);
|
||||
}
|
||||
}
|
||||
for (TaskScopeStateEntity state : queryOwnedPendingCozeStates(true, 50)) {
|
||||
if (state != null && state.getId() != null) {
|
||||
merged.putIfAbsent(state.getId(), state);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(merged.values());
|
||||
}
|
||||
|
||||
private List<TaskScopeStateEntity> queryOwnedPendingCozeStates(boolean oldestFirst, int limit) {
|
||||
LambdaQueryWrapper<TaskScopeStateEntity> wrapper = new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||
.in(TaskScopeStateEntity::getCozeStatus, List.of(COZE_STATUS_SUBMITTED, COZE_STATUS_RUNNING))
|
||||
.and(ownerWrapper -> ownerWrapper
|
||||
.apply("JSON_UNQUOTE(JSON_EXTRACT(state_json, '$.ownerInstanceId')) IS NULL")
|
||||
.or()
|
||||
.apply("JSON_UNQUOTE(JSON_EXTRACT(state_json, '$.ownerInstanceId')) = ''")
|
||||
.or()
|
||||
.apply("JSON_UNQUOTE(JSON_EXTRACT(state_json, '$.ownerInstanceId')) = {0}", currentInstanceId()))
|
||||
.last("limit " + Math.max(1, Math.min(limit, 100)));
|
||||
if (oldestFirst) {
|
||||
wrapper.orderByAsc(TaskScopeStateEntity::getUpdatedAt);
|
||||
} else {
|
||||
wrapper.orderByDesc(TaskScopeStateEntity::getUpdatedAt);
|
||||
}
|
||||
List<TaskScopeStateEntity> states = taskScopeStateMapper.selectList(wrapper);
|
||||
return states == null ? List.of() : states;
|
||||
}
|
||||
|
||||
private List<TaskChunkEntity> loadSubmittedChunks(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return List.of();
|
||||
@@ -1492,6 +1523,10 @@ public class AppearancePatentTaskService {
|
||||
if (key.isBlank() || !queuedRowKeys.add(key)) {
|
||||
continue;
|
||||
}
|
||||
AppearancePatentResultRowDto persistedRow = persistedRows.get(key);
|
||||
if (hasResolvedCozeFields(persistedRow)) {
|
||||
continue;
|
||||
}
|
||||
candidates.add(new CozeCandidate(chunk.getScopeHash(), chunk.getChunkIndex(), row));
|
||||
}
|
||||
}
|
||||
@@ -1530,7 +1565,7 @@ public class AppearancePatentTaskService {
|
||||
if (chunks == null || chunks.isEmpty()) {
|
||||
return countPendingCozeStates(task.getId()) > 0;
|
||||
}
|
||||
if (properties.getCozeToken() == null || properties.getCozeToken().isBlank()) {
|
||||
if (!cozeClient.hasConfiguredCredential()) {
|
||||
log.warn("[appearance-patent] coze token not configured, skip async coze taskId={} jobId={}",
|
||||
task.getId(), job.getId());
|
||||
return false;
|
||||
@@ -1586,8 +1621,10 @@ public class AppearancePatentTaskService {
|
||||
return COZE_STATUS_SUBMITTED.equals(existing.getCozeStatus())
|
||||
|| COZE_STATUS_RUNNING.equals(existing.getCozeStatus());
|
||||
}
|
||||
AppearancePatentCozeClient.CozeCredentialRef credential = cozeClient.nextCredential();
|
||||
try {
|
||||
AppearancePatentCozeClient.CozeSubmitResponse submit = submitCozeWorkflowThrottled(batchRows, prompt, apiKey);
|
||||
AppearancePatentCozeClient.CozeSubmitResponse submit = submitCozeWorkflowThrottled(
|
||||
batchRows, prompt, apiKey, credential, true);
|
||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||
List<AppearancePatentResultRowDto> cozeRows = cozeClient.mergeRowsFromDataText(batchRows, submit.immediateData());
|
||||
mergeCozeRowsIntoSubmittedChunks(task, cozeRows, allRowsByBaseId);
|
||||
@@ -1600,19 +1637,84 @@ public class AppearancePatentTaskService {
|
||||
return false;
|
||||
}
|
||||
saveCozeBatchState(task, result, job, batchRows, batchScopeKey, batchScopeHash,
|
||||
batchIndex, batchTotal, submit.executeId());
|
||||
log.info("[appearance-patent] coze async submitted taskId={} jobId={} rows={} batch={}/{} executeId={}",
|
||||
task.getId(), job.getId(), batchRows.size(), batchIndex, batchTotal, submit.executeId());
|
||||
batchIndex, batchTotal, submit.executeId(), submit.credentialName());
|
||||
log.info("[appearance-patent] coze async submitted taskId={} jobId={} rows={} batch={}/{} credential={} executeId={}",
|
||||
task.getId(), job.getId(), batchRows.size(), batchIndex, batchTotal,
|
||||
submit.credentialName(), submit.executeId());
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
String message = firstNonBlank(ex.getMessage(), "Coze submit failed");
|
||||
log.warn("[appearance-patent] coze async submit failed taskId={} jobId={} rows={} batch={}/{} err={}",
|
||||
task.getId(), job.getId(), batchRows.size(), batchIndex, batchTotal, message);
|
||||
if (isCozeThrottleLockTimeout(message)) {
|
||||
savePendingCozeBatchState(task, result, job, batchRows, batchScopeKey, batchScopeHash,
|
||||
batchIndex, batchTotal, message, credential.name());
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
return true;
|
||||
}
|
||||
mergeCozeRowsIntoSubmittedChunks(task, cozeClient.markRowsFailed(batchRows, message), allRowsByBaseId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCozeThrottleLockTimeout(String message) {
|
||||
return normalize(message).toLowerCase(Locale.ROOT).contains("coze submit throttle lock timeout");
|
||||
}
|
||||
|
||||
private void savePendingCozeBatchState(FileTaskEntity task,
|
||||
FileResultEntity result,
|
||||
TaskFileJobEntity job,
|
||||
List<AppearancePatentResultRowDto> batchRows,
|
||||
String batchScopeKey,
|
||||
String batchScopeHash,
|
||||
int batchIndex,
|
||||
int batchTotal,
|
||||
String pendingReason,
|
||||
String credentialName) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
CozeBatchContext context = new CozeBatchContext(
|
||||
job.getId(),
|
||||
result.getId(),
|
||||
null,
|
||||
null,
|
||||
batchIndex,
|
||||
batchTotal,
|
||||
currentInstanceId(),
|
||||
0,
|
||||
credentialName
|
||||
);
|
||||
String batchPayload = writeJson(batchRows, "serialize pending coze batch payload failed");
|
||||
String storedBatchPayload = storeSharedCozeBatchPayload(task.getId(), batchScopeHash, batchPayload);
|
||||
TaskScopeStateEntity state = new TaskScopeStateEntity();
|
||||
state.setTaskId(task.getId());
|
||||
state.setModuleType(MODULE_TYPE);
|
||||
state.setScopeKey(batchScopeKey);
|
||||
state.setScopeHash(batchScopeHash);
|
||||
state.setParsedPayloadJson(storedBatchPayload);
|
||||
state.setStateJson(writeJson(context, "serialize pending coze batch context failed"));
|
||||
state.setCozeStatus(COZE_STATUS_RUNNING);
|
||||
state.setCozeSubmittedAt(now);
|
||||
state.setCozeLastPolledAt(null);
|
||||
state.setCozeAttemptCount(0);
|
||||
state.setCozeError(firstNonBlank(pendingReason, "Coze submit queued by throttle"));
|
||||
state.setChunkTotal(batchTotal);
|
||||
state.setReceivedChunkCount(batchIndex);
|
||||
state.setCompleted(0);
|
||||
state.setCreatedAt(now);
|
||||
state.setUpdatedAt(now);
|
||||
try {
|
||||
taskScopeStateMapper.insert(state);
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
log.info("[appearance-patent] coze async submit queued by throttle taskId={} jobId={} rows={} batch={}/{}",
|
||||
task.getId(), job.getId(), batchRows.size(), batchIndex, batchTotal);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
transientPayloadStorageService.deletePayloadIfPresent(storedBatchPayload);
|
||||
log.info("[appearance-patent] duplicate pending coze batch state ignored taskId={} scope={}",
|
||||
task.getId(), batchScopeKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCozeBatchState(FileTaskEntity task,
|
||||
FileResultEntity result,
|
||||
TaskFileJobEntity job,
|
||||
@@ -1621,7 +1723,8 @@ public class AppearancePatentTaskService {
|
||||
String batchScopeHash,
|
||||
int batchIndex,
|
||||
int batchTotal,
|
||||
String executeId) {
|
||||
String executeId,
|
||||
String credentialName) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
CozeBatchContext context = new CozeBatchContext(
|
||||
job.getId(),
|
||||
@@ -1631,7 +1734,8 @@ public class AppearancePatentTaskService {
|
||||
batchIndex,
|
||||
batchTotal,
|
||||
currentInstanceId(),
|
||||
0
|
||||
0,
|
||||
credentialName
|
||||
);
|
||||
String batchPayload = writeJson(batchRows, "serialize coze batch payload failed");
|
||||
String storedBatchPayload = storeSharedCozeBatchPayload(task.getId(), batchScopeHash, batchPayload);
|
||||
@@ -1689,6 +1793,10 @@ public class AppearancePatentTaskService {
|
||||
try {
|
||||
TaskScopeStateEntity state = taskScopeStateMapper.selectById(stateId);
|
||||
if (state == null || state.getCozeExecuteId() == null || state.getCozeExecuteId().isBlank()) {
|
||||
if (state != null && state.getCozeExecuteId() == null
|
||||
&& (COZE_STATUS_SUBMITTED.equals(state.getCozeStatus()) || COZE_STATUS_RUNNING.equals(state.getCozeStatus()))) {
|
||||
retryPendingCozeSubmitState(state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!(COZE_STATUS_SUBMITTED.equals(state.getCozeStatus()) || COZE_STATUS_RUNNING.equals(state.getCozeStatus()))) {
|
||||
@@ -1715,7 +1823,9 @@ public class AppearancePatentTaskService {
|
||||
log.info("[appearance-patent] coze poll start taskId={} stateId={} executeId={} jobId={} chunk={} batch={}/{}",
|
||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(),
|
||||
context.jobId(), context.chunkIndex(), context.batchIndex(), context.batchTotal());
|
||||
AppearancePatentCozeClient.CozePollResponse poll = cozeClient.pollWorkflow(state.getCozeExecuteId());
|
||||
AppearancePatentCozeClient.CozePollResponse poll = cozeClient.pollWorkflow(
|
||||
state.getCozeExecuteId(),
|
||||
cozeClient.credentialByName(context.credentialName()));
|
||||
if (!poll.hasPayload() && !poll.isFinished() && !isCozeStateTimedOut(state)) {
|
||||
log.info("[appearance-patent] coze poll pending taskId={} stateId={} executeId={} status={}",
|
||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(), poll.status());
|
||||
@@ -1804,7 +1914,8 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
try {
|
||||
AppearancePatentCozeClient.CozeSubmitResponse submit =
|
||||
submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task));
|
||||
submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task),
|
||||
cozeClient.credentialByName(context.credentialName()), false);
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||
List<AppearancePatentResultRowDto> cozeRows =
|
||||
@@ -1853,23 +1964,54 @@ public class AppearancePatentTaskService {
|
||||
private AppearancePatentCozeClient.CozeSubmitResponse submitCozeWorkflowThrottled(
|
||||
List<AppearancePatentResultRowDto> rows,
|
||||
String prompt,
|
||||
String apiKey) throws Exception {
|
||||
DistributedJobLockService.LockHandle lockHandle = acquireCozeSubmitLock();
|
||||
if (lockHandle == null) {
|
||||
throw new IllegalStateException("Coze submit throttle lock timeout");
|
||||
}
|
||||
try (lockHandle) {
|
||||
AppearancePatentCozeClient.CozeSubmitResponse response = cozeClient.submitWorkflow(rows, prompt, apiKey);
|
||||
sleepQuietly(COZE_SUBMIT_MIN_INTERVAL_MILLIS);
|
||||
return response;
|
||||
String apiKey,
|
||||
AppearancePatentCozeClient.CozeCredentialRef credential,
|
||||
boolean allowCredentialFallback) throws Exception {
|
||||
int attempts = allowCredentialFallback ? Math.max(1, cozeClient.configuredCredentialCount()) : 1;
|
||||
AppearancePatentCozeClient.CozeCredentialRef currentCredential =
|
||||
credential == null ? cozeClient.nextCredential() : credential;
|
||||
Exception lastFailure = null;
|
||||
for (int i = 0; i < attempts; i++) {
|
||||
DistributedJobLockService.LockHandle lockHandle = acquireCozeSubmitLock(currentCredential);
|
||||
if (lockHandle == null) {
|
||||
lastFailure = new IllegalStateException("Coze submit throttle lock timeout");
|
||||
currentCredential = cozeClient.nextCredential();
|
||||
continue;
|
||||
}
|
||||
CozeCredentialPoolService.BorrowedCredential borrowedCredential =
|
||||
cozeCredentialPoolService.borrow(MODULE_TYPE, toPoolCredential(currentCredential));
|
||||
if (borrowedCredential == null) {
|
||||
lockHandle.close();
|
||||
lastFailure = new IllegalStateException("Coze credential concurrency limit reached");
|
||||
currentCredential = cozeClient.nextCredential();
|
||||
continue;
|
||||
}
|
||||
try (lockHandle; borrowedCredential) {
|
||||
return cozeClient.submitWorkflow(rows, prompt, apiKey, currentCredential);
|
||||
} finally {
|
||||
sleepQuietly(COZE_SUBMIT_MIN_INTERVAL_MILLIS);
|
||||
}
|
||||
}
|
||||
throw lastFailure == null ? new IllegalStateException("Coze submit failed") : lastFailure;
|
||||
}
|
||||
|
||||
private DistributedJobLockService.LockHandle acquireCozeSubmitLock() {
|
||||
private CozeCredentialPoolService.CozeCredential toPoolCredential(AppearancePatentCozeClient.CozeCredentialRef credential) {
|
||||
if (credential == null) {
|
||||
return null;
|
||||
}
|
||||
return new CozeCredentialPoolService.CozeCredential(
|
||||
credential.name(),
|
||||
credential.workflowId(),
|
||||
credential.token(),
|
||||
credential.maxConcurrent());
|
||||
}
|
||||
|
||||
private DistributedJobLockService.LockHandle acquireCozeSubmitLock(AppearancePatentCozeClient.CozeCredentialRef credential) {
|
||||
long deadline = System.currentTimeMillis() + COZE_SUBMIT_LOCK_WAIT_MILLIS;
|
||||
String credentialName = credential == null ? "default" : firstNonBlank(credential.name(), "default");
|
||||
while (System.currentTimeMillis() <= deadline) {
|
||||
DistributedJobLockService.LockHandle lockHandle =
|
||||
distributedJobLockService.tryLock("appearance-patent:coze-submit", COZE_SUBMIT_LOCK_TTL);
|
||||
distributedJobLockService.tryLock("appearance-patent:coze-submit:" + credentialName, COZE_SUBMIT_LOCK_TTL);
|
||||
if (lockHandle != null) {
|
||||
return lockHandle;
|
||||
}
|
||||
@@ -1915,14 +2057,16 @@ public class AppearancePatentTaskService {
|
||||
int partIndex = 1;
|
||||
for (List<AppearancePatentResultRowDto> partRows : partitions) {
|
||||
AppearancePatentCozeClient.CozeSubmitResponse submit =
|
||||
submitCozeWorkflowThrottled(partRows, readAiPrompt(task), readApiKey(task));
|
||||
submitCozeWorkflowThrottled(partRows, readAiPrompt(task), readApiKey(task),
|
||||
cozeClient.credentialByName(context.credentialName()), false);
|
||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||
List<AppearancePatentResultRowDto> cozeRows =
|
||||
cozeClient.mergeRowsFromDataText(partRows, submit.immediateData());
|
||||
mergeCozeRowsIntoSubmittedChunks(task, cozeRows, allRowsByBaseId);
|
||||
submittedAny = true;
|
||||
} else if (submit.executeId() != null && !submit.executeId().isBlank()) {
|
||||
saveSplitRetryCozeBatchState(state, context, partRows, partIndex, partitions.size(), retryCount, submit.executeId());
|
||||
saveSplitRetryCozeBatchState(state, context, partRows, partIndex, partitions.size(), retryCount,
|
||||
submit.executeId(), submit.credentialName());
|
||||
submittedAny = true;
|
||||
}
|
||||
partIndex++;
|
||||
@@ -1950,7 +2094,8 @@ public class AppearancePatentTaskService {
|
||||
int partIndex,
|
||||
int partTotal,
|
||||
int retryCount,
|
||||
String executeId) {
|
||||
String executeId,
|
||||
String credentialName) {
|
||||
String scopeKey = parent.getScopeKey() + ":split:" + retryCount + ":" + partIndex;
|
||||
String scopeHash = DigestUtil.sha256Hex(scopeKey);
|
||||
CozeBatchContext context = new CozeBatchContext(
|
||||
@@ -1961,7 +2106,8 @@ public class AppearancePatentTaskService {
|
||||
partIndex,
|
||||
partTotal,
|
||||
parentContext.ownerInstanceId(),
|
||||
retryCount
|
||||
retryCount,
|
||||
firstNonBlank(credentialName, parentContext.credentialName())
|
||||
);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String batchPayload = writeJson(batchRows, "serialize split coze batch payload failed");
|
||||
@@ -2058,6 +2204,24 @@ public class AppearancePatentTaskService {
|
||||
.set(TaskScopeStateEntity::getUpdatedAt, now)) > 0;
|
||||
}
|
||||
|
||||
private boolean tryClaimPendingCozeSubmitState(TaskScopeStateEntity state) {
|
||||
if (state == null || state.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long intervalMillis = Math.max(1000L, PENDING_COZE_RETRY_INTERVAL_MILLIS);
|
||||
return taskScopeStateMapper.update(null, new LambdaUpdateWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getId, state.getId())
|
||||
.in(TaskScopeStateEntity::getCozeStatus, List.of(COZE_STATUS_SUBMITTED, COZE_STATUS_RUNNING))
|
||||
.and(wrapper -> wrapper
|
||||
.isNull(TaskScopeStateEntity::getCozeLastPolledAt)
|
||||
.or()
|
||||
.le(TaskScopeStateEntity::getCozeLastPolledAt, now.minus(Duration.ofMillis(intervalMillis))))
|
||||
.set(TaskScopeStateEntity::getCozeStatus, COZE_STATUS_RUNNING)
|
||||
.set(TaskScopeStateEntity::getCozeLastPolledAt, now)
|
||||
.set(TaskScopeStateEntity::getUpdatedAt, now)) > 0;
|
||||
}
|
||||
|
||||
private void markCozeStateTerminal(TaskScopeStateEntity state, String status, String error) {
|
||||
taskScopeStateMapper.update(null, new LambdaUpdateWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getId, state.getId())
|
||||
@@ -2275,7 +2439,8 @@ public class AppearancePatentTaskService {
|
||||
context.batchIndex(),
|
||||
context.batchTotal(),
|
||||
context.ownerInstanceId(),
|
||||
submitRetryCount
|
||||
submitRetryCount,
|
||||
context.credentialName()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2374,6 +2539,115 @@ public class AppearancePatentTaskService {
|
||||
return "task:" + taskId + ":owner:" + firstNonBlank(ownerFromTask(task), currentInstanceId());
|
||||
}
|
||||
|
||||
private void retryPendingCozeSubmitState(TaskScopeStateEntity state) {
|
||||
if (state == null || state.getId() == null) {
|
||||
return;
|
||||
}
|
||||
if (!tryClaimPendingCozeSubmitState(state)) {
|
||||
log.info("[appearance-patent] coze submit retry skipped by claim guard taskId={} stateId={} lastPolledAt={}",
|
||||
state.getTaskId(), state.getId(), state.getCozeLastPolledAt());
|
||||
return;
|
||||
}
|
||||
CozeBatchContext context = readCozeBatchContext(state);
|
||||
if (context == null || context.jobId() == null || context.resultId() == null) {
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze pending submit context missing");
|
||||
return;
|
||||
}
|
||||
List<AppearancePatentResultRowDto> batchRows = readCozeBatchRows(state);
|
||||
if (batchRows.isEmpty()) {
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze pending submit payload missing");
|
||||
maybeFinalizeCozeJobLocked(state.getTaskId(), context);
|
||||
return;
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(state.getTaskId());
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze pending submit task missing");
|
||||
return;
|
||||
}
|
||||
Map<String, AppearancePatentResultRowDto> currentRows = loadPersistedResultRows(task.getId());
|
||||
List<AppearancePatentResultRowDto> currentBatchRows = batchRows.stream()
|
||||
.map(row -> currentRows.get(rowKey(row)))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
if (currentBatchRows.size() == batchRows.size()
|
||||
&& currentBatchRows.stream().allMatch(this::hasResolvedCozeFields)) {
|
||||
markCozeStateTerminal(state, COZE_STATUS_DONE, "Coze rows already resolved by another batch");
|
||||
maybeFinalizeCozeJobLocked(state.getTaskId(), context);
|
||||
log.info("[appearance-patent] coze pending submit skipped because rows already resolved taskId={} stateId={} jobId={} rows={}",
|
||||
state.getTaskId(), state.getId(), context.jobId(), batchRows.size());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
taskFileJobService.touchRunning(context.jobId());
|
||||
AppearancePatentCozeClient.CozeSubmitResponse submit =
|
||||
submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task),
|
||||
cozeClient.credentialByName(context.credentialName()), false);
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||
List<AppearancePatentResultRowDto> cozeRows =
|
||||
cozeClient.mergeRowsFromDataText(batchRows, submit.immediateData());
|
||||
mergeCozeRowsIntoSubmittedChunks(task, cozeRows, allRowsByBaseId);
|
||||
markCozeStateTerminal(state, COZE_STATUS_DONE, null);
|
||||
maybeFinalizeCozeJobLocked(state.getTaskId(), context);
|
||||
return;
|
||||
}
|
||||
if (submit.executeId() == null || submit.executeId().isBlank()) {
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze async execute_id missing");
|
||||
mergeCozeRowsIntoSubmittedChunks(task,
|
||||
cozeClient.markRowsFailed(batchRows, "Coze async execute_id missing"),
|
||||
allRowsByBaseId);
|
||||
maybeFinalizeCozeJobLocked(state.getTaskId(), context);
|
||||
return;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
taskScopeStateMapper.update(null, new LambdaUpdateWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getId, state.getId())
|
||||
.in(TaskScopeStateEntity::getCozeStatus, List.of(COZE_STATUS_SUBMITTED, COZE_STATUS_RUNNING))
|
||||
.set(TaskScopeStateEntity::getCozeExecuteId, submit.executeId())
|
||||
.set(TaskScopeStateEntity::getCozeStatus, COZE_STATUS_SUBMITTED)
|
||||
.set(TaskScopeStateEntity::getCozeSubmittedAt, now)
|
||||
.set(TaskScopeStateEntity::getCozeLastPolledAt, null)
|
||||
.set(TaskScopeStateEntity::getCozeError, null)
|
||||
.set(TaskScopeStateEntity::getUpdatedAt, now));
|
||||
touchJavaSideTaskActivity(state.getTaskId());
|
||||
log.info("[appearance-patent] coze pending submit retried taskId={} stateId={} jobId={} rows={} executeId={}",
|
||||
state.getTaskId(), state.getId(), context.jobId(), batchRows.size(), submit.executeId());
|
||||
} catch (Exception ex) {
|
||||
String message = firstNonBlank(ex.getMessage(), "Coze pending submit retry failed");
|
||||
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)) {
|
||||
keepPendingCozeSubmitState(state, message);
|
||||
return;
|
||||
}
|
||||
int nextAttemptCount = cozeAttemptCount(state) + 1;
|
||||
if (nextAttemptCount >= MAX_COZE_SUBMIT_RETRY_COUNT) {
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
String finalMessage = message + " after " + nextAttemptCount + " submit attempts";
|
||||
mergeCozeRowsIntoSubmittedChunks(task,
|
||||
cozeClient.markRowsFailed(batchRows, finalMessage),
|
||||
allRowsByBaseId);
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, finalMessage);
|
||||
maybeFinalizeCozeJobLocked(state.getTaskId(), context);
|
||||
return;
|
||||
}
|
||||
updateCozeStateRunning(state, message);
|
||||
}
|
||||
}
|
||||
|
||||
private void keepPendingCozeSubmitState(TaskScopeStateEntity state, String error) {
|
||||
int updated = taskScopeStateMapper.update(null, new LambdaUpdateWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getId, state.getId())
|
||||
.in(TaskScopeStateEntity::getCozeStatus, List.of(COZE_STATUS_SUBMITTED, COZE_STATUS_RUNNING))
|
||||
.set(TaskScopeStateEntity::getCozeStatus, COZE_STATUS_RUNNING)
|
||||
.set(TaskScopeStateEntity::getCozeExecuteId, null)
|
||||
.set(TaskScopeStateEntity::getCozeError, error)
|
||||
.set(TaskScopeStateEntity::getUpdatedAt, LocalDateTime.now()));
|
||||
if (updated > 0) {
|
||||
touchJavaSideTaskActivity(state.getTaskId());
|
||||
}
|
||||
}
|
||||
|
||||
private String currentInstanceId() {
|
||||
String instanceId = instanceMetadata == null ? null : instanceMetadata.getInstanceId();
|
||||
return firstNonBlank(instanceId, "unknown-instance");
|
||||
@@ -2475,6 +2749,12 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
int safeTotal = Math.max(1, total);
|
||||
int safeCompleted = Math.max(0, Math.min(completed, safeTotal));
|
||||
TaskProgressSnapshotEntity existing = taskProgressSnapshotService.find(task.getId(), MODULE_TYPE);
|
||||
int displayPercent = calculateDisplayProgressPercent(safeCompleted, safeTotal, job, LocalDateTime.now());
|
||||
int previousDisplayPercent = calculateSnapshotDisplayPercent(existing, job);
|
||||
if (previousDisplayPercent > displayPercent && safeCompleted < safeTotal) {
|
||||
displayPercent = previousDisplayPercent;
|
||||
}
|
||||
taskProgressSnapshotService.save(
|
||||
task.getId(),
|
||||
MODULE_TYPE,
|
||||
@@ -2484,10 +2764,57 @@ public class AppearancePatentTaskService {
|
||||
0,
|
||||
job.getScopeKey(),
|
||||
message,
|
||||
Map.of("phase", "RESULT_FILE", "jobId", job.getId())
|
||||
Map.of("phase", "RESULT_FILE", "jobId", job.getId(), "displayPercent", displayPercent)
|
||||
);
|
||||
}
|
||||
|
||||
private int calculateDisplayProgressPercent(int current,
|
||||
int total,
|
||||
TaskFileJobEntity job,
|
||||
LocalDateTime baseTime) {
|
||||
if (total <= 0) {
|
||||
return 0;
|
||||
}
|
||||
current = Math.max(0, Math.min(current, total));
|
||||
int percent = Math.max(1, Math.min(99, (int) Math.floor(current * 100.0 / total)));
|
||||
if (job != null && STATUS_RUNNING.equals(job.getStatus())) {
|
||||
long elapsedSeconds = baseTime == null ? 0 : Math.max(0, Duration.between(baseTime, LocalDateTime.now()).getSeconds());
|
||||
if (current <= 0) {
|
||||
int firstRealProgressPercent = Math.max(1, Math.min(99, (int) Math.floor(100.0 / total)));
|
||||
int waitingCap = Math.max(8, Math.min(35, firstRealProgressPercent - 1));
|
||||
percent = Math.max(percent, Math.min(waitingCap, 8 + (int) (elapsedSeconds / 6)));
|
||||
} else if (current < total) {
|
||||
percent = Math.max(percent, Math.min(92, percent + (int) (elapsedSeconds / 10)));
|
||||
}
|
||||
}
|
||||
return percent;
|
||||
}
|
||||
|
||||
private int calculateSnapshotDisplayPercent(TaskProgressSnapshotEntity snapshot, TaskFileJobEntity job) {
|
||||
if (snapshot == null) {
|
||||
return 0;
|
||||
}
|
||||
int total = snapshot.getTotalCount() == null ? 0 : snapshot.getTotalCount();
|
||||
int current = snapshot.getSuccessCount() == null ? 0 : snapshot.getSuccessCount();
|
||||
int percent = total <= 0 ? 0 : calculateDisplayProgressPercent(current, total, job, snapshot.getUpdatedAt());
|
||||
return Math.max(percent, extractSnapshotDisplayPercent(snapshot));
|
||||
}
|
||||
|
||||
private int extractSnapshotDisplayPercent(TaskProgressSnapshotEntity snapshot) {
|
||||
if (snapshot == null || snapshot.getSnapshotJson() == null || snapshot.getSnapshotJson().isBlank()) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
JsonNode displayPercent = objectMapper.readTree(snapshot.getSnapshotJson()).path("displayPercent");
|
||||
if (!displayPercent.isNumber()) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Math.min(100, displayPercent.asInt()));
|
||||
} catch (Exception ignored) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCozePipelineProgress(FileTaskEntity task, TaskFileJobEntity job) {
|
||||
if (task == null || task.getId() == null || job == null || job.getId() == null) {
|
||||
return;
|
||||
@@ -2565,28 +2892,160 @@ public class AppearancePatentTaskService {
|
||||
if (!outputDir.exists() && !outputDir.mkdirs()) {
|
||||
throw new BusinessException("创建结果目录失败");
|
||||
}
|
||||
String filename = safeFileStem(result.getSourceFilename()) + "-result.xlsx";
|
||||
String tempFilename = safeFileStem(result.getSourceFilename())
|
||||
+ "-" + task.getId()
|
||||
+ "-" + result.getId()
|
||||
+ "-" + UUID.randomUUID()
|
||||
+ "-result.xlsx";
|
||||
File xlsx = new File(outputDir, tempFilename);
|
||||
List<SourceRows> sourceRows = splitRowsBySourceFile(parsed, receivedRows, result.getSourceFilename());
|
||||
List<SourceResultWorkbook> workbooks = new ArrayList<>();
|
||||
File zip = null;
|
||||
try {
|
||||
writeResultWorkbook(xlsx, parsed, receivedRows, resultMap);
|
||||
String objectKey = ossStorageService.uploadResultFile(xlsx, MODULE_TYPE);
|
||||
for (SourceRows item : sourceRows) {
|
||||
String filename = safeFileStem(item.sourceFilename()) + "-result.xlsx";
|
||||
String tempFilename = safeFileStem(item.sourceFilename())
|
||||
+ "-" + task.getId()
|
||||
+ "-" + result.getId()
|
||||
+ "-" + UUID.randomUUID()
|
||||
+ "-result.xlsx";
|
||||
File xlsx = new File(outputDir, tempFilename);
|
||||
writeResultWorkbook(xlsx, parsed, item.rows(), resultMap);
|
||||
workbooks.add(new SourceResultWorkbook(xlsx, filename, item.rows().size()));
|
||||
}
|
||||
if (workbooks.isEmpty()) {
|
||||
throw new BusinessException("外观专利检测结果为空,请稍后重试生成结果文件");
|
||||
}
|
||||
|
||||
File uploadFile;
|
||||
String filename;
|
||||
String contentType;
|
||||
if (workbooks.size() == 1) {
|
||||
SourceResultWorkbook workbook = workbooks.get(0);
|
||||
uploadFile = workbook.file();
|
||||
filename = workbook.filename();
|
||||
contentType = CONTENT_TYPE_XLSX;
|
||||
} else {
|
||||
filename = safeFileStem(result.getSourceFilename()) + "-result.zip";
|
||||
String tempFilename = safeFileStem(result.getSourceFilename())
|
||||
+ "-" + task.getId()
|
||||
+ "-" + result.getId()
|
||||
+ "-" + UUID.randomUUID()
|
||||
+ "-result.zip";
|
||||
zip = new File(outputDir, tempFilename);
|
||||
packageResultWorkbooksAsZip(zip, workbooks);
|
||||
uploadFile = zip;
|
||||
contentType = CONTENT_TYPE_ZIP;
|
||||
}
|
||||
|
||||
String objectKey = ossStorageService.uploadResultFile(uploadFile, MODULE_TYPE);
|
||||
result.setResultFilename(filename);
|
||||
result.setResultFileUrl(objectKey);
|
||||
result.setResultFileSize(xlsx.length());
|
||||
result.setResultContentType(CONTENT_TYPE_XLSX);
|
||||
result.setResultFileSize(uploadFile.length());
|
||||
result.setResultContentType(contentType);
|
||||
result.setRowCount(receivedRows.size());
|
||||
} finally {
|
||||
if (xlsx.exists() && !xlsx.delete()) {
|
||||
log.warn("[appearance-patent] delete temp xlsx failed file={}", xlsx);
|
||||
for (SourceResultWorkbook workbook : workbooks) {
|
||||
if (workbook.file().exists() && !workbook.file().delete()) {
|
||||
log.warn("[appearance-patent] delete temp xlsx failed file={}", workbook.file());
|
||||
}
|
||||
}
|
||||
if (zip != null && zip.exists() && !zip.delete()) {
|
||||
log.warn("[appearance-patent] delete temp zip failed file={}", zip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<SourceRows> splitRowsBySourceFile(AppearancePatentParsedPayloadDto parsed,
|
||||
List<AppearancePatentParsedRowVo> receivedRows,
|
||||
String fallbackFilename) {
|
||||
String defaultFilename = firstNonBlank(fallbackFilename, "appearance-patent");
|
||||
Map<String, SourceRowsBuilder> builders = new LinkedHashMap<>();
|
||||
if (parsed != null && parsed.getSourceFiles() != null) {
|
||||
for (AppearancePatentSourceFileDto sourceFile : parsed.getSourceFiles()) {
|
||||
if (sourceFile == null) {
|
||||
continue;
|
||||
}
|
||||
String sourceFileKey = normalize(sourceFile.getFileKey());
|
||||
if (sourceFileKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
builders.putIfAbsent(sourceFileKey,
|
||||
new SourceRowsBuilder(sourceFileKey, firstNonBlank(sourceFile.getOriginalFilename(), defaultFilename)));
|
||||
}
|
||||
}
|
||||
|
||||
for (AppearancePatentParsedRowVo row : receivedRows == null ? List.<AppearancePatentParsedRowVo>of() : receivedRows) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
String sourceFileKey = normalize(row.getSourceFileKey());
|
||||
if (sourceFileKey.isBlank()) {
|
||||
sourceFileKey = normalize(row.getSourceFilename());
|
||||
}
|
||||
if (sourceFileKey.isBlank()) {
|
||||
sourceFileKey = "__default__";
|
||||
}
|
||||
SourceRowsBuilder builder = builders.computeIfAbsent(sourceFileKey,
|
||||
key -> new SourceRowsBuilder(key, firstNonBlank(row.getSourceFilename(), defaultFilename)));
|
||||
if (builder.sourceFilename().isBlank() || Objects.equals(builder.sourceFilename(), defaultFilename)) {
|
||||
builder.setSourceFilename(firstNonBlank(row.getSourceFilename(), defaultFilename));
|
||||
}
|
||||
builder.rows().add(row);
|
||||
}
|
||||
|
||||
return builders.values().stream()
|
||||
.filter(builder -> !builder.rows().isEmpty())
|
||||
.map(builder -> new SourceRows(builder.sourceFileKey(), firstNonBlank(builder.sourceFilename(), defaultFilename), builder.rows()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void packageResultWorkbooksAsZip(File zip, List<SourceResultWorkbook> workbooks) {
|
||||
Set<String> entryNames = new LinkedHashSet<>();
|
||||
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip))) {
|
||||
byte[] buffer = new byte[8192];
|
||||
for (SourceResultWorkbook workbook : workbooks) {
|
||||
writeResultZipEntry(zos, buffer, workbook.file(), uniqueZipEntryName(workbook.filename(), entryNames));
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new BusinessException("生成外观专利检测结果压缩包失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeResultZipEntry(ZipOutputStream zos,
|
||||
byte[] buffer,
|
||||
File file,
|
||||
String entryName) throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(entryName));
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
int len;
|
||||
while ((len = inputStream.read(buffer)) > 0) {
|
||||
zos.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
private String uniqueZipEntryName(String filename, Set<String> entryNames) {
|
||||
String safeFilename = normalizeZipFilename(filename);
|
||||
if (entryNames.add(safeFilename)) {
|
||||
return safeFilename;
|
||||
}
|
||||
int dotIndex = safeFilename.lastIndexOf('.');
|
||||
String stem = dotIndex > 0 ? safeFilename.substring(0, dotIndex) : safeFilename;
|
||||
String extension = dotIndex > 0 ? safeFilename.substring(dotIndex) : "";
|
||||
int index = 2;
|
||||
while (true) {
|
||||
String candidate = stem + "(" + index + ")" + extension;
|
||||
if (entryNames.add(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeZipFilename(String filename) {
|
||||
String normalized = firstNonBlank(filename, "appearance-patent-result.xlsx").replace('\\', '/');
|
||||
while (normalized.startsWith("/")) {
|
||||
normalized = normalized.substring(1);
|
||||
}
|
||||
return normalized.isBlank() ? "appearance-patent-result.xlsx" : normalized;
|
||||
}
|
||||
|
||||
private List<AppearancePatentParsedRowVo> filterReceivedParsedRows(List<AppearancePatentParsedRowVo> parsedRows,
|
||||
Map<String, AppearancePatentResultRowDto> resultMap) {
|
||||
if (parsedRows == null || parsedRows.isEmpty() || resultMap == null || resultMap.isEmpty()) {
|
||||
@@ -3141,19 +3600,10 @@ public class AppearancePatentTaskService {
|
||||
if (total <= 0) {
|
||||
return;
|
||||
}
|
||||
current = Math.max(0, Math.min(current, total));
|
||||
int percent = Math.max(1, Math.min(99, (int) Math.floor(current * 100.0 / total)));
|
||||
if (job != null && STATUS_RUNNING.equals(job.getStatus())) {
|
||||
LocalDateTime baseTime = snapshot.getUpdatedAt() != null ? snapshot.getUpdatedAt() : job.getUpdatedAt();
|
||||
long elapsedSeconds = baseTime == null ? 0 : Math.max(0, Duration.between(baseTime, LocalDateTime.now()).getSeconds());
|
||||
if (current <= 0) {
|
||||
int firstRealProgressPercent = Math.max(1, Math.min(99, (int) Math.floor(100.0 / total)));
|
||||
int waitingCap = Math.max(8, Math.min(35, firstRealProgressPercent - 1));
|
||||
percent = Math.max(percent, Math.min(waitingCap, 8 + (int) (elapsedSeconds / 6)));
|
||||
} else if (current < total) {
|
||||
percent = Math.max(percent, Math.min(92, percent + (int) (elapsedSeconds / 10)));
|
||||
}
|
||||
}
|
||||
LocalDateTime baseTime = snapshot.getUpdatedAt() != null ? snapshot.getUpdatedAt() : (job == null ? null : job.getUpdatedAt());
|
||||
int percent = calculateDisplayProgressPercent(current, total, job, baseTime);
|
||||
percent = Math.max(percent, extractSnapshotDisplayPercent(snapshot));
|
||||
percent = Boolean.TRUE.equals(vo.getFileReady()) ? 100 : Math.min(99, percent);
|
||||
vo.setFileProgressCurrent(current);
|
||||
vo.setFileProgressTotal(total);
|
||||
vo.setFileProgressPercent(percent);
|
||||
@@ -3514,7 +3964,8 @@ public class AppearancePatentTaskService {
|
||||
Integer batchIndex,
|
||||
Integer batchTotal,
|
||||
String ownerInstanceId,
|
||||
Integer submitRetryCount) {
|
||||
Integer submitRetryCount,
|
||||
String credentialName) {
|
||||
}
|
||||
|
||||
private record CozeCandidate(String chunkScopeHash,
|
||||
@@ -3522,6 +3973,43 @@ public class AppearancePatentTaskService {
|
||||
AppearancePatentResultRowDto row) {
|
||||
}
|
||||
|
||||
private static class SourceRowsBuilder {
|
||||
private final String sourceFileKey;
|
||||
private String sourceFilename;
|
||||
private final List<AppearancePatentParsedRowVo> rows = new ArrayList<>();
|
||||
|
||||
private SourceRowsBuilder(String sourceFileKey, String sourceFilename) {
|
||||
this.sourceFileKey = sourceFileKey;
|
||||
this.sourceFilename = sourceFilename;
|
||||
}
|
||||
|
||||
private String sourceFileKey() {
|
||||
return sourceFileKey;
|
||||
}
|
||||
|
||||
private String sourceFilename() {
|
||||
return sourceFilename;
|
||||
}
|
||||
|
||||
private void setSourceFilename(String sourceFilename) {
|
||||
this.sourceFilename = sourceFilename;
|
||||
}
|
||||
|
||||
private List<AppearancePatentParsedRowVo> rows() {
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
private record SourceRows(String sourceFileKey,
|
||||
String sourceFilename,
|
||||
List<AppearancePatentParsedRowVo> rows) {
|
||||
}
|
||||
|
||||
private record SourceResultWorkbook(File file,
|
||||
String filename,
|
||||
int rowCount) {
|
||||
}
|
||||
|
||||
private record ParsedWorkbook(int totalRows, int droppedRows, List<String> headers, List<AppearancePatentParsedRowVo> allRows) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user