/** * TradeMate API Client * Communicates with TradeMate backend from the browser extension. * All functions are async, return parsed JSON or throw on error. * Errors from 402/403 carry structured data for upgrade prompts. */ const STORAGE_KEYS = { API_URL: 'trademate_api_url', TOKEN: 'trademate_token', }; export class ApiError extends Error { constructor(message, status, data = {}) { super(message); this.name = 'ApiError'; this.status = status; this.data = data; // parsed response body (may contain credits_remaining) this.isCreditError = status === 402; } } async function getConfig() { const { apiUrl, token } = await chrome.storage.local.get([STORAGE_KEYS.API_URL, STORAGE_KEYS.TOKEN]); if (!apiUrl) throw new ApiError('请先设置 API 地址', 0); if (!token) throw new ApiError('请先设置 API Token', 0); return { apiUrl: apiUrl.replace(/\/+$/, ''), token }; } async function request(path, options = {}) { const { apiUrl, token } = await getConfig(); const url = `${apiUrl}${path}`; const res = await fetch(url, { method: options.method || 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, ...options.headers, }, body: options.body ? JSON.stringify(options.body) : undefined, }); if (!res.ok) { let detail = `HTTP ${res.status}`; let body = {}; try { body = await res.json(); detail = body.detail || detail; } catch {} throw new ApiError(detail, res.status, body); } return res.json(); } /** Login: get JWT token from username + password */ export async function login(username, password, apiUrl) { const url = `${apiUrl.replace(/\/+$/, '')}/api/v1/auth/login`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); if (!res.ok) { let detail = '登录失败'; try { const e = await res.json(); detail = e.detail || detail; } catch {} throw new ApiError(detail, res.status); } const data = await res.json(); return data.access_token; } /** Get current credit balance */ export async function getBalance() { const data = await request('/api/v1/credits/balance', { method: 'GET' }); return { balance: data.balance ?? 0, totalPurchased: data.total_purchased ?? 0, totalUsed: data.total_used ?? 0, subscription: data.subscription ?? null, freeTrialUsed: data.free_trial_used ?? false, dailyFreeTranslateCharsLeft: data.daily_free_translate_chars_left ?? 0, rates: data.rates ?? {}, }; } /** Get available subscription plans */ export async function getSubscriptionPlans() { return request('/api/v1/credits/subscription-plans', { method: 'GET' }); } /** Get available credit packages */ export async function getCreditPackages() { return request('/api/v1/credits/packages', { method: 'GET' }); } /** Translate text */ export async function translate(text, targetLang = 'zh', sourceLang = 'auto') { return request('/api/v1/translate', { body: { text, target_lang: targetLang, source_lang: sourceLang, context: 'trade' }, }); } /** Generate reply suggestions */ export async function generateReply(inquiry, tone = 'professional', count = 3) { return request('/api/v1/translate/reply', { body: { inquiry, tone, count }, }); } /** Search for potential customers */ export async function searchLeads(productDescription, targetMarket = 'US') { return request('/api/v1/discovery/search', { body: { product_description: productDescription, target_market: targetMarket }, }); } /** Generate marketing copy */ export async function generateMarketing(productName, description, style = 'professional', count = 3) { return request('/api/v1/marketing/generate', { body: { product_name: productName, description, style, count, target: 'US importers', language: 'en', }, }); } /** Generate keywords */ export async function generateKeywords(productName, description, count = 10) { return request('/api/v1/marketing/keywords', { body: { product_name: productName, description, count, language: 'en' }, }); }