// client/ai/pub/jjxDoReview.js module.exports = { main: async (event) => { let { data = {}, userInfo } = event; const { record_id, file_id, user_id, file_name, file_size } = data; if (!record_id || !file_id) return { code: -1, msg: '参数错误' }; const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG'; const BOT_ID = '7689821072762011698'; const db = uniCloud.database(); const _ = db.command; try { // 构造请求 const objectString = JSON.stringify([ { type: 'file', file_id: file_id }, { type: 'text', text: '请审查这份合同' } ]); // 调用 Coze 接口(流式模式) const chatRes = await uniCloud.httpclient.request('https://api.coze.cn/v3/chat', { method: 'POST', dataType: 'stream', // 关键:改成 stream contentType: 'json', timeout: 300000, // 5分钟超时 headers: { 'Authorization': 'Bearer ' + COZE_TOKEN, 'Accept': 'text/event-stream' }, data: { bot_id: BOT_ID, user_id: user_id || 'anonymous', stream: true, auto_save_history: true, additional_messages: [{ role: 'user', content: objectString, content_type: 'object_string' }] } }); // 从流中逐步解析 const stream = chatRes.data || chatRes.res; let buffer = ''; let answer = ''; let conversationId = ''; let lastUpdateLen = 0; // 返回 Promise,等待流结束 return await new Promise((resolve, reject) => { stream.on('data', (chunk) => { buffer += chunk.toString(); const blocks = buffer.split('\n\n'); buffer = blocks.pop(); for (const block of blocks) { const lines = block.split('\n'); let eventName = ''; let dataStr = ''; for (const line of lines) { if (line.startsWith('event:')) eventName = line.slice(6).trim(); if (line.startsWith('data:')) dataStr = line.slice(5).trim(); } if (!dataStr || dataStr === '[DONE]' || dataStr === '"[DONE]"') continue; try { const p = JSON.parse(dataStr); if (p.conversation_id) conversationId = p.conversation_id; // 处理回答内容 // 注意:conversation.message.completed 携带的是该条消息的完整内容, // 若前面已经通过 delta 拼过,再拼一次就会重复,这里做去重 if (p.type === 'answer' && p.content) { const isDup = eventName === 'conversation.message.completed' && answer.indexOf(p.content) >= 0; if (!isDup) { answer += p.content; } // 每积累 200 字更新一次数据库(前端轮询能看到进度) if (answer.length - lastUpdateLen >= 200) { lastUpdateLen = answer.length; db.collection('jjx-review-records').doc(record_id).update({ result: answer, status: 'reviewing', update_time: new Date() }).catch(() => {}); } } // 处理工具响应(报告下载链接) if (p.type === 'tool_response' && p.content) { try { const toolData = JSON.parse(p.content); if (toolData.url) answer += '\n\n📎 报告下载:' + toolData.url; } catch (e) { const urlMatch = p.content.match(/https?:\/\/[^\s\)\]\"\<]+\.pdf[^\s\)\]\"\<]*/i); if (urlMatch) answer += '\n\n📎 报告下载:' + urlMatch[0]; } } } catch (e) {} } }); stream.on('end', async () => { if (!answer) { answer = '审查未能获取结果,请稍后重试'; } try { // 计算消耗积分 const pointsUsed = Math.max(150, Math.ceil((file_size || 0) / (100 * 1024)) * 150); // 扣积分 const userPoints = await db.collection('jjx-user-points').doc(user_id).get(); if (userPoints.data.length && userPoints.data[0].balance >= pointsUsed) { await db.collection('jjx-user-points').doc(user_id).update({ balance: _.inc(-pointsUsed), total_spent: _.inc(pointsUsed), update_time: new Date() }); const updatedPoints = await db.collection('jjx-user-points').doc(user_id).get(); await db.collection('jjx-points-log').add({ user_id: user_id, type: 'spend', amount: -pointsUsed, balance_after: updatedPoints.data[0] ? updatedPoints.data[0].balance : 0, reason: '合同审查 — ' + (file_name || ''), biz_type: 'contract_review', biz_id: record_id, _add_time: new Date() }); } // 保存 HTML 报告到云存储 let htmlUrl = ''; try { // 如果回复本身就是 HTML(包含 或 ) let htmlContent = answer; const htmlMatch = answer.match(/```html\s*([\s\S]*?)```/); if (htmlMatch && htmlMatch[1]) { htmlContent = htmlMatch[1].trim(); } if (htmlContent.includes(' { await db.collection('jjx-review-records').doc(record_id).update({ status: 'failed', result: '审查失败: ' + err.message, error: err.message, update_time: new Date() }); reject(err); }); }); } catch (e) { // 审查失败,更新记录 await db.collection('jjx-review-records').doc(record_id).update({ status: 'failed', result: '审查失败: ' + e.message, error: e.message, update_time: new Date() }); return { code: -1, msg: '审查失败: ' + e.message }; } } };