weaver-work-cli 0.1.32 → 0.1.36

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 (211) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/{archive-UMGX72K6.js → archive-XBVH7JU7.js} +2 -2
  3. package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-MZM3EKFQ.js} +2 -2
  4. package/dist/chunks/{asset-GLEGKFG5.js → asset-4XO7UOUW.js} +2 -2
  5. package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-VXSWANAC.js} +3 -2
  6. package/dist/chunks/{blog-E44OTVDA.js → blog-PGZKOPSC.js} +2 -2
  7. package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-BH4ZVGUN.js} +2 -2
  8. package/dist/chunks/{calendar-JKPIRKFD.js → calendar-MXEMTIO4.js} +2 -2
  9. package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
  10. package/dist/chunks/chunk-3SO6QL6U.js +73 -0
  11. package/dist/chunks/chunk-5DQEEGCC.js +229 -0
  12. package/dist/chunks/{chunk-G3QJULO2.js → chunk-6LLJFKJE.js} +1 -1
  13. package/dist/chunks/{chunk-C6R3B2ML.js → chunk-BPE37FPB.js} +35 -7
  14. package/dist/chunks/chunk-BYATLLEF.js +68 -0
  15. package/dist/chunks/chunk-BYBSCN4X.js +932 -0
  16. package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-F746NKNF.js} +1 -1
  17. package/dist/chunks/{chunk-GCG2NMWR.js → chunk-HXJQI5NM.js} +4 -2
  18. package/dist/chunks/{chunk-YRVDW4WN.js → chunk-I6QGIBDM.js} +2 -2
  19. package/dist/chunks/{chunk-H7J4DC5R.js → chunk-JCSN3JGM.js} +2 -2
  20. package/dist/chunks/{chunk-2GA5ZIQL.js → chunk-JNG42JKG.js} +30 -1
  21. package/dist/chunks/{chunk-DW6AUUKS.js → chunk-JUXVYKII.js} +9 -4
  22. package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-JYKABYCV.js} +2 -2
  23. package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-NCOVUAZK.js} +2 -2
  24. package/dist/chunks/{chunk-E4Z23GHM.js → chunk-QHFANTX7.js} +1114 -904
  25. package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-QYGO5EE2.js} +22 -1148
  26. package/dist/chunks/{chunk-IURUFGZK.js → chunk-T72N74ZT.js} +50 -33
  27. package/dist/chunks/{chunk-H6XSTSLM.js → chunk-TC6MZAO3.js} +137 -204
  28. package/dist/chunks/{chunk-WKI2FBIQ.js → chunk-VPUTVEMC.js} +737 -341
  29. package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-VPXOPQJI.js} +2 -2
  30. package/dist/chunks/chunk-WXFPWCXG.js +774 -0
  31. package/dist/chunks/{chunk-4YC43QQ4.js → chunk-XMGSYD6S.js} +4 -4
  32. package/dist/chunks/conflict-JAB2LJEZ.js +21 -0
  33. package/dist/chunks/continuation-RN6PZN2M.js +11 -0
  34. package/dist/chunks/contract-XI2EEIZU.js +9110 -0
  35. package/dist/chunks/cowork-SCPGLBA4.js +2527 -0
  36. package/dist/chunks/ebuilder-form-EYLHNOTA.js +121 -0
  37. package/dist/chunks/{ehr-HMFY6VIL.js → ehr-EAP77IMT.js} +1344 -27
  38. package/dist/chunks/{esb-ZYSVB7SG.js → esb-2TGGXEE7.js} +4 -4
  39. package/dist/chunks/{fna-B7RKPAZD.js → fna-2X7ZE7BU.js} +13 -8
  40. package/dist/chunks/{host-OCH2BCCV.js → host-C6EL2RKL.js} +3 -3
  41. package/dist/chunks/{host-45WED2KK.js → host-I4NRK4MX.js} +3 -3
  42. package/dist/chunks/{host-I4Z6BFYX.js → host-Q4LZGCRU.js} +3 -3
  43. package/dist/chunks/host-UEI5FGDM.js +23 -0
  44. package/dist/chunks/{host-KNZ52LJZ.js → host-XCLDDXKQ.js} +3 -3
  45. package/dist/chunks/{hrm-2KAZGUHI.js → hrm-YXERJM76.js} +3 -3
  46. package/dist/chunks/{im-ACJJWH4U.js → im-A4VZN2TQ.js} +88 -12
  47. package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-2Z6YPMIS.js} +3 -3
  48. package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-AR5GMKZB.js} +2 -2
  49. package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-QXW7P4X3.js} +4 -4
  50. package/dist/chunks/{mail-S2HDDXO2.js → mail-7GZ6CPBJ.js} +45 -24
  51. package/dist/chunks/{meeting-QVN3I37L.js → meeting-3NMGA3MB.js} +278 -43
  52. package/dist/chunks/{okr-QNG4VC2Q.js → okr-BZ6XUOAG.js} +2 -2
  53. package/dist/chunks/operations-5QL3AECO.js +11 -0
  54. package/dist/chunks/{plan-A25EMW6U.js → plan-DR5CMUYP.js} +2 -2
  55. package/dist/chunks/{project-G6XV66DZ.js → project-6J63MK33.js} +2 -2
  56. package/dist/chunks/{registry-DC6KR3NH.js → registry-KDD7DHNK.js} +4 -4
  57. package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
  58. package/dist/chunks/{registry-7C5UMIPL.js → registry-TKX2E7C7.js} +2 -2
  59. package/dist/chunks/{registry-TI3CIUXF.js → registry-VRTNC6UR.js} +8 -6
  60. package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-XCWRMB5P.js} +4 -3
  61. package/dist/chunks/{schedule-KFIITGSK.js → schedule-IVBICP32.js} +4 -3
  62. package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-D74NFK3C.js} +179 -6
  63. package/dist/index.js +35 -25
  64. package/dist/internal/e10/auth-recovery.js +14 -4
  65. package/dist/internal/e10/fetch-timing.js +31 -0
  66. package/dist/internal/e10/request-runtime.js +5 -0
  67. package/dist/shortcuts/contract/host.js +34 -59
  68. package/dist/shortcuts/cowork/continuation.js +70 -0
  69. package/dist/shortcuts/cowork/errors.js +59 -0
  70. package/dist/shortcuts/cowork/host.js +180 -0
  71. package/dist/shortcuts/cowork/index.js +166 -0
  72. package/dist/shortcuts/cowork/manifest.js +65 -0
  73. package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
  74. package/dist/shortcuts/cowork/operations/item-write.js +380 -0
  75. package/dist/shortcuts/cowork/operations/member-write.js +283 -0
  76. package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
  77. package/dist/shortcuts/cowork/operations/read.js +423 -0
  78. package/dist/shortcuts/cowork/operations/registry.js +39 -0
  79. package/dist/shortcuts/cowork/operations/review-write.js +157 -0
  80. package/dist/shortcuts/cowork/operations/shared.js +215 -0
  81. package/dist/shortcuts/cowork/operations/types.js +39 -0
  82. package/dist/shortcuts/cowork/operations.js +19 -0
  83. package/dist/shortcuts/ehr/host.js +4 -1
  84. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
  85. package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
  86. package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
  87. package/dist/shortcuts/ehr/operations/recruit-write.js +43 -16
  88. package/dist/shortcuts/ehr/operations/recruit.js +182 -10
  89. package/dist/shortcuts/ehr/operations/registry.js +6 -0
  90. package/dist/shortcuts/fna/operations/expense-chart.js +19 -7
  91. package/dist/shortcuts/im/diagnostics.js +97 -0
  92. package/dist/shortcuts/im/host.js +58 -31
  93. package/dist/shortcuts/im/index.js +7 -2
  94. package/dist/shortcuts/im/operations/at-mention.js +127 -0
  95. package/dist/shortcuts/im/operations/file-user.js +10 -2
  96. package/dist/shortcuts/im/operations/group-read.js +26 -5
  97. package/dist/shortcuts/im/operations/group-write.js +35 -28
  98. package/dist/shortcuts/im/operations/msg-sync.js +7 -2
  99. package/dist/shortcuts/im/operations/msg-write.js +76 -10
  100. package/dist/shortcuts/im/operations/quote-mention.js +119 -0
  101. package/dist/shortcuts/im/operations/session.js +1 -1
  102. package/dist/shortcuts/im/operations/shared.js +10 -0
  103. package/dist/shortcuts/im/operations/types.js +13 -0
  104. package/dist/shortcuts/index.js +10 -0
  105. package/dist/shortcuts/mail/host.js +12 -1
  106. package/dist/shortcuts/mail/operations/types.js +17 -2
  107. package/dist/shortcuts/meeting/host.js +27 -34
  108. package/dist/shortcuts/meeting/index.js +79 -1
  109. package/dist/shortcuts/meeting/manifest.js +10 -0
  110. package/dist/shortcuts/meeting/operations/create.js +4 -1
  111. package/dist/shortcuts/meeting/operations/eb/catalog.js +10 -0
  112. package/dist/shortcuts/meeting/operations/eb/conflict.js +84 -8
  113. package/dist/shortcuts/meeting/operations/eb/create-flow.js +1195 -140
  114. package/dist/shortcuts/meeting/operations/eb/factory.js +170 -8
  115. package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
  116. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +50 -5
  117. package/dist/shortcuts/meeting/operations/eb-fallback.js +163 -0
  118. package/dist/shortcuts/meeting/operations/env.js +99 -46
  119. package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
  120. package/dist/shortcuts/meeting/operations/invitation.js +79 -20
  121. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  122. package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
  123. package/dist/shortcuts/meeting/operations.js +16 -0
  124. package/dist/shortcuts/workflow/endpoints.js +8 -3
  125. package/dist/shortcuts/workflow/index.js +110 -1
  126. package/dist/shortcuts/workflow/operations/form.js +204 -36
  127. package/dist/shortcuts/workflow/operations/read.js +104 -23
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
  132. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  133. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
  134. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
  135. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
  136. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
  137. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
  138. package/dist/shortcuts/workflow/operations.js +23 -1
  139. package/docs/e10-auth.md +1 -1
  140. package/docs/ehr.md +1 -1
  141. package/docs/fna.md +2 -1
  142. package/docs/im.md +8 -1
  143. package/docs/todo-smart-convert/README.md +1 -1
  144. package/docs/todo-smart-convert/data-contract.md +2 -2
  145. package/docs/todo-smart-convert/subgroup-rules.md +6 -0
  146. package/package.json +1 -1
  147. package/skill-template/business-info.json +18 -0
  148. package/skill-template/domains/cowork.md +14 -0
  149. package/skill-template/domains/yimiaoban.md +3 -3
  150. package/skill-template/skill-template.md +1 -1
  151. package/skills/weaver-e10-blog/SKILL.md +5 -5
  152. package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
  153. package/skills/weaver-e10-calendar/SKILL.md +5 -5
  154. package/skills/weaver-e10-cowork/SKILL.md +101 -0
  155. package/skills/weaver-e10-cowork/references/cowork-read.md +90 -0
  156. package/skills/weaver-e10-cowork/references/cowork-write.md +131 -0
  157. package/skills/weaver-e10-cowork/references/safety-boundaries.md +45 -0
  158. package/skills/weaver-e10-esb/SKILL.md +5 -5
  159. package/skills/weaver-e10-hrm/SKILL.md +5 -5
  160. package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
  161. package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
  162. package/skills/weaver-e10-jucailin/SKILL.md +38 -14
  163. package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
  164. package/skills/weaver-e10-jucailin/references/recruit-read.md +25 -0
  165. package/skills/weaver-e10-jucailin/references/recruit-write.md +13 -0
  166. package/skills/weaver-e10-jucailin/references/salary.md +15 -11
  167. package/skills/weaver-e10-mail/SKILL.md +17 -14
  168. package/skills/weaver-e10-meeting/SKILL.md +442 -46
  169. package/skills/weaver-e10-meeting/references/meeting-eb.md +135 -3
  170. package/skills/weaver-e10-meeting/references/meeting-invitation.md +28 -2
  171. package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
  172. package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
  173. package/skills/weaver-e10-meeting.zip +0 -0
  174. package/skills/weaver-e10-okr/SKILL.md +5 -5
  175. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  176. package/skills/weaver-e10-plan/SKILL.md +5 -5
  177. package/skills/weaver-e10-qiyecheng/SKILL.md +5 -2
  178. package/skills/weaver-e10-qiyecheng/references/catalog.md +2 -2
  179. package/skills/weaver-e10-qiyecheng/references/expense-chart.md +15 -1
  180. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +22 -21
  181. package/skills/weaver-e10-shared/SKILL.md +12 -12
  182. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
  183. package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
  184. package/skills/weaver-e10-workflow/SKILL.md +29 -5
  185. package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
  186. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
  187. package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
  188. package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
  189. package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
  190. package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
  191. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
  192. package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
  193. package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
  194. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
  195. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
  196. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
  197. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
  198. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
  199. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
  200. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
  201. package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
  202. package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
  203. package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
  204. package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
  205. package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
  206. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
  207. package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
  208. package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
  209. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
  210. package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
  211. package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
@@ -0,0 +1,127 @@
1
+ import { imValidationError } from '../errors.js';
2
+ import { extractGroupMembers, isSystemMember, str, unwrapInner } from './shared.js';
3
+ /**
4
+ * 群聊 @ 提及(@所有人 / @具体人)。
5
+ *
6
+ * 服务端契约(2026-10-29 实测 yunteams,群 1793244595701000005):
7
+ * - 只能通过 `sendGroupMsg` 的 **`msg` 字段**(完整原生 msg JSON 字符串)携带 @;
8
+ * `txt` 字段传 JSON 字符串会被服务端原样塞进 `dt[0].txt.v`,客户端直接显示 JSON 原文(踩坑确认)。
9
+ * - 原生结构:`{ bgc:'', cnt:'0', dev:5, display:'', dt:[{txt:{aus, ft:'china', next:'0', v}}],
10
+ * guid:'0', idx:'0', opb:['received','reply'], scid, sname, suid, sysId:'', type:'1', ver:'1.0', weaOaId:'' }`
11
+ * - `txt.aus` 条目:@具体人 `{ cid:<人cid>, fd:'@', id:<人uid>, nm:<姓名>, t:'1' }`;
12
+ * @所有人固定 `{ cid:'10000', fd:'@', id:'88', nm:'全体成员', t:'2' }`。
13
+ * - `txt.v` 中每个 @ 位置写占位符 `:@<id>;`(冒号+@+id+分号),其余文本原样保留;
14
+ * 客户端渲染时用 aus 把占位符还原为 `@<nm>`。
15
+ */
16
+ /** @全体成员 的服务端固定条目。 */
17
+ export const AT_ALL_ENTRY = { cid: '10000', fd: '@', id: '88', nm: '全体成员', t: '2' };
18
+ function truthy(value) {
19
+ return value === true || value === 'true' || value === '1';
20
+ }
21
+ function uidList(value) {
22
+ if (value === undefined || value === null || value === '')
23
+ return [];
24
+ const raw = Array.isArray(value)
25
+ ? value.map((x) => str(x))
26
+ : String(value).split(/[,,;;\s]+/);
27
+ return raw.map((x) => x.trim()).filter((x) => x && x !== '0');
28
+ }
29
+ /** 调用方是否带了任何 @ 入参(含显式无效值,用于单聊报错)。 */
30
+ export function hasAtInput(args) {
31
+ return args.atAll !== undefined || args.atUids !== undefined;
32
+ }
33
+ /**
34
+ * 解析 @ 入参为 aus 条目列表。
35
+ * @具体人必须逐个核实已在目标群(getGroupUsers,member_mask=6 只取 uid+cid),
36
+ * 不在群内直接拒绝(错误码 at_member_not_in_group);姓名一律走 hrm 解析(不用 IM 返回的 name)。
37
+ */
38
+ export async function resolveAtMentions(args, env, groupId) {
39
+ const atAll = truthy(args.atAll);
40
+ const atUids = uidList(args.atUids);
41
+ if (!atAll && !atUids.length)
42
+ return [];
43
+ const entries = [];
44
+ if (atAll)
45
+ entries.push({ ...AT_ALL_ENTRY });
46
+ if (atUids.length) {
47
+ let members = [];
48
+ try {
49
+ const res = await env.host.request({
50
+ method: 'POST',
51
+ path: '/api/em/msg/executeIm/group/getGroupUsers',
52
+ body: { group_id: groupId, type: 0, sync_type: 0, member_mask: 6, client_uc: '0' },
53
+ operation: 'im.msg.send.group.atMembers',
54
+ });
55
+ members = extractGroupMembers(unwrapInner(res));
56
+ }
57
+ catch (error) {
58
+ throw imValidationError('at_members_unavailable', `无法核实 @ 目标是否在群内:拉取群成员失败(${error instanceof Error ? error.message : String(error)})`, { groupId, atUids });
59
+ }
60
+ const inGroup = new Map();
61
+ for (const m of members) {
62
+ if (isSystemMember(m))
63
+ continue;
64
+ inGroup.set(str(m.uid), str(m.cid));
65
+ }
66
+ const missing = atUids.filter((uid) => !inGroup.has(uid));
67
+ if (missing.length) {
68
+ throw imValidationError('at_member_not_in_group', `以下 @ 目标不在群 ${groupId} 内,已拒绝发送:${missing.join(', ')}`, { groupId, missing, atUids });
69
+ }
70
+ const personMap = await env.host.resolveUsersByIds([...new Set(atUids)]);
71
+ for (const uid of atUids) {
72
+ entries.push({ cid: inGroup.get(uid) || '', fd: '@', id: uid, nm: personMap[uid]?.name || uid, t: '1' });
73
+ }
74
+ }
75
+ return entries;
76
+ }
77
+ function escapeRegExp(value) {
78
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
79
+ }
80
+ /**
81
+ * 把 @ 入参落进文本:文本中出现的 `@<姓名>`(@全体成员兼认 `@所有人`)替换为 `:@<id>;` 占位符;
82
+ * 未在文本中出现的提及按传入顺序前置(`:@<id>;` 两空格分隔,仿客户端格式)。
83
+ * 返回原生 txt 需要的 `v`(占位文本)与 `aus`(按 v 中出现顺序)。
84
+ */
85
+ export function buildAtText(text, entries) {
86
+ let v = String(text ?? '');
87
+ const used = new Set();
88
+ for (const entry of entries) {
89
+ const names = entry.t === '2' ? ['全体成员', '所有人'] : [entry.nm];
90
+ for (const nm of names) {
91
+ const re = new RegExp(`@${escapeRegExp(nm)}`, 'g');
92
+ if (!re.test(v))
93
+ continue;
94
+ v = v.replace(re, () => `:@${entry.id};`);
95
+ used.add(entry);
96
+ break;
97
+ }
98
+ }
99
+ const rest = entries.filter((e) => !used.has(e));
100
+ if (rest.length) {
101
+ const prefix = rest.map((e) => `:@${e.id};`).join(' ');
102
+ v = v.trim() ? `${prefix} ${v}` : prefix;
103
+ }
104
+ const aus = [...used, ...rest];
105
+ return { v, aus };
106
+ }
107
+ /** 组装 sendGroupMsg 的 `msg` 字段值(完整原生 msg JSON 字符串)。 */
108
+ export function buildAtMsgJson(v, aus, self, extra = {}) {
109
+ return JSON.stringify({
110
+ bgc: '',
111
+ cnt: '0',
112
+ dev: 5,
113
+ display: '',
114
+ dt: [{ txt: { aus, ft: 'china', next: '0', v } }],
115
+ guid: '0',
116
+ idx: '0',
117
+ opb: ['received', 'reply'],
118
+ scid: self.cid,
119
+ sname: self.name,
120
+ suid: self.uid,
121
+ sysId: '',
122
+ type: '1',
123
+ ver: '1.0',
124
+ weaOaId: '',
125
+ ...extra,
126
+ });
127
+ }
@@ -72,7 +72,11 @@ const filePreview = {
72
72
  toUid: { type: 'string', description: '单聊场景对方 uid' },
73
73
  toCid: { type: 'string', description: '单聊场景对方 cid' },
74
74
  imgFormat: { type: 'string', enum: ['small', 'large', 'original'], description: '图片规格(默认 small)' },
75
- }, { required: ['fileId', 'msgid'], description: '媒体预览 URL' }),
75
+ }, {
76
+ required: ['fileId', 'msgid'],
77
+ anyOf: [{ required: ['groupId'] }, { required: ['toUid', 'toCid'] }],
78
+ description: '媒体预览 URL(groupId 群聊 或 toUid+toCid 单聊,二选一)',
79
+ }),
76
80
  handler: async (args, { host }) => {
77
81
  const kind = String(args.kind ?? 'img') === 'video' ? 'video' : 'img';
78
82
  const fileId = String(args.fileId);
@@ -137,7 +141,11 @@ const fileDownload = {
137
141
  toCid: { type: 'string', description: '单聊场景对方 cid(与 groupId 二选一)' },
138
142
  imgFormat: { type: 'string', enum: ['small', 'large', 'original'], description: '图片规格(默认 small;要原图传 original)' },
139
143
  output: { type: 'string', minLength: 1, description: '本地输出文件路径(父目录必须存在,已存在则拒绝)' },
140
- }, { required: ['fileId', 'msgid', 'output'], description: '媒体/文件落地到本地' }),
144
+ }, {
145
+ required: ['fileId', 'msgid', 'output'],
146
+ anyOf: [{ required: ['groupId'] }, { required: ['toUid', 'toCid'] }],
147
+ description: '媒体/文件落地到本地(groupId 群聊 或 toUid+toCid 单聊,二选一)',
148
+ }),
141
149
  handler: async (args, { host }) => {
142
150
  const fileId = String(args.fileId ?? '');
143
151
  const msgid = String(args.msgid ?? '');
@@ -1,6 +1,6 @@
1
1
  import { imValidationError } from '../errors.js';
2
2
  import { READ, schemaOf, imSuccess, } from './types.js';
3
- import { assertImOk, compact, extractGroupMembers, formatFullTime, splitIds, str, toPositiveInt, toUint, unwrapInner, } from './shared.js';
3
+ import { assertImOk, compact, extractGroupMembers, formatFullTime, isSystemMember, splitIds, str, toPositiveInt, toUint, unwrapInner, } from './shared.js';
4
4
  const SEARCH_TYPE_MASK = {
5
5
  type: 1,
6
6
  key: 2,
@@ -153,7 +153,7 @@ const groupInfo = {
153
153
  inputSchema: schemaOf({
154
154
  groupIds: { type: 'array', items: { type: 'string' }, description: '群 id 列表(逗号串或数组)' },
155
155
  groupId: { type: 'string', description: '单群 id(groupIds 的便捷形式)' },
156
- }, { description: 'groupIds / groupId 至少其一' }),
156
+ }, { requiredAny: ['groupIds', 'groupId'], description: '按群 id 查基础信息(groupIds / groupId 至少其一)' }),
157
157
  handler: async (args, { host }) => {
158
158
  let ids = [];
159
159
  if (Array.isArray(args.groupIds))
@@ -186,12 +186,23 @@ const groupInfo = {
186
186
  maskName: maskTypeName(Number(g.mask ?? 0)),
187
187
  userNum: g.user_num,
188
188
  maxNum: g.max_num,
189
- hostUid: str(g.host_uid ?? g.hostUid ?? ''),
189
+ /** 群创建者 uid(服务端字段 host_uid);**不是群主**,群主以 group.users 的 role=3 为准。 */
190
+ creatorUid: str(g.host_uid ?? g.hostUid ?? ''),
191
+ /** 群创建者 cid(服务端字段 host_cid),与 creatorUid 配对。 */
192
+ creatorCid: str(g.host_cid ?? g.hostCid ?? ''),
190
193
  state: Number(g.state ?? -1),
191
194
  stateName: stateName(Number(g.state ?? -1)),
192
195
  createTime: formatFullTime(g.create_time ?? g.createTime),
193
196
  }));
194
- return imSuccess('im.group.info', { total: groups.length, groups });
197
+ return imSuccess('im.group.info', {
198
+ total: groups.length,
199
+ groups,
200
+ fieldNotes: {
201
+ creatorUid: '群创建者(host_uid),不是群主;创建后转让群主也不会变。',
202
+ creatorCid: '群创建者 cid(host_cid)。',
203
+ owner: '群主请用 im.group.users 的 role=3(或 im.group.admins)获取,不要用 creatorUid 判断。',
204
+ },
205
+ });
195
206
  },
196
207
  };
197
208
  /** 群成员列表。 */
@@ -233,7 +244,11 @@ const groupUsers = {
233
244
  const hasCid = (memberMask & 2) !== 0;
234
245
  const hasUid = (memberMask & 4) !== 0;
235
246
  const roleNames = { 0: '不可用', 1: '普通成员', 2: '管理员', 3: '群主', 4: '客户', 5: '负责人', 6: '协助人员' };
236
- const personIds = members.map((m) => String(m?.uid ?? '')).filter((x) => x && x !== '0');
247
+ // 系统成员没有 hrm 资料,不必进解析批次(省一次无效 id,也避免被当成「解析失败」)
248
+ const personIds = members
249
+ .filter((m) => !isSystemMember(m))
250
+ .map((m) => String(m?.uid ?? ''))
251
+ .filter((x) => x && x !== '0');
237
252
  // 姓名一律走 hrm 解析(不看 member_mask 的 name 位,也不使用 IM 返回的 name 字段)
238
253
  const personMap = personIds.length ? await host.resolveUsersByIds([...new Set(personIds)]) : {};
239
254
  const rows = members.map((m) => ({
@@ -243,10 +258,16 @@ const groupUsers = {
243
258
  role: hasRole ? Number(m.role_type ?? m.role ?? -1) : undefined,
244
259
  roleName: hasRole ? roleNames[Number(m.role_type ?? m.role ?? -1)] ?? '-' : undefined,
245
260
  addTime: hasAddTime ? formatFullTime(m.add_time ?? m.addTime) : undefined,
261
+ /** 服务端自动补入的系统成员(可选功能,不是真实员工),统计真实成员时排除。 */
262
+ isSystem: hasCid ? isSystemMember(m) : undefined,
246
263
  }));
264
+ const personRows = rows.filter((row) => row.isSystem !== true);
247
265
  return imSuccess('im.group.users', {
248
266
  groupId,
267
+ /** 成员总数(含系统成员,即服务端返回的原始条数) */
249
268
  totalNum: inner?.total_num ?? rows.length,
269
+ /** 真实成员数(排除系统成员 cid=10002),判断「只剩自己」请用这个 */
270
+ personNum: personRows.length,
250
271
  endFlag: inner?.end_flag,
251
272
  serverUc: str(inner?.server_uc ?? ''),
252
273
  members: rows,
@@ -1,6 +1,6 @@
1
1
  import { imValidationError } from '../errors.js';
2
2
  import { schemaOf, WRITE, } from './types.js';
3
- import { assertImOk, compact, splitIds, str, toUint, unwrapInner } from './shared.js';
3
+ import { assertImOk, compact, extractGroupMembers, isSystemMember, splitIds, str, toUint, unwrapInner } from './shared.js';
4
4
  import { makeWritePair } from './write-util.js';
5
5
  /** 解析 uid 列表为 {cid,uid} 成员,支持 failed 移除。 */
6
6
  async function resolveMembers(args, env) {
@@ -59,7 +59,7 @@ const groupCreatePair = makeWritePair({
59
59
  }
60
60
  const { users, failed, names: resolvedNames } = await resolveMembers(args, env);
61
61
  if (failed.length && args.removeFailed !== true) {
62
- throw imValidationError('members_resolve_failed', `以下成员信息获取失败(cid/uid 缺失或为 0):${failed.map((f) => f.name || f.uid).join('、')};确认移除请传 removeFailed:true`, { details: failed });
62
+ throw imValidationError('members_resolve_failed', `以下成员信息获取失败(cid/uid 缺失或为 0):${failed.map((f) => f.name || f.uid).join('、')};确认移除请传 removeFailed:true`, failed);
63
63
  }
64
64
  if (users.length === 0)
65
65
  throw imValidationError('members_empty', '没有可入群的成员');
@@ -98,6 +98,8 @@ const groupCreatePair = makeWritePair({
98
98
  members: users.map((u) => u.uid),
99
99
  adminCount: admins.length,
100
100
  removeFailed: args.removeFailed === true ? failed.map((f) => f.uid) : [],
101
+ /** 服务端可选能力:建群时会自动补入系统成员(cid=10002),实际成员数可能比 users 多 1。 */
102
+ systemMemberNote: '建群后服务端可能自动补入 1 个系统成员(cid=10002,群内系统消息载体,租户侧可选开启);它不是真实员工,也不是本次传入的成员,不需要也不建议单独踢出。',
101
103
  },
102
104
  snapshot: { name, users, admins, failed, removeFailed: args.removeFailed === true },
103
105
  };
@@ -194,28 +196,29 @@ const groupExitPair = makeWritePair({
194
196
  if (!groupId || groupId === '0')
195
197
  throw imValidationError('id_invalid', 'groupId 必须非 0');
196
198
  /**
197
- * 只读回查群人数与群主:实测「群主/最后一个成员退群」后该群会变成无主空群,
199
+ * 只读回查群主与真实人数:实测「群主/最后一个真实成员退群」后该群会变成无主空群,
198
200
  * 之后 join / destroy 都会被服务端拒绝(1210),必须由客户端或管理员处理。
199
201
  * 因此这两种情形要求调用方显式传 acknowledgeOrphanRisk:true 才签发 continuation。
202
+ *
203
+ * 群主判定只认 **成员列表的 role=3**(getGroupUsers):group/queryGroupInfo 的 host_uid
204
+ * 是「群创建者」,转让群主后不会更新,用它判群主会误判(实测已复现)。
200
205
  */
201
206
  let memberCount;
202
207
  let isOwner = false;
203
208
  try {
204
- const infoRes = await env.host.request({
209
+ const usersRes = await env.host.request({
205
210
  method: 'POST',
206
- path: '/api/em/msg/executeIm/group/queryGroupInfo',
207
- body: { trans_id: 0, type: 0, mask: 0, group_id: [groupId], flag: 0 },
208
- operation: 'im.group.exit.plan.info',
211
+ path: '/api/em/msg/executeIm/group/getGroupUsers',
212
+ body: { group_id: groupId, type: 0, sync_type: 0, member_mask: 142, client_uc: '0' },
213
+ operation: 'im.group.exit.plan.members',
209
214
  });
210
- const info = (unwrapInner(infoRes)?.datas || [])[0];
211
- if (info) {
212
- const raw = info.user_num ?? info.userNum;
213
- memberCount = raw === undefined || raw === null ? undefined : Number(raw);
214
- const hostUid = str(info.host_uid ?? info.hostUid ?? '');
215
- if (hostUid) {
216
- const self = await env.host.getSelfIdentity();
217
- isOwner = hostUid === String(self.uid);
218
- }
215
+ const members = extractGroupMembers(unwrapInner(usersRes));
216
+ if (members.length) {
217
+ const self = await env.host.getSelfIdentity();
218
+ const selfRow = members.find((m) => str(m?.uid) === String(self.uid));
219
+ isOwner = Number(selfRow?.role_type ?? selfRow?.role ?? -1) === 3;
220
+ // 系统成员(cid=10002,产品可选能力)不是真实成员,不能算进「是否只剩自己」
221
+ memberCount = members.filter((m) => !isSystemMember(m)).length;
219
222
  }
220
223
  }
221
224
  catch {
@@ -223,7 +226,7 @@ const groupExitPair = makeWritePair({
223
226
  }
224
227
  const orphanRisk = isOwner || (memberCount !== undefined && memberCount <= 1);
225
228
  if (orphanRisk && args.acknowledgeOrphanRisk !== true) {
226
- throw imValidationError('owner_exit_ack_required', `退群风险确认缺失:${isOwner ? '你是该群群主' : '你是该群最后一个成员'},退群后该群会变成无主空群,之后 join/destroy 都会被服务端拒绝(1210),只能由客户端或管理员处理。确认仍要退群请传 acknowledgeOrphanRisk:true`, { details: { groupId, isOwner, memberCount } });
229
+ throw imValidationError('owner_exit_ack_required', `退群风险确认缺失:${isOwner ? '你是该群群主' : '你是该群最后一个成员'},退群后该群会变成无主空群,之后 join/destroy 都会被服务端拒绝(1210),只能由客户端或管理员处理。确认仍要退群请传 acknowledgeOrphanRisk:true`, { groupId, isOwner, memberCount });
227
230
  }
228
231
  return {
229
232
  summary: {
@@ -537,12 +540,16 @@ const groupAnnounceModifyPair = makeWritePair({
537
540
  };
538
541
  },
539
542
  });
540
- function buildWriteDefinition(name, method, summary, inputProps, pair) {
543
+ function buildWriteDefinition(name, method, summary, inputProps, pair, condition = {}) {
541
544
  const requiredInput = Object.keys(inputProps).filter((k) => {
542
545
  const prop = inputProps[k];
543
546
  return prop?.required === true;
544
547
  });
545
- const prepareSchema = schemaOf({ ...inputProps, removeFailed: { type: 'boolean', description: '确认移除解析失败成员' } }, { required: requiredInput, description: summary });
548
+ const prepareSchema = schemaOf({ ...inputProps, removeFailed: { type: 'boolean', description: '确认移除解析失败成员' } }, {
549
+ ...condition,
550
+ required: [...new Set([...(condition.required || []), ...requiredInput])],
551
+ description: summary,
552
+ });
546
553
  return [
547
554
  {
548
555
  name: `${name}.prepare`,
@@ -579,30 +586,30 @@ const groupWriteOps = [
579
586
  name: { type: 'string', description: '群名(缺省自动生成)' },
580
587
  names: { type: 'array', items: { type: 'string' }, description: '与 users 顺序对应姓名(用于默认群名)' },
581
588
  admins: { type: 'array', items: { type: 'string' }, description: '管理员 uid' },
582
- }, groupCreatePair),
589
+ }, groupCreatePair, { required: ['users'] }),
583
590
  ...buildWriteDefinition('im.group.invite', 'group.invite', '邀请成员入群', {
584
591
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
585
592
  users: { type: 'array', items: { type: 'string' }, description: '被邀请 uid' },
586
- }, groupInvitePair),
593
+ }, groupInvitePair, { required: ['groupId', 'users'] }),
587
594
  ...buildWriteDefinition('im.group.kick', 'group.kick', '踢出群成员', {
588
595
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
589
596
  users: { type: 'array', items: { type: 'string' }, description: '被踢 uid' },
590
- }, groupKickPair),
597
+ }, groupKickPair, { required: ['groupId', 'users'] }),
591
598
  ...buildWriteDefinition('im.group.exit', 'group.exit', '退出群聊', {
592
599
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
593
600
  acknowledgeOrphanRisk: {
594
601
  type: 'boolean',
595
602
  description: '群主或最后一个成员退群时必须显式传 true(退群后该群会变成无主空群,无法再 join/destroy)',
596
603
  },
597
- }, groupExitPair),
604
+ }, groupExitPair, { required: ['groupId'] }),
598
605
  ...buildWriteDefinition('im.group.destroy', 'group.destroy', '解散群聊(仅群主,不可恢复)', {
599
606
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
600
- }, groupDestroyPair),
607
+ }, groupDestroyPair, { required: ['groupId'] }),
601
608
  ...buildWriteDefinition('im.group.join', 'group.join', '申请加入群聊', {
602
609
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id(token 二选一)' },
603
610
  token: { type: 'string', description: '群 token/二维码加入' },
604
611
  mark: { type: 'string', maxLength: 100, description: '申请理由' },
605
- }, groupJoinPair),
612
+ }, groupJoinPair, { requiredAny: ['groupId', 'token'] }),
606
613
  ...buildWriteDefinition('im.group.modify', 'group.modify', '批量修改群属性(batModGroupInfo)', {
607
614
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
608
615
  num: { type: 'integer', description: '最大人数' },
@@ -614,7 +621,7 @@ const groupWriteOps = [
614
621
  creator: { type: 'string', description: '转让群主(单 uid)' },
615
622
  copyGroupids: { type: 'array', items: { type: 'string' }, description: '拷贝群列表' },
616
623
  gtId: { type: 'string', description: '关联群类型 id' },
617
- }, groupModifyPair),
624
+ }, groupModifyPair, { required: ['groupId'] }),
618
625
  ...buildWriteDefinition('im.group.rename', 'group.rename', '修改群信息(一次一项:群名/历史/群主/显示/打扰/标记/gtid)', {
619
626
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
620
627
  name: { type: 'string', description: '改群名' },
@@ -624,7 +631,7 @@ const groupWriteOps = [
624
631
  msgSetting: { type: 'integer', description: '消息打扰设置' },
625
632
  mark: { type: 'integer', description: '标记位' },
626
633
  gtid: { type: 'string', description: 'GTID' },
627
- }, groupRenamePair),
634
+ }, groupRenamePair, { required: ['groupId'] }),
628
635
  ...buildWriteDefinition('im.group.announce.modify', 'group.announce.modify', '新增/修改/删除群公告', {
629
636
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
630
637
  annouce: { type: 'string', description: '公告内容(删除时不需要)' },
@@ -633,6 +640,6 @@ const groupWriteOps = [
633
640
  notice: { type: 'integer', enum: [0, 1, 2], description: '0 发通知(默认)/1 不发/2 发并@全体' },
634
641
  fmark: { type: 'integer', enum: [0, 1], description: '新人必看' },
635
642
  related: { type: 'string', description: '关联附件 id' },
636
- }, groupAnnounceModifyPair),
643
+ }, groupAnnounceModifyPair, { required: ['groupId'] }),
637
644
  ];
638
645
  export const groupWriteOperations = groupWriteOps;
@@ -2,6 +2,7 @@ import { imValidationError } from '../errors.js';
2
2
  import { READ, schemaOf, imSuccess, } from './types.js';
3
3
  import { assertImOk, clampTimeRangeTo7Days, compact, DEFAULT_PAGE_SIZE, formatFullTime, makeMinMaxIdByTm, paginationMeta, parseMsg, parseTimeToSec, pullPaged, rangeShrinkMeta, resolveAuthUid, sortMessagesNewestFirst, str, todayRange, unwrapInner, } from './shared.js';
4
4
  import { collapseNewlines, collectI18nIds, collectPersonIds, extractFiles, extractImgs, extractVideos, parseContent, senderName, } from './msg-parse.js';
5
+ import { extractQuoteSummary } from './quote-mention.js';
5
6
  const MAX_MSG_ID = String((1n << 63n) - 1n);
6
7
  /**
7
8
  * 时间/最新/显式 id 计算 start/end(与源 skill 一致):
@@ -151,6 +152,7 @@ async function enrichMessages(rawRows, host, options = {}) {
151
152
  });
152
153
  }
153
154
  const sender = senderName(msgObj || {}, row.from_uid, options.authUid, nameMap);
155
+ const quoteSummary = extractQuoteSummary(msgObj);
154
156
  enriched.push({
155
157
  msgid: str(row.ser_msgid),
156
158
  sender,
@@ -161,6 +163,9 @@ async function enrichMessages(rawRows, host, options = {}) {
161
163
  typeName: parsed.typeName,
162
164
  title: options.titleFirst ? parsed.title : undefined,
163
165
  content: collapseNewlines(parsed.content),
166
+ ...(quoteSummary
167
+ ? { quote: quoteSummary, quoteTime: Number(quoteSummary.msgtime) > 0 ? formatFullTime(Math.floor(Number(quoteSummary.msgtime) / 1000)) : undefined }
168
+ : {}),
164
169
  must: parsed.must,
165
170
  fileName: parsed.fileName,
166
171
  media,
@@ -401,7 +406,7 @@ const msgReadSingle = {
401
406
  toCid: { type: 'string', pattern: '^\\d+$', description: '对方 cid' },
402
407
  msgids: { type: 'array', items: { type: 'string' }, description: '自己发出的消息 id(最多 50)' },
403
408
  recvMsgids: { type: 'array', items: { type: 'string' }, description: '自己接收的消息 id(最多 50)' },
404
- }, { required: ['toUid', 'toCid'], description: '查询单聊阅读状态(msgids/recvMsgids 至少其一)' }),
409
+ }, { required: ['toUid', 'toCid'], requiredAny: ['msgids', 'recvMsgids'], description: '查询单聊阅读状态(msgids / recvMsgids 至少其一)' }),
405
410
  handler: async (args, { host }) => {
406
411
  const hasSent = Array.isArray(args.msgids) && args.msgids.length > 0;
407
412
  const hasRecv = Array.isArray(args.recvMsgids) && args.recvMsgids.length > 0;
@@ -449,7 +454,7 @@ const msgReadGroup = {
449
454
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
450
455
  msgids: { type: 'array', items: { type: 'string' }, description: '自己发出的消息 id(最多 50)' },
451
456
  recvMsgids: { type: 'array', items: { type: 'string' }, description: '别人发出的消息 id(最多 50)' },
452
- }, { required: ['groupId'], description: '查询群消息阅读状态' }),
457
+ }, { required: ['groupId'], requiredAny: ['msgids', 'recvMsgids'], description: '查询群消息阅读状态(msgids / recvMsgids 至少其一)' }),
453
458
  handler: async (args, { host }) => {
454
459
  const hasSent = Array.isArray(args.msgids) && args.msgids.length > 0;
455
460
  const hasRecv = Array.isArray(args.recvMsgids) && args.recvMsgids.length > 0;
@@ -3,6 +3,8 @@ import { resolve } from 'node:path';
3
3
  import { imValidationError } from '../errors.js';
4
4
  import { schemaOf, } from './types.js';
5
5
  import { assertImOk, compact, extractGroupMembers, str } from './shared.js';
6
+ import { buildAtMsgJson, buildAtText, hasAtInput, resolveAtMentions } from './at-mention.js';
7
+ import { hasQuoteInput, resolveQuotedRef } from './quote-mention.js';
6
8
  import { fileObjToFile, fileObjToImg, makeWritePair } from './write-util.js';
7
9
  /** 校验本地文件存在,返回绝对路径。 */
8
10
  function assertLocalFile(pathValue, field) {
@@ -37,7 +39,18 @@ const sendSinglePair = makeWritePair({
37
39
  if (!toUid || !toCid || toUid === '0' || toCid === '0') {
38
40
  throw imValidationError('id_invalid', 'toUid/toCid 必须非 0');
39
41
  }
42
+ if (hasAtInput(args)) {
43
+ throw imValidationError('at_not_supported', '@ 提及仅群聊消息支持(单聊发送不接受 atAll/atUids)');
44
+ }
45
+ if (args.replyMsgId !== undefined && args.replyMsgId !== null && args.replyMsgId !== '') {
46
+ throw imValidationError('reply_not_supported', '单聊仅支持引用(quoteMsgId),不支持回复(replyMsgId)');
47
+ }
40
48
  const content = normalizeContent(args);
49
+ // 引用(仅单聊·文本):prepare 阶段在单聊会话内定位被引消息并组装 ref
50
+ const quote = hasQuoteInput(args) ? await resolveQuotedRef(args, env, { toUid, toCid }) : null;
51
+ if (quote && !content.txt) {
52
+ throw imValidationError('quote_requires_txt', 'quoteMsgId 仅支持文本消息(txt)');
53
+ }
41
54
  const summary = {
42
55
  action: '发送单聊消息',
43
56
  toUid,
@@ -51,6 +64,11 @@ const sendSinglePair = makeWritePair({
51
64
  snapshot.txt = content.txt;
52
65
  summary.textPreview = content.txt.length > 60 ? `${content.txt.slice(0, 60)}…` : content.txt;
53
66
  }
67
+ if (quote) {
68
+ snapshot.quoteKind = quote.kind;
69
+ snapshot.quoteRef = quote.ref;
70
+ summary.quote = { kind: quote.kind, quotedMsgId: quote.quotedMsgId, fromName: quote.fromName, textPreview: quote.text.length > 40 ? `${quote.text.slice(0, 40)}…` : quote.text };
71
+ }
54
72
  if (content.imgPath) {
55
73
  const abs = assertLocalFile(content.imgPath, 'img');
56
74
  snapshot.imgPath = abs;
@@ -65,8 +83,15 @@ const sendSinglePair = makeWritePair({
65
83
  },
66
84
  async execute(plan, env) {
67
85
  const body = { reciver: { cid: plan.toCid, uid: plan.toUid } };
68
- if (plan.txt !== undefined)
86
+ const quoteRef = plan.quoteRef ?? null;
87
+ if (quoteRef) {
88
+ // 引用:完整原生 msg JSON 走 msg 字段(与群聊同款承载方式,实测单聊同样生效)
89
+ const self = await env.host.getSelfIdentity();
90
+ body.msg = buildAtMsgJson(String(plan.txt ?? ''), [], self, { ref: quoteRef });
91
+ }
92
+ else if (plan.txt !== undefined) {
69
93
  body.txt = plan.txt;
94
+ }
70
95
  if (plan.imgPath) {
71
96
  const fileObj = await env.host.uploadFile(plan.imgPath, { shareUsers: plan.toUid });
72
97
  body.img = fileObjToImg(fileObj);
@@ -93,12 +118,31 @@ const sendGroupPair = makeWritePair({
93
118
  if (!groupId || groupId === '0')
94
119
  throw imValidationError('id_invalid', 'groupId 必须非 0');
95
120
  const content = normalizeContent(args);
121
+ // @ 提及(仅群聊):@具体人必须在群内(prepare 阶段逐个核实),@所有人走服务端固定 id=88
122
+ const atEntries = await resolveAtMentions(args, env, groupId);
123
+ if (atEntries.length && !content.txt) {
124
+ throw imValidationError('at_requires_txt', 'atAll/atUids 仅支持文本消息(txt)');
125
+ }
126
+ // 引用/回复(仅群聊文本):prepare 阶段定位被引消息并组装 ref/rmsg
127
+ const quote = hasQuoteInput(args) ? await resolveQuotedRef(args, env, { groupId }) : null;
128
+ if (quote && !content.txt) {
129
+ throw imValidationError('quote_requires_txt', 'quoteMsgId/replyMsgId 仅支持文本消息(txt)');
130
+ }
96
131
  const summary = { action: '发送群聊消息', groupId, hasTxt: Boolean(content.txt), hasImg: Boolean(content.imgPath), hasFile: Boolean(content.filePath) };
97
132
  const snapshot = { groupId };
98
133
  if (content.txt) {
99
134
  snapshot.txt = content.txt;
100
135
  summary.textPreview = content.txt.length > 60 ? `${content.txt.slice(0, 60)}…` : content.txt;
101
136
  }
137
+ if (atEntries.length) {
138
+ snapshot.atEntries = atEntries;
139
+ summary.at = atEntries.map((e) => (e.t === '2' ? { target: 'ALL', name: e.nm, id: e.id } : { uid: e.id, cid: e.cid, name: e.nm }));
140
+ }
141
+ if (quote) {
142
+ snapshot.quoteKind = quote.kind;
143
+ snapshot.quoteRef = quote.ref;
144
+ summary.quote = { kind: quote.kind, quotedMsgId: quote.quotedMsgId, fromName: quote.fromName, textPreview: quote.text.length > 40 ? `${quote.text.slice(0, 40)}…` : quote.text };
145
+ }
102
146
  if (content.imgPath) {
103
147
  const abs = assertLocalFile(content.imgPath, 'img');
104
148
  snapshot.imgPath = abs;
@@ -113,8 +157,17 @@ const sendGroupPair = makeWritePair({
113
157
  },
114
158
  async execute(plan, env) {
115
159
  const body = { groupId: plan.groupId };
116
- if (plan.txt !== undefined)
160
+ const atEntries = Array.isArray(plan.atEntries) ? plan.atEntries : [];
161
+ const quoteRef = plan.quoteRef ?? null;
162
+ if (atEntries.length || quoteRef) {
163
+ // @ / 引用 / 回复:完整原生 msg JSON 走 msg 字段(txt 字段无法携带 aus/ref,实测会被当纯文本展示)
164
+ const { v, aus } = buildAtText(String(plan.txt ?? ''), atEntries);
165
+ const self = await env.host.getSelfIdentity();
166
+ body.msg = buildAtMsgJson(v, aus, self, quoteRef ? { [plan.quoteKind === 'reply' ? 'rmsg' : 'ref']: quoteRef } : {});
167
+ }
168
+ else if (plan.txt !== undefined) {
117
169
  body.txt = plan.txt;
170
+ }
118
171
  if (plan.imgPath) {
119
172
  const fileObj = await env.host.uploadFile(plan.imgPath, { shareGroups: plan.groupId });
120
173
  body.img = fileObjToImg(fileObj);
@@ -516,7 +569,7 @@ const dingSendPair = makeWritePair({
516
569
  return { id: str(res?.data?.id ?? ''), msg: res?.data?.msg ?? '' };
517
570
  },
518
571
  });
519
- function buildMsgWriteDefinition(name, method, summary, inputProps, pair) {
572
+ function buildMsgWriteDefinition(name, method, summary, inputProps, pair, condition = {}) {
520
573
  const requiredInput = Object.keys(inputProps).filter((k) => {
521
574
  const prop = inputProps[k];
522
575
  return prop?.required === true;
@@ -531,7 +584,7 @@ function buildMsgWriteDefinition(name, method, summary, inputProps, pair) {
531
584
  capability: 'msg',
532
585
  risk: 'read-before-write',
533
586
  requiresConfirmation: true,
534
- inputSchema: schemaOf(inputProps, { required: requiredInput, description: summary }),
587
+ inputSchema: schemaOf(inputProps, { required: requiredInput, ...condition, description: summary }),
535
588
  handler: pair.prepare,
536
589
  },
537
590
  {
@@ -552,19 +605,27 @@ function buildMsgWriteDefinition(name, method, summary, inputProps, pair) {
552
605
  ];
553
606
  }
554
607
  const msgWriteOps = [
555
- ...buildMsgWriteDefinition('im.msg.send.single', 'msg.send.single', '发送单聊消息(文本/图片/文件)', {
608
+ ...buildMsgWriteDefinition('im.msg.send.single', 'msg.send.single', '发送单聊消息(文本/图片/文件,文本支持引用)', {
556
609
  toUid: { type: 'string', pattern: '^\\d+$', description: '接收人 uid' },
557
610
  toCid: { type: 'string', pattern: '^\\d+$', description: '接收人 cid' },
558
611
  txt: { type: 'string', description: '文本内容' },
612
+ quoteMsgId: { type: 'string', pattern: '^\\d+$', description: '引用该消息(单聊仅支持引用,不支持回复;仅 txt 文本消息);传单聊会话内消息 id(ser_msgid),prepare 阶段自动定位并组装 ref' },
559
613
  img: { type: 'string', description: '本地图片路径(发送前会上传到 E10 文件服务)' },
560
614
  file: { type: 'string', description: '本地文件路径' },
561
- }, sendSinglePair),
562
- ...buildMsgWriteDefinition('im.msg.send.group', 'msg.send.group', '发送群聊消息(文本/图片/文件)', {
615
+ }, sendSinglePair, {
616
+ required: ['toUid', 'toCid'],
617
+ requiredAny: ['txt', 'img', 'file'],
618
+ }),
619
+ ...buildMsgWriteDefinition('im.msg.send.group', 'msg.send.group', '发送群聊消息(文本/图片/文件,文本支持 @ 提及与引用/回复)', {
563
620
  groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
564
- txt: { type: 'string', description: '文本内容' },
621
+ txt: { type: 'string', description: '文本内容;@ 位置直接写 @<姓名>(如 @张三)或 @全体成员/@所有人' },
622
+ atAll: { type: 'boolean', description: '@全体成员(服务端固定 id=88),仅群聊、仅 txt 文本消息生效' },
623
+ atUids: { type: 'array', items: { type: 'string' }, description: '@具体人 uid 列表,仅群聊;prepare 阶段会逐个核实是否在群内,不在群内则拒绝发送' },
624
+ quoteMsgId: { type: 'string', pattern: '^\\d+$', description: '引用该消息(与 replyMsgId 互斥,仅群聊、仅 txt 文本消息);传消息 id(ser_msgid),prepare 阶段自动定位被引消息并组装 ref' },
625
+ replyMsgId: { type: 'string', pattern: '^\\d+$', description: '回复该消息(与 quoteMsgId 互斥,仅群聊、仅 txt 文本消息);传消息 id(ser_msgid),prepare 阶段自动定位被引消息并组装 rmsg' },
565
626
  img: { type: 'string', description: '本地图片路径' },
566
627
  file: { type: 'string', description: '本地文件路径' },
567
- }, sendGroupPair),
628
+ }, sendGroupPair, { required: ['groupId'], requiredAny: ['txt', 'img', 'file'] }),
568
629
  ...buildMsgWriteDefinition('im.msg.withdraw', 'msg.withdraw', '撤回消息(单聊/群聊,限本人 2 分钟内)', {
569
630
  type: { type: 'string', enum: ['single', 'group'], description: 'single 或 group' },
570
631
  msgid: { type: 'string', pattern: '^\\d+$', description: '消息 id(ser_msgid)' },
@@ -588,6 +649,11 @@ const msgWriteOps = [
588
649
  toUids: { type: 'array', items: { type: 'string' }, description: '指定接收人 uid' },
589
650
  sendMsg: { type: 'integer', enum: [0, 1], description: '发短信(默认 1 发)' },
590
651
  showType: { type: 'integer', enum: [0, 1], description: '0 全体可见(默认)/1 仅发送人可见' },
591
- }, dingSendPair),
652
+ }, dingSendPair, {
653
+ allOf: [
654
+ { anyOf: [{ required: ['groupId'] }, { required: ['toUid'] }] },
655
+ { anyOf: [{ required: ['txt'] }, { required: ['convertMsgid'] }] },
656
+ ],
657
+ }),
592
658
  ];
593
659
  export const msgWriteOperations = msgWriteOps;