import {getSchema} from '../../../../lib/schemas'; import type {NextApiRequest,NextApiResponse} from 'next'; import formidable from 'formidable'; import {readFile,unlink} from 'node:fs/promises'; import {randomUUID} from 'node:crypto'; import {buildSchema,InputError} from '../../../../lib/schema'; import {authorize,saveTask,links,Task} from '../../../../lib/store'; export const config={api:{bodyParser:false}}; export default async function handler(req:NextApiRequest,res:NextApiResponse){ if(!authorize(req,res))return; if(req.method!=='POST'){res.setHeader('Allow','POST');return res.status(405).json({error:{code:'METHOD_NOT_ALLOWED',message:'Use POST'}});} const cleanup:string[]=[]; try{let schemaId:any,fields:any,text='',title='提取任务',file:{name:string;mime:string;data:Buffer}|undefined; if(req.headers['content-type']?.startsWith('multipart/form-data')){const form=formidable({maxFileSize:12*1024*1024,maxTotalFileSize:12*1024*1024,maxFiles:1,maxFields:5,maxFieldsSize:300000,filter:p=>p.name==='file'});form.on('fileBegin',(_,f)=>cleanup.push(f.filepath));const [v,fs]=await form.parse(req);try{fields=JSON.parse(v.fields?.[0]||'null');}catch{throw new InputError('fields 必须是 JSON');}schemaId=v.schemaId?.[0];text=v.text?.[0]||'';title=v.title?.[0]||title;const f=fs.file?.[0];if(f)file={name:f.originalFilename||'document',mime:f.mimetype||'',data:await readFile(f.filepath)}; }else if(req.headers['content-type']?.startsWith('application/json')){const chunks:Buffer[]=[];let size=0;for await(const c of req){size+=c.length;if(size>300000)throw new InputError('请求最多 300 KB');chunks.push(Buffer.from(c));}let b;try{b=JSON.parse(Buffer.concat(chunks).toString());}catch{throw new InputError('无效 JSON');}fields=b?.fields;schemaId=b?.schemaId;text=b?.text;title=b?.title||title; }else throw new InputError('使用 JSON 或 multipart/form-data'); if(schemaId){if(fields!=null)throw new InputError('fields 和 schemaId 只能提供一个');try{fields=(await getSchema(String(schemaId))).fields;}catch(e:any){if(e.code==='ENOENT'||e.message==='NOT_FOUND')throw new InputError('schemaId 对应的 schema 不存在');throw e;}}const schema=buildSchema(fields);if(typeof title!=='string')throw new InputError('标题必须是文本');title=title.slice(0,120); if(!file){if(typeof text!=='string'||!text.trim()||text.length>100000)throw new InputError('请提供文件或 1–100000 字符文本');file={name:'document.txt',mime:'text/plain; charset=utf-8',data:Buffer.from(text)};} if(!/\.(pdf|png|jpe?g|webp|txt|md|csv|json)$/i.test(file.name))throw new InputError('支持 PDF、图片、TXT、MD、CSV、JSON'); if(/\.pdf$/i.test(file.name)){if(file.data.subarray(0,5).toString()!=='%PDF-')throw new InputError('PDF 文件内容无效');file.mime='application/pdf';} const task:Task={id:randomUUID(),title,fields,schema,createdAt:new Date().toISOString(),document:{name:file.name,mime:file.mime,size:file.data.length},status:'ready'}; await saveTask(task,file.data);res.status(201).json({...task,links:links(task.id)}); }catch(e:any){const invalid=e instanceof InputError||e.httpCode===413;res.status(invalid?400:500).json({error:{code:invalid?'INVALID_REQUEST':'CREATE_FAILED',message:invalid?e.message:'任务创建失败'}});}finally{await Promise.all(cleanup.map(p=>unlink(p).catch(()=>{})));} }