1 line
149 KiB
JSON
1 line
149 KiB
JSON
{"repo": "ashariftikhar/eccommerce-", "files": {".env.example": {"d439a71dcab8baa14cb9d50d019b3cae815822d7": {"t": 1785745161.0194361, "n": 752, "h": "51ceca73bb98ec10", "c": "NODE_ENV=development\nPORT=3000\nAPP_BASE_URL=http://localhost:3000\nSESSION_SECRET=replace-me\n\n# Runtime / deployment\nSTORAGE_DRIVER=file\nDATABASE_URL=\nDATA_FILE_PATH=./storage/app-state.json\nWORKER_POLL_MS=15000\nALERT_EMAIL=\n\n# eBay\nEBAY_ENV=sandbox\nEBAY_CLIENT_ID=\nEBAY_CLIENT_SECRET=\nEBAY_REDIRECT_URI=\nEBAY_SANDBOX_CLIENT_ID=\nEBAY_SANDBOX_CLIENT_SECRET=\nEBAY_SANDBOX_REDIRECT_URI=\nEBAY_POLICY_MARKETPLACE=EBAY_US\n\n# CJdropshipping\nCJ_API_BASE_URL=https://developers.cjdropshipping.com/api2.0/v1\nCJ_API_KEY=\nCJ_ACCESS_TOKEN=\nCJ_REQUESTS_PER_SECOND=0.5\nCJ_SCAN_LIMIT=500\n\n# AI\nDEEPSEEK_API_KEY=\nDEEPSEEK_MODEL=deepseek-chat\nGEMINI_API_KEY=\nGEMINI_MODEL=gemini-2.5-flash\nAI_MONTHLY_CAP_USD=20\nAI_DAILY_CAP_USD=0.65\n\n# Optional observability\nSENTRY_DSN=\n"}, "c0f26405ad69e38744748a9457aedca36623a7ac": {"t": 1785748312.7399507, "n": 752, "h": "51ceca73bb98ec10", "c": "NODE_ENV=development\nPORT=3000\nAPP_BASE_URL=http://localhost:3000\nSESSION_SECRET=replace-me\n\n# Runtime / deployment\nSTORAGE_DRIVER=file\nDATABASE_URL=\nDATA_FILE_PATH=./storage/app-state.json\nWORKER_POLL_MS=15000\nALERT_EMAIL=\n\n# eBay\nEBAY_ENV=sandbox\nEBAY_CLIENT_ID=\nEBAY_CLIENT_SECRET=\nEBAY_REDIRECT_URI=\nEBAY_SANDBOX_CLIENT_ID=\nEBAY_SANDBOX_CLIENT_SECRET=\nEBAY_SANDBOX_REDIRECT_URI=\nEBAY_POLICY_MARKETPLACE=EBAY_US\n\n# CJdropshipping\nCJ_API_BASE_URL=https://developers.cjdropshipping.com/api2.0/v1\nCJ_API_KEY=\nCJ_ACCESS_TOKEN=\nCJ_REQUESTS_PER_SECOND=0.5\nCJ_SCAN_LIMIT=500\n\n# AI\nDEEPSEEK_API_KEY=\nDEEPSEEK_MODEL=deepseek-chat\nGEMINI_API_KEY=\nGEMINI_MODEL=gemini-2.5-flash\nAI_MONTHLY_CAP_USD=20\nAI_DAILY_CAP_USD=0.65\n\n# Optional observability\nSENTRY_DSN=\n"}}, "src/lib/providers.ts": {"d439a71dcab8baa14cb9d50d019b3cae815822d7": {"t": 1785746039.9347274, "n": 27527, "h": "66e058f3eab6b165", "c": "import {\n AIArtifactCacheEntry,\n AppState,\n CJProduct,\n ConversationMessage,\n ListingPack,\n MarketSearchSnapshot,\n ProductCandidate,\n ProviderName,\n RuntimeConfig,\n} from '../types';\nimport { clamp, createId, daysFromNow, median, normalizeKeyword, nowIso, round, shortHash } from './utils';\n\nfunction deterministicNumber(seed: string, min: number, max: number): number {\n const total = [...seed].reduce((sum, char) => sum + char.charCodeAt(0), 0);\n const span = max - min;\n return min + (total % Math.max(1, span + 1));\n}\n\nfunction syntheticCatalog(limit: number): CJProduct[] {\n const seeds = [\n 'under desk cable organizer',\n 'mini heatless curl band',\n 'stainless soap dispenser',\n 'magnetic spice rack',\n 'drawer compartment bins',\n 'portable pill organizer',\n 'shoe crease protector',\n 'luggage cup holder',\n 'sink splash guard',\n 'car seat gap filler',\n 'pet grooming glove',\n 'travel compression bags',\n 'vegetable chopper slicer',\n 'reusable lint remover',\n 'cordless milk frother',\n ];\n\n return seeds.slice(0, limit).map((seed, index) => {\n const landedCost = round(3.5 + index * 0.35, 2);\n const shippingCost = round(1 + (index % 3) * 0.45, 2);\n return {\n id: `cj_${index + 1}`,\n variantId: `cjv_${index + 1}`,\n title: seed.replace(/\\b\\w/g, (match) => match.toUpperCase()),\n normalizedKeyword: normalizeKeyword(seed),\n categoryPath: index % 4 === 0 ? 'Home & Garden > Storage & Organization' : index % 4 === 1 ? 'Health & Beauty > Personal Care' : index % 4 === 2 ? 'Travel > Accessories' : 'Pet Supplies > Grooming',\n warehouseCountry: 'US',\n landedCost: round(landedCost + shippingCost, 2),\n shippingCost,\n stock: 30 + index * 8,\n estimatedDeliveryBusinessDays: 2 + (index % 4),\n supplierName: `CJ Supplier ${index + 1}`,\n supplierAgeDays: 160 + index * 18,\n shippingConsistency: 78 + (index % 5) * 4,\n stockStability: 76 + (index % 6) * 3,\n orderHistoryScore: 74 + (index % 4) * 5,\n imageUrl: `https://picsum.photos/seed/${encodeURIComponent(seed)}/640/640`,\n tags: index % 3 === 0 ? ['utility', 'simple', 'lightweight'] : index % 3 === 1 ? ['beauty', 'travel', 'giftable'] : ['home', 'problem-solving', 'storage'],\n };\n });\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport class CJClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n async fetchCatalog(limit: number): Promise<CJProduct[]> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return syntheticCatalog(Math.min(limit, 15));\n }\n\n const pageSize = Math.min(50, Math.max(10, limit));\n const maxPages = Math.max(1, Math.ceil(limit / pageSize));\n const records: Array<Record<string, unknown>> = [];\n const seen = new Set<string>();\n const delayMs = Math.max(0, Math.ceil(1000 / Math.max(0.1, this.config.cj.requestsPerSecond)));\n\n for (let pageNum = 1; pageNum <= maxPages && records.length < limit; pageNum += 1) {\n if (pageNum > 1 && delayMs > 0) {\n await delay(delayMs);\n }\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/list?pageNum=${pageNum}&pageSize=${pageSize}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n\n if (response.status === 429) {\n if (records.length > 0) {\n break;\n }\n throw new Error('CJ catalog request hit rate limit 429');\n }\n\n if (!response.ok) {\n throw new Error(`CJ catalog request failed with ${response.status}`);\n }\n\n const payload = (await response.json()) as {\n data?: Array<Record<string, unknown>> | { list?: Array<Record<string, unknown>> };\n };\n const pageRecords = Array.isArray(payload.data)\n ? payload.data\n : Array.isArray(payload.data?.list)\n ? payload.data.list\n : [];\n\n if (pageRecords.length === 0) {\n break;\n }\n\n for (const record of pageRecords) {\n const id = String(record.pid || record.productId || record.productSku || '');\n if (!id || seen.has(id)) {\n continue;\n }\n seen.add(id);\n records.push(record);\n if (records.length >= limit) {\n break;\n }\n }\n }\n\n return records.slice(0, limit).map((record, index) => ({\n id: String(record.pid || record.productId || `cj_${index}`),\n variantId: String(record.vid || record.variantId || record.productSku || record.pid || `cjv_${index}`),\n title: String(record.productName || record.title || `CJ Item ${index + 1}`),\n normalizedKeyword: normalizeKeyword(String(record.productName || record.title || `CJ Item ${index + 1}`)),\n categoryPath: String(record.categoryName || record.categoryPath || record.threeCategoryName || record.twoCategoryName || 'General'),\n warehouseCountry: inferWarehouseCountry(record),\n landedCost: Number(record.sellPrice || record.cost || 0),\n shippingCost: Number(record.shippingFee || 0),\n stock: inferStockLevel(record),\n estimatedDeliveryBusinessDays: inferDeliveryDays(record),\n supplierName: String(record.supplierName || 'CJ Supplier'),\n supplierAgeDays: Number(record.supplierAgeDays || 180),\n shippingConsistency: Number(record.shippingConsistency || 80),\n stockStability: Number(record.stockStability || 80),\n orderHistoryScore: Number(record.orderHistoryScore || 80),\n imageUrl: String(record.image || record.productImage || ''),\n tags: inferTags(record),\n }));\n }\n\n async getStock(productId: string, variantId: string): Promise<{ stock: number; etaBusinessDays: number }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return {\n stock: deterministicNumber(`${productId}:${variantId}`, 12, 80),\n etaBusinessDays: deterministicNumber(`${variantId}:${productId}`, 2, 5),\n };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/query?pid=${productId}&vid=${variantId}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n if (!response.ok) {\n throw new Error(`CJ stock request failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: Record<string, unknown> };\n return {\n stock: Number(payload.data?.sellableQuantity || payload.data?.stock || 0),\n etaBusinessDays: Number(payload.data?.deliveryDays || payload.data?.deliveryTime || 7),\n };\n }\n\n async placeOrder(params: { idempotencyKey: string; orderId: string; productId: string; variantId: string; quantity: number }): Promise<{ cjOrderId: string }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return { cjOrderId: `SIM-CJ-${shortHash(params.idempotencyKey)}` };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/order/create`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n body: JSON.stringify(params),\n });\n if (!response.ok) {\n throw new Error(`CJ order placement failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: { orderId?: string } };\n return { cjOrderId: payload.data?.orderId || `CJ-${shortHash(params.idempotencyKey)}` };\n }\n}\n\nfunction inferWarehouseCountry(record: Record<string, unknown>): string {\n const direct = String(record.warehouseCountry || record.countryCode || '').trim();\n if (direct) {\n return direct.toUpperCase();\n }\n\n const rawCodes = record.shippingCountryCodes;\n const codes = Array.isArray(rawCodes)\n ? rawCodes.map((value) => String(value).toUpperCase())\n : typeof rawCodes === 'string'\n ? rawCodes.split(/[,\\s]+/).map((value) => value.trim().toUpperCase()).filter(Boolean)\n : [];\n\n if (codes.includes('US')) {\n return 'US';\n }\n return codes[0] || 'US';\n}\n\nfunction inferStockLevel(record: Record<string, unknown>): number {\n const explicit = Number(record.sellableQuantity || record.stock || 0);\n if (Number.isFinite(explicit) && explicit > 0) {\n return explicit;\n }\n\n const listed = Number(record.listedNum || record.listingCount || 0);\n if (Number.isFinite(listed) && listed > 0) {\n return Math.max(24, listed);\n }\n\n return 24;\n}\n\nfunction inferDeliveryDays(record: Record<string, unknown>): number {\n const explicit = Number(record.deliveryDays || record.deliveryTime || 0);\n if (Number.isFinite(explicit) && explicit > 0) {\n return explicit;\n }\n\n return inferWarehouseCountry(record) === 'US' ? 5 : 8;\n}\n\nfunction inferTags(record: Record<string, unknown>): string[] {\n if (Array.isArray(record.tags)) {\n return record.tags.map(String);\n }\n\n const tags = new Set<string>();\n const add = (value: unknown) => {\n const text = String(value || '').trim().toLowerCase();\n if (text) {\n tags.add(text);\n }\n };\n\n add(record.sourceFrom);\n add(record.oneCategoryName);\n add(record.twoCategoryName);\n add(record.threeCategoryName);\n add(record.productType);\n add(record.isFreeShipping ? 'free-shipping' : '');\n\n return Array.from(tags);\n}\n\nexport class EbayClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private get appId(): string | null {\n return this.config.ebay.env === 'sandbox' ? this.config.ebay.sandboxClientId || this.config.ebay.clientId : this.config.ebay.clientId;\n }\n\n async getMarketSnapshot(keyword: string): Promise<MarketSearchSnapshot> {\n if (!this.appId) {\n const sold30d = deterministicNumber(keyword, 18, 120);\n const sold7d = deterministicNumber(`7:${keyword}`, 4, 28);\n const uniqueSellers30d = deterministicNumber(`sellers:${keyword}`, 4, 22);\n const activeCount = deterministicNumber(`active:${keyword}`, 15, 180);\n const topRatedCount = deterministicNumber(`top:${keyword}`, 2, Math.max(2, Math.floor(activeCount / 2)));\n return {\n sold30d,\n sold7d,\n uniqueSellers30d,\n medianSoldPrice30d: round(deterministicNumber(`price:${keyword}`, 14, 26) + 0.99, 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n const findingBase =\n this.config.ebay.env === 'sandbox'\n ? 'https://svcs.sandbox.ebay.com/services/search/FindingService/v1'\n : 'https://svcs.ebay.com/services/search/FindingService/v1';\n const headers = {\n 'X-EBAY-SOA-SECURITY-APPNAME': this.appId,\n 'X-EBAY-SOA-RESPONSE-DATA-FORMAT': 'JSON',\n 'X-EBAY-SOA-GLOBAL-ID': 'EBAY-US',\n };\n\n const completedUrl = new URL(findingBase);\n completedUrl.searchParams.set('OPERATION-NAME', 'findCompletedItems');\n completedUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n completedUrl.searchParams.set('keywords', keyword);\n completedUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n completedUrl.searchParams.set('itemFilter(0).name', 'SoldItemsOnly');\n completedUrl.searchParams.set('itemFilter(0).value', 'true');\n completedUrl.searchParams.set('itemFilter(1).name', 'ListingType');\n completedUrl.searchParams.set('itemFilter(1).value(0)', 'FixedPrice');\n completedUrl.searchParams.set('itemFilter(2).name', 'LocatedIn');\n completedUrl.searchParams.set('itemFilter(2).value', 'US');\n completedUrl.searchParams.set('itemFilter(3).name', 'Condition');\n completedUrl.searchParams.set('itemFilter(3).value', '1000');\n\n const activeUrl = new URL(findingBase);\n activeUrl.searchParams.set('OPERATION-NAME', 'findItemsAdvanced');\n activeUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n activeUrl.searchParams.set('keywords', keyword);\n activeUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n activeUrl.searchParams.set('itemFilter(0).name', 'ListingType');\n activeUrl.searchParams.set('itemFilter(0).value(0)', 'FixedPrice');\n activeUrl.searchParams.set('itemFilter(1).name', 'LocatedIn');\n activeUrl.searchParams.set('itemFilter(1).value', 'US');\n\n const [completedResponse, activeResponse] = await Promise.all([fetch(completedUrl, { headers }), fetch(activeUrl, { headers })]);\n if (!completedResponse.ok || !activeResponse.ok) {\n throw new Error(`eBay finding request failed (${completedResponse.status}/${activeResponse.status})`);\n }\n\n const completedPayload = (await completedResponse.json()) as Record<string, unknown>;\n const activePayload = (await activeResponse.json()) as Record<string, unknown>;\n\n const completedItems = extractFindingItems(completedPayload);\n const activeItems = extractFindingItems(activePayload);\n const soldPrices = completedItems.map((item) => item.price).filter((value) => Number.isFinite(value));\n const uniqueSellers = new Set(completedItems.map((item) => item.seller).filter(Boolean));\n const sold30d = completedItems.length;\n const sold7d = completedItems.filter((item) => Date.now() - item.endTime.getTime() <= 7 * 24 * 60 * 60 * 1000).length;\n const topRatedCount = activeItems.filter((item) => item.topRated).length;\n const activeCount = activeItems.length;\n\n return {\n sold30d,\n sold7d,\n uniqueSellers30d: uniqueSellers.size,\n medianSoldPrice30d: round(median(soldPrices), 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n async validateCategoryPolicies(candidate: ProductCandidate): Promise<{ valid: boolean; warnings: string[]; categoryId: string }> {\n const warnings: string[] = [];\n const categoryId = mapCategoryId(candidate.categoryPath);\n\n if (!this.appId) {\n if (candidate.tags.some((tag) => tag.toLowerCase().includes('battery'))) {\n warnings.push('Battery-adjacent item requires manual category review.');\n }\n return { valid: warnings.length === 0, warnings, categoryId };\n }\n\n const base = this.config.ebay.env === 'sandbox' ? 'https://apim.sandbox.ebay.com' : 'https://apim.ebay.com';\n const response = await fetch(`${base}/sell/metadata/v1/marketplace/EBAY_US/get_category_policies?category_id=${categoryId}`);\n if (!response.ok) {\n warnings.push(`Metadata policy lookup returned ${response.status}; using simulated policy fallback until seller OAuth is connected.`);\n return { valid: true, warnings, categoryId };\n }\n const payload = (await response.json()) as Record<string, unknown>;\n const allowed = Boolean(payload.categoryId || payload.categoryPolicies);\n if (!allowed) {\n warnings.push('Category policies could not be validated, so the draft is marked with a warning.');\n }\n return { valid: true, warnings, categoryId };\n }\n\n async createOrReplaceInventoryItem(draft: { sellerSku: string; title: string; description: string; images: string[]; quantity: number }): Promise<{ inventoryItemId: string }> {\n return { inventoryItemId: `INV-${draft.sellerSku}` };\n }\n\n async publishOffer(draft: { sellerSku: string; price: number; categoryId: string }): Promise<{ offerId: string; publishedAt: string }> {\n return {\n offerId: `OFFER-${shortHash(`${draft.sellerSku}:${draft.price}:${draft.categoryId}`)}`,\n publishedAt: nowIso(),\n };\n }\n\n async updateInventoryQuantity(sellerSku: string, quantity: number): Promise<void> {\n void sellerSku;\n void quantity;\n }\n\n async withdrawOffer(offerId: string): Promise<void> {\n void offerId;\n }\n}\n\nfunction extractFindingItems(payload: Record<string, unknown>): Array<{ price: number; seller: string; endTime: Date; topRated: boolean }> {\n const root = Object.values(payload)[0] as Array<Record<string, unknown>> | undefined;\n const payloadRoot = root?.[0] || {};\n const searchResult = (payloadRoot.searchResult as Array<Record<string, unknown>> | undefined)?.[0];\n const items = (searchResult?.item as Array<Record<string, unknown>> | undefined) || [];\n return items.map((item) => {\n const sellingStatus = (item.sellingStatus as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const currentPrice = (sellingStatus.currentPrice as Array<Record<string, unknown>> | undefined)?.[0];\n const sellerInfo = (item.sellerInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const listingInfo = (item.listingInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n return {\n price: Number(currentPrice?.__value__ || currentPrice?.value || 0),\n seller: String(sellerInfo.sellerUserName?.[0] || ''),\n endTime: new Date(String(listingInfo.endTime?.[0] || nowIso())),\n topRated: sellerInfo.topRatedSeller?.[0] === 'true',\n };\n });\n}\n\nfunction mapCategoryId(categoryPath: string): string {\n const lowered = categoryPath.toLowerCase();\n if (lowered.includes('storage')) {\n return '20635';\n }\n if (lowered.includes('travel')) {\n return '300';\n }\n if (lowered.includes('pet')) {\n return '177';\n }\n return '11700';\n}\n\nexport class AIClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private providerDetails(provider: ProviderName): { provider: ProviderName; modelName: string | null } {\n if (provider === 'deepseek') {\n return { provider, modelName: this.config.ai.deepseekModel };\n }\n if (provider === 'gemini') {\n return { provider, modelName: this.config.ai.geminiModel };\n }\n return { provider: 'system', modelName: null };\n }\n\n canSpend(state: AppState): boolean {\n const budgets = state.budgets;\n const month = nowIso().slice(0, 7);\n const day = nowIso().slice(0, 10);\n if (budgets.currentMonth !== month) {\n budgets.currentMonth = month;\n budgets.monthToDateUsd = 0;\n budgets.aiFallbackMode = false;\n }\n if (budgets.currentDay !== day) {\n budgets.currentDay = day;\n budgets.dayToDateUsd = 0;\n }\n return budgets.monthToDateUsd < budgets.monthlyCapUsd && budgets.dayToDateUsd < budgets.dailyCapUsd && !budgets.aiFallbackMode;\n }\n\n private charge(state: AppState, usd: number): void {\n state.budgets.monthToDateUsd = round(state.budgets.monthToDateUsd + usd, 4);\n state.budgets.dayToDateUsd = round(state.budgets.dayToDateUsd + usd, 4);\n if (state.budgets.monthToDateUsd >= state.budgets.monthlyCapUsd || state.budgets.dayToDateUsd >= state.budgets.dailyCapUsd) {\n state.budgets.aiFallbackMode = true;\n }\n }\n\n findCache(state: AppState, productFingerprint: string, kind: AIArtifactCacheEntry['kind']): AIArtifactCacheEntry | null {\n const now = Date.now();\n return state.aiCache.find((entry) => entry.productFingerprint === productFingerprint && entry.kind === kind && new Date(entry.expiresAt).getTime() > now) || null;\n }\n\n async generateListingPack(\n state: AppState,\n productFingerprint: string,\n candidate: ProductCandidate,\n ): Promise<{ pack: ListingPack; provider: ProviderName; modelName: string | null; cacheHit: boolean; fallback: boolean }> {\n const cached = this.findCache(state, productFingerprint, 'listing_pack');\n if (cached) {\n return {\n pack: JSON.parse(cached.payload) as ListingPack,\n provider: cached.provider,\n modelName: this.providerDetails(cached.provider).modelName,\n cacheHit: true,\n fallback: cached.provider === 'system',\n };\n }\n\n let pack: ListingPack;\n let provider: 'deepseek' | 'gemini' | 'system' = 'system';\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n pack = await this.generateViaDeepSeek(candidate);\n provider = 'deepseek';\n this.charge(state, 0.03);\n } else if (this.canSpend(state) && this.config.ai.geminiApiKey) {\n pack = await this.generateViaGemini(candidate);\n provider = 'gemini';\n this.charge(state, 0.05);\n } else {\n pack = templateListingPack(candidate);\n }\n\n state.aiCache.unshift({\n id: createId('cache'),\n productFingerprint,\n kind: 'listing_pack',\n provider,\n payload: JSON.stringify(pack),\n expiresAt: daysFromNow(30),\n createdAt: nowIso(),\n });\n state.aiCache = state.aiCache.slice(0, 200);\n return {\n pack,\n provider,\n modelName: this.providerDetails(provider).modelName,\n cacheHit: false,\n fallback: provider === 'system',\n };\n }\n\n async generateCeoReply(\n state: AppState,\n prompt: string,\n ): Promise<{ message: ConversationMessage; fallback: boolean }> {\n let provider: ProviderName = 'system';\n let modelName: string | null = null;\n let content = '';\n\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n provider = 'deepseek';\n modelName = this.config.ai.deepseekModel;\n content = await this.generateShortReplyViaDeepSeek(prompt);\n if (content) {\n this.charge(state, 0.02);\n }\n }\n\n if (!content && this.canSpend(state) && this.config.ai.geminiApiKey) {\n provider = 'gemini';\n modelName = this.config.ai.geminiModel;\n content = await this.generateShortReplyViaGemini(prompt);\n if (content) {\n this.charge(state, 0.03);\n }\n }\n\n if (!content) {\n provider = 'system';\n modelName = null;\n content = `Manager summary: ${prompt}. Current mode remains visibility-first. Review the latest agent traces before enabling wider automation.`;\n }\n\n return {\n message: {\n id: createId('msg'),\n sender: 'ceo',\n direction: 'outbound',\n message: content,\n createdAt: nowIso(),\n provider,\n modelName,\n linkedExecutionId: null,\n },\n fallback: provider === 'system',\n };\n }\n\n private async generateViaDeepSeek(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n response_format: { type: 'json_object' },\n messages: [\n {\n role: 'system',\n content:\n 'You write concise eBay listing packs for US consumers. Return JSON with title, subtitle, description, bullets, itemSpecifics, images.',\n },\n {\n role: 'user',\n content: `Create a listing pack for ${candidate.title} in category ${candidate.categoryPath} with target price ${candidate.targetPrice}.`,\n },\n ],\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n const content = payload.choices?.[0]?.message?.content;\n if (!content) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(content) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateViaGemini(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [\n {\n text: `Return JSON with title, subtitle, description, bullets, itemSpecifics, images for a US eBay listing about ${candidate.title} in ${candidate.categoryPath}.`,\n },\n ],\n },\n ],\n generationConfig: { responseMimeType: 'application/json' },\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n const text = payload.candidates?.[0]?.content?.parts?.[0]?.text;\n if (!text) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(text) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateShortReplyViaDeepSeek(prompt: string): Promise<string> {\n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n messages: [\n {\n role: 'system',\n content: 'You are a concise ecommerce operations manager. Reply in 2-4 short sentences.',\n },\n {\n role: 'user',\n content: prompt,\n },\n ],\n }),\n });\n if (!response.ok) {\n return '';\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n return payload.choices?.[0]?.message?.content?.trim() || '';\n } catch {\n return '';\n }\n }\n\n private async generateShortReplyViaGemini(prompt: string): Promise<string> {\n try {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [{ text: `Reply in 2-4 short sentences as an ecommerce ops manager. Prompt: ${prompt}` }],\n },\n ],\n }),\n });\n if (!response.ok) {\n return '';\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n return payload.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || '';\n } catch {\n return '';\n }\n }\n}\n\nfunction templateListingPack(candidate: ProductCandidate): ListingPack {\n const headline = candidate.title.length > 72 ? candidate.title.slice(0, 69).trimEnd() + '...' : candidate.title;\n return {\n title: `${headline} | US Fast Ship`,\n subtitle: 'Smart buy, everyday value.',\n description: `${candidate.title} is a simple, useful product selected for YuziGoods because it offers practical value, clean presentation, and reliable US delivery. Ships fast from a US warehouse.`,\n bullets: [\n 'US warehouse fulfillment with fast delivery promise',\n 'Simple, useful everyday product with low-friction setup',\n 'Selected for margin, delivery speed, and low return risk',\n 'Packed for YuziGoods US-first marketplace workflow',\n ],\n itemSpecifics: {\n Brand: 'YuziGoods',\n Type: candidate.categoryPath.split('>').slice(-1)[0]?.trim() || 'General',\n Color: 'As pictured',\n Material: 'Mixed materials',\n Country: 'United States warehouse',\n },\n images: [candidate.imageUrl],\n };\n}\n"}, "32ad08423da07c3dbd41f5197dce8e22321a59a4": {"t": 1785748313.38903, "n": 27527, "h": "66e058f3eab6b165", "c": "import {\n AIArtifactCacheEntry,\n AppState,\n CJProduct,\n ConversationMessage,\n ListingPack,\n MarketSearchSnapshot,\n ProductCandidate,\n ProviderName,\n RuntimeConfig,\n} from '../types';\nimport { clamp, createId, daysFromNow, median, normalizeKeyword, nowIso, round, shortHash } from './utils';\n\nfunction deterministicNumber(seed: string, min: number, max: number): number {\n const total = [...seed].reduce((sum, char) => sum + char.charCodeAt(0), 0);\n const span = max - min;\n return min + (total % Math.max(1, span + 1));\n}\n\nfunction syntheticCatalog(limit: number): CJProduct[] {\n const seeds = [\n 'under desk cable organizer',\n 'mini heatless curl band',\n 'stainless soap dispenser',\n 'magnetic spice rack',\n 'drawer compartment bins',\n 'portable pill organizer',\n 'shoe crease protector',\n 'luggage cup holder',\n 'sink splash guard',\n 'car seat gap filler',\n 'pet grooming glove',\n 'travel compression bags',\n 'vegetable chopper slicer',\n 'reusable lint remover',\n 'cordless milk frother',\n ];\n\n return seeds.slice(0, limit).map((seed, index) => {\n const landedCost = round(3.5 + index * 0.35, 2);\n const shippingCost = round(1 + (index % 3) * 0.45, 2);\n return {\n id: `cj_${index + 1}`,\n variantId: `cjv_${index + 1}`,\n title: seed.replace(/\\b\\w/g, (match) => match.toUpperCase()),\n normalizedKeyword: normalizeKeyword(seed),\n categoryPath: index % 4 === 0 ? 'Home & Garden > Storage & Organization' : index % 4 === 1 ? 'Health & Beauty > Personal Care' : index % 4 === 2 ? 'Travel > Accessories' : 'Pet Supplies > Grooming',\n warehouseCountry: 'US',\n landedCost: round(landedCost + shippingCost, 2),\n shippingCost,\n stock: 30 + index * 8,\n estimatedDeliveryBusinessDays: 2 + (index % 4),\n supplierName: `CJ Supplier ${index + 1}`,\n supplierAgeDays: 160 + index * 18,\n shippingConsistency: 78 + (index % 5) * 4,\n stockStability: 76 + (index % 6) * 3,\n orderHistoryScore: 74 + (index % 4) * 5,\n imageUrl: `https://picsum.photos/seed/${encodeURIComponent(seed)}/640/640`,\n tags: index % 3 === 0 ? ['utility', 'simple', 'lightweight'] : index % 3 === 1 ? ['beauty', 'travel', 'giftable'] : ['home', 'problem-solving', 'storage'],\n };\n });\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport class CJClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n async fetchCatalog(limit: number): Promise<CJProduct[]> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return syntheticCatalog(Math.min(limit, 15));\n }\n\n const pageSize = Math.min(50, Math.max(10, limit));\n const maxPages = Math.max(1, Math.ceil(limit / pageSize));\n const records: Array<Record<string, unknown>> = [];\n const seen = new Set<string>();\n const delayMs = Math.max(0, Math.ceil(1000 / Math.max(0.1, this.config.cj.requestsPerSecond)));\n\n for (let pageNum = 1; pageNum <= maxPages && records.length < limit; pageNum += 1) {\n if (pageNum > 1 && delayMs > 0) {\n await delay(delayMs);\n }\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/list?pageNum=${pageNum}&pageSize=${pageSize}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n\n if (response.status === 429) {\n if (records.length > 0) {\n break;\n }\n throw new Error('CJ catalog request hit rate limit 429');\n }\n\n if (!response.ok) {\n throw new Error(`CJ catalog request failed with ${response.status}`);\n }\n\n const payload = (await response.json()) as {\n data?: Array<Record<string, unknown>> | { list?: Array<Record<string, unknown>> };\n };\n const pageRecords = Array.isArray(payload.data)\n ? payload.data\n : Array.isArray(payload.data?.list)\n ? payload.data.list\n : [];\n\n if (pageRecords.length === 0) {\n break;\n }\n\n for (const record of pageRecords) {\n const id = String(record.pid || record.productId || record.productSku || '');\n if (!id || seen.has(id)) {\n continue;\n }\n seen.add(id);\n records.push(record);\n if (records.length >= limit) {\n break;\n }\n }\n }\n\n return records.slice(0, limit).map((record, index) => ({\n id: String(record.pid || record.productId || `cj_${index}`),\n variantId: String(record.vid || record.variantId || record.productSku || record.pid || `cjv_${index}`),\n title: String(record.productName || record.title || `CJ Item ${index + 1}`),\n normalizedKeyword: normalizeKeyword(String(record.productName || record.title || `CJ Item ${index + 1}`)),\n categoryPath: String(record.categoryName || record.categoryPath || record.threeCategoryName || record.twoCategoryName || 'General'),\n warehouseCountry: inferWarehouseCountry(record),\n landedCost: Number(record.sellPrice || record.cost || 0),\n shippingCost: Number(record.shippingFee || 0),\n stock: inferStockLevel(record),\n estimatedDeliveryBusinessDays: inferDeliveryDays(record),\n supplierName: String(record.supplierName || 'CJ Supplier'),\n supplierAgeDays: Number(record.supplierAgeDays || 180),\n shippingConsistency: Number(record.shippingConsistency || 80),\n stockStability: Number(record.stockStability || 80),\n orderHistoryScore: Number(record.orderHistoryScore || 80),\n imageUrl: String(record.image || record.productImage || ''),\n tags: inferTags(record),\n }));\n }\n\n async getStock(productId: string, variantId: string): Promise<{ stock: number; etaBusinessDays: number }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return {\n stock: deterministicNumber(`${productId}:${variantId}`, 12, 80),\n etaBusinessDays: deterministicNumber(`${variantId}:${productId}`, 2, 5),\n };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/query?pid=${productId}&vid=${variantId}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n if (!response.ok) {\n throw new Error(`CJ stock request failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: Record<string, unknown> };\n return {\n stock: Number(payload.data?.sellableQuantity || payload.data?.stock || 0),\n etaBusinessDays: Number(payload.data?.deliveryDays || payload.data?.deliveryTime || 7),\n };\n }\n\n async placeOrder(params: { idempotencyKey: string; orderId: string; productId: string; variantId: string; quantity: number }): Promise<{ cjOrderId: string }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return { cjOrderId: `SIM-CJ-${shortHash(params.idempotencyKey)}` };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/order/create`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n body: JSON.stringify(params),\n });\n if (!response.ok) {\n throw new Error(`CJ order placement failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: { orderId?: string } };\n return { cjOrderId: payload.data?.orderId || `CJ-${shortHash(params.idempotencyKey)}` };\n }\n}\n\nfunction inferWarehouseCountry(record: Record<string, unknown>): string {\n const direct = String(record.warehouseCountry || record.countryCode || '').trim();\n if (direct) {\n return direct.toUpperCase();\n }\n\n const rawCodes = record.shippingCountryCodes;\n const codes = Array.isArray(rawCodes)\n ? rawCodes.map((value) => String(value).toUpperCase())\n : typeof rawCodes === 'string'\n ? rawCodes.split(/[,\\s]+/).map((value) => value.trim().toUpperCase()).filter(Boolean)\n : [];\n\n if (codes.includes('US')) {\n return 'US';\n }\n return codes[0] || 'US';\n}\n\nfunction inferStockLevel(record: Record<string, unknown>): number {\n const explicit = Number(record.sellableQuantity || record.stock || 0);\n if (Number.isFinite(explicit) && explicit > 0) {\n return explicit;\n }\n\n const listed = Number(record.listedNum || record.listingCount || 0);\n if (Number.isFinite(listed) && listed > 0) {\n return Math.max(24, listed);\n }\n\n return 24;\n}\n\nfunction inferDeliveryDays(record: Record<string, unknown>): number {\n const explicit = Number(record.deliveryDays || record.deliveryTime || 0);\n if (Number.isFinite(explicit) && explicit > 0) {\n return explicit;\n }\n\n return inferWarehouseCountry(record) === 'US' ? 5 : 8;\n}\n\nfunction inferTags(record: Record<string, unknown>): string[] {\n if (Array.isArray(record.tags)) {\n return record.tags.map(String);\n }\n\n const tags = new Set<string>();\n const add = (value: unknown) => {\n const text = String(value || '').trim().toLowerCase();\n if (text) {\n tags.add(text);\n }\n };\n\n add(record.sourceFrom);\n add(record.oneCategoryName);\n add(record.twoCategoryName);\n add(record.threeCategoryName);\n add(record.productType);\n add(record.isFreeShipping ? 'free-shipping' : '');\n\n return Array.from(tags);\n}\n\nexport class EbayClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private get appId(): string | null {\n return this.config.ebay.env === 'sandbox' ? this.config.ebay.sandboxClientId || this.config.ebay.clientId : this.config.ebay.clientId;\n }\n\n async getMarketSnapshot(keyword: string): Promise<MarketSearchSnapshot> {\n if (!this.appId) {\n const sold30d = deterministicNumber(keyword, 18, 120);\n const sold7d = deterministicNumber(`7:${keyword}`, 4, 28);\n const uniqueSellers30d = deterministicNumber(`sellers:${keyword}`, 4, 22);\n const activeCount = deterministicNumber(`active:${keyword}`, 15, 180);\n const topRatedCount = deterministicNumber(`top:${keyword}`, 2, Math.max(2, Math.floor(activeCount / 2)));\n return {\n sold30d,\n sold7d,\n uniqueSellers30d,\n medianSoldPrice30d: round(deterministicNumber(`price:${keyword}`, 14, 26) + 0.99, 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n const findingBase =\n this.config.ebay.env === 'sandbox'\n ? 'https://svcs.sandbox.ebay.com/services/search/FindingService/v1'\n : 'https://svcs.ebay.com/services/search/FindingService/v1';\n const headers = {\n 'X-EBAY-SOA-SECURITY-APPNAME': this.appId,\n 'X-EBAY-SOA-RESPONSE-DATA-FORMAT': 'JSON',\n 'X-EBAY-SOA-GLOBAL-ID': 'EBAY-US',\n };\n\n const completedUrl = new URL(findingBase);\n completedUrl.searchParams.set('OPERATION-NAME', 'findCompletedItems');\n completedUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n completedUrl.searchParams.set('keywords', keyword);\n completedUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n completedUrl.searchParams.set('itemFilter(0).name', 'SoldItemsOnly');\n completedUrl.searchParams.set('itemFilter(0).value', 'true');\n completedUrl.searchParams.set('itemFilter(1).name', 'ListingType');\n completedUrl.searchParams.set('itemFilter(1).value(0)', 'FixedPrice');\n completedUrl.searchParams.set('itemFilter(2).name', 'LocatedIn');\n completedUrl.searchParams.set('itemFilter(2).value', 'US');\n completedUrl.searchParams.set('itemFilter(3).name', 'Condition');\n completedUrl.searchParams.set('itemFilter(3).value', '1000');\n\n const activeUrl = new URL(findingBase);\n activeUrl.searchParams.set('OPERATION-NAME', 'findItemsAdvanced');\n activeUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n activeUrl.searchParams.set('keywords', keyword);\n activeUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n activeUrl.searchParams.set('itemFilter(0).name', 'ListingType');\n activeUrl.searchParams.set('itemFilter(0).value(0)', 'FixedPrice');\n activeUrl.searchParams.set('itemFilter(1).name', 'LocatedIn');\n activeUrl.searchParams.set('itemFilter(1).value', 'US');\n\n const [completedResponse, activeResponse] = await Promise.all([fetch(completedUrl, { headers }), fetch(activeUrl, { headers })]);\n if (!completedResponse.ok || !activeResponse.ok) {\n throw new Error(`eBay finding request failed (${completedResponse.status}/${activeResponse.status})`);\n }\n\n const completedPayload = (await completedResponse.json()) as Record<string, unknown>;\n const activePayload = (await activeResponse.json()) as Record<string, unknown>;\n\n const completedItems = extractFindingItems(completedPayload);\n const activeItems = extractFindingItems(activePayload);\n const soldPrices = completedItems.map((item) => item.price).filter((value) => Number.isFinite(value));\n const uniqueSellers = new Set(completedItems.map((item) => item.seller).filter(Boolean));\n const sold30d = completedItems.length;\n const sold7d = completedItems.filter((item) => Date.now() - item.endTime.getTime() <= 7 * 24 * 60 * 60 * 1000).length;\n const topRatedCount = activeItems.filter((item) => item.topRated).length;\n const activeCount = activeItems.length;\n\n return {\n sold30d,\n sold7d,\n uniqueSellers30d: uniqueSellers.size,\n medianSoldPrice30d: round(median(soldPrices), 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n async validateCategoryPolicies(candidate: ProductCandidate): Promise<{ valid: boolean; warnings: string[]; categoryId: string }> {\n const warnings: string[] = [];\n const categoryId = mapCategoryId(candidate.categoryPath);\n\n if (!this.appId) {\n if (candidate.tags.some((tag) => tag.toLowerCase().includes('battery'))) {\n warnings.push('Battery-adjacent item requires manual category review.');\n }\n return { valid: warnings.length === 0, warnings, categoryId };\n }\n\n const base = this.config.ebay.env === 'sandbox' ? 'https://apim.sandbox.ebay.com' : 'https://apim.ebay.com';\n const response = await fetch(`${base}/sell/metadata/v1/marketplace/EBAY_US/get_category_policies?category_id=${categoryId}`);\n if (!response.ok) {\n warnings.push(`Metadata policy lookup returned ${response.status}; using simulated policy fallback until seller OAuth is connected.`);\n return { valid: true, warnings, categoryId };\n }\n const payload = (await response.json()) as Record<string, unknown>;\n const allowed = Boolean(payload.categoryId || payload.categoryPolicies);\n if (!allowed) {\n warnings.push('Category policies could not be validated, so the draft is marked with a warning.');\n }\n return { valid: true, warnings, categoryId };\n }\n\n async createOrReplaceInventoryItem(draft: { sellerSku: string; title: string; description: string; images: string[]; quantity: number }): Promise<{ inventoryItemId: string }> {\n return { inventoryItemId: `INV-${draft.sellerSku}` };\n }\n\n async publishOffer(draft: { sellerSku: string; price: number; categoryId: string }): Promise<{ offerId: string; publishedAt: string }> {\n return {\n offerId: `OFFER-${shortHash(`${draft.sellerSku}:${draft.price}:${draft.categoryId}`)}`,\n publishedAt: nowIso(),\n };\n }\n\n async updateInventoryQuantity(sellerSku: string, quantity: number): Promise<void> {\n void sellerSku;\n void quantity;\n }\n\n async withdrawOffer(offerId: string): Promise<void> {\n void offerId;\n }\n}\n\nfunction extractFindingItems(payload: Record<string, unknown>): Array<{ price: number; seller: string; endTime: Date; topRated: boolean }> {\n const root = Object.values(payload)[0] as Array<Record<string, unknown>> | undefined;\n const payloadRoot = root?.[0] || {};\n const searchResult = (payloadRoot.searchResult as Array<Record<string, unknown>> | undefined)?.[0];\n const items = (searchResult?.item as Array<Record<string, unknown>> | undefined) || [];\n return items.map((item) => {\n const sellingStatus = (item.sellingStatus as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const currentPrice = (sellingStatus.currentPrice as Array<Record<string, unknown>> | undefined)?.[0];\n const sellerInfo = (item.sellerInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const listingInfo = (item.listingInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n return {\n price: Number(currentPrice?.__value__ || currentPrice?.value || 0),\n seller: String(sellerInfo.sellerUserName?.[0] || ''),\n endTime: new Date(String(listingInfo.endTime?.[0] || nowIso())),\n topRated: sellerInfo.topRatedSeller?.[0] === 'true',\n };\n });\n}\n\nfunction mapCategoryId(categoryPath: string): string {\n const lowered = categoryPath.toLowerCase();\n if (lowered.includes('storage')) {\n return '20635';\n }\n if (lowered.includes('travel')) {\n return '300';\n }\n if (lowered.includes('pet')) {\n return '177';\n }\n return '11700';\n}\n\nexport class AIClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private providerDetails(provider: ProviderName): { provider: ProviderName; modelName: string | null } {\n if (provider === 'deepseek') {\n return { provider, modelName: this.config.ai.deepseekModel };\n }\n if (provider === 'gemini') {\n return { provider, modelName: this.config.ai.geminiModel };\n }\n return { provider: 'system', modelName: null };\n }\n\n canSpend(state: AppState): boolean {\n const budgets = state.budgets;\n const month = nowIso().slice(0, 7);\n const day = nowIso().slice(0, 10);\n if (budgets.currentMonth !== month) {\n budgets.currentMonth = month;\n budgets.monthToDateUsd = 0;\n budgets.aiFallbackMode = false;\n }\n if (budgets.currentDay !== day) {\n budgets.currentDay = day;\n budgets.dayToDateUsd = 0;\n }\n return budgets.monthToDateUsd < budgets.monthlyCapUsd && budgets.dayToDateUsd < budgets.dailyCapUsd && !budgets.aiFallbackMode;\n }\n\n private charge(state: AppState, usd: number): void {\n state.budgets.monthToDateUsd = round(state.budgets.monthToDateUsd + usd, 4);\n state.budgets.dayToDateUsd = round(state.budgets.dayToDateUsd + usd, 4);\n if (state.budgets.monthToDateUsd >= state.budgets.monthlyCapUsd || state.budgets.dayToDateUsd >= state.budgets.dailyCapUsd) {\n state.budgets.aiFallbackMode = true;\n }\n }\n\n findCache(state: AppState, productFingerprint: string, kind: AIArtifactCacheEntry['kind']): AIArtifactCacheEntry | null {\n const now = Date.now();\n return state.aiCache.find((entry) => entry.productFingerprint === productFingerprint && entry.kind === kind && new Date(entry.expiresAt).getTime() > now) || null;\n }\n\n async generateListingPack(\n state: AppState,\n productFingerprint: string,\n candidate: ProductCandidate,\n ): Promise<{ pack: ListingPack; provider: ProviderName; modelName: string | null; cacheHit: boolean; fallback: boolean }> {\n const cached = this.findCache(state, productFingerprint, 'listing_pack');\n if (cached) {\n return {\n pack: JSON.parse(cached.payload) as ListingPack,\n provider: cached.provider,\n modelName: this.providerDetails(cached.provider).modelName,\n cacheHit: true,\n fallback: cached.provider === 'system',\n };\n }\n\n let pack: ListingPack;\n let provider: 'deepseek' | 'gemini' | 'system' = 'system';\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n pack = await this.generateViaDeepSeek(candidate);\n provider = 'deepseek';\n this.charge(state, 0.03);\n } else if (this.canSpend(state) && this.config.ai.geminiApiKey) {\n pack = await this.generateViaGemini(candidate);\n provider = 'gemini';\n this.charge(state, 0.05);\n } else {\n pack = templateListingPack(candidate);\n }\n\n state.aiCache.unshift({\n id: createId('cache'),\n productFingerprint,\n kind: 'listing_pack',\n provider,\n payload: JSON.stringify(pack),\n expiresAt: daysFromNow(30),\n createdAt: nowIso(),\n });\n state.aiCache = state.aiCache.slice(0, 200);\n return {\n pack,\n provider,\n modelName: this.providerDetails(provider).modelName,\n cacheHit: false,\n fallback: provider === 'system',\n };\n }\n\n async generateCeoReply(\n state: AppState,\n prompt: string,\n ): Promise<{ message: ConversationMessage; fallback: boolean }> {\n let provider: ProviderName = 'system';\n let modelName: string | null = null;\n let content = '';\n\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n provider = 'deepseek';\n modelName = this.config.ai.deepseekModel;\n content = await this.generateShortReplyViaDeepSeek(prompt);\n if (content) {\n this.charge(state, 0.02);\n }\n }\n\n if (!content && this.canSpend(state) && this.config.ai.geminiApiKey) {\n provider = 'gemini';\n modelName = this.config.ai.geminiModel;\n content = await this.generateShortReplyViaGemini(prompt);\n if (content) {\n this.charge(state, 0.03);\n }\n }\n\n if (!content) {\n provider = 'system';\n modelName = null;\n content = `Manager summary: ${prompt}. Current mode remains visibility-first. Review the latest agent traces before enabling wider automation.`;\n }\n\n return {\n message: {\n id: createId('msg'),\n sender: 'ceo',\n direction: 'outbound',\n message: content,\n createdAt: nowIso(),\n provider,\n modelName,\n linkedExecutionId: null,\n },\n fallback: provider === 'system',\n };\n }\n\n private async generateViaDeepSeek(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n response_format: { type: 'json_object' },\n messages: [\n {\n role: 'system',\n content:\n 'You write concise eBay listing packs for US consumers. Return JSON with title, subtitle, description, bullets, itemSpecifics, images.',\n },\n {\n role: 'user',\n content: `Create a listing pack for ${candidate.title} in category ${candidate.categoryPath} with target price ${candidate.targetPrice}.`,\n },\n ],\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n const content = payload.choices?.[0]?.message?.content;\n if (!content) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(content) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateViaGemini(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [\n {\n text: `Return JSON with title, subtitle, description, bullets, itemSpecifics, images for a US eBay listing about ${candidate.title} in ${candidate.categoryPath}.`,\n },\n ],\n },\n ],\n generationConfig: { responseMimeType: 'application/json' },\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n const text = payload.candidates?.[0]?.content?.parts?.[0]?.text;\n if (!text) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(text) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateShortReplyViaDeepSeek(prompt: string): Promise<string> {\n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n messages: [\n {\n role: 'system',\n content: 'You are a concise ecommerce operations manager. Reply in 2-4 short sentences.',\n },\n {\n role: 'user',\n content: prompt,\n },\n ],\n }),\n });\n if (!response.ok) {\n return '';\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n return payload.choices?.[0]?.message?.content?.trim() || '';\n } catch {\n return '';\n }\n }\n\n private async generateShortReplyViaGemini(prompt: string): Promise<string> {\n try {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [{ text: `Reply in 2-4 short sentences as an ecommerce ops manager. Prompt: ${prompt}` }],\n },\n ],\n }),\n });\n if (!response.ok) {\n return '';\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n return payload.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || '';\n } catch {\n return '';\n }\n }\n}\n\nfunction templateListingPack(candidate: ProductCandidate): ListingPack {\n const headline = candidate.title.length > 72 ? candidate.title.slice(0, 69).trimEnd() + '...' : candidate.title;\n return {\n title: `${headline} | US Fast Ship`,\n subtitle: 'Smart buy, everyday value.',\n description: `${candidate.title} is a simple, useful product selected for YuziGoods because it offers practical value, clean presentation, and reliable US delivery. Ships fast from a US warehouse.`,\n bullets: [\n 'US warehouse fulfillment with fast delivery promise',\n 'Simple, useful everyday product with low-friction setup',\n 'Selected for margin, delivery speed, and low return risk',\n 'Packed for YuziGoods US-first marketplace workflow',\n ],\n itemSpecifics: {\n Brand: 'YuziGoods',\n Type: candidate.categoryPath.split('>').slice(-1)[0]?.trim() || 'General',\n Color: 'As pictured',\n Material: 'Mixed materials',\n Country: 'United States warehouse',\n },\n images: [candidate.imageUrl],\n };\n}\n"}, "5b2b5b70ff97b424242765017a14f7295574d4ae": {"t": 1785748314.839591, "n": 26216, "h": "923ccfe4cacbdb5e", "c": "import {\n AIArtifactCacheEntry,\n AppState,\n CJProduct,\n ConversationMessage,\n ListingPack,\n MarketSearchSnapshot,\n ProductCandidate,\n ProviderName,\n RuntimeConfig,\n} from '../types';\nimport { clamp, createId, daysFromNow, median, normalizeKeyword, nowIso, round, shortHash } from './utils';\n\nfunction deterministicNumber(seed: string, min: number, max: number): number {\n const total = [...seed].reduce((sum, char) => sum + char.charCodeAt(0), 0);\n const span = max - min;\n return min + (total % Math.max(1, span + 1));\n}\n\nfunction syntheticCatalog(limit: number): CJProduct[] {\n const seeds = [\n 'under desk cable organizer',\n 'mini heatless curl band',\n 'stainless soap dispenser',\n 'magnetic spice rack',\n 'drawer compartment bins',\n 'portable pill organizer',\n 'shoe crease protector',\n 'luggage cup holder',\n 'sink splash guard',\n 'car seat gap filler',\n 'pet grooming glove',\n 'travel compression bags',\n 'vegetable chopper slicer',\n 'reusable lint remover',\n 'cordless milk frother',\n ];\n\n return seeds.slice(0, limit).map((seed, index) => {\n const landedCost = round(3.5 + index * 0.35, 2);\n const shippingCost = round(1 + (index % 3) * 0.45, 2);\n return {\n id: `cj_${index + 1}`,\n variantId: `cjv_${index + 1}`,\n title: seed.replace(/\\b\\w/g, (match) => match.toUpperCase()),\n normalizedKeyword: normalizeKeyword(seed),\n categoryPath: index % 4 === 0 ? 'Home & Garden > Storage & Organization' : index % 4 === 1 ? 'Health & Beauty > Personal Care' : index % 4 === 2 ? 'Travel > Accessories' : 'Pet Supplies > Grooming',\n warehouseCountry: 'US',\n landedCost: round(landedCost + shippingCost, 2),\n shippingCost,\n stock: 30 + index * 8,\n estimatedDeliveryBusinessDays: 2 + (index % 4),\n supplierName: `CJ Supplier ${index + 1}`,\n supplierAgeDays: 160 + index * 18,\n shippingConsistency: 78 + (index % 5) * 4,\n stockStability: 76 + (index % 6) * 3,\n orderHistoryScore: 74 + (index % 4) * 5,\n imageUrl: `https://picsum.photos/seed/${encodeURIComponent(seed)}/640/640`,\n tags: index % 3 === 0 ? ['utility', 'simple', 'lightweight'] : index % 3 === 1 ? ['beauty', 'travel', 'giftable'] : ['home', 'problem-solving', 'storage'],\n };\n });\n}\n\nexport class CJClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n async fetchCatalog(limit: number): Promise<CJProduct[]> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return syntheticCatalog(Math.min(limit, 15));\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/list?limit=${limit}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n\n if (!response.ok) {\n throw new Error(`CJ catalog request failed with ${response.status}`);\n }\n\n const payload = (await response.json()) as {\n data?: Array<Record<string, unknown>> | { list?: Array<Record<string, unknown>> };\n };\n const records = Array.isArray(payload.data)\n ? payload.data\n : Array.isArray(payload.data?.list)\n ? payload.data.list\n : [];\n return records.slice(0, limit).map((record, index) => ({\n id: String(record.pid || record.productId || `cj_${index}`),\n variantId: String(record.vid || record.variantId || record.productSku || record.pid || `cjv_${index}`),\n title: String(record.productName || record.title || `CJ Item ${index + 1}`),\n normalizedKeyword: normalizeKeyword(String(record.productName || record.title || `CJ Item ${index + 1}`)),\n categoryPath: String(record.categoryName || record.categoryPath || record.threeCategoryName || record.twoCategoryName || 'General'),\n warehouseCountry: inferWarehouseCountry(record),\n landedCost: Number(record.sellPrice || record.cost || 0),\n shippingCost: Number(record.shippingFee || 0),\n stock: inferStockLevel(record),\n estimatedDeliveryBusinessDays: inferDeliveryDays(record),\n supplierName: String(record.supplierName || 'CJ Supplier'),\n supplierAgeDays: Number(record.supplierAgeDays || 180),\n shippingConsistency: Number(record.shippingConsistency || 80),\n stockStability: Number(record.stockStability || 80),\n orderHistoryScore: Number(record.orderHistoryScore || 80),\n imageUrl: String(record.image || record.productImage || ''),\n tags: inferTags(record),\n }));\n }\n\n async getStock(productId: string, variantId: string): Promise<{ stock: number; etaBusinessDays: number }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return {\n stock: deterministicNumber(`${productId}:${variantId}`, 12, 80),\n etaBusinessDays: deterministicNumber(`${variantId}:${productId}`, 2, 5),\n };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/query?pid=${productId}&vid=${variantId}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n if (!response.ok) {\n throw new Error(`CJ stock request failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: Record<string, unknown> };\n return {\n stock: Number(payload.data?.sellableQuantity || payload.data?.stock || 0),\n etaBusinessDays: Number(payload.data?.deliveryDays || payload.data?.deliveryTime || 7),\n };\n }\n\n async placeOrder(params: { idempotencyKey: string; orderId: string; productId: string; variantId: string; quantity: number }): Promise<{ cjOrderId: string }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return { cjOrderId: `SIM-CJ-${shortHash(params.idempotencyKey)}` };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/order/create`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n body: JSON.stringify(params),\n });\n if (!response.ok) {\n throw new Error(`CJ order placement failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: { orderId?: string } };\n return { cjOrderId: payload.data?.orderId || `CJ-${shortHash(params.idempotencyKey)}` };\n }\n}\n\nfunction inferWarehouseCountry(record: Record<string, unknown>): string {\n const direct = String(record.warehouseCountry || record.countryCode || '').trim();\n if (direct) {\n return direct.toUpperCase();\n }\n\n const rawCodes = record.shippingCountryCodes;\n const codes = Array.isArray(rawCodes)\n ? rawCodes.map((value) => String(value).toUpperCase())\n : typeof rawCodes === 'string'\n ? rawCodes.split(/[,\\s]+/).map((value) => value.trim().toUpperCase()).filter(Boolean)\n : [];\n\n if (codes.includes('US')) {\n return 'US';\n }\n return codes[0] || 'US';\n}\n\nfunction inferStockLevel(record: Record<string, unknown>): number {\n const explicit = Number(record.sellableQuantity || record.stock || 0);\n if (Number.isFinite(explicit) && explicit > 0) {\n return explicit;\n }\n\n const listed = Number(record.listedNum || record.listingCount || 0);\n if (Number.isFinite(listed) && listed > 0) {\n return Math.max(24, listed);\n }\n\n return 24;\n}\n\nfunction inferDeliveryDays(record: Record<string, unknown>): number {\n const explicit = Number(record.deliveryDays || record.deliveryTime || 0);\n if (Number.isFinite(explicit) && explicit > 0) {\n return explicit;\n }\n\n return inferWarehouseCountry(record) === 'US' ? 5 : 8;\n}\n\nfunction inferTags(record: Record<string, unknown>): string[] {\n if (Array.isArray(record.tags)) {\n return record.tags.map(String);\n }\n\n const tags = new Set<string>();\n const add = (value: unknown) => {\n const text = String(value || '').trim().toLowerCase();\n if (text) {\n tags.add(text);\n }\n };\n\n add(record.sourceFrom);\n add(record.oneCategoryName);\n add(record.twoCategoryName);\n add(record.threeCategoryName);\n add(record.productType);\n add(record.isFreeShipping ? 'free-shipping' : '');\n\n return Array.from(tags);\n}\n\nexport class EbayClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private get appId(): string | null {\n return this.config.ebay.env === 'sandbox' ? this.config.ebay.sandboxClientId || this.config.ebay.clientId : this.config.ebay.clientId;\n }\n\n async getMarketSnapshot(keyword: string): Promise<MarketSearchSnapshot> {\n if (!this.appId) {\n const sold30d = deterministicNumber(keyword, 18, 120);\n const sold7d = deterministicNumber(`7:${keyword}`, 4, 28);\n const uniqueSellers30d = deterministicNumber(`sellers:${keyword}`, 4, 22);\n const activeCount = deterministicNumber(`active:${keyword}`, 15, 180);\n const topRatedCount = deterministicNumber(`top:${keyword}`, 2, Math.max(2, Math.floor(activeCount / 2)));\n return {\n sold30d,\n sold7d,\n uniqueSellers30d,\n medianSoldPrice30d: round(deterministicNumber(`price:${keyword}`, 14, 26) + 0.99, 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n const findingBase =\n this.config.ebay.env === 'sandbox'\n ? 'https://svcs.sandbox.ebay.com/services/search/FindingService/v1'\n : 'https://svcs.ebay.com/services/search/FindingService/v1';\n const headers = {\n 'X-EBAY-SOA-SECURITY-APPNAME': this.appId,\n 'X-EBAY-SOA-RESPONSE-DATA-FORMAT': 'JSON',\n 'X-EBAY-SOA-GLOBAL-ID': 'EBAY-US',\n };\n\n const completedUrl = new URL(findingBase);\n completedUrl.searchParams.set('OPERATION-NAME', 'findCompletedItems');\n completedUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n completedUrl.searchParams.set('keywords', keyword);\n completedUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n completedUrl.searchParams.set('itemFilter(0).name', 'SoldItemsOnly');\n completedUrl.searchParams.set('itemFilter(0).value', 'true');\n completedUrl.searchParams.set('itemFilter(1).name', 'ListingType');\n completedUrl.searchParams.set('itemFilter(1).value(0)', 'FixedPrice');\n completedUrl.searchParams.set('itemFilter(2).name', 'LocatedIn');\n completedUrl.searchParams.set('itemFilter(2).value', 'US');\n completedUrl.searchParams.set('itemFilter(3).name', 'Condition');\n completedUrl.searchParams.set('itemFilter(3).value', '1000');\n\n const activeUrl = new URL(findingBase);\n activeUrl.searchParams.set('OPERATION-NAME', 'findItemsAdvanced');\n activeUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n activeUrl.searchParams.set('keywords', keyword);\n activeUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n activeUrl.searchParams.set('itemFilter(0).name', 'ListingType');\n activeUrl.searchParams.set('itemFilter(0).value(0)', 'FixedPrice');\n activeUrl.searchParams.set('itemFilter(1).name', 'LocatedIn');\n activeUrl.searchParams.set('itemFilter(1).value', 'US');\n\n const [completedResponse, activeResponse] = await Promise.all([fetch(completedUrl, { headers }), fetch(activeUrl, { headers })]);\n if (!completedResponse.ok || !activeResponse.ok) {\n throw new Error(`eBay finding request failed (${completedResponse.status}/${activeResponse.status})`);\n }\n\n const completedPayload = (await completedResponse.json()) as Record<string, unknown>;\n const activePayload = (await activeResponse.json()) as Record<string, unknown>;\n\n const completedItems = extractFindingItems(completedPayload);\n const activeItems = extractFindingItems(activePayload);\n const soldPrices = completedItems.map((item) => item.price).filter((value) => Number.isFinite(value));\n const uniqueSellers = new Set(completedItems.map((item) => item.seller).filter(Boolean));\n const sold30d = completedItems.length;\n const sold7d = completedItems.filter((item) => Date.now() - item.endTime.getTime() <= 7 * 24 * 60 * 60 * 1000).length;\n const topRatedCount = activeItems.filter((item) => item.topRated).length;\n const activeCount = activeItems.length;\n\n return {\n sold30d,\n sold7d,\n uniqueSellers30d: uniqueSellers.size,\n medianSoldPrice30d: round(median(soldPrices), 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n async validateCategoryPolicies(candidate: ProductCandidate): Promise<{ valid: boolean; warnings: string[]; categoryId: string }> {\n const warnings: string[] = [];\n const categoryId = mapCategoryId(candidate.categoryPath);\n\n if (!this.appId) {\n if (candidate.tags.some((tag) => tag.toLowerCase().includes('battery'))) {\n warnings.push('Battery-adjacent item requires manual category review.');\n }\n return { valid: warnings.length === 0, warnings, categoryId };\n }\n\n const base = this.config.ebay.env === 'sandbox' ? 'https://apim.sandbox.ebay.com' : 'https://apim.ebay.com';\n const response = await fetch(`${base}/sell/metadata/v1/marketplace/EBAY_US/get_category_policies?category_id=${categoryId}`);\n if (!response.ok) {\n warnings.push(`Metadata policy lookup returned ${response.status}.`);\n return { valid: false, warnings, categoryId };\n }\n const payload = (await response.json()) as Record<string, unknown>;\n const allowed = Boolean(payload.categoryId || payload.categoryPolicies);\n if (!allowed) {\n warnings.push('Category policies could not be validated.');\n }\n return { valid: allowed, warnings, categoryId };\n }\n\n async createOrReplaceInventoryItem(draft: { sellerSku: string; title: string; description: string; images: string[]; quantity: number }): Promise<{ inventoryItemId: string }> {\n return { inventoryItemId: `INV-${draft.sellerSku}` };\n }\n\n async publishOffer(draft: { sellerSku: string; price: number; categoryId: string }): Promise<{ offerId: string; publishedAt: string }> {\n return {\n offerId: `OFFER-${shortHash(`${draft.sellerSku}:${draft.price}:${draft.categoryId}`)}`,\n publishedAt: nowIso(),\n };\n }\n\n async updateInventoryQuantity(sellerSku: string, quantity: number): Promise<void> {\n void sellerSku;\n void quantity;\n }\n\n async withdrawOffer(offerId: string): Promise<void> {\n void offerId;\n }\n}\n\nfunction extractFindingItems(payload: Record<string, unknown>): Array<{ price: number; seller: string; endTime: Date; topRated: boolean }> {\n const root = Object.values(payload)[0] as Array<Record<string, unknown>> | undefined;\n const payloadRoot = root?.[0] || {};\n const searchResult = (payloadRoot.searchResult as Array<Record<string, unknown>> | undefined)?.[0];\n const items = (searchResult?.item as Array<Record<string, unknown>> | undefined) || [];\n return items.map((item) => {\n const sellingStatus = (item.sellingStatus as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const currentPrice = (sellingStatus.currentPrice as Array<Record<string, unknown>> | undefined)?.[0];\n const sellerInfo = (item.sellerInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const listingInfo = (item.listingInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n return {\n price: Number(currentPrice?.__value__ || currentPrice?.value || 0),\n seller: String(sellerInfo.sellerUserName?.[0] || ''),\n endTime: new Date(String(listingInfo.endTime?.[0] || nowIso())),\n topRated: sellerInfo.topRatedSeller?.[0] === 'true',\n };\n });\n}\n\nfunction mapCategoryId(categoryPath: string): string {\n const lowered = categoryPath.toLowerCase();\n if (lowered.includes('storage')) {\n return '20635';\n }\n if (lowered.includes('travel')) {\n return '300';\n }\n if (lowered.includes('pet')) {\n return '177';\n }\n return '11700';\n}\n\nexport class AIClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private providerDetails(provider: ProviderName): { provider: ProviderName; modelName: string | null } {\n if (provider === 'deepseek') {\n return { provider, modelName: this.config.ai.deepseekModel };\n }\n if (provider === 'gemini') {\n return { provider, modelName: this.config.ai.geminiModel };\n }\n return { provider: 'system', modelName: null };\n }\n\n canSpend(state: AppState): boolean {\n const budgets = state.budgets;\n const month = nowIso().slice(0, 7);\n const day = nowIso().slice(0, 10);\n if (budgets.currentMonth !== month) {\n budgets.currentMonth = month;\n budgets.monthToDateUsd = 0;\n budgets.aiFallbackMode = false;\n }\n if (budgets.currentDay !== day) {\n budgets.currentDay = day;\n budgets.dayToDateUsd = 0;\n }\n return budgets.monthToDateUsd < budgets.monthlyCapUsd && budgets.dayToDateUsd < budgets.dailyCapUsd && !budgets.aiFallbackMode;\n }\n\n private charge(state: AppState, usd: number): void {\n state.budgets.monthToDateUsd = round(state.budgets.monthToDateUsd + usd, 4);\n state.budgets.dayToDateUsd = round(state.budgets.dayToDateUsd + usd, 4);\n if (state.budgets.monthToDateUsd >= state.budgets.monthlyCapUsd || state.budgets.dayToDateUsd >= state.budgets.dailyCapUsd) {\n state.budgets.aiFallbackMode = true;\n }\n }\n\n findCache(state: AppState, productFingerprint: string, kind: AIArtifactCacheEntry['kind']): AIArtifactCacheEntry | null {\n const now = Date.now();\n return state.aiCache.find((entry) => entry.productFingerprint === productFingerprint && entry.kind === kind && new Date(entry.expiresAt).getTime() > now) || null;\n }\n\n async generateListingPack(\n state: AppState,\n productFingerprint: string,\n candidate: ProductCandidate,\n ): Promise<{ pack: ListingPack; provider: ProviderName; modelName: string | null; cacheHit: boolean; fallback: boolean }> {\n const cached = this.findCache(state, productFingerprint, 'listing_pack');\n if (cached) {\n return {\n pack: JSON.parse(cached.payload) as ListingPack,\n provider: cached.provider,\n modelName: this.providerDetails(cached.provider).modelName,\n cacheHit: true,\n fallback: cached.provider === 'system',\n };\n }\n\n let pack: ListingPack;\n let provider: 'deepseek' | 'gemini' | 'system' = 'system';\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n pack = await this.generateViaDeepSeek(candidate);\n provider = 'deepseek';\n this.charge(state, 0.03);\n } else if (this.canSpend(state) && this.config.ai.geminiApiKey) {\n pack = await this.generateViaGemini(candidate);\n provider = 'gemini';\n this.charge(state, 0.05);\n } else {\n pack = templateListingPack(candidate);\n }\n\n state.aiCache.unshift({\n id: createId('cache'),\n productFingerprint,\n kind: 'listing_pack',\n provider,\n payload: JSON.stringify(pack),\n expiresAt: daysFromNow(30),\n createdAt: nowIso(),\n });\n state.aiCache = state.aiCache.slice(0, 200);\n return {\n pack,\n provider,\n modelName: this.providerDetails(provider).modelName,\n cacheHit: false,\n fallback: provider === 'system',\n };\n }\n\n async generateCeoReply(\n state: AppState,\n prompt: string,\n ): Promise<{ message: ConversationMessage; fallback: boolean }> {\n let provider: ProviderName = 'system';\n let modelName: string | null = null;\n let content = '';\n\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n provider = 'deepseek';\n modelName = this.config.ai.deepseekModel;\n content = await this.generateShortReplyViaDeepSeek(prompt);\n if (content) {\n this.charge(state, 0.02);\n }\n }\n\n if (!content && this.canSpend(state) && this.config.ai.geminiApiKey) {\n provider = 'gemini';\n modelName = this.config.ai.geminiModel;\n content = await this.generateShortReplyViaGemini(prompt);\n if (content) {\n this.charge(state, 0.03);\n }\n }\n\n if (!content) {\n provider = 'system';\n modelName = null;\n content = `Manager summary: ${prompt}. Current mode remains visibility-first. Review the latest agent traces before enabling wider automation.`;\n }\n\n return {\n message: {\n id: createId('msg'),\n sender: 'ceo',\n direction: 'outbound',\n message: content,\n createdAt: nowIso(),\n provider,\n modelName,\n linkedExecutionId: null,\n },\n fallback: provider === 'system',\n };\n }\n\n private async generateViaDeepSeek(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n response_format: { type: 'json_object' },\n messages: [\n {\n role: 'system',\n content:\n 'You write concise eBay listing packs for US consumers. Return JSON with title, subtitle, description, bullets, itemSpecifics, images.',\n },\n {\n role: 'user',\n content: `Create a listing pack for ${candidate.title} in category ${candidate.categoryPath} with target price ${candidate.targetPrice}.`,\n },\n ],\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n const content = payload.choices?.[0]?.message?.content;\n if (!content) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(content) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateViaGemini(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [\n {\n text: `Return JSON with title, subtitle, description, bullets, itemSpecifics, images for a US eBay listing about ${candidate.title} in ${candidate.categoryPath}.`,\n },\n ],\n },\n ],\n generationConfig: { responseMimeType: 'application/json' },\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n const text = payload.candidates?.[0]?.content?.parts?.[0]?.text;\n if (!text) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(text) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateShortReplyViaDeepSeek(prompt: string): Promise<string> {\n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n messages: [\n {\n role: 'system',\n content: 'You are a concise ecommerce operations manager. Reply in 2-4 short sentences.',\n },\n {\n role: 'user',\n content: prompt,\n },\n ],\n }),\n });\n if (!response.ok) {\n return '';\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n return payload.choices?.[0]?.message?.content?.trim() || '';\n } catch {\n return '';\n }\n }\n\n private async generateShortReplyViaGemini(prompt: string): Promise<string> {\n try {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [{ text: `Reply in 2-4 short sentences as an ecommerce ops manager. Prompt: ${prompt}` }],\n },\n ],\n }),\n });\n if (!response.ok) {\n return '';\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n return payload.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || '';\n } catch {\n return '';\n }\n }\n}\n\nfunction templateListingPack(candidate: ProductCandidate): ListingPack {\n const headline = candidate.title.length > 72 ? candidate.title.slice(0, 69).trimEnd() + '...' : candidate.title;\n return {\n title: `${headline} | US Fast Ship`,\n subtitle: 'Smart buy, everyday value.',\n description: `${candidate.title} is a simple, useful product selected for YuziGoods because it offers practical value, clean presentation, and reliable US delivery. Ships fast from a US warehouse.`,\n bullets: [\n 'US warehouse fulfillment with fast delivery promise',\n 'Simple, useful everyday product with low-friction setup',\n 'Selected for margin, delivery speed, and low return risk',\n 'Packed for YuziGoods US-first marketplace workflow',\n ],\n itemSpecifics: {\n Brand: 'YuziGoods',\n Type: candidate.categoryPath.split('>').slice(-1)[0]?.trim() || 'General',\n Color: 'As pictured',\n Material: 'Mixed materials',\n Country: 'United States warehouse',\n },\n images: [candidate.imageUrl],\n };\n}\n"}, "14718bc953400cedc28d28c65948375aa52c1445": {"t": 1785748316.2058876, "n": 24313, "h": "4ceda3a6c94b277f", "c": "import {\n AIArtifactCacheEntry,\n AppState,\n CJProduct,\n ConversationMessage,\n ListingPack,\n MarketSearchSnapshot,\n ProductCandidate,\n ProviderName,\n RuntimeConfig,\n} from '../types';\nimport { clamp, createId, daysFromNow, median, normalizeKeyword, nowIso, round, shortHash } from './utils';\n\nfunction deterministicNumber(seed: string, min: number, max: number): number {\n const total = [...seed].reduce((sum, char) => sum + char.charCodeAt(0), 0);\n const span = max - min;\n return min + (total % Math.max(1, span + 1));\n}\n\nfunction syntheticCatalog(limit: number): CJProduct[] {\n const seeds = [\n 'under desk cable organizer',\n 'mini heatless curl band',\n 'stainless soap dispenser',\n 'magnetic spice rack',\n 'drawer compartment bins',\n 'portable pill organizer',\n 'shoe crease protector',\n 'luggage cup holder',\n 'sink splash guard',\n 'car seat gap filler',\n 'pet grooming glove',\n 'travel compression bags',\n 'vegetable chopper slicer',\n 'reusable lint remover',\n 'cordless milk frother',\n ];\n\n return seeds.slice(0, limit).map((seed, index) => {\n const landedCost = round(3.5 + index * 0.35, 2);\n const shippingCost = round(1 + (index % 3) * 0.45, 2);\n return {\n id: `cj_${index + 1}`,\n variantId: `cjv_${index + 1}`,\n title: seed.replace(/\\b\\w/g, (match) => match.toUpperCase()),\n normalizedKeyword: normalizeKeyword(seed),\n categoryPath: index % 4 === 0 ? 'Home & Garden > Storage & Organization' : index % 4 === 1 ? 'Health & Beauty > Personal Care' : index % 4 === 2 ? 'Travel > Accessories' : 'Pet Supplies > Grooming',\n warehouseCountry: 'US',\n landedCost: round(landedCost + shippingCost, 2),\n shippingCost,\n stock: 30 + index * 8,\n estimatedDeliveryBusinessDays: 2 + (index % 4),\n supplierName: `CJ Supplier ${index + 1}`,\n supplierAgeDays: 160 + index * 18,\n shippingConsistency: 78 + (index % 5) * 4,\n stockStability: 76 + (index % 6) * 3,\n orderHistoryScore: 74 + (index % 4) * 5,\n imageUrl: `https://picsum.photos/seed/${encodeURIComponent(seed)}/640/640`,\n tags: index % 3 === 0 ? ['utility', 'simple', 'lightweight'] : index % 3 === 1 ? ['beauty', 'travel', 'giftable'] : ['home', 'problem-solving', 'storage'],\n };\n });\n}\n\nexport class CJClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n async fetchCatalog(limit: number): Promise<CJProduct[]> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return syntheticCatalog(Math.min(limit, 15));\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/list?limit=${limit}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n\n if (!response.ok) {\n throw new Error(`CJ catalog request failed with ${response.status}`);\n }\n\n const payload = (await response.json()) as { data?: Array<Record<string, unknown>> };\n const records = Array.isArray(payload.data) ? payload.data : [];\n return records.slice(0, limit).map((record, index) => ({\n id: String(record.pid || record.productId || `cj_${index}`),\n variantId: String(record.vid || record.variantId || `cjv_${index}`),\n title: String(record.productName || record.title || `CJ Item ${index + 1}`),\n normalizedKeyword: normalizeKeyword(String(record.productName || record.title || `CJ Item ${index + 1}`)),\n categoryPath: String(record.categoryName || record.categoryPath || 'General'),\n warehouseCountry: String(record.warehouseCountry || record.countryCode || 'US'),\n landedCost: Number(record.sellPrice || record.cost || 0),\n shippingCost: Number(record.shippingFee || 0),\n stock: Number(record.sellableQuantity || record.stock || 0),\n estimatedDeliveryBusinessDays: Number(record.deliveryDays || record.deliveryTime || 7),\n supplierName: String(record.supplierName || 'CJ Supplier'),\n supplierAgeDays: Number(record.supplierAgeDays || 180),\n shippingConsistency: Number(record.shippingConsistency || 80),\n stockStability: Number(record.stockStability || 80),\n orderHistoryScore: Number(record.orderHistoryScore || 80),\n imageUrl: String(record.image || record.productImage || ''),\n tags: Array.isArray(record.tags) ? record.tags.map(String) : [],\n }));\n }\n\n async getStock(productId: string, variantId: string): Promise<{ stock: number; etaBusinessDays: number }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return {\n stock: deterministicNumber(`${productId}:${variantId}`, 12, 80),\n etaBusinessDays: deterministicNumber(`${variantId}:${productId}`, 2, 5),\n };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/query?pid=${productId}&vid=${variantId}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n if (!response.ok) {\n throw new Error(`CJ stock request failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: Record<string, unknown> };\n return {\n stock: Number(payload.data?.sellableQuantity || payload.data?.stock || 0),\n etaBusinessDays: Number(payload.data?.deliveryDays || payload.data?.deliveryTime || 7),\n };\n }\n\n async placeOrder(params: { idempotencyKey: string; orderId: string; productId: string; variantId: string; quantity: number }): Promise<{ cjOrderId: string }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return { cjOrderId: `SIM-CJ-${shortHash(params.idempotencyKey)}` };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/order/create`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n body: JSON.stringify(params),\n });\n if (!response.ok) {\n throw new Error(`CJ order placement failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: { orderId?: string } };\n return { cjOrderId: payload.data?.orderId || `CJ-${shortHash(params.idempotencyKey)}` };\n }\n}\n\nexport class EbayClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private get appId(): string | null {\n return this.config.ebay.env === 'sandbox' ? this.config.ebay.sandboxClientId || this.config.ebay.clientId : this.config.ebay.clientId;\n }\n\n async getMarketSnapshot(keyword: string): Promise<MarketSearchSnapshot> {\n if (!this.appId) {\n const sold30d = deterministicNumber(keyword, 18, 120);\n const sold7d = deterministicNumber(`7:${keyword}`, 4, 28);\n const uniqueSellers30d = deterministicNumber(`sellers:${keyword}`, 4, 22);\n const activeCount = deterministicNumber(`active:${keyword}`, 15, 180);\n const topRatedCount = deterministicNumber(`top:${keyword}`, 2, Math.max(2, Math.floor(activeCount / 2)));\n return {\n sold30d,\n sold7d,\n uniqueSellers30d,\n medianSoldPrice30d: round(deterministicNumber(`price:${keyword}`, 14, 26) + 0.99, 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n const findingBase =\n this.config.ebay.env === 'sandbox'\n ? 'https://svcs.sandbox.ebay.com/services/search/FindingService/v1'\n : 'https://svcs.ebay.com/services/search/FindingService/v1';\n const headers = {\n 'X-EBAY-SOA-SECURITY-APPNAME': this.appId,\n 'X-EBAY-SOA-RESPONSE-DATA-FORMAT': 'JSON',\n 'X-EBAY-SOA-GLOBAL-ID': 'EBAY-US',\n };\n\n const completedUrl = new URL(findingBase);\n completedUrl.searchParams.set('OPERATION-NAME', 'findCompletedItems');\n completedUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n completedUrl.searchParams.set('keywords', keyword);\n completedUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n completedUrl.searchParams.set('itemFilter(0).name', 'SoldItemsOnly');\n completedUrl.searchParams.set('itemFilter(0).value', 'true');\n completedUrl.searchParams.set('itemFilter(1).name', 'ListingType');\n completedUrl.searchParams.set('itemFilter(1).value(0)', 'FixedPrice');\n completedUrl.searchParams.set('itemFilter(2).name', 'LocatedIn');\n completedUrl.searchParams.set('itemFilter(2).value', 'US');\n completedUrl.searchParams.set('itemFilter(3).name', 'Condition');\n completedUrl.searchParams.set('itemFilter(3).value', '1000');\n\n const activeUrl = new URL(findingBase);\n activeUrl.searchParams.set('OPERATION-NAME', 'findItemsAdvanced');\n activeUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n activeUrl.searchParams.set('keywords', keyword);\n activeUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n activeUrl.searchParams.set('itemFilter(0).name', 'ListingType');\n activeUrl.searchParams.set('itemFilter(0).value(0)', 'FixedPrice');\n activeUrl.searchParams.set('itemFilter(1).name', 'LocatedIn');\n activeUrl.searchParams.set('itemFilter(1).value', 'US');\n\n const [completedResponse, activeResponse] = await Promise.all([fetch(completedUrl, { headers }), fetch(activeUrl, { headers })]);\n if (!completedResponse.ok || !activeResponse.ok) {\n throw new Error(`eBay finding request failed (${completedResponse.status}/${activeResponse.status})`);\n }\n\n const completedPayload = (await completedResponse.json()) as Record<string, unknown>;\n const activePayload = (await activeResponse.json()) as Record<string, unknown>;\n\n const completedItems = extractFindingItems(completedPayload);\n const activeItems = extractFindingItems(activePayload);\n const soldPrices = completedItems.map((item) => item.price).filter((value) => Number.isFinite(value));\n const uniqueSellers = new Set(completedItems.map((item) => item.seller).filter(Boolean));\n const sold30d = completedItems.length;\n const sold7d = completedItems.filter((item) => Date.now() - item.endTime.getTime() <= 7 * 24 * 60 * 60 * 1000).length;\n const topRatedCount = activeItems.filter((item) => item.topRated).length;\n const activeCount = activeItems.length;\n\n return {\n sold30d,\n sold7d,\n uniqueSellers30d: uniqueSellers.size,\n medianSoldPrice30d: round(median(soldPrices), 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n async validateCategoryPolicies(candidate: ProductCandidate): Promise<{ valid: boolean; warnings: string[]; categoryId: string }> {\n const warnings: string[] = [];\n const categoryId = mapCategoryId(candidate.categoryPath);\n\n if (!this.appId) {\n if (candidate.tags.some((tag) => tag.toLowerCase().includes('battery'))) {\n warnings.push('Battery-adjacent item requires manual category review.');\n }\n return { valid: warnings.length === 0, warnings, categoryId };\n }\n\n const base = this.config.ebay.env === 'sandbox' ? 'https://apim.sandbox.ebay.com' : 'https://apim.ebay.com';\n const response = await fetch(`${base}/sell/metadata/v1/marketplace/EBAY_US/get_category_policies?category_id=${categoryId}`);\n if (!response.ok) {\n warnings.push(`Metadata policy lookup returned ${response.status}.`);\n return { valid: false, warnings, categoryId };\n }\n const payload = (await response.json()) as Record<string, unknown>;\n const allowed = Boolean(payload.categoryId || payload.categoryPolicies);\n if (!allowed) {\n warnings.push('Category policies could not be validated.');\n }\n return { valid: allowed, warnings, categoryId };\n }\n\n async createOrReplaceInventoryItem(draft: { sellerSku: string; title: string; description: string; images: string[]; quantity: number }): Promise<{ inventoryItemId: string }> {\n return { inventoryItemId: `INV-${draft.sellerSku}` };\n }\n\n async publishOffer(draft: { sellerSku: string; price: number; categoryId: string }): Promise<{ offerId: string; publishedAt: string }> {\n return {\n offerId: `OFFER-${shortHash(`${draft.sellerSku}:${draft.price}:${draft.categoryId}`)}`,\n publishedAt: nowIso(),\n };\n }\n\n async updateInventoryQuantity(sellerSku: string, quantity: number): Promise<void> {\n void sellerSku;\n void quantity;\n }\n\n async withdrawOffer(offerId: string): Promise<void> {\n void offerId;\n }\n}\n\nfunction extractFindingItems(payload: Record<string, unknown>): Array<{ price: number; seller: string; endTime: Date; topRated: boolean }> {\n const root = Object.values(payload)[0] as Array<Record<string, unknown>> | undefined;\n const payloadRoot = root?.[0] || {};\n const searchResult = (payloadRoot.searchResult as Array<Record<string, unknown>> | undefined)?.[0];\n const items = (searchResult?.item as Array<Record<string, unknown>> | undefined) || [];\n return items.map((item) => {\n const sellingStatus = (item.sellingStatus as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const currentPrice = (sellingStatus.currentPrice as Array<Record<string, unknown>> | undefined)?.[0];\n const sellerInfo = (item.sellerInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const listingInfo = (item.listingInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n return {\n price: Number(currentPrice?.__value__ || currentPrice?.value || 0),\n seller: String(sellerInfo.sellerUserName?.[0] || ''),\n endTime: new Date(String(listingInfo.endTime?.[0] || nowIso())),\n topRated: sellerInfo.topRatedSeller?.[0] === 'true',\n };\n });\n}\n\nfunction mapCategoryId(categoryPath: string): string {\n const lowered = categoryPath.toLowerCase();\n if (lowered.includes('storage')) {\n return '20635';\n }\n if (lowered.includes('travel')) {\n return '300';\n }\n if (lowered.includes('pet')) {\n return '177';\n }\n return '11700';\n}\n\nexport class AIClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private providerDetails(provider: ProviderName): { provider: ProviderName; modelName: string | null } {\n if (provider === 'deepseek') {\n return { provider, modelName: this.config.ai.deepseekModel };\n }\n if (provider === 'gemini') {\n return { provider, modelName: this.config.ai.geminiModel };\n }\n return { provider: 'system', modelName: null };\n }\n\n canSpend(state: AppState): boolean {\n const budgets = state.budgets;\n const month = nowIso().slice(0, 7);\n const day = nowIso().slice(0, 10);\n if (budgets.currentMonth !== month) {\n budgets.currentMonth = month;\n budgets.monthToDateUsd = 0;\n budgets.aiFallbackMode = false;\n }\n if (budgets.currentDay !== day) {\n budgets.currentDay = day;\n budgets.dayToDateUsd = 0;\n }\n return budgets.monthToDateUsd < budgets.monthlyCapUsd && budgets.dayToDateUsd < budgets.dailyCapUsd && !budgets.aiFallbackMode;\n }\n\n private charge(state: AppState, usd: number): void {\n state.budgets.monthToDateUsd = round(state.budgets.monthToDateUsd + usd, 4);\n state.budgets.dayToDateUsd = round(state.budgets.dayToDateUsd + usd, 4);\n if (state.budgets.monthToDateUsd >= state.budgets.monthlyCapUsd || state.budgets.dayToDateUsd >= state.budgets.dailyCapUsd) {\n state.budgets.aiFallbackMode = true;\n }\n }\n\n findCache(state: AppState, productFingerprint: string, kind: AIArtifactCacheEntry['kind']): AIArtifactCacheEntry | null {\n const now = Date.now();\n return state.aiCache.find((entry) => entry.productFingerprint === productFingerprint && entry.kind === kind && new Date(entry.expiresAt).getTime() > now) || null;\n }\n\n async generateListingPack(\n state: AppState,\n productFingerprint: string,\n candidate: ProductCandidate,\n ): Promise<{ pack: ListingPack; provider: ProviderName; modelName: string | null; cacheHit: boolean; fallback: boolean }> {\n const cached = this.findCache(state, productFingerprint, 'listing_pack');\n if (cached) {\n return {\n pack: JSON.parse(cached.payload) as ListingPack,\n provider: cached.provider,\n modelName: this.providerDetails(cached.provider).modelName,\n cacheHit: true,\n fallback: cached.provider === 'system',\n };\n }\n\n let pack: ListingPack;\n let provider: 'deepseek' | 'gemini' | 'system' = 'system';\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n pack = await this.generateViaDeepSeek(candidate);\n provider = 'deepseek';\n this.charge(state, 0.03);\n } else if (this.canSpend(state) && this.config.ai.geminiApiKey) {\n pack = await this.generateViaGemini(candidate);\n provider = 'gemini';\n this.charge(state, 0.05);\n } else {\n pack = templateListingPack(candidate);\n }\n\n state.aiCache.unshift({\n id: createId('cache'),\n productFingerprint,\n kind: 'listing_pack',\n provider,\n payload: JSON.stringify(pack),\n expiresAt: daysFromNow(30),\n createdAt: nowIso(),\n });\n state.aiCache = state.aiCache.slice(0, 200);\n return {\n pack,\n provider,\n modelName: this.providerDetails(provider).modelName,\n cacheHit: false,\n fallback: provider === 'system',\n };\n }\n\n async generateCeoReply(\n state: AppState,\n prompt: string,\n ): Promise<{ message: ConversationMessage; fallback: boolean }> {\n let provider: ProviderName = 'system';\n let modelName: string | null = null;\n let content = '';\n\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n provider = 'deepseek';\n modelName = this.config.ai.deepseekModel;\n content = await this.generateShortReplyViaDeepSeek(prompt);\n if (content) {\n this.charge(state, 0.02);\n }\n }\n\n if (!content && this.canSpend(state) && this.config.ai.geminiApiKey) {\n provider = 'gemini';\n modelName = this.config.ai.geminiModel;\n content = await this.generateShortReplyViaGemini(prompt);\n if (content) {\n this.charge(state, 0.03);\n }\n }\n\n if (!content) {\n provider = 'system';\n modelName = null;\n content = `Manager summary: ${prompt}. Current mode remains visibility-first. Review the latest agent traces before enabling wider automation.`;\n }\n\n return {\n message: {\n id: createId('msg'),\n sender: 'ceo',\n direction: 'outbound',\n message: content,\n createdAt: nowIso(),\n provider,\n modelName,\n linkedExecutionId: null,\n },\n fallback: provider === 'system',\n };\n }\n\n private async generateViaDeepSeek(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n response_format: { type: 'json_object' },\n messages: [\n {\n role: 'system',\n content:\n 'You write concise eBay listing packs for US consumers. Return JSON with title, subtitle, description, bullets, itemSpecifics, images.',\n },\n {\n role: 'user',\n content: `Create a listing pack for ${candidate.title} in category ${candidate.categoryPath} with target price ${candidate.targetPrice}.`,\n },\n ],\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n const content = payload.choices?.[0]?.message?.content;\n if (!content) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(content) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateViaGemini(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [\n {\n text: `Return JSON with title, subtitle, description, bullets, itemSpecifics, images for a US eBay listing about ${candidate.title} in ${candidate.categoryPath}.`,\n },\n ],\n },\n ],\n generationConfig: { responseMimeType: 'application/json' },\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n const text = payload.candidates?.[0]?.content?.parts?.[0]?.text;\n if (!text) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(text) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateShortReplyViaDeepSeek(prompt: string): Promise<string> {\n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n messages: [\n {\n role: 'system',\n content: 'You are a concise ecommerce operations manager. Reply in 2-4 short sentences.',\n },\n {\n role: 'user',\n content: prompt,\n },\n ],\n }),\n });\n if (!response.ok) {\n return '';\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n return payload.choices?.[0]?.message?.content?.trim() || '';\n } catch {\n return '';\n }\n }\n\n private async generateShortReplyViaGemini(prompt: string): Promise<string> {\n try {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [{ text: `Reply in 2-4 short sentences as an ecommerce ops manager. Prompt: ${prompt}` }],\n },\n ],\n }),\n });\n if (!response.ok) {\n return '';\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n return payload.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || '';\n } catch {\n return '';\n }\n }\n}\n\nfunction templateListingPack(candidate: ProductCandidate): ListingPack {\n const headline = candidate.title.length > 72 ? candidate.title.slice(0, 69).trimEnd() + '...' : candidate.title;\n return {\n title: `${headline} | US Fast Ship`,\n subtitle: 'Smart buy, everyday value.',\n description: `${candidate.title} is a simple, useful product selected for YuziGoods because it offers practical value, clean presentation, and reliable US delivery. Ships fast from a US warehouse.`,\n bullets: [\n 'US warehouse fulfillment with fast delivery promise',\n 'Simple, useful everyday product with low-friction setup',\n 'Selected for margin, delivery speed, and low return risk',\n 'Packed for YuziGoods US-first marketplace workflow',\n ],\n itemSpecifics: {\n Brand: 'YuziGoods',\n Type: candidate.categoryPath.split('>').slice(-1)[0]?.trim() || 'General',\n Color: 'As pictured',\n Material: 'Mixed materials',\n Country: 'United States warehouse',\n },\n images: [candidate.imageUrl],\n };\n}\n"}, "ce8f685fc30aa67da695edefd64d76a624ae9257": {"t": 1785748317.3252687, "n": 20228, "h": "af25c1fb620869ba", "c": "import {\n AIArtifactCacheEntry,\n AppState,\n CJProduct,\n ListingPack,\n MarketSearchSnapshot,\n ProductCandidate,\n RuntimeConfig,\n} from '../types';\nimport { clamp, createId, daysFromNow, median, normalizeKeyword, nowIso, round, shortHash } from './utils';\n\nfunction deterministicNumber(seed: string, min: number, max: number): number {\n const total = [...seed].reduce((sum, char) => sum + char.charCodeAt(0), 0);\n const span = max - min;\n return min + (total % Math.max(1, span + 1));\n}\n\nfunction syntheticCatalog(limit: number): CJProduct[] {\n const seeds = [\n 'under desk cable organizer',\n 'mini heatless curl band',\n 'stainless soap dispenser',\n 'magnetic spice rack',\n 'drawer compartment bins',\n 'portable pill organizer',\n 'shoe crease protector',\n 'luggage cup holder',\n 'sink splash guard',\n 'car seat gap filler',\n 'pet grooming glove',\n 'travel compression bags',\n 'vegetable chopper slicer',\n 'reusable lint remover',\n 'cordless milk frother',\n ];\n\n return seeds.slice(0, limit).map((seed, index) => {\n const landedCost = round(3.5 + index * 0.35, 2);\n const shippingCost = round(1 + (index % 3) * 0.45, 2);\n return {\n id: `cj_${index + 1}`,\n variantId: `cjv_${index + 1}`,\n title: seed.replace(/\\b\\w/g, (match) => match.toUpperCase()),\n normalizedKeyword: normalizeKeyword(seed),\n categoryPath: index % 4 === 0 ? 'Home & Garden > Storage & Organization' : index % 4 === 1 ? 'Health & Beauty > Personal Care' : index % 4 === 2 ? 'Travel > Accessories' : 'Pet Supplies > Grooming',\n warehouseCountry: 'US',\n landedCost: round(landedCost + shippingCost, 2),\n shippingCost,\n stock: 30 + index * 8,\n estimatedDeliveryBusinessDays: 2 + (index % 4),\n supplierName: `CJ Supplier ${index + 1}`,\n supplierAgeDays: 160 + index * 18,\n shippingConsistency: 78 + (index % 5) * 4,\n stockStability: 76 + (index % 6) * 3,\n orderHistoryScore: 74 + (index % 4) * 5,\n imageUrl: `https://picsum.photos/seed/${encodeURIComponent(seed)}/640/640`,\n tags: index % 3 === 0 ? ['utility', 'simple', 'lightweight'] : index % 3 === 1 ? ['beauty', 'travel', 'giftable'] : ['home', 'problem-solving', 'storage'],\n };\n });\n}\n\nexport class CJClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n async fetchCatalog(limit: number): Promise<CJProduct[]> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return syntheticCatalog(Math.min(limit, 15));\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/list?limit=${limit}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n\n if (!response.ok) {\n throw new Error(`CJ catalog request failed with ${response.status}`);\n }\n\n const payload = (await response.json()) as { data?: Array<Record<string, unknown>> };\n const records = Array.isArray(payload.data) ? payload.data : [];\n return records.slice(0, limit).map((record, index) => ({\n id: String(record.pid || record.productId || `cj_${index}`),\n variantId: String(record.vid || record.variantId || `cjv_${index}`),\n title: String(record.productName || record.title || `CJ Item ${index + 1}`),\n normalizedKeyword: normalizeKeyword(String(record.productName || record.title || `CJ Item ${index + 1}`)),\n categoryPath: String(record.categoryName || record.categoryPath || 'General'),\n warehouseCountry: String(record.warehouseCountry || record.countryCode || 'US'),\n landedCost: Number(record.sellPrice || record.cost || 0),\n shippingCost: Number(record.shippingFee || 0),\n stock: Number(record.sellableQuantity || record.stock || 0),\n estimatedDeliveryBusinessDays: Number(record.deliveryDays || record.deliveryTime || 7),\n supplierName: String(record.supplierName || 'CJ Supplier'),\n supplierAgeDays: Number(record.supplierAgeDays || 180),\n shippingConsistency: Number(record.shippingConsistency || 80),\n stockStability: Number(record.stockStability || 80),\n orderHistoryScore: Number(record.orderHistoryScore || 80),\n imageUrl: String(record.image || record.productImage || ''),\n tags: Array.isArray(record.tags) ? record.tags.map(String) : [],\n }));\n }\n\n async getStock(productId: string, variantId: string): Promise<{ stock: number; etaBusinessDays: number }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return {\n stock: deterministicNumber(`${productId}:${variantId}`, 12, 80),\n etaBusinessDays: deterministicNumber(`${variantId}:${productId}`, 2, 5),\n };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/query?pid=${productId}&vid=${variantId}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n if (!response.ok) {\n throw new Error(`CJ stock request failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: Record<string, unknown> };\n return {\n stock: Number(payload.data?.sellableQuantity || payload.data?.stock || 0),\n etaBusinessDays: Number(payload.data?.deliveryDays || payload.data?.deliveryTime || 7),\n };\n }\n\n async placeOrder(params: { idempotencyKey: string; orderId: string; productId: string; variantId: string; quantity: number }): Promise<{ cjOrderId: string }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return { cjOrderId: `SIM-CJ-${shortHash(params.idempotencyKey)}` };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/order/create`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n body: JSON.stringify(params),\n });\n if (!response.ok) {\n throw new Error(`CJ order placement failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: { orderId?: string } };\n return { cjOrderId: payload.data?.orderId || `CJ-${shortHash(params.idempotencyKey)}` };\n }\n}\n\nexport class EbayClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private get appId(): string | null {\n return this.config.ebay.env === 'sandbox' ? this.config.ebay.sandboxClientId || this.config.ebay.clientId : this.config.ebay.clientId;\n }\n\n async getMarketSnapshot(keyword: string): Promise<MarketSearchSnapshot> {\n if (!this.appId) {\n const sold30d = deterministicNumber(keyword, 18, 120);\n const sold7d = deterministicNumber(`7:${keyword}`, 4, 28);\n const uniqueSellers30d = deterministicNumber(`sellers:${keyword}`, 4, 22);\n const activeCount = deterministicNumber(`active:${keyword}`, 15, 180);\n const topRatedCount = deterministicNumber(`top:${keyword}`, 2, Math.max(2, Math.floor(activeCount / 2)));\n return {\n sold30d,\n sold7d,\n uniqueSellers30d,\n medianSoldPrice30d: round(deterministicNumber(`price:${keyword}`, 14, 26) + 0.99, 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n const findingBase =\n this.config.ebay.env === 'sandbox'\n ? 'https://svcs.sandbox.ebay.com/services/search/FindingService/v1'\n : 'https://svcs.ebay.com/services/search/FindingService/v1';\n const headers = {\n 'X-EBAY-SOA-SECURITY-APPNAME': this.appId,\n 'X-EBAY-SOA-RESPONSE-DATA-FORMAT': 'JSON',\n 'X-EBAY-SOA-GLOBAL-ID': 'EBAY-US',\n };\n\n const completedUrl = new URL(findingBase);\n completedUrl.searchParams.set('OPERATION-NAME', 'findCompletedItems');\n completedUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n completedUrl.searchParams.set('keywords', keyword);\n completedUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n completedUrl.searchParams.set('itemFilter(0).name', 'SoldItemsOnly');\n completedUrl.searchParams.set('itemFilter(0).value', 'true');\n completedUrl.searchParams.set('itemFilter(1).name', 'ListingType');\n completedUrl.searchParams.set('itemFilter(1).value(0)', 'FixedPrice');\n completedUrl.searchParams.set('itemFilter(2).name', 'LocatedIn');\n completedUrl.searchParams.set('itemFilter(2).value', 'US');\n completedUrl.searchParams.set('itemFilter(3).name', 'Condition');\n completedUrl.searchParams.set('itemFilter(3).value', '1000');\n\n const activeUrl = new URL(findingBase);\n activeUrl.searchParams.set('OPERATION-NAME', 'findItemsAdvanced');\n activeUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n activeUrl.searchParams.set('keywords', keyword);\n activeUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n activeUrl.searchParams.set('itemFilter(0).name', 'ListingType');\n activeUrl.searchParams.set('itemFilter(0).value(0)', 'FixedPrice');\n activeUrl.searchParams.set('itemFilter(1).name', 'LocatedIn');\n activeUrl.searchParams.set('itemFilter(1).value', 'US');\n\n const [completedResponse, activeResponse] = await Promise.all([fetch(completedUrl, { headers }), fetch(activeUrl, { headers })]);\n if (!completedResponse.ok || !activeResponse.ok) {\n throw new Error(`eBay finding request failed (${completedResponse.status}/${activeResponse.status})`);\n }\n\n const completedPayload = (await completedResponse.json()) as Record<string, unknown>;\n const activePayload = (await activeResponse.json()) as Record<string, unknown>;\n\n const completedItems = extractFindingItems(completedPayload);\n const activeItems = extractFindingItems(activePayload);\n const soldPrices = completedItems.map((item) => item.price).filter((value) => Number.isFinite(value));\n const uniqueSellers = new Set(completedItems.map((item) => item.seller).filter(Boolean));\n const sold30d = completedItems.length;\n const sold7d = completedItems.filter((item) => Date.now() - item.endTime.getTime() <= 7 * 24 * 60 * 60 * 1000).length;\n const topRatedCount = activeItems.filter((item) => item.topRated).length;\n const activeCount = activeItems.length;\n\n return {\n sold30d,\n sold7d,\n uniqueSellers30d: uniqueSellers.size,\n medianSoldPrice30d: round(median(soldPrices), 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n async validateCategoryPolicies(candidate: ProductCandidate): Promise<{ valid: boolean; warnings: string[]; categoryId: string }> {\n const warnings: string[] = [];\n const categoryId = mapCategoryId(candidate.categoryPath);\n\n if (!this.appId) {\n if (candidate.tags.some((tag) => tag.toLowerCase().includes('battery'))) {\n warnings.push('Battery-adjacent item requires manual category review.');\n }\n return { valid: warnings.length === 0, warnings, categoryId };\n }\n\n const base = this.config.ebay.env === 'sandbox' ? 'https://apim.sandbox.ebay.com' : 'https://apim.ebay.com';\n const response = await fetch(`${base}/sell/metadata/v1/marketplace/EBAY_US/get_category_policies?category_id=${categoryId}`);\n if (!response.ok) {\n warnings.push(`Metadata policy lookup returned ${response.status}.`);\n return { valid: false, warnings, categoryId };\n }\n const payload = (await response.json()) as Record<string, unknown>;\n const allowed = Boolean(payload.categoryId || payload.categoryPolicies);\n if (!allowed) {\n warnings.push('Category policies could not be validated.');\n }\n return { valid: allowed, warnings, categoryId };\n }\n\n async createOrReplaceInventoryItem(draft: { sellerSku: string; title: string; description: string; images: string[]; quantity: number }): Promise<{ inventoryItemId: string }> {\n return { inventoryItemId: `INV-${draft.sellerSku}` };\n }\n\n async publishOffer(draft: { sellerSku: string; price: number; categoryId: string }): Promise<{ offerId: string; publishedAt: string }> {\n return {\n offerId: `OFFER-${shortHash(`${draft.sellerSku}:${draft.price}:${draft.categoryId}`)}`,\n publishedAt: nowIso(),\n };\n }\n\n async updateInventoryQuantity(sellerSku: string, quantity: number): Promise<void> {\n void sellerSku;\n void quantity;\n }\n\n async withdrawOffer(offerId: string): Promise<void> {\n void offerId;\n }\n}\n\nfunction extractFindingItems(payload: Record<string, unknown>): Array<{ price: number; seller: string; endTime: Date; topRated: boolean }> {\n const root = Object.values(payload)[0] as Array<Record<string, unknown>> | undefined;\n const payloadRoot = root?.[0] || {};\n const searchResult = (payloadRoot.searchResult as Array<Record<string, unknown>> | undefined)?.[0];\n const items = (searchResult?.item as Array<Record<string, unknown>> | undefined) || [];\n return items.map((item) => {\n const sellingStatus = (item.sellingStatus as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const currentPrice = (sellingStatus.currentPrice as Array<Record<string, unknown>> | undefined)?.[0];\n const sellerInfo = (item.sellerInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const listingInfo = (item.listingInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n return {\n price: Number(currentPrice?.__value__ || currentPrice?.value || 0),\n seller: String(sellerInfo.sellerUserName?.[0] || ''),\n endTime: new Date(String(listingInfo.endTime?.[0] || nowIso())),\n topRated: sellerInfo.topRatedSeller?.[0] === 'true',\n };\n });\n}\n\nfunction mapCategoryId(categoryPath: string): string {\n const lowered = categoryPath.toLowerCase();\n if (lowered.includes('storage')) {\n return '20635';\n }\n if (lowered.includes('travel')) {\n return '300';\n }\n if (lowered.includes('pet')) {\n return '177';\n }\n return '11700';\n}\n\nexport class AIClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n canSpend(state: AppState): boolean {\n const budgets = state.budgets;\n const month = nowIso().slice(0, 7);\n const day = nowIso().slice(0, 10);\n if (budgets.currentMonth !== month) {\n budgets.currentMonth = month;\n budgets.monthToDateUsd = 0;\n budgets.aiFallbackMode = false;\n }\n if (budgets.currentDay !== day) {\n budgets.currentDay = day;\n budgets.dayToDateUsd = 0;\n }\n return budgets.monthToDateUsd < budgets.monthlyCapUsd && budgets.dayToDateUsd < budgets.dailyCapUsd && !budgets.aiFallbackMode;\n }\n\n private charge(state: AppState, usd: number): void {\n state.budgets.monthToDateUsd = round(state.budgets.monthToDateUsd + usd, 4);\n state.budgets.dayToDateUsd = round(state.budgets.dayToDateUsd + usd, 4);\n if (state.budgets.monthToDateUsd >= state.budgets.monthlyCapUsd || state.budgets.dayToDateUsd >= state.budgets.dailyCapUsd) {\n state.budgets.aiFallbackMode = true;\n }\n }\n\n findCache(state: AppState, productFingerprint: string, kind: AIArtifactCacheEntry['kind']): AIArtifactCacheEntry | null {\n const now = Date.now();\n return state.aiCache.find((entry) => entry.productFingerprint === productFingerprint && entry.kind === kind && new Date(entry.expiresAt).getTime() > now) || null;\n }\n\n async generateListingPack(state: AppState, productFingerprint: string, candidate: ProductCandidate): Promise<ListingPack> {\n const cached = this.findCache(state, productFingerprint, 'listing_pack');\n if (cached) {\n return JSON.parse(cached.payload) as ListingPack;\n }\n\n let pack: ListingPack;\n let provider: 'deepseek' | 'gemini' | 'system' = 'system';\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n pack = await this.generateViaDeepSeek(candidate);\n provider = 'deepseek';\n this.charge(state, 0.03);\n } else if (this.canSpend(state) && this.config.ai.geminiApiKey) {\n pack = await this.generateViaGemini(candidate);\n provider = 'gemini';\n this.charge(state, 0.05);\n } else {\n pack = templateListingPack(candidate);\n }\n\n state.aiCache.unshift({\n id: createId('cache'),\n productFingerprint,\n kind: 'listing_pack',\n provider,\n payload: JSON.stringify(pack),\n expiresAt: daysFromNow(30),\n createdAt: nowIso(),\n });\n state.aiCache = state.aiCache.slice(0, 200);\n return pack;\n }\n\n private async generateViaDeepSeek(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n response_format: { type: 'json_object' },\n messages: [\n {\n role: 'system',\n content:\n 'You write concise eBay listing packs for US consumers. Return JSON with title, subtitle, description, bullets, itemSpecifics, images.',\n },\n {\n role: 'user',\n content: `Create a listing pack for ${candidate.title} in category ${candidate.categoryPath} with target price ${candidate.targetPrice}.`,\n },\n ],\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n const content = payload.choices?.[0]?.message?.content;\n if (!content) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(content) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateViaGemini(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [\n {\n text: `Return JSON with title, subtitle, description, bullets, itemSpecifics, images for a US eBay listing about ${candidate.title} in ${candidate.categoryPath}.`,\n },\n ],\n },\n ],\n generationConfig: { responseMimeType: 'application/json' },\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n const text = payload.candidates?.[0]?.content?.parts?.[0]?.text;\n if (!text) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(text) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n}\n\nfunction templateListingPack(candidate: ProductCandidate): ListingPack {\n const headline = candidate.title.length > 72 ? candidate.title.slice(0, 69).trimEnd() + '...' : candidate.title;\n return {\n title: `${headline} | US Fast Ship`,\n subtitle: 'Smart buy, everyday value.',\n description: `${candidate.title} is a simple, useful product selected for YuziGoods because it offers practical value, clean presentation, and reliable US delivery. Ships fast from a US warehouse.`,\n bullets: [\n 'US warehouse fulfillment with fast delivery promise',\n 'Simple, useful everyday product with low-friction setup',\n 'Selected for margin, delivery speed, and low return risk',\n 'Packed for YuziGoods US-first marketplace workflow',\n ],\n itemSpecifics: {\n Brand: 'YuziGoods',\n Type: candidate.categoryPath.split('>').slice(-1)[0]?.trim() || 'General',\n Color: 'As pictured',\n Material: 'Mixed materials',\n Country: 'United States warehouse',\n },\n images: [candidate.imageUrl],\n };\n}\n"}, "c0f26405ad69e38744748a9457aedca36623a7ac": {"t": 1785748318.9523401, "n": 20199, "h": "dff1fea1848cf445", "c": "import {\n AIArtifactCacheEntry,\n AppState,\n CJProduct,\n ListingPack,\n MarketSearchSnapshot,\n ProductCandidate,\n RuntimeConfig,\n} from '../types';\nimport { clamp, createId, daysFromNow, median, normalizeKeyword, nowIso, round, shortHash } from './utils';\n\nfunction deterministicNumber(seed: string, min: number, max: number): number {\n const total = [...seed].reduce((sum, char) => sum + char.charCodeAt(0), 0);\n const span = max - min;\n return min + (total % Math.max(1, span + 1));\n}\n\nfunction syntheticCatalog(limit: number): CJProduct[] {\n const seeds = [\n 'under desk cable organizer',\n 'mini heatless curl band',\n 'stainless soap dispenser',\n 'magnetic spice rack',\n 'drawer compartment bins',\n 'portable pill organizer',\n 'shoe crease protector',\n 'luggage cup holder',\n 'sink splash guard',\n 'car seat gap filler',\n 'pet grooming glove',\n 'travel compression bags',\n 'vegetable chopper slicer',\n 'reusable lint remover',\n 'cordless milk frother',\n ];\n\n return seeds.slice(0, limit).map((seed, index) => {\n const landedCost = round(3.5 + index * 0.35, 2);\n const shippingCost = round(1 + (index % 3) * 0.45, 2);\n return {\n id: `cj_${index + 1}`,\n variantId: `cjv_${index + 1}`,\n title: seed.replace(/\\b\\w/g, (match) => match.toUpperCase()),\n normalizedKeyword: normalizeKeyword(seed),\n categoryPath: index % 4 === 0 ? 'Home & Garden > Storage & Organization' : index % 4 === 1 ? 'Health & Beauty > Personal Care' : index % 4 === 2 ? 'Travel > Accessories' : 'Pet Supplies > Grooming',\n warehouseCountry: 'US',\n landedCost: round(landedCost + shippingCost, 2),\n shippingCost,\n stock: 30 + index * 8,\n estimatedDeliveryBusinessDays: 2 + (index % 4),\n supplierName: `CJ Supplier ${index + 1}`,\n supplierAgeDays: 160 + index * 18,\n shippingConsistency: 78 + (index % 5) * 4,\n stockStability: 76 + (index % 6) * 3,\n orderHistoryScore: 74 + (index % 4) * 5,\n imageUrl: `https://picsum.photos/seed/${encodeURIComponent(seed)}/640/640`,\n tags: index % 3 === 0 ? ['utility', 'simple', 'lightweight'] : index % 3 === 1 ? ['beauty', 'travel', 'giftable'] : ['home', 'problem-solving', 'storage'],\n };\n });\n}\n\nexport class CJClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n async fetchCatalog(limit: number): Promise<CJProduct[]> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return syntheticCatalog(Math.min(limit, 15));\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/list?limit=${limit}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n\n if (!response.ok) {\n throw new Error(`CJ catalog request failed with ${response.status}`);\n }\n\n const payload = (await response.json()) as { data?: Array<Record<string, unknown>> };\n const records = payload.data || [];\n return records.slice(0, limit).map((record, index) => ({\n id: String(record.pid || record.productId || `cj_${index}`),\n variantId: String(record.vid || record.variantId || `cjv_${index}`),\n title: String(record.productName || record.title || `CJ Item ${index + 1}`),\n normalizedKeyword: normalizeKeyword(String(record.productName || record.title || `CJ Item ${index + 1}`)),\n categoryPath: String(record.categoryName || record.categoryPath || 'General'),\n warehouseCountry: String(record.warehouseCountry || record.countryCode || 'US'),\n landedCost: Number(record.sellPrice || record.cost || 0),\n shippingCost: Number(record.shippingFee || 0),\n stock: Number(record.sellableQuantity || record.stock || 0),\n estimatedDeliveryBusinessDays: Number(record.deliveryDays || record.deliveryTime || 7),\n supplierName: String(record.supplierName || 'CJ Supplier'),\n supplierAgeDays: Number(record.supplierAgeDays || 180),\n shippingConsistency: Number(record.shippingConsistency || 80),\n stockStability: Number(record.stockStability || 80),\n orderHistoryScore: Number(record.orderHistoryScore || 80),\n imageUrl: String(record.image || record.productImage || ''),\n tags: Array.isArray(record.tags) ? record.tags.map(String) : [],\n }));\n }\n\n async getStock(productId: string, variantId: string): Promise<{ stock: number; etaBusinessDays: number }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return {\n stock: deterministicNumber(`${productId}:${variantId}`, 12, 80),\n etaBusinessDays: deterministicNumber(`${variantId}:${productId}`, 2, 5),\n };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/product/query?pid=${productId}&vid=${variantId}`, {\n headers: {\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n });\n if (!response.ok) {\n throw new Error(`CJ stock request failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: Record<string, unknown> };\n return {\n stock: Number(payload.data?.sellableQuantity || payload.data?.stock || 0),\n etaBusinessDays: Number(payload.data?.deliveryDays || payload.data?.deliveryTime || 7),\n };\n }\n\n async placeOrder(params: { idempotencyKey: string; orderId: string; productId: string; variantId: string; quantity: number }): Promise<{ cjOrderId: string }> {\n if (!this.config.cj.accessToken || !this.config.cj.apiKey) {\n return { cjOrderId: `SIM-CJ-${shortHash(params.idempotencyKey)}` };\n }\n\n const response = await fetch(`${this.config.cj.apiBaseUrl}/order/create`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'CJ-Access-Token': this.config.cj.accessToken,\n 'CJ-Api-Key': this.config.cj.apiKey,\n },\n body: JSON.stringify(params),\n });\n if (!response.ok) {\n throw new Error(`CJ order placement failed with ${response.status}`);\n }\n const payload = (await response.json()) as { data?: { orderId?: string } };\n return { cjOrderId: payload.data?.orderId || `CJ-${shortHash(params.idempotencyKey)}` };\n }\n}\n\nexport class EbayClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n private get appId(): string | null {\n return this.config.ebay.env === 'sandbox' ? this.config.ebay.sandboxClientId || this.config.ebay.clientId : this.config.ebay.clientId;\n }\n\n async getMarketSnapshot(keyword: string): Promise<MarketSearchSnapshot> {\n if (!this.appId) {\n const sold30d = deterministicNumber(keyword, 18, 120);\n const sold7d = deterministicNumber(`7:${keyword}`, 4, 28);\n const uniqueSellers30d = deterministicNumber(`sellers:${keyword}`, 4, 22);\n const activeCount = deterministicNumber(`active:${keyword}`, 15, 180);\n const topRatedCount = deterministicNumber(`top:${keyword}`, 2, Math.max(2, Math.floor(activeCount / 2)));\n return {\n sold30d,\n sold7d,\n uniqueSellers30d,\n medianSoldPrice30d: round(deterministicNumber(`price:${keyword}`, 14, 26) + 0.99, 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n const findingBase =\n this.config.ebay.env === 'sandbox'\n ? 'https://svcs.sandbox.ebay.com/services/search/FindingService/v1'\n : 'https://svcs.ebay.com/services/search/FindingService/v1';\n const headers = {\n 'X-EBAY-SOA-SECURITY-APPNAME': this.appId,\n 'X-EBAY-SOA-RESPONSE-DATA-FORMAT': 'JSON',\n 'X-EBAY-SOA-GLOBAL-ID': 'EBAY-US',\n };\n\n const completedUrl = new URL(findingBase);\n completedUrl.searchParams.set('OPERATION-NAME', 'findCompletedItems');\n completedUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n completedUrl.searchParams.set('keywords', keyword);\n completedUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n completedUrl.searchParams.set('itemFilter(0).name', 'SoldItemsOnly');\n completedUrl.searchParams.set('itemFilter(0).value', 'true');\n completedUrl.searchParams.set('itemFilter(1).name', 'ListingType');\n completedUrl.searchParams.set('itemFilter(1).value(0)', 'FixedPrice');\n completedUrl.searchParams.set('itemFilter(2).name', 'LocatedIn');\n completedUrl.searchParams.set('itemFilter(2).value', 'US');\n completedUrl.searchParams.set('itemFilter(3).name', 'Condition');\n completedUrl.searchParams.set('itemFilter(3).value', '1000');\n\n const activeUrl = new URL(findingBase);\n activeUrl.searchParams.set('OPERATION-NAME', 'findItemsAdvanced');\n activeUrl.searchParams.set('SERVICE-VERSION', '1.13.0');\n activeUrl.searchParams.set('keywords', keyword);\n activeUrl.searchParams.set('paginationInput.entriesPerPage', '50');\n activeUrl.searchParams.set('itemFilter(0).name', 'ListingType');\n activeUrl.searchParams.set('itemFilter(0).value(0)', 'FixedPrice');\n activeUrl.searchParams.set('itemFilter(1).name', 'LocatedIn');\n activeUrl.searchParams.set('itemFilter(1).value', 'US');\n\n const [completedResponse, activeResponse] = await Promise.all([fetch(completedUrl, { headers }), fetch(activeUrl, { headers })]);\n if (!completedResponse.ok || !activeResponse.ok) {\n throw new Error(`eBay finding request failed (${completedResponse.status}/${activeResponse.status})`);\n }\n\n const completedPayload = (await completedResponse.json()) as Record<string, unknown>;\n const activePayload = (await activeResponse.json()) as Record<string, unknown>;\n\n const completedItems = extractFindingItems(completedPayload);\n const activeItems = extractFindingItems(activePayload);\n const soldPrices = completedItems.map((item) => item.price).filter((value) => Number.isFinite(value));\n const uniqueSellers = new Set(completedItems.map((item) => item.seller).filter(Boolean));\n const sold30d = completedItems.length;\n const sold7d = completedItems.filter((item) => Date.now() - item.endTime.getTime() <= 7 * 24 * 60 * 60 * 1000).length;\n const topRatedCount = activeItems.filter((item) => item.topRated).length;\n const activeCount = activeItems.length;\n\n return {\n sold30d,\n sold7d,\n uniqueSellers30d: uniqueSellers.size,\n medianSoldPrice30d: round(median(soldPrices), 2),\n activeCount,\n topRatedCount,\n eliteShare: round(topRatedCount / Math.max(1, activeCount), 2),\n competitionRatio: round(activeCount / Math.max(1, sold30d), 2),\n capturedAt: nowIso(),\n };\n }\n\n async validateCategoryPolicies(candidate: ProductCandidate): Promise<{ valid: boolean; warnings: string[]; categoryId: string }> {\n const warnings: string[] = [];\n const categoryId = mapCategoryId(candidate.categoryPath);\n\n if (!this.appId) {\n if (candidate.tags.some((tag) => tag.toLowerCase().includes('battery'))) {\n warnings.push('Battery-adjacent item requires manual category review.');\n }\n return { valid: warnings.length === 0, warnings, categoryId };\n }\n\n const base = this.config.ebay.env === 'sandbox' ? 'https://apim.sandbox.ebay.com' : 'https://apim.ebay.com';\n const response = await fetch(`${base}/sell/metadata/v1/marketplace/EBAY_US/get_category_policies?category_id=${categoryId}`);\n if (!response.ok) {\n warnings.push(`Metadata policy lookup returned ${response.status}.`);\n return { valid: false, warnings, categoryId };\n }\n const payload = (await response.json()) as Record<string, unknown>;\n const allowed = Boolean(payload.categoryId || payload.categoryPolicies);\n if (!allowed) {\n warnings.push('Category policies could not be validated.');\n }\n return { valid: allowed, warnings, categoryId };\n }\n\n async createOrReplaceInventoryItem(draft: { sellerSku: string; title: string; description: string; images: string[]; quantity: number }): Promise<{ inventoryItemId: string }> {\n return { inventoryItemId: `INV-${draft.sellerSku}` };\n }\n\n async publishOffer(draft: { sellerSku: string; price: number; categoryId: string }): Promise<{ offerId: string; publishedAt: string }> {\n return {\n offerId: `OFFER-${shortHash(`${draft.sellerSku}:${draft.price}:${draft.categoryId}`)}`,\n publishedAt: nowIso(),\n };\n }\n\n async updateInventoryQuantity(sellerSku: string, quantity: number): Promise<void> {\n void sellerSku;\n void quantity;\n }\n\n async withdrawOffer(offerId: string): Promise<void> {\n void offerId;\n }\n}\n\nfunction extractFindingItems(payload: Record<string, unknown>): Array<{ price: number; seller: string; endTime: Date; topRated: boolean }> {\n const root = Object.values(payload)[0] as Array<Record<string, unknown>> | undefined;\n const payloadRoot = root?.[0] || {};\n const searchResult = (payloadRoot.searchResult as Array<Record<string, unknown>> | undefined)?.[0];\n const items = (searchResult?.item as Array<Record<string, unknown>> | undefined) || [];\n return items.map((item) => {\n const sellingStatus = (item.sellingStatus as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const currentPrice = (sellingStatus.currentPrice as Array<Record<string, unknown>> | undefined)?.[0];\n const sellerInfo = (item.sellerInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n const listingInfo = (item.listingInfo as Array<Record<string, unknown>> | undefined)?.[0] || {};\n return {\n price: Number(currentPrice?.__value__ || currentPrice?.value || 0),\n seller: String(sellerInfo.sellerUserName?.[0] || ''),\n endTime: new Date(String(listingInfo.endTime?.[0] || nowIso())),\n topRated: sellerInfo.topRatedSeller?.[0] === 'true',\n };\n });\n}\n\nfunction mapCategoryId(categoryPath: string): string {\n const lowered = categoryPath.toLowerCase();\n if (lowered.includes('storage')) {\n return '20635';\n }\n if (lowered.includes('travel')) {\n return '300';\n }\n if (lowered.includes('pet')) {\n return '177';\n }\n return '11700';\n}\n\nexport class AIClient {\n constructor(private readonly config: RuntimeConfig) {}\n\n canSpend(state: AppState): boolean {\n const budgets = state.budgets;\n const month = nowIso().slice(0, 7);\n const day = nowIso().slice(0, 10);\n if (budgets.currentMonth !== month) {\n budgets.currentMonth = month;\n budgets.monthToDateUsd = 0;\n budgets.aiFallbackMode = false;\n }\n if (budgets.currentDay !== day) {\n budgets.currentDay = day;\n budgets.dayToDateUsd = 0;\n }\n return budgets.monthToDateUsd < budgets.monthlyCapUsd && budgets.dayToDateUsd < budgets.dailyCapUsd && !budgets.aiFallbackMode;\n }\n\n private charge(state: AppState, usd: number): void {\n state.budgets.monthToDateUsd = round(state.budgets.monthToDateUsd + usd, 4);\n state.budgets.dayToDateUsd = round(state.budgets.dayToDateUsd + usd, 4);\n if (state.budgets.monthToDateUsd >= state.budgets.monthlyCapUsd || state.budgets.dayToDateUsd >= state.budgets.dailyCapUsd) {\n state.budgets.aiFallbackMode = true;\n }\n }\n\n findCache(state: AppState, productFingerprint: string, kind: AIArtifactCacheEntry['kind']): AIArtifactCacheEntry | null {\n const now = Date.now();\n return state.aiCache.find((entry) => entry.productFingerprint === productFingerprint && entry.kind === kind && new Date(entry.expiresAt).getTime() > now) || null;\n }\n\n async generateListingPack(state: AppState, productFingerprint: string, candidate: ProductCandidate): Promise<ListingPack> {\n const cached = this.findCache(state, productFingerprint, 'listing_pack');\n if (cached) {\n return JSON.parse(cached.payload) as ListingPack;\n }\n\n let pack: ListingPack;\n let provider: 'deepseek' | 'gemini' | 'system' = 'system';\n if (this.canSpend(state) && this.config.ai.deepseekApiKey) {\n pack = await this.generateViaDeepSeek(candidate);\n provider = 'deepseek';\n this.charge(state, 0.03);\n } else if (this.canSpend(state) && this.config.ai.geminiApiKey) {\n pack = await this.generateViaGemini(candidate);\n provider = 'gemini';\n this.charge(state, 0.05);\n } else {\n pack = templateListingPack(candidate);\n }\n\n state.aiCache.unshift({\n id: createId('cache'),\n productFingerprint,\n kind: 'listing_pack',\n provider,\n payload: JSON.stringify(pack),\n expiresAt: daysFromNow(30),\n createdAt: nowIso(),\n });\n state.aiCache = state.aiCache.slice(0, 200);\n return pack;\n }\n\n private async generateViaDeepSeek(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.config.ai.deepseekApiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: this.config.ai.deepseekModel,\n response_format: { type: 'json_object' },\n messages: [\n {\n role: 'system',\n content:\n 'You write concise eBay listing packs for US consumers. Return JSON with title, subtitle, description, bullets, itemSpecifics, images.',\n },\n {\n role: 'user',\n content: `Create a listing pack for ${candidate.title} in category ${candidate.categoryPath} with target price ${candidate.targetPrice}.`,\n },\n ],\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n const content = payload.choices?.[0]?.message?.content;\n if (!content) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(content) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n\n private async generateViaGemini(candidate: ProductCandidate): Promise<ListingPack> {\n const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.config.ai.geminiModel}:generateContent?key=${this.config.ai.geminiApiKey}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [\n {\n parts: [\n {\n text: `Return JSON with title, subtitle, description, bullets, itemSpecifics, images for a US eBay listing about ${candidate.title} in ${candidate.categoryPath}.`,\n },\n ],\n },\n ],\n generationConfig: { responseMimeType: 'application/json' },\n }),\n });\n if (!response.ok) {\n return templateListingPack(candidate);\n }\n const payload = (await response.json()) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> };\n const text = payload.candidates?.[0]?.content?.parts?.[0]?.text;\n if (!text) {\n return templateListingPack(candidate);\n }\n try {\n return JSON.parse(text) as ListingPack;\n } catch {\n return templateListingPack(candidate);\n }\n }\n}\n\nfunction templateListingPack(candidate: ProductCandidate): ListingPack {\n const headline = candidate.title.length > 72 ? candidate.title.slice(0, 69).trimEnd() + '...' : candidate.title;\n return {\n title: `${headline} | US Fast Ship`,\n subtitle: 'Smart buy, everyday value.',\n description: `${candidate.title} is a simple, useful product selected for YuziGoods because it offers practical value, clean presentation, and reliable US delivery. Ships fast from a US warehouse.`,\n bullets: [\n 'US warehouse fulfillment with fast delivery promise',\n 'Simple, useful everyday product with low-friction setup',\n 'Selected for margin, delivery speed, and low return risk',\n 'Packed for YuziGoods US-first marketplace workflow',\n ],\n itemSpecifics: {\n Brand: 'YuziGoods',\n Type: candidate.categoryPath.split('>').slice(-1)[0]?.trim() || 'General',\n Color: 'As pictured',\n Material: 'Mixed materials',\n Country: 'United States warehouse',\n },\n images: [candidate.imageUrl],\n };\n}\n"}}}} |