| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- import 'dotenv/config';
- import { ParseRestClient } from '../src/db/parse-rest.client.js';
- import { loadConfig } from '../src/config/env.js';
- import { ParseRestListingAiRepository } from '../src/modules/listing-ai/repositories/parse-rest-listing-ai.repository.js';
- import { FmodeAiClient } from '../src/modules/ai-gateway/client.js';
- import { FmodeJdVocAiScoringProvider, ListingAiService } from '../src/modules/listing-ai/listing-ai.service.js';
- import { FmodeGeminiImageReviewProvider } from '../src/modules/listing-ai/image-review/gemini-image-review.provider.js';
- import { scoreJdVocRules } from '../src/modules/listing-ai/scoring/jd-voc-rule-engine.js';
- const args = new Set(process.argv.slice(2));
- const value = (prefix: string, fallback: string) => process.argv.slice(2).find((item) => item.startsWith(`${prefix}=`))?.slice(prefix.length + 1) ?? fallback;
- const apply = args.has('--apply');
- const includeAi = args.has('--ai');
- const includeImages = args.has('--images');
- const retryFailed = args.has('--retry-failed');
- const rollbackCheck = args.has('--rollback-check');
- const limit = Math.max(1, Math.min(625, Number(value('--limit', '100')) || 100));
- const cohort = value('--cohort', 'listing-jd-v3-formal-625');
- if (cohort !== 'listing-jd-v3-formal-625') throw new Error('unsupported_jd_voc_cohort');
- const config = loadConfig();
- if (config.storageDriver !== 'parse_rest') throw new Error('jd_voc_migration_requires_parse_rest');
- const client = new ParseRestClient({ serverUrl: config.parse.serverUrl, appId: config.parse.appId, masterKey: config.parse.masterKey, timeoutMs: config.parse.timeoutMs });
- const repository = new ParseRestListingAiRepository(client);
- const aiClient = new FmodeAiClient({ ...config.ai, timeoutMs: Math.min(config.ai.timeoutMs, 45_000) });
- const aiProvider = includeAi ? new FmodeJdVocAiScoringProvider(aiClient, config.listingAi.jdVocAiModel) : undefined;
- 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;
- const service = new ListingAiService(repository, undefined, () => new Date(), Math.min(config.listingAi.concurrency, 4), config.listingAi.maxAiItemsPerJob, aiProvider, imageProvider, config.listingAi.jdVocDisplayDefault, true);
- if (rollbackCheck) {
- const [legacy,jdVoc]=await Promise.all([repository.listCurrentScores(config.auth.defaultWorkspaceId),repository.listJdVocScores(config.auth.defaultWorkspaceId)]);
- 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));
- process.exit(legacy.length>0?0:2);
- }
- const sources = (await repository.listAllSources(config.auth.defaultWorkspaceId, 'jd')).slice(0, limit);
- const rows: Array<{ productId: string; sourceHash: string; executionKey: string | null; rules: string; ai: string; image: string; error: string | null }> = [];
- for (const source of sources) {
- try {
- const context = await repository.getJdVocRuleContext(source.workspaceId, source.platform, source.productId);
- const preview = scoreJdVocRules(source, context, { now: new Date().toISOString() });
- if (preview.sourceHash !== source.sourceHash) throw new Error('source_hash_mismatch');
- if (!apply) {
- 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 });
- continue;
- }
- const rules = await service.scoreJdVocRules({ workspaceId: source.workspaceId, platform: source.platform, productId: source.productId, context, force: retryFailed });
- let ai = 'skipped'; let image = 'skipped'; let error: string | null = null;
- if (includeAi) {
- const result = await service.scoreJdVocWithAi({ workspaceId: source.workspaceId, platform: source.platform, productId: source.productId, context, force: retryFailed });
- ai = result.hybrid ? 'completed' : result.errorCode ? 'failed' : 'blocked';
- error = result.errorCode;
- }
- if (includeImages) {
- const result = await service.reviewJdVocImages({ workspaceId: source.workspaceId, platform: source.platform, productId: source.productId, force: retryFailed });
- image = result.imageReview?.status ?? 'failed';
- }
- rows.push({ productId: source.productId, sourceHash: source.sourceHash, executionKey: rules.executionKey, rules: 'completed', ai, image, error });
- } catch (error) {
- 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' });
- }
- }
- const report = {
- mode: apply ? 'apply' : 'dry-run', cohort, requested: limit, resolved: sources.length,
- rulesCompleted: rows.filter((row) => row.rules === 'completed' || row.rules === 'dry-run').length,
- aiCompleted: rows.filter((row) => row.ai === 'completed').length,
- imageCompleted: rows.filter((row) => row.image === 'shadow_completed').length,
- failed: rows.filter((row) => row.error).length,
- rubricVersion: 'jd-voc-v0.5', slots: ['jd_voc_rules', 'jd_voc_hybrid_ai'],
- 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 })),
- };
- console.log(JSON.stringify(report, null, 2));
- if (report.resolved !== report.requested || report.failed) process.exitCode = 2;
|