| 1234567891011121314151617181920212223242526272829303132333435 |
- 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};
- }
|