提交更新

This commit is contained in:
super
2026-05-06 00:01:49 +08:00
parent 203937335d
commit a00ff1804c
22 changed files with 796 additions and 308 deletions

View File

@@ -24,6 +24,11 @@
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
</div>
<div class="secret-card">
<div class="section-title">密钥</div>
<input v-model="cozeApiKey" class="secret-input" type="password" autocomplete="off" spellcheck="false" />
</div>
<div class="run-row">
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
{{ parsing ? '解析中...' : '解析并创建任务' }}
@@ -34,9 +39,9 @@
</div>
<p class="loading-msg">Python 回传字段按 asin国家url标题提交后端接收分片后按 10 条一批调用 Coze</p>
<div v-if="parseResult" class="parse-card">
<div>任务 ID{{ parseResult.taskId }}</div>
<div>总行数{{ parseResult.totalRows }}有效{{ parseResult.acceptedRows }}分组{{ parseResult.groupCount || 0 }}过滤{{ parseResult.droppedRows }}</div>
<div v-if="visibleTaskSummary" class="parse-card">
<div>任务 ID{{ visibleTaskSummary.taskId }}</div>
<div>总行数{{ visibleTaskSummary.totalRows }}有效{{ visibleTaskSummary.acceptedRows }}分组{{ visibleTaskSummary.groupCount || 0 }}过滤{{ visibleTaskSummary.droppedRows }}</div>
</div>
<pre v-if="queuePayloadText" class="queue-payload">{{ queuePayloadText }}</pre>
</aside>
@@ -171,6 +176,8 @@ const selectedFileNames = ref<string[]>([])
const uploadedFiles = ref<UploadFileVo[]>([])
const parseResult = ref<AppearancePatentParseVo | null>(null)
const parsedGroups = ref<AppearancePatentParsedGroup[]>([])
type TaskSummary = Pick<AppearancePatentParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
const queuedTaskSummary = ref<TaskSummary | null>(null)
const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。
请直接给出明确结论(有侵权风险 或 未发现明显侵权风险),并严格按照以下三个维度提供精简的排查理由:
外观维度: 评估产品外形、图案设计是否与欧洲/英国常见外观专利雷同。
@@ -178,6 +185,7 @@ const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲
标题维度: 排查标题中是否包含大牌商标、敏感词或版权保护词汇。
结论XX`
const aiPrompt = ref('')
const cozeApiKey = ref('')
const parsing = ref(false)
const pushing = ref(false)
const queuePayloadText = ref('')
@@ -203,6 +211,7 @@ const parsedRows = computed<AppearancePatentParsedRow[]>(() =>
parsedGroups.value.flatMap((group) => group.items || []),
)
const previewRows = computed(() => parsedGroups.value[0]?.items || [])
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
const historyOnlyItems = computed(() =>
historyItems.value.filter((i) => {
@@ -215,6 +224,28 @@ function effectiveAiPrompt() {
return aiPrompt.value.trim() || defaultAiPrompt
}
function effectiveCozeApiKey() {
return cozeApiKey.value.trim()
}
function maskSecret(secret: string) {
if (!secret) return ''
if (secret.length <= 10) return '***'
return `${secret.slice(0, 6)}***${secret.slice(-4)}`
}
function payloadForDisplay<T extends { data?: Record<string, unknown> }>(payload: T) {
return {
...payload,
data: payload.data
? {
...payload.data,
api_key: maskSecret(String(payload.data.api_key || '')),
}
: payload.data,
}
}
function uidForStorage() {
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
}
@@ -223,6 +254,16 @@ function pollingKey() {
return `appearance-patent:tasks:${uidForStorage()}`
}
function toTaskSummary(result: AppearancePatentParseVo): TaskSummary {
return {
taskId: result.taskId,
totalRows: result.totalRows,
acceptedRows: result.acceptedRows,
groupCount: result.groupCount,
droppedRows: result.droppedRows,
}
}
function savePollingIds() {
if (typeof window === 'undefined') return
const ids = Array.from(new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value]))
@@ -274,6 +315,7 @@ async function selectFiles() {
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
parseResult.value = null
parsedGroups.value = []
queuedTaskSummary.value = null
queuePayloadText.value = ''
}
@@ -296,6 +338,7 @@ async function selectFolder() {
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
parseResult.value = null
parsedGroups.value = []
queuedTaskSummary.value = null
queuePayloadText.value = ''
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
} catch (error) {
@@ -308,6 +351,10 @@ async function parseFiles() {
ElMessage.warning('请先选择 Excel')
return
}
if (!effectiveCozeApiKey()) {
ElMessage.warning('请先填写密钥')
return
}
parsing.value = true
try {
const files: UploadedFileRef[] = uploadedFiles.value.map((f) => ({
@@ -315,8 +362,9 @@ async function parseFiles() {
originalFilename: f.originalFilename,
relativePath: f.relativePath,
}))
const res = await parseAppearancePatent(files, effectiveAiPrompt())
const res = await parseAppearancePatent(files, effectiveAiPrompt(), effectiveCozeApiKey())
parseResult.value = res
queuedTaskSummary.value = null
parsedGroups.value = res.groups?.length
? res.groups
: (res.items?.length
@@ -341,7 +389,8 @@ async function parseFiles() {
async function pushToPythonQueue() {
const api = getPywebviewApi()
const taskId = parseResult.value?.taskId
const currentParseResult = parseResult.value
const taskId = currentParseResult?.taskId
if (!taskId || !parsedRows.value.length) {
ElMessage.warning('请先解析文件')
return
@@ -357,7 +406,8 @@ async function pushToPythonQueue() {
ts: Date.now(),
data: {
taskId,
prompt: parseResult.value?.aiPrompt || effectiveAiPrompt(),
prompt: currentParseResult.aiPrompt || effectiveAiPrompt(),
api_key: effectiveCozeApiKey(),
groups: parsedGroups.value.map((group) => ({
sourceFileKey: group.sourceFileKey || '',
sourceFilename: group.sourceFilename || '',
@@ -389,13 +439,14 @@ async function pushToPythonQueue() {
})),
},
}
queuePayloadText.value = JSON.stringify(payload, null, 2)
queuePayloadText.value = JSON.stringify(payloadForDisplay(payload), null, 2)
await activateAppearancePatentTask(taskId)
const result = await api.enqueue_json(payload)
if (!result?.success) {
ElMessage.error(result?.error || '推送失败')
return
}
queuedTaskSummary.value = toTaskSummary(currentParseResult)
addPollingTask(taskId)
clearParsedTask()
await loadDashboard()
@@ -721,9 +772,10 @@ onUnmounted(() => {
.btn-run:disabled { opacity: .55; cursor: not-allowed; }
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
.selected-files span { display: block; margin: 4px 0; }
.prompt-card { margin-bottom: 18px; }
.prompt-card, .secret-card { margin-bottom: 18px; }
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
.prompt-input:focus { border-color: #3498db; }
.secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; outline: none; }
.prompt-input:focus, .secret-input:focus { border-color: #3498db; }
.prompt-default-label { margin-top: 10px; color: #8d8d8d; font-size: 12px; }
.prompt-preview { margin-top: 10px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #9ea7b3; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }

View File

@@ -6,7 +6,7 @@
<aside class="left-panel">
<div class="section-title">上传文件</div>
<div class="upload-zone">
<div class="hint">选择 Excel 后点击解析后端会提取 idASIN国家并保留整数 id 与子数据的第一条</div>
<div class="hint">选择 Excel 后点击解析后端会提取 idASIN国家价格每一行都会独立送检</div>
<div class="btns">
<button type="button" class="opt-btn" @click="selectFiles">选择 Excel</button>
<button type="button" class="opt-btn" @click="selectFolder">选择文件夹</button>
@@ -24,6 +24,11 @@
<div class="prompt-preview">{{ defaultAiPrompt }}</div>
</div>
<div class="secret-card">
<div class="section-title">密钥</div>
<input v-model="cozeApiKey" class="secret-input" type="password" autocomplete="off" spellcheck="false" />
</div>
<div class="run-row">
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
{{ parsing ? '解析中...' : '解析并创建任务' }}
@@ -32,11 +37,11 @@
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
</button>
</div>
<p class="loading-msg">Python 回传字段按 asin国家url标题提交后端接收分片后按 10 条一批调用 Coze</p>
<p class="loading-msg">Python 回传字段按 idasin国家价格标题图片链接数组提交后端接收分片后按 10 条一批送检</p>
<div v-if="parseResult" class="parse-card">
<div>任务 ID{{ parseResult.taskId }}</div>
<div>总行数{{ parseResult.totalRows }}有效{{ parseResult.acceptedRows }}分组{{ parseResult.groupCount || 0 }}过滤{{ parseResult.droppedRows }}</div>
<div v-if="visibleTaskSummary" class="parse-card">
<div>任务 ID{{ visibleTaskSummary.taskId }}</div>
<div>总行数{{ visibleTaskSummary.totalRows }}有效{{ visibleTaskSummary.acceptedRows }}过滤{{ visibleTaskSummary.droppedRows }}</div>
</div>
<pre v-if="queuePayloadText" class="queue-payload">{{ queuePayloadText }}</pre>
</aside>
@@ -67,13 +72,14 @@
<div class="result-list-wrap preview-wrap">
<div class="result-list-header">
<span>解析结果</span>
<span v-if="parsedRows.length" class="muted">显示首组 {{ previewRows.length }} / {{ parseResult?.groupCount || parsedGroups.length }} {{ parsedRows.length }} </span>
<span v-if="parsedRows.length" class="muted"> {{ parsedRows.length }} </span>
</div>
<div v-if="!parsedRows.length" class="empty-tasks">暂无解析结果</div>
<el-table v-else :data="previewRows" height="120" class="result-table">
<el-table-column prop="displayId" label="ID" width="90" />
<el-table-column prop="asin" label="ASIN" width="130" />
<el-table-column prop="country" label="国家" width="100" />
<el-table-column prop="price" label="价格" width="90" />
<el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
<el-table-column prop="url" label="URL" min-width="160" show-overflow-tooltip />
</el-table>
@@ -171,13 +177,14 @@ const selectedFileNames = ref<string[]>([])
const uploadedFiles = ref<UploadFileVo[]>([])
const parseResult = ref<SimilarAsinParseVo | null>(null)
const parsedGroups = ref<SimilarAsinParsedGroup[]>([])
const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。
请直接给出明确结论(有侵权风险 或 未发现明显侵权风险),并严格按照以下三个维度提供精简的排查理由:
外观维度: 评估产品外形、图案设计是否与欧洲/英国常见外观专利雷同
专利维度: 评估产品的核心技术或物理结构是否存在侵权可能
标题维度: 排查标题中是否包含大牌商标、敏感词或版权保护词汇
结论XX`
type TaskSummary = Pick<SimilarAsinParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
const queuedTaskSummary = ref<TaskSummary | null>(null)
const defaultAiPrompt = `1. 图案与形象限制不含各国国旗及类似图案无各国领导人画像不含圣诞老人形象排除迪士尼、漫威、三丽鸥等所有指定动漫IP的图案与关联物品且不涉及知名油画作品图案
2. 品类排除:非图书、刀具、服装;不含药品、带容器的粉末及液体;与吸烟行为无关,无医疗标志
3. 功能与配件要求:不可充电且不带电池,无磁吸功能;配件多的情况下需完全匹配
4. 规格与外观限制不含体积大的家具类物品如椅子、桌子、大镜子、柜子不带任何品牌logo无多种款式可选。`
const aiPrompt = ref('')
const cozeApiKey = ref('')
const parsing = ref(false)
const pushing = ref(false)
const queuePayloadText = ref('')
@@ -200,9 +207,12 @@ const dashboard = ref<SimilarAsinDashboardVo>({
const historyItems = ref<SimilarAsinHistoryItem[]>([])
const parsedRows = computed<SimilarAsinParsedRow[]>(() =>
parsedGroups.value.flatMap((group) => group.items || []),
parseResult.value?.items?.length
? parseResult.value.items
: parsedGroups.value.flatMap((group) => group.items || []),
)
const previewRows = computed(() => parsedGroups.value[0]?.items || [])
const previewRows = computed(() => parsedRows.value)
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
const historyOnlyItems = computed(() =>
historyItems.value.filter((i) => {
@@ -215,6 +225,28 @@ function effectiveAiPrompt() {
return aiPrompt.value.trim() || defaultAiPrompt
}
function effectiveCozeApiKey() {
return cozeApiKey.value.trim()
}
function maskSecret(secret: string) {
if (!secret) return ''
if (secret.length <= 10) return '***'
return `${secret.slice(0, 6)}***${secret.slice(-4)}`
}
function payloadForDisplay<T extends { data?: Record<string, unknown> }>(payload: T) {
return {
...payload,
data: payload.data
? {
...payload.data,
api_key: maskSecret(String(payload.data.api_key || '')),
}
: payload.data,
}
}
function uidForStorage() {
return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
}
@@ -223,6 +255,16 @@ function pollingKey() {
return `similar-asin:tasks:${uidForStorage()}`
}
function toTaskSummary(result: SimilarAsinParseVo): TaskSummary {
return {
taskId: result.taskId,
totalRows: result.totalRows,
acceptedRows: result.acceptedRows,
groupCount: result.groupCount,
droppedRows: result.droppedRows,
}
}
function savePollingIds() {
if (typeof window === 'undefined') return
const ids = Array.from(new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value]))
@@ -274,6 +316,7 @@ async function selectFiles() {
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
parseResult.value = null
parsedGroups.value = []
queuedTaskSummary.value = null
queuePayloadText.value = ''
}
@@ -296,6 +339,7 @@ async function selectFolder() {
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
parseResult.value = null
parsedGroups.value = []
queuedTaskSummary.value = null
queuePayloadText.value = ''
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
} catch (error) {
@@ -308,6 +352,10 @@ async function parseFiles() {
ElMessage.warning('请先选择 Excel')
return
}
if (!effectiveCozeApiKey()) {
ElMessage.warning('请先填写密钥')
return
}
parsing.value = true
try {
const files: UploadedFileRef[] = uploadedFiles.value.map((f) => ({
@@ -315,23 +363,22 @@ async function parseFiles() {
originalFilename: f.originalFilename,
relativePath: f.relativePath,
}))
const res = await parseSimilarAsin(files, effectiveAiPrompt())
const res = await parseSimilarAsin(files, effectiveAiPrompt(), effectiveCozeApiKey())
parseResult.value = res
parsedGroups.value = res.groups?.length
? res.groups
: (res.items?.length
? [{
sourceFileKey: res.items[0]?.sourceFileKey,
sourceFilename: res.items[0]?.sourceFilename,
groupKey: res.items[0]?.groupKey,
baseId: '',
displayId: res.items[0]?.displayId,
itemCount: res.items.length,
items: res.items,
}]
: [])
queuedTaskSummary.value = null
parsedGroups.value = res.items?.length
? [{
sourceFileKey: res.items[0]?.sourceFileKey,
sourceFilename: res.items[0]?.sourceFilename,
groupKey: `similar-asin:${res.taskId}`,
baseId: '',
displayId: res.items[0]?.displayId,
itemCount: res.items.length,
items: res.items,
}]
: []
queuePayloadText.value = ''
ElMessage.success(`解析完成,共 ${res.groupCount || 0} 组 / ${res.acceptedRows}`)
ElMessage.success(`解析完成,共 ${res.acceptedRows}`)
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '解析失败')
} finally {
@@ -341,7 +388,8 @@ async function parseFiles() {
async function pushToPythonQueue() {
const api = getPywebviewApi()
const taskId = parseResult.value?.taskId
const currentParseResult = parseResult.value
const taskId = currentParseResult?.taskId
if (!taskId || !parsedRows.value.length) {
ElMessage.warning('请先解析文件')
return
@@ -350,6 +398,10 @@ async function pushToPythonQueue() {
ElMessage.error('当前环境未启用 pywebview enqueue_json')
return
}
if (!effectiveCozeApiKey()) {
ElMessage.warning('请先填写密钥')
return
}
pushing.value = true
try {
const payload = {
@@ -357,7 +409,8 @@ async function pushToPythonQueue() {
ts: Date.now(),
data: {
taskId,
prompt: parseResult.value?.aiPrompt || effectiveAiPrompt(),
prompt: currentParseResult.aiPrompt || effectiveAiPrompt(),
api_key: effectiveCozeApiKey(),
groups: parsedGroups.value.map((group) => ({
sourceFileKey: group.sourceFileKey || '',
sourceFilename: group.sourceFilename || '',
@@ -372,8 +425,10 @@ async function pushToPythonQueue() {
id: row.displayId,
asin: row.asin,
country: row.country,
price: row.price || '',
url: row.url || '',
title: row.title || '',
target_urls: [],
})),
})),
rows: parsedRows.value.map((row) => ({
@@ -384,18 +439,21 @@ async function pushToPythonQueue() {
id: row.displayId,
asin: row.asin,
country: row.country,
price: row.price || '',
url: row.url || '',
title: row.title || '',
target_urls: [],
})),
},
}
queuePayloadText.value = JSON.stringify(payload, null, 2)
queuePayloadText.value = JSON.stringify(payloadForDisplay(payload), null, 2)
await activateSimilarAsinTask(taskId)
const result = await api.enqueue_json(payload)
if (!result?.success) {
ElMessage.error(result?.error || '推送失败')
return
}
queuedTaskSummary.value = toTaskSummary(currentParseResult)
addPollingTask(taskId)
clearParsedTask()
await loadDashboard()
@@ -721,9 +779,10 @@ onUnmounted(() => {
.btn-run:disabled { opacity: .55; cursor: not-allowed; }
.selected-files { margin-top: 14px; color: #999; font-size: 12px; word-break: break-all; }
.selected-files span { display: block; margin: 4px 0; }
.prompt-card { margin-bottom: 18px; }
.prompt-card, .secret-card { margin-bottom: 18px; }
.prompt-input { width: 100%; box-sizing: border-box; resize: vertical; min-height: 180px; padding: 10px 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; line-height: 1.6; outline: none; }
.prompt-input:focus { border-color: #3498db; }
.secret-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border: 1px solid #333; border-radius: 8px; background: #202020; color: #d8d8d8; font-size: 12px; outline: none; }
.prompt-input:focus, .secret-input:focus { border-color: #3498db; }
.prompt-default-label { margin-top: 10px; color: #8d8d8d; font-size: 12px; }
.prompt-preview { margin-top: 10px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #9ea7b3; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }

View File

@@ -1531,15 +1531,16 @@ export interface AppearancePatentTaskBatchVo {
missingTaskIds?: number[];
}
export function parseAppearancePatent(files: UploadedFileRef[], aiPrompt: string) {
export function parseAppearancePatent(files: UploadedFileRef[], aiPrompt: string, apiKey?: string) {
return unwrapJavaResponse(
post<
JavaApiResponse<AppearancePatentParseVo>,
{ user_id: number; files: UploadedFileRef[]; ai_prompt: string }
{ user_id: number; files: UploadedFileRef[]; ai_prompt: string; api_key?: string }
>(`${JAVA_API_PREFIX}/appearance-patent/parse`, {
user_id: getCurrentUserId(),
files,
ai_prompt: aiPrompt,
api_key: apiKey,
}),
);
}
@@ -1615,6 +1616,7 @@ export interface SimilarAsinParsedRow {
groupKey?: string;
asin: string;
country: string;
price?: string;
url?: string;
title?: string;
}
@@ -1694,15 +1696,16 @@ export interface SimilarAsinTaskBatchVo {
missingTaskIds?: number[];
}
export function parseSimilarAsin(files: UploadedFileRef[], aiPrompt: string) {
export function parseSimilarAsin(files: UploadedFileRef[], aiPrompt: string, apiKey?: string) {
return unwrapJavaResponse(
post<
JavaApiResponse<SimilarAsinParseVo>,
{ user_id: number; files: UploadedFileRef[]; ai_prompt: string }
{ user_id: number; files: UploadedFileRef[]; ai_prompt: string; api_key?: string }
>(`${JAVA_API_PREFIX}/similar-asin/parse`, {
user_id: getCurrentUserId(),
files,
ai_prompt: aiPrompt,
api_key: apiKey,
}),
);
}