weaver-work-cli 0.1.28 → 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.
Files changed (219) hide show
  1. package/dist/chunks/archive-U766FTFH.js +2537 -0
  2. package/dist/chunks/archive-oa-DMWEKX3L.js +1317 -0
  3. package/dist/chunks/asset-TCR7VDEV.js +2109 -0
  4. package/dist/chunks/attachment-content-AQNX5N3S.js +23 -0
  5. package/dist/chunks/blog-EVZWGDVM.js +2157 -0
  6. package/dist/chunks/caizhilian-JOWS6SL5.js +10087 -0
  7. package/dist/chunks/calendar-5WNCV66Q.js +1195 -0
  8. package/dist/chunks/chunk-2DUV44UX.js +15606 -0
  9. package/dist/chunks/chunk-324YPTC2.js +1607 -0
  10. package/dist/chunks/chunk-65QMVWHS.js +231 -0
  11. package/dist/chunks/chunk-6V7ZLF34.js +387 -0
  12. package/dist/chunks/chunk-AC7KJWC5.js +358 -0
  13. package/dist/chunks/chunk-AJ62BGAW.js +29 -0
  14. package/dist/chunks/chunk-BUSC6UAE.js +43 -0
  15. package/dist/chunks/chunk-CVZ2JOSW.js +62 -0
  16. package/dist/chunks/chunk-D4LTJSJD.js +477 -0
  17. package/dist/chunks/chunk-EG6ZU444.js +654 -0
  18. package/dist/chunks/chunk-EZALDOQH.js +383 -0
  19. package/dist/chunks/chunk-FMPJJYYI.js +106 -0
  20. package/dist/chunks/chunk-G2JSIUWF.js +769 -0
  21. package/dist/chunks/chunk-GCG2NMWR.js +703 -0
  22. package/dist/chunks/chunk-GPMFD4RD.js +75 -0
  23. package/dist/chunks/chunk-H3WDBYJW.js +508 -0
  24. package/dist/chunks/chunk-H6XSTSLM.js +2357 -0
  25. package/dist/chunks/chunk-J7RJTZSV.js +5556 -0
  26. package/dist/chunks/chunk-LUSPKZKY.js +53 -0
  27. package/dist/chunks/chunk-NIAJ5ZCG.js +73 -0
  28. package/dist/chunks/chunk-NOODWMCL.js +584 -0
  29. package/dist/chunks/chunk-OOUTBLZ4.js +51 -0
  30. package/dist/chunks/chunk-P26ZWSKU.js +73 -0
  31. package/dist/chunks/chunk-QWY6PGL6.js +3472 -0
  32. package/dist/chunks/chunk-R2IJAK7T.js +464 -0
  33. package/dist/chunks/chunk-R7O5PUWO.js +205 -0
  34. package/dist/chunks/chunk-RCGYCOMG.js +6962 -0
  35. package/dist/chunks/chunk-VZTXRG6X.js +403 -0
  36. package/dist/chunks/chunk-WHD4XMRP.js +161 -0
  37. package/dist/chunks/chunk-YLCYX7LE.js +371 -0
  38. package/dist/chunks/chunk-ZEDWSH32.js +68 -0
  39. package/dist/chunks/continuation-2CKSBLA6.js +11 -0
  40. package/dist/chunks/continuation-HZIZRZBX.js +13 -0
  41. package/dist/chunks/continuation-NLECEJPX.js +11 -0
  42. package/dist/chunks/ebuilder-form-NJ4YK6CC.js +2276 -0
  43. package/dist/chunks/ehr-MMYN36VA.js +5241 -0
  44. package/dist/chunks/employee-SAK2QYXX.js +11 -0
  45. package/dist/chunks/esb-HIWHOU7Q.js +141 -0
  46. package/dist/chunks/fna-KBACQTA5.js +1444 -0
  47. package/dist/chunks/host-3OHS5QUX.js +21 -0
  48. package/dist/chunks/host-IG6KEH5Q.js +19 -0
  49. package/dist/chunks/host-KQL5N4HO.js +23 -0
  50. package/dist/chunks/host-LLSZQCWR.js +17 -0
  51. package/dist/chunks/hrm-LYEC67ZQ.js +244 -0
  52. package/dist/chunks/im-RN2UKAEU.js +252 -0
  53. package/dist/chunks/invoice-EEPHLAF6.js +5179 -0
  54. package/dist/chunks/jiuchuanhui-WVVZYSJE.js +1940 -0
  55. package/dist/chunks/lazy-dispatch-MDOFFL2S.js +28 -0
  56. package/dist/chunks/mail-LNQLTF6L.js +2695 -0
  57. package/dist/chunks/meeting-XSD3BDVQ.js +504 -0
  58. package/dist/chunks/msg-parse-4GSQ2WTO.js +41 -0
  59. package/dist/chunks/okr-TMWTIU2K.js +2482 -0
  60. package/dist/chunks/plan-A2FSFA7T.js +2025 -0
  61. package/dist/chunks/project-T26QLBQF.js +1361 -0
  62. package/dist/chunks/registry-2VLUAESA.js +14 -0
  63. package/dist/chunks/registry-JCFEYXOE.js +18 -0
  64. package/dist/chunks/registry-NYPYMZXG.js +16 -0
  65. package/dist/chunks/registry-Q7OQJ3SU.js +19 -0
  66. package/dist/chunks/registry-ZJMJVWFA.js +19 -0
  67. package/dist/chunks/room-recommend-UR4ZDZS6.js +20 -0
  68. package/dist/chunks/schedule-5L2PM4M6.js +24 -0
  69. package/dist/chunks/state-Y2XBP6YE.js +15 -0
  70. package/dist/chunks/workflow-TR6PG2SL.js +245 -0
  71. package/dist/cmd/argv.js +45 -0
  72. package/dist/cmd/docs/index.js +3 -3
  73. package/dist/cmd/lazy-dispatch.js +20 -0
  74. package/dist/cmd/root.js +2 -4
  75. package/dist/cmd/setup/index.js +2 -2
  76. package/dist/cmd/skills/index.js +2 -2
  77. package/dist/core/package-paths.js +47 -0
  78. package/dist/index.js +163 -7
  79. package/dist/internal/e10/auth/xiaoe.js +3 -3
  80. package/dist/internal/e10/request-runtime.js +18 -62
  81. package/dist/internal/skills/detector.js +2 -2
  82. package/dist/shortcuts/contract/data/performance-catalog.js +1306 -0
  83. package/dist/shortcuts/contract/host.js +91 -35
  84. package/dist/shortcuts/contract/index.js +213 -0
  85. package/dist/shortcuts/contract/manifest.js +928 -0
  86. package/dist/shortcuts/contract/ooxml/docx.js +295 -0
  87. package/dist/shortcuts/contract/ooxml/xlsx.js +221 -0
  88. package/dist/shortcuts/contract/ooxml/zip.js +158 -0
  89. package/dist/shortcuts/contract/operations/assistant.js +1156 -0
  90. package/dist/shortcuts/contract/operations/clause-library.js +522 -0
  91. package/dist/shortcuts/contract/operations/compare.js +193 -0
  92. package/dist/shortcuts/contract/operations/credit-page.js +34 -0
  93. package/dist/shortcuts/contract/operations/flow.js +98 -0
  94. package/dist/shortcuts/contract/operations/page.js +66 -0
  95. package/dist/shortcuts/contract/operations/performance.js +733 -0
  96. package/dist/shortcuts/contract/operations/quote.js +557 -0
  97. package/dist/shortcuts/contract/operations/recommend.js +78 -0
  98. package/dist/shortcuts/contract/operations/registry.js +18 -0
  99. package/dist/shortcuts/contract/operations/review.js +83 -0
  100. package/dist/shortcuts/contract/operations/shared.js +37 -0
  101. package/dist/shortcuts/contract/operations/upload.js +108 -0
  102. package/dist/shortcuts/contract/operations/write.js +3 -2
  103. package/dist/shortcuts/index.js +140 -40
  104. package/dist/shortcuts/invoice/host.js +2 -1
  105. package/dist/shortcuts/invoice/manifest.js +36 -12
  106. package/dist/shortcuts/invoice/operations/issuing-qys.js +132 -36
  107. package/dist/shortcuts/invoice/operations/reim-auto.js +9 -0
  108. package/dist/shortcuts/invoice/operations/reim.js +81 -5
  109. package/dist/shortcuts/invoice/operations/shared.js +12 -5
  110. package/dist/shortcuts/meeting/host.js +45 -3
  111. package/dist/shortcuts/meeting/invitation/docx.js +442 -0
  112. package/dist/shortcuts/meeting/operations/attachment-content.js +418 -0
  113. package/dist/shortcuts/meeting/operations/attachment.js +442 -0
  114. package/dist/shortcuts/meeting/operations/eb/cache.js +57 -0
  115. package/dist/shortcuts/meeting/operations/eb/catalog.js +343 -28
  116. package/dist/shortcuts/meeting/operations/eb/conflict.js +372 -11
  117. package/dist/shortcuts/meeting/operations/eb/create-flow.js +710 -0
  118. package/dist/shortcuts/meeting/operations/eb/decision.js +47 -1
  119. package/dist/shortcuts/meeting/operations/eb/factory.js +129 -11
  120. package/dist/shortcuts/meeting/operations/eb/index.js +122 -2
  121. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +378 -0
  122. package/dist/shortcuts/meeting/operations/eb/room-summary.js +84 -0
  123. package/dist/shortcuts/meeting/operations/eb/schedule.js +450 -0
  124. package/dist/shortcuts/meeting/operations/from-group-chat.js +576 -0
  125. package/dist/shortcuts/meeting/operations/invitation.js +458 -0
  126. package/dist/shortcuts/meeting/operations/minute-content.js +266 -0
  127. package/dist/shortcuts/meeting/operations/pipeline.js +489 -0
  128. package/dist/shortcuts/meeting/operations/qrcode.js +131 -0
  129. package/dist/shortcuts/meeting/operations/receipts.js +1 -1
  130. package/dist/shortcuts/meeting/operations/registry.js +18 -0
  131. package/dist/shortcuts/meeting/operations/summary.js +276 -0
  132. package/dist/shortcuts/meeting/operations/task-notify.js +325 -0
  133. package/dist/shortcuts/meeting/operations.js +42 -1
  134. package/dist/shortcuts/meeting/qr/encoder.js +427 -0
  135. package/dist/shortcuts/meeting/qr/render.js +103 -0
  136. package/dist/shortcuts/workflow/index.js +3 -0
  137. package/dist/shortcuts/workflow/operations/form.js +17 -5
  138. package/dist/shortcuts/workflow/operations/read.js +13 -6
  139. package/dist/shortcuts/workflow/operations/registry.js +2 -0
  140. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +331 -0
  141. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +133 -0
  142. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +326 -0
  143. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +113 -0
  144. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +259 -0
  145. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoListRows.js +64 -0
  146. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoOutputPlanner.js +28 -0
  147. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.aggregate.js +70 -0
  148. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +267 -0
  149. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +392 -0
  150. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +165 -0
  151. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +448 -0
  152. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +348 -0
  153. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +222 -0
  154. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +66 -0
  155. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +502 -0
  156. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +540 -0
  157. package/docs/_catalog.md +1 -0
  158. package/docs/contract.md +39 -4
  159. package/docs/invoice.md +7 -7
  160. package/docs/todo-smart-convert/README.md +171 -0
  161. package/docs/todo-smart-convert/data-contract.md +236 -0
  162. package/docs/todo-smart-convert/mock/ai-todo-query-target.json +209 -0
  163. package/docs/todo-smart-convert/subgroup-rules.md +245 -0
  164. package/package.json +3 -2
  165. package/scripts/bundle.mjs +63 -0
  166. package/scripts/package-skill.mjs +1 -1
  167. package/skill-template/business-info.json +25 -4
  168. package/skill-template/domains/jinchengda.md +16 -3
  169. package/skill-template/domains/yepiaotong.md +3 -3
  170. package/skills/weaver-e10-jinchengda/SKILL.md +111 -31
  171. package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +7 -13
  172. package/skills/weaver-e10-jinchengda/references/contract-ai-assistant.md +150 -0
  173. package/skills/weaver-e10-jinchengda/references/contract-ai-review.md +87 -0
  174. package/skills/weaver-e10-jinchengda/references/contract-clause-library-crud.md +111 -0
  175. package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +0 -15
  176. package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +0 -13
  177. package/skills/weaver-e10-jinchengda/references/contract-credit-page.md +50 -0
  178. package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +4 -2
  179. package/skills/weaver-e10-jinchengda/references/contract-doc-compare.md +74 -0
  180. package/skills/weaver-e10-jinchengda/references/contract-flow-launch.md +84 -0
  181. package/skills/weaver-e10-jinchengda/references/contract-performance-query.md +29 -0
  182. package/skills/weaver-e10-jinchengda/references/contract-quote-to-contract.md +28 -0
  183. package/skills/weaver-e10-jinchengda/references/contract-request-status.md +2 -21
  184. package/skills/weaver-e10-jinchengda/references/contract-request-write.md +0 -24
  185. package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +0 -15
  186. package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +0 -17
  187. package/skills/weaver-e10-jinchengda/references/contract-template-recommend.md +70 -0
  188. package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +0 -15
  189. package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +0 -15
  190. package/skills/weaver-e10-jinchengda/references/source-manifest.json +1168 -152
  191. package/skills/weaver-e10-meeting/SKILL.md +493 -17
  192. package/skills/weaver-e10-meeting/references/meeting-change.md +4 -4
  193. package/skills/weaver-e10-meeting/references/meeting-create.md +6 -6
  194. package/skills/weaver-e10-meeting/references/meeting-eb.md +503 -27
  195. package/skills/weaver-e10-meeting/references/meeting-invitation.md +113 -0
  196. package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +6 -6
  197. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +425 -0
  198. package/skills/weaver-e10-meeting/references/meeting-queries.md +4 -4
  199. package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +15 -15
  200. package/skills/weaver-e10-meeting/references/meeting-verification.md +19 -11
  201. package/skills/weaver-e10-meeting.zip +0 -0
  202. package/skills/weaver-e10-workflow/SKILL.md +23 -1
  203. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +282 -0
  204. package/skills/weaver-e10-workflow/scripts/todo-summary.cjs +695 -0
  205. package/skills/weaver-e10-yepiaotong/SKILL.md +15 -11
  206. package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +1 -1
  207. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +10 -7
  208. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +41 -14
  209. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +54 -26
  210. package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +6 -5
  211. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +14 -11
  212. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +97 -56
  213. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +84 -60
  214. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +142 -105
  215. package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +53 -194
  216. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +7 -7
  217. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +164 -72
  218. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +8 -2
  219. package/templates/meeting/invitation.docx +0 -0
@@ -0,0 +1,276 @@
1
+ import { meetingValidationError } from '../errors.js';
2
+ import { meetingSuccess, schemaOf, SCHEMA_ID } from './types.js';
3
+ import { computeChatWindow, composeTranscript, sampleMessages } from './from-group-chat.js';
4
+ const ALL_SECTIONS = ['minutes', 'tasks', 'attachments', 'chat'];
5
+ function textOf(value) {
6
+ return value === undefined || value === null ? '' : String(value);
7
+ }
8
+ function toRows(result) {
9
+ return Array.isArray(result?.data?.rows) ? result.data.rows : [];
10
+ }
11
+ function mainTableOf(row) {
12
+ return ((row?.mainTable ?? row ?? {}) ?? {});
13
+ }
14
+ function latestMinute(rows) {
15
+ if (rows.length === 0)
16
+ return null;
17
+ const scored = rows
18
+ .map((row) => ({ row, at: textOf(mainTableOf(row).update_time) || textOf(mainTableOf(row).create_time) }))
19
+ // 时间串是 `YYYY-MM-DD HH:mm:ss`,字典序即时间序;排不出就保持原顺序
20
+ .sort((a, b) => (a.at === b.at ? 0 : a.at < b.at ? 1 : -1));
21
+ return mainTableOf(scored[0].row);
22
+ }
23
+ const meetingSummaryGet = {
24
+ name: 'meeting.summary.get',
25
+ resource: 'summary',
26
+ method: 'get',
27
+ action: 'meeting.summary.get',
28
+ summary: '总结会议的材料聚合(只读、零副作用):一次拉齐会议主表 + 纪要原文/结构化纪要 + 任务进度 + 附件正文 + 群聊原文,供智能体自己总结;哪一路取不到会在 warnings 里说明',
29
+ capability: 'summary',
30
+ risk: 'read',
31
+ inputSchema: schemaOf({
32
+ mt_id: { ...SCHEMA_ID, description: '会议 id(必填)' },
33
+ minuteId: {
34
+ ...SCHEMA_ID,
35
+ description: '指定纪要 id;省略时取该会议**最新**一条纪要',
36
+ },
37
+ groupId: { type: 'string', description: '群 id;省略时取会议 imchat_groupid' },
38
+ include: {
39
+ type: 'array',
40
+ items: { type: 'string', enum: ALL_SECTIONS },
41
+ description: '只要哪几路材料,默认四路全要',
42
+ },
43
+ withAttachmentText: {
44
+ type: 'boolean',
45
+ description: '是否抽取附件正文(默认 true)。只需清单时可传 false 省掉下载',
46
+ },
47
+ maxAttachmentChars: { type: 'number', description: '每个附件返回正文的最大字符数(默认 4000)' },
48
+ maxChatChars: { type: 'number', description: '群聊原文最大字符数(默认 20000)' },
49
+ chatFrom: { type: 'string', description: '群聊起始时间,覆盖默认窗口' },
50
+ chatTo: { type: 'string', description: '群聊结束时间,覆盖默认窗口' },
51
+ chatNum: { type: 'number', description: '群聊拉取条数(默认 50)' },
52
+ keepSystem: { type: 'boolean', description: '保留系统通知(入群/改名等),默认过滤' },
53
+ taskPageSize: { type: 'number', description: '任务分页大小(默认 50)' },
54
+ }, { required: ['mt_id'] }),
55
+ handler: async (args, env) => {
56
+ const mtId = textOf(args.mt_id).trim();
57
+ if (!mtId)
58
+ throw meetingValidationError('mt_id_required', '必须提供会议 id(mt_id)');
59
+ const ctx = env.ctx;
60
+ const { getMeetingOperation } = await import('./registry.js');
61
+ const warnings = [];
62
+ const timings = {};
63
+ const include = Array.isArray(args.include) && args.include.length > 0
64
+ ? args.include.filter((item) => ALL_SECTIONS.includes(item))
65
+ : ALL_SECTIONS;
66
+ const wants = (section) => include.includes(section);
67
+ // ① 会议主表(群 id 也从这里来)
68
+ let stepStart = Date.now();
69
+ const listed = await getMeetingOperation('meeting.eb.meeting.list').handler({ mt_id: mtId, pageNo: 1, pageSize: 1 }, env);
70
+ const meeting = mainTableOf(toRows(listed)[0]);
71
+ if (!meeting.mt_name && !meeting.id) {
72
+ throw meetingValidationError('not_found', `未找到会议 id=${mtId},或当前账号无权限查看`, { id: mtId });
73
+ }
74
+ timings.readMeeting = Date.now() - stepStart;
75
+ const groupId = textOf(args.groupId).trim() || textOf(meeting.imchat_groupid).trim();
76
+ const overview = {
77
+ id: textOf(meeting.id) || mtId,
78
+ name: textOf(meeting.mt_name),
79
+ begin: textOf(meeting.begin_datetime),
80
+ end: textOf(meeting.end_datetime),
81
+ address: textOf(meeting.address),
82
+ topic: textOf(meeting.mt_desc),
83
+ creator: textOf(meeting.mt_creator),
84
+ status: textOf(meeting.mt_status),
85
+ groupId: groupId || null,
86
+ };
87
+ // ② 纪要
88
+ let minute = null;
89
+ if (wants('minutes')) {
90
+ stepStart = Date.now();
91
+ try {
92
+ let target = mainTableOf({});
93
+ const givenMinuteId = textOf(args.minuteId).trim();
94
+ if (givenMinuteId) {
95
+ const detail = await getMeetingOperation('meeting.eb.minute.detail').handler({ id: givenMinuteId }, env);
96
+ target = mainTableOf(detail?.data?.data ?? detail?.data);
97
+ if (!textOf(target.id))
98
+ throw new Error(`纪要 ${givenMinuteId} 读取为空`);
99
+ }
100
+ else {
101
+ const minutes = await getMeetingOperation('meeting.eb.minute.list').handler({ mt_id: mtId, pageNo: 1, pageSize: 20 }, env);
102
+ const rows = toRows(minutes);
103
+ const latest = latestMinute(rows);
104
+ if (!latest) {
105
+ warnings.push('该会议还没有纪要(minutes 段为 null)');
106
+ }
107
+ else {
108
+ const detail = await getMeetingOperation('meeting.eb.minute.detail').handler({ id: textOf(latest.id) }, env);
109
+ target = mainTableOf(detail?.data?.data ?? detail?.data);
110
+ // ⚠️ 列表有记录、详情却取空时必须说出来:否则 minute 静默变 null,
111
+ // 智能体会以为"这场会没纪要",而其实是我们没读出来。
112
+ if (!textOf(target.id)) {
113
+ warnings.push(`纪要 ${textOf(latest.id)} 详情读取为空,minutes 段为 null`);
114
+ }
115
+ }
116
+ }
117
+ if (textOf(target.id)) {
118
+ minute = {
119
+ id: textOf(target.id),
120
+ subject: textOf(target.mt_minutes_subject),
121
+ status: textOf(target.minutes_status),
122
+ documentId: textOf(target.mt_minutes) || null,
123
+ // 原文:群聊 + 用户材料合并后的全文(无群时只有材料)
124
+ content: textOf(target.minutes_content),
125
+ contentLength: textOf(target.minutes_content).length,
126
+ // ESB 生成的结构化纪要(富文本),是总结时信息密度最高的一份
127
+ parseContent: textOf(target.parse_content),
128
+ parseContentLength: textOf(target.parse_content).length,
129
+ };
130
+ }
131
+ }
132
+ catch (error) {
133
+ warnings.push(`纪要读取失败:${String(error?.message ?? error).slice(0, 120)}`);
134
+ }
135
+ timings.minutes = Date.now() - stepStart;
136
+ }
137
+ // ③ 任务情况
138
+ let tasks = null;
139
+ if (wants('tasks')) {
140
+ stepStart = Date.now();
141
+ try {
142
+ const result = await getMeetingOperation('meeting.eb.decisionTask.list').handler({ mt_id: mtId, pageNo: 1, pageSize: Number(args.taskPageSize) > 0 ? Number(args.taskPageSize) : 50 }, env);
143
+ const progress = result?.data?.progress ?? null;
144
+ tasks = {
145
+ total: Number(result?.data?.total ?? 0),
146
+ // progress.summary: total/done/overdue/inProgress/pending
147
+ // progress.items[]: 每条任务 title + plan + 每个执行人的 finish_status
148
+ progress,
149
+ };
150
+ if (Number(result?.data?.total ?? 0) === 0) {
151
+ warnings.push('该会议还没有决议任务(tasks.total=0)');
152
+ }
153
+ }
154
+ catch (error) {
155
+ warnings.push(`任务读取失败:${String(error?.message ?? error).slice(0, 120)}`);
156
+ }
157
+ timings.tasks = Date.now() - stepStart;
158
+ }
159
+ // ④ 会议资料(附件)
160
+ let attachments = null;
161
+ if (wants('attachments')) {
162
+ stepStart = Date.now();
163
+ try {
164
+ const list = await getMeetingOperation('meeting.attachment.list').handler({ mtId }, env);
165
+ const items = Array.isArray(list?.data?.items) ? list.data.items : [];
166
+ const withText = args.withAttachmentText !== false;
167
+ let texts = [];
168
+ if (withText && items.length > 0) {
169
+ try {
170
+ const extracted = await getMeetingOperation('meeting.attachment.extract').handler({ mtId, maxChars: Number(args.maxAttachmentChars) > 0 ? Number(args.maxAttachmentChars) : 4000 }, env);
171
+ texts = Array.isArray(extracted?.data?.items) ? extracted.data.items : [];
172
+ }
173
+ catch (error) {
174
+ warnings.push(`附件正文抽取失败(清单仍可用):${String(error?.message ?? error).slice(0, 120)}`);
175
+ }
176
+ }
177
+ attachments = {
178
+ count: items.length,
179
+ items,
180
+ texts,
181
+ };
182
+ if (items.length === 0)
183
+ warnings.push('该会议没有附件');
184
+ }
185
+ catch (error) {
186
+ warnings.push(`附件清单读取失败:${String(error?.message ?? error).slice(0, 120)}`);
187
+ }
188
+ timings.attachments = Date.now() - stepStart;
189
+ }
190
+ // ⑤ 群聊(只读,复用 from-group-chat 的时间窗/真名/过滤/采样口径,但**不建纪要**)
191
+ let chat = null;
192
+ if (wants('chat')) {
193
+ stepStart = Date.now();
194
+ if (!groupId) {
195
+ warnings.push('该会议没有关联群聊(imchat_groupid 为空),chat 段为 null');
196
+ }
197
+ else {
198
+ try {
199
+ const { createImHost } = await import('../../im/host.js');
200
+ const { createImContinuationSigner } = await import('../../im/continuation.js');
201
+ const { getImOperation } = await import('../../im/operations/registry.js');
202
+ const imEnv = {
203
+ ctx,
204
+ // 测试环境靠 env.imHost 注入假实现,否则会真的打到线上群聊
205
+ host: env.imHost ?? createImHost(ctx),
206
+ continuation: createImContinuationSigner(ctx),
207
+ };
208
+ const window = computeChatWindow(meeting);
209
+ const from = textOf(args.chatFrom).trim() || window.from;
210
+ const to = textOf(args.chatTo).trim() || window.to;
211
+ const pulled = await getImOperation('im.msg.sync.group').handler({ groupId, num: Number(args.chatNum) > 0 ? Number(args.chatNum) : 50, from, to }, imEnv);
212
+ const messages = Array.isArray(pulled?.data?.messages) ? pulled.data.messages : [];
213
+ // sender='我' 不是人名,用 from_uid 反查真名
214
+ const nameByUid = {};
215
+ const uids = [...new Set(messages.map((m) => textOf(m.from_uid)).filter(Boolean))];
216
+ if (uids.length > 0) {
217
+ try {
218
+ const resolved = await getImOperation('im.person.resolveByIds').handler({ ids: uids }, imEnv);
219
+ for (const person of resolved?.data?.employees ?? []) {
220
+ if (person?.uid && person?.name)
221
+ nameByUid[String(person.uid)] = String(person.name);
222
+ }
223
+ }
224
+ catch (error) {
225
+ warnings.push(`群聊说话人姓名解析失败,已回退为原始 sender:${String(error?.message ?? error).slice(0, 80)}`);
226
+ }
227
+ }
228
+ const sorted = [...messages].sort((a, b) => Number(a.msgtime ?? 0) - Number(b.msgtime ?? 0));
229
+ const threshold = 200;
230
+ let workMessages = sorted;
231
+ let sampledDropped = 0;
232
+ if (sorted.length > threshold) {
233
+ const sampled = sampleMessages(sorted, 60);
234
+ workMessages = sampled.kept;
235
+ sampledDropped = sampled.dropped;
236
+ warnings.push(`群消息 ${sorted.length} 条超过阈值 ${threshold},已采样去噪(保留 ${sampled.kept.length}、丢弃 ${sampledDropped});`
237
+ + '需要全量请调大 chatNum 或收窄 chatFrom/chatTo');
238
+ }
239
+ const composed = composeTranscript(workMessages, nameByUid, args.keepSystem === true);
240
+ let transcript = composed.transcript;
241
+ const maxChars = Number(args.maxChatChars) > 0 ? Number(args.maxChatChars) : 20_000;
242
+ let truncated = false;
243
+ if (transcript.length > maxChars) {
244
+ transcript = transcript.slice(0, maxChars);
245
+ truncated = true;
246
+ warnings.push(`群聊原文 ${composed.transcript.length} 字超过 maxChatChars=${maxChars},已截断`);
247
+ }
248
+ if (composed.ignoredSystem > 0) {
249
+ warnings.push(`已忽略 ${composed.ignoredSystem} 条系统通知(入群/改名等);需要保留传 keepSystem=true`);
250
+ }
251
+ chat = {
252
+ groupId,
253
+ from,
254
+ to,
255
+ messageCount: messages.length,
256
+ kept: composed.kept,
257
+ ignoredSystem: composed.ignoredSystem,
258
+ sampledDropped,
259
+ truncated,
260
+ transcript,
261
+ };
262
+ if (messages.length === 0) {
263
+ warnings.push(`群聊在 ${from} ~ ${to} 窗口内没有消息(可放宽 chatFrom/chatTo)`);
264
+ }
265
+ }
266
+ catch (error) {
267
+ warnings.push(`群聊读取失败:${String(error?.message ?? error).slice(0, 120)}`);
268
+ }
269
+ }
270
+ timings.chat = Date.now() - stepStart;
271
+ }
272
+ timings.total = Object.values(timings).reduce((sum, value) => sum + value, 0);
273
+ return meetingSuccess('meeting.summary.get', { id: mtId, overview, minute, tasks, attachments, chat, include, timings }, warnings.length > 0 ? { warnings } : {});
274
+ },
275
+ };
276
+ export const meetingSummaryOperations = [meetingSummaryGet];
@@ -0,0 +1,325 @@
1
+ /**
2
+ * 把会议决议任务推送到会关联的群聊。
3
+ *
4
+ * 背景(2026-10-27 用户提出):决议任务生成后,团队成员并不知道自己被派了活,
5
+ * 还得有人口头通知。这里把「会议 → 群 → 任务清单」这条通知链做进 CLI。
6
+ *
7
+ * 设计取舍:
8
+ * - **默认只预览、不发送**:往群里发消息对外可见且不可撤回(超时后),
9
+ * 所以不带 `confirm:true` 时只回预览文本,由调用方确认后再发。
10
+ * - 复用 `im.msg.send.group` 的 prepare/apply,不自己拼 IM 请求 ——
11
+ * IM 侧的鉴权、文本转义、错误码都在那层处理过了。
12
+ * - 任务清单按「未完成优先」排序,已完成的可选择不发(`onlyPending` 默认 true),
13
+ * 否则一次会议攒到后期,群里天天重复收到一大堆已完成项。
14
+ */
15
+ import { meetingValidationError } from '../errors.js';
16
+ import { meetingSuccess } from './types.js';
17
+ function textOf(value) {
18
+ return value === undefined || value === null ? '' : String(value);
19
+ }
20
+ /** 任务是否未完成(`decision_status` 是数组,id=0 表示未完成)。 */
21
+ export function isPendingTask(task) {
22
+ const raw = task.decision_status;
23
+ const list = Array.isArray(raw) ? raw : raw ? [raw] : [];
24
+ if (list.length === 0)
25
+ return true;
26
+ return list.some((item) => textOf(item?.id) === '0' || textOf(item?.name) === '未完成');
27
+ }
28
+ /** 执行人姓名列表。 */
29
+ export function executorNames(task) {
30
+ const raw = task.executors;
31
+ const list = Array.isArray(raw) ? raw : raw ? [raw] : [];
32
+ return list.map((item) => textOf(item?.name).trim()).filter(Boolean);
33
+ }
34
+ /**
35
+ * 拼群消息文本。抽成纯函数便于单测 —— 排序、截断、空执行人这几条都得有断言,
36
+ * 群消息发出去就收不回来了,不能靠"看着差不多"。
37
+ */
38
+ export function formatTaskMessage(input) {
39
+ const onlyPending = input.onlyPending !== false;
40
+ const maxTasks = typeof input.maxTasks === 'number' && input.maxTasks > 0 ? input.maxTasks : 20;
41
+ const all = input.tasks.filter((task) => textOf(task.decision_title).trim());
42
+ const pending = all.filter(isPendingTask).length;
43
+ const selected = (onlyPending ? all.filter(isPendingTask) : all).sort((a, b) => {
44
+ // 未完成在前;同状态按计划开始时间正序(先到期的先看)
45
+ const pa = isPendingTask(a) ? 0 : 1;
46
+ const pb = isPendingTask(b) ? 0 : 1;
47
+ if (pa !== pb)
48
+ return pa - pb;
49
+ return textOf(a.begin_datetime).localeCompare(textOf(b.begin_datetime));
50
+ });
51
+ const listed = selected.slice(0, maxTasks);
52
+ const title = textOf(input.title).trim() || `【会议任务通知】${input.meetingName}`;
53
+ const lines = [title, ''];
54
+ const begin = textOf(input.begin).trim();
55
+ const end = textOf(input.end).trim();
56
+ if (begin || end) {
57
+ lines.push(`会议时间:${[begin, end].filter(Boolean).join(' ~ ')}`);
58
+ }
59
+ const progress = input.progress ?? {};
60
+ const totalText = typeof progress.total === 'number' ? progress.total : all.length;
61
+ const doneText = typeof progress.done === 'number' ? progress.done : all.length - pending;
62
+ lines.push(`任务进度:共 ${totalText} 项,已完成 ${doneText},待办 ${Math.max(totalText - doneText, 0)}`);
63
+ lines.push('');
64
+ lines.push(selected.length === 0
65
+ ? '本次会议的任务已全部完成,无需跟进。'
66
+ : '各位,本次会议已明确以下任务,请按计划推进并在群内反馈进度:');
67
+ lines.push('');
68
+ listed.forEach((task, index) => {
69
+ lines.push(`${index + 1}. ${textOf(task.decision_title).trim()}`);
70
+ const people = executorNames(task);
71
+ const render = input.renderName ?? ((name) => name);
72
+ lines.push(` 负责人:${people.length > 0 ? people.map(render).join('、') : '未指派'}`);
73
+ const plan = [textOf(task.begin_datetime).trim(), textOf(task.end_datetime).trim()].filter(Boolean).join(' ~ ');
74
+ if (plan)
75
+ lines.push(` 计划:${plan}`);
76
+ if (!isPendingTask(task))
77
+ lines.push(' 状态:已完成');
78
+ });
79
+ if (selected.length > listed.length) {
80
+ lines.push('');
81
+ lines.push(`…另有 ${selected.length - listed.length} 项未列出(可到会议详情查看)`);
82
+ }
83
+ lines.push('');
84
+ lines.push(textOf(input.footer).trim() || '(本消息由会议助手自动推送)');
85
+ return { text: lines.join('\n'), listed: listed.length, pending, total: totalText, selected: listed };
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // @ 到具体执行人(2026-10-27 真机摸清,用户客户端确认「蓝色可点」)
89
+ //
90
+ // E10 群消息的 @ 由两部分组成:
91
+ // txt 里的占位符 `…:@<uid>; …` ← 不蓝就说明只有这个、没带 atUsers
92
+ // 请求体 atUsers `[{ cid, uid }]` ← ⭐ 决定「是不是真 @」的字段
93
+ // 服务端收到 atUsers 后会自己生成落库的 `dt[0].txt.aus`(带 cid/id/nm/ob/t)。
94
+ //
95
+ // 逐项实测(同一段文本,只改请求字段):
96
+ // txt 里写 @姓名 → 纯文本,不变蓝 ❌
97
+ // txt + aus + dt(自己拼 aus) → aus 被丢掉,不变蓝 ❌
98
+ // txt + atUsers → aus 落库 1~N 条,蓝色可点 ✅(单人/双人都验过)
99
+ //
100
+ // 所以:**必须带 atUsers**,aus/dt 不用自己拼。
101
+ // 由于 `im.msg.send.group` 只支持纯文本,会议模块直接调 IM 发送端点(im 模块只调用、不改)。
102
+ // ---------------------------------------------------------------------------
103
+ const GREETING = '各位,本次会议已明确以下任务,请按计划推进并在群内反馈进度:';
104
+ /** 把任务执行人匹配成群成员(按姓名),返回能 @ 的人和匹配不到的人。 */
105
+ export function matchMentions(tasks, members) {
106
+ const byName = new Map();
107
+ for (const member of members) {
108
+ if (member.name)
109
+ byName.set(member.name.trim(), member);
110
+ }
111
+ const matched = [];
112
+ const missing = [];
113
+ const seenUid = new Set();
114
+ const seenName = new Set();
115
+ for (const task of tasks) {
116
+ for (const name of executorNames(task)) {
117
+ const hit = byName.get(name.trim());
118
+ if (!hit) {
119
+ if (!seenName.has(name)) {
120
+ seenName.add(name);
121
+ missing.push(name);
122
+ }
123
+ continue;
124
+ }
125
+ if (seenUid.has(hit.uid))
126
+ continue;
127
+ seenUid.add(hit.uid);
128
+ matched.push(hit);
129
+ }
130
+ }
131
+ return { matched, missing };
132
+ }
133
+ /**
134
+ * 生成「@人」那一行(不含正文其它部分):
135
+ * - display:`@王五 @张三 请关注以下任务`(预览给人看)
136
+ * - wire:`:@uid1; :@uid2; 请关注以下任务`(发送给服务端)
137
+ * - atUsers:决定"是不是真 @"的字段
138
+ */
139
+ export function buildMentionLine(matched) {
140
+ if (matched.length === 0)
141
+ return { displayLine: '', wireLine: '', atUsers: [] };
142
+ return {
143
+ displayLine: `${matched.map((member) => `@${member.name}`).join(' ')} 请关注以下任务`,
144
+ wireLine: `${matched.map((member) => `:@${member.uid};`).join(' ')} 请关注以下任务`,
145
+ // ⚠️ 只传 cid + uid;服务端据此生成 aus,多传的字段会被丢掉
146
+ atUsers: matched.map((member) => ({ cid: member.cid, uid: member.uid })),
147
+ };
148
+ }
149
+ /** 把 @ 行插到「各位,…」那句之前;正文里没有那句时附在末尾,避免悄悄丢掉 @。 */
150
+ export function insertMentionLine(text, line) {
151
+ if (!line)
152
+ return text;
153
+ if (text.includes(GREETING))
154
+ return text.replace(GREETING, `${line}\n\n${GREETING}`);
155
+ return `${text}\n${line}`;
156
+ }
157
+ const meetingDecisionNotifyGroup = {
158
+ name: 'meeting.decision.notifyGroup',
159
+ resource: 'decision',
160
+ method: 'notifyGroup',
161
+ action: 'meeting.decision.notifyGroup',
162
+ summary: '把会议的决议任务整理成通知推送到关联群聊:默认只预览不发送,带 confirm:true 才真正发出',
163
+ capability: 'decision',
164
+ risk: 'high-risk-write',
165
+ requiresConfirmation: true,
166
+ inputSchema: {
167
+ type: 'object',
168
+ additionalProperties: false,
169
+ properties: {
170
+ mt_id: { type: 'string', pattern: '^\\d+$', description: '会议 id(必填)' },
171
+ groupId: { type: 'string', description: '群 id;省略时取会议记录的 imchat_groupid' },
172
+ onlyPending: {
173
+ type: 'boolean',
174
+ description: '只推送未完成任务(默认 true)。设为 false 会把已完成项也列出来',
175
+ },
176
+ maxTasks: { type: 'integer', minimum: 1, maximum: 100, description: '消息里最多列多少条任务(默认 20)' },
177
+ title: { type: 'string', description: '消息标题,默认「【会议任务通知】<会议名>」' },
178
+ footer: { type: 'string', description: '消息末尾说明,默认「(本消息由会议助手自动推送)」' },
179
+ mention: {
180
+ type: 'boolean',
181
+ description: '是否 @ 到任务执行人(默认 true)。执行人必须是群成员才能 @ 上,否则只在正文里写名字',
182
+ },
183
+ confirm: {
184
+ type: 'boolean',
185
+ description: '真正发送需要显式传 true;不传只返回预览(群消息对外可见,发出去撤不回)',
186
+ },
187
+ },
188
+ required: ['mt_id'],
189
+ },
190
+ handler: async (args, env) => {
191
+ const startedAt = Date.now();
192
+ const warnings = [];
193
+ const mtId = textOf(args.mt_id).trim();
194
+ if (!mtId)
195
+ throw meetingValidationError('mt_id_required', '必须提供会议 id(mt_id)');
196
+ const { getMeetingOperation } = await import('./registry.js');
197
+ const ctx = env.ctx;
198
+ // ① 会议 → 群 + 展示信息
199
+ const listed = await getMeetingOperation('meeting.eb.meeting.list').handler({ mt_id: mtId, pageNo: 1, pageSize: 1 }, env);
200
+ const meeting = (listed?.data?.rows ?? [])[0]?.mainTable ?? null;
201
+ if (!meeting) {
202
+ throw meetingValidationError('not_found', `未找到会议 id=${mtId},或当前账号无权限查看`, { id: mtId });
203
+ }
204
+ const groupId = textOf(args.groupId).trim() || textOf(meeting.imchat_groupid).trim();
205
+ if (!groupId) {
206
+ throw meetingValidationError('group_missing', `会议「${textOf(meeting.mt_name)}」没有 imchat_groupid,无法推送;请显式传 groupId`, { id: mtId });
207
+ }
208
+ // ② 决议任务
209
+ let taskList;
210
+ try {
211
+ taskList = await getMeetingOperation('meeting.eb.decisionTask.list').handler({ mt_id: mtId, pageNo: 1, pageSize: 100 }, env);
212
+ }
213
+ catch (error) {
214
+ throw meetingValidationError('task_list_failed', `读取决议任务失败:${String(error?.message ?? error).slice(0, 120)}`, { id: mtId });
215
+ }
216
+ // ⚠️ detail1 在 rows[] 顶层,mainTable 里没有 —— 这里两个都兜一下
217
+ const tasks = (taskList?.data?.rows ?? []).map((row) => ({
218
+ ...(row?.mainTable ?? {}),
219
+ ...row,
220
+ }));
221
+ if (tasks.length === 0) {
222
+ return meetingSuccess('meeting.decision.notifyGroup', { id: mtId, groupId, total: 0, sent: false, text: '' }, { warnings: ['该会议还没有决议任务,未推送(先调 meeting.eb.decision.publish 生成任务)'] });
223
+ }
224
+ const buildInput = {
225
+ meetingName: textOf(meeting.mt_name),
226
+ begin: textOf(meeting.begin_datetime),
227
+ end: textOf(meeting.end_datetime),
228
+ tasks,
229
+ progress: taskList?.data?.progress?.summary ?? undefined,
230
+ onlyPending: args.onlyPending !== false,
231
+ maxTasks: typeof args.maxTasks === 'number' ? args.maxTasks : 20,
232
+ title: textOf(args.title),
233
+ footer: textOf(args.footer),
234
+ meetingId: mtId,
235
+ };
236
+ const formatted = formatTaskMessage(buildInput);
237
+ const base = {
238
+ id: mtId,
239
+ groupId,
240
+ total: formatted.total,
241
+ pending: formatted.pending,
242
+ listed: formatted.listed,
243
+ charCount: formatted.text.length,
244
+ text: formatted.text,
245
+ };
246
+ // ②b @ 到执行人:先拿群成员表(uid + cid,atUsers 需要这两个)
247
+ const { createImHost } = await import('../../im/host.js');
248
+ const imHost = createImHost(ctx);
249
+ let displayText = formatted.text;
250
+ let wireText = formatted.text;
251
+ let atUsers = [];
252
+ const mentioned = [];
253
+ if (args.mention !== false) {
254
+ try {
255
+ const { getImOperation } = await import('../../im/operations/registry.js');
256
+ const membersResult = await getImOperation('im.group.users').handler({ groupId }, { ctx, host: imHost });
257
+ const members = (membersResult?.data?.members ?? []).map((item) => ({
258
+ uid: textOf(item?.uid),
259
+ cid: textOf(item?.cid),
260
+ name: textOf(item?.name),
261
+ })).filter((item) => item.uid && item.name);
262
+ if (membersResult?.data?.endFlag === false) {
263
+ warnings.push('群成员较多,只取到部分成员;未在返回里的执行人无法 @');
264
+ }
265
+ const { matched, missing } = matchMentions(formatted.selected, members);
266
+ // 「@ 人」既要有开头那一行,也要让每条任务的「负责人:」变成可点 @
267
+ const uidByName = new Map(matched.map((member) => [member.name.trim(), member.uid]));
268
+ const mentionLine = buildMentionLine(matched);
269
+ displayText = insertMentionLine(formatTaskMessage({ ...buildInput, renderName: (name) => (uidByName.has(name.trim()) ? `@${name}` : name) }).text, mentionLine.displayLine);
270
+ wireText = insertMentionLine(formatTaskMessage({
271
+ ...buildInput,
272
+ renderName: (name) => (uidByName.has(name.trim()) ? `:@${uidByName.get(name.trim())};` : name),
273
+ }).text, mentionLine.wireLine);
274
+ atUsers = mentionLine.atUsers;
275
+ mentioned.push(...matched.map((member) => member.name));
276
+ if (missing.length > 0) {
277
+ warnings.push(`以下执行人不在群成员里(或姓名不一致),只能写在正文、无法 @:${missing.join('、')}`);
278
+ }
279
+ }
280
+ catch (error) {
281
+ warnings.push(`取群成员失败,本次不 @ 人:${String(error?.message ?? error).slice(0, 80)}`);
282
+ }
283
+ }
284
+ base.text = displayText;
285
+ base.charCount = displayText.length;
286
+ base.mentioned = mentioned;
287
+ // ③ 没确认就只回预览(群消息对外可见,默认不擅自发)
288
+ if (args.confirm !== true) {
289
+ return meetingSuccess('meeting.decision.notifyGroup', { ...base, sent: false, preview: true }, {
290
+ warnings: [
291
+ '这是预览,**尚未发送**。确认内容无误后带 confirm:true 再调一次才会真正发到群里',
292
+ `目标群:${groupId}`,
293
+ ...(mentioned.length > 0
294
+ ? [`将 @ ${mentioned.length} 人:${mentioned.join('、')}(发送时用 uid 占位符,预览显示为姓名)`]
295
+ : []),
296
+ ],
297
+ });
298
+ }
299
+ // ④ 真正发送
300
+ // @ 必须靠请求体的 `atUsers`(服务端据此生成 dt[0].txt.aus 并渲染成蓝色可点),
301
+ // 自己拼 aus/dt 会被丢掉;所以这里直接调 IM 发送端点,而不是只支持纯文本的 `im.msg.send.group`。
302
+ let sent = false;
303
+ try {
304
+ const body = { groupId, txt: wireText };
305
+ if (atUsers.length > 0)
306
+ body.atUsers = atUsers;
307
+ const res = await imHost.request({
308
+ method: 'POST',
309
+ path: '/api/em/msg/executeIm/send/sendGroupMsg',
310
+ body,
311
+ operation: 'meeting.decision.notifyGroup',
312
+ });
313
+ const action = res?.data?.actionMsg ?? {};
314
+ if (Number(action.code) !== 0) {
315
+ throw new Error(`服务端拒绝:${textOf(action.message) || '未知原因'}`);
316
+ }
317
+ sent = true;
318
+ }
319
+ catch (error) {
320
+ throw meetingValidationError('send_failed', `发送群消息失败:${String(error?.message ?? error).slice(0, 160)}`, { id: mtId, groupId });
321
+ }
322
+ return meetingSuccess('meeting.decision.notifyGroup', { ...base, sent, preview: false, durationMs: Date.now() - startedAt }, warnings.length > 0 ? { warnings } : undefined);
323
+ },
324
+ };
325
+ export const meetingDecisionNotifyOperations = [meetingDecisionNotifyGroup];
@@ -1,5 +1,32 @@
1
+ import { appendFile, mkdir } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
1
3
  import { getMeetingOperation } from './operations/registry.js';
2
4
  import { assertNoForbiddenKeys, asObject } from './operations/shared.js';
5
+ import { meetingStateDir } from './operations/env.js';
6
+ /**
7
+ * 调用流水(best-effort):每次 operation 追加一行 JSON 到 `<state>/meeting/logs/calls.jsonl`。
8
+ *
9
+ * 用途:用户反馈"建个会要很久"时,先看**到底调了几次、每次多久**,
10
+ * 而不是靠猜(是 CLI 慢,还是对话里多轮往返)。写入失败一律忽略,绝不影响业务。
11
+ */
12
+ function recordCall(context, operation, startedAt, ok, errorType) {
13
+ try {
14
+ const file = join(meetingStateDir(context.ctx), 'logs', 'calls.jsonl');
15
+ const line = JSON.stringify({
16
+ at: new Date().toISOString(),
17
+ operation,
18
+ ms: Date.now() - startedAt,
19
+ ok,
20
+ ...(errorType ? { errorType } : {}),
21
+ });
22
+ void mkdir(dirname(file), { recursive: true })
23
+ .then(() => appendFile(file, `${line}\n`, 'utf8'))
24
+ .catch(() => undefined);
25
+ }
26
+ catch {
27
+ // 记日志失败不影响主流程
28
+ }
29
+ }
3
30
  export async function executeMeetingOperation(operation, rawInput, context) {
4
31
  const definition = getMeetingOperation(operation);
5
32
  const input = asObject(rawInput);
@@ -14,6 +41,20 @@ export async function executeMeetingOperation(operation, rawInput, context) {
14
41
  getSession() {
15
42
  return context.host.getSession();
16
43
  },
44
+ // 二进制下载必须透传:漏掉它会让 meeting.attachment.download/extract 报"host 不支持下载"
45
+ // (包装 host 只挑两个方法时最容易被漏掉,2026-10-28 真机踩过)。
46
+ ...(context.host.download
47
+ ? { download: (request) => context.host.download(request) }
48
+ : {}),
17
49
  };
18
- return definition.handler(input, { ...context, host });
50
+ const startedAt = Date.now();
51
+ try {
52
+ const envelope = await definition.handler(input, { ...context, host });
53
+ recordCall(context, operation, startedAt, true);
54
+ return envelope;
55
+ }
56
+ catch (error) {
57
+ recordCall(context, operation, startedAt, false, String(error?.type ?? 'unknown'));
58
+ throw error;
59
+ }
19
60
  }