/** * 原图分类 /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) : 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, 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 = {} 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) } }