Files
jjx-wap/uniCloud-tcb/cloudfunctions/jjxReviewTimer/index.js
T

239 lines
8.6 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.
'use strict';
// 定时任务:每分钟检查 reviewing 状态的记录,触发审查
const db = uniCloud.database();
const _ = db.command;
// 导入审查执行函数(从 router 的 service 目录)
// 注意:定时任务是独立云函数,不能直接 require router 的 service
// 所以把审查逻辑直接写在这里
const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG';
const BOT_ID = '7689821072762011698';
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('<!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);
}
// 更新记录为完成
await db.collection('jjx-review-records').doc(record_id).update({
status: 'completed',
result: answer,
html_url: htmlUrl,
token_count: tokenCount,
points_used: pointsUsed,
conversation_id: conversationId || '',
update_time: new Date()
});
console.log('审查完成:', record_id, file_name);
return { 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()
});
console.error('审查失败:', record_id, e.message);
return { code: -1, msg: '审查失败: ' + e.message };
}
}
exports.main = async (event, context) => {
// 先把卡在 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 };
};