'use strict'; // 定时任务:每分钟检查 reviewing 状态的记录,触发审查 const db = uniCloud.database(); const _ = db.command; // 导入审查执行函数(从 router 的 service 目录) // 注意:定时任务是独立云函数,不能直接 require router 的 service // 所以把审查逻辑直接写在这里 const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG'; const BOT_ID = '7689821072762011698'; const { buildDocx, extractReportData, extractDocxLink, isDocxConvertible } = require('./buildDocx.js'); async function doReview(record) { const { _id: record_id, file_id, user_id, file_name, file_size } = record; const objectString = JSON.stringify([ { type: 'file', file_id: file_id }, { type: 'text', text: '请审查这份合同' } ]); try { // 1. 非流式发起对话(立即返回 chat_id) const chatRes = await uniCloud.httpclient.request('https://api.coze.cn/v3/chat', { method: 'POST', dataType: 'json', contentType: 'json', timeout: 30000, headers: { 'Authorization': 'Bearer ' + COZE_TOKEN }, data: { bot_id: BOT_ID, user_id: user_id || 'anonymous', stream: false, auto_save_history: true, additional_messages: [{ role: 'user', content: objectString, content_type: 'object_string' }] } }); const chatData = chatRes.data && chatRes.data.data; const chatId = chatData && chatData.id; const conversationId = chatData && chatData.conversation_id; console.log('[审查] chat_id:', chatId, 'conversation_id:', conversationId); if (!chatId) { throw new Error('未获取到 chat_id: ' + JSON.stringify(chatRes.data)); } // 保存 chat_id 和 conversation_id 到数据库 await db.collection('jjx-review-records').doc(record_id).update({ chat_id: chatId, conversation_id: conversationId || '', update_time: new Date() }); console.log('[审查] 已保存 chat_id:', chatId); // 2. 轮询对话状态(每5秒查一次,最多5分钟) let answer = ''; let tokenCount = 0; for (let i = 0; i < 60; i++) { await new Promise(r => setTimeout(r, 5000)); const statusRes = await uniCloud.httpclient.request( 'https://api.coze.cn/v3/chat/retrieve?chat_id=' + chatId + '&conversation_id=' + conversationId, { method: 'GET', dataType: 'json', headers: { 'Authorization': 'Bearer ' + COZE_TOKEN }, timeout: 15000 } ); const status = statusRes.data && statusRes.data.data && statusRes.data.data.status; console.log('[审查] 第', i + 1, '次轮询, 状态:', status); if (status === 'completed') { // 保存 token 消耗信息 const usage = statusRes.data && statusRes.data.data && statusRes.data.data.usage; tokenCount = (usage && usage.token_count) || 0; console.log('[审查] Token 消耗:', tokenCount); // 3. 获取消息列表 const msgRes = await uniCloud.httpclient.request( 'https://api.coze.cn/v3/chat/message/list?chat_id=' + chatId + '&conversation_id=' + conversationId, { method: 'GET', dataType: 'json', headers: { 'Authorization': 'Bearer ' + COZE_TOKEN }, timeout: 15000 } ); const messages = msgRes.data && msgRes.data.data; if (Array.isArray(messages)) { for (const msg of messages) { if (msg.type === 'answer' && msg.role === 'assistant' && msg.content) { answer = msg.content; // answer 类型的完整内容 } if (msg.type === 'tool_response' && msg.content) { try { const toolData = JSON.parse(msg.content); if (toolData.url) answer += '\n\n📎 报告下载:' + toolData.url; } catch (e) {} } } } break; } if (status === 'failed') { throw new Error('对话失败'); } } if (!answer) answer = '审查未能获取结果,请稍后重试'; console.log('[审查] 结果长度:', answer.length); // 计算消耗积分 // 积分 = token消耗 / 100(最低1积分) const pointsUsed = Math.max(1, Math.round(tokenCount / 100)); console.log('[积分] token_count:', tokenCount, '→ 扣除积分:', pointsUsed); // 扣积分 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 || '') + '(' + tokenCount + ' token)', biz_type: 'contract_review', biz_id: record_id, _add_time: new Date() }); } // 保存 HTML 报告到云存储 let htmlUrl = ''; try { let htmlContent = answer; const htmlMatch = answer.match(/```html\s*([\s\S]*?)```/); if (htmlMatch && htmlMatch[1]) htmlContent = htmlMatch[1].trim(); if (htmlContent.includes(' { // 先把卡在 processing 超过10分钟的记录重置为 reviewing(超时保护) const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000); const stuckRecords = await db.collection('jjx-review-records') .where({ status: 'processing', update_time: _.lt(tenMinutesAgo) }) .get(); for (const stuck of stuckRecords.data) { await db.collection('jjx-review-records').doc(stuck._id).update({ status: 'reviewing', update_time: new Date() }); } // 查询所有 reviewing 状态的记录(超过1分钟的,避免重复触发) const oneMinuteAgo = new Date(Date.now() - 60 * 1000); const pendingRecords = await db.collection('jjx-review-records') .where({ status: 'reviewing', review_started_at: _.lt(oneMinuteAgo) }) .limit(5) // 每次最多处理5条 .get(); if (!pendingRecords.data.length) { return { code: 0, msg: '无待处理记录' }; } console.log('发现待处理审查记录:', pendingRecords.data.length, '条'); // 逐条处理(串行,避免并发问题) const results = []; for (const record of pendingRecords.data) { // 先标记为 processing,避免重复触发 await db.collection('jjx-review-records').doc(record._id).update({ status: 'processing', update_time: new Date() }); try { const result = await doReview(record); results.push({ record_id: record._id, ...result }); } catch (e) { // 审查失败,重置为 reviewing 以便下次重试 await db.collection('jjx-review-records').doc(record._id).update({ status: 'reviewing', update_time: new Date() }); console.error('审查异常,已重置状态:', record._id, e.message); results.push({ record_id: record._id, code: -1, msg: e.message }); } } return { code: 0, msg: '处理完成', data: results }; };