From ca4a2cd07a354ef1cc54b0cf20b72bcce203e26f Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Wed, 27 May 2026 12:41:59 +0800 Subject: [PATCH] =?UTF-8?q?python=E7=AB=AF=E7=99=BB=E5=BD=95=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/appearance-patent-CDFDTiWm.css | 1 + app/assets/appearance-patent-eAIM0l0f.css | 1 - app/assets/appearance-patent.js | 10 +- app/assets/similar-asin-B5CvMmwV.css | 1 - app/assets/similar-asin-BgG5p7TZ.css | 1 + app/assets/similar-asin.js | 2 +- app/jwt_util.py | 40 +++++- .../client/SimilarAsinCozeClient.java | 30 ++-- .../controller/SimilarAsinController.java | 23 ++- .../service/SimilarAsinTaskService.java | 132 +++++++++++------- .../util/ExcelCellImageWriter.java | 6 +- .../util/ExcelCellImageWriterTest.java | 10 +- .../brand/components/BrandSimilarAsinTab.vue | 2 +- 13 files changed, 170 insertions(+), 89 deletions(-) create mode 100644 app/assets/appearance-patent-CDFDTiWm.css delete mode 100644 app/assets/appearance-patent-eAIM0l0f.css delete mode 100644 app/assets/similar-asin-B5CvMmwV.css create mode 100644 app/assets/similar-asin-BgG5p7TZ.css diff --git a/app/assets/appearance-patent-CDFDTiWm.css b/app/assets/appearance-patent-CDFDTiWm.css new file mode 100644 index 0000000..1840601 --- /dev/null +++ b/app/assets/appearance-patent-CDFDTiWm.css @@ -0,0 +1 @@ +.module-page[data-v-66aa0b14]{min-height:100vh;background:#1a1a1a}.main-content[data-v-66aa0b14]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-66aa0b14]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-66aa0b14]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-66aa0b14],.subsection-title[data-v-66aa0b14]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-66aa0b14]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-66aa0b14],.loading-msg[data-v-66aa0b14],.files[data-v-66aa0b14],.muted[data-v-66aa0b14]{color:#888;font-size:12px;line-height:1.5}.link[data-v-66aa0b14]{color:#6ea8fe;text-decoration:none}.link[data-v-66aa0b14]:hover{color:#9fc5ff}.btns[data-v-66aa0b14],.run-row[data-v-66aa0b14]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-66aa0b14],.btn-run[data-v-66aa0b14],.btn-delete[data-v-66aa0b14],.download[data-v-66aa0b14]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-66aa0b14]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-66aa0b14]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-66aa0b14]{background:#27ae60}.btn-run[data-v-66aa0b14]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-66aa0b14]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-66aa0b14]{display:block;margin:4px 0}.prompt-card[data-v-66aa0b14],.secret-card[data-v-66aa0b14]{margin-bottom:18px}.secret-card[data-v-66aa0b14]{display:none}.prompt-input[data-v-66aa0b14]{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}.secret-input[data-v-66aa0b14]{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[data-v-66aa0b14]:focus,.secret-input[data-v-66aa0b14]:focus{border-color:#3498db}.parse-card[data-v-66aa0b14],.queue-payload[data-v-66aa0b14]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-66aa0b14]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-66aa0b14]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-66aa0b14]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-66aa0b14]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-66aa0b14]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-66aa0b14]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-66aa0b14]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-66aa0b14]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-66aa0b14]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-66aa0b14]{color:#666;font-size:13px;padding:18px;text-align:center}.result-table[data-v-66aa0b14]{--el-table-bg-color: #222;--el-table-tr-bg-color: #222;--el-table-header-bg-color: #2a2a2a;--el-table-text-color: #ccc;--el-table-border-color: #333}.task-list[data-v-66aa0b14]{list-style:none;margin:0;padding:12px}.task-item[data-v-66aa0b14]{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-66aa0b14]{flex:1;min-width:0}.id[data-v-66aa0b14]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-66aa0b14]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-66aa0b14]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-66aa0b14]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-66aa0b14]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-66aa0b14]{background:#3498db2e;color:#3498db}.status.pending[data-v-66aa0b14]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-66aa0b14]{margin-top:6px;color:#e0b96d}.file-progress[data-v-66aa0b14]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-66aa0b14]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-66aa0b14]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-66aa0b14]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.download[data-v-66aa0b14]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-66aa0b14]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-66aa0b14]{flex-direction:column;height:auto}.left-panel[data-v-66aa0b14]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-66aa0b14]{grid-template-columns:repeat(2,minmax(0,1fr))}} diff --git a/app/assets/appearance-patent-eAIM0l0f.css b/app/assets/appearance-patent-eAIM0l0f.css deleted file mode 100644 index aa134c1..0000000 --- a/app/assets/appearance-patent-eAIM0l0f.css +++ /dev/null @@ -1 +0,0 @@ -.module-page[data-v-f1e69f37]{min-height:100vh;background:#1a1a1a}.main-content[data-v-f1e69f37]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-f1e69f37]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-f1e69f37]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-f1e69f37],.subsection-title[data-v-f1e69f37]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-f1e69f37]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-f1e69f37],.loading-msg[data-v-f1e69f37],.files[data-v-f1e69f37],.muted[data-v-f1e69f37]{color:#888;font-size:12px;line-height:1.5}.link[data-v-f1e69f37]{color:#6ea8fe;text-decoration:none}.link[data-v-f1e69f37]:hover{color:#9fc5ff}.btns[data-v-f1e69f37],.run-row[data-v-f1e69f37]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-f1e69f37],.btn-run[data-v-f1e69f37],.btn-delete[data-v-f1e69f37],.download[data-v-f1e69f37]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-f1e69f37]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-f1e69f37]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-f1e69f37]{background:#27ae60}.btn-run[data-v-f1e69f37]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-f1e69f37]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-f1e69f37]{display:block;margin:4px 0}.prompt-card[data-v-f1e69f37],.secret-card[data-v-f1e69f37]{margin-bottom:18px}.secret-card[data-v-f1e69f37]{display:none}.prompt-input[data-v-f1e69f37]{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}.secret-input[data-v-f1e69f37]{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[data-v-f1e69f37]:focus,.secret-input[data-v-f1e69f37]:focus{border-color:#3498db}.parse-card[data-v-f1e69f37],.queue-payload[data-v-f1e69f37]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-f1e69f37]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-f1e69f37]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-f1e69f37]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-f1e69f37]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-f1e69f37]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-f1e69f37]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-f1e69f37]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-f1e69f37]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-f1e69f37]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-f1e69f37]{color:#666;font-size:13px;padding:18px;text-align:center}.result-table[data-v-f1e69f37]{--el-table-bg-color: #222;--el-table-tr-bg-color: #222;--el-table-header-bg-color: #2a2a2a;--el-table-text-color: #ccc;--el-table-border-color: #333}.task-list[data-v-f1e69f37]{list-style:none;margin:0;padding:12px}.task-item[data-v-f1e69f37]{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-f1e69f37]{flex:1;min-width:0}.id[data-v-f1e69f37]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-f1e69f37]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-f1e69f37]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-f1e69f37]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-f1e69f37]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-f1e69f37]{background:#3498db2e;color:#3498db}.status.pending[data-v-f1e69f37]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-f1e69f37]{margin-top:6px;color:#e0b96d}.file-progress[data-v-f1e69f37]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-f1e69f37]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-f1e69f37]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-f1e69f37]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.download[data-v-f1e69f37]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-f1e69f37]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-f1e69f37]{flex-direction:column;height:auto}.left-panel[data-v-f1e69f37]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-f1e69f37]{grid-template-columns:repeat(2,minmax(0,1fr))}} diff --git a/app/assets/appearance-patent.js b/app/assets/appearance-patent.js index 826650f..2e8307d 100644 --- a/app/assets/appearance-patent.js +++ b/app/assets/appearance-patent.js @@ -1,9 +1 @@ -import{d as Ke,o as Je,L as qe,a as i,c,b as je,B as Ve,e as a,F as se,r as te,t as r,w as Oe,G as Xe,f as k,N as ge,n as Ye,g as v,h as j,i as D,E as f,V as Qe,W as We,X as Ze,U as he,Y as es,Z as ss,$ as ts,a0 as as,a1 as ns,a2 as ls,a3 as rs,_ as os,q as us,s as is}from"./pywebview-3bFwh9Cg.js";import{e as cs}from"./brand-D__LhhPM.js";import{c as ds}from"./categorized-timers-JPA-olTr.js";const fs={class:"page-shell module-page"},ps={class:"main-content"},vs={class:"left-panel"},gs={class:"upload-zone"},hs={class:"selected-files"},ys={key:0},_s={class:"prompt-card"},ks={class:"run-row"},Is=["disabled"],Ps=["disabled"],ws={key:0,class:"parse-card"},Cs={key:1,class:"queue-payload"},Ss={class:"right-panel"},bs={class:"task-list-wrap"},ms={class:"clean-result-summary"},Ns={class:"summary-card"},Fs={class:"summary-card"},Ts={class:"summary-card"},Rs={class:"summary-card"},As={class:"result-list-wrap"},Es={key:0,class:"empty-tasks"},xs={key:1,class:"task-list clean-result-list"},Us={class:"left"},Ds={class:"id"},$s={class:"files"},Ms={class:"files"},zs={class:"files"},Gs={class:"files"},Ls={key:0,class:"files result-hint"},Bs={key:1,class:"file-progress"},Hs={class:"file-progress-meta"},Ks={class:"file-progress-track"},Js={class:"task-right"},qs={class:"status running"},js=["onClick"],Vs={class:"result-list-wrap"},Os={key:0,class:"empty-tasks"},Xs={key:1,class:"task-list clean-result-list"},Ys={class:"left"},Qs={class:"id"},Ws={class:"files"},Zs={class:"files"},et={class:"files"},st={key:0,class:"files"},tt={key:1,class:"files result-hint"},at={key:2,class:"file-progress"},nt={class:"file-progress-meta"},lt={class:"file-progress-track"},rt={key:3,class:"files"},ot={class:"task-right"},ut=["onClick"],it=["onClick"],ye=`请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。 -请直接给出明确结论(有侵权风险 或 未发现明显侵权风险),并严格按照以下三个维度提供精简的排查理由: -外观维度: 评估产品外形、图案设计是否与欧洲/英国常见外观专利雷同。 -专利维度: 评估产品的核心技术或物理结构是否存在侵权可能。 -标题维度: 排查标题中是否包含大牌商标、敏感词或版权保护词汇。 -结论:XX`,ct=3e3,dt=Ke({__name:"BrandAppearancePatentTab",setup(pt){const $=v([]),F=v([]),P=v(null),T=v(null),M=v(""),z=v(!1),G=v(!1),C=v(""),p=v([]),u=v([]),h=v(null),V=v(!1);let R=null,ae=0,A=!1;const E=ds("appearance-patent"),x=v({pendingTaskCount:0,processedTaskCount:0,successTaskCount:0,failedTaskCount:0}),g=v([]),w=v({}),b=j(()=>P.value||T.value),O=j(()=>{const e=P.value;return!e?.taskId||g.value.some(t=>t.taskId===e.taskId)?null:{taskId:e.taskId,sourceFilename:e.sourceFilename,rowCount:e.acceptedRows,taskStatus:"PENDING",success:!1}}),ne=j(()=>{const e=new Set([...p.value,...u.value]),s=g.value.filter(l=>{const n=_(l);return l.taskId!=null&&(e.has(l.taskId)||n==="RUNNING"||n==="PENDING")}).map(l=>re(l));for(const[l,n]of Object.entries(w.value)){const o=Number(l);!Number.isFinite(o)||o<=0||!e.has(o)&&_(n)!=="RUNNING"&&_(n)!=="PENDING"||s.some(d=>d.taskId===o)||s.push(re(n))}return O.value?s.some(l=>l.taskId===O.value?.taskId)?s:[O.value,...s]:s}),le=j(()=>g.value.filter(e=>{const s=_(e);return s!=="RUNNING"&&s!=="PENDING"}));function re(e){if(e.taskId==null)return e;const s=w.value[e.taskId];return s?{...e,...s,sourceFilename:s.sourceFilename||e.sourceFilename,resultFilename:s.resultFilename||e.resultFilename,rowCount:s.rowCount??e.rowCount,createdAt:s.createdAt||e.createdAt}:e}function _e(){const e=M.value.trim();return e?`${ye} - -新增条件要求: -${e}`:ye}function ke(){return M.value.trim()}function L(){return ls().trim()}function Ie(){return typeof window<"u"&&window.localStorage.getItem("uid")||"0"}function X(){return`appearance-patent:tasks:${Ie()}`}function Pe(e){return{taskId:e.taskId,totalRows:e.totalRows,acceptedRows:e.acceptedRows,groupCount:e.groupCount,droppedRows:e.droppedRows}}function S(){if(typeof window>"u")return;const e=Array.from(new Set([...p.value,...u.value]));e.length?window.localStorage.setItem(X(),JSON.stringify(e)):window.localStorage.removeItem(X())}function we(){if(!(typeof window>"u"))try{const e=window.localStorage.getItem(X()),s=e?JSON.parse(e):[];p.value=Array.isArray(s)?s.filter(t=>typeof t=="number"&&t>0):[]}catch{p.value=[]}}function B(e){if(!e)return"-";const s=new Date(e);if(Number.isNaN(s.getTime()))return e;const t=s.getFullYear(),l=String(s.getMonth()+1).padStart(2,"0"),n=String(s.getDate()).padStart(2,"0"),o=String(s.getHours()).padStart(2,"0"),d=String(s.getMinutes()).padStart(2,"0"),y=String(s.getSeconds()).padStart(2,"0");return`${t}-${l}-${n} ${o}:${d}:${y}`}async function oe(e){const s=D();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传能力");const t=[];for(const l of e){const n=typeof l=="string"?l:l.absolutePath,o=typeof l=="string"?void 0:l.relativePath,d=await s.upload_file_to_java(n,o);if(!d?.success||!d.data)throw new Error(d?.error||d?.message||`上传失败:${n}`);t.push(d.data)}return t}async function Ce(){const e=D();if(!e?.select_brand_xlsx_files||!e.upload_file_to_java){f.warning("当前环境不支持文件选择或上传");return}const s=await e.select_brand_xlsx_files();if(!s?.length)return;const t=await oe(s);F.value=t,$.value=t.map(l=>l.relativePath||l.originalFilename||l.fileKey),P.value=null,T.value=null,C.value=""}async function Se(){const e=D();if(!e?.select_brand_folder){f.warning("当前环境不支持文件夹选择,请在本地客户端中打开");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await cs(s);if(!t.success||!t.items?.length){f.warning(t.error||"该文件夹下没有可用的 xlsx 文件");return}const l=await oe(t.items);F.value=l,$.value=t.items.map(n=>n.relativePath||n.absolutePath),P.value=null,T.value=null,C.value="",f.success(`已选择文件夹内 ${t.items.length} 个 Excel 文件`)}catch(s){f.error(s instanceof Error?s.message:"选择失败")}}async function be(){if(!F.value.length){f.warning("请先选择 Excel");return}if(!L()){f.warning("请先在左上角设置中填写外观专利密钥");return}z.value=!0;try{const e=F.value.map(t=>({fileKey:t.fileKey,originalFilename:t.originalFilename,relativePath:t.relativePath})),s=await Qe(e,_e(),L());P.value=s,T.value=null,C.value="",f.success(`解析完成,共 ${s.groupCount||0} 组 / ${s.acceptedRows} 条`)}catch(e){f.error(e instanceof Error?e.message:"解析失败")}finally{z.value=!1}}async function me(){const e=D(),s=P.value,t=s?.taskId;if(!t){f.warning("请先解析文件");return}if(!e?.enqueue_json){f.error("当前环境未启用 pywebview enqueue_json");return}if(!L()){f.warning("请先在左上角设置中填写外观专利密钥");return}G.value=!0;try{const n=(await We(t)).groups||[],o={type:"appearance-patent-run",ts:Date.now(),data:{taskId:t,prompt:ke(),api_key:L(),sourceFileCount:s.sourceFileCount||0,totalRows:s.totalRows||0,acceptedRows:s.acceptedRows||0,groupCount:s.groupCount||0,groups:n.map(y=>({sourceFileKey:y.sourceFileKey||"",sourceFilename:y.sourceFilename||"",groupKey:y.groupKey||"",baseId:y.baseId||"",displayId:y.displayId||"",items:(y.items||[]).map(I=>({sourceFileKey:I.sourceFileKey||"",sourceFilename:I.sourceFilename||"",rowToken:I.rowToken||"",groupKey:I.groupKey||"",id:I.displayId,asin:I.asin,country:I.country,url:I.url||"",title:I.title||""}))}))}};C.value=`任务 ${t} 已准备推送,共 ${n.length} 组`,await Ze(t);const d=await e.enqueue_json(o);if(!d?.success){f.error(d?.error||"推送失败");return}T.value=Pe(s),Fe(t),Ne(),await H(),await K({force:!0}),f.success("已推送到 Python 队列")}finally{G.value=!1}}function Ne(){P.value=null,C.value=""}function Fe(e){p.value.includes(e)||(p.value=[...p.value,e],S()),m()}function Y(e){if(p.value=p.value.filter(s=>s!==e),!u.value.includes(e)){const s={...w.value};delete s[e],w.value=s}S()}function Te(e){u.value.includes(e)||(u.value=[...u.value,e],S()),m()}function Q(e){if(u.value=u.value.filter(s=>s!==e),!p.value.includes(e)){const s={...w.value};delete s[e],w.value=s}S()}function m(){A||h.value==null&&Re(!0)}function ue(){h.value!=null&&(E.clearTimer("task-poll",h.value),h.value=null)}function Re(e=!1){if(A)return;if(h.value!=null){if(!e)return;E.clearTimer("task-poll",h.value),h.value=null}const s=async()=>{h.value=null,!A&&(!p.value.length&&!u.value.length||(await Ae(),!A&&h.value==null&&(p.value.length||u.value.length)&&(h.value=E.setTimeout("task-poll",s,he()))))};e?s():h.value=E.setTimeout("task-poll",s,he())}async function Ae(){if(V.value||!p.value.length&&!u.value.length){!p.value.length&&!u.value.length&&ue();return}V.value=!0;try{let e=!1,s=!1;const t=Array.from(new Set([...p.value,...u.value]));if(t.length){const l=await rs(t,{force:u.value.length>0});for(const n of l.items||[]){const o=n.task;if(!o?.id)continue;const d=n.items?.[0];if(d&&(w.value={...w.value,[o.id]:d},Ee(d)),(o.status==="SUCCESS"||o.status==="FAILED")&&(Y(o.id),e=!0,o.status==="SUCCESS"?Te(o.id):Q(o.id)),d&&o.status==="SUCCESS"&&u.value.includes(o.id)){const y=(d.fileStatus||"").toUpperCase();(d.fileReady||y==="SUCCESS"||y==="FAILED")&&(Q(o.id),e=!0,s=!0)}}l.missingTaskIds?.length&&(l.missingTaskIds.forEach(n=>{Y(n),Q(n)}),s=!0,e=!0)}e&&await H(),s&&await K(),xe()}finally{V.value=!1}}function Ee(e){if(!e.taskId&&!e.resultId)return;const s=g.value.findIndex(t=>e.resultId!=null&&t.resultId===e.resultId||e.taskId!=null&&t.taskId===e.taskId);s>=0?g.value[s]={...g.value[s],...e}:g.value=[e,...g.value]}function xe(){if(!u.value.length)return;const e=[];for(const s of u.value){const t=g.value.find(o=>o.taskId===s);if(!t){e.push(s);continue}const l=(t.taskStatus||"").toUpperCase(),n=(t.fileStatus||"").toUpperCase();l==="FAILED"||t.fileReady||n==="SUCCESS"||n==="FAILED"||l==="SUCCESS"&&e.push(s)}u.value=e,S(),u.value.length&&m()}function Ue(){const e=g.value.filter(s=>s.taskId!=null&&N(s)).map(s=>s.taskId);u.value=Array.from(new Set(e)),S(),u.value.length&&m()}function De(){const e=g.value.filter(s=>s.taskId!=null&&_(s)==="RUNNING").map(s=>s.taskId);e.length&&(p.value=Array.from(new Set([...p.value,...e])),S(),m())}async function H(){x.value=await es()}async function K(e={}){const s=Date.now();if(!(!e.force&&g.value.length&&s-ae{g.value=$e(g.value,t.items||[]),ae=Date.now()}).finally(()=>{R=null}),R)}function $e(e,s){const t=new Map,l=new Map;for(const n of e||[])n.taskId!=null&&t.set(n.taskId,n),n.resultId!=null&&l.set(n.resultId,n);return(s||[]).map(n=>{const o=n.resultId!=null?l.get(n.resultId):n.taskId!=null?t.get(n.taskId):void 0;return Me(o,n)})}function Me(e,s){return!e||!((_(e)==="RUNNING"||N(e)||U(e))&&!U(s)&&!ee(s))?s:{...s,fileJobId:s.fileJobId??e.fileJobId,fileStatus:s.fileStatus||e.fileStatus,fileError:s.fileError||e.fileError,fileReady:s.fileReady||e.fileReady,fileProgressPercent:W(s.fileProgressPercent)?s.fileProgressPercent:e.fileProgressPercent,fileProgressCurrent:W(s.fileProgressCurrent)?s.fileProgressCurrent:e.fileProgressCurrent,fileProgressTotal:W(s.fileProgressTotal)?s.fileProgressTotal:e.fileProgressTotal,fileProgressMessage:(s.fileProgressMessage||"").trim()?s.fileProgressMessage:e.fileProgressMessage}}function W(e){const s=Number(e);return Number.isFinite(s)&&s>0}function ie(e){const s=_(e);return s==="RUNNING"?"执行中":s==="PENDING"?"已解析待推送":N(e)?"结果生成中":Z(e)?"结果生成失败":s==="SUCCESS"||e.success?"已完成":s==="FAILED"?"失败":"等待中"}function ze(e){const s=_(e);return s==="RUNNING"||N(e)?"running":Z(e)||s==="FAILED"?"failed":s==="SUCCESS"||e.success?"success":"pending"}function N(e){const s=_(e),t=(e.fileStatus||"").toUpperCase();return!(s!=="SUCCESS"||ee(e)||t==="FAILED"||t==="SUCCESS")}function _(e){return(e.taskStatus||"").toUpperCase()}function Z(e){return(e.fileStatus||"").toUpperCase()==="FAILED"}function J(e){if(Z(e))return e.fileError||"结果文件生成失败,请稍后重试或检查后端日志";if(U(e)||!N(e))return"";const s=(e.fileStatus||"").toUpperCase();return s==="RUNNING"?"任务已完成,正在等待 Coze 回流并组装结果文件,下载按钮稍后出现":s==="PENDING"?"任务已完成,结果文件已入队,正在等待后端开始处理":"任务已完成,正在生成结果文件,下载按钮稍后出现"}function ee(e){return!!(e.resultId&&(e.fileReady||e.downloadUrl))}function q(e){const s=Number(e.fileProgressPercent||0);return Number.isFinite(s)&&s>0?Math.max(0,Math.min(100,Math.round(s))):pe(e)?fe(e)==="PENDING"?6:10:0}function ce(e,s){const t=(e.fileProgressMessage||"").trim();return t?Ge(t):s}function Ge(e){const s=e.match(/^Coze submitted (\d+) batches, completed (\d+), waiting for results$/);if(s)return`已提交 Coze ${s[1]} 批,已完成 ${s[2]} 批,等待结果回流`;const t=e.match(/^Coze completed (\d+) batches, waiting for remaining Python data$/);return t?`Coze 已完成 ${t[1]} 批,等待 Python 继续回传数据`:{"Receiving Python data, waiting for 50 rows before Coze":"正在接收 Python 数据,累计 50 条后提交 Coze","Submitting Coze":"正在提交 Coze","Coze submitted, waiting for result":"Coze 已提交,等待结果回流","Waiting for Python upload":"等待 Python 继续回传数据","Coze results ready, assembling xlsx":"Coze 结果已回流,正在组装 xlsx","Assembling xlsx":"正在组装 xlsx","Uploading result file":"正在上传结果文件","Result file generated":"结果文件已生成"}[e]||e}function U(e){return(_(e)==="RUNNING"||N(e))&&(de(e)||pe(e))}function de(e){return Le(e)||Number(e.fileProgressPercent||0)>0||!!(e.fileProgressMessage||"").trim()}function Le(e){return(e.fileProgressTotal||0)>0}function fe(e){return(e.fileStatus||"").toUpperCase()}function Be(e){const s=fe(e);return e.fileJobId!=null||s==="PENDING"||s==="RUNNING"}function pe(e){return Be(e)&&!de(e)}async function He(e){if(!e.resultId)return;const s=D();if(!s?.save_file_from_url_new){f.error("当前客户端未提供下载能力");return}const t=e.downloadUrl||ts(e.resultId),l=e.resultFilename||`${e.sourceFilename||"appearance-patent"}.xlsx`,n=await s.save_file_from_url_new(t,l);n.success?f.success(`已保存: ${n.path||l}`):n.error&&n.error!=="用户取消"&&f.error(n.error)}async function ve(e){try{e.taskStatus==="RUNNING"&&e.taskId?(await as(e.taskId),Y(e.taskId)):e.resultId&&await ns(e.resultId),await H(),await K({force:!0}),f.success("已删除")}catch(s){f.error(s instanceof Error?s.message:"删除失败")}}return Je(async()=>{we(),await Promise.all([H().catch(()=>{}),K().catch(()=>{})]),De(),Ue(),(p.value.length||u.value.length)&&m()}),qe(()=>{A=!0,ue(),E.clearScope()}),(e,s)=>(i(),c("div",fs,[je(Ve,{active:"appearance-patent"}),a("div",ps,[a("aside",vs,[s[3]||(s[3]=a("div",{class:"section-title"},"上传文件",-1)),a("div",gs,[s[1]||(s[1]=a("div",{class:"hint"},"选择 Excel 后点击解析,后端会提取 id、ASIN、国家,并保留整数 id 与子数据的第一条。",-1)),a("div",{class:"btns"},[a("button",{type:"button",class:"opt-btn",onClick:Ce},"选择 Excel"),a("button",{type:"button",class:"opt-btn",onClick:Se},"选择文件夹")]),a("div",hs,[$.value.length?(i(!0),c(se,{key:1},te($.value,t=>(i(),c("span",{key:t},r(t),1))),128)):(i(),c("span",ys,"暂未选择文件"))])]),a("div",_s,[s[2]||(s[2]=a("div",{class:"section-title"},"新增条件要求",-1)),Oe(a("textarea",{"onUpdate:modelValue":s[0]||(s[0]=t=>M.value=t),class:"prompt-input",rows:"8",placeholder:"可选,仅在激活任务时传递这里填写的 prompt"},null,512),[[Xe,M.value]])]),a("div",ks,[a("button",{type:"button",class:"btn-run",disabled:z.value||!F.value.length,onClick:be},r(z.value?"解析中...":"解析并创建任务"),9,Is),a("button",{type:"button",class:"btn-run btn-queue",disabled:G.value||!P.value?.taskId,onClick:me},r(G.value?"推送中...":"推送到 Python 队列"),9,Ps)]),s[4]||(s[4]=a("p",{class:"loading-msg"},"Python 回传字段按 asin、国家、url、标题提交;后端接收分片后累计 50 条调用 Coze,当前任务区展示 Coze 回流和结果组装进度。",-1)),b.value?(i(),c("div",ws,[a("div",null,"任务 ID:"+r(b.value.taskId),1),a("div",null,"总行数:"+r(b.value.totalRows)+",有效:"+r(b.value.acceptedRows)+",分组:"+r(b.value.groupCount||0)+",过滤:"+r(b.value.droppedRows),1)])):k("",!0),C.value?(i(),c("pre",Cs,r(C.value),1)):k("",!0)]),a("section",Ss,[s[12]||(s[12]=a("div",{class:"panel-header"},"外观专利检测",-1)),a("div",bs,[a("div",ms,[a("div",Ns,[s[5]||(s[5]=a("span",{class:"summary-label"},"运行中任务",-1)),a("strong",null,r(x.value.pendingTaskCount),1)]),a("div",Fs,[s[6]||(s[6]=a("span",{class:"summary-label"},"已结束任务",-1)),a("strong",null,r(x.value.processedTaskCount),1)]),a("div",Ts,[s[7]||(s[7]=a("span",{class:"summary-label"},"成功任务",-1)),a("strong",null,r(x.value.successTaskCount),1)]),a("div",Rs,[s[8]||(s[8]=a("span",{class:"summary-label"},"失败任务",-1)),a("strong",null,r(x.value.failedTaskCount),1)])]),s[11]||(s[11]=a("div",{class:"subsection-title"},"匹配任务",-1)),a("div",As,[s[9]||(s[9]=a("div",{class:"result-list-header"},[a("span",null,"当前任务")],-1)),ne.value.length?(i(),c("ul",xs,[(i(!0),c(se,null,te(ne.value,t=>(i(),c("li",{key:`cur-${t.taskId}`,class:"task-item"},[a("div",Us,[a("span",Ds,r(t.sourceFilename||"外观专利检测"),1),a("div",$s,"任务 ID:"+r(t.taskId),1),a("div",Ms,"开始时间:"+r(B(t.startedAt||t.createdAt)),1),a("div",zs,"结束时间:"+r(B(t.finishedAt)),1),a("div",Gs,"行数:"+r(t.rowCount??"-"),1),J(t)?(i(),c("div",Ls,r(J(t)),1)):k("",!0),U(t)?(i(),c("div",Bs,[a("div",Hs,[a("span",null,r(ce(t,"处理中")),1),a("span",null,r(q(t))+"%",1)]),a("div",Ks,[a("div",{class:"file-progress-bar",style:ge({width:`${q(t)}%`})},null,4)])])):k("",!0)]),a("div",Js,[a("span",qs,r(ie(t)),1),a("button",{type:"button",class:"btn-delete",onClick:l=>ve(t)},"删除",8,js)])]))),128))])):(i(),c("div",Es,"暂无当前任务"))]),a("div",Vs,[s[10]||(s[10]=a("div",{class:"result-list-header"},[a("span",null,"历史记录")],-1)),le.value.length?(i(),c("ul",Xs,[(i(!0),c(se,null,te(le.value,t=>(i(),c("li",{key:`his-${t.resultId}-${t.taskId}`,class:"task-item"},[a("div",Ys,[a("span",Qs,r(t.sourceFilename||"外观专利检测"),1),a("div",Ws,"任务 ID:"+r(t.taskId??"-"),1),a("div",Zs,"开始时间:"+r(B(t.startedAt||t.createdAt)),1),a("div",et,"结束时间:"+r(B(t.finishedAt)),1),t.resultFilename?(i(),c("div",st,r(t.resultFilename||"下载结果"),1)):k("",!0),J(t)?(i(),c("div",tt,r(J(t)),1)):k("",!0),U(t)?(i(),c("div",at,[a("div",nt,[a("span",null,r(ce(t,"结果生成中")),1),a("span",null,r(q(t))+"%",1)]),a("div",lt,[a("div",{class:"file-progress-bar",style:ge({width:`${q(t)}%`})},null,4)])])):k("",!0),t.error?(i(),c("div",rt,"错误:"+r(t.error),1)):k("",!0)]),a("div",ot,[a("span",{class:Ye(["status",ze(t)])},r(ie(t)),3),ee(t)?(i(),c("button",{key:0,type:"button",class:"download",onClick:l=>He(t)},"下载",8,ut)):k("",!0),t.resultId?(i(),c("button",{key:1,type:"button",class:"btn-delete",onClick:l=>ve(t)},"删除",8,it)):k("",!0)])]))),128))])):(i(),c("div",Os,"暂无历史记录"))])])])])]))}}),ft=os(dt,[["__scopeId","data-v-f1e69f37"]]);us(ft).use(is).mount("#app"); +import{d as He,o as Ke,L as Je,a as i,c,b as qe,B as je,e as a,F as se,r as te,t as r,w as Ve,G as Oe,f as k,N as ge,n as Ye,g as v,h as j,i as D,E as f,V as Qe,W as We,X as Xe,U as he,Y as Ze,Z as es,$ as ss,a0 as ts,a1 as as,a2 as ns,a3 as ls,_ as rs,q as os,s as us}from"./pywebview-3bFwh9Cg.js";import{e as is}from"./brand-D__LhhPM.js";import{c as cs}from"./categorized-timers-JPA-olTr.js";const ds={class:"page-shell module-page"},fs={class:"main-content"},ps={class:"left-panel"},vs={class:"upload-zone"},gs={class:"selected-files"},hs={key:0},ys={class:"prompt-card"},_s={class:"run-row"},ks=["disabled"],Is=["disabled"],Ps={key:0,class:"parse-card"},ws={key:1,class:"queue-payload"},Cs={class:"right-panel"},Ss={class:"task-list-wrap"},bs={class:"clean-result-summary"},ms={class:"summary-card"},Ns={class:"summary-card"},Fs={class:"summary-card"},Ts={class:"summary-card"},Rs={class:"result-list-wrap"},As={key:0,class:"empty-tasks"},Es={key:1,class:"task-list clean-result-list"},xs={class:"left"},Us={class:"id"},Ds={class:"files"},$s={class:"files"},Ms={class:"files"},zs={class:"files"},Gs={key:0,class:"files result-hint"},Ls={key:1,class:"file-progress"},Bs={class:"file-progress-meta"},Hs={class:"file-progress-track"},Ks={class:"task-right"},Js={class:"status running"},qs=["onClick"],js={class:"result-list-wrap"},Vs={key:0,class:"empty-tasks"},Os={key:1,class:"task-list clean-result-list"},Ys={class:"left"},Qs={class:"id"},Ws={class:"files"},Xs={class:"files"},Zs={class:"files"},et={key:0,class:"files"},st={key:1,class:"files result-hint"},tt={key:2,class:"file-progress"},at={class:"file-progress-meta"},nt={class:"file-progress-track"},lt={key:3,class:"files"},rt={class:"task-right"},ot=["onClick"],ut=["onClick"],it=3e3,ct=He({__name:"BrandAppearancePatentTab",setup(ft){const $=v([]),F=v([]),P=v(null),T=v(null),M=v(""),z=v(!1),G=v(!1),C=v(""),p=v([]),u=v([]),h=v(null),V=v(!1);let R=null,ae=0,A=!1;const E=cs("appearance-patent"),x=v({pendingTaskCount:0,processedTaskCount:0,successTaskCount:0,failedTaskCount:0}),g=v([]),w=v({}),b=j(()=>P.value||T.value),O=j(()=>{const e=P.value;return!e?.taskId||g.value.some(t=>t.taskId===e.taskId)?null:{taskId:e.taskId,sourceFilename:e.sourceFilename,rowCount:e.acceptedRows,taskStatus:"PENDING",success:!1}}),ne=j(()=>{const e=new Set([...p.value,...u.value]),s=g.value.filter(l=>{const n=_(l);return l.taskId!=null&&(e.has(l.taskId)||n==="RUNNING"||n==="PENDING")}).map(l=>re(l));for(const[l,n]of Object.entries(w.value)){const o=Number(l);!Number.isFinite(o)||o<=0||!e.has(o)&&_(n)!=="RUNNING"&&_(n)!=="PENDING"||s.some(d=>d.taskId===o)||s.push(re(n))}return O.value?s.some(l=>l.taskId===O.value?.taskId)?s:[O.value,...s]:s}),le=j(()=>g.value.filter(e=>{const s=_(e);return s!=="RUNNING"&&s!=="PENDING"}));function re(e){if(e.taskId==null)return e;const s=w.value[e.taskId];return s?{...e,...s,sourceFilename:s.sourceFilename||e.sourceFilename,resultFilename:s.resultFilename||e.resultFilename,rowCount:s.rowCount??e.rowCount,createdAt:s.createdAt||e.createdAt}:e}function ye(){return M.value.trim()}function _e(){return M.value.trim()}function L(){return ns().trim()}function ke(){return typeof window<"u"&&window.localStorage.getItem("uid")||"0"}function Y(){return`appearance-patent:tasks:${ke()}`}function Ie(e){return{taskId:e.taskId,totalRows:e.totalRows,acceptedRows:e.acceptedRows,groupCount:e.groupCount,droppedRows:e.droppedRows}}function S(){if(typeof window>"u")return;const e=Array.from(new Set([...p.value,...u.value]));e.length?window.localStorage.setItem(Y(),JSON.stringify(e)):window.localStorage.removeItem(Y())}function Pe(){if(!(typeof window>"u"))try{const e=window.localStorage.getItem(Y()),s=e?JSON.parse(e):[];p.value=Array.isArray(s)?s.filter(t=>typeof t=="number"&&t>0):[]}catch{p.value=[]}}function B(e){if(!e)return"-";const s=new Date(e);if(Number.isNaN(s.getTime()))return e;const t=s.getFullYear(),l=String(s.getMonth()+1).padStart(2,"0"),n=String(s.getDate()).padStart(2,"0"),o=String(s.getHours()).padStart(2,"0"),d=String(s.getMinutes()).padStart(2,"0"),y=String(s.getSeconds()).padStart(2,"0");return`${t}-${l}-${n} ${o}:${d}:${y}`}async function oe(e){const s=D();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传能力");const t=[];for(const l of e){const n=typeof l=="string"?l:l.absolutePath,o=typeof l=="string"?void 0:l.relativePath,d=await s.upload_file_to_java(n,o);if(!d?.success||!d.data)throw new Error(d?.error||d?.message||`上传失败:${n}`);t.push(d.data)}return t}async function we(){const e=D();if(!e?.select_brand_xlsx_files||!e.upload_file_to_java){f.warning("当前环境不支持文件选择或上传");return}const s=await e.select_brand_xlsx_files();if(!s?.length)return;const t=await oe(s);F.value=t,$.value=t.map(l=>l.relativePath||l.originalFilename||l.fileKey),P.value=null,T.value=null,C.value=""}async function Ce(){const e=D();if(!e?.select_brand_folder){f.warning("当前环境不支持文件夹选择,请在本地客户端中打开");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await is(s);if(!t.success||!t.items?.length){f.warning(t.error||"该文件夹下没有可用的 xlsx 文件");return}const l=await oe(t.items);F.value=l,$.value=t.items.map(n=>n.relativePath||n.absolutePath),P.value=null,T.value=null,C.value="",f.success(`已选择文件夹内 ${t.items.length} 个 Excel 文件`)}catch(s){f.error(s instanceof Error?s.message:"选择失败")}}async function Se(){if(!F.value.length){f.warning("请先选择 Excel");return}if(!L()){f.warning("请先在左上角设置中填写外观专利密钥");return}z.value=!0;try{const e=F.value.map(t=>({fileKey:t.fileKey,originalFilename:t.originalFilename,relativePath:t.relativePath})),s=await Qe(e,ye(),L());P.value=s,T.value=null,C.value="",f.success(`解析完成,共 ${s.groupCount||0} 组 / ${s.acceptedRows} 条`)}catch(e){f.error(e instanceof Error?e.message:"解析失败")}finally{z.value=!1}}async function be(){const e=D(),s=P.value,t=s?.taskId;if(!t){f.warning("请先解析文件");return}if(!e?.enqueue_json){f.error("当前环境未启用 pywebview enqueue_json");return}if(!L()){f.warning("请先在左上角设置中填写外观专利密钥");return}G.value=!0;try{const n=(await We(t)).groups||[],o={type:"appearance-patent-run",ts:Date.now(),data:{taskId:t,prompt:_e(),api_key:L(),sourceFileCount:s.sourceFileCount||0,totalRows:s.totalRows||0,acceptedRows:s.acceptedRows||0,groupCount:s.groupCount||0,groups:n.map(y=>({sourceFileKey:y.sourceFileKey||"",sourceFilename:y.sourceFilename||"",groupKey:y.groupKey||"",baseId:y.baseId||"",displayId:y.displayId||"",items:(y.items||[]).map(I=>({sourceFileKey:I.sourceFileKey||"",sourceFilename:I.sourceFilename||"",rowToken:I.rowToken||"",groupKey:I.groupKey||"",id:I.displayId,asin:I.asin,country:I.country,url:I.url||"",title:I.title||""}))}))}};C.value=`任务 ${t} 已准备推送,共 ${n.length} 组`,await Xe(t);const d=await e.enqueue_json(o);if(!d?.success){f.error(d?.error||"推送失败");return}T.value=Ie(s),Ne(t),me(),await H(),await K({force:!0}),f.success("已推送到 Python 队列")}finally{G.value=!1}}function me(){P.value=null,C.value=""}function Ne(e){p.value.includes(e)||(p.value=[...p.value,e],S()),m()}function Q(e){if(p.value=p.value.filter(s=>s!==e),!u.value.includes(e)){const s={...w.value};delete s[e],w.value=s}S()}function Fe(e){u.value.includes(e)||(u.value=[...u.value,e],S()),m()}function W(e){if(u.value=u.value.filter(s=>s!==e),!p.value.includes(e)){const s={...w.value};delete s[e],w.value=s}S()}function m(){A||h.value==null&&Te(!0)}function ue(){h.value!=null&&(E.clearTimer("task-poll",h.value),h.value=null)}function Te(e=!1){if(A)return;if(h.value!=null){if(!e)return;E.clearTimer("task-poll",h.value),h.value=null}const s=async()=>{h.value=null,!A&&(!p.value.length&&!u.value.length||(await Re(),!A&&h.value==null&&(p.value.length||u.value.length)&&(h.value=E.setTimeout("task-poll",s,he()))))};e?s():h.value=E.setTimeout("task-poll",s,he())}async function Re(){if(V.value||!p.value.length&&!u.value.length){!p.value.length&&!u.value.length&&ue();return}V.value=!0;try{let e=!1,s=!1;const t=Array.from(new Set([...p.value,...u.value]));if(t.length){const l=await ls(t,{force:u.value.length>0});for(const n of l.items||[]){const o=n.task;if(!o?.id)continue;const d=n.items?.[0];if(d&&(w.value={...w.value,[o.id]:d},Ae(d)),(o.status==="SUCCESS"||o.status==="FAILED")&&(Q(o.id),e=!0,o.status==="SUCCESS"?Fe(o.id):W(o.id)),d&&o.status==="SUCCESS"&&u.value.includes(o.id)){const y=(d.fileStatus||"").toUpperCase();(d.fileReady||y==="SUCCESS"||y==="FAILED")&&(W(o.id),e=!0,s=!0)}}l.missingTaskIds?.length&&(l.missingTaskIds.forEach(n=>{Q(n),W(n)}),s=!0,e=!0)}e&&await H(),s&&await K(),Ee()}finally{V.value=!1}}function Ae(e){if(!e.taskId&&!e.resultId)return;const s=g.value.findIndex(t=>e.resultId!=null&&t.resultId===e.resultId||e.taskId!=null&&t.taskId===e.taskId);s>=0?g.value[s]={...g.value[s],...e}:g.value=[e,...g.value]}function Ee(){if(!u.value.length)return;const e=[];for(const s of u.value){const t=g.value.find(o=>o.taskId===s);if(!t){e.push(s);continue}const l=(t.taskStatus||"").toUpperCase(),n=(t.fileStatus||"").toUpperCase();l==="FAILED"||t.fileReady||n==="SUCCESS"||n==="FAILED"||l==="SUCCESS"&&e.push(s)}u.value=e,S(),u.value.length&&m()}function xe(){const e=g.value.filter(s=>s.taskId!=null&&N(s)).map(s=>s.taskId);u.value=Array.from(new Set(e)),S(),u.value.length&&m()}function Ue(){const e=g.value.filter(s=>s.taskId!=null&&_(s)==="RUNNING").map(s=>s.taskId);e.length&&(p.value=Array.from(new Set([...p.value,...e])),S(),m())}async function H(){x.value=await Ze()}async function K(e={}){const s=Date.now();if(!(!e.force&&g.value.length&&s-ae{g.value=De(g.value,t.items||[]),ae=Date.now()}).finally(()=>{R=null}),R)}function De(e,s){const t=new Map,l=new Map;for(const n of e||[])n.taskId!=null&&t.set(n.taskId,n),n.resultId!=null&&l.set(n.resultId,n);return(s||[]).map(n=>{const o=n.resultId!=null?l.get(n.resultId):n.taskId!=null?t.get(n.taskId):void 0;return $e(o,n)})}function $e(e,s){return!e||!((_(e)==="RUNNING"||N(e)||U(e))&&!U(s)&&!ee(s))?s:{...s,fileJobId:s.fileJobId??e.fileJobId,fileStatus:s.fileStatus||e.fileStatus,fileError:s.fileError||e.fileError,fileReady:s.fileReady||e.fileReady,fileProgressPercent:X(s.fileProgressPercent)?s.fileProgressPercent:e.fileProgressPercent,fileProgressCurrent:X(s.fileProgressCurrent)?s.fileProgressCurrent:e.fileProgressCurrent,fileProgressTotal:X(s.fileProgressTotal)?s.fileProgressTotal:e.fileProgressTotal,fileProgressMessage:(s.fileProgressMessage||"").trim()?s.fileProgressMessage:e.fileProgressMessage}}function X(e){const s=Number(e);return Number.isFinite(s)&&s>0}function ie(e){const s=_(e);return s==="RUNNING"?"执行中":s==="PENDING"?"已解析待推送":N(e)?"结果生成中":Z(e)?"结果生成失败":s==="SUCCESS"||e.success?"已完成":s==="FAILED"?"失败":"等待中"}function Me(e){const s=_(e);return s==="RUNNING"||N(e)?"running":Z(e)||s==="FAILED"?"failed":s==="SUCCESS"||e.success?"success":"pending"}function N(e){const s=_(e),t=(e.fileStatus||"").toUpperCase();return!(s!=="SUCCESS"||ee(e)||t==="FAILED"||t==="SUCCESS")}function _(e){return(e.taskStatus||"").toUpperCase()}function Z(e){return(e.fileStatus||"").toUpperCase()==="FAILED"}function J(e){if(Z(e))return e.fileError||"结果文件生成失败,请稍后重试或检查后端日志";if(U(e)||!N(e))return"";const s=(e.fileStatus||"").toUpperCase();return s==="RUNNING"?"任务已完成,正在等待 Coze 回流并组装结果文件,下载按钮稍后出现":s==="PENDING"?"任务已完成,结果文件已入队,正在等待后端开始处理":"任务已完成,正在生成结果文件,下载按钮稍后出现"}function ee(e){return!!(e.resultId&&(e.fileReady||e.downloadUrl))}function q(e){const s=Number(e.fileProgressPercent||0);return Number.isFinite(s)&&s>0?Math.max(0,Math.min(100,Math.round(s))):pe(e)?fe(e)==="PENDING"?6:10:0}function ce(e,s){const t=(e.fileProgressMessage||"").trim();return t?ze(t):s}function ze(e){const s=e.match(/^Coze submitted (\d+) batches, completed (\d+), waiting for results$/);if(s)return`已提交 Coze ${s[1]} 批,已完成 ${s[2]} 批,等待结果回流`;const t=e.match(/^Coze completed (\d+) batches, waiting for remaining Python data$/);return t?`Coze 已完成 ${t[1]} 批,等待 Python 继续回传数据`:{"Receiving Python data, waiting for 50 rows before Coze":"正在接收 Python 数据,累计 50 条后提交 Coze","Submitting Coze":"正在提交 Coze","Coze submitted, waiting for result":"Coze 已提交,等待结果回流","Waiting for Python upload":"等待 Python 继续回传数据","Coze results ready, assembling xlsx":"Coze 结果已回流,正在组装 xlsx","Assembling xlsx":"正在组装 xlsx","Uploading result file":"正在上传结果文件","Result file generated":"结果文件已生成"}[e]||e}function U(e){return(_(e)==="RUNNING"||N(e))&&(de(e)||pe(e))}function de(e){return Ge(e)||Number(e.fileProgressPercent||0)>0||!!(e.fileProgressMessage||"").trim()}function Ge(e){return(e.fileProgressTotal||0)>0}function fe(e){return(e.fileStatus||"").toUpperCase()}function Le(e){const s=fe(e);return e.fileJobId!=null||s==="PENDING"||s==="RUNNING"}function pe(e){return Le(e)&&!de(e)}async function Be(e){if(!e.resultId)return;const s=D();if(!s?.save_file_from_url_new){f.error("当前客户端未提供下载能力");return}const t=e.downloadUrl||ss(e.resultId),l=e.resultFilename||`${e.sourceFilename||"appearance-patent"}.xlsx`,n=await s.save_file_from_url_new(t,l);n.success?f.success(`已保存: ${n.path||l}`):n.error&&n.error!=="用户取消"&&f.error(n.error)}async function ve(e){try{e.taskStatus==="RUNNING"&&e.taskId?(await ts(e.taskId),Q(e.taskId)):e.resultId&&await as(e.resultId),await H(),await K({force:!0}),f.success("已删除")}catch(s){f.error(s instanceof Error?s.message:"删除失败")}}return Ke(async()=>{Pe(),await Promise.all([H().catch(()=>{}),K().catch(()=>{})]),Ue(),xe(),(p.value.length||u.value.length)&&m()}),Je(()=>{A=!0,ue(),E.clearScope()}),(e,s)=>(i(),c("div",ds,[qe(je,{active:"appearance-patent"}),a("div",fs,[a("aside",ps,[s[3]||(s[3]=a("div",{class:"section-title"},"上传文件",-1)),a("div",vs,[s[1]||(s[1]=a("div",{class:"hint"},"选择 Excel 后点击解析,后端会提取 id、ASIN、国家,并保留整数 id 与子数据的第一条。",-1)),a("div",{class:"btns"},[a("button",{type:"button",class:"opt-btn",onClick:we},"选择 Excel"),a("button",{type:"button",class:"opt-btn",onClick:Ce},"选择文件夹")]),a("div",gs,[$.value.length?(i(!0),c(se,{key:1},te($.value,t=>(i(),c("span",{key:t},r(t),1))),128)):(i(),c("span",hs,"暂未选择文件"))])]),a("div",ys,[s[2]||(s[2]=a("div",{class:"section-title"},"新增条件要求",-1)),Ve(a("textarea",{"onUpdate:modelValue":s[0]||(s[0]=t=>M.value=t),class:"prompt-input",rows:"8",placeholder:"可选,留空则不向 Coze 传 prompt 字段;填写后将作为 prompt 透传"},null,512),[[Oe,M.value]])]),a("div",_s,[a("button",{type:"button",class:"btn-run",disabled:z.value||!F.value.length,onClick:Se},r(z.value?"解析中...":"解析并创建任务"),9,ks),a("button",{type:"button",class:"btn-run btn-queue",disabled:G.value||!P.value?.taskId,onClick:be},r(G.value?"推送中...":"推送到 Python 队列"),9,Is)]),s[4]||(s[4]=a("p",{class:"loading-msg"},"Python 回传字段按 asin、国家、url、标题提交;后端接收分片后累计 50 条调用 Coze,当前任务区展示 Coze 回流和结果组装进度。",-1)),b.value?(i(),c("div",Ps,[a("div",null,"任务 ID:"+r(b.value.taskId),1),a("div",null,"总行数:"+r(b.value.totalRows)+",有效:"+r(b.value.acceptedRows)+",分组:"+r(b.value.groupCount||0)+",过滤:"+r(b.value.droppedRows),1)])):k("",!0),C.value?(i(),c("pre",ws,r(C.value),1)):k("",!0)]),a("section",Cs,[s[12]||(s[12]=a("div",{class:"panel-header"},"外观专利检测",-1)),a("div",Ss,[a("div",bs,[a("div",ms,[s[5]||(s[5]=a("span",{class:"summary-label"},"运行中任务",-1)),a("strong",null,r(x.value.pendingTaskCount),1)]),a("div",Ns,[s[6]||(s[6]=a("span",{class:"summary-label"},"已结束任务",-1)),a("strong",null,r(x.value.processedTaskCount),1)]),a("div",Fs,[s[7]||(s[7]=a("span",{class:"summary-label"},"成功任务",-1)),a("strong",null,r(x.value.successTaskCount),1)]),a("div",Ts,[s[8]||(s[8]=a("span",{class:"summary-label"},"失败任务",-1)),a("strong",null,r(x.value.failedTaskCount),1)])]),s[11]||(s[11]=a("div",{class:"subsection-title"},"匹配任务",-1)),a("div",Rs,[s[9]||(s[9]=a("div",{class:"result-list-header"},[a("span",null,"当前任务")],-1)),ne.value.length?(i(),c("ul",Es,[(i(!0),c(se,null,te(ne.value,t=>(i(),c("li",{key:`cur-${t.taskId}`,class:"task-item"},[a("div",xs,[a("span",Us,r(t.sourceFilename||"外观专利检测"),1),a("div",Ds,"任务 ID:"+r(t.taskId),1),a("div",$s,"开始时间:"+r(B(t.startedAt||t.createdAt)),1),a("div",Ms,"结束时间:"+r(B(t.finishedAt)),1),a("div",zs,"行数:"+r(t.rowCount??"-"),1),J(t)?(i(),c("div",Gs,r(J(t)),1)):k("",!0),U(t)?(i(),c("div",Ls,[a("div",Bs,[a("span",null,r(ce(t,"处理中")),1),a("span",null,r(q(t))+"%",1)]),a("div",Hs,[a("div",{class:"file-progress-bar",style:ge({width:`${q(t)}%`})},null,4)])])):k("",!0)]),a("div",Ks,[a("span",Js,r(ie(t)),1),a("button",{type:"button",class:"btn-delete",onClick:l=>ve(t)},"删除",8,qs)])]))),128))])):(i(),c("div",As,"暂无当前任务"))]),a("div",js,[s[10]||(s[10]=a("div",{class:"result-list-header"},[a("span",null,"历史记录")],-1)),le.value.length?(i(),c("ul",Os,[(i(!0),c(se,null,te(le.value,t=>(i(),c("li",{key:`his-${t.resultId}-${t.taskId}`,class:"task-item"},[a("div",Ys,[a("span",Qs,r(t.sourceFilename||"外观专利检测"),1),a("div",Ws,"任务 ID:"+r(t.taskId??"-"),1),a("div",Xs,"开始时间:"+r(B(t.startedAt||t.createdAt)),1),a("div",Zs,"结束时间:"+r(B(t.finishedAt)),1),t.resultFilename?(i(),c("div",et,r(t.resultFilename||"下载结果"),1)):k("",!0),J(t)?(i(),c("div",st,r(J(t)),1)):k("",!0),U(t)?(i(),c("div",tt,[a("div",at,[a("span",null,r(ce(t,"结果生成中")),1),a("span",null,r(q(t))+"%",1)]),a("div",nt,[a("div",{class:"file-progress-bar",style:ge({width:`${q(t)}%`})},null,4)])])):k("",!0),t.error?(i(),c("div",lt,"错误:"+r(t.error),1)):k("",!0)]),a("div",rt,[a("span",{class:Ye(["status",Me(t)])},r(ie(t)),3),ee(t)?(i(),c("button",{key:0,type:"button",class:"download",onClick:l=>Be(t)},"下载",8,ot)):k("",!0),t.resultId?(i(),c("button",{key:1,type:"button",class:"btn-delete",onClick:l=>ve(t)},"删除",8,ut)):k("",!0)])]))),128))])):(i(),c("div",Vs,"暂无历史记录"))])])])])]))}}),dt=rs(ct,[["__scopeId","data-v-66aa0b14"]]);os(dt).use(us).mount("#app"); diff --git a/app/assets/similar-asin-B5CvMmwV.css b/app/assets/similar-asin-B5CvMmwV.css deleted file mode 100644 index d3f728b..0000000 --- a/app/assets/similar-asin-B5CvMmwV.css +++ /dev/null @@ -1 +0,0 @@ -.module-page[data-v-49f8fcb3]{min-height:100vh;background:#1a1a1a}.main-content[data-v-49f8fcb3]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-49f8fcb3]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-49f8fcb3]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-49f8fcb3],.subsection-title[data-v-49f8fcb3]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-49f8fcb3]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-49f8fcb3],.loading-msg[data-v-49f8fcb3],.files[data-v-49f8fcb3],.muted[data-v-49f8fcb3]{color:#888;font-size:12px;line-height:1.5}.link[data-v-49f8fcb3]{color:#6ea8fe;text-decoration:none}.link[data-v-49f8fcb3]:hover{color:#9fc5ff}.btns[data-v-49f8fcb3],.run-row[data-v-49f8fcb3]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-49f8fcb3],.btn-run[data-v-49f8fcb3],.btn-delete[data-v-49f8fcb3],.download[data-v-49f8fcb3]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-49f8fcb3]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-49f8fcb3]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-49f8fcb3]{background:#27ae60}.btn-run[data-v-49f8fcb3]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-49f8fcb3]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-49f8fcb3]{display:block;margin:4px 0}.parse-card[data-v-49f8fcb3],.queue-payload[data-v-49f8fcb3]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-49f8fcb3]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-49f8fcb3]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-49f8fcb3]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-49f8fcb3]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-49f8fcb3]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-49f8fcb3]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-49f8fcb3]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-49f8fcb3]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-49f8fcb3]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-49f8fcb3]{color:#666;font-size:13px;padding:18px;text-align:center}.task-list[data-v-49f8fcb3]{list-style:none;margin:0;padding:12px}.task-item[data-v-49f8fcb3]{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-49f8fcb3]{flex:1;min-width:0}.id[data-v-49f8fcb3]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-49f8fcb3]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-49f8fcb3]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-49f8fcb3]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-49f8fcb3]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-49f8fcb3]{background:#3498db2e;color:#3498db}.status.pending[data-v-49f8fcb3]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-49f8fcb3]{margin-top:6px;color:#e0b96d}.file-progress[data-v-49f8fcb3]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-49f8fcb3]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-49f8fcb3]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-49f8fcb3]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.file-progress-count[data-v-49f8fcb3]{margin-top:4px;color:#858585;font-size:11px}.download[data-v-49f8fcb3]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-49f8fcb3]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-49f8fcb3]{flex-direction:column;height:auto}.left-panel[data-v-49f8fcb3]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-49f8fcb3]{grid-template-columns:repeat(2,minmax(0,1fr))}} diff --git a/app/assets/similar-asin-BgG5p7TZ.css b/app/assets/similar-asin-BgG5p7TZ.css new file mode 100644 index 0000000..da2f4d9 --- /dev/null +++ b/app/assets/similar-asin-BgG5p7TZ.css @@ -0,0 +1 @@ +.module-page[data-v-a27e815e]{min-height:100vh;background:#1a1a1a}.main-content[data-v-a27e815e]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-a27e815e]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-a27e815e]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-a27e815e],.subsection-title[data-v-a27e815e]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-a27e815e]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-a27e815e],.loading-msg[data-v-a27e815e],.files[data-v-a27e815e],.muted[data-v-a27e815e]{color:#888;font-size:12px;line-height:1.5}.link[data-v-a27e815e]{color:#6ea8fe;text-decoration:none}.link[data-v-a27e815e]:hover{color:#9fc5ff}.btns[data-v-a27e815e],.run-row[data-v-a27e815e]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-a27e815e],.btn-run[data-v-a27e815e],.btn-delete[data-v-a27e815e],.download[data-v-a27e815e]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-a27e815e]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-a27e815e]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-a27e815e]{background:#27ae60}.btn-run[data-v-a27e815e]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-a27e815e]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-a27e815e]{display:block;margin:4px 0}.parse-card[data-v-a27e815e],.queue-payload[data-v-a27e815e]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-a27e815e]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-a27e815e]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-a27e815e]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-a27e815e]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-a27e815e]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-a27e815e]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-a27e815e]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-a27e815e]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-a27e815e]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-a27e815e]{color:#666;font-size:13px;padding:18px;text-align:center}.task-list[data-v-a27e815e]{list-style:none;margin:0;padding:12px}.task-item[data-v-a27e815e]{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-a27e815e]{flex:1;min-width:0}.id[data-v-a27e815e]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-a27e815e]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-a27e815e]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-a27e815e]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-a27e815e]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-a27e815e]{background:#3498db2e;color:#3498db}.status.pending[data-v-a27e815e]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-a27e815e]{margin-top:6px;color:#e0b96d}.file-progress[data-v-a27e815e]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-a27e815e]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-a27e815e]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-a27e815e]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.file-progress-count[data-v-a27e815e]{margin-top:4px;color:#858585;font-size:11px}.download[data-v-a27e815e]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-a27e815e]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-a27e815e]{flex-direction:column;height:auto}.left-panel[data-v-a27e815e]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-a27e815e]{grid-template-columns:repeat(2,minmax(0,1fr))}} diff --git a/app/assets/similar-asin.js b/app/assets/similar-asin.js index e5b2240..2a4c822 100644 --- a/app/assets/similar-asin.js +++ b/app/assets/similar-asin.js @@ -1 +1 @@ -import{d as Le,o as Be,L as He,a as o,c as i,b as Je,B as qe,e as a,F as Z,r as ee,t as l,f as h,N as ge,n as je,g as p,h as J,i as A,E as c,a4 as Ke,a5 as Oe,U as pe,a6 as Ve,a7 as Ye,a8 as Qe,a9 as We,aa as Xe,a2 as Ze,ab as es,_ as ss,q as ts,s as as}from"./pywebview-3bFwh9Cg.js";import{e as rs}from"./brand-D__LhhPM.js";import{c as ns}from"./categorized-timers-JPA-olTr.js";const ls={class:"page-shell module-page"},os={class:"main-content"},is={class:"left-panel"},us={class:"upload-zone"},cs={class:"selected-files"},ds={key:0},fs={class:"run-row"},vs=["disabled"],gs=["disabled"],ps={key:0,class:"parse-card"},hs={key:1,class:"queue-payload"},_s={class:"right-panel"},ks={class:"task-list-wrap"},ys={class:"clean-result-summary"},Is={class:"summary-card"},Ss={class:"summary-card"},ws={class:"summary-card"},Ps={class:"summary-card"},Cs={class:"result-list-wrap"},bs={key:0,class:"empty-tasks"},Ns={key:1,class:"task-list clean-result-list"},ms={class:"left"},Ts={class:"id"},Fs={class:"files"},Rs={class:"files"},Es={class:"files"},As={class:"files"},Us={key:0,class:"files result-hint"},Ds={key:1,class:"file-progress"},xs={class:"file-progress-meta"},$s={class:"file-progress-track"},Ms={key:0,class:"file-progress-count"},zs={class:"task-right"},Gs={class:"status running"},Ls=["onClick"],Bs={class:"result-list-wrap"},Hs={key:0,class:"empty-tasks"},Js={key:1,class:"task-list clean-result-list"},qs={class:"left"},js={class:"id"},Ks={class:"files"},Os={class:"files"},Vs={class:"files"},Ys={key:0,class:"files"},Qs={key:1,class:"files result-hint"},Ws={key:2,class:"file-progress"},Xs={class:"file-progress-meta"},Zs={class:"file-progress-track"},et={key:0,class:"file-progress-count"},st={key:3,class:"files"},tt={class:"task-right"},at=["onClick"],rt=["onClick"],nt=3e3,lt=Le({__name:"BrandSimilarAsinTab",setup(it){const U=p([]),C=p([]),k=p(null),b=p(null),D=p(!1),x=p(!1),I=p(""),f=p([]),u=p([]),_=p(null),q=p(!1);let N=null,se=0,m=!1;const T=ns("similar-asin"),F=p({pendingTaskCount:0,processedTaskCount:0,successTaskCount:0,failedTaskCount:0}),v=p([]),R=J(()=>k.value||b.value),j=J(()=>{const e=k.value;return!e?.taskId||v.value.some(t=>t.taskId===e.taskId)?null:{taskId:e.taskId,sourceFilename:e.sourceFilename,rowCount:e.acceptedRows,taskStatus:"PENDING",success:!1}}),te=J(()=>{const e=new Set([...f.value,...u.value]),s=v.value.filter(r=>{const n=y(r);return r.taskId!=null&&(e.has(r.taskId)||n==="RUNNING"||n==="PENDING")});return j.value?s.some(r=>r.taskId===j.value?.taskId)?s:[j.value,...s]:s}),ae=J(()=>v.value.filter(e=>{const s=y(e);return s!=="RUNNING"&&s!=="PENDING"}));function $(){return Ze().trim()}function M(e){if(!e)return"-";const s=new Date(e);if(Number.isNaN(s.getTime()))return e;const t=s.getFullYear(),r=String(s.getMonth()+1).padStart(2,"0"),n=String(s.getDate()).padStart(2,"0"),d=String(s.getHours()).padStart(2,"0"),g=String(s.getMinutes()).padStart(2,"0"),H=String(s.getSeconds()).padStart(2,"0");return`${t}-${r}-${n} ${d}:${g}:${H}`}function he(e){return e?e.length<=10?"***":`${e.slice(0,6)}***${e.slice(-4)}`:""}function _e(e){return{...e,data:e.data?{...e.data,api_key:he(String(e.data.api_key||""))}:e.data}}function re(){return typeof window<"u"&&window.localStorage.getItem("uid")||"0"}function ke(){return Number(re())||0}function K(){return`similar-asin:tasks:${re()}`}function ye(e){return{taskId:e.taskId,totalRows:e.totalRows,acceptedRows:e.acceptedRows,groupCount:e.groupCount,droppedRows:e.droppedRows}}function S(){if(typeof window>"u")return;const e=Array.from(new Set([...f.value,...u.value]));e.length?window.localStorage.setItem(K(),JSON.stringify(e)):window.localStorage.removeItem(K())}function Ie(){if(!(typeof window>"u"))try{const e=window.localStorage.getItem(K()),s=e?JSON.parse(e):[];f.value=Array.isArray(s)?s.filter(t=>typeof t=="number"&&t>0):[]}catch{f.value=[]}}async function ne(e){const s=A();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传能力");const t=[];for(const r of e){const n=typeof r=="string"?r:r.absolutePath,d=typeof r=="string"?void 0:r.relativePath,g=await s.upload_file_to_java(n,d);if(!g?.success||!g.data)throw new Error(g?.error||g?.message||`上传失败:${n}`);t.push(g.data)}return t}async function Se(){const e=A();if(!e?.select_brand_xlsx_files||!e.upload_file_to_java){c.warning("当前桌面端未提供文件选择或上传能力");return}const s=await e.select_brand_xlsx_files();if(!s?.length)return;const t=await ne(s);C.value=t,U.value=t.map(r=>r.relativePath||r.originalFilename||r.fileKey),k.value=null,b.value=null,I.value=""}async function we(){const e=A();if(!e?.select_brand_folder||!e.upload_file_to_java){c.warning("当前桌面端未提供文件夹选择或上传能力");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await rs(s);if(!t.success||!t.items?.length){c.warning(t.error||"该文件夹下没有可用的 xlsx 文件");return}const r=await ne(t.items);C.value=r,U.value=t.items.map(n=>n.relativePath||n.absolutePath),k.value=null,b.value=null,I.value="",c.success(`已选择文件夹内 ${t.items.length} 个 Excel 文件`)}catch(s){c.error(s instanceof Error?s.message:"选择失败")}}async function Pe(){if(!C.value.length){c.warning("请先选择 Excel");return}if(!$()){c.warning("请先在左上角设置中填写货源查询密钥");return}D.value=!0;try{const e=C.value.map(t=>({fileKey:t.fileKey,originalFilename:t.originalFilename,relativePath:t.relativePath})),s=await Ke(e,$());k.value=s,b.value=null,I.value="",c.success(`解析完成,共 ${s.acceptedRows} 条`)}catch(e){c.error(e instanceof Error?e.message:"解析失败")}finally{D.value=!1}}async function Ce(){const e=A(),s=k.value,t=s?.taskId;if(!t){c.warning("请先解析文件");return}if(!e?.enqueue_json){c.error("当前环境未启用 pywebview enqueue_json");return}if(!$()){c.warning("请先在左上角设置中填写货源查询密钥");return}x.value=!0;try{const r={type:"similar-asin-run",ts:Date.now(),data:{taskId:t,user_id:ke(),api_key:$(),sourceFileCount:s.sourceFileCount||0,totalRows:s.totalRows||0,acceptedRows:s.acceptedRows||0,groupCount:s.groupCount||0}};I.value=JSON.stringify(_e(r),null,2),await Oe(t);const n=await e.enqueue_json(r);if(!n?.success){c.error(n?.error||"推送失败");return}b.value=ye(s),Ne(t),be(),await z(),await G({force:!0}),c.success("已推送到 Python 队列")}finally{x.value=!1}}function be(){k.value=null,I.value=""}function Ne(e){f.value.includes(e)||(f.value=[...f.value,e],S()),w()}function O(e){f.value=f.value.filter(s=>s!==e),S()}function me(e){u.value.includes(e)||(u.value=[...u.value,e],S()),w()}function V(e){u.value=u.value.filter(s=>s!==e),S()}function w(){m||_.value==null&&Te(!0)}function le(){_.value!=null&&(T.clearTimer("task-poll",_.value),_.value=null)}function Te(e=!1){if(m)return;if(_.value!=null){if(!e)return;T.clearTimer("task-poll",_.value),_.value=null}const s=async()=>{_.value=null,!m&&(!f.value.length&&!u.value.length||(await Fe(),!m&&_.value==null&&(f.value.length||u.value.length)&&(_.value=T.setTimeout("task-poll",s,pe()))))};e?s():_.value=T.setTimeout("task-poll",s,pe())}async function Fe(){if(q.value||!f.value.length&&!u.value.length){!f.value.length&&!u.value.length&&le();return}q.value=!0;try{let e=!1,s=!1;const t=Array.from(new Set([...f.value,...u.value]));if(t.length){const r=await es(t,{force:u.value.length>0});for(const n of r.items||[]){const d=n.task;if(!d?.id)continue;const g=n.items?.[0];if(g&&Re(g),(d.status==="SUCCESS"||d.status==="FAILED")&&(O(d.id),e=!0,d.status==="SUCCESS"?me(d.id):V(d.id)),g&&d.status==="SUCCESS"&&u.value.includes(d.id)){const H=(g.fileStatus||"").toUpperCase();(g.fileReady||H==="SUCCESS"||H==="FAILED")&&(V(d.id),e=!0,s=!0)}}r.missingTaskIds?.length&&(r.missingTaskIds.forEach(n=>{O(n),V(n)}),s=!0,e=!0)}e&&await z(),s&&await G(),Ee()}finally{q.value=!1}}function Re(e){if(!e.taskId&&!e.resultId)return;const s=v.value.findIndex(t=>e.resultId!=null&&t.resultId===e.resultId||e.taskId!=null&&t.taskId===e.taskId);s>=0?v.value[s]={...v.value[s],...e}:v.value=[e,...v.value]}function Ee(){if(!u.value.length)return;const e=[];for(const s of u.value){const t=v.value.find(d=>d.taskId===s);if(!t){e.push(s);continue}const r=(t.taskStatus||"").toUpperCase(),n=(t.fileStatus||"").toUpperCase();r==="FAILED"||t.fileReady||n==="SUCCESS"||n==="FAILED"||r==="SUCCESS"&&e.push(s)}u.value=e,S(),u.value.length&&w()}function Ae(){const e=v.value.filter(s=>s.taskId!=null&&P(s)).map(s=>s.taskId);u.value=Array.from(new Set(e)),S(),u.value.length&&w()}function Ue(){const e=v.value.filter(s=>s.taskId!=null&&y(s)==="RUNNING").map(s=>s.taskId);e.length&&(f.value=Array.from(new Set([...f.value,...e])),S(),w())}async function z(){F.value=await Ve()}async function G(e={}){const s=Date.now();if(!(!e.force&&v.value.length&&s-se{v.value=De(v.value,t.items||[]),se=Date.now()}).finally(()=>{N=null}),N)}function De(e,s){const t=new Map,r=new Map;for(const n of e||[])n.taskId!=null&&t.set(n.taskId,n),n.resultId!=null&&r.set(n.resultId,n);return(s||[]).map(n=>{const d=n.resultId!=null?r.get(n.resultId):n.taskId!=null?t.get(n.taskId):void 0;return xe(d,n)})}function xe(e,s){return!e||!((y(e)==="RUNNING"||P(e)||E(e))&&!E(s)&&!W(s))?s:{...s,fileJobId:s.fileJobId??e.fileJobId,fileStatus:s.fileStatus||e.fileStatus,fileError:s.fileError||e.fileError,fileReady:s.fileReady||e.fileReady,fileProgressPercent:Y(s.fileProgressPercent)?s.fileProgressPercent:e.fileProgressPercent,fileProgressCurrent:Y(s.fileProgressCurrent)?s.fileProgressCurrent:e.fileProgressCurrent,fileProgressTotal:Y(s.fileProgressTotal)?s.fileProgressTotal:e.fileProgressTotal,fileProgressMessage:(s.fileProgressMessage||"").trim()?s.fileProgressMessage:e.fileProgressMessage}}function Y(e){const s=Number(e);return Number.isFinite(s)&&s>0}function oe(e){const s=y(e);return s==="RUNNING"?"执行中":s==="PENDING"?"已解析待推送":P(e)?"结果生成中":Q(e)?"结果生成失败":s==="SUCCESS"||e.success?"已完成":s==="FAILED"?"失败":"等待中"}function $e(e){const s=y(e);return s==="RUNNING"||P(e)?"running":Q(e)||s==="FAILED"?"failed":s==="SUCCESS"||e.success?"success":"pending"}function P(e){const s=y(e),t=(e.fileStatus||"").toUpperCase();return!(s!=="SUCCESS"||W(e)||t==="FAILED"||t==="SUCCESS")}function y(e){return(e.taskStatus||"").toUpperCase()}function Q(e){return(e.fileStatus||"").toUpperCase()==="FAILED"}function L(e){if(Q(e))return e.fileError||"结果文件生成失败,请稍后重试或检查后端日志";if(E(e)||!P(e))return"";const s=(e.fileStatus||"").toUpperCase();return s==="RUNNING"?"任务已完成,正在等待 Coze 回流并组装结果文件,下载按钮稍后出现":s==="PENDING"?"任务已完成,结果文件已入队,正在等待后端开始处理":"任务已完成,正在生成结果文件,下载按钮稍后出现"}function W(e){return!!(e.resultId&&(e.fileReady||e.downloadUrl))}function B(e){const s=Number(e.fileProgressPercent||0);return Number.isFinite(s)&&s>0?Math.max(0,Math.min(100,Math.round(s))):fe(e)?de(e)==="PENDING"?6:10:0}function ie(e,s){const t=(e.fileProgressMessage||"").trim();return t?/coze/i.test(t)||/Coze|回流/.test(t)?/submitting/i.test(t)||/提交/.test(t)?"正在提交 Coze 批次":/生成结果|结果文件|组装/i.test(t)?"Coze 已回流,正在生成结果文件":/waiting/i.test(t)||/等待/.test(t)||/\d+\s*\/\s*\d+/.test(t)?"Coze 回流中,等待结果回传":"Coze 处理中":/assembling|xlsx|组装|生成结果/i.test(t)?"正在组装结果文件":t:s}function ue(e){const s=(e.fileProgressMessage||"").trim(),t=Me(s),r=t?.current??e.fileProgressCurrent??0,n=t?.total??e.fileProgressTotal??0;return/coze/i.test(s)||/Coze|回流/.test(s)?/submitting/i.test(s)||/提交/.test(s)?`Coze 提交批次 ${r}/${n}`:`Coze 回流批次 ${r}/${n}`:`当前阶段 ${r}/${n}`}function Me(e){const s=e.match(/(\d+)\s*\/\s*(\d+)/);if(!s)return null;const t=Number(s[1]),r=Number(s[2]);return!Number.isFinite(t)||!Number.isFinite(r)?null:{current:t,total:r}}function E(e){return(y(e)==="RUNNING"||P(e))&&(ce(e)||fe(e))}function ce(e){return X(e)||Number(e.fileProgressPercent||0)>0||!!(e.fileProgressMessage||"").trim()}function X(e){return(e.fileProgressTotal||0)>0}function de(e){return(e.fileStatus||"").toUpperCase()}function ze(e){const s=de(e);return e.fileJobId!=null||s==="PENDING"||s==="RUNNING"}function fe(e){return ze(e)&&!ce(e)}async function Ge(e){if(!e.resultId)return;const s=A();if(!s?.save_file_from_url_new){c.error("当前客户端未提供下载能力");return}const t=e.downloadUrl||Qe(e.resultId),r=e.resultFilename||`${e.sourceFilename||"similar-asin"}.xlsx`,n=await s.save_file_from_url_new(t,r);n.success?c.success(`已保存: ${n.path||r}`):n.error&&n.error!=="用户取消"&&c.error(n.error)}async function ve(e){try{e.taskStatus==="RUNNING"&&e.taskId?(await We(e.taskId),O(e.taskId)):e.resultId&&await Xe(e.resultId),await z(),await G({force:!0}),c.success("已删除")}catch(s){c.error(s instanceof Error?s.message:"删除失败")}}return Be(async()=>{Ie(),await Promise.all([z().catch(()=>{}),G().catch(()=>{})]),Ae(),Ue(),(f.value.length||u.value.length)&&w()}),He(()=>{m=!0,le(),T.clearScope()}),(e,s)=>(o(),i("div",ls,[Je(qe,{active:"similar-asin"}),a("div",os,[a("aside",is,[s[1]||(s[1]=a("div",{class:"section-title"},"上传文件",-1)),a("div",us,[s[0]||(s[0]=a("div",{class:"hint"},"选择 Excel 后点击解析,后端会提取 id、ASIN、国家、价格,每一行都会独立送检。",-1)),a("div",{class:"btns"},[a("button",{type:"button",class:"opt-btn",onClick:Se},"选择 Excel"),a("button",{type:"button",class:"opt-btn",onClick:we},"选择文件夹")]),a("div",cs,[U.value.length?(o(!0),i(Z,{key:1},ee(U.value,t=>(o(),i("span",{key:t},l(t),1))),128)):(o(),i("span",ds,"暂未选择文件"))])]),a("div",fs,[a("button",{type:"button",class:"btn-run",disabled:D.value||!C.value.length,onClick:Pe},l(D.value?"解析中...":"解析并创建任务"),9,vs),a("button",{type:"button",class:"btn-run btn-queue",disabled:x.value||!k.value?.taskId,onClick:Ce},l(x.value?"推送中...":"推送到 Python 队列"),9,gs)]),s[2]||(s[2]=a("p",{class:"loading-msg"},"Python 回传字段按 id、asin、国家、价格、标题、图片链接数组提交;后端接收分片后按 10 条一批送检,结果区展示真实 Coze 批次进度。",-1)),R.value?(o(),i("div",ps,[a("div",null,"任务 ID:"+l(R.value.taskId),1),a("div",null,"总行数:"+l(R.value.totalRows)+",有效:"+l(R.value.acceptedRows)+",过滤:"+l(R.value.droppedRows),1)])):h("",!0),I.value?(o(),i("pre",hs,l(I.value),1)):h("",!0)]),a("section",_s,[s[10]||(s[10]=a("div",{class:"panel-header"},"货源查询",-1)),a("div",ks,[a("div",ys,[a("div",Is,[s[3]||(s[3]=a("span",{class:"summary-label"},"运行中任务",-1)),a("strong",null,l(F.value.pendingTaskCount),1)]),a("div",Ss,[s[4]||(s[4]=a("span",{class:"summary-label"},"已结束任务",-1)),a("strong",null,l(F.value.processedTaskCount),1)]),a("div",ws,[s[5]||(s[5]=a("span",{class:"summary-label"},"成功任务",-1)),a("strong",null,l(F.value.successTaskCount),1)]),a("div",Ps,[s[6]||(s[6]=a("span",{class:"summary-label"},"失败任务",-1)),a("strong",null,l(F.value.failedTaskCount),1)])]),s[9]||(s[9]=a("div",{class:"subsection-title"},"匹配任务",-1)),a("div",Cs,[s[7]||(s[7]=a("div",{class:"result-list-header"},[a("span",null,"当前任务")],-1)),te.value.length?(o(),i("ul",Ns,[(o(!0),i(Z,null,ee(te.value,t=>(o(),i("li",{key:`cur-${t.taskId}`,class:"task-item"},[a("div",ms,[a("span",Ts,l(t.sourceFilename||"货源查询"),1),a("div",Fs,"任务 ID:"+l(t.taskId),1),a("div",Rs,"开始时间:"+l(M(t.startedAt||t.createdAt)),1),a("div",Es,"结束时间:"+l(M(t.finishedAt)),1),a("div",As,"行数:"+l(t.rowCount??"-"),1),L(t)?(o(),i("div",Us,l(L(t)),1)):h("",!0),E(t)?(o(),i("div",Ds,[a("div",xs,[a("span",null,l(ie(t,"处理中")),1),a("span",null,"总进度 "+l(B(t))+"%",1)]),a("div",$s,[a("div",{class:"file-progress-bar",style:ge({width:`${B(t)}%`})},null,4)]),X(t)?(o(),i("div",Ms,l(ue(t)),1)):h("",!0)])):h("",!0)]),a("div",zs,[a("span",Gs,l(oe(t)),1),a("button",{type:"button",class:"btn-delete",onClick:r=>ve(t)},"删除",8,Ls)])]))),128))])):(o(),i("div",bs,"暂无当前任务"))]),a("div",Bs,[s[8]||(s[8]=a("div",{class:"result-list-header"},[a("span",null,"历史记录")],-1)),ae.value.length?(o(),i("ul",Js,[(o(!0),i(Z,null,ee(ae.value,t=>(o(),i("li",{key:`his-${t.resultId}-${t.taskId}`,class:"task-item"},[a("div",qs,[a("span",js,l(t.sourceFilename||"货源查询"),1),a("div",Ks,"任务 ID:"+l(t.taskId??"-"),1),a("div",Os,"开始时间:"+l(M(t.startedAt||t.createdAt)),1),a("div",Vs,"结束时间:"+l(M(t.finishedAt)),1),t.resultFilename?(o(),i("div",Ys,l(t.resultFilename||"下载结果"),1)):h("",!0),L(t)?(o(),i("div",Qs,l(L(t)),1)):h("",!0),E(t)?(o(),i("div",Ws,[a("div",Xs,[a("span",null,l(ie(t,"结果生成中")),1),a("span",null,"总进度 "+l(B(t))+"%",1)]),a("div",Zs,[a("div",{class:"file-progress-bar",style:ge({width:`${B(t)}%`})},null,4)]),X(t)?(o(),i("div",et,l(ue(t)),1)):h("",!0)])):h("",!0),t.error?(o(),i("div",st,"错误:"+l(t.error),1)):h("",!0)]),a("div",tt,[a("span",{class:je(["status",$e(t)])},l(oe(t)),3),W(t)?(o(),i("button",{key:0,type:"button",class:"download",onClick:r=>Ge(t)},"下载",8,at)):h("",!0),t.resultId?(o(),i("button",{key:1,type:"button",class:"btn-delete",onClick:r=>ve(t)},"删除",8,rt)):h("",!0)])]))),128))])):(o(),i("div",Hs,"暂无历史记录"))])])])])]))}}),ot=ss(lt,[["__scopeId","data-v-49f8fcb3"]]);ts(ot).use(as).mount("#app"); +import{d as Le,o as Be,L as He,a as o,c as i,b as Je,B as qe,e as a,F as Z,r as ee,t as l,f as h,N as ge,n as je,g as p,h as J,i as A,E as c,a4 as Ke,a5 as Oe,U as pe,a6 as Ve,a7 as Ye,a8 as Qe,a9 as We,aa as Xe,a2 as Ze,ab as es,_ as ss,q as ts,s as as}from"./pywebview-3bFwh9Cg.js";import{e as rs}from"./brand-D__LhhPM.js";import{c as ns}from"./categorized-timers-JPA-olTr.js";const ls={class:"page-shell module-page"},os={class:"main-content"},is={class:"left-panel"},us={class:"upload-zone"},cs={class:"selected-files"},ds={key:0},fs={class:"run-row"},vs=["disabled"],gs=["disabled"],ps={key:0,class:"parse-card"},hs={key:1,class:"queue-payload"},_s={class:"right-panel"},ks={class:"task-list-wrap"},ys={class:"clean-result-summary"},Is={class:"summary-card"},Ss={class:"summary-card"},ws={class:"summary-card"},Ps={class:"summary-card"},Cs={class:"result-list-wrap"},bs={key:0,class:"empty-tasks"},ms={key:1,class:"task-list clean-result-list"},Ns={class:"left"},Ts={class:"id"},Fs={class:"files"},Rs={class:"files"},Es={class:"files"},As={class:"files"},Us={key:0,class:"files result-hint"},Ds={key:1,class:"file-progress"},xs={class:"file-progress-meta"},$s={class:"file-progress-track"},Ms={key:0,class:"file-progress-count"},zs={class:"task-right"},Gs={class:"status running"},Ls=["onClick"],Bs={class:"result-list-wrap"},Hs={key:0,class:"empty-tasks"},Js={key:1,class:"task-list clean-result-list"},qs={class:"left"},js={class:"id"},Ks={class:"files"},Os={class:"files"},Vs={class:"files"},Ys={key:0,class:"files"},Qs={key:1,class:"files result-hint"},Ws={key:2,class:"file-progress"},Xs={class:"file-progress-meta"},Zs={class:"file-progress-track"},et={key:0,class:"file-progress-count"},st={key:3,class:"files"},tt={class:"task-right"},at=["onClick"],rt=["onClick"],nt=3e3,lt=Le({__name:"BrandSimilarAsinTab",setup(it){const U=p([]),C=p([]),k=p(null),b=p(null),D=p(!1),x=p(!1),I=p(""),f=p([]),u=p([]),_=p(null),q=p(!1);let m=null,se=0,N=!1;const T=ns("similar-asin"),F=p({pendingTaskCount:0,processedTaskCount:0,successTaskCount:0,failedTaskCount:0}),v=p([]),R=J(()=>k.value||b.value),j=J(()=>{const e=k.value;return!e?.taskId||v.value.some(t=>t.taskId===e.taskId)?null:{taskId:e.taskId,sourceFilename:e.sourceFilename,rowCount:e.acceptedRows,taskStatus:"PENDING",success:!1}}),te=J(()=>{const e=new Set([...f.value,...u.value]),s=v.value.filter(r=>{const n=y(r);return r.taskId!=null&&(e.has(r.taskId)||n==="RUNNING"||n==="PENDING")});return j.value?s.some(r=>r.taskId===j.value?.taskId)?s:[j.value,...s]:s}),ae=J(()=>v.value.filter(e=>{const s=y(e);return s!=="RUNNING"&&s!=="PENDING"}));function $(){return Ze().trim()}function M(e){if(!e)return"-";const s=new Date(e);if(Number.isNaN(s.getTime()))return e;const t=s.getFullYear(),r=String(s.getMonth()+1).padStart(2,"0"),n=String(s.getDate()).padStart(2,"0"),d=String(s.getHours()).padStart(2,"0"),g=String(s.getMinutes()).padStart(2,"0"),H=String(s.getSeconds()).padStart(2,"0");return`${t}-${r}-${n} ${d}:${g}:${H}`}function he(e){return e?e.length<=10?"***":`${e.slice(0,6)}***${e.slice(-4)}`:""}function _e(e){return{...e,data:e.data?{...e.data,api_key:he(String(e.data.api_key||""))}:e.data}}function re(){return typeof window<"u"&&window.localStorage.getItem("uid")||"0"}function ke(){return Number(re())||0}function K(){return`similar-asin:tasks:${re()}`}function ye(e){return{taskId:e.taskId,totalRows:e.totalRows,acceptedRows:e.acceptedRows,groupCount:e.groupCount,droppedRows:e.droppedRows}}function S(){if(typeof window>"u")return;const e=Array.from(new Set([...f.value,...u.value]));e.length?window.localStorage.setItem(K(),JSON.stringify(e)):window.localStorage.removeItem(K())}function Ie(){if(!(typeof window>"u"))try{const e=window.localStorage.getItem(K()),s=e?JSON.parse(e):[];f.value=Array.isArray(s)?s.filter(t=>typeof t=="number"&&t>0):[]}catch{f.value=[]}}async function ne(e){const s=A();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传能力");const t=[];for(const r of e){const n=typeof r=="string"?r:r.absolutePath,d=typeof r=="string"?void 0:r.relativePath,g=await s.upload_file_to_java(n,d);if(!g?.success||!g.data)throw new Error(g?.error||g?.message||`上传失败:${n}`);t.push(g.data)}return t}async function Se(){const e=A();if(!e?.select_brand_xlsx_files||!e.upload_file_to_java){c.warning("当前桌面端未提供文件选择或上传能力");return}const s=await e.select_brand_xlsx_files();if(!s?.length)return;const t=await ne(s);C.value=t,U.value=t.map(r=>r.relativePath||r.originalFilename||r.fileKey),k.value=null,b.value=null,I.value=""}async function we(){const e=A();if(!e?.select_brand_folder||!e.upload_file_to_java){c.warning("当前桌面端未提供文件夹选择或上传能力");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await rs(s);if(!t.success||!t.items?.length){c.warning(t.error||"该文件夹下没有可用的 xlsx 文件");return}const r=await ne(t.items);C.value=r,U.value=t.items.map(n=>n.relativePath||n.absolutePath),k.value=null,b.value=null,I.value="",c.success(`已选择文件夹内 ${t.items.length} 个 Excel 文件`)}catch(s){c.error(s instanceof Error?s.message:"选择失败")}}async function Pe(){if(!C.value.length){c.warning("请先选择 Excel");return}if(!$()){c.warning("请先在左上角设置中填写货源查询密钥");return}D.value=!0;try{const e=C.value.map(t=>({fileKey:t.fileKey,originalFilename:t.originalFilename,relativePath:t.relativePath})),s=await Ke(e,$());k.value=s,b.value=null,I.value="",c.success(`解析完成,共 ${s.acceptedRows} 条`)}catch(e){c.error(e instanceof Error?e.message:"解析失败")}finally{D.value=!1}}async function Ce(){const e=A(),s=k.value,t=s?.taskId;if(!t){c.warning("请先解析文件");return}if(!e?.enqueue_json){c.error("当前环境未启用 pywebview enqueue_json");return}if(!$()){c.warning("请先在左上角设置中填写货源查询密钥");return}x.value=!0;try{const r={type:"similar-asin-run",ts:Date.now(),data:{taskId:t,user_id:ke(),api_key:$(),sourceFileCount:s.sourceFileCount||0,totalRows:s.totalRows||0,acceptedRows:s.acceptedRows||0,groupCount:s.groupCount||0}};I.value=JSON.stringify(_e(r),null,2),await Oe(t);const n=await e.enqueue_json(r);if(!n?.success){c.error(n?.error||"推送失败");return}b.value=ye(s),me(t),be(),await z(),await G({force:!0}),c.success("已推送到 Python 队列")}finally{x.value=!1}}function be(){k.value=null,I.value=""}function me(e){f.value.includes(e)||(f.value=[...f.value,e],S()),w()}function O(e){f.value=f.value.filter(s=>s!==e),S()}function Ne(e){u.value.includes(e)||(u.value=[...u.value,e],S()),w()}function V(e){u.value=u.value.filter(s=>s!==e),S()}function w(){N||_.value==null&&Te(!0)}function le(){_.value!=null&&(T.clearTimer("task-poll",_.value),_.value=null)}function Te(e=!1){if(N)return;if(_.value!=null){if(!e)return;T.clearTimer("task-poll",_.value),_.value=null}const s=async()=>{_.value=null,!N&&(!f.value.length&&!u.value.length||(await Fe(),!N&&_.value==null&&(f.value.length||u.value.length)&&(_.value=T.setTimeout("task-poll",s,pe()))))};e?s():_.value=T.setTimeout("task-poll",s,pe())}async function Fe(){if(q.value||!f.value.length&&!u.value.length){!f.value.length&&!u.value.length&&le();return}q.value=!0;try{let e=!1,s=!1;const t=Array.from(new Set([...f.value,...u.value]));if(t.length){const r=await es(t,{force:u.value.length>0});for(const n of r.items||[]){const d=n.task;if(!d?.id)continue;const g=n.items?.[0];if(g&&Re(g),(d.status==="SUCCESS"||d.status==="FAILED")&&(O(d.id),e=!0,d.status==="SUCCESS"?Ne(d.id):V(d.id)),g&&d.status==="SUCCESS"&&u.value.includes(d.id)){const H=(g.fileStatus||"").toUpperCase();(g.fileReady||H==="SUCCESS"||H==="FAILED")&&(V(d.id),e=!0,s=!0)}}r.missingTaskIds?.length&&(r.missingTaskIds.forEach(n=>{O(n),V(n)}),s=!0,e=!0)}e&&await z(),s&&await G(),Ee()}finally{q.value=!1}}function Re(e){if(!e.taskId&&!e.resultId)return;const s=v.value.findIndex(t=>e.resultId!=null&&t.resultId===e.resultId||e.taskId!=null&&t.taskId===e.taskId);s>=0?v.value[s]={...v.value[s],...e}:v.value=[e,...v.value]}function Ee(){if(!u.value.length)return;const e=[];for(const s of u.value){const t=v.value.find(d=>d.taskId===s);if(!t){e.push(s);continue}const r=(t.taskStatus||"").toUpperCase(),n=(t.fileStatus||"").toUpperCase();r==="FAILED"||t.fileReady||n==="SUCCESS"||n==="FAILED"||r==="SUCCESS"&&e.push(s)}u.value=e,S(),u.value.length&&w()}function Ae(){const e=v.value.filter(s=>s.taskId!=null&&P(s)).map(s=>s.taskId);u.value=Array.from(new Set(e)),S(),u.value.length&&w()}function Ue(){const e=v.value.filter(s=>s.taskId!=null&&y(s)==="RUNNING").map(s=>s.taskId);e.length&&(f.value=Array.from(new Set([...f.value,...e])),S(),w())}async function z(){F.value=await Ve()}async function G(e={}){const s=Date.now();if(!(!e.force&&v.value.length&&s-se{v.value=De(v.value,t.items||[]),se=Date.now()}).finally(()=>{m=null}),m)}function De(e,s){const t=new Map,r=new Map;for(const n of e||[])n.taskId!=null&&t.set(n.taskId,n),n.resultId!=null&&r.set(n.resultId,n);return(s||[]).map(n=>{const d=n.resultId!=null?r.get(n.resultId):n.taskId!=null?t.get(n.taskId):void 0;return xe(d,n)})}function xe(e,s){return!e||!((y(e)==="RUNNING"||P(e)||E(e))&&!E(s)&&!W(s))?s:{...s,fileJobId:s.fileJobId??e.fileJobId,fileStatus:s.fileStatus||e.fileStatus,fileError:s.fileError||e.fileError,fileReady:s.fileReady||e.fileReady,fileProgressPercent:Y(s.fileProgressPercent)?s.fileProgressPercent:e.fileProgressPercent,fileProgressCurrent:Y(s.fileProgressCurrent)?s.fileProgressCurrent:e.fileProgressCurrent,fileProgressTotal:Y(s.fileProgressTotal)?s.fileProgressTotal:e.fileProgressTotal,fileProgressMessage:(s.fileProgressMessage||"").trim()?s.fileProgressMessage:e.fileProgressMessage}}function Y(e){const s=Number(e);return Number.isFinite(s)&&s>0}function oe(e){const s=y(e);return s==="RUNNING"?"执行中":s==="PENDING"?"已解析待推送":P(e)?"结果生成中":Q(e)?"结果生成失败":s==="SUCCESS"||e.success?"已完成":s==="FAILED"?"失败":"等待中"}function $e(e){const s=y(e);return s==="RUNNING"||P(e)?"running":Q(e)||s==="FAILED"?"failed":s==="SUCCESS"||e.success?"success":"pending"}function P(e){const s=y(e),t=(e.fileStatus||"").toUpperCase();return!(s!=="SUCCESS"||W(e)||t==="FAILED"||t==="SUCCESS")}function y(e){return(e.taskStatus||"").toUpperCase()}function Q(e){return(e.fileStatus||"").toUpperCase()==="FAILED"}function L(e){if(Q(e))return e.fileError||"结果文件生成失败,请稍后重试或检查后端日志";if(E(e)||!P(e))return"";const s=(e.fileStatus||"").toUpperCase();return s==="RUNNING"?"任务已完成,正在等待 Coze 回流并组装结果文件,下载按钮稍后出现":s==="PENDING"?"任务已完成,结果文件已入队,正在等待后端开始处理":"任务已完成,正在生成结果文件,下载按钮稍后出现"}function W(e){return!!(e.resultId&&(e.fileReady||e.downloadUrl))}function B(e){const s=Number(e.fileProgressPercent||0);return Number.isFinite(s)&&s>0?Math.max(0,Math.min(100,Math.round(s))):fe(e)?de(e)==="PENDING"?6:10:0}function ie(e,s){const t=(e.fileProgressMessage||"").trim();return t?/coze/i.test(t)||/Coze|回流/.test(t)?/submitting/i.test(t)||/提交/.test(t)?"正在提交 Coze 批次":/生成结果|结果文件|组装/i.test(t)?"Coze 已回流,正在生成结果文件":/waiting/i.test(t)||/等待/.test(t)||/\d+\s*\/\s*\d+/.test(t)?"Coze 回流中,等待结果回传":"Coze 处理中":/assembling|xlsx|组装|生成结果/i.test(t)?"正在组装结果文件":t:s}function ue(e){const s=(e.fileProgressMessage||"").trim(),t=Me(s),r=t?.current??e.fileProgressCurrent??0,n=t?.total??e.fileProgressTotal??0;return/coze/i.test(s)||/Coze|回流/.test(s)?/submitting/i.test(s)||/提交/.test(s)?`Coze 提交批次 ${r}/${n}`:`Coze 回流批次 ${r}/${n}`:`当前阶段 ${r}/${n}`}function Me(e){const s=e.match(/(\d+)\s*\/\s*(\d+)/);if(!s)return null;const t=Number(s[1]),r=Number(s[2]);return!Number.isFinite(t)||!Number.isFinite(r)?null:{current:t,total:r}}function E(e){return(y(e)==="RUNNING"||P(e))&&(ce(e)||fe(e))}function ce(e){return X(e)||Number(e.fileProgressPercent||0)>0||!!(e.fileProgressMessage||"").trim()}function X(e){return(e.fileProgressTotal||0)>0}function de(e){return(e.fileStatus||"").toUpperCase()}function ze(e){const s=de(e);return e.fileJobId!=null||s==="PENDING"||s==="RUNNING"}function fe(e){return ze(e)&&!ce(e)}async function Ge(e){if(!e.resultId)return;const s=A();if(!s?.save_file_from_url_new){c.error("当前客户端未提供下载能力");return}const t=Qe(e.resultId),r=e.resultFilename||`${e.sourceFilename||"similar-asin"}.xlsx`,n=await s.save_file_from_url_new(t,r);n.success?c.success(`已保存: ${n.path||r}`):n.error&&n.error!=="用户取消"&&c.error(n.error)}async function ve(e){try{e.taskStatus==="RUNNING"&&e.taskId?(await We(e.taskId),O(e.taskId)):e.resultId&&await Xe(e.resultId),await z(),await G({force:!0}),c.success("已删除")}catch(s){c.error(s instanceof Error?s.message:"删除失败")}}return Be(async()=>{Ie(),await Promise.all([z().catch(()=>{}),G().catch(()=>{})]),Ae(),Ue(),(f.value.length||u.value.length)&&w()}),He(()=>{N=!0,le(),T.clearScope()}),(e,s)=>(o(),i("div",ls,[Je(qe,{active:"similar-asin"}),a("div",os,[a("aside",is,[s[1]||(s[1]=a("div",{class:"section-title"},"上传文件",-1)),a("div",us,[s[0]||(s[0]=a("div",{class:"hint"},"选择 Excel 后点击解析,后端会提取 id、ASIN、国家、价格,每一行都会独立送检。",-1)),a("div",{class:"btns"},[a("button",{type:"button",class:"opt-btn",onClick:Se},"选择 Excel"),a("button",{type:"button",class:"opt-btn",onClick:we},"选择文件夹")]),a("div",cs,[U.value.length?(o(!0),i(Z,{key:1},ee(U.value,t=>(o(),i("span",{key:t},l(t),1))),128)):(o(),i("span",ds,"暂未选择文件"))])]),a("div",fs,[a("button",{type:"button",class:"btn-run",disabled:D.value||!C.value.length,onClick:Pe},l(D.value?"解析中...":"解析并创建任务"),9,vs),a("button",{type:"button",class:"btn-run btn-queue",disabled:x.value||!k.value?.taskId,onClick:Ce},l(x.value?"推送中...":"推送到 Python 队列"),9,gs)]),s[2]||(s[2]=a("p",{class:"loading-msg"},"Python 回传字段按 id、asin、国家、价格、标题、图片链接数组提交;后端接收分片后按 10 条一批送检,结果区展示真实 Coze 批次进度。",-1)),R.value?(o(),i("div",ps,[a("div",null,"任务 ID:"+l(R.value.taskId),1),a("div",null,"总行数:"+l(R.value.totalRows)+",有效:"+l(R.value.acceptedRows)+",过滤:"+l(R.value.droppedRows),1)])):h("",!0),I.value?(o(),i("pre",hs,l(I.value),1)):h("",!0)]),a("section",_s,[s[10]||(s[10]=a("div",{class:"panel-header"},"货源查询",-1)),a("div",ks,[a("div",ys,[a("div",Is,[s[3]||(s[3]=a("span",{class:"summary-label"},"运行中任务",-1)),a("strong",null,l(F.value.pendingTaskCount),1)]),a("div",Ss,[s[4]||(s[4]=a("span",{class:"summary-label"},"已结束任务",-1)),a("strong",null,l(F.value.processedTaskCount),1)]),a("div",ws,[s[5]||(s[5]=a("span",{class:"summary-label"},"成功任务",-1)),a("strong",null,l(F.value.successTaskCount),1)]),a("div",Ps,[s[6]||(s[6]=a("span",{class:"summary-label"},"失败任务",-1)),a("strong",null,l(F.value.failedTaskCount),1)])]),s[9]||(s[9]=a("div",{class:"subsection-title"},"匹配任务",-1)),a("div",Cs,[s[7]||(s[7]=a("div",{class:"result-list-header"},[a("span",null,"当前任务")],-1)),te.value.length?(o(),i("ul",ms,[(o(!0),i(Z,null,ee(te.value,t=>(o(),i("li",{key:`cur-${t.taskId}`,class:"task-item"},[a("div",Ns,[a("span",Ts,l(t.sourceFilename||"货源查询"),1),a("div",Fs,"任务 ID:"+l(t.taskId),1),a("div",Rs,"开始时间:"+l(M(t.startedAt||t.createdAt)),1),a("div",Es,"结束时间:"+l(M(t.finishedAt)),1),a("div",As,"行数:"+l(t.rowCount??"-"),1),L(t)?(o(),i("div",Us,l(L(t)),1)):h("",!0),E(t)?(o(),i("div",Ds,[a("div",xs,[a("span",null,l(ie(t,"处理中")),1),a("span",null,"总进度 "+l(B(t))+"%",1)]),a("div",$s,[a("div",{class:"file-progress-bar",style:ge({width:`${B(t)}%`})},null,4)]),X(t)?(o(),i("div",Ms,l(ue(t)),1)):h("",!0)])):h("",!0)]),a("div",zs,[a("span",Gs,l(oe(t)),1),a("button",{type:"button",class:"btn-delete",onClick:r=>ve(t)},"删除",8,Ls)])]))),128))])):(o(),i("div",bs,"暂无当前任务"))]),a("div",Bs,[s[8]||(s[8]=a("div",{class:"result-list-header"},[a("span",null,"历史记录")],-1)),ae.value.length?(o(),i("ul",Js,[(o(!0),i(Z,null,ee(ae.value,t=>(o(),i("li",{key:`his-${t.resultId}-${t.taskId}`,class:"task-item"},[a("div",qs,[a("span",js,l(t.sourceFilename||"货源查询"),1),a("div",Ks,"任务 ID:"+l(t.taskId??"-"),1),a("div",Os,"开始时间:"+l(M(t.startedAt||t.createdAt)),1),a("div",Vs,"结束时间:"+l(M(t.finishedAt)),1),t.resultFilename?(o(),i("div",Ys,l(t.resultFilename||"下载结果"),1)):h("",!0),L(t)?(o(),i("div",Qs,l(L(t)),1)):h("",!0),E(t)?(o(),i("div",Ws,[a("div",Xs,[a("span",null,l(ie(t,"结果生成中")),1),a("span",null,"总进度 "+l(B(t))+"%",1)]),a("div",Zs,[a("div",{class:"file-progress-bar",style:ge({width:`${B(t)}%`})},null,4)]),X(t)?(o(),i("div",et,l(ue(t)),1)):h("",!0)])):h("",!0),t.error?(o(),i("div",st,"错误:"+l(t.error),1)):h("",!0)]),a("div",tt,[a("span",{class:je(["status",$e(t)])},l(oe(t)),3),W(t)?(o(),i("button",{key:0,type:"button",class:"download",onClick:r=>Ge(t)},"下载",8,at)):h("",!0),t.resultId?(o(),i("button",{key:1,type:"button",class:"btn-delete",onClick:r=>ve(t)},"删除",8,rt)):h("",!0)])]))),128))])):(o(),i("div",Hs,"暂无历史记录"))])])])])]))}}),ot=ss(lt,[["__scopeId","data-v-a27e815e"]]);ts(ot).use(as).mount("#app"); diff --git a/app/jwt_util.py b/app/jwt_util.py index 366dc22..186ab7e 100644 --- a/app/jwt_util.py +++ b/app/jwt_util.py @@ -4,6 +4,7 @@ JWT 工具:解析 Java 端签发的 token、从请求中提取 token。 """ import os import sys +import time from typing import Optional, Tuple from flask import request @@ -25,6 +26,14 @@ except ImportError as _e: _DEFAULT_SECRET = "please-change-this-secret-please-rotate-at-least-32-bytes" COOKIE_NAME = os.getenv("AIIMAGE_AUTH_COOKIE_NAME", "aiimage_token") +# 桌面端与 Java 服务器时钟可能漂移(笔记本休眠、用户手动改时间、跨时区等), +# 给 JWT exp/nbf 校验留出容差,避免 /api/auth/sync 因为时间不同步而 401。 +# 默认 5 分钟,可通过 AIIMAGE_JWT_LEEWAY_SECONDS 覆盖。 +try: + _JWT_LEEWAY_SECONDS = int(os.getenv("AIIMAGE_JWT_LEEWAY_SECONDS", "300")) +except ValueError: + _JWT_LEEWAY_SECONDS = 300 + def _signing_key() -> bytes: """与 Java JwtService.signingKey 保持一致:UTF-8 字节,不足 32 字节右侧补 0。""" @@ -35,6 +44,26 @@ def _signing_key() -> bytes: return key_bytes +def _describe_clock_skew(token: str) -> str: + """解析 token 里的 iat/exp,与本地时钟比较,返回 ' iat=.. exp=.. now=.. skew=..s' 字符串。 + 仅供日志使用,不做安全决策;解析失败返回空串,不影响主流程。""" + if pyjwt is None: + return "" + try: + unverified = pyjwt.decode(token, options={"verify_signature": False, "verify_exp": False, "verify_iat": False, "verify_nbf": False}) + except Exception: + return "" + iat = unverified.get("iat") + exp = unverified.get("exp") + now = int(time.time()) + parts = [f"now={now}"] + if isinstance(iat, (int, float)): + parts.append(f"iat={int(iat)} skew_iat={now - int(iat)}s") + if isinstance(exp, (int, float)): + parts.append(f"exp={int(exp)} skew_exp={now - int(exp)}s") + return ", " + " ".join(parts) + + def parse_token_with_reason(token: str) -> Tuple[Optional[dict], Optional[str]]: """解析 JWT,返回 (payload, error_reason)。 payload 命中时 error_reason 为 None;失败时 payload 为 None,error_reason 描述根因。""" @@ -49,10 +78,17 @@ def parse_token_with_reason(token: str) -> Tuple[Optional[dict], Optional[str]]: return None, f"无法解析 token header: {type(e).__name__}: {e}" alg = header.get("alg") or "?" try: - payload = pyjwt.decode(token, _signing_key(), algorithms=["HS256", "HS384", "HS512"]) + payload = pyjwt.decode( + token, + _signing_key(), + algorithms=["HS256", "HS384", "HS512"], + leeway=_JWT_LEEWAY_SECONDS, + ) except Exception as e: # 常见:ExpiredSignatureError / InvalidSignatureError / DecodeError / InvalidAlgorithmError - return None, f"{type(e).__name__}: {e} (token alg={alg})" + # 把 token 内 exp/iat 与本地时钟一起打出来,定位"时间漂移"类失败更直接 + skew_info = _describe_clock_skew(token) + return None, f"{type(e).__name__}: {e} (token alg={alg}{skew_info})" sub = payload.get("sub") try: user_id = int(sub) if sub is not None else None diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java index 2ff6692..f04f2d2 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java @@ -89,7 +89,7 @@ public class SimilarAsinCozeClient { String dataText = extractResultDataText(pollRoot); String outputText = dataText.isBlank() ? extractWorkflowOutputText(pollRoot) : ""; String failureMessage = isFailedWorkflowStatus(status) - ? firstNonBlank(resolveFailureMessage(pollRoot), "Coze async workflow failed") + ? firstNonBlank(resolveFailureMessage(pollRoot), "Coze 异步工作流失败") : ""; return new CozePollResponse(executeId, status, dataText, outputText, failureMessage, writeJson(pollRoot), resolvedCredential.name()); @@ -214,19 +214,19 @@ public class SimilarAsinCozeClient { String status = normalize(resolveWorkflowStatus(pollRoot)).toUpperCase(Locale.ROOT); if (isFailedWorkflowStatus(status)) { - throw new IllegalStateException(firstNonBlank(resolveFailureMessage(pollRoot), "Coze async workflow failed")); + throw new IllegalStateException(firstNonBlank(resolveFailureMessage(pollRoot), "Coze 异步工作流失败")); } if (isSuccessfulWorkflowStatus(status)) { String outputText = extractWorkflowOutputText(pollRoot); if (!outputText.isBlank()) { return wrapDataPayload(outputText); } - throw new IllegalStateException("Coze async workflow completed without output"); + throw new IllegalStateException("Coze 异步工作流已完成但没有输出结果"); } sleepQuietly(Math.max(200, properties.getCozePollIntervalMillis())); } - throw new IllegalStateException("Coze async workflow poll timeout"); + throw new IllegalStateException("Coze 异步工作流轮询超时"); } private String postWorkflow(List rows, @@ -781,8 +781,8 @@ public class SimilarAsinCozeClient { || message.contains("429") || message.toLowerCase(Locale.ROOT).contains("rate limit") || message.toLowerCase(Locale.ROOT).contains("retry later") - || message.contains("\u9650\u6d41") - || message.contains("\u7a0d\u540e\u91cd\u8bd5") + || message.contains("限流") + || message.contains("稍后重试") || message.toLowerCase(Locale.ROOT).contains("timeout"); } @@ -1095,7 +1095,7 @@ public class SimilarAsinCozeClient { try { return objectMapper.writeValueAsString(value); } catch (Exception ex) { - throw new IllegalStateException("Failed to serialize Coze payload", ex); + throw new IllegalStateException("Coze 请求载荷序列化失败", ex); } } @@ -1118,13 +1118,13 @@ public class SimilarAsinCozeClient { Thread.sleep(delayMillis); } catch (InterruptedException interruptedException) { Thread.currentThread().interrupt(); - throw new IllegalStateException("Coze workflow interrupted", interruptedException); + throw new IllegalStateException("Coze 工作流被中断", interruptedException); } } private void ensureNotInterrupted() { if (Thread.currentThread().isInterrupted()) { - throw new IllegalStateException("Coze workflow interrupted"); + throw new IllegalStateException("Coze 工作流被中断"); } } @@ -1132,7 +1132,7 @@ public class SimilarAsinCozeClient { if (ex instanceof RuntimeException runtimeException) { return runtimeException; } - return new IllegalStateException(nonBlank(ex.getMessage(), "Coze call failed"), ex); + return new IllegalStateException(nonBlank(ex.getMessage(), "Coze 调用失败"), ex); } private JsonNode firstNonNull(JsonNode left, JsonNode right) { @@ -1227,19 +1227,19 @@ public class SimilarAsinCozeClient { private String failureMessage(Exception ex) { if (ex instanceof PartialCozeResultException partial) { - return "Coze result incomplete(" + partial.resolvedCount() + "/" + partial.expectedCount() + ")"; + return "Coze 结果不完整(" + partial.resolvedCount() + "/" + partial.expectedCount() + ")"; } String message = ex == null ? null : ex.getMessage(); if (message == null || message.isBlank()) { - return "Coze call failed"; + return "Coze 调用失败"; } if (message.contains("Workflow node execution limit exceeded")) { - return "Coze workflow node execution limit exceeded"; + return "Coze 工作流节点执行超限"; } if (message.contains("Read timed out")) { - return "Coze call timed out"; + return "Coze 调用超时"; } - return "Coze call failed: " + message; + return "Coze 调用失败:" + message; } private String stripBearer(String token) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/controller/SimilarAsinController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/controller/SimilarAsinController.java index fa2e1b5..5c6a291 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/controller/SimilarAsinController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/controller/SimilarAsinController.java @@ -13,11 +13,13 @@ import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryVo; import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParseVo; import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinTaskBatchVo; import com.nanri.aiimage.modules.similarasin.service.SimilarAsinTaskService; +import com.nanri.aiimage.modules.similarasin.service.SimilarAsinTaskService.ResultDownloadInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -36,6 +38,7 @@ import java.util.List; @RestController @RequiredArgsConstructor +@Slf4j @RequestMapping("/api/similar-asin") @Tag( name = "相似 ASIN 检测", @@ -174,22 +177,30 @@ public class SimilarAsinController { @Parameter(description = "当前用户 ID", required = true, example = "1") @RequestParam("user_id") Long userId, jakarta.servlet.http.HttpServletResponse response) { - String url = service.resolveResultDownloadUrl(resultId, userId); - String filename = service.resolveResultDownloadFilename(resultId, userId); - if (url == null || url.isBlank()) { + ResultDownloadInfo download = service.resolveResultDownloadInfo(resultId, userId); + if (download.url() == null || download.url().isBlank()) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果"); } try { - response.setContentType("application/octet-stream"); - DownloadHeaderUtil.setAttachment(response, filename); - try (InputStream in = URI.create(url).toURL().openStream()) { + response.setContentType(download.contentType()); + if (download.size() > 0) { + response.setContentLengthLong(download.size()); + } + DownloadHeaderUtil.setAttachment(response, download.filename()); + long written = 0L; + try (InputStream in = URI.create(download.url()).toURL().openStream()) { byte[] buffer = new byte[65536]; int read; while ((read = in.read(buffer)) != -1) { response.getOutputStream().write(buffer, 0, read); + written += read; } response.getOutputStream().flush(); } + if (download.size() > 0 && written != download.size()) { + log.warn("[相似ASIN] 下载文件大小不一致 结果ID={} 预期字节数={} 实际写出字节数={} 文件名={}", + resultId, download.size(), written, download.filename()); + } } catch (Exception ex) { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "下载失败"); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java index 9a08e6b..68c4926 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java @@ -600,7 +600,7 @@ public class SimilarAsinTaskService { public void submitResult(Long taskId, SimilarAsinSubmitResultRequest request) { TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, TASK_LOCK_WAIT_MILLIS); if (lockHandle == null) { - throw new BusinessException(40902, "Task is processing the previous result chunk, please retry later"); + throw new BusinessException(40902, "任务正在处理上一批结果,请稍后重试"); } try (lockHandle) { submitResultLocked(taskId, request); @@ -730,7 +730,7 @@ public class SimilarAsinTaskService { taskScopeStateMapper.delete(new LambdaQueryWrapper().eq(TaskScopeStateEntity::getTaskId, taskId).eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)); taskChunkMapper.delete(new LambdaQueryWrapper().eq(TaskChunkEntity::getTaskId, taskId).eq(TaskChunkEntity::getModuleType, MODULE_TYPE)); taskCacheService.deleteTaskCache(taskId); - // 同步清理 task_file_job,避免被删除任务遗留的 PENDING/FAILED 行被 TaskResultFileJobWorker 反复扫描出 task not found。 + // 同步清理 task_file_job,避免被删除任务遗留的 PENDING/FAILED 行被 TaskResultFileJobWorker 反复扫描出“任务不存在”。 taskFileJobService.deleteTaskJobs(taskId, MODULE_TYPE); fileTaskMapper.deleteById(taskId); // P1-1:任务被删除时一并清理滑窗记录,防止内存泄漏。 @@ -743,7 +743,7 @@ public class SimilarAsinTaskService { if (row == null || !MODULE_TYPE.equals(row.getModuleType()) || !Objects.equals(userId, row.getUserId())) { throw new BusinessException("记录不存在"); } - // 同步清理 task_file_job,避免被删除历史记录残留的 ASSEMBLE_RESULT job 被反复扫描出 result record not found。 + // 同步清理 task_file_job,避免被删除历史记录残留的 ASSEMBLE_RESULT job 被反复扫描出“结果记录不存在”。 taskFileJobService.deleteResultJobs(row.getTaskId(), MODULE_TYPE, resultId); fileResultMapper.deleteById(resultId); } @@ -769,6 +769,36 @@ public class SimilarAsinTaskService { : row.getResultFilename(); } + public ResultDownloadInfo resolveResultDownloadInfo(Long resultId, Long userId) { + FileResultEntity row = fileResultMapper.selectById(resultId); + if (row == null || !MODULE_TYPE.equals(row.getModuleType()) || !Objects.equals(userId, row.getUserId())) { + throw new BusinessException("记录不存在"); + } + if (row.getResultFileUrl() == null || row.getResultFileUrl().isBlank()) { + throw new BusinessException("暂无可下载文件"); + } + String filename = row.getResultFilename() == null || row.getResultFilename().isBlank() + ? "similar-asin-" + resultId + ".xlsx" + : row.getResultFilename(); + String contentType = row.getResultContentType() == null || row.getResultContentType().isBlank() + ? guessDownloadContentType(filename) + : row.getResultContentType(); + return new ResultDownloadInfo( + ossStorageService.generateFreshDownloadUrl(row.getResultFileUrl()), + filename, + contentType, + row.getResultFileSize() == null ? 0L : row.getResultFileSize() + ); + } + + private String guessDownloadContentType(String filename) { + String normalized = filename == null ? "" : filename.trim().toLowerCase(Locale.ROOT); + if (normalized.endsWith(".zip")) { + return CONTENT_TYPE_ZIP; + } + return CONTENT_TYPE_XLSX; + } + public void finalizeStaleTasks() { if (transactionManager != null) { LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes())); @@ -1251,7 +1281,7 @@ public class SimilarAsinTaskService { persistedRows.put(rowKey(row), row); } String payloadJson = writeJson(rows == null ? List.of() : rows, "结果序列化失败"); - payloadJson = writeJson(new ArrayList<>(persistedRows.values()), "similar ASIN chunk payload merge failed"); + payloadJson = writeJson(new ArrayList<>(persistedRows.values()), "相似ASIN分片载荷合并失败"); String oldPayload = chunk.getPayloadJson(); String storedPayload = transientPayloadStorageService.storeChunkPayloadVersioned(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson); chunk.setPayloadJson(storedPayload); @@ -1260,7 +1290,7 @@ public class SimilarAsinTaskService { int updated = taskChunkMapper.updateById(chunk); if (updated <= 0) { transientPayloadStorageService.deletePayloadIfPresent(storedPayload); - throw new IllegalStateException("similar ASIN chunk payload update failed"); + throw new IllegalStateException("相似ASIN分片载荷更新失败"); } transientPayloadStorageService.deleteReplacedPayloadIfNeeded(oldPayload, storedPayload); } @@ -1535,8 +1565,8 @@ public class SimilarAsinTaskService { flushBufferedCozeResults(task.getId()); assembleResultWorkbook(task, result); } catch (Exception ex) { - log.warn("[similar-asin] assemble result workbook failed taskId={} err={}", task.getId(), ex.getMessage()); - finalError = firstNonBlank(finalError, "Generate similar ASIN result failed"); + log.warn("[相似ASIN] 组装结果工作簿失败 任务ID={} 错误={}", task.getId(), ex.getMessage()); + finalError = firstNonBlank(finalError, "生成相似ASIN结果失败"); } } } @@ -1564,7 +1594,7 @@ public class SimilarAsinTaskService { if (shouldAssembleResult) { enqueueResultAssembly(task, result, true); } else if (assembleWorkbook && failed) { - log.warn("[similar-asin] skip failed task workbook assembly because no persisted result rows taskId={}", task.getId()); + log.warn("[相似ASIN] 跳过失败任务的工作簿组装,因为没有已持久化结果行 任务ID={}", task.getId()); } } taskCacheService.deleteTaskCache(task.getId()); @@ -1599,7 +1629,7 @@ public class SimilarAsinTaskService { && job != null && "RUNNING".equals(job.getStatus()) && countPendingCozeStates(task.getId()) == 0) { - taskFileJobService.requeue(job.getId(), "Python upload finished, assembling xlsx"); + taskFileJobService.requeue(job.getId(), "Python 上传完成,正在组装 xlsx"); } return job; } @@ -1625,22 +1655,22 @@ public class SimilarAsinTaskService { public boolean processResultFileJob(TaskFileJobEntity job) { if (job == null || job.getTaskId() == null || job.getResultId() == null) { - throw new BusinessException("result file job arguments are incomplete"); + throw new BusinessException("结果文件任务参数不完整"); } FileTaskEntity task = fileTaskMapper.selectById(job.getTaskId()); if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { - throw new BusinessException("task not found"); + throw new BusinessException("任务不存在"); } ensureTaskOwnedByCurrentInstance(task, "assemble result file"); job.setScopeKey(buildTaskOwnerScopeKey(task)); if (!isJobOwnedByCurrentInstance(job)) { - log.info("[similar-asin] skip result file job because owner is another instance jobId={} taskId={} owner={} current={}", + log.info("[相似ASIN] 跳过结果文件任务,因为归属实例不是当前实例 文件任务ID={} 任务ID={} 归属实例={} 当前实例={}", job.getId(), job.getTaskId(), ownerFromScopeKey(job.getScopeKey()), currentInstanceId()); return false; } FileResultEntity result = fileResultMapper.selectById(job.getResultId()); if (result == null || !MODULE_TYPE.equals(result.getModuleType())) { - throw new BusinessException("result record not found"); + throw new BusinessException("结果记录不存在"); } List chunks = taskChunkMapper.selectList(new LambdaQueryWrapper() .eq(TaskChunkEntity::getTaskId, task.getId()) @@ -1653,7 +1683,7 @@ public class SimilarAsinTaskService { if (countPendingCozeStates(task.getId()) > 0) { taskFileJobService.touchRunning(job.getId()); touchJavaSideTaskActivity(task.getId()); - saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze submitted, waiting for result"); + saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze 已提交,等待结果"); return false; } // P0-3:stale-recovery / 异常路径兜底 —— maybeFinalizeCozeJob 可能未能成功 flush @@ -1661,24 +1691,24 @@ public class SimilarAsinTaskService { try { flushBufferedCozeResults(task.getId()); } catch (Exception flushEx) { - log.warn("[similar-asin] result file job flush buffered coze results failed taskId={} jobId={} err={}", + log.warn("[相似ASIN] 结果文件任务刷新缓冲区 Coze 结果失败 任务ID={} 文件任务ID={} 错误={}", task.getId(), job.getId(), flushEx.getMessage(), flushEx); - throw new BusinessException("flush buffered coze results failed: " - + firstNonBlank(flushEx.getMessage(), "unknown error")); + throw new BusinessException("刷新缓冲区 Coze 结果失败:" + + firstNonBlank(flushEx.getMessage(), "未知错误")); } - saveFileBuildProgress(task, job, totalProgressUnits, 0, "Submitting Coze"); + saveFileBuildProgress(task, job, totalProgressUnits, 0, "正在提交 Coze"); boolean pendingCoze = submitCozeBatches(task, result, job, chunks, allRowsByBaseId); if (pendingCoze) { taskFileJobService.touchRunning(job.getId()); touchJavaSideTaskActivity(task.getId()); - saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze submitted, waiting for result"); + saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze 已提交,等待结果"); return false; } if (STATUS_RUNNING.equals(task.getStatus()) && !isResultSubmissionComplete(task.getId())) { taskFileJobService.touchRunning(job.getId()); touchJavaSideTaskActivity(task.getId()); saveFileBuildProgress(task, job, totalProgressUnits, Math.max(1, cozeWorkUnits), - "Waiting for Python upload, submitting Coze every " + batchSize + " rows"); + "等待 Python 上传,每 " + batchSize + " 行提交一次 Coze"); return false; } completeCozeFileJob(task, result, job, totalProgressUnits, cozeWorkUnits); @@ -2298,16 +2328,16 @@ public class SimilarAsinTaskService { return; } String failureMessage = poll.isFailed() - ? firstNonBlank(poll.failureMessage(), "Coze async workflow failed") + ? firstNonBlank(poll.failureMessage(), "Coze 异步工作流失败") : ""; if (!poll.hasPayload() && failureMessage.isBlank()) { failureMessage = isCozeStateTimedOut(state) - ? "Coze async workflow poll timeout" - : "Coze async workflow completed without output"; + ? "Coze 异步工作流轮询超时" + : "Coze 异步工作流已完成但没有输出结果"; } List batchRows = readCozeBatchRows(state); if (batchRows.isEmpty() && failureMessage.isBlank()) { - failureMessage = "Coze batch payload missing"; + failureMessage = "Coze 批次载荷缺失"; } List cozeRows; if (failureMessage.isBlank()) { @@ -2911,9 +2941,9 @@ public class SimilarAsinTaskService { } /** - * P2-10\uff1aCoze \u5931\u8d25\u7edf\u4e00\u5206\u7c7b\uff08\u66ff\u4ee3 isRetryableCozeFailure / shouldSplitCozeBatchForRetry / - * isCozeThrottleLockTimeout / isDeterministicCozeInputFailure \u7b49\u6563\u843d\u5728\u591a\u5904\u7684\u5173\u952e\u8bcd\u5224\u65ad\uff09\u3002 - * \u5173\u952e\u5b57\u96c6\u4e2d\u5728\u6b64\uff0c\u6240\u6709\u8c03\u7528\u65b9\u5171\u4eab\u540c\u4e00\u6765\u6e90\u907f\u514d\u6f02\u79fb\u3002 + * P2-10:Coze 失败统一分类(替代 isRetryableCozeFailure / shouldSplitCozeBatchForRetry / + * isCozeThrottleLockTimeout / isDeterministicCozeInputFailure 等散落在多处的关键词判断)。 + * 关键词集中在此,所有调用方共享同一来源避免漂移。 */ static final class CozeFailureClassifier { private CozeFailureClassifier() {} @@ -2929,7 +2959,7 @@ public class SimilarAsinTaskService { return trimmed.toLowerCase(Locale.ROOT); } - /** Coze \u5de5\u4f5c\u6d41\u7a7a\u8f93\u5165\u515c\u5e95 / 720712000 \u663e\u5f0f\u8f93\u5165\u6821\u9a8c\u5931\u8d25\uff1a\u76f4\u63a5 markFailed\uff0c\u4e0d\u8fdb retry\u3002 */ + /** Coze 工作流空输入兜底 / 720712000 显式输入校验失败:直接标记失败,不进入重试。 */ static boolean isDeterministicInputFailure(String msg) { String n = norm(msg); if (n.isEmpty()) { @@ -2938,25 +2968,25 @@ public class SimilarAsinTaskService { return DETERMINISTIC_INPUT_FAILURE_PATTERN.matcher(n).find(); } - /** \u8282\u6d41\u9501\u7b49\u5f85\u8d85\u65f6\uff08\u63d0\u4ea4\u9501\uff09\uff0c\u533a\u522b\u4e8e poll \u7b49\u5f85 Coze \u81ea\u8eab\u8d85\u65f6\u3002 */ + /** 节流锁等待超时(提交锁),区别于轮询等待 Coze 自身超时。 */ static boolean isThrottleLockTimeout(String msg) { return norm(msg).contains("coze submit throttle lock timeout"); } /** - * P1-5\uff1a720712008 / "node executed out of limit" / "\u5de5\u4f5c\u6d41\u8282\u70b9\u6267\u884c\u8d85\u9650" - * \u8fd9\u4e00\u7c7b\u5f3a\u70c8\u6697\u793a"\u5355\u70b9\u6b7b\u5faa\u73af"\uff0csplit \u65f6\u76f4\u63a5\u62c6 size=1 \u9694\u79bb\u6bd2\u884c\uff0c - * \u907f\u514d\u4e8c\u5206\u591a\u6b21\u6d6a\u8d39\u63d0\u4ea4\u914d\u989d\u3002 + * P1-5:720712008 / "node executed out of limit" / "工作流节点执行超限" + * 这一类强烈暗示“单点死循环”,拆分时直接拆到 size=1 隔离问题行, + * 避免二分多次浪费提交配额。 */ static boolean isPoisonRow(String msg) { String n = norm(msg); return n.contains("720712008") || n.contains("node executed out of limit") || n.contains("execution limit") - || n.contains("\u5de5\u4f5c\u6d41\u8282\u70b9\u6267\u884c\u8d85\u9650"); + || n.contains("工作流节点执行超限"); } - /** \u547d\u4e2d\u540e\u5141\u8bb8"\u540c batch \u91cd\u8bd5"\u3002 */ + /** 命中后允许同批次重试。 */ static boolean isRetryable(String msg) { if (isDeterministicInputFailure(msg)) { return false; @@ -2974,13 +3004,13 @@ public class SimilarAsinTaskService { || n.contains("720712008") || n.contains("720701002") || n.contains("plugin limit") - || n.contains("\u9650\u6d41") - || n.contains("\u7a0d\u540e\u91cd\u8bd5") - || n.contains("\u5de5\u4f5c\u6d41\u8282\u70b9\u6267\u884c\u8d85\u9650") - || n.contains("\u8c03\u7528\u8d85\u65f6"); + || n.contains("限流") + || n.contains("稍后重试") + || n.contains("工作流节点执行超限") + || n.contains("调用超时"); } - /** \u547d\u4e2d\u540e\u4f18\u5148\u8d70 split retry\uff08\u62c6 batch\uff09\u3002 */ + /** 命中后优先走拆批重试。 */ static boolean shouldSplitForRetry(String msg) { String n = norm(msg); return n.contains("timeout") @@ -2990,8 +3020,8 @@ public class SimilarAsinTaskService { || n.contains("execution limit") || n.contains("720712008") || n.contains("720701002") - || n.contains("\u5de5\u4f5c\u6d41\u8282\u70b9\u6267\u884c\u8d85\u9650") - || n.contains("\u8c03\u7528\u8d85\u65f6"); + || n.contains("工作流节点执行超限") + || n.contains("调用超时"); } } @@ -3153,9 +3183,9 @@ public class SimilarAsinTaskService { } catch (Exception ex) { TaskFileJobEntity job = taskFileJobService.findAssembleJob(taskId, MODULE_TYPE, context.resultId()); if (job != null) { - taskFileJobService.markFailed(job, firstNonBlank(ex.getMessage(), "similar ASIN result file build failed")); + taskFileJobService.markFailed(job, firstNonBlank(ex.getMessage(), "相似ASIN结果文件生成失败")); } - log.warn("[similar-asin] coze async finalize failed taskId={} resultId={} err={}", + log.warn("[相似ASIN] Coze 异步收尾失败 任务ID={} 结果ID={} 错误={}", taskId, context.resultId(), ex.getMessage(), ex); } } @@ -3188,15 +3218,15 @@ public class SimilarAsinTaskService { try { flushBufferedCozeResults(taskId); } catch (Exception flushEx) { - String message = firstNonBlank(flushEx.getMessage(), "flush buffered coze results failed"); - log.warn("[similar-asin] coze finalize flush failed, marking job failed taskId={} jobId={} err={}", + String message = firstNonBlank(flushEx.getMessage(), "刷新缓冲区 Coze 结果失败"); + log.warn("[相似ASIN] Coze 收尾刷新缓冲区失败,已标记文件任务失败 任务ID={} 文件任务ID={} 错误={}", taskId, job.getId(), message, flushEx); taskFileJobService.markFailed(job, message); return; } - boolean requeued = taskFileJobService.requeue(job.getId(), "Coze results ready, assembling xlsx"); + boolean requeued = taskFileJobService.requeue(job.getId(), "Coze 结果已就绪,正在组装 xlsx"); if (requeued) { - log.info("[similar-asin] coze async results ready, result file job requeued taskId={} jobId={} resultId={}", + log.info("[相似ASIN] Coze 异步结果已就绪,结果文件任务已重新入队 任务ID={} 文件任务ID={} 结果ID={}", taskId, job.getId(), context.resultId()); } } @@ -3567,7 +3597,7 @@ public class SimilarAsinTaskService { } if (failedGroups > 0) { // 让 finalize 能感知到失败,调用方应避免继续 requeue assemble。 - throw new IllegalStateException("flush buffered coze results failed groups=" + failedGroups); + throw new IllegalStateException("刷新缓冲区 Coze 结果失败,失败分组数=" + failedGroups); } } @@ -5451,6 +5481,12 @@ public class SimilarAsinTaskService { int rowCount) { } + public record ResultDownloadInfo(String url, + String filename, + String contentType, + long size) { + } + private record ParsedWorkbook(int totalRows, int droppedRows, List headers, List allRows) { } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/ExcelCellImageWriter.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/ExcelCellImageWriter.java index e2a3550..da765df 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/ExcelCellImageWriter.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/ExcelCellImageWriter.java @@ -169,7 +169,11 @@ public final class ExcelCellImageWriter { int idx = 0; int patchedCount = 0; for (RegisteredCellImage image : session.images) { - String replacement = "#VALUE!"; + // [MS-XLSX] §2.2.10: cell @vm 是 1-based valueMetadata 索引,0 表示"无 metadata"。 + // futureMetadata/valueMetadata/rdrichvalue 内部块仍是 0-based,所以这里写 idx+1 + // 与 buildMetadata/buildRdRichValue 中的 i 解耦。Excel 365 在 vm=0 时会把 cell + // 直接当作无图片的 #VALUE! 错误格渲染(任务 10498 J2 复现),必须改 1-based。 + String replacement = "#VALUE!"; Pattern pattern = Pattern.compile("]*\\br=\"" + Pattern.quote(image.cellRef) + "\")[^>]*(?:/>|>.*?)", Pattern.DOTALL); Matcher matcher = pattern.matcher(content); diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/ExcelCellImageWriterTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/ExcelCellImageWriterTest.java index 9494415..f72b883 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/ExcelCellImageWriterTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/ExcelCellImageWriterTest.java @@ -17,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class ExcelCellImageWriterTest { @Test - void patchedCellImageValueMetadataUsesZeroBasedVmIndexes() throws Exception { + void patchedCellImageValueMetadataUsesOneBasedVmIndexes() throws Exception { Path dir = Files.createTempDirectory("excel-cell-image-test-"); Path xlsx = dir.resolve("result.xlsx"); writeMinimalWorkbook(xlsx); @@ -30,13 +30,15 @@ class ExcelCellImageWriterTest { try (ZipFile zip = new ZipFile(xlsx.toFile())) { String sheet = read(zip, "xl/worksheets/sheet1.xml"); - assertTrue(sheet.contains("#VALUE!")); - assertTrue(sheet.contains("#VALUE!")); + // cell @vm 是 [MS-XLSX] 规范里的 1-based 索引(0 表示"无 metadata")。 + assertTrue(sheet.contains("#VALUE!")); + assertTrue(sheet.contains("#VALUE!")); + // futureMetadata 内部块仍然是 0-based。 String metadata = read(zip, "xl/metadata.xml"); assertTrue(metadata.contains("")); assertTrue(metadata.contains("")); - assertEquals(2, maxVm(sheet) + 1); + assertEquals(2, maxVm(sheet)); } } diff --git a/frontend-vue/src/pages/brand/components/BrandSimilarAsinTab.vue b/frontend-vue/src/pages/brand/components/BrandSimilarAsinTab.vue index 0558388..04745cc 100644 --- a/frontend-vue/src/pages/brand/components/BrandSimilarAsinTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandSimilarAsinTab.vue @@ -827,7 +827,7 @@ async function downloadResult(item: SimilarAsinHistoryItem) { ElMessage.error('当前客户端未提供下载能力') return } - const url = item.downloadUrl || getSimilarAsinResultDownloadUrl(item.resultId) + const url = getSimilarAsinResultDownloadUrl(item.resultId) const filename = item.resultFilename || `${item.sourceFilename || 'similar-asin'}.xlsx` const result = await api.save_file_from_url_new(url, filename) if (result.success) {