migrate-jd-voc-score-slots.ts 5.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import 'dotenv/config';
  2. import { ParseRestClient } from '../src/db/parse-rest.client.js';
  3. import { loadConfig } from '../src/config/env.js';
  4. import { ParseRestListingAiRepository } from '../src/modules/listing-ai/repositories/parse-rest-listing-ai.repository.js';
  5. import { FmodeAiClient } from '../src/modules/ai-gateway/client.js';
  6. import { FmodeJdVocAiScoringProvider, ListingAiService } from '../src/modules/listing-ai/listing-ai.service.js';
  7. import { FmodeGeminiImageReviewProvider } from '../src/modules/listing-ai/image-review/gemini-image-review.provider.js';
  8. import { scoreJdVocRules } from '../src/modules/listing-ai/scoring/jd-voc-rule-engine.js';
  9. const args = new Set(process.argv.slice(2));
  10. const value = (prefix: string, fallback: string) => process.argv.slice(2).find((item) => item.startsWith(`${prefix}=`))?.slice(prefix.length + 1) ?? fallback;
  11. const apply = args.has('--apply');
  12. const includeAi = args.has('--ai');
  13. const includeImages = args.has('--images');
  14. const retryFailed = args.has('--retry-failed');
  15. const rollbackCheck = args.has('--rollback-check');
  16. const limit = Math.max(1, Math.min(625, Number(value('--limit', '100')) || 100));
  17. const cohort = value('--cohort', 'listing-jd-v3-formal-625');
  18. if (cohort !== 'listing-jd-v3-formal-625') throw new Error('unsupported_jd_voc_cohort');
  19. const config = loadConfig();
  20. if (config.storageDriver !== 'parse_rest') throw new Error('jd_voc_migration_requires_parse_rest');
  21. const client = new ParseRestClient({ serverUrl: config.parse.serverUrl, appId: config.parse.appId, masterKey: config.parse.masterKey, timeoutMs: config.parse.timeoutMs });
  22. const repository = new ParseRestListingAiRepository(client);
  23. const aiClient = new FmodeAiClient({ ...config.ai, timeoutMs: Math.min(config.ai.timeoutMs, 45_000) });
  24. const aiProvider = includeAi ? new FmodeJdVocAiScoringProvider(aiClient, config.listingAi.jdVocAiModel) : undefined;
  25. const imageProvider = includeImages ? new FmodeGeminiImageReviewProvider({ baseUrl: process.env.FMODE_LLM_BASE_URL ?? config.ai.baseUrl, token: process.env.FMODE_LLM_API_KEY ?? config.ai.token, timeoutMs: 45_000 }) : undefined;
  26. const service = new ListingAiService(repository, undefined, () => new Date(), Math.min(config.listingAi.concurrency, 4), config.listingAi.maxAiItemsPerJob, aiProvider, imageProvider, config.listingAi.jdVocDisplayDefault, true);
  27. if (rollbackCheck) {
  28. const [legacy,jdVoc]=await Promise.all([repository.listCurrentScores(config.auth.defaultWorkspaceId),repository.listJdVocScores(config.auth.defaultWorkspaceId)]);
  29. console.log(JSON.stringify({mode:'rollback-check',workspaceId:config.auth.defaultWorkspaceId,legacyRows:legacy.length,jdVocRows:jdVoc.length,legacyReadable:legacy.length>0,jdVocRules:jdVoc.filter((item)=>item.scoreKind==='jd_voc_rules').length,jdVocHybrid:jdVoc.filter((item)=>item.scoreKind==='jd_voc_hybrid_ai').length,destructiveActions:0},null,2));
  30. process.exit(legacy.length>0?0:2);
  31. }
  32. const sources = (await repository.listAllSources(config.auth.defaultWorkspaceId, 'jd')).slice(0, limit);
  33. const rows: Array<{ productId: string; sourceHash: string; executionKey: string | null; rules: string; ai: string; image: string; error: string | null }> = [];
  34. for (const source of sources) {
  35. try {
  36. const context = await repository.getJdVocRuleContext(source.workspaceId, source.platform, source.productId);
  37. const preview = scoreJdVocRules(source, context, { now: new Date().toISOString() });
  38. if (preview.sourceHash !== source.sourceHash) throw new Error('source_hash_mismatch');
  39. if (!apply) {
  40. rows.push({ productId: source.productId, sourceHash: source.sourceHash, executionKey: preview.executionKey, rules: 'dry-run', ai: includeAi ? 'dry-run' : 'skipped', image: includeImages ? 'dry-run' : 'skipped', error: null });
  41. continue;
  42. }
  43. const rules = await service.scoreJdVocRules({ workspaceId: source.workspaceId, platform: source.platform, productId: source.productId, context, force: retryFailed });
  44. let ai = 'skipped'; let image = 'skipped'; let error: string | null = null;
  45. if (includeAi) {
  46. const result = await service.scoreJdVocWithAi({ workspaceId: source.workspaceId, platform: source.platform, productId: source.productId, context, force: retryFailed });
  47. ai = result.hybrid ? 'completed' : result.errorCode ? 'failed' : 'blocked';
  48. error = result.errorCode;
  49. }
  50. if (includeImages) {
  51. const result = await service.reviewJdVocImages({ workspaceId: source.workspaceId, platform: source.platform, productId: source.productId, force: retryFailed });
  52. image = result.imageReview?.status ?? 'failed';
  53. }
  54. rows.push({ productId: source.productId, sourceHash: source.sourceHash, executionKey: rules.executionKey, rules: 'completed', ai, image, error });
  55. } catch (error) {
  56. rows.push({ productId: source.productId, sourceHash: source.sourceHash, executionKey: null, rules: 'failed', ai: 'skipped', image: 'skipped', error: error instanceof Error ? error.message.slice(0, 120) : 'migration_failed' });
  57. }
  58. }
  59. const report = {
  60. mode: apply ? 'apply' : 'dry-run', cohort, requested: limit, resolved: sources.length,
  61. rulesCompleted: rows.filter((row) => row.rules === 'completed' || row.rules === 'dry-run').length,
  62. aiCompleted: rows.filter((row) => row.ai === 'completed').length,
  63. imageCompleted: rows.filter((row) => row.image === 'shadow_completed').length,
  64. failed: rows.filter((row) => row.error).length,
  65. rubricVersion: 'jd-voc-v0.5', slots: ['jd_voc_rules', 'jd_voc_hybrid_ai'],
  66. samples: rows.slice(0, 5).map((row) => ({ productId: row.productId, sourceHash: row.sourceHash, executionKey: row.executionKey, rules: row.rules, ai: row.ai, image: row.image, error: row.error })),
  67. };
  68. console.log(JSON.stringify(report, null, 2));
  69. if (report.resolved !== report.requested || report.failed) process.exitCode = 2;