提交优化更新

This commit is contained in:
super
2026-05-29 16:49:18 +08:00
parent 2ed1250604
commit 225d13fb6e
36 changed files with 370 additions and 220 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{bN as r}from"./java-modules-DzYrjxib.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e}; import{bN as r}from"./java-modules-B8c-YG5x.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.module-page[data-v-39de0e76]{min-height:100vh;background:#1a1a1a}.main-content[data-v-39de0e76]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-39de0e76]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-39de0e76]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-39de0e76],.subsection-title[data-v-39de0e76]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-39de0e76]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-39de0e76],.loading-msg[data-v-39de0e76],.files[data-v-39de0e76],.muted[data-v-39de0e76]{color:#888;font-size:12px;line-height:1.5}.link[data-v-39de0e76]{color:#6ea8fe;text-decoration:none}.link[data-v-39de0e76]:hover{color:#9fc5ff}.btns[data-v-39de0e76],.run-row[data-v-39de0e76]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-39de0e76],.btn-run[data-v-39de0e76],.btn-delete[data-v-39de0e76],.download[data-v-39de0e76]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-39de0e76]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-39de0e76]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-39de0e76]{background:#27ae60}.btn-run[data-v-39de0e76]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-39de0e76]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-39de0e76]{display:block;margin:4px 0}.parse-card[data-v-39de0e76],.queue-payload[data-v-39de0e76]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-39de0e76]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-39de0e76]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-39de0e76]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-39de0e76]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-39de0e76]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-39de0e76]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-39de0e76]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-39de0e76]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-39de0e76]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-39de0e76]{color:#666;font-size:13px;padding:18px;text-align:center}.task-list[data-v-39de0e76]{list-style:none;margin:0;padding:12px}.task-item[data-v-39de0e76]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid #2a2a2a;border-radius:8px;margin-bottom:8px;background:#222}.left[data-v-39de0e76]{flex:1;min-width:0}.id[data-v-39de0e76]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-39de0e76]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-39de0e76]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-39de0e76]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-39de0e76]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-39de0e76]{background:#3498db2e;color:#3498db}.status.pending[data-v-39de0e76]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-39de0e76]{margin-top:6px;color:#e0b96d}.file-progress[data-v-39de0e76]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-39de0e76]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-39de0e76]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-39de0e76]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.file-progress-count[data-v-39de0e76]{margin-top:4px;color:#858585;font-size:11px}.download[data-v-39de0e76]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-39de0e76]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-39de0e76]{flex-direction:column;height:auto}.left-panel[data-v-39de0e76]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-39de0e76]{grid-template-columns:repeat(2,minmax(0,1fr))}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>格式转换 - 数富AI</title> <title>格式转换 - 数富AI</title>
<script type="module" crossorigin src="/assets/convert.js"></script> <script type="module" crossorigin src="/assets/convert.js"></script>
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js"> <link rel="modulepreload" crossorigin href="/assets/java-modules-B8c-YG5x.js">
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js"> <link rel="modulepreload" crossorigin href="/assets/brand-COze15GJ.js">
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css"> <link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
<link rel="stylesheet" crossorigin href="/assets/convert-6oxZNMye.css"> <link rel="stylesheet" crossorigin href="/assets/convert-6oxZNMye.css">
</head> </head>

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>数据去重 - 数富AI</title> <title>数据去重 - 数富AI</title>
<script type="module" crossorigin src="/assets/dedupe.js"></script> <script type="module" crossorigin src="/assets/dedupe.js"></script>
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js"> <link rel="modulepreload" crossorigin href="/assets/java-modules-B8c-YG5x.js">
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js"> <link rel="modulepreload" crossorigin href="/assets/brand-COze15GJ.js">
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css"> <link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
<link rel="stylesheet" crossorigin href="/assets/dedupe-DpfPYQDv.css"> <link rel="stylesheet" crossorigin href="/assets/dedupe-DpfPYQDv.css">
</head> </head>

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>删除品牌 - 数富AI</title> <title>删除品牌 - 数富AI</title>
<script type="module" crossorigin src="/assets/delete-brand.js"></script> <script type="module" crossorigin src="/assets/delete-brand.js"></script>
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js"> <link rel="modulepreload" crossorigin href="/assets/java-modules-B8c-YG5x.js">
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js"> <link rel="modulepreload" crossorigin href="/assets/brand-COze15GJ.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/java-modules-l5anrOZ2.css"> <link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
<link rel="stylesheet" crossorigin href="/assets/delete-brand-DZ3x5hOX.css"> <link rel="stylesheet" crossorigin href="/assets/delete-brand-DZ3x5hOX.css">

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>数据拆分 - 数富AI</title> <title>数据拆分 - 数富AI</title>
<script type="module" crossorigin src="/assets/split.js"></script> <script type="module" crossorigin src="/assets/split.js"></script>
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js"> <link rel="modulepreload" crossorigin href="/assets/java-modules-B8c-YG5x.js">
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js"> <link rel="modulepreload" crossorigin href="/assets/brand-COze15GJ.js">
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css"> <link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
<link rel="stylesheet" crossorigin href="/assets/split-CR1PtDQE.css"> <link rel="stylesheet" crossorigin href="/assets/split-CR1PtDQE.css">
</head> </head>

View File

@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.collectdata.model.dto; package com.nanri.aiimage.modules.collectdata.model.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import com.fasterxml.jackson.annotation.JsonAlias;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -14,8 +15,16 @@ public class CollectDataFiltersDto {
@Schema(description = "金额(手动输入),可为空", example = "59.99") @Schema(description = "金额(手动输入),可为空", example = "59.99")
private BigDecimal amount; private BigDecimal amount;
@Schema(description = "排名(手动输入),可为空", example = "100000") @JsonAlias({"min_amount"})
private Integer rank; @Schema(description = "最小金额,可为空", example = "10.00")
private BigDecimal minAmount;
@JsonAlias({"max_amount"})
@Schema(description = "最大金额,可为空", example = "59.99")
private BigDecimal maxAmount;
@Schema(description = "是否启用排名条件")
private Boolean rank;
@Schema(description = "是否包含 FBA 商品") @Schema(description = "是否包含 FBA 商品")
private Boolean fba; private Boolean fba;
@@ -25,4 +34,8 @@ public class CollectDataFiltersDto {
@Schema(description = "国家代码列表,按用户拖拽顺序保存", example = "[\"DE\",\"UK\"]") @Schema(description = "国家代码列表,按用户拖拽顺序保存", example = "[\"DE\",\"UK\"]")
private List<String> countryCodes = new ArrayList<>(); private List<String> countryCodes = new ArrayList<>();
@JsonAlias({"country_code"})
@Schema(description = "单选国家代码,采集数据页面当前只选择一个国家", example = "DE")
private String countryCode;
} }

View File

@@ -42,6 +42,10 @@ public class SimilarAsinCozeClient {
private volatile RestClient sharedRestClient; private volatile RestClient sharedRestClient;
public List<SimilarAsinResultRowDto> inspect(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) { public List<SimilarAsinResultRowDto> inspect(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
return inspect(rows, prompt, apiKey, false);
}
public List<SimilarAsinResultRowDto> inspect(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) {
if (rows == null || rows.isEmpty()) { if (rows == null || rows.isEmpty()) {
return List.of(); return List.of();
} }
@@ -50,7 +54,7 @@ public class SimilarAsinCozeClient {
return rows.stream().map(this::copy).toList(); return rows.stream().map(this::copy).toList();
} }
try { try {
return inspectWithFallback(rows, prompt, apiKey); return inspectWithFallback(rows, prompt, apiKey, imgSwitch);
} 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);
@@ -59,15 +63,20 @@ public class SimilarAsinCozeClient {
} }
public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception { public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
return submitWorkflow(rows, prompt, apiKey, nextCredential()); return submitWorkflow(rows, prompt, apiKey, false, nextCredential());
}
public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) throws Exception {
return submitWorkflow(rows, prompt, apiKey, imgSwitch, nextCredential());
} }
public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows, public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows,
String prompt, String prompt,
String apiKey, String apiKey,
boolean imgSwitch,
CozeCredentialRef credential) throws Exception { CozeCredentialRef credential) throws Exception {
CozeCredentialRef resolvedCredential = resolveCredential(credential); CozeCredentialRef resolvedCredential = resolveCredential(credential);
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, resolvedCredential)); JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, imgSwitch, resolvedCredential));
ensureSuccess(submitRoot); ensureSuccess(submitRoot);
return new CozeSubmitResponse( return new CozeSubmitResponse(
extractExecuteId(submitRoot), extractExecuteId(submitRoot),
@@ -109,12 +118,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, String apiKey) { private List<SimilarAsinResultRowDto> inspectWithFallback(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) {
try { try {
if (rows.size() == 1) { if (rows.size() == 1) {
return inspectSingleRowWithRetry(rows, prompt, apiKey); return inspectSingleRowWithRetry(rows, prompt, apiKey, imgSwitch);
} }
InspectAttempt attempt = inspectOnce(rows, prompt, apiKey); InspectAttempt attempt = inspectOnce(rows, prompt, apiKey, imgSwitch);
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());
} }
@@ -125,17 +134,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, apiKey)); merged.addAll(inspectPartitionWithFailureFallback(rows.subList(0, middle), prompt, apiKey, imgSwitch));
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(middle, rows.size()), prompt, apiKey)); merged.addAll(inspectPartitionWithFailureFallback(rows.subList(middle, rows.size()), prompt, apiKey, imgSwitch));
return merged; return merged;
} }
throw propagate(ex); throw propagate(ex);
} }
} }
private List<SimilarAsinResultRowDto> inspectPartitionWithFailureFallback(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) { private List<SimilarAsinResultRowDto> inspectPartitionWithFailureFallback(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) {
try { try {
return inspectWithFallback(rows, prompt, apiKey); return inspectWithFallback(rows, prompt, apiKey, imgSwitch);
} 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);
@@ -143,12 +152,12 @@ public class SimilarAsinCozeClient {
} }
} }
private List<SimilarAsinResultRowDto> inspectSingleRowWithRetry(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception { private List<SimilarAsinResultRowDto> inspectSingleRowWithRetry(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) 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, apiKey); InspectAttempt attempt = inspectOnce(rows, prompt, apiKey, imgSwitch);
if (attempt.resolvedCount() == rows.size()) { if (attempt.resolvedCount() == rows.size()) {
return attempt.mergedRows(); return attempt.mergedRows();
} }
@@ -179,16 +188,16 @@ 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, String apiKey) throws Exception { private InspectAttempt inspectOnce(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) throws Exception {
String raw = runWorkflowAsyncAndWait(rows, prompt, apiKey); String raw = runWorkflowAsyncAndWait(rows, prompt, apiKey, imgSwitch);
List<CozeResult> results = parseResults(raw); List<CozeResult> results = parseResults(raw);
List<SimilarAsinResultRowDto> merged = mergeRows(rows, results); List<SimilarAsinResultRowDto> merged = mergeRows(rows, results);
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, String apiKey) throws Exception { private String runWorkflowAsyncAndWait(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) throws Exception {
CozeCredentialRef credential = nextCredential(); CozeCredentialRef credential = nextCredential();
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, credential)); JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, imgSwitch, credential));
ensureSuccess(submitRoot); ensureSuccess(submitRoot);
String immediateData = extractResultDataText(submitRoot); String immediateData = extractResultDataText(submitRoot);
@@ -232,8 +241,9 @@ public class SimilarAsinCozeClient {
private String postWorkflow(List<SimilarAsinResultRowDto> rows, private String postWorkflow(List<SimilarAsinResultRowDto> rows,
String prompt, String prompt,
String apiKey, String apiKey,
boolean imgSwitch,
CozeCredentialRef credential) { CozeCredentialRef credential) {
Map<String, Object> parameters = buildParameters(rows, prompt, apiKey); Map<String, Object> parameters = buildParameters(rows, prompt, apiKey, imgSwitch);
Map<String, Object> body = new LinkedHashMap<>(); Map<String, Object> body = new LinkedHashMap<>();
body.put("workflow_id", credential.workflowId()); body.put("workflow_id", credential.workflowId());
body.put("parameters", parameters); body.put("parameters", parameters);
@@ -354,7 +364,7 @@ public class SimilarAsinCozeClient {
return credentials; return credentials;
} }
private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) { private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) {
List<String> asins = rows.stream().map(row -> safeText(row.getAsin())).toList(); List<String> asins = rows.stream().map(row -> safeText(row.getAsin())).toList();
List<String> titles = rows.stream().map(row -> safeText(firstNonBlank(row.getTitle(), row.getAsin()))).toList(); List<String> titles = rows.stream().map(row -> safeText(firstNonBlank(row.getTitle(), row.getAsin()))).toList();
List<String> skus = rows.stream().map(row -> safeText(row.getSku())).toList(); List<String> skus = rows.stream().map(row -> safeText(row.getSku())).toList();
@@ -375,6 +385,7 @@ public class SimilarAsinCozeClient {
if (includeLegacyApiKey && apiKey != null && !apiKey.isBlank()) { if (includeLegacyApiKey && apiKey != null && !apiKey.isBlank()) {
parameters.put("api_key", apiKey.trim()); parameters.put("api_key", apiKey.trim());
} }
parameters.put("img_switch", imgSwitch);
return parameters; return parameters;
} }

View File

@@ -32,4 +32,9 @@ public class SimilarAsinParseRequest {
@Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥。") @Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥。")
@NotBlank(message = "密钥不能为空") @NotBlank(message = "密钥不能为空")
private String apiKey; private String apiKey;
@JsonProperty("img_switch")
@JsonAlias({"imgSwitch"})
@Schema(description = "传递给 Coze workflow parameters.img_switch 的图片检测开关true 为开启false 为关闭。")
private Boolean imgSwitch = Boolean.FALSE;
} }

View File

@@ -17,6 +17,9 @@ public class SimilarAsinParsedPayloadDto {
@Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥") @Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥")
private String apiKey; private String apiKey;
@Schema(description = "传递给 Coze workflow parameters.img_switch 的图片检测开关")
private Boolean imgSwitch = Boolean.FALSE;
@Schema(description = "本次解析的源文件列表") @Schema(description = "本次解析的源文件列表")
private List<SimilarAsinSourceFileDto> sourceFiles = new ArrayList<>(); private List<SimilarAsinSourceFileDto> sourceFiles = new ArrayList<>();

View File

@@ -33,6 +33,9 @@ public class SimilarAsinParseVo {
@Schema(description = "本任务最终使用的 AI 提示词") @Schema(description = "本任务最终使用的 AI 提示词")
private String aiPrompt; private String aiPrompt;
@Schema(description = "图片检测开关true 为开启false 为关闭")
private Boolean imgSwitch = Boolean.FALSE;
@Schema(description = "兼容旧前端的平铺有效行列表,现为全部有效行") @Schema(description = "兼容旧前端的平铺有效行列表,现为全部有效行")
private List<SimilarAsinParsedRowVo> items = new ArrayList<>(); private List<SimilarAsinParsedRowVo> items = new ArrayList<>();

View File

@@ -401,11 +401,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(), request.getApiKey(), sourceFiles, mergedHeaders, groups, allRows); String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), request.getApiKey(), request.getImgSwitch(), 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(), request.getApiKey(), sourceFiles, parsedPayloadPointer)); task.setResultJson(buildTaskResultJson(request.getAiPrompt(), request.getApiKey(), request.getImgSwitch(), sourceFiles, parsedPayloadPointer));
task.setUpdatedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now());
fileTaskMapper.updateById(task); fileTaskMapper.updateById(task);
@@ -444,6 +444,7 @@ public class SimilarAsinTaskService {
vo.setDroppedRows(droppedRows); vo.setDroppedRows(droppedRows);
vo.setGroupCount(groups.size()); vo.setGroupCount(groups.size());
vo.setAiPrompt(normalize(request.getAiPrompt())); vo.setAiPrompt(normalize(request.getAiPrompt()));
vo.setImgSwitch(Boolean.TRUE.equals(request.getImgSwitch()));
vo.setItems(new ArrayList<>(allRows)); vo.setItems(new ArrayList<>(allRows));
vo.setGroups(groups); vo.setGroups(groups);
long finishedAt = System.nanoTime(); long finishedAt = System.nanoTime();
@@ -1229,10 +1230,11 @@ public class SimilarAsinTaskService {
} }
String prompt = readAiPrompt(task); String prompt = readAiPrompt(task);
String apiKey = readApiKey(task); String apiKey = readApiKey(task);
boolean imgSwitch = readImgSwitch(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, apiKey)); result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt, apiKey, imgSwitch));
if (progressHook != null) { if (progressHook != null) {
progressHook.run(); progressHook.run();
} }
@@ -1543,6 +1545,14 @@ public class SimilarAsinTaskService {
} }
} }
private boolean readImgSwitch(FileTaskEntity task) {
try {
return Boolean.TRUE.equals(readParsedPayload(task).getImgSwitch());
} catch (Exception ignored) {
return false;
}
}
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;
@@ -1910,6 +1920,7 @@ public class SimilarAsinTaskService {
} }
String prompt = readAiPrompt(task); String prompt = readAiPrompt(task);
String apiKey = readApiKey(task); String apiKey = readApiKey(task);
boolean imgSwitch = readImgSwitch(task);
int batchSize = Math.max(1, properties.getCozeBatchSize()); int batchSize = Math.max(1, properties.getCozeBatchSize());
// P1-1检测到 720712008 风暴时强制把 batch 降到 1隔离毒行 // P1-1检测到 720712008 风暴时强制把 batch 降到 1隔离毒行
// 持续 5+ 次提交命中率 ≥ 40% 才会触发,正常波动不影响吞吐。 // 持续 5+ 次提交命中率 ≥ 40% 才会触发,正常波动不影响吞吐。
@@ -1974,7 +1985,7 @@ public class SimilarAsinTaskService {
int batchIndex = 1; int batchIndex = 1;
for (int i = 0; i < submitLimit; i += batchSize) { for (int i = 0; i < submitLimit; i += batchSize) {
List<CozeCandidate> batchCandidates = readyCandidates.subList(i, Math.min(i + batchSize, submitLimit)); List<CozeCandidate> batchCandidates = readyCandidates.subList(i, Math.min(i + batchSize, submitLimit));
pending |= submitCozeBatch(task, result, job, batchCandidates, batchIndex, batchTotal, prompt, apiKey, allRowsByBaseId); pending |= submitCozeBatch(task, result, job, batchCandidates, batchIndex, batchTotal, prompt, apiKey, imgSwitch, allRowsByBaseId);
batchIndex++; batchIndex++;
} }
return pending || countPendingCozeStates(task.getId()) > 0; return pending || countPendingCozeStates(task.getId()) > 0;
@@ -2089,6 +2100,7 @@ public class SimilarAsinTaskService {
int batchTotal, int batchTotal,
String prompt, String prompt,
String apiKey, String apiKey,
boolean imgSwitch,
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId) { Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId) {
if (batchCandidates == null || batchCandidates.isEmpty()) { if (batchCandidates == null || batchCandidates.isEmpty()) {
return false; return false;
@@ -2115,7 +2127,7 @@ public class SimilarAsinTaskService {
SimilarAsinCozeClient.CozeCredentialRef credential = cozeClient.nextCredential(); SimilarAsinCozeClient.CozeCredentialRef credential = cozeClient.nextCredential();
try { try {
SimilarAsinCozeClient.CozeSubmitResponse submit = submitCozeWorkflowThrottled( SimilarAsinCozeClient.CozeSubmitResponse submit = submitCozeWorkflowThrottled(
batchRows, prompt, apiKey, credential, true); batchRows, prompt, apiKey, imgSwitch, credential, true);
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());
String emptyResultMessage = emptyCozeResultMessage(cozeRows, batchRows.size()); String emptyResultMessage = emptyCozeResultMessage(cozeRows, batchRows.size());
@@ -2426,7 +2438,7 @@ public class SimilarAsinTaskService {
} }
try { try {
SimilarAsinCozeClient.CozeSubmitResponse submit = SimilarAsinCozeClient.CozeSubmitResponse submit =
submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task), submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task), readImgSwitch(task),
cozeClient.credentialByName(context.credentialName()), false); cozeClient.credentialByName(context.credentialName()), false);
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = allRowsByBaseIdForPoll(task); Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = allRowsByBaseIdForPoll(task);
if (submit.immediateData() != null && !submit.immediateData().isBlank()) { if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
@@ -2488,6 +2500,7 @@ public class SimilarAsinTaskService {
List<SimilarAsinResultRowDto> rows, List<SimilarAsinResultRowDto> rows,
String prompt, String prompt,
String apiKey, String apiKey,
boolean imgSwitch,
SimilarAsinCozeClient.CozeCredentialRef credential, SimilarAsinCozeClient.CozeCredentialRef credential,
boolean allowCredentialFallback) throws Exception { boolean allowCredentialFallback) throws Exception {
int attempts = allowCredentialFallback ? Math.max(1, cozeClient.configuredCredentialCount()) : 1; int attempts = allowCredentialFallback ? Math.max(1, cozeClient.configuredCredentialCount()) : 1;
@@ -2521,7 +2534,7 @@ public class SimilarAsinTaskService {
} }
try (lockHandle; borrowedCredential) { try (lockHandle; borrowedCredential) {
SimilarAsinCozeClient.CozeSubmitResponse response = SimilarAsinCozeClient.CozeSubmitResponse response =
cozeClient.submitWorkflow(rows, prompt, apiKey, currentCredential); cozeClient.submitWorkflow(rows, prompt, apiKey, imgSwitch, currentCredential);
// 提交完成立即记录时间戳,用于下次进入循环时计算节流等待。 // 提交完成立即记录时间戳,用于下次进入循环时计算节流等待。
lastCozeSubmitAtByCredential.put(credentialKey, System.currentTimeMillis()); lastCozeSubmitAtByCredential.put(credentialKey, System.currentTimeMillis());
return response; return response;
@@ -2713,7 +2726,7 @@ public class SimilarAsinTaskService {
int partIndex = 1; int partIndex = 1;
for (List<SimilarAsinResultRowDto> partRows : partitions) { for (List<SimilarAsinResultRowDto> partRows : partitions) {
SimilarAsinCozeClient.CozeSubmitResponse submit = SimilarAsinCozeClient.CozeSubmitResponse submit =
submitCozeWorkflowThrottled(partRows, readAiPrompt(task), readApiKey(task), submitCozeWorkflowThrottled(partRows, readAiPrompt(task), readApiKey(task), readImgSwitch(task),
cozeClient.credentialByName(context.credentialName()), false); cozeClient.credentialByName(context.credentialName()), false);
if (submit.immediateData() != null && !submit.immediateData().isBlank()) { if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
List<SimilarAsinResultRowDto> cozeRows = List<SimilarAsinResultRowDto> cozeRows =
@@ -2847,7 +2860,7 @@ public class SimilarAsinTaskService {
try { try {
taskFileJobService.touchRunning(context.jobId()); taskFileJobService.touchRunning(context.jobId());
SimilarAsinCozeClient.CozeSubmitResponse submit = SimilarAsinCozeClient.CozeSubmitResponse submit =
submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task), submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task), readImgSwitch(task),
cozeClient.credentialByName(context.credentialName()), false); cozeClient.credentialByName(context.credentialName()), false);
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = allRowsByBaseIdForPoll(task); Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = allRowsByBaseIdForPoll(task);
if (submit.immediateData() != null && !submit.immediateData().isBlank()) { if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
@@ -3691,13 +3704,26 @@ public class SimilarAsinTaskService {
if (taskId == null || taskId <= 0) { if (taskId == null || taskId <= 0) {
return false; return false;
} }
Long count = taskScopeStateMapper.selectCount(new LambdaQueryWrapper<TaskScopeStateEntity>() List<TaskScopeStateEntity> states = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
.eq(TaskScopeStateEntity::getTaskId, taskId) .eq(TaskScopeStateEntity::getTaskId, taskId)
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE) .eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
.isNull(TaskScopeStateEntity::getCozeStatus) .isNull(TaskScopeStateEntity::getCozeStatus)
.isNotNull(TaskScopeStateEntity::getLastChunkAt) .isNotNull(TaskScopeStateEntity::getLastChunkAt));
.eq(TaskScopeStateEntity::getCompleted, 1)); if (states == null || states.isEmpty()) {
return count != null && count > 0; return false;
}
boolean hasCompletedScope = false;
for (TaskScopeStateEntity state : states) {
if (state == null) {
continue;
}
if (Integer.valueOf(1).equals(state.getCompleted())) {
hasCompletedScope = true;
continue;
}
return false;
}
return hasCompletedScope;
} }
private boolean isJavaSideProcessing(Long taskId) { private boolean isJavaSideProcessing(Long taskId) {
@@ -3737,6 +3763,9 @@ public class SimilarAsinTaskService {
SimilarAsinResultRowDto row) { SimilarAsinResultRowDto row) {
} }
private record PythonUploadProgress(int current, int total, String unit) {
}
private String buildTaskOwnerScopeKey(FileTaskEntity task) { private String buildTaskOwnerScopeKey(FileTaskEntity task) {
Long taskId = task == null ? null : task.getId(); Long taskId = task == null ? null : task.getId();
return "task:" + taskId + ":owner:" + firstNonBlank(ownerFromTask(task), currentInstanceId()); return "task:" + taskId + ":owner:" + firstNonBlank(ownerFromTask(task), currentInstanceId());
@@ -4812,8 +4841,8 @@ public class SimilarAsinTaskService {
vo.setSourceFilename(row.getSourceFilename()); vo.setSourceFilename(row.getSourceFilename());
vo.setResultFilename(row.getResultFilename()); vo.setResultFilename(row.getResultFilename());
vo.setDownloadUrl(buildFreshDownloadUrl(row)); vo.setDownloadUrl(buildFreshDownloadUrl(row));
attachFileJobState(vo, row, job);
vo.setTaskStatus(task == null ? null : task.getStatus()); vo.setTaskStatus(task == null ? null : task.getStatus());
attachFileJobState(vo, row, job, task);
vo.setSuccess(row.getSuccess() != null && row.getSuccess() == 1); vo.setSuccess(row.getSuccess() != null && row.getSuccess() == 1);
vo.setError(row.getErrorMessage()); vo.setError(row.getErrorMessage());
vo.setRowCount(row.getRowCount()); vo.setRowCount(row.getRowCount());
@@ -4838,20 +4867,20 @@ public class SimilarAsinTaskService {
} }
} }
private void attachFileJobState(SimilarAsinHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job) { private void attachFileJobState(SimilarAsinHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job, FileTaskEntity task) {
vo.setFileReady(row.getResultFileUrl() != null && !row.getResultFileUrl().isBlank()); vo.setFileReady(row.getResultFileUrl() != null && !row.getResultFileUrl().isBlank());
if (job == null) { if (job == null) {
vo.setFileStatus(Boolean.TRUE.equals(vo.getFileReady()) ? "SUCCESS" : null); vo.setFileStatus(Boolean.TRUE.equals(vo.getFileReady()) ? "SUCCESS" : null);
attachFileProgress(vo, row, job); attachFileProgress(vo, row, job, task);
return; return;
} }
vo.setFileJobId(job.getId()); vo.setFileJobId(job.getId());
vo.setFileStatus(job.getStatus()); vo.setFileStatus(job.getStatus());
vo.setFileError(job.getErrorMessage()); vo.setFileError(job.getErrorMessage());
attachFileProgress(vo, row, job); attachFileProgress(vo, row, job, task);
} }
private void attachFileProgress(SimilarAsinHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job) { private void attachFileProgress(SimilarAsinHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job, FileTaskEntity task) {
if (row == null || row.getTaskId() == null) { if (row == null || row.getTaskId() == null) {
return; return;
} }
@@ -4866,22 +4895,22 @@ public class SimilarAsinTaskService {
int cozeCompleted = countCompletedCozeStates(taskId); int cozeCompleted = countCompletedCozeStates(taskId);
int cozePending = countPendingCozeStates(taskId); int cozePending = countPendingCozeStates(taskId);
boolean uploadComplete = isResultSubmissionComplete(taskId); boolean uploadComplete = isResultSubmissionComplete(taskId);
if (!uploadComplete) {
if (task != null && !STATUS_RUNNING.equals(task.getStatus())) {
return;
}
attachPythonUploadProgress(vo, taskId);
return;
}
TaskProgressSnapshotEntity snapshot = taskProgressSnapshotService.find(taskId, MODULE_TYPE); TaskProgressSnapshotEntity snapshot = taskProgressSnapshotService.find(taskId, MODULE_TYPE);
if (cozeCompleted + cozePending > 0) { if (cozeCompleted + cozePending > 0) {
int total = calculateCozeDisplayTotal(snapshot, cozeCompleted + cozePending); int total = calculateCozeDisplayTotal(snapshot, cozeCompleted + cozePending);
if (!uploadComplete) {
total = Math.max(total, cozeCompleted + cozePending + 1);
}
int current = Math.max(0, Math.min(cozeCompleted, total)); int current = Math.max(0, Math.min(cozeCompleted, total));
vo.setFileProgressCurrent(current); vo.setFileProgressCurrent(current);
vo.setFileProgressTotal(total); vo.setFileProgressTotal(total);
int percent = calculateDisplayProgressPercent(current, total, job, snapshot == null ? null : snapshot.getUpdatedAt()); int percent = calculateDisplayProgressPercent(current, total, job, snapshot == null ? null : snapshot.getUpdatedAt());
percent = Math.max(percent, calculateSnapshotDisplayPercent(snapshot, job)); percent = Math.max(percent, calculateSnapshotDisplayPercent(snapshot, job));
vo.setFileProgressPercent(Math.min(uploadComplete ? 99 : 98, percent)); vo.setFileProgressPercent(Math.min(99, percent));
if (!uploadComplete) {
vo.setFileProgressMessage(buildUploadingCozeProgressMessage(current, total, cozePending));
return;
}
vo.setFileProgressMessage(cozePending > 0 vo.setFileProgressMessage(cozePending > 0
? buildCozeProgressMessage(current, total, cozePending) ? buildCozeProgressMessage(current, total, cozePending)
: "Coze 已回流 " + current + "/" + total + " 批次,正在生成结果文件"); : "Coze 已回流 " + current + "/" + total + " 批次,正在生成结果文件");
@@ -4905,6 +4934,121 @@ public class SimilarAsinTaskService {
vo.setFileProgressMessage(snapshot.getMessage()); vo.setFileProgressMessage(snapshot.getMessage());
} }
private void attachPythonUploadProgress(SimilarAsinHistoryItemVo vo, Long taskId) {
PythonUploadProgress progress = resolvePythonUploadProgress(taskId);
int total = progress.total() > 0 ? progress.total() : Math.max(1, progress.current() + 1);
int current = Math.max(0, Math.min(progress.current(), total));
int percent = current <= 0 ? 1 : Math.min(95, (int) Math.floor(current * 100.0 / total));
vo.setFileProgressCurrent(current);
vo.setFileProgressTotal(total);
vo.setFileProgressPercent(percent);
vo.setFileProgressMessage(buildPythonUploadProgressMessage(current, total, progress.unit()));
}
private PythonUploadProgress resolvePythonUploadProgress(Long taskId) {
PythonUploadProgress chunkProgress = resolvePythonChunkProgress(taskId);
if (chunkProgress.total() > 0) {
return chunkProgress;
}
if (chunkProgress.current() > 0) {
return new PythonUploadProgress(chunkProgress.current(), chunkProgress.current() + 1, chunkProgress.unit());
}
int totalRows = 0;
try {
FileTaskEntity task = fileTaskMapper.selectById(taskId);
totalRows = allRowCount(task);
} catch (Exception ignored) {
totalRows = 0;
}
int uploadedRows = countSubmittedRows(taskId);
if (totalRows <= 0 && uploadedRows > 0) {
totalRows = uploadedRows + 1;
}
return new PythonUploadProgress(uploadedRows, totalRows, "");
}
private PythonUploadProgress resolvePythonChunkProgress(Long taskId) {
if (taskId == null || taskId <= 0) {
return new PythonUploadProgress(0, 0, "分片");
}
List<TaskScopeStateEntity> states = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
.eq(TaskScopeStateEntity::getTaskId, taskId)
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
.isNull(TaskScopeStateEntity::getCozeStatus)
.orderByDesc(TaskScopeStateEntity::getUpdatedAt));
if (states == null || states.isEmpty()) {
return resolvePythonChunkProgressFromChunks(taskId);
}
int current = 0;
int total = 0;
for (TaskScopeStateEntity state : states) {
if (state == null) {
continue;
}
int stateTotal = state.getChunkTotal() == null ? 0 : state.getChunkTotal();
int stateCurrent = state.getReceivedChunkCount() == null ? 0 : state.getReceivedChunkCount();
if (stateTotal > total || total <= 0 && stateCurrent > current) {
total = stateTotal;
current = stateCurrent;
}
}
return new PythonUploadProgress(current, total, "分片");
}
private PythonUploadProgress resolvePythonChunkProgressFromChunks(Long taskId) {
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
.eq(TaskChunkEntity::getTaskId, taskId)
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE));
if (chunks == null || chunks.isEmpty()) {
return new PythonUploadProgress(0, 0, "分片");
}
Map<String, Integer> receivedByScope = new LinkedHashMap<>();
Map<String, Integer> totalByScope = new LinkedHashMap<>();
for (TaskChunkEntity chunk : chunks) {
if (chunk == null) {
continue;
}
String scopeHash = firstNonBlank(chunk.getScopeHash(), "");
receivedByScope.merge(scopeHash, 1, Integer::sum);
if (chunk.getChunkTotal() != null && chunk.getChunkTotal() > 0) {
totalByScope.merge(scopeHash, chunk.getChunkTotal(), Math::max);
}
}
int current = 0;
int total = 0;
for (Map.Entry<String, Integer> entry : receivedByScope.entrySet()) {
int scopeCurrent = entry.getValue() == null ? 0 : entry.getValue();
int scopeTotal = totalByScope.getOrDefault(entry.getKey(), 0);
if (scopeTotal > total || total <= 0 && scopeCurrent > current) {
current = scopeCurrent;
total = scopeTotal;
}
}
return new PythonUploadProgress(current, total, "分片");
}
private int countSubmittedRows(Long taskId) {
if (taskId == null || taskId <= 0) {
return 0;
}
int total = 0;
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
.eq(TaskChunkEntity::getTaskId, taskId)
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE));
if (chunks == null || chunks.isEmpty()) {
return 0;
}
for (TaskChunkEntity chunk : chunks) {
total += readChunkRows(chunk).size();
}
return total;
}
private String buildPythonUploadProgressMessage(int current, int total, String unit) {
String safeUnit = unit == null || unit.isBlank() ? "分片" : unit;
return "等待 Python 回传,已回传 " + Math.max(0, current) + "/" + Math.max(1, total) + " " + safeUnit;
}
private int calculateCozeDisplayTotal(TaskProgressSnapshotEntity snapshot, int observedCozeStates) { private int calculateCozeDisplayTotal(TaskProgressSnapshotEntity snapshot, int observedCozeStates) {
int observed = Math.max(0, observedCozeStates); int observed = Math.max(0, observedCozeStates);
// 小任务 totalCount <= 3 时直接使用 observed避免 totalCount - 3 → 0 引起进度条 0/0。 // 小任务 totalCount <= 3 时直接使用 observed避免 totalCount - 3 → 0 引起进度条 0/0。
@@ -4924,15 +5068,6 @@ public class SimilarAsinTaskService {
} }
return "Coze 已回流 " + safeCompleted + "/" + safeTotal + " 批次,等待结果文件"; return "Coze 已回流 " + safeCompleted + "/" + safeTotal + " 批次,等待结果文件";
} }
private String buildUploadingCozeProgressMessage(int completed, int total, int pending) {
int safeTotal = Math.max(1, total);
int safeCompleted = Math.max(0, Math.min(completed, safeTotal));
int submitted = Math.max(safeCompleted, Math.min(safeTotal, safeCompleted + Math.max(0, pending)));
if (pending > 0) {
return "Python 仍在回传Coze 已提交 " + submitted + "/" + safeTotal + " 批次";
}
return "Python 仍在回传Coze 已回流 " + safeCompleted + "/" + safeTotal + " 批次";
}
private String fmt(LocalDateTime t) { private String fmt(LocalDateTime t) {
return t == null ? null : t.toString(); return t == null ? null : t.toString();
@@ -4968,10 +5103,11 @@ 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, String apiKey, List<SimilarAsinSourceFileDto> sourceFiles, List<String> headers, List<SimilarAsinParsedGroupVo> groups, List<SimilarAsinParsedRowVo> allRows) { private String buildParsedPayloadJson(String aiPrompt, String apiKey, Boolean imgSwitch, 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.setApiKey(normalize(apiKey));
payload.setImgSwitch(Boolean.TRUE.equals(imgSwitch));
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(allRows == null ? List.of() : new ArrayList<>(allRows)); payload.setItems(allRows == null ? List.of() : new ArrayList<>(allRows));
@@ -4980,10 +5116,11 @@ public class SimilarAsinTaskService {
return writeJson(payload, "保存解析结果失败"); return writeJson(payload, "保存解析结果失败");
} }
private String buildTaskResultJson(String aiPrompt, String apiKey, List<SimilarAsinSourceFileDto> sourceFiles, String parsedPayloadPointer) { private String buildTaskResultJson(String aiPrompt, String apiKey, Boolean imgSwitch, 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("apiKey", normalize(apiKey));
payload.put("imgSwitch", Boolean.TRUE.equals(imgSwitch));
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)

View File

@@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS biz_coze_credential (
INSERT INTO biz_coze_credential (module_type, credential_name, workflow_id, token, enabled, max_concurrent, sort_order) INSERT INTO biz_coze_credential (module_type, credential_name, workflow_id, token, enabled, max_concurrent, sort_order)
VALUES VALUES
('APPEARANCE_PATENT', 'appearance-1', '7639685157562089513', 'Bearer sat_CztofPRhIKFKeaPZBIRxocfckqhsdCZZ45NPhkf7WOZjbX36vnVSfVV30T6u2rSX', 1, 0, 10), ('APPEARANCE_PATENT', 'appearance-1', '7639685157562089513', 'Bearer sat_CztofPRhIKFKeaPZBIRxocfckqhsdCZZ45NPhkf7WOZjbX36vnVSfVV30T6u2rSX', 1, 0, 10),
('APPEARANCE_PATENT', 'appearance-2', '7639688221823778850', 'Bearer sat_2VsTpfWXYG3EkCo8OQfLwJEWqhduCL4NEw7sSCsABppkW61kkSRPIac9coNlEnHE', 1, 0, 20), ('APPEARANCE_PATENT', 'appearance-2', '7644952624353329202', 'Bearer sat_2VsTpfWXYG3EkCo8OQfLwJEWqhduCL4NEw7sSCsABppkW61kkSRPIac9coNlEnHE', 1, 0, 20),
('SIMILAR_ASIN', 'similar-1', '7639708860686024756', 'Bearer sat_vqmb97BiXbMW3XVyG9htWQKjZ2F55CaW2FRrT9bDdPha7a3hxmXnMNCE1XNHdCVU', 1, 0, 10), ('SIMILAR_ASIN', 'similar-1', '7639708860686024756', 'Bearer sat_vqmb97BiXbMW3XVyG9htWQKjZ2F55CaW2FRrT9bDdPha7a3hxmXnMNCE1XNHdCVU', 1, 0, 10),
('SIMILAR_ASIN', 'similar-2', '7635328462404583478', 'Bearer sat_gsInckdsN0qqhnZSK7K4Zevw6neJCHmYHyIbm85oGgJIGEmSdkH5OFctSKLsAHvT', 1, 0, 20) ('SIMILAR_ASIN', 'similar-2', '7635328462404583478', 'Bearer sat_gsInckdsN0qqhnZSK7K4Zevw6neJCHmYHyIbm85oGgJIGEmSdkH5OFctSKLsAHvT', 1, 0, 20)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE

View File

@@ -0,0 +1,4 @@
UPDATE biz_coze_credential
SET workflow_id = '7644952624353329202'
WHERE module_type = 'APPEARANCE_PATENT'
AND credential_name = 'appearance-2';

View File

@@ -25,28 +25,32 @@
<div class="section-title">筛选条件</div> <div class="section-title">筛选条件</div>
<div class="filter-zone"> <div class="filter-zone">
<div class="filter-row"> <div class="filter-row">
<label class="filter-label">金额手动输入</label> <label class="filter-label">金额区间</label>
<input <div class="range-inputs">
v-model.number="filters.amount" <input
type="number" v-model.number="filters.minAmount"
step="0.01" type="number"
min="0" step="0.01"
class="filter-input" min="0"
placeholder="例如59.99" class="filter-input"
/> placeholder="最低"
</div> />
<div class="filter-row"> <span class="range-separator"></span>
<label class="filter-label">排名</label> <input
<input v-model.number="filters.maxAmount"
v-model.number="filters.rank" type="number"
type="number" step="0.01"
step="1" min="0"
min="0" class="filter-input"
class="filter-input" placeholder="最高"
placeholder="例如100000" />
/> </div>
</div> </div>
<div class="filter-checks"> <div class="filter-checks">
<label class="filter-check-row">
<input type="checkbox" class="filter-check-input" v-model="filters.rank" />
<span class="filter-check-text">排名</span>
</label>
<label class="filter-check-row"> <label class="filter-check-row">
<input type="checkbox" class="filter-check-input" v-model="filters.fba" /> <input type="checkbox" class="filter-check-input" v-model="filters.fba" />
<span class="filter-check-text">FBA</span> <span class="filter-check-text">FBA</span>
@@ -59,43 +63,29 @@
</div> </div>
<div class="section-title country-section-title"> <div class="section-title country-section-title">
<span>国家与顺序</span> <span>采集国家</span>
<span v-if="countryPrefSaving" class="country-pref-saving">保存中</span> <span v-if="countryPrefSaving" class="country-pref-saving">保存中</span>
</div> </div>
<p class="hint country-pref-hint"> <p class="hint country-pref-hint">
勾选需要采集的国家暂定 5 至少保留 1 上方勾选区<strong>先按已选顺序</strong>排列其余未选国家排在后面拖拽下方列表可调整顺序 每次任务只采集一个国家选择后会自动保存下次进入默认使用该国家
</p> </p>
<div class="country-pref-checks"> <div class="country-pref-radios">
<label v-for="row in countryCheckboxRows" :key="row.code" class="country-check-row"> <label
v-for="row in COUNTRY_OPTIONS"
:key="row.code"
class="country-check-row"
:class="{ selected: selectedCountryCode === row.code }"
>
<input <input
type="checkbox" type="radio"
name="collect-data-country"
class="country-check-input" class="country-check-input"
:checked="isCountrySelected(row.code)" :checked="selectedCountryCode === row.code"
:disabled="isCountrySelectionLocked(row.code)" @change="selectCountry(row.code)"
@change="onCountryNativeChange(row.code, $event)"
/> />
<span class="country-check-text">{{ row.label }}{{ row.code }}</span> <span class="country-check-text">{{ row.label }}{{ row.code }}</span>
</label> </label>
</div> </div>
<div v-if="orderedCountryCodes.length" class="country-order-panel">
<div class="country-order-caption">已选顺序拖拽 调整</div>
<div class="country-order-list">
<div
v-for="(code, idx) in orderedCountryCodes"
:key="code"
class="country-drag-row"
:class="{ dragging: dragCountryIndex === idx }"
draggable="true"
@dragstart="onCountryDragStart(idx)"
@dragend="onCountryDragEnd"
@dragover.prevent
@drop.prevent="onCountryDrop(idx)"
>
<span class="drag-handle" title="拖动排序"></span>
<span class="country-drag-label">{{ countryLabel(code) }}{{ code }}</span>
</div>
</div>
</div>
<div class="run-row"> <div class="run-row">
<button <button
@@ -235,19 +225,21 @@ const submitting = ref(false)
const pushing = ref(false) const pushing = ref(false)
const filters = reactive<{ const filters = reactive<{
amount: number | null minAmount: number | null
rank: number | null maxAmount: number | null
rank: boolean
fba: boolean fba: boolean
fbm: boolean fbm: boolean
}>({ }>({
amount: null, minAmount: null,
rank: null, maxAmount: null,
rank: false,
fba: false, fba: false,
fbm: false, fbm: false,
}) })
const orderedCountryCodes = ref<string[]>(['DE', 'UK', 'FR', 'IT', 'ES']) const selectedCountryCode = ref<string>('DE')
const dragCountryIndex = ref<number | null>(null) const selectedCountryCodes = computed(() => selectedCountryCode.value ? [selectedCountryCode.value] : [])
/** 防止接口慢回包覆盖用户已经做出的修改 */ /** 防止接口慢回包覆盖用户已经做出的修改 */
const countryPrefUserTouched = ref(false) const countryPrefUserTouched = ref(false)
const countryPrefSaving = ref(false) const countryPrefSaving = ref(false)
@@ -304,65 +296,11 @@ function countryLabel(code: string) {
return row?.label ?? code return row?.label ?? code
} }
const countryCheckboxRows = computed(() => { function selectCountry(code: string) {
const sel = orderedCountryCodes.value if (!COUNTRY_OPTIONS.some((row) => row.code === code)) return
const selectedSet = new Set(sel) if (selectedCountryCode.value === code) return
const selectedPart = sel.map((code) => ({ code, label: countryLabel(code) }))
const rest = COUNTRY_OPTIONS.filter((o) => !selectedSet.has(o.code)).map((o) => ({
code: o.code,
label: o.label,
}))
return [...selectedPart, ...rest]
})
function isCountrySelected(code: string) {
return orderedCountryCodes.value.includes(code)
}
function isCountrySelectionLocked(code: string) {
return orderedCountryCodes.value.length === 1 && orderedCountryCodes.value[0] === code
}
function onCountryNativeChange(code: string, e: Event) {
const el = e.target as HTMLInputElement | null
if (!el) return
if (!el.checked && isCountrySelectionLocked(code)) {
el.checked = true
ElMessage.warning('至少保留 1 个国家')
return
}
countryPrefUserTouched.value = true countryPrefUserTouched.value = true
if (el.checked) { selectedCountryCode.value = code
if (!orderedCountryCodes.value.includes(code)) {
orderedCountryCodes.value = [...orderedCountryCodes.value, code]
}
} else {
if (orderedCountryCodes.value.length <= 1) {
ElMessage.warning('至少保留 1 个国家')
return
}
orderedCountryCodes.value = orderedCountryCodes.value.filter((c) => c !== code)
}
scheduleSaveCountryPref()
}
function onCountryDragStart(index: number) {
dragCountryIndex.value = index
}
function onCountryDragEnd() {
dragCountryIndex.value = null
}
function onCountryDrop(toIndex: number) {
const from = dragCountryIndex.value
dragCountryIndex.value = null
if (from == null || from === toIndex) return
countryPrefUserTouched.value = true
const arr = [...orderedCountryCodes.value]
const [item] = arr.splice(from, 1)
arr.splice(toIndex, 0, item)
orderedCountryCodes.value = arr
scheduleSaveCountryPref() scheduleSaveCountryPref()
} }
@@ -377,10 +315,10 @@ function scheduleSaveCountryPref() {
} }
async function persistCountryPref() { async function persistCountryPref() {
if (!orderedCountryCodes.value.length) return if (!selectedCountryCode.value) return
countryPrefSaving.value = true countryPrefSaving.value = true
try { try {
await putCollectDataCountryPreference([...orderedCountryCodes.value]) await putCollectDataCountryPreference([selectedCountryCode.value])
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存国家顺序失败') ElMessage.error(error instanceof Error ? error.message : '保存国家顺序失败')
} finally { } finally {
@@ -394,7 +332,7 @@ async function loadCountryPreference() {
if (countryPrefUserTouched.value) return if (countryPrefUserTouched.value) return
const codes = vo?.country_codes const codes = vo?.country_codes
if (Array.isArray(codes) && codes.length) { if (Array.isArray(codes) && codes.length) {
orderedCountryCodes.value = [...codes] selectedCountryCode.value = String(codes[0] || 'DE')
} }
} catch { } catch {
/* 接口失败时保留本地默认 */ /* 接口失败时保留本地默认 */
@@ -464,16 +402,24 @@ async function selectFolder() {
} }
function buildFiltersPayload() { function buildFiltersPayload() {
const minAmount = filters.minAmount === null || filters.minAmount === undefined || Number.isNaN(filters.minAmount)
? null
: filters.minAmount
const maxAmount = filters.maxAmount === null || filters.maxAmount === undefined || Number.isNaN(filters.maxAmount)
? null
: filters.maxAmount
return { return {
amount: filters.amount === null || filters.amount === undefined || Number.isNaN(filters.amount) amount: maxAmount ?? minAmount,
? null minAmount,
: filters.amount, maxAmount,
rank: filters.rank === null || filters.rank === undefined || Number.isNaN(filters.rank) min_amount: minAmount,
? null max_amount: maxAmount,
: filters.rank, rank: !!filters.rank,
fba: !!filters.fba, fba: !!filters.fba,
fbm: !!filters.fbm, fbm: !!filters.fbm,
countryCodes: [...orderedCountryCodes.value], countryCodes: [...selectedCountryCodes.value],
countryCode: selectedCountryCode.value,
country_code: selectedCountryCode.value,
} }
} }
@@ -482,8 +428,8 @@ async function submitCollect() {
ElMessage.warning('请先选择 Excel 文件') ElMessage.warning('请先选择 Excel 文件')
return return
} }
if (!orderedCountryCodes.value.length) { if (!selectedCountryCode.value) {
ElMessage.warning('至少保留 1 个国家') ElMessage.warning('请选择采集国家')
return return
} }
submitting.value = true submitting.value = true
@@ -722,6 +668,7 @@ onBeforeUnmount(() => {
} }
.filter-input { .filter-input {
flex: 1; flex: 1;
min-width: 0;
padding: 6px 10px; padding: 6px 10px;
background: #1e1e1e; background: #1e1e1e;
border: 1px solid #3a3a3a; border: 1px solid #3a3a3a;
@@ -731,6 +678,8 @@ onBeforeUnmount(() => {
outline: none; outline: none;
} }
.filter-input:focus { border-color: #3498db; } .filter-input:focus { border-color: #3498db; }
.range-inputs { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }
.range-separator { color: #777; font-size: 12px; white-space: nowrap; }
.filter-checks { display: flex; gap: 16px; } .filter-checks { display: flex; gap: 16px; }
.filter-check-row { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; color: #ccc; font-size: 12px; } .filter-check-row { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; color: #ccc; font-size: 12px; }
.filter-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; } .filter-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; }
@@ -738,17 +687,11 @@ onBeforeUnmount(() => {
.country-pref-hint { margin-top: -4px; } .country-pref-hint { margin-top: -4px; }
.country-section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .country-section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.country-pref-saving { color: #3498db; font-size: 11px; } .country-pref-saving { color: #3498db; font-size: 11px; }
.country-pref-checks { display: flex; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 12px; } .country-pref-radios { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 14px; }
.country-check-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-size: 12px; color: #ccc; } .country-check-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-size: 12px; color: #ccc; padding: 8px 10px; border: 1px solid #333; border-radius: 8px; background: #252525; }
.country-check-row.selected { border-color: #409eff; background: rgba(64, 158, 255, .12); color: #e6f2ff; }
.country-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; flex-shrink: 0; } .country-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; flex-shrink: 0; }
.country-check-text { line-height: 1.3; } .country-check-text { line-height: 1.3; }
.country-order-panel { border: 1px solid #333; border-radius: 8px; padding: 10px 12px; background: #252525; margin-bottom: 14px; }
.country-order-caption { font-size: 11px; color: #777; margin-bottom: 8px; }
.country-order-list { display: flex; flex-direction: column; gap: 6px; }
.country-drag-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: #2c2c2c; border-radius: 6px; border: 1px solid #3a3a3a; cursor: grab; user-select: none; }
.country-drag-row.dragging { opacity: 0.55; border-color: #3498db; }
.drag-handle { color: #666; font-size: 12px; letter-spacing: -2px; }
.country-drag-label { font-size: 12px; color: #ccc; }
.queue-debug-card { margin-top: 14px; border: 1px solid #2a2a2a; border-radius: 8px; padding: 10px 12px; background: #1f1f1f; } .queue-debug-card { margin-top: 14px; border: 1px solid #2a2a2a; border-radius: 8px; padding: 10px 12px; background: #1f1f1f; }
.queue-debug-title { margin-top: 0; } .queue-debug-title { margin-top: 0; }

View File

@@ -17,6 +17,18 @@
</div> </div>
</div> </div>
<div class="condition-card">
<div class="section-title">检测条件</div>
<label class="switch-row">
<span>
<strong>图片检测</strong>
<em>开启后请求 Coze 时携带 img_switch=true</em>
</span>
<input v-model="imgSwitch" type="checkbox" />
<i></i>
</label>
</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 ? '解析中...' : '解析并创建任务' }}
@@ -171,6 +183,7 @@ type TaskSummary = Pick<SimilarAsinParseVo, 'taskId' | 'totalRows' | 'acceptedRo
const queuedTaskSummary = ref<TaskSummary | null>(null) const queuedTaskSummary = ref<TaskSummary | null>(null)
const parsing = ref(false) const parsing = ref(false)
const pushing = ref(false) const pushing = ref(false)
const imgSwitch = ref(false)
const queuePayloadText = ref('') const queuePayloadText = ref('')
const pollingTaskIds = ref<number[]>([]) const pollingTaskIds = ref<number[]>([])
const pendingFileTaskIds = ref<number[]>([]) const pendingFileTaskIds = ref<number[]>([])
@@ -374,7 +387,7 @@ async function parseFiles() {
originalFilename: f.originalFilename, originalFilename: f.originalFilename,
relativePath: f.relativePath, relativePath: f.relativePath,
})) }))
const res = await parseSimilarAsin(files, effectiveCozeApiKey()) const res = await parseSimilarAsin(files, effectiveCozeApiKey(), imgSwitch.value)
parseResult.value = res parseResult.value = res
queuedTaskSummary.value = null queuedTaskSummary.value = null
queuePayloadText.value = '' queuePayloadText.value = ''
@@ -874,6 +887,16 @@ onUnmounted(() => {
.right-panel { flex: 1; min-width: 0; background: #1a1a1a; display: flex; flex-direction: column; } .right-panel { flex: 1; min-width: 0; background: #1a1a1a; display: flex; flex-direction: column; }
.section-title, .subsection-title { font-size: 13px; color: #bbb; margin-bottom: 10px; } .section-title, .subsection-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
.upload-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 18px; background: #252525; margin-bottom: 18px; } .upload-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 18px; background: #252525; margin-bottom: 18px; }
.condition-card { margin-bottom: 18px; padding: 14px 16px; border: 1px solid #2f3a34; border-radius: 10px; background: linear-gradient(135deg, #222a25, #202020); }
.switch-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; color: #d8e4dc; cursor: pointer; user-select: none; }
.switch-row span { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.switch-row strong { font-size: 13px; font-weight: 600; }
.switch-row em { color: #8d9a91; font-size: 12px; font-style: normal; line-height: 1.4; }
.switch-row input { position: absolute; opacity: 0; pointer-events: none; }
.switch-row i { position: relative; width: 46px; height: 24px; flex: 0 0 auto; border-radius: 999px; background: #3a3a3a; box-shadow: inset 0 0 0 1px #4b4b4b; transition: background .2s ease, box-shadow .2s ease; }
.switch-row i::after { content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #c7c7c7; transition: transform .2s ease, background .2s ease; }
.switch-row input:checked + i { background: #27ae60; box-shadow: inset 0 0 0 1px #42d17a; }
.switch-row input:checked + i::after { transform: translateX(22px); background: #fff; }
.hint, .loading-msg, .files, .muted { color: #888; font-size: 12px; line-height: 1.5; } .hint, .loading-msg, .files, .muted { color: #888; font-size: 12px; line-height: 1.5; }
.link { color: #6ea8fe; text-decoration: none; } .link { color: #6ea8fe; text-decoration: none; }
.link:hover { color: #9fc5ff; } .link:hover { color: #9fc5ff; }

View File

@@ -1718,6 +1718,7 @@ export interface SimilarAsinParseVo {
droppedRows: number; droppedRows: number;
groupCount?: number; groupCount?: number;
aiPrompt?: string; aiPrompt?: string;
imgSwitch?: boolean;
items: SimilarAsinParsedRow[]; items: SimilarAsinParsedRow[];
groups?: SimilarAsinParsedGroup[]; groups?: SimilarAsinParsedGroup[];
} }
@@ -1776,15 +1777,16 @@ export interface SimilarAsinTaskBatchVo {
missingTaskIds?: number[]; missingTaskIds?: number[];
} }
export function parseSimilarAsin(files: UploadedFileRef[], apiKey?: string) { export function parseSimilarAsin(files: UploadedFileRef[], apiKey?: string, imgSwitch = false) {
return unwrapJavaResponse( return unwrapJavaResponse(
post< post<
JavaApiResponse<SimilarAsinParseVo>, JavaApiResponse<SimilarAsinParseVo>,
{ user_id: number; files: UploadedFileRef[]; api_key?: string } { user_id: number; files: UploadedFileRef[]; api_key?: string; img_switch: boolean }
>(`${JAVA_API_PREFIX}/similar-asin/parse`, { >(`${JAVA_API_PREFIX}/similar-asin/parse`, {
user_id: getCurrentUserId(), user_id: getCurrentUserId(),
files, files,
api_key: apiKey, api_key: apiKey,
img_switch: imgSwitch,
}), }),
); );
} }
@@ -2231,10 +2233,16 @@ export interface CollectDataSourceFile {
export interface CollectDataFilters { export interface CollectDataFilters {
amount?: number | string | null; amount?: number | string | null;
rank?: number | null; minAmount?: number | string | null;
maxAmount?: number | string | null;
min_amount?: number | string | null;
max_amount?: number | string | null;
rank?: boolean | null;
fba?: boolean | null; fba?: boolean | null;
fbm?: boolean | null; fbm?: boolean | null;
countryCodes?: string[]; countryCodes?: string[];
countryCode?: string | null;
country_code?: string | null;
} }
export interface CollectDataParseRequest { export interface CollectDataParseRequest {