workshop-first-live-import.mjs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. // Import the captured official-page snapshots through the existing secure Skill.
  2. // Collection was performed by Codex; the workshop Agent imports archived real sources.
  3. import fs from 'node:fs';
  4. import path from 'node:path';
  5. import crypto from 'node:crypto';
  6. import {fileURLToPath} from 'node:url';
  7. import {spawnSync} from 'node:child_process';
  8. export const TASK_KEY='KSF-真实资料-20261102-01';
  9. const check=(ok,code='LIVE_IMPORT_MISMATCH')=>{if(!ok)throw Object.assign(Error(code),{code});};
  10. const safe=e=>/^[A-Z][A-Z0-9_]+$/.test(e?.code||'')?e.code:'LIVE_IMPORT_FAILED';
  11. const canonical=v=>v&&typeof v==='object'?Array.isArray(v)?'['+v.map(canonical).join(',')+']':'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canonical(v[k])).join(',')+'}':JSON.stringify(v);
  12. export function validateLiveContext(c){
  13. check(c&&Object.keys(c).length===3&&Object.keys(c).every(k=>['version','taskKey','agentProfileKey'].includes(k)),'INVALID_LIVE_CONTEXT');
  14. check(c.version==='ksf.first-live-import.v1'&&c.taskKey===TASK_KEY&&/^agent-account:[a-f0-9]{64}$/.test(c.agentProfileKey||''),'INVALID_LIVE_CONTEXT');return c;
  15. }
  16. export async function runFirstLiveImport({context,bundle,invoke,checkpoint=()=>{}}){
  17. validateLiveContext(context);
  18. check(bundle?.version==='ksf.first-live-source-batch.v1'&&bundle.taskKey===TASK_KEY&&bundle.batches?.length===2&&bundle.facts?.length===9,'INVALID_LIVE_BUNDLE');
  19. const ids=[],results=[];
  20. const run=async(command,input,suffix)=>{
  21. const requestId=TASK_KEY+':'+suffix;
  22. checkpoint({status:'in-flight',command,requestId,imports:structuredClone(ids)});
  23. const r=await invoke(command,input,requestId);
  24. checkpoint({status:'received',command,requestId,result:r,imports:structuredClone(ids)});return r;
  25. };
  26. const contract=await run('workshop-contract',{},'contract');
  27. check(contract.deduplication?.schema==='ksf.ingest-dedup.v1','DEDUP_CAPABILITY_UNAVAILABLE');
  28. // Validate the entire delivery before issuing any import.
  29. for(const batch of bundle.batches){
  30. const {task,capture}=batch;
  31. check(task?.source?.dataMode==='live'&&task.source.sourceKind==='web'&&task.source.sourceUrl===capture.url&&task.source.collectedAt===capture.collectedAt,'INVALID_LIVE_BUNDLE');
  32. check(['https://www.uni-president.com.cn/products-jiangbanmian.asp','https://www.uni-president.com.cn/products-qiaomianguan.asp'].includes(capture.url),'INVALID_LIVE_BUNDLE');
  33. check(crypto.createHash('sha256').update(task.source.rawText).digest('hex')===capture.rawTextSha256,'LIVE_SOURCE_HASH_MISMATCH');
  34. for(const row of task.records){
  35. check(['KfCategory','KfProduct','KfEvidence'].includes(row.className)&&row.data.dataMode==='live','INVALID_LIVE_BUNDLE');
  36. if(row.className==='KfEvidence')check(task.source.rawText.includes(row.data.quote)&&row.data.locator.url===capture.url&&row.data.productRef?.$item,'LIVE_QUOTE_MISMATCH');
  37. }
  38. }
  39. for(const batch of bundle.batches){
  40. const {task,key}=batch;
  41. const preflight=await run('workshop-dedup',task,key+':preflight');
  42. check(preflight.readOnly===true&&preflight.schema==='ksf.ingest-dedup.v1','DEDUP_RECEIPT_MISMATCH');
  43. const result=await run('workshop-import',task,key+':import');
  44. ids.push({key,sourceId:result.sourceId,items:result.items});
  45. checkpoint({status:'import-recorded',imports:structuredClone(ids)});
  46. check(result.status==='succeeded'&&result.dataMode==='live'&&result.formalReportPublished===false&&result.sourceId&&result.items?.length===task.records.length,'LIVE_IMPORT_RECEIPT_MISMATCH');
  47. const map=new Map(result.items.map(x=>[x.itemKey,x]));check(map.size===task.records.length,'LIVE_IMPORT_RECEIPT_MISMATCH');
  48. const source=await run('workshop-read',{className:'KfSource',objectId:result.sourceId},key+':read-source');
  49. for(const field of ['rawText','sourceUrl','sourceKind','dataMode'])check(source.record?.[field]===task.source[field],'LIVE_SOURCE_READBACK_MISMATCH');
  50. for(const row of task.records){
  51. const saved=map.get(row.itemKey);check(saved?.className===row.className&&saved.status==='succeeded'&&saved.objectId&&['created','unchanged'].includes(saved.operation),'LIVE_IMPORT_RECEIPT_MISMATCH');
  52. const {record}=await run('workshop-read',{className:row.className,objectId:saved.objectId},key+':read-'+row.itemKey);
  53. check(record?.objectId===saved.objectId&&record.dataMode==='live','LIVE_ENTITY_READBACK_MISMATCH');
  54. for(const [field,value]of Object.entries(row.data)){
  55. if(value?.$item)check(record[field]?.objectId===map.get(value.$item)?.objectId,'LIVE_REFERENCE_MISMATCH');
  56. else if(contract.allowedClasses?.find(c=>c.className===row.className)?.fields?.find(f=>f.name===field)?.type==='Date')check(Date.parse(record[field]?.iso||record[field])===Date.parse(value),'LIVE_ENTITY_READBACK_MISMATCH');
  57. else check(canonical(record[field])===canonical(value),'LIVE_ENTITY_READBACK_MISMATCH');
  58. }
  59. if(row.className!=='KfCategory')check(record.sourceRef?.objectId===result.sourceId,'LIVE_SOURCE_REFERENCE_MISMATCH');
  60. }
  61. results.push({key,sourceId:result.sourceId,sourceOperation:result.sourceOperation,counts:result.counts,items:result.items});
  62. }
  63. const categoryIds=results.map(r=>r.items.find(i=>i.className==='KfCategory')?.objectId);
  64. check(categoryIds[0]&&categoryIds[0]===categoryIds[1],'LIVE_CATEGORY_REUSE_MISMATCH');
  65. const receipt={ok:true,operation:'first-live-official-sources-imported',taskKey:TASK_KEY,dataMode:'live',sourcePageCount:2,productEntryCount:9,evidenceCount:9,categoryId:categoryIds[0],imports:results,sourceAndEntityReadbackVerified:true,collectionPerformedBy:'Codex',importExecutedBy:'current workshop Agent via secure entry',ecommerceOrSocialCollected:false,formalReportPublished:false,pendingQuestions:bundle.pendingQuestions};
  66. checkpoint({status:'complete',receipt});return receipt;
  67. }
  68. if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
  69. try{
  70. const args=process.argv.slice(2);check(args.length===5&&args[0]==='--input'&&args[2]==='--state-dir'&&args[4]==='--execute','USAGE');
  71. check(process.platform==='linux'&&process.geteuid()===10000,'HERMES_UID_REQUIRED');
  72. const input=args[1],state=args[3],parent=path.dirname(state);
  73. check(path.isAbsolute(input)&&path.isAbsolute(state),'ABSOLUTE_PATH_REQUIRED');
  74. for(const p of [input,parent])check(fs.realpathSync(p)===p,'UNSAFE_LIVE_PATH');
  75. for(const [p,mode]of [[input,0o600],[parent,0o700]]){const s=fs.statSync(p);check(s.uid===process.geteuid()&&(s.mode&0o777)===mode,'PRIVATE_LIVE_PATH_REQUIRED');}
  76. const context=validateLiveContext(JSON.parse(fs.readFileSync(input,'utf8')));
  77. const bundlePath=path.join(path.dirname(fileURLToPath(import.meta.url)),'first-live-batch.json');
  78. const bytes=fs.readFileSync(bundlePath);
  79. const expected='0118c861ae9a7da5e4d361497050200a2e4e595c7a2d76c733a22970cd179326';
  80. check(crypto.createHash('sha256').update(bytes).digest('hex')===expected,'LIVE_BUNDLE_HASH_MISMATCH');
  81. const bundle=JSON.parse(bytes),worker='/opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs';
  82. check(fs.statSync(worker).isFile(),'SECURE_ENTRY_UNAVAILABLE');
  83. check(!fs.existsSync(state),'LIVE_STATE_EXISTS');fs.mkdirSync(state,{mode:0o700});
  84. const journal={version:'ksf.first-live-journal.v1',at:new Date().toISOString(),taskKey:TASK_KEY,status:'running',steps:[]};
  85. const persist=()=>fs.writeFileSync(path.join(state,'journal.json'),JSON.stringify(journal,null,2),{mode:0o600});persist();
  86. let count=0;
  87. const invoke=async(command,task,requestId)=>{
  88. check(['workshop-contract','workshop-dedup','workshop-import','workshop-read'].includes(command),'COMMAND_NOT_ALLOWED');
  89. const name=String(++count).padStart(2,'0')+'-'+command,file=path.join(state,name+'.input.json');
  90. fs.writeFileSync(file,JSON.stringify(task,null,2),{mode:0o600,flag:'wx'});
  91. const p=spawnSync(process.execPath,[worker,command,...(command==='workshop-import'?['--agent-profile',context.agentProfileKey]:[]),'--input',file,'--request-id',requestId],{encoding:'utf8',timeout:660000,maxBuffer:4*1024*1024,windowsHide:true});
  92. fs.writeFileSync(path.join(state,name+'.transport.json'),JSON.stringify({exitCode:p.status,signal:p.signal,spawnFailed:!!p.error,stdout:p.stdout||'',stderr:p.stderr||''}),{mode:0o600,flag:'wx'});
  93. check(!p.error&&!p.signal,'LIVE_REQUEST_UNCERTAIN');let receipt;
  94. try{receipt=JSON.parse(p.stdout?.trim()?p.stdout:p.stderr);}catch{check(false,'LIVE_RECEIPT_INVALID');}
  95. fs.writeFileSync(path.join(state,name+'.receipt.json'),JSON.stringify(receipt,null,2),{mode:0o600,flag:'wx'});
  96. if(!(p.status===0&&receipt?.ok===true&&receipt.entryResult?.ok===true)){const code=receipt?.entryResult?.error?.code||receipt?.error?.code;check(false,/^[A-Z][A-Z0-9_]+$/.test(code||'')?code:'LIVE_COMMAND_FAILED');}
  97. check(receipt.command===command&&receipt.requestId===requestId&&receipt.entryResult.command===command&&receipt.entryResult.requestId===requestId,'LIVE_RECEIPT_BINDING_MISMATCH');
  98. return receipt.entryResult.result;
  99. };
  100. try{
  101. const receipt=await runFirstLiveImport({context,bundle,invoke,checkpoint:e=>{journal.status=e.status;journal.steps.push(e);persist();}});
  102. fs.writeFileSync(path.join(state,'summary.json'),JSON.stringify(receipt,null,2),{mode:0o600,flag:'wx'});console.log(JSON.stringify(receipt,null,2));
  103. }catch(e){journal.status='stopped';journal.error={code:safe(e),message:'保留原键及现场,错误不代表零写入;不重试不删目录不换键。'};persist();throw e;}
  104. }catch(e){console.error(JSON.stringify({ok:false,error:{code:safe(e),message:'真实资料导入停止;保留编号与回执,不输出原始异常或凭据。'}}));process.exitCode=1;}
  105. }