| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768 |
- import 'dotenv/config';
- import { mkdir, writeFile } from 'node:fs/promises';
- import { dirname, resolve } from 'node:path';
- import { loadConfig } from '../src/config/env.js';
- import { ParseRestClient } from '../src/db/parse-rest.client.js';
- import type { JdVocAiScoreOutput, } from '../src/modules/listing-ai/scoring/jd-voc-ai-rubric.js';
- import type { JdVocDimensionKey, JdVocScoreResult } from '../src/modules/listing-ai/domain.js';
- import { ParseRestListingAiRepository } from '../src/modules/listing-ai/repositories/parse-rest-listing-ai.repository.js';
- import { composeJdVocHybridScore } from '../src/modules/listing-ai/scoring/jd-voc-ai-rubric.js';
- import { scoreJdVocRules } from '../src/modules/listing-ai/scoring/jd-voc-rule-engine.js';
- const EXPECTED = 625;
- const MODEL = 'codex-jd-voc-evaluator-v1';
- const SEMANTIC_DIMENSIONS: readonly Exclude<JdVocDimensionKey,'media'>[] = ['search','voc','selling','facts','competitive'];
- const LEVEL_FACTOR = { strong:1, pass:.9, weak:.55, fail:.2, unknown:0 } as const;
- const args = new Map(process.argv.slice(2).map((arg)=>{const [key,...rest]=arg.split('=');return[key!,rest.join('=')||'true'];}));
- const apply = args.get('--apply') === 'true';
- const concurrency = Math.max(1,Math.min(12,Number(args.get('--concurrency')??6)));
- function judge(baseline:JdVocScoreResult):JdVocAiScoreOutput{
- const assessments = SEMANTIC_DIMENSIONS.map((key)=>{
- const dimension=baseline.dimensions.find((item)=>item.key===key)!;
- const available=dimension.evidence.filter((item)=>item.outcome!=='unknown'&&item.maxPoints>0);
- const verifiedMax=available.reduce((sum,item)=>sum+item.maxPoints,0);
- const semanticEarned=available.reduce((sum,item)=>sum+item.maxPoints*LEVEL_FACTOR[item.level??(item.outcome==='pass'?'pass':'fail')],0);
- const score=verifiedMax?Math.round(semanticEarned/verifiedMax*dimension.maxScore*10)/10:0;
- const evidenceIds=available.sort((a,b)=>b.maxPoints-a.maxPoints||a.ruleId.localeCompare(b.ruleId)).slice(0,3).map((item)=>item.id);
- const counts={pass:available.filter((item)=>item.outcome==='pass').length,weak:available.filter((item)=>item.level==='weak').length,fail:available.filter((item)=>item.outcome==='fail'&&item.level!=='weak').length};
- return{dimension:key,score:evidenceIds.length?score:0,evidenceIds,rationale:`按固定证据校准:${counts.pass}项通过、${counts.weak}项部分满足、${counts.fail}项失败;仅使用该维度规则证据。`,confidence:Math.round(dimension.coverage)/100};
- });
- const suggestions=baseline.actions.filter((item)=>item.dimension!=='media'&&item.evidenceIds.length).slice(0,12).map((item)=>({title:item.title,action:item.action,evidenceIds:item.evidenceIds}));
- return{assessments,suggestions,summary:'Codex 按 jd-voc-v0.5 固定证据等级校准五个文本语义维度;media 保持规则分,未使用外部知识。',latencyMs:0};
- }
- async function concurrent<T>(items:T[],worker:(item:T,index:number)=>Promise<void>):Promise<void>{let cursor=0;await Promise.all(Array.from({length:Math.min(concurrency,items.length)},async()=>{while(cursor<items.length){const index=cursor++;await worker(items[index]!,index);}}));}
- const config=loadConfig();
- if(config.storageDriver!=='parse_rest')throw new Error('codex_jd_voc_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 sources=await repository.listAllSources(config.auth.defaultWorkspaceId,'jd');
- if(sources.length!==EXPECTED)throw new Error(`source_count_mismatch:${sources.length}:${EXPECTED}`);
- const existing=await repository.listJdVocScores(config.auth.defaultWorkspaceId);
- const existingRules=new Map(existing.filter((item)=>item.scoreKind==='jd_voc_rules').map((item)=>[item.productId,item]));
- const results:Array<{rules:JdVocScoreResult;hybrid:JdVocScoreResult}>=new Array(sources.length);
- await concurrent(sources,async(source,index)=>{
- const context=await repository.getJdVocRuleContext(source.workspaceId,source.platform,source.productId);
- let rules=scoreJdVocRules(source,context,{now:new Date().toISOString()});
- const previous=existingRules.get(source.productId);
- if(previous?.sourceHash===source.sourceHash&&previous.imageReview?.status==='shadow_completed')rules={...rules,imageReview:previous.imageReview};
- const hybrid=composeJdVocHybridScore({baseline:rules,output:judge(rules),model:MODEL,source,now:new Date().toISOString()});
- results[index]={rules,hybrid};
- });
- const invalid=results.filter(({hybrid})=>hybrid.dimensions.length!==6||hybrid.aiReview?.assessments.length!==5||hybrid.aiReview.assessments.some((item)=>(item.score??0)>0&&!item.evidenceIds.length));
- if(invalid.length)throw new Error(`invalid_codex_results:${invalid.length}`);
- const summary={mode:apply?'apply':'dry-run',model:MODEL,requested:EXPECTED,completed:results.length,invalid:invalid.length,rules:results.length,hybrid:results.length,blocked:results.filter(({rules})=>rules.coverage.status==='blocked').length,partial:results.filter(({hybrid})=>hybrid.coverage.status==='partial').length,averageHybrid:Math.round(results.reduce((sum,{hybrid})=>sum+(hybrid.overallScore??0),0)/results.length*10)/10};
- if(!apply){console.log(JSON.stringify(summary,null,2));process.exit();}
- const now=new Date().toISOString();
- const backupPath=resolve(args.get('--backup')??`logs/jd-voc-current-backup-${now.replace(/[:.]/g,'-')}.json`);
- await mkdir(dirname(backupPath),{recursive:true});
- await writeFile(backupPath,JSON.stringify({workspaceId:config.auth.defaultWorkspaceId,createdAt:now,scores:existing},null,2),'utf8');
- await concurrent(results,async({rules,hybrid})=>{await repository.upsertJdVocCurrentScore(rules);await repository.upsertJdVocCurrentScore(hybrid);});
- const persisted=await repository.listJdVocScores(config.auth.defaultWorkspaceId);
- const codex=persisted.filter((item)=>item.scoreKind==='jd_voc_hybrid_ai'&&item.aiReview?.model===MODEL);
- const byProduct=new Map(codex.map((item)=>[item.productId,item]));
- const mismatches=sources.filter((source)=>{const score=byProduct.get(source.productId);return!score||score.sourceHash!==source.sourceHash||score.dimensions.length!==6||score.aiReview?.assessments.length!==5;});
- console.log(JSON.stringify({...summary,backupPath,persistedRules:persisted.filter((item)=>item.scoreKind==='jd_voc_rules').length,persistedHybrid:persisted.filter((item)=>item.scoreKind==='jd_voc_hybrid_ai').length,codexVerified:codex.length,mismatches:mismatches.length},null,2));
- if(mismatches.length||codex.length!==EXPECTED)process.exitCode=2;
|