weaver-work-cli 0.1.4 → 0.1.5

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 (87) hide show
  1. package/dist/internal/e10/request-runtime.js +3 -2
  2. package/dist/shortcuts/index.js +4 -0
  3. package/dist/shortcuts/meeting/continuation.js +70 -0
  4. package/dist/shortcuts/meeting/errors.js +63 -0
  5. package/dist/shortcuts/meeting/host.js +167 -0
  6. package/dist/shortcuts/meeting/index.js +123 -0
  7. package/dist/shortcuts/meeting/manifest.js +64 -0
  8. package/dist/shortcuts/meeting/operations/catalogs.js +58 -0
  9. package/dist/shortcuts/meeting/operations/change.js +242 -0
  10. package/dist/shortcuts/meeting/operations/conflicts.js +142 -0
  11. package/dist/shortcuts/meeting/operations/create.js +349 -0
  12. package/dist/shortcuts/meeting/operations/env.js +191 -0
  13. package/dist/shortcuts/meeting/operations/lifecycle.js +139 -0
  14. package/dist/shortcuts/meeting/operations/meetings.js +73 -0
  15. package/dist/shortcuts/meeting/operations/receipts.js +195 -0
  16. package/dist/shortcuts/meeting/operations/registry.js +47 -0
  17. package/dist/shortcuts/meeting/operations/rooms.js +132 -0
  18. package/dist/shortcuts/meeting/operations/shared.js +112 -0
  19. package/dist/shortcuts/meeting/operations/signs.js +41 -0
  20. package/dist/shortcuts/meeting/operations/types.js +25 -0
  21. package/dist/shortcuts/meeting/operations.js +19 -0
  22. package/dist/shortcuts/yimiaoban/continuation.js +70 -0
  23. package/dist/shortcuts/yimiaoban/errors.js +61 -0
  24. package/dist/shortcuts/yimiaoban/host.js +469 -0
  25. package/dist/shortcuts/yimiaoban/index.js +271 -0
  26. package/dist/shortcuts/yimiaoban/manifest.js +28 -0
  27. package/dist/shortcuts/yimiaoban/operations/file-user.js +288 -0
  28. package/dist/shortcuts/yimiaoban/operations/group-read.js +477 -0
  29. package/dist/shortcuts/yimiaoban/operations/group-write.js +591 -0
  30. package/dist/shortcuts/yimiaoban/operations/msg-parse.js +429 -0
  31. package/dist/shortcuts/yimiaoban/operations/msg-sync.js +519 -0
  32. package/dist/shortcuts/yimiaoban/operations/msg-write.js +593 -0
  33. package/dist/shortcuts/yimiaoban/operations/person.js +102 -0
  34. package/dist/shortcuts/yimiaoban/operations/registry.js +38 -0
  35. package/dist/shortcuts/yimiaoban/operations/session.js +437 -0
  36. package/dist/shortcuts/yimiaoban/operations/shared.js +236 -0
  37. package/dist/shortcuts/yimiaoban/operations/types.js +27 -0
  38. package/dist/shortcuts/yimiaoban/operations/write-util.js +78 -0
  39. package/dist/shortcuts/yimiaoban/operations.js +8 -0
  40. package/docs/_catalog.md +6 -0
  41. package/docs/meeting.md +48 -0
  42. package/docs/yimiaoban.md +86 -0
  43. package/package.json +1 -1
  44. package/skill-template/business-info.json +4 -1
  45. package/skill-template/domains/meeting.md +3 -0
  46. package/skill-template/domains/yimiaoban.md +21 -0
  47. package/skills/weaver-e10-calendar/SKILL.md +1 -0
  48. package/skills/weaver-e10-esb/SKILL.md +1 -0
  49. package/skills/weaver-e10-esb/references/source-manifest.json +3 -3
  50. package/skills/weaver-e10-hrm/SKILL.md +1 -0
  51. package/skills/weaver-e10-jiuchuanhui/SKILL.md +1 -0
  52. package/skills/weaver-e10-jucailin/SKILL.md +1 -0
  53. package/skills/weaver-e10-mail/SKILL.md +1 -0
  54. package/skills/weaver-e10-meeting/SKILL.md +106 -0
  55. package/skills/weaver-e10-meeting/product.json +8 -0
  56. package/skills/weaver-e10-meeting/references/meeting-change.md +67 -0
  57. package/skills/weaver-e10-meeting/references/meeting-create.md +52 -0
  58. package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +57 -0
  59. package/skills/weaver-e10-meeting/references/meeting-queries.md +61 -0
  60. package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +48 -0
  61. package/skills/weaver-e10-meeting/references/meeting-update.md +7 -0
  62. package/skills/weaver-e10-meeting/references/source-manifest.json +50 -0
  63. package/skills/weaver-e10-plan/SKILL.md +1 -0
  64. package/skills/weaver-e10-plan/references/source-manifest.json +3 -3
  65. package/skills/weaver-e10-qiyecheng/SKILL.md +1 -0
  66. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +3 -3
  67. package/skills/weaver-e10-shared/SKILL.md +4 -0
  68. package/skills/weaver-e10-skill-maker/SKILL.md +1 -0
  69. package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +1 -1
  70. package/skills/weaver-e10-wenshuding/SKILL.md +1 -0
  71. package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
  72. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +5 -5
  73. package/skills/weaver-e10-yimiaoban/SKILL.md +104 -0
  74. package/skills/weaver-e10-yimiaoban/product.json +8 -0
  75. package/skills/weaver-e10-yimiaoban/references/ding-write.md +68 -0
  76. package/skills/weaver-e10-yimiaoban/references/error-codes.md +85 -0
  77. package/skills/weaver-e10-yimiaoban/references/field-resolution.md +46 -0
  78. package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +54 -0
  79. package/skills/weaver-e10-yimiaoban/references/group-read.md +55 -0
  80. package/skills/weaver-e10-yimiaoban/references/group-write.md +72 -0
  81. package/skills/weaver-e10-yimiaoban/references/msg-read.md +59 -0
  82. package/skills/weaver-e10-yimiaoban/references/msg-write.md +64 -0
  83. package/skills/weaver-e10-yimiaoban/references/person.md +44 -0
  84. package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +61 -0
  85. package/skills/weaver-e10-yimiaoban/references/session-sysmsg.md +47 -0
  86. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +485 -0
  87. package/skills/weaver-e10-ziguanjia/SKILL.md +1 -0
@@ -0,0 +1,593 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { ymbValidationError } from '../errors.js';
4
+ import { schemaOf, } from './types.js';
5
+ import { assertImOk, compact, extractGroupMembers, str } from './shared.js';
6
+ import { fileObjToFile, fileObjToImg, makeWritePair } from './write-util.js';
7
+ /** 校验本地文件存在,返回绝对路径。 */
8
+ function assertLocalFile(pathValue, field) {
9
+ const abs = resolve(String(pathValue ?? ''));
10
+ if (!existsSync(abs))
11
+ throw ymbValidationError('file_not_found', `${field} 文件不存在: ${abs}`);
12
+ return abs;
13
+ }
14
+ /**
15
+ * 单聊/群聊发送内容归一化:txt / img / file 至少一种。
16
+ * 🔴 全空白文本(空格 / 换行)不算内容,禁止发送空白消息。
17
+ */
18
+ function normalizeContent(args) {
19
+ const txtRaw = args.txt !== undefined && args.txt !== null ? String(args.txt) : '';
20
+ const txt = txtRaw.trim() !== '' ? txtRaw : undefined;
21
+ const imgPath = args.img || args.imgPath ? String(args.img || args.imgPath) : undefined;
22
+ const filePath = args.file || args.filePath ? String(args.file || args.filePath) : undefined;
23
+ if (!txt && !imgPath && !filePath) {
24
+ throw ymbValidationError('content_required', 'txt / img / file 至少提供一种(空白文本不算内容,禁止发送空白消息)');
25
+ }
26
+ if (imgPath && filePath) {
27
+ throw ymbValidationError('content_conflict', 'img 与 file 不能同时提供(一次发一种媒体)');
28
+ }
29
+ return { txt, imgPath, filePath };
30
+ }
31
+ // ── 发单聊 ──
32
+ const sendSinglePair = makeWritePair({
33
+ action: 'yimiaoban.msg.send.single',
34
+ async plan(args, env) {
35
+ const toUid = str(args.toUid);
36
+ const toCid = str(args.toCid);
37
+ if (!toUid || !toCid || toUid === '0' || toCid === '0') {
38
+ throw ymbValidationError('id_invalid', 'toUid/toCid 必须非 0');
39
+ }
40
+ const content = normalizeContent(args);
41
+ const summary = {
42
+ action: '发送单聊消息',
43
+ toUid,
44
+ toCid,
45
+ hasTxt: Boolean(content.txt),
46
+ hasImg: Boolean(content.imgPath),
47
+ hasFile: Boolean(content.filePath),
48
+ };
49
+ const snapshot = { toUid, toCid };
50
+ if (content.txt) {
51
+ snapshot.txt = content.txt;
52
+ summary.textPreview = content.txt.length > 60 ? `${content.txt.slice(0, 60)}…` : content.txt;
53
+ }
54
+ if (content.imgPath) {
55
+ const abs = assertLocalFile(content.imgPath, 'img');
56
+ snapshot.imgPath = abs;
57
+ summary.fileName = abs.split(/[\\/]/).pop();
58
+ }
59
+ if (content.filePath) {
60
+ const abs = assertLocalFile(content.filePath, 'file');
61
+ snapshot.filePath = abs;
62
+ summary.fileName = abs.split(/[\\/]/).pop();
63
+ }
64
+ return { summary, snapshot };
65
+ },
66
+ async execute(plan, env) {
67
+ const body = { reciver: { cid: plan.toCid, uid: plan.toUid } };
68
+ if (plan.txt !== undefined)
69
+ body.txt = plan.txt;
70
+ if (plan.imgPath) {
71
+ const fileObj = await env.host.uploadFile(plan.imgPath, { shareUsers: plan.toUid });
72
+ body.img = fileObjToImg(fileObj);
73
+ }
74
+ if (plan.filePath) {
75
+ const fileObj = await env.host.uploadFile(plan.filePath, { shareUsers: plan.toUid });
76
+ body.file = fileObjToFile(fileObj);
77
+ }
78
+ const res = await env.host.request({
79
+ method: 'POST',
80
+ path: '/api/em/msg/executeIm/send/sendSingleMsg',
81
+ body,
82
+ operation: 'yimiaoban.msg.send.single',
83
+ });
84
+ assertImOk(res, '发送单聊消息');
85
+ return { toUid: plan.toUid, toCid: plan.toCid, sent: true };
86
+ },
87
+ });
88
+ // ── 发群聊 ──
89
+ const sendGroupPair = makeWritePair({
90
+ action: 'yimiaoban.msg.send.group',
91
+ async plan(args, env) {
92
+ const groupId = str(args.groupId);
93
+ if (!groupId || groupId === '0')
94
+ throw ymbValidationError('id_invalid', 'groupId 必须非 0');
95
+ const content = normalizeContent(args);
96
+ const summary = { action: '发送群聊消息', groupId, hasTxt: Boolean(content.txt), hasImg: Boolean(content.imgPath), hasFile: Boolean(content.filePath) };
97
+ const snapshot = { groupId };
98
+ if (content.txt) {
99
+ snapshot.txt = content.txt;
100
+ summary.textPreview = content.txt.length > 60 ? `${content.txt.slice(0, 60)}…` : content.txt;
101
+ }
102
+ if (content.imgPath) {
103
+ const abs = assertLocalFile(content.imgPath, 'img');
104
+ snapshot.imgPath = abs;
105
+ summary.fileName = abs.split(/[\\/]/).pop();
106
+ }
107
+ if (content.filePath) {
108
+ const abs = assertLocalFile(content.filePath, 'file');
109
+ snapshot.filePath = abs;
110
+ summary.fileName = abs.split(/[\\/]/).pop();
111
+ }
112
+ return { summary, snapshot };
113
+ },
114
+ async execute(plan, env) {
115
+ const body = { groupId: plan.groupId };
116
+ if (plan.txt !== undefined)
117
+ body.txt = plan.txt;
118
+ if (plan.imgPath) {
119
+ const fileObj = await env.host.uploadFile(plan.imgPath, { shareGroups: plan.groupId });
120
+ body.img = fileObjToImg(fileObj);
121
+ }
122
+ if (plan.filePath) {
123
+ const fileObj = await env.host.uploadFile(plan.filePath, { shareGroups: plan.groupId });
124
+ body.file = fileObjToFile(fileObj);
125
+ }
126
+ const res = await env.host.request({
127
+ method: 'POST',
128
+ path: '/api/em/msg/executeIm/send/sendGroupMsg',
129
+ body,
130
+ operation: 'yimiaoban.msg.send.group',
131
+ });
132
+ assertImOk(res, '发送群聊消息');
133
+ return { groupId: plan.groupId, sent: true };
134
+ },
135
+ });
136
+ // ── 撤回消息 ──
137
+ /** 递归查找撤回时限字段(time_out,接口返回,单位秒)。 */
138
+ function findWithdrawTimeOut(value) {
139
+ if (!value || typeof value !== 'object')
140
+ return null;
141
+ if (value.time_out !== undefined && value.time_out !== null && String(value.time_out) !== '') {
142
+ return String(value.time_out);
143
+ }
144
+ for (const key of Object.keys(value)) {
145
+ const found = findWithdrawTimeOut(value[key]);
146
+ if (found !== null)
147
+ return found;
148
+ }
149
+ return null;
150
+ }
151
+ /** 撤回时限展示:秒 → 「X 分钟 Y 秒」(>=60 秒时)。 */
152
+ function formatWithdrawInterval(seconds) {
153
+ const total = parseInt(seconds, 10);
154
+ if (!Number.isFinite(total) || total <= 0)
155
+ return `${seconds} 秒`;
156
+ if (total < 60)
157
+ return `${total} 秒`;
158
+ const minutes = Math.floor(total / 60);
159
+ const rest = total % 60;
160
+ return rest > 0 ? `${minutes} 分钟 ${rest} 秒` : `${minutes} 分钟`;
161
+ }
162
+ const withdrawPair = makeWritePair({
163
+ action: 'yimiaoban.msg.withdraw',
164
+ async plan(args, env) {
165
+ const type = String(args.type || 'single');
166
+ if (type !== 'single' && type !== 'group')
167
+ throw ymbValidationError('type_invalid', 'type 必须是 single 或 group');
168
+ const msgid = str(args.msgid);
169
+ if (!msgid || msgid === '0')
170
+ throw ymbValidationError('id_invalid', 'msgid 必须非 0');
171
+ const snapshot = { type, msgid };
172
+ let source;
173
+ if (type === 'single') {
174
+ const toUid = str(args.toUid);
175
+ const toCid = str(args.toCid);
176
+ if (!toUid || !toCid || toUid === '0' || toCid === '0')
177
+ throw ymbValidationError('id_invalid', 'toUid/toCid 必须非 0(撤回单聊消息)');
178
+ snapshot.toUid = toUid;
179
+ snapshot.toCid = toCid;
180
+ source = { flag: 0, user: { cid: toCid, uid: toUid } };
181
+ }
182
+ else {
183
+ const groupId = str(args.groupId);
184
+ if (!groupId || groupId === '0')
185
+ throw ymbValidationError('id_invalid', 'groupId 必须非 0');
186
+ snapshot.groupId = groupId;
187
+ source = { flag: 1, group_id: groupId };
188
+ }
189
+ return {
190
+ summary: { action: type === 'single' ? '撤回单聊消息' : '撤回群聊消息', msgid, source },
191
+ snapshot: { ...snapshot, source },
192
+ };
193
+ },
194
+ async execute(plan, env) {
195
+ // 撤回体需操作者自身 cid/uid/姓名(auth 非 0)
196
+ const self = await env.host.getSelfIdentity();
197
+ const name = self.name || self.uid || '我';
198
+ const withdrawMsg = JSON.stringify({
199
+ display: '0',
200
+ dt: [{ txt: { id18n: '103838', kvs: { '%NAME%': name, '%UNAME%': '' }, ot: '1', ou: { c: self.cid, u: self.uid }, v: `${name}撤回了一条消息` } }],
201
+ scid: self.cid,
202
+ suid: self.uid,
203
+ sname: name,
204
+ type: '10',
205
+ ver: '1.0',
206
+ });
207
+ const body = {
208
+ source: plan.source,
209
+ msgid: plan.msgid,
210
+ dev: 5,
211
+ msg: withdrawMsg,
212
+ title: withdrawMsg,
213
+ trans_id: String(Math.floor(Date.now() / 1000)),
214
+ sensitive_confirm: '0',
215
+ };
216
+ const res = await env.host.request({
217
+ method: 'POST',
218
+ path: '/api/em/msg/executeIm/msg/makeMsgWithdraw',
219
+ body,
220
+ operation: 'yimiaoban.msg.withdraw',
221
+ });
222
+ const actCode = Number(res?.data?.actionMsg?.code ?? res?.code ?? -1);
223
+ const timeOut = findWithdrawTimeOut(res);
224
+ const interval = timeOut !== null ? formatWithdrawInterval(timeOut) : '';
225
+ if (actCode === 1505) {
226
+ throw ymbValidationError('withdraw_timeout', `撤销超时${interval ? `(撤回时限 ${interval})` : '(发出超过约 2 分钟)'}:消息发送已超过可撤回时限,无法撤回`, { code: 1505, details: interval ? { withdrawInterval: interval } : undefined });
227
+ }
228
+ if (actCode === 1506)
229
+ throw ymbValidationError('withdraw_not_owner', '消息不是自己发送的,无法撤回', { code: 1506 });
230
+ if (actCode === 1507)
231
+ throw ymbValidationError('withdraw_edit_timeout', '编辑撤回超时', { code: 1507 });
232
+ if (actCode !== 0)
233
+ throw ymbValidationError('withdraw_failed', `撤回失败(code=${actCode})`, { code: actCode });
234
+ return {
235
+ msgid: plan.msgid,
236
+ withdrawn: true,
237
+ ...(interval ? { withdrawInterval: interval, withdrawIntervalSeconds: Number(timeOut) } : {}),
238
+ };
239
+ },
240
+ });
241
+ // ── 群置顶/取消置顶 ──
242
+ const topSetPair = makeWritePair({
243
+ action: 'yimiaoban.msg.top.set',
244
+ async plan(args, env) {
245
+ const groupId = str(args.groupId);
246
+ if (!groupId || groupId === '0')
247
+ throw ymbValidationError('id_invalid', 'groupId 必须非 0');
248
+ const msgid = str(args.msgid);
249
+ if (!msgid || msgid === '0')
250
+ throw ymbValidationError('id_invalid', 'msgid 必须非 0');
251
+ let type;
252
+ if (args.cancel === true)
253
+ type = 2;
254
+ else if (args.type !== undefined) {
255
+ type = Number(args.type);
256
+ if (type !== 1 && type !== 2)
257
+ throw ymbValidationError('type_invalid', 'type 必须是 1(置顶)或 2(取消置顶)');
258
+ }
259
+ else
260
+ type = 1;
261
+ return {
262
+ summary: { action: type === 1 ? '群消息置顶' : '取消群消息置顶', groupId, msgid },
263
+ snapshot: { groupId, msgid, type },
264
+ };
265
+ },
266
+ async execute(plan, env) {
267
+ const body = { group_id: plan.groupId, type: plan.type, msgid: plan.msgid };
268
+ const res = await env.host.request({
269
+ method: 'POST',
270
+ path: '/api/em/msg/executeIm/msg/setGroupTopMsg',
271
+ body,
272
+ operation: 'yimiaoban.msg.top.set',
273
+ });
274
+ assertImOk(res, plan.type === 1 ? '置顶消息' : '取消置顶');
275
+ return { groupId: plan.groupId, msgid: plan.msgid, type: plan.type };
276
+ },
277
+ });
278
+ // ── 必达 sendDing ──
279
+ const dingSendPair = makeWritePair({
280
+ action: 'yimiaoban.ding.send',
281
+ async plan(args, env) {
282
+ const groupId = args.groupId !== undefined && args.groupId !== '' ? str(args.groupId) : '';
283
+ const toUid = args.toUid !== undefined && args.toUid !== '' ? str(args.toUid) : '';
284
+ const convertMsgid = args.convertMsgid !== undefined && args.convertMsgid !== '' ? str(args.convertMsgid) : '';
285
+ if (!groupId && !toUid)
286
+ throw ymbValidationError('target_required', 'groupId(群必达)与 toUid(单聊必达)至少其一');
287
+ if (groupId && groupId === '0')
288
+ throw ymbValidationError('id_invalid', 'groupId 必须非 0');
289
+ const txtRaw = args.txt !== undefined && args.txt !== null ? String(args.txt) : '';
290
+ const txt = txtRaw.trim() !== '' ? txtRaw : '';
291
+ if (!convertMsgid && !txt)
292
+ throw ymbValidationError('content_required', '普通必达需 txt;转必达需 convertMsgid');
293
+ if (args.unreadOnly === true && !groupId) {
294
+ throw ymbValidationError('unread_only_group_only', 'unreadOnly 仅支持群聊转必达(需提供 groupId)');
295
+ }
296
+ if (!groupId && convertMsgid && !str(args.toCid)) {
297
+ throw ymbValidationError('to_cid_required', '单聊转必达需提供 toCid(用于按消息 id 拉取源消息)');
298
+ }
299
+ const summary = {
300
+ action: convertMsgid ? '已有消息转必达' : '发送必达消息',
301
+ targetType: groupId ? '群聊' : '单聊',
302
+ ...(groupId ? { groupId } : { toUid }),
303
+ ...(convertMsgid ? { convertMsgid } : { textPreview: txt.length > 60 ? `${txt.slice(0, 60)}…` : txt }),
304
+ sendMsg: args.sendMsg !== undefined ? Number(args.sendMsg) : 1,
305
+ showType: args.showType !== undefined ? Number(args.showType) : 0,
306
+ };
307
+ const snapshot = {
308
+ groupId: groupId || undefined,
309
+ toUid: toUid || undefined,
310
+ toCid: args.toCid !== undefined ? str(args.toCid) : undefined,
311
+ txt: txt || undefined,
312
+ convertMsgid: convertMsgid || undefined,
313
+ unreadOnly: args.unreadOnly === true,
314
+ toUids: args.toUids !== undefined ? (Array.isArray(args.toUids) ? args.toUids.map((x) => String(x)) : String(args.toUids).split(',').map((s) => s.trim()).filter(Boolean)) : undefined,
315
+ sendMsg: args.sendMsg !== undefined ? Number(args.sendMsg) : 1,
316
+ showType: args.showType !== undefined ? Number(args.showType) : 0,
317
+ removeFailed: args.removeFailed === true,
318
+ };
319
+ // 群场景:接收人集合在 prepare(只读)阶段确定并进入确认摘要,写阶段直接使用,
320
+ // 避免「用户确认后才发现没有可发对象」,也与源 sendDing.js 的先算接收人再发送一致。
321
+ if (groupId) {
322
+ const self = await env.host.getSelfIdentity();
323
+ const myUid = self.uid || (await env.host.getSession()).userId;
324
+ const requested = Array.isArray(snapshot.toUids)
325
+ ? [...new Set(snapshot.toUids.map((x) => String(x)))]
326
+ : [];
327
+ const groupUsers = [];
328
+ let receiverSource = '全体成员';
329
+ if (requested.length) {
330
+ receiverSource = '指定人员';
331
+ const map = await env.host.resolveUsersByIds(requested);
332
+ const failed = requested.filter((uid) => !map[uid]?.cid || map[uid].cid === '0');
333
+ if (failed.length && snapshot.removeFailed !== true) {
334
+ throw ymbValidationError('members_resolve_failed', `指定接收人信息获取失败(cid 缺失或为 0):${failed.map((uid) => map[uid]?.name || uid).join('、')};确认跳过请传 removeFailed:true`, { details: failed });
335
+ }
336
+ for (const uid of requested) {
337
+ const cid = map[uid]?.cid;
338
+ if (!cid || cid === '0' || uid === myUid)
339
+ continue;
340
+ groupUsers.push({ cid, uid });
341
+ }
342
+ }
343
+ else if (snapshot.unreadOnly === true) {
344
+ receiverSource = '仅未读人员';
345
+ const readRes = await env.host.request({
346
+ method: 'POST',
347
+ path: '/api/em/msg/executeIm/group/syncGroupReadStatus',
348
+ body: { trans_id: 0, group_id: groupId, msgid: convertMsgid || '0' },
349
+ operation: 'yimiaoban.ding.send.unread',
350
+ });
351
+ const readDatas = readRes?.data?.data?.datas || [];
352
+ const unread = readDatas.filter((d) => Number(d.status) === 0);
353
+ const uidArr = [
354
+ ...new Set(unread.map((d) => String(d?.user?.uid)).filter((uid) => Boolean(uid) && uid !== myUid)),
355
+ ];
356
+ if (uidArr.length === 0) {
357
+ throw ymbValidationError('unread_empty', '群内没有未读人员(unreadOnly=true 不自动回退全员):请确认改发全体成员、指定人员,或放弃发送');
358
+ }
359
+ const map = await env.host.resolveUsersByIds(uidArr);
360
+ for (const uid of uidArr) {
361
+ const cid = map[uid]?.cid;
362
+ if (cid && cid !== '0')
363
+ groupUsers.push({ cid, uid });
364
+ }
365
+ }
366
+ else {
367
+ const memRes = await env.host.request({
368
+ method: 'POST',
369
+ path: '/api/em/msg/executeIm/group/getGroupUsers',
370
+ body: { group_id: groupId, type: 0, sync_type: 0, member_mask: 6, client_uc: '0' },
371
+ operation: 'yimiaoban.ding.send.members',
372
+ });
373
+ const uidArr = [...new Set(extractGroupMembers(memRes?.data?.data)
374
+ .map((m) => String(m?.uid))
375
+ .filter((uid) => uid && uid !== myUid))];
376
+ const map = await env.host.resolveUsersByIds(uidArr);
377
+ for (const uid of uidArr) {
378
+ const cid = map[uid]?.cid;
379
+ if (cid && cid !== '0')
380
+ groupUsers.push({ cid, uid });
381
+ }
382
+ }
383
+ if (groupUsers.length === 0) {
384
+ throw ymbValidationError('members_empty', '群内没有可接收必达消息的成员(已排除本人;指定/未读人员可能为空)');
385
+ }
386
+ snapshot.groupUsers = groupUsers;
387
+ snapshot.receiveUserIds = groupUsers.map((u) => u.uid).join(',');
388
+ summary.receiverSource = receiverSource;
389
+ summary.receiverCount = groupUsers.length;
390
+ }
391
+ else {
392
+ summary.receiverSource = '单聊对方';
393
+ summary.receiverCount = 1;
394
+ }
395
+ return { summary, snapshot };
396
+ },
397
+ async execute(plan, env) {
398
+ const self = await env.host.getSelfIdentity();
399
+ const session = await env.host.getSession();
400
+ const myUid = self.uid || session.userId;
401
+ const myCid = self.cid;
402
+ const myName = self.name || myUid;
403
+ let receiveUserIds = '';
404
+ let targetType;
405
+ let targetId = '';
406
+ let mkind;
407
+ let msgEntity;
408
+ let msgTitle;
409
+ let groupUsers;
410
+ let fileIds = '';
411
+ let extraInfo = '';
412
+ let content = '';
413
+ if (plan.convertMsgid) {
414
+ // 转必达:先取源消息
415
+ const sourceReq = plan.groupId
416
+ ? { trans_id: 0, dev: 4, type: 0, enc_type: 0, datas: [{ group_id: plan.groupId, start: plan.convertMsgid, end: '0', max_num: 1 }] }
417
+ : { trans_id: 0, dev: 4, enc_type: 0, datas: [{ from_cid: plan.toCid, from_uid: plan.toUid, start: plan.convertMsgid, end: '0', max_num: 1 }] };
418
+ const sourcePath = plan.groupId ? '/api/em/msg/executeIm/chat/syncGroupMsg' : '/api/em/msg/executeIm/msg/syncSingleMsg';
419
+ const srcRes = await env.host.request({ method: 'POST', path: sourcePath, body: sourceReq, operation: 'yimiaoban.ding.send.source' });
420
+ const srcMsg = srcRes?.data?.data?.datas?.[0]?.datas?.[0] ?? srcRes?.data?.data?.datas?.[0];
421
+ if (!srcMsg?.msg)
422
+ throw ymbValidationError('source_not_found', `未找到源消息 ${plan.convertMsgid}`);
423
+ let msgObj;
424
+ try {
425
+ msgObj = typeof srcMsg.msg === 'string' ? JSON.parse(srcMsg.msg) : srcMsg.msg;
426
+ }
427
+ catch {
428
+ throw ymbValidationError('source_invalid', '源消息内容无法解析');
429
+ }
430
+ const mtype = Number(msgObj.type ?? 0);
431
+ let tag;
432
+ let titleDt;
433
+ const nm = msgObj.dt?.[0]?.file?.nm || msgObj.dt?.[0]?.img?.nm || msgObj.dt?.[0]?.media?.nm || '';
434
+ if (mtype === 4) {
435
+ tag = '2';
436
+ content = `[文件]${nm}`;
437
+ mkind = 512;
438
+ titleDt = [{ txt: { v: nm } }];
439
+ fileIds = String(msgObj.dt?.[0]?.file?.id || '');
440
+ }
441
+ else if (mtype === 2) {
442
+ tag = '0';
443
+ content = '[图片]';
444
+ mkind = 64;
445
+ titleDt = [];
446
+ fileIds = String(msgObj.dt?.[0]?.img?.src || msgObj.dt?.[0]?.img?.id || '');
447
+ }
448
+ else if (mtype === 7) {
449
+ tag = '1';
450
+ content = '[视频]';
451
+ mkind = 2048;
452
+ titleDt = [];
453
+ fileIds = String(msgObj.dt?.[0]?.media?.src || msgObj.dt?.[0]?.media?.id || '');
454
+ }
455
+ else {
456
+ tag = '3';
457
+ content = msgObj.dt?.[0]?.txt?.v || '';
458
+ mkind = 1;
459
+ titleDt = [{ txt: { v: content } }];
460
+ }
461
+ msgEntity = JSON.stringify({ ...msgObj, tf: '1' });
462
+ msgTitle = JSON.stringify({ sname: myName, suid: myUid, scid: myCid, display: '0', type: String(mtype), dt: titleDt, vv: '', des: '', tf: '1' });
463
+ extraInfo = JSON.stringify({ msgSource: { source: { tag, msgid: String(plan.convertMsgid), flag: plan.groupId ? 1 : 0, ...(plan.groupId ? { group_id: plan.groupId } : { user: { cid: plan.toCid, uid: plan.toUid } }) } } });
464
+ }
465
+ else {
466
+ // 普通必达
467
+ content = plan.txt;
468
+ mkind = 1;
469
+ msgEntity = JSON.stringify({
470
+ ver: '1.0', dev: 5, guid: '0', cnt: '0', idx: '0',
471
+ sname: myName, suid: myUid, scid: myCid, display: '', sysId: '', weaOaId: '', bgc: '',
472
+ type: '1', dt: [{ txt: { v: plan.txt, next: '0', ft: 'china' } }], tf: '1',
473
+ });
474
+ msgTitle = JSON.stringify({ sname: myName, suid: myUid, scid: myCid, display: '0', type: '1', dt: [{ txt: { v: plan.txt, next: '0', ft: 'china' } }], vv: '', des: '', tf: '1' });
475
+ }
476
+ // 接收人
477
+ if (plan.groupId) {
478
+ targetType = 2;
479
+ targetId = plan.groupId;
480
+ // 接收人已在 prepare 阶段解析并写入快照(用户确认的就是这批人)
481
+ groupUsers = Array.isArray(plan.groupUsers) ? plan.groupUsers : [];
482
+ receiveUserIds = str(plan.receiveUserIds) || groupUsers.map((u) => u.uid).join(',');
483
+ if (!receiveUserIds)
484
+ throw ymbValidationError('members_empty', '群内没有可接收必达消息的成员');
485
+ }
486
+ else {
487
+ targetType = 1;
488
+ const toUid = plan.toUid;
489
+ targetId = `${myUid}|${toUid}`;
490
+ receiveUserIds = toUid;
491
+ }
492
+ const body = compact({
493
+ content,
494
+ receiveUserIds,
495
+ fileIds,
496
+ sendMsg: plan.sendMsg,
497
+ showType: plan.showType,
498
+ mkind,
499
+ msgEntity,
500
+ msgTitle,
501
+ targetType,
502
+ targetId,
503
+ groupUsers,
504
+ extraInfo: extraInfo || undefined,
505
+ });
506
+ const res = await env.host.request({
507
+ method: 'POST',
508
+ path: '/api/em/msg/createDing',
509
+ body,
510
+ operation: 'yimiaoban.ding.send',
511
+ });
512
+ const code = Number(res?.code ?? -1);
513
+ if (code !== 200 || !res?.status) {
514
+ throw ymbValidationError('ding_failed', `必达发送失败:code=${code} msg=${res?.msg ?? ''}`, { code });
515
+ }
516
+ return { id: str(res?.data?.id ?? ''), msg: res?.data?.msg ?? '' };
517
+ },
518
+ });
519
+ function buildMsgWriteDefinition(name, method, summary, inputProps, pair) {
520
+ const requiredInput = Object.keys(inputProps).filter((k) => {
521
+ const prop = inputProps[k];
522
+ return prop?.required === true;
523
+ });
524
+ return [
525
+ {
526
+ name: `${name}.prepare`,
527
+ resource: 'msg',
528
+ method: `${method}.prepare`,
529
+ action: `${name}.prepare`,
530
+ summary: `${summary}(prepare:只读校验 + 差异摘要 + 签发确认)`,
531
+ capability: 'msg',
532
+ risk: 'read-before-write',
533
+ requiresConfirmation: true,
534
+ inputSchema: schemaOf(inputProps, { required: requiredInput, description: summary }),
535
+ handler: pair.prepare,
536
+ },
537
+ {
538
+ name: `${name}.apply`,
539
+ resource: 'msg',
540
+ method: `${method}.apply`,
541
+ action: `${name}.apply`,
542
+ summary: `${summary}(apply:需 confirm:true + continuation)`,
543
+ capability: 'msg',
544
+ risk: 'high-risk-write',
545
+ requiresConfirmation: true,
546
+ inputSchema: schemaOf({
547
+ confirm: { type: 'boolean', description: '必须为 true' },
548
+ continuation: { type: 'string', minLength: 1 },
549
+ }, { required: ['confirm', 'continuation'], description: summary }),
550
+ handler: pair.apply,
551
+ },
552
+ ];
553
+ }
554
+ const msgWriteOps = [
555
+ ...buildMsgWriteDefinition('yimiaoban.msg.send.single', 'msg.send.single', '发送单聊消息(文本/图片/文件)', {
556
+ toUid: { type: 'string', pattern: '^\\d+$', description: '接收人 uid' },
557
+ toCid: { type: 'string', pattern: '^\\d+$', description: '接收人 cid' },
558
+ txt: { type: 'string', description: '文本内容' },
559
+ img: { type: 'string', description: '本地图片路径(发送前会上传到 E10 文件服务)' },
560
+ file: { type: 'string', description: '本地文件路径' },
561
+ }, sendSinglePair),
562
+ ...buildMsgWriteDefinition('yimiaoban.msg.send.group', 'msg.send.group', '发送群聊消息(文本/图片/文件)', {
563
+ groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
564
+ txt: { type: 'string', description: '文本内容' },
565
+ img: { type: 'string', description: '本地图片路径' },
566
+ file: { type: 'string', description: '本地文件路径' },
567
+ }, sendGroupPair),
568
+ ...buildMsgWriteDefinition('yimiaoban.msg.withdraw', 'msg.withdraw', '撤回消息(单聊/群聊,限本人 2 分钟内)', {
569
+ type: { type: 'string', enum: ['single', 'group'], description: 'single 或 group' },
570
+ msgid: { type: 'string', pattern: '^\\d+$', description: '消息 id(ser_msgid)' },
571
+ toUid: { type: 'string', pattern: '^\\d+$', description: '单聊:对方 uid' },
572
+ toCid: { type: 'string', pattern: '^\\d+$', description: '单聊:对方 cid' },
573
+ groupId: { type: 'string', pattern: '^\\d+$', description: '群聊:群 id' },
574
+ }, withdrawPair),
575
+ ...buildMsgWriteDefinition('yimiaoban.msg.top.set', 'msg.top.set', '群消息置顶 / 取消置顶', {
576
+ groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
577
+ msgid: { type: 'string', pattern: '^\\d+$', description: '目标消息 id' },
578
+ type: { type: 'integer', enum: [1, 2], description: '1 置顶 / 2 取消' },
579
+ cancel: { type: 'boolean', description: '等价 type=2' },
580
+ }, topSetPair),
581
+ ...buildMsgWriteDefinition('yimiaoban.ding.send', 'ding.send', '发送必达消息 / 已有消息转必达(createDing)', {
582
+ groupId: { type: 'string', pattern: '^\\d+$', description: '群场景群 id' },
583
+ toUid: { type: 'string', pattern: '^\\d+$', description: '单聊场景对方 uid' },
584
+ toCid: { type: 'string', pattern: '^\\d+$', description: '单聊转必达对方 cid' },
585
+ txt: { type: 'string', description: '普通必达文本' },
586
+ convertMsgid: { type: 'string', pattern: '^\\d+$', description: '转必达源消息 id' },
587
+ unreadOnly: { type: 'boolean', description: '群转必达仅发给未读人员' },
588
+ toUids: { type: 'array', items: { type: 'string' }, description: '指定接收人 uid' },
589
+ sendMsg: { type: 'integer', enum: [0, 1], description: '发短信(默认 1 发)' },
590
+ showType: { type: 'integer', enum: [0, 1], description: '0 全体可见(默认)/1 仅发送人可见' },
591
+ }, dingSendPair),
592
+ ];
593
+ export const msgWriteOperations = msgWriteOps;