// Full sample acceptance, executed on the real workshop Agent after tool registration. // No Agent/employee identity creation here; no credentials in inputs or output. import fs from 'node:fs'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; import {spawnSync} from 'node:child_process'; const fail=code=>{throw Object.assign(Error(code),{code});}; const check=(ok,code='ACCEPTANCE_MISMATCH')=>{if(!ok)fail(code);}; const KEY='KSF-企微实测-20261101-K'; export function validateContext(c){ check(c&&typeof c==='object'&&!Array.isArray(c),'INVALID_ACCEPTANCE_CONTEXT'); check(Object.keys(c).every(k=>['version','workshopKey','agentProfileKey','participantProfileKey','conversationKey','groupId'].includes(k)),'INVALID_ACCEPTANCE_CONTEXT'); check(c.version==='ksf.full-workshop-acceptance.v1'&&c.workshopKey===KEY,'INVALID_ACCEPTANCE_CONTEXT'); check(/^agent-account:[a-f0-9]{64}$/.test(c.agentProfileKey)&&/^participant-account:[a-f0-9]{64}$/.test(c.participantProfileKey),'TRUSTED_PROFILE_KEYS_REQUIRED'); check(typeof c.conversationKey==='string'&&c.conversationKey.trim()&&c.conversationKey.length<=200,'CURRENT_CONVERSATION_REQUIRED'); check(c.groupId===undefined||typeof c.groupId==='string','INVALID_ACCEPTANCE_CONTEXT');return c; } export async function runFullChain({context,invoke,publicRead,checkpoint=()=>{}}){ const c=validateContext(context),key=c.workshopKey,results={},artifacts=[],ids={}; const run=async(command,task,suffix)=>{ const requestId=key+':'+suffix;checkpoint({status:'in-flight',command,requestId,ids:{...ids}}); const result=await invoke(command,task,requestId); results[suffix]=result;checkpoint({status:'received',command,requestId,result,ids:{...ids}});return result; }; const contract=await run('workshop-contract',{},'contract'); check(contract.workshop?.identitySchema==='ksf.agent-self.v1'&&contract.workshop?.channelIdentitySchema==='ksf.wecom-aibot-sender.v1','IDENTITY_CAPABILITY_UNAVAILABLE'); const start=await run('workshop-start',{workshopKey:key,participant:{profileKey:c.participantProfileKey},conversation:{key:c.conversationKey,channel:'wecom'},groupId:c.groupId||'',dataMode:'sample'},'start'); ids.sessionId=start.sessionId;check(ids.sessionId,'MISSING_SESSION_ID'); const imported=await run('workshop-import',{source:{title:'【样例】全流程验收素材定义,无真实产品/工厂依据',sourceKind:'dataset',dataMode:'sample',rawText:'仅为链路验收构造。品类:拌面;产品:演练拌面占位;包装:袋装待验证;原料:面粉占位;工艺:混合占位。所有人群、需求、规格、配方、工艺、成本均未验证。'},records:[ {itemKey:'category',className:'KfCategory',externalKey:key+':category',data:{dataMode:'sample',code:'KSF-K-20261101-NOODLE',name:'【样例】拌面验收分类',taxonomyVersion:'ksf-acceptance-v1',axis:'产品形态',scopeNote:'构造分类,仅供工作坊链路验证'}}, {itemKey:'product',className:'KfProduct',externalKey:key+':product',data:{dataMode:'sample',name:'【样例】演练拌面占位',brandName:'演练占位品牌',categoryRef:{$item:'category'},isOwnBrand:false,productLevel:'product',packaging:'袋装待验证',sellingPoints:['待验证占位'],targetAudience:'待研究',usageScenarios:['待验证场景']}}, {itemKey:'material',className:'KfMaterial',externalKey:key+':material',data:{dataMode:'sample',name:'【样例】面粉关联占位',materialType:'ingredient',specification:'未知,未获供应链依据',function:'检查配料引用'}}, {itemKey:'process',className:'KfProcess',externalKey:key+':process',data:{dataMode:'sample',name:'【样例】混合路线占位',processType:'mixing',processKind:'route',description:'无设备/温度/时间依据,仅检查工艺引用'}} ]},'library'); for(const name of ['category','product','material','process']){ids[name+'Id']=imported.items?.find(x=>x.itemKey===name)?.objectId;check(ids[name+'Id'],'IMPORT_RECEIPT_MISMATCH');} const search=async(className,keyword,suffix)=>run('workshop-search',{className,keyword,filters:{dataMode:'sample'},limit:100},suffix); const materialSearch=await search('KfMaterial','面粉关联占位','search-material'); check(materialSearch.records?.some(r=>r.objectId===ids.materialId),'MATERIAL_SEARCH_MISMATCH'); const task=(stage,title,type,projectId,questions)=>({sessionId:ids.sessionId,...(projectId?{projectId}:{}),taskKey:key+':'+stage+(projectId?':project':''),version:1,stageCode:stage,title:'【样例】'+title,artifactType:type,category:'拌面',rawText:'构造的全流程验收材料:'+title+'。无真实消费者资料,不能作为市场判断。',sourceIds:[],pendingQuestions:questions,report:{executive:['构造样例,仅验证数字人—员工—项目—阶段—方案—看板关联;未经员工正式确认。'],chapters:[{id:stage.toLowerCase(),title,stage,sections:[{kind:'para',title:'本阶段成果',text:'本阶段仅记录待验证事项,不构成市场结论、企业审批或量产判断。'},{kind:'table',title:'待验证清单',data:{cols:['问题','状态'],rows:questions.map(q=>[q,'待验证'])}}]}]}}); const save=async(stage,title,type,projectId,suffix,questions,extra={})=>{const r=await run('workshop-save',{...task(stage,title,type,projectId,questions),...extra},suffix);check(r.artifactId,'MISSING_ARTIFACT_ID');artifacts.push({stage,artifactId:r.artifactId,projectId:projectId||null});return r;}; await save('S1','面类边界与竞品待查清单','category_report',null,'s1',['拌面/汤面/湿面分类口径待确认','品牌/SKU/规格/包装/渠道价格待采集']); await save('S2','人群场景与痛点待验证','pain_list',null,'s2',['真实消费者原文尚未采集','目标人群、场景、痛点和需求强度待验证']); const opportunity=await save('S3','待检索选择的机会卡','opportunity_card',null,'s3',['赛道×用户×场景假设待验证'],{opportunity:{key:key+':opportunity',title:'【样例】K轮拌面机会待验证',demandSummary:'构造的选题演练,不是消费者结论',targetAudience:'待研究人群',usageScenario:'待研究场景',fusionDefinition:'拌面赛道×待研究人群×待研究场景,均待证据验证'}}); ids.opportunityId=opportunity.opportunityId;check(ids.opportunityId,'MISSING_OPPORTUNITY_ID'); const found=await search('KfOpportunity','K轮拌面机会待验证','search-opportunity'); const selected=found.records?.find(r=>r.objectId===ids.opportunityId);check(selected&&selected.dataMode==='sample','OPPORTUNITY_SEARCH_MISMATCH'); const project=await run('project-create',{sessionId:ids.sessionId,opportunityId:selected.objectId,projectKey:key+':project',title:'【样例】真实消息驱动拌面新品全流程验收'},'project'); ids.projectId=project.projectId;check(ids.projectId,'MISSING_PROJECT_ID'); await save('S3','所选机会与项目边界','opportunity_card',ids.projectId,'project-s3',['已选样例机会,真实需求与竞争证据仍缺失']); const status=async(suffix,expected)=>{const r=await run('project-status',{sessionId:ids.sessionId,projectId:ids.projectId},suffix);check(r.project?.progressState===expected,'PROJECT_STATE_MISMATCH');return r;}; const progress=async(toStage,state,note,suffix)=>run('project-progress',{sessionId:ids.sessionId,projectId:ids.projectId,toStage,state,note},suffix); await status('project-s3-read','in_progress'); await progress('S4','in_progress','S3成果已保存,进入验证练习','s4-start'); await progress('S4','blocked','缺真实消费者、供应链和成本依据;验证看板卡住状态','blocked'); await status('blocked-read','blocked'); const blockedBoard=await publicRead();check(blockedBoard.project?.some(p=>p.objectId===ids.projectId&&p.progressState==='blocked'),'PUBLIC_BLOCKED_MISMATCH');checkpoint({status:'public-blocked-verified',ids:{...ids}}); await progress('S4','in_progress','继续sample技术流程,资料缺口仍保留,不声明真实验证通过','resume'); await save('S4','需求工艺成本验证缺口','assessment_report',ids.projectId,'s4',['消费者证据未采集','配方比例和工艺参数未核实','成本/法规/包装适配未验证']); await progress('S5','in_progress','S4缺口已记录,进入方案组装练习','s5-start'); const concept=await run('concept-save',{sessionId:ids.sessionId,projectId:ids.projectId,version:1,concept:{name:'【样例】拌面新品方案字段联调',targetAudience:'待研究,非消费者结论',usageScenario:'待验证场景',specification:{packaging:'袋装候选,待验证',netWeightG:null,categoryId:ids.categoryId,referenceProductId:ids.productId},valueProposition:'待验证买点',flavor:'待验证口味',sellingPoints:['待验证卖点'],productionSteps:[{processId:ids.processId,step:'样例混合路线,无工艺参数依据'}],pendingQuestions:['真实人群与需求证据缺失','净含量、配方比例、加工参数、包装适配和成本待验证']},formulaItems:[{materialId:ids.materialId,component:'面体',basis:'用量未知;仅验证原料关系',note:'不填虚构比例/用量'}]},'concept'); ids.conceptId=concept.conceptId;check(ids.conceptId,'MISSING_CONCEPT_ID'); const final=await save('S5','新品方案与未决事项','concept',ids.projectId,'s5',['sample课堂链路完成不等于量产可行','真实消费者证据、工厂试验、成本与合规待完成']);ids.artifactId=final.artifactId; await progress('S5','completed','仅sample课堂任务全流程完成,不代表企业审批或量产','complete'); const complete=await status('final-read','completed'); check(complete.project.stageCode==='S5'&&complete.concept?.objectId===ids.conceptId,'FINAL_PROJECT_MISMATCH'); check(complete.formulaItems?.some(x=>x.materialRef?.objectId===ids.materialId),'FORMULA_REFERENCE_MISMATCH'); check(complete.concept.productionSteps?.some(x=>x.processId===ids.processId),'PROCESS_REFERENCE_MISMATCH'); check(complete.artifacts?.length===3&&complete.artifacts.every(x=>x.dataMode==='sample'&&x.dashboardState==='workshop'),'PROJECT_ARTIFACT_MISMATCH'); check(complete.events?.some(x=>x.eventType==='blocked'||x.type==='blocked')||results['blocked-read'].project.progressState==='blocked','BLOCKED_EVENT_MISMATCH'); for(const row of artifacts){const read=await run('workshop-read',{className:'KfArtifact',objectId:row.artifactId},'read-'+row.artifactId);check(read.record?.dataMode==='sample'&&read.record?.sessionRef?.objectId===ids.sessionId,'ARTIFACT_READBACK_MISMATCH');} const raw=await publicRead(); check(raw.project?.some(p=>p.objectId===ids.projectId&&p.progressState==='completed'&&p.stageCode==='S5'),'PUBLIC_PROJECT_MISMATCH'); check(raw.opportunity?.some(p=>p.objectId===ids.opportunityId),'PUBLIC_OPPORTUNITY_MISMATCH'); for(const row of artifacts)check(raw.artifact?.some(a=>a.objectId===row.artifactId&&a.dataMode==='sample'),'PUBLIC_ARTIFACT_MISMATCH'); const receipt={ok:true,operation:'full-workshop-api-chain-verified',...ids,artifacts,reportUrl:'https://kangshifu.fmode.cn/report/'+ids.artifactId,membersUrl:'https://kangshifu.fmode.cn/members',stage:'S5',progressState:'completed',projectArtifactCount:complete.artifacts.length,eventCount:complete.events.length,dataMode:'sample',formalReportPublished:false,publicBoardDataVerified:true,browserVerified:false,actualPlatformCollectionPerformed:false}; checkpoint({status:'complete',receipt});return receipt; } async function readPublic(){ const raw={project:[],artifact:[],opportunity:[],session:[]};let cursor=null; for(let i=0;i<500;i++){ 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,...(cursor?{cursor}:{})}}),redirect:'error',signal:AbortSignal.timeout(30000)}); const result=(await response.json())?.result;check(response.ok&&result?.ok&&result.data,'PUBLIC_READ_FAILED'); for(const k of Object.keys(raw)){check(Array.isArray(result.data[k]),'PUBLIC_SHAPE_MISMATCH');raw[k].push(...result.data[k]);} cursor=result.data.nextCursor;if(!cursor)return raw; }fail('PUBLIC_PAGINATION_LIMIT'); } if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){ try{ const a=process.argv.slice(2);check(a.length===5&&a[0]==='--input'&&a[2]==='--state-dir'&&a[4]==='--execute','USAGE'); check(process.platform==='linux'&&process.geteuid()===10000,'HERMES_UID_REQUIRED'); check(path.isAbsolute(a[1])&&path.isAbsolute(a[3]),'ABSOLUTE_PATH_REQUIRED'); for(const file of [a[1],path.dirname(a[3])]){check(fs.realpathSync(file)===file,'UNSAFE_ACCEPTANCE_PATH');} const st=fs.lstatSync(a[1]);check(st.isFile()&&st.uid===process.geteuid()&&(st.mode&0o777)===0o600,'PRIVATE_CONTEXT_REQUIRED'); const context=validateContext(JSON.parse(fs.readFileSync(a[1],'utf8'))); const worker='/opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs'; // This runner never auto-retries a remote write or resumes an ambiguous operation. fs.mkdirSync(a[3],{mode:0o700});check(fs.realpathSync(a[3])===a[3],'UNSAFE_ACCEPTANCE_PATH'); const journal={version:'ksf.full-workshop-journal.v1',at:new Date().toISOString(),steps:[],status:'running'}; const log=path.join(a[3],'journal.json'),persist=()=>fs.writeFileSync(log,JSON.stringify(journal,null,2),{mode:0o600});persist(); let sequence=0; const invoke=async(command,task,requestId)=>{ const name=String(++sequence).padStart(2,'0')+'-'+command; const input=path.join(a[3],name+'.input.json');fs.writeFileSync(input,JSON.stringify(task,null,2),{flag:'wx',mode:0o600}); const readonly=['workshop-contract','workshop-search','workshop-read','project-status'].includes(command); const args=[worker,command,...(readonly?[]:['--agent-profile',context.agentProfileKey]),'--input',input,'--request-id',requestId]; const p=spawnSync(process.execPath,args,{encoding:'utf8',timeout:660000,maxBuffer:4*1024*1024,windowsHide:true}); if(p.error)fail('ACCEPTANCE_REQUEST_UNCERTAIN'); let receipt;try{receipt=JSON.parse(p.status===0?p.stdout:p.stderr);}catch{fail('ACCEPTANCE_RECEIPT_INVALID');} fs.writeFileSync(path.join(a[3],name+'.receipt.json'),JSON.stringify(receipt,null,2),{flag:'wx',mode:0o600}); check(p.status===0&&receipt.ok&&receipt.entryResult?.ok,receipt.error?.code||receipt.entryResult?.error?.code||'ACCEPTANCE_COMMAND_FAILED'); return receipt.entryResult.result; }; try{ const receipt=await runFullChain({context,invoke,publicRead:readPublic,checkpoint:event=>{journal.steps.push(event);journal.status=event.status;persist();}}); console.log(JSON.stringify(receipt,null,2)); }catch(e){journal.status='stopped';journal.error={code:/^[A-Z][A-Z0-9_]+$/.test(e.code||'')?e.code:'ACCEPTANCE_FAILED',message:'保留已落库编号与原业务键,不自动重试、清理或换键;查journal及逐步回执。'};persist();throw e;} }catch(e){console.error(JSON.stringify({ok:false,error:{code:/^[A-Z][A-Z0-9_]+$/.test(e.code||'')?e.code:'ACCEPTANCE_FAILED',message:'完整链路验收停止;保留现场与已有编号,不输出账号或凭据。'}}));process.exitCode=1;} }