forked from natuka/web.puabadge.com
888 lines
24 KiB
TypeScript
888 lines
24 KiB
TypeScript
import axios from 'axios'
|
||
import request from '@/utils/request'
|
||
import { localStorage } from '@/utils/local-storage'
|
||
|
||
/** 京东 BFF:与 agent 对话页同构接口,路径前缀为 /jd */
|
||
const PREFIX = 'jd'
|
||
|
||
function buildApiUrl(path: string) {
|
||
const base = String(process.env.VUE_APP_API_BASE_URL || '/api/').replace(/\/+$/, '')
|
||
return `${base}/${String(path).replace(/^\/+/, '')}`
|
||
}
|
||
|
||
function buildAuthHeaders() {
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
const code = localStorage.get('code')
|
||
const userId = localStorage.get('userId')
|
||
if (code) headers.Authorization = String(code)
|
||
if (userId) headers.Userid = String(userId)
|
||
return headers
|
||
}
|
||
|
||
function asRecord(value: unknown) {
|
||
return value && typeof value === 'object' && !Array.isArray(value)
|
||
? (value as Record<string, unknown>)
|
||
: null
|
||
}
|
||
|
||
function textOf(value: unknown) {
|
||
return value == null ? '' : String(value)
|
||
}
|
||
|
||
function listOf(value: unknown) {
|
||
return Array.isArray(value) ? value : []
|
||
}
|
||
|
||
function asCount(value: unknown) {
|
||
const number = Number(value)
|
||
return Number.isFinite(number) ? number : 0
|
||
}
|
||
|
||
export function normalizeOssUrl(url: string) {
|
||
if (!url) return ''
|
||
let s = String(url).replace(/\\u0026/gi, '&').replace(/\\u003d/gi, '=').trim()
|
||
if (s.indexOf('//') === 0) s = `https:${s}`
|
||
if (/^http:\/\//i.test(s)) s = `https://${s.slice(7)}`
|
||
return s
|
||
}
|
||
|
||
function imageUrlOf(item: Record<string, unknown>) {
|
||
return normalizeOssUrl(
|
||
textOf(item.url || item.file_url || item.image_url || item.cover_url || item.result_url),
|
||
)
|
||
}
|
||
|
||
function guessContentType(file: File | Blob, filename?: string) {
|
||
if (file.type) return file.type
|
||
const name = String(filename || '').toLowerCase()
|
||
if (name.endsWith('.png')) return 'image/png'
|
||
if (name.endsWith('.webp')) return 'image/webp'
|
||
if (name.endsWith('.gif')) return 'image/gif'
|
||
return 'image/jpeg'
|
||
}
|
||
|
||
function guessFilename(file?: File) {
|
||
if (file && (file as File).name) return (file as File).name
|
||
return 'photo.jpg'
|
||
}
|
||
|
||
function formatClock(value: unknown) {
|
||
const n = asCount(value)
|
||
if (!n) {
|
||
const s = textOf(value)
|
||
if (/^\d{1,2}:\d{2}$/.test(s)) return s
|
||
return ''
|
||
}
|
||
const ms = n > 1e12 ? n : n * 1000
|
||
const d = new Date(ms)
|
||
if (Number.isNaN(d.getTime())) return ''
|
||
const h = String(d.getHours()).padStart(2, '0')
|
||
const m = String(d.getMinutes()).padStart(2, '0')
|
||
return `${h}:${m}`
|
||
}
|
||
|
||
// ---------- 类型(对齐 agent-api) ----------
|
||
|
||
export type UploadPurpose = 'origin' | 'external_effect'
|
||
export type ImageGenStatusCode = 'pending' | 'processing' | 'success' | 'failed'
|
||
|
||
export interface UploadTicket {
|
||
upload_id: string
|
||
upload_url: string
|
||
file_url: string
|
||
content_type: string
|
||
expire_at: number
|
||
}
|
||
|
||
export interface ImageGenSubmitInput {
|
||
session_id: string
|
||
/** 导入生图传 upload_id;编辑效果图不传 */
|
||
upload_id?: string
|
||
/** 编辑效果图传 image_id;导入生图不传 */
|
||
image_id?: string
|
||
prompt?: string
|
||
scene_prop?: string
|
||
custom_text?: string
|
||
/** 效果图编辑 edit;原图编辑 / 普通立即生成 generate */
|
||
mode?: 'generate' | 'edit'
|
||
}
|
||
|
||
export interface ImageGenImage {
|
||
image_id: string
|
||
path: string
|
||
url: string
|
||
index: number
|
||
}
|
||
|
||
export interface ImageGenTask {
|
||
task_id: string
|
||
status: ImageGenStatusCode
|
||
}
|
||
|
||
export interface ImageGenStatus {
|
||
task_id: string
|
||
status: ImageGenStatusCode
|
||
error: string
|
||
style: string
|
||
prompt: string
|
||
images: ImageGenImage[]
|
||
}
|
||
|
||
export interface EffectSubmitInput {
|
||
session_id: string
|
||
upload_id: string
|
||
product_id: number
|
||
product_name?: string
|
||
user_message?: string
|
||
mounts_id?: number
|
||
scene_prop?: string
|
||
custom_text?: string
|
||
base_type?: number
|
||
frame_type?: number
|
||
mount_id?: number
|
||
pid?: number
|
||
proportion_id?: number
|
||
pose_id?: number
|
||
style_id?: number
|
||
prompt?: string
|
||
}
|
||
|
||
export interface EffectSubmitResult {
|
||
task_id: string
|
||
status: ImageGenStatusCode
|
||
error: string
|
||
image_id: string
|
||
url: string
|
||
images: ImageGenImage[]
|
||
}
|
||
|
||
export interface OrderConfirmInput {
|
||
task_id: string
|
||
image_id?: string
|
||
cate_id: number
|
||
product_id?: number
|
||
top_prod_id?: number
|
||
shop_id: number
|
||
print_size?: number
|
||
quantity?: number
|
||
contact_name?: string
|
||
contact_mobile?: string
|
||
address?: string
|
||
country_id?: number
|
||
province_id?: string
|
||
city_id?: string
|
||
county_id?: string
|
||
figure_type: number
|
||
check_id?: number
|
||
out_trade_no?: string
|
||
remark?: string
|
||
shape_id?: number
|
||
font_id?: number
|
||
custom_text?: string
|
||
back_text?: string
|
||
up_text?: string
|
||
down_text?: string
|
||
}
|
||
|
||
export interface OrderConfirmResult {
|
||
order_id: number
|
||
pid: number
|
||
message: string
|
||
out_trade_no?: string
|
||
}
|
||
|
||
export interface SessionDetailUpload {
|
||
upload_id: string
|
||
status: string
|
||
purpose: UploadPurpose | ''
|
||
path: string
|
||
url: string
|
||
}
|
||
|
||
export interface SessionDetailImage {
|
||
image_id: string
|
||
path: string
|
||
url: string
|
||
index: number
|
||
}
|
||
|
||
export interface SessionDetailTask {
|
||
task_id: string
|
||
upload_id: string
|
||
status: string
|
||
error: string
|
||
source: string
|
||
provider: string
|
||
product_name: string
|
||
style_code: string
|
||
style_name: string
|
||
prompt: string
|
||
n: number
|
||
pid: number
|
||
/** generate | edit,编辑效果图时右侧气泡只展示提示词 */
|
||
mode: 'generate' | 'edit' | ''
|
||
created_at: number
|
||
completed_at: number
|
||
duration: number
|
||
images: SessionDetailImage[]
|
||
}
|
||
|
||
export const SESSION_RECORD_TYPES = [
|
||
'session_created',
|
||
'upload',
|
||
'upload_confirmed',
|
||
'imagegen_submit',
|
||
'imagegen_success',
|
||
'imagegen_failed',
|
||
'external_effect',
|
||
'order',
|
||
] as const
|
||
|
||
export type SessionRecordType = (typeof SESSION_RECORD_TYPES)[number]
|
||
|
||
export interface SessionDetailRecord {
|
||
type: SessionRecordType | ''
|
||
at: number
|
||
message: string
|
||
upload_id: string
|
||
task_id: string
|
||
status: string
|
||
product_name: string
|
||
image_count: number
|
||
}
|
||
|
||
export interface SessionDetail {
|
||
session_id: string
|
||
shop_id: number
|
||
customer_name: string
|
||
subtitle: string
|
||
created_at: number
|
||
updated_at: number
|
||
uploads: SessionDetailUpload[]
|
||
tasks: SessionDetailTask[]
|
||
records: SessionDetailRecord[]
|
||
}
|
||
|
||
export type SessionHistoryItem =
|
||
| {
|
||
role: 'user'
|
||
text: string
|
||
image?: string
|
||
time?: string
|
||
taskId?: string
|
||
uploadId?: string
|
||
}
|
||
| {
|
||
role: 'agent'
|
||
type: 'result'
|
||
image: string
|
||
meta?: string
|
||
time?: string
|
||
durationSec?: number
|
||
taskId?: string
|
||
imageId?: string
|
||
uploadId?: string
|
||
prompt?: string
|
||
}
|
||
|
||
export function isExternalEffect(value: string | undefined) {
|
||
return value === 'external_effect'
|
||
}
|
||
|
||
// ---------- 解析 ----------
|
||
|
||
function uploadPurpose(value: unknown): UploadPurpose | '' {
|
||
return value === 'origin' || value === 'external_effect' ? value : ''
|
||
}
|
||
|
||
function sessionRecordType(value: unknown): SessionRecordType | '' {
|
||
const text = textOf(value).trim().toLowerCase()
|
||
return (SESSION_RECORD_TYPES as readonly string[]).includes(text)
|
||
? (text as SessionRecordType)
|
||
: ''
|
||
}
|
||
|
||
function normalizeDetailImage(raw: unknown): SessionDetailImage | null {
|
||
const item = asRecord(raw)
|
||
if (!item) return null
|
||
const url = imageUrlOf(item)
|
||
if (!url) return null
|
||
return {
|
||
image_id: textOf(item.image_id || item.id),
|
||
path: textOf(item.path),
|
||
url,
|
||
index: asCount(item.index),
|
||
}
|
||
}
|
||
|
||
function normalizeDetailTask(raw: unknown): SessionDetailTask | null {
|
||
const item = asRecord(raw)
|
||
if (!item || !item.task_id) return null
|
||
const images = listOf(item.images ?? item.image_list ?? item.results)
|
||
.map((image) => normalizeDetailImage(image))
|
||
.filter((image): image is SessionDetailImage => Boolean(image))
|
||
.sort((a, b) => a.index - b.index)
|
||
if (!images.length) {
|
||
const url = imageUrlOf(item)
|
||
if (url) {
|
||
images.push({
|
||
image_id: textOf(item.image_id || item.id),
|
||
path: textOf(item.path),
|
||
url,
|
||
index: 0,
|
||
})
|
||
}
|
||
}
|
||
const modeRaw = textOf(item.mode || item.image_gen_mode || item.imageGenMode).toLowerCase()
|
||
const mode: SessionDetailTask['mode'] =
|
||
modeRaw === 'edit' || modeRaw === 'generate' ? modeRaw : ''
|
||
return {
|
||
task_id: textOf(item.task_id),
|
||
upload_id: textOf(item.upload_id),
|
||
status: textOf(item.status),
|
||
error: textOf(item.error || item.message),
|
||
source: textOf(item.source),
|
||
provider: textOf(item.provider),
|
||
product_name: textOf(item.product_name),
|
||
style_code: textOf(item.style_code),
|
||
style_name: textOf(item.style_name),
|
||
prompt: textOf(item.prompt),
|
||
n: asCount(item.n),
|
||
pid: asCount(item.pid),
|
||
mode,
|
||
created_at: asCount(item.created_at),
|
||
completed_at: asCount(item.completed_at),
|
||
duration: asCount(item.duration),
|
||
images,
|
||
}
|
||
}
|
||
|
||
function normalizeDetailRecord(raw: unknown): SessionDetailRecord | null {
|
||
const item = asRecord(raw)
|
||
if (!item) return null
|
||
return {
|
||
type: sessionRecordType(item.type),
|
||
at: asCount(item.at),
|
||
message: textOf(item.message),
|
||
upload_id: textOf(item.upload_id),
|
||
task_id: textOf(item.task_id),
|
||
status: textOf(item.status),
|
||
product_name: textOf(item.product_name),
|
||
image_count: asCount(item.image_count),
|
||
}
|
||
}
|
||
|
||
/** 解析 session/detail(对齐 agent normalizeSessionDetail) */
|
||
export function normalizeSessionDetail(data: unknown): SessionDetail {
|
||
const record = asRecord(data) || {}
|
||
const uploads = listOf(record.uploads)
|
||
.map((item) => asRecord(item))
|
||
.filter((item): item is Record<string, unknown> => Boolean(item && item.upload_id))
|
||
.map((item) => ({
|
||
upload_id: textOf(item.upload_id),
|
||
status: textOf(item.status),
|
||
purpose: uploadPurpose(item.purpose),
|
||
path: textOf(item.path),
|
||
url: normalizeOssUrl(textOf(item.url)),
|
||
}))
|
||
const tasks = listOf(record.tasks)
|
||
.map((item) => normalizeDetailTask(item))
|
||
.filter((item): item is SessionDetailTask => Boolean(item))
|
||
.sort((a, b) => a.created_at - b.created_at)
|
||
const records = listOf(record.records)
|
||
.map((item) => normalizeDetailRecord(item))
|
||
.filter((item): item is SessionDetailRecord => Boolean(item))
|
||
.sort((a, b) => a.at - b.at)
|
||
return {
|
||
session_id: textOf(record.session_id),
|
||
shop_id: asCount(record.shop_id),
|
||
customer_name: textOf(record.customer_name),
|
||
subtitle: textOf(record.subtitle),
|
||
created_at: asCount(record.created_at),
|
||
updated_at: asCount(record.updated_at || record.created_at),
|
||
uploads,
|
||
tasks,
|
||
records,
|
||
}
|
||
}
|
||
|
||
function uploadOf(detail: SessionDetail, uploadId: string) {
|
||
return uploadId ? detail.uploads.find((item) => item.upload_id === uploadId) : undefined
|
||
}
|
||
|
||
function isBroughtTask(detail: SessionDetail, task: SessionDetailTask) {
|
||
const upload = uploadOf(detail, task.upload_id)
|
||
return (
|
||
isExternalEffect(upload?.purpose) ||
|
||
isExternalEffect(task.source) ||
|
||
task.provider === 'user_effect'
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 将 session/detail 转为对话气泡历史(对齐 agent applySessionDetail → turns)
|
||
*/
|
||
export function sessionDetailToHistory(detail: SessionDetail): SessionHistoryItem[] {
|
||
const history: SessionHistoryItem[] = []
|
||
const defaultPrompt = '按所选产品、风格生成手办效果图~'
|
||
|
||
detail.tasks.forEach((task) => {
|
||
const upload = uploadOf(detail, task.upload_id)
|
||
const photo = upload?.url || ''
|
||
const brought = isBroughtTask(detail, task)
|
||
const time = formatClock(task.completed_at || task.created_at)
|
||
const seconds =
|
||
task.duration > 0
|
||
? task.duration
|
||
: task.completed_at > task.created_at
|
||
? task.completed_at - task.created_at
|
||
: 0
|
||
const images = task.images.filter((image) => image.url)
|
||
const resultImages =
|
||
images.length > 0
|
||
? images
|
||
: brought && photo
|
||
? [{ image_id: '', path: '', url: photo, index: 0 }]
|
||
: []
|
||
|
||
if (!resultImages.length && task.status !== 'success') return
|
||
|
||
// 右侧气泡:purpose=origin 显示原图;external_effect 只展示提示词
|
||
const userImage = upload?.purpose === 'origin' && photo ? photo : undefined
|
||
const userText =
|
||
task.prompt ||
|
||
(brought || upload?.purpose === 'external_effect'
|
||
? '这是我在别的平台生成的卡通效果图,不用再调整了,直接帮我下单吧~'
|
||
: defaultPrompt)
|
||
|
||
history.push({
|
||
role: 'user',
|
||
text: userText,
|
||
image: userImage,
|
||
time: time || undefined,
|
||
taskId: task.task_id,
|
||
uploadId: task.upload_id,
|
||
})
|
||
|
||
resultImages.forEach((image) => {
|
||
history.push({
|
||
role: 'agent',
|
||
type: 'result',
|
||
image: image.url,
|
||
time: time || undefined,
|
||
durationSec: seconds,
|
||
meta: time
|
||
? `${time}${seconds > 0 ? ` · 生图用时 ${seconds}s` : ''}`
|
||
: seconds > 0
|
||
? `生图用时 ${seconds}s`
|
||
: '',
|
||
taskId: task.task_id,
|
||
imageId: image.image_id || undefined,
|
||
uploadId: task.upload_id,
|
||
prompt: task.prompt || undefined,
|
||
})
|
||
})
|
||
})
|
||
|
||
return history
|
||
}
|
||
|
||
/** 兼容旧调用:解析 detail 并产出 history */
|
||
export function parseSessionDetail(res: any): SessionDetail & {
|
||
sessionId: string
|
||
history: SessionHistoryItem[]
|
||
raw: any
|
||
} {
|
||
const detail = normalizeSessionDetail(res)
|
||
return {
|
||
...detail,
|
||
sessionId: detail.session_id,
|
||
history: sessionDetailToHistory(detail),
|
||
raw: res,
|
||
}
|
||
}
|
||
|
||
/** 将接口 body 归一成对象(后端若返回 text/plain,axios 会得到 JSON 字符串) */
|
||
function coerceResponseBody(res: unknown): Record<string, any> {
|
||
if (res == null) return {}
|
||
if (typeof res === 'string') {
|
||
const text = res.replace(/^\uFEFF/, '').trim()
|
||
if (!text) return {}
|
||
try {
|
||
const parsed = JSON.parse(text)
|
||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||
? (parsed as Record<string, any>)
|
||
: {}
|
||
} catch {
|
||
const start = text.indexOf('{')
|
||
const end = text.lastIndexOf('}')
|
||
if (start >= 0 && end > start) {
|
||
try {
|
||
const parsed = JSON.parse(text.slice(start, end + 1))
|
||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||
? (parsed as Record<string, any>)
|
||
: {}
|
||
} catch {
|
||
return {}
|
||
}
|
||
}
|
||
return {}
|
||
}
|
||
}
|
||
if (typeof res === 'object' && !Array.isArray(res)) return res as Record<string, any>
|
||
return {}
|
||
}
|
||
|
||
/** 从 upload/url 业务体提取票据(兼容整包 / 已解包 / JSON 字符串) */
|
||
export function parseUploadUrlResult(res: any): UploadTicket {
|
||
const root = coerceResponseBody(res)
|
||
// 优先整包 { code, data: { upload_id, upload_url } }
|
||
const fromEnvelope =
|
||
root.data && typeof root.data === 'object' && !Array.isArray(root.data)
|
||
? (root.data as Record<string, any>)
|
||
: null
|
||
const data =
|
||
fromEnvelope && (fromEnvelope.upload_id || fromEnvelope.uploadId || fromEnvelope.upload_url)
|
||
? fromEnvelope
|
||
: root
|
||
return {
|
||
upload_id: textOf(data.upload_id || data.uploadId),
|
||
upload_url: normalizeOssUrl(
|
||
textOf(data.upload_url || data.uploadUrl || data.put_url || data.putUrl),
|
||
),
|
||
file_url: normalizeOssUrl(textOf(data.file_url || data.fileUrl)),
|
||
content_type: textOf(data.content_type || data.contentType) || 'image/jpeg',
|
||
expire_at: asCount(data.expire_at || data.expireAt),
|
||
}
|
||
}
|
||
|
||
function normalizeImageGenStatus(data: unknown, taskId: string): ImageGenStatus {
|
||
const record = asRecord(data) || {}
|
||
const images = listOf(record.images ?? record.image_list ?? record.results)
|
||
.map((item) => normalizeDetailImage(item))
|
||
.filter((item): item is SessionDetailImage => Boolean(item))
|
||
.map((item) => ({
|
||
image_id: item.image_id,
|
||
path: item.path,
|
||
url: item.url,
|
||
index: item.index,
|
||
}))
|
||
if (!images.length) {
|
||
const url = imageUrlOf(record)
|
||
if (url) {
|
||
images.push({
|
||
image_id: textOf(record.image_id || record.id),
|
||
path: textOf(record.path),
|
||
url,
|
||
index: 0,
|
||
})
|
||
}
|
||
}
|
||
const statusText = textOf(record.status).toLowerCase()
|
||
const status: ImageGenStatusCode =
|
||
statusText === 'pending' ||
|
||
statusText === 'processing' ||
|
||
statusText === 'success' ||
|
||
statusText === 'failed'
|
||
? statusText
|
||
: images.length
|
||
? 'success'
|
||
: 'pending'
|
||
return {
|
||
task_id: textOf(record.task_id) || taskId,
|
||
status,
|
||
error: textOf(record.error),
|
||
style: textOf(record.style),
|
||
prompt: textOf(record.prompt),
|
||
images,
|
||
}
|
||
}
|
||
|
||
/** 兼容旧 parseImageGenStatus 调用 */
|
||
export function parseImageGenStatus(res: any, taskId = '') {
|
||
const normalized = normalizeImageGenStatus(res, taskId)
|
||
const first = normalized.images[0]
|
||
return {
|
||
taskId: normalized.task_id,
|
||
status: normalized.status,
|
||
done: normalized.status === 'success' || Boolean(first?.url),
|
||
failed: normalized.status === 'failed',
|
||
resultUrl: first?.url || '',
|
||
imageId: first?.image_id || '',
|
||
images: normalized.images,
|
||
error: normalized.error,
|
||
raw: res,
|
||
}
|
||
}
|
||
|
||
function normalizeEffectSubmit(data: unknown): EffectSubmitResult {
|
||
const record = asRecord(data) || {}
|
||
const images = listOf(record.images)
|
||
.map((item) => normalizeDetailImage(item))
|
||
.filter((item): item is SessionDetailImage => Boolean(item))
|
||
.map((item) => ({
|
||
image_id: item.image_id,
|
||
path: item.path,
|
||
url: item.url,
|
||
index: item.index,
|
||
}))
|
||
const statusText = textOf(record.status)
|
||
const status: ImageGenStatusCode =
|
||
statusText === 'pending' ||
|
||
statusText === 'processing' ||
|
||
statusText === 'success' ||
|
||
statusText === 'failed'
|
||
? statusText
|
||
: 'success'
|
||
return {
|
||
task_id: textOf(record.task_id),
|
||
status,
|
||
error: textOf(record.error),
|
||
image_id: textOf(record.image_id) || images[0]?.image_id || '',
|
||
url:
|
||
normalizeOssUrl(textOf(record.url || record.file_url || record.effect_url)) ||
|
||
images[0]?.url ||
|
||
'',
|
||
images,
|
||
}
|
||
}
|
||
|
||
// ---------- 接口(参数对齐 agent-api,路径用 /jd) ----------
|
||
|
||
/** GET /jd/session/detail?session_id= */
|
||
export const getSessionDetail = (params: { session_id: string } | Record<string, any>) => {
|
||
const sessionId =
|
||
typeof params === 'object' && params
|
||
? textOf((params as any).session_id).trim()
|
||
: ''
|
||
return request(`${PREFIX}/session/detail`, {
|
||
method: 'GET',
|
||
params: {
|
||
...params,
|
||
session_id: sessionId || textOf((params as any)?.session_id),
|
||
},
|
||
}).then((res) => normalizeSessionDetail(res))
|
||
}
|
||
|
||
/** GET /jd/session/images */
|
||
export const getSessionImages = (query: {
|
||
shop_id?: number
|
||
session_id?: string
|
||
order?: 'asc' | 'desc'
|
||
page?: number
|
||
size?: number
|
||
} = {}) => {
|
||
const params: Record<string, any> = {
|
||
page: query.page && query.page > 0 ? query.page : 1,
|
||
size: Math.min(100, Math.max(1, query.size || 20)),
|
||
}
|
||
if (query.shop_id) params.shop_id = query.shop_id
|
||
if (query.session_id?.trim()) params.session_id = query.session_id.trim()
|
||
if (query.order === 'asc' || query.order === 'desc') params.order = query.order
|
||
return request(`${PREFIX}/session/images`, {
|
||
method: 'GET',
|
||
params,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* POST /jd/upload/url
|
||
* 使用独立 axios(不走全局 response 拦截器),直接读 response.data
|
||
*/
|
||
export const getUploadUrl = async (input: {
|
||
session_id: string
|
||
filename?: string
|
||
content_type?: string
|
||
purpose?: UploadPurpose
|
||
}): Promise<UploadTicket> => {
|
||
const response = await axios.post(
|
||
buildApiUrl(`${PREFIX}/upload/url`),
|
||
{
|
||
session_id: input.session_id,
|
||
filename: input.filename || '',
|
||
content_type: input.content_type || 'image/jpeg',
|
||
purpose: input.purpose || 'origin',
|
||
},
|
||
{
|
||
headers: buildAuthHeaders(),
|
||
timeout: 30000,
|
||
},
|
||
)
|
||
|
||
// 注意:若 Content-Type 不是 application/json,axios 会把 body 留成 string
|
||
const ticket = parseUploadUrlResult(response?.data)
|
||
|
||
if (!ticket.upload_id) {
|
||
throw new Error('签发上传地址失败:缺少 upload_id')
|
||
}
|
||
if (!ticket.upload_url) {
|
||
throw new Error('签发上传地址失败:缺少 upload_url')
|
||
}
|
||
return ticket
|
||
}
|
||
|
||
/** PUT OSS */
|
||
export async function putUpload(uploadUrl: string, file: Blob, contentType: string) {
|
||
const response = await fetch(uploadUrl, {
|
||
method: 'PUT',
|
||
headers: contentType ? { 'Content-Type': contentType } : undefined,
|
||
body: file,
|
||
})
|
||
if (!response.ok) throw new Error('图片上传失败')
|
||
}
|
||
|
||
/** POST /jd/upload/confirm */
|
||
export const confirmUpload = (input: { session_id: string; upload_id: string }) => {
|
||
return request(`${PREFIX}/upload/confirm`, {
|
||
method: 'POST',
|
||
data: {
|
||
session_id: input.session_id,
|
||
upload_id: input.upload_id,
|
||
},
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 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 */
|
||
export const submitImageGen = (input: ImageGenSubmitInput) => {
|
||
const data: Record<string, string> = {
|
||
session_id: input.session_id,
|
||
prompt: input.prompt || '',
|
||
scene_prop: input.scene_prop || '',
|
||
custom_text: input.custom_text || '',
|
||
mode: input.mode || 'generate',
|
||
}
|
||
const uploadId = String(input.upload_id || '').trim()
|
||
const imageId = String(input.image_id || '').trim()
|
||
// 导入生图:upload_id;编辑效果图:image_id(互斥)
|
||
if (uploadId) data.upload_id = uploadId
|
||
if (imageId) data.image_id = imageId
|
||
return request(`${PREFIX}/imagegen/submit`, {
|
||
method: 'POST',
|
||
data,
|
||
}) as Promise<ImageGenTask>
|
||
}
|
||
|
||
/** POST /jd/effect/submit */
|
||
export const submitExternalEffect = (input: EffectSubmitInput) => {
|
||
return request(`${PREFIX}/effect/submit`, {
|
||
method: 'POST',
|
||
data: {
|
||
session_id: input.session_id,
|
||
upload_id: input.upload_id,
|
||
product_id: input.product_id,
|
||
product_name: input.product_name || '',
|
||
user_message: input.user_message || '',
|
||
mounts_id: input.mounts_id ?? 0,
|
||
scene_prop: input.scene_prop || '',
|
||
custom_text: input.custom_text || '',
|
||
base_type: input.base_type ?? 0,
|
||
frame_type: input.frame_type ?? 0,
|
||
mount_id: input.mount_id ?? 0,
|
||
pid: input.pid ?? 0,
|
||
proportion_id: input.proportion_id ?? 0,
|
||
pose_id: input.pose_id ?? 0,
|
||
style_id: input.style_id ?? 0,
|
||
prompt: input.prompt || '',
|
||
},
|
||
}).then(normalizeEffectSubmit)
|
||
}
|
||
|
||
/** GET /jd/imagegen/status?task_id= */
|
||
export const getImageGenStatus = (taskId: string) => {
|
||
return request(`${PREFIX}/imagegen/status`, {
|
||
method: 'GET',
|
||
params: { task_id: taskId },
|
||
}).then((data) => normalizeImageGenStatus(data, taskId))
|
||
}
|
||
|
||
/** POST /jd/order/confirm */
|
||
export const confirmOrder = (input: OrderConfirmInput) => {
|
||
const shapeId = input.shape_id ?? 0
|
||
return request(`${PREFIX}/order/confirm`, {
|
||
method: 'POST',
|
||
data: {
|
||
...input,
|
||
top_prod_id: input.top_prod_id ?? shapeId ?? 0,
|
||
shape_id: shapeId,
|
||
font_id: input.font_id ?? 0,
|
||
custom_text: input.custom_text || '',
|
||
back_text: input.back_text || '',
|
||
up_text: input.up_text || '',
|
||
down_text: input.down_text || '',
|
||
check_id: input.check_id ?? 0,
|
||
country_id: input.country_id ?? 45,
|
||
province_id: input.province_id || '',
|
||
city_id: input.city_id || '',
|
||
county_id: input.county_id || '',
|
||
},
|
||
}) as Promise<OrderConfirmResult>
|
||
}
|
||
|
||
/**
|
||
* 导入阶段:签发 URL → PUT OSS(不 confirm)
|
||
* 拿到 upload_id 后可立刻调 subject/classify
|
||
*/
|
||
export async function prepareUpload(
|
||
file: File | Blob,
|
||
options: {
|
||
session_id: string
|
||
filename?: string
|
||
purpose?: UploadPurpose
|
||
},
|
||
) {
|
||
const name = options.filename || guessFilename(file as File)
|
||
const contentType = guessContentType(file, name)
|
||
const ticket = await getUploadUrl({
|
||
session_id: options.session_id,
|
||
filename: name,
|
||
content_type: contentType,
|
||
purpose: options.purpose || 'origin',
|
||
})
|
||
await putUpload(ticket.upload_url, file, ticket.content_type || contentType)
|
||
return {
|
||
uploadId: ticket.upload_id,
|
||
fileUrl: ticket.file_url,
|
||
contentType: ticket.content_type || contentType,
|
||
filename: name,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 完整上传:签发 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
|
||
}
|
||
|