// Read-only inventory for the project work report. Never fetch identity/config tables. import fs from 'node:fs'; import crypto from 'node:crypto'; import {request,classRows} from '../server/scripts/platform-client.mjs'; const schemas=(await request('GET','/schemas')).results; const groups={ 品类与竞品:['KfCategory','KfCategoryRelation','KfProduct','KfSku','KfRetailPriceSnapshot'], 消费者与证据:['KfRawXhsNote','KfRawXhsComment','KfRawDouyinVideo','KfRawDouyinComment','KfRawTaobaoItem','KfRawTaobaoReview','KfRawJdItem','KfRawJdReview','KfRawBilibiliPost','KfEvidence','KfEvidenceLink','KfInsight'], 市场与验证:['KfMarketMetric','KfAssessment'], 原料工艺与供应链:['KfMaterial','KfProcess','KfProcessStep','KfStandard','KfSupplier','KfSupplyQuote','KfRaw1688Offer'], 工作坊与成果:['KfSession','KfArtifact','KfOpportunity','KfProject','KfConcept','KfFormulaItem','KfStageEvent'], 来源与采集:['KfSource','KfVocRun','KfImportJob','KfImportItem'], 追问与问卷:['KfReviewIssue','KfQuestionnaireQuestion','KfQuestionnaireAnswer'] }; const wanted=['objectId','dataMode','recordState','title','name','sourceKind','platform','sourceRef','sourceIds','externalObjectId','idOrigin','reportType','artifactType','stageCode','dashboardState','progressState','pendingQuestions','status','version','content','demandSummary','targetAudience','usageScenario','specification','description','quote','text','sourceUrl','observedRetailPrice','priceObservedAt','unit','value','metricName','question','answer','rawAnswer','code','sourceRefs','evidenceRefs','reportContentHash','summary','rawText','sources','verificationState','body','evidenceLevel','claimKind','report','stage','questionList']; const result={at:new Date().toISOString(),readOnly:true,businessWrites:0,groups,classes:[],functionStatus:[],publicBoard:{}}; const names=[...new Set(Object.values(groups).flat())]; for(let offset=0;offset{ const schema=schemas.find(x=>x.className===name);if(!schema)return{name,exists:false,total:0,complete:true}; const keys=wanted.filter(k=>schema.fields[k]||k==='objectId'),rows=[];let after=null,total=0,complete=false; for(let page=0;page<10;page++){ const q=new URLSearchParams({keys:keys.join(','),limit:'1000',order:'objectId',...(page===0?{count:'1'}:{}),...(after?{where:JSON.stringify({objectId:{$gt:after}})}:{})}); const response=await request('GET','/classes/'+name+'?'+q);if(page===0)total=response.count; if(!Array.isArray(response.results))throw Error('BAD_RESULTS');rows.push(...response.results); if(response.results.length<1000){complete=true;break;}after=response.results.at(-1).objectId; } const modes={},states={};for(const row of rows){modes[row.dataMode||'unmarked']=(modes[row.dataMode||'unmarked']||0)+1;states[row.recordState||'unmarked']=(states[row.recordState||'unmarked']||0)+1;} return{name,exists:true,total,read:rows.length,complete,modes,states,rows}; })); settled.forEach((x,i)=>result.classes.push(x.status==='fulfilled'?x.value:{name:names[offset+i],error:'READ_FAILED'})); } const functions=await classRows('Function'); for(const name of ['ksfStartWorkshopSession','ksfGetIngestContract','ksfRegisterSource','ksfSubmitImport','ksfCheckDuplicates']){ const rows=functions.filter(x=>x.name===name&&x.path==='/'+name);result.functionStatus.push({name,found:rows.length===1,...(rows.length===1?{sha256:crypto.createHash('sha256').update(rows[0].code).digest('hex')}:{})}); } const response=await fetch('https://kangshifu.fmode.cn/api/functions',{method:'POST',headers:{'Content-Type':'application/json','X-Parse-Application-Id':'FKieRbIoGt_main'},body:JSON.stringify({path:'/ksfGetPublicBoard',params:{limit:500}}),redirect:'error',signal:AbortSignal.timeout(30000)}); const publicResult=(await response.json()).result; result.publicBoard={http:response.status,ok:publicResult?.ok===true,asOf:publicResult?.data?.asOf,nextCursor:publicResult?.data?.nextCursor||null,counts:Object.fromEntries(Object.entries(publicResult?.data||{}).filter(([k,v])=>Array.isArray(v)).map(([k,v])=>[k,v.length]))}; fs.writeFileSync('work/workshop-report-inventory-20261102.json',JSON.stringify(result,null,2),{mode:0o600}); console.log(JSON.stringify({...result,classes:result.classes.map(({rows,...x})=>({...x,titles:rows?.map(r=>r.title||r.name||r.question||r.metricName).filter(Boolean).slice(0,8)}))},null,2));