weaver-work-cli 0.1.28 → 0.1.29
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/shortcuts/invoice/host.js +2 -1
- package/dist/shortcuts/invoice/manifest.js +2 -1
- package/dist/shortcuts/invoice/operations/issuing-qys.js +132 -36
- package/dist/shortcuts/workflow/index.js +3 -0
- package/dist/shortcuts/workflow/operations/read.js +13 -6
- package/dist/shortcuts/workflow/operations/registry.js +2 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +331 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +133 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +326 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +113 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +259 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoListRows.js +64 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoOutputPlanner.js +28 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.aggregate.js +70 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +267 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +392 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +165 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +448 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +348 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +222 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +66 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +502 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +540 -0
- package/docs/_catalog.md +1 -0
- package/docs/invoice.md +3 -3
- package/docs/todo-smart-convert/README.md +171 -0
- package/docs/todo-smart-convert/data-contract.md +236 -0
- package/docs/todo-smart-convert/mock/ai-todo-query-target.json +209 -0
- package/docs/todo-smart-convert/subgroup-rules.md +245 -0
- package/package.json +1 -1
- package/scripts/package-skill.mjs +1 -1
- package/skill-template/domains/yepiaotong.md +2 -2
- package/skills/weaver-e10-workflow/SKILL.md +23 -1
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +282 -0
- package/skills/weaver-e10-workflow/scripts/todo-summary.cjs +695 -0
- package/skills/weaver-e10-yepiaotong/SKILL.md +6 -6
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +10 -7
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +41 -14
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +54 -26
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +2 -2
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 待办的**业务性质**判定 + 确定性文案(CLI 侧,纯函数、不联网、不调模型)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要这一层:待办汇总里「怎么分组、组摘要写什么」原来全交给模型,
|
|
5
|
+
* 结果是同一批数据每次说法都不一样、还要多烧一轮 AI。但其中大部分其实是**业务规则**,
|
|
6
|
+
* 可以完全确定下来:
|
|
7
|
+
*
|
|
8
|
+
* | 业务性质 | 分组规则 | 组摘要 |
|
|
9
|
+
* | --- | --- | --- |
|
|
10
|
+
* | 请假类 | ≤1 天合并,其余单条 | 固定模板(条数 / 假别 / 发起人) |
|
|
11
|
+
* | 财务类 | ≤100 元合并,其余单条 | 固定模板(金额区间 / 合计 / 费用类数 / 发起人) |
|
|
12
|
+
* | 其他类 | 整组一个汇总项 | CLI 固定模板 |
|
|
13
|
+
* | 内部留言 | 整组一个汇总项;可由 AI 补写组摘要 | 留给模型 |
|
|
14
|
+
* | 满意度 | 全部单条展示 | 不生成摘要 |
|
|
15
|
+
*
|
|
16
|
+
* 边界:
|
|
17
|
+
* - 本模块**只做判定与拼字符串**,不读缓存、不打接口、不改结构;
|
|
18
|
+
* - 数字(条数、金额区间、合计、假别数)由这里算,模型不许再数一遍——和
|
|
19
|
+
* 「计数只来自 displayData」是同一条原则;
|
|
20
|
+
* - 判据是**流程名 + 表单字段名**,两处都认,改名或换租户也能兜住。
|
|
21
|
+
*/
|
|
22
|
+
import { INTERNAL_MESSAGE_WORKFLOW_PATTERN, normalizeDetailFieldName } from './detailFieldRules.js';
|
|
23
|
+
/** 中文标签(只用于 warnings / 文档,不进结构)。 */
|
|
24
|
+
export const BUSINESS_KIND_LABEL = {
|
|
25
|
+
leave: '请假类',
|
|
26
|
+
finance: '财务类',
|
|
27
|
+
'internal-message': '内部留言类',
|
|
28
|
+
satisfaction: '满意度类',
|
|
29
|
+
'issue-support': '客户问题支持类',
|
|
30
|
+
other: '其他类',
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* 客户问题支持类:**只认这两个精确 workflowName**(2026-10-26 用户指定)。
|
|
34
|
+
*
|
|
35
|
+
* 不用模糊匹配——系统里还有「Ecology客户问题支持解决流程new /(明细)/ -重庆机构」
|
|
36
|
+
* 「FWSX-老客户问题支持解决流程」等同族流程,它们不在规则范围内。
|
|
37
|
+
*/
|
|
38
|
+
export const ISSUE_SUPPORT_WORKFLOW_NAMES = [
|
|
39
|
+
'Ecology客户问题支持解决流程',
|
|
40
|
+
'E10客户问题支持解决流程',
|
|
41
|
+
];
|
|
42
|
+
/** 客户问题支持类:无侧重点时固定产出的汇总项标题。 */
|
|
43
|
+
export const ISSUE_SUPPORT_GROUP_TITLE = '已过期及今天即将过期的流程';
|
|
44
|
+
/** 请假类汇总项阈值:小于等于 1 天。 */
|
|
45
|
+
export const LEAVE_DAY_THRESHOLD = 1;
|
|
46
|
+
/** 财务类汇总项阈值:**单笔小于等于 100 元**。 */
|
|
47
|
+
export const FINANCE_AMOUNT_THRESHOLD = 100;
|
|
48
|
+
/** 请假类汇总项标题(固定)。 */
|
|
49
|
+
export const LEAVE_GROUP_TITLE = `${LEAVE_DAY_THRESHOLD}天以内`;
|
|
50
|
+
/** 财务类汇总项标题(固定)。 */
|
|
51
|
+
export const FINANCE_GROUP_TITLE = `${FINANCE_AMOUNT_THRESHOLD}元以内`;
|
|
52
|
+
/** 请假类:覆盖常见假别命名(租户里叫「休假申请」「调休单」的也认)。 */
|
|
53
|
+
const LEAVE_PATTERN = /(?:请假|休假|年假|调休|病假|事假|婚假|产假|陪产|丧假|探亲假|工伤假|哺乳假|公休)/u;
|
|
54
|
+
/** 财务类:报销/付款/借款/差旅这类「和钱有关」的流程。 */
|
|
55
|
+
const FINANCE_PATTERN = /(?:报销|费用|付款|请款|借款|发票|差旅|对公|资金|核销)/u;
|
|
56
|
+
const SATISFACTION_PATTERN = /(?:满意度|满意调查|评价调查|服务评价|问卷调查)/u;
|
|
57
|
+
/**
|
|
58
|
+
* 业务流程 → 业务性质。满意度必须先判定,避免「费用报销满意度调查」被归为财务。
|
|
59
|
+
*/
|
|
60
|
+
export function detectBusinessKind(workflowName) {
|
|
61
|
+
const name = String(workflowName ?? '').trim();
|
|
62
|
+
if (!name)
|
|
63
|
+
return 'other';
|
|
64
|
+
// 精确名优先:同族里带 new / (明细)/ -重庆机构 / FWSX-老客户 的都不属于这一类
|
|
65
|
+
if (ISSUE_SUPPORT_WORKFLOW_NAMES.includes(name))
|
|
66
|
+
return 'issue-support';
|
|
67
|
+
if (SATISFACTION_PATTERN.test(name))
|
|
68
|
+
return 'satisfaction';
|
|
69
|
+
if (INTERNAL_MESSAGE_WORKFLOW_PATTERN.test(name))
|
|
70
|
+
return 'internal-message';
|
|
71
|
+
if (LEAVE_PATTERN.test(name))
|
|
72
|
+
return 'leave';
|
|
73
|
+
if (FINANCE_PATTERN.test(name))
|
|
74
|
+
return 'finance';
|
|
75
|
+
return 'other';
|
|
76
|
+
}
|
|
77
|
+
/** 请假、财务、客户问题支持类的组摘要由 CLI 固定生成(其他类不再产汇总项,见 2026-10-26 规则)。 */
|
|
78
|
+
export function isDeterministicGroupKind(kind) {
|
|
79
|
+
return kind === 'leave' || kind === 'finance' || kind === 'issue-support';
|
|
80
|
+
}
|
|
81
|
+
export const INTENT_RULES = [
|
|
82
|
+
{
|
|
83
|
+
id: 'leave',
|
|
84
|
+
label: '请假维度',
|
|
85
|
+
match: /请假|休假|年假|调休|病假|事假|婚假|产假|陪产|丧假|探亲|工伤|哺乳|假别|假期|假种|天数/u,
|
|
86
|
+
kinds: ['leave'],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'finance',
|
|
90
|
+
label: '财务维度',
|
|
91
|
+
match: /报销|费用|金额|发票|差旅|借款|付款|请款|科目|票据|对公|核销/u,
|
|
92
|
+
kinds: ['finance'],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'issue-support',
|
|
96
|
+
label: '问题支持维度',
|
|
97
|
+
match: /问题|模块|处理阶段|客户问题|工单|支持/u,
|
|
98
|
+
kinds: ['issue-support'],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'generic',
|
|
102
|
+
label: '通用维度',
|
|
103
|
+
match: /客户|发起人|创建人|提交人|时间|日期|紧急|等级|优先级|主题|标题|类型|类别|状态|节点|内容/u,
|
|
104
|
+
kinds: ['internal-message', 'other'],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
/** 意图是否作用于本业务类型;返回 false 时调用方按「没有意图」处理。 */
|
|
108
|
+
export function intentAppliesToKind(focus, kind) {
|
|
109
|
+
const text = String(focus ?? '').trim();
|
|
110
|
+
if (!text || kind === 'satisfaction')
|
|
111
|
+
return false;
|
|
112
|
+
const specific = INTENT_RULES.filter((rule) => rule.id !== 'generic' && rule.match.test(text));
|
|
113
|
+
if (specific.length > 0)
|
|
114
|
+
return specific.some((rule) => rule.kinds.includes(kind));
|
|
115
|
+
const generic = INTENT_RULES.find((rule) => rule.id === 'generic');
|
|
116
|
+
return generic ? generic.kinds.includes(kind) : false;
|
|
117
|
+
}
|
|
118
|
+
export function requestSummaryStrategy(kind) {
|
|
119
|
+
return kind === 'satisfaction' ? 'title' : 'ai';
|
|
120
|
+
}
|
|
121
|
+
/** 是否需要**模型**写单条摘要(满意度类走标题兜底,不算在内)。 */
|
|
122
|
+
export function needsRequestSummary(kind) {
|
|
123
|
+
return requestSummaryStrategy(kind) === 'ai';
|
|
124
|
+
}
|
|
125
|
+
/** 是否用条目标题直接当摘要(确定性、不调模型、保证非空)。 */
|
|
126
|
+
export function usesTitleSummary(kind) {
|
|
127
|
+
return requestSummaryStrategy(kind) === 'title';
|
|
128
|
+
}
|
|
129
|
+
/** 字段名 → 语义:这几个正则都作用在 `normalizeDetailFieldName` 之后的名字上。 */
|
|
130
|
+
const LEAVE_TYPE_FIELD_PATTERN = /^(?:假别|假类|请假类型|请假类别|假期类型|假期类别|休假类型|休假类别)$/u;
|
|
131
|
+
const LEAVE_DAYS_FIELD_PATTERN = /^(?:(?:请假|休假|假期|调休)(?:天数|时长|合计)|(?:请假|休假|假期|调休)?天数|天数合计|合计天数|总天数|时长)$/u;
|
|
132
|
+
const AMOUNT_FIELD_PATTERN = /^(?:人民币金额|报销金额|报销总金额|实报金额|实际支付金额|申请金额|付款金额|借款金额|请款金额|总金额|金额合计|合计金额|费用合计|小写金额|总价|价税合计|金额|合计|总计)(?:[((]元[))])?$/u;
|
|
133
|
+
const FEE_CATEGORY_FIELD_PATTERN = /^(?:费用类型|费用类别|报销类型|报销类别|费用项目|开支项目|费用科目|费用归属|费用名称)$/u;
|
|
134
|
+
/** 这些字段里出现的「金额」不是业务金额(大写金额、账号、备注里的数字)。 */
|
|
135
|
+
const AMOUNT_FIELD_EXCLUDE = /(?:大写|说明|备注|账号|卡号|电话|编号|单号|日期|时间)/u;
|
|
136
|
+
/** 常见假别(标题里没有结构化字段时的兜底)。长的放前面,避免「产假」先命中「假」。 */
|
|
137
|
+
const LEAVE_TYPE_KEYWORDS = [
|
|
138
|
+
'陪产假', '探亲假', '工伤假', '哺乳假', '产假', '婚假', '丧假', '病假', '事假',
|
|
139
|
+
'年假', '调休', '补休', '公休', '休假',
|
|
140
|
+
];
|
|
141
|
+
/** 常见费用类别(同上,只用于凑「涵盖 N 类费用」的类别名)。 */
|
|
142
|
+
const FEE_CATEGORY_KEYWORDS = [
|
|
143
|
+
'业务招待', '差旅', '交通', '住宿', '餐饮', '餐费', '办公', '通讯', '培训', '会议',
|
|
144
|
+
'快递', '福利', '采购', '广告', '维修', '租赁', '油费', '过路费',
|
|
145
|
+
];
|
|
146
|
+
/** 去 HTML / 实体 / 多余空白:字段值在裁剪前可能是富文本。 */
|
|
147
|
+
function plainText(raw) {
|
|
148
|
+
return String(raw ?? '')
|
|
149
|
+
.replace(/<(style|script)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, ' ')
|
|
150
|
+
.replace(/<[^>]+>/g, ' ')
|
|
151
|
+
.replace(/ /gi, ' ')
|
|
152
|
+
.replace(/&/gi, '&')
|
|
153
|
+
.replace(/\s+/g, ' ')
|
|
154
|
+
.trim();
|
|
155
|
+
}
|
|
156
|
+
/** 从文本里取第一个「带单位的数字」:`1天` / `0.5 天` / `8小时` / `¥1,280.50` / `310 元`。 */
|
|
157
|
+
function numberOf(text, unit) {
|
|
158
|
+
const matched = unit.exec(text);
|
|
159
|
+
if (!matched)
|
|
160
|
+
return undefined;
|
|
161
|
+
const value = Number(String(matched[1]).replace(/,/gu, ''));
|
|
162
|
+
return Number.isFinite(value) ? value : undefined;
|
|
163
|
+
}
|
|
164
|
+
const DAY_UNIT = /(\d[\d,]*(?:\.\d+)?)\s*天/u;
|
|
165
|
+
const HOUR_UNIT = /(\d[\d,]*(?:\.\d+)?)\s*(?:小时|h|H)/u;
|
|
166
|
+
const MONEY_UNIT = /[¥¥]?\s*(\d[\d,]*(?:\.\d{1,2})?)\s*元?/u;
|
|
167
|
+
const BARE_NUMBER = /^(\d[\d,]*(?:\.\d+)?)$/u;
|
|
168
|
+
/**
|
|
169
|
+
* 表单字段 + 标题 → 结构化指标。
|
|
170
|
+
*
|
|
171
|
+
* 取值优先级:**字段 > 标题**。字段是用户真填的,标题往往是流程模板拼出来的,
|
|
172
|
+
* 但标题至少能在「表单没这个字段」时兜住常见的 `请假申请-事假-1天` / `报销 88 元` 写法。
|
|
173
|
+
*
|
|
174
|
+
* 只认**强特征**:`天数/时长` 这类字段名、`X天`、`X元`。宁可取不到(下游退回单条展示),
|
|
175
|
+
* 也不要把 `3天训练营` 误当成请假、把 `工号 12345` 误当成金额。
|
|
176
|
+
*/
|
|
177
|
+
export function extractRowMetrics(fields = [], title) {
|
|
178
|
+
const metrics = {};
|
|
179
|
+
const named = fields
|
|
180
|
+
.map((field) => ({ name: normalizeDetailFieldName(field?.fieldName), value: plainText(field?.value) }))
|
|
181
|
+
.filter((item) => item.name !== '' && item.value !== '');
|
|
182
|
+
// —— 请假:假别 + 天数 ——
|
|
183
|
+
const leaveTypeField = named.find((item) => LEAVE_TYPE_FIELD_PATTERN.test(item.name));
|
|
184
|
+
if (leaveTypeField)
|
|
185
|
+
metrics.leaveType = leaveTypeField.value;
|
|
186
|
+
const daysField = named.find((item) => LEAVE_DAYS_FIELD_PATTERN.test(item.name));
|
|
187
|
+
if (daysField) {
|
|
188
|
+
const text = daysField.value;
|
|
189
|
+
const days = numberOf(text, DAY_UNIT)
|
|
190
|
+
?? (numberOf(text, HOUR_UNIT) !== undefined ? numberOf(text, HOUR_UNIT) / 8 : undefined)
|
|
191
|
+
?? (BARE_NUMBER.test(text.replace(/[天\s]/gu, '')) ? Number(text.replace(/[天\s]/gu, '')) : undefined);
|
|
192
|
+
if (days !== undefined && days > 0 && days <= 366)
|
|
193
|
+
metrics.leaveDays = days;
|
|
194
|
+
}
|
|
195
|
+
// —— 财务:金额 + 费用类别 ——
|
|
196
|
+
const amountField = named.find((item) => AMOUNT_FIELD_PATTERN.test(item.name) && !AMOUNT_FIELD_EXCLUDE.test(item.name));
|
|
197
|
+
if (amountField) {
|
|
198
|
+
const amount = numberOf(amountField.value, MONEY_UNIT)
|
|
199
|
+
?? (BARE_NUMBER.test(amountField.value.replace(/,/gu, '')) ? Number(amountField.value.replace(/,/gu, '')) : undefined);
|
|
200
|
+
if (amount !== undefined && amount > 0)
|
|
201
|
+
metrics.amount = amount;
|
|
202
|
+
}
|
|
203
|
+
const categoryField = named.find((item) => FEE_CATEGORY_FIELD_PATTERN.test(item.name));
|
|
204
|
+
if (categoryField)
|
|
205
|
+
metrics.feeCategory = categoryField.value;
|
|
206
|
+
// —— 标题兜底 ——
|
|
207
|
+
const titleText = plainText(title);
|
|
208
|
+
if (titleText) {
|
|
209
|
+
if (metrics.leaveType === undefined) {
|
|
210
|
+
const hit = LEAVE_TYPE_KEYWORDS.find((keyword) => titleText.includes(keyword));
|
|
211
|
+
if (hit)
|
|
212
|
+
metrics.leaveType = hit;
|
|
213
|
+
}
|
|
214
|
+
if (metrics.leaveDays === undefined) {
|
|
215
|
+
// 标题里常见 `请假天数:0.88`(没有「天」字),先按「字段名:数值」兜一层。
|
|
216
|
+
const days = numberOf(titleText, DAY_UNIT)
|
|
217
|
+
?? numberOf(titleText, /(?:请假|休假|假期|调休)天数\s*[::]\s*(\d[\d,]*(?:\.\d+)?)/u);
|
|
218
|
+
if (days !== undefined && days > 0 && days <= 366)
|
|
219
|
+
metrics.leaveDays = days;
|
|
220
|
+
else {
|
|
221
|
+
const hours = numberOf(titleText, HOUR_UNIT);
|
|
222
|
+
if (hours !== undefined && hours > 0)
|
|
223
|
+
metrics.leaveDays = Math.round((hours / 8) * 100) / 100;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (metrics.amount === undefined) {
|
|
227
|
+
// 标题里也常见 `报销总金额:90.00`(没有「元」字),同样补一层「金额:数值」。
|
|
228
|
+
const amount = numberOf(titleText, /[¥¥]\s*(\d[\d,]*(?:\.\d{1,2})?)/u)
|
|
229
|
+
?? numberOf(titleText, /(\d[\d,]*(?:\.\d{1,2})?)\s*元/u)
|
|
230
|
+
?? numberOf(titleText, /(?:报销|费用|申请|实报|支付)?(?:总)?金额\s*[::]\s*(\d[\d,]*(?:\.\d{1,2})?)/u);
|
|
231
|
+
if (amount !== undefined && amount > 0)
|
|
232
|
+
metrics.amount = amount;
|
|
233
|
+
}
|
|
234
|
+
if (metrics.feeCategory === undefined) {
|
|
235
|
+
const hit = FEE_CATEGORY_KEYWORDS.find((keyword) => titleText.includes(keyword));
|
|
236
|
+
if (hit)
|
|
237
|
+
metrics.feeCategory = hit;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return metrics;
|
|
241
|
+
}
|
|
242
|
+
/** 金额/天数展示:整数不带小数,非整数保留两位(`36` / `36.50` / `0.5`)。 */
|
|
243
|
+
export function formatNumber(value) {
|
|
244
|
+
if (!Number.isFinite(value))
|
|
245
|
+
return '0';
|
|
246
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
247
|
+
}
|
|
248
|
+
/** 发起人去重(保序、去空白)。 */
|
|
249
|
+
export function normalizeCreators(names) {
|
|
250
|
+
const result = [];
|
|
251
|
+
for (const raw of names) {
|
|
252
|
+
const name = String(raw ?? '').trim();
|
|
253
|
+
if (name && !result.includes(name))
|
|
254
|
+
result.push(name);
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
/** 去重(保序、去空白),用于假别/费用类别这类分类名。 */
|
|
259
|
+
export function uniqueTexts(values) {
|
|
260
|
+
const result = [];
|
|
261
|
+
for (const raw of values) {
|
|
262
|
+
const text = String(raw ?? '').trim();
|
|
263
|
+
if (text && !result.includes(text))
|
|
264
|
+
result.push(text);
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
/** 组摘要的固定尾巴(三个模板共用)。 */
|
|
269
|
+
export const GROUP_SUMMARY_TAIL_TEXT = '可展开逐条查看或直接汇总处理';
|
|
270
|
+
/**
|
|
271
|
+
* 固定文案里的发起人短语:`由A、B发起。` / `由A、B、C等发起。`
|
|
272
|
+
*
|
|
273
|
+
* 口径(2026-10-27 用户定):**超过 2 人**才加「等」。1~2 人把名字列全,加「等」是多余的;
|
|
274
|
+
* 3 人以上只列前 3 个并用「等」收尾。没有发起人时返回空串。
|
|
275
|
+
*/
|
|
276
|
+
export function creatorClauseOf(creators) {
|
|
277
|
+
const all = normalizeCreators(creators);
|
|
278
|
+
const shown = all.slice(0, 3);
|
|
279
|
+
if (shown.length === 0)
|
|
280
|
+
return '';
|
|
281
|
+
return `由${shown.join('、')}${all.length > 2 ? '等' : ''}发起。`;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* 请假类固定文案:
|
|
285
|
+
* `共{条数}条,涵盖{假别数}类假别({列举}),由{A}、{B}、{C}等发起。可展开逐条查看或直接汇总处理`
|
|
286
|
+
*
|
|
287
|
+
* 假别取去重值,最多列 3 个;**列出来的少于全部去重值时才补「等」**(没有列完才加)。
|
|
288
|
+
* 一个都取不到时只说「1 类假别」,不编造名字。
|
|
289
|
+
*/
|
|
290
|
+
export function formatLeaveGroupSummary(input) {
|
|
291
|
+
const allTypes = uniqueTexts(input.leaveTypes);
|
|
292
|
+
const types = allTypes.slice(0, 3);
|
|
293
|
+
const typeCount = allTypes.length > 0 ? allTypes.length : 1;
|
|
294
|
+
const typeText = types.length > 0
|
|
295
|
+
? `${typeCount}类假别(${types.join('、')}${allTypes.length > types.length ? '等' : ''})`
|
|
296
|
+
: `${typeCount}类假别`;
|
|
297
|
+
return `共${input.count}条,涵盖${typeText},${creatorClauseOf(input.creators)}${GROUP_SUMMARY_TAIL_TEXT}`;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* 财务类固定文案:
|
|
301
|
+
* `单笔 ¥{最小} ~ ¥{最大},合计 ¥{合计},涵盖{n}类费用({列举}),由{A}、{B}等发起。可展开逐条查看或直接汇总处理`
|
|
302
|
+
*
|
|
303
|
+
* 费用类别与请假假别同口径:去重、最多列 3 个、**没列完才补「等」**;一个类别都取不到时只说「1 类费用」。
|
|
304
|
+
* 合计值直接给纯文本 `¥{金额}`(2026-10-27 用户要求去掉 `<span>` 红字包裹:载荷会被内联进聊天消息,
|
|
305
|
+
* 客户端把 `<span …>` 当真实 HTML 解析,导致其后内容整段被吞)。
|
|
306
|
+
*/
|
|
307
|
+
export function formatFinanceGroupSummary(input) {
|
|
308
|
+
const allCategories = uniqueTexts(input.categories);
|
|
309
|
+
const categories = allCategories.slice(0, 3);
|
|
310
|
+
const categoryText = allCategories.length > 0
|
|
311
|
+
? `${allCategories.length}类费用(${categories.join('、')}${allCategories.length > categories.length ? '等' : ''})`
|
|
312
|
+
: '1类费用';
|
|
313
|
+
const sum = `¥${formatNumber(input.sum)}`;
|
|
314
|
+
return `单笔 ¥${formatNumber(input.min)} ~ ¥${formatNumber(input.max)},合计 ${sum},`
|
|
315
|
+
+ `涵盖${categoryText},${creatorClauseOf(input.creators)}${GROUP_SUMMARY_TAIL_TEXT}`;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 客户问题支持类:无侧重点时的固定组摘要。
|
|
319
|
+
*
|
|
320
|
+
* 文案口径固定为「N 条已过期或今日到期 + 其余 M 条单条」,与子组内容一一对应,
|
|
321
|
+
* 不让模型再数一遍(条数由 CLI 算)。列出的发起人少于总数时用「等」收尾。
|
|
322
|
+
*/
|
|
323
|
+
export function formatIssueSupportGroupSummary(input) {
|
|
324
|
+
const allCreators = normalizeCreators(input.creators);
|
|
325
|
+
const shown = allCreators.slice(0, 3);
|
|
326
|
+
const creatorText = shown.length > 0
|
|
327
|
+
? `由 ${shown.join('、')}${allCreators.length > shown.length ? ' 等' : ''} ${allCreators.length} 人发起,`
|
|
328
|
+
: '';
|
|
329
|
+
const restText = input.restCount > 0 ? `其余 ${input.restCount} 条按单条展示,` : '';
|
|
330
|
+
return `含 ${input.count} 条已过期或今日到期的问题流程,${creatorText}${restText}${GROUP_SUMMARY_TAIL_TEXT}`;
|
|
331
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 详情表单的**业务性质约束**(CLI 侧预处理)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么规则要在 CLI 里,而不是写进提示词让模型自觉:
|
|
5
|
+
* - `主送` 的值是几十个部门的清单(真机样本一条 300+ 字),对摘要零信息量,却会
|
|
6
|
+
* ①挤占 `summaryFields` 的 12 个字段名额(把真正的业务字段挤出去)、②撑大喂给模型的上下文;
|
|
7
|
+
* - `内部留言` 这类流程的业务内容只有一个 `留言内容`,其余(被留言人 / 抄送人 / 留言人分部 /
|
|
8
|
+
* 被留言人数)都是分发元数据。靠模型"看懂后忽略"既不稳定,又白烧 token。
|
|
9
|
+
* 这两条由**数据形态决定**,属于取数出口就该做掉的裁剪:在 `fetchRequestDetailsBatch`
|
|
10
|
+
* 与 `workflow.summary` 两个出口统一生效,下游(摘要批次 / 缓存 / 渲染)看到的都是同一份口径。
|
|
11
|
+
*
|
|
12
|
+
* 边界(不要越界):
|
|
13
|
+
* - 本模块只做**字段取舍**,不做摘要、不做渲染,也不联网;
|
|
14
|
+
* - 不改变"空详情"的语义:内部留言类若连 `留言内容` 都没有,仍返回空数组(= 没业务内容),
|
|
15
|
+
* 绝不拿 `发起人/发起时间` 冒充内容。
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* 字段名归一:去 HTML、去包裹性括号、去首尾与内部空白。
|
|
19
|
+
*
|
|
20
|
+
* 归一后 `【主送】`、`主送 `、`<span>主送</span>` 都收敛成 `主送`,规则才不会被排版差异绕过。
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeDetailFieldName(raw) {
|
|
23
|
+
return String(raw ?? '')
|
|
24
|
+
.replace(/<[^>]*>/g, ' ')
|
|
25
|
+
.replace(/[【】[\]()()]/g, '')
|
|
26
|
+
.replace(/\s+/g, '')
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 分发类字段:值是收件人 / 抄送范围的清单,**无条件丢弃**(用户口径:"无脑去")。
|
|
31
|
+
*
|
|
32
|
+
* 覆盖 `主送` 与 `抄送` 两个家族;二者都是分发元数据。
|
|
33
|
+
* 后缀只认 `人/单位/部门/对象/范围`——`主送内容` 这类真正的业务字段不会被误伤。
|
|
34
|
+
* 改这里 = 全链路生效(批量取数 / `workflow.summary` 卡片摘要)。
|
|
35
|
+
*/
|
|
36
|
+
export const DETAIL_DISTRIBUTION_FIELD_PATTERN = /^(?:主送|抄送)(?:人|单位|部门|对象|范围)?$/u;
|
|
37
|
+
export function isDistributionDetailField(name) {
|
|
38
|
+
return DETAIL_DISTRIBUTION_FIELD_PATTERN.test(normalizeDetailFieldName(name));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 丢掉分发类字段(保持原顺序)。
|
|
42
|
+
*
|
|
43
|
+
* 在**挑选之前**调用:`selectSummaryFields` 有字段名额上限,先丢掉才不会让 `主送`
|
|
44
|
+
* 占着一个名额、把真正的业务字段挤到第 13 个去。
|
|
45
|
+
*/
|
|
46
|
+
export function dropDistributionDetailFields(fields) {
|
|
47
|
+
return fields.filter((field) => !isDistributionDetailField(field?.fieldName));
|
|
48
|
+
}
|
|
49
|
+
/** 内部留言类流程:流程名命中,或表单结构里有「留言内容」。 */
|
|
50
|
+
export const INTERNAL_MESSAGE_WORKFLOW_PATTERN = /(?:内部留言|内部消息|留言板|意见建议|^\s*留言)/u;
|
|
51
|
+
export const INTERNAL_MESSAGE_CONTENT_LABEL = '留言内容';
|
|
52
|
+
export const INTERNAL_MESSAGE_INITIATOR_LABEL = '发起人';
|
|
53
|
+
export const INTERNAL_MESSAGE_TIME_LABEL = '发起时间';
|
|
54
|
+
/**
|
|
55
|
+
* 是否内部留言类详情。
|
|
56
|
+
*
|
|
57
|
+
* 双判据(流程名 + 表单结构)而不是只看流程名:租户里同类流程可能叫「内部留言」「留言板」
|
|
58
|
+
* 「意见建议」,改个名字规则就失效;反之只要表单里有 `留言内容`,就必然是留言类详情。
|
|
59
|
+
*/
|
|
60
|
+
export function isInternalMessageDetail(workflowName, fieldNames = []) {
|
|
61
|
+
if (INTERNAL_MESSAGE_WORKFLOW_PATTERN.test(String(workflowName ?? '')))
|
|
62
|
+
return true;
|
|
63
|
+
return fieldNames.some((name) => normalizeDetailFieldName(name) === INTERNAL_MESSAGE_CONTENT_LABEL);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 从表单字段里挑出「留言内容」。
|
|
67
|
+
*
|
|
68
|
+
* 优先精确名,再退到含「留言」的字段(`留言正文` / `留言`),最后才认 `内容`——
|
|
69
|
+
* 顺序很重要:`被留言人` 也含「留言」,但它不是内容,只能靠前面的精确/后缀规则排除。
|
|
70
|
+
*/
|
|
71
|
+
export function pickInternalMessageContentField(fields) {
|
|
72
|
+
const named = fields
|
|
73
|
+
.map((field) => ({ field, name: normalizeDetailFieldName(field?.fieldName) }))
|
|
74
|
+
.filter((item) => item.name !== '');
|
|
75
|
+
const exact = named.find((item) => item.name === INTERNAL_MESSAGE_CONTENT_LABEL);
|
|
76
|
+
if (exact)
|
|
77
|
+
return exact.field;
|
|
78
|
+
const message = named.find((item) => item.name.includes('留言') && !item.name.includes('留言人'));
|
|
79
|
+
if (message)
|
|
80
|
+
return message.field;
|
|
81
|
+
const content = named.find((item) => item.name === '内容' || item.name.endsWith('内容'));
|
|
82
|
+
return content?.field;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 内部留言类:**只保留** 留言内容 / 发起人 / 发起时间(顺序固定)。
|
|
86
|
+
*
|
|
87
|
+
* 返回的是**未清洗的原始值**,由调用方按自己的清洗口径处理(批量取数用 `sanitizeFieldValue`,
|
|
88
|
+
* `workflow.summary` 用 `normalizeDetailSummaryValue`)——两个出口的截断长度不同,不能在这里写死。
|
|
89
|
+
*
|
|
90
|
+
* `留言内容` 为空 ⇒ 返回空数组:这是「表单没有业务内容」,不是「只有发起人」。
|
|
91
|
+
*/
|
|
92
|
+
export function pickInternalMessageFieldPairs(fields, basicInfo = {}) {
|
|
93
|
+
const content = pickInternalMessageContentField(fields);
|
|
94
|
+
const contentValue = String(content?.value ?? '').trim();
|
|
95
|
+
if (!contentValue)
|
|
96
|
+
return [];
|
|
97
|
+
const name = normalizeDetailFieldName(content?.fieldName) || INTERNAL_MESSAGE_CONTENT_LABEL;
|
|
98
|
+
const pairs = [{ fieldName: name, value: contentValue }];
|
|
99
|
+
const creatorName = String(basicInfo.creatorName ?? '').trim();
|
|
100
|
+
if (creatorName)
|
|
101
|
+
pairs.push({ fieldName: INTERNAL_MESSAGE_INITIATOR_LABEL, value: creatorName });
|
|
102
|
+
const createdAt = String(basicInfo.createdAt ?? '').trim();
|
|
103
|
+
if (createdAt)
|
|
104
|
+
pairs.push({ fieldName: INTERNAL_MESSAGE_TIME_LABEL, value: createdAt });
|
|
105
|
+
return pairs;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `workflow.detail`(人看的完整详情面板)用的字段裁剪。
|
|
109
|
+
*
|
|
110
|
+
* 与摘要链路同一套业务口径,但**只做字段取舍、不丢正文**:
|
|
111
|
+
* - 分发字段(主送/抄送)丢弃——它们是收件人清单,占满面板又不带信息;
|
|
112
|
+
* - 内部留言类只留 `留言内容`(`发起人/发起时间` 已在基础信息区,不重复出现)。
|
|
113
|
+
*
|
|
114
|
+
* 超长留言在这里**不丢正文**:摘要有标题可以兜底,人看表单时丢了正文就什么都看不到。
|
|
115
|
+
*
|
|
116
|
+
* 入参出参都是原字段对象(保留 `fieldId` / `valueHtml` / `displayMode` 等),
|
|
117
|
+
* 只过滤不重建——否则富文本链接、全宽展示这些渲染属性会丢。
|
|
118
|
+
*/
|
|
119
|
+
export function trimDetailFieldsForDisplay(fields, workflowName) {
|
|
120
|
+
const usable = dropDistributionDetailFields(fields);
|
|
121
|
+
if (!isInternalMessageDetail(workflowName, usable.map((field) => field?.fieldName)))
|
|
122
|
+
return usable;
|
|
123
|
+
const content = pickInternalMessageContentField(usable);
|
|
124
|
+
return content ? [content] : [];
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// 两个「出口」的业务裁剪(逻辑放这里,`read.ts` 只留调用点)
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
/** `workflow.detail` 面板裁剪:返回保留的字段与被隐藏的条数(warning 用)。 */
|
|
130
|
+
export function trimDetailPanelFields(fields, workflowName) {
|
|
131
|
+
const kept = trimDetailFieldsForDisplay(fields, workflowName);
|
|
132
|
+
return { fields: kept, hiddenCount: fields.length - kept.length };
|
|
133
|
+
}
|