diff --git a/src/api/jingdong.ts b/src/api/jingdong.ts index f71b440..09a2f7a 100644 --- a/src/api/jingdong.ts +++ b/src/api/jingdong.ts @@ -712,7 +712,14 @@ export const getUploadUrl = async (input: { ) // 注意:若 Content-Type 不是 application/json,axios 会把 body 留成 string - const ticket = parseUploadUrlResult(response?.data) + const body = coerceResponseBody(response?.data) + const code = Number(body.code) + // 业务失败(如会话不存在)需抛出原文,避免被吞成「缺少 upload_id」 + if (Number.isFinite(code) && code !== 1000 && code !== 0) { + throw new Error(textOf(body.message || body.msg) || '签发上传地址失败') + } + + const ticket = parseUploadUrlResult(body) if (!ticket.upload_id) { throw new Error('签发上传地址失败:缺少 upload_id') diff --git a/src/config/jingdong.ts b/src/config/jingdong.ts index 6c6cc96..afaaac3 100644 --- a/src/config/jingdong.ts +++ b/src/config/jingdong.ts @@ -64,6 +64,8 @@ export const jingdongConfig = { needProductTitle: (label: string) => `请填写${label}后再发送`, uploadingTip: '正在上传照片,请稍候…', inspectingTip: '正在识别照片,请稍候…', + sessionLoadingTip: '页面加载中,请稍候再上传…', + sessionMissing: '会话未就绪,请稍候再试或刷新页面', /** 仅提示,不拦截发送 */ subjectMismatchTip: '上传图片的主体数量与下单主体数量不一致,生图时候会按下单的主体数量自动调整主体', subjectMismatchToast: (got: string, need: string) => diff --git a/src/utils/jingdong-error.ts b/src/utils/jingdong-error.ts index 0dcc83c..b56d377 100644 --- a/src/utils/jingdong-error.ts +++ b/src/utils/jingdong-error.ts @@ -32,6 +32,7 @@ const KNOWN: Record = { 效果图加载失败: '效果图加载失败,请重试', '效果图加载失败,请重试': '效果图加载失败,请重试', 'product_id不在Agent开放分类': '当前产品暂不支持在线生图,请联系客服', + 会话不存在: jingdongConfig.messages.sessionMissing, } /** 生图 / 编辑:内容策略单独翻译;Processing/ApiError 走场景兜底文案 */ diff --git a/src/views/jingdong/index.vue b/src/views/jingdong/index.vue index 2ba5dfb..a8cd0a4 100644 --- a/src/views/jingdong/index.vue +++ b/src/views/jingdong/index.vue @@ -205,6 +205,10 @@ const remainCount = ref(0) /** 打底时已统计到的成功次数;新增成功 = 当前成功数 - baselineUsed */ const remainBaselineUsed = ref(0) const sessionId = ref('') +/** orders/stat 的 session_id 已通过 session/detail 确认可用 */ +const sessionReady = ref(false) +/** 会话初始化结束但仍未就绪(需刷新) */ +const sessionBootFailed = ref(false) const subjectName = ref('') /** stat 返回的主体文案(订单约定主体,标签始终展示此值) */ const subjectNameFromStat = ref('') @@ -368,6 +372,11 @@ const canSend = computed(() => { const composerStatusTip = computed(() => { if (isBusy.value) return config.messages.generatingTip + if (!sessionReady.value) { + return sessionBootFailed.value + ? config.messages.sessionMissing + : config.messages.sessionLoadingTip + } if (uploading.value) return config.messages.uploadingTip if (inspecting.value) return config.messages.inspectingTip if (remainCount.value <= 0) return config.messages.noRemain @@ -445,11 +454,26 @@ function scrollToBottom() { }) } +function isSessionMissingError(error: unknown) { + const msg = + error instanceof Error + ? String(error.message || '') + : typeof error === 'string' + ? error + : String((error as any)?.message || (error as any)?.msg || '') + return /会话不存在|session\s*(not\s*found|missing|invalid|expired)/i.test(msg) +} + function triggerUpload() { if (isBusy.value) { showToast(config.messages.generatingTip) return } + if (!sessionReady.value) { + showToast(config.messages.sessionLoadingTip) + void fetchOrderStat() + return + } if (isEditing.value) { showToast('编辑效果图时请直接描述想调整的内容') return @@ -509,20 +533,40 @@ async function startPendingUploadAndClassify(file: File) { photoInspect.value = null subjectMatched.value = true - const sid = String(sessionId.value || '').trim() - if (!sid) { - showToast(friendlyError('缺少会话信息,请刷新后重试', 'common')) + let sid = String(sessionId.value || '').trim() + if (!sid || !sessionReady.value) { + await fetchOrderStat() + if (serial !== uploadSerial) return + sid = String(sessionId.value || '').trim() + } + if (!sid || !sessionReady.value) { + showToast(config.messages.sessionMissing) return } uploading.value = true inspecting.value = false try { + const runPrepare = () => + jingdongApi.prepareUpload(file, { + session_id: sid, + purpose: 'origin', + }) + // 仅签发 url 并 PUT,不 confirm - const prepared = await jingdongApi.prepareUpload(file, { - session_id: sid, - purpose: 'origin', - }) + let prepared + try { + prepared = await runPrepare() + } catch (error) { + // 偶发「会话不存在」:重拉会话后自动再试一次 + if (!isSessionMissingError(error)) throw error + sessionReady.value = false + await fetchOrderStat() + if (serial !== uploadSerial) return + sid = String(sessionId.value || '').trim() + if (!sid || !sessionReady.value) throw error + prepared = await runPrepare() + } if (serial !== uploadSerial) return pendingUploadId.value = prepared.uploadId lastUploadId.value = prepared.uploadId @@ -530,7 +574,9 @@ async function startPendingUploadAndClassify(file: File) { if (serial !== uploadSerial) return pendingUploadId.value = '' showToast({ - message: friendlyError(error, 'upload'), + message: isSessionMissingError(error) + ? config.messages.sessionMissing + : friendlyError(error, 'upload'), duration: 2800, }) return @@ -1012,12 +1058,13 @@ function applySessionHistory(detail: ReturnType { const id = String(sid || '').trim() if (!id) { sessionDetail.value = null seedRemainFromApi(orderStat.value?.remain_count) - return Promise.resolve() + return Promise.resolve(false) } return jingdongApi .getSessionDetail({ session_id: id }) @@ -1030,17 +1077,31 @@ function fetchSessionDetail(sid: string) { } sessionDetail.value = parsed applySessionHistory(parsed) + return true }) .catch(() => { sessionDetail.value = null seedRemainFromApi(orderStat.value?.remain_count) + return false }) } +/** detail 偶发未就绪时短退避重试,避免过早放开上传 */ +async function waitForSessionDetail(sid: string, attempts = 3): Promise { + for (let i = 0; i < attempts; i += 1) { + const ok = await fetchSessionDetail(sid) + if (ok) return true + if (i < attempts - 1) await sleep(350 * (i + 1)) + } + return false +} + function fetchOrderStat() { + sessionReady.value = false + sessionBootFailed.value = false return badgeApi .getOrderStat({}) - .then((res: any) => { + .then(async (res: any) => { // interceptor 已解包 data:remain_count / subject_name / type_name / session_id / prod_id ... orderStat.value = res || {} subjectNameFromStat.value = String(res?.subject_name || '').trim() @@ -1048,13 +1109,22 @@ function fetchOrderStat() { styleName.value = String(res?.style_name || '').trim() // 产品名称展示取 cate_name typeName.value = String(res?.cate_name || '').trim() - sessionId.value = String(res?.session_id || '').trim() + const sid = String(res?.session_id || '').trim() + sessionId.value = sid subjectMatched.value = true // 先记下接口值;等会话历史回来后再按已生成次数扣除 const apiRemain = Number(res?.remain_count) remainCount.value = Number.isFinite(apiRemain) ? Math.max(0, apiRemain) : 0 remainBaselineUsed.value = 0 - return fetchSessionDetail(sessionId.value) + if (!sid) { + sessionReady.value = false + sessionBootFailed.value = true + return false + } + const ok = await waitForSessionDetail(sid) + sessionReady.value = ok + sessionBootFailed.value = !ok + return ok }) .catch(() => { orderStat.value = null @@ -1065,7 +1135,10 @@ function fetchOrderStat() { typeName.value = '' sessionId.value = '' sessionDetail.value = null + sessionReady.value = false + sessionBootFailed.value = true subjectMatched.value = true + return false }) }