score-listings-ai.ts 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940
  1. import 'dotenv/config';
  2. import { loadConfig } from '../src/config/env.js';
  3. import { createDatabasePool } from '../src/db/pool.js';
  4. import { ParseRestClient } from '../src/db/parse-rest.client.js';
  5. import { ensureListingParseSchemas } from '../src/db/parse-rest.schema.js';
  6. import { FmodeAiClient } from '../src/modules/ai-gateway/client.js';
  7. import { FmodeListingAiScoringProvider, ListingAiService } from '../src/modules/listing-ai/listing-ai.service.js';
  8. import { ParseRestListingAiRepository } from '../src/modules/listing-ai/repositories/parse-rest-listing-ai.repository.js';
  9. import { PostgresListingAiRepository } from '../src/modules/listing-ai/repositories/postgres-listing-ai.repository.js';
  10. const args = new Map(process.argv.slice(2).map((arg) => { const [key, ...rest] = arg.split('='); return [key!, rest.join('=') || 'true']; }));
  11. async function main() {
  12. const config = loadConfig(); const workspaceId = args.get('--workspace') ?? config.auth.defaultWorkspaceId;
  13. const limit = Math.max(1, Math.min(10, Number(args.get('--limit') ?? 1)));
  14. let close = async () => {}; let repository;
  15. if (config.storageDriver === 'parse_rest') {
  16. const client = new ParseRestClient({ serverUrl: config.parse.serverUrl, appId: config.parse.appId, masterKey: config.parse.masterKey, timeoutMs: config.parse.timeoutMs });
  17. await ensureListingParseSchemas(client); repository = new ParseRestListingAiRepository(client);
  18. } else {
  19. const pool = createDatabasePool(config); repository = new PostgresListingAiRepository(pool); close = async () => pool.end();
  20. }
  21. const requestedIds = (args.get('--product-ids') ?? '').split(',').map((item) => item.trim()).filter(Boolean);
  22. 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);
  23. if (!sources.length) throw new Error('listing_ai_no_ready_sources');
  24. const provider = new FmodeListingAiScoringProvider(new FmodeAiClient(config.ai), config.listingAi.model);
  25. const service = new ListingAiService(repository, provider, () => new Date(), config.listingAi.concurrency, config.listingAi.maxAiItemsPerJob);
  26. const key = args.get('--idempotency-key') ?? `listing-jd-ai-v5-${Date.now()}`;
  27. try {
  28. 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' });
  29. await service.processJob(workspaceId, job.id);
  30. let completed = await repository.getJob(workspaceId, job.id);
  31. while (completed && ['queued', 'running'].includes(completed.status)) { await new Promise((resolve) => setTimeout(resolve, 1_000)); completed = await repository.getJob(workspaceId, job.id); }
  32. const results = await Promise.all(sources.map((source) => repository.getCurrentScore(workspaceId, source.productId, 'formal_ai')));
  33. 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));
  34. if (!completed || !['completed', 'partial'].includes(completed.status)) process.exitCode = 2;
  35. } finally { await close(); }
  36. }
  37. main().catch((error) => { console.error(`[score-listings-ai] ${error instanceof Error ? error.message : error}`); process.exitCode = 1; });