weaver-work-cli 0.1.29 → 0.1.32
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-UMGX72K6.js +2537 -0
- package/dist/chunks/archive-oa-N2IYBLF3.js +1317 -0
- package/dist/chunks/asset-GLEGKFG5.js +2109 -0
- package/dist/chunks/attachment-content-AQNX5N3S.js +23 -0
- package/dist/chunks/blog-E44OTVDA.js +2157 -0
- package/dist/chunks/caizhilian-DQUFZXZL.js +10087 -0
- package/dist/chunks/calendar-JKPIRKFD.js +1195 -0
- package/dist/chunks/chunk-2GA5ZIQL.js +1615 -0
- package/dist/chunks/chunk-4YC43QQ4.js +403 -0
- package/dist/chunks/chunk-6EFWDL7Y.js +161 -0
- package/dist/chunks/chunk-6RKD4DRZ.js +5556 -0
- package/dist/chunks/chunk-AJ62BGAW.js +29 -0
- package/dist/chunks/chunk-BUSC6UAE.js +43 -0
- package/dist/chunks/chunk-C6R3B2ML.js +358 -0
- package/dist/chunks/chunk-CVZ2JOSW.js +62 -0
- package/dist/chunks/chunk-DW6AUUKS.js +508 -0
- package/dist/chunks/chunk-E4Z23GHM.js +6962 -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-G3QJULO2.js +387 -0
- package/dist/chunks/chunk-GCG2NMWR.js +703 -0
- package/dist/chunks/chunk-GPMFD4RD.js +75 -0
- package/dist/chunks/chunk-H6XSTSLM.js +2357 -0
- package/dist/chunks/chunk-H7J4DC5R.js +464 -0
- package/dist/chunks/chunk-IURUFGZK.js +477 -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-WKI2FBIQ.js +15606 -0
- package/dist/chunks/chunk-WSKBSQ2U.js +231 -0
- package/dist/chunks/chunk-YLCYX7LE.js +371 -0
- package/dist/chunks/chunk-YRVDW4WN.js +205 -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-V5FG2EHC.js +2276 -0
- package/dist/chunks/ehr-HMFY6VIL.js +5241 -0
- package/dist/chunks/employee-SAK2QYXX.js +11 -0
- package/dist/chunks/esb-ZYSVB7SG.js +141 -0
- package/dist/chunks/fna-B7RKPAZD.js +1444 -0
- package/dist/chunks/host-45WED2KK.js +21 -0
- package/dist/chunks/host-I4Z6BFYX.js +23 -0
- package/dist/chunks/host-KNZ52LJZ.js +17 -0
- package/dist/chunks/host-OCH2BCCV.js +19 -0
- package/dist/chunks/hrm-2KAZGUHI.js +244 -0
- package/dist/chunks/im-ACJJWH4U.js +252 -0
- package/dist/chunks/invoice-ZK3WCX2U.js +5179 -0
- package/dist/chunks/jiuchuanhui-5PQD3IL2.js +1940 -0
- package/dist/chunks/lazy-dispatch-JQXBW3GM.js +28 -0
- package/dist/chunks/mail-S2HDDXO2.js +2695 -0
- package/dist/chunks/meeting-QVN3I37L.js +504 -0
- package/dist/chunks/msg-parse-4GSQ2WTO.js +41 -0
- package/dist/chunks/okr-QNG4VC2Q.js +2482 -0
- package/dist/chunks/plan-A25EMW6U.js +2025 -0
- package/dist/chunks/project-G6XV66DZ.js +1361 -0
- package/dist/chunks/registry-2VLUAESA.js +14 -0
- package/dist/chunks/registry-7C5UMIPL.js +19 -0
- package/dist/chunks/registry-DC6KR3NH.js +18 -0
- package/dist/chunks/registry-NYPYMZXG.js +16 -0
- package/dist/chunks/registry-TI3CIUXF.js +19 -0
- package/dist/chunks/room-recommend-WRTHO6Y2.js +20 -0
- package/dist/chunks/schedule-KFIITGSK.js +24 -0
- package/dist/chunks/state-Y2XBP6YE.js +15 -0
- package/dist/chunks/workflow-DJ7SYWAF.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 +61 -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,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 群聊消息 → 会议纪要(一条命令跑完)。
|
|
3
|
+
*
|
|
4
|
+
* 背景(2026-10-26 真机跑通,2026-10-27 固化为操作):
|
|
5
|
+
* 会议记录里带 `imchat_groupid`,正常用法是**拿着会议生成纪要**,而不是拿群反查会议。
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* 会议 id → imchat_groupid
|
|
9
|
+
* → im.msg.sync.group 拉消息(**返回倒序,必须按 msgtime 升序重排**)
|
|
10
|
+
* → 过滤 typeName=通知 的系统消息(可选保留)
|
|
11
|
+
* → sender='我' 换真名(im.person.resolveByIds,按 from_uid)
|
|
12
|
+
* → 拼「时间 说话人:内容」
|
|
13
|
+
* → 建纪要(minutes_content, status=4)
|
|
14
|
+
* → [pipeline=minutes] ESB 633 生成纪要文案 → parse_content, status=1
|
|
15
|
+
* → [pipeline=document] ESB 628 生成文档 → mt_minutes + status=2
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ 实测耗时(11 条消息):拉消息 2.1s、拼原文 1.3s、建纪要 0.6s,
|
|
19
|
+
* 但 **ESB 633 生成纪要文案要 37s 左右** —— 这是最慢的一步,不是卡死。
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ `mtAIMinutePageID` 是租户级固定值(本环境 `1325287772302860413`),没有接口能查。
|
|
22
|
+
* 这里不写死:优先用入参,其次从该会议已有纪要里反查,都拿不到就留空并给 warning。
|
|
23
|
+
*/
|
|
24
|
+
import { meetingValidationError } from '../errors.js';
|
|
25
|
+
import { meetingSuccess } from './types.js';
|
|
26
|
+
/** ESB 动作流 id 的**内置默认值**(参考租户配置)。换租户请用入参 `flowGenerateMinutes` / `flowGenerateDocument` 覆盖。 */
|
|
27
|
+
const FLOW_GENERATE_MINUTES = '1325287772302863633';
|
|
28
|
+
const FLOW_GENERATE_DOCUMENT = '1325287772302863628';
|
|
29
|
+
function textOf(value) {
|
|
30
|
+
return value === undefined || value === null ? '' : String(value);
|
|
31
|
+
}
|
|
32
|
+
const DAY_MS = 24 * 3600_000;
|
|
33
|
+
/** 解析 E10 的本地时间串;只给日期时补 `00:00:00`,否则会得到 Invalid Date 静默退回默认窗口。 */
|
|
34
|
+
function parseLocalTime(value) {
|
|
35
|
+
const text = textOf(value).trim();
|
|
36
|
+
if (!text)
|
|
37
|
+
return null;
|
|
38
|
+
const normalized = /^\d{4}-\d{2}-\d{2}$/u.test(text)
|
|
39
|
+
? `${text} 00:00:00`
|
|
40
|
+
: text.length <= 16
|
|
41
|
+
? `${text}:00`
|
|
42
|
+
: text;
|
|
43
|
+
const parsed = new Date(normalized.replace(/-/g, '/'));
|
|
44
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
45
|
+
}
|
|
46
|
+
function formatStamp(d) {
|
|
47
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
48
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} `
|
|
49
|
+
+ `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 群聊默认时间窗。⚠️ im 模块默认只拉「当天」,而群里分工多是**会前**发的,
|
|
53
|
+
* 只拉当天会漏;窗口也别无限放大(im 侧最多 7 天)。
|
|
54
|
+
*
|
|
55
|
+
* from = max(会议创建时间, 会议开始前 7 天) —— 覆盖会前在群里分工的那几天
|
|
56
|
+
* to = min(现在, 会议结束后 2 小时) —— 覆盖会后收尾,但不拉到未来
|
|
57
|
+
*
|
|
58
|
+
* 抽成纯函数是为了 `meeting.summary`(只读)能复用同一套口径,不复制粘贴。
|
|
59
|
+
*/
|
|
60
|
+
export function computeChatWindow(meeting, now = new Date()) {
|
|
61
|
+
const begin = parseLocalTime(meeting.begin_datetime);
|
|
62
|
+
const end = parseLocalTime(meeting.end_datetime);
|
|
63
|
+
const created = parseLocalTime(meeting.create_time);
|
|
64
|
+
const fromCandidates = [created, begin ? new Date(begin.getTime() - 7 * DAY_MS) : null]
|
|
65
|
+
.filter((item) => item !== null);
|
|
66
|
+
const windowFrom = fromCandidates.length > 0
|
|
67
|
+
? new Date(Math.max(...fromCandidates.map((d) => d.getTime())))
|
|
68
|
+
: new Date(now.getTime() - 7 * DAY_MS);
|
|
69
|
+
const toCandidates = [now, end ? new Date(end.getTime() + 120 * 60_000) : null]
|
|
70
|
+
.filter((item) => item !== null);
|
|
71
|
+
const windowTo = new Date(Math.min(...toCandidates.map((d) => d.getTime())));
|
|
72
|
+
return { from: formatStamp(windowFrom), to: formatStamp(windowTo) };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 大消息量保护:群里几千几万条时,既不能把 CLI 拖死,也不能把 ESB 633 撑爆
|
|
76
|
+
* (它要把 minutes_content 整段喂给模型)。这里做**采样去噪**,规则透明可解释:
|
|
77
|
+
*
|
|
78
|
+
* 1. 必留:被 @ 的消息、含任务/决策关键词的消息、系统通知(若 keepSystem);
|
|
79
|
+
* 2. 丢:纯表情/单字回复(嗯、好、收到…)、连续同人重复、正文过短的灌水;
|
|
80
|
+
* 3. 每人每 N 条最多留 M 条,保证不同人的声音都在,但不被刷屏者淹没。
|
|
81
|
+
*
|
|
82
|
+
* 采样是**显式**的:返回 `sampled: true` 与 `kept/total`,让调用方如实告诉用户"已采样"。
|
|
83
|
+
*/
|
|
84
|
+
const NOISE_MAX_LEN = 4;
|
|
85
|
+
const NOISE_WORDS = new Set([
|
|
86
|
+
'嗯', '恩', '好', '好的', '好滴', '收到', 'ok', 'OK', 'Ok', '行', '可以', '对的', '是的',
|
|
87
|
+
'在', '在的', '哈哈', '哈哈哈', '谢谢', '拜拜', '早', '晚安', '1', '?', '?', '。。。',
|
|
88
|
+
]);
|
|
89
|
+
const KEYWORD_PATTERN = /(负责|决定|结论|确定|定稿|截止|之前|完成|上线|提测|风险|问题|方案|需求|排期|汇报|跟进|@)/;
|
|
90
|
+
function isNoise(content) {
|
|
91
|
+
const text = content.trim();
|
|
92
|
+
if (text.length === 0)
|
|
93
|
+
return false; // 非文本消息由上层占位处理,不在此丢弃
|
|
94
|
+
if (text.length <= NOISE_MAX_LEN && NOISE_WORDS.has(text))
|
|
95
|
+
return true;
|
|
96
|
+
// 纯表情/纯标点
|
|
97
|
+
if (/^[\p{Emoji}\p{P}\s]+$/u.test(text))
|
|
98
|
+
return true;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
/** 按「必留 / 去噪 / 每人限额」采样,返回保留下来的消息(保持原顺序)。 */
|
|
102
|
+
export function sampleMessages(messages, perSenderLimit = 60) {
|
|
103
|
+
const kept = [];
|
|
104
|
+
const perSender = new Map();
|
|
105
|
+
let dropped = 0;
|
|
106
|
+
let lastSpeaker = '';
|
|
107
|
+
let lastContent = '';
|
|
108
|
+
for (const message of messages) {
|
|
109
|
+
const content = textOf(message.content).trim();
|
|
110
|
+
const speaker = textOf(message.from_uid) || textOf(message.sender);
|
|
111
|
+
const mustKeep = KEYWORD_PATTERN.test(content);
|
|
112
|
+
if (!mustKeep) {
|
|
113
|
+
// 连续同一人复读同样内容 → 丢
|
|
114
|
+
if (speaker === lastSpeaker && content !== '' && content === lastContent) {
|
|
115
|
+
dropped += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (isNoise(content)) {
|
|
119
|
+
dropped += 1;
|
|
120
|
+
lastSpeaker = speaker;
|
|
121
|
+
lastContent = content;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const used = perSender.get(speaker) ?? 0;
|
|
125
|
+
if (used >= perSenderLimit) {
|
|
126
|
+
dropped += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
perSender.set(speaker, used + 1);
|
|
130
|
+
}
|
|
131
|
+
kept.push(message);
|
|
132
|
+
lastSpeaker = speaker;
|
|
133
|
+
lastContent = content;
|
|
134
|
+
}
|
|
135
|
+
return { kept, dropped };
|
|
136
|
+
}
|
|
137
|
+
/** 单条消息 → 一行纪要原文。非文本消息只占位,**不编造内容**。 */
|
|
138
|
+
function formatLine(message, nameByUid) {
|
|
139
|
+
const speaker = nameByUid[textOf(message.from_uid)] || textOf(message.sender) || '未知';
|
|
140
|
+
const content = textOf(message.content).trim();
|
|
141
|
+
if (content)
|
|
142
|
+
return `${textOf(message.time)} ${speaker}:${content}`;
|
|
143
|
+
const media = Array.isArray(message.media) && message.media.length > 0 ? message.media[0] : null;
|
|
144
|
+
return `${textOf(message.time)} ${speaker}:[${media ? '附件/图片' : '无文字内容'}]`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 把群消息整理成纪要原文。抽成纯函数是为了能离线单测 ——
|
|
148
|
+
* 排序方向、系统消息过滤、空内容占位这三条口径都踩过坑,必须有断言守着。
|
|
149
|
+
*
|
|
150
|
+
* 返回 `{ transcript, kept, ignoredSystem }`;`kept` 是保留下来的条数。
|
|
151
|
+
*/
|
|
152
|
+
export function composeTranscript(messages, nameByUid, keepSystem) {
|
|
153
|
+
// ⚠️ im.msg.sync.group 返回是**倒序(新→旧)**,不重排会把纪要时间线写反
|
|
154
|
+
const ordered = [...messages].sort((a, b) => Number(a.msgtime ?? 0) - Number(b.msgtime ?? 0));
|
|
155
|
+
const keptMessages = keepSystem ? ordered : ordered.filter((m) => textOf(m.typeName) !== '通知');
|
|
156
|
+
const ignoredSystem = ordered.length - keptMessages.length;
|
|
157
|
+
return {
|
|
158
|
+
transcript: keptMessages.map((message) => formatLine(message, nameByUid)).join('\n'),
|
|
159
|
+
kept: keptMessages.length,
|
|
160
|
+
ignoredSystem,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// 摘要版(用户 2026-10-27 选定方案 B)
|
|
165
|
+
//
|
|
166
|
+
// 采样压完仍超上限时(几万条那种群),不能把几十万字塞给 ESB 633 —— 它不是
|
|
167
|
+
// "慢",是**会被撑爆**。这里只保留"分工 / 结论 / 待办"这类有决策价值的发言,
|
|
168
|
+
// 并明确标注这是摘要版;完整原文另存文件,供需要时人工查阅。
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
const DIGEST_PATTERN = /(@|负责|分工|安排|决定|结论|确定|定稿|通过|不同意|同意|截止|完成|上线|提测|排期|汇报|跟进|待办|风险|问题|方案|需求|下周|本周|明天|后天|月底|之前|号前)/;
|
|
171
|
+
/** 只保留有决策价值的发言,压成摘要版;仍超上限则按行截断。 */
|
|
172
|
+
export function buildDigest(messages, nameByUid, keepSystem, maxChars) {
|
|
173
|
+
const ordered = [...messages].sort((a, b) => Number(a.msgtime ?? 0) - Number(b.msgtime ?? 0));
|
|
174
|
+
const base = keepSystem ? ordered : ordered.filter((m) => textOf(m.typeName) !== '通知');
|
|
175
|
+
const essence = base.filter((message) => {
|
|
176
|
+
const content = textOf(message.content).trim();
|
|
177
|
+
if (!content)
|
|
178
|
+
return false; // 摘要版里占位行没有信息量
|
|
179
|
+
return DIGEST_PATTERN.test(content);
|
|
180
|
+
});
|
|
181
|
+
const dropped = base.length - essence.length;
|
|
182
|
+
const header = `【群聊摘要:共 ${base.length} 条消息,仅保留与分工/结论/待办相关的 ${essence.length} 条,省略 ${dropped} 条】`;
|
|
183
|
+
let lines = essence.map((message) => formatLine(message, nameByUid));
|
|
184
|
+
let truncated = false;
|
|
185
|
+
let transcript = [header, ...lines].join('\n');
|
|
186
|
+
if (transcript.length > maxChars) {
|
|
187
|
+
// 按行截断,避免切在句子中间;保留最早的发言(通常是分工与背景)
|
|
188
|
+
const kept = [];
|
|
189
|
+
let used = header.length + 1;
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
if (used + line.length + 1 > maxChars)
|
|
192
|
+
break;
|
|
193
|
+
kept.push(line);
|
|
194
|
+
used += line.length + 1;
|
|
195
|
+
}
|
|
196
|
+
truncated = true;
|
|
197
|
+
lines = kept;
|
|
198
|
+
transcript = [header, ...kept, `…(以下省略 ${essence.length - kept.length} 条,原文过长)`].join('\n');
|
|
199
|
+
}
|
|
200
|
+
return { transcript, kept: lines.length, dropped, truncated };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 完整原文落盘。摘要版一旦启用,用户很可能想核原文 —— 不能只在返回值里
|
|
204
|
+
* 留一句"已省略",得给一个真能打开的路径。
|
|
205
|
+
* 落在 CLI state 目录(不进工作区,避免污染用户仓库)。
|
|
206
|
+
*/
|
|
207
|
+
async function saveTranscript(ctx, mtId, transcript) {
|
|
208
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
209
|
+
const { join } = await import('node:path');
|
|
210
|
+
const { stateDir } = await import('../../../core/state.js');
|
|
211
|
+
const dir = join(stateDir(ctx), 'transcripts');
|
|
212
|
+
await mkdir(dir, { recursive: true });
|
|
213
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
214
|
+
const now = new Date();
|
|
215
|
+
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
216
|
+
const filePath = join(dir, `${mtId}-${stamp}.txt`);
|
|
217
|
+
await writeFile(filePath, transcript, 'utf8');
|
|
218
|
+
return filePath;
|
|
219
|
+
}
|
|
220
|
+
const meetingMinuteFromGroupChat = {
|
|
221
|
+
name: 'meeting.minute.fromGroupChat',
|
|
222
|
+
resource: 'minute',
|
|
223
|
+
method: 'fromGroupChat',
|
|
224
|
+
action: 'meeting.minute.fromGroupChat',
|
|
225
|
+
summary: '把会议关联群聊的消息整理成纪要:拉消息→按时间正序→过滤系统通知→拼原文→建纪要;可选继续生成纪要文案与文档',
|
|
226
|
+
capability: 'minute',
|
|
227
|
+
risk: 'high-risk-write',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
properties: {
|
|
232
|
+
mt_id: { type: 'string', pattern: '^\\d+$', description: '会议 id(必填)' },
|
|
233
|
+
subject: { type: 'string', description: '纪要标题,默认「群聊纪要-<会议名>」' },
|
|
234
|
+
groupId: { type: 'string', description: '群 id;省略时取会议记录的 imchat_groupid' },
|
|
235
|
+
files: {
|
|
236
|
+
type: 'array',
|
|
237
|
+
items: { type: 'string' },
|
|
238
|
+
description: '用户提供的会议材料(本地文件路径,可多个):txt/md/csv/json/docx/xlsx/pptx 会抽成文本一并写进纪要。'
|
|
239
|
+
+ '**只要会议有 imchat_groupid,群聊也会自动并入**,两者不冲突',
|
|
240
|
+
},
|
|
241
|
+
content: { type: 'string', description: '用户直接给的纪要正文/要点(与 files 可同时给),同样并入纪要' },
|
|
242
|
+
num: { type: 'integer', minimum: 1, maximum: 50, description: '拉取条数上限(默认 50)' },
|
|
243
|
+
from: {
|
|
244
|
+
type: 'string',
|
|
245
|
+
description: '起始时间;省略时取「会议创建时间 与 会议开始前 7 天 的较晚者」(会前在群里分工的那几天要覆盖到)',
|
|
246
|
+
},
|
|
247
|
+
to: { type: 'string', description: '结束时间;省略时取「当前时间 与 会议结束后 2 小时 的较早者」' },
|
|
248
|
+
sampling: {
|
|
249
|
+
type: 'boolean',
|
|
250
|
+
description: '消息超过 sampleThreshold 时是否采样去噪(默认 true)。关掉会把全部消息塞进原文,几千条时极慢且会撑爆 AI',
|
|
251
|
+
},
|
|
252
|
+
sampleThreshold: {
|
|
253
|
+
type: 'integer',
|
|
254
|
+
minimum: 10,
|
|
255
|
+
description: '触发采样的消息条数阈值(默认 200)',
|
|
256
|
+
},
|
|
257
|
+
perSenderLimit: {
|
|
258
|
+
type: 'integer',
|
|
259
|
+
minimum: 5,
|
|
260
|
+
description: '采样时每人最多保留多少条非关键消息(默认 60),防止刷屏者淹没别人',
|
|
261
|
+
},
|
|
262
|
+
digest: {
|
|
263
|
+
type: 'boolean',
|
|
264
|
+
description: '原文超过 maxTranscriptChars 时是否改用摘要版(默认 true)。摘要版只保留分工/结论/待办,'
|
|
265
|
+
+ '完整原文另存文件;设为 false 会硬塞全文,几万字时 ESB 生成纪要大概率失败',
|
|
266
|
+
},
|
|
267
|
+
maxTranscriptChars: {
|
|
268
|
+
type: 'integer',
|
|
269
|
+
minimum: 2000,
|
|
270
|
+
description: '原文长度上限(默认 20000 字)。超过则触发摘要版,避免把超长文本喂给 ESB',
|
|
271
|
+
},
|
|
272
|
+
keepSystem: {
|
|
273
|
+
type: 'boolean',
|
|
274
|
+
description: '是否保留系统通知(typeName=通知,如入群/改名)。默认 false 过滤掉,但会告知忽略了几条',
|
|
275
|
+
},
|
|
276
|
+
pipeline: {
|
|
277
|
+
type: 'string',
|
|
278
|
+
enum: ['text', 'minutes', 'document'],
|
|
279
|
+
description: 'text=只拼原文写进纪要(默认,最快);minutes=继续调 ESB 生成纪要文案;document=再生成文档上传 OA',
|
|
280
|
+
},
|
|
281
|
+
employeeId: { type: 'string', description: 'ESB 触发人 id,省略时用当前登录人' },
|
|
282
|
+
aiPageId: {
|
|
283
|
+
type: 'string',
|
|
284
|
+
description: '智能纪要页面 id(mt_minutes_ai_page)。省略时从该会议已有纪要反查',
|
|
285
|
+
},
|
|
286
|
+
flowGenerateMinutes: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
pattern: '^\\d+$',
|
|
289
|
+
description: 'ESB「根据解析文本生成智能纪要文本」流程 id。省略时用内置默认值;**换租户必须覆盖**'
|
|
290
|
+
+ '(流程 id 是租户级配置,各环境不同)',
|
|
291
|
+
},
|
|
292
|
+
flowGenerateDocument: {
|
|
293
|
+
type: 'string',
|
|
294
|
+
pattern: '^\\d+$',
|
|
295
|
+
description: 'ESB「保存解析并生成文档」流程 id。省略时用内置默认值;换租户必须覆盖',
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
required: ['mt_id'],
|
|
299
|
+
},
|
|
300
|
+
handler: async (args, env) => {
|
|
301
|
+
const startedAt = Date.now();
|
|
302
|
+
const timings = {};
|
|
303
|
+
const warnings = [];
|
|
304
|
+
const mark = (label, from) => {
|
|
305
|
+
timings[label] = Date.now() - from;
|
|
306
|
+
};
|
|
307
|
+
const mtId = textOf(args.mt_id).trim();
|
|
308
|
+
if (!mtId)
|
|
309
|
+
throw meetingValidationError('mt_id_required', '必须提供会议 id(mt_id)');
|
|
310
|
+
const pipeline = ['text', 'minutes', 'document'].includes(args.pipeline)
|
|
311
|
+
? args.pipeline
|
|
312
|
+
: 'text';
|
|
313
|
+
const { getMeetingOperation } = await import('./registry.js');
|
|
314
|
+
const ctx = env.ctx;
|
|
315
|
+
// ① 会议 → 群 id
|
|
316
|
+
let stepStart = Date.now();
|
|
317
|
+
const listed = await getMeetingOperation('meeting.eb.meeting.list').handler({ mt_id: mtId, pageNo: 1, pageSize: 1 }, env);
|
|
318
|
+
const meeting = (listed?.data?.rows ?? [])[0]?.mainTable ?? null;
|
|
319
|
+
if (!meeting) {
|
|
320
|
+
throw meetingValidationError('not_found', `未找到会议 id=${mtId},或当前账号无权限查看`, { id: mtId });
|
|
321
|
+
}
|
|
322
|
+
const groupId = textOf(args.groupId).trim() || textOf(meeting.imchat_groupid).trim();
|
|
323
|
+
const extraFiles = Array.isArray(args.files)
|
|
324
|
+
? args.files.map((item) => textOf(item).trim()).filter((item) => item !== '')
|
|
325
|
+
: [];
|
|
326
|
+
const extraContent = textOf(args.content).trim();
|
|
327
|
+
const hasExtraSource = extraFiles.length > 0 || extraContent !== '';
|
|
328
|
+
if (!groupId && !hasExtraSource) {
|
|
329
|
+
throw meetingValidationError('group_missing', `会议「${textOf(meeting.mt_name)}」没有 imchat_groupid,也没有提供会议材料(files/content),无法生成纪要`, { id: mtId });
|
|
330
|
+
}
|
|
331
|
+
mark('readMeeting', stepStart);
|
|
332
|
+
// ② 拉群消息(走 im 模块,复用其翻页/区间逻辑)
|
|
333
|
+
const pullStart = Date.now();
|
|
334
|
+
const { createImHost } = await import('../../im/host.js');
|
|
335
|
+
const { createImContinuationSigner } = await import('../../im/continuation.js');
|
|
336
|
+
const { getImOperation } = await import('../../im/operations/registry.js');
|
|
337
|
+
const imEnv = { ctx, host: createImHost(ctx), continuation: createImContinuationSigner(ctx) };
|
|
338
|
+
const syncOp = getImOperation('im.msg.sync.group');
|
|
339
|
+
const chatWindow = computeChatWindow(meeting);
|
|
340
|
+
const defaultFrom = chatWindow.from;
|
|
341
|
+
const defaultTo = chatWindow.to;
|
|
342
|
+
// 有群才拉;只给材料(files/content)时不拉群,直接走合并
|
|
343
|
+
const pulled = groupId
|
|
344
|
+
? await syncOp.handler({
|
|
345
|
+
groupId,
|
|
346
|
+
num: typeof args.num === 'number' ? args.num : 50,
|
|
347
|
+
from: args.from ? String(args.from) : defaultFrom,
|
|
348
|
+
to: args.to ? String(args.to) : defaultTo,
|
|
349
|
+
}, imEnv)
|
|
350
|
+
: { data: { messages: [] } };
|
|
351
|
+
const messages = pulled?.data?.messages ?? [];
|
|
352
|
+
mark('pullMessages', pullStart);
|
|
353
|
+
if (messages.length === 0 && !hasExtraSource) {
|
|
354
|
+
return meetingSuccess('meeting.minute.fromGroupChat', { id: mtId, groupId, messageCount: 0, minuteId: null, timings }, { warnings: ['该群在指定时间范围内没有消息,且未提供会议材料,未建纪要'] });
|
|
355
|
+
}
|
|
356
|
+
if (messages.length === 0 && hasExtraSource) {
|
|
357
|
+
warnings.push('群聊在指定时间窗内没有消息,本次仅使用用户提供的会议材料生成纪要');
|
|
358
|
+
}
|
|
359
|
+
// ③ 说话人真名(sender='我' 时尤其需要)
|
|
360
|
+
stepStart = Date.now();
|
|
361
|
+
const uids = [...new Set(messages.map((m) => textOf(m.from_uid)).filter(Boolean))];
|
|
362
|
+
const nameByUid = {};
|
|
363
|
+
if (uids.length > 0) {
|
|
364
|
+
try {
|
|
365
|
+
const resolveOp = getImOperation('im.person.resolveByIds');
|
|
366
|
+
const resolved = await resolveOp.handler({ ids: uids }, imEnv);
|
|
367
|
+
for (const person of resolved?.data?.employees ?? []) {
|
|
368
|
+
if (person?.uid && person?.name)
|
|
369
|
+
nameByUid[String(person.uid)] = String(person.name);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
warnings.push(`说话人姓名解析失败,已回退为原始 sender:${String(error?.message ?? error).slice(0, 80)}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
mark('resolveSenders', stepStart);
|
|
377
|
+
// ④ 重排 + (必要时采样)+ 过滤 + 拼原文
|
|
378
|
+
const keepSystem = args.keepSystem === true;
|
|
379
|
+
const sorted = [...messages].sort((a, b) => Number(a.msgtime ?? 0) - Number(b.msgtime ?? 0));
|
|
380
|
+
const threshold = typeof args.sampleThreshold === 'number' && args.sampleThreshold > 0 ? args.sampleThreshold : 200;
|
|
381
|
+
const perSenderLimit = typeof args.perSenderLimit === 'number' && args.perSenderLimit > 0 ? args.perSenderLimit : 60;
|
|
382
|
+
const sampling = args.sampling !== false;
|
|
383
|
+
let workMessages = sorted;
|
|
384
|
+
let sampledDropped = 0;
|
|
385
|
+
const sampled = sampling && sorted.length > threshold;
|
|
386
|
+
if (sampled) {
|
|
387
|
+
const result = sampleMessages(sorted, perSenderLimit);
|
|
388
|
+
workMessages = result.kept;
|
|
389
|
+
sampledDropped = result.dropped;
|
|
390
|
+
warnings.push(`消息 ${sorted.length} 条超过阈值 ${threshold},已采样:采样阶段保留 ${result.kept.length} 条、丢弃 ${result.dropped} 条灌水/重复。`
|
|
391
|
+
+ '需要全量请调大 sampleThreshold,或收窄 from/to 时间窗');
|
|
392
|
+
}
|
|
393
|
+
const { transcript: groupTranscript, kept: fullKept, ignoredSystem } = composeTranscript(workMessages, nameByUid, keepSystem);
|
|
394
|
+
// 用户提供的材料(文件/正文)也要进纪要 —— 与群聊**合并**而不是二选一。
|
|
395
|
+
// 口径:会议有 imchat_groupid 时群聊自动并入;用户再给材料就一起进。
|
|
396
|
+
const extraSections = [];
|
|
397
|
+
for (const file of extraFiles) {
|
|
398
|
+
try {
|
|
399
|
+
const { readFile } = await import('node:fs/promises');
|
|
400
|
+
const buffer = await readFile(file);
|
|
401
|
+
const { extractText } = await import('./attachment-content.js');
|
|
402
|
+
const extracted = extractText(buffer, file, '');
|
|
403
|
+
if (extracted.text.trim()) {
|
|
404
|
+
extraSections.push(`【材料:${file.split(/[\\/]/).pop() ?? file}】\n${extracted.text.trim()}`);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
warnings.push(`材料 ${file} 无法抽取文本(${extracted.kind}),已跳过`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
warnings.push(`材料 ${file} 读取失败:${String(error?.message ?? error).slice(0, 80)}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (extraContent)
|
|
415
|
+
extraSections.push(`【材料:用户提供的纪要正文】\n${extraContent}`);
|
|
416
|
+
const extraTranscript = extraSections.join('\n\n');
|
|
417
|
+
const fullTranscript = [
|
|
418
|
+
extraTranscript ? `===== 用户提供的会议材料 =====\n${extraTranscript}` : '',
|
|
419
|
+
groupTranscript ? `===== 群聊记录(${workMessages.length} 条)=====\n${groupTranscript}` : '',
|
|
420
|
+
].filter(Boolean).join('\n\n');
|
|
421
|
+
if (ignoredSystem > 0)
|
|
422
|
+
warnings.push(`已忽略 ${ignoredSystem} 条系统通知(入群/改名等);需要保留请传 keepSystem=true`);
|
|
423
|
+
if (!fullTranscript.trim()) {
|
|
424
|
+
return meetingSuccess('meeting.minute.fromGroupChat', { id: mtId, groupId, messageCount: messages.length, ignoredSystem, minuteId: null, timings }, { warnings: ['过滤系统消息后没有可用的文字内容,未建纪要'] });
|
|
425
|
+
}
|
|
426
|
+
// ④b 超长保护:采样后仍超上限 → 摘要版(方案 B)
|
|
427
|
+
// ESB 633 会把 minutes_content 整段喂给模型,几十万字不是慢而是会失败。
|
|
428
|
+
const maxChars = typeof args.maxTranscriptChars === 'number' && args.maxTranscriptChars > 0 ? args.maxTranscriptChars : 20_000;
|
|
429
|
+
let transcript = fullTranscript;
|
|
430
|
+
let kept = fullKept;
|
|
431
|
+
let digestMode = false;
|
|
432
|
+
let digestDropped = 0;
|
|
433
|
+
let digestTruncated = false;
|
|
434
|
+
let fullTranscriptPath = null;
|
|
435
|
+
if (fullTranscript.length > maxChars) {
|
|
436
|
+
if (args.digest === false) {
|
|
437
|
+
warnings.push(`原文 ${fullTranscript.length} 字超过上限 ${maxChars},但已显式关闭摘要版(digest=false):`
|
|
438
|
+
+ 'ESB 生成纪要很可能失败,建议收窄 from/to 时间窗或分批处理');
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const digest = buildDigest(workMessages, nameByUid, keepSystem, maxChars);
|
|
442
|
+
transcript = digest.transcript;
|
|
443
|
+
kept = digest.kept;
|
|
444
|
+
digestDropped = digest.dropped;
|
|
445
|
+
digestTruncated = digest.truncated;
|
|
446
|
+
digestMode = true;
|
|
447
|
+
try {
|
|
448
|
+
fullTranscriptPath = await saveTranscript(ctx, mtId, fullTranscript);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
warnings.push(`完整原文落盘失败:${String(error?.message ?? error).slice(0, 80)}`);
|
|
452
|
+
}
|
|
453
|
+
warnings.push(`原文 ${fullTranscript.length} 字超过上限 ${maxChars},已改用**摘要版**:`
|
|
454
|
+
+ `只保留分工/结论/待办相关发言 ${digest.kept} 条(省略 ${digest.dropped} 条闲聊`
|
|
455
|
+
+ `${digest.truncated ? ',摘要本身仍超长已按时间截断' : ''})。`
|
|
456
|
+
+ (fullTranscriptPath ? `完整原文已存到 ${fullTranscriptPath}。` : '')
|
|
457
|
+
+ '据此生成的纪要是摘要口径,与逐字全文有差异。');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// ⑤ 智能纪要页面 id(租户级常量,能反查就反查)
|
|
461
|
+
let aiPageId = textOf(args.aiPageId).trim();
|
|
462
|
+
if (!aiPageId) {
|
|
463
|
+
try {
|
|
464
|
+
const minuteList = await getMeetingOperation('meeting.eb.minute.list').handler({ mt_id: mtId, pageNo: 1, pageSize: 20 }, env);
|
|
465
|
+
const withPage = (minuteList?.data?.rows ?? [])
|
|
466
|
+
.map((row) => row.mainTable ?? row)
|
|
467
|
+
.find((row) => textOf(row?.mt_minutes_ai_page).trim());
|
|
468
|
+
if (withPage)
|
|
469
|
+
aiPageId = textOf(withPage.mt_minutes_ai_page).trim();
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// 反查失败不影响主流程
|
|
473
|
+
}
|
|
474
|
+
if (!aiPageId) {
|
|
475
|
+
warnings.push('未取到 mt_minutes_ai_page(可传 aiPageId);ESB 解析该字段可能必填,缺它会失败');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// ⑥ 建纪要并写原文
|
|
479
|
+
stepStart = Date.now();
|
|
480
|
+
const subject = textOf(args.subject).trim() || `群聊纪要-${textOf(meeting.mt_name)}`;
|
|
481
|
+
const prepared = await getMeetingOperation('meeting.eb.minute.create.prepare').handler({
|
|
482
|
+
mt_id: mtId,
|
|
483
|
+
mt_minutes_subject: subject,
|
|
484
|
+
minutes_content: transcript,
|
|
485
|
+
minutes_status: '4',
|
|
486
|
+
...(aiPageId ? { mt_minutes_ai_page: aiPageId } : {}),
|
|
487
|
+
...(meeting.begin_datetime ? { begin_datetime: textOf(meeting.begin_datetime) } : {}),
|
|
488
|
+
...(meeting.end_datetime ? { end_datetime: textOf(meeting.end_datetime) } : {}),
|
|
489
|
+
}, env);
|
|
490
|
+
const applied = await getMeetingOperation('meeting.eb.minute.create.apply').handler({ confirm: true, continuation: prepared?.data?.continuation }, env);
|
|
491
|
+
const minuteId = textOf((applied?.data?.dataIds ?? [])[0]);
|
|
492
|
+
if (!minuteId) {
|
|
493
|
+
throw meetingValidationError('minute_create_failed', '纪要创建未返回 id', { applied: applied?.data ?? null });
|
|
494
|
+
}
|
|
495
|
+
mark('createMinute', stepStart);
|
|
496
|
+
const result = {
|
|
497
|
+
id: mtId,
|
|
498
|
+
groupId,
|
|
499
|
+
minuteId,
|
|
500
|
+
subject,
|
|
501
|
+
messageCount: messages.length,
|
|
502
|
+
keptMessages: kept,
|
|
503
|
+
ignoredSystem,
|
|
504
|
+
sampled,
|
|
505
|
+
sampledDropped,
|
|
506
|
+
digestMode,
|
|
507
|
+
digestDropped,
|
|
508
|
+
digestTruncated,
|
|
509
|
+
fullTranscriptPath,
|
|
510
|
+
transcriptLength: transcript.length,
|
|
511
|
+
fullTranscriptLength: fullTranscript.length,
|
|
512
|
+
pipeline,
|
|
513
|
+
aiPageId: aiPageId || null,
|
|
514
|
+
};
|
|
515
|
+
if (pipeline === 'text') {
|
|
516
|
+
result.timings = { ...timings, total: Date.now() - startedAt };
|
|
517
|
+
return meetingSuccess('meeting.minute.fromGroupChat', result, warnings.length > 0 ? { warnings } : undefined);
|
|
518
|
+
}
|
|
519
|
+
// ⑦ 可选:ESB 633 生成纪要文案 / 628 生成文档
|
|
520
|
+
stepStart = Date.now();
|
|
521
|
+
const session = await env.host.getSession();
|
|
522
|
+
const employeeId = textOf(args.employeeId).trim() || textOf(session?.userId);
|
|
523
|
+
// 流程 id 是租户级配置:优先用入参覆盖,否则用内置默认值(参考租户)
|
|
524
|
+
const flowMinutes = textOf(args.flowGenerateMinutes).trim() || FLOW_GENERATE_MINUTES;
|
|
525
|
+
const flowDocument = textOf(args.flowGenerateDocument).trim() || FLOW_GENERATE_DOCUMENT;
|
|
526
|
+
if (!employeeId) {
|
|
527
|
+
warnings.push('未取到 ESB 触发人 id(employeeId),已跳过后续 ESB 调用');
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
const { createEsbHost } = await import('../../esb/host.js');
|
|
531
|
+
const { createEsbContinuationSigner } = await import('../../esb/continuation.js');
|
|
532
|
+
const { getEsbOperation } = await import('../../esb/operations/registry.js');
|
|
533
|
+
const esbEnv = { ctx, host: createEsbHost(ctx), continuation: createEsbContinuationSigner(ctx) };
|
|
534
|
+
const prepareOp = getEsbOperation('esb.trigger.prepare');
|
|
535
|
+
const applyOp = getEsbOperation('esb.trigger.apply');
|
|
536
|
+
const trigger = async (flowId, mainTable) => {
|
|
537
|
+
const pre = await prepareOp.handler({ employeeId, esbFlowId: flowId, customParams: { mainTable } }, esbEnv);
|
|
538
|
+
return applyOp.handler({ continuation: pre?.data?.continuation, confirm: true }, esbEnv);
|
|
539
|
+
};
|
|
540
|
+
try {
|
|
541
|
+
await trigger(flowMinutes, { minuteId });
|
|
542
|
+
mark('esbMinuteText', stepStart);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
// ESB 是同步阻塞的,超时 ≠ 没执行;如实记录,稍后以回读结果为准
|
|
546
|
+
warnings.push(`ESB 生成纪要文案未正常返回(${String(error?.message ?? error).slice(0, 100)});已按回读结果继续`);
|
|
547
|
+
}
|
|
548
|
+
if (pipeline === 'document') {
|
|
549
|
+
const stepDocStart = Date.now();
|
|
550
|
+
const detail = await getMeetingOperation('meeting.eb.minute.detail').handler({ id: minuteId }, env);
|
|
551
|
+
const parseContent = textOf(detail?.data?.data?.parse_content);
|
|
552
|
+
if (!parseContent.trim()) {
|
|
553
|
+
warnings.push('纪要文案为空,已跳过生成文档(文本生成可能仍在后台进行,可稍后用 meeting.minute.parseStatus 复查)');
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
try {
|
|
557
|
+
await trigger(flowDocument, { parse_content: parseContent, mt_minute_id: minuteId });
|
|
558
|
+
mark('esbDocument', stepDocStart);
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
warnings.push(`ESB 生成文档未正常返回(${String(error?.message ?? error).slice(0, 100)})`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// ⑧ 回读最终状态(以服务端为准)
|
|
567
|
+
const final = await getMeetingOperation('meeting.eb.minute.detail').handler({ id: minuteId }, env);
|
|
568
|
+
const row = final?.data?.data ?? {};
|
|
569
|
+
result.finalStatus = textOf(row.minutes_status);
|
|
570
|
+
result.parseContentLength = textOf(row.parse_content).length;
|
|
571
|
+
result.documentId = textOf(row.mt_minutes) || null;
|
|
572
|
+
result.timings = { ...timings, total: Date.now() - startedAt };
|
|
573
|
+
return meetingSuccess('meeting.minute.fromGroupChat', result, warnings.length > 0 ? { warnings } : undefined);
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
export const meetingMinuteFromGroupChatOperations = [meetingMinuteFromGroupChat];
|