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[] = ['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(items:T[],worker:(item:T,index:number)=>Promise):Promise{let cursor=0;await Promise.all(Array.from({length:Math.min(concurrency,items.length)},async()=>{while(cursoritem.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;