import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import {fileURLToPath} from 'node:url'; import {readAgentProfileByKey,readParticipantProfile,bindOwnAgent,bindRegisteredParticipant,assertOwnSession,redactIdentity} from './ksf-identity.mjs'; const self=fileURLToPath(import.meta.url); const commands={ 'workshop-start':'ksfStartWorkshopSession','workshop-save':'ksfSaveWorkshopTask', 'project-create':'ksfCreateProjectFromOpportunity','project-progress':'ksfTransitionProject','employee-read':'ksfReadWecomEmployee','employee-name':'ksfEmployeeDisplayName', 'concept-save':'ksfSaveWorkshopConcept','project-status':'ksfGetWorkshopProject','workshop-search':'ksfSearchRecords', 'workshop-import':null,'workshop-dedup':'ksfCheckDuplicates','workshop-read':'ksfGetRecord','workshop-contract':'ksfGetIngestContract','workshop-schema':'ksfGetDataDictionary' }; const sha=x=>crypto.createHash('sha256').update(x).digest('hex'); const fail=(code,message)=>{throw Object.assign(new Error(message||code),{code});}; const canonical=x=>x&&typeof x==='object'?Array.isArray(x)?'['+x.map(canonical).join(',')+']':'{'+Object.keys(x).sort().map(k=>JSON.stringify(k)+':'+canonical(x[k])).join(',')+'}':JSON.stringify(x); export async function runWorkshop(command,task,{requestId,call,agentProfile,participantProfile}={}){ if(!Object.hasOwn(commands,command)||!task||typeof task!=='object'||Array.isArray(task))fail('INVALID_INPUT'); if(typeof requestId!=='string'||!requestId||task.requestId&&task.requestId!==requestId)fail('BAD_REQUEST_ID'); if(!['project-status','workshop-search','workshop-read','workshop-contract','workshop-dedup','workshop-schema'].includes(command)&&!agentProfile)fail('AGENT_PROFILE_REQUIRED'); if(['workshop-start','employee-read','employee-name'].includes(command)&&!participantProfile)fail('PARTICIPANT_PROFILE_REQUIRED'); if(['employee-read','employee-name'].includes(command)){ const naming=command==='employee-name',hasName=Object.hasOwn(task,'displayName'),hasSkip=Object.hasOwn(task,'skip'),writing=hasName||hasSkip; if(Object.keys(task).some(k=>!['sessionId','participant','requestId',...(naming?['displayName','skip']:[])].includes(k))||typeof task.sessionId!=='string'||!task.sessionId)fail('INVALID_INPUT'); if(naming&&(hasName&&hasSkip||hasName&&(typeof task.displayName!=='string'||!task.displayName.trim()||[...task.displayName.trim()].length>40||/[\p{Cc}\p{Cf}]/u.test(task.displayName))||hasSkip&&task.skip!==true))fail('INVALID_INPUT'); if(agentProfile.role!=='workshop')fail('AGENT_ROLE_MISMATCH'); bindRegisteredParticipant(task.participant,participantProfile); const current=await call('ksfGetRecord',{className:'KfSession',objectId:task.sessionId}); assertOwnSession(current.record,agentProfile); if(current.record?.meta?.attribution?.participantKey!=='participant:'+sha(canonical([current.record.workshopKey,participantProfile.key])))fail('PARTICIPANT_SESSION_MISMATCH'); const result=await call(commands[command],{sessionId:task.sessionId,participantProfileKey:participantProfile.key,agentProfileKey:agentProfile.key,requestId,...(naming&&hasName?{displayName:task.displayName}:{}),...(naming&&hasSkip?{skip:true}:{})}); if(naming){ if(result?.sessionId!==task.sessionId||result.readOnly!==!writing||result.persisted!==writing||!['not_asked','provided','skipped'].includes(result.promptState)||result.businessRecordsChanged!==0||!writing&&result.databaseWrites!==0||writing&&hasName&&result.source!=='wecom_directory'&&result.displayName!==task.displayName.trim().normalize('NFC'))fail('EMPLOYEE_NAME_RECEIPT_MISMATCH'); if(typeof result.nameVerified!=='boolean'||result.nameVerified&&result.source!=='wecom_directory'||!result.nameVerified&&![null,'self_reported'].includes(result.source)||result.promptState==='provided'&&(typeof result.displayName!=='string'||!result.displayName.trim())||result.promptState!=='provided'&&result.displayName!==null)fail('EMPLOYEE_NAME_RECEIPT_MISMATCH'); return redactIdentity(result); } if(result?.readOnly!==true||result.databaseWrites!==0||result.sessionId!==task.sessionId||result.employeeIdentity?.status!=='verified'||result.employeeIdentity.persisted!==false||result.employee?.nameVerified!==true||result.employee.source!=='wecom_directory'||typeof result.employee.name!=='string'||!result.employee.name.trim())fail('EMPLOYEE_READ_RECEIPT_MISMATCH'); return redactIdentity(result); } if(agentProfile){ if(!['project-status','workshop-search','workshop-read','workshop-contract','workshop-dedup','workshop-schema'].includes(command)&&agentProfile.role!=='workshop')fail('AGENT_ROLE_MISMATCH'); if(command==='workshop-start'){ task={...task,participant:bindRegisteredParticipant(task.participant,participantProfile)}; task=bindOwnAgent(task,agentProfile); const contract=await call('ksfGetIngestContract',{}); if(contract.workshop?.identitySchema!=='ksf.agent-self.v1')fail('IDENTITY_CAPABILITY_UNAVAILABLE','服务端身份扩展尚未就绪;未创建会话'); if(task.participant.account?.identifierType==='wecom_aibot_sender'){ if(contract.workshop?.channelIdentitySchema!=='ksf.wecom-aibot-sender.v1')fail('CHANNEL_IDENTITY_CAPABILITY_UNAVAILABLE'); if(task.participant.account.scopeId!==agentProfile.account.accountId||agentProfile.account.platform!=='wecom'||task.conversation?.channel!=='wecom')fail('CHANNEL_AGENT_MISMATCH'); } } else if(['workshop-save','project-create','project-progress','concept-save'].includes(command)){ const current=await call('ksfGetRecord',{className:'KfSession',objectId:task.sessionId}); assertOwnSession(current.record,agentProfile); }else if(command==='workshop-import'&&agentProfile.role!=='workshop')fail('AGENT_ROLE_MISMATCH'); } if(command!=='workshop-import')return redactIdentity(await call(commands[command],{...task,requestId})); const allowed=['source','records','requestId'];if(Object.keys(task).some(k=>!allowed.includes(k)))fail('INVALID_INPUT'); if(!task.source?.rawText||typeof task.source.rawText!=='string'||!Array.isArray(task.records)||task.records.length<1||task.records.length>200)fail('INVALID_INPUT','需要原始资料 source 和 1–200 条 records'); const contract=await call('ksfGetIngestContract',{});const classes=new Map(contract.allowedClasses.map(x=>[x.className,x])); const excluded=['KfArtifact','KfConcept','KfFormulaItem','KfOpportunity','KfProject','KfSession','KfStageEvent']; const keys=new Set(); for(const item of task.records){ if(!item.itemKey||keys.has(item.itemKey)||!item.externalKey||!classes.has(item.className)||excluded.includes(item.className)||!item.data||item.data.dataMode!==task.source.dataMode)fail('INVALID_INPUT','条目键、类型或取得模式不合法;报告/机会/方案请用对应工作坊命令');keys.add(item.itemKey); } if(contract.deduplication?.schema!=='ksf.ingest-dedup.v1')fail('DEDUP_CAPABILITY_UNAVAILABLE','先升级查重服务,未登记来源或提交导入'); const deduplication=await call('ksfCheckDuplicates',{source:task.source,records:task.records}); if(deduplication.schema!=='ksf.ingest-dedup.v1'||deduplication.readOnly!==true)fail('DEDUP_RECEIPT_MISMATCH'); const registered=await call('ksfRegisterSource',{requestId:requestId+':source',source:task.source,contentHash:sha(Buffer.from(task.source.rawText,'utf8'))}); const refs={},pending=task.records.slice(),items=[]; const resolve=value=>{ if(Array.isArray(value))return value.map(resolve); if(value&&typeof value==='object'){ if(Object.keys(value).length===1&&typeof value.$item==='string'){if(!refs[value.$item])fail('UNRESOLVED_DEPENDENCY');return refs[value.$item];} return Object.fromEntries(Object.entries(value).map(([k,v])=>[k,resolve(v)])); }return value; }; let active=null; try { while(pending.length){ let progress=false; for(let i=0;i!refs[k]))continue; active={itemKey:item.itemKey,className:item.className,requestId:requestId+':item:'+item.itemKey}; const record=resolve(item.data);const def=classes.get(item.className);if(def.fields.some(x=>x.name==='sourceRef'))record.sourceRef={__type:'Pointer',className:'KfSource',objectId:registered.sourceId}; const p={requestId:requestId+':item:'+item.itemKey,schemaVersion:contract.schemaVersion,sourceId:registered.sourceId,producer:{skillName:'ksf-workshop-runtime',skillVersion:'2026.11.02',...(agentProfile?{agent:bindOwnAgent({},agentProfile).agent}:{})},items:[{itemKey:item.itemKey,className:item.className,externalKey:item.externalKey,data:record}]}; const checked=await call('ksfValidateImport',p);if(!checked.valid)fail(checked.itemResults?.[0]?.errors?.[0]?.code||'VALIDATION_FAILED','结构化条目预检失败,保留已返回编号和业务键'); const result=await call('ksfSubmitImport',p);active.jobId=result.jobId;const receipt=await call('ksfGetImportResult',{jobId:result.jobId,limit:200});const row=receipt.items?.[0]; if(receipt.job?.status!=='succeeded'||receipt.items?.length!==1||receipt.nextCursor||row.itemKey!==item.itemKey||row.status!=='succeeded'||row.snapshotState!=='available'||!row.objectId)fail('RECEIPT_MISMATCH'); refs[item.itemKey]={__type:'Pointer',className:item.className,objectId:row.objectId};items.push({itemKey:item.itemKey,className:item.className,objectId:row.objectId,jobId:result.jobId,status:row.status,operation:row.operation}); pending.splice(i--,1);progress=true;active=null; } if(!progress)fail('UNRESOLVED_DEPENDENCY','依赖缺失或循环;保留业务键,不自动换键重试'); }} catch(error) { error.partial={sourceId:registered.sourceId,items,pendingItemKeys:pending.map(x=>x.itemKey),...(active?{active}:{}),status:'stopped',dataMode:task.source.dataMode}; throw error; } return{sourceId:registered.sourceId,sourceOperation:registered.operation,items,counts:{created:items.filter(x=>x.operation==='created').length,unchanged:items.filter(x=>x.operation==='unchanged').length},deduplication:{schema:deduplication.schema,checked:deduplication.items.length},status:'succeeded',dataMode:task.source.dataMode,formalReportPublished:false}; } export function createWorkshopCaller({token,url='https://kangshifu.fmode.cn/api/functions',appId='FKieRbIoGt_main',fetchImpl=fetch}){ if(!token)fail('MISSING_SERVICE_TOKEN');if(url!=='https://kangshifu.fmode.cn/api/functions'||appId!=='FKieRbIoGt_main')fail('TARGET_MISMATCH'); return async(name,params)=>{ let r;try{r=await fetchImpl(url,{method:'POST',headers:{'Content-Type':'application/json','X-Parse-Application-Id':appId,'X-Ksf-Service-Token':token},body:JSON.stringify({path:'/'+name,params}),redirect:'error',signal:AbortSignal.timeout(45000)});}catch{fail('REMOTE_REQUEST_UNCERTAIN','请求结果未知,请保持原 requestId 查回状态;不自动重试或换键');} const result=(await r.json().catch(()=>null))?.result; if(!r.ok||result?.ok!==true||result.errors?.length)throw Object.assign(new Error('服务调用失败'),{code:result?.errors?.[0]?.code||'REMOTE_FAILED',remoteFunction:name,remoteResponseReceived:true});return result.data; }; } if(process.argv[1]&&path.resolve(process.argv[1])===self){ const base={skill:'ksf-workshop-runtime',invocationId:crypto.randomUUID(),at:new Date().toISOString(),entry:self,entrySha256:sha(fs.readFileSync(self))}; try{ const [command,...args]=process.argv.slice(2);const flags={};for(let i=0;i