weaver-work-cli 0.1.31 → 0.1.33

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 (186) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/{archive-U766FTFH.js → archive-FVFY6GZN.js} +2 -2
  3. package/dist/chunks/{archive-oa-DMWEKX3L.js → archive-oa-NW3OD4UV.js} +2 -2
  4. package/dist/chunks/{asset-TCR7VDEV.js → asset-7QJKMIDV.js} +2 -2
  5. package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-OSAVA6K3.js} +3 -2
  6. package/dist/chunks/{blog-EVZWGDVM.js → blog-I7SDPNW4.js} +2 -2
  7. package/dist/chunks/{caizhilian-JOWS6SL5.js → caizhilian-O37ZIQNY.js} +2 -2
  8. package/dist/chunks/{calendar-5WNCV66Q.js → calendar-7KXE4ED3.js} +2 -2
  9. package/dist/chunks/{chunk-VZTXRG6X.js → chunk-2FLWXFVS.js} +4 -4
  10. package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
  11. package/dist/chunks/chunk-3SO6QL6U.js +73 -0
  12. package/dist/chunks/{chunk-H3WDBYJW.js → chunk-6ATGWRZM.js} +6 -4
  13. package/dist/chunks/chunk-BYATLLEF.js +68 -0
  14. package/dist/chunks/chunk-BYBSCN4X.js +932 -0
  15. package/dist/chunks/{chunk-324YPTC2.js → chunk-D223PTXU.js} +11 -2
  16. package/dist/chunks/{chunk-R2IJAK7T.js → chunk-EEWPQE3O.js} +2 -2
  17. package/dist/chunks/{chunk-J7RJTZSV.js → chunk-GFPXQIY2.js} +2 -2
  18. package/dist/chunks/{chunk-AC7KJWC5.js → chunk-HEL5XTYI.js} +12 -6
  19. package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-IHUYP6LO.js} +1 -1
  20. package/dist/chunks/{chunk-R7O5PUWO.js → chunk-ILMBNVSG.js} +2 -2
  21. package/dist/chunks/{chunk-6V7ZLF34.js → chunk-KAS45KNR.js} +1 -1
  22. package/dist/chunks/{chunk-H6XSTSLM.js → chunk-MEEFGWTM.js} +54 -196
  23. package/dist/chunks/{chunk-GCG2NMWR.js → chunk-MQ2I5AA3.js} +4 -2
  24. package/dist/chunks/chunk-NHA62QD7.js +774 -0
  25. package/dist/chunks/{chunk-2DUV44UX.js → chunk-OCU4RLHC.js} +641 -309
  26. package/dist/chunks/{chunk-65QMVWHS.js → chunk-PRHJGPMA.js} +2 -2
  27. package/dist/chunks/{chunk-WHD4XMRP.js → chunk-Q6LWB5HX.js} +2 -2
  28. package/dist/chunks/{chunk-D4LTJSJD.js → chunk-RBGOMK4Y.js} +50 -33
  29. package/dist/chunks/chunk-VBCFFFGT.js +226 -0
  30. package/dist/chunks/{ebuilder-form-NJ4YK6CC.js → chunk-WRRJ4GG2.js} +22 -1148
  31. package/dist/chunks/{chunk-RCGYCOMG.js → chunk-WY74UBJ6.js} +264 -750
  32. package/dist/chunks/conflict-SFNEVJY5.js +21 -0
  33. package/dist/chunks/continuation-RN6PZN2M.js +11 -0
  34. package/dist/chunks/cowork-CKWYHXYB.js +2508 -0
  35. package/dist/chunks/ebuilder-form-KOLJRVZD.js +121 -0
  36. package/dist/chunks/{ehr-MMYN36VA.js → ehr-WFOC3B4D.js} +1174 -20
  37. package/dist/chunks/{esb-HIWHOU7Q.js → esb-LJZTN663.js} +4 -4
  38. package/dist/chunks/{fna-KBACQTA5.js → fna-F7QRIIZC.js} +2 -2
  39. package/dist/chunks/{host-3OHS5QUX.js → host-BCGUVEWX.js} +3 -3
  40. package/dist/chunks/host-BQBRHVR3.js +23 -0
  41. package/dist/chunks/{host-KQL5N4HO.js → host-DGE2TZD4.js} +3 -3
  42. package/dist/chunks/{host-LLSZQCWR.js → host-KXPTEWFS.js} +3 -3
  43. package/dist/chunks/{host-IG6KEH5Q.js → host-VQ6JJXGQ.js} +3 -3
  44. package/dist/chunks/{hrm-LYEC67ZQ.js → hrm-NI6XAKW5.js} +3 -3
  45. package/dist/chunks/{im-RN2UKAEU.js → im-7TRTF3KT.js} +88 -12
  46. package/dist/chunks/{invoice-EEPHLAF6.js → invoice-AOXWJKYT.js} +3 -3
  47. package/dist/chunks/{jiuchuanhui-WVVZYSJE.js → jiuchuanhui-7NQWEZAW.js} +2 -2
  48. package/dist/chunks/{lazy-dispatch-MDOFFL2S.js → lazy-dispatch-RNS4EE4S.js} +4 -4
  49. package/dist/chunks/{mail-LNQLTF6L.js → mail-VDHJAB4O.js} +3 -3
  50. package/dist/chunks/{meeting-XSD3BDVQ.js → meeting-GA7XCNNP.js} +214 -43
  51. package/dist/chunks/{okr-TMWTIU2K.js → okr-TQIRERPQ.js} +2 -2
  52. package/dist/chunks/operations-5QL3AECO.js +11 -0
  53. package/dist/chunks/{plan-A2FSFA7T.js → plan-4RY4LN2P.js} +2 -2
  54. package/dist/chunks/{project-T26QLBQF.js → project-D6XT2OC5.js} +2 -2
  55. package/dist/chunks/{registry-JCFEYXOE.js → registry-JHRIDQ3A.js} +4 -4
  56. package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
  57. package/dist/chunks/{registry-ZJMJVWFA.js → registry-QKMI43Q6.js} +8 -6
  58. package/dist/chunks/{registry-Q7OQJ3SU.js → registry-VOGKFFTR.js} +2 -2
  59. package/dist/chunks/{room-recommend-UR4ZDZS6.js → room-recommend-D4EUOOWY.js} +4 -3
  60. package/dist/chunks/{schedule-5L2PM4M6.js → schedule-XPFBHPCV.js} +4 -3
  61. package/dist/chunks/{workflow-TR6PG2SL.js → workflow-HLBZRWIY.js} +172 -6
  62. package/dist/core/package-paths.js +16 -2
  63. package/dist/index.js +30 -25
  64. package/dist/internal/e10/auth-recovery.js +14 -4
  65. package/dist/shortcuts/contract/host.js +34 -59
  66. package/dist/shortcuts/cowork/continuation.js +70 -0
  67. package/dist/shortcuts/cowork/errors.js +59 -0
  68. package/dist/shortcuts/cowork/host.js +172 -0
  69. package/dist/shortcuts/cowork/index.js +166 -0
  70. package/dist/shortcuts/cowork/manifest.js +65 -0
  71. package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
  72. package/dist/shortcuts/cowork/operations/item-write.js +380 -0
  73. package/dist/shortcuts/cowork/operations/member-write.js +283 -0
  74. package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
  75. package/dist/shortcuts/cowork/operations/read.js +423 -0
  76. package/dist/shortcuts/cowork/operations/registry.js +39 -0
  77. package/dist/shortcuts/cowork/operations/review-write.js +157 -0
  78. package/dist/shortcuts/cowork/operations/shared.js +215 -0
  79. package/dist/shortcuts/cowork/operations/types.js +24 -0
  80. package/dist/shortcuts/cowork/operations.js +19 -0
  81. package/dist/shortcuts/ehr/host.js +4 -1
  82. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
  83. package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
  84. package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
  85. package/dist/shortcuts/ehr/operations/recruit-write.js +8 -16
  86. package/dist/shortcuts/ehr/operations/recruit.js +35 -10
  87. package/dist/shortcuts/ehr/operations/registry.js +6 -0
  88. package/dist/shortcuts/im/diagnostics.js +97 -0
  89. package/dist/shortcuts/im/host.js +58 -31
  90. package/dist/shortcuts/im/index.js +7 -2
  91. package/dist/shortcuts/im/operations/at-mention.js +127 -0
  92. package/dist/shortcuts/im/operations/file-user.js +10 -2
  93. package/dist/shortcuts/im/operations/group-read.js +26 -5
  94. package/dist/shortcuts/im/operations/group-write.js +35 -28
  95. package/dist/shortcuts/im/operations/msg-sync.js +7 -2
  96. package/dist/shortcuts/im/operations/msg-write.js +76 -10
  97. package/dist/shortcuts/im/operations/quote-mention.js +119 -0
  98. package/dist/shortcuts/im/operations/session.js +1 -1
  99. package/dist/shortcuts/im/operations/shared.js +10 -0
  100. package/dist/shortcuts/im/operations/types.js +13 -0
  101. package/dist/shortcuts/index.js +5 -0
  102. package/dist/shortcuts/mail/host.js +4 -1
  103. package/dist/shortcuts/meeting/host.js +27 -34
  104. package/dist/shortcuts/meeting/operations/create.js +4 -1
  105. package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -0
  106. package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -7
  107. package/dist/shortcuts/meeting/operations/eb/create-flow.js +168 -26
  108. package/dist/shortcuts/meeting/operations/eb/factory.js +69 -1
  109. package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
  110. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +14 -3
  111. package/dist/shortcuts/meeting/operations/eb-fallback.js +159 -0
  112. package/dist/shortcuts/meeting/operations/env.js +93 -46
  113. package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
  114. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  115. package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
  116. package/dist/shortcuts/meeting/operations.js +16 -0
  117. package/dist/shortcuts/workflow/index.js +110 -1
  118. package/dist/shortcuts/workflow/operations/form.js +24 -3
  119. package/dist/shortcuts/workflow/operations/read.js +104 -23
  120. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
  121. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
  122. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
  123. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
  124. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  125. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
  126. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
  127. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
  130. package/docs/e10-auth.md +1 -1
  131. package/docs/im.md +8 -1
  132. package/docs/todo-smart-convert/README.md +1 -1
  133. package/docs/todo-smart-convert/data-contract.md +2 -2
  134. package/docs/todo-smart-convert/subgroup-rules.md +6 -0
  135. package/package.json +1 -1
  136. package/skill-template/domains/yimiaoban.md +3 -3
  137. package/skill-template/skill-template.md +1 -1
  138. package/skills/weaver-e10-blog/SKILL.md +5 -5
  139. package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
  140. package/skills/weaver-e10-calendar/SKILL.md +5 -5
  141. package/skills/weaver-e10-esb/SKILL.md +5 -5
  142. package/skills/weaver-e10-hrm/SKILL.md +5 -5
  143. package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
  144. package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
  145. package/skills/weaver-e10-jucailin/SKILL.md +18 -12
  146. package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
  147. package/skills/weaver-e10-mail/SKILL.md +5 -5
  148. package/skills/weaver-e10-meeting/SKILL.md +171 -25
  149. package/skills/weaver-e10-meeting/references/meeting-eb.md +4 -3
  150. package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
  151. package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
  152. package/skills/weaver-e10-meeting.zip +0 -0
  153. package/skills/weaver-e10-okr/SKILL.md +5 -5
  154. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  155. package/skills/weaver-e10-plan/SKILL.md +5 -5
  156. package/skills/weaver-e10-shared/SKILL.md +12 -12
  157. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
  158. package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
  159. package/skills/weaver-e10-workflow/SKILL.md +29 -5
  160. package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
  161. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
  162. package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
  163. package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
  164. package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
  165. package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
  166. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
  167. package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
  168. package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
  169. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
  170. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
  171. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
  172. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
  173. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
  174. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
  175. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
  176. package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
  177. package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
  178. package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
  179. package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
  180. package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
  181. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
  182. package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
  183. package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
  184. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
  185. package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
  186. package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
@@ -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;
@@ -0,0 +1,119 @@
1
+ import { imValidationError } from '../errors.js';
2
+ import { assertImOk, str, unwrapInner } from './shared.js';
3
+ import { parseContent, parseMsg } from './msg-parse.js';
4
+ /** 调用方是否带了引用/回复入参。 */
5
+ export function hasQuoteInput(args) {
6
+ return args.quoteMsgId !== undefined || args.replyMsgId !== undefined;
7
+ }
8
+ /** 解析被引消息 id(纯数字、非 0)。 */
9
+ function quotedId(value) {
10
+ const id = str(value);
11
+ if (!id || !/^\d+$/.test(id) || id === '0') {
12
+ throw imValidationError('quote_id_invalid', 'quoteMsgId/replyMsgId 必须是非 0 的纯数字消息 id');
13
+ }
14
+ return id;
15
+ }
16
+ /** 从消息对象提取被引内容纯文本(txt 渲染 / [图片] / [文件]名)。 */
17
+ function quotedContentText(msgObj) {
18
+ if (!msgObj)
19
+ return '';
20
+ const parsed = parseContent(msgObj);
21
+ return str(parsed.content).trim();
22
+ }
23
+ /**
24
+ * prepare 阶段解析引用/回复入参:定位被引消息并组装 ref/rmsg 对象。
25
+ * - quoteMsgId 与 replyMsgId 互斥(quote_conflict);
26
+ * - 被引消息拉不到/找不到时报 quote_msg_not_found;
27
+ * - 被引人姓名走 hrm(resolveUsersByIds),回落消息内嵌 sname。
28
+ */
29
+ export async function resolveQuotedRef(args, env, target) {
30
+ const hasQuote = args.quoteMsgId !== undefined && args.quoteMsgId !== null && args.quoteMsgId !== '';
31
+ const hasReply = args.replyMsgId !== undefined && args.replyMsgId !== null && args.replyMsgId !== '';
32
+ if (hasQuote && hasReply) {
33
+ throw imValidationError('quote_conflict', 'quoteMsgId(引用)与 replyMsgId(回复)只能二选一');
34
+ }
35
+ if (!hasQuote && !hasReply)
36
+ return null;
37
+ const kind = hasQuote ? 'quote' : 'reply';
38
+ const msgId = quotedId(hasQuote ? args.quoteMsgId : args.replyMsgId);
39
+ const isSingle = !target.groupId;
40
+ if (isSingle && (!target.toUid || !target.toCid)) {
41
+ throw imValidationError('quote_target_required', '单聊引用需要 toUid + toCid 才能定位被引消息');
42
+ }
43
+ const scopeLabel = isSingle ? `与 ${target.toUid} 的单聊` : `群 ${target.groupId}`;
44
+ let rows = [];
45
+ try {
46
+ // 单条定位:id ± 1 秒(id 为纳秒级时间戳,开边界语义)
47
+ const idBig = BigInt(msgId);
48
+ const window = { start: String(idBig + 1000000000n), end: String(idBig - 1000000000n), max_num: 5 };
49
+ const res = isSingle
50
+ ? await env.host.request({
51
+ method: 'POST',
52
+ path: '/api/em/msg/executeIm/msg/syncSingleMsg',
53
+ body: { trans_id: 0, dev: 4, enc_type: 0, datas: [{ from_cid: String(target.toCid), from_uid: String(target.toUid), ...window }] },
54
+ operation: 'im.msg.send.single.quoteResolve',
55
+ })
56
+ : await env.host.request({
57
+ method: 'POST',
58
+ path: '/api/em/msg/executeIm/chat/syncGroupMsg',
59
+ body: { trans_id: 0, dev: 4, type: 0, enc_type: 0, datas: [{ group_id: String(target.groupId), ...window }] },
60
+ operation: 'im.msg.send.group.quoteResolve',
61
+ });
62
+ assertImOk(res, '定位被引消息');
63
+ const outer = unwrapInner(res)?.datas || [];
64
+ for (const group of outer)
65
+ rows.push(...(group?.datas || group?.data || []));
66
+ }
67
+ catch (error) {
68
+ throw imValidationError('quote_msg_not_found', `无法定位被引消息 ${msgId}:${error instanceof Error ? error.message : String(error)}`, { ...target, quotedMsgId: msgId });
69
+ }
70
+ const row = rows.find((r) => str(r?.ser_msgid) === msgId);
71
+ if (!row) {
72
+ throw imValidationError('quote_msg_not_found', `消息 ${msgId} 不在${scopeLabel}内(或已被撤回),无法引用`, {
73
+ ...target,
74
+ quotedMsgId: msgId,
75
+ });
76
+ }
77
+ const msgObj = parseMsg(row.msg);
78
+ const fromUid = str(row.from_uid) || str(msgObj?.suid);
79
+ const fromCid = str(row.from_cid) || str(msgObj?.scid);
80
+ let fromName = str(msgObj?.sname);
81
+ if (fromUid) {
82
+ try {
83
+ const personMap = await env.host.resolveUsersByIds([fromUid]);
84
+ fromName = personMap[fromUid]?.name || fromName;
85
+ }
86
+ catch {
87
+ // hrm 失败时回落消息内嵌姓名
88
+ }
89
+ }
90
+ const ref = {
91
+ id: msgId,
92
+ t: str(msgObj?.type ?? row.kind ?? '1') || '1',
93
+ f: '0',
94
+ time: Number(row.msgtime) > 0 ? Number(row.msgtime) * 1000 : Date.now(),
95
+ fname: fromName || fromUid || '',
96
+ fuser: { cid: fromCid, uid: fromUid },
97
+ rcon: quotedContentText(msgObj),
98
+ display: '',
99
+ ...(kind === 'reply' ? { oid: msgId } : {}),
100
+ };
101
+ return { kind, quotedMsgId: msgId, fromName: ref.fname, fromUid, text: ref.rcon, ref };
102
+ }
103
+ /** 拉取侧:从消息对象提取顶层引用/回复摘要(msg.msg.ref / msg.msg.rmsg)。 */
104
+ export function extractQuoteSummary(msgObj) {
105
+ const raw = msgObj ? (msgObj.rmsg ?? msgObj.ref) : undefined;
106
+ if (!raw || typeof raw !== 'object')
107
+ return undefined;
108
+ const kind = msgObj?.rmsg ? 'reply' : 'quote';
109
+ return {
110
+ kind,
111
+ id: str(raw.id),
112
+ msgType: str(raw.t),
113
+ fromName: str(raw.fname),
114
+ fromUid: str(raw.fuser?.uid),
115
+ fromCid: str(raw.fuser?.cid),
116
+ msgtime: raw.time,
117
+ text: str(raw.rcon),
118
+ };
119
+ }
@@ -241,7 +241,7 @@ const sysmsgTypeSync = {
241
241
  inputSchema: schemaOf({
242
242
  gids: { type: 'array', items: { type: 'integer' }, description: '系统分组 id(与 tids 至少其一)' },
243
243
  tids: { type: 'array', items: { type: 'integer' }, description: '消息类型 id' },
244
- }, { description: 'gids / tids 至少其一' }),
244
+ }, { requiredAny: ['gids', 'tids'], description: '按 gids/tids 拉取系统消息分组/类型配置(至少其一)' }),
245
245
  handler: async (args, { host }) => {
246
246
  const gids = Array.isArray(args.gids) ? args.gids.map((x) => Number(x)).filter((n) => Number.isInteger(n)) : [];
247
247
  const tids = Array.isArray(args.tids) ? args.tids.map((x) => Number(x)).filter((n) => Number.isInteger(n)) : [];
@@ -244,6 +244,16 @@ export function assertImOk(value, actionLabel) {
244
244
  export function unwrapInner(value) {
245
245
  return value?.data?.data ?? value?.data;
246
246
  }
247
+ /**
248
+ * 服务端在建群/入群时自动补入的「系统成员」cid(群内系统消息的投递载体)。
249
+ * 属产品可选能力(租户侧可关闭),不是真实员工:姓名会回落成 uid,hrm 解析不到。
250
+ * 统计真实成员数、判断「是否只剩自己」时必须排除它。
251
+ */
252
+ export const SYSTEM_MEMBER_CID = '10002';
253
+ /** 是否服务端系统成员(非真实员工)。 */
254
+ export function isSystemMember(member) {
255
+ return str(member?.cid) === SYSTEM_MEMBER_CID;
256
+ }
247
257
  /** 群成员条目判定(成员行至少含 uid 或 user 身份)。 */
248
258
  function isMemberRow(value) {
249
259
  return Boolean(value) && typeof value === 'object' && (value.uid !== undefined || value.user !== undefined);
@@ -8,11 +8,24 @@ export function imSuccess(action, data, meta) {
8
8
  }
9
9
  /** 构造「对象类型 + 严格禁止额外字段」的 JSON Schema。 */
10
10
  export function schemaOf(properties, options = {}) {
11
+ const clauses = [];
12
+ if (options.requiredAny?.length)
13
+ clauses.push({ anyOf: options.requiredAny.map((key) => ({ required: [key] })) });
14
+ if (options.anyOf?.length)
15
+ clauses.push({ anyOf: options.anyOf });
16
+ if (options.allOf?.length)
17
+ clauses.push(...options.allOf);
18
+ const condition = clauses.length === 1
19
+ ? clauses[0]
20
+ : clauses.length > 1
21
+ ? { allOf: clauses }
22
+ : {};
11
23
  return {
12
24
  type: 'object',
13
25
  additionalProperties: false,
14
26
  properties,
15
27
  ...(options.required?.length ? { required: options.required } : {}),
28
+ ...condition,
16
29
  ...(options.description ? { description: options.description } : {}),
17
30
  };
18
31
  }
@@ -35,6 +35,11 @@ export const builtinShortcuts = [
35
35
  description: '泛微E10聚才林(人事/出勤/薪酬)CLI',
36
36
  load: async () => (await import('./ehr/index.js')).ehrShortcut,
37
37
  }),
38
+ lazyShortcut({
39
+ name: 'cowork',
40
+ description: '泛微E10协作区(协作事项/讨论/审批/共享权限)CLI',
41
+ load: async () => (await import('./cowork/index.js')).coworkShortcut,
42
+ }),
38
43
  lazyShortcut({
39
44
  name: 'archive',
40
45
  description: 'E10 文书定档案管理 CLI',
@@ -96,7 +96,10 @@ function mailMessage(value) {
96
96
  export function createMailHost(ctx, dependencies = {}) {
97
97
  let e10Session = null;
98
98
  let sessionContext = null;
99
- const requireSession = dependencies.requireSession || requireE10Session;
99
+ // 与 workflow 模块同口径(2026-10-29 实测):auth 文件里已有登录时落盘的 userId,
100
+ // 业务命令不需要 teamsCheck 的登录人信息富化 ⇒ 跳过它,省掉每次调用一次串行 HTTPS 往返(实测约 150~250ms)。
101
+ const requireSession = dependencies.requireSession
102
+ || ((innerCtx) => requireE10Session(innerCtx, { skipProfileEnrichment: true }));
100
103
  async function ensureSession() {
101
104
  if (!e10Session) {
102
105
  e10Session = await requireSession(ctx);