Files
jjx-wap/uniCloud-tcb/cloudfunctions/router/service/client/ai/pub/jjxDoReview.js
T

222 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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(包含 <!DOCTYPE html> 或 <html>)
let htmlContent = answer;
const htmlMatch = answer.match(/```html\s*([\s\S]*?)```/);
if (htmlMatch && htmlMatch[1]) {
htmlContent = htmlMatch[1].trim();
}
if (htmlContent.includes('<!DOCTYPE') || htmlContent.includes('<html')) {
const cloudPath = 'review-reports/' + Date.now() + '_' + (file_name || 'report').replace(/\.[^.]+$/, '') + '.html';
const uploadRes = await uniCloud.uploadFile({
cloudPath: cloudPath,
fileContent: Buffer.from(htmlContent, 'utf-8')
});
if (uploadRes.fileID) {
const urlRes = await uniCloud.getTempFileURL({ fileList: [uploadRes.fileID] });
if (urlRes.fileList && urlRes.fileList[0]) {
htmlUrl = urlRes.fileList[0].tempFileURL;
}
}
}
} catch (e) {
console.log('HTML报告上传失败:', e.message);
}
// 解析 JSON 结构化数据(如果有的话)
let contractInfo = {};
try {
const jsonMatch = answer.match(/```json\s*([\s\S]*?)```/);
if (jsonMatch) contractInfo = JSON.parse(jsonMatch[1].trim());
} catch (e) {}
// 更新审查记录为完成
await db.collection('jjx-review-records').doc(record_id).update({
status: 'completed',
result: answer,
html_url: htmlUrl,
contract_info: contractInfo,
points_used: pointsUsed,
conversation_id: conversationId || '',
update_time: new Date()
});
resolve({ code: 0, msg: '审查完成' });
} catch (e) {
// 更新为失败
await db.collection('jjx-review-records').doc(record_id).update({
status: 'failed',
result: '审查失败: ' + e.message,
error: e.message,
update_time: new Date()
});
reject(e);
}
});
stream.on('error', async (err) => {
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 };
}
}
};