python端登录完成
This commit is contained in:
@@ -89,7 +89,7 @@ public class SimilarAsinCozeClient {
|
||||
String dataText = extractResultDataText(pollRoot);
|
||||
String outputText = dataText.isBlank() ? extractWorkflowOutputText(pollRoot) : "";
|
||||
String failureMessage = isFailedWorkflowStatus(status)
|
||||
? firstNonBlank(resolveFailureMessage(pollRoot), "Coze async workflow failed")
|
||||
? firstNonBlank(resolveFailureMessage(pollRoot), "Coze 异步工作流失败")
|
||||
: "";
|
||||
return new CozePollResponse(executeId, status, dataText, outputText, failureMessage, writeJson(pollRoot),
|
||||
resolvedCredential.name());
|
||||
@@ -214,19 +214,19 @@ public class SimilarAsinCozeClient {
|
||||
|
||||
String status = normalize(resolveWorkflowStatus(pollRoot)).toUpperCase(Locale.ROOT);
|
||||
if (isFailedWorkflowStatus(status)) {
|
||||
throw new IllegalStateException(firstNonBlank(resolveFailureMessage(pollRoot), "Coze async workflow failed"));
|
||||
throw new IllegalStateException(firstNonBlank(resolveFailureMessage(pollRoot), "Coze 异步工作流失败"));
|
||||
}
|
||||
if (isSuccessfulWorkflowStatus(status)) {
|
||||
String outputText = extractWorkflowOutputText(pollRoot);
|
||||
if (!outputText.isBlank()) {
|
||||
return wrapDataPayload(outputText);
|
||||
}
|
||||
throw new IllegalStateException("Coze async workflow completed without output");
|
||||
throw new IllegalStateException("Coze 异步工作流已完成但没有输出结果");
|
||||
}
|
||||
sleepQuietly(Math.max(200, properties.getCozePollIntervalMillis()));
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Coze async workflow poll timeout");
|
||||
throw new IllegalStateException("Coze 异步工作流轮询超时");
|
||||
}
|
||||
|
||||
private String postWorkflow(List<SimilarAsinResultRowDto> rows,
|
||||
@@ -781,8 +781,8 @@ public class SimilarAsinCozeClient {
|
||||
|| message.contains("429")
|
||||
|| message.toLowerCase(Locale.ROOT).contains("rate limit")
|
||||
|| message.toLowerCase(Locale.ROOT).contains("retry later")
|
||||
|| message.contains("\u9650\u6d41")
|
||||
|| message.contains("\u7a0d\u540e\u91cd\u8bd5")
|
||||
|| message.contains("限流")
|
||||
|| message.contains("稍后重试")
|
||||
|| message.toLowerCase(Locale.ROOT).contains("timeout");
|
||||
}
|
||||
|
||||
@@ -1095,7 +1095,7 @@ public class SimilarAsinCozeClient {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to serialize Coze payload", ex);
|
||||
throw new IllegalStateException("Coze 请求载荷序列化失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,13 +1118,13 @@ public class SimilarAsinCozeClient {
|
||||
Thread.sleep(delayMillis);
|
||||
} catch (InterruptedException interruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Coze workflow interrupted", interruptedException);
|
||||
throw new IllegalStateException("Coze 工作流被中断", interruptedException);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNotInterrupted() {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new IllegalStateException("Coze workflow interrupted");
|
||||
throw new IllegalStateException("Coze 工作流被中断");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1132,7 +1132,7 @@ public class SimilarAsinCozeClient {
|
||||
if (ex instanceof RuntimeException runtimeException) {
|
||||
return runtimeException;
|
||||
}
|
||||
return new IllegalStateException(nonBlank(ex.getMessage(), "Coze call failed"), ex);
|
||||
return new IllegalStateException(nonBlank(ex.getMessage(), "Coze 调用失败"), ex);
|
||||
}
|
||||
|
||||
private JsonNode firstNonNull(JsonNode left, JsonNode right) {
|
||||
@@ -1227,19 +1227,19 @@ public class SimilarAsinCozeClient {
|
||||
|
||||
private String failureMessage(Exception ex) {
|
||||
if (ex instanceof PartialCozeResultException partial) {
|
||||
return "Coze result incomplete(" + partial.resolvedCount() + "/" + partial.expectedCount() + ")";
|
||||
return "Coze 结果不完整(" + partial.resolvedCount() + "/" + partial.expectedCount() + ")";
|
||||
}
|
||||
String message = ex == null ? null : ex.getMessage();
|
||||
if (message == null || message.isBlank()) {
|
||||
return "Coze call failed";
|
||||
return "Coze 调用失败";
|
||||
}
|
||||
if (message.contains("Workflow node execution limit exceeded")) {
|
||||
return "Coze workflow node execution limit exceeded";
|
||||
return "Coze 工作流节点执行超限";
|
||||
}
|
||||
if (message.contains("Read timed out")) {
|
||||
return "Coze call timed out";
|
||||
return "Coze 调用超时";
|
||||
}
|
||||
return "Coze call failed: " + message;
|
||||
return "Coze 调用失败:" + message;
|
||||
}
|
||||
|
||||
private String stripBearer(String token) {
|
||||
|
||||
@@ -13,11 +13,13 @@ import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryVo;
|
||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParseVo;
|
||||
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinTaskBatchVo;
|
||||
import com.nanri.aiimage.modules.similarasin.service.SimilarAsinTaskService;
|
||||
import com.nanri.aiimage.modules.similarasin.service.SimilarAsinTaskService.ResultDownloadInfo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -36,6 +38,7 @@ import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@RequestMapping("/api/similar-asin")
|
||||
@Tag(
|
||||
name = "相似 ASIN 检测",
|
||||
@@ -174,22 +177,30 @@ public class SimilarAsinController {
|
||||
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||
@RequestParam("user_id") Long userId,
|
||||
jakarta.servlet.http.HttpServletResponse response) {
|
||||
String url = service.resolveResultDownloadUrl(resultId, userId);
|
||||
String filename = service.resolveResultDownloadFilename(resultId, userId);
|
||||
if (url == null || url.isBlank()) {
|
||||
ResultDownloadInfo download = service.resolveResultDownloadInfo(resultId, userId);
|
||||
if (download.url() == null || download.url().isBlank()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果");
|
||||
}
|
||||
try {
|
||||
response.setContentType("application/octet-stream");
|
||||
DownloadHeaderUtil.setAttachment(response, filename);
|
||||
try (InputStream in = URI.create(url).toURL().openStream()) {
|
||||
response.setContentType(download.contentType());
|
||||
if (download.size() > 0) {
|
||||
response.setContentLengthLong(download.size());
|
||||
}
|
||||
DownloadHeaderUtil.setAttachment(response, download.filename());
|
||||
long written = 0L;
|
||||
try (InputStream in = URI.create(download.url()).toURL().openStream()) {
|
||||
byte[] buffer = new byte[65536];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
response.getOutputStream().write(buffer, 0, read);
|
||||
written += read;
|
||||
}
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
if (download.size() > 0 && written != download.size()) {
|
||||
log.warn("[相似ASIN] 下载文件大小不一致 结果ID={} 预期字节数={} 实际写出字节数={} 文件名={}",
|
||||
resultId, download.size(), written, download.filename());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "下载失败");
|
||||
}
|
||||
|
||||
@@ -600,7 +600,7 @@ public class SimilarAsinTaskService {
|
||||
public void submitResult(Long taskId, SimilarAsinSubmitResultRequest request) {
|
||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, TASK_LOCK_WAIT_MILLIS);
|
||||
if (lockHandle == null) {
|
||||
throw new BusinessException(40902, "Task is processing the previous result chunk, please retry later");
|
||||
throw new BusinessException(40902, "任务正在处理上一批结果,请稍后重试");
|
||||
}
|
||||
try (lockHandle) {
|
||||
submitResultLocked(taskId, request);
|
||||
@@ -730,7 +730,7 @@ public class SimilarAsinTaskService {
|
||||
taskScopeStateMapper.delete(new LambdaQueryWrapper<TaskScopeStateEntity>().eq(TaskScopeStateEntity::getTaskId, taskId).eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE));
|
||||
taskChunkMapper.delete(new LambdaQueryWrapper<TaskChunkEntity>().eq(TaskChunkEntity::getTaskId, taskId).eq(TaskChunkEntity::getModuleType, MODULE_TYPE));
|
||||
taskCacheService.deleteTaskCache(taskId);
|
||||
// 同步清理 task_file_job,避免被删除任务遗留的 PENDING/FAILED 行被 TaskResultFileJobWorker 反复扫描出 task not found。
|
||||
// 同步清理 task_file_job,避免被删除任务遗留的 PENDING/FAILED 行被 TaskResultFileJobWorker 反复扫描出“任务不存在”。
|
||||
taskFileJobService.deleteTaskJobs(taskId, MODULE_TYPE);
|
||||
fileTaskMapper.deleteById(taskId);
|
||||
// P1-1:任务被删除时一并清理滑窗记录,防止内存泄漏。
|
||||
@@ -743,7 +743,7 @@ public class SimilarAsinTaskService {
|
||||
if (row == null || !MODULE_TYPE.equals(row.getModuleType()) || !Objects.equals(userId, row.getUserId())) {
|
||||
throw new BusinessException("记录不存在");
|
||||
}
|
||||
// 同步清理 task_file_job,避免被删除历史记录残留的 ASSEMBLE_RESULT job 被反复扫描出 result record not found。
|
||||
// 同步清理 task_file_job,避免被删除历史记录残留的 ASSEMBLE_RESULT job 被反复扫描出“结果记录不存在”。
|
||||
taskFileJobService.deleteResultJobs(row.getTaskId(), MODULE_TYPE, resultId);
|
||||
fileResultMapper.deleteById(resultId);
|
||||
}
|
||||
@@ -769,6 +769,36 @@ public class SimilarAsinTaskService {
|
||||
: row.getResultFilename();
|
||||
}
|
||||
|
||||
public ResultDownloadInfo resolveResultDownloadInfo(Long resultId, Long userId) {
|
||||
FileResultEntity row = fileResultMapper.selectById(resultId);
|
||||
if (row == null || !MODULE_TYPE.equals(row.getModuleType()) || !Objects.equals(userId, row.getUserId())) {
|
||||
throw new BusinessException("记录不存在");
|
||||
}
|
||||
if (row.getResultFileUrl() == null || row.getResultFileUrl().isBlank()) {
|
||||
throw new BusinessException("暂无可下载文件");
|
||||
}
|
||||
String filename = row.getResultFilename() == null || row.getResultFilename().isBlank()
|
||||
? "similar-asin-" + resultId + ".xlsx"
|
||||
: row.getResultFilename();
|
||||
String contentType = row.getResultContentType() == null || row.getResultContentType().isBlank()
|
||||
? guessDownloadContentType(filename)
|
||||
: row.getResultContentType();
|
||||
return new ResultDownloadInfo(
|
||||
ossStorageService.generateFreshDownloadUrl(row.getResultFileUrl()),
|
||||
filename,
|
||||
contentType,
|
||||
row.getResultFileSize() == null ? 0L : row.getResultFileSize()
|
||||
);
|
||||
}
|
||||
|
||||
private String guessDownloadContentType(String filename) {
|
||||
String normalized = filename == null ? "" : filename.trim().toLowerCase(Locale.ROOT);
|
||||
if (normalized.endsWith(".zip")) {
|
||||
return CONTENT_TYPE_ZIP;
|
||||
}
|
||||
return CONTENT_TYPE_XLSX;
|
||||
}
|
||||
|
||||
public void finalizeStaleTasks() {
|
||||
if (transactionManager != null) {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
@@ -1251,7 +1281,7 @@ public class SimilarAsinTaskService {
|
||||
persistedRows.put(rowKey(row), row);
|
||||
}
|
||||
String payloadJson = writeJson(rows == null ? List.of() : rows, "结果序列化失败");
|
||||
payloadJson = writeJson(new ArrayList<>(persistedRows.values()), "similar ASIN chunk payload merge failed");
|
||||
payloadJson = writeJson(new ArrayList<>(persistedRows.values()), "相似ASIN分片载荷合并失败");
|
||||
String oldPayload = chunk.getPayloadJson();
|
||||
String storedPayload = transientPayloadStorageService.storeChunkPayloadVersioned(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
|
||||
chunk.setPayloadJson(storedPayload);
|
||||
@@ -1260,7 +1290,7 @@ public class SimilarAsinTaskService {
|
||||
int updated = taskChunkMapper.updateById(chunk);
|
||||
if (updated <= 0) {
|
||||
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
||||
throw new IllegalStateException("similar ASIN chunk payload update failed");
|
||||
throw new IllegalStateException("相似ASIN分片载荷更新失败");
|
||||
}
|
||||
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(oldPayload, storedPayload);
|
||||
}
|
||||
@@ -1535,8 +1565,8 @@ public class SimilarAsinTaskService {
|
||||
flushBufferedCozeResults(task.getId());
|
||||
assembleResultWorkbook(task, result);
|
||||
} catch (Exception ex) {
|
||||
log.warn("[similar-asin] assemble result workbook failed taskId={} err={}", task.getId(), ex.getMessage());
|
||||
finalError = firstNonBlank(finalError, "Generate similar ASIN result failed");
|
||||
log.warn("[相似ASIN] 组装结果工作簿失败 任务ID={} 错误={}", task.getId(), ex.getMessage());
|
||||
finalError = firstNonBlank(finalError, "生成相似ASIN结果失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1564,7 +1594,7 @@ public class SimilarAsinTaskService {
|
||||
if (shouldAssembleResult) {
|
||||
enqueueResultAssembly(task, result, true);
|
||||
} else if (assembleWorkbook && failed) {
|
||||
log.warn("[similar-asin] skip failed task workbook assembly because no persisted result rows taskId={}", task.getId());
|
||||
log.warn("[相似ASIN] 跳过失败任务的工作簿组装,因为没有已持久化结果行 任务ID={}", task.getId());
|
||||
}
|
||||
}
|
||||
taskCacheService.deleteTaskCache(task.getId());
|
||||
@@ -1599,7 +1629,7 @@ public class SimilarAsinTaskService {
|
||||
&& job != null
|
||||
&& "RUNNING".equals(job.getStatus())
|
||||
&& countPendingCozeStates(task.getId()) == 0) {
|
||||
taskFileJobService.requeue(job.getId(), "Python upload finished, assembling xlsx");
|
||||
taskFileJobService.requeue(job.getId(), "Python 上传完成,正在组装 xlsx");
|
||||
}
|
||||
return job;
|
||||
}
|
||||
@@ -1625,22 +1655,22 @@ public class SimilarAsinTaskService {
|
||||
|
||||
public boolean processResultFileJob(TaskFileJobEntity job) {
|
||||
if (job == null || job.getTaskId() == null || job.getResultId() == null) {
|
||||
throw new BusinessException("result file job arguments are incomplete");
|
||||
throw new BusinessException("结果文件任务参数不完整");
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(job.getTaskId());
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("task not found");
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
ensureTaskOwnedByCurrentInstance(task, "assemble result file");
|
||||
job.setScopeKey(buildTaskOwnerScopeKey(task));
|
||||
if (!isJobOwnedByCurrentInstance(job)) {
|
||||
log.info("[similar-asin] skip result file job because owner is another instance jobId={} taskId={} owner={} current={}",
|
||||
log.info("[相似ASIN] 跳过结果文件任务,因为归属实例不是当前实例 文件任务ID={} 任务ID={} 归属实例={} 当前实例={}",
|
||||
job.getId(), job.getTaskId(), ownerFromScopeKey(job.getScopeKey()), currentInstanceId());
|
||||
return false;
|
||||
}
|
||||
FileResultEntity result = fileResultMapper.selectById(job.getResultId());
|
||||
if (result == null || !MODULE_TYPE.equals(result.getModuleType())) {
|
||||
throw new BusinessException("result record not found");
|
||||
throw new BusinessException("结果记录不存在");
|
||||
}
|
||||
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, task.getId())
|
||||
@@ -1653,7 +1683,7 @@ public class SimilarAsinTaskService {
|
||||
if (countPendingCozeStates(task.getId()) > 0) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze submitted, waiting for result");
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze 已提交,等待结果");
|
||||
return false;
|
||||
}
|
||||
// P0-3:stale-recovery / 异常路径兜底 —— maybeFinalizeCozeJob 可能未能成功 flush
|
||||
@@ -1661,24 +1691,24 @@ public class SimilarAsinTaskService {
|
||||
try {
|
||||
flushBufferedCozeResults(task.getId());
|
||||
} catch (Exception flushEx) {
|
||||
log.warn("[similar-asin] result file job flush buffered coze results failed taskId={} jobId={} err={}",
|
||||
log.warn("[相似ASIN] 结果文件任务刷新缓冲区 Coze 结果失败 任务ID={} 文件任务ID={} 错误={}",
|
||||
task.getId(), job.getId(), flushEx.getMessage(), flushEx);
|
||||
throw new BusinessException("flush buffered coze results failed: "
|
||||
+ firstNonBlank(flushEx.getMessage(), "unknown error"));
|
||||
throw new BusinessException("刷新缓冲区 Coze 结果失败:"
|
||||
+ firstNonBlank(flushEx.getMessage(), "未知错误"));
|
||||
}
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 0, "Submitting Coze");
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 0, "正在提交 Coze");
|
||||
boolean pendingCoze = submitCozeBatches(task, result, job, chunks, allRowsByBaseId);
|
||||
if (pendingCoze) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze submitted, waiting for result");
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze 已提交,等待结果");
|
||||
return false;
|
||||
}
|
||||
if (STATUS_RUNNING.equals(task.getStatus()) && !isResultSubmissionComplete(task.getId())) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, Math.max(1, cozeWorkUnits),
|
||||
"Waiting for Python upload, submitting Coze every " + batchSize + " rows");
|
||||
"等待 Python 上传,每 " + batchSize + " 行提交一次 Coze");
|
||||
return false;
|
||||
}
|
||||
completeCozeFileJob(task, result, job, totalProgressUnits, cozeWorkUnits);
|
||||
@@ -2298,16 +2328,16 @@ public class SimilarAsinTaskService {
|
||||
return;
|
||||
}
|
||||
String failureMessage = poll.isFailed()
|
||||
? firstNonBlank(poll.failureMessage(), "Coze async workflow failed")
|
||||
? firstNonBlank(poll.failureMessage(), "Coze 异步工作流失败")
|
||||
: "";
|
||||
if (!poll.hasPayload() && failureMessage.isBlank()) {
|
||||
failureMessage = isCozeStateTimedOut(state)
|
||||
? "Coze async workflow poll timeout"
|
||||
: "Coze async workflow completed without output";
|
||||
? "Coze 异步工作流轮询超时"
|
||||
: "Coze 异步工作流已完成但没有输出结果";
|
||||
}
|
||||
List<SimilarAsinResultRowDto> batchRows = readCozeBatchRows(state);
|
||||
if (batchRows.isEmpty() && failureMessage.isBlank()) {
|
||||
failureMessage = "Coze batch payload missing";
|
||||
failureMessage = "Coze 批次载荷缺失";
|
||||
}
|
||||
List<SimilarAsinResultRowDto> cozeRows;
|
||||
if (failureMessage.isBlank()) {
|
||||
@@ -2911,9 +2941,9 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
|
||||
/**
|
||||
* P2-10\uff1aCoze \u5931\u8d25\u7edf\u4e00\u5206\u7c7b\uff08\u66ff\u4ee3 isRetryableCozeFailure / shouldSplitCozeBatchForRetry /
|
||||
* isCozeThrottleLockTimeout / isDeterministicCozeInputFailure \u7b49\u6563\u843d\u5728\u591a\u5904\u7684\u5173\u952e\u8bcd\u5224\u65ad\uff09\u3002
|
||||
* \u5173\u952e\u5b57\u96c6\u4e2d\u5728\u6b64\uff0c\u6240\u6709\u8c03\u7528\u65b9\u5171\u4eab\u540c\u4e00\u6765\u6e90\u907f\u514d\u6f02\u79fb\u3002
|
||||
* P2-10:Coze 失败统一分类(替代 isRetryableCozeFailure / shouldSplitCozeBatchForRetry /
|
||||
* isCozeThrottleLockTimeout / isDeterministicCozeInputFailure 等散落在多处的关键词判断)。
|
||||
* 关键词集中在此,所有调用方共享同一来源避免漂移。
|
||||
*/
|
||||
static final class CozeFailureClassifier {
|
||||
private CozeFailureClassifier() {}
|
||||
@@ -2929,7 +2959,7 @@ public class SimilarAsinTaskService {
|
||||
return trimmed.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
/** Coze \u5de5\u4f5c\u6d41\u7a7a\u8f93\u5165\u515c\u5e95 / 720712000 \u663e\u5f0f\u8f93\u5165\u6821\u9a8c\u5931\u8d25\uff1a\u76f4\u63a5 markFailed\uff0c\u4e0d\u8fdb retry\u3002 */
|
||||
/** Coze 工作流空输入兜底 / 720712000 显式输入校验失败:直接标记失败,不进入重试。 */
|
||||
static boolean isDeterministicInputFailure(String msg) {
|
||||
String n = norm(msg);
|
||||
if (n.isEmpty()) {
|
||||
@@ -2938,25 +2968,25 @@ public class SimilarAsinTaskService {
|
||||
return DETERMINISTIC_INPUT_FAILURE_PATTERN.matcher(n).find();
|
||||
}
|
||||
|
||||
/** \u8282\u6d41\u9501\u7b49\u5f85\u8d85\u65f6\uff08\u63d0\u4ea4\u9501\uff09\uff0c\u533a\u522b\u4e8e poll \u7b49\u5f85 Coze \u81ea\u8eab\u8d85\u65f6\u3002 */
|
||||
/** 节流锁等待超时(提交锁),区别于轮询等待 Coze 自身超时。 */
|
||||
static boolean isThrottleLockTimeout(String msg) {
|
||||
return norm(msg).contains("coze submit throttle lock timeout");
|
||||
}
|
||||
|
||||
/**
|
||||
* P1-5\uff1a720712008 / "node executed out of limit" / "\u5de5\u4f5c\u6d41\u8282\u70b9\u6267\u884c\u8d85\u9650"
|
||||
* \u8fd9\u4e00\u7c7b\u5f3a\u70c8\u6697\u793a"\u5355\u70b9\u6b7b\u5faa\u73af"\uff0csplit \u65f6\u76f4\u63a5\u62c6 size=1 \u9694\u79bb\u6bd2\u884c\uff0c
|
||||
* \u907f\u514d\u4e8c\u5206\u591a\u6b21\u6d6a\u8d39\u63d0\u4ea4\u914d\u989d\u3002
|
||||
* P1-5:720712008 / "node executed out of limit" / "工作流节点执行超限"
|
||||
* 这一类强烈暗示“单点死循环”,拆分时直接拆到 size=1 隔离问题行,
|
||||
* 避免二分多次浪费提交配额。
|
||||
*/
|
||||
static boolean isPoisonRow(String msg) {
|
||||
String n = norm(msg);
|
||||
return n.contains("720712008")
|
||||
|| n.contains("node executed out of limit")
|
||||
|| n.contains("execution limit")
|
||||
|| n.contains("\u5de5\u4f5c\u6d41\u8282\u70b9\u6267\u884c\u8d85\u9650");
|
||||
|| n.contains("工作流节点执行超限");
|
||||
}
|
||||
|
||||
/** \u547d\u4e2d\u540e\u5141\u8bb8"\u540c batch \u91cd\u8bd5"\u3002 */
|
||||
/** 命中后允许同批次重试。 */
|
||||
static boolean isRetryable(String msg) {
|
||||
if (isDeterministicInputFailure(msg)) {
|
||||
return false;
|
||||
@@ -2974,13 +3004,13 @@ public class SimilarAsinTaskService {
|
||||
|| n.contains("720712008")
|
||||
|| n.contains("720701002")
|
||||
|| n.contains("plugin limit")
|
||||
|| n.contains("\u9650\u6d41")
|
||||
|| n.contains("\u7a0d\u540e\u91cd\u8bd5")
|
||||
|| n.contains("\u5de5\u4f5c\u6d41\u8282\u70b9\u6267\u884c\u8d85\u9650")
|
||||
|| n.contains("\u8c03\u7528\u8d85\u65f6");
|
||||
|| n.contains("限流")
|
||||
|| n.contains("稍后重试")
|
||||
|| n.contains("工作流节点执行超限")
|
||||
|| n.contains("调用超时");
|
||||
}
|
||||
|
||||
/** \u547d\u4e2d\u540e\u4f18\u5148\u8d70 split retry\uff08\u62c6 batch\uff09\u3002 */
|
||||
/** 命中后优先走拆批重试。 */
|
||||
static boolean shouldSplitForRetry(String msg) {
|
||||
String n = norm(msg);
|
||||
return n.contains("timeout")
|
||||
@@ -2990,8 +3020,8 @@ public class SimilarAsinTaskService {
|
||||
|| n.contains("execution limit")
|
||||
|| n.contains("720712008")
|
||||
|| n.contains("720701002")
|
||||
|| n.contains("\u5de5\u4f5c\u6d41\u8282\u70b9\u6267\u884c\u8d85\u9650")
|
||||
|| n.contains("\u8c03\u7528\u8d85\u65f6");
|
||||
|| n.contains("工作流节点执行超限")
|
||||
|| n.contains("调用超时");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3153,9 +3183,9 @@ public class SimilarAsinTaskService {
|
||||
} catch (Exception ex) {
|
||||
TaskFileJobEntity job = taskFileJobService.findAssembleJob(taskId, MODULE_TYPE, context.resultId());
|
||||
if (job != null) {
|
||||
taskFileJobService.markFailed(job, firstNonBlank(ex.getMessage(), "similar ASIN result file build failed"));
|
||||
taskFileJobService.markFailed(job, firstNonBlank(ex.getMessage(), "相似ASIN结果文件生成失败"));
|
||||
}
|
||||
log.warn("[similar-asin] coze async finalize failed taskId={} resultId={} err={}",
|
||||
log.warn("[相似ASIN] Coze 异步收尾失败 任务ID={} 结果ID={} 错误={}",
|
||||
taskId, context.resultId(), ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
@@ -3188,15 +3218,15 @@ public class SimilarAsinTaskService {
|
||||
try {
|
||||
flushBufferedCozeResults(taskId);
|
||||
} catch (Exception flushEx) {
|
||||
String message = firstNonBlank(flushEx.getMessage(), "flush buffered coze results failed");
|
||||
log.warn("[similar-asin] coze finalize flush failed, marking job failed taskId={} jobId={} err={}",
|
||||
String message = firstNonBlank(flushEx.getMessage(), "刷新缓冲区 Coze 结果失败");
|
||||
log.warn("[相似ASIN] Coze 收尾刷新缓冲区失败,已标记文件任务失败 任务ID={} 文件任务ID={} 错误={}",
|
||||
taskId, job.getId(), message, flushEx);
|
||||
taskFileJobService.markFailed(job, message);
|
||||
return;
|
||||
}
|
||||
boolean requeued = taskFileJobService.requeue(job.getId(), "Coze results ready, assembling xlsx");
|
||||
boolean requeued = taskFileJobService.requeue(job.getId(), "Coze 结果已就绪,正在组装 xlsx");
|
||||
if (requeued) {
|
||||
log.info("[similar-asin] coze async results ready, result file job requeued taskId={} jobId={} resultId={}",
|
||||
log.info("[相似ASIN] Coze 异步结果已就绪,结果文件任务已重新入队 任务ID={} 文件任务ID={} 结果ID={}",
|
||||
taskId, job.getId(), context.resultId());
|
||||
}
|
||||
}
|
||||
@@ -3567,7 +3597,7 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
if (failedGroups > 0) {
|
||||
// 让 finalize 能感知到失败,调用方应避免继续 requeue assemble。
|
||||
throw new IllegalStateException("flush buffered coze results failed groups=" + failedGroups);
|
||||
throw new IllegalStateException("刷新缓冲区 Coze 结果失败,失败分组数=" + failedGroups);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5451,6 +5481,12 @@ public class SimilarAsinTaskService {
|
||||
int rowCount) {
|
||||
}
|
||||
|
||||
public record ResultDownloadInfo(String url,
|
||||
String filename,
|
||||
String contentType,
|
||||
long size) {
|
||||
}
|
||||
|
||||
private record ParsedWorkbook(int totalRows, int droppedRows, List<String> headers, List<SimilarAsinParsedRowVo> allRows) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,11 @@ public final class ExcelCellImageWriter {
|
||||
int idx = 0;
|
||||
int patchedCount = 0;
|
||||
for (RegisteredCellImage image : session.images) {
|
||||
String replacement = "<c r=\"" + image.cellRef + "\" t=\"e\" vm=\"" + idx + "\"><v>#VALUE!</v></c>";
|
||||
// [MS-XLSX] §2.2.10: cell @vm 是 1-based valueMetadata 索引,0 表示"无 metadata"。
|
||||
// futureMetadata/valueMetadata/rdrichvalue 内部块仍是 0-based,所以这里写 idx+1
|
||||
// 与 buildMetadata/buildRdRichValue 中的 i 解耦。Excel 365 在 vm=0 时会把 cell
|
||||
// 直接当作无图片的 #VALUE! 错误格渲染(任务 10498 J2 复现),必须改 1-based。
|
||||
String replacement = "<c r=\"" + image.cellRef + "\" t=\"e\" vm=\"" + (idx + 1) + "\"><v>#VALUE!</v></c>";
|
||||
Pattern pattern = Pattern.compile("<c\\b(?=[^>]*\\br=\"" + Pattern.quote(image.cellRef)
|
||||
+ "\")[^>]*(?:/>|>.*?</c>)", Pattern.DOTALL);
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
|
||||
@@ -17,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class ExcelCellImageWriterTest {
|
||||
|
||||
@Test
|
||||
void patchedCellImageValueMetadataUsesZeroBasedVmIndexes() throws Exception {
|
||||
void patchedCellImageValueMetadataUsesOneBasedVmIndexes() throws Exception {
|
||||
Path dir = Files.createTempDirectory("excel-cell-image-test-");
|
||||
Path xlsx = dir.resolve("result.xlsx");
|
||||
writeMinimalWorkbook(xlsx);
|
||||
@@ -30,13 +30,15 @@ class ExcelCellImageWriterTest {
|
||||
|
||||
try (ZipFile zip = new ZipFile(xlsx.toFile())) {
|
||||
String sheet = read(zip, "xl/worksheets/sheet1.xml");
|
||||
assertTrue(sheet.contains("<c r=\"J2\" t=\"e\" vm=\"0\"><v>#VALUE!</v></c>"));
|
||||
assertTrue(sheet.contains("<c r=\"K2\" t=\"e\" vm=\"1\"><v>#VALUE!</v></c>"));
|
||||
// cell @vm 是 [MS-XLSX] 规范里的 1-based 索引(0 表示"无 metadata")。
|
||||
assertTrue(sheet.contains("<c r=\"J2\" t=\"e\" vm=\"1\"><v>#VALUE!</v></c>"));
|
||||
assertTrue(sheet.contains("<c r=\"K2\" t=\"e\" vm=\"2\"><v>#VALUE!</v></c>"));
|
||||
|
||||
// futureMetadata 内部块仍然是 0-based。
|
||||
String metadata = read(zip, "xl/metadata.xml");
|
||||
assertTrue(metadata.contains("<xlrd:rvb i=\"0\"/>"));
|
||||
assertTrue(metadata.contains("<xlrd:rvb i=\"1\"/>"));
|
||||
assertEquals(2, maxVm(sheet) + 1);
|
||||
assertEquals(2, maxVm(sheet));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user