weaver-work-cli 0.1.29 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/archive-U766FTFH.js +2537 -0
- package/dist/chunks/archive-oa-DMWEKX3L.js +1317 -0
- package/dist/chunks/asset-TCR7VDEV.js +2109 -0
- package/dist/chunks/attachment-content-AQNX5N3S.js +23 -0
- package/dist/chunks/blog-EVZWGDVM.js +2157 -0
- package/dist/chunks/caizhilian-JOWS6SL5.js +10087 -0
- package/dist/chunks/calendar-5WNCV66Q.js +1195 -0
- package/dist/chunks/chunk-2DUV44UX.js +15606 -0
- package/dist/chunks/chunk-324YPTC2.js +1607 -0
- package/dist/chunks/chunk-65QMVWHS.js +231 -0
- package/dist/chunks/chunk-6V7ZLF34.js +387 -0
- package/dist/chunks/chunk-AC7KJWC5.js +358 -0
- package/dist/chunks/chunk-AJ62BGAW.js +29 -0
- package/dist/chunks/chunk-BUSC6UAE.js +43 -0
- package/dist/chunks/chunk-CVZ2JOSW.js +62 -0
- package/dist/chunks/chunk-D4LTJSJD.js +477 -0
- package/dist/chunks/chunk-EG6ZU444.js +654 -0
- package/dist/chunks/chunk-EZALDOQH.js +383 -0
- package/dist/chunks/chunk-FMPJJYYI.js +106 -0
- package/dist/chunks/chunk-G2JSIUWF.js +769 -0
- package/dist/chunks/chunk-GCG2NMWR.js +703 -0
- package/dist/chunks/chunk-GPMFD4RD.js +75 -0
- package/dist/chunks/chunk-H3WDBYJW.js +508 -0
- package/dist/chunks/chunk-H6XSTSLM.js +2357 -0
- package/dist/chunks/chunk-J7RJTZSV.js +5556 -0
- package/dist/chunks/chunk-LUSPKZKY.js +53 -0
- package/dist/chunks/chunk-NIAJ5ZCG.js +73 -0
- package/dist/chunks/chunk-NOODWMCL.js +584 -0
- package/dist/chunks/chunk-OOUTBLZ4.js +51 -0
- package/dist/chunks/chunk-P26ZWSKU.js +73 -0
- package/dist/chunks/chunk-QWY6PGL6.js +3472 -0
- package/dist/chunks/chunk-R2IJAK7T.js +464 -0
- package/dist/chunks/chunk-R7O5PUWO.js +205 -0
- package/dist/chunks/chunk-RCGYCOMG.js +6962 -0
- package/dist/chunks/chunk-VZTXRG6X.js +403 -0
- package/dist/chunks/chunk-WHD4XMRP.js +161 -0
- package/dist/chunks/chunk-YLCYX7LE.js +371 -0
- package/dist/chunks/chunk-ZEDWSH32.js +68 -0
- package/dist/chunks/continuation-2CKSBLA6.js +11 -0
- package/dist/chunks/continuation-HZIZRZBX.js +13 -0
- package/dist/chunks/continuation-NLECEJPX.js +11 -0
- package/dist/chunks/ebuilder-form-NJ4YK6CC.js +2276 -0
- package/dist/chunks/ehr-MMYN36VA.js +5241 -0
- package/dist/chunks/employee-SAK2QYXX.js +11 -0
- package/dist/chunks/esb-HIWHOU7Q.js +141 -0
- package/dist/chunks/fna-KBACQTA5.js +1444 -0
- package/dist/chunks/host-3OHS5QUX.js +21 -0
- package/dist/chunks/host-IG6KEH5Q.js +19 -0
- package/dist/chunks/host-KQL5N4HO.js +23 -0
- package/dist/chunks/host-LLSZQCWR.js +17 -0
- package/dist/chunks/hrm-LYEC67ZQ.js +244 -0
- package/dist/chunks/im-RN2UKAEU.js +252 -0
- package/dist/chunks/invoice-EEPHLAF6.js +5179 -0
- package/dist/chunks/jiuchuanhui-WVVZYSJE.js +1940 -0
- package/dist/chunks/lazy-dispatch-MDOFFL2S.js +28 -0
- package/dist/chunks/mail-LNQLTF6L.js +2695 -0
- package/dist/chunks/meeting-XSD3BDVQ.js +504 -0
- package/dist/chunks/msg-parse-4GSQ2WTO.js +41 -0
- package/dist/chunks/okr-TMWTIU2K.js +2482 -0
- package/dist/chunks/plan-A2FSFA7T.js +2025 -0
- package/dist/chunks/project-T26QLBQF.js +1361 -0
- package/dist/chunks/registry-2VLUAESA.js +14 -0
- package/dist/chunks/registry-JCFEYXOE.js +18 -0
- package/dist/chunks/registry-NYPYMZXG.js +16 -0
- package/dist/chunks/registry-Q7OQJ3SU.js +19 -0
- package/dist/chunks/registry-ZJMJVWFA.js +19 -0
- package/dist/chunks/room-recommend-UR4ZDZS6.js +20 -0
- package/dist/chunks/schedule-5L2PM4M6.js +24 -0
- package/dist/chunks/state-Y2XBP6YE.js +15 -0
- package/dist/chunks/workflow-TR6PG2SL.js +245 -0
- package/dist/cmd/argv.js +45 -0
- package/dist/cmd/docs/index.js +3 -3
- package/dist/cmd/lazy-dispatch.js +20 -0
- package/dist/cmd/root.js +2 -4
- package/dist/cmd/setup/index.js +2 -2
- package/dist/cmd/skills/index.js +2 -2
- package/dist/core/package-paths.js +47 -0
- package/dist/index.js +163 -7
- package/dist/internal/e10/auth/xiaoe.js +3 -3
- package/dist/internal/e10/request-runtime.js +18 -62
- package/dist/internal/skills/detector.js +2 -2
- package/dist/shortcuts/contract/data/performance-catalog.js +1306 -0
- package/dist/shortcuts/contract/host.js +91 -35
- package/dist/shortcuts/contract/index.js +213 -0
- package/dist/shortcuts/contract/manifest.js +928 -0
- package/dist/shortcuts/contract/ooxml/docx.js +295 -0
- package/dist/shortcuts/contract/ooxml/xlsx.js +221 -0
- package/dist/shortcuts/contract/ooxml/zip.js +158 -0
- package/dist/shortcuts/contract/operations/assistant.js +1156 -0
- package/dist/shortcuts/contract/operations/clause-library.js +522 -0
- package/dist/shortcuts/contract/operations/compare.js +193 -0
- package/dist/shortcuts/contract/operations/credit-page.js +34 -0
- package/dist/shortcuts/contract/operations/flow.js +98 -0
- package/dist/shortcuts/contract/operations/page.js +66 -0
- package/dist/shortcuts/contract/operations/performance.js +733 -0
- package/dist/shortcuts/contract/operations/quote.js +557 -0
- package/dist/shortcuts/contract/operations/recommend.js +78 -0
- package/dist/shortcuts/contract/operations/registry.js +18 -0
- package/dist/shortcuts/contract/operations/review.js +83 -0
- package/dist/shortcuts/contract/operations/shared.js +37 -0
- package/dist/shortcuts/contract/operations/upload.js +108 -0
- package/dist/shortcuts/contract/operations/write.js +3 -2
- package/dist/shortcuts/index.js +140 -40
- package/dist/shortcuts/invoice/manifest.js +34 -11
- package/dist/shortcuts/invoice/operations/reim-auto.js +9 -0
- package/dist/shortcuts/invoice/operations/reim.js +81 -5
- package/dist/shortcuts/invoice/operations/shared.js +12 -5
- package/dist/shortcuts/meeting/host.js +45 -3
- package/dist/shortcuts/meeting/invitation/docx.js +442 -0
- package/dist/shortcuts/meeting/operations/attachment-content.js +418 -0
- package/dist/shortcuts/meeting/operations/attachment.js +442 -0
- package/dist/shortcuts/meeting/operations/eb/cache.js +57 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +343 -28
- package/dist/shortcuts/meeting/operations/eb/conflict.js +372 -11
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +710 -0
- package/dist/shortcuts/meeting/operations/eb/decision.js +47 -1
- package/dist/shortcuts/meeting/operations/eb/factory.js +129 -11
- package/dist/shortcuts/meeting/operations/eb/index.js +122 -2
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +378 -0
- package/dist/shortcuts/meeting/operations/eb/room-summary.js +84 -0
- package/dist/shortcuts/meeting/operations/eb/schedule.js +450 -0
- package/dist/shortcuts/meeting/operations/from-group-chat.js +576 -0
- package/dist/shortcuts/meeting/operations/invitation.js +458 -0
- package/dist/shortcuts/meeting/operations/minute-content.js +266 -0
- package/dist/shortcuts/meeting/operations/pipeline.js +489 -0
- package/dist/shortcuts/meeting/operations/qrcode.js +131 -0
- package/dist/shortcuts/meeting/operations/receipts.js +1 -1
- package/dist/shortcuts/meeting/operations/registry.js +18 -0
- package/dist/shortcuts/meeting/operations/summary.js +276 -0
- package/dist/shortcuts/meeting/operations/task-notify.js +325 -0
- package/dist/shortcuts/meeting/operations.js +42 -1
- package/dist/shortcuts/meeting/qr/encoder.js +427 -0
- package/dist/shortcuts/meeting/qr/render.js +103 -0
- package/dist/shortcuts/workflow/operations/form.js +17 -5
- package/docs/contract.md +39 -4
- package/docs/invoice.md +5 -5
- package/package.json +3 -2
- package/scripts/bundle.mjs +63 -0
- package/skill-template/business-info.json +25 -4
- package/skill-template/domains/jinchengda.md +16 -3
- package/skill-template/domains/yepiaotong.md +1 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +111 -31
- package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +7 -13
- package/skills/weaver-e10-jinchengda/references/contract-ai-assistant.md +150 -0
- package/skills/weaver-e10-jinchengda/references/contract-ai-review.md +87 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-library-crud.md +111 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +0 -13
- package/skills/weaver-e10-jinchengda/references/contract-credit-page.md +50 -0
- package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +4 -2
- package/skills/weaver-e10-jinchengda/references/contract-doc-compare.md +74 -0
- package/skills/weaver-e10-jinchengda/references/contract-flow-launch.md +84 -0
- package/skills/weaver-e10-jinchengda/references/contract-performance-query.md +29 -0
- package/skills/weaver-e10-jinchengda/references/contract-quote-to-contract.md +28 -0
- package/skills/weaver-e10-jinchengda/references/contract-request-status.md +2 -21
- package/skills/weaver-e10-jinchengda/references/contract-request-write.md +0 -24
- package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +0 -17
- package/skills/weaver-e10-jinchengda/references/contract-template-recommend.md +70 -0
- package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +0 -15
- package/skills/weaver-e10-jinchengda/references/source-manifest.json +1168 -152
- package/skills/weaver-e10-meeting/SKILL.md +493 -17
- package/skills/weaver-e10-meeting/references/meeting-change.md +4 -4
- package/skills/weaver-e10-meeting/references/meeting-create.md +6 -6
- package/skills/weaver-e10-meeting/references/meeting-eb.md +503 -27
- package/skills/weaver-e10-meeting/references/meeting-invitation.md +113 -0
- package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +6 -6
- package/skills/weaver-e10-meeting/references/meeting-minute-file.md +425 -0
- package/skills/weaver-e10-meeting/references/meeting-queries.md +4 -4
- package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +15 -15
- package/skills/weaver-e10-meeting/references/meeting-verification.md +19 -11
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-yepiaotong/SKILL.md +9 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +1 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +6 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +14 -11
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +97 -56
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +84 -60
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +142 -105
- package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +53 -194
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +164 -72
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +6 -0
- package/templates/meeting/invitation.docx +0 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⑧ 报价单生成合同:xlsx 解析 + docx 模板填充(零依赖 OOXML)。
|
|
3
|
+
*
|
|
4
|
+
* 语义与源脚本 quote_to_contract.py 对齐:
|
|
5
|
+
* - 硬门禁:没有真实 .docx 模板禁止生成合同,禁止把报价单直接渲染成合同。
|
|
6
|
+
* - sheet 由用户选择,禁止自动选(sheets 只列名)。
|
|
7
|
+
* - 分层打分:精确(100) > 去括号精确(98) > 别名是标签子串(70-95) > 标签是别名子串(60) > 字符重合(40);
|
|
8
|
+
* “大写/小写”硬约束避免“甲方”被“甲方联系人”反超。
|
|
9
|
+
* - 纵向合并:文本列可继承、数值列只归首行(防止 20 万被重复计成 200 万)。
|
|
10
|
+
* - 一致性校验:分节明细求和 vs 表内合计、明细总计 vs 标准价合计、折扣提示。
|
|
11
|
+
*/
|
|
12
|
+
import { basename, resolve } from 'node:path';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { contractValidationError } from '../errors.js';
|
|
15
|
+
import { requireNonEmptyString } from './shared.js';
|
|
16
|
+
import { listSheetNames, readGrid } from '../ooxml/xlsx.js';
|
|
17
|
+
import { fillDocx, labelBefore } from '../ooxml/docx.js';
|
|
18
|
+
// ---------------------------------------------------------------- 人民币大写
|
|
19
|
+
const DIGITS = '零壹贰叁肆伍陆柒捌玖';
|
|
20
|
+
const UNITS = ['', '拾', '佰', '仟'];
|
|
21
|
+
const GROUPS = ['', '万', '亿', '万亿'];
|
|
22
|
+
export function rmbUpper(num) {
|
|
23
|
+
const s = String(num ?? '').replace(/,/gu, '').trim();
|
|
24
|
+
if (!s || !/^-?\d+(\.\d+)?$/.test(s))
|
|
25
|
+
return '';
|
|
26
|
+
const neg = s.startsWith('-');
|
|
27
|
+
const v = parseFloat(s);
|
|
28
|
+
const cents = Math.round(Math.abs(v) * 100);
|
|
29
|
+
const yuan = Math.floor(cents / 100);
|
|
30
|
+
const jiaoFen = cents % 100;
|
|
31
|
+
const jiao = Math.floor(jiaoFen / 10);
|
|
32
|
+
const fen = jiaoFen % 10;
|
|
33
|
+
const chunk4 = (g) => {
|
|
34
|
+
let out = '';
|
|
35
|
+
let zero = false;
|
|
36
|
+
for (let pos = 3; pos >= 0; pos--) {
|
|
37
|
+
const d = Math.floor(g / 10 ** pos) % 10;
|
|
38
|
+
if (d === 0) {
|
|
39
|
+
zero = true;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
if (zero && out)
|
|
43
|
+
out += '零';
|
|
44
|
+
zero = false;
|
|
45
|
+
out += DIGITS[d] + UNITS[pos];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
50
|
+
const intPart = (n) => {
|
|
51
|
+
if (n === 0)
|
|
52
|
+
return '零';
|
|
53
|
+
const groups = [];
|
|
54
|
+
let x = n;
|
|
55
|
+
while (x > 0) {
|
|
56
|
+
groups.push(x % 10000);
|
|
57
|
+
x = Math.floor(x / 10000);
|
|
58
|
+
}
|
|
59
|
+
let res = '';
|
|
60
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
61
|
+
const g = groups[i];
|
|
62
|
+
if (g === 0) {
|
|
63
|
+
if (res && i > 0 && groups.slice(0, i).some((v) => v !== 0) && !res.endsWith('零'))
|
|
64
|
+
res += '零';
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (res && g < 1000 && !res.endsWith('零'))
|
|
68
|
+
res += '零';
|
|
69
|
+
res += chunk4(g) + GROUPS[i];
|
|
70
|
+
}
|
|
71
|
+
return res.replace(/^零+|零+$/gu, '') || '零';
|
|
72
|
+
};
|
|
73
|
+
let res = intPart(yuan) + '元';
|
|
74
|
+
if (jiao === 0 && fen === 0) {
|
|
75
|
+
res += '整';
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
if (jiao)
|
|
79
|
+
res += DIGITS[jiao] + '角';
|
|
80
|
+
else if (fen)
|
|
81
|
+
res += '零';
|
|
82
|
+
if (fen)
|
|
83
|
+
res += DIGITS[fen] + '分';
|
|
84
|
+
}
|
|
85
|
+
return (neg ? '负' : '') + res;
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------- xlsx 报价单解析
|
|
88
|
+
const HEADER_ALIASES = {
|
|
89
|
+
no: ['编号', '序号'],
|
|
90
|
+
name: ['功能模块', '涉及功能模块', '授权许可项', '实施服务项', '报价内容', '名称'],
|
|
91
|
+
way: ['产品实现方式'],
|
|
92
|
+
desc: ['功能描述', '详细描述', '描述', '说明'],
|
|
93
|
+
price: ['单价'],
|
|
94
|
+
qty: ['数量'],
|
|
95
|
+
amount: ['金额'],
|
|
96
|
+
remark: ['备注'],
|
|
97
|
+
};
|
|
98
|
+
const SECTION_RE = /^([一二三四五六七八九十]+)\s*、\s*(.+)$/u;
|
|
99
|
+
function isNum(s) {
|
|
100
|
+
const t = s.replace(/,/gu, '');
|
|
101
|
+
return t !== '' && /^-?\d+(\.\d+)?$/u.test(t);
|
|
102
|
+
}
|
|
103
|
+
function toNum(s) {
|
|
104
|
+
const t = s.replace(/,/gu, '');
|
|
105
|
+
if (!t || !/^-?\d+(\.\d+)?$/u.test(t))
|
|
106
|
+
return null;
|
|
107
|
+
const f = parseFloat(t);
|
|
108
|
+
return Number.isInteger(f) ? f : f;
|
|
109
|
+
}
|
|
110
|
+
function fmtNum(v) {
|
|
111
|
+
if (v === null || v === undefined || v === '')
|
|
112
|
+
return '';
|
|
113
|
+
const s = String(v).replace(/[,¥¥]/gu, '').trim();
|
|
114
|
+
const f = Number(s);
|
|
115
|
+
if (!Number.isFinite(f))
|
|
116
|
+
return String(v);
|
|
117
|
+
return f === Math.trunc(f) ? f.toLocaleString('en-US') : f.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
118
|
+
}
|
|
119
|
+
export function parseQuote(buffer, sheetName, keepRemark = false, includeZero = false) {
|
|
120
|
+
const names = listSheetNames(buffer);
|
|
121
|
+
if (names.length === 0)
|
|
122
|
+
throw contractValidationError('xlsx_invalid', 'xlsx 中没有工作表。');
|
|
123
|
+
const sheet = sheetName ?? names[0];
|
|
124
|
+
const sheetIndex = names.indexOf(sheet);
|
|
125
|
+
if (sheetIndex < 0) {
|
|
126
|
+
throw contractValidationError('sheet_not_found', `sheet 不存在:${sheet};可选:${names.join('、')}`);
|
|
127
|
+
}
|
|
128
|
+
const grid = readGrid(buffer, sheetIndex);
|
|
129
|
+
const rowvals = (r, numeric = false) => {
|
|
130
|
+
const out = [];
|
|
131
|
+
for (let c = 1; c <= grid.maxCol; c++) {
|
|
132
|
+
const direct = grid.values.get(r)?.get(c);
|
|
133
|
+
let v = direct?.text ?? '';
|
|
134
|
+
if (v === '' && !numeric)
|
|
135
|
+
v = grid.textMerged.get(`${r}:${c}`)?.text ?? '';
|
|
136
|
+
out.push(v.replace(/\u00a0/gu, ' ').trim());
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
};
|
|
140
|
+
const numrow = (r) => {
|
|
141
|
+
const out = [];
|
|
142
|
+
for (let c = 1; c <= grid.maxCol; c++) {
|
|
143
|
+
const direct = grid.values.get(r)?.get(c);
|
|
144
|
+
let v = direct?.text ?? '';
|
|
145
|
+
// 数值列:纵向合并值只归首行——非首行一律视为空
|
|
146
|
+
if (grid.numMerged.has(`${r}:${c}`) && grid.textMerged.get(`${r}:${c}`) !== undefined) {
|
|
147
|
+
// 首行本身:numMerged 与 textMerged 都指向自身,直接用原值
|
|
148
|
+
}
|
|
149
|
+
else if (grid.textMerged.get(`${r}:${c}`) !== undefined) {
|
|
150
|
+
v = '';
|
|
151
|
+
}
|
|
152
|
+
out.push(v.replace(/\u00a0/gu, ' ').trim());
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
};
|
|
156
|
+
const rows = [];
|
|
157
|
+
const nrows = [];
|
|
158
|
+
for (let r = 1; r <= grid.maxRow; r++) {
|
|
159
|
+
rows.push(rowvals(r));
|
|
160
|
+
nrows.push(numrow(r));
|
|
161
|
+
}
|
|
162
|
+
const firstIdx = (row, keys) => {
|
|
163
|
+
for (let i = 0; i < row.length; i++)
|
|
164
|
+
if (keys.includes(row[i]))
|
|
165
|
+
return i;
|
|
166
|
+
return null;
|
|
167
|
+
};
|
|
168
|
+
// ---- 元信息
|
|
169
|
+
const meta = { customer: '', quote_date: '', sales: '', phone: '' };
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
const joined = row.join('');
|
|
172
|
+
for (const [label, key] of [['客户名称', 'customer'], ['报价日期', 'quote_date'], ['客户经理', 'sales'], ['联系电话', 'phone']]) {
|
|
173
|
+
if (!joined.includes(label) || meta[key])
|
|
174
|
+
continue;
|
|
175
|
+
const i = firstIdx(row, [label + ':', label + ':', label]);
|
|
176
|
+
if (i === null)
|
|
177
|
+
continue;
|
|
178
|
+
for (let j = i + 1; j < row.length; j++) {
|
|
179
|
+
const v = row[j];
|
|
180
|
+
if (v && v.replace(/[::]$/u, '') !== label && !['报价日期', '客户经理', '联系电话'].some((x) => v.startsWith(x))) {
|
|
181
|
+
meta[key] = v.replace(/[::]$/u, '');
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// ---- 项目总价区
|
|
188
|
+
const totals = {};
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
for (const [label, key] of [['标准价合计', 'standard_total'], ['最终优惠报价', 'final_total']]) {
|
|
191
|
+
if (totals[key] !== undefined)
|
|
192
|
+
continue;
|
|
193
|
+
const i = firstIdx(row, [label + ':', label + ':', label]);
|
|
194
|
+
if (i === null)
|
|
195
|
+
continue;
|
|
196
|
+
for (let j = i + 1; j < row.length; j++) {
|
|
197
|
+
if (isNum(row[j])) {
|
|
198
|
+
totals[key] = toNum(row[j]);
|
|
199
|
+
totals[key + '_upper'] = rmbUpper(totals[key]);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// ---- 分节明细
|
|
206
|
+
const sections = [];
|
|
207
|
+
let cur = null;
|
|
208
|
+
let headerIdx = null;
|
|
209
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
210
|
+
const row = rows[ri];
|
|
211
|
+
const nrow = nrows[ri];
|
|
212
|
+
let m = null;
|
|
213
|
+
for (const v of row) {
|
|
214
|
+
m = SECTION_RE.exec(v);
|
|
215
|
+
if (m)
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
if (m) {
|
|
219
|
+
const title = m[2].replace(/(单价[::].*?)|\(单价[::].*?\)/gu, '').trim();
|
|
220
|
+
cur = { code: m[1], title, items: [], total: null };
|
|
221
|
+
sections.push(cur);
|
|
222
|
+
headerIdx = null;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!cur)
|
|
226
|
+
continue;
|
|
227
|
+
if (row.some((v) => v.startsWith('合计'))) {
|
|
228
|
+
for (const v of row) {
|
|
229
|
+
if (isNum(v)) {
|
|
230
|
+
cur.total = toNum(v);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
headerIdx = null;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!headerIdx) {
|
|
238
|
+
const ino = firstIdx(row, HEADER_ALIASES.no);
|
|
239
|
+
const iamt = firstIdx(row, HEADER_ALIASES.amount);
|
|
240
|
+
if (ino !== null && iamt !== null) {
|
|
241
|
+
headerIdx = Object.fromEntries(Object.entries(HEADER_ALIASES).map(([k, v]) => [k, firstIdx(row, v)]));
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const g = (k) => {
|
|
246
|
+
const i = headerIdx[k];
|
|
247
|
+
return i !== null && i !== undefined && i < row.length ? row[i] : '';
|
|
248
|
+
};
|
|
249
|
+
const gn = (k) => {
|
|
250
|
+
const i = headerIdx[k];
|
|
251
|
+
return i !== null && i !== undefined && i < nrow.length ? nrow[i] : '';
|
|
252
|
+
};
|
|
253
|
+
const no = g('no');
|
|
254
|
+
const name = g('name');
|
|
255
|
+
const price = toNum(gn('price'));
|
|
256
|
+
const qty = toNum(gn('qty'));
|
|
257
|
+
let amount = toNum(gn('amount'));
|
|
258
|
+
if (!name && (amount === null || amount === undefined))
|
|
259
|
+
continue;
|
|
260
|
+
if ((amount === null || amount === undefined) && price !== null) {
|
|
261
|
+
amount = price * (qty !== null && qty !== undefined ? qty : 1);
|
|
262
|
+
}
|
|
263
|
+
if (!includeZero && (qty === 0 || amount === 0))
|
|
264
|
+
continue;
|
|
265
|
+
let desc = g('desc').replace(/\s+/gu, ' ').trim();
|
|
266
|
+
if (desc.length > 120)
|
|
267
|
+
desc = desc.slice(0, 120) + '…';
|
|
268
|
+
const item = { no, name, way: g('way'), desc, price, qty, amount };
|
|
269
|
+
if (keepRemark)
|
|
270
|
+
item.remark = g('remark').replace(/\s+/gu, ' ').slice(0, 200);
|
|
271
|
+
cur.items.push(item);
|
|
272
|
+
}
|
|
273
|
+
// ---- 一致性校验
|
|
274
|
+
const checks = [];
|
|
275
|
+
for (const s of sections) {
|
|
276
|
+
s.sum_items = s.items.reduce((acc, i) => acc + (Number(i.amount) || 0), 0);
|
|
277
|
+
if (s.total !== null && s.sum_items !== s.total) {
|
|
278
|
+
checks.push({ section: `${s.code}、${s.title}`, sheet_total: s.total, sum_items: s.sum_items, diff: s.sum_items - s.total });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const grand = sections.reduce((acc, s) => acc + (s.sum_items ?? 0), 0);
|
|
282
|
+
totals.sum_items_grand = grand;
|
|
283
|
+
const standard = totals.standard_total;
|
|
284
|
+
totals.has_mismatch = checks.length > 0 || (standard !== undefined && grand !== standard);
|
|
285
|
+
totals.diff_vs_standard = standard === undefined ? null : grand - standard;
|
|
286
|
+
const warnings = [];
|
|
287
|
+
if (checks.length) {
|
|
288
|
+
warnings.push('分节明细求和与表内『合计』不一致,请与报价人核对:' +
|
|
289
|
+
checks.map((c) => `${c.section} 表内${c.sheet_total}/明细${c.sum_items}`).join(';'));
|
|
290
|
+
}
|
|
291
|
+
if (totals.diff_vs_standard) {
|
|
292
|
+
warnings.push(`全部明细求和 ${grand} 与『标准价合计』${standard} 不一致,差额 ${totals.diff_vs_standard}`);
|
|
293
|
+
}
|
|
294
|
+
const final = totals.final_total;
|
|
295
|
+
if (final !== undefined && standard !== undefined && final !== standard) {
|
|
296
|
+
warnings.push(`存在折扣:标准价 ${standard} → 最终优惠报价 ${final},合同金额应以最终优惠报价为准`);
|
|
297
|
+
}
|
|
298
|
+
if (!meta.customer)
|
|
299
|
+
warnings.push('报价单未填写『客户名称』,签约主体必须由用户提供');
|
|
300
|
+
return { source_sheet: sheet, available_sheets: names, meta, totals, sections, checks, warnings };
|
|
301
|
+
}
|
|
302
|
+
// ---------------------------------------------------------------- docx 填充(字段打分)
|
|
303
|
+
const FIELD_ALIASES = {
|
|
304
|
+
party_a: ['甲方', '买方', '客户名称', '客户', '采购方', '需方', '购货方', '甲方名称', '买方名称'],
|
|
305
|
+
party_a_addr: ['甲方地址', '买方地址', '客户地址', '甲方住所', '注册地址'],
|
|
306
|
+
party_a_contact: ['甲方联系人', '买方联系人', '客户联系人', '项目联系人'],
|
|
307
|
+
party_a_phone: ['甲方电话', '买方电话', '联系电话', '客户电话', '甲方联系方式'],
|
|
308
|
+
party_b: ['乙方', '卖方', '供方', '销售方', '乙方名称', '卖方名称'],
|
|
309
|
+
party_b_addr: ['乙方地址', '卖方地址', '乙方住所'],
|
|
310
|
+
party_b_contact: ['乙方联系人', '卖方联系人', '客户经理', '销售经理'],
|
|
311
|
+
party_b_phone: ['乙方电话', '卖方电话'],
|
|
312
|
+
sign_date: ['签订日期', '签署日期', '签订时间', '合同日期', '日期'],
|
|
313
|
+
quote_date: ['报价日期', '报价时间'],
|
|
314
|
+
amount: ['合同总金额', '合同金额', '总金额', '合同总价', '项目总价', '金额合计', '合同总价款', '总价'],
|
|
315
|
+
amount_upper: ['大写金额', '金额大写', '人民币大写', '大写'],
|
|
316
|
+
software_fee: ['软件功能模块费用', '软件功能费用', '软件费用', '模块费用'],
|
|
317
|
+
license_fee: ['使用授权许可费用', '授权许可费用', '许可费用', '授权费用'],
|
|
318
|
+
license_qty: ['许可数量', '用户数', '授权用户数', '注册用户数'],
|
|
319
|
+
implement_fee: ['项目实施服务费用', '实施服务费用', '实施费用'],
|
|
320
|
+
ai_fee: ['合同AI扩展能力费用', 'AI扩展能力费用', 'AI能力费用', '智能助手费用'],
|
|
321
|
+
thirdparty_fee: ['第三方费用', '第三方产品费用'],
|
|
322
|
+
project_name: ['项目名称', '项目名称及内容', '合同名称'],
|
|
323
|
+
delivery: ['交付期限', '交付时间', '实施周期', '项目周期', '履行期限'],
|
|
324
|
+
payment: ['付款方式', '支付方式', '结算方式', '付款条件'],
|
|
325
|
+
sales: ['客户经理', '报价人', '销售代表'],
|
|
326
|
+
};
|
|
327
|
+
const NUMERIC_KEYS = new Set(['amount', 'software_fee', 'license_fee', 'license_qty', 'implement_fee', 'ai_fee', 'thirdparty_fee']);
|
|
328
|
+
function cleanLabel(label) {
|
|
329
|
+
let lab = label.replace(/\u00a0/gu, ' ').trim();
|
|
330
|
+
for (const ch of [':', ':', '(买方)', '(买方)', '(卖方)', '(卖方)'])
|
|
331
|
+
lab = lab.split(ch).join('');
|
|
332
|
+
return lab.replace(/[((]/gu, '').replace(/[))]/gu, '').trim();
|
|
333
|
+
}
|
|
334
|
+
export function scoreLabel(label, key) {
|
|
335
|
+
const lab = cleanLabel(label);
|
|
336
|
+
if (!lab)
|
|
337
|
+
return 0;
|
|
338
|
+
if (lab in FIELD_ALIASES)
|
|
339
|
+
return lab === key ? 100 : 0;
|
|
340
|
+
if (lab.includes('大写') && key !== 'amount_upper')
|
|
341
|
+
return 0;
|
|
342
|
+
if (lab.includes('小写') && key === 'amount_upper')
|
|
343
|
+
return 0;
|
|
344
|
+
const aliases = FIELD_ALIASES[key] ?? [];
|
|
345
|
+
if (aliases.includes(lab))
|
|
346
|
+
return 100;
|
|
347
|
+
for (const a of aliases) {
|
|
348
|
+
if (lab === a.replace(/[((]/gu, '').replace(/[))]/gu, ''))
|
|
349
|
+
return 98;
|
|
350
|
+
}
|
|
351
|
+
let best = 0;
|
|
352
|
+
for (const a of aliases) {
|
|
353
|
+
if (a.length >= 2 && lab.includes(a)) {
|
|
354
|
+
best = Math.max(best, Math.min(95, 70 + Math.floor((30 * a.length) / Math.max(lab.length, 1))));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const a of aliases) {
|
|
358
|
+
if (lab.length >= 3 && a.includes(lab))
|
|
359
|
+
best = Math.max(best, 60);
|
|
360
|
+
}
|
|
361
|
+
if (best)
|
|
362
|
+
return best;
|
|
363
|
+
for (const a of aliases) {
|
|
364
|
+
const common = [...new Set(a)].filter((ch) => lab.includes(ch)).length;
|
|
365
|
+
if (common >= 2)
|
|
366
|
+
best = Math.max(best, 40);
|
|
367
|
+
}
|
|
368
|
+
return best;
|
|
369
|
+
}
|
|
370
|
+
export function makeFiller(data) {
|
|
371
|
+
const valOf = (k) => {
|
|
372
|
+
const v = data[k];
|
|
373
|
+
if (v === null || v === undefined || v === '')
|
|
374
|
+
return '';
|
|
375
|
+
return NUMERIC_KEYS.has(k) ? fmtNum(v) : String(v);
|
|
376
|
+
};
|
|
377
|
+
return {
|
|
378
|
+
bestKey(label, floor) {
|
|
379
|
+
let bk = null;
|
|
380
|
+
let bs = 0;
|
|
381
|
+
for (const k of Object.keys(FIELD_ALIASES)) {
|
|
382
|
+
const s = scoreLabel(label, k);
|
|
383
|
+
if (s > bs) {
|
|
384
|
+
bk = k;
|
|
385
|
+
bs = s;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return bs >= floor ? { key: bk, score: bs } : { key: null, score: bs };
|
|
389
|
+
},
|
|
390
|
+
valOf,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
// ---------------------------------------------------------------- 操作定义
|
|
394
|
+
function assertContractDocument(path) {
|
|
395
|
+
const absolute = resolve(path);
|
|
396
|
+
if (!/\.(docx)$/iu.test(absolute)) {
|
|
397
|
+
throw contractValidationError('bad_document', '只支持 .docx 模板;.doc 老格式请先另存为 .docx。');
|
|
398
|
+
}
|
|
399
|
+
return absolute;
|
|
400
|
+
}
|
|
401
|
+
export const contractQuoteOperations = [
|
|
402
|
+
{
|
|
403
|
+
name: 'contract.quote.rmb',
|
|
404
|
+
inputSchema: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
additionalProperties: false,
|
|
407
|
+
properties: { num: { type: 'string' } },
|
|
408
|
+
required: ['num'],
|
|
409
|
+
},
|
|
410
|
+
async execute(input) {
|
|
411
|
+
const text = rmbUpper(requireNonEmptyString(input.num, 'num'));
|
|
412
|
+
if (!text)
|
|
413
|
+
throw contractValidationError('bad_number', '数值应为纯数字(可含千分位逗号)。');
|
|
414
|
+
return { status: 'DONE', data: { num: input.num, rmb_upper: text } };
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'contract.quote.sheets',
|
|
419
|
+
risk: 'read-with-upload',
|
|
420
|
+
inputSchema: {
|
|
421
|
+
type: 'object',
|
|
422
|
+
additionalProperties: false,
|
|
423
|
+
properties: { path: { type: 'string' } },
|
|
424
|
+
required: ['path'],
|
|
425
|
+
},
|
|
426
|
+
async execute(input) {
|
|
427
|
+
const path = resolve(String(input.path));
|
|
428
|
+
const sheets = listSheetNames(readFileSync(path));
|
|
429
|
+
if (!sheets.length)
|
|
430
|
+
throw contractValidationError('xlsx_invalid', 'xlsx 中没有工作表。');
|
|
431
|
+
return {
|
|
432
|
+
status: 'DONE',
|
|
433
|
+
data: { sheets, note: '把候选 sheet 列给用户选择;不要自动选第一个以外的 sheet。' },
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'contract.quote.parse',
|
|
439
|
+
risk: 'read-with-upload',
|
|
440
|
+
inputSchema: {
|
|
441
|
+
type: 'object',
|
|
442
|
+
additionalProperties: false,
|
|
443
|
+
properties: {
|
|
444
|
+
path: { type: 'string' },
|
|
445
|
+
sheet: { type: 'string' },
|
|
446
|
+
keepRemark: { type: 'boolean' },
|
|
447
|
+
includeZero: { type: 'boolean', description: '保留数量为0/金额0的未选购项' },
|
|
448
|
+
},
|
|
449
|
+
required: ['path'],
|
|
450
|
+
},
|
|
451
|
+
async execute(input) {
|
|
452
|
+
const path = resolve(String(input.path));
|
|
453
|
+
const parsed = parseQuote(readFileSync(path), input.sheet ? String(input.sheet) : null, Boolean(input.keepRemark), Boolean(input.includeZero));
|
|
454
|
+
return { status: 'DONE', data: parsed };
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: 'contract.quote.fill',
|
|
459
|
+
risk: 'read-with-upload',
|
|
460
|
+
inputSchema: {
|
|
461
|
+
type: 'object',
|
|
462
|
+
additionalProperties: false,
|
|
463
|
+
properties: {
|
|
464
|
+
template: { type: 'string', description: '真实合同模板 .docx 路径(必须)' },
|
|
465
|
+
data: { type: 'string', description: '数据 JSON 文件路径;含 __sections 时追加明细表' },
|
|
466
|
+
out: { type: 'string' },
|
|
467
|
+
detail: { type: 'string', enum: ['append', 'none'] },
|
|
468
|
+
mapOut: { type: 'string', description: '映射日志输出路径(filled/unfilled)' },
|
|
469
|
+
},
|
|
470
|
+
required: ['template', 'data', 'out'],
|
|
471
|
+
},
|
|
472
|
+
async execute(input) {
|
|
473
|
+
const template = assertContractDocument(String(input.template));
|
|
474
|
+
if (!existsSync(template)) {
|
|
475
|
+
throw contractValidationError('template_missing', '合同模板缺失:必须先取到真实的 .docx 模板才能生成合同。取不到就停止并请用户提供模板文件;禁止把报价单内容直接生成成一份合同。');
|
|
476
|
+
}
|
|
477
|
+
let data;
|
|
478
|
+
try {
|
|
479
|
+
data = JSON.parse(readFileSync(resolve(String(input.data)), 'utf8'));
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
throw contractValidationError('bad_data', `数据 JSON 无法读取或解析:${error instanceof Error ? error.message : String(error)}`);
|
|
483
|
+
}
|
|
484
|
+
const detail = input.detail === 'none' ? 'none' : 'append';
|
|
485
|
+
const filler = makeFiller(data);
|
|
486
|
+
const { buffer, result } = fillDocx(template, filler, {
|
|
487
|
+
detail,
|
|
488
|
+
sections: (data['__sections'] ?? undefined),
|
|
489
|
+
amount: (data['amount'] ?? null),
|
|
490
|
+
});
|
|
491
|
+
const out = resolve(String(input.out));
|
|
492
|
+
writeFileSync(out, buffer);
|
|
493
|
+
const response = {
|
|
494
|
+
out,
|
|
495
|
+
template: basename(template),
|
|
496
|
+
filled_count: result.filled.length,
|
|
497
|
+
filled: result.filled,
|
|
498
|
+
unfilled: result.unfilled,
|
|
499
|
+
warning: result.unfilled.length > 0
|
|
500
|
+
? '存在未填充占位/空白:请把 unfilled 列表给用户确认,缺的信息让用户补充后再填,不要编造。'
|
|
501
|
+
: null,
|
|
502
|
+
};
|
|
503
|
+
if (input.mapOut) {
|
|
504
|
+
writeFileSync(resolve(String(input.mapOut)), JSON.stringify({ filled: result.filled, unfilled: result.unfilled }, null, 2), 'utf8');
|
|
505
|
+
response.map_out = resolve(String(input.mapOut));
|
|
506
|
+
}
|
|
507
|
+
return { status: 'DONE', data: response };
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
];
|
|
511
|
+
export function quoteContractContracts() {
|
|
512
|
+
return [
|
|
513
|
+
{
|
|
514
|
+
name: 'contract.quote.rmb',
|
|
515
|
+
summary: '数字转人民币大写(纯本地)',
|
|
516
|
+
risk: 'read',
|
|
517
|
+
inputSchema: {
|
|
518
|
+
type: 'object', additionalProperties: false,
|
|
519
|
+
properties: { num: { type: 'string' } }, required: ['num'],
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: 'contract.quote.sheets',
|
|
524
|
+
summary: '列出报价单 xlsx 的全部 sheet 名',
|
|
525
|
+
risk: 'read-with-upload',
|
|
526
|
+
inputSchema: {
|
|
527
|
+
type: 'object', additionalProperties: false,
|
|
528
|
+
properties: { path: { type: 'string' } }, required: ['path'],
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: 'contract.quote.parse',
|
|
533
|
+
summary: '解析报价单 xlsx:元信息/总价/分节明细/一致性校验',
|
|
534
|
+
risk: 'read-with-upload',
|
|
535
|
+
inputSchema: {
|
|
536
|
+
type: 'object', additionalProperties: false,
|
|
537
|
+
properties: {
|
|
538
|
+
path: { type: 'string' }, sheet: { type: 'string' },
|
|
539
|
+
keepRemark: { type: 'boolean' }, includeZero: { type: 'boolean' },
|
|
540
|
+
}, required: ['path'],
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
name: 'contract.quote.fill',
|
|
545
|
+
summary: '用真实 .docx 模板 + 数据 JSON 生成合同 docx',
|
|
546
|
+
risk: 'read-with-upload',
|
|
547
|
+
inputSchema: {
|
|
548
|
+
type: 'object', additionalProperties: false,
|
|
549
|
+
properties: {
|
|
550
|
+
template: { type: 'string' }, data: { type: 'string' }, out: { type: 'string' },
|
|
551
|
+
detail: { type: 'string', enum: ['append', 'none'] }, mapOut: { type: 'string' },
|
|
552
|
+
}, required: ['template', 'data', 'out'],
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
];
|
|
556
|
+
}
|
|
557
|
+
export { labelBefore };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { contractPolicyError } from '../errors.js';
|
|
2
|
+
import { recommendPageUrl } from './page.js';
|
|
3
|
+
import { requireNonEmptyString } from './shared.js';
|
|
4
|
+
/** 「合同范文管理」对象的固定 obj_id,不是环境占位符,禁止替换。 */
|
|
5
|
+
export const RECOMMEND_TEXT_OBJ_ID = '1290191829260230657';
|
|
6
|
+
function text(value) {
|
|
7
|
+
if (typeof value === 'string' && value.trim())
|
|
8
|
+
return value;
|
|
9
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
10
|
+
return String(value);
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function mapRecommendation(item) {
|
|
14
|
+
const labels = (item.extract_labels ?? {});
|
|
15
|
+
return {
|
|
16
|
+
rank: item.rank,
|
|
17
|
+
fileName: item.file_name,
|
|
18
|
+
matchScore: item.match_score,
|
|
19
|
+
matchReason: item.match_reason,
|
|
20
|
+
matchedFields: item.matched_fields,
|
|
21
|
+
templateId: text(item.template_id) ?? text(item.id),
|
|
22
|
+
docId: text(item.doc_id),
|
|
23
|
+
files: item.file_id,
|
|
24
|
+
classificationId: text(item.classification_id),
|
|
25
|
+
creator: item.creator,
|
|
26
|
+
updater: item.updater,
|
|
27
|
+
createTime: item.create_time,
|
|
28
|
+
updateTime: item.update_time,
|
|
29
|
+
labels: {
|
|
30
|
+
title: labels.title,
|
|
31
|
+
summary: labels.summary,
|
|
32
|
+
contractType: labels.contract_type,
|
|
33
|
+
riskLevel: labels.risk_level,
|
|
34
|
+
amountRange: labels.amount_range,
|
|
35
|
+
industry: labels.industry,
|
|
36
|
+
scene: labels.scene,
|
|
37
|
+
role: labels.role,
|
|
38
|
+
paymentTerms: labels.payment_terms,
|
|
39
|
+
keyClauses: labels.key_clauses,
|
|
40
|
+
jurisdiction: labels.jurisdiction,
|
|
41
|
+
tags: labels.tags,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export const contractRecommendOperations = [
|
|
46
|
+
{
|
|
47
|
+
name: 'contract.recommend.doc',
|
|
48
|
+
async execute(input, { host }) {
|
|
49
|
+
const content = requireNonEmptyString(input.content, 'content', 4000);
|
|
50
|
+
const value = await host.contractRequest({
|
|
51
|
+
method: 'POST',
|
|
52
|
+
path: '/api/ebd/contract/file/recommendContractDoc',
|
|
53
|
+
body: { content, textObjId: RECOMMEND_TEXT_OBJ_ID },
|
|
54
|
+
// 服务端 AI 语义匹配本身需要 30~40 秒,超时放到 180 秒,不要因慢而重试。
|
|
55
|
+
timeoutMs: 180_000,
|
|
56
|
+
});
|
|
57
|
+
const data = (value?.data ?? {});
|
|
58
|
+
const cacheKey = text(data.cacheKey);
|
|
59
|
+
if (!cacheKey) {
|
|
60
|
+
throw contractPolicyError('recommend_cache_key_missing', '推荐接口未返回 cacheKey,无法打开推荐结果页', {
|
|
61
|
+
path: '/api/ebd/contract/file/recommendContractDoc',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const list = Array.isArray(data.recommendList) ? data.recommendList : [];
|
|
65
|
+
const session = await host.getSession();
|
|
66
|
+
return {
|
|
67
|
+
status: list.length ? 'COMPLETE' : 'EMPTY',
|
|
68
|
+
data: {
|
|
69
|
+
cacheKey,
|
|
70
|
+
total: list.length,
|
|
71
|
+
recommendations: list.map(mapRecommendation),
|
|
72
|
+
url: recommendPageUrl(session.baseUrl, cacheKey),
|
|
73
|
+
page: { width: 1000 },
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
];
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { contractValidationError } from '../errors.js';
|
|
2
2
|
import { contractOperations } from '../manifest.js';
|
|
3
|
+
import { contractAssistantOperations } from './assistant.js';
|
|
4
|
+
import { contractClauseLibraryOperations } from './clause-library.js';
|
|
3
5
|
import { contractClauseReferenceOperations } from './clause-reference.js';
|
|
6
|
+
import { contractCompareOperations } from './compare.js';
|
|
7
|
+
import { contractCreditPageOperations } from './credit-page.js';
|
|
8
|
+
import { contractFlowOperations } from './flow.js';
|
|
9
|
+
import { contractPerformanceOperations } from './performance.js';
|
|
10
|
+
import { contractQuoteOperations } from './quote.js';
|
|
11
|
+
import { contractRecommendOperations } from './recommend.js';
|
|
4
12
|
import { contractRequestOperations } from './request.js';
|
|
13
|
+
import { contractReviewOperations } from './review.js';
|
|
5
14
|
import { contractSubjectOperations } from './subject.js';
|
|
6
15
|
import { contractTemplateOperations } from './template.js';
|
|
7
16
|
import { contractWorkflowOperations } from './workflow.js';
|
|
@@ -9,10 +18,19 @@ function buildRegistry() {
|
|
|
9
18
|
const registry = new Map();
|
|
10
19
|
const handlers = [
|
|
11
20
|
...contractTemplateOperations,
|
|
21
|
+
...contractClauseLibraryOperations,
|
|
12
22
|
...contractClauseReferenceOperations,
|
|
13
23
|
...contractSubjectOperations,
|
|
14
24
|
...contractWorkflowOperations,
|
|
15
25
|
...contractRequestOperations,
|
|
26
|
+
...contractReviewOperations,
|
|
27
|
+
...contractFlowOperations,
|
|
28
|
+
...contractRecommendOperations,
|
|
29
|
+
...contractCompareOperations,
|
|
30
|
+
...contractCreditPageOperations,
|
|
31
|
+
...contractAssistantOperations,
|
|
32
|
+
...contractPerformanceOperations,
|
|
33
|
+
...contractQuoteOperations,
|
|
16
34
|
];
|
|
17
35
|
for (const handler of handlers) {
|
|
18
36
|
if (registry.has(handler.name))
|