提交更新
This commit is contained in:
@@ -14,8 +14,8 @@ public class AppearancePatentProperties {
|
|||||||
private int cozeBatchSize = 10;
|
private int cozeBatchSize = 10;
|
||||||
private int cozeConnectTimeoutMillis = 10000;
|
private int cozeConnectTimeoutMillis = 10000;
|
||||||
private int cozeReadTimeoutMillis = 60000;
|
private int cozeReadTimeoutMillis = 60000;
|
||||||
private int cozePollIntervalMillis = 2000;
|
private int cozePollIntervalMillis = 5000;
|
||||||
private int cozePollTimeoutMillis = 120000;
|
private int cozePollTimeoutMillis = 600000;
|
||||||
private int staleTimeoutMinutes = 20;
|
private int staleTimeoutMinutes = 20;
|
||||||
private String staleFinalizeCron = "0 */2 * * * *";
|
private String staleFinalizeCron = "0 */2 * * * *";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ public class SimilarAsinProperties {
|
|||||||
private int cozeBatchSize = 10;
|
private int cozeBatchSize = 10;
|
||||||
private int cozeConnectTimeoutMillis = 10000;
|
private int cozeConnectTimeoutMillis = 10000;
|
||||||
private int cozeReadTimeoutMillis = 60000;
|
private int cozeReadTimeoutMillis = 60000;
|
||||||
private int cozePollIntervalMillis = 2000;
|
private int cozePollIntervalMillis = 5000;
|
||||||
private int cozePollTimeoutMillis = 120000;
|
private int cozePollTimeoutMillis = 600000;
|
||||||
private int staleTimeoutMinutes = 20;
|
private int staleTimeoutMinutes = 20;
|
||||||
private String staleFinalizeCron = "0 */2 * * * *";
|
private String staleFinalizeCron = "0 */2 * * * *";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ public class TaskOperationLockConfig implements WebMvcConfigurer {
|
|||||||
private static final class TaskOperationLockInterceptor implements HandlerInterceptor {
|
private static final class TaskOperationLockInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
private static final Pattern TASK_PATH = Pattern.compile(".*/api/([^/]+)/tasks/(\\d+)(?:/.*)?$");
|
private static final Pattern TASK_PATH = Pattern.compile(".*/api/([^/]+)/tasks/(\\d+)(?:/.*)?$");
|
||||||
|
private static final Pattern TASK_RESULT_PATH = Pattern.compile(".*/api/([^/]+)/tasks/(\\d+)/result/?$");
|
||||||
private static final Set<String> MUTATING_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE");
|
private static final Set<String> MUTATING_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE");
|
||||||
|
private static final long RESULT_SUBMIT_WAIT_MILLIS = 10 * 60 * 1000L;
|
||||||
private static final String LOCK_ATTRIBUTE = TaskOperationLockInterceptor.class.getName() + ".LOCK";
|
private static final String LOCK_ATTRIBUTE = TaskOperationLockInterceptor.class.getName() + ".LOCK";
|
||||||
|
|
||||||
private final TaskDistributedLockService taskDistributedLockService;
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
@@ -53,17 +55,25 @@ public class TaskOperationLockConfig implements WebMvcConfigurer {
|
|||||||
}
|
}
|
||||||
String moduleType = matcher.group(1).trim().toUpperCase(Locale.ROOT).replace('-', '_');
|
String moduleType = matcher.group(1).trim().toUpperCase(Locale.ROOT).replace('-', '_');
|
||||||
Long taskId = Long.valueOf(matcher.group(2));
|
Long taskId = Long.valueOf(matcher.group(2));
|
||||||
|
long waitMillis = resolveWaitMillis(request.getMethod(), uri);
|
||||||
TaskDistributedLockService.LockHandle lockHandle =
|
TaskDistributedLockService.LockHandle lockHandle =
|
||||||
taskDistributedLockService.acquire(moduleType, taskId, TaskDistributedLockService.DEFAULT_WAIT_MILLIS);
|
taskDistributedLockService.acquire(moduleType, taskId, waitMillis);
|
||||||
if (lockHandle == null) {
|
if (lockHandle == null) {
|
||||||
log.info("[task-operation-lock] rejected busy task operation method={} uri={} moduleType={} taskId={}",
|
log.info("[task-operation-lock] rejected busy task operation method={} uri={} moduleType={} taskId={} waitMillis={}",
|
||||||
request.getMethod(), uri, moduleType, taskId);
|
request.getMethod(), uri, moduleType, taskId, waitMillis);
|
||||||
throw new BusinessException(40902, "TASK_LOCK_BUSY");
|
throw new BusinessException(40902, "TASK_LOCK_BUSY");
|
||||||
}
|
}
|
||||||
request.setAttribute(LOCK_ATTRIBUTE, lockHandle);
|
request.setAttribute(LOCK_ATTRIBUTE, lockHandle);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long resolveWaitMillis(String method, String uri) {
|
||||||
|
if ("POST".equals(method) && TASK_RESULT_PATH.matcher(uri == null ? "" : uri).matches()) {
|
||||||
|
return RESULT_SUBMIT_WAIT_MILLIS;
|
||||||
|
}
|
||||||
|
return TaskDistributedLockService.DEFAULT_WAIT_MILLIS;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterCompletion(HttpServletRequest request,
|
public void afterCompletion(HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class AppearancePatentCozeClient {
|
|||||||
private final AppearancePatentProperties properties;
|
private final AppearancePatentProperties properties;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public List<AppearancePatentResultRowDto> inspect(List<AppearancePatentResultRowDto> rows, String prompt) {
|
public List<AppearancePatentResultRowDto> inspect(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) {
|
||||||
if (rows == null || rows.isEmpty()) {
|
if (rows == null || rows.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ public class AppearancePatentCozeClient {
|
|||||||
return rows.stream().map(this::copy).toList();
|
return rows.stream().map(this::copy).toList();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return inspectWithFallback(rows, prompt);
|
return inspectWithFallback(rows, prompt, apiKey);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
String failureMessage = failureMessage(ex);
|
String failureMessage = failureMessage(ex);
|
||||||
log.warn("[appearance-patent] coze batch failed size={} err={}", rows.size(), failureMessage);
|
log.warn("[appearance-patent] coze batch failed size={} err={}", rows.size(), failureMessage);
|
||||||
@@ -47,8 +47,8 @@ public class AppearancePatentCozeClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CozeSubmitResponse submitWorkflow(List<AppearancePatentResultRowDto> rows, String prompt) throws Exception {
|
public CozeSubmitResponse submitWorkflow(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt));
|
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey));
|
||||||
ensureSuccess(submitRoot);
|
ensureSuccess(submitRoot);
|
||||||
return new CozeSubmitResponse(
|
return new CozeSubmitResponse(
|
||||||
extractExecuteId(submitRoot),
|
extractExecuteId(submitRoot),
|
||||||
@@ -83,12 +83,12 @@ public class AppearancePatentCozeClient {
|
|||||||
return rows.stream().map(this::copy).map(row -> markFailed(row, failureMessage)).toList();
|
return rows.stream().map(this::copy).map(row -> markFailed(row, failureMessage)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AppearancePatentResultRowDto> inspectWithFallback(List<AppearancePatentResultRowDto> rows, String prompt) {
|
private List<AppearancePatentResultRowDto> inspectWithFallback(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) {
|
||||||
try {
|
try {
|
||||||
if (rows.size() == 1) {
|
if (rows.size() == 1) {
|
||||||
return inspectSingleRowWithRetry(rows, prompt);
|
return inspectSingleRowWithRetry(rows, prompt, apiKey);
|
||||||
}
|
}
|
||||||
InspectAttempt attempt = inspectOnce(rows, prompt);
|
InspectAttempt attempt = inspectOnce(rows, prompt, apiKey);
|
||||||
if (attempt.resolvedCount() < rows.size()) {
|
if (attempt.resolvedCount() < rows.size()) {
|
||||||
throw new PartialCozeResultException(attempt.resolvedCount(), rows.size(), attempt.rawResultCount());
|
throw new PartialCozeResultException(attempt.resolvedCount(), rows.size(), attempt.rawResultCount());
|
||||||
}
|
}
|
||||||
@@ -99,17 +99,17 @@ public class AppearancePatentCozeClient {
|
|||||||
log.warn("[appearance-patent] coze batch fallback split size={} left={} right={} err={}",
|
log.warn("[appearance-patent] coze batch fallback split size={} left={} right={} err={}",
|
||||||
rows.size(), middle, rows.size() - middle, failureMessage(ex));
|
rows.size(), middle, rows.size() - middle, failureMessage(ex));
|
||||||
List<AppearancePatentResultRowDto> merged = new ArrayList<>(rows.size());
|
List<AppearancePatentResultRowDto> merged = new ArrayList<>(rows.size());
|
||||||
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(0, middle), prompt));
|
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(0, middle), prompt, apiKey));
|
||||||
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(middle, rows.size()), prompt));
|
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(middle, rows.size()), prompt, apiKey));
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
throw propagate(ex);
|
throw propagate(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AppearancePatentResultRowDto> inspectPartitionWithFailureFallback(List<AppearancePatentResultRowDto> rows, String prompt) {
|
private List<AppearancePatentResultRowDto> inspectPartitionWithFailureFallback(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) {
|
||||||
try {
|
try {
|
||||||
return inspectWithFallback(rows, prompt);
|
return inspectWithFallback(rows, prompt, apiKey);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
String failureMessage = failureMessage(ex);
|
String failureMessage = failureMessage(ex);
|
||||||
log.warn("[appearance-patent] coze partition failed size={} err={}", rows.size(), failureMessage);
|
log.warn("[appearance-patent] coze partition failed size={} err={}", rows.size(), failureMessage);
|
||||||
@@ -117,12 +117,12 @@ public class AppearancePatentCozeClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AppearancePatentResultRowDto> inspectSingleRowWithRetry(List<AppearancePatentResultRowDto> rows, String prompt) throws Exception {
|
private List<AppearancePatentResultRowDto> inspectSingleRowWithRetry(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||||
AppearancePatentResultRowDto row = rows.getFirst();
|
AppearancePatentResultRowDto row = rows.getFirst();
|
||||||
PartialCozeResultException lastFailure = null;
|
PartialCozeResultException lastFailure = null;
|
||||||
for (int attemptIndex = 1; attemptIndex <= 3; attemptIndex++) {
|
for (int attemptIndex = 1; attemptIndex <= 3; attemptIndex++) {
|
||||||
try {
|
try {
|
||||||
InspectAttempt attempt = inspectOnce(rows, prompt);
|
InspectAttempt attempt = inspectOnce(rows, prompt, apiKey);
|
||||||
if (attempt.resolvedCount() == rows.size()) {
|
if (attempt.resolvedCount() == rows.size()) {
|
||||||
return attempt.mergedRows();
|
return attempt.mergedRows();
|
||||||
}
|
}
|
||||||
@@ -153,8 +153,8 @@ public class AppearancePatentCozeClient {
|
|||||||
throw lastFailure == null ? new PartialCozeResultException(0, rows.size(), 0) : lastFailure;
|
throw lastFailure == null ? new PartialCozeResultException(0, rows.size(), 0) : lastFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private InspectAttempt inspectOnce(List<AppearancePatentResultRowDto> rows, String prompt) throws Exception {
|
private InspectAttempt inspectOnce(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||||
String raw = runWorkflowAsyncAndWait(rows, prompt);
|
String raw = runWorkflowAsyncAndWait(rows, prompt, apiKey);
|
||||||
List<CozeResult> results = parseResults(raw);
|
List<CozeResult> results = parseResults(raw);
|
||||||
if (rows.size() > 1 && !results.isEmpty() && results.stream().noneMatch(this::hasIdentity)) {
|
if (rows.size() > 1 && !results.isEmpty() && results.stream().noneMatch(this::hasIdentity)) {
|
||||||
throw new PartialCozeResultException(0, rows.size(), results.size());
|
throw new PartialCozeResultException(0, rows.size(), results.size());
|
||||||
@@ -163,8 +163,8 @@ public class AppearancePatentCozeClient {
|
|||||||
return new InspectAttempt(raw, merged, resolvedCount(merged), results.size());
|
return new InspectAttempt(raw, merged, resolvedCount(merged), results.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String runWorkflowAsyncAndWait(List<AppearancePatentResultRowDto> rows, String prompt) throws Exception {
|
private String runWorkflowAsyncAndWait(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt));
|
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey));
|
||||||
ensureSuccess(submitRoot);
|
ensureSuccess(submitRoot);
|
||||||
|
|
||||||
String immediateData = extractResultDataText(submitRoot);
|
String immediateData = extractResultDataText(submitRoot);
|
||||||
@@ -205,15 +205,15 @@ public class AppearancePatentCozeClient {
|
|||||||
throw new IllegalStateException("Coze async workflow poll timeout");
|
throw new IllegalStateException("Coze async workflow poll timeout");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String postWorkflow(List<AppearancePatentResultRowDto> rows, String prompt) {
|
private String postWorkflow(List<AppearancePatentResultRowDto> rows, String prompt, String apiKey) {
|
||||||
Map<String, Object> parameters = buildParameters(rows, prompt);
|
Map<String, Object> parameters = buildParameters(rows, prompt, apiKey);
|
||||||
Map<String, Object> body = new LinkedHashMap<>();
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
body.put("workflow_id", properties.getCozeWorkflowId());
|
body.put("workflow_id", properties.getCozeWorkflowId());
|
||||||
body.put("parameters", parameters);
|
body.put("parameters", parameters);
|
||||||
body.put("is_async", Boolean.TRUE);
|
body.put("is_async", Boolean.TRUE);
|
||||||
log.info("[appearance-patent] coze request url={} body={}",
|
log.info("[appearance-patent] coze request url={} body={}",
|
||||||
joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()),
|
joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()),
|
||||||
writeJson(body));
|
writeJson(maskCozeRequestBody(body)));
|
||||||
|
|
||||||
RestClient.RequestBodySpec request = restClient().post()
|
RestClient.RequestBodySpec request = restClient().post()
|
||||||
.uri(joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()))
|
.uri(joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()))
|
||||||
@@ -255,7 +255,7 @@ public class AppearancePatentCozeClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildParameters(List<AppearancePatentResultRowDto> rows, String prompt) {
|
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> groupKeys = rows.stream().map(row -> nonBlank(row.getGroupKey(), rowKey(row))).toList();
|
||||||
List<String> rowIds = rows.stream().map(row -> nonBlank(row.getId(), "")).toList();
|
List<String> rowIds = rows.stream().map(row -> nonBlank(row.getId(), "")).toList();
|
||||||
List<String> asins = rows.stream().map(row -> nonBlank(row.getAsin(), "")).toList();
|
List<String> asins = rows.stream().map(row -> nonBlank(row.getAsin(), "")).toList();
|
||||||
@@ -268,9 +268,38 @@ public class AppearancePatentCozeClient {
|
|||||||
parameters.put("url_list", urls);
|
parameters.put("url_list", urls);
|
||||||
parameters.put("items", buildItemObjects(rows, groupKeys, rowIds, asins, countries, titles, urls));
|
parameters.put("items", buildItemObjects(rows, groupKeys, rowIds, asins, countries, titles, urls));
|
||||||
parameters.put("prompt", prompt == null ? "" : prompt);
|
parameters.put("prompt", prompt == null ? "" : prompt);
|
||||||
|
if (apiKey != null && !apiKey.isBlank()) {
|
||||||
|
parameters.put("api_key", apiKey.trim());
|
||||||
|
}
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> maskCozeRequestBody(Map<String, Object> body) {
|
||||||
|
Map<String, Object> masked = new LinkedHashMap<>(body);
|
||||||
|
Object parametersObj = masked.get("parameters");
|
||||||
|
if (parametersObj instanceof Map<?, ?> parameters) {
|
||||||
|
Map<String, Object> maskedParameters = new LinkedHashMap<>((Map<String, Object>) parameters);
|
||||||
|
Object apiKey = maskedParameters.get("api_key");
|
||||||
|
if (apiKey instanceof String apiKeyText && !apiKeyText.isBlank()) {
|
||||||
|
maskedParameters.put("api_key", maskSecret(apiKeyText));
|
||||||
|
}
|
||||||
|
masked.put("parameters", maskedParameters);
|
||||||
|
}
|
||||||
|
return masked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String maskSecret(String secret) {
|
||||||
|
String normalized = secret == null ? "" : secret.trim();
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (normalized.length() <= 10) {
|
||||||
|
return "***";
|
||||||
|
}
|
||||||
|
return normalized.substring(0, 6) + "***" + normalized.substring(normalized.length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
private List<Map<String, Object>> buildItemObjects(List<AppearancePatentResultRowDto> rows,
|
private List<Map<String, Object>> buildItemObjects(List<AppearancePatentResultRowDto> rows,
|
||||||
List<String> groupKeys,
|
List<String> groupKeys,
|
||||||
List<String> rowIds,
|
List<String> rowIds,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonAlias;
|
|||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -25,4 +26,10 @@ public class AppearancePatentParseRequest {
|
|||||||
@JsonAlias({"aiPrompt", "prompt"})
|
@JsonAlias({"aiPrompt", "prompt"})
|
||||||
@Schema(description = "AI 提示词。非必填;为空时前端会使用默认提示词。后端会保存该提示词,并在调用 Coze workflow 时作为 prompt 参数传入。", example = "请排查这些亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。")
|
@Schema(description = "AI 提示词。非必填;为空时前端会使用默认提示词。后端会保存该提示词,并在调用 Coze workflow 时作为 prompt 参数传入。", example = "请排查这些亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。")
|
||||||
private String aiPrompt;
|
private String aiPrompt;
|
||||||
|
|
||||||
|
@JsonProperty("api_key")
|
||||||
|
@JsonAlias({"apiKey"})
|
||||||
|
@Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥。")
|
||||||
|
@NotBlank(message = "密钥不能为空")
|
||||||
|
private String apiKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ public class AppearancePatentParsedPayloadDto {
|
|||||||
@Schema(description = "AI 提示词")
|
@Schema(description = "AI 提示词")
|
||||||
private String aiPrompt;
|
private String aiPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥")
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
@Schema(description = "本次解析的源文件列表")
|
@Schema(description = "本次解析的源文件列表")
|
||||||
private List<AppearancePatentSourceFileDto> sourceFiles = new ArrayList<>();
|
private List<AppearancePatentSourceFileDto> sourceFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
||||||
import com.nanri.aiimage.config.AppearancePatentProperties;
|
import com.nanri.aiimage.config.AppearancePatentProperties;
|
||||||
|
import com.nanri.aiimage.config.InstanceMetadata;
|
||||||
import com.nanri.aiimage.config.StorageProperties;
|
import com.nanri.aiimage.config.StorageProperties;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.client.AppearancePatentCozeClient;
|
import com.nanri.aiimage.modules.appearancepatent.client.AppearancePatentCozeClient;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParseRequest;
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParseRequest;
|
||||||
@@ -131,6 +132,7 @@ public class AppearancePatentTaskService {
|
|||||||
private final PlatformTransactionManager transactionManager;
|
private final PlatformTransactionManager transactionManager;
|
||||||
private final DistributedJobLockService distributedJobLockService;
|
private final DistributedJobLockService distributedJobLockService;
|
||||||
private final TaskDistributedLockService taskDistributedLockService;
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
|
private final InstanceMetadata instanceMetadata;
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("cozeTaskExecutor")
|
@Qualifier("cozeTaskExecutor")
|
||||||
private TaskExecutor cozeTaskExecutor;
|
private TaskExecutor cozeTaskExecutor;
|
||||||
@@ -199,11 +201,11 @@ public class AppearancePatentTaskService {
|
|||||||
|
|
||||||
String aggregateScopeKey = buildAggregateScopeKey(sourceFiles);
|
String aggregateScopeKey = buildAggregateScopeKey(sourceFiles);
|
||||||
String sourceScopeHash = DigestUtil.sha256Hex(aggregateScopeKey);
|
String sourceScopeHash = DigestUtil.sha256Hex(aggregateScopeKey);
|
||||||
String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), sourceFiles, mergedHeaders, groups, allRows);
|
String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), request.getApiKey(), sourceFiles, mergedHeaders, groups, allRows);
|
||||||
long payloadBuiltAt = System.nanoTime();
|
long payloadBuiltAt = System.nanoTime();
|
||||||
String parsedPayloadPointer = storeParsedPayload(task.getId(), sourceScopeHash, parsedPayload);
|
String parsedPayloadPointer = storeParsedPayload(task.getId(), sourceScopeHash, parsedPayload);
|
||||||
long payloadStoredAt = System.nanoTime();
|
long payloadStoredAt = System.nanoTime();
|
||||||
task.setResultJson(buildTaskResultJson(request.getAiPrompt(), sourceFiles, parsedPayloadPointer));
|
task.setResultJson(buildTaskResultJson(request.getAiPrompt(), request.getApiKey(), sourceFiles, parsedPayloadPointer));
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
|
|
||||||
@@ -267,6 +269,7 @@ public class AppearancePatentTaskService {
|
|||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
||||||
throw new BusinessException("任务不存在");
|
throw new BusinessException("任务不存在");
|
||||||
}
|
}
|
||||||
|
ensureTaskOwnedByCurrentInstance(task, "activate");
|
||||||
if (STATUS_SUCCESS.equals(task.getStatus()) || STATUS_FAILED.equals(task.getStatus())) {
|
if (STATUS_SUCCESS.equals(task.getStatus()) || STATUS_FAILED.equals(task.getStatus())) {
|
||||||
throw new BusinessException("任务已结束");
|
throw new BusinessException("任务已结束");
|
||||||
}
|
}
|
||||||
@@ -429,6 +432,7 @@ public class AppearancePatentTaskService {
|
|||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
throw new BusinessException("任务不存在");
|
throw new BusinessException("任务不存在");
|
||||||
}
|
}
|
||||||
|
ensureTaskOwnedByCurrentInstance(task, "submit result");
|
||||||
if (!STATUS_RUNNING.equals(task.getStatus())) {
|
if (!STATUS_RUNNING.equals(task.getStatus())) {
|
||||||
throw new BusinessException("任务不是运行中状态");
|
throw new BusinessException("任务不是运行中状态");
|
||||||
}
|
}
|
||||||
@@ -620,6 +624,7 @@ public class AppearancePatentTaskService {
|
|||||||
throw new BusinessException("任务不是运行中状态");
|
throw new BusinessException("任务不是运行中状态");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureTaskOwnedByCurrentInstance(task, "submit result");
|
||||||
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
|
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
|
||||||
int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal();
|
int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal();
|
||||||
boolean done = Boolean.TRUE.equals(request.getDone());
|
boolean done = Boolean.TRUE.equals(request.getDone());
|
||||||
@@ -772,10 +777,11 @@ public class AppearancePatentTaskService {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
String prompt = readAiPrompt(task);
|
String prompt = readAiPrompt(task);
|
||||||
|
String apiKey = readApiKey(task);
|
||||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||||
List<AppearancePatentResultRowDto> result = new ArrayList<>();
|
List<AppearancePatentResultRowDto> result = new ArrayList<>();
|
||||||
for (int i = 0; i < items.size(); i += batchSize) {
|
for (int i = 0; i < items.size(); i += batchSize) {
|
||||||
result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt));
|
result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt, apiKey));
|
||||||
if (progressHook != null) {
|
if (progressHook != null) {
|
||||||
progressHook.run();
|
progressHook.run();
|
||||||
}
|
}
|
||||||
@@ -1060,6 +1066,14 @@ public class AppearancePatentTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String readApiKey(FileTaskEntity task) {
|
||||||
|
try {
|
||||||
|
return normalize(readParsedPayload(task).getApiKey());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void finalizeTask(FileTaskEntity task, String error, int rowCount, boolean assembleWorkbook) {
|
private void finalizeTask(FileTaskEntity task, String error, int rowCount, boolean assembleWorkbook) {
|
||||||
String finalError = error;
|
String finalError = error;
|
||||||
FileResultEntity result = null;
|
FileResultEntity result = null;
|
||||||
@@ -1098,7 +1112,7 @@ public class AppearancePatentTaskService {
|
|||||||
}
|
}
|
||||||
fileResultMapper.updateById(result);
|
fileResultMapper.updateById(result);
|
||||||
if (!failed && assembleWorkbook) {
|
if (!failed && assembleWorkbook) {
|
||||||
taskFileJobService.enqueueAssembleResult(task.getId(), MODULE_TYPE, result.getId(), "task:" + task.getId());
|
taskFileJobService.enqueueAssembleResult(task.getId(), MODULE_TYPE, result.getId(), buildTaskOwnerScopeKey(task.getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
taskCacheService.deleteTaskCache(task.getId());
|
taskCacheService.deleteTaskCache(task.getId());
|
||||||
@@ -1112,6 +1126,11 @@ public class AppearancePatentTaskService {
|
|||||||
if (job == null || job.getTaskId() == null || job.getResultId() == null) {
|
if (job == null || job.getTaskId() == null || job.getResultId() == null) {
|
||||||
throw new BusinessException("result file job arguments are incomplete");
|
throw new BusinessException("result file job arguments are incomplete");
|
||||||
}
|
}
|
||||||
|
if (!isJobOwnedByCurrentInstance(job)) {
|
||||||
|
log.info("[appearance-patent] skip result file job because owner is another instance jobId={} taskId={} owner={} current={}",
|
||||||
|
job.getId(), job.getTaskId(), ownerFromScopeKey(job.getScopeKey()), currentInstanceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(job.getTaskId(), TASK_LOCK_WAIT_MILLIS);
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(job.getTaskId(), TASK_LOCK_WAIT_MILLIS);
|
||||||
if (lockHandle == null) {
|
if (lockHandle == null) {
|
||||||
taskFileJobService.requeue(job.getId(), "Task is busy, waiting for appearance patent result merge");
|
taskFileJobService.requeue(job.getId(), "Task is busy, waiting for appearance patent result merge");
|
||||||
@@ -1159,12 +1178,6 @@ public class AppearancePatentTaskService {
|
|||||||
|
|
||||||
@Scheduled(fixedDelayString = "${aiimage.appearance-patent.coze-poll-delay-ms:5000}")
|
@Scheduled(fixedDelayString = "${aiimage.appearance-patent.coze-poll-delay-ms:5000}")
|
||||||
public void pollPendingCozeJobs() {
|
public void pollPendingCozeJobs() {
|
||||||
DistributedJobLockService.LockHandle lockHandle =
|
|
||||||
distributedJobLockService.tryLock("appearance-patent:coze-poll", Duration.ofMinutes(1));
|
|
||||||
if (lockHandle == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try (lockHandle) {
|
|
||||||
List<TaskScopeStateEntity> states = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
List<TaskScopeStateEntity> states = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||||
.in(TaskScopeStateEntity::getCozeStatus, List.of(COZE_STATUS_SUBMITTED, COZE_STATUS_RUNNING))
|
.in(TaskScopeStateEntity::getCozeStatus, List.of(COZE_STATUS_SUBMITTED, COZE_STATUS_RUNNING))
|
||||||
@@ -1176,7 +1189,7 @@ public class AppearancePatentTaskService {
|
|||||||
}
|
}
|
||||||
log.info("[appearance-patent] coze poll picked pending states count={}", states.size());
|
log.info("[appearance-patent] coze poll picked pending states count={}", states.size());
|
||||||
for (TaskScopeStateEntity state : states) {
|
for (TaskScopeStateEntity state : states) {
|
||||||
if (state == null || state.getId() == null) {
|
if (state == null || state.getId() == null || !isCozeStateOwnedByCurrentInstance(state)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Long stateId = state.getId();
|
Long stateId = state.getId();
|
||||||
@@ -1193,7 +1206,6 @@ public class AppearancePatentTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private boolean submitCozeBatches(FileTaskEntity task,
|
private boolean submitCozeBatches(FileTaskEntity task,
|
||||||
FileResultEntity result,
|
FileResultEntity result,
|
||||||
@@ -1209,6 +1221,7 @@ public class AppearancePatentTaskService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String prompt = readAiPrompt(task);
|
String prompt = readAiPrompt(task);
|
||||||
|
String apiKey = readApiKey(task);
|
||||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||||
boolean pending = false;
|
boolean pending = false;
|
||||||
for (TaskChunkEntity chunk : chunks) {
|
for (TaskChunkEntity chunk : chunks) {
|
||||||
@@ -1225,7 +1238,7 @@ public class AppearancePatentTaskService {
|
|||||||
for (int i = 0; i < unresolvedRows.size(); i += batchSize) {
|
for (int i = 0; i < unresolvedRows.size(); i += batchSize) {
|
||||||
List<AppearancePatentResultRowDto> batchRows =
|
List<AppearancePatentResultRowDto> batchRows =
|
||||||
unresolvedRows.subList(i, Math.min(i + batchSize, unresolvedRows.size()));
|
unresolvedRows.subList(i, Math.min(i + batchSize, unresolvedRows.size()));
|
||||||
pending |= submitCozeBatch(task, result, job, chunk, batchRows, batchIndex, batchTotal, prompt, allRowsByBaseId);
|
pending |= submitCozeBatch(task, result, job, chunk, batchRows, batchIndex, batchTotal, prompt, apiKey, allRowsByBaseId);
|
||||||
batchIndex++;
|
batchIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1240,6 +1253,7 @@ public class AppearancePatentTaskService {
|
|||||||
int batchIndex,
|
int batchIndex,
|
||||||
int batchTotal,
|
int batchTotal,
|
||||||
String prompt,
|
String prompt,
|
||||||
|
String apiKey,
|
||||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId) {
|
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId) {
|
||||||
if (batchRows == null || batchRows.isEmpty()) {
|
if (batchRows == null || batchRows.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1256,7 +1270,7 @@ public class AppearancePatentTaskService {
|
|||||||
|| COZE_STATUS_RUNNING.equals(existing.getCozeStatus());
|
|| COZE_STATUS_RUNNING.equals(existing.getCozeStatus());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
AppearancePatentCozeClient.CozeSubmitResponse submit = cozeClient.submitWorkflow(batchRows, prompt);
|
AppearancePatentCozeClient.CozeSubmitResponse submit = cozeClient.submitWorkflow(batchRows, prompt, apiKey);
|
||||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||||
List<AppearancePatentResultRowDto> cozeRows = cozeClient.mergeRowsFromDataText(batchRows, submit.immediateData());
|
List<AppearancePatentResultRowDto> cozeRows = cozeClient.mergeRowsFromDataText(batchRows, submit.immediateData());
|
||||||
mergeCozeRowsIntoChunk(task, chunk.getScopeHash(), chunk.getChunkIndex(), cozeRows, allRowsByBaseId);
|
mergeCozeRowsIntoChunk(task, chunk.getScopeHash(), chunk.getChunkIndex(), cozeRows, allRowsByBaseId);
|
||||||
@@ -1305,7 +1319,8 @@ public class AppearancePatentTaskService {
|
|||||||
chunk.getScopeHash(),
|
chunk.getScopeHash(),
|
||||||
chunk.getChunkIndex(),
|
chunk.getChunkIndex(),
|
||||||
batchIndex,
|
batchIndex,
|
||||||
batchTotal
|
batchTotal,
|
||||||
|
currentInstanceId()
|
||||||
);
|
);
|
||||||
String batchPayload = writeJson(batchRows, "serialize coze batch payload failed");
|
String batchPayload = writeJson(batchRows, "serialize coze batch payload failed");
|
||||||
String storedBatchPayload = storeSharedCozeBatchPayload(task.getId(), batchScopeHash, batchPayload);
|
String storedBatchPayload = storeSharedCozeBatchPayload(task.getId(), batchScopeHash, batchPayload);
|
||||||
@@ -1341,6 +1356,11 @@ public class AppearancePatentTaskService {
|
|||||||
if (lockState != null) {
|
if (lockState != null) {
|
||||||
taskIdForLock = lockState.getTaskId();
|
taskIdForLock = lockState.getTaskId();
|
||||||
}
|
}
|
||||||
|
if (!isCozeStateOwnedByCurrentInstance(lockState)) {
|
||||||
|
log.info("[appearance-patent] coze poll skipped because owner is another instance taskId={} stateId={} owner={} current={}",
|
||||||
|
taskIdForLock, stateId, ownerFromCozeState(lockState), currentInstanceId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskIdForLock, 0L);
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskIdForLock, 0L);
|
||||||
if (lockHandle == null) {
|
if (lockHandle == null) {
|
||||||
if (taskIdForLock != null) {
|
if (taskIdForLock != null) {
|
||||||
@@ -1375,6 +1395,11 @@ public class AppearancePatentTaskService {
|
|||||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze batch context missing");
|
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze batch context missing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isOwnerCurrent(context.ownerInstanceId())) {
|
||||||
|
log.info("[appearance-patent] coze poll skipped after context refresh because owner is another instance taskId={} stateId={} owner={} current={}",
|
||||||
|
state.getTaskId(), state.getId(), context.ownerInstanceId(), currentInstanceId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
taskFileJobService.touchRunning(context.jobId());
|
taskFileJobService.touchRunning(context.jobId());
|
||||||
log.info("[appearance-patent] coze poll start taskId={} stateId={} executeId={} jobId={} chunk={} batch={}/{}",
|
log.info("[appearance-patent] coze poll start taskId={} stateId={} executeId={} jobId={} chunk={} batch={}/{}",
|
||||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(),
|
state.getTaskId(), state.getId(), state.getCozeExecuteId(),
|
||||||
@@ -1664,6 +1689,69 @@ public class AppearancePatentTaskService {
|
|||||||
+ ":batch:" + batchIndex;
|
+ ":batch:" + batchIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildTaskOwnerScopeKey(Long taskId) {
|
||||||
|
return "task:" + taskId + ":owner:" + currentInstanceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String currentInstanceId() {
|
||||||
|
String instanceId = instanceMetadata == null ? null : instanceMetadata.getInstanceId();
|
||||||
|
return firstNonBlank(instanceId, "unknown-instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isJobOwnedByCurrentInstance(TaskFileJobEntity job) {
|
||||||
|
return job == null || isOwnerCurrent(ownerFromScopeKey(job.getScopeKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTaskOwnedByCurrentInstance(FileTaskEntity task, String operation) {
|
||||||
|
String owner = ownerFromTask(task);
|
||||||
|
if (isOwnerCurrent(owner)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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, "该任务已绑定到另一台服务实例处理,请通过原实例继续处理");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCozeStateOwnedByCurrentInstance(TaskScopeStateEntity state) {
|
||||||
|
return state == null || isOwnerCurrent(ownerFromCozeState(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOwnerCurrent(String owner) {
|
||||||
|
return owner == null || owner.isBlank() || Objects.equals(owner, currentInstanceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ownerFromCozeState(TaskScopeStateEntity state) {
|
||||||
|
CozeBatchContext context = readCozeBatchContext(state);
|
||||||
|
return context == null ? null : context.ownerInstanceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ownerFromTask(FileTaskEntity task) {
|
||||||
|
if (task == null || task.getResultJson() == null || task.getResultJson().isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String owner = objectMapper.readTree(task.getResultJson()).path("ownerInstanceId").asText("");
|
||||||
|
return owner.isBlank() ? null : owner;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("[appearance-patent] read task owner failed taskId={} err={}",
|
||||||
|
task.getId(), ex.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ownerFromScopeKey(String scopeKey) {
|
||||||
|
if (scopeKey == null || scopeKey.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String marker = ":owner:";
|
||||||
|
int index = scopeKey.lastIndexOf(marker);
|
||||||
|
if (index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String owner = scopeKey.substring(index + marker.length()).trim();
|
||||||
|
return owner.isBlank() ? null : owner;
|
||||||
|
}
|
||||||
|
|
||||||
private String abbreviate(String value, int maxLength) {
|
private String abbreviate(String value, int maxLength) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
@@ -2340,7 +2428,9 @@ public class AppearancePatentTaskService {
|
|||||||
LocalDateTime baseTime = snapshot.getUpdatedAt() != null ? snapshot.getUpdatedAt() : job.getUpdatedAt();
|
LocalDateTime baseTime = snapshot.getUpdatedAt() != null ? snapshot.getUpdatedAt() : job.getUpdatedAt();
|
||||||
long elapsedSeconds = baseTime == null ? 0 : Math.max(0, Duration.between(baseTime, LocalDateTime.now()).getSeconds());
|
long elapsedSeconds = baseTime == null ? 0 : Math.max(0, Duration.between(baseTime, LocalDateTime.now()).getSeconds());
|
||||||
if (current <= 0) {
|
if (current <= 0) {
|
||||||
percent = Math.max(percent, Math.min(35, 8 + (int) (elapsedSeconds / 6)));
|
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) {
|
} else if (current < total) {
|
||||||
percent = Math.max(percent, Math.min(92, percent + (int) (elapsedSeconds / 10)));
|
percent = Math.max(percent, Math.min(92, percent + (int) (elapsedSeconds / 10)));
|
||||||
}
|
}
|
||||||
@@ -2379,9 +2469,10 @@ public class AppearancePatentTaskService {
|
|||||||
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildParsedPayloadJson(String aiPrompt, List<AppearancePatentSourceFileDto> sourceFiles, List<String> headers, List<AppearancePatentParsedGroupVo> groups, List<AppearancePatentParsedRowVo> allRows) {
|
private String buildParsedPayloadJson(String aiPrompt, String apiKey, List<AppearancePatentSourceFileDto> sourceFiles, List<String> headers, List<AppearancePatentParsedGroupVo> groups, List<AppearancePatentParsedRowVo> allRows) {
|
||||||
AppearancePatentParsedPayloadDto payload = new AppearancePatentParsedPayloadDto();
|
AppearancePatentParsedPayloadDto payload = new AppearancePatentParsedPayloadDto();
|
||||||
payload.setAiPrompt(normalize(aiPrompt));
|
payload.setAiPrompt(normalize(aiPrompt));
|
||||||
|
payload.setApiKey(normalize(apiKey));
|
||||||
payload.setSourceFiles(sourceFiles == null ? List.of() : sourceFiles);
|
payload.setSourceFiles(sourceFiles == null ? List.of() : sourceFiles);
|
||||||
payload.setHeaders(headers == null ? List.of() : headers);
|
payload.setHeaders(headers == null ? List.of() : headers);
|
||||||
payload.setItems(List.of());
|
payload.setItems(List.of());
|
||||||
@@ -2390,14 +2481,16 @@ public class AppearancePatentTaskService {
|
|||||||
return writeJson(payload, "保存解析结果失败");
|
return writeJson(payload, "保存解析结果失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildTaskResultJson(String aiPrompt, List<AppearancePatentSourceFileDto> sourceFiles, String parsedPayloadPointer) {
|
private String buildTaskResultJson(String aiPrompt, String apiKey, List<AppearancePatentSourceFileDto> sourceFiles, String parsedPayloadPointer) {
|
||||||
Map<String, Object> payload = new LinkedHashMap<>();
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
payload.put("aiPrompt", normalize(aiPrompt));
|
payload.put("aiPrompt", normalize(aiPrompt));
|
||||||
|
payload.put("apiKey", normalize(apiKey));
|
||||||
payload.put("sourceFileKeys", sourceFiles == null ? List.of() : sourceFiles.stream()
|
payload.put("sourceFileKeys", sourceFiles == null ? List.of() : sourceFiles.stream()
|
||||||
.map(AppearancePatentSourceFileDto::getFileKey)
|
.map(AppearancePatentSourceFileDto::getFileKey)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.toList());
|
.toList());
|
||||||
payload.put("parsedPayloadRef", parsedPayloadPointer);
|
payload.put("parsedPayloadRef", parsedPayloadPointer);
|
||||||
|
payload.put("ownerInstanceId", currentInstanceId());
|
||||||
return writeJson(payload, "保存任务结果索引失败");
|
return writeJson(payload, "保存任务结果索引失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2661,7 +2754,8 @@ public class AppearancePatentTaskService {
|
|||||||
String chunkScopeHash,
|
String chunkScopeHash,
|
||||||
Integer chunkIndex,
|
Integer chunkIndex,
|
||||||
Integer batchIndex,
|
Integer batchIndex,
|
||||||
Integer batchTotal) {
|
Integer batchTotal,
|
||||||
|
String ownerInstanceId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ParsedWorkbook(int totalRows, int droppedRows, List<String> headers, List<AppearancePatentParsedRowVo> allRows) {
|
private record ParsedWorkbook(int totalRows, int droppedRows, List<String> headers, List<AppearancePatentParsedRowVo> allRows) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class SimilarAsinCozeClient {
|
|||||||
private final SimilarAsinProperties properties;
|
private final SimilarAsinProperties properties;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public List<SimilarAsinResultRowDto> inspect(List<SimilarAsinResultRowDto> rows, String prompt) {
|
public List<SimilarAsinResultRowDto> inspect(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||||
if (rows == null || rows.isEmpty()) {
|
if (rows == null || rows.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ public class SimilarAsinCozeClient {
|
|||||||
return rows.stream().map(this::copy).toList();
|
return rows.stream().map(this::copy).toList();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return inspectWithFallback(rows, prompt);
|
return inspectWithFallback(rows, prompt, apiKey);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
String failureMessage = failureMessage(ex);
|
String failureMessage = failureMessage(ex);
|
||||||
log.warn("[similar-asin] coze batch failed size={} err={}", rows.size(), failureMessage);
|
log.warn("[similar-asin] coze batch failed size={} err={}", rows.size(), failureMessage);
|
||||||
@@ -47,8 +47,8 @@ public class SimilarAsinCozeClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows, String prompt) throws Exception {
|
public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt));
|
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey));
|
||||||
ensureSuccess(submitRoot);
|
ensureSuccess(submitRoot);
|
||||||
return new CozeSubmitResponse(
|
return new CozeSubmitResponse(
|
||||||
extractExecuteId(submitRoot),
|
extractExecuteId(submitRoot),
|
||||||
@@ -83,12 +83,12 @@ public class SimilarAsinCozeClient {
|
|||||||
return rows.stream().map(this::copy).map(row -> markFailed(row, failureMessage)).toList();
|
return rows.stream().map(this::copy).map(row -> markFailed(row, failureMessage)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SimilarAsinResultRowDto> inspectWithFallback(List<SimilarAsinResultRowDto> rows, String prompt) {
|
private List<SimilarAsinResultRowDto> inspectWithFallback(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||||
try {
|
try {
|
||||||
if (rows.size() == 1) {
|
if (rows.size() == 1) {
|
||||||
return inspectSingleRowWithRetry(rows, prompt);
|
return inspectSingleRowWithRetry(rows, prompt, apiKey);
|
||||||
}
|
}
|
||||||
InspectAttempt attempt = inspectOnce(rows, prompt);
|
InspectAttempt attempt = inspectOnce(rows, prompt, apiKey);
|
||||||
if (attempt.resolvedCount() < rows.size()) {
|
if (attempt.resolvedCount() < rows.size()) {
|
||||||
throw new PartialCozeResultException(attempt.resolvedCount(), rows.size(), attempt.rawResultCount());
|
throw new PartialCozeResultException(attempt.resolvedCount(), rows.size(), attempt.rawResultCount());
|
||||||
}
|
}
|
||||||
@@ -99,17 +99,17 @@ public class SimilarAsinCozeClient {
|
|||||||
log.warn("[similar-asin] coze batch fallback split size={} left={} right={} err={}",
|
log.warn("[similar-asin] coze batch fallback split size={} left={} right={} err={}",
|
||||||
rows.size(), middle, rows.size() - middle, failureMessage(ex));
|
rows.size(), middle, rows.size() - middle, failureMessage(ex));
|
||||||
List<SimilarAsinResultRowDto> merged = new ArrayList<>(rows.size());
|
List<SimilarAsinResultRowDto> merged = new ArrayList<>(rows.size());
|
||||||
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(0, middle), prompt));
|
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(0, middle), prompt, apiKey));
|
||||||
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(middle, rows.size()), prompt));
|
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(middle, rows.size()), prompt, apiKey));
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
throw propagate(ex);
|
throw propagate(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SimilarAsinResultRowDto> inspectPartitionWithFailureFallback(List<SimilarAsinResultRowDto> rows, String prompt) {
|
private List<SimilarAsinResultRowDto> inspectPartitionWithFailureFallback(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||||
try {
|
try {
|
||||||
return inspectWithFallback(rows, prompt);
|
return inspectWithFallback(rows, prompt, apiKey);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
String failureMessage = failureMessage(ex);
|
String failureMessage = failureMessage(ex);
|
||||||
log.warn("[similar-asin] coze partition failed size={} err={}", rows.size(), failureMessage);
|
log.warn("[similar-asin] coze partition failed size={} err={}", rows.size(), failureMessage);
|
||||||
@@ -117,12 +117,12 @@ public class SimilarAsinCozeClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SimilarAsinResultRowDto> inspectSingleRowWithRetry(List<SimilarAsinResultRowDto> rows, String prompt) throws Exception {
|
private List<SimilarAsinResultRowDto> inspectSingleRowWithRetry(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||||
SimilarAsinResultRowDto row = rows.getFirst();
|
SimilarAsinResultRowDto row = rows.getFirst();
|
||||||
PartialCozeResultException lastFailure = null;
|
PartialCozeResultException lastFailure = null;
|
||||||
for (int attemptIndex = 1; attemptIndex <= 3; attemptIndex++) {
|
for (int attemptIndex = 1; attemptIndex <= 3; attemptIndex++) {
|
||||||
try {
|
try {
|
||||||
InspectAttempt attempt = inspectOnce(rows, prompt);
|
InspectAttempt attempt = inspectOnce(rows, prompt, apiKey);
|
||||||
if (attempt.resolvedCount() == rows.size()) {
|
if (attempt.resolvedCount() == rows.size()) {
|
||||||
return attempt.mergedRows();
|
return attempt.mergedRows();
|
||||||
}
|
}
|
||||||
@@ -153,8 +153,8 @@ public class SimilarAsinCozeClient {
|
|||||||
throw lastFailure == null ? new PartialCozeResultException(0, rows.size(), 0) : lastFailure;
|
throw lastFailure == null ? new PartialCozeResultException(0, rows.size(), 0) : lastFailure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private InspectAttempt inspectOnce(List<SimilarAsinResultRowDto> rows, String prompt) throws Exception {
|
private InspectAttempt inspectOnce(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||||
String raw = runWorkflowAsyncAndWait(rows, prompt);
|
String raw = runWorkflowAsyncAndWait(rows, prompt, apiKey);
|
||||||
List<CozeResult> results = parseResults(raw);
|
List<CozeResult> results = parseResults(raw);
|
||||||
if (rows.size() > 1 && !results.isEmpty() && results.stream().noneMatch(this::hasIdentity)) {
|
if (rows.size() > 1 && !results.isEmpty() && results.stream().noneMatch(this::hasIdentity)) {
|
||||||
throw new PartialCozeResultException(0, rows.size(), results.size());
|
throw new PartialCozeResultException(0, rows.size(), results.size());
|
||||||
@@ -163,8 +163,8 @@ public class SimilarAsinCozeClient {
|
|||||||
return new InspectAttempt(raw, merged, resolvedCount(merged), results.size());
|
return new InspectAttempt(raw, merged, resolvedCount(merged), results.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String runWorkflowAsyncAndWait(List<SimilarAsinResultRowDto> rows, String prompt) throws Exception {
|
private String runWorkflowAsyncAndWait(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt));
|
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey));
|
||||||
ensureSuccess(submitRoot);
|
ensureSuccess(submitRoot);
|
||||||
|
|
||||||
String immediateData = extractResultDataText(submitRoot);
|
String immediateData = extractResultDataText(submitRoot);
|
||||||
@@ -205,15 +205,15 @@ public class SimilarAsinCozeClient {
|
|||||||
throw new IllegalStateException("Coze async workflow poll timeout");
|
throw new IllegalStateException("Coze async workflow poll timeout");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String postWorkflow(List<SimilarAsinResultRowDto> rows, String prompt) {
|
private String postWorkflow(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||||
Map<String, Object> parameters = buildParameters(rows, prompt);
|
Map<String, Object> parameters = buildParameters(rows, prompt, apiKey);
|
||||||
Map<String, Object> body = new LinkedHashMap<>();
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
body.put("workflow_id", properties.getCozeWorkflowId());
|
body.put("workflow_id", properties.getCozeWorkflowId());
|
||||||
body.put("parameters", parameters);
|
body.put("parameters", parameters);
|
||||||
body.put("is_async", Boolean.TRUE);
|
body.put("is_async", Boolean.TRUE);
|
||||||
log.info("[similar-asin] coze request url={} body={}",
|
log.info("[similar-asin] coze request url={} body={}",
|
||||||
joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()),
|
joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()),
|
||||||
writeJson(body));
|
writeJson(maskCozeRequestBody(body)));
|
||||||
|
|
||||||
RestClient.RequestBodySpec request = restClient().post()
|
RestClient.RequestBodySpec request = restClient().post()
|
||||||
.uri(joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()))
|
.uri(joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()))
|
||||||
@@ -255,11 +255,12 @@ public class SimilarAsinCozeClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt) {
|
private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||||
List<String> groupKeys = rows.stream().map(row -> nonBlank(row.getGroupKey(), rowKey(row))).toList();
|
List<String> groupKeys = rows.stream().map(row -> nonBlank(row.getGroupKey(), rowKey(row))).toList();
|
||||||
List<String> rowIds = rows.stream().map(row -> nonBlank(row.getId(), "")).toList();
|
List<String> rowIds = rows.stream().map(row -> nonBlank(row.getId(), "")).toList();
|
||||||
List<String> asins = rows.stream().map(row -> nonBlank(row.getAsin(), "")).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> countries = rows.stream().map(row -> nonBlank(row.getCountry(), "")).toList();
|
||||||
|
List<String> prices = rows.stream().map(row -> nonBlank(row.getPrice(), "")).toList();
|
||||||
List<String> titles = rows.stream().map(row -> nonBlank(row.getTitle(), row.getAsin())).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();
|
List<String> urls = rows.stream().map(row -> nonBlank(row.getUrl(), "")).toList();
|
||||||
List<List<String>> urlLists = rows.stream().map(SimilarAsinResultRowDto::getUrls).toList();
|
List<List<String>> urlLists = rows.stream().map(SimilarAsinResultRowDto::getUrls).toList();
|
||||||
@@ -268,17 +269,48 @@ public class SimilarAsinCozeClient {
|
|||||||
parameters.put("title_list", titles);
|
parameters.put("title_list", titles);
|
||||||
parameters.put("url_list", urls);
|
parameters.put("url_list", urls);
|
||||||
parameters.put("url_lists", urlLists);
|
parameters.put("url_lists", urlLists);
|
||||||
parameters.put("items", buildItemObjects(rows, groupKeys, rowIds, asins, countries, titles, urlLists));
|
parameters.put("items", buildItemObjects(rows, groupKeys, rowIds, asins, countries, prices, titles, urls, urlLists));
|
||||||
parameters.put("prompt", prompt == null ? "" : prompt);
|
parameters.put("prompt", prompt == null ? "" : prompt);
|
||||||
|
if (apiKey != null && !apiKey.isBlank()) {
|
||||||
|
parameters.put("api_key", apiKey.trim());
|
||||||
|
}
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> maskCozeRequestBody(Map<String, Object> body) {
|
||||||
|
Map<String, Object> masked = new LinkedHashMap<>(body);
|
||||||
|
Object parametersObj = masked.get("parameters");
|
||||||
|
if (parametersObj instanceof Map<?, ?> parameters) {
|
||||||
|
Map<String, Object> maskedParameters = new LinkedHashMap<>((Map<String, Object>) parameters);
|
||||||
|
Object apiKey = maskedParameters.get("api_key");
|
||||||
|
if (apiKey instanceof String apiKeyText && !apiKeyText.isBlank()) {
|
||||||
|
maskedParameters.put("api_key", maskSecret(apiKeyText));
|
||||||
|
}
|
||||||
|
masked.put("parameters", maskedParameters);
|
||||||
|
}
|
||||||
|
return masked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String maskSecret(String secret) {
|
||||||
|
String normalized = secret == null ? "" : secret.trim();
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (normalized.length() <= 10) {
|
||||||
|
return "***";
|
||||||
|
}
|
||||||
|
return normalized.substring(0, 6) + "***" + normalized.substring(normalized.length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
private List<Map<String, Object>> buildItemObjects(List<SimilarAsinResultRowDto> rows,
|
private List<Map<String, Object>> buildItemObjects(List<SimilarAsinResultRowDto> rows,
|
||||||
List<String> groupKeys,
|
List<String> groupKeys,
|
||||||
List<String> rowIds,
|
List<String> rowIds,
|
||||||
List<String> asins,
|
List<String> asins,
|
||||||
List<String> countries,
|
List<String> countries,
|
||||||
|
List<String> prices,
|
||||||
List<String> titles,
|
List<String> titles,
|
||||||
|
List<String> urls,
|
||||||
List<List<String>> urlLists) {
|
List<List<String>> urlLists) {
|
||||||
List<Map<String, Object>> items = new ArrayList<>(rows.size());
|
List<Map<String, Object>> items = new ArrayList<>(rows.size());
|
||||||
for (int i = 0; i < rows.size(); i++) {
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
@@ -287,9 +319,11 @@ public class SimilarAsinCozeClient {
|
|||||||
item.put("row_id", rowIds.get(i));
|
item.put("row_id", rowIds.get(i));
|
||||||
item.put("asin", asins.get(i));
|
item.put("asin", asins.get(i));
|
||||||
item.put("country", countries.get(i));
|
item.put("country", countries.get(i));
|
||||||
|
item.put("price", prices.get(i));
|
||||||
item.put("title", titles.get(i));
|
item.put("title", titles.get(i));
|
||||||
item.put("url", urlLists.get(i));
|
item.put("url", urls.get(i));
|
||||||
item.put("urls", urlLists.get(i));
|
item.put("urls", urlLists.get(i));
|
||||||
|
item.put("target_urls", urlLists.get(i));
|
||||||
items.add(item);
|
items.add(item);
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
@@ -319,7 +353,16 @@ public class SimilarAsinCozeClient {
|
|||||||
text(firstNonNull(
|
text(firstNonNull(
|
||||||
firstNonNull(node.get("country"), node.get("site")),
|
firstNonNull(node.get("country"), node.get("site")),
|
||||||
firstNonNull(itemNode.get("country"), itemNode.get("site")))),
|
firstNonNull(itemNode.get("country"), itemNode.get("site")))),
|
||||||
|
text(firstNonNull(
|
||||||
|
firstNonNull(node.get("price"), node.get("价格")),
|
||||||
|
firstNonNull(itemNode.get("price"), itemNode.get("价格")))),
|
||||||
text(firstNonNull(node.get("title"), firstNonNull(itemNode.get("title"), itemNode.get("title_risk")))),
|
text(firstNonNull(node.get("title"), firstNonNull(itemNode.get("title"), itemNode.get("title_risk")))),
|
||||||
|
text(firstNonNull(
|
||||||
|
firstNonNull(node.get("is_stock"), firstNonNull(node.get("isStock"), firstNonNull(node.get("stock"), node.get("是否有货")))),
|
||||||
|
firstNonNull(itemNode.get("is_stock"), firstNonNull(itemNode.get("isStock"), firstNonNull(itemNode.get("stock"), itemNode.get("是否有货")))))),
|
||||||
|
text(firstNonNull(
|
||||||
|
firstNonNull(node.get("similarity"), firstNonNull(node.get("similarity_rate"), node.get("相似度"))),
|
||||||
|
firstNonNull(itemNode.get("similarity"), firstNonNull(itemNode.get("similarity_rate"), itemNode.get("相似度"))))),
|
||||||
text(firstNonNull(node.get("appearance"),
|
text(firstNonNull(node.get("appearance"),
|
||||||
firstNonNull(node.get("appearance_risk"),
|
firstNonNull(node.get("appearance_risk"),
|
||||||
firstNonNull(itemNode.get("appearance"), itemNode.get("appearance_risk"))))),
|
firstNonNull(itemNode.get("appearance"), itemNode.get("appearance_risk"))))),
|
||||||
@@ -396,6 +439,9 @@ public class SimilarAsinCozeClient {
|
|||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = resultByCompositeKey.get(rowKey(row.getId(), row.getAsin(), row.getCountry()));
|
result = resultByCompositeKey.get(rowKey(row.getId(), row.getAsin(), row.getCountry()));
|
||||||
}
|
}
|
||||||
|
if (result == null && i < results.size() && isIndexedResultForRow(results.get(i), row)) {
|
||||||
|
result = results.get(i);
|
||||||
|
}
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = resultByAsinCountry.get(asinCountryKey(row.getAsin(), row.getCountry()));
|
result = resultByAsinCountry.get(asinCountryKey(row.getAsin(), row.getCountry()));
|
||||||
}
|
}
|
||||||
@@ -431,11 +477,25 @@ public class SimilarAsinCozeClient {
|
|||||||
|| !normalize(result.asin()).isBlank();
|
|| !normalize(result.asin()).isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isIndexedResultForRow(CozeResult result, SimilarAsinResultRowDto row) {
|
||||||
|
if (result == null || row == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!normalize(result.groupKey()).isBlank() || !normalize(result.rowId()).isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String resultAsin = normalize(result.asin()).toUpperCase(Locale.ROOT);
|
||||||
|
return resultAsin.isBlank() || resultAsin.equals(normalize(row.getAsin()).toUpperCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
private void applyResult(SimilarAsinResultRowDto row, CozeResult result) {
|
private void applyResult(SimilarAsinResultRowDto row, CozeResult result) {
|
||||||
if (row == null || result == null) {
|
if (row == null || result == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
row.setTitleRisk(result.title());
|
row.setTitleRisk(result.title());
|
||||||
|
row.setPrice(nonBlank(result.price(), row.getPrice()));
|
||||||
|
row.setIsStock(result.isStock());
|
||||||
|
row.setSimilarity(result.similarity());
|
||||||
row.setAppearanceRisk(result.appearance());
|
row.setAppearanceRisk(result.appearance());
|
||||||
row.setPatentRisk(result.patent());
|
row.setPatentRisk(result.patent());
|
||||||
row.setConclusion(result.result());
|
row.setConclusion(result.result());
|
||||||
@@ -460,6 +520,7 @@ public class SimilarAsinCozeClient {
|
|||||||
row.setId(source.getId());
|
row.setId(source.getId());
|
||||||
row.setAsin(source.getAsin());
|
row.setAsin(source.getAsin());
|
||||||
row.setCountry(source.getCountry());
|
row.setCountry(source.getCountry());
|
||||||
|
row.setPrice(source.getPrice());
|
||||||
row.setUrls(source.getUrls());
|
row.setUrls(source.getUrls());
|
||||||
row.setTitle(source.getTitle());
|
row.setTitle(source.getTitle());
|
||||||
row.setError(source.getError());
|
row.setError(source.getError());
|
||||||
@@ -468,6 +529,8 @@ public class SimilarAsinCozeClient {
|
|||||||
row.setAppearanceRisk(source.getAppearanceRisk());
|
row.setAppearanceRisk(source.getAppearanceRisk());
|
||||||
row.setPatentRisk(source.getPatentRisk());
|
row.setPatentRisk(source.getPatentRisk());
|
||||||
row.setConclusion(source.getConclusion());
|
row.setConclusion(source.getConclusion());
|
||||||
|
row.setIsStock(source.getIsStock());
|
||||||
|
row.setSimilarity(source.getSimilarity());
|
||||||
row.setTitleReason(source.getTitleReason());
|
row.setTitleReason(source.getTitleReason());
|
||||||
row.setAppearanceReason(source.getAppearanceReason());
|
row.setAppearanceReason(source.getAppearanceReason());
|
||||||
row.setPatentReason(source.getPatentReason());
|
row.setPatentReason(source.getPatentReason());
|
||||||
@@ -529,7 +592,9 @@ public class SimilarAsinCozeClient {
|
|||||||
return !normalize(row.getTitleRisk()).isBlank()
|
return !normalize(row.getTitleRisk()).isBlank()
|
||||||
|| !normalize(row.getAppearanceRisk()).isBlank()
|
|| !normalize(row.getAppearanceRisk()).isBlank()
|
||||||
|| !normalize(row.getPatentRisk()).isBlank()
|
|| !normalize(row.getPatentRisk()).isBlank()
|
||||||
|| !normalize(row.getConclusion()).isBlank();
|
|| !normalize(row.getConclusion()).isBlank()
|
||||||
|
|| !normalize(row.getIsStock()).isBlank()
|
||||||
|
|| !normalize(row.getSimilarity()).isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureSuccess(JsonNode root) {
|
private void ensureSuccess(JsonNode root) {
|
||||||
@@ -656,6 +721,13 @@ public class SimilarAsinCozeClient {
|
|||||||
|| item.has("patent_risk")
|
|| item.has("patent_risk")
|
||||||
|| item.has("result")
|
|| item.has("result")
|
||||||
|| item.has("conclusion")
|
|| item.has("conclusion")
|
||||||
|
|| item.has("is_stock")
|
||||||
|
|| item.has("isStock")
|
||||||
|
|| item.has("stock")
|
||||||
|
|| item.has("是否有货")
|
||||||
|
|| item.has("similarity")
|
||||||
|
|| item.has("similarity_rate")
|
||||||
|
|| item.has("相似度")
|
||||||
|| item.has("title_reason")
|
|| item.has("title_reason")
|
||||||
|| item.has("titleReason")
|
|| item.has("titleReason")
|
||||||
|| item.has("appearance_reason")
|
|| item.has("appearance_reason")
|
||||||
@@ -909,7 +981,10 @@ public class SimilarAsinCozeClient {
|
|||||||
String rowId,
|
String rowId,
|
||||||
String asin,
|
String asin,
|
||||||
String country,
|
String country,
|
||||||
|
String price,
|
||||||
String title,
|
String title,
|
||||||
|
String isStock,
|
||||||
|
String similarity,
|
||||||
String appearance,
|
String appearance,
|
||||||
String patent,
|
String patent,
|
||||||
String result,
|
String result,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.nanri.aiimage.modules.similarasin.model.dto;
|
|||||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -25,4 +26,10 @@ public class SimilarAsinParseRequest {
|
|||||||
@JsonAlias({"aiPrompt", "prompt"})
|
@JsonAlias({"aiPrompt", "prompt"})
|
||||||
@Schema(description = "AI 提示词。非必填;为空时前端会使用默认提示词。后端会保存该提示词,并在调用 Coze workflow 时作为 prompt 参数传入。", example = "请排查这些亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。")
|
@Schema(description = "AI 提示词。非必填;为空时前端会使用默认提示词。后端会保存该提示词,并在调用 Coze workflow 时作为 prompt 参数传入。", example = "请排查这些亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。")
|
||||||
private String aiPrompt;
|
private String aiPrompt;
|
||||||
|
|
||||||
|
@JsonProperty("api_key")
|
||||||
|
@JsonAlias({"apiKey"})
|
||||||
|
@Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥。")
|
||||||
|
@NotBlank(message = "密钥不能为空")
|
||||||
|
private String apiKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ public class SimilarAsinParsedPayloadDto {
|
|||||||
@Schema(description = "AI 提示词")
|
@Schema(description = "AI 提示词")
|
||||||
private String aiPrompt;
|
private String aiPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥")
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
@Schema(description = "本次解析的源文件列表")
|
@Schema(description = "本次解析的源文件列表")
|
||||||
private List<SimilarAsinSourceFileDto> sourceFiles = new ArrayList<>();
|
private List<SimilarAsinSourceFileDto> sourceFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ public class SimilarAsinResultRowDto {
|
|||||||
@Schema(description = "站点或国家。来自 Excel 的国家列,例如英国、德国、法国。", example = "英国")
|
@Schema(description = "站点或国家。来自 Excel 的国家列,例如英国、德国、法国。", example = "英国")
|
||||||
private String country;
|
private String country;
|
||||||
|
|
||||||
|
@JsonAlias({"price", "价格"})
|
||||||
|
@Schema(description = "商品价格。来自 Excel 解析或 Python 回传。", example = "12.29")
|
||||||
|
private String price;
|
||||||
|
|
||||||
@Schema(description = "兼容旧链路的首个图片 URL。新链路允许 url 传字符串或数组;数组会同步写入 urls。", example = "https://webstatic.aiproxy.vip/output/20260425/103322/demo.jpg")
|
@Schema(description = "兼容旧链路的首个图片 URL。新链路允许 url 传字符串或数组;数组会同步写入 urls。", example = "https://webstatic.aiproxy.vip/output/20260425/103322/demo.jpg")
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
@@ -49,23 +53,31 @@ public class SimilarAsinResultRowDto {
|
|||||||
@JsonAlias({"productTitle", "product_title", "itemTitle", "item_title", "商品标题", "商品名称", "标题"})
|
@JsonAlias({"productTitle", "product_title", "itemTitle", "item_title", "商品标题", "商品名称", "标题"})
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
|
@JsonAlias({"is_stock", "isStock", "stock", "是否有货"})
|
||||||
|
@Schema(description = "Coze 返回的是否有货结果。", example = "有货", accessMode = Schema.AccessMode.READ_ONLY)
|
||||||
|
private String isStock;
|
||||||
|
|
||||||
|
@JsonAlias({"similarity", "similarity_rate", "similarityRate", "相似度"})
|
||||||
|
@Schema(description = "Coze 返回的相似度结果。", example = "80%", accessMode = Schema.AccessMode.READ_ONLY)
|
||||||
|
private String similarity;
|
||||||
|
|
||||||
@Schema(description = "单行错误信息。通常用于记录 Python 单行处理异常;Coze 失败时后端会尽量保留原始行,不强行写入风险结果。", example = "图片地址为空")
|
@Schema(description = "单行错误信息。通常用于记录 Python 单行处理异常;Coze 失败时后端会尽量保留原始行,不强行写入风险结果。", example = "图片地址为空")
|
||||||
private String error;
|
private String error;
|
||||||
|
|
||||||
@Schema(description = "单行完成标记。当前主要使用请求体顶层 done 控制任务收尾,该字段仅作兼容。", example = "true")
|
@Schema(description = "单行完成标记。当前主要使用请求体顶层 done 控制任务收尾,该字段仅作兼容。", example = "true")
|
||||||
private Boolean done;
|
private Boolean done;
|
||||||
|
|
||||||
@Schema(description = "Java 调用 Coze 后生成的标题维度检测结果,对应最终 xlsx 的“标题维度(商标)”列。Python 回传请求中不要传该字段;即使传入,后端也会以 Java/Coze 处理结果为准。", example = "标题未发现明显商标侵权风险。", accessMode = Schema.AccessMode.READ_ONLY)
|
@Schema(description = "兼容旧版 Coze 返回中的标题维度结果字段;当前相似 ASIN 结果文件不再输出该列。", example = "标题未发现明显商标侵权风险。", accessMode = Schema.AccessMode.READ_ONLY)
|
||||||
private String titleRisk;
|
private String titleRisk;
|
||||||
|
|
||||||
@Schema(description = "Java 调用 Coze 后生成的外观维度检测结果,对应最终 xlsx 的“外观维度(外观设计专利)”列。Python 回传请求中不要传该字段。", example = "未发现明显外观设计专利侵权风险。", accessMode = Schema.AccessMode.READ_ONLY)
|
@Schema(description = "兼容旧版 Coze 返回中的外观维度结果字段;当前相似 ASIN 结果文件不再输出该列。", example = "未发现明显外观设计专利侵权风险。", accessMode = Schema.AccessMode.READ_ONLY)
|
||||||
private String appearanceRisk;
|
private String appearanceRisk;
|
||||||
|
|
||||||
@JsonAlias({"patent ", "patent"})
|
@JsonAlias({"patent ", "patent"})
|
||||||
@Schema(description = "Java 调用 Coze 后生成的专利维度检测结果,对应最终 xlsx 的“专利维度(发明/实用新型专利)”列。兼容 Coze 返回字段 patent 和 patent 后带空格的情况;Python 回传请求中不要传该字段。", example = "未发现明显发明或实用新型专利侵权风险。", accessMode = Schema.AccessMode.READ_ONLY)
|
@Schema(description = "兼容旧版 Coze 返回中的专利维度结果字段;当前相似 ASIN 结果文件不再输出该列。", example = "未发现明显发明或实用新型专利侵权风险。", accessMode = Schema.AccessMode.READ_ONLY)
|
||||||
private String patentRisk;
|
private String patentRisk;
|
||||||
|
|
||||||
@Schema(description = "Java 调用 Coze 后生成的最终结论,对应最终 xlsx 的“结论”列。Python 回传请求中不要传该字段。", example = "未发现明显侵权风险", accessMode = Schema.AccessMode.READ_ONLY)
|
@Schema(description = "兼容旧版 Coze 返回中的结论字段;当前相似 ASIN 结果文件不再输出该列。", example = "未发现明显侵权风险", accessMode = Schema.AccessMode.READ_ONLY)
|
||||||
private String conclusion;
|
private String conclusion;
|
||||||
|
|
||||||
@JsonAlias({"title_reason", "titleReason"})
|
@JsonAlias({"title_reason", "titleReason"})
|
||||||
@@ -118,6 +130,7 @@ public class SimilarAsinResultRowDto {
|
|||||||
"mainImages", "main_images", "mainImageUrls", "main_image_urls",
|
"mainImages", "main_images", "mainImageUrls", "main_image_urls",
|
||||||
"productImages", "product_images", "productImageUrls", "product_image_urls",
|
"productImages", "product_images", "productImageUrls", "product_image_urls",
|
||||||
"images", "imgs", "pics", "pictures", "links", "imageLinks", "image_links",
|
"images", "imgs", "pics", "pictures", "links", "imageLinks", "image_links",
|
||||||
|
"target_urls", "targetUrls", "图片链接数组",
|
||||||
"图片链接", "商品图片", "商品主图", "主图", "主图链接"
|
"图片链接", "商品图片", "商品主图", "主图", "主图链接"
|
||||||
})
|
})
|
||||||
public void setUrls(Object urls) {
|
public void setUrls(Object urls) {
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public class SimilarAsinParsedRowVo {
|
|||||||
@Schema(description = "国家或站点。", example = "英国")
|
@Schema(description = "国家或站点。", example = "英国")
|
||||||
private String country;
|
private String country;
|
||||||
|
|
||||||
|
@Schema(description = "商品价格。", example = "12.29")
|
||||||
|
private String price;
|
||||||
|
|
||||||
@Schema(description = "商品图片 URL 或商品 URL,供 Coze 检测使用。", example = "https://webstatic.aiproxy.vip/output/demo.jpg")
|
@Schema(description = "商品图片 URL 或商品 URL,供 Coze 检测使用。", example = "https://webstatic.aiproxy.vip/output/demo.jpg")
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
||||||
|
import com.nanri.aiimage.config.InstanceMetadata;
|
||||||
import com.nanri.aiimage.config.SimilarAsinProperties;
|
import com.nanri.aiimage.config.SimilarAsinProperties;
|
||||||
import com.nanri.aiimage.config.StorageProperties;
|
import com.nanri.aiimage.config.StorageProperties;
|
||||||
import com.nanri.aiimage.modules.similarasin.client.SimilarAsinCozeClient;
|
import com.nanri.aiimage.modules.similarasin.client.SimilarAsinCozeClient;
|
||||||
@@ -99,15 +100,15 @@ public class SimilarAsinTaskService {
|
|||||||
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||||
private static final int RESULT_ROWS_READ_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 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 List<String> RESULT_HEADERS = List.of(
|
private static final List<String> RESULT_HEADERS = List.of(
|
||||||
"id",
|
"id",
|
||||||
"asin",
|
"asin",
|
||||||
"国家",
|
"国家",
|
||||||
"价格",
|
"价格",
|
||||||
"标题维度(商标)",
|
"是否有货",
|
||||||
"外观维度(外观设计专利)",
|
"相似度"
|
||||||
"专利维度(发明/实用新型专利)",
|
|
||||||
"结论"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private final LocalFileStorageService localFileStorageService;
|
private final LocalFileStorageService localFileStorageService;
|
||||||
@@ -127,6 +128,7 @@ public class SimilarAsinTaskService {
|
|||||||
private final TransientPayloadStorageService transientPayloadStorageService;
|
private final TransientPayloadStorageService transientPayloadStorageService;
|
||||||
private final PlatformTransactionManager transactionManager;
|
private final PlatformTransactionManager transactionManager;
|
||||||
private final DistributedJobLockService distributedJobLockService;
|
private final DistributedJobLockService distributedJobLockService;
|
||||||
|
private final InstanceMetadata instanceMetadata;
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("cozeTaskExecutor")
|
@Qualifier("cozeTaskExecutor")
|
||||||
private TaskExecutor cozeTaskExecutor;
|
private TaskExecutor cozeTaskExecutor;
|
||||||
@@ -195,11 +197,11 @@ public class SimilarAsinTaskService {
|
|||||||
|
|
||||||
String aggregateScopeKey = buildAggregateScopeKey(sourceFiles);
|
String aggregateScopeKey = buildAggregateScopeKey(sourceFiles);
|
||||||
String sourceScopeHash = DigestUtil.sha256Hex(aggregateScopeKey);
|
String sourceScopeHash = DigestUtil.sha256Hex(aggregateScopeKey);
|
||||||
String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), sourceFiles, mergedHeaders, groups, allRows);
|
String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), request.getApiKey(), sourceFiles, mergedHeaders, groups, allRows);
|
||||||
long payloadBuiltAt = System.nanoTime();
|
long payloadBuiltAt = System.nanoTime();
|
||||||
String parsedPayloadPointer = storeParsedPayload(task.getId(), sourceScopeHash, parsedPayload);
|
String parsedPayloadPointer = storeParsedPayload(task.getId(), sourceScopeHash, parsedPayload);
|
||||||
long payloadStoredAt = System.nanoTime();
|
long payloadStoredAt = System.nanoTime();
|
||||||
task.setResultJson(buildTaskResultJson(request.getAiPrompt(), sourceFiles, parsedPayloadPointer));
|
task.setResultJson(buildTaskResultJson(request.getAiPrompt(), request.getApiKey(), sourceFiles, parsedPayloadPointer));
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
|
|
||||||
@@ -386,6 +388,16 @@ public class SimilarAsinTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void submitResult(Long taskId, SimilarAsinSubmitResultRequest request) {
|
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");
|
||||||
|
}
|
||||||
|
try (lockHandle) {
|
||||||
|
submitResultLocked(taskId, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitResultLocked(Long taskId, SimilarAsinSubmitResultRequest request) {
|
||||||
if (transactionManager != null) {
|
if (transactionManager != null) {
|
||||||
SubmitContext context = inNewTransaction(() -> persistSubmittedChunk(taskId, request));
|
SubmitContext context = inNewTransaction(() -> persistSubmittedChunk(taskId, request));
|
||||||
inNewTransaction(() -> {
|
inNewTransaction(() -> {
|
||||||
@@ -402,6 +414,7 @@ public class SimilarAsinTaskService {
|
|||||||
throw new BusinessException("任务不是运行中状态");
|
throw new BusinessException("任务不是运行中状态");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureTaskOwnedByCurrentInstance(task, "submit result");
|
||||||
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
|
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
|
||||||
int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal();
|
int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal();
|
||||||
String scopeKey = firstNonBlank(request.getSubmissionId(), "task:" + taskId);
|
String scopeKey = firstNonBlank(request.getSubmissionId(), "task:" + taskId);
|
||||||
@@ -530,6 +543,9 @@ public class SimilarAsinTaskService {
|
|||||||
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
||||||
.last("limit 50"));
|
.last("limit 50"));
|
||||||
for (FileTaskEntity task : tasks) {
|
for (FileTaskEntity task : tasks) {
|
||||||
|
if (!isOwnerCurrent(ownerFromTask(task))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (isJavaSideProcessing(task.getId())) {
|
if (isJavaSideProcessing(task.getId())) {
|
||||||
touchJavaSideTaskActivity(task.getId());
|
touchJavaSideTaskActivity(task.getId());
|
||||||
continue;
|
continue;
|
||||||
@@ -559,6 +575,9 @@ public class SimilarAsinTaskService {
|
|||||||
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
||||||
.last("limit 50"));
|
.last("limit 50"));
|
||||||
for (FileTaskEntity task : tasks) {
|
for (FileTaskEntity task : tasks) {
|
||||||
|
if (!isOwnerCurrent(ownerFromTask(task))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (isJavaSideProcessing(task.getId())) {
|
if (isJavaSideProcessing(task.getId())) {
|
||||||
touchJavaSideTaskActivity(task.getId());
|
touchJavaSideTaskActivity(task.getId());
|
||||||
continue;
|
continue;
|
||||||
@@ -586,6 +605,7 @@ public class SimilarAsinTaskService {
|
|||||||
throw new BusinessException("任务不是运行中状态");
|
throw new BusinessException("任务不是运行中状态");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureTaskOwnedByCurrentInstance(task, "submit result");
|
||||||
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
|
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
|
||||||
int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal();
|
int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal();
|
||||||
boolean done = Boolean.TRUE.equals(request.getDone());
|
boolean done = Boolean.TRUE.equals(request.getDone());
|
||||||
@@ -738,10 +758,11 @@ public class SimilarAsinTaskService {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
String prompt = readAiPrompt(task);
|
String prompt = readAiPrompt(task);
|
||||||
|
String apiKey = readApiKey(task);
|
||||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||||
List<SimilarAsinResultRowDto> result = new ArrayList<>();
|
List<SimilarAsinResultRowDto> result = new ArrayList<>();
|
||||||
for (int i = 0; i < items.size(); i += batchSize) {
|
for (int i = 0; i < items.size(); i += batchSize) {
|
||||||
result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt));
|
result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt, apiKey));
|
||||||
if (progressHook != null) {
|
if (progressHook != null) {
|
||||||
progressHook.run();
|
progressHook.run();
|
||||||
}
|
}
|
||||||
@@ -818,17 +839,9 @@ public class SimilarAsinTaskService {
|
|||||||
List<SimilarAsinResultRowDto> result = new ArrayList<>();
|
List<SimilarAsinResultRowDto> result = new ArrayList<>();
|
||||||
Set<String> emittedKeys = new LinkedHashSet<>();
|
Set<String> emittedKeys = new LinkedHashSet<>();
|
||||||
for (SimilarAsinResultRowDto representative : representatives) {
|
for (SimilarAsinResultRowDto representative : representatives) {
|
||||||
List<SimilarAsinParsedRowVo> siblings = resolveSiblingRows(representative, allRowsByBaseId);
|
String key = firstNonBlank(normalize(representative.getRowToken()), rowKey(representative));
|
||||||
if (siblings == null || siblings.isEmpty()) {
|
if (emittedKeys.add(key)) {
|
||||||
result.add(representative);
|
result.add(representative);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (SimilarAsinParsedRowVo sibling : siblings) {
|
|
||||||
String key = rowKey(sibling);
|
|
||||||
if (!emittedKeys.add(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
result.add(copyResultToSibling(representative, sibling));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -877,32 +890,13 @@ public class SimilarAsinTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<SimilarAsinResultRowDto> pickGroupRepresentativesForCoze(java.util.Collection<SimilarAsinResultRowDto> rows) {
|
private List<SimilarAsinResultRowDto> pickGroupRepresentativesForCoze(java.util.Collection<SimilarAsinResultRowDto> rows) {
|
||||||
Map<String, List<SimilarAsinResultRowDto>> groupedRows = new LinkedHashMap<>();
|
|
||||||
if (rows == null) {
|
if (rows == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
for (SimilarAsinResultRowDto row : rows) {
|
|
||||||
if (row == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String key = firstNonBlank(normalize(row.getGroupKey()), rowKey(row));
|
|
||||||
groupedRows.computeIfAbsent(key, ignored -> new ArrayList<>()).add(row);
|
|
||||||
}
|
|
||||||
List<SimilarAsinResultRowDto> representatives = new ArrayList<>();
|
List<SimilarAsinResultRowDto> representatives = new ArrayList<>();
|
||||||
for (List<SimilarAsinResultRowDto> siblings : groupedRows.values()) {
|
for (SimilarAsinResultRowDto row : rows) {
|
||||||
boolean alreadyResolved = siblings.stream().anyMatch(this::hasResolvedCozeFields);
|
if (row != null && !hasResolvedCozeFields(row) && shouldInspectRow(row)) {
|
||||||
if (alreadyResolved) {
|
representatives.add(row);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
SimilarAsinResultRowDto candidate = null;
|
|
||||||
for (SimilarAsinResultRowDto sibling : siblings) {
|
|
||||||
if (shouldInspectRow(sibling)) {
|
|
||||||
candidate = sibling;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (candidate != null) {
|
|
||||||
representatives.add(candidate);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return representatives;
|
return representatives;
|
||||||
@@ -1004,6 +998,7 @@ public class SimilarAsinTaskService {
|
|||||||
row.setId(sibling.getDisplayId());
|
row.setId(sibling.getDisplayId());
|
||||||
row.setAsin(sibling.getAsin());
|
row.setAsin(sibling.getAsin());
|
||||||
row.setCountry(sibling.getCountry());
|
row.setCountry(sibling.getCountry());
|
||||||
|
row.setPrice(firstNonBlank(representative.getPrice(), sibling.getPrice()));
|
||||||
if (representative.hasImageUrl()) {
|
if (representative.hasImageUrl()) {
|
||||||
row.setUrls(representative.getUrls());
|
row.setUrls(representative.getUrls());
|
||||||
} else {
|
} else {
|
||||||
@@ -1015,6 +1010,8 @@ public class SimilarAsinTaskService {
|
|||||||
row.setAppearanceRisk(representative.getAppearanceRisk());
|
row.setAppearanceRisk(representative.getAppearanceRisk());
|
||||||
row.setPatentRisk(representative.getPatentRisk());
|
row.setPatentRisk(representative.getPatentRisk());
|
||||||
row.setConclusion(representative.getConclusion());
|
row.setConclusion(representative.getConclusion());
|
||||||
|
row.setIsStock(representative.getIsStock());
|
||||||
|
row.setSimilarity(representative.getSimilarity());
|
||||||
row.setTitleReason(representative.getTitleReason());
|
row.setTitleReason(representative.getTitleReason());
|
||||||
row.setAppearanceReason(representative.getAppearanceReason());
|
row.setAppearanceReason(representative.getAppearanceReason());
|
||||||
row.setPatentReason(representative.getPatentReason());
|
row.setPatentReason(representative.getPatentReason());
|
||||||
@@ -1030,6 +1027,14 @@ public class SimilarAsinTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String readApiKey(FileTaskEntity task) {
|
||||||
|
try {
|
||||||
|
return normalize(readParsedPayload(task).getApiKey());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void finalizeTask(FileTaskEntity task, String error, int rowCount, boolean assembleWorkbook) {
|
private void finalizeTask(FileTaskEntity task, String error, int rowCount, boolean assembleWorkbook) {
|
||||||
String finalError = error;
|
String finalError = error;
|
||||||
FileResultEntity result = null;
|
FileResultEntity result = null;
|
||||||
@@ -1068,7 +1073,7 @@ public class SimilarAsinTaskService {
|
|||||||
}
|
}
|
||||||
fileResultMapper.updateById(result);
|
fileResultMapper.updateById(result);
|
||||||
if (!failed && assembleWorkbook) {
|
if (!failed && assembleWorkbook) {
|
||||||
taskFileJobService.enqueueAssembleResult(task.getId(), MODULE_TYPE, result.getId(), "task:" + task.getId());
|
taskFileJobService.enqueueAssembleResult(task.getId(), MODULE_TYPE, result.getId(), buildTaskOwnerScopeKey(task));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
taskCacheService.deleteTaskCache(task.getId());
|
taskCacheService.deleteTaskCache(task.getId());
|
||||||
@@ -1082,6 +1087,11 @@ public class SimilarAsinTaskService {
|
|||||||
if (job == null || job.getTaskId() == null || job.getResultId() == null) {
|
if (job == null || job.getTaskId() == null || job.getResultId() == null) {
|
||||||
throw new BusinessException("result file job arguments are incomplete");
|
throw new BusinessException("result file job arguments are incomplete");
|
||||||
}
|
}
|
||||||
|
if (!isJobOwnedByCurrentInstance(job)) {
|
||||||
|
log.info("[similar-asin] skip result file job because owner is another instance jobId={} taskId={} owner={} current={}",
|
||||||
|
job.getId(), job.getTaskId(), ownerFromScopeKey(job.getScopeKey()), currentInstanceId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
FileTaskEntity task = fileTaskMapper.selectById(job.getTaskId());
|
FileTaskEntity task = fileTaskMapper.selectById(job.getTaskId());
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
throw new BusinessException("task not found");
|
throw new BusinessException("task not found");
|
||||||
@@ -1128,7 +1138,7 @@ public class SimilarAsinTaskService {
|
|||||||
}
|
}
|
||||||
log.info("[similar-asin] coze poll picked pending states count={}", states.size());
|
log.info("[similar-asin] coze poll picked pending states count={}", states.size());
|
||||||
for (TaskScopeStateEntity state : states) {
|
for (TaskScopeStateEntity state : states) {
|
||||||
if (state == null || state.getId() == null) {
|
if (state == null || state.getId() == null || !isCozeStateOwnedByCurrentInstance(state)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
cozeTaskExecutor.execute(() -> pollPendingCozeState(state.getId()));
|
cozeTaskExecutor.execute(() -> pollPendingCozeState(state.getId()));
|
||||||
@@ -1150,6 +1160,7 @@ public class SimilarAsinTaskService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String prompt = readAiPrompt(task);
|
String prompt = readAiPrompt(task);
|
||||||
|
String apiKey = readApiKey(task);
|
||||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||||
boolean pending = false;
|
boolean pending = false;
|
||||||
for (TaskChunkEntity chunk : chunks) {
|
for (TaskChunkEntity chunk : chunks) {
|
||||||
@@ -1166,7 +1177,7 @@ public class SimilarAsinTaskService {
|
|||||||
for (int i = 0; i < unresolvedRows.size(); i += batchSize) {
|
for (int i = 0; i < unresolvedRows.size(); i += batchSize) {
|
||||||
List<SimilarAsinResultRowDto> batchRows =
|
List<SimilarAsinResultRowDto> batchRows =
|
||||||
unresolvedRows.subList(i, Math.min(i + batchSize, unresolvedRows.size()));
|
unresolvedRows.subList(i, Math.min(i + batchSize, unresolvedRows.size()));
|
||||||
pending |= submitCozeBatch(task, result, job, chunk, batchRows, batchIndex, batchTotal, prompt, allRowsByBaseId);
|
pending |= submitCozeBatch(task, result, job, chunk, batchRows, batchIndex, batchTotal, prompt, apiKey, allRowsByBaseId);
|
||||||
batchIndex++;
|
batchIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1181,6 +1192,7 @@ public class SimilarAsinTaskService {
|
|||||||
int batchIndex,
|
int batchIndex,
|
||||||
int batchTotal,
|
int batchTotal,
|
||||||
String prompt,
|
String prompt,
|
||||||
|
String apiKey,
|
||||||
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId) {
|
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId) {
|
||||||
if (batchRows == null || batchRows.isEmpty()) {
|
if (batchRows == null || batchRows.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1197,7 +1209,7 @@ public class SimilarAsinTaskService {
|
|||||||
|| COZE_STATUS_RUNNING.equals(existing.getCozeStatus());
|
|| COZE_STATUS_RUNNING.equals(existing.getCozeStatus());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
SimilarAsinCozeClient.CozeSubmitResponse submit = cozeClient.submitWorkflow(batchRows, prompt);
|
SimilarAsinCozeClient.CozeSubmitResponse submit = cozeClient.submitWorkflow(batchRows, prompt, apiKey);
|
||||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||||
List<SimilarAsinResultRowDto> cozeRows = cozeClient.mergeRowsFromDataText(batchRows, submit.immediateData());
|
List<SimilarAsinResultRowDto> cozeRows = cozeClient.mergeRowsFromDataText(batchRows, submit.immediateData());
|
||||||
mergeCozeRowsIntoChunk(task, chunk.getScopeHash(), chunk.getChunkIndex(), cozeRows, allRowsByBaseId);
|
mergeCozeRowsIntoChunk(task, chunk.getScopeHash(), chunk.getChunkIndex(), cozeRows, allRowsByBaseId);
|
||||||
@@ -1246,7 +1258,8 @@ public class SimilarAsinTaskService {
|
|||||||
chunk.getScopeHash(),
|
chunk.getScopeHash(),
|
||||||
chunk.getChunkIndex(),
|
chunk.getChunkIndex(),
|
||||||
batchIndex,
|
batchIndex,
|
||||||
batchTotal
|
batchTotal,
|
||||||
|
currentInstanceId()
|
||||||
);
|
);
|
||||||
String batchPayload = writeJson(batchRows, "serialize coze batch payload failed");
|
String batchPayload = writeJson(batchRows, "serialize coze batch payload failed");
|
||||||
String storedBatchPayload = transientPayloadStorageService.storeParsedPayloadFast(
|
String storedBatchPayload = transientPayloadStorageService.storeParsedPayloadFast(
|
||||||
@@ -1278,6 +1291,25 @@ public class SimilarAsinTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void pollPendingCozeState(Long stateId) {
|
private void pollPendingCozeState(Long stateId) {
|
||||||
|
TaskScopeStateEntity lockState = taskScopeStateMapper.selectById(stateId);
|
||||||
|
if (lockState == null || lockState.getCozeExecuteId() == null || lockState.getCozeExecuteId().isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isCozeStateOwnedByCurrentInstance(lockState)) {
|
||||||
|
log.info("[similar-asin] coze poll skipped because owner is another instance taskId={} stateId={} owner={} current={}",
|
||||||
|
lockState.getTaskId(), stateId, ownerFromCozeState(lockState), currentInstanceId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TaskDistributedLockService.LockHandle taskLockHandle = acquireTaskLock(lockState.getTaskId(), 0L);
|
||||||
|
if (taskLockHandle == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (taskLockHandle) {
|
||||||
|
pollPendingCozeStateLocked(stateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pollPendingCozeStateLocked(Long stateId) {
|
||||||
TaskScopeStateEntity state = taskScopeStateMapper.selectById(stateId);
|
TaskScopeStateEntity state = taskScopeStateMapper.selectById(stateId);
|
||||||
if (state == null || state.getCozeExecuteId() == null || state.getCozeExecuteId().isBlank()) {
|
if (state == null || state.getCozeExecuteId() == null || state.getCozeExecuteId().isBlank()) {
|
||||||
return;
|
return;
|
||||||
@@ -1285,16 +1317,7 @@ public class SimilarAsinTaskService {
|
|||||||
if (!(COZE_STATUS_SUBMITTED.equals(state.getCozeStatus()) || COZE_STATUS_RUNNING.equals(state.getCozeStatus()))) {
|
if (!(COZE_STATUS_SUBMITTED.equals(state.getCozeStatus()) || COZE_STATUS_RUNNING.equals(state.getCozeStatus()))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TaskDistributedLockService.LockHandle taskLockHandle = acquireTaskLock(state.getTaskId(), 0L);
|
if (!isCozeStateOwnedByCurrentInstance(state)) {
|
||||||
if (taskLockHandle == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try (taskLockHandle) {
|
|
||||||
state = taskScopeStateMapper.selectById(stateId);
|
|
||||||
if (state == null || state.getCozeExecuteId() == null || state.getCozeExecuteId().isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!(COZE_STATUS_SUBMITTED.equals(state.getCozeStatus()) || COZE_STATUS_RUNNING.equals(state.getCozeStatus()))) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!tryClaimCozeStateForPoll(state)) {
|
if (!tryClaimCozeStateForPoll(state)) {
|
||||||
@@ -1305,6 +1328,11 @@ public class SimilarAsinTaskService {
|
|||||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze batch context missing");
|
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze batch context missing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isOwnerCurrent(context.ownerInstanceId())) {
|
||||||
|
log.info("[similar-asin] coze poll skipped after context refresh because owner is another instance taskId={} stateId={} owner={} current={}",
|
||||||
|
state.getTaskId(), state.getId(), context.ownerInstanceId(), currentInstanceId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
taskFileJobService.touchRunning(context.jobId());
|
taskFileJobService.touchRunning(context.jobId());
|
||||||
try {
|
try {
|
||||||
SimilarAsinCozeClient.CozePollResponse poll = cozeClient.pollWorkflow(state.getCozeExecuteId());
|
SimilarAsinCozeClient.CozePollResponse poll = cozeClient.pollWorkflow(state.getCozeExecuteId());
|
||||||
@@ -1321,6 +1349,9 @@ public class SimilarAsinTaskService {
|
|||||||
: "Coze async workflow completed without output";
|
: "Coze async workflow completed without output";
|
||||||
}
|
}
|
||||||
List<SimilarAsinResultRowDto> batchRows = readCozeBatchRows(state);
|
List<SimilarAsinResultRowDto> batchRows = readCozeBatchRows(state);
|
||||||
|
if (batchRows.isEmpty() && failureMessage.isBlank()) {
|
||||||
|
failureMessage = "Coze batch payload missing";
|
||||||
|
}
|
||||||
List<SimilarAsinResultRowDto> cozeRows = failureMessage.isBlank()
|
List<SimilarAsinResultRowDto> cozeRows = failureMessage.isBlank()
|
||||||
? cozeClient.mergeRowsFromDataText(batchRows, poll.resolvedPayloadText())
|
? cozeClient.mergeRowsFromDataText(batchRows, poll.resolvedPayloadText())
|
||||||
: cozeClient.markRowsFailed(batchRows, failureMessage);
|
: cozeClient.markRowsFailed(batchRows, failureMessage);
|
||||||
@@ -1355,7 +1386,6 @@ public class SimilarAsinTaskService {
|
|||||||
updateCozeStateRunning(state, message);
|
updateCozeStateRunning(state, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCozeStateRunning(TaskScopeStateEntity state, String error) {
|
private void updateCozeStateRunning(TaskScopeStateEntity state, String error) {
|
||||||
int updated = taskScopeStateMapper.update(null, new LambdaUpdateWrapper<TaskScopeStateEntity>()
|
int updated = taskScopeStateMapper.update(null, new LambdaUpdateWrapper<TaskScopeStateEntity>()
|
||||||
@@ -1410,6 +1440,9 @@ public class SimilarAsinTaskService {
|
|||||||
if (taskId == null || context == null || countPendingCozeStates(taskId) > 0) {
|
if (taskId == null || context == null || countPendingCozeStates(taskId) > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isOwnerCurrent(context.ownerInstanceId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, 0L);
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, 0L);
|
||||||
if (lockHandle == null) {
|
if (lockHandle == null) {
|
||||||
return;
|
return;
|
||||||
@@ -1442,7 +1475,7 @@ public class SimilarAsinTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private TaskDistributedLockService.LockHandle acquireTaskLock(Long taskId, long waitMillis) {
|
private TaskDistributedLockService.LockHandle acquireTaskLock(Long taskId, long waitMillis) {
|
||||||
TaskDistributedLockService.LockHandle lockHandle = taskDistributedLockService.acquire(MODULE_TYPE, taskId, waitMillis);
|
TaskDistributedLockService.LockHandle lockHandle = taskDistributedLockService.acquire(MODULE_TYPE, taskId, TASK_LOCK_TTL, waitMillis);
|
||||||
if (lockHandle == null) {
|
if (lockHandle == null) {
|
||||||
log.info("[similar-asin] skip task operation because task lock is busy taskId={}", taskId);
|
log.info("[similar-asin] skip task operation because task lock is busy taskId={}", taskId);
|
||||||
}
|
}
|
||||||
@@ -1454,6 +1487,9 @@ public class SimilarAsinTaskService {
|
|||||||
TaskFileJobEntity job,
|
TaskFileJobEntity job,
|
||||||
int totalProgressUnits,
|
int totalProgressUnits,
|
||||||
int cozeWorkUnits) {
|
int cozeWorkUnits) {
|
||||||
|
if (countPendingCozeStates(task.getId()) > 0) {
|
||||||
|
throw new BusinessException("Coze result is still processing, cannot generate result file yet");
|
||||||
|
}
|
||||||
int assembleProgress = Math.max(1, Math.min(totalProgressUnits - 2, cozeWorkUnits));
|
int assembleProgress = Math.max(1, Math.min(totalProgressUnits - 2, cozeWorkUnits));
|
||||||
saveFileBuildProgress(task, job, totalProgressUnits, assembleProgress, "Assembling xlsx");
|
saveFileBuildProgress(task, job, totalProgressUnits, assembleProgress, "Assembling xlsx");
|
||||||
assembleResultWorkbook(task, result);
|
assembleResultWorkbook(task, result);
|
||||||
@@ -1572,6 +1608,69 @@ public class SimilarAsinTaskService {
|
|||||||
+ ":batch:" + batchIndex;
|
+ ":batch:" + batchIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildTaskOwnerScopeKey(FileTaskEntity task) {
|
||||||
|
Long taskId = task == null ? null : task.getId();
|
||||||
|
return "task:" + taskId + ":owner:" + firstNonBlank(ownerFromTask(task), currentInstanceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String currentInstanceId() {
|
||||||
|
String instanceId = instanceMetadata == null ? null : instanceMetadata.getInstanceId();
|
||||||
|
return firstNonBlank(instanceId, "unknown-instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isJobOwnedByCurrentInstance(TaskFileJobEntity job) {
|
||||||
|
return job == null || isOwnerCurrent(ownerFromScopeKey(job.getScopeKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTaskOwnedByCurrentInstance(FileTaskEntity task, String operation) {
|
||||||
|
String owner = ownerFromTask(task);
|
||||||
|
if (isOwnerCurrent(owner)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.warn("[similar-asin] reject task operation because owner is another instance taskId={} operation={} owner={} current={}",
|
||||||
|
task == null ? null : task.getId(), operation, owner, currentInstanceId());
|
||||||
|
throw new BusinessException(40903, "This task is bound to another service instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCozeStateOwnedByCurrentInstance(TaskScopeStateEntity state) {
|
||||||
|
return state == null || isOwnerCurrent(ownerFromCozeState(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOwnerCurrent(String owner) {
|
||||||
|
return owner == null || owner.isBlank() || Objects.equals(owner, currentInstanceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ownerFromCozeState(TaskScopeStateEntity state) {
|
||||||
|
CozeBatchContext context = readCozeBatchContext(state);
|
||||||
|
return context == null ? null : context.ownerInstanceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ownerFromTask(FileTaskEntity task) {
|
||||||
|
if (task == null || task.getResultJson() == null || task.getResultJson().isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String owner = objectMapper.readTree(task.getResultJson()).path("ownerInstanceId").asText("");
|
||||||
|
return owner.isBlank() ? null : owner;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("[similar-asin] read task owner failed taskId={} err={}", task.getId(), ex.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ownerFromScopeKey(String scopeKey) {
|
||||||
|
if (scopeKey == null || scopeKey.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String marker = ":owner:";
|
||||||
|
int index = scopeKey.lastIndexOf(marker);
|
||||||
|
if (index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String owner = scopeKey.substring(index + marker.length()).trim();
|
||||||
|
return owner.isBlank() ? null : owner;
|
||||||
|
}
|
||||||
|
|
||||||
private int countCozeWorkUnits(List<TaskChunkEntity> chunks, int batchSize) {
|
private int countCozeWorkUnits(List<TaskChunkEntity> chunks, int batchSize) {
|
||||||
if (chunks == null || chunks.isEmpty()) {
|
if (chunks == null || chunks.isEmpty()) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1632,11 +1731,8 @@ public class SimilarAsinTaskService {
|
|||||||
long resolvedRows = parsed.getAllItems().stream()
|
long resolvedRows = parsed.getAllItems().stream()
|
||||||
.filter(row -> findResultRow(row, resultMap) != null || findResultRowByAsin(row.getAsin(), resultMap) != null)
|
.filter(row -> findResultRow(row, resultMap) != null || findResultRowByAsin(row.getAsin(), resultMap) != null)
|
||||||
.count();
|
.count();
|
||||||
long reasonRows = resultMap.values().stream()
|
log.info("[similar-asin] assemble workbook taskId={} parsedRows={} resultRows={} resolvedRows={}",
|
||||||
.filter(this::hasReasonFields)
|
task.getId(), parsed.getAllItems().size(), resultMap.size(), resolvedRows);
|
||||||
.count();
|
|
||||||
log.info("[similar-asin] assemble workbook taskId={} parsedRows={} resultRows={} resolvedRows={} reasonRows={}",
|
|
||||||
task.getId(), parsed.getAllItems().size(), resultMap.size(), resolvedRows, reasonRows);
|
|
||||||
if (!parsed.getAllItems().isEmpty() && resultMap.isEmpty()) {
|
if (!parsed.getAllItems().isEmpty() && resultMap.isEmpty()) {
|
||||||
throw new BusinessException("相似ASIN检测结果为空,请稍后重试生成结果文件");
|
throw new BusinessException("相似ASIN检测结果为空,请稍后重试生成结果文件");
|
||||||
}
|
}
|
||||||
@@ -1707,15 +1803,13 @@ public class SimilarAsinTaskService {
|
|||||||
|
|
||||||
private void writeResultWorkbook(File xlsx, SimilarAsinParsedPayloadDto parsed, Map<String, SimilarAsinResultRowDto> resultMap) {
|
private void writeResultWorkbook(File xlsx, SimilarAsinParsedPayloadDto parsed, Map<String, SimilarAsinResultRowDto> resultMap) {
|
||||||
try (SXSSFWorkbook workbook = new SXSSFWorkbook(200); FileOutputStream fos = new FileOutputStream(xlsx)) {
|
try (SXSSFWorkbook workbook = new SXSSFWorkbook(200); FileOutputStream fos = new FileOutputStream(xlsx)) {
|
||||||
Sheet sheet = workbook.createSheet("相似ASIN检测结果");
|
Sheet sheet = workbook.createSheet("相似asin检测");
|
||||||
CellStyle headerStyle = workbook.createCellStyle();
|
CellStyle headerStyle = workbook.createCellStyle();
|
||||||
Font font = workbook.createFont();
|
Font font = workbook.createFont();
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
headerStyle.setFont(font);
|
headerStyle.setFont(font);
|
||||||
|
|
||||||
List<String> resultHeaders = new ArrayList<>(RESULT_HEADERS);
|
List<String> resultHeaders = new ArrayList<>(RESULT_HEADERS);
|
||||||
resultHeaders.add(4, "标题");
|
|
||||||
resultHeaders.add(5, "图片链接");
|
|
||||||
Row header = sheet.createRow(0);
|
Row header = sheet.createRow(0);
|
||||||
for (int i = 0; i < resultHeaders.size(); i++) {
|
for (int i = 0; i < resultHeaders.size(); i++) {
|
||||||
Cell cell = header.createCell(i);
|
Cell cell = header.createCell(i);
|
||||||
@@ -1729,24 +1823,17 @@ public class SimilarAsinTaskService {
|
|||||||
if (resultRow == null) {
|
if (resultRow == null) {
|
||||||
resultRow = findResultRowByAsin(parsedRow.getAsin(), resultMap);
|
resultRow = findResultRowByAsin(parsedRow.getAsin(), resultMap);
|
||||||
}
|
}
|
||||||
String missingReason = "";
|
|
||||||
if (resultRow == null) {
|
|
||||||
missingReason = hasPromptFields(parsedRow) ? "未匹配到检测结果" : "未送检:缺少标题或图片";
|
|
||||||
}
|
|
||||||
Row row = sheet.createRow(rowIndex++);
|
Row row = sheet.createRow(rowIndex++);
|
||||||
int col = 0;
|
int col = 0;
|
||||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getDisplayId(), parsedRow.getSourceId()));
|
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getDisplayId(), parsedRow.getSourceId()));
|
||||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getAsin(), ""));
|
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getAsin(), ""));
|
||||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getCountry(), ""));
|
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getCountry(), ""));
|
||||||
row.createCell(col++).setCellValue(readValueByHeader(parsedRow, "价格", "price"));
|
row.createCell(col++).setCellValue(resultRow == null
|
||||||
row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getTitle(), "") : firstNonBlank(resultRow.getTitle(), parsedRow.getTitle()));
|
? firstNonBlank(parsedRow.getPrice(), readValueByHeader(parsedRow, "价格", "price"))
|
||||||
row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getUrl(), "") : imageUrlCellValue(resultRow, parsedRow.getUrl()));
|
: firstNonBlank(resultRow.getPrice(), firstNonBlank(parsedRow.getPrice(), readValueByHeader(parsedRow, "价格", "price"))));
|
||||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getTitleRisk()));
|
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getIsStock(), ""));
|
||||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getAppearanceRisk()));
|
row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getSimilarity(), ""));
|
||||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getPatentRisk()));
|
|
||||||
row.createCell(col).setCellValue(resultRow == null ? "未送检" : userFacingConclusion(resultRow));
|
|
||||||
}
|
}
|
||||||
writeReasonSheet(workbook, headerStyle, parsed, resultMap);
|
|
||||||
workbook.write(fos);
|
workbook.write(fos);
|
||||||
workbook.dispose();
|
workbook.dispose();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -1754,39 +1841,6 @@ public class SimilarAsinTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeReasonSheet(SXSSFWorkbook workbook,
|
|
||||||
CellStyle headerStyle,
|
|
||||||
SimilarAsinParsedPayloadDto parsed,
|
|
||||||
Map<String, SimilarAsinResultRowDto> resultMap) {
|
|
||||||
Sheet sheet = workbook.createSheet("原因");
|
|
||||||
Row header = sheet.createRow(0);
|
|
||||||
List<String> headers = List.of("ASIN", "外观原因", "专利原因", "标题原因");
|
|
||||||
for (int i = 0; i < headers.size(); i++) {
|
|
||||||
Cell cell = header.createCell(i);
|
|
||||||
cell.setCellValue(headers.get(i));
|
|
||||||
cell.setCellStyle(headerStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> writtenAsins = new LinkedHashSet<>();
|
|
||||||
int rowIndex = 1;
|
|
||||||
for (SimilarAsinParsedRowVo parsedRow : parsed.getAllItems()) {
|
|
||||||
String asin = normalize(parsedRow.getAsin()).toUpperCase(Locale.ROOT);
|
|
||||||
if (asin.isBlank() || !writtenAsins.add(asin)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
SimilarAsinResultRowDto resultRow = findResultRow(parsedRow, resultMap);
|
|
||||||
if (resultRow == null) {
|
|
||||||
resultRow = findResultRowByAsin(asin, resultMap);
|
|
||||||
}
|
|
||||||
Row row = sheet.createRow(rowIndex++);
|
|
||||||
row.createCell(0).setCellValue(asin);
|
|
||||||
row.createCell(1).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getAppearanceReason(), ""));
|
|
||||||
row.createCell(2).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getPatentReason(), ""));
|
|
||||||
row.createCell(3).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getTitleReason(), ""));
|
|
||||||
rowIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SimilarAsinResultRowDto findResultRowByAsin(String asin, Map<String, SimilarAsinResultRowDto> resultMap) {
|
private SimilarAsinResultRowDto findResultRowByAsin(String asin, Map<String, SimilarAsinResultRowDto> resultMap) {
|
||||||
String normalizedAsin = normalize(asin).toUpperCase(Locale.ROOT);
|
String normalizedAsin = normalize(asin).toUpperCase(Locale.ROOT);
|
||||||
if (normalizedAsin.isBlank() || resultMap == null || resultMap.isEmpty()) {
|
if (normalizedAsin.isBlank() || resultMap == null || resultMap.isEmpty()) {
|
||||||
@@ -1797,7 +1851,7 @@ public class SimilarAsinTaskService {
|
|||||||
if (row == null || !normalizedAsin.equals(normalize(row.getAsin()).toUpperCase(Locale.ROOT))) {
|
if (row == null || !normalizedAsin.equals(normalize(row.getAsin()).toUpperCase(Locale.ROOT))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (hasResolvedCozeFields(row) || hasReasonFields(row)) {
|
if (hasResolvedCozeFields(row)) {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
if (fallback == null) {
|
if (fallback == null) {
|
||||||
@@ -1807,15 +1861,6 @@ public class SimilarAsinTaskService {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasReasonFields(SimilarAsinResultRowDto row) {
|
|
||||||
if (row == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !normalize(row.getAppearanceReason()).isBlank()
|
|
||||||
|| !normalize(row.getPatentReason()).isBlank()
|
|
||||||
|| !normalize(row.getTitleReason()).isBlank();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String imageUrlCellValue(SimilarAsinResultRowDto resultRow, String fallbackUrl) {
|
private String imageUrlCellValue(SimilarAsinResultRowDto resultRow, String fallbackUrl) {
|
||||||
if (resultRow == null) {
|
if (resultRow == null) {
|
||||||
return firstNonBlank(fallbackUrl, "");
|
return firstNonBlank(fallbackUrl, "");
|
||||||
@@ -1840,6 +1885,7 @@ public class SimilarAsinTaskService {
|
|||||||
int idCol = findRequiredHeader(headerMap, "id");
|
int idCol = findRequiredHeader(headerMap, "id");
|
||||||
int asinCol = findRequiredHeader(headerMap, "asin");
|
int asinCol = findRequiredHeader(headerMap, "asin");
|
||||||
int countryCol = findRequiredHeader(headerMap, "国家", "country");
|
int countryCol = findRequiredHeader(headerMap, "国家", "country");
|
||||||
|
int priceCol = findOptionalHeaderExact(headerMap, "价格", "price");
|
||||||
int urlCol = findOptionalHeaderExact(headerMap,
|
int urlCol = findOptionalHeaderExact(headerMap,
|
||||||
"url", "rul", "link", "image", "img", "pic", "picture",
|
"url", "rul", "link", "image", "img", "pic", "picture",
|
||||||
"链接", "商品链接", "图片", "商品图片", "主图", "商品主图", "图片链接", "主图链接");
|
"链接", "商品链接", "图片", "商品图片", "主图", "商品主图", "图片链接", "主图链接");
|
||||||
@@ -1882,6 +1928,7 @@ public class SimilarAsinTaskService {
|
|||||||
vo.setRowToken(buildRowToken(source.getFileKey(), vo.getRowIndex()));
|
vo.setRowToken(buildRowToken(source.getFileKey(), vo.getRowIndex()));
|
||||||
vo.setAsin(asin);
|
vo.setAsin(asin);
|
||||||
vo.setCountry(country);
|
vo.setCountry(country);
|
||||||
|
vo.setPrice(priceCol >= 0 ? cell(row, priceCol, formatter) : "");
|
||||||
vo.setUrl(urlCol >= 0 ? cell(row, urlCol, formatter) : "");
|
vo.setUrl(urlCol >= 0 ? cell(row, urlCol, formatter) : "");
|
||||||
vo.setTitle(titleCol >= 0 ? cell(row, titleCol, formatter) : "");
|
vo.setTitle(titleCol >= 0 ? cell(row, titleCol, formatter) : "");
|
||||||
vo.setValues(readRowValues(row, headers, formatter));
|
vo.setValues(readRowValues(row, headers, formatter));
|
||||||
@@ -1938,7 +1985,9 @@ public class SimilarAsinTaskService {
|
|||||||
return hasUsableCozeField(row.getTitleRisk())
|
return hasUsableCozeField(row.getTitleRisk())
|
||||||
|| hasUsableCozeField(row.getAppearanceRisk())
|
|| hasUsableCozeField(row.getAppearanceRisk())
|
||||||
|| hasUsableCozeField(row.getPatentRisk())
|
|| hasUsableCozeField(row.getPatentRisk())
|
||||||
|| hasUsableCozeField(row.getConclusion());
|
|| hasUsableCozeField(row.getConclusion())
|
||||||
|
|| hasUsableCozeField(row.getIsStock())
|
||||||
|
|| hasUsableCozeField(row.getSimilarity());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasUsableCozeField(String value) {
|
private boolean hasUsableCozeField(String value) {
|
||||||
@@ -2183,9 +2232,10 @@ public class SimilarAsinTaskService {
|
|||||||
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildParsedPayloadJson(String aiPrompt, List<SimilarAsinSourceFileDto> sourceFiles, List<String> headers, List<SimilarAsinParsedGroupVo> groups, List<SimilarAsinParsedRowVo> allRows) {
|
private String buildParsedPayloadJson(String aiPrompt, String apiKey, List<SimilarAsinSourceFileDto> sourceFiles, List<String> headers, List<SimilarAsinParsedGroupVo> groups, List<SimilarAsinParsedRowVo> allRows) {
|
||||||
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
|
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
|
||||||
payload.setAiPrompt(normalize(aiPrompt));
|
payload.setAiPrompt(normalize(aiPrompt));
|
||||||
|
payload.setApiKey(normalize(apiKey));
|
||||||
payload.setSourceFiles(sourceFiles == null ? List.of() : sourceFiles);
|
payload.setSourceFiles(sourceFiles == null ? List.of() : sourceFiles);
|
||||||
payload.setHeaders(headers == null ? List.of() : headers);
|
payload.setHeaders(headers == null ? List.of() : headers);
|
||||||
payload.setItems(List.of());
|
payload.setItems(List.of());
|
||||||
@@ -2194,14 +2244,16 @@ public class SimilarAsinTaskService {
|
|||||||
return writeJson(payload, "保存解析结果失败");
|
return writeJson(payload, "保存解析结果失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildTaskResultJson(String aiPrompt, List<SimilarAsinSourceFileDto> sourceFiles, String parsedPayloadPointer) {
|
private String buildTaskResultJson(String aiPrompt, String apiKey, List<SimilarAsinSourceFileDto> sourceFiles, String parsedPayloadPointer) {
|
||||||
Map<String, Object> payload = new LinkedHashMap<>();
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
payload.put("aiPrompt", normalize(aiPrompt));
|
payload.put("aiPrompt", normalize(aiPrompt));
|
||||||
|
payload.put("apiKey", normalize(apiKey));
|
||||||
payload.put("sourceFileKeys", sourceFiles == null ? List.of() : sourceFiles.stream()
|
payload.put("sourceFileKeys", sourceFiles == null ? List.of() : sourceFiles.stream()
|
||||||
.map(SimilarAsinSourceFileDto::getFileKey)
|
.map(SimilarAsinSourceFileDto::getFileKey)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.toList());
|
.toList());
|
||||||
payload.put("parsedPayloadRef", parsedPayloadPointer);
|
payload.put("parsedPayloadRef", parsedPayloadPointer);
|
||||||
|
payload.put("ownerInstanceId", currentInstanceId());
|
||||||
return writeJson(payload, "保存任务结果索引失败");
|
return writeJson(payload, "保存任务结果索引失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2439,7 +2491,8 @@ public class SimilarAsinTaskService {
|
|||||||
String chunkScopeHash,
|
String chunkScopeHash,
|
||||||
Integer chunkIndex,
|
Integer chunkIndex,
|
||||||
Integer batchIndex,
|
Integer batchIndex,
|
||||||
Integer batchTotal) {
|
Integer batchTotal,
|
||||||
|
String ownerInstanceId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ParsedWorkbook(int totalRows, int droppedRows, List<String> headers, List<SimilarAsinParsedRowVo> allRows) {
|
private record ParsedWorkbook(int totalRows, int droppedRows, List<String> headers, List<SimilarAsinParsedRowVo> allRows) {
|
||||||
|
|||||||
@@ -33,9 +33,12 @@ public class TaskFileJobDispatchCoordinator {
|
|||||||
if (published) {
|
if (published) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean dispatched = taskFileJobLocalDispatcher.dispatch(event.jobId(), event.taskId(), event.moduleType());
|
boolean dispatched = taskFileJobLocalDispatcher.dispatch(event.jobId(), event.taskId(), event.moduleType(), true);
|
||||||
if (dispatched) {
|
if (dispatched) {
|
||||||
log.info("[task-file-job] local async dispatch scheduled jobId={} taskId={} moduleType={}",
|
log.info("[task-file-job] forced local async dispatch scheduled jobId={} taskId={} moduleType={}",
|
||||||
|
event.jobId(), event.taskId(), event.moduleType());
|
||||||
|
} else {
|
||||||
|
log.warn("[task-file-job] local dispatch unavailable after mq publish fallback jobId={} taskId={} moduleType={}",
|
||||||
event.jobId(), event.taskId(), event.moduleType());
|
event.jobId(), event.taskId(), event.moduleType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,33 @@ public class TaskFileJobLocalDispatcher {
|
|||||||
private boolean localDispatchEnabled;
|
private boolean localDispatchEnabled;
|
||||||
|
|
||||||
public boolean dispatch(Long jobId, Long taskId, String moduleType) {
|
public boolean dispatch(Long jobId, Long taskId, String moduleType) {
|
||||||
if (!localDispatchEnabled || jobId == null || jobId <= 0) {
|
return dispatch(jobId, taskId, moduleType, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean dispatch(Long jobId, Long taskId, String moduleType, boolean force) {
|
||||||
|
if (jobId == null || jobId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!force && !localDispatchEnabled) {
|
||||||
|
log.info("[task-file-job] local dispatch disabled jobId={} taskId={} moduleType={}",
|
||||||
|
jobId, taskId, moduleType);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
taskFileJobDispatchExecutor.execute(() -> {
|
taskFileJobDispatchExecutor.execute(() -> processLocally(jobId, taskId, moduleType));
|
||||||
|
return true;
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
log.warn("[task-file-job] local dispatch executor rejected jobId={} taskId={} moduleType={} msg={}",
|
||||||
|
jobId, taskId, moduleType, ex.getMessage(), ex);
|
||||||
|
if (force) {
|
||||||
|
processLocally(jobId, taskId, moduleType);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processLocally(Long jobId, Long taskId, String moduleType) {
|
||||||
try {
|
try {
|
||||||
TaskFileJobEntity job = taskFileJobMapper.selectById(jobId);
|
TaskFileJobEntity job = taskFileJobMapper.selectById(jobId);
|
||||||
if (job == null) {
|
if (job == null) {
|
||||||
@@ -50,12 +72,5 @@ public class TaskFileJobLocalDispatcher {
|
|||||||
log.warn("[task-file-job] local dispatch failed jobId={} taskId={} moduleType={} msg={}",
|
log.warn("[task-file-job] local dispatch failed jobId={} taskId={} moduleType={} msg={}",
|
||||||
jobId, taskId, moduleType, ex.getMessage(), ex);
|
jobId, taskId, moduleType, ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (RuntimeException ex) {
|
|
||||||
log.warn("[task-file-job] local dispatch executor rejected jobId={} taskId={} moduleType={} msg={}",
|
|
||||||
jobId, taskId, moduleType, ex.getMessage(), ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.nanri.aiimage.modules.task.service;
|
package com.nanri.aiimage.modules.task.service;
|
||||||
|
|
||||||
import com.nanri.aiimage.modules.appearancepatent.service.AppearancePatentTaskService;
|
import com.nanri.aiimage.modules.appearancepatent.service.AppearancePatentTaskService;
|
||||||
|
import com.nanri.aiimage.config.InstanceMetadata;
|
||||||
import com.nanri.aiimage.modules.brand.service.BrandTaskService;
|
import com.nanri.aiimage.modules.brand.service.BrandTaskService;
|
||||||
import com.nanri.aiimage.modules.deletebrand.service.DeleteBrandRunService;
|
import com.nanri.aiimage.modules.deletebrand.service.DeleteBrandRunService;
|
||||||
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskService;
|
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskService;
|
||||||
@@ -33,6 +34,7 @@ public class TaskResultFileJobWorker {
|
|||||||
private final TaskResultPayloadService taskResultPayloadService;
|
private final TaskResultPayloadService taskResultPayloadService;
|
||||||
private final FileResultMapper fileResultMapper;
|
private final FileResultMapper fileResultMapper;
|
||||||
private final TaskFileJobLocalDispatcher taskFileJobLocalDispatcher;
|
private final TaskFileJobLocalDispatcher taskFileJobLocalDispatcher;
|
||||||
|
private final InstanceMetadata instanceMetadata;
|
||||||
private final ShopMatchTaskService shopMatchTaskService;
|
private final ShopMatchTaskService shopMatchTaskService;
|
||||||
private final PriceTrackTaskService priceTrackTaskService;
|
private final PriceTrackTaskService priceTrackTaskService;
|
||||||
private final ProductRiskTaskService productRiskTaskService;
|
private final ProductRiskTaskService productRiskTaskService;
|
||||||
@@ -86,6 +88,11 @@ public class TaskResultFileJobWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void process(TaskFileJobEntity job) {
|
public void process(TaskFileJobEntity job) {
|
||||||
|
if (isOwnerScopedCozeJob(job) && !isOwnedByCurrentInstance(job)) {
|
||||||
|
log.info("[task-file-job] skip owner-scoped job because owner is another instance jobId={} taskId={} moduleType={} owner={} current={}",
|
||||||
|
job.getId(), job.getTaskId(), job.getModuleType(), ownerFromScopeKey(job.getScopeKey()), currentInstanceId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (job == null || job.getId() == null || !taskFileJobService.markRunning(job.getId())) {
|
if (job == null || job.getId() == null || !taskFileJobService.markRunning(job.getId())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -147,6 +154,36 @@ public class TaskResultFileJobWorker {
|
|||||||
return result == null ? null : result.getResultFileUrl();
|
return result == null ? null : result.getResultFileUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isOwnerScopedCozeJob(TaskFileJobEntity job) {
|
||||||
|
if (job == null || job.getModuleType() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return "APPEARANCE_PATENT".equals(job.getModuleType()) || "SIMILAR_ASIN".equals(job.getModuleType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOwnedByCurrentInstance(TaskFileJobEntity job) {
|
||||||
|
String owner = ownerFromScopeKey(job == null ? null : job.getScopeKey());
|
||||||
|
return owner == null || owner.isBlank() || owner.equals(currentInstanceId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String currentInstanceId() {
|
||||||
|
String instanceId = instanceMetadata == null ? null : instanceMetadata.getInstanceId();
|
||||||
|
return instanceId == null || instanceId.isBlank() ? "unknown-instance" : instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ownerFromScopeKey(String scopeKey) {
|
||||||
|
if (scopeKey == null || scopeKey.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String marker = ":owner:";
|
||||||
|
int index = scopeKey.lastIndexOf(marker);
|
||||||
|
if (index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String owner = scopeKey.substring(index + marker.length()).trim();
|
||||||
|
return owner.isBlank() ? null : owner;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean dispatch(TaskFileJobEntity job) {
|
private boolean dispatch(TaskFileJobEntity job) {
|
||||||
String moduleType = job.getModuleType();
|
String moduleType = job.getModuleType();
|
||||||
if ("SHOP_MATCH".equals(moduleType)) {
|
if ("SHOP_MATCH".equals(moduleType)) {
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ aiimage:
|
|||||||
coze-batch-size: ${AIIMAGE_APPEARANCE_PATENT_COZE_BATCH_SIZE:10}
|
coze-batch-size: ${AIIMAGE_APPEARANCE_PATENT_COZE_BATCH_SIZE:10}
|
||||||
coze-connect-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_CONNECT_TIMEOUT_MILLIS:10000}
|
coze-connect-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_CONNECT_TIMEOUT_MILLIS:10000}
|
||||||
coze-read-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_READ_TIMEOUT_MILLIS:60000}
|
coze-read-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_READ_TIMEOUT_MILLIS:60000}
|
||||||
|
coze-poll-interval-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_POLL_INTERVAL_MILLIS:5000}
|
||||||
|
coze-poll-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_POLL_TIMEOUT_MILLIS:600000}
|
||||||
stale-timeout-minutes: ${AIIMAGE_APPEARANCE_PATENT_STALE_TIMEOUT_MINUTES:20}
|
stale-timeout-minutes: ${AIIMAGE_APPEARANCE_PATENT_STALE_TIMEOUT_MINUTES:20}
|
||||||
stale-finalize-cron: ${AIIMAGE_APPEARANCE_PATENT_STALE_FINALIZE_CRON:0 */2 * * * *}
|
stale-finalize-cron: ${AIIMAGE_APPEARANCE_PATENT_STALE_FINALIZE_CRON:0 */2 * * * *}
|
||||||
similar-asin:
|
similar-asin:
|
||||||
@@ -160,6 +162,8 @@ aiimage:
|
|||||||
coze-batch-size: ${AIIMAGE_SIMILAR_ASIN_COZE_BATCH_SIZE:10}
|
coze-batch-size: ${AIIMAGE_SIMILAR_ASIN_COZE_BATCH_SIZE:10}
|
||||||
coze-connect-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_CONNECT_TIMEOUT_MILLIS:10000}
|
coze-connect-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_CONNECT_TIMEOUT_MILLIS:10000}
|
||||||
coze-read-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_READ_TIMEOUT_MILLIS:60000}
|
coze-read-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_READ_TIMEOUT_MILLIS:60000}
|
||||||
|
coze-poll-interval-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_POLL_INTERVAL_MILLIS:5000}
|
||||||
|
coze-poll-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_POLL_TIMEOUT_MILLIS:600000}
|
||||||
stale-timeout-minutes: ${AIIMAGE_SIMILAR_ASIN_STALE_TIMEOUT_MINUTES:20}
|
stale-timeout-minutes: ${AIIMAGE_SIMILAR_ASIN_STALE_TIMEOUT_MINUTES:20}
|
||||||
stale-finalize-cron: ${AIIMAGE_SIMILAR_ASIN_STALE_FINALIZE_CRON:0 */2 * * * *}
|
stale-finalize-cron: ${AIIMAGE_SIMILAR_ASIN_STALE_FINALIZE_CRON:0 */2 * * * *}
|
||||||
security:
|
security:
|
||||||
|
|||||||
@@ -24,6 +24,11 @@
|
|||||||
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
|
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="secret-card">
|
||||||
|
<div class="section-title">密钥</div>
|
||||||
|
<input v-model="cozeApiKey" class="secret-input" type="password" autocomplete="off" spellcheck="false" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="run-row">
|
<div class="run-row">
|
||||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||||
@@ -34,9 +39,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="loading-msg">Python 回传字段按 asin、国家、url、标题提交;后端接收分片后按 10 条一批调用 Coze。</p>
|
<p class="loading-msg">Python 回传字段按 asin、国家、url、标题提交;后端接收分片后按 10 条一批调用 Coze。</p>
|
||||||
|
|
||||||
<div v-if="parseResult" class="parse-card">
|
<div v-if="visibleTaskSummary" class="parse-card">
|
||||||
<div>任务 ID:{{ parseResult.taskId }}</div>
|
<div>任务 ID:{{ visibleTaskSummary.taskId }}</div>
|
||||||
<div>总行数:{{ parseResult.totalRows }},有效:{{ parseResult.acceptedRows }},分组:{{ parseResult.groupCount || 0 }},过滤:{{ parseResult.droppedRows }}</div>
|
<div>总行数:{{ visibleTaskSummary.totalRows }},有效:{{ visibleTaskSummary.acceptedRows }},分组:{{ visibleTaskSummary.groupCount || 0 }},过滤:{{ visibleTaskSummary.droppedRows }}</div>
|
||||||
</div>
|
</div>
|
||||||
<pre v-if="queuePayloadText" class="queue-payload">{{ queuePayloadText }}</pre>
|
<pre v-if="queuePayloadText" class="queue-payload">{{ queuePayloadText }}</pre>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -171,6 +176,8 @@ const selectedFileNames = ref<string[]>([])
|
|||||||
const uploadedFiles = ref<UploadFileVo[]>([])
|
const uploadedFiles = ref<UploadFileVo[]>([])
|
||||||
const parseResult = ref<AppearancePatentParseVo | null>(null)
|
const parseResult = ref<AppearancePatentParseVo | null>(null)
|
||||||
const parsedGroups = ref<AppearancePatentParsedGroup[]>([])
|
const parsedGroups = ref<AppearancePatentParsedGroup[]>([])
|
||||||
|
type TaskSummary = Pick<AppearancePatentParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
|
||||||
|
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||||
const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。
|
const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。
|
||||||
请直接给出明确结论(有侵权风险 或 未发现明显侵权风险),并严格按照以下三个维度提供精简的排查理由:
|
请直接给出明确结论(有侵权风险 或 未发现明显侵权风险),并严格按照以下三个维度提供精简的排查理由:
|
||||||
外观维度: 评估产品外形、图案设计是否与欧洲/英国常见外观专利雷同。
|
外观维度: 评估产品外形、图案设计是否与欧洲/英国常见外观专利雷同。
|
||||||
@@ -178,6 +185,7 @@ const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲
|
|||||||
标题维度: 排查标题中是否包含大牌商标、敏感词或版权保护词汇。
|
标题维度: 排查标题中是否包含大牌商标、敏感词或版权保护词汇。
|
||||||
结论:XX`
|
结论:XX`
|
||||||
const aiPrompt = ref('')
|
const aiPrompt = ref('')
|
||||||
|
const cozeApiKey = ref('')
|
||||||
const parsing = ref(false)
|
const parsing = ref(false)
|
||||||
const pushing = ref(false)
|
const pushing = ref(false)
|
||||||
const queuePayloadText = ref('')
|
const queuePayloadText = ref('')
|
||||||
@@ -203,6 +211,7 @@ const parsedRows = computed<AppearancePatentParsedRow[]>(() =>
|
|||||||
parsedGroups.value.flatMap((group) => group.items || []),
|
parsedGroups.value.flatMap((group) => group.items || []),
|
||||||
)
|
)
|
||||||
const previewRows = computed(() => parsedGroups.value[0]?.items || [])
|
const previewRows = computed(() => parsedGroups.value[0]?.items || [])
|
||||||
|
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
|
||||||
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
|
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
|
||||||
const historyOnlyItems = computed(() =>
|
const historyOnlyItems = computed(() =>
|
||||||
historyItems.value.filter((i) => {
|
historyItems.value.filter((i) => {
|
||||||
@@ -215,6 +224,28 @@ function effectiveAiPrompt() {
|
|||||||
return aiPrompt.value.trim() || defaultAiPrompt
|
return aiPrompt.value.trim() || defaultAiPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function effectiveCozeApiKey() {
|
||||||
|
return cozeApiKey.value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskSecret(secret: string) {
|
||||||
|
if (!secret) return ''
|
||||||
|
if (secret.length <= 10) return '***'
|
||||||
|
return `${secret.slice(0, 6)}***${secret.slice(-4)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function payloadForDisplay<T extends { data?: Record<string, unknown> }>(payload: T) {
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
data: payload.data
|
||||||
|
? {
|
||||||
|
...payload.data,
|
||||||
|
api_key: maskSecret(String(payload.data.api_key || '')),
|
||||||
|
}
|
||||||
|
: payload.data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function uidForStorage() {
|
function uidForStorage() {
|
||||||
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
||||||
}
|
}
|
||||||
@@ -223,6 +254,16 @@ function pollingKey() {
|
|||||||
return `appearance-patent:tasks:${uidForStorage()}`
|
return `appearance-patent:tasks:${uidForStorage()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toTaskSummary(result: AppearancePatentParseVo): TaskSummary {
|
||||||
|
return {
|
||||||
|
taskId: result.taskId,
|
||||||
|
totalRows: result.totalRows,
|
||||||
|
acceptedRows: result.acceptedRows,
|
||||||
|
groupCount: result.groupCount,
|
||||||
|
droppedRows: result.droppedRows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function savePollingIds() {
|
function savePollingIds() {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
const ids = Array.from(new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value]))
|
const ids = Array.from(new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value]))
|
||||||
@@ -274,6 +315,7 @@ async function selectFiles() {
|
|||||||
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
||||||
parseResult.value = null
|
parseResult.value = null
|
||||||
parsedGroups.value = []
|
parsedGroups.value = []
|
||||||
|
queuedTaskSummary.value = null
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +338,7 @@ async function selectFolder() {
|
|||||||
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||||
parseResult.value = null
|
parseResult.value = null
|
||||||
parsedGroups.value = []
|
parsedGroups.value = []
|
||||||
|
queuedTaskSummary.value = null
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -308,6 +351,10 @@ async function parseFiles() {
|
|||||||
ElMessage.warning('请先选择 Excel')
|
ElMessage.warning('请先选择 Excel')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!effectiveCozeApiKey()) {
|
||||||
|
ElMessage.warning('请先填写密钥')
|
||||||
|
return
|
||||||
|
}
|
||||||
parsing.value = true
|
parsing.value = true
|
||||||
try {
|
try {
|
||||||
const files: UploadedFileRef[] = uploadedFiles.value.map((f) => ({
|
const files: UploadedFileRef[] = uploadedFiles.value.map((f) => ({
|
||||||
@@ -315,8 +362,9 @@ async function parseFiles() {
|
|||||||
originalFilename: f.originalFilename,
|
originalFilename: f.originalFilename,
|
||||||
relativePath: f.relativePath,
|
relativePath: f.relativePath,
|
||||||
}))
|
}))
|
||||||
const res = await parseAppearancePatent(files, effectiveAiPrompt())
|
const res = await parseAppearancePatent(files, effectiveAiPrompt(), effectiveCozeApiKey())
|
||||||
parseResult.value = res
|
parseResult.value = res
|
||||||
|
queuedTaskSummary.value = null
|
||||||
parsedGroups.value = res.groups?.length
|
parsedGroups.value = res.groups?.length
|
||||||
? res.groups
|
? res.groups
|
||||||
: (res.items?.length
|
: (res.items?.length
|
||||||
@@ -341,7 +389,8 @@ async function parseFiles() {
|
|||||||
|
|
||||||
async function pushToPythonQueue() {
|
async function pushToPythonQueue() {
|
||||||
const api = getPywebviewApi()
|
const api = getPywebviewApi()
|
||||||
const taskId = parseResult.value?.taskId
|
const currentParseResult = parseResult.value
|
||||||
|
const taskId = currentParseResult?.taskId
|
||||||
if (!taskId || !parsedRows.value.length) {
|
if (!taskId || !parsedRows.value.length) {
|
||||||
ElMessage.warning('请先解析文件')
|
ElMessage.warning('请先解析文件')
|
||||||
return
|
return
|
||||||
@@ -357,7 +406,8 @@ async function pushToPythonQueue() {
|
|||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
data: {
|
data: {
|
||||||
taskId,
|
taskId,
|
||||||
prompt: parseResult.value?.aiPrompt || effectiveAiPrompt(),
|
prompt: currentParseResult.aiPrompt || effectiveAiPrompt(),
|
||||||
|
api_key: effectiveCozeApiKey(),
|
||||||
groups: parsedGroups.value.map((group) => ({
|
groups: parsedGroups.value.map((group) => ({
|
||||||
sourceFileKey: group.sourceFileKey || '',
|
sourceFileKey: group.sourceFileKey || '',
|
||||||
sourceFilename: group.sourceFilename || '',
|
sourceFilename: group.sourceFilename || '',
|
||||||
@@ -389,13 +439,14 @@ async function pushToPythonQueue() {
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
queuePayloadText.value = JSON.stringify(payloadForDisplay(payload), null, 2)
|
||||||
await activateAppearancePatentTask(taskId)
|
await activateAppearancePatentTask(taskId)
|
||||||
const result = await api.enqueue_json(payload)
|
const result = await api.enqueue_json(payload)
|
||||||
if (!result?.success) {
|
if (!result?.success) {
|
||||||
ElMessage.error(result?.error || '推送失败')
|
ElMessage.error(result?.error || '推送失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
queuedTaskSummary.value = toTaskSummary(currentParseResult)
|
||||||
addPollingTask(taskId)
|
addPollingTask(taskId)
|
||||||
clearParsedTask()
|
clearParsedTask()
|
||||||
await loadDashboard()
|
await loadDashboard()
|
||||||
@@ -721,9 +772,10 @@ onUnmounted(() => {
|
|||||||
.btn-run:disabled { opacity: .55; cursor: not-allowed; }
|
.btn-run:disabled { opacity: .55; cursor: not-allowed; }
|
||||||
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
|
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
|
||||||
.selected-files span { display: block; margin: 4px 0; }
|
.selected-files span { display: block; margin: 4px 0; }
|
||||||
.prompt-card { margin-bottom: 18px; }
|
.prompt-card, .secret-card { margin-bottom: 18px; }
|
||||||
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
|
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
|
||||||
.prompt-input:focus { border-color: #3498db; }
|
.secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; outline: none; }
|
||||||
|
.prompt-input:focus, .secret-input:focus { border-color: #3498db; }
|
||||||
.prompt-default-label { margin-top: 10px; color: #8d8d8d; font-size: 12px; }
|
.prompt-default-label { margin-top: 10px; color: #8d8d8d; font-size: 12px; }
|
||||||
.prompt-preview { margin-top: 10px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #9ea7b3; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
|
.prompt-preview { margin-top: 10px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #9ea7b3; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
|
||||||
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
|
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<aside class="left-panel">
|
<aside class="left-panel">
|
||||||
<div class="section-title">上传文件</div>
|
<div class="section-title">上传文件</div>
|
||||||
<div class="upload-zone">
|
<div class="upload-zone">
|
||||||
<div class="hint">选择 Excel 后点击解析,后端会提取 id、ASIN、国家,并保留整数 id 与子数据的第一条。</div>
|
<div class="hint">选择 Excel 后点击解析,后端会提取 id、ASIN、国家、价格,每一行都会独立送检。</div>
|
||||||
<div class="btns">
|
<div class="btns">
|
||||||
<button type="button" class="opt-btn" @click="selectFiles">选择 Excel</button>
|
<button type="button" class="opt-btn" @click="selectFiles">选择 Excel</button>
|
||||||
<button type="button" class="opt-btn" @click="selectFolder">选择文件夹</button>
|
<button type="button" class="opt-btn" @click="selectFolder">选择文件夹</button>
|
||||||
@@ -24,6 +24,11 @@
|
|||||||
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
|
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="secret-card">
|
||||||
|
<div class="section-title">密钥</div>
|
||||||
|
<input v-model="cozeApiKey" class="secret-input" type="password" autocomplete="off" spellcheck="false" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="run-row">
|
<div class="run-row">
|
||||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||||
@@ -32,11 +37,11 @@
|
|||||||
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
|
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="loading-msg">Python 回传字段按 asin、国家、url、标题提交;后端接收分片后按 10 条一批调用 Coze。</p>
|
<p class="loading-msg">Python 回传字段按 id、asin、国家、价格、标题、图片链接数组提交;后端接收分片后按 10 条一批送检。</p>
|
||||||
|
|
||||||
<div v-if="parseResult" class="parse-card">
|
<div v-if="visibleTaskSummary" class="parse-card">
|
||||||
<div>任务 ID:{{ parseResult.taskId }}</div>
|
<div>任务 ID:{{ visibleTaskSummary.taskId }}</div>
|
||||||
<div>总行数:{{ parseResult.totalRows }},有效:{{ parseResult.acceptedRows }},分组:{{ parseResult.groupCount || 0 }},过滤:{{ parseResult.droppedRows }}</div>
|
<div>总行数:{{ visibleTaskSummary.totalRows }},有效:{{ visibleTaskSummary.acceptedRows }},过滤:{{ visibleTaskSummary.droppedRows }}</div>
|
||||||
</div>
|
</div>
|
||||||
<pre v-if="queuePayloadText" class="queue-payload">{{ queuePayloadText }}</pre>
|
<pre v-if="queuePayloadText" class="queue-payload">{{ queuePayloadText }}</pre>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -67,13 +72,14 @@
|
|||||||
<div class="result-list-wrap preview-wrap">
|
<div class="result-list-wrap preview-wrap">
|
||||||
<div class="result-list-header">
|
<div class="result-list-header">
|
||||||
<span>解析结果</span>
|
<span>解析结果</span>
|
||||||
<span v-if="parsedRows.length" class="muted">显示首组 {{ previewRows.length }} 条 / 共 {{ parseResult?.groupCount || parsedGroups.length }} 组,{{ parsedRows.length }} 条</span>
|
<span v-if="parsedRows.length" class="muted">共 {{ parsedRows.length }} 条</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!parsedRows.length" class="empty-tasks">暂无解析结果</div>
|
<div v-if="!parsedRows.length" class="empty-tasks">暂无解析结果</div>
|
||||||
<el-table v-else :data="previewRows" height="120" class="result-table">
|
<el-table v-else :data="previewRows" height="120" class="result-table">
|
||||||
<el-table-column prop="displayId" label="ID" width="90" />
|
<el-table-column prop="displayId" label="ID" width="90" />
|
||||||
<el-table-column prop="asin" label="ASIN" width="130" />
|
<el-table-column prop="asin" label="ASIN" width="130" />
|
||||||
<el-table-column prop="country" label="国家" width="100" />
|
<el-table-column prop="country" label="国家" width="100" />
|
||||||
|
<el-table-column prop="price" label="价格" width="90" />
|
||||||
<el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column prop="url" label="URL" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="url" label="URL" min-width="160" show-overflow-tooltip />
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -171,13 +177,14 @@ const selectedFileNames = ref<string[]>([])
|
|||||||
const uploadedFiles = ref<UploadFileVo[]>([])
|
const uploadedFiles = ref<UploadFileVo[]>([])
|
||||||
const parseResult = ref<SimilarAsinParseVo | null>(null)
|
const parseResult = ref<SimilarAsinParseVo | null>(null)
|
||||||
const parsedGroups = ref<SimilarAsinParsedGroup[]>([])
|
const parsedGroups = ref<SimilarAsinParsedGroup[]>([])
|
||||||
const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。
|
type TaskSummary = Pick<SimilarAsinParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
|
||||||
请直接给出明确结论(有侵权风险 或 未发现明显侵权风险),并严格按照以下三个维度提供精简的排查理由:
|
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||||
外观维度: 评估产品外形、图案设计是否与欧洲/英国常见外观专利雷同。
|
const defaultAiPrompt = `1. 图案与形象限制:不含各国国旗及类似图案,无各国领导人画像;不含圣诞老人形象;排除迪士尼、漫威、三丽鸥等所有指定动漫IP的图案与关联物品,且不涉及知名油画作品图案。
|
||||||
专利维度: 评估产品的核心技术或物理结构是否存在侵权可能。
|
2. 品类排除:非图书、刀具、服装;不含药品、带容器的粉末及液体;与吸烟行为无关,无医疗标志。
|
||||||
标题维度: 排查标题中是否包含大牌商标、敏感词或版权保护词汇。
|
3. 功能与配件要求:不可充电且不带电池,无磁吸功能;配件多的情况下需完全匹配。
|
||||||
结论:XX`
|
4. 规格与外观限制:不含体积大的家具类物品(如椅子、桌子、大镜子、柜子);不带任何品牌logo;无多种款式可选。`
|
||||||
const aiPrompt = ref('')
|
const aiPrompt = ref('')
|
||||||
|
const cozeApiKey = ref('')
|
||||||
const parsing = ref(false)
|
const parsing = ref(false)
|
||||||
const pushing = ref(false)
|
const pushing = ref(false)
|
||||||
const queuePayloadText = ref('')
|
const queuePayloadText = ref('')
|
||||||
@@ -200,9 +207,12 @@ const dashboard = ref<SimilarAsinDashboardVo>({
|
|||||||
const historyItems = ref<SimilarAsinHistoryItem[]>([])
|
const historyItems = ref<SimilarAsinHistoryItem[]>([])
|
||||||
|
|
||||||
const parsedRows = computed<SimilarAsinParsedRow[]>(() =>
|
const parsedRows = computed<SimilarAsinParsedRow[]>(() =>
|
||||||
parsedGroups.value.flatMap((group) => group.items || []),
|
parseResult.value?.items?.length
|
||||||
|
? parseResult.value.items
|
||||||
|
: parsedGroups.value.flatMap((group) => group.items || []),
|
||||||
)
|
)
|
||||||
const previewRows = computed(() => parsedGroups.value[0]?.items || [])
|
const previewRows = computed(() => parsedRows.value)
|
||||||
|
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
|
||||||
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
|
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
|
||||||
const historyOnlyItems = computed(() =>
|
const historyOnlyItems = computed(() =>
|
||||||
historyItems.value.filter((i) => {
|
historyItems.value.filter((i) => {
|
||||||
@@ -215,6 +225,28 @@ function effectiveAiPrompt() {
|
|||||||
return aiPrompt.value.trim() || defaultAiPrompt
|
return aiPrompt.value.trim() || defaultAiPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function effectiveCozeApiKey() {
|
||||||
|
return cozeApiKey.value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskSecret(secret: string) {
|
||||||
|
if (!secret) return ''
|
||||||
|
if (secret.length <= 10) return '***'
|
||||||
|
return `${secret.slice(0, 6)}***${secret.slice(-4)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function payloadForDisplay<T extends { data?: Record<string, unknown> }>(payload: T) {
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
data: payload.data
|
||||||
|
? {
|
||||||
|
...payload.data,
|
||||||
|
api_key: maskSecret(String(payload.data.api_key || '')),
|
||||||
|
}
|
||||||
|
: payload.data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function uidForStorage() {
|
function uidForStorage() {
|
||||||
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
||||||
}
|
}
|
||||||
@@ -223,6 +255,16 @@ function pollingKey() {
|
|||||||
return `similar-asin:tasks:${uidForStorage()}`
|
return `similar-asin:tasks:${uidForStorage()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toTaskSummary(result: SimilarAsinParseVo): TaskSummary {
|
||||||
|
return {
|
||||||
|
taskId: result.taskId,
|
||||||
|
totalRows: result.totalRows,
|
||||||
|
acceptedRows: result.acceptedRows,
|
||||||
|
groupCount: result.groupCount,
|
||||||
|
droppedRows: result.droppedRows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function savePollingIds() {
|
function savePollingIds() {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
const ids = Array.from(new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value]))
|
const ids = Array.from(new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value]))
|
||||||
@@ -274,6 +316,7 @@ async function selectFiles() {
|
|||||||
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
||||||
parseResult.value = null
|
parseResult.value = null
|
||||||
parsedGroups.value = []
|
parsedGroups.value = []
|
||||||
|
queuedTaskSummary.value = null
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +339,7 @@ async function selectFolder() {
|
|||||||
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||||
parseResult.value = null
|
parseResult.value = null
|
||||||
parsedGroups.value = []
|
parsedGroups.value = []
|
||||||
|
queuedTaskSummary.value = null
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -308,6 +352,10 @@ async function parseFiles() {
|
|||||||
ElMessage.warning('请先选择 Excel')
|
ElMessage.warning('请先选择 Excel')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!effectiveCozeApiKey()) {
|
||||||
|
ElMessage.warning('请先填写密钥')
|
||||||
|
return
|
||||||
|
}
|
||||||
parsing.value = true
|
parsing.value = true
|
||||||
try {
|
try {
|
||||||
const files: UploadedFileRef[] = uploadedFiles.value.map((f) => ({
|
const files: UploadedFileRef[] = uploadedFiles.value.map((f) => ({
|
||||||
@@ -315,23 +363,22 @@ async function parseFiles() {
|
|||||||
originalFilename: f.originalFilename,
|
originalFilename: f.originalFilename,
|
||||||
relativePath: f.relativePath,
|
relativePath: f.relativePath,
|
||||||
}))
|
}))
|
||||||
const res = await parseSimilarAsin(files, effectiveAiPrompt())
|
const res = await parseSimilarAsin(files, effectiveAiPrompt(), effectiveCozeApiKey())
|
||||||
parseResult.value = res
|
parseResult.value = res
|
||||||
parsedGroups.value = res.groups?.length
|
queuedTaskSummary.value = null
|
||||||
? res.groups
|
parsedGroups.value = res.items?.length
|
||||||
: (res.items?.length
|
|
||||||
? [{
|
? [{
|
||||||
sourceFileKey: res.items[0]?.sourceFileKey,
|
sourceFileKey: res.items[0]?.sourceFileKey,
|
||||||
sourceFilename: res.items[0]?.sourceFilename,
|
sourceFilename: res.items[0]?.sourceFilename,
|
||||||
groupKey: res.items[0]?.groupKey,
|
groupKey: `similar-asin:${res.taskId}`,
|
||||||
baseId: '',
|
baseId: '',
|
||||||
displayId: res.items[0]?.displayId,
|
displayId: res.items[0]?.displayId,
|
||||||
itemCount: res.items.length,
|
itemCount: res.items.length,
|
||||||
items: res.items,
|
items: res.items,
|
||||||
}]
|
}]
|
||||||
: [])
|
: []
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
ElMessage.success(`解析完成,共 ${res.groupCount || 0} 组 / ${res.acceptedRows} 条`)
|
ElMessage.success(`解析完成,共 ${res.acceptedRows} 条`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e instanceof Error ? e.message : '解析失败')
|
ElMessage.error(e instanceof Error ? e.message : '解析失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -341,7 +388,8 @@ async function parseFiles() {
|
|||||||
|
|
||||||
async function pushToPythonQueue() {
|
async function pushToPythonQueue() {
|
||||||
const api = getPywebviewApi()
|
const api = getPywebviewApi()
|
||||||
const taskId = parseResult.value?.taskId
|
const currentParseResult = parseResult.value
|
||||||
|
const taskId = currentParseResult?.taskId
|
||||||
if (!taskId || !parsedRows.value.length) {
|
if (!taskId || !parsedRows.value.length) {
|
||||||
ElMessage.warning('请先解析文件')
|
ElMessage.warning('请先解析文件')
|
||||||
return
|
return
|
||||||
@@ -350,6 +398,10 @@ async function pushToPythonQueue() {
|
|||||||
ElMessage.error('当前环境未启用 pywebview enqueue_json')
|
ElMessage.error('当前环境未启用 pywebview enqueue_json')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!effectiveCozeApiKey()) {
|
||||||
|
ElMessage.warning('请先填写密钥')
|
||||||
|
return
|
||||||
|
}
|
||||||
pushing.value = true
|
pushing.value = true
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -357,7 +409,8 @@ async function pushToPythonQueue() {
|
|||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
data: {
|
data: {
|
||||||
taskId,
|
taskId,
|
||||||
prompt: parseResult.value?.aiPrompt || effectiveAiPrompt(),
|
prompt: currentParseResult.aiPrompt || effectiveAiPrompt(),
|
||||||
|
api_key: effectiveCozeApiKey(),
|
||||||
groups: parsedGroups.value.map((group) => ({
|
groups: parsedGroups.value.map((group) => ({
|
||||||
sourceFileKey: group.sourceFileKey || '',
|
sourceFileKey: group.sourceFileKey || '',
|
||||||
sourceFilename: group.sourceFilename || '',
|
sourceFilename: group.sourceFilename || '',
|
||||||
@@ -372,8 +425,10 @@ async function pushToPythonQueue() {
|
|||||||
id: row.displayId,
|
id: row.displayId,
|
||||||
asin: row.asin,
|
asin: row.asin,
|
||||||
country: row.country,
|
country: row.country,
|
||||||
|
price: row.price || '',
|
||||||
url: row.url || '',
|
url: row.url || '',
|
||||||
title: row.title || '',
|
title: row.title || '',
|
||||||
|
target_urls: [],
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
rows: parsedRows.value.map((row) => ({
|
rows: parsedRows.value.map((row) => ({
|
||||||
@@ -384,18 +439,21 @@ async function pushToPythonQueue() {
|
|||||||
id: row.displayId,
|
id: row.displayId,
|
||||||
asin: row.asin,
|
asin: row.asin,
|
||||||
country: row.country,
|
country: row.country,
|
||||||
|
price: row.price || '',
|
||||||
url: row.url || '',
|
url: row.url || '',
|
||||||
title: row.title || '',
|
title: row.title || '',
|
||||||
|
target_urls: [],
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
queuePayloadText.value = JSON.stringify(payloadForDisplay(payload), null, 2)
|
||||||
await activateSimilarAsinTask(taskId)
|
await activateSimilarAsinTask(taskId)
|
||||||
const result = await api.enqueue_json(payload)
|
const result = await api.enqueue_json(payload)
|
||||||
if (!result?.success) {
|
if (!result?.success) {
|
||||||
ElMessage.error(result?.error || '推送失败')
|
ElMessage.error(result?.error || '推送失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
queuedTaskSummary.value = toTaskSummary(currentParseResult)
|
||||||
addPollingTask(taskId)
|
addPollingTask(taskId)
|
||||||
clearParsedTask()
|
clearParsedTask()
|
||||||
await loadDashboard()
|
await loadDashboard()
|
||||||
@@ -721,9 +779,10 @@ onUnmounted(() => {
|
|||||||
.btn-run:disabled { opacity: .55; cursor: not-allowed; }
|
.btn-run:disabled { opacity: .55; cursor: not-allowed; }
|
||||||
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
|
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
|
||||||
.selected-files span { display: block; margin: 4px 0; }
|
.selected-files span { display: block; margin: 4px 0; }
|
||||||
.prompt-card { margin-bottom: 18px; }
|
.prompt-card, .secret-card { margin-bottom: 18px; }
|
||||||
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
|
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
|
||||||
.prompt-input:focus { border-color: #3498db; }
|
.secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; outline: none; }
|
||||||
|
.prompt-input:focus, .secret-input:focus { border-color: #3498db; }
|
||||||
.prompt-default-label { margin-top: 10px; color: #8d8d8d; font-size: 12px; }
|
.prompt-default-label { margin-top: 10px; color: #8d8d8d; font-size: 12px; }
|
||||||
.prompt-preview { margin-top: 10px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #9ea7b3; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
|
.prompt-preview { margin-top: 10px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #9ea7b3; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
|
||||||
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
|
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
|
||||||
|
|||||||
@@ -1531,15 +1531,16 @@ export interface AppearancePatentTaskBatchVo {
|
|||||||
missingTaskIds?: number[];
|
missingTaskIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseAppearancePatent(files: UploadedFileRef[], aiPrompt: string) {
|
export function parseAppearancePatent(files: UploadedFileRef[], aiPrompt: string, apiKey?: string) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<
|
post<
|
||||||
JavaApiResponse<AppearancePatentParseVo>,
|
JavaApiResponse<AppearancePatentParseVo>,
|
||||||
{ user_id: number; files: UploadedFileRef[]; ai_prompt: string }
|
{ user_id: number; files: UploadedFileRef[]; ai_prompt: string; api_key?: string }
|
||||||
>(`${JAVA_API_PREFIX}/appearance-patent/parse`, {
|
>(`${JAVA_API_PREFIX}/appearance-patent/parse`, {
|
||||||
user_id: getCurrentUserId(),
|
user_id: getCurrentUserId(),
|
||||||
files,
|
files,
|
||||||
ai_prompt: aiPrompt,
|
ai_prompt: aiPrompt,
|
||||||
|
api_key: apiKey,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1615,6 +1616,7 @@ export interface SimilarAsinParsedRow {
|
|||||||
groupKey?: string;
|
groupKey?: string;
|
||||||
asin: string;
|
asin: string;
|
||||||
country: string;
|
country: string;
|
||||||
|
price?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
@@ -1694,15 +1696,16 @@ export interface SimilarAsinTaskBatchVo {
|
|||||||
missingTaskIds?: number[];
|
missingTaskIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSimilarAsin(files: UploadedFileRef[], aiPrompt: string) {
|
export function parseSimilarAsin(files: UploadedFileRef[], aiPrompt: string, apiKey?: string) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<
|
post<
|
||||||
JavaApiResponse<SimilarAsinParseVo>,
|
JavaApiResponse<SimilarAsinParseVo>,
|
||||||
{ user_id: number; files: UploadedFileRef[]; ai_prompt: string }
|
{ user_id: number; files: UploadedFileRef[]; ai_prompt: string; api_key?: string }
|
||||||
>(`${JAVA_API_PREFIX}/similar-asin/parse`, {
|
>(`${JAVA_API_PREFIX}/similar-asin/parse`, {
|
||||||
user_id: getCurrentUserId(),
|
user_id: getCurrentUserId(),
|
||||||
files,
|
files,
|
||||||
ai_prompt: aiPrompt,
|
ai_prompt: aiPrompt,
|
||||||
|
api_key: apiKey,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>外观专利检测</title>
|
<title>外观专利检测</title>
|
||||||
<script type="module" crossorigin src="/assets/appearance-patent.js"></script>
|
<script type="module" crossorigin src="/assets/appearance-patent.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-RBTI4RuH.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-1lGM3Wzk.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-C-qhaDJK.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/appearance-patent-TI-osB39.css">
|
<link rel="stylesheet" crossorigin href="/assets/appearance-patent-DaODE9AU.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
18
new_web_source/similar-asin.html
Normal file
18
new_web_source/similar-asin.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>相似ASIN检测</title>
|
||||||
|
<script type="module" crossorigin src="/assets/similar-asin.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-RBTI4RuH.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/brand-1lGM3Wzk.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-C-qhaDJK.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/similar-asin-B2xrJRis.css">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user