import 'dotenv/config'; import { loadConfig } from '../src/config/env.js'; import { createDatabasePool } from '../src/db/pool.js'; import { ParseRestClient } from '../src/db/parse-rest.client.js'; import { ensureListingParseSchemas } from '../src/db/parse-rest.schema.js'; import { FmodeAiClient } from '../src/modules/ai-gateway/client.js'; import { FmodeListingAiScoringProvider, ListingAiService } from '../src/modules/listing-ai/listing-ai.service.js'; import { ParseRestListingAiRepository } from '../src/modules/listing-ai/repositories/parse-rest-listing-ai.repository.js'; import { PostgresListingAiRepository } from '../src/modules/listing-ai/repositories/postgres-listing-ai.repository.js'; const args = new Map(process.argv.slice(2).map((arg) => { const [key, ...rest] = arg.split('='); return [key!, rest.join('=') || 'true']; })); async function main() { const config = loadConfig(); const workspaceId = args.get('--workspace') ?? config.auth.defaultWorkspaceId; const limit = Math.max(1, Math.min(10, Number(args.get('--limit') ?? 1))); let close = async () => {}; let repository; if (config.storageDriver === 'parse_rest') { const client = new ParseRestClient({ serverUrl: config.parse.serverUrl, appId: config.parse.appId, masterKey: config.parse.masterKey, timeoutMs: config.parse.timeoutMs }); await ensureListingParseSchemas(client); repository = new ParseRestListingAiRepository(client); } else { const pool = createDatabasePool(config); repository = new PostgresListingAiRepository(pool); close = async () => pool.end(); } const requestedIds = (args.get('--product-ids') ?? '').split(',').map((item) => item.trim()).filter(Boolean); const sources = (await repository.listAllSources(workspaceId, 'jd')).filter((source) => requestedIds.length ? requestedIds.includes(source.productId) : source.detailStatus === 'available' && Boolean(source.marketing?.sellingPoints.length) && Boolean(source.categoryContext?.ruleVersion) && Boolean(source.vocEvidence?.length) && Boolean(source.descriptionStructure?.observed)).slice(0, limit); if (!sources.length) throw new Error('listing_ai_no_ready_sources'); const provider = new FmodeListingAiScoringProvider(new FmodeAiClient(config.ai), config.listingAi.model); const service = new ListingAiService(repository, provider, () => new Date(), config.listingAi.concurrency, config.listingAi.maxAiItemsPerJob); const key = args.get('--idempotency-key') ?? `listing-jd-ai-v5-${Date.now()}`; try { const job = await service.enqueueScoreJob({ workspaceId, platform: 'jd', scope: { mode: 'selected', productIds: sources.map((source) => source.productId) }, includeAiSuggestions: true, rescorePolicy: args.get('--force') === 'true' ? 'force' : 'reuse', idempotencyKey: key, requestedBy: 'listing-ai-score-script' }); await service.processJob(workspaceId, job.id); let completed = await repository.getJob(workspaceId, job.id); while (completed && ['queued', 'running'].includes(completed.status)) { await new Promise((resolve) => setTimeout(resolve, 1_000)); completed = await repository.getJob(workspaceId, job.id); } const results = await Promise.all(sources.map((source) => repository.getCurrentScore(workspaceId, source.productId, 'formal_ai'))); console.log(JSON.stringify({ job: completed, results: results.map((result) => result ? { productId: result.productId, overallScore: result.overallScore, aiStatus: result.aiStatus, model: result.model } : null) }, null, 2)); if (!completed || !['completed', 'partial'].includes(completed.status)) process.exitCode = 2; } finally { await close(); } } main().catch((error) => { console.error(`[score-listings-ai] ${error instanceof Error ? error.message : error}`); process.exitCode = 1; });