/** * 后端 HTTP 客户端 —— 仅 background 使用(有 host_permissions,不受 CORS 约束)。 * 契约对齐 server 端 /api/materials(见 docs/v2/api.md)。 */ import type { ScanResult } from '../collector/scan'; import type { TextEdits } from '../export/builder'; export interface MaterialsPayload { product_id: string | null; source: { platform: string; itemId: string | null; url: string; collectedAt: number; }; texts: Array<{ kind: string; content: string; pairs?: Array<{ key: string; value: string }> | null }>; images: Array<{ groupKey: string; groupName: string; variantName?: string | null; url: string; index: number; type: string; dedupeKey?: string | null; }>; refererOrigin?: string; } /** 用(可能已二次修改的)文本 + 已勾选图片,组装 /api/materials 请求体 */ export function buildMaterialsPayload( result: ScanResult, selectedKeys: Set, edits?: TextEdits, ): MaterialsPayload { const text = (kind: string) => result.texts.find((t) => t.kind === kind)?.content ?? ''; const title = edits?.title ?? text('title'); const price = edits?.price ?? text('price'); const brand = edits?.brand ?? text('brand'); const desc = edits?.desc ?? text('desc'); const sellingPoints = edits?.sellingPoints ?? text('selling_point'); const params: Array<{ key: string; value: string }> = [ ...(edits?.params ?? result.texts.find((t) => t.kind === 'params')?.pairs ?? []), ]; // 包装重量 / 包装尺寸合并进参数(后端存到 raw.params,studio 里再映射为 Ozon 字段) if (edits?.weight) params.push({ key: '包装重量', value: edits.weight }); const dimSuffix = edits?.dimsUnit === 'mm' ? ' mm' : ' cm'; if (edits?.dims?.l) params.push({ key: '包装长度', value: `${edits.dims.l}${dimSuffix}` }); if (edits?.dims?.w) params.push({ key: '包装宽度', value: `${edits.dims.w}${dimSuffix}` }); if (edits?.dims?.h) params.push({ key: '包装高度', value: `${edits.dims.h}${dimSuffix}` }); const texts: MaterialsPayload['texts'] = []; if (title) texts.push({ kind: 'title', content: title }); if (price) texts.push({ kind: 'price', content: price }); if (brand) texts.push({ kind: 'brand', content: brand }); if (params.length) texts.push({ kind: 'params', content: '', pairs: params }); if (sellingPoints) texts.push({ kind: 'selling_point', content: sellingPoints }); if (desc) texts.push({ kind: 'desc', content: desc }); const images = result.images .filter((img) => selectedKeys.has(img.key)) .map((img) => ({ groupKey: img.groupKey, groupName: img.groupName, variantName: img.variantName ?? null, url: img.url, index: img.index, type: img.type, dedupeKey: img.url, })); return { product_id: null, source: { platform: result.platform, itemId: result.itemId, url: result.url, collectedAt: result.scannedAt, }, texts, images, refererOrigin: 'https://www.ozon.ru', }; } export async function uploadMaterials( baseUrl: string, token: string, payload: MaterialsPayload, ): Promise<{ product_id: string; stage: string; assets_queued: number }> { const headers: Record = { 'Content-Type': 'application/json' }; if (token) headers.Authorization = `Bearer ${token}`; // 单用户宽松模式:token 可空 const res = await fetch(`${baseUrl.replace(/\/$/, '')}/api/materials`, { method: 'POST', headers, body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data?.detail || `上传失败 HTTP ${res.status}`); } return data; }