forked from natuka/web.puabadge.com
修改:jingdong的识别主体接口替换
This commit is contained in:
1 parent
b25e3ca473
commit
4979739521
4 files changed
+171
-121
No files matched your search
+39
-6
@@ -754,6 +754,23 @@ export const confirmUpload = (input: { session_id: string; upload_id: string })
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /jd/subject/classify
|
||||||
|
* 图片上传完成后识别主体(session_id + upload_id)
|
||||||
|
*/
|
||||||
|
export const classifySubject = (input: {
|
||||||
|
session_id: string
|
||||||
|
upload_id: string
|
||||||
|
}) => {
|
||||||
|
return request('jd/subject/classify', {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
session_id: input.session_id,
|
||||||
|
upload_id: input.upload_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** POST /jd/imagegen/submit */
|
/** POST /jd/imagegen/submit */
|
||||||
export const submitImageGen = (input: ImageGenSubmitInput) => {
|
export const submitImageGen = (input: ImageGenSubmitInput) => {
|
||||||
return request(`${PREFIX}/imagegen/submit`, {
|
return request(`${PREFIX}/imagegen/submit`, {
|
||||||
@@ -838,9 +855,10 @@ export const confirmOrder = (input: OrderConfirmInput) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传原图:签发 URL → PUT OSS → confirm(需 session_id)
|
* 导入阶段:签发 URL → PUT OSS(不 confirm)
|
||||||
|
* 拿到 upload_id 后可立刻调 subject/classify
|
||||||
*/
|
*/
|
||||||
export async function uploadOriginal(
|
export async function prepareUpload(
|
||||||
file: File | Blob,
|
file: File | Blob,
|
||||||
options: {
|
options: {
|
||||||
session_id: string
|
session_id: string
|
||||||
@@ -857,10 +875,6 @@ export async function uploadOriginal(
|
|||||||
purpose: options.purpose || 'origin',
|
purpose: options.purpose || 'origin',
|
||||||
})
|
})
|
||||||
await putUpload(ticket.upload_url, file, ticket.content_type || contentType)
|
await putUpload(ticket.upload_url, file, ticket.content_type || contentType)
|
||||||
await confirmUpload({
|
|
||||||
session_id: options.session_id,
|
|
||||||
upload_id: ticket.upload_id,
|
|
||||||
})
|
|
||||||
return {
|
return {
|
||||||
uploadId: ticket.upload_id,
|
uploadId: ticket.upload_id,
|
||||||
fileUrl: ticket.file_url,
|
fileUrl: ticket.file_url,
|
||||||
@@ -869,3 +883,22 @@ export async function uploadOriginal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整上传:签发 URL → PUT OSS → confirm(需 session_id)
|
||||||
|
*/
|
||||||
|
export async function uploadOriginal(
|
||||||
|
file: File | Blob,
|
||||||
|
options: {
|
||||||
|
session_id: string
|
||||||
|
filename?: string
|
||||||
|
purpose?: UploadPurpose
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const prepared = await prepareUpload(file, options)
|
||||||
|
await confirmUpload({
|
||||||
|
session_id: options.session_id,
|
||||||
|
upload_id: prepared.uploadId,
|
||||||
|
})
|
||||||
|
return prepared
|
||||||
|
}
|
||||||
|
|
||||||
@@ -59,9 +59,10 @@ export const jingdongConfig = {
|
|||||||
needEditText: '请输入想调整的内容后再发送',
|
needEditText: '请输入想调整的内容后再发送',
|
||||||
needPhotoOrText: '请上传照片或输入描述后再发送',
|
needPhotoOrText: '请上传照片或输入描述后再发送',
|
||||||
needProductTitle: (label: string) => `请填写${label}后再发送`,
|
needProductTitle: (label: string) => `请填写${label}后再发送`,
|
||||||
|
uploadingTip: '正在上传照片,请稍候…',
|
||||||
inspectingTip: '正在识别照片,请稍候…',
|
inspectingTip: '正在识别照片,请稍候…',
|
||||||
/** 仅提示,不拦截发送 */
|
/** 仅提示,不拦截发送 */
|
||||||
subjectMismatchTip: '上传图片的主体数量与下单主体数量不一致',
|
subjectMismatchTip: '上传图片的主体数量与下单主体数量不一致,生图时候会按下单的主体数量自动调整主体',
|
||||||
subjectMismatchToast: (got: string, need: string) =>
|
subjectMismatchToast: (got: string, need: string) =>
|
||||||
`上传图片主体(${got})与下单主体(${need})数量不一致`,
|
`上传图片主体(${got})与下单主体(${need})数量不一致`,
|
||||||
generating: '正在生成效果图,请稍候…',
|
generating: '正在生成效果图,请稍候…',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 原图分类 /classify(对齐 agent image-inspect)
|
* 图片主体识别结果(由后端 jd/subject/classify 返回后规范化)
|
||||||
* - 本地:Vite 代理 `/concept-classify` → `/v1/classify`
|
|
||||||
* - 线上:直连 seetacloud
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ImageKind = 'cartoon' | 'real_photo' | 'other' | 'uncertain'
|
export type ImageKind = 'cartoon' | 'real_photo' | 'other' | 'uncertain'
|
||||||
@@ -13,6 +11,8 @@ export interface ImageInspect {
|
|||||||
petCount: number
|
petCount: number
|
||||||
reviewStatus: string
|
reviewStatus: string
|
||||||
conclusion: string
|
conclusion: string
|
||||||
|
/** 主体识别 check_id,后续下单可用 */
|
||||||
|
checkId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InspectCancelled extends Error {
|
export class InspectCancelled extends Error {
|
||||||
@@ -22,10 +22,6 @@ export class InspectCancelled extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const INSPECT_TIMEOUT_MS = 120_000
|
|
||||||
const DEFAULT_INSPECT_HOST = 'https://u252424-8570-01f615a5.cqa1.seetacloud.com:8443'
|
|
||||||
const DEFAULT_INSPECT_REMOTE_URL = `${DEFAULT_INSPECT_HOST}/v1/classify`
|
|
||||||
|
|
||||||
function asRecord(value: unknown) {
|
function asRecord(value: unknown) {
|
||||||
return value && typeof value === 'object' && !Array.isArray(value)
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
? (value as Record<string, unknown>)
|
? (value as Record<string, unknown>)
|
||||||
@@ -42,24 +38,6 @@ export function mapInspectImageType(raw: unknown): ImageKind {
|
|||||||
return 'uncertain'
|
return 'uncertain'
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveInspectUrl() {
|
|
||||||
const fromEnv = String(
|
|
||||||
(import.meta as any).env?.VITE_CONCEPT_INSPECT_URL || '',
|
|
||||||
)
|
|
||||||
.trim()
|
|
||||||
.replace(/\/+$/, '')
|
|
||||||
if (fromEnv) {
|
|
||||||
if (/\/v1\/classify$/i.test(fromEnv)) return fromEnv
|
|
||||||
return `${fromEnv}/v1/classify`
|
|
||||||
}
|
|
||||||
if ((import.meta as any).env?.DEV) return '/concept-classify'
|
|
||||||
return DEFAULT_INSPECT_REMOTE_URL
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveInspectToken() {
|
|
||||||
return String((import.meta as any).env?.VITE_CONCEPT_INSPECT_TOKEN || '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
function countField(data: Record<string, unknown>, keys: string[]) {
|
function countField(data: Record<string, unknown>, keys: string[]) {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = Number(data[key])
|
const value = Number(data[key])
|
||||||
@@ -68,9 +46,19 @@ function countField(data: Record<string, unknown>, keys: string[]) {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeInspect(body: unknown): ImageInspect {
|
/** 规范化 classify 接口 data(拦截器已解包 code=1000 的 data) */
|
||||||
|
export function normalizeInspect(body: unknown): ImageInspect {
|
||||||
const root = asRecord(body) || {}
|
const root = asRecord(body) || {}
|
||||||
const data = asRecord(root.data) || root
|
const nested = asRecord(root.data)
|
||||||
|
// 兼容完整响应 { code, data } / 已解包的 data 本体
|
||||||
|
const data =
|
||||||
|
nested &&
|
||||||
|
(nested.image_type != null ||
|
||||||
|
nested.person_count != null ||
|
||||||
|
nested.is_cartoon != null ||
|
||||||
|
nested.check_id != null)
|
||||||
|
? nested
|
||||||
|
: root
|
||||||
const imageType = mapInspectImageType(data.image_type ?? data.imageType)
|
const imageType = mapInspectImageType(data.image_type ?? data.imageType)
|
||||||
const isCartoonRaw = data.is_cartoon ?? data.isCartoon
|
const isCartoonRaw = data.is_cartoon ?? data.isCartoon
|
||||||
const isCartoon =
|
const isCartoon =
|
||||||
@@ -87,6 +75,7 @@ function normalizeInspect(body: unknown): ImageInspect {
|
|||||||
petCount: countField(data, ['pet_count', 'petCount']),
|
petCount: countField(data, ['pet_count', 'petCount']),
|
||||||
reviewStatus: String(data.review_status || ''),
|
reviewStatus: String(data.review_status || ''),
|
||||||
conclusion: String(data.conclusion || ''),
|
conclusion: String(data.conclusion || ''),
|
||||||
|
checkId: countField(data, ['check_id', 'checkId']),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,49 +93,3 @@ export function formatSubjectFromInspect(result: ImageInspect) {
|
|||||||
if (result.petCount > 0) parts.push(`${result.petCount}宠物`)
|
if (result.petCount > 0) parts.push(`${result.petCount}宠物`)
|
||||||
return parts.join('+')
|
return parts.join('+')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function inspectImage(file: File, signal?: AbortSignal) {
|
|
||||||
if (signal?.aborted) throw new InspectCancelled()
|
|
||||||
const url = resolveInspectUrl()
|
|
||||||
const token = resolveInspectToken()
|
|
||||||
const form = new FormData()
|
|
||||||
form.append('image', file, file.name || 'image.jpg')
|
|
||||||
const headers: Record<string, string> = {}
|
|
||||||
if (token) headers.Authorization = `Bearer ${token}`
|
|
||||||
|
|
||||||
const controller = new AbortController()
|
|
||||||
let timedOut = false
|
|
||||||
const timer = window.setTimeout(() => {
|
|
||||||
timedOut = true
|
|
||||||
controller.abort()
|
|
||||||
}, INSPECT_TIMEOUT_MS)
|
|
||||||
const onAbort = () => controller.abort()
|
|
||||||
signal?.addEventListener('abort', onAbort, { once: true })
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
body: form,
|
|
||||||
headers,
|
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 400) throw new Error('图片上传错误,请检查文件后重试')
|
|
||||||
if (response.status === 401) throw new Error('图片识别鉴权失败')
|
|
||||||
if (response.status === 413) throw new Error('图片过大,请压缩后重试')
|
|
||||||
throw new Error(`图片识别失败(${response.status})`)
|
|
||||||
}
|
|
||||||
const raw: unknown = await response.json()
|
|
||||||
return normalizeInspect(raw)
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
||||||
if (timedOut) throw new Error('图片识别超时,请稍后重试')
|
|
||||||
throw new InspectCancelled()
|
|
||||||
}
|
|
||||||
if (error instanceof TypeError) throw new Error('图片识别失败,请稍后重试')
|
|
||||||
throw error instanceof Error ? error : new Error('图片识别失败')
|
|
||||||
} finally {
|
|
||||||
window.clearTimeout(timer)
|
|
||||||
signal?.removeEventListener('abort', onAbort)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+114
-41
@@ -95,7 +95,7 @@
|
|||||||
:editing-hint="config.messages.editingHint"
|
:editing-hint="config.messages.editingHint"
|
||||||
:exit-edit-label="config.messages.exitEdit"
|
:exit-edit-label="config.messages.exitEdit"
|
||||||
:disabled="isBusy"
|
:disabled="isBusy"
|
||||||
:inspecting="inspecting"
|
:inspecting="inspecting || uploading"
|
||||||
:can-send="canSend"
|
:can-send="canSend"
|
||||||
:status-tip="composerStatusTip"
|
:status-tip="composerStatusTip"
|
||||||
:tip-warn="composerTipWarn"
|
:tip-warn="composerTipWarn"
|
||||||
@@ -124,8 +124,7 @@ import * as badgeApi from '@/api/badge'
|
|||||||
import * as jingdongApi from '@/api/jingdong'
|
import * as jingdongApi from '@/api/jingdong'
|
||||||
import { jingdongConfig as config } from '@/config/jingdong'
|
import { jingdongConfig as config } from '@/config/jingdong'
|
||||||
import {
|
import {
|
||||||
InspectCancelled,
|
normalizeInspect,
|
||||||
inspectImage,
|
|
||||||
type ImageInspect,
|
type ImageInspect,
|
||||||
} from '@/services/jingdong/image-inspect'
|
} from '@/services/jingdong/image-inspect'
|
||||||
import { flyImageToRect, rectFromElement, type FlyRect } from '@/utils/fly-image'
|
import { flyImageToRect, rectFromElement, type FlyRect } from '@/utils/fly-image'
|
||||||
@@ -219,16 +218,19 @@ const titleInvalid = ref(false)
|
|||||||
const pendingPreview = ref('')
|
const pendingPreview = ref('')
|
||||||
const pendingObjectUrl = ref('')
|
const pendingObjectUrl = ref('')
|
||||||
const pendingFile = ref<File | null>(null)
|
const pendingFile = ref<File | null>(null)
|
||||||
|
/** 选图后预上传得到的 upload_id,发送时复用 */
|
||||||
|
const pendingUploadId = ref('')
|
||||||
const blobUrls = new Set<string>()
|
const blobUrls = new Set<string>()
|
||||||
const editingResultId = ref<string | null>(null)
|
const editingResultId = ref<string | null>(null)
|
||||||
const isBusy = ref(false)
|
const isBusy = ref(false)
|
||||||
|
const uploading = ref(false)
|
||||||
const inspecting = ref(false)
|
const inspecting = ref(false)
|
||||||
const photoInspect = ref<ImageInspect | null>(null)
|
const photoInspect = ref<ImageInspect | null>(null)
|
||||||
const lastUploadId = ref('')
|
const lastUploadId = ref('')
|
||||||
const pendingFinish = ref<PendingFinish | null>(null)
|
const pendingFinish = ref<PendingFinish | null>(null)
|
||||||
let inspectSerial = 0
|
|
||||||
let inspectAbort: AbortController | null = null
|
|
||||||
let pollActive = false
|
let pollActive = false
|
||||||
|
/** 切换/清空图片时作废进行中的上传与识别 */
|
||||||
|
let uploadSerial = 0
|
||||||
|
|
||||||
function createBaseMessages(): ChatMessage[] {
|
function createBaseMessages(): ChatMessage[] {
|
||||||
return [
|
return [
|
||||||
@@ -348,9 +350,11 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
const canSend = computed(() => {
|
const canSend = computed(() => {
|
||||||
if (isBusy.value || inspecting.value) return false
|
if (isBusy.value || uploading.value || inspecting.value) return false
|
||||||
if (remainCount.value <= 0) return false
|
if (remainCount.value <= 0) return false
|
||||||
if (!hasRequiredTitle.value) return false
|
if (!hasRequiredTitle.value) return false
|
||||||
|
// 上传未完成时不可发(避免缺少 upload_id)
|
||||||
|
if (pendingPreview.value && !pendingUploadId.value) return false
|
||||||
if (isEditing.value) {
|
if (isEditing.value) {
|
||||||
// 编辑:需有飞入的效果图 + 调整文案
|
// 编辑:需有飞入的效果图 + 调整文案
|
||||||
return !!pendingPreview.value && draft.value.trim().length > 0
|
return !!pendingPreview.value && draft.value.trim().length > 0
|
||||||
@@ -362,6 +366,7 @@ const canSend = computed(() => {
|
|||||||
|
|
||||||
const composerStatusTip = computed(() => {
|
const composerStatusTip = computed(() => {
|
||||||
if (isBusy.value) return config.messages.generatingTip
|
if (isBusy.value) return config.messages.generatingTip
|
||||||
|
if (uploading.value) return config.messages.uploadingTip
|
||||||
if (inspecting.value) return config.messages.inspectingTip
|
if (inspecting.value) return config.messages.inspectingTip
|
||||||
if (remainCount.value <= 0) return config.messages.noRemain
|
if (remainCount.value <= 0) return config.messages.noRemain
|
||||||
if (pendingPreview.value && !subjectMatched.value) return config.messages.subjectMismatchTip
|
if (pendingPreview.value && !subjectMatched.value) return config.messages.subjectMismatchTip
|
||||||
@@ -448,6 +453,10 @@ function triggerUpload() {
|
|||||||
showToast('编辑效果图时请直接描述想调整的内容')
|
showToast('编辑效果图时请直接描述想调整的内容')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (uploading.value) {
|
||||||
|
showToast(config.messages.uploadingTip)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (inspecting.value) {
|
if (inspecting.value) {
|
||||||
showToast(config.messages.inspectingTip)
|
showToast(config.messages.inspectingTip)
|
||||||
return
|
return
|
||||||
@@ -472,10 +481,11 @@ function revokePendingObjectUrl() {
|
|||||||
* keepObjectUrl=true:气泡仍要用 blob 预览,只断开 pending 引用,不 revoke。
|
* keepObjectUrl=true:气泡仍要用 blob 预览,只断开 pending 引用,不 revoke。
|
||||||
*/
|
*/
|
||||||
function clearPendingPreview(options?: { keepObjectUrl?: boolean }) {
|
function clearPendingPreview(options?: { keepObjectUrl?: boolean }) {
|
||||||
inspectAbort?.abort()
|
uploadSerial += 1
|
||||||
inspectAbort = null
|
uploading.value = false
|
||||||
inspecting.value = false
|
inspecting.value = false
|
||||||
pendingFile.value = null
|
pendingFile.value = null
|
||||||
|
pendingUploadId.value = ''
|
||||||
photoInspect.value = null
|
photoInspect.value = null
|
||||||
subjectMatched.value = true
|
subjectMatched.value = true
|
||||||
if (subjectNameFromStat.value) subjectName.value = subjectNameFromStat.value
|
if (subjectNameFromStat.value) subjectName.value = subjectNameFromStat.value
|
||||||
@@ -488,6 +498,69 @@ function clearPendingPreview(options?: { keepObjectUrl?: boolean }) {
|
|||||||
pendingPreview.value = ''
|
pendingPreview.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入阶段:upload/url(+ PUT OSS)→ subject/classify
|
||||||
|
* confirm 留到提交时再调
|
||||||
|
*/
|
||||||
|
async function startPendingUploadAndClassify(file: File) {
|
||||||
|
const serial = ++uploadSerial
|
||||||
|
pendingUploadId.value = ''
|
||||||
|
photoInspect.value = null
|
||||||
|
subjectMatched.value = true
|
||||||
|
|
||||||
|
const sid = String(sessionId.value || '').trim()
|
||||||
|
if (!sid) {
|
||||||
|
showToast(friendlyError('缺少会话信息,请刷新后重试', 'common'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploading.value = true
|
||||||
|
inspecting.value = false
|
||||||
|
try {
|
||||||
|
// 仅签发 url 并 PUT,不 confirm
|
||||||
|
const prepared = await jingdongApi.prepareUpload(file, {
|
||||||
|
session_id: sid,
|
||||||
|
purpose: 'origin',
|
||||||
|
})
|
||||||
|
if (serial !== uploadSerial) return
|
||||||
|
pendingUploadId.value = prepared.uploadId
|
||||||
|
lastUploadId.value = prepared.uploadId
|
||||||
|
} catch (error) {
|
||||||
|
if (serial !== uploadSerial) return
|
||||||
|
pendingUploadId.value = ''
|
||||||
|
showToast({
|
||||||
|
message: friendlyError(error, 'upload'),
|
||||||
|
duration: 2800,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
if (serial === uploadSerial) uploading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serial !== uploadSerial || !pendingUploadId.value) return
|
||||||
|
|
||||||
|
inspecting.value = true
|
||||||
|
try {
|
||||||
|
const classifyRaw = await jingdongApi.classifySubject({
|
||||||
|
session_id: sid,
|
||||||
|
upload_id: pendingUploadId.value,
|
||||||
|
})
|
||||||
|
if (serial !== uploadSerial) return
|
||||||
|
const inspectResult = normalizeInspect(classifyRaw)
|
||||||
|
photoInspect.value = inspectResult
|
||||||
|
if (subjectNameFromStat.value) subjectName.value = subjectNameFromStat.value
|
||||||
|
checkSubjectMatch(inspectResult)
|
||||||
|
} catch (error) {
|
||||||
|
if (serial !== uploadSerial) return
|
||||||
|
showToast({
|
||||||
|
message: friendlyError(error, 'inspect'),
|
||||||
|
duration: 2800,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
if (serial === uploadSerial) inspecting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onClearPreview() {
|
function onClearPreview() {
|
||||||
clearPendingPreview()
|
clearPendingPreview()
|
||||||
// 编辑态清掉预览图时同步退出编辑,保留已输入提示词
|
// 编辑态清掉预览图时同步退出编辑,保留已输入提示词
|
||||||
@@ -505,44 +578,25 @@ async function fileFromImageUrl(url: string, filename = 'effect.jpg') {
|
|||||||
return new File([blob], name, { type })
|
return new File([blob], name, { type })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runPhotoRecognize(file: File) {
|
|
||||||
inspectAbort?.abort()
|
|
||||||
const controller = new AbortController()
|
|
||||||
inspectAbort = controller
|
|
||||||
const serial = ++inspectSerial
|
|
||||||
inspecting.value = true
|
|
||||||
photoInspect.value = null
|
|
||||||
subjectMatched.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const inspectResult = await inspectImage(file, controller.signal)
|
|
||||||
if (serial !== inspectSerial) return
|
|
||||||
|
|
||||||
photoInspect.value = inspectResult
|
|
||||||
// 标签仍展示订单主体;识别结果只用于校验
|
|
||||||
if (subjectNameFromStat.value) subjectName.value = subjectNameFromStat.value
|
|
||||||
|
|
||||||
checkSubjectMatch(inspectResult)
|
|
||||||
} catch (error) {
|
|
||||||
if (serial !== inspectSerial || error instanceof InspectCancelled) return
|
|
||||||
showToast(friendlyError(error, 'inspect'))
|
|
||||||
} finally {
|
|
||||||
if (serial === inspectSerial) inspecting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFileChange(e: Event) {
|
function onFileChange(e: Event) {
|
||||||
const input = e.target as HTMLInputElement
|
const input = e.target as HTMLInputElement
|
||||||
const file = input.files?.[0]
|
const file = input.files?.[0]
|
||||||
input.value = ''
|
input.value = ''
|
||||||
if (!file) return
|
if (!file) return
|
||||||
|
uploadSerial += 1
|
||||||
|
uploading.value = false
|
||||||
|
inspecting.value = false
|
||||||
|
photoInspect.value = null
|
||||||
|
subjectMatched.value = true
|
||||||
|
pendingUploadId.value = ''
|
||||||
revokePendingObjectUrl()
|
revokePendingObjectUrl()
|
||||||
const url = URL.createObjectURL(file)
|
const url = URL.createObjectURL(file)
|
||||||
trackBlobUrl(url)
|
trackBlobUrl(url)
|
||||||
pendingObjectUrl.value = url
|
pendingObjectUrl.value = url
|
||||||
pendingPreview.value = url
|
pendingPreview.value = url
|
||||||
pendingFile.value = file
|
pendingFile.value = file
|
||||||
void runPhotoRecognize(file)
|
// 导入后立刻上传,成功后再识别主体
|
||||||
|
void startPendingUploadAndClassify(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewImages(images: string[], startPosition = 0) {
|
function previewImages(images: string[], startPosition = 0) {
|
||||||
@@ -601,10 +655,12 @@ async function enterEdit(resultId: string, sourceRect?: FlyRect | null) {
|
|||||||
// 先放入预览(飞入过程中隐藏),并准备重新上传用的文件
|
// 先放入预览(飞入过程中隐藏),并准备重新上传用的文件
|
||||||
pendingPreview.value = msg.image
|
pendingPreview.value = msg.image
|
||||||
subjectMatched.value = true
|
subjectMatched.value = true
|
||||||
|
pendingUploadId.value = ''
|
||||||
void fileFromImageUrl(msg.image)
|
void fileFromImageUrl(msg.image)
|
||||||
.then((file) => {
|
.then((file) => {
|
||||||
if (editingResultId.value !== resultId) return
|
if (editingResultId.value !== resultId) return
|
||||||
pendingFile.value = file
|
pendingFile.value = file
|
||||||
|
void startPendingUploadAndClassify(file)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (editingResultId.value === resultId) {
|
if (editingResultId.value === resultId) {
|
||||||
@@ -778,17 +834,27 @@ async function startGenerate(opts: {
|
|||||||
pendingFinish.value = null
|
pendingFinish.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let uploadId = ''
|
if (!opts.file && !pendingUploadId.value) {
|
||||||
if (!opts.file) {
|
|
||||||
throw new Error(opts.mode === 'edit' ? '缺少可编辑的效果图' : '请先上传照片')
|
throw new Error(opts.mode === 'edit' ? '缺少可编辑的效果图' : '请先上传照片')
|
||||||
}
|
}
|
||||||
// 卡通 / 真人统一走生图:上传 purpose=origin,再 imagegen/submit
|
// 复用导入阶段的 upload_id;若无则兜底:url+PUT(仍不在这里 classify)
|
||||||
const uploaded = await jingdongApi.uploadOriginal(opts.file, {
|
let uploadId = String(pendingUploadId.value || '').trim()
|
||||||
|
if (!uploadId) {
|
||||||
|
if (!opts.file) throw new Error(opts.mode === 'edit' ? '缺少可编辑的效果图' : '请先上传照片')
|
||||||
|
const prepared = await jingdongApi.prepareUpload(opts.file, {
|
||||||
session_id: sid,
|
session_id: sid,
|
||||||
purpose: 'origin',
|
purpose: 'origin',
|
||||||
})
|
})
|
||||||
uploadId = uploaded.uploadId
|
uploadId = prepared.uploadId
|
||||||
|
pendingUploadId.value = uploadId
|
||||||
lastUploadId.value = uploadId
|
lastUploadId.value = uploadId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交阶段:confirm → imagegen/submit
|
||||||
|
await jingdongApi.confirmUpload({
|
||||||
|
session_id: sid,
|
||||||
|
upload_id: uploadId,
|
||||||
|
})
|
||||||
|
|
||||||
const styleId = resolveStyleId()
|
const styleId = resolveStyleId()
|
||||||
const sceneProp = String(opts.sceneProp || '').trim()
|
const sceneProp = String(opts.sceneProp || '').trim()
|
||||||
@@ -854,10 +920,18 @@ function onSend() {
|
|||||||
showToast(config.messages.generatingTip)
|
showToast(config.messages.generatingTip)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (uploading.value) {
|
||||||
|
showToast(config.messages.uploadingTip)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (inspecting.value) {
|
if (inspecting.value) {
|
||||||
showToast(config.messages.inspectingTip)
|
showToast(config.messages.inspectingTip)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (pendingPreview.value && !pendingUploadId.value) {
|
||||||
|
showToast(config.messages.uploadingTip)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (remainCount.value <= 0) {
|
if (remainCount.value <= 0) {
|
||||||
showToast(config.messages.noRemain)
|
showToast(config.messages.noRemain)
|
||||||
@@ -1023,7 +1097,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
pollActive = false
|
pollActive = false
|
||||||
inspectAbort?.abort()
|
|
||||||
blobUrls.forEach((url) => URL.revokeObjectURL(url))
|
blobUrls.forEach((url) => URL.revokeObjectURL(url))
|
||||||
blobUrls.clear()
|
blobUrls.clear()
|
||||||
pendingObjectUrl.value = ''
|
pendingObjectUrl.value = ''
|
||||||
|
|||||||
Reference in new issue
Block a user