weaver-work-cli 0.1.29 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/archive-U766FTFH.js +2537 -0
- package/dist/chunks/archive-oa-DMWEKX3L.js +1317 -0
- package/dist/chunks/asset-TCR7VDEV.js +2109 -0
- package/dist/chunks/attachment-content-AQNX5N3S.js +23 -0
- package/dist/chunks/blog-EVZWGDVM.js +2157 -0
- package/dist/chunks/caizhilian-JOWS6SL5.js +10087 -0
- package/dist/chunks/calendar-5WNCV66Q.js +1195 -0
- package/dist/chunks/chunk-2DUV44UX.js +15606 -0
- package/dist/chunks/chunk-324YPTC2.js +1607 -0
- package/dist/chunks/chunk-65QMVWHS.js +231 -0
- package/dist/chunks/chunk-6V7ZLF34.js +387 -0
- package/dist/chunks/chunk-AC7KJWC5.js +358 -0
- package/dist/chunks/chunk-AJ62BGAW.js +29 -0
- package/dist/chunks/chunk-BUSC6UAE.js +43 -0
- package/dist/chunks/chunk-CVZ2JOSW.js +62 -0
- package/dist/chunks/chunk-D4LTJSJD.js +477 -0
- package/dist/chunks/chunk-EG6ZU444.js +654 -0
- package/dist/chunks/chunk-EZALDOQH.js +383 -0
- package/dist/chunks/chunk-FMPJJYYI.js +106 -0
- package/dist/chunks/chunk-G2JSIUWF.js +769 -0
- package/dist/chunks/chunk-GCG2NMWR.js +703 -0
- package/dist/chunks/chunk-GPMFD4RD.js +75 -0
- package/dist/chunks/chunk-H3WDBYJW.js +508 -0
- package/dist/chunks/chunk-H6XSTSLM.js +2357 -0
- package/dist/chunks/chunk-J7RJTZSV.js +5556 -0
- package/dist/chunks/chunk-LUSPKZKY.js +53 -0
- package/dist/chunks/chunk-NIAJ5ZCG.js +73 -0
- package/dist/chunks/chunk-NOODWMCL.js +584 -0
- package/dist/chunks/chunk-OOUTBLZ4.js +51 -0
- package/dist/chunks/chunk-P26ZWSKU.js +73 -0
- package/dist/chunks/chunk-QWY6PGL6.js +3472 -0
- package/dist/chunks/chunk-R2IJAK7T.js +464 -0
- package/dist/chunks/chunk-R7O5PUWO.js +205 -0
- package/dist/chunks/chunk-RCGYCOMG.js +6962 -0
- package/dist/chunks/chunk-VZTXRG6X.js +403 -0
- package/dist/chunks/chunk-WHD4XMRP.js +161 -0
- package/dist/chunks/chunk-YLCYX7LE.js +371 -0
- package/dist/chunks/chunk-ZEDWSH32.js +68 -0
- package/dist/chunks/continuation-2CKSBLA6.js +11 -0
- package/dist/chunks/continuation-HZIZRZBX.js +13 -0
- package/dist/chunks/continuation-NLECEJPX.js +11 -0
- package/dist/chunks/ebuilder-form-NJ4YK6CC.js +2276 -0
- package/dist/chunks/ehr-MMYN36VA.js +5241 -0
- package/dist/chunks/employee-SAK2QYXX.js +11 -0
- package/dist/chunks/esb-HIWHOU7Q.js +141 -0
- package/dist/chunks/fna-KBACQTA5.js +1444 -0
- package/dist/chunks/host-3OHS5QUX.js +21 -0
- package/dist/chunks/host-IG6KEH5Q.js +19 -0
- package/dist/chunks/host-KQL5N4HO.js +23 -0
- package/dist/chunks/host-LLSZQCWR.js +17 -0
- package/dist/chunks/hrm-LYEC67ZQ.js +244 -0
- package/dist/chunks/im-RN2UKAEU.js +252 -0
- package/dist/chunks/invoice-EEPHLAF6.js +5179 -0
- package/dist/chunks/jiuchuanhui-WVVZYSJE.js +1940 -0
- package/dist/chunks/lazy-dispatch-MDOFFL2S.js +28 -0
- package/dist/chunks/mail-LNQLTF6L.js +2695 -0
- package/dist/chunks/meeting-XSD3BDVQ.js +504 -0
- package/dist/chunks/msg-parse-4GSQ2WTO.js +41 -0
- package/dist/chunks/okr-TMWTIU2K.js +2482 -0
- package/dist/chunks/plan-A2FSFA7T.js +2025 -0
- package/dist/chunks/project-T26QLBQF.js +1361 -0
- package/dist/chunks/registry-2VLUAESA.js +14 -0
- package/dist/chunks/registry-JCFEYXOE.js +18 -0
- package/dist/chunks/registry-NYPYMZXG.js +16 -0
- package/dist/chunks/registry-Q7OQJ3SU.js +19 -0
- package/dist/chunks/registry-ZJMJVWFA.js +19 -0
- package/dist/chunks/room-recommend-UR4ZDZS6.js +20 -0
- package/dist/chunks/schedule-5L2PM4M6.js +24 -0
- package/dist/chunks/state-Y2XBP6YE.js +15 -0
- package/dist/chunks/workflow-TR6PG2SL.js +245 -0
- package/dist/cmd/argv.js +45 -0
- package/dist/cmd/docs/index.js +3 -3
- package/dist/cmd/lazy-dispatch.js +20 -0
- package/dist/cmd/root.js +2 -4
- package/dist/cmd/setup/index.js +2 -2
- package/dist/cmd/skills/index.js +2 -2
- package/dist/core/package-paths.js +47 -0
- package/dist/index.js +163 -7
- package/dist/internal/e10/auth/xiaoe.js +3 -3
- package/dist/internal/e10/request-runtime.js +18 -62
- package/dist/internal/skills/detector.js +2 -2
- package/dist/shortcuts/contract/data/performance-catalog.js +1306 -0
- package/dist/shortcuts/contract/host.js +91 -35
- package/dist/shortcuts/contract/index.js +213 -0
- package/dist/shortcuts/contract/manifest.js +928 -0
- package/dist/shortcuts/contract/ooxml/docx.js +295 -0
- package/dist/shortcuts/contract/ooxml/xlsx.js +221 -0
- package/dist/shortcuts/contract/ooxml/zip.js +158 -0
- package/dist/shortcuts/contract/operations/assistant.js +1156 -0
- package/dist/shortcuts/contract/operations/clause-library.js +522 -0
- package/dist/shortcuts/contract/operations/compare.js +193 -0
- package/dist/shortcuts/contract/operations/credit-page.js +34 -0
- package/dist/shortcuts/contract/operations/flow.js +98 -0
- package/dist/shortcuts/contract/operations/page.js +66 -0
- package/dist/shortcuts/contract/operations/performance.js +733 -0
- package/dist/shortcuts/contract/operations/quote.js +557 -0
- package/dist/shortcuts/contract/operations/recommend.js +78 -0
- package/dist/shortcuts/contract/operations/registry.js +18 -0
- package/dist/shortcuts/contract/operations/review.js +83 -0
- package/dist/shortcuts/contract/operations/shared.js +37 -0
- package/dist/shortcuts/contract/operations/upload.js +108 -0
- package/dist/shortcuts/contract/operations/write.js +3 -2
- package/dist/shortcuts/index.js +140 -40
- package/dist/shortcuts/invoice/manifest.js +34 -11
- package/dist/shortcuts/invoice/operations/reim-auto.js +9 -0
- package/dist/shortcuts/invoice/operations/reim.js +81 -5
- package/dist/shortcuts/invoice/operations/shared.js +12 -5
- package/dist/shortcuts/meeting/host.js +45 -3
- package/dist/shortcuts/meeting/invitation/docx.js +442 -0
- package/dist/shortcuts/meeting/operations/attachment-content.js +418 -0
- package/dist/shortcuts/meeting/operations/attachment.js +442 -0
- package/dist/shortcuts/meeting/operations/eb/cache.js +57 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +343 -28
- package/dist/shortcuts/meeting/operations/eb/conflict.js +372 -11
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +710 -0
- package/dist/shortcuts/meeting/operations/eb/decision.js +47 -1
- package/dist/shortcuts/meeting/operations/eb/factory.js +129 -11
- package/dist/shortcuts/meeting/operations/eb/index.js +122 -2
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +378 -0
- package/dist/shortcuts/meeting/operations/eb/room-summary.js +84 -0
- package/dist/shortcuts/meeting/operations/eb/schedule.js +450 -0
- package/dist/shortcuts/meeting/operations/from-group-chat.js +576 -0
- package/dist/shortcuts/meeting/operations/invitation.js +458 -0
- package/dist/shortcuts/meeting/operations/minute-content.js +266 -0
- package/dist/shortcuts/meeting/operations/pipeline.js +489 -0
- package/dist/shortcuts/meeting/operations/qrcode.js +131 -0
- package/dist/shortcuts/meeting/operations/receipts.js +1 -1
- package/dist/shortcuts/meeting/operations/registry.js +18 -0
- package/dist/shortcuts/meeting/operations/summary.js +276 -0
- package/dist/shortcuts/meeting/operations/task-notify.js +325 -0
- package/dist/shortcuts/meeting/operations.js +42 -1
- package/dist/shortcuts/meeting/qr/encoder.js +427 -0
- package/dist/shortcuts/meeting/qr/render.js +103 -0
- package/dist/shortcuts/workflow/operations/form.js +17 -5
- package/docs/contract.md +39 -4
- package/docs/invoice.md +5 -5
- package/package.json +3 -2
- package/scripts/bundle.mjs +63 -0
- package/skill-template/business-info.json +25 -4
- package/skill-template/domains/jinchengda.md +16 -3
- package/skill-template/domains/yepiaotong.md +1 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +111 -31
- package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +7 -13
- package/skills/weaver-e10-jinchengda/references/contract-ai-assistant.md +150 -0
- package/skills/weaver-e10-jinchengda/references/contract-ai-review.md +87 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-library-crud.md +111 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +0 -13
- package/skills/weaver-e10-jinchengda/references/contract-credit-page.md +50 -0
- package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +4 -2
- package/skills/weaver-e10-jinchengda/references/contract-doc-compare.md +74 -0
- package/skills/weaver-e10-jinchengda/references/contract-flow-launch.md +84 -0
- package/skills/weaver-e10-jinchengda/references/contract-performance-query.md +29 -0
- package/skills/weaver-e10-jinchengda/references/contract-quote-to-contract.md +28 -0
- package/skills/weaver-e10-jinchengda/references/contract-request-status.md +2 -21
- package/skills/weaver-e10-jinchengda/references/contract-request-write.md +0 -24
- package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +0 -17
- package/skills/weaver-e10-jinchengda/references/contract-template-recommend.md +70 -0
- package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +0 -15
- package/skills/weaver-e10-jinchengda/references/source-manifest.json +1168 -152
- package/skills/weaver-e10-meeting/SKILL.md +493 -17
- package/skills/weaver-e10-meeting/references/meeting-change.md +4 -4
- package/skills/weaver-e10-meeting/references/meeting-create.md +6 -6
- package/skills/weaver-e10-meeting/references/meeting-eb.md +503 -27
- package/skills/weaver-e10-meeting/references/meeting-invitation.md +113 -0
- package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +6 -6
- package/skills/weaver-e10-meeting/references/meeting-minute-file.md +425 -0
- package/skills/weaver-e10-meeting/references/meeting-queries.md +4 -4
- package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +15 -15
- package/skills/weaver-e10-meeting/references/meeting-verification.md +19 -11
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-yepiaotong/SKILL.md +9 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +1 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +6 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +14 -11
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +97 -56
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +84 -60
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +142 -105
- package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +53 -194
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +164 -72
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +6 -0
- package/templates/meeting/invitation.docx +0 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一条命令跑完:群聊 → 纪要 → 文档 → 决议任务 → 推回群聊。
|
|
3
|
+
*
|
|
4
|
+
* 背景(2026-10-27 用户确认):前面四段各自验证通过,但小程序里要调 3~4 次、
|
|
5
|
+
* 每次多 1.7 秒冷启动,串起来才好用。
|
|
6
|
+
*
|
|
7
|
+
* 设计原则:
|
|
8
|
+
* - **默认跑到文档为止**(`level` 默认 `document`);`tasks` / `notify` 属写操作,
|
|
9
|
+
* 必须显式 `confirm:true` 才执行 —— 不带 confirm 时给出"将要创建的任务 + 将要发出的
|
|
10
|
+
* 群消息"预览,由调用方给用户确认后再跑第二次。
|
|
11
|
+
* - **不重复建决议**:会议已有任务时默认跳过(`skipExistingTasks` 默认 true),
|
|
12
|
+
* 否则每次重跑都会再糊一层任务上去(这个坑我在测试环境踩过)。
|
|
13
|
+
* - 群消息内容用**本次刚创建的任务**拼,不去回查服务端 —— ESL 联动生成任务有延迟,
|
|
14
|
+
* 回查可能拿到空列表,导致发出"没有任务"的通知。
|
|
15
|
+
*/
|
|
16
|
+
import { meetingValidationError } from '../errors.js';
|
|
17
|
+
import { meetingSuccess } from './types.js';
|
|
18
|
+
import { formatTaskMessage } from './task-notify.js';
|
|
19
|
+
function textOf(value) {
|
|
20
|
+
return value === undefined || value === null ? '' : String(value);
|
|
21
|
+
}
|
|
22
|
+
const LEVELS = ['text', 'minutes', 'document', 'tasks', 'notify'];
|
|
23
|
+
/** 从纪要原文里提取「@某人 负责 xxx」式分工。 */
|
|
24
|
+
export function extractAssignments(transcript) {
|
|
25
|
+
const found = [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
for (const rawLine of transcript.split('\n')) {
|
|
28
|
+
const line = rawLine.trim();
|
|
29
|
+
if (!line || line.startsWith('【'))
|
|
30
|
+
continue; // 摘要表头不是发言
|
|
31
|
+
const colon = line.indexOf(':');
|
|
32
|
+
if (colon < 0)
|
|
33
|
+
continue;
|
|
34
|
+
const body = line.slice(colon + 1).trimStart();
|
|
35
|
+
// 跳过我们自己推送到群里的通知/摘要(内容以【开头),否则会把"测试(@王五)"
|
|
36
|
+
// 这类转述又提取一遍,凭空多出任务
|
|
37
|
+
if (body.startsWith('【'))
|
|
38
|
+
continue;
|
|
39
|
+
if (!body.includes('负责'))
|
|
40
|
+
continue;
|
|
41
|
+
// ⚠️ `@` 必须处在词首(行首/空白/中文标点之后):
|
|
42
|
+
// 否则 `yanfa3@163.com` 里的 `@163.com` 会被当成"@了 163.com 这个人"。
|
|
43
|
+
// 账号本身允许含 `@` 与 `.`(邮箱式账号 `caiwu2@163.com`),所以不能按 `@` 截断。
|
|
44
|
+
const mentions = [...body.matchAll(/(^|[\s,,。;;、::((])[@@]([^\s,,。;;、::()()]+)/gu)].map((match) => {
|
|
45
|
+
const at = (match.index ?? 0) + match[1].length;
|
|
46
|
+
const account = match[2].replace(/[))】\],,。;;、::]+$/u, '');
|
|
47
|
+
return { at, account, end: at + 1 + match[2].length };
|
|
48
|
+
}).filter((mention) => mention.account.length > 0);
|
|
49
|
+
// 没有 @ 的写法也要能抽:`卜伟领:负责竞品梳理,周五前给初稿`
|
|
50
|
+
// (用户提供的会议材料/纪要文件里常见这种句式,不一定带 @)。
|
|
51
|
+
if (mentions.length === 0) {
|
|
52
|
+
// 「负责人是 xxx」是描述,不是分工;带 @ 的行交给上面的提及逻辑,这里不重复处理
|
|
53
|
+
if (!/^负责(?!人)/u.test(body))
|
|
54
|
+
continue;
|
|
55
|
+
if (/[@@]/u.test(body))
|
|
56
|
+
continue;
|
|
57
|
+
const speaker = line.slice(0, colon).trim().split(/\s+/u).pop() ?? '';
|
|
58
|
+
if (!speaker)
|
|
59
|
+
continue;
|
|
60
|
+
const title = body
|
|
61
|
+
.slice('负责'.length)
|
|
62
|
+
.replace(/^[\s::,,、-]+/u, '')
|
|
63
|
+
.replace(/[。;;,,、\s]+$/u, '')
|
|
64
|
+
.trim()
|
|
65
|
+
.slice(0, 60);
|
|
66
|
+
if (!title)
|
|
67
|
+
continue;
|
|
68
|
+
const key = `${speaker}|${title}`;
|
|
69
|
+
if (seen.has(key))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(key);
|
|
72
|
+
found.push({ account: speaker, title });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
mentions.forEach((mention, index) => {
|
|
76
|
+
const segmentEnd = index + 1 < mentions.length ? mentions[index + 1].at : body.length;
|
|
77
|
+
const segment = body.slice(mention.end, segmentEnd);
|
|
78
|
+
// 「负责」归属于它前面那个人;隔着别人的 @ 就说明不是这句的主语
|
|
79
|
+
if (!segment.includes('负责'))
|
|
80
|
+
return;
|
|
81
|
+
const after = segment.slice(segment.indexOf('负责') + '负责'.length);
|
|
82
|
+
const title = after.replace(/^[\s::,,、-]+/u, '').replace(/[。;;,,、\s]+$/u, '').trim().slice(0, 60);
|
|
83
|
+
if (!title)
|
|
84
|
+
return;
|
|
85
|
+
const key = `${mention.account}|${title}`;
|
|
86
|
+
if (seen.has(key))
|
|
87
|
+
return;
|
|
88
|
+
seen.add(key);
|
|
89
|
+
found.push({ account: mention.account, title });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
/** 计划截止时间:从起点往后 N 天的 18:00。 */
|
|
95
|
+
export function defaultDueDate(begin, days) {
|
|
96
|
+
const trimmed = begin.trim();
|
|
97
|
+
// ⚠️ 只给日期时不能拼 `:00`(会变成 `2026-10-30:00`,解析成 Invalid Date 后静默退回今天)
|
|
98
|
+
const normalized = /^\d{4}-\d{2}-\d{2}$/u.test(trimmed)
|
|
99
|
+
? `${trimmed} 00:00:00`
|
|
100
|
+
: trimmed.length <= 16
|
|
101
|
+
? `${trimmed}:00`
|
|
102
|
+
: trimmed;
|
|
103
|
+
const base = new Date(normalized.replace(/-/g, '/'));
|
|
104
|
+
const start = Number.isNaN(base.getTime()) ? new Date() : base;
|
|
105
|
+
const due = new Date(start.getTime() + Math.max(days, 1) * 24 * 3600_000);
|
|
106
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
107
|
+
return `${due.getFullYear()}-${pad(due.getMonth() + 1)}-${pad(due.getDate())} 18:00`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 等「决议任务」被 ESB 联动生成出来。任务数涨了才算成功。
|
|
111
|
+
* 抽出来是为了能单测 —— 这段等待直接决定群消息内容对不对,不能只靠"看着像通了"。
|
|
112
|
+
*/
|
|
113
|
+
export async function waitForTaskIncrease(fetchTotal, baseline, timeoutMs = 15_000, intervalMs = 2_000) {
|
|
114
|
+
const start = Date.now();
|
|
115
|
+
let total = baseline;
|
|
116
|
+
while (Date.now() - start < timeoutMs) {
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
118
|
+
total = await fetchTotal();
|
|
119
|
+
if (total > baseline)
|
|
120
|
+
return { total, waitedMs: Date.now() - start, increased: true };
|
|
121
|
+
}
|
|
122
|
+
return { total, waitedMs: Date.now() - start, increased: false };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 决定"要建哪些任务"。
|
|
126
|
+
* 主路径是调用方(智能体)读原文后自己分析出的 `decisions` —— 任务该由模型按语义理解;
|
|
127
|
+
* 关键词匹配只是没传 decisions 时的兜底,覆盖不到「我来出方案」「先请 @某人 梳理」这类句式。
|
|
128
|
+
*/
|
|
129
|
+
export function resolveTaskAssignments(decisions, transcript) {
|
|
130
|
+
const provided = Array.isArray(decisions) ? decisions : [];
|
|
131
|
+
const providedAssignments = provided
|
|
132
|
+
.map((item) => ({
|
|
133
|
+
account: textOf(item?.executor).trim(),
|
|
134
|
+
title: textOf(item?.title).trim(),
|
|
135
|
+
}))
|
|
136
|
+
.filter((item) => item.account !== '' && item.title !== '');
|
|
137
|
+
if (providedAssignments.length > 0)
|
|
138
|
+
return { assignments: providedAssignments, source: 'agent' };
|
|
139
|
+
return {
|
|
140
|
+
assignments: extractAssignments(transcript),
|
|
141
|
+
source: 'keyword',
|
|
142
|
+
...(provided.length > 0
|
|
143
|
+
? { warning: '传入的 decisions 没有可用条目(executor/title 为空),已回退到关键词兜底' }
|
|
144
|
+
: {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const meetingPipelineRun = {
|
|
148
|
+
name: 'meeting.pipeline.run',
|
|
149
|
+
resource: 'pipeline',
|
|
150
|
+
method: 'run',
|
|
151
|
+
action: 'meeting.pipeline.run',
|
|
152
|
+
summary: '一条命令从群聊跑到任务:群消息→纪要原文→纪要文案(ESB633)→文档(ESB628)→提交决议(ESB 自动生成任务)→推送群聊;提交决议/发群需 confirm:true',
|
|
153
|
+
capability: 'pipeline',
|
|
154
|
+
risk: 'high-risk-write',
|
|
155
|
+
requiresConfirmation: true,
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
properties: {
|
|
160
|
+
mt_id: { type: 'string', pattern: '^\\d+$', description: '会议 id(必填)' },
|
|
161
|
+
level: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
enum: [...LEVELS],
|
|
164
|
+
description: 'text=只出原文;minutes=生成纪要文案;document=再生成文档(默认);'
|
|
165
|
+
+ 'tasks=再提交决议(决议任务由 ESB 自动生成,CLI 不手工建);notify=再把任务推到群聊。'
|
|
166
|
+
+ 'tasks/notify 需要 confirm:true',
|
|
167
|
+
},
|
|
168
|
+
confirm: {
|
|
169
|
+
type: 'boolean',
|
|
170
|
+
description: '执行 tasks/notify(建任务、发群消息)必须显式 true;不传则只跑到文档并给出预览',
|
|
171
|
+
},
|
|
172
|
+
skipExistingTasks: {
|
|
173
|
+
type: 'boolean',
|
|
174
|
+
description: '会议已有决议任务时跳过创建(默认 true),避免重复叠加',
|
|
175
|
+
},
|
|
176
|
+
taskDueDays: { type: 'integer', minimum: 1, maximum: 90, description: '任务计划截止 = 会议结束 + N 天(默认 7)' },
|
|
177
|
+
subject: { type: 'string', description: '纪要标题,默认「群聊纪要-<会议名>」' },
|
|
178
|
+
minuteId: {
|
|
179
|
+
type: 'string',
|
|
180
|
+
pattern: '^\\d+$',
|
|
181
|
+
description: '复用已有纪要 id(跳过「群聊→纪要」这一段)。预览后确认时传回,避免重复建纪要',
|
|
182
|
+
},
|
|
183
|
+
files: {
|
|
184
|
+
type: 'array',
|
|
185
|
+
items: { type: 'string' },
|
|
186
|
+
description: '用户提供的会议材料本地路径(可多个);只要会议有 imchat_groupid,群聊仍会自动并入',
|
|
187
|
+
},
|
|
188
|
+
content: { type: 'string', description: '用户直接给的纪要正文/要点,与 files 可同时给' },
|
|
189
|
+
decisions: {
|
|
190
|
+
type: 'array',
|
|
191
|
+
description: '**主路径**:调用方(智能体)自己读完纪要原文后分析出的任务。'
|
|
192
|
+
+ '给了就以它为准,不再走关键词兜底 —— 任务该由模型理解语义,不该靠正则猜',
|
|
193
|
+
items: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
additionalProperties: false,
|
|
196
|
+
properties: {
|
|
197
|
+
executor: { type: 'string', description: '执行人姓名或账号(HRM 查得到即可)' },
|
|
198
|
+
title: { type: 'string', description: '任务标题' },
|
|
199
|
+
},
|
|
200
|
+
required: ['executor', 'title'],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
groupId: { type: 'string', description: '群 id;省略时取会议 imchat_groupid' },
|
|
204
|
+
onlyPending: { type: 'boolean', description: '推送时只列未完成任务(默认 true)' },
|
|
205
|
+
aiPageId: { type: 'string', description: '智能纪要页面 id,省略时自动反查' },
|
|
206
|
+
employeeId: { type: 'string', description: 'ESB 触发人 id,省略时用当前登录人' },
|
|
207
|
+
},
|
|
208
|
+
required: ['mt_id'],
|
|
209
|
+
},
|
|
210
|
+
handler: async (args, env) => {
|
|
211
|
+
const startedAt = Date.now();
|
|
212
|
+
const timings = {};
|
|
213
|
+
const warnings = [];
|
|
214
|
+
const mtId = textOf(args.mt_id).trim();
|
|
215
|
+
if (!mtId)
|
|
216
|
+
throw meetingValidationError('mt_id_required', '必须提供会议 id(mt_id)');
|
|
217
|
+
const level = LEVELS.includes(String(args.level))
|
|
218
|
+
? String(args.level)
|
|
219
|
+
: 'document';
|
|
220
|
+
const needsConfirm = level === 'tasks' || level === 'notify';
|
|
221
|
+
const confirmed = args.confirm === true;
|
|
222
|
+
// 未确认时只跑到文档,把"要写什么"先摆出来
|
|
223
|
+
const executedLevel = needsConfirm && !confirmed ? 'document' : level;
|
|
224
|
+
const { getMeetingOperation } = await import('./registry.js');
|
|
225
|
+
const ctx = env.ctx;
|
|
226
|
+
// ① 群聊 → 纪要(复用已固化的操作,含摘要版与时间窗口径)
|
|
227
|
+
// 传 minuteId 可复用已有纪要:预览→确认的两步走时,避免每次都新建一条纪要(每次 20~30 秒)
|
|
228
|
+
let stepStart = Date.now();
|
|
229
|
+
const reuseMinuteId = textOf(args.minuteId).trim();
|
|
230
|
+
let minuteId = '';
|
|
231
|
+
if (reuseMinuteId) {
|
|
232
|
+
minuteId = reuseMinuteId;
|
|
233
|
+
timings.minute = 0;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const upstreamPipeline = executedLevel === 'text' ? 'text' : executedLevel === 'minutes' ? 'minutes' : 'document';
|
|
237
|
+
const minuteResult = await getMeetingOperation('meeting.minute.fromGroupChat').handler({
|
|
238
|
+
mt_id: mtId,
|
|
239
|
+
pipeline: upstreamPipeline,
|
|
240
|
+
...(args.subject ? { subject: String(args.subject) } : {}),
|
|
241
|
+
...(args.groupId ? { groupId: String(args.groupId) } : {}),
|
|
242
|
+
// 用户提供的材料也要一起进纪要(会议有 imchat_groupid 时群聊照样自动并入)
|
|
243
|
+
...(Array.isArray(args.files) ? { files: args.files } : {}),
|
|
244
|
+
...(args.content ? { content: String(args.content) } : {}),
|
|
245
|
+
...(args.aiPageId ? { aiPageId: String(args.aiPageId) } : {}),
|
|
246
|
+
...(args.employeeId ? { employeeId: String(args.employeeId) } : {}),
|
|
247
|
+
}, env);
|
|
248
|
+
warnings.push(...(minuteResult?.warnings ?? []));
|
|
249
|
+
const minuteData = minuteResult?.data ?? {};
|
|
250
|
+
minuteId = textOf(minuteData.minuteId);
|
|
251
|
+
timings.minute = Date.now() - stepStart;
|
|
252
|
+
}
|
|
253
|
+
if (!minuteId) {
|
|
254
|
+
return meetingSuccess('meeting.pipeline.run', { id: mtId, level, executedLevel, minuteId: null, timings }, { warnings: [...warnings, '未能创建纪要,流程中止(详见 minute 阶段返回)'] });
|
|
255
|
+
}
|
|
256
|
+
// ② 回读原文(任务提取要用,且以服务端为准)
|
|
257
|
+
stepStart = Date.now();
|
|
258
|
+
const detail = await getMeetingOperation('meeting.eb.minute.detail').handler({ id: minuteId }, env);
|
|
259
|
+
const minute = detail?.data?.data ?? {};
|
|
260
|
+
const transcript = textOf(minute.minutes_content);
|
|
261
|
+
const parseContent = textOf(minute.parse_content);
|
|
262
|
+
timings.readMinute = Date.now() - stepStart;
|
|
263
|
+
const base = {
|
|
264
|
+
id: mtId,
|
|
265
|
+
level,
|
|
266
|
+
executedLevel,
|
|
267
|
+
minuteId,
|
|
268
|
+
transcriptLength: transcript.length,
|
|
269
|
+
parseContentLength: parseContent.length,
|
|
270
|
+
documentId: textOf(minute.mt_minutes) || null,
|
|
271
|
+
finalStatus: textOf(minute.minutes_status),
|
|
272
|
+
};
|
|
273
|
+
// ③ 目标里没有 tasks/notify 就到此为止
|
|
274
|
+
// ⚠️ 这里必须看**原始 level**,不能看 executedLevel —— 未确认时 executedLevel 被降级成
|
|
275
|
+
// `document`,若按它判断就会在给出预览之前直接返回(曾踩过)。
|
|
276
|
+
if (level !== 'tasks' && level !== 'notify')
|
|
277
|
+
return finish(base);
|
|
278
|
+
// ④ 确定分工
|
|
279
|
+
// 主路径是**调用方(智能体)自己读原文分析后传进来的 decisions**:任务该由模型按语义理解,
|
|
280
|
+
// 关键词匹配只是没传 decisions 时的兜底(覆盖不到「我来出方案」「先请 @某人 梳理」这类句式)。
|
|
281
|
+
stepStart = Date.now();
|
|
282
|
+
const decided = resolveTaskAssignments(args.decisions, transcript);
|
|
283
|
+
const assignments = decided.assignments;
|
|
284
|
+
const taskSource = decided.source;
|
|
285
|
+
timings.extractTasks = Date.now() - stepStart;
|
|
286
|
+
if (decided.warning)
|
|
287
|
+
warnings.push(decided.warning);
|
|
288
|
+
if (assignments.length === 0) {
|
|
289
|
+
return finish({
|
|
290
|
+
...base,
|
|
291
|
+
tasks: { source: taskSource, extracted: 0, resolved: 0, created: false, unresolved: [] },
|
|
292
|
+
notify: null,
|
|
293
|
+
}, [
|
|
294
|
+
'没有可提交的任务,未创建。关键词兜底没识别到「负责」式分工 —— '
|
|
295
|
+
+ '**推荐由智能体读 minutes_content 自己分析,用 decisions 参数传进来**(能覆盖「我来出方案」「先请 @某人 梳理」等句式)',
|
|
296
|
+
]);
|
|
297
|
+
}
|
|
298
|
+
// ⑤ 账号 → HRM 员工 id(邮箱式账号必须用 username 查,email/keyword 查不到)
|
|
299
|
+
stepStart = Date.now();
|
|
300
|
+
const { createHrmHost } = await import('../../hrm/host.js');
|
|
301
|
+
const { getHrmOperation } = await import('../../hrm/operations/registry.js');
|
|
302
|
+
const hrmEnv = { ctx, host: createHrmHost(ctx) };
|
|
303
|
+
const queryOp = getHrmOperation('hrm.employee.query');
|
|
304
|
+
const resolved = [];
|
|
305
|
+
const unresolved = [];
|
|
306
|
+
for (const item of assignments) {
|
|
307
|
+
let person = null;
|
|
308
|
+
for (const params of [{ username: item.account }, { keyword: item.account }]) {
|
|
309
|
+
try {
|
|
310
|
+
const found = await queryOp.handler({ ...params, pageSize: 5 }, hrmEnv);
|
|
311
|
+
const row = (found?.data?.rows ?? [])[0];
|
|
312
|
+
if (row?.id) {
|
|
313
|
+
person = row;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// 查不到就换下一种方式,不中断
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (person) {
|
|
322
|
+
resolved.push({ account: item.account, title: item.title, id: textOf(person.id), name: textOf(person.name) || item.account });
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
unresolved.push(item.account);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
timings.resolveExecutors = Date.now() - stepStart;
|
|
329
|
+
if (unresolved.length > 0) {
|
|
330
|
+
warnings.push(`以下执行人在 HRM 里没查到,已跳过对应任务(不猜人):${[...new Set(unresolved)].join('、')}`);
|
|
331
|
+
}
|
|
332
|
+
// 任务行(本地构造,供预览与推送共用)
|
|
333
|
+
const meetingInfo = await (async () => {
|
|
334
|
+
const listed = await getMeetingOperation('meeting.eb.meeting.list').handler({ mt_id: mtId, pageNo: 1, pageSize: 1 }, env);
|
|
335
|
+
return (listed?.data?.rows ?? [])[0]?.mainTable ?? {};
|
|
336
|
+
})();
|
|
337
|
+
const beginAt = textOf(meetingInfo.end_datetime).trim()
|
|
338
|
+
|| textOf(meetingInfo.begin_datetime).trim()
|
|
339
|
+
|| (() => {
|
|
340
|
+
const now = new Date();
|
|
341
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
342
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
343
|
+
})();
|
|
344
|
+
const dueDays = typeof args.taskDueDays === 'number' && args.taskDueDays > 0 ? args.taskDueDays : 7;
|
|
345
|
+
const dueAt = defaultDueDate(beginAt, dueDays);
|
|
346
|
+
const plannedRows = resolved.map((item) => ({
|
|
347
|
+
decision_title: item.title,
|
|
348
|
+
executors: [{ name: item.name, id: item.id }],
|
|
349
|
+
begin_datetime: beginAt,
|
|
350
|
+
end_datetime: dueAt,
|
|
351
|
+
decision_status: [{ id: '0', name: '未完成' }],
|
|
352
|
+
}));
|
|
353
|
+
// ⑥ 未确认 / 只到 tasks → 给出预览或只建任务
|
|
354
|
+
const existing = await getMeetingOperation('meeting.eb.decisionTask.list').handler({ mt_id: mtId, pageNo: 1, pageSize: 1 }, env);
|
|
355
|
+
const existingTotal = Number(existing?.data?.total ?? 0);
|
|
356
|
+
const skipExisting = args.skipExistingTasks !== false;
|
|
357
|
+
if (needsConfirm && !confirmed) {
|
|
358
|
+
const preview = formatTaskMessage({
|
|
359
|
+
meetingName: textOf(meetingInfo.mt_name),
|
|
360
|
+
begin: textOf(meetingInfo.begin_datetime),
|
|
361
|
+
end: textOf(meetingInfo.end_datetime),
|
|
362
|
+
tasks: plannedRows,
|
|
363
|
+
onlyPending: args.onlyPending !== false,
|
|
364
|
+
});
|
|
365
|
+
return finish({
|
|
366
|
+
...base,
|
|
367
|
+
needsConfirm: true,
|
|
368
|
+
tasks: {
|
|
369
|
+
source: taskSource,
|
|
370
|
+
extracted: assignments.length,
|
|
371
|
+
resolved: resolved.length,
|
|
372
|
+
created: false,
|
|
373
|
+
existingTasks: existingTotal,
|
|
374
|
+
unresolved: [...new Set(unresolved)],
|
|
375
|
+
planned: resolved.map((item) => ({
|
|
376
|
+
title: item.title,
|
|
377
|
+
executor: item.name,
|
|
378
|
+
begin: beginAt,
|
|
379
|
+
end: dueAt,
|
|
380
|
+
})),
|
|
381
|
+
},
|
|
382
|
+
notifyPreview: level === 'notify' ? preview.text : null,
|
|
383
|
+
}, [
|
|
384
|
+
...warnings,
|
|
385
|
+
`这是预览,**尚未建任务、也尚未发群消息**。确认后带 \`confirm:true\` 重跑;`
|
|
386
|
+
+ `建议同时带 \`minuteId=${minuteId}\` 复用本次纪要,避免再生成一条`,
|
|
387
|
+
]);
|
|
388
|
+
}
|
|
389
|
+
// ⑦ 提交决议(概述 + 明细 + 标记已决议)——**不手工建任务**
|
|
390
|
+
// ⚠️ 2026-10-27 用户澄清 + 真机复核:`update_mt_isdecision`(标记已决议)会触发用户配置的 ESB,
|
|
391
|
+
// 按每条决议明细**自动生成**一个决议任务;本文不调用 `meeting.eb.decisionTask.create`,
|
|
392
|
+
// 否则同一件事会有两个任务(create_mt_decision_task 只在 ESB 没触发时补建)。
|
|
393
|
+
let created = false;
|
|
394
|
+
if (plannedRows.length === 0) {
|
|
395
|
+
warnings.push('没有可指派的执行人,未创建决议任务');
|
|
396
|
+
}
|
|
397
|
+
else if (skipExisting && existingTotal > 0) {
|
|
398
|
+
warnings.push(`该会议已有 ${existingTotal} 条决议任务,按 skipExistingTasks 跳过创建(要重建立传 skipExistingTasks=false)`);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
stepStart = Date.now();
|
|
402
|
+
const published = await getMeetingOperation('meeting.eb.decision.publish.prepare').handler({
|
|
403
|
+
mt_id: mtId,
|
|
404
|
+
mt_content: `群聊纪要自动提取的 ${plannedRows.length} 项分工:${plannedRows.map((row) => row.decision_title).join('、')}`,
|
|
405
|
+
detail1: plannedRows.map((row) => ({
|
|
406
|
+
decision_title: row.decision_title,
|
|
407
|
+
executors: row.executors[0].id,
|
|
408
|
+
begin_datetime: row.begin_datetime,
|
|
409
|
+
end_datetime: row.end_datetime,
|
|
410
|
+
decision_status: '0',
|
|
411
|
+
})),
|
|
412
|
+
}, env);
|
|
413
|
+
await getMeetingOperation('meeting.eb.decision.publish.apply').handler({ confirm: true, continuation: published?.data?.continuation }, env);
|
|
414
|
+
created = true;
|
|
415
|
+
timings.publishTasks = Date.now() - stepStart;
|
|
416
|
+
}
|
|
417
|
+
// ⑧ 推送群聊(前提:服务端确实已经有决议任务了才发)
|
|
418
|
+
let notify = null;
|
|
419
|
+
if (executedLevel === 'notify') {
|
|
420
|
+
stepStart = Date.now();
|
|
421
|
+
const readTaskTotal = async () => {
|
|
422
|
+
const check = await getMeetingOperation('meeting.eb.decisionTask.list').handler({ mt_id: mtId, pageNo: 1, pageSize: 1 }, env);
|
|
423
|
+
return Number(check?.data?.total ?? 0);
|
|
424
|
+
};
|
|
425
|
+
// ⚠️ 任务由用户配置的 ESB 联动生成,**有 3~6 秒延迟**。刚 publish 完立刻推群,
|
|
426
|
+
// 通知里会还是上一批任务(实测踩过:新任务 4 条已建,群里列的还是旧 7 条)。
|
|
427
|
+
let taskTotal = existingTotal;
|
|
428
|
+
if (created) {
|
|
429
|
+
const waited = await waitForTaskIncrease(readTaskTotal, existingTotal);
|
|
430
|
+
timings.waitTasks = waited.waitedMs;
|
|
431
|
+
taskTotal = waited.total;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
taskTotal = await readTaskTotal();
|
|
435
|
+
}
|
|
436
|
+
// ⛔ 没有任务就不发:群里收到"共 0 项"的通知比不发更糟
|
|
437
|
+
if (taskTotal <= 0) {
|
|
438
|
+
warnings.push(created
|
|
439
|
+
? '决议已提交,但服务端仍未生成任务(ESB 联动延迟或未启用),**本次未推送群消息**;'
|
|
440
|
+
+ '稍后确认任务生成后再用 meeting.decision.notifyGroup 推送'
|
|
441
|
+
: '该会议没有决议任务,**本次未推送群消息**');
|
|
442
|
+
return finish({
|
|
443
|
+
...base,
|
|
444
|
+
tasks: {
|
|
445
|
+
source: taskSource,
|
|
446
|
+
extracted: assignments.length,
|
|
447
|
+
resolved: resolved.length,
|
|
448
|
+
created,
|
|
449
|
+
existingTasks: existingTotal,
|
|
450
|
+
unresolved: [...new Set(unresolved)],
|
|
451
|
+
planned: resolved.map((item) => ({ title: item.title, executor: item.name, begin: beginAt, end: dueAt })),
|
|
452
|
+
},
|
|
453
|
+
notify: { sent: false, groupId: null, charCount: 0, skipped: 'no_tasks' },
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
const notifyData = await getMeetingOperation('meeting.decision.notifyGroup').handler({
|
|
457
|
+
mt_id: mtId,
|
|
458
|
+
...(args.groupId ? { groupId: String(args.groupId) } : {}),
|
|
459
|
+
// 用户提供的材料也要一起进纪要(会议有 imchat_groupid 时群聊照样自动并入)
|
|
460
|
+
...(Array.isArray(args.files) ? { files: args.files } : {}),
|
|
461
|
+
...(args.content ? { content: String(args.content) } : {}),
|
|
462
|
+
onlyPending: args.onlyPending !== false,
|
|
463
|
+
confirm: true,
|
|
464
|
+
}, env);
|
|
465
|
+
const nd = notifyData?.data ?? {};
|
|
466
|
+
notify = { sent: nd.sent === true, groupId: nd.groupId ?? null, charCount: nd.charCount ?? 0 };
|
|
467
|
+
warnings.push(...(notifyData?.warnings ?? []));
|
|
468
|
+
timings.notify = Date.now() - stepStart;
|
|
469
|
+
}
|
|
470
|
+
return finish({
|
|
471
|
+
...base,
|
|
472
|
+
tasks: {
|
|
473
|
+
source: taskSource,
|
|
474
|
+
extracted: assignments.length,
|
|
475
|
+
resolved: resolved.length,
|
|
476
|
+
created,
|
|
477
|
+
existingTasks: existingTotal,
|
|
478
|
+
unresolved: [...new Set(unresolved)],
|
|
479
|
+
planned: resolved.map((item) => ({ title: item.title, executor: item.name, begin: beginAt, end: dueAt })),
|
|
480
|
+
},
|
|
481
|
+
notify,
|
|
482
|
+
});
|
|
483
|
+
function finish(payload, extraWarnings = []) {
|
|
484
|
+
const allWarnings = [...warnings, ...extraWarnings];
|
|
485
|
+
return meetingSuccess('meeting.pipeline.run', { ...payload, timings: { ...timings, total: Date.now() - startedAt } }, allWarnings.length > 0 ? { warnings: [...new Set(allWarnings)] } : undefined);
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
export const meetingPipelineOperations = [meetingPipelineRun];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { meetingValidationError } from '../errors.js';
|
|
4
|
+
import { meetingSuccess, schemaOf } from './types.js';
|
|
5
|
+
import { meetingStateDir } from './env.js';
|
|
6
|
+
import { encodeQrText } from '../qr/encoder.js';
|
|
7
|
+
import { renderQrPng, renderQrSvg } from '../qr/render.js';
|
|
8
|
+
const ECC_LEVELS = ['L', 'M', 'Q', 'H'];
|
|
9
|
+
function stamp() {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
12
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
13
|
+
}
|
|
14
|
+
function safeName(input) {
|
|
15
|
+
return input.replace(/[\\/:*?"<>|\s]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `meeting.qrcode.render` —— 文本 → 二维码图片(PNG / SVG),本地零依赖生成。
|
|
19
|
+
*
|
|
20
|
+
* 适用场景:参会码(门禁/食堂)、签到码、会议链接码、议程扫码等「一段文本要变成能被扫的图」。
|
|
21
|
+
* 只负责**画码**,不负责**造码内容**:码里装什么由业务接口决定(例如 ESB 短信里的免登链接、
|
|
22
|
+
* 平台 getQrCode 返回的签到票据)。内容来源那一层单独接,避免把外部契约猜进 CLI。
|
|
23
|
+
*/
|
|
24
|
+
const meetingQrRender = {
|
|
25
|
+
name: 'meeting.qrcode.render',
|
|
26
|
+
resource: 'qrcode',
|
|
27
|
+
method: 'render',
|
|
28
|
+
action: 'meeting.qrcode.render',
|
|
29
|
+
summary: '把一段文本渲染成二维码图片(PNG/SVG,零依赖本地生成);参会码/签到码/链接码通用,默认输出到 ~/.weaver-work-cli/meeting/qrcodes/',
|
|
30
|
+
capability: 'qrcode',
|
|
31
|
+
risk: 'read',
|
|
32
|
+
inputSchema: schemaOf({
|
|
33
|
+
text: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: '二维码内容(必填)。URL / JSON / 票据串均可;UTF-8 编码,中文按 3 字节计容量',
|
|
36
|
+
},
|
|
37
|
+
outPath: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: '输出文件完整路径(可选)。相对路径按当前工作目录解析;缺省写到 ~/.weaver-work-cli/meeting/qrcodes/qr-<时间戳>.png',
|
|
40
|
+
},
|
|
41
|
+
fileName: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: '仅指定文件名(不含目录),与缺省目录组合,如 参会码-张三.png;outPath 优先',
|
|
44
|
+
},
|
|
45
|
+
format: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
enum: ['png', 'svg'],
|
|
48
|
+
description: '输出格式(默认 png);svg 为矢量,适合印刷或嵌文档',
|
|
49
|
+
},
|
|
50
|
+
ecc: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
enum: ECC_LEVELS,
|
|
53
|
+
description: '纠错等级(默认 M):L≈7% / M≈15% / Q≈25% / H≈30% 冗余。会场灯光差、码面易磨损用 Q 或 H;内容越长等级越高版本越大',
|
|
54
|
+
},
|
|
55
|
+
scale: {
|
|
56
|
+
type: 'number',
|
|
57
|
+
description: '每个模块的像素边长(默认 8,范围 1-64)。屏幕展示 ≥6,打印 ≥10',
|
|
58
|
+
},
|
|
59
|
+
margin: {
|
|
60
|
+
type: 'number',
|
|
61
|
+
description: '静区宽度,单位=模块(默认 4,规范要求 ≥4,不要小于 4,否则扫码器识别率下降)',
|
|
62
|
+
},
|
|
63
|
+
mask: {
|
|
64
|
+
type: 'number',
|
|
65
|
+
description: '强制掩码 0-7(一般不用传);不传按罚分自动择优',
|
|
66
|
+
},
|
|
67
|
+
}, { required: ['text'] }),
|
|
68
|
+
handler: async (args, env) => {
|
|
69
|
+
const text = typeof args.text === 'string' ? args.text : '';
|
|
70
|
+
if (text.trim() === '') {
|
|
71
|
+
throw meetingValidationError('qrcode_text_required', '生成二维码必须提供 text(二维码内容)');
|
|
72
|
+
}
|
|
73
|
+
const format = args.format === 'svg' ? 'svg' : 'png';
|
|
74
|
+
const ecc = ECC_LEVELS.includes(args.ecc) ? args.ecc : 'M';
|
|
75
|
+
const scale = args.scale === undefined ? 8 : Number(args.scale);
|
|
76
|
+
const margin = args.margin === undefined ? 4 : Number(args.margin);
|
|
77
|
+
if (!Number.isFinite(scale) || scale < 1 || scale > 64) {
|
|
78
|
+
throw meetingValidationError('qrcode_scale_invalid', `scale 必须是 1-64 的数字,收到 ${String(args.scale)}`);
|
|
79
|
+
}
|
|
80
|
+
if (!Number.isFinite(margin) || margin < 0 || margin > 32) {
|
|
81
|
+
throw meetingValidationError('qrcode_margin_invalid', `margin 必须是 0-32 的数字,收到 ${String(args.margin)}`);
|
|
82
|
+
}
|
|
83
|
+
if (args.mask !== undefined) {
|
|
84
|
+
const maskValue = Number(args.mask);
|
|
85
|
+
if (!Number.isInteger(maskValue) || maskValue < 0 || maskValue > 7) {
|
|
86
|
+
throw meetingValidationError('qrcode_mask_invalid', `mask 必须是 0-7 的整数,收到 ${String(args.mask)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let matrix;
|
|
90
|
+
try {
|
|
91
|
+
matrix = encodeQrText(text, {
|
|
92
|
+
ecc,
|
|
93
|
+
mask: args.mask === undefined ? undefined : Number(args.mask),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
throw meetingValidationError('qrcode_encode_failed', `二维码编码失败:${message}`);
|
|
99
|
+
}
|
|
100
|
+
const imageScale = Math.floor(scale);
|
|
101
|
+
const imageMargin = Math.floor(margin);
|
|
102
|
+
const imageSize = (matrix.size + imageMargin * 2) * imageScale;
|
|
103
|
+
const defaultDir = join(meetingStateDir(env.ctx), 'qrcodes');
|
|
104
|
+
const fallbackName = `qr-${stamp()}.${format}`;
|
|
105
|
+
const explicitName = typeof args.fileName === 'string' && args.fileName.trim() !== ''
|
|
106
|
+
? `${safeName(args.fileName.replace(/\.(png|svg)$/i, ''))}.${format}`
|
|
107
|
+
: fallbackName;
|
|
108
|
+
const target = typeof args.outPath === 'string' && args.outPath.trim() !== ''
|
|
109
|
+
? (isAbsolute(args.outPath) ? resolve(args.outPath) : resolve(process.cwd(), args.outPath))
|
|
110
|
+
: join(defaultDir, explicitName);
|
|
111
|
+
await mkdir(dirname(target), { recursive: true });
|
|
112
|
+
const payload = format === 'svg'
|
|
113
|
+
? Buffer.from(renderQrSvg(matrix, { scale: imageScale, margin: imageMargin }), 'utf8')
|
|
114
|
+
: renderQrPng(matrix, { scale: imageScale, margin: imageMargin });
|
|
115
|
+
await writeFile(target, payload);
|
|
116
|
+
return meetingSuccess('meeting.qrcode.render', {
|
|
117
|
+
path: target,
|
|
118
|
+
format,
|
|
119
|
+
imageSize,
|
|
120
|
+
scale: imageScale,
|
|
121
|
+
margin: imageMargin,
|
|
122
|
+
modules: matrix.size,
|
|
123
|
+
version: matrix.version,
|
|
124
|
+
ecc: matrix.ecc,
|
|
125
|
+
mask: matrix.mask,
|
|
126
|
+
byteLength: matrix.byteLength,
|
|
127
|
+
textPreview: text.length > 60 ? `${text.slice(0, 60)}…` : text,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
export const meetingQrOperations = [meetingQrRender];
|
|
@@ -49,7 +49,7 @@ const meetingReceiptList = {
|
|
|
49
49
|
const body = {
|
|
50
50
|
mtId: toIdString(args.mtId, 'mtId'),
|
|
51
51
|
isMobile: args.isMobile === undefined ? false : args.isMobile === true,
|
|
52
|
-
// 2026-10-08
|
|
52
|
+
// 2026-10-08 真机实测(某环境):**不传 memberType 必 500「系统内部错误」**,
|
|
53
53
|
// 传 1 即正常返回。历史文档只提醒了 isMobile,实际 memberType 同样不能缺省。
|
|
54
54
|
memberType: args.memberType === undefined ? 1 : args.memberType,
|
|
55
55
|
current: typeof args.current === 'number' && args.current > 0 ? args.current : 1,
|
|
@@ -10,6 +10,15 @@ import { meetingLifecycleOperations } from './lifecycle.js';
|
|
|
10
10
|
import { meetingReceiptOperations } from './receipts.js';
|
|
11
11
|
import { meetingSignOperations } from './signs.js';
|
|
12
12
|
import { meetingLinkOperations } from './link.js';
|
|
13
|
+
import { meetingAttachmentOperations } from './attachment.js';
|
|
14
|
+
import { meetingAttachmentContentOperations } from './attachment-content.js';
|
|
15
|
+
import { meetingMinuteContentOperations } from './minute-content.js';
|
|
16
|
+
import { meetingMinuteFromGroupChatOperations } from './from-group-chat.js';
|
|
17
|
+
import { meetingDecisionNotifyOperations } from './task-notify.js';
|
|
18
|
+
import { meetingPipelineOperations } from './pipeline.js';
|
|
19
|
+
import { meetingInvitationOperations } from './invitation.js';
|
|
20
|
+
import { meetingQrOperations } from './qrcode.js';
|
|
21
|
+
import { meetingSummaryOperations } from './summary.js';
|
|
13
22
|
import { meetingEbOperations } from './eb/index.js';
|
|
14
23
|
function buildRegistry() {
|
|
15
24
|
const registry = new Map();
|
|
@@ -25,6 +34,15 @@ function buildRegistry() {
|
|
|
25
34
|
...meetingReceiptOperations,
|
|
26
35
|
...meetingSignOperations,
|
|
27
36
|
...meetingLinkOperations,
|
|
37
|
+
...meetingAttachmentOperations,
|
|
38
|
+
...meetingAttachmentContentOperations,
|
|
39
|
+
...meetingMinuteContentOperations,
|
|
40
|
+
...meetingMinuteFromGroupChatOperations,
|
|
41
|
+
...meetingDecisionNotifyOperations,
|
|
42
|
+
...meetingPipelineOperations,
|
|
43
|
+
...meetingInvitationOperations,
|
|
44
|
+
...meetingQrOperations,
|
|
45
|
+
...meetingSummaryOperations,
|
|
28
46
|
...meetingEbOperations,
|
|
29
47
|
];
|
|
30
48
|
for (const definition of definitions) {
|