weaver-work-cli 0.1.20 → 0.1.22
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/README.md +19 -11
- package/dist/core/refs.js +39 -36
- package/dist/internal/e10/auth/commands.js +33 -18
- package/dist/internal/e10/auth/session.js +60 -2
- package/dist/internal/e10/auth/xiaoe.js +253 -53
- package/dist/internal/e10/context.js +18 -6
- package/dist/internal/e10/request-runtime.js +3 -1
- package/dist/shortcuts/caizhilian/operations/confirm.js +1 -1
- package/dist/shortcuts/caizhilian/operations/handlers/file-collection.js +901 -20
- package/dist/shortcuts/caizhilian/operations/handlers/folder.js +55 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +4 -4
- package/dist/shortcuts/caizhilian/operations/specs.js +75 -0
- package/dist/shortcuts/hrm/manifest.js +16 -9
- package/dist/shortcuts/hrm/operations/registry.js +2 -0
- package/dist/shortcuts/hrm/operations/resolve.js +449 -0
- package/dist/shortcuts/invoice/host.js +5 -4
- package/dist/shortcuts/meeting/operations/eb/association.js +151 -0
- package/dist/shortcuts/meeting/operations/eb/attendance-summary.js +153 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +185 -13
- package/dist/shortcuts/meeting/operations/eb/conflict.js +17 -11
- package/dist/shortcuts/meeting/operations/eb/decision.js +385 -0
- package/dist/shortcuts/meeting/operations/eb/factory.js +222 -35
- package/dist/shortcuts/meeting/operations/eb/index.js +18 -1
- package/dist/shortcuts/meeting/operations/eb/link.js +212 -0
- package/dist/shortcuts/meeting/operations/eb/task-progress.js +123 -0
- package/dist/shortcuts/meeting/operations/env.js +78 -61
- package/dist/shortcuts/meeting/operations/link.js +125 -0
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/shared.js +13 -0
- package/dist/shortcuts/workflow/host.js +5 -1
- package/dist/shortcuts/workflow/operations/form.js +95 -11
- package/dist/shortcuts/workflow/operations/read.js +785 -26
- package/docs/e10-auth.md +8 -2
- package/docs/hrm.md +34 -2
- package/docs/invoice.md +3 -0
- package/package.json +2 -2
- package/scripts/xiaoe_env.py +118 -0
- package/skill-template/domains/hrm.md +2 -1
- package/skill-template/skill-template.md +1 -1
- package/skills/weaver-e10-blog/SKILL.md +1 -1
- package/skills/weaver-e10-caizhilian/SKILL.md +5 -4
- package/skills/weaver-e10-caizhilian/references/file-collection.md +37 -0
- package/skills/weaver-e10-caizhilian/references/folder.md +5 -1
- package/skills/weaver-e10-caizhilian/references/setting-attach.md +35 -5
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +620 -5
- package/skills/weaver-e10-calendar/SKILL.md +1 -1
- package/skills/weaver-e10-ebuilder-form/SKILL.md +1 -1
- package/skills/weaver-e10-esb/SKILL.md +1 -1
- package/skills/weaver-e10-hrm/SKILL.md +52 -5
- package/skills/weaver-e10-hrm/references/org.md +107 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +1 -1
- package/skills/weaver-e10-jucailin/SKILL.md +1 -1
- package/skills/weaver-e10-mail/SKILL.md +1 -1
- package/skills/weaver-e10-meeting/SKILL.md +27 -11
- package/skills/weaver-e10-meeting/references/meeting-eb.md +162 -7
- package/skills/weaver-e10-meeting/references/meeting-verification.md +271 -0
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-okr/SKILL.md +1 -1
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-plan/SKILL.md +1 -1
- package/skills/weaver-e10-qiyecheng/SKILL.md +1 -1
- package/skills/weaver-e10-shared/SKILL.md +10 -13
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -17
- package/skills/weaver-e10-shared/references/non-interactive-environment.md +21 -14
- package/skills/weaver-e10-shijingran/SKILL.md +1 -1
- package/skills/weaver-e10-skill-maker/SKILL.md +1 -1
- package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +1 -1
- package/skills/weaver-e10-wenshuding/SKILL.md +1 -1
- package/skills/weaver-e10-workflow/SKILL.md +19 -19
- package/skills/weaver-e10-workflow/references/source-manifest.json +13 -13
- package/skills/weaver-e10-workflow/references/workflow-create.md +11 -7
- package/skills/weaver-e10-workflow/references/workflow-query.md +9 -14
- package/skills/weaver-e10-yepiaotong/SKILL.md +1 -2
- package/skills/weaver-e10-yimiaoban/SKILL.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
- package/skills/weaver-e10-ziguanjia/SKILL.md +1 -1
|
@@ -1211,7 +1211,13 @@ function remarkIdentityName(rawName, remarkValue) {
|
|
|
1211
1211
|
return '';
|
|
1212
1212
|
return REMARK_IDENTITY_NAMES[String(Number(text))] ?? '';
|
|
1213
1213
|
}
|
|
1214
|
-
|
|
1214
|
+
/** 列表响应归一化(来源:Python normalize_workflow_list_response)。
|
|
1215
|
+
*
|
|
1216
|
+
* `includeSummary` 决定卡片是否渲染摘要行(见 `renderCardMarkdown`):legacy / 低版本模式传
|
|
1217
|
+
* `true`(标题/摘要/辅助三行),smart / 智能分组模式与 `batchType` 批量取数保持 `false`
|
|
1218
|
+
* (标题/辅助两行)。
|
|
1219
|
+
*/
|
|
1220
|
+
function normalizeListResponse(response, category, categoryName, baseUrl = '', includeSummary = false) {
|
|
1215
1221
|
const record = responseData(response);
|
|
1216
1222
|
const data = record?.data && typeof record.data === 'object' ? record.data : {};
|
|
1217
1223
|
const records = toArray(record?.data?.displayData ?? record?.displayData ?? record?.data?.list ?? record?.list ?? []);
|
|
@@ -1261,7 +1267,12 @@ function normalizeListResponse(response, category, categoryName, baseUrl = '') {
|
|
|
1261
1267
|
recordObj.requestLevel),
|
|
1262
1268
|
badges: uniqueBadges,
|
|
1263
1269
|
badgeObjects: buildBadgeObjects(uniqueBadges),
|
|
1270
|
+
// 摘要槽位初始值(来源:Python normalize_workflow_list_response 的 item 构造):
|
|
1271
|
+
// 逐卡补详情(enrichWorkflowCardSummaries → applyCardSummaryResult)会原地改写这三个键。
|
|
1272
|
+
summaryFields: [],
|
|
1273
|
+
summarySource: 'pendingDetailSummary',
|
|
1264
1274
|
};
|
|
1275
|
+
item.cardSummary = '摘要暂未生成,可打开流程查看';
|
|
1265
1276
|
item.cardMeta = buildCardMeta(item, category);
|
|
1266
1277
|
return item;
|
|
1267
1278
|
});
|
|
@@ -1283,6 +1294,11 @@ function normalizeListResponse(response, category, categoryName, baseUrl = '') {
|
|
|
1283
1294
|
const card = {
|
|
1284
1295
|
index: item.index,
|
|
1285
1296
|
title: item.flowName,
|
|
1297
|
+
summary: item.cardSummary,
|
|
1298
|
+
summarySource: item.summarySource,
|
|
1299
|
+
summaryFields: item.summaryFields,
|
|
1300
|
+
// 发起人姓名等禁止进入摘要的词(脚本生成摘要时过滤,见 generateBusinessSummary)
|
|
1301
|
+
summaryForbiddenTerms: listSummaryForbiddenTerms(item),
|
|
1286
1302
|
badges: item.badges,
|
|
1287
1303
|
badgeObjects: item.badgeObjects,
|
|
1288
1304
|
meta: item.cardMeta,
|
|
@@ -1305,7 +1321,7 @@ function normalizeListResponse(response, category, categoryName, baseUrl = '') {
|
|
|
1305
1321
|
},
|
|
1306
1322
|
};
|
|
1307
1323
|
card.url = cardWorkflowOpenUrl(card, baseUrl);
|
|
1308
|
-
card.renderMarkdown = renderCardMarkdown(card);
|
|
1324
|
+
card.renderMarkdown = renderCardMarkdown(card, includeSummary);
|
|
1309
1325
|
return card;
|
|
1310
1326
|
}),
|
|
1311
1327
|
};
|
|
@@ -1620,8 +1636,19 @@ const workflowSearch = {
|
|
|
1620
1636
|
finalMarkdown: buildSearchFinalMarkdown(null, probes, total, category, true, searchMode === 'smart', waitReadTotal),
|
|
1621
1637
|
});
|
|
1622
1638
|
}
|
|
1623
|
-
|
|
1639
|
+
// 卡片摘要行只在 **legacy(低版本模式)** 生成——契约见 references/workflow-query.md
|
|
1640
|
+
// 「执行模式与返回形态」的「卡片摘要行」一行(低版本=标题/摘要/辅助三行;智能分组=标题/辅助
|
|
1641
|
+
// 两行)。`batchType` 批量取数只关心 requestId,跳过摘要、不白跑 N 次详情接口
|
|
1642
|
+
// (来源:Python execute_workflow_search_legacy 的 `include_summary=True` 与 batch 分支
|
|
1643
|
+
// 强制 `no_card_summaries`)。
|
|
1644
|
+
const withCardSummaries = searchMode === 'legacy' && !batchType;
|
|
1645
|
+
const normalized = normalizeListResponse(listResponse, category, CATEGORY_NAMES[category], session.baseUrl ?? '', withCardSummaries);
|
|
1624
1646
|
normalized.total = total;
|
|
1647
|
+
// 先把当前页卡片补上摘要,再落索引——这样序号定位拿到的 items 也带摘要(V11.3 同步了
|
|
1648
|
+
// sessionMapPayload,本实现改为「补完再落盘」,语义等价)。
|
|
1649
|
+
if (withCardSummaries) {
|
|
1650
|
+
await enrichWorkflowCardSummaries(env, normalized, asInt(args.pageSize) ?? normalized.pageSize);
|
|
1651
|
+
}
|
|
1625
1652
|
const index = await saveListIndex(env, normalized);
|
|
1626
1653
|
return workflowSuccess('workflow.search', await enforceSearchStdoutBudget({
|
|
1627
1654
|
type: 'workflowSearch',
|
|
@@ -1634,7 +1661,7 @@ const workflowSearch = {
|
|
|
1634
1661
|
// 条目只输出机器可读的定位/标识/可操作性字段——渲染一律用 finalMarkdown,
|
|
1635
1662
|
// 不再回显渲染字段(此前 cards / cardItems / renderMarkdown 三份互相重复)。
|
|
1636
1663
|
cards: normalized.items.map(slimIndexItem),
|
|
1637
|
-
finalMarkdown: buildSearchFinalMarkdown(normalized, probes, total, category, false, searchMode === 'smart', waitReadTotal),
|
|
1664
|
+
finalMarkdown: buildSearchFinalMarkdown(normalized, probes, total, category, false, searchMode === 'smart', waitReadTotal, withCardSummaries),
|
|
1638
1665
|
queryId: index.queryId,
|
|
1639
1666
|
sessionMap: { queryId: index.queryId, itemCount: normalized.items.length },
|
|
1640
1667
|
}, env, { json: 'last_search.json', md: 'last_search.md' }));
|
|
@@ -1701,7 +1728,7 @@ function todoProbeNotice(probes) {
|
|
|
1701
1728
|
return `另外,该条件下有${parts.join('、')},建议优先关注。`;
|
|
1702
1729
|
}
|
|
1703
1730
|
/** workflow.search 最终 Markdown(来源:Python build_workflow_search_final_markdown) */
|
|
1704
|
-
function buildSearchFinalMarkdown(normalized, probes, total, category, totalOnly, smartMode, waitReadTotal = null) {
|
|
1731
|
+
function buildSearchFinalMarkdown(normalized, probes, total, category, totalOnly, smartMode, waitReadTotal = null, includeSummary = false) {
|
|
1705
1732
|
const label = CATEGORY_NAMES[category];
|
|
1706
1733
|
const lines = [];
|
|
1707
1734
|
if (totalOnly) {
|
|
@@ -1724,7 +1751,7 @@ function buildSearchFinalMarkdown(normalized, probes, total, category, totalOnly
|
|
|
1724
1751
|
lines.push(`另有 ${asInt(waitReadTotal)} 条待阅流程。`);
|
|
1725
1752
|
}
|
|
1726
1753
|
}
|
|
1727
|
-
const body = normalized ? renderListMarkdown(normalized) : '';
|
|
1754
|
+
const body = normalized ? renderListMarkdown(normalized, includeSummary) : '';
|
|
1728
1755
|
if (body)
|
|
1729
1756
|
lines.push(body);
|
|
1730
1757
|
return lines.filter(Boolean).join('\n\n');
|
|
@@ -1795,19 +1822,27 @@ function todoTabName(tabid) {
|
|
|
1795
1822
|
return '待处理';
|
|
1796
1823
|
}
|
|
1797
1824
|
/** 取一组待办明细(list 接口)并按请求窗口归一化(来源:Python normalize_todo_group_list)。 */
|
|
1798
|
-
|
|
1825
|
+
/** 取一个待办分组的分页明细(来源:Python normalize_todo_group_list,L11987)。
|
|
1826
|
+
*
|
|
1827
|
+
* 低版本模式(legacy)走 CLI 侧分组,卡片同样是**三行**(契约见 references/workflow-query.md
|
|
1828
|
+
* 「卡片摘要行」一行)——这里就地补当前页前 `summaryLimit` 张卡片的摘要
|
|
1829
|
+
* (来源:Python `normalize_todo_group_list` 内的 `enrich_workflow_card_summaries`)。
|
|
1830
|
+
*/
|
|
1831
|
+
async function fetchLegacyTodoGroupList(env, query, total, baseUrl, summaryLimit = 0) {
|
|
1799
1832
|
const response = await env.host.workflowRequest({
|
|
1800
1833
|
method: 'POST',
|
|
1801
1834
|
path: WORKFLOW_ENDPOINTS.TODO_LIST,
|
|
1802
1835
|
operation: 'workflow.search.group.list',
|
|
1803
1836
|
body: query,
|
|
1804
1837
|
});
|
|
1805
|
-
const normalized = normalizeListResponse(response, 'todo', CATEGORY_NAMES.todo, baseUrl);
|
|
1838
|
+
const normalized = normalizeListResponse(response, 'todo', CATEGORY_NAMES.todo, baseUrl, true);
|
|
1806
1839
|
const window = (query.customDisplayPage || {});
|
|
1807
1840
|
normalized.current = asInt(window.current) ?? normalized.current;
|
|
1808
1841
|
normalized.pageSize = asInt(window.size) ?? normalized.pageSize;
|
|
1809
1842
|
normalized.total = total;
|
|
1810
1843
|
normalized.hasMore = (asInt(normalized.pageSize) ?? 0) * (asInt(normalized.current) ?? 1) < total;
|
|
1844
|
+
if (summaryLimit > 0)
|
|
1845
|
+
await enrichWorkflowCardSummaries(env, normalized, summaryLimit);
|
|
1811
1846
|
return normalized;
|
|
1812
1847
|
}
|
|
1813
1848
|
/** 跨组去重:剔除已被更高优先级分组展示过的流程(来源:Python filter_group_normalized_to_unseen)。 */
|
|
@@ -1863,7 +1898,7 @@ function combineLegacyTodoGroups(groupResults) {
|
|
|
1863
1898
|
for (const card of cards) {
|
|
1864
1899
|
const rid = asString(card?.internal?.requestId) ?? '';
|
|
1865
1900
|
card.index = groupIndex;
|
|
1866
|
-
card.renderMarkdown = renderCardMarkdown(card);
|
|
1901
|
+
card.renderMarkdown = renderCardMarkdown(card, true);
|
|
1867
1902
|
const item = itemByRid.get(rid);
|
|
1868
1903
|
if (item) {
|
|
1869
1904
|
item.index = groupIndex;
|
|
@@ -1949,6 +1984,11 @@ async function executeLegacyGroupedTodoSearch(env, args) {
|
|
|
1949
1984
|
const baseUrl = session.baseUrl ?? '';
|
|
1950
1985
|
const mainSize = Math.max(1, Math.min(asInt(args.pageSize) ?? 10, 100));
|
|
1951
1986
|
const auxSize = Math.min(mainSize, 5);
|
|
1987
|
+
// 摘要补全条数:主组 = min(主列表页大小, 页大小),特殊组 = min(3, 页大小)
|
|
1988
|
+
// (来源:Python execute_grouped_todo_search 的 summary_limit_main / summary_limit_aux)
|
|
1989
|
+
const summaryItemLimit = asInt(args.pageSize) ?? mainSize;
|
|
1990
|
+
const summaryLimitMain = Math.min(mainSize, summaryItemLimit);
|
|
1991
|
+
const summaryLimitAux = Math.min(3, summaryItemLimit);
|
|
1952
1992
|
const mainTabid = tabid || SUB_CATEGORY_TABS.todo['待处理'];
|
|
1953
1993
|
const mainQuery = todoQueryForTab(query, mainTabid, mainSize);
|
|
1954
1994
|
const personalQuery = todoQueryForTab(query, '7', mainSize);
|
|
@@ -2021,12 +2061,13 @@ async function executeLegacyGroupedTodoSearch(env, args) {
|
|
|
2021
2061
|
const mainListQuery = fallbackToAll ? allQuery : mainQuery;
|
|
2022
2062
|
const mainListTotal = fallbackToAll ? allTotal : mainTotal;
|
|
2023
2063
|
const listJobs = {
|
|
2024
|
-
main_list: fetchLegacyTodoGroupList(env, mainListQuery, mainListTotal, baseUrl),
|
|
2064
|
+
main_list: fetchLegacyTodoGroupList(env, mainListQuery, mainListTotal, baseUrl, summaryLimitMain),
|
|
2025
2065
|
};
|
|
2026
|
-
if (showOverdue)
|
|
2027
|
-
listJobs.overdue_list = fetchLegacyTodoGroupList(env, overdueQuery, overdueTotal, baseUrl);
|
|
2066
|
+
if (showOverdue) {
|
|
2067
|
+
listJobs.overdue_list = fetchLegacyTodoGroupList(env, overdueQuery, overdueTotal, baseUrl, summaryLimitAux);
|
|
2068
|
+
}
|
|
2028
2069
|
if (showUrgent) {
|
|
2029
|
-
listJobs.urgent_list = fetchLegacyTodoGroupList(env, urgentQuery, urgentTotal, baseUrl);
|
|
2070
|
+
listJobs.urgent_list = fetchLegacyTodoGroupList(env, urgentQuery, urgentTotal, baseUrl, summaryLimitAux);
|
|
2030
2071
|
}
|
|
2031
2072
|
const listPairs = await Promise.all(Object.entries(listJobs).map(async ([key, task]) => [key, await task]));
|
|
2032
2073
|
const lists = {};
|
|
@@ -2186,11 +2227,17 @@ function extractTotal(response) {
|
|
|
2186
2227
|
}
|
|
2187
2228
|
return null;
|
|
2188
2229
|
}
|
|
2189
|
-
/**
|
|
2230
|
+
/** 单张卡片的 Markdown:标题行(含徽标)+ **可选**摘要行 + 元信息行(来源:Python render_card_markdown)。
|
|
2231
|
+
*
|
|
2232
|
+
* `includeSummary` 决定是否渲染摘要行(第 2 行)——**契约**见 `references/workflow-query.md`
|
|
2233
|
+
* 「执行模式与返回形态」的「卡片摘要行」一行:
|
|
2234
|
+
* - `true`(legacy / 低版本模式)→ **标题/摘要/辅助三行**,摘要由 `enrichWorkflowCardSummaries`
|
|
2235
|
+
* 逐卡补详情(REQUEST_DETAIL)后生成;
|
|
2236
|
+
* - `false`(smart / 智能分组模式,以及 `batchType` 批量取数)→ **标题/辅助两行**。
|
|
2190
2237
|
*
|
|
2191
|
-
*
|
|
2238
|
+
* 默认 `false`:避免未经富化的卡片被渲染出一行占位摘要(`batchType` 与 plan 卡片都不补摘要)。
|
|
2192
2239
|
*/
|
|
2193
|
-
function renderCardMarkdown(card) {
|
|
2240
|
+
function renderCardMarkdown(card, includeSummary = false) {
|
|
2194
2241
|
const badgeTexts = (card.badgeObjects || [])
|
|
2195
2242
|
.map((badge) => stripHtml(badge?.text))
|
|
2196
2243
|
.filter(Boolean)
|
|
@@ -2208,15 +2255,25 @@ function renderCardMarkdown(card) {
|
|
|
2208
2255
|
const url = asString(card.url) ?? '';
|
|
2209
2256
|
const title = url ? `[${stripHtml(titleText)}](${url})` : stripHtml(titleText);
|
|
2210
2257
|
const lines = [`**${title}**${badges} `];
|
|
2258
|
+
if (includeSummary) {
|
|
2259
|
+
let summaryText = stripHtml(card.summary ?? '');
|
|
2260
|
+
// 脚本自动摘要信息量不足时被标记为 needsModelSummary,摘要行给占位提示,
|
|
2261
|
+
// 模型基于 summaryContext 生成后替换(来源:Python render_card_markdown 的占位分支)。
|
|
2262
|
+
if (card.needsModelSummary && (!summaryText || summaryText === '摘要暂未生成,可打开流程查看')) {
|
|
2263
|
+
summaryText = '(摘要待模型生成)';
|
|
2264
|
+
}
|
|
2265
|
+
lines.push(`${summaryText} `);
|
|
2266
|
+
}
|
|
2211
2267
|
if (meta)
|
|
2212
2268
|
lines.push(meta);
|
|
2213
2269
|
return lines.join('\n');
|
|
2214
2270
|
}
|
|
2215
|
-
|
|
2271
|
+
/** 卡片列表 Markdown:`includeSummary` 透传给单卡渲染(见 `renderCardMarkdown`)。 */
|
|
2272
|
+
function renderListMarkdown(normalized, includeSummary = false) {
|
|
2216
2273
|
const items = normalized.cardItems || [];
|
|
2217
2274
|
if (!items.length)
|
|
2218
2275
|
return '无匹配流程';
|
|
2219
|
-
return items.map((card) => renderCardMarkdown(card)).join('\n\n---\n\n');
|
|
2276
|
+
return items.map((card) => renderCardMarkdown(card, includeSummary)).join('\n\n---\n\n');
|
|
2220
2277
|
}
|
|
2221
2278
|
// plan 取数分页口径(来源:Python _PLAN_GROUP_FETCH_PAGE_SIZE / _PLAN_GROUP_FETCH_MAX_PAGES
|
|
2222
2279
|
// 与 _plan_execute_slot 的 pageSize=100 / max_pages=500)
|
|
@@ -3756,13 +3813,627 @@ const DETAIL_SUMMARY_BOOL_QUESTION_PATTERN = /^(是否|是否为|是否包含|
|
|
|
3756
3813
|
const DETAIL_SUMMARY_REGISTER_META_PATTERN = /(备案|环境|版本|基线|编号|序号|编码|流水号|工单|紧急|优先级|级别|程度|渠道|来源|状态)/;
|
|
3757
3814
|
const DETAIL_SUMMARY_WORKFLOW_META_PATTERN = /^(发起|提交|创建|制单|填报).*(人|时间)$|(当前|审批|处理|流转|流程|待办).*(节点|状态)|(停留|超时|到达当前节点).*(时间|时长)|^(节点|状态|审批中|已结束|草稿)$/;
|
|
3758
3815
|
const SUMMARY_MEANINGLESS_VALUES = new Set(['0', '无', '暂无', '无内容', '-', '--', 'null', 'none', 'undefined', '请填写', '请选择']);
|
|
3816
|
+
// 内容信息量评分的取值信号(来源:Python SUMMARY_STRUCTURED_LIST_ITEM_PATTERN 等,L941-944)
|
|
3817
|
+
const SUMMARY_STRUCTURED_LIST_ITEM_PATTERN = /(?:^|[\n;;])\s*[1-9一二三四五六七八九十]\s*[、..]/g;
|
|
3818
|
+
const SUMMARY_VALUE_LABEL_PATTERN = /(需求概述|事项|说明|原因|背景|价值|目标|方案|内容|描述)\s*[::]/;
|
|
3819
|
+
const SUMMARY_VALUE_AMOUNT_PATTERN = /\d+(?:\.\d+)?\s*(?:元|天|小时|工时|日|万|%|%)/;
|
|
3820
|
+
const SUMMARY_VALUE_DATE_PATTERN = /\d{4}[-/年]\d{1,2}/;
|
|
3821
|
+
// 语义角色的补充模式(来源:Python SUMMARY_START_FIELD_PATTERN / SUMMARY_END_FIELD_PATTERN /
|
|
3822
|
+
// SUMMARY_ROLE_WORD_PATTERN,L1088-1097)
|
|
3823
|
+
const SUMMARY_START_FIELD_PATTERN = /(开始|起始|起算|生效|启用)/;
|
|
3824
|
+
const SUMMARY_END_FIELD_PATTERN = /(结束|截止|终止|到期|完成)/;
|
|
3825
|
+
const SUMMARY_ROLE_WORD_PATTERN = /(事由|原因|说明|概述|描述|内容|背景|价值|需求|事项|金额|总额|合计|预算|成本|收入|付款|收款|借款|费用|类型|类别|分类|方式|性质|用途|开始|起始|起算|生效|启用|结束|截止|终止|到期|完成|日期|时间|预计|天数|人天|工时|时长|期限|周期|负责人|责任人|主管|审批人|经手人)/g;
|
|
3826
|
+
// 描述类字段的提炼预算与模型兜底阈值(来源:Python L1240-1249 / L1261-1262)
|
|
3827
|
+
const SUMMARY_REASON_TEXT_BUDGET = 75;
|
|
3828
|
+
const SUMMARY_MODEL_FALLBACK_MIN_LEN = 10;
|
|
3829
|
+
const SUMMARY_MODEL_INSTRUCTION = '该卡片脚本自动摘要信息量不足,请基于 summaryContext 理解表单字段数据后生成中文摘要,'
|
|
3830
|
+
+ '优先体现申请事项、金额、时间、类别、原因、说明、核心需求/描述等业务重点;'
|
|
3831
|
+
+ '不要只复述流程标题,不要编造,不要出现发起人姓名;字数控制在 100 字以内。';
|
|
3832
|
+
/** 标签优先级(主标签「需求概述/事项/说明」排前,来源:Python SUMMARY_LABEL_PRIORITY,L1327) */
|
|
3833
|
+
const SUMMARY_LABEL_PRIORITY = ['需求概述', '事项', '说明', '原因', '背景', '价值'];
|
|
3834
|
+
/** 逐卡补详情时的并发上限。
|
|
3835
|
+
*
|
|
3836
|
+
* 来源:V11.3 Python `enrich_workflow_card_summaries` 的 `max_workers=min(len,4)`。
|
|
3837
|
+
* **本实现有意上调为 5**(09-24 用户决策):默认 `pageSize=5` 时 4 个 worker 必然把当前页 5 张卡
|
|
3838
|
+
* 拆成 **4+1 两轮**(多一个 RTT);5 个 worker 正好一轮打完。**改回 4 之前先确认为什么要退回。**
|
|
3839
|
+
*/
|
|
3840
|
+
const SUMMARY_ENRICH_MAX_WORKERS = 5;
|
|
3841
|
+
/** 详情接口在「列表卡片补摘要」链路里的短超时。
|
|
3842
|
+
*
|
|
3843
|
+
* 来源:V11.3 `http_timeout_seconds("REQUEST_DETAIL")` 返回 **5s**(其余接口 10s)——列表补摘要
|
|
3844
|
+
* 属于尽力而为,单个详情卡顿不得拖垮整条查询。
|
|
3845
|
+
*/
|
|
3846
|
+
const SUMMARY_DETAIL_TIMEOUT_MS = 5_000;
|
|
3847
|
+
/** 按**字符**数截断,优先在中文标点边界收尾(来源:Python truncate_text,L848)。
|
|
3848
|
+
*
|
|
3849
|
+
* 与 `truncateUtf8Text`(按字节硬截,服务 stdout 预算)是两件事:本函数按字符计数,并在 limit
|
|
3850
|
+
* 内最后一个中文标点处收尾,避免把句子/词截成半截(如「…是流程引擎的待…」)。`stripHtml`
|
|
3851
|
+
* 已完成空白折叠与首尾裁剪,与 Python 里先 `re.sub(r"\s+"," ")` 再 `strip()` 等价。
|
|
3852
|
+
*/
|
|
3853
|
+
function truncateText(value, limit = 120) {
|
|
3854
|
+
const text = stripHtml(value);
|
|
3855
|
+
if (text.length <= limit)
|
|
3856
|
+
return text;
|
|
3857
|
+
let cut = limit;
|
|
3858
|
+
for (const sep of ['。', '!', '?', ';', ',', '、', ':']) {
|
|
3859
|
+
const idx = text.lastIndexOf(sep, limit - 1);
|
|
3860
|
+
if (idx >= Math.floor(limit * 0.5) && idx + 1 > cut)
|
|
3861
|
+
cut = idx + 1;
|
|
3862
|
+
}
|
|
3863
|
+
return `${text.slice(0, cut).replace(/[ ,、;:。!?]+$/g, '')}…`;
|
|
3864
|
+
}
|
|
3865
|
+
/** 字段内容信息量评分(来源:Python score_summary_field_value,L947)。
|
|
3866
|
+
*
|
|
3867
|
+
* 内容理解优先、字段名仅作权重:结构化编号列表 > 标签结构 > 长文本 > 业务数字/日期。
|
|
3868
|
+
* 解决「表单顺序第一个命中描述模式」让短泛化字段挤掉核心长文本的问题。
|
|
3869
|
+
*/
|
|
3870
|
+
function scoreSummaryFieldValue(name, value) {
|
|
3871
|
+
const text = (value ?? '').trim();
|
|
3872
|
+
if (!text)
|
|
3873
|
+
return 0;
|
|
3874
|
+
let score = 0;
|
|
3875
|
+
const numberedItems = text.split(SUMMARY_STRUCTURED_LIST_ITEM_PATTERN).length - 1;
|
|
3876
|
+
if (numberedItems >= 2)
|
|
3877
|
+
score += 12;
|
|
3878
|
+
else if (numberedItems)
|
|
3879
|
+
score += 6;
|
|
3880
|
+
if (SUMMARY_VALUE_LABEL_PATTERN.test(text))
|
|
3881
|
+
score += 8;
|
|
3882
|
+
if (text.length >= 200)
|
|
3883
|
+
score += 10;
|
|
3884
|
+
else if (text.length >= 50)
|
|
3885
|
+
score += 6;
|
|
3886
|
+
else if (text.length >= 20)
|
|
3887
|
+
score += 3;
|
|
3888
|
+
if (SUMMARY_VALUE_AMOUNT_PATTERN.test(text))
|
|
3889
|
+
score += 5;
|
|
3890
|
+
else if (SUMMARY_VALUE_DATE_PATTERN.test(text))
|
|
3891
|
+
score += 4;
|
|
3892
|
+
if (/(概述|描述|事项|说明)/.test(name))
|
|
3893
|
+
score += 4;
|
|
3894
|
+
else if (/(原因|背景|价值|需求|内容)/.test(name))
|
|
3895
|
+
score += 2;
|
|
3896
|
+
return score;
|
|
3897
|
+
}
|
|
3898
|
+
/** 描述类字段按信息量降序(来源:Python best_reason_fields,L1100)。稳定排序,同分保持表单顺序。 */
|
|
3899
|
+
function bestReasonFields(fields) {
|
|
3900
|
+
const candidates = [];
|
|
3901
|
+
for (const field of fields) {
|
|
3902
|
+
const name = stripHtml(field?.fieldName ?? '');
|
|
3903
|
+
const value = stripHtml(field?.value ?? '');
|
|
3904
|
+
if (!name || !value)
|
|
3905
|
+
continue;
|
|
3906
|
+
if (!SUMMARY_REASON_FIELD_PATTERN.test(name))
|
|
3907
|
+
continue;
|
|
3908
|
+
candidates.push({ fieldName: name, value, componentKey: asString(field?.componentKey) ?? '' });
|
|
3909
|
+
}
|
|
3910
|
+
candidates.sort((a, b) => scoreSummaryFieldValue(b.fieldName, b.value) - scoreSummaryFieldValue(a.fieldName, a.value));
|
|
3911
|
+
return candidates;
|
|
3912
|
+
}
|
|
3913
|
+
/** 字段归一为通用业务语义角色(来源:Python summary_semantic_role,L1127)。不依赖具体流程模板。 */
|
|
3914
|
+
function summarySemanticRole(field) {
|
|
3915
|
+
const name = stripHtml(field?.fieldName ?? '');
|
|
3916
|
+
const component = stripHtml(field?.componentKey ?? '').toLowerCase();
|
|
3917
|
+
if (!name)
|
|
3918
|
+
return 'other';
|
|
3919
|
+
if (SUMMARY_REASON_FIELD_PATTERN.test(name))
|
|
3920
|
+
return 'reason';
|
|
3921
|
+
if (SUMMARY_AMOUNT_FIELD_PATTERN.test(name))
|
|
3922
|
+
return 'amount';
|
|
3923
|
+
if (SUMMARY_TYPE_FIELD_PATTERN.test(name))
|
|
3924
|
+
return 'type';
|
|
3925
|
+
const isTemporal = SUMMARY_DATE_FIELD_PATTERN.test(name) || component.includes('date') || component.includes('time');
|
|
3926
|
+
if (isTemporal && SUMMARY_START_FIELD_PATTERN.test(name))
|
|
3927
|
+
return 'start';
|
|
3928
|
+
if (isTemporal && SUMMARY_END_FIELD_PATTERN.test(name))
|
|
3929
|
+
return 'end';
|
|
3930
|
+
if (SUMMARY_DAY_FIELD_PATTERN.test(name))
|
|
3931
|
+
return 'duration';
|
|
3932
|
+
if (SUMMARY_PERSON_FIELD_PATTERN.test(name))
|
|
3933
|
+
return 'person';
|
|
3934
|
+
if (isTemporal)
|
|
3935
|
+
return 'date';
|
|
3936
|
+
return 'other';
|
|
3937
|
+
}
|
|
3938
|
+
/** 提取字段业务前缀,让「报销原因/报销类型」等同域字段优先组合(来源:Python summary_field_stem,L1153)。 */
|
|
3939
|
+
function summaryFieldStem(name) {
|
|
3940
|
+
return stripHtml(name)
|
|
3941
|
+
.replace(SUMMARY_ROLE_WORD_PATTERN, '')
|
|
3942
|
+
.replace(/^[()()【】\[\]_\-\u2014::/ ]+|[()()【】\[\]_\-\u2014::/ ]+$/g, '');
|
|
3943
|
+
}
|
|
3944
|
+
/** 单字段在指定语义角色下的得分(来源:Python semantic_field_score,L1158)。 */
|
|
3945
|
+
function semanticFieldScore(field, role, anchorStem = '') {
|
|
3946
|
+
const name = stripHtml(field?.fieldName ?? '');
|
|
3947
|
+
const value = stripHtml(field?.value ?? '');
|
|
3948
|
+
const component = stripHtml(field?.componentKey ?? '').toLowerCase();
|
|
3949
|
+
let score = 0;
|
|
3950
|
+
if (role === 'reason') {
|
|
3951
|
+
score += scoreSummaryFieldValue(name, value);
|
|
3952
|
+
}
|
|
3953
|
+
else if (role === 'start' || role === 'end' || role === 'date') {
|
|
3954
|
+
if (/\d{4}[-/.年]\d{1,2}/.test(value))
|
|
3955
|
+
score += 8;
|
|
3956
|
+
else if (/\d{1,2}:\d{2}/.test(value))
|
|
3957
|
+
score += 3;
|
|
3958
|
+
if (component.includes('date'))
|
|
3959
|
+
score += 2;
|
|
3960
|
+
}
|
|
3961
|
+
else if (role === 'type') {
|
|
3962
|
+
score += /^[\d.]+$/.test(value) ? 0.5 : 4;
|
|
3963
|
+
score += Math.min(name.length, 8) * 0.2;
|
|
3964
|
+
}
|
|
3965
|
+
else if (role === 'amount' || role === 'duration') {
|
|
3966
|
+
score += /\d/.test(value) ? 4 : 1;
|
|
3967
|
+
}
|
|
3968
|
+
else {
|
|
3969
|
+
score += Math.min(value.length, 20) * 0.1;
|
|
3970
|
+
}
|
|
3971
|
+
const stem = summaryFieldStem(name);
|
|
3972
|
+
if (anchorStem && stem && (stem === anchorStem || stem.includes(anchorStem) || anchorStem.includes(stem))) {
|
|
3973
|
+
score += 8;
|
|
3974
|
+
}
|
|
3975
|
+
return score;
|
|
3976
|
+
}
|
|
3977
|
+
/** 指定语义角色下得分最高的字段;同分取表单顺序更靠前的(来源:Python best_semantic_field,L1185)。 */
|
|
3978
|
+
function bestSemanticField(fields, role, anchorStem = '') {
|
|
3979
|
+
const candidates = fields.filter((field) => summarySemanticRole(field) === role);
|
|
3980
|
+
if (!candidates.length)
|
|
3981
|
+
return null;
|
|
3982
|
+
return candidates.reduce((best, field) => semanticFieldScore(field, role, anchorStem) > semanticFieldScore(best, role, anchorStem) ? field : best);
|
|
3983
|
+
}
|
|
3984
|
+
/** 起止/日期片段(来源:Python summary_temporal_fragment,L1196)。 */
|
|
3985
|
+
function summaryTemporalFragment(fields, anchorStem = '') {
|
|
3986
|
+
const start = bestSemanticField(fields, 'start', anchorStem);
|
|
3987
|
+
const end = bestSemanticField(fields, 'end', anchorStem);
|
|
3988
|
+
if (start) {
|
|
3989
|
+
const startName = stripHtml(start.fieldName);
|
|
3990
|
+
const startValue = stripHtml(start.value);
|
|
3991
|
+
const endValue = end ? stripHtml(end.value) : '';
|
|
3992
|
+
if (endValue && endValue !== startValue)
|
|
3993
|
+
return `${startName}${startValue}至${endValue}`;
|
|
3994
|
+
return `${startName}${startValue}`;
|
|
3995
|
+
}
|
|
3996
|
+
const dateField = bestSemanticField(fields, 'date', anchorStem);
|
|
3997
|
+
if (dateField)
|
|
3998
|
+
return `${stripHtml(dateField.fieldName)}${stripHtml(dateField.value)}`;
|
|
3999
|
+
return '';
|
|
4000
|
+
}
|
|
4001
|
+
/** 值补单位(来源:Python value_with_unit,L1212)。 */
|
|
4002
|
+
function valueWithUnit(value, unit) {
|
|
4003
|
+
const text = stripHtml(value);
|
|
4004
|
+
if (!text)
|
|
4005
|
+
return '';
|
|
4006
|
+
return text.endsWith(unit) ? text : `${text}${unit}`;
|
|
4007
|
+
}
|
|
4008
|
+
/** 按句末符号取前 N 句(来源:Python truncate_by_sentences,L1219)。
|
|
4009
|
+
*
|
|
4010
|
+
* 分隔符**不含英文句点**——否则版本号(`2.164.0.RELEASE`)、小数、编号会被当成句末,
|
|
4011
|
+
* 摘要被截成「发布版本2.164」。
|
|
4012
|
+
*/
|
|
4013
|
+
function truncateBySentences(text, maxSentences = 2) {
|
|
4014
|
+
if (!text)
|
|
4015
|
+
return '';
|
|
4016
|
+
const keep = [];
|
|
4017
|
+
for (const part of text.split(/(?<=[。!?;;\n])/)) {
|
|
4018
|
+
if (!part.trim())
|
|
4019
|
+
continue;
|
|
4020
|
+
keep.push(part);
|
|
4021
|
+
if (keep.length >= maxSentences)
|
|
4022
|
+
break;
|
|
4023
|
+
}
|
|
4024
|
+
return keep.join('').replace(/^[,;\s ]+|[,;\s ]+$/g, '');
|
|
4025
|
+
}
|
|
4026
|
+
/** 提炼描述类字段的核心内容(来源:Python summarize_reason_text,L1265)。
|
|
4027
|
+
*
|
|
4028
|
+
* 先按句级(。!?;)取前 2 句;仍超预算则按分句级(,;)累计直到接近预算,保证在语义边界
|
|
4029
|
+
* 收尾;分句累计不足一半预算时退回标点边界截断。分句不含顿号「、」,否则「1、2、」等序号会被
|
|
4030
|
+
* 切成独立分句,产生「…审批;2」这类残词结尾。
|
|
4031
|
+
*/
|
|
4032
|
+
function summarizeReasonText(text, maxLen = SUMMARY_REASON_TEXT_BUDGET) {
|
|
4033
|
+
const value = stripHtml(text).replace(/^[ ,。;;]+|[ ,。;;]+$/g, '');
|
|
4034
|
+
if (!value)
|
|
4035
|
+
return '';
|
|
4036
|
+
if (value.length <= maxLen)
|
|
4037
|
+
return value;
|
|
4038
|
+
const sentences = value.split(/(?<=[。!?;;])/).filter((sentence) => sentence.trim());
|
|
4039
|
+
if (sentences.length > 1) {
|
|
4040
|
+
const joined = sentences.slice(0, 2).join('').replace(/^[ ,。;;]+|[ ,。;;]+$/g, '');
|
|
4041
|
+
if (joined.length <= maxLen)
|
|
4042
|
+
return joined;
|
|
4043
|
+
}
|
|
4044
|
+
let joined = '';
|
|
4045
|
+
for (const clause of value.split(/(?<=[,;])/)) {
|
|
4046
|
+
if (joined.length + clause.length > maxLen)
|
|
4047
|
+
break;
|
|
4048
|
+
joined += clause;
|
|
4049
|
+
}
|
|
4050
|
+
joined = joined.replace(/^[ ,、;]+|[ ,、;]+$/g, '');
|
|
4051
|
+
// 补全未闭合括号(分句常切在括号内容中间)
|
|
4052
|
+
if ((joined.match(/(/g) ?? []).length > (joined.match(/)/g) ?? []).length)
|
|
4053
|
+
joined += ')';
|
|
4054
|
+
if (joined.length >= Math.floor(maxLen * 0.5))
|
|
4055
|
+
return joined;
|
|
4056
|
+
return truncateText(value, maxLen);
|
|
4057
|
+
}
|
|
4058
|
+
/** 清洗并裁剪自动摘要(来源:Python clean_generated_summary,L1301)。
|
|
4059
|
+
*
|
|
4060
|
+
* `splitSentences=false` 时不做整体切句——reason 分支已单独提炼过描述片段,拼接后切句会把
|
|
4061
|
+
* 补充字段(金额/时间)当第 3 句切掉;此时只按长度 + 标点边界收尾。
|
|
4062
|
+
*/
|
|
4063
|
+
function cleanGeneratedSummary(text, forbiddenTerms = null, limit = 100, splitSentences = true) {
|
|
4064
|
+
let value = stripHtml(text).replace(/^[ ,。;;]+|[ ,。;;]+$/g, '');
|
|
4065
|
+
for (const term of forbiddenTerms ?? []) {
|
|
4066
|
+
const cleanTerm = stripHtml(term);
|
|
4067
|
+
if (cleanTerm)
|
|
4068
|
+
value = value.split(cleanTerm).join('');
|
|
4069
|
+
}
|
|
4070
|
+
value = value.replace(/\s+/g, ' ').replace(/^[ ,。;;]+|[ ,。;;]+$/g, '');
|
|
4071
|
+
if (splitSentences)
|
|
4072
|
+
value = truncateBySentences(value, 2);
|
|
4073
|
+
return truncateText(value, limit);
|
|
4074
|
+
}
|
|
4075
|
+
/** 剥离标签前的中文修饰词(来源:Python trim_label_prefix,L1330)。
|
|
4076
|
+
*
|
|
4077
|
+
* 「需求背景」中的「背景」被识别为低优先级标签时,标签 start 落在「背」字,前面的「需求」
|
|
4078
|
+
* 会残留在上一段末尾;若 endPos 前紧邻中文,回退剥离最多 4 个修饰字。
|
|
4079
|
+
*/
|
|
4080
|
+
function trimLabelPrefix(text, endPos) {
|
|
4081
|
+
let i = endPos - 1;
|
|
4082
|
+
while (i >= 0 && endPos - i <= 4) {
|
|
4083
|
+
const code = text.charCodeAt(i);
|
|
4084
|
+
if (code < 0x4e00 || code > 0x9fff)
|
|
4085
|
+
break;
|
|
4086
|
+
i -= 1;
|
|
4087
|
+
}
|
|
4088
|
+
return i + 1;
|
|
4089
|
+
}
|
|
4090
|
+
/** 从带标签的长文本中提取核心描述内容(来源:Python extract_labeled_summary_value,L1343)。
|
|
4091
|
+
*
|
|
4092
|
+
* 先定位所有「标签:」出现位置(允许「需求背景」中的「背景」被识别为低优先级标签),按标签
|
|
4093
|
+
* 优先级取最高优先级的标签;内容 = 该标签冒号之后、到下一个标签之前的切片。这样「需求概述」
|
|
4094
|
+
* 存在时必取「需求概述」内容,「需求背景」不会抢位。过短的截断残句(长度 < 2)丢弃。
|
|
4095
|
+
*/
|
|
4096
|
+
function extractLabeledSummaryValue(value) {
|
|
4097
|
+
const text = stripHtml(value);
|
|
4098
|
+
if (!text)
|
|
4099
|
+
return '';
|
|
4100
|
+
const labelRe = new RegExp(`(${SUMMARY_LABEL_PRIORITY.join('|')})\\s*[::]`, 'g');
|
|
4101
|
+
const matches = [];
|
|
4102
|
+
let matched = labelRe.exec(text);
|
|
4103
|
+
while (matched !== null) {
|
|
4104
|
+
matches.push({ label: matched[1], start: matched.index, end: matched.index + matched[0].length });
|
|
4105
|
+
if (matched.index === labelRe.lastIndex)
|
|
4106
|
+
labelRe.lastIndex += 1;
|
|
4107
|
+
matched = labelRe.exec(text);
|
|
4108
|
+
}
|
|
4109
|
+
if (!matches.length)
|
|
4110
|
+
return text;
|
|
4111
|
+
const ordered = matches
|
|
4112
|
+
.slice()
|
|
4113
|
+
.sort((a, b) => SUMMARY_LABEL_PRIORITY.indexOf(a.label) - SUMMARY_LABEL_PRIORITY.indexOf(b.label));
|
|
4114
|
+
const laterPositions = matches.filter((item) => item.start > ordered[0].start).map((item) => item.start);
|
|
4115
|
+
let endPos = laterPositions.length ? Math.min(...laterPositions) : text.length;
|
|
4116
|
+
if (laterPositions.length)
|
|
4117
|
+
endPos = trimLabelPrefix(text, endPos);
|
|
4118
|
+
const candidate = text.slice(ordered[0].end, endPos).trim();
|
|
4119
|
+
// 只有「过短」才判为截断残句;较长的「…」结尾(完整编号列表)仍保留
|
|
4120
|
+
if (candidate.length >= 2 && (!candidate.endsWith('…') || candidate.length >= 20))
|
|
4121
|
+
return candidate;
|
|
4122
|
+
for (const match of ordered.slice(1)) {
|
|
4123
|
+
const later2 = laterPositions.filter((position) => position > match.start);
|
|
4124
|
+
let end2 = later2.length ? Math.min(...later2) : text.length;
|
|
4125
|
+
if (later2.length)
|
|
4126
|
+
end2 = trimLabelPrefix(text, end2);
|
|
4127
|
+
const cand2 = text.slice(match.end, end2).trim();
|
|
4128
|
+
if (cand2.length >= 2 && (!cand2.endsWith('…') || cand2.length >= 20))
|
|
4129
|
+
return cand2;
|
|
4130
|
+
}
|
|
4131
|
+
return text;
|
|
4132
|
+
}
|
|
4133
|
+
/** 摘要上下文(来源:Python build_summary_context_text,L1072)。
|
|
4134
|
+
*
|
|
4135
|
+
* 卡片 `needsModelSummary=true` 时,模型据此理解表单字段、生成摘要;`summaryContext` 随卡片
|
|
4136
|
+
* 保留在归一化结果里(本项目的输出只保留 `finalMarkdown`,模型直接按占位提示改写该行即可)。
|
|
4137
|
+
*/
|
|
4138
|
+
function buildSummaryContextText(fields, limit = 600) {
|
|
4139
|
+
const parts = fields
|
|
4140
|
+
.filter((field) => field?.fieldName && field?.value)
|
|
4141
|
+
.map((field) => `${field.fieldName}:${field.value}`);
|
|
4142
|
+
return truncateText(parts.join(';'), limit);
|
|
4143
|
+
}
|
|
4144
|
+
/** 摘要禁用词(发起人姓名等,来源:Python list_summary_forbidden_terms,L1514)。 */
|
|
4145
|
+
function listSummaryForbiddenTerms(item) {
|
|
4146
|
+
const terms = [];
|
|
4147
|
+
const creatorName = stripHtml(item?.creatorName ?? '');
|
|
4148
|
+
if (creatorName)
|
|
4149
|
+
terms.push(creatorName);
|
|
4150
|
+
for (const metaItem of item?.cardMeta ?? []) {
|
|
4151
|
+
if (!metaItem || typeof metaItem !== 'object')
|
|
4152
|
+
continue;
|
|
4153
|
+
const label = stripHtml(metaItem.label);
|
|
4154
|
+
const value = stripHtml(metaItem.value);
|
|
4155
|
+
if (['发起人', '申请人', '发起人/申请人'].includes(label) && value)
|
|
4156
|
+
terms.push(value);
|
|
4157
|
+
}
|
|
4158
|
+
const deduped = [];
|
|
4159
|
+
const seen = new Set();
|
|
4160
|
+
for (const term of terms) {
|
|
4161
|
+
if (term.length < 2 || seen.has(term))
|
|
4162
|
+
continue;
|
|
4163
|
+
seen.add(term);
|
|
4164
|
+
deduped.push(term);
|
|
4165
|
+
}
|
|
4166
|
+
return deduped;
|
|
4167
|
+
}
|
|
4168
|
+
/** 二次过滤:剔除人员类字段与命中禁用词的字段(来源:Python filter_card_summary_fields,L3341)。 */
|
|
4169
|
+
function filterCardSummaryFields(card, fields) {
|
|
4170
|
+
const forbidden = new Set((card?.summaryForbiddenTerms ?? []).map((term) => stripHtml(term)).filter(Boolean));
|
|
4171
|
+
const filtered = [];
|
|
4172
|
+
for (const field of fields) {
|
|
4173
|
+
const name = stripHtml(field?.fieldName ?? '');
|
|
4174
|
+
const value = stripHtml(field?.value ?? '');
|
|
4175
|
+
if (!name || !value)
|
|
4176
|
+
continue;
|
|
4177
|
+
if (['发起人', '申请人', '提交人', '创建人', '制单人', '填报人', '报销人员'].includes(name))
|
|
4178
|
+
continue;
|
|
4179
|
+
if (forbidden.has(value))
|
|
4180
|
+
continue;
|
|
4181
|
+
filtered.push(field);
|
|
4182
|
+
}
|
|
4183
|
+
return filtered;
|
|
4184
|
+
}
|
|
4185
|
+
/** 详情取不到表单数据时的卡片提示(来源:Python detail_summary_unavailable_message,L3289)。 */
|
|
4186
|
+
function detailSummaryUnavailableMessage(formData) {
|
|
4187
|
+
if (formData && formData.available === false)
|
|
4188
|
+
return '详情接口未返回表单数据,可打开流程查看';
|
|
4189
|
+
return '该流程暂无表单数据,暂时无法生成摘要';
|
|
4190
|
+
}
|
|
4191
|
+
/** 详情接口失败时的卡片提示(来源:Python detail_summary_unavailable_from_error,L3535)。
|
|
4192
|
+
*
|
|
4193
|
+
* ⚠️ 与 V11.3 的唯一偏差:V11.3 把超时文案写死成「10 秒内未返回」,而它自己的
|
|
4194
|
+
* `http_timeout_seconds("REQUEST_DETAIL")` 返回的是 **5s**(文案与实现不一致)。这里由常量派生,
|
|
4195
|
+
* 保证文案与实际超时一致。
|
|
4196
|
+
*/
|
|
4197
|
+
function detailSummaryUnavailableFromError(error) {
|
|
4198
|
+
const reason = String(error?.stopReason ?? '').trim();
|
|
4199
|
+
if (reason === 'http_timeout')
|
|
4200
|
+
return `详情接口 ${SUMMARY_DETAIL_TIMEOUT_MS / 1000} 秒内未返回,摘要暂不可用,可打开流程查看`;
|
|
4201
|
+
if (reason === 'login_expired')
|
|
4202
|
+
return '系统认证信息已失效,摘要暂不可用';
|
|
4203
|
+
if (reason === 'api_business_error')
|
|
4204
|
+
return '详情接口返回业务错误,摘要暂不可用';
|
|
4205
|
+
return '该流程暂无表单数据,暂时无法生成摘要';
|
|
4206
|
+
}
|
|
4207
|
+
/** 把补摘要链路里的异常归一为 V11.3 的 `stopReason` 词表(供文案分流)。
|
|
4208
|
+
*
|
|
4209
|
+
* 映射:网络超时 → `http_timeout`;登录失效 → `login_expired`;接口业务/HTTP 错误 →
|
|
4210
|
+
* `api_business_error`。其余(协议异常、程序缺陷)走默认文案。
|
|
4211
|
+
*/
|
|
4212
|
+
function summaryStopReasonOf(error) {
|
|
4213
|
+
const structured = error;
|
|
4214
|
+
if (structured?.type === 'network')
|
|
4215
|
+
return structured.subtype === 'timeout' ? 'http_timeout' : 'http_status';
|
|
4216
|
+
if (structured?.type === 'authentication')
|
|
4217
|
+
return 'login_expired';
|
|
4218
|
+
if (structured?.type === 'api')
|
|
4219
|
+
return 'api_business_error';
|
|
4220
|
+
return 'unknown';
|
|
4221
|
+
}
|
|
4222
|
+
/** 单卡详情补全(来源:Python fetch_card_summary_result,L3546)。
|
|
4223
|
+
*
|
|
4224
|
+
* 取该卡片 requestId 的详情表单字段 → 摘要候选筛选 → 返回待生成的字段清单。**不改未读状态**
|
|
4225
|
+
* (`isReadRequest=false`)。失败不抛错,返回「摘要不可用」的降级结果。
|
|
4226
|
+
*/
|
|
4227
|
+
async function fetchCardSummaryResult(env, card, userId) {
|
|
4228
|
+
const requestId = asString(card?.internal?.requestId) ?? '';
|
|
4229
|
+
if (!requestId) {
|
|
4230
|
+
return {
|
|
4231
|
+
card,
|
|
4232
|
+
requested: false,
|
|
4233
|
+
summary: '详情接口未返回 requestId,摘要不可用',
|
|
4234
|
+
source: 'workflowDetailUnavailable',
|
|
4235
|
+
fields: [],
|
|
4236
|
+
error: { stopReason: 'missing_request_id' },
|
|
4237
|
+
};
|
|
4238
|
+
}
|
|
4239
|
+
let detailResponse;
|
|
4240
|
+
try {
|
|
4241
|
+
const payload = {
|
|
4242
|
+
requestId,
|
|
4243
|
+
isGetRequestBaseInfo: true,
|
|
4244
|
+
isGetRequestLog: false,
|
|
4245
|
+
isGetFormData: true,
|
|
4246
|
+
// 列表卡片补摘要不得改变未读状态——只有用户主动查看流程内容时 isReadRequest 才为 true
|
|
4247
|
+
isReadRequest: false,
|
|
4248
|
+
};
|
|
4249
|
+
if (userId)
|
|
4250
|
+
payload.userId = userId;
|
|
4251
|
+
detailResponse = await env.host.workflowRequest({
|
|
4252
|
+
method: 'POST',
|
|
4253
|
+
path: WORKFLOW_ENDPOINTS.REQUEST_DETAIL,
|
|
4254
|
+
operation: 'workflow.search.summary.detail',
|
|
4255
|
+
body: payload,
|
|
4256
|
+
timeoutMs: SUMMARY_DETAIL_TIMEOUT_MS,
|
|
4257
|
+
});
|
|
4258
|
+
}
|
|
4259
|
+
catch (error) {
|
|
4260
|
+
const stopReason = summaryStopReasonOf(error);
|
|
4261
|
+
return {
|
|
4262
|
+
card,
|
|
4263
|
+
requested: true,
|
|
4264
|
+
summary: detailSummaryUnavailableFromError({ stopReason }),
|
|
4265
|
+
source: 'workflowDetailUnavailable',
|
|
4266
|
+
fields: [],
|
|
4267
|
+
error: { requestId, stopReason },
|
|
4268
|
+
};
|
|
4269
|
+
}
|
|
4270
|
+
const detail = normalizeDetailResponse(detailResponse);
|
|
4271
|
+
const formFields = Array.isArray(detail?.formData?.fields) ? detail.formData.fields : [];
|
|
4272
|
+
const selectedFields = filterCardSummaryFields(card, selectSummaryFields(formFields, 12));
|
|
4273
|
+
if (selectedFields.length) {
|
|
4274
|
+
return {
|
|
4275
|
+
card,
|
|
4276
|
+
requested: true,
|
|
4277
|
+
summary: '',
|
|
4278
|
+
source: 'workflowDetailFormDataAutoSummary',
|
|
4279
|
+
fields: selectedFields,
|
|
4280
|
+
error: null,
|
|
4281
|
+
};
|
|
4282
|
+
}
|
|
4283
|
+
return {
|
|
4284
|
+
card,
|
|
4285
|
+
requested: true,
|
|
4286
|
+
summary: detailSummaryUnavailableMessage(detail?.formData ?? {}),
|
|
4287
|
+
source: 'workflowDetailUnavailable',
|
|
4288
|
+
fields: [],
|
|
4289
|
+
error: null,
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
/** 把补摘要结果写回卡片与索引条目(来源:Python apply_card_summary_result,L3588)。
|
|
4293
|
+
*
|
|
4294
|
+
* 摘要行渲染统一走 `renderCardMarkdown(card, true)`——补到摘要的卡片按三行渲染。
|
|
4295
|
+
*/
|
|
4296
|
+
function applyCardSummaryResult(normalized, result) {
|
|
4297
|
+
const card = result?.card;
|
|
4298
|
+
if (!card || typeof card !== 'object')
|
|
4299
|
+
return;
|
|
4300
|
+
let summary = String(result.summary ?? '');
|
|
4301
|
+
const source = String(result.source ?? 'workflowDetailUnavailable');
|
|
4302
|
+
const selectedFields = Array.isArray(result.fields) ? result.fields : [];
|
|
4303
|
+
const summaryContext = buildSummaryContextText(selectedFields);
|
|
4304
|
+
let needsModel = false;
|
|
4305
|
+
if (source === 'workflowDetailFormDataAutoSummary') {
|
|
4306
|
+
const forbiddenTerms = (card.summaryForbiddenTerms ?? [])
|
|
4307
|
+
.map((term) => stripHtml(term))
|
|
4308
|
+
.filter(Boolean);
|
|
4309
|
+
summary = generateBusinessSummary(selectedFields, forbiddenTerms) || summaryContext;
|
|
4310
|
+
// 脚本自动摘要信息量不足(过短/空)→ 标记 needsModelSummary,保留
|
|
4311
|
+
// summaryFields/summaryContext/summaryInstruction 交模型理解字段数据后生成
|
|
4312
|
+
if (stripHtml(summary).length < SUMMARY_MODEL_FALLBACK_MIN_LEN) {
|
|
4313
|
+
summary = '摘要暂未生成,可打开流程查看';
|
|
4314
|
+
needsModel = true;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
card.summary = summary;
|
|
4318
|
+
card.summarySource = source;
|
|
4319
|
+
card.summaryFields = selectedFields;
|
|
4320
|
+
card.summaryContext = summaryContext;
|
|
4321
|
+
card.needsModelSummary = needsModel;
|
|
4322
|
+
if (needsModel)
|
|
4323
|
+
card.summaryInstruction = SUMMARY_MODEL_INSTRUCTION;
|
|
4324
|
+
else
|
|
4325
|
+
delete card.summaryInstruction;
|
|
4326
|
+
delete card.renderMarkdownTemplate;
|
|
4327
|
+
delete card.renderHtml;
|
|
4328
|
+
delete card.renderHtmlTemplate;
|
|
4329
|
+
card.renderMarkdown = renderCardMarkdown(card, true);
|
|
4330
|
+
updateItemSummary(normalized, card.index, summary, source, selectedFields, needsModel);
|
|
4331
|
+
}
|
|
4332
|
+
/** 同步摘要到索引条目(来源:Python update_item_summary,L3320)。
|
|
4333
|
+
*
|
|
4334
|
+
* V11.3 还会同步 `sessionMapPayload.items`;本实现在 `workflow.search` 里是
|
|
4335
|
+
* **先补摘要、再 `saveListIndex`**,索引落盘的就是这份已原地改写的 `items`,语义等价。
|
|
4336
|
+
*/
|
|
4337
|
+
function updateItemSummary(normalized, index, summary, summarySource, fields, needsModel = false) {
|
|
4338
|
+
const items = normalized.items;
|
|
4339
|
+
if (!Array.isArray(items))
|
|
4340
|
+
return;
|
|
4341
|
+
for (const item of items) {
|
|
4342
|
+
if (!item || typeof item !== 'object')
|
|
4343
|
+
continue;
|
|
4344
|
+
if (String(item.index) !== String(index))
|
|
4345
|
+
continue;
|
|
4346
|
+
item.cardSummary = summary;
|
|
4347
|
+
item.summarySource = summarySource;
|
|
4348
|
+
item.summaryFields = fields;
|
|
4349
|
+
item.needsModelSummary = needsModel;
|
|
4350
|
+
item.summaryContext = buildSummaryContextText(fields);
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
/** 逐卡补摘要(来源:Python enrich_workflow_card_summaries,L3625)。
|
|
4355
|
+
*
|
|
4356
|
+
* 只为**当前页前 maxItems 张**卡片补详情,不为未展示页预取;并发上限 4 + 5s 短超时,单卡失败
|
|
4357
|
+
* 只降级为该卡片的「摘要不可用」提示,不影响整条查询。
|
|
4358
|
+
*/
|
|
4359
|
+
async function enrichWorkflowCardSummaries(env, normalized, maxItems = 10) {
|
|
4360
|
+
const cardItems = normalized.cardItems;
|
|
4361
|
+
if (!Array.isArray(cardItems) || maxItems <= 0) {
|
|
4362
|
+
normalized.summaryEnrichment = {
|
|
4363
|
+
enabled: false,
|
|
4364
|
+
reason: Array.isArray(cardItems) ? 'max_items_lte_zero' : 'no_card_items',
|
|
4365
|
+
};
|
|
4366
|
+
return normalized;
|
|
4367
|
+
}
|
|
4368
|
+
const targetCards = cardItems.slice(0, maxItems).filter((card) => card && typeof card === 'object');
|
|
4369
|
+
// 会话只取一次(V11.3 把 runtime 传进每个 worker),避免 N 张卡重复解析登录态
|
|
4370
|
+
const session = await env.host.getSession();
|
|
4371
|
+
const userId = session.userId || undefined;
|
|
4372
|
+
let completed = 0;
|
|
4373
|
+
let unavailable = 0;
|
|
4374
|
+
let detailsRequested = 0;
|
|
4375
|
+
const errors = [];
|
|
4376
|
+
const maxWorkers = Math.max(1, Math.min(targetCards.length, SUMMARY_ENRICH_MAX_WORKERS));
|
|
4377
|
+
let cursor = 0;
|
|
4378
|
+
const worker = async () => {
|
|
4379
|
+
while (cursor < targetCards.length) {
|
|
4380
|
+
const card = targetCards[cursor];
|
|
4381
|
+
cursor += 1;
|
|
4382
|
+
let result;
|
|
4383
|
+
try {
|
|
4384
|
+
result = await fetchCardSummaryResult(env, card, userId);
|
|
4385
|
+
}
|
|
4386
|
+
catch (error) {
|
|
4387
|
+
result = {
|
|
4388
|
+
card: {},
|
|
4389
|
+
requested: true,
|
|
4390
|
+
summary: '详情接口异常,摘要暂不可用',
|
|
4391
|
+
source: 'workflowDetailUnavailable',
|
|
4392
|
+
fields: [],
|
|
4393
|
+
error: { stopReason: 'summary_worker_exception', error: errorText(error) },
|
|
4394
|
+
};
|
|
4395
|
+
}
|
|
4396
|
+
if (result.requested)
|
|
4397
|
+
detailsRequested += 1;
|
|
4398
|
+
if (result.source === 'workflowDetailFormDataAutoSummary')
|
|
4399
|
+
completed += 1;
|
|
4400
|
+
else
|
|
4401
|
+
unavailable += 1;
|
|
4402
|
+
if (result.error && typeof result.error === 'object')
|
|
4403
|
+
errors.push(result.error);
|
|
4404
|
+
applyCardSummaryResult(normalized, result);
|
|
4405
|
+
}
|
|
4406
|
+
};
|
|
4407
|
+
await Promise.all(Array.from({ length: maxWorkers }, () => worker()));
|
|
4408
|
+
normalized.summaryEnrichment = {
|
|
4409
|
+
enabled: true,
|
|
4410
|
+
source: 'REQUEST_DETAIL',
|
|
4411
|
+
requested: detailsRequested,
|
|
4412
|
+
completed,
|
|
4413
|
+
unavailable,
|
|
4414
|
+
maxItems,
|
|
4415
|
+
mode: 'parallel',
|
|
4416
|
+
maxWorkers,
|
|
4417
|
+
errors: errors.slice(0, 10),
|
|
4418
|
+
rules: [
|
|
4419
|
+
'列表接口 displayData 只提供流程系统字段;summaryContext 来自详情接口表单字段',
|
|
4420
|
+
'最终卡片摘要已由命令根据表单业务字段生成,不混入发起人、发起时间、当前节点、流程状态、停留时长等辅助信息或流程元信息',
|
|
4421
|
+
'每个卡片的 summaryForbiddenTerms 记录了发起人姓名等禁止进入摘要的词;脚本生成摘要时会过滤这些词',
|
|
4422
|
+
'最终回复直接输出 renderMarkdown,不要重新生成摘要、不要重写模板结构、不要把 cardItems 改写成普通编号段落或连续纯文本',
|
|
4423
|
+
'只为当前页卡片补摘要,不为未展示页预取详情',
|
|
4424
|
+
'详情接口未返回表单数据时,只在对应卡片展示不可用提示,不使用系统字段伪造摘要',
|
|
4425
|
+
],
|
|
4426
|
+
};
|
|
4427
|
+
return normalized;
|
|
4428
|
+
}
|
|
3759
4429
|
function normalizeDetailSummaryValue(_name, value) {
|
|
3760
4430
|
let text = stripHtml(value);
|
|
3761
4431
|
text = text.replace(/https?:\/\/\S+/gi, '').trim();
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
4432
|
+
// 与 V11.3 一致:标点只裁**首尾**(Python `.strip(" ::;;,.,。")`),不删正文里的标点——
|
|
4433
|
+
// 旧实现用 `/[...]/g` 删掉了「金额:1000」这类值里的冒号,摘要读起来是「金额1000」。
|
|
4434
|
+
text = text.replace(/\s+/g, ' ').replace(/^[ ::;;,.,。]+|[ ::;;,.,。]+$/g, '');
|
|
4435
|
+
// 截断同样走 V11.3 的标点边界口径(旧实现是硬 slice,会把词截成半截)
|
|
4436
|
+
return truncateText(text, 180);
|
|
3766
4437
|
}
|
|
3767
4438
|
function isMeaninglessSummaryValue(value) {
|
|
3768
4439
|
const text = (value || '').trim();
|
|
@@ -3842,10 +4513,98 @@ function selectSummaryFields(fields, limit = 12) {
|
|
|
3842
4513
|
}
|
|
3843
4514
|
return candidates;
|
|
3844
4515
|
}
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
4516
|
+
/** 业务摘要生成(来源:Python generate_business_summary,L1385)。
|
|
4517
|
+
*
|
|
4518
|
+
* 把任意工作流的表单字段归一为「原因 / 金额 / 类型 / 起止时间 / 时长 / 负责人」等通用语义角色,
|
|
4519
|
+
* 按固定优先级拼成一句话(最多 3 个片段、总长 100 字内),**不依赖具体流程模板**。语义角色凑不齐
|
|
4520
|
+
* 时用尚未使用的业务字段兜底补齐。信息量不足(< `SUMMARY_MODEL_FALLBACK_MIN_LEN`)时返回空串,
|
|
4521
|
+
* 由调用方标记 `needsModelSummary` 交模型基于 `summaryContext` 理解生成。
|
|
4522
|
+
*
|
|
4523
|
+
* `forbiddenTerms` 是发起人姓名等禁止进入摘要的词(见 `listSummaryForbiddenTerms`)。
|
|
4524
|
+
*/
|
|
4525
|
+
function generateBusinessSummary(fields, forbiddenTerms = null) {
|
|
4526
|
+
const cleanFields = [];
|
|
4527
|
+
for (const field of fields) {
|
|
4528
|
+
const name = stripHtml(field?.fieldName ?? '');
|
|
4529
|
+
const value = stripHtml(field?.value ?? '');
|
|
4530
|
+
if (!name || !value)
|
|
4531
|
+
continue;
|
|
4532
|
+
if ((forbiddenTerms ?? []).some((term) => stripHtml(term) && value.includes(stripHtml(term))))
|
|
4533
|
+
continue;
|
|
4534
|
+
cleanFields.push({ fieldName: name, value, componentKey: asString(field?.componentKey) ?? '' });
|
|
4535
|
+
}
|
|
4536
|
+
if (!cleanFields.length)
|
|
4537
|
+
return '';
|
|
4538
|
+
const reasonCandidates = bestReasonFields(cleanFields);
|
|
4539
|
+
let reasonField = reasonCandidates.length ? reasonCandidates[0] : null;
|
|
4540
|
+
const anchorStem = reasonField ? summaryFieldStem(reasonField.fieldName) : '';
|
|
4541
|
+
const amountField = bestSemanticField(cleanFields, 'amount', anchorStem);
|
|
4542
|
+
const typeField = bestSemanticField(cleanFields, 'type', anchorStem);
|
|
4543
|
+
const durationField = bestSemanticField(cleanFields, 'duration', anchorStem);
|
|
4544
|
+
const personField = bestSemanticField(cleanFields, 'person', anchorStem);
|
|
4545
|
+
const temporalFragment = summaryTemporalFragment(cleanFields, anchorStem);
|
|
4546
|
+
const fragments = [];
|
|
4547
|
+
const usedPairs = new Set();
|
|
4548
|
+
const pairKey = (field) => `${stripHtml(field?.fieldName)}\u0000${stripHtml(field?.value)}`;
|
|
4549
|
+
const appendFragment = (text, field) => {
|
|
4550
|
+
const value = stripHtml(text);
|
|
4551
|
+
if (!value || fragments.length >= 3)
|
|
4552
|
+
return;
|
|
4553
|
+
let pair = null;
|
|
4554
|
+
if (field) {
|
|
4555
|
+
pair = pairKey(field);
|
|
4556
|
+
if (usedPairs.has(pair))
|
|
4557
|
+
return;
|
|
4558
|
+
}
|
|
4559
|
+
if (fragments.includes(value))
|
|
4560
|
+
return;
|
|
4561
|
+
fragments.push(value);
|
|
4562
|
+
if (pair)
|
|
4563
|
+
usedPairs.add(pair);
|
|
4564
|
+
};
|
|
4565
|
+
let reasonText = '';
|
|
4566
|
+
let reasonScore = 0;
|
|
4567
|
+
if (reasonField) {
|
|
4568
|
+
reasonScore = scoreSummaryFieldValue(stripHtml(reasonField.fieldName), stripHtml(reasonField.value));
|
|
4569
|
+
reasonText = summarizeReasonText(extractLabeledSummaryValue(stripHtml(reasonField.value)));
|
|
4570
|
+
// 主字段提炼后过短(< 8 字)→ 顺延到信息量次高的描述字段
|
|
4571
|
+
if (reasonText.length < 8) {
|
|
4572
|
+
for (const alternative of reasonCandidates.slice(1)) {
|
|
4573
|
+
const alternativeText = summarizeReasonText(extractLabeledSummaryValue(stripHtml(alternative.value)));
|
|
4574
|
+
if (alternativeText.length > reasonText.length) {
|
|
4575
|
+
reasonField = alternative;
|
|
4576
|
+
reasonText = alternativeText;
|
|
4577
|
+
reasonScore = scoreSummaryFieldValue(stripHtml(alternative.fieldName), stripHtml(alternative.value));
|
|
4578
|
+
break;
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
if (reasonText && reasonScore >= 4)
|
|
4584
|
+
appendFragment(reasonText, reasonField);
|
|
4585
|
+
if (amountField)
|
|
4586
|
+
appendFragment(`${stripHtml(amountField.fieldName)}${valueWithUnit(stripHtml(amountField.value), '元')}`, amountField);
|
|
4587
|
+
if (typeField)
|
|
4588
|
+
appendFragment(`${stripHtml(typeField.fieldName)}${stripHtml(typeField.value)}`, typeField);
|
|
4589
|
+
appendFragment(temporalFragment);
|
|
4590
|
+
if (durationField)
|
|
4591
|
+
appendFragment(`${stripHtml(durationField.fieldName)}${stripHtml(durationField.value)}`, durationField);
|
|
4592
|
+
if (reasonText && reasonScore < 4) {
|
|
4593
|
+
appendFragment(reasonField ? `${stripHtml(reasonField.fieldName)}${reasonText}` : reasonText, reasonField);
|
|
4594
|
+
}
|
|
4595
|
+
if (personField)
|
|
4596
|
+
appendFragment(`${stripHtml(personField.fieldName)}${stripHtml(personField.value)}`, personField);
|
|
4597
|
+
// 语义角色仍不足时,用尚未使用的业务字段补齐(通用兜底,不按工作流写特例)
|
|
4598
|
+
for (const field of cleanFields) {
|
|
4599
|
+
if (usedPairs.has(`${field.fieldName}\u0000${field.value}`))
|
|
4600
|
+
continue;
|
|
4601
|
+
if (field.value && fragments.some((fragment) => fragment.includes(field.value)))
|
|
4602
|
+
continue;
|
|
4603
|
+
appendFragment(`${field.fieldName}${field.value}`, field);
|
|
4604
|
+
if (fragments.length >= 3)
|
|
4605
|
+
break;
|
|
4606
|
+
}
|
|
4607
|
+
return cleanGeneratedSummary(fragments.join(','), forbiddenTerms, 100, false);
|
|
3849
4608
|
}
|
|
3850
4609
|
// ---------------------------------------------------------------------------
|
|
3851
4610
|
// workflow.open(来源:cmd_workflow_open / build_workflow_open_url)
|