266 lines
11 KiB
JavaScript
266 lines
11 KiB
JavaScript
// Markdown 转 HTML
|
|
function mdToHtml(md) {
|
|
if (!md) return '';
|
|
let html = md;
|
|
// 标题
|
|
html = html.replace(/^####\s+(.+)$/gm, '<h5>$1</h5>');
|
|
html = html.replace(/^###\s+(.+)$/gm, '<h4>$1</h4>');
|
|
html = html.replace(/^##\s+(.+)$/gm, '<h3>$1</h3>');
|
|
html = html.replace(/^#\s+(.+)$/gm, '<h2>$1</h2>');
|
|
// 加粗
|
|
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
// 链接
|
|
html = html.replace(/(https?:\/\/[^\s\)\]\"\<]+)/g, '<a href="$1" target="_blank">$1</a>');
|
|
// 代码块
|
|
html = html.replace(/```[\w]*\n([\s\S]*?)```/g, '<pre class="code-block">$1</pre>');
|
|
// 行内代码
|
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
// 列表
|
|
html = html.replace(/^[\-\•]\s+(.+)$/gm, '<li>$1</li>');
|
|
html = html.replace(/^(\d+)\.\s+(.+)$/gm, '<li>$1. $2</li>');
|
|
// 换行
|
|
html = html.replace(/\n\n/g, '</p><p>');
|
|
html = html.replace(/\n/g, '<br>');
|
|
html = '<p>' + html + '</p>';
|
|
// 清理空段落
|
|
html = html.replace(/<p>\s*<\/p>/g, '');
|
|
return html;
|
|
}
|
|
|
|
// 生成精美 HTML 报告
|
|
function generateHtmlReport(markdown, fileName, reportDate, info) {
|
|
const riskLevel = info.risk_level || '待评估';
|
|
const riskSummary = info.risk_summary || '';
|
|
const riskCount = info.risk_count || { high: 0, medium: 0, low: 0 };
|
|
const contractName = info.contract_name || fileName || '合同审查报告';
|
|
const contractType = info.contract_type || '';
|
|
|
|
// 风险等级颜色
|
|
let riskColor = '#67c23a';
|
|
if (riskLevel.includes('高')) riskColor = '#f56c6c';
|
|
else if (riskLevel.includes('中高')) riskColor = '#e6a23c';
|
|
else if (riskLevel.includes('中')) riskColor = '#e6a23c';
|
|
else if (riskLevel.includes('中低')) riskColor = '#409eff';
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${contractName} - 审查报告</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: #f5f7fa; color: #333; line-height: 1.8; }
|
|
.container { max-width: 900px; margin: 0 auto; padding: 20px; }
|
|
.header { background: linear-gradient(135deg, #a92924, #d4574e); color: #fff; padding: 40px; border-radius: 12px 12px 0 0; }
|
|
.header h1 { font-size: 24px; margin-bottom: 8px; }
|
|
.header .subtitle { opacity: 0.85; font-size: 14px; }
|
|
.summary-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; padding: 24px 40px; background: #fff; border-bottom: 1px solid #eee; }
|
|
.summary-card { text-align: center; padding: 16px; border-radius: 8px; background: #f9f9f9; }
|
|
.summary-card .label { font-size: 12px; color: #999; margin-bottom: 4px; }
|
|
.summary-card .value { font-size: 20px; font-weight: 700; }
|
|
.body { background: #fff; padding: 32px 40px; border-radius: 0 0 12px 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
|
|
.body h2 { font-size: 18px; color: #222; margin: 28px 0 12px; padding-bottom: 8px; border-bottom: 2px solid #a92924; }
|
|
.body h3 { font-size: 16px; color: #333; margin: 20px 0 8px; }
|
|
.body h4 { font-size: 14px; color: #444; margin: 16px 0 6px; }
|
|
.body p { margin: 8px 0; font-size: 14px; }
|
|
.body li { margin: 4px 0 4px 20px; font-size: 14px; }
|
|
.body strong { color: #a92924; }
|
|
.body a { color: #a92924; text-decoration: underline; }
|
|
.risk-badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 12px; font-weight: 600; color: #fff; }
|
|
.risk-high { background: #f56c6c; }
|
|
.risk-medium { background: #e6a23c; }
|
|
.risk-low { background: #67c23a; }
|
|
.code-block { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 12px 16px; font-size: 13px; overflow-x: auto; margin: 8px 0; font-family: "SFMono-Regular", Consolas, monospace; }
|
|
.disclaimer { margin-top: 32px; padding: 16px 20px; background: #fff8e1; border-left: 4px solid #e6a23c; border-radius: 4px; font-size: 12px; color: #b8860b; }
|
|
.footer { text-align: center; padding: 24px; font-size: 12px; color: #999; }
|
|
@media print { body { background: #fff; } .container { padding: 0; } .header { border-radius: 0; } .body { box-shadow: none; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>📋 合同审查报告</h1>
|
|
<div class="subtitle">${contractName} | ${reportDate}</div>
|
|
</div>
|
|
<div class="summary-cards">
|
|
<div class="summary-card">
|
|
<div class="label">综合风险等级</div>
|
|
<div class="value" style="color:${riskColor}">${riskLevel}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="label">风险统计</div>
|
|
<div class="value"><span style="color:#f56c6c">${riskCount.high||0}</span> / <span style="color:#e6a23c">${riskCount.medium||0}</span> / <span style="color:#67c23a">${riskCount.low||0}</span></div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="label">合同类型</div>
|
|
<div class="value" style="font-size:14px">${contractType || '待识别'}</div>
|
|
</div>
|
|
</div>
|
|
<div class="body">
|
|
<h2>总体评估</h2>
|
|
<p>${riskSummary || '暂无总体评估'}</p>
|
|
<h2>详细审查意见</h2>
|
|
${mdToHtml(markdown.replace(/```json[\s\S]*?```/, ''))}
|
|
<div class="disclaimer">
|
|
⚠️ <strong>免责声明</strong>:本报告由 AI 系统自动生成,仅供参考,不构成正式法律意见。涉及重大金额或复杂交易的合同,建议咨询执业律师。
|
|
</div>
|
|
</div>
|
|
<div class="footer">
|
|
金久信签约宝 · AI 合同审查服务 | 报告生成时间:${new Date().toLocaleString('zh-CN')}
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
module.exports = {
|
|
main: async (event) => {
|
|
let { data = {}, userInfo } = event;
|
|
const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG';
|
|
const BOT_ID = '7689821072762011698';
|
|
const db = uniCloud.database();
|
|
const _ = db.command;
|
|
|
|
let fileId = data.file_id;
|
|
|
|
// ===== 模式1:上传文件 + 创建待审查记录 =====
|
|
if (data.upload_only && data.file_base64) {
|
|
const fileBuffer = Buffer.from(data.file_base64, 'base64');
|
|
const boundary = '----FormBoundary' + Date.now();
|
|
const header = Buffer.from(
|
|
'--' + boundary + '\r\n' +
|
|
'Content-Disposition: form-data; name="file"; filename="' + (data.file_name || 'file') + '"\r\n' +
|
|
'Content-Type: application/octet-stream\r\n\r\n'
|
|
);
|
|
const footer = Buffer.from('\r\n--' + boundary + '--\r\n');
|
|
const uploadBody = Buffer.concat([header, fileBuffer, footer]);
|
|
|
|
try {
|
|
const uploadRes = await uniCloud.httpclient.request('https://api.coze.cn/v1/files/upload', {
|
|
method: 'POST',
|
|
dataType: 'json',
|
|
headers: {
|
|
'Authorization': 'Bearer ' + COZE_TOKEN,
|
|
'Content-Type': 'multipart/form-data; boundary=' + boundary,
|
|
'Content-Length': uploadBody.length.toString()
|
|
},
|
|
content: uploadBody,
|
|
timeout: 600000
|
|
});
|
|
if (uploadRes.data.code !== 0) {
|
|
return { code: -1, msg: '文件上传失败: ' + uploadRes.data.msg };
|
|
}
|
|
fileId = uploadRes.data.data.id;
|
|
|
|
// 同时上传到 uniCloud 云存储(用于查看/下载)
|
|
let fileUrl = '';
|
|
try {
|
|
const cloudPath = 'review-files/' + Date.now() + '_' + (data.file_name || 'file');
|
|
const uploadFileRes = await uniCloud.uploadFile({
|
|
cloudPath: cloudPath,
|
|
fileContent: fileBuffer
|
|
});
|
|
if (uploadFileRes.fileID) {
|
|
const urlRes = await uniCloud.getTempFileURL({
|
|
fileList: [uploadFileRes.fileID]
|
|
});
|
|
if (urlRes.fileList && urlRes.fileList[0]) {
|
|
fileUrl = urlRes.fileList[0].tempFileURL;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('云存储上传失败(不影响审查):', e.message);
|
|
}
|
|
|
|
// 创建待审查记录
|
|
const recordRes = await db.collection('jjx-review-records').add({
|
|
user_id: (data && data.uid) || 'anonymous',
|
|
file_name: data.file_name || '',
|
|
file_size: data.file_size || 0,
|
|
file_id: fileId,
|
|
file_url: fileUrl,
|
|
chat_id: '',
|
|
conversation_id: '',
|
|
status: 'uploaded',
|
|
result: '',
|
|
points_used: 0,
|
|
_add_time: new Date(),
|
|
update_time: new Date()
|
|
});
|
|
|
|
return { code: 0, msg: 'ok', data: { file_id: fileId, record_id: recordRes.id, file_url: fileUrl } };
|
|
} catch (e) {
|
|
return { code: -1, msg: '上传异常: ' + e.message };
|
|
}
|
|
}
|
|
|
|
// ===== 模式2:发起审查(只更新状态,立即返回)=====
|
|
if (!fileId) return { code: -1, msg: '缺少 file_id' };
|
|
|
|
const recordId = data.record_id;
|
|
if (!recordId) return { code: -1, msg: '缺少 record_id' };
|
|
|
|
// 检查当前状态
|
|
const record = await db.collection('jjx-review-records').doc(recordId).get();
|
|
if (record.data.length) {
|
|
const rec = record.data[0];
|
|
|
|
// 已完成,直接返回结果
|
|
if (rec.status === 'completed') {
|
|
return {
|
|
code: 0, msg: 'ok',
|
|
data: {
|
|
record_id: recordId,
|
|
status: 'completed',
|
|
result: rec.result,
|
|
points_used: rec.points_used,
|
|
html_url: rec.html_url || '',
|
|
contract_info: rec.contract_info || {}
|
|
}
|
|
};
|
|
}
|
|
|
|
// 正在审查中,直接返回
|
|
if (rec.status === 'reviewing') {
|
|
return { code: 0, msg: 'ok', data: { record_id: recordId, status: 'reviewing' } };
|
|
}
|
|
|
|
// 失败,返回错误
|
|
if (rec.status === 'failed') {
|
|
return { code: 0, msg: 'ok', data: { record_id: recordId, status: 'failed', result: rec.result || '审查失败' } };
|
|
}
|
|
}
|
|
|
|
// 更新状态为审查中
|
|
await db.collection('jjx-review-records').doc(recordId).update({
|
|
file_id: fileId,
|
|
file_name: data.file_name || '',
|
|
file_size: data.file_size || 0,
|
|
status: 'reviewing',
|
|
review_started_at: new Date(),
|
|
update_time: new Date()
|
|
});
|
|
|
|
// ===== 触发实际审查(await 确保数据库更新完成)=====
|
|
try {
|
|
const doReview = require('./jjxDoReview.js');
|
|
await doReview.main({
|
|
data: {
|
|
record_id: recordId,
|
|
file_id: fileId,
|
|
user_id: (userInfo && userInfo.uid) || 'anonymous',
|
|
file_name: data.file_name || '',
|
|
file_size: data.file_size || 0
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.error('审查任务执行失败:', e);
|
|
}
|
|
|
|
return {
|
|
code: 0, msg: 'ok',
|
|
data: { record_id: recordId, status: 'reviewing' }
|
|
};
|
|
}
|
|
}; |