From 3011f41df14037498305ed756b61c3fddc5224b8 Mon Sep 17 00:00:00 2001
From: ingkele <598792849@qq.com>
Date: Fri, 23 Oct 2026 19:07:12 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20AI=E5=AE=A1=E6=9F=A5=E6=8A=A5=E5=91=8A?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=8B=E8=BD=BDWord=20+=20=E6=88=91?=
=?UTF-8?q?=E7=9A=84=E6=8A=A5=E5=91=8A=E6=94=AF=E6=8C=81=E9=87=8D=E6=96=B0?=
=?UTF-8?q?=E5=AE=A1=E6=9F=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 云函数jjxReviewTimer完成审查后自动将HTML报告转换为Word文档并上传
- 新增buildDocx.js纯正则解析HTML报告生成A4公文版式docx(三级数据提取兼容结构漂移)
- 兼容智能体插件直接返回docx链接(jjx-docx-link标记协议)
- router/jjxDoReview同步支持Word生成(try降级不影响主流程)
- 前端报告列表新增下载Word/重新审查按钮
- 修复上传/查询取真实uid(vk.getVuex而非页面本地data)
---
pages/contract/index.vue | 59 +-
.../jjxReviewTimer/buildDocx.js | 922 ++++++++++++++++++
.../cloudfunctions/jjxReviewTimer/index.js | 43 +
.../jjxReviewTimer/local-test.js | 336 +++++++
.../jjxReviewTimer/package.json | 5 +-
.../router/service/client/ai/pub/buildDocx.js | 922 ++++++++++++++++++
.../service/client/ai/pub/jjxDoReview.js | 55 ++
7 files changed, 2339 insertions(+), 3 deletions(-)
create mode 100644 uniCloud-tcb/cloudfunctions/jjxReviewTimer/buildDocx.js
create mode 100644 uniCloud-tcb/cloudfunctions/jjxReviewTimer/local-test.js
create mode 100644 uniCloud-tcb/cloudfunctions/router/service/client/ai/pub/buildDocx.js
diff --git a/pages/contract/index.vue b/pages/contract/index.vue
index 2a2ce20..51a741f 100644
--- a/pages/contract/index.vue
+++ b/pages/contract/index.vue
@@ -612,8 +612,10 @@
已完成
失败
报告下载
+ 下载 Word
查看上传文件
在线查看报告
+ 重新审查
@@ -1770,14 +1772,16 @@
try {
const res = await vk.callFunction({
url: 'client/ai/pub/getMyReports',
- data: { pageIndex: 1, pageSize: 50 }
+ data: { pageIndex: 1, pageSize: 50, uid: vk.getVuex('$user.userInfo._id') || '' }
});
if (res.code === 0 && res.data && res.data.rows) {
this.reportList = res.data.rows.map(r => ({
fileName: r.file_name,
fileSize: r.file_size,
+ fileId: r.file_id || '',
fileUrl: r.file_url || '',
htmlUrl: r.html_url || '',
+ docxUrl: r.docx_url || '',
time: dayjs(r._add_time && r._add_time['$date'] ? r._add_time['$date'] : r._add_time).format('YYYY-MM-DD HH:mm:ss'),
amount: r.points_used || 0,
result: r.result,
@@ -1796,6 +1800,56 @@
this.reviewResult = r.result || '暂无审查结果';
this.reviewFile = { name: r.fileName, size: r.fileSize || 0 };
},
+ // 待审查记录 → 重新发起审查
+ async reReview(r) {
+ if (!r.fileId) {
+ this.$message.warning('该记录缺少文件信息,请重新上传合同');
+ return;
+ }
+ try {
+ await this.$confirm(
+ `确认对「${r.fileName}」发起审查?审查完成后将按文件大小扣除相应积分。`,
+ '重新审查',
+ { confirmButtonText: '确认审查', cancelButtonText: '取消', type: 'warning' }
+ );
+ } catch (e) {
+ return;
+ }
+ // 积分校验
+ try {
+ const balanceRes = await vk.callFunction({ url: 'client/points/kh/getBalance' });
+ if (balanceRes.code !== 0 || !balanceRes.data || balanceRes.data.balance < 200) {
+ this.$message.warning('积分不足,请先充值');
+ return;
+ }
+ } catch (e) {
+ this.$message.error('积分查询失败,请稍后重试');
+ return;
+ }
+ this.$set(r, '_submitting', true);
+ try {
+ const res = await vk.callFunction({
+ url: 'client/ai/pub/review',
+ data: {
+ record_id: r.recordId,
+ file_id: r.fileId,
+ file_name: r.fileName,
+ file_size: r.fileSize || 0,
+ uid: vk.getVuex('$user.userInfo._id') || ''
+ }
+ });
+ if (res.code === 0) {
+ r.status = (res.data && res.data.status) || 'reviewing';
+ this.$message.success('已提交审查,AI 正在分析中,预计 3-5 分钟完成');
+ setTimeout(() => this.queryReports(), 1500);
+ } else {
+ this.$message.error(res.msg || '提交审查失败');
+ }
+ } catch (e) {
+ this.$message.error('提交审查异常');
+ }
+ this.$set(r, '_submitting', false);
+ },
previewFile(r) {
if (!r.fileUrl) {
this.$message.warning('文件链接不存在');
@@ -1841,6 +1895,7 @@
file_base64: fileBase64,
file_name: file.name,
file_size: file.size,
+ uid: vk.getVuex('$user.userInfo._id') || '',
upload_only: true
}
});
@@ -1887,7 +1942,7 @@
file_id: this.reviewFileId,
file_name: this.reviewFile.name,
file_size: this.reviewFile.size,
- uid: this.userInfo.uid
+ uid: vk.getVuex('$user.userInfo._id') || ''
}
});
diff --git a/uniCloud-tcb/cloudfunctions/jjxReviewTimer/buildDocx.js b/uniCloud-tcb/cloudfunctions/jjxReviewTimer/buildDocx.js
new file mode 100644
index 0000000..f69dcc4
--- /dev/null
+++ b/uniCloud-tcb/cloudfunctions/jjxReviewTimer/buildDocx.js
@@ -0,0 +1,922 @@
+'use strict';
+
+/**
+ * 合同审查报告 → Word(.docx) 生成器
+ *
+ * 数据来源:智能体在 HTML 报告末尾输出的结构化 JSON
+ *
+ *
+ * 排版目标:正式法律文书风格,A4 纵向,可直接打印装订
+ */
+
+const {
+ Document,
+ Packer,
+ Paragraph,
+ TextRun,
+ Table,
+ TableRow,
+ TableCell,
+ WidthType,
+ AlignmentType,
+ BorderStyle,
+ ShadingType,
+ TableLayoutType,
+ Footer,
+ Header,
+ PageNumber,
+ VerticalAlign,
+ ExternalHyperlink
+} = require('docx');
+
+const FONT = 'Microsoft YaHei';
+const FONT_SONG = 'SimSun';
+
+const C = {
+ primary: '1A5276',
+ danger: 'E74C3C',
+ success: '27AE60',
+ warning: 'F39C12',
+ text: '222222',
+ gray: '777777',
+ lightBg: 'F5F7F9',
+ pinkBg: 'FDF2F2',
+ border: 'D0D7DE'
+};
+
+const PAGE_BORDER = {
+ top: { style: BorderStyle.SINGLE, size: 4, color: C.border },
+ bottom: { style: BorderStyle.SINGLE, size: 4, color: C.border },
+ left: { style: BorderStyle.SINGLE, size: 4, color: C.border },
+ right: { style: BorderStyle.SINGLE, size: 4, color: C.border },
+ insideHorizontal: { style: BorderStyle.SINGLE, size: 4, color: C.border },
+ insideVertical: { style: BorderStyle.SINGLE, size: 4, color: C.border }
+};
+
+// ---------- 基础元件 ----------
+
+function run(text, o) {
+ o = o || {};
+ const opt = {
+ text: String(text === null || text === undefined ? '' : text),
+ font: o.font || FONT,
+ size: o.size || 21,
+ bold: !!o.bold,
+ italics: !!o.italics,
+ color: o.color || C.text
+ };
+ if (o.underline) opt.underline = {};
+ return new TextRun(opt);
+}
+
+function para(children, o) {
+ o = o || {};
+ const opt = {
+ children: Array.isArray(children) ? children : [children],
+ spacing: {
+ before: o.before === undefined ? 60 : o.before,
+ after: o.after === undefined ? 60 : o.after,
+ line: o.line || 320
+ }
+ };
+ if (o.alignment) opt.alignment = o.alignment;
+ if (o.indent) opt.indent = o.indent;
+ if (o.border) opt.border = o.border;
+ if (o.shading) opt.shading = { type: ShadingType.CLEAR, fill: o.shading, color: 'auto' };
+ return new Paragraph(opt);
+}
+
+function cell(children, o) {
+ o = o || {};
+ const opt = {
+ children: Array.isArray(children) ? children : [children],
+ verticalAlign: VerticalAlign.CENTER,
+ margins: { top: 60, bottom: 60, left: 110, right: 110 }
+ };
+ if (o.shading) opt.shading = { type: ShadingType.CLEAR, fill: o.shading, color: 'auto' };
+ if (o.width) opt.width = { size: o.width, type: WidthType.PERCENTAGE };
+ if (o.columnSpan) opt.columnSpan = o.columnSpan;
+ return new TableCell(opt);
+}
+
+function table(rows, widths) {
+ return new Table({
+ rows: rows,
+ width: { size: 100, type: WidthType.PERCENTAGE },
+ columnWidths: widths,
+ layout: TableLayoutType.AUTOFIT,
+ borders: PAGE_BORDER
+ });
+}
+
+function sectionTitle(no, text) {
+ return para([run(no + '、' + text, { size: 28, bold: true, color: C.primary })], {
+ before: 320,
+ after: 140,
+ border: { bottom: { style: BorderStyle.SINGLE, size: 8, color: C.primary, space: 4 } }
+ });
+}
+
+function pick(obj, keys, dft) {
+ if (!obj) return dft;
+ for (let i = 0; i < keys.length; i++) {
+ const v = obj[keys[i]];
+ if (v !== undefined && v !== null && v !== '') return v;
+ }
+ return dft;
+}
+
+function asArray(v) {
+ if (!v) return [];
+ return Array.isArray(v) ? v : [v];
+}
+
+// ---------- 风险条目 ----------
+
+function riskBlock(item, color, levelText) {
+ const clause = pick(item, ['clause', 'no', '条款', 'clauseNo'], '');
+ const issue = pick(item, ['issue', 'risk', 'problem', '描述', '问题'], '');
+ const suggest = pick(item, ['suggestion', 'fix', 'advice', '建议', '修改建议'], '');
+ const basis = pick(item, ['legal_basis', 'legalBasis', 'basis', '法律依据'], '');
+ const blocks = [];
+
+ blocks.push(para([
+ run('[' + levelText + ']', { bold: true, color: color, size: 21 }),
+ run(clause ? ' 第 ' + clause + ' 条' : ' 合同条款', { bold: true, color: color, size: 21 })
+ ], { before: 140, after: 40 }));
+
+ blocks.push(para([
+ run('问题:', { bold: true, size: 21 }),
+ run(issue, { size: 21 })
+ ], { after: 40, indent: { left: 240 } }));
+
+ if (suggest) {
+ blocks.push(para([
+ run('建议:', { bold: true, size: 21 }),
+ run(suggest, { size: 21 })
+ ], { after: 40, indent: { left: 240 } }));
+ }
+
+ if (basis) {
+ blocks.push(para([
+ run('依据:', { bold: true, size: 21 }),
+ run(basis, { size: 21, color: C.gray })
+ ], { after: 80, indent: { left: 240 } }));
+ }
+
+ return blocks;
+}
+
+function riskSection(items, color, levelText, emptyText) {
+ const list = asArray(items);
+ if (!list.length) {
+ return [para([run(emptyText || '无', { size: 21, color: C.gray })], { after: 80 })];
+ }
+ let out = [];
+ list.forEach(function (it) {
+ out = out.concat(riskBlock(it, color, levelText));
+ });
+ return out;
+}
+
+// ---------- 主函数 ----------
+
+async function buildDocx(input) {
+ const data = input.data || {};
+ const fileName = input.fileName || '合同审查报告';
+ const contractName = pick(data, ['contract_name', 'contractName', '协议名称', 'name'], fileName.replace(/\.[^.]+$/, ''));
+ const reportTitle = pick(data, ['report_title', 'reportTitle', '报告标题'], '合同审查报告');
+ const reviewDate = pick(data, ['review_date', 'reviewDate', '审核日期'], new Date().toLocaleDateString('zh-CN'));
+ const summary = pick(data, ['summary', '统计'], {}) || {};
+ const conclusion = pick(data, ['conclusion', '审核结论', 'summary_text'], '');
+ const highRisks = pick(data, ['high_risks', 'highRisks', '高风险'], []);
+ const mediumRisks = pick(data, ['medium_risks', 'mediumRisks', '中风险'], []);
+ const lowRisks = pick(data, ['low_risks', 'lowRisks', '低风险'], []);
+ const suggestions = pick(data, ['suggestions', '优化建议', 'advice_table'], []);
+ const templates = pick(data, ['clause_templates', 'clauseTemplates', '补充条款'], []);
+ const legalBases = pick(data, ['legal_bases', 'legalBases', '法律依据'], []);
+ const cases = pick(data, ['cases', '案例'], []);
+ const disclaimer = pick(data, ['disclaimer', '免责声明'], '');
+
+ const nCompliant = pick(summary, ['compliant', '合规'], 0);
+ const nPartial = pick(summary, ['partial', '部分合规'], 0);
+ const nHigh = pick(summary, ['high', '高风险'], asArray(highRisks).length);
+ const nMedium = pick(summary, ['medium', '中风险'], asArray(mediumRisks).length);
+
+ const children = [];
+
+ // ===== 抬头 =====
+ children.push(para([run(reportTitle, { size: 44, bold: true, color: C.primary })], {
+ alignment: AlignmentType.CENTER,
+ before: 200,
+ after: 120
+ }));
+ children.push(para([run(contractName, { size: 26, color: C.gray })], {
+ alignment: AlignmentType.CENTER,
+ after: 60
+ }));
+ children.push(para([run('审核日期:' + reviewDate + ' 出具机构:金久信签约宝', { size: 20, color: C.gray })], {
+ alignment: AlignmentType.CENTER,
+ after: 240
+ }));
+
+ // ===== 目录 =====
+ children.push(sectionTitle('一', '报告目录'));
+ const tocItems = [
+ '二、审核结论',
+ '三、需处理问题(高风险 / 中风险)',
+ '四、优化建议汇总表',
+ '五、补充条款模板',
+ '六、法律依据',
+ '七、相关案例',
+ '八、免责声明'
+ ];
+ tocItems.forEach(function (item) {
+ children.push(para([run(item, { size: 21 })], { after: 30, indent: { left: 240 } }));
+ });
+
+ // ===== 审核结论 =====
+ children.push(sectionTitle('二', '审核结论'));
+
+ const statRow = new TableRow({
+ children: [
+ cell([para([run(String(nCompliant), { size: 40, bold: true, color: C.success })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
+ para([run('合规条款', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
+ { shading: 'F0F9EB', width: 25 }),
+ cell([para([run(String(nPartial), { size: 40, bold: true, color: C.warning })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
+ para([run('部分合规', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
+ { shading: 'FDF6EC', width: 25 }),
+ cell([para([run(String(nHigh), { size: 40, bold: true, color: C.danger })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
+ para([run('高风险', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
+ { shading: 'FEF0F0', width: 25 }),
+ cell([para([run(String(nMedium), { size: 40, bold: true, color: C.warning })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
+ para([run('中风险', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
+ { shading: 'FDF6EC', width: 25 })
+ ]
+ });
+ children.push(table([statRow], [2000, 2000, 2000, 2000]));
+
+ if (conclusion) {
+ children.push(para([run('审核结论', { size: 21, bold: true, color: C.primary })], { before: 200, after: 80 }));
+ String(conclusion).split(/\n+/).forEach(function (line) {
+ if (line.trim()) {
+ children.push(para([run(line.trim(), { size: 21 })], { after: 60, indent: { left: 240 } }));
+ }
+ });
+ }
+
+ // ===== 需处理问题 =====
+ children.push(sectionTitle('三', '需处理问题'));
+
+ children.push(para([run('(一)高风险问题', { size: 24, bold: true, color: C.danger })], { before: 140, after: 80 }));
+ riskSection(highRisks, C.danger, '高风险', '本次审查未发现高风险问题。').forEach(function (p) { children.push(p); });
+
+ children.push(para([run('(二)中风险问题', { size: 24, bold: true, color: C.warning })], { before: 200, after: 80 }));
+ riskSection(mediumRisks, C.warning, '中风险', '本次审查未发现中风险问题。').forEach(function (p) { children.push(p); });
+
+ if (asArray(lowRisks).length) {
+ children.push(para([run('(三)低风险问题', { size: 24, bold: true, color: C.gray })], { before: 200, after: 80 }));
+ riskSection(lowRisks, C.gray, '低风险', '无').forEach(function (p) { children.push(p); });
+ }
+
+ // ===== 优化建议汇总表 =====
+ children.push(sectionTitle('四', '优化建议汇总表'));
+ const sugList = asArray(suggestions);
+ if (!sugList.length) {
+ children.push(para([run('无', { size: 21, color: C.gray })]));
+ } else {
+ const headRow = new TableRow({
+ tableHeader: true,
+ children: [
+ cell([para([run('优先级', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 12 }),
+ cell([para([run('条款编号', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 15 }),
+ cell([para([run('问题描述', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 38 }),
+ cell([para([run('建议方案', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 35 })
+ ]
+ });
+ const rows = [headRow];
+ sugList.forEach(function (s, i) {
+ const lv = String(pick(s, ['priority', 'level', '优先级'], 'M')).toUpperCase();
+ const lvBg = lv === 'H' ? C.danger : (lv === 'L' ? '999999' : C.warning);
+ rows.push(new TableRow({
+ children: [
+ cell([para([run(lv, { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: lvBg, width: 12 }),
+ cell([para([run(pick(s, ['clause', 'no', '条款编号'], '-'), { size: 19 })], { alignment: AlignmentType.CENTER })], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 15 }),
+ cell([para([run(pick(s, ['issue', 'problem', '问题描述', '描述'], '-'), { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 38 }),
+ cell([para([run(pick(s, ['solution', 'suggestion', '建议方案', '建议'], '-'), { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 35 })
+ ]
+ }));
+ });
+ children.push(table(rows, [1200, 1500, 3800, 3500]));
+ }
+
+ // ===== 补充条款模板 =====
+ children.push(sectionTitle('五', '补充条款模板'));
+ const tplList = asArray(templates);
+ if (!tplList.length) {
+ children.push(para([run('无', { size: 21, color: C.gray })]));
+ } else {
+ tplList.forEach(function (t, i) {
+ const title = pick(t, ['title', 'name', '条款名称'], '补充条款 ' + (i + 1));
+ const content = pick(t, ['content', 'text', '条文', '内容'], '');
+ children.push(para([run(String(i + 1) + '. ' + title, { size: 21, bold: true, color: C.primary })], { before: 140, after: 60 }));
+ String(content).split(/\n+/).forEach(function (line) {
+ if (line.trim()) {
+ children.push(para([run(line.trim(), { size: 20, font: FONT_SONG })], {
+ after: 50,
+ indent: { left: 240 },
+ shading: C.lightBg,
+ border: { left: { style: BorderStyle.SINGLE, size: 12, color: C.primary, space: 6 } }
+ }));
+ }
+ });
+ });
+ }
+
+ // ===== 法律依据 =====
+ children.push(sectionTitle('六', '法律依据'));
+ const lawList = asArray(legalBases);
+ if (!lawList.length) {
+ children.push(para([run('本报告未引用具体法条,相关意见为实务建议,具体法律适用需律师人工核实。', { size: 20, color: C.gray })]));
+ } else {
+ lawList.forEach(function (l, i) {
+ const law = pick(l, ['law', 'name', '法律名称'], '');
+ const article = pick(l, ['article', 'no', '条款编号'], '');
+ const content = pick(l, ['content', 'text', '引用内容', '内容'], '');
+ children.push(para([
+ run(String(i + 1) + '. ', { size: 20, bold: true }),
+ run(law + (article ? ' ' + article : ''), { size: 20, bold: true, color: C.primary }),
+ run(content ? ':' + content : '', { size: 20 })
+ ], { after: 60, indent: { left: 120 } }));
+ });
+ }
+
+ // ===== 相关案例 =====
+ children.push(sectionTitle('七', '相关案例'));
+ const caseList = asArray(cases);
+ if (!caseList.length) {
+ children.push(para([run('暂未检索到公开案例,建议人工核实。', { size: 20, color: C.gray })]));
+ } else {
+ const caseHead = new TableRow({
+ tableHeader: true,
+ children: [
+ cell([para([run('序号', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 8 }),
+ cell([para([run('案件名称·案由', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 26 }),
+ cell([para([run('法院·机构', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 14 }),
+ cell([para([run('年份', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 8 }),
+ cell([para([run('判决要点', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 28 }),
+ cell([para([run('与风险点关联', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 16 })
+ ]
+ });
+ const caseRows = [caseHead];
+ caseList.forEach(function (c, i) {
+ const name = pick(c, ['name', 'title', '案件名称'], '-');
+ const url = pick(c, ['url', 'link', '链接'], '');
+ const court = pick(c, ['court', 'org', '法院'], '-');
+ const year = pick(c, ['year', '年份'], '-');
+ const result = pick(c, ['result', 'point', '判决要点', '要点'], '-');
+ const relation = pick(c, ['relation', '关联', '与本次风险点关联'], '-');
+ const nameCellChildren = [para([run(name, { size: 19, bold: true })], { after: url ? 20 : 0 })];
+ if (url) {
+ const host = url.replace(/^https?:\/\//i, '').split('/')[0];
+ nameCellChildren.push(para([
+ new ExternalHyperlink({
+ link: url,
+ children: [run('来源:' + host + ' ↗', { size: 16, color: '2E74B5', underline: true })]
+ })
+ ], {}));
+ }
+ caseRows.push(new TableRow({
+ children: [
+ cell([para([run(String(i + 1), { size: 19 })], { alignment: AlignmentType.CENTER })], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 8 }),
+ cell(nameCellChildren, { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 26 }),
+ cell([para([run(court, { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 14 }),
+ cell([para([run(year, { size: 19 })], { alignment: AlignmentType.CENTER })], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 8 }),
+ cell([para([run(result, { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 28 }),
+ cell([para([run(relation, { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 16 })
+ ]
+ }));
+ });
+ children.push(table(caseRows, [700, 2300, 1300, 700, 2500, 1500]));
+ }
+
+ // ===== 免责声明 =====
+ children.push(sectionTitle('八', '免责声明'));
+ const disText = disclaimer || '本报告由人工智能根据用户上传的合同文本自动生成,仅供内部风险排查参考,不构成正式法律意见,亦不能替代执业律师的专业审查。涉及重大交易、争议解决或诉讼事项,请务必委托执业律师复核。报告中的法律法规引用以官方公布文本为准,案例信息来源于公开检索结果,具体适用请人工核实。';
+ String(disText).split(/\n+/).forEach(function (line) {
+ if (line.trim()) {
+ children.push(para([run(line.trim(), { size: 19, color: C.gray })], {
+ after: 50,
+ shading: C.pinkBg,
+ indent: { left: 120, right: 120 },
+ border: { left: { style: BorderStyle.SINGLE, size: 12, color: C.danger, space: 6 } }
+ }));
+ }
+ });
+
+ // ===== 文档 =====
+ const doc = new Document({
+ creator: '金久信签约宝',
+ title: contractName + ' - 合同审查报告',
+ description: 'AI 合同审查报告',
+ styles: {
+ default: {
+ document: {
+ run: { font: FONT, size: 21, color: C.text }
+ }
+ }
+ },
+ sections: [{
+ properties: {
+ page: {
+ margin: { top: 1134, right: 1134, bottom: 1134, left: 1134 }
+ }
+ },
+ headers: {
+ default: new Header({
+ children: [
+ para([
+ run('金久信签约宝 · AI 合同审查报告', { size: 18, color: C.gray }),
+ run(' ' + contractName, { size: 18, color: C.gray })
+ ], { alignment: AlignmentType.RIGHT, after: 0 })
+ ]
+ })
+ },
+ footers: {
+ default: new Footer({
+ children: [
+ new Paragraph({
+ alignment: AlignmentType.CENTER,
+ children: [
+ new TextRun({ text: '第 ', font: FONT, size: 18, color: C.gray }),
+ new TextRun({ children: [PageNumber.CURRENT], font: FONT, size: 18, color: C.gray }),
+ new TextRun({ text: ' 页 / 共 ', font: FONT, size: 18, color: C.gray }),
+ new TextRun({ children: [PageNumber.TOTAL_PAGES], font: FONT, size: 18, color: C.gray }),
+ new TextRun({ text: ' 页', font: FONT, size: 18, color: C.gray })
+ ]
+ })
+ ]
+ })
+ },
+ children: children
+ }]
+ });
+
+ return await Packer.toBuffer(doc);
+}
+
+// ---------- HTML 解析(生产数据兜底通道) ----------
+// 智能体实际返回的是纯 HTML 报告,未必带 jjx-report-data 数据块,
+// 这里按区块结构把 HTML 还原成结构化数据,无需修改智能体提示词。
+
+function decodeEntities(s) {
+ return String(s || '')
+ .replace(/ /g, ' ')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'|'/g, "'")
+ .replace(/&/g, '&')
+ .replace(/(\d+);/g, function (m, d) { return String.fromCharCode(Number(d)); })
+ .replace(/([0-9a-fA-F]+);/g, function (m, h) { return String.fromCharCode(parseInt(h, 16)); });
+}
+
+function stripTags(html) {
+ return decodeEntities(
+ String(html || '')
+ .replace(/
/gi, '\n')
+ .replace(/<\/(p|div|li|tr|h\d|section)>/gi, '\n')
+ .replace(/
]*>/gi, '\n')
+ .replace(/<[^>]+>/g, '')
+ )
+ .replace(/[ \t\u00a0]+/g, ' ')
+ .replace(/\n[ ]+/g, '\n')
+ .replace(/\n{3,}/g, '\n\n')
+ .trim();
+}
+
+function pickDiv(html, cls) {
+ const re = new RegExp(']*class="[^"]*\\b' + cls + '\\b[^"]*"[^>]*>([\\s\\S]*?)<\\/div>', 'i');
+ const m = String(html || '').match(re);
+ return m ? stripTags(m[1]) : '';
+}
+
+// 按