import crypto from 'node:crypto'; // 不读取管理配置,也不向客户端提供 masterKey。 export function createFmodeClient({url=process.env.KSF_FUNCTIONS_URL||'https://kangshifu.fmode.cn/api/functions',appId=process.env.KSF_PARSE_APP_ID||'FKieRbIoGt_main',serviceToken=process.env.KSF_SERVICE_TOKEN,sessionToken=process.env.KSF_PARSE_SESSION_TOKEN,reportMetadata=null,fetchImpl=fetch}={}){ const endpoint=new URL(url); if(endpoint.protocol!=='https:'||endpoint.hostname!=='kangshifu.fmode.cn'||endpoint.pathname!=='/api/functions'||appId!=='FKieRbIoGt_main')throw Error('不是康师傅正式动态函数入口'); const headers={'Content-Type':'application/json','X-Parse-Application-Id':appId}; if(sessionToken)headers['X-Parse-Session-Token']=sessionToken; else if(serviceToken)headers['X-Ksf-Service-Token']=serviceToken; function checkUrl(value){const target=new URL(value);if(target.origin!==endpoint.origin||target.pathname!==endpoint.pathname)throw Error('文件通道不属于当前后端');return target;} async function callFn(name,params={}){ if(!/^ksf[A-Za-z]+$/.test(name))throw Error('函数名不合法'); if(['ksfValidateImport','ksfSubmitImport'].includes(name)&&reportMetadata){ params={...params,items:params.items.map(item=>item.className==='KfArtifact'?{...item,data:{stageCode:reportMetadata.stageCode,contentSchema:'ksf.html-outline.v1',...item.data}}:item)}; } const response=await fetchImpl(url,{method:'POST',headers,body:JSON.stringify({path:'/'+name,params}),redirect:'error',signal:AbortSignal.timeout(45000)}); let body;try{body=await response.json();}catch{throw Error('函数返回非 JSON');} return{http:response.status,ok:response.ok,body}; } async function invoke(name,params={}){const r=await callFn(name,params),v=r.body.result;if(r.http!==200||v?.ok!==true||!Array.isArray(v.errors)||v.errors.length)throw Object.assign(Error(v?.errors?.map(e=>e.code+': '+e.message).join(';')||'函数调用失败'),{response:r});return v.data;} async function transfer(upload,bytes){ const target=checkUrl(upload.url);if(target.searchParams.get('path')!=='/ksf/upload'||!['POST','PUT'].includes(upload.method))throw Error('上传准备信息无效'); const r=await fetchImpl(target,{method:upload.method,headers:{...upload.headers,'Content-Type':'application/octet-stream'},body:bytes,redirect:'error',signal:AbortSignal.timeout(45000)}); const receipt=await r.json();const actual=crypto.createHash('sha256').update(bytes).digest('hex'); if(r.status!==200||receipt.ok!==true||receipt.sizeBytes!==bytes.length||receipt.sha256!==actual||!receipt.fileId)throw Error('文件传输失败或服务端字节回执不完整'); return{ok:true,http:r.status,url:upload.url,method:upload.method,bytes:receipt.sizeBytes,sha256:receipt.sha256,fileId:receipt.fileId}; } async function confirmation(previewId,reportIdentity){ const state=await invoke('ksfGetPreviewConfirmation',{previewId});if(state.status!=='confirmed')throw Error('等待员工在确认页操作,不能构造确认凭据'); const r=await invoke('ksfConfirmReport',{requestId:'confirm-receipt:'+state.eventId,reportIdentity,confirmationEvent:{eventId:state.eventId}}); const i=r.confirmedIdentity; return{reportSeriesKey:i.reportSeriesKey,version:i.version,reportContentHash:i.reportContentHash,confirmationReceipt:r.confirmationReceipt,contentConfirmedByRef:{__type:'Pointer',className:'_User',objectId:i.confirmedBy},contentConfirmedAt:i.confirmedAt}; } return{callFn,invoke,transfer,confirmation}; }