Files
ece.suwa3d.com/src/services/jingdong/image-inspect.ts
T
2026-11-03 14:13:25 +08:00

153 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 原图分类 /classify(对齐 agent image-inspect)
* - 本地:Vite 代理 `/concept-classify` → `/v1/classify`
* - 线上:直连 seetacloud
*/
export type ImageKind = 'cartoon' | 'real_photo' | 'other' | 'uncertain'
export interface ImageInspect {
imageType: ImageKind
isCartoon: boolean
personCount: number
petCount: number
reviewStatus: string
conclusion: string
}
export class InspectCancelled extends Error {
constructor() {
super('cancelled')
this.name = 'InspectCancelled'
}
}
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) {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: null
}
export function mapInspectImageType(raw: unknown): ImageKind {
const text = String(raw ?? '')
.trim()
.toLowerCase()
if (text === 'cartoon' || text === '1') return 'cartoon'
if (text === 'real_photo' || text === 'real' || text === '2') return 'real_photo'
if (text === 'other') return 'other'
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[]) {
for (const key of keys) {
const value = Number(data[key])
if (Number.isFinite(value)) return Math.max(0, Math.trunc(value))
}
return 0
}
function normalizeInspect(body: unknown): ImageInspect {
const root = asRecord(body) || {}
const data = asRecord(root.data) || root
const imageType = mapInspectImageType(data.image_type ?? data.imageType)
const isCartoonRaw = data.is_cartoon ?? data.isCartoon
const isCartoon =
typeof isCartoonRaw === 'boolean' ? isCartoonRaw : imageType === 'cartoon'
return {
imageType,
isCartoon,
personCount: countField(data, [
'person_count',
'personCount',
'headcount',
'head_count',
]),
petCount: countField(data, ['pet_count', 'petCount']),
reviewStatus: String(data.review_status || ''),
conclusion: String(data.conclusion || ''),
}
}
export function isCartoonImage(result: ImageInspect) {
return result.isCartoon || result.imageType === 'cartoon'
}
export function figureTypeOf(result: ImageInspect) {
return result.imageType === 'real_photo' ? 1 : 0
}
export function formatSubjectFromInspect(result: ImageInspect) {
const parts: string[] = []
if (result.personCount > 0) parts.push(`${result.personCount}人`)
if (result.petCount > 0) parts.push(`${result.petCount}宠物`)
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)
}
}