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
@@ -42,7 +42,7 @@ const LIST_KEYWORDS = {
42
42
  biz_config: ['业务配置映射表'],
43
43
  };
44
44
  /** 列表类型 -> 缓存键 + 表单逻辑名(candidate.list 四列表映射,同 recruit-list.mjs TYPES) */
45
- const LIST_TYPES = {
45
+ export const LIST_TYPES = {
46
46
  demand: { listKey: 'demand', formType: 'demand_manage', title: '招聘需求' },
47
47
  position: { listKey: 'position', formType: 'position_manage', title: '招聘职位' },
48
48
  entry: { listKey: 'entry', formType: 'entry_manage', title: '入职管理' },
@@ -58,7 +58,7 @@ const INTERNAL_KEYS = new Set([
58
58
  const DISPLAY_UI_KEYS = new Set([
59
59
  ...INTERNAL_KEYS, 'operate', 'isToDo', 'todoPropSpan', 'flow_status',
60
60
  ]);
61
- function randHex32() {
61
+ export function randHex32() {
62
62
  return randomUUID().replace(/-/g, '');
63
63
  }
64
64
  function randHex6() {
@@ -419,7 +419,7 @@ export function createRecruitContextLoader() {
419
419
  return { load };
420
420
  }
421
421
  /** EB 列表通用查询体(同 recruit-list.mjs baseBody) */
422
- function buildListBody(params) {
422
+ export function buildListBody(params) {
423
423
  return {
424
424
  current: params.current ?? 1,
425
425
  pageSize: params.pageSize ?? 10,
@@ -444,7 +444,7 @@ function buildListBody(params) {
444
444
  };
445
445
  }
446
446
  /** 列表 + 计数并行;displayData 优先(同 recruit-search/recruit-list 的显示层策略) */
447
- async function fetchListRows(host, objId, listId, body) {
447
+ export async function fetchListRows(host, objId, listId, body) {
448
448
  const countBody = { ...body };
449
449
  delete countBody.current;
450
450
  delete countBody.pageSize;
@@ -466,8 +466,33 @@ async function fetchListRows(host, objId, listId, body) {
466
466
  const hasDisplay = Array.isArray(displayRows) && displayRows.length > 0;
467
467
  const rows = hasDisplay ? displayRows : rawRows;
468
468
  const countData = countResponse ? normalizePaged(countResponse).data : null;
469
- const total = countData?.total !== undefined ? countData.total : rows.length;
470
- return { rows, displayRows: hasDisplay ? displayRows : null, total };
469
+ const pageSize = typeof body.pageSize === 'number'
470
+ ? body.pageSize
471
+ : (typeof body.pageSize === 'string' ? (Number(body.pageSize) || rows.length) : rows.length);
472
+ // 总数取值(不输出假值,对齐 ehr recruit-list.mjs 三态降级):
473
+ // ① 计数接口可用(200 且 total 为数字)→ 精确;
474
+ // ② 计数不可用但本页未满(rows < pageSize,即已到末页)→ 总数=已取回行数(精确);
475
+ // ③ 计数不可用且本页已满 → total=null(总数不可知),附 note 提示加大 pageSize。
476
+ // 背景:部分租户/列表 getListDataCount 恒 500,且 getListDatas 的 total 不可信,唯一可靠判据是分页本身。
477
+ const countUsable = countData !== null && typeof countData.total === 'number';
478
+ let total;
479
+ let totalExact;
480
+ let totalNote;
481
+ if (countUsable) {
482
+ total = countData.total;
483
+ totalExact = true;
484
+ }
485
+ else if (rows.length < pageSize) {
486
+ total = rows.length;
487
+ totalExact = true;
488
+ totalNote = '计数接口不可用,本页未满(已到末页),总数按已取回行数得出';
489
+ }
490
+ else {
491
+ total = null;
492
+ totalExact = false;
493
+ totalNote = '计数接口不可用且本页已满,总数不可知(可调大 pageSize 取回全部行数)';
494
+ }
495
+ return { rows, displayRows: hasDisplay ? displayRows : null, total, totalExact, totalNote };
471
496
  }
472
497
  // ─────────────────────────────────────────────────────────────────────────────
473
498
  // operations
@@ -537,7 +562,7 @@ const candidateSearch = {
537
562
  pageSize: toNumber(args.pageSize, 'pageSize') ?? 10,
538
563
  conditions,
539
564
  });
540
- const { rows, displayRows, total } = await fetchListRows(env.host, talentObjId, talentList.listId, body);
565
+ const { rows, displayRows, total, totalExact, totalNote } = await fetchListRows(env.host, talentObjId, talentList.listId, body);
541
566
  // 显示层优先:displayData 已内联人员/字典中文,零额外请求;
542
567
  // 缺失时回退原始层:字典映射 + 纯 ID 经 hrm getEmployeeByIds 批量反查(兜底,行为不变)
543
568
  let creatorNameMap = {};
@@ -590,7 +615,7 @@ const candidateSearch = {
590
615
  createTime: row?.create_time || null,
591
616
  };
592
617
  });
593
- return ehrSuccess('ehr.recruit.candidate.search', { total, rows: dataRows });
618
+ return ehrSuccess('ehr.recruit.candidate.search', { total, totalExact, ...(totalNote ? { totalNote } : {}), rows: dataRows });
594
619
  },
595
620
  };
596
621
  const candidateList = {
@@ -632,7 +657,7 @@ const candidateList = {
632
657
  current: toPositiveInt(args.current, 'current') ?? 1,
633
658
  pageSize: toNumber(args.pageSize, 'pageSize') ?? 10,
634
659
  });
635
- const { rows, displayRows, total } = await fetchListRows(env.host, formObjId, listParam.listId, body);
660
+ const { rows, displayRows, total, totalExact, totalNote } = await fetchListRows(env.host, formObjId, listParam.listId, body);
636
661
  const outRows = rows.map((row) => {
637
662
  const cleaned = { form_data_id: row?.form_data_id || row?.id };
638
663
  for (const [key, value] of Object.entries(row || {})) {
@@ -669,7 +694,7 @@ const candidateList = {
669
694
  return cleaned;
670
695
  });
671
696
  return ehrSuccess('ehr.recruit.candidate.list', {
672
- type, total, current: body.current, pageSize: body.pageSize, rows: outRows,
697
+ type, total, totalExact, ...(totalNote ? { totalNote } : {}), current: body.current, pageSize: body.pageSize, rows: outRows,
673
698
  });
674
699
  },
675
700
  };
@@ -6,6 +6,9 @@ import { hrDataSourceOperations } from './hr-ds.js';
6
6
  import { hrReportOperations } from './hr-report.js';
7
7
  import { recruitOperations } from './recruit.js';
8
8
  import { recruitWriteOperations } from './recruit-write.js';
9
+ import { recruitStatsOperations } from './recruit-stats.js';
10
+ import { recruitPositionOperations } from './recruit-position.js';
11
+ import { recruitCandidateImportOperations } from './recruit-candidate-import.js';
9
12
  import { salaryOperations } from './salary.js';
10
13
  function buildRegistry() {
11
14
  const registry = new Map();
@@ -18,6 +21,9 @@ function buildRegistry() {
18
21
  ...hrReportOperations,
19
22
  ...recruitOperations,
20
23
  ...recruitWriteOperations,
24
+ ...recruitStatsOperations,
25
+ ...recruitPositionOperations,
26
+ ...recruitCandidateImportOperations,
21
27
  ];
22
28
  for (const definition of definitions) {
23
29
  if (registry.has(definition.name)) {
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 易秒办(im)域内的诊断能力:**只动本模块目录,不改公共 core/cmd/internal**。
3
+ *
4
+ * 两个能力都通过公共已有的扩展点实现:
5
+ * - 失败信封双写:`im` 自己的 catch 分支里写 stdout + stderr,不扩 `Output` 类;
6
+ * - 请求级追踪:走 `HookManager.registerE10RequestInterceptor()`,不碰 `E10RequestRuntime`。
7
+ */
8
+ const TRACE_ENV = 'WEAVER_WORK_CLI_TRACE';
9
+ /** 同一进程内只注册一次拦截器(`forCommand()` 复用同一个 HookManager 实例)。 */
10
+ const registeredHooks = new WeakSet();
11
+ function truthy(value) {
12
+ const raw = String(value ?? '').toLowerCase();
13
+ return raw === '1' || raw === 'true' || raw === 'yes';
14
+ }
15
+ /** 从当前命令向上找 `im --trace`:命令级开关,不占用全局选项槽位。 */
16
+ function traceFromCommand(ctx) {
17
+ let current = ctx.activeCommand;
18
+ for (let depth = 0; current && depth <= 3; depth += 1) {
19
+ const opts = typeof current.opts === 'function' ? current.opts() : undefined;
20
+ if (opts?.trace === true)
21
+ return true;
22
+ current = current.parent ?? undefined;
23
+ }
24
+ return false;
25
+ }
26
+ export function imTraceEnabled(ctx) {
27
+ if (truthy(ctx.env?.[TRACE_ENV]) || truthy(process.env[TRACE_ENV]))
28
+ return true;
29
+ try {
30
+ return traceFromCommand(ctx);
31
+ }
32
+ catch {
33
+ // 单测里的 ctx 可能是最小 mock:追踪开关不得影响请求本身
34
+ return false;
35
+ }
36
+ }
37
+ /** 按 path 记录在途请求的起始时间;同 path 并发时按 FIFO 配对。 */
38
+ const inflight = new Map();
39
+ function remember(path) {
40
+ const list = inflight.get(path) ?? [];
41
+ list.push(Date.now());
42
+ inflight.set(path, list);
43
+ }
44
+ function forget(path) {
45
+ const list = inflight.get(path);
46
+ if (!list?.length)
47
+ return undefined;
48
+ const startedAt = list.shift();
49
+ if (!list.length)
50
+ inflight.delete(path);
51
+ return startedAt;
52
+ }
53
+ function label(request) {
54
+ const operation = request.operation || '-';
55
+ return `${request.method} ${request.path} op=${operation}`;
56
+ }
57
+ function traceInterceptor() {
58
+ return {
59
+ beforeRequest({ request }) {
60
+ remember(request.path);
61
+ },
62
+ afterResponse({ request, response }) {
63
+ const startedAt = forget(request.path);
64
+ const ms = startedAt === undefined ? '-' : `${Date.now() - startedAt}ms`;
65
+ process.stderr.write(`[trace] ${label(request)} status=${response.status} ${ms}\n`);
66
+ },
67
+ onError({ request, error }) {
68
+ const startedAt = forget(request.path);
69
+ const ms = startedAt === undefined ? '-' : `${Date.now() - startedAt}ms`;
70
+ const code = error?.subtype
71
+ || error?.code
72
+ || 'unknown';
73
+ // 只打方法/路径/operation/错误码/耗时,绝不打印请求头与请求体(可能含凭证)
74
+ process.stderr.write(`[trace] ${label(request)} status=ERR code=${code} ${ms}\n`);
75
+ },
76
+ };
77
+ }
78
+ /** 按需注册追踪拦截器:未开启 trace 时完全零开销。 */
79
+ export function registerImTrace(ctx) {
80
+ if (!imTraceEnabled(ctx))
81
+ return;
82
+ const hooks = ctx.hooks;
83
+ if (!hooks || registeredHooks.has(hooks))
84
+ return;
85
+ registeredHooks.add(hooks);
86
+ ctx.hooks.registerE10RequestInterceptor(traceInterceptor());
87
+ }
88
+ /**
89
+ * 失败信封:**stdout 与 stderr 各写一份**(内容相同)。
90
+ * stdout 是主通道 —— 只读 stdout 的调用方也能拿到结构化错误;
91
+ * stderr 保留一份,兼容「stderr 有内容 = 失败」的历史判断。
92
+ */
93
+ export function printImFailure(envelope) {
94
+ const text = `${JSON.stringify(envelope, null, 2)}\n`;
95
+ process.stdout.write(text);
96
+ process.stderr.write(text);
97
+ }
@@ -94,6 +94,7 @@ export function createImHost(ctx, dependencies = {}) {
94
94
  return createE10RequestRuntime(ctx, await ensureSession(), {
95
95
  interceptors: dependencies.interceptors,
96
96
  fetch: dependencies.fetch,
97
+ recoverAuthentication: dependencies.recoverAuthentication,
97
98
  });
98
99
  }
99
100
  async function request(request) {
@@ -144,41 +145,53 @@ export function createImHost(ctx, dependencies = {}) {
144
145
  }
145
146
  async function requestBinary(url, headers = {}) {
146
147
  const session = await ensureSession();
147
- const controller = new AbortController();
148
- const timer = setTimeout(() => controller.abort(), 60_000);
148
+ const baseUrl = new URL(session.getBaseUrl());
149
+ let target;
149
150
  try {
150
- const resp = await (dependencies.fetch || globalThis.fetch)(url, {
151
- headers: {
152
- accept: 'application/json, text/plain, */*',
153
- cookie: session.cookieHeader(),
154
- eteamsid: session.eteamsId,
155
- 'user-agent': session.uaHeader(),
156
- origin: session.getBaseUrl(),
157
- ...headers,
158
- },
159
- redirect: 'manual',
160
- signal: controller.signal,
151
+ target = new URL(url, baseUrl);
152
+ }
153
+ catch {
154
+ throw imValidationError('download_url_invalid', '下载地址不是合法 URL');
155
+ }
156
+ if (target.origin !== baseUrl.origin) {
157
+ throw imValidationError('download_url_cross_origin', '下载地址不允许跨 E10 环境');
158
+ }
159
+ assertPath(target.pathname);
160
+ const query = {};
161
+ for (const [key, value] of target.searchParams) {
162
+ const previous = query[key];
163
+ if (previous === undefined)
164
+ query[key] = value;
165
+ else if (Array.isArray(previous))
166
+ previous.push(value);
167
+ else
168
+ query[key] = [previous, value];
169
+ }
170
+ let response;
171
+ try {
172
+ response = await (await runtime()).requestBinary({
173
+ module: 'im',
174
+ operation: 'im.file.download',
175
+ method: 'GET',
176
+ path: target.pathname,
177
+ query,
178
+ headers,
179
+ timeoutMs: 60_000,
161
180
  });
162
- if (resp.status === 401 || resp.status === 302) {
163
- throw imAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE);
164
- }
165
- if (resp.status !== 200) {
166
- throw new ImCliError('api', 'download_failed', `下载失败 HTTP ${resp.status}`, {
167
- code: resp.status,
168
- exitCode: 1,
169
- });
170
- }
171
- return Buffer.from(await resp.arrayBuffer());
172
181
  }
173
182
  catch (error) {
174
- if (error instanceof ImCliError)
175
- throw error;
176
- const message = error instanceof Error ? error.message : String(error);
177
- throw imNetworkError('transport', `下载失败:${message}`, { retryable: true });
183
+ mapRuntimeNetworkError(error);
184
+ }
185
+ if (response.status === 401 || response.status === 302) {
186
+ throw imAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE);
178
187
  }
179
- finally {
180
- clearTimeout(timer);
188
+ if (response.status !== 200) {
189
+ throw new ImCliError('api', 'download_failed', `下载失败 HTTP ${response.status}`, {
190
+ code: response.status,
191
+ exitCode: 1,
192
+ });
181
193
  }
194
+ return response.content;
182
195
  }
183
196
  /** teamsCheck:解析当前登录人的 imUid/imCid/name(IM uid 语义 = 员工 id)。 */
184
197
  async function getSelfIdentity() {
@@ -248,11 +261,25 @@ export function createImHost(ctx, dependencies = {}) {
248
261
  timeoutMs: 15_000,
249
262
  });
250
263
  const preData = preRes?.data || {};
264
+ /**
265
+ * 秒传分支:服务端按 MD5 命中已有文件时**不再返回 preUploadId**,而是直接给出 fileObj
266
+ * (`uploadType: "second"`)。实测两种结构:
267
+ * { uploadType: 'second', fileObj: {...} }
268
+ * { uploadType: 'second', imUploadInfoDto: { uploadType: 'second', fileObj: {...}, transcode: {...} } }
269
+ * 旧实现只认 preUploadId,导致「同一张图/同一个文件第二次发送」必定失败(upload_check_failed,
270
+ * 但响应本身是 code:200 status:true)。命中秒传时直接复用 fileObj,跳过 valid/upload。
271
+ */
272
+ const preInfo = preData.imUploadInfoDto || preData;
273
+ const preFileObj = preInfo?.fileObj && (preInfo.fileObj.id || preInfo.fileObj.fileid) ? preInfo.fileObj : null;
274
+ const preUploadType = String(preData.uploadType || preInfo?.uploadType || '');
275
+ if (preFileObj && (!preData.preUploadId || preUploadType === 'second')) {
276
+ return preFileObj;
277
+ }
251
278
  if (!preData.preUploadId) {
252
- throw new ImCliError('api', 'upload_check_failed', `preUploadCheck 失败`, { details: preRes, exitCode: 1 });
279
+ throw new ImCliError('api', 'upload_check_failed', `preUploadCheck 失败(未返回 preUploadId,也没有可复用的秒传 fileObj)`, { details: preRes, exitCode: 1 });
253
280
  }
254
281
  const preUploadId = String(preData.preUploadId);
255
- const uploadType = preData.uploadType || 'common';
282
+ const uploadType = preUploadType || 'common';
256
283
  // 第 2 步:实传
257
284
  const upForm = new FormData();
258
285
  upForm.append('data', new Blob([data], { type: mime }), name);
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { ExitCode, isCliError, toErrorEnvelope } from '../../core/errors.js';
3
3
  import { createImContinuationSigner } from './continuation.js';
4
+ import { printImFailure, registerImTrace } from './diagnostics.js';
4
5
  import { imValidationError } from './errors.js';
5
6
  import { createImHost } from './host.js';
6
7
  import { imManifest } from './manifest.js';
@@ -81,6 +82,8 @@ function failureEnvelope(error) {
81
82
  };
82
83
  }
83
84
  async function executeAndPrint(ctx, operation, input) {
85
+ // 排障/接口计数:`im --trace` 或 WEAVER_WORK_CLI_TRACE=1(实现完全在 im 域内,不改公共 runtime)
86
+ registerImTrace(ctx);
84
87
  try {
85
88
  const result = await executeImOperation(operation, input, {
86
89
  ctx,
@@ -90,7 +93,7 @@ async function executeAndPrint(ctx, operation, input) {
90
93
  ctx.output.write(successEnvelope(operation, result), { json: true });
91
94
  }
92
95
  catch (error) {
93
- console.error(JSON.stringify(failureEnvelope(error), null, 2));
96
+ printImFailure(failureEnvelope(error));
94
97
  process.exitCode = isCliError(error) ? error.exitCode : ExitCode.GeneralError;
95
98
  }
96
99
  }
@@ -110,7 +113,9 @@ export const imShortcut = {
110
113
  description: '泛微E10易秒办(即时通讯)CLI',
111
114
  category: 'shortcut',
112
115
  register({ program, action }) {
113
- const im = program.command('im').description('泛微E10易秒办:消息/群组/会话/系统消息');
116
+ const im = program.command('im')
117
+ .description('泛微E10易秒办:消息/群组/会话/系统消息')
118
+ .option('--trace', '每个 E10 请求在 stderr 打一行 [trace](method/path/op/status/耗时,不含凭证)');
114
119
  im.command('schema')
115
120
  .description('Print im operation manifest for agents')
116
121
  .action(action((ctx) => {
@@ -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,