weaver-work-cli 0.1.36 → 0.1.39

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 (169) hide show
  1. package/README.md +4 -2
  2. package/dist/chunks/{archive-XBVH7JU7.js → archive-ENZYBBME.js} +559 -68
  3. package/dist/chunks/{archive-oa-MZM3EKFQ.js → archive-oa-YWLEC5V4.js} +4 -3
  4. package/dist/chunks/{asset-4XO7UOUW.js → asset-OOV6DWPA.js} +8 -7
  5. package/dist/chunks/asset-idle-RE632MY5.js +670 -0
  6. package/dist/chunks/{attachment-content-VXSWANAC.js → attachment-content-RWN2FCLL.js} +3 -3
  7. package/dist/chunks/{blog-PGZKOPSC.js → blog-PEPOJAL7.js} +5 -4
  8. package/dist/chunks/{caizhilian-BH4ZVGUN.js → caizhilian-WYIQDVPJ.js} +5 -4
  9. package/dist/chunks/{calendar-MXEMTIO4.js → calendar-CIARBAOX.js} +4 -3
  10. package/dist/chunks/{chunk-NCOVUAZK.js → chunk-36DIFZAH.js} +4 -3
  11. package/dist/chunks/{chunk-I6QGIBDM.js → chunk-BJGXMFGD.js} +4 -3
  12. package/dist/chunks/{chunk-TC6MZAO3.js → chunk-CQHGNCK2.js} +52 -2
  13. package/dist/chunks/{chunk-WXFPWCXG.js → chunk-EINRJHQ3.js} +3 -3
  14. package/dist/chunks/{chunk-JUXVYKII.js → chunk-EZGFHJFW.js} +201 -19
  15. package/dist/chunks/{chunk-VPUTVEMC.js → chunk-F2EQXUEE.js} +436 -290
  16. package/dist/chunks/{chunk-JYKABYCV.js → chunk-GVPJZXJ2.js} +3 -3
  17. package/dist/chunks/{chunk-6LLJFKJE.js → chunk-HJYVHXUQ.js} +1 -1
  18. package/dist/chunks/{chunk-T72N74ZT.js → chunk-I2HB737U.js} +5 -4
  19. package/dist/chunks/{chunk-VPXOPQJI.js → chunk-K3RUEEOL.js} +4 -2
  20. package/dist/chunks/{chunk-HXJQI5NM.js → chunk-KTCB5R5H.js} +3 -2
  21. package/dist/chunks/{chunk-XMGSYD6S.js → chunk-L4N7B7ZD.js} +4 -4
  22. package/dist/chunks/{chunk-QYGO5EE2.js → chunk-MUQUHKPG.js} +4 -3
  23. package/dist/chunks/{chunk-QHFANTX7.js → chunk-NLK2RZKG.js} +1348 -90
  24. package/dist/chunks/{chunk-F746NKNF.js → chunk-P2DAISYW.js} +1 -1
  25. package/dist/chunks/{chunk-BPE37FPB.js → chunk-T7JDJJ7H.js} +3 -3
  26. package/dist/chunks/{chunk-JNG42JKG.js → chunk-XXJOSZTQ.js} +6 -6
  27. package/dist/chunks/{chunk-5DQEEGCC.js → chunk-YS5DPN4T.js} +1 -1
  28. package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
  29. package/dist/chunks/{chunk-JCSN3JGM.js → chunk-ZXRNHZTE.js} +7 -6
  30. package/dist/chunks/{conflict-JAB2LJEZ.js → conflict-DRMDOL56.js} +2 -2
  31. package/dist/chunks/{contract-XI2EEIZU.js → contract-73MERZWN.js} +4 -3
  32. package/dist/chunks/{cowork-SCPGLBA4.js → cowork-HNGBPD7A.js} +4 -3
  33. package/dist/chunks/{ebuilder-form-EYLHNOTA.js → ebuilder-form-OY6DVNRK.js} +3 -3
  34. package/dist/chunks/{ehr-EAP77IMT.js → ehr-RAQNEQO7.js} +4 -3
  35. package/dist/chunks/{esb-2TGGXEE7.js → esb-6XV7C7YV.js} +4 -4
  36. package/dist/chunks/{fna-2X7ZE7BU.js → fna-XTSRB7G7.js} +4 -3
  37. package/dist/chunks/{host-Q4LZGCRU.js → host-3F5UM5GN.js} +3 -3
  38. package/dist/chunks/{host-XCLDDXKQ.js → host-A7J6GDUP.js} +3 -3
  39. package/dist/chunks/{host-I4NRK4MX.js → host-VGPVP7YF.js} +3 -3
  40. package/dist/chunks/{host-C6EL2RKL.js → host-YAQAPYNP.js} +3 -3
  41. package/dist/chunks/{host-UEI5FGDM.js → host-YMBXWFWX.js} +3 -3
  42. package/dist/chunks/{hrm-YXERJM76.js → hrm-TLHKC545.js} +3 -3
  43. package/dist/chunks/{im-A4VZN2TQ.js → im-YOUCFMUK.js} +3 -3
  44. package/dist/chunks/{invoice-2Z6YPMIS.js → invoice-WNBS2B4C.js} +3 -3
  45. package/dist/chunks/{jiuchuanhui-AR5GMKZB.js → jiuchuanhui-XABWS5RN.js} +4 -3
  46. package/dist/chunks/{lazy-dispatch-QXW7P4X3.js → lazy-dispatch-NLZ5UQQI.js} +5 -5
  47. package/dist/chunks/{mail-7GZ6CPBJ.js → mail-LRYMBFNL.js} +4 -3
  48. package/dist/chunks/{meeting-3NMGA3MB.js → meeting-ESXL6MFI.js} +13 -12
  49. package/dist/chunks/{okr-BZ6XUOAG.js → okr-EOV65SSF.js} +4 -3
  50. package/dist/chunks/{plan-DR5CMUYP.js → plan-YFPVKW6Y.js} +4 -3
  51. package/dist/chunks/{project-6J63MK33.js → project-MDVZUYQW.js} +4 -3
  52. package/dist/chunks/{registry-VRTNC6UR.js → registry-QCLZEDAP.js} +8 -8
  53. package/dist/chunks/{registry-KDD7DHNK.js → registry-VT5Y2HLA.js} +4 -4
  54. package/dist/chunks/{registry-TKX2E7C7.js → registry-YX4CSEMD.js} +3 -3
  55. package/dist/chunks/{room-recommend-XCWRMB5P.js → room-recommend-L3MWH4JW.js} +4 -4
  56. package/dist/chunks/{schedule-IVBICP32.js → schedule-UASAVDVD.js} +4 -4
  57. package/dist/chunks/{workflow-D74NFK3C.js → workflow-KDGIUCNW.js} +9 -6
  58. package/dist/core/output.js +16 -6
  59. package/dist/index.js +33 -28
  60. package/dist/internal/e10/auth/messages.js +1 -0
  61. package/dist/internal/e10/auth-recovery.js +127 -7
  62. package/dist/internal/e10/client.js +16 -2
  63. package/dist/internal/e10/context.js +16 -3
  64. package/dist/internal/e10/request-runtime.js +106 -8
  65. package/dist/shortcuts/archive/host.js +5 -2
  66. package/dist/shortcuts/archive/manifest.js +1 -1
  67. package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
  68. package/dist/shortcuts/archive/operations/flow-url.js +1 -1
  69. package/dist/shortcuts/archive/operations/prelib.js +472 -0
  70. package/dist/shortcuts/archive/operations/registry.js +2 -0
  71. package/dist/shortcuts/archive/operations/upload.js +1 -1
  72. package/dist/shortcuts/archive-oa/host.js +2 -1
  73. package/dist/shortcuts/asset/host.js +6 -5
  74. package/dist/shortcuts/asset-idle/errors.js +44 -0
  75. package/dist/shortcuts/asset-idle/host.js +161 -0
  76. package/dist/shortcuts/asset-idle/index.js +114 -0
  77. package/dist/shortcuts/asset-idle/manifest.js +159 -0
  78. package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
  79. package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
  80. package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
  81. package/dist/shortcuts/asset-idle/operations/types.js +9 -0
  82. package/dist/shortcuts/asset-idle/operations.js +15 -0
  83. package/dist/shortcuts/blog/host.js +3 -2
  84. package/dist/shortcuts/caizhilian/host.js +3 -2
  85. package/dist/shortcuts/calendar/host.js +2 -1
  86. package/dist/shortcuts/contract/host.js +2 -1
  87. package/dist/shortcuts/cowork/host.js +2 -1
  88. package/dist/shortcuts/ebuilder-form/host.js +3 -1
  89. package/dist/shortcuts/ehr/host.js +2 -1
  90. package/dist/shortcuts/esb/host.js +2 -1
  91. package/dist/shortcuts/fna/host.js +2 -1
  92. package/dist/shortcuts/hrm/host.js +2 -1
  93. package/dist/shortcuts/im/host.js +3 -2
  94. package/dist/shortcuts/index.js +5 -0
  95. package/dist/shortcuts/invoice/host.js +5 -4
  96. package/dist/shortcuts/jiuchuanhui/host.js +2 -1
  97. package/dist/shortcuts/mail/host.js +2 -1
  98. package/dist/shortcuts/meeting/host.js +3 -2
  99. package/dist/shortcuts/meeting/invitation/text.js +71 -0
  100. package/dist/shortcuts/meeting/operations/eb/create-flow.js +1073 -50
  101. package/dist/shortcuts/meeting/operations/eb/crm-search.js +478 -0
  102. package/dist/shortcuts/meeting/operations/eb/factory.js +60 -1
  103. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  104. package/dist/shortcuts/meeting/operations/invitation.js +122 -9
  105. package/dist/shortcuts/okr/host.js +2 -1
  106. package/dist/shortcuts/plan/host.js +2 -1
  107. package/dist/shortcuts/project/host.js +2 -1
  108. package/dist/shortcuts/workflow/host.js +3 -0
  109. package/dist/shortcuts/workflow/index.js +1 -1
  110. package/dist/shortcuts/workflow/operations/form.js +44 -11
  111. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +77 -26
  112. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
  113. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +59 -117
  114. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
  115. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
  116. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +20 -6
  117. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
  118. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
  119. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +4 -2
  120. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
  121. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +110 -20
  122. package/docs/_catalog.md +3 -0
  123. package/docs/asset-idle.md +45 -0
  124. package/docs/e10-auth.md +1 -1
  125. package/docs/todo-smart-convert/subgroup-rules.md +1 -1
  126. package/package.json +1 -1
  127. package/scripts/package-skill.mjs +2 -0
  128. package/skill-template/business-info.json +14 -1
  129. package/skill-template/domains/asset-idle.md +13 -0
  130. package/skill-template/domains/wenshuding.md +3 -0
  131. package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
  132. package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
  133. package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
  134. package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
  135. package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
  136. package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
  137. package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
  138. package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
  139. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
  140. package/skills/weaver-e10-caizhilian/references/files.md +1 -1
  141. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
  142. package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
  143. package/skills/weaver-e10-cowork/SKILL.md +2 -0
  144. package/skills/weaver-e10-jucailin/SKILL.md +2 -0
  145. package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
  146. package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
  147. package/skills/weaver-e10-mail/SKILL.md +3 -0
  148. package/skills/weaver-e10-meeting/SKILL.md +250 -301
  149. package/skills/weaver-e10-meeting/references/meeting-eb.md +203 -0
  150. package/skills/weaver-e10-meeting/references/meeting-invitation.md +37 -5
  151. package/skills/weaver-e10-meeting.zip +0 -0
  152. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  153. package/skills/weaver-e10-shared/SKILL.md +5 -5
  154. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
  155. package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
  156. package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
  157. package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
  158. package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
  159. package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
  160. package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
  161. package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
  162. package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
  163. package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
  164. package/skills/weaver-e10-workflow/SKILL.md +5 -2
  165. package/skills/weaver-e10-workflow/aigroup/default.json +154 -19
  166. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +59 -27
  167. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
  168. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +292 -42
  169. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
@@ -0,0 +1,478 @@
1
+ import { meetingSuccess, schemaOf } from '../types.js';
2
+ import { meetingValidationError } from '../../errors.js';
3
+ /**
4
+ * 客户(CRM 主数据)模糊查询(EB 版会议链路用)。
5
+ *
6
+ * 数据来源:ESB 动作流 `esbFlowId=904205116264026112`,请求体:
7
+ * `{ customParams: { mainTable: { custom_name: "<客户名片段>" } }, esbFlowId }`
8
+ * 返回 `customData.mainTable = { mc, sjID }`:`sjID`=客户 id,`mc`=客户名称。
9
+ *
10
+ * ✅ 入参字段名就是 `custom_name`(2026-10-31 真机定案,**不要再猜 mc/khmc/customerName**):
11
+ * 泛微 → {mc:"泛微", sjID:"1331361148057387759"}
12
+ * 腾讯 → {mc:"深圳市腾讯斐泰科技有限公司", sjID:"1320482515670098010"}
13
+ * 不存在的客户XYZ → {mc:"", sjID:""}(空行,不是报错)
14
+ * 之前误判「入参模板为空所以传什么都进不去」的原因:`esb.input-format`(copyCustomData)对这个动作流
15
+ * 返回 `customParams: null` —— 那是「**没有自定义入参模板 = 透传**」,不是「模板是空对象 = 丢弃入参」。
16
+ * ⇒ 模板为 null 时照常发参数(默认字段名 `custom_name`);只有模板明确是 `mainTable: {}` 才判阻塞。
17
+ *
18
+ * ⛔ 三个真机事实(写业务逻辑必须按这个来):
19
+ * 1. **一次只回 1 条**:该动作流是「按名称模糊匹配并取首个命中」,加 `pageSize`/`rows` 都不改变结果;
20
+ * 所以"模糊查 10 条"在服务端做不到,`limit` 只是本地上限。返回 1 条 ≠ 库里只有 1 个匹配,
21
+ * 名称与关键字不完全一致时**必须回问用户确认**,不能拿首个匹配直接写 `crm_members`。
22
+ * 2. **`hasData` 恒为 0**,命中时也是 0 ⇒ **不能**用它判断"没有这个客户",判空只看 `mc`/`sjID` 是否为空。
23
+ * 3. 匹配是「包含」关系:`斐泰` 能命中「深圳市腾讯斐泰科技有限公司」;给全称则 id 稳定。
24
+ *
25
+ * 出参解析仍做多形态兼容(动作流改成回列表也不用改代码):
26
+ * customData.mainTable 单条 / mainTable.<list|rows|items|data…> / detail1|detail2…
27
+ * 每行按 `sjID`(客户 id)与 `mc`(名称)取值,兼容 id/customerId、name/sjmc 等别名。
28
+ */
29
+ /** 用户指定的客户查询动作流(可在入参里覆盖,换环境时不用改代码)。 */
30
+ const DEFAULT_CRM_FLOW_ID = '904205116264026112';
31
+ /** 该动作流的「客户名称」入参字段(2026-10-31 真机确认,模板为 null 时也用它)。 */
32
+ const DEFAULT_KEYWORD_FIELD = 'custom_name';
33
+ /** 入参里「客户名称」字段的候选名,按优先级挑;模板没配时兜底用 `custom_name`。 */
34
+ const KEYWORD_FIELD_HINTS = [
35
+ 'custom_name', 'mc', 'sjmc', 'khmc', 'customerName', 'customer_name', 'clientName', 'name', 'keyword', 'keywordName', 'searchKey',
36
+ ];
37
+ /** 出参里「客户 id」字段的候选名。 */
38
+ const ID_KEYS = ['sjID', 'sjid', 'sj_id', 'sjjId', 'customerId', 'custId', 'clientId', 'id'];
39
+ /** 出参里「客户名称」字段的候选名。 */
40
+ const NAME_KEYS = ['mc', 'sjmc', 'sj_name', 'customerName', 'custName', 'clientName', 'name'];
41
+ /** customData 下可能承载列表的字段名。 */
42
+ const LIST_KEYS = [
43
+ 'detail1', 'detail2', 'detail3', 'detail', 'details', 'detailList',
44
+ 'list', 'rows', 'items', 'data', 'records', 'result', 'customers', 'sjList',
45
+ ];
46
+ function asRecord(value) {
47
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
48
+ }
49
+ function pickFirst(row, keys) {
50
+ for (const key of keys) {
51
+ const value = row[key];
52
+ if (value === undefined || value === null)
53
+ continue;
54
+ if (typeof value === 'object') {
55
+ const nested = asRecord(value);
56
+ if (nested) {
57
+ const inner = pickFirst(nested, keys) || pickFirst(nested, ['id', 'name']);
58
+ if (inner)
59
+ return inner;
60
+ }
61
+ continue;
62
+ }
63
+ const text = String(value).trim();
64
+ if (text !== '')
65
+ return text;
66
+ }
67
+ return '';
68
+ }
69
+ /** 明细行常被包成 `{ mainTable: {...} }`,统一剥一层。 */
70
+ function unwrapRow(value) {
71
+ const record = asRecord(value);
72
+ if (!record)
73
+ return null;
74
+ const main = asRecord(record.mainTable);
75
+ return main ?? record;
76
+ }
77
+ /** 从 customData 里挖出所有可能承载客户行的候选数组。 */
78
+ function collectCandidateLists(customData) {
79
+ const found = [];
80
+ const main = asRecord(customData.mainTable);
81
+ if (main) {
82
+ // ① 单条:mainTable 自己就是一行。即使 mc/sjID 都是空串也放进来,
83
+ // 让 normalizeCustomers 丢掉后落到「没有匹配到客户」,而不是误报「结构不认识」
84
+ found.push({ source: 'customData.mainTable', rows: [main] });
85
+ // ② mainTable 里再套一层列表
86
+ for (const key of LIST_KEYS) {
87
+ const value = main[key];
88
+ if (Array.isArray(value))
89
+ found.push({ source: `customData.mainTable.${key}`, rows: value });
90
+ }
91
+ }
92
+ // ③ customData 直属列表(detail1 等)
93
+ for (const key of LIST_KEYS) {
94
+ const value = customData[key];
95
+ if (Array.isArray(value))
96
+ found.push({ source: `customData.${key}`, rows: value });
97
+ }
98
+ return found;
99
+ }
100
+ /** 客户行里「联系人明细 / 地址明细」的候选字段名(ESB 动作流输出)。 */
101
+ const CONTACT_LIST_KEYS = ['lxr', 'contacts', 'contactList', 'lxrList'];
102
+ const ADDRESS_LIST_KEYS = ['khdz', 'addresses', 'addressList', 'dz'];
103
+ /**
104
+ * 联系人字段候选名。第一项是 e-cology 当前客户表单的真机字段 id(2026-10-31 抓取),
105
+ * 后面的泛化名用于换租户;都取不到时按值的形态猜(见 parseCrmContacts)。
106
+ */
107
+ const CONTACT_NAME_KEYS = ['field_1173221220888062706', 'xm', 'lxrxm', 'contactName', 'name'];
108
+ const CONTACT_MOBILE_KEYS = ['field_1173221220888062708', 'sjhm', 'mobile', 'phone', 'tel', 'lxrdh'];
109
+ const CONTACT_DEPT_KEYS = ['field_1173221220888062707', 'bm', 'dept', 'department', 'bmmc'];
110
+ const MOBILE_RE = /^[+\d][\d\s-]{6,}$/;
111
+ const DEPT_RE = /(部|科|处|室|中心|事业部|分公司|集团|公司)$/;
112
+ /** 取行里第一个数组型字段(`lxr` / `khdz` 这类明细)。 */
113
+ function pickRawList(row, keys) {
114
+ for (const key of keys) {
115
+ if (Array.isArray(row[key]))
116
+ return row[key];
117
+ }
118
+ return undefined;
119
+ }
120
+ /**
121
+ * 解析 ESB `lxr` 明细 → 联系人数组。
122
+ *
123
+ * 真机形状(e-cology,2026-10-31):
124
+ * ```json
125
+ * [{"subForm_1173221220888060716":"477618176453313734",
126
+ * "field_1173221220888062706":"利明", // 姓名
127
+ * "field_1173221220888062708":"11111111111", // 手机号
128
+ * "field_1173221220888062707":"信息部"}] // 部门
129
+ * ```
130
+ */
131
+ export function parseCrmContacts(raw) {
132
+ if (!Array.isArray(raw))
133
+ return [];
134
+ const out = [];
135
+ const seen = new Set();
136
+ for (const item of raw) {
137
+ const row = asRecord(item);
138
+ if (!row)
139
+ continue;
140
+ const entries = Object.entries(row).filter(([key, value]) => !key.startsWith('subForm') && value !== null && value !== undefined && String(value).trim() !== '');
141
+ if (entries.length === 0)
142
+ continue;
143
+ const pick = (keys) => {
144
+ for (const key of keys) {
145
+ const hit = entries.find(([k]) => k === key);
146
+ if (hit)
147
+ return String(hit[1]).trim();
148
+ }
149
+ return '';
150
+ };
151
+ let name = pick(CONTACT_NAME_KEYS);
152
+ let mobile = pick(CONTACT_MOBILE_KEYS);
153
+ let dept = pick(CONTACT_DEPT_KEYS);
154
+ // 兜底:字段 id 换租户会变,按值的形态补空缺(手机号像号码、部门带「部/室/中心」结尾)
155
+ for (const [, value] of entries) {
156
+ const text = String(value).trim();
157
+ if (text === '')
158
+ continue;
159
+ if (!mobile && MOBILE_RE.test(text)) {
160
+ mobile = text;
161
+ continue;
162
+ }
163
+ if (!name && !DEPT_RE.test(text) && !/^\d+$/.test(text)) {
164
+ name = text;
165
+ continue;
166
+ }
167
+ if (!dept && DEPT_RE.test(text))
168
+ dept = text;
169
+ }
170
+ if (name === '' && mobile === '')
171
+ continue;
172
+ // ESB 明细里同一个人常重复出现(同一联系人多条业务记录),按「姓名+手机号」去重
173
+ const dedupeKey = `${name}|${mobile}`;
174
+ if (seen.has(dedupeKey))
175
+ continue;
176
+ seen.add(dedupeKey);
177
+ out.push({ name, mobile, ...(dept === '' ? {} : { dept }) });
178
+ }
179
+ return out;
180
+ }
181
+ /**
182
+ * 解析 ESB `khdz` 明细 → 地址字符串数组。
183
+ *
184
+ * ⛔ 当前动作流只回明细行 id(`{"subForm_…715":"5756415858274037939"}`),**没有地址文本**,
185
+ * 所以这里正常返回空数组;`addressHint` 会提示 ESB 侧补地址字段。
186
+ */
187
+ export function parseCrmAddresses(raw) {
188
+ if (!Array.isArray(raw))
189
+ return [];
190
+ const out = [];
191
+ for (const item of raw) {
192
+ const row = asRecord(item);
193
+ if (!row)
194
+ continue;
195
+ const parts = Object.entries(row)
196
+ .filter(([key, value]) => !key.startsWith('subForm') && value !== null && value !== undefined
197
+ && String(value).trim() !== '')
198
+ .map(([, value]) => String(value).trim());
199
+ if (parts.length > 0)
200
+ out.push(parts.join(' '));
201
+ }
202
+ return out;
203
+ }
204
+ /** 把任意行数组转成客户数组(去重、丢空行)。 */
205
+ export function normalizeCustomers(rows, limit) {
206
+ const seen = new Map();
207
+ for (const raw of rows) {
208
+ const row = unwrapRow(raw);
209
+ if (!row)
210
+ continue;
211
+ const customerId = pickFirst(row, ID_KEYS);
212
+ const name = pickFirst(row, NAME_KEYS);
213
+ // 客户 id 与名称都为空 = 输出模板的占位空行,直接丢
214
+ if (customerId === '' && name === '')
215
+ continue;
216
+ // 有 id 没名字说明动作流没把名称带出来,仍保留(调用方至少能按 id 用)
217
+ const key = customerId !== '' ? customerId : `name:${name}`;
218
+ if (seen.has(key))
219
+ continue;
220
+ // 联系人与地址是客户行里的明细数组(lxr / khdz),不是客户列表本身 —— 别被 collectCandidateLists 当行用
221
+ const contacts = parseCrmContacts(pickRawList(row, CONTACT_LIST_KEYS));
222
+ const addresses = parseCrmAddresses(pickRawList(row, ADDRESS_LIST_KEYS));
223
+ seen.set(key, {
224
+ customerId,
225
+ name,
226
+ ...(contacts.length > 0 ? { contacts } : {}),
227
+ ...(addresses.length > 0 ? { addresses } : {}),
228
+ });
229
+ }
230
+ const all = [...seen.values()];
231
+ const total = all.length;
232
+ return { items: limit > 0 ? all.slice(0, limit) : all, total };
233
+ }
234
+ /**
235
+ * 解析 ESB 触发结果 → 客户列表。
236
+ *
237
+ * 导出便于单测:动作流输出结构变了只改这里,不用真机验证。
238
+ */
239
+ export function parseCrmSearchPayload(actionData, options) {
240
+ const action = asRecord(actionData) ?? {};
241
+ const responseData = asRecord(action.responseData) ?? {};
242
+ const customData = asRecord(responseData.customData) ?? {};
243
+ const rawHasData = action.hasData;
244
+ const hasData = rawHasData === undefined || rawHasData === null ? null : Number(rawHasData) > 0;
245
+ const templateFields = options.templateFields ?? [];
246
+ const base = {
247
+ keyword: options.keyword,
248
+ limit: options.limit,
249
+ total: 0,
250
+ items: [],
251
+ truncated: false,
252
+ source: 'customData',
253
+ hasData,
254
+ exact: false,
255
+ singleBestMatch: false,
256
+ message: '',
257
+ templateFields,
258
+ keywordField: options.keywordField ?? null,
259
+ };
260
+ // 只有「模板存在且是空对象」才判阻塞:那种情况下参数会被服务端丢弃,返回什么都不可信
261
+ if (options.templateBlocked === true) {
262
+ return {
263
+ ...base,
264
+ blocked: 'esb_template_empty',
265
+ message: '动作流配了「自定义入参模板」但主表字段为空(customParams.mainTable = {}),'
266
+ + '触发时传入的查询条件会被丢弃,因此查不到客户。'
267
+ + '需要先在 ESB 设计器里给该动作流补入参(`custom_name` 客户名称)后再查。',
268
+ };
269
+ }
270
+ const lists = collectCandidateLists(customData);
271
+ const best = lists
272
+ .map((item) => ({ ...item, normalized: normalizeCustomers(item.rows, options.limit) }))
273
+ .sort((a, b) => b.normalized.total - a.normalized.total)[0];
274
+ if (!best) {
275
+ return {
276
+ ...base,
277
+ message: '动作流返回里没有可识别的客户结构(既没有 mainTable 行,也没有 detail/list 之类的列表),'
278
+ + '请检查动作流的输出模板是否仍输出 sjID/mc',
279
+ };
280
+ }
281
+ if (best.normalized.total === 0) {
282
+ // ⛔ hasData 恒 0,不能用它下结论;空 mc/sjID 空行才是「没匹配到」
283
+ return {
284
+ ...base,
285
+ source: best.source,
286
+ message: `按「${options.keyword}」没有匹配到客户(动作流返回空行)`,
287
+ };
288
+ }
289
+ const items = best.normalized.items;
290
+ const singleBestMatch = best.normalized.total === 1;
291
+ const exact = singleBestMatch && items[0].name === options.keyword;
292
+ // 客户地址:动作流配了 khdz 却只回明细行 id 时,说清楚是「输出缺字段」而不是「这个客户没地址」
293
+ const rawAddress = pickRawList(best.rows[0], ADDRESS_LIST_KEYS);
294
+ const addressHint = Array.isArray(rawAddress) && rawAddress.length > 0 && (items[0].addresses?.length ?? 0) === 0
295
+ ? `动作流的「客户地址」(khdz)只回了 ${rawAddress.length} 条明细行 id,没有地址文本 —— `
296
+ + '需要在 ESB 输出模板里补上地址字段(如 详细地址/省市区),否则邀请函和明细表里拿不到地址。'
297
+ : undefined;
298
+ return {
299
+ ...base,
300
+ items,
301
+ total: best.normalized.total,
302
+ truncated: best.normalized.total > options.limit,
303
+ source: best.source,
304
+ exact,
305
+ singleBestMatch,
306
+ ...(addressHint ? { addressHint } : {}),
307
+ message: exact
308
+ ? `精确命中客户「${items[0].name}」(${items[0].customerId})`
309
+ : `按「${options.keyword}」模糊命中 ${best.normalized.total} 个客户`
310
+ + (singleBestMatch ? '(动作流只回首个匹配)' : '')
311
+ + (best.normalized.total > options.limit ? `,已按 limit=${options.limit} 截断` : ''),
312
+ };
313
+ }
314
+ /** 从模板里挑「客户名称」入参字段名。 */
315
+ export function pickKeywordField(templateMainTable) {
316
+ const keys = Object.keys(templateMainTable ?? {});
317
+ if (keys.length === 0)
318
+ return null;
319
+ for (const hint of KEYWORD_FIELD_HINTS) {
320
+ const hit = keys.find((key) => key.toLowerCase() === hint.toLowerCase());
321
+ if (hit)
322
+ return hit;
323
+ }
324
+ // 模板里只有一个客制化字段时,它就是查询条件
325
+ return keys.length === 1 ? keys[0] : null;
326
+ }
327
+ /** 从模板里挑「每页条数」字段名(有的动作流用 pageSize / limit / rows)。 */
328
+ function pickLimitField(templateMainTable) {
329
+ const keys = Object.keys(templateMainTable ?? {});
330
+ const hints = ['pageSize', 'pagesize', 'limit', 'rows', 'size', 'num'];
331
+ for (const hint of hints) {
332
+ const hit = keys.find((key) => key.toLowerCase() === hint.toLowerCase());
333
+ if (hit)
334
+ return hit;
335
+ }
336
+ return null;
337
+ }
338
+ async function runCrmSearchFlow(env, input) {
339
+ const session = await env.host.getSession();
340
+ const employeeId = String(input.employeeId ?? session?.userId ?? '').trim();
341
+ if (!employeeId) {
342
+ throw meetingValidationError('crm_employee_required', '取不到当前登录用户 id,无法触发 ESB 客户查询');
343
+ }
344
+ const { createEsbHost } = await import('../../../esb/host.js');
345
+ const { createEsbContinuationSigner } = await import('../../../esb/continuation.js');
346
+ const { getEsbOperation } = await import('../../../esb/operations/registry.js');
347
+ const esbEnv = {
348
+ ctx: env.ctx,
349
+ host: createEsbHost(env.ctx),
350
+ continuation: createEsbContinuationSigner(env.ctx),
351
+ };
352
+ const templateOp = getEsbOperation('esb.input-format');
353
+ const prepareOp = getEsbOperation('esb.trigger.prepare');
354
+ const applyOp = getEsbOperation('esb.trigger.apply');
355
+ // ① 先读模板:只为决定「用哪个入参字段名」。
356
+ // customParams=null = 没配模板 = 透传(照常发参数);mainTable={} 才是「模板空、入参被丢弃」
357
+ let templateMainTable = null;
358
+ let customParamsTemplate = null;
359
+ let templateReadOk = false;
360
+ try {
361
+ const template = await templateOp.handler({ applicationId: input.flowId }, esbEnv);
362
+ customParamsTemplate = asRecord(template?.data?.customParams);
363
+ templateMainTable = asRecord(customParamsTemplate?.mainTable);
364
+ templateReadOk = true;
365
+ }
366
+ catch {
367
+ // 读模板失败不阻塞查询,按默认字段名(custom_name)试
368
+ }
369
+ const templateFields = Object.keys(templateMainTable ?? {});
370
+ const templateBlocked = templateReadOk && customParamsTemplate !== null && templateFields.length === 0;
371
+ const keywordField = pickKeywordField(templateMainTable) ?? DEFAULT_KEYWORD_FIELD;
372
+ const limitField = pickLimitField(templateMainTable);
373
+ const mainTable = {};
374
+ mainTable[keywordField] = input.keyword;
375
+ if (limitField)
376
+ mainTable[limitField] = input.limit;
377
+ const pre = await prepareOp.handler({
378
+ esbFlowId: input.flowId,
379
+ employeeId,
380
+ customParams: { mainTable },
381
+ }, esbEnv);
382
+ const applied = await applyOp.handler({ continuation: pre?.data?.continuation, confirm: true }, esbEnv);
383
+ const actionData = applied?.data?.actionData;
384
+ const parsed = parseCrmSearchPayload(actionData, {
385
+ keyword: input.keyword,
386
+ limit: input.limit,
387
+ templateFields,
388
+ keywordField,
389
+ templateBlocked,
390
+ });
391
+ if (actionData && typeof actionData === 'object') {
392
+ // reminder 是数字枚举,0=无提示;String(0) 非空,别当成提示文案
393
+ const reminder = actionData.reminder;
394
+ const reminderText = reminder === undefined || reminder === null ? '' : String(reminder);
395
+ if (reminderText !== '' && reminderText !== '0')
396
+ parsed.actionMessage = reminderText;
397
+ }
398
+ return { result: parsed, raw: applied?.data };
399
+ }
400
+ const ebCrmSearch = {
401
+ name: 'meeting.eb.crm.search',
402
+ resource: 'eb',
403
+ method: 'meeting.eb.crm.search',
404
+ action: 'meeting.eb.crm.search',
405
+ summary: '按客户名称查询 CRM 客户(ESB 动作流 904205116264026112,入参 custom_name,出参 sjID/mc);'
406
+ + '动作流一次只回首个匹配,名称不完全一致时须回问用户确认',
407
+ capability: 'eb.crm',
408
+ risk: 'read',
409
+ inputSchema: schemaOf({
410
+ keyword: {
411
+ type: 'string',
412
+ minLength: 1,
413
+ description: '客户名称,走动作流入参 custom_name(包含匹配):给全称最稳,片段只回首个匹配',
414
+ },
415
+ limit: { type: 'number', description: '返回条数上限,默认 10(当前动作流一次只回 1 条,仅作兼容)' },
416
+ flowId: { type: 'string', description: '覆盖 ESB 动作流 id(默认 904205116264026112)' },
417
+ employeeId: { type: 'string', description: '覆盖触发人员 id(默认当前登录用户)' },
418
+ includeRaw: { type: 'boolean', description: 'true=附带 ESB 原始返回,便于排查输出结构' },
419
+ }, { required: ['keyword'] }),
420
+ handler: async (args, env) => {
421
+ const keyword = String(args.keyword ?? '').trim();
422
+ if (!keyword) {
423
+ throw meetingValidationError('crm_keyword_required', 'keyword 不能为空(客户名称关键字)');
424
+ }
425
+ const limitRaw = Number(args.limit ?? 10);
426
+ const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(Math.floor(limitRaw), 100) : 10;
427
+ const { result, raw } = await runCrmSearchFlow(env, {
428
+ keyword,
429
+ limit,
430
+ flowId: String(args.flowId ?? '').trim() || DEFAULT_CRM_FLOW_ID,
431
+ ...(args.employeeId ? { employeeId: String(args.employeeId) } : {}),
432
+ });
433
+ const warnings = [];
434
+ if (result.blocked === 'esb_template_empty') {
435
+ warnings.push(result.message);
436
+ warnings.push('⛔ 不要回复「没有这个客户」:这是动作流入参模板未配置,属于配置问题。');
437
+ }
438
+ else if (result.items.length === 0) {
439
+ warnings.push(`按「${keyword}」没有匹配到客户;可换更短的关键字(去掉地域/后缀),`
440
+ + '或确认该客户是否已在 CRM 建档(这是「查无」,可以如实回复用户)。');
441
+ }
442
+ else if (!result.exact) {
443
+ warnings.push(`动作流一次只回首个匹配:「${result.items[0].name}」(${result.items[0].customerId}),`
444
+ + `与「${keyword}」不完全一致。请把**命中全名原样念出来问"是这家吗"**,用户认了再把该 id `
445
+ + '写进 create.flow.prepare 的 crm_members,不要猜。'
446
+ + '⛔ 不要问成"填进参会客户字段还是只写进会议内容备注"这种选择题 —— 客户一律要关联客户档案,'
447
+ + '"只写备注"是明令禁止的做法。');
448
+ }
449
+ if (result.items.some((item) => item.name === '')) {
450
+ warnings.push('有客户只返回了 sjID 没返回 mc(名称),请让动作流输出 mc 字段。');
451
+ }
452
+ const firstCustomer = result.items[0];
453
+ if (firstCustomer) {
454
+ const contacts = firstCustomer.contacts ?? [];
455
+ if (contacts.length > 0) {
456
+ // ⛔ 2026-10-31 用户纠正:有几位就**全带几位**,不要弹「选哪位」的框。
457
+ // "该客户有 3 位联系人,本次参会客户联系人选哪位" 这种选择框是多余的 ——
458
+ // 明细表本来就是多行结构,一人一行全写进去即可;只有用户明确说"只写某某"才覆盖。
459
+ warnings.push(`该客户有 ${contacts.length} 位联系人:`
460
+ + contacts.map((c) => `${c.name}${c.dept ? `(${c.dept})` : ''} ${c.mobile || '无手机号'}`).join('、')
461
+ + '。建会时传 `crm_members` 就会把这**全部联系人自动写成「参会客户明细」行**(一人一行),'
462
+ + '⛔ 不要问用户"选哪位"、也不要只带其中一位;'
463
+ + '只有用户明确说"只写某某"时才用 '
464
+ + 'crm_member_details:[{"xm":"<姓名>","sjhm":"<手机号>","zw":"<部门或职位>"}] 覆盖。');
465
+ }
466
+ else {
467
+ warnings.push('该客户**没有联系人**(动作流未输出 lxr,或该客户没建档联系人)—— 这是唯一需要问联系人的场景。'
468
+ + '可以问用户「要不要补一下联系人姓名和手机号(方便的话再给职位)」,'
469
+ + '再以 crm_member_details:[{"xm":"张三","sjhm":"13800000000"}] 传给 create.flow.prepare/quick;'
470
+ + '用户说"没有"就留空,不要硬编。');
471
+ }
472
+ if (result.addressHint)
473
+ warnings.push(result.addressHint);
474
+ }
475
+ return meetingSuccess('meeting.eb.crm.search', { ...result, ...(args.includeRaw === true ? { raw } : {}) }, warnings.length > 0 ? { warnings } : {});
476
+ },
477
+ };
478
+ export const meetingEbCrmOperations = [ebCrmSearch];
@@ -7,6 +7,57 @@ import { isWriteKind } from './catalog.js';
7
7
  export const EB_BASE = '/api/ebuilder/form/formdata/v2';
8
8
  const EB_READ_OP = { list: 'getFormDataList', detail: 'getFormDataByPk', get: 'getFormDataList' };
9
9
  const EB_WRITE_OP = { create: 'saveFormData', update: 'updateFormData', delete: 'deleteFormData' };
10
+ /** 详情里的人员类字段:值可能是员工 id,补一份姓名回显,别让调用方把 id 当成"没填/填错了"。 */
11
+ const PERSONNEL_FIELDS = [
12
+ 'caller', 'contacter', 'mt_creator', 'creator', 'updater',
13
+ 'hrm_members', 'attend_user', 'inviters', 'reporting_user', 'topic_presenter',
14
+ ];
15
+ /**
16
+ * 人员字段 id → 姓名。
17
+ *
18
+ * ⛔ 2026-10-31 用户反馈「联系人那一栏显示成一串数字」:detail 接口回的是**原始值**
19
+ * (页面建的单存 id、工作流建的单存姓名文本),调用方看到 id 就以为字段没填好。
20
+ * 这里只补一份 `personnelText`(字段名 → 姓名),**不动原字段**,避免破坏既有消费方。
21
+ */
22
+ async function buildPersonnelText(env, data) {
23
+ const ids = [];
24
+ for (const key of PERSONNEL_FIELDS) {
25
+ const raw = String(data?.[key] ?? '').trim();
26
+ // 已经是姓名文本(含中文/括号)就不用查;纯数字与逗号才算 id 列表
27
+ if (raw === '' || !/^[\d,\s]+$/u.test(raw))
28
+ continue;
29
+ ids.push(...raw.split(',').map((one) => one.trim()).filter((one) => one !== ''));
30
+ }
31
+ if (ids.length === 0)
32
+ return {};
33
+ const names = new Map();
34
+ try {
35
+ const { hrmEmployeeOperations } = await import('../../../hrm/operations/employee.js');
36
+ const batch = hrmEmployeeOperations.find((operation) => operation.name === 'hrm.employee.batch');
37
+ if (batch) {
38
+ const result = await batch.handler({ ids: [...new Set(ids)] }, env);
39
+ for (const row of (result?.data?.rows ?? result?.data?.userList ?? [])) {
40
+ const id = String(row.id ?? '').trim();
41
+ const name = String(row.name ?? '').trim();
42
+ if (id && name)
43
+ names.set(id, name);
44
+ }
45
+ }
46
+ }
47
+ catch {
48
+ return {};
49
+ }
50
+ const out = {};
51
+ for (const key of PERSONNEL_FIELDS) {
52
+ const raw = String(data?.[key] ?? '').trim();
53
+ if (raw === '' || !/^[\d,\s]+$/u.test(raw))
54
+ continue;
55
+ const mapped = raw.split(',').map((one) => names.get(one.trim()) ?? one.trim()).filter((one) => one !== '');
56
+ if (mapped.length > 0)
57
+ out[key] = mapped.join(',');
58
+ }
59
+ return out;
60
+ }
10
61
  const CONTINUATION_TTL_SECONDS = 600;
11
62
  /**
12
63
  * EB 响应成功判定。
@@ -230,7 +281,15 @@ function buildReadOperation(endpoint) {
230
281
  detailNodes[key] = value;
231
282
  }
232
283
  }
233
- return meetingSuccess(endpoint.operation, { id, data, ...(Object.keys(detailNodes).length > 0 ? { details: detailNodes } : {}) }, endpoint.notes ? { meta: { notes: endpoint.notes } } : {});
284
+ const personnelText = endpoint.operation.startsWith('meeting.')
285
+ ? await buildPersonnelText(env, data)
286
+ : {};
287
+ return meetingSuccess(endpoint.operation, {
288
+ id,
289
+ data,
290
+ ...(Object.keys(personnelText).length > 0 ? { personnelText } : {}),
291
+ ...(Object.keys(detailNodes).length > 0 ? { details: detailNodes } : {}),
292
+ }, endpoint.notes ? { meta: { notes: endpoint.notes } } : {});
234
293
  }
235
294
  const query = {};
236
295
  for (const param of endpoint.queryParams ?? []) {
@@ -6,6 +6,7 @@ import { ebMeetingAssociation } from './association.js';
6
6
  import { ebRoomRecommendOperation, recommendRooms, rememberFrequentRoom } from './room-recommend.js';
7
7
  import { meetingValidationError } from '../../errors.js';
8
8
  import { meetingEbScheduleOperations } from './schedule.js';
9
+ import { meetingEbCrmOperations } from './crm-search.js';
9
10
  import { meetingEbCreateFlowOperations } from './create-flow.js';
10
11
  /**
11
12
  * 会议 EB 版(eBuilder)operations。
@@ -167,6 +168,7 @@ export const meetingEbOperations = [
167
168
  ...meetingEbConflictOperations,
168
169
  ...meetingEbDecisionOperations,
169
170
  ...meetingEbScheduleOperations,
171
+ ...meetingEbCrmOperations,
170
172
  ...meetingEbCreateFlowOperations,
171
173
  ebRoomRecommendOperation,
172
174
  ];