weaver-work-cli 0.1.38 → 0.1.40

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 (166) hide show
  1. package/README.md +7 -3
  2. package/dist/chunks/{archive-PDV724YK.js → archive-JHTONRST.js} +556 -66
  3. package/dist/chunks/{archive-oa-4XE4MNXQ.js → archive-oa-FCHLCPHC.js} +2 -2
  4. package/dist/chunks/{asset-PDL2QE2N.js → asset-RLIHC7DB.js} +2 -2
  5. package/dist/chunks/asset-idle-KVOFTLCP.js +670 -0
  6. package/dist/chunks/{attachment-content-MDDGOE5E.js → attachment-content-RZYDB3MB.js} +3 -3
  7. package/dist/chunks/{blog-UYR3EUXW.js → blog-42OZTJNP.js} +2 -2
  8. package/dist/chunks/{caizhilian-LG2X5NMF.js → caizhilian-KRK547K5.js} +2 -2
  9. package/dist/chunks/{calendar-FG4BAZHE.js → calendar-RTWAMKHD.js} +2 -2
  10. package/dist/chunks/{chunk-J7USS3ED.js → chunk-4CTESSKI.js} +4 -4
  11. package/dist/chunks/{chunk-OJQUJZQF.js → chunk-4TEDNCON.js} +45 -5
  12. package/dist/chunks/chunk-57IECMWX.js +487 -0
  13. package/dist/chunks/{chunk-ELYDWBZJ.js → chunk-7BRU4EFH.js} +13 -7
  14. package/dist/chunks/{chunk-6LTFIFLM.js → chunk-7E6MUOEK.js} +2 -2
  15. package/dist/chunks/{chunk-KKRL3REV.js → chunk-7U577Q7U.js} +2 -2
  16. package/dist/chunks/{chunk-UJJD4QWF.js → chunk-CAQNBBQA.js} +2 -2
  17. package/dist/chunks/{chunk-CORFRD7Q.js → chunk-CXRQWOZ5.js} +2 -2
  18. package/dist/chunks/{chunk-GGJFMNJM.js → chunk-FLS4DPGP.js} +110 -16
  19. package/dist/chunks/{chunk-JSMFPO43.js → chunk-I566ZYNF.js} +1 -1
  20. package/dist/chunks/{chunk-6FTTSA45.js → chunk-IZSSVYAG.js} +2 -2
  21. package/dist/chunks/{chunk-XJY7G4DD.js → chunk-JWRZM2H2.js} +3 -3
  22. package/dist/chunks/{chunk-NI6GIPIM.js → chunk-LFMSDBZN.js} +529 -395
  23. package/dist/chunks/{chunk-VRYHGG6O.js → chunk-LN447PY2.js} +92 -51
  24. package/dist/chunks/{chunk-NR6RKXPA.js → chunk-NETCCARD.js} +4 -2
  25. package/dist/chunks/{chunk-Z32KWY6G.js → chunk-NJKREDA3.js} +1 -1
  26. package/dist/chunks/{chunk-FUWVHMVV.js → chunk-SPRHHSVC.js} +2 -2
  27. package/dist/chunks/{chunk-IZUDA353.js → chunk-T6EGVBKU.js} +1 -1
  28. package/dist/chunks/{chunk-DMV2TOMH.js → chunk-VNBU55SL.js} +428 -145
  29. package/dist/chunks/{chunk-UZZBA6PV.js → chunk-WFFNM65B.js} +47 -6
  30. package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
  31. package/dist/chunks/{conflict-BQZEQ4SP.js → conflict-VR55AOOO.js} +2 -2
  32. package/dist/chunks/{contract-XIRAFTEV.js → contract-HI4FNPYE.js} +2 -2
  33. package/dist/chunks/{cowork-BLG62BL2.js → cowork-HJLI353D.js} +12 -5
  34. package/dist/chunks/{ebuilder-form-NUKG5QW4.js → ebuilder-form-HTGI63UU.js} +3 -3
  35. package/dist/chunks/{ehr-2STW5ILH.js → ehr-GYXIYH3A.js} +2 -2
  36. package/dist/chunks/env-DW3G4HUO.js +19 -0
  37. package/dist/chunks/{esb-UUDQUNKY.js → esb-I5INCEFE.js} +4 -4
  38. package/dist/chunks/factory-TKXGXS6L.js +30 -0
  39. package/dist/chunks/{fna-6RCY4NU5.js → fna-K2MHTQ24.js} +2 -2
  40. package/dist/chunks/group-chat-VYBNMJLG.js +18 -0
  41. package/dist/chunks/{host-FWDOSRH5.js → host-256GVCAH.js} +3 -3
  42. package/dist/chunks/{host-EJZFROU3.js → host-D5SERMAJ.js} +3 -3
  43. package/dist/chunks/{host-RHM3NPXV.js → host-JM4YVHFM.js} +3 -3
  44. package/dist/chunks/{host-7RXEDMAH.js → host-L3VLITMT.js} +3 -3
  45. package/dist/chunks/{host-UFF33TWJ.js → host-ZUQTJYYK.js} +3 -3
  46. package/dist/chunks/{hrm-PKJ6ELSH.js → hrm-VLRBKTZB.js} +3 -3
  47. package/dist/chunks/{im-YJAXS7CO.js → im-QK3VTLXK.js} +8 -8
  48. package/dist/chunks/{invoice-HWVSXGX6.js → invoice-T5JTA32H.js} +3 -3
  49. package/dist/chunks/{jiuchuanhui-L7VKILZB.js → jiuchuanhui-P2FNEQVZ.js} +2 -2
  50. package/dist/chunks/{lazy-dispatch-NMYEIHRH.js → lazy-dispatch-XRRI46M6.js} +5 -5
  51. package/dist/chunks/{mail-B42RRU7G.js → mail-3Y7MJ6PS.js} +9 -4
  52. package/dist/chunks/{meeting-CFYVBJSS.js → meeting-M7LTXGJE.js} +11 -10
  53. package/dist/chunks/{okr-GCLF6V7W.js → okr-OBC6S67L.js} +2 -2
  54. package/dist/chunks/{plan-ZVL7GBRI.js → plan-5H6I53SK.js} +2 -2
  55. package/dist/chunks/{project-4YWAAE2K.js → project-L75QZV4S.js} +2 -2
  56. package/dist/chunks/{registry-EP4VTHZS.js → registry-KMHCPAAD.js} +9 -8
  57. package/dist/chunks/{registry-OZSMDVPP.js → registry-QGHFNUWS.js} +2 -2
  58. package/dist/chunks/{registry-FRJ6H7BT.js → registry-RXPN6BBT.js} +4 -4
  59. package/dist/chunks/{registry-SG2PGVCF.js → registry-UGRBCBHO.js} +3 -3
  60. package/dist/chunks/{room-recommend-R3JVUUAC.js → room-recommend-A3UDC3CA.js} +4 -4
  61. package/dist/chunks/{schedule-NIJBTYDO.js → schedule-5GCACCK7.js} +4 -4
  62. package/dist/chunks/{workflow-BB24DFBY.js → workflow-M5ZJ7DWF.js} +11 -8
  63. package/dist/core/output.js +16 -6
  64. package/dist/index.js +33 -28
  65. package/dist/internal/e10/auth/commands.js +109 -56
  66. package/dist/internal/e10/auth/messages.js +1 -0
  67. package/dist/internal/e10/auth/session.js +11 -1
  68. package/dist/internal/e10/auth/xiaoe.js +29 -4
  69. package/dist/internal/e10/auth-recovery.js +13 -6
  70. package/dist/internal/e10/client.js +13 -0
  71. package/dist/internal/e10/context.js +24 -3
  72. package/dist/internal/e10/request-runtime.js +12 -1
  73. package/dist/shortcuts/archive/host.js +2 -0
  74. package/dist/shortcuts/archive/manifest.js +1 -1
  75. package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
  76. package/dist/shortcuts/archive/operations/flow-url.js +1 -1
  77. package/dist/shortcuts/archive/operations/prelib.js +472 -0
  78. package/dist/shortcuts/archive/operations/registry.js +2 -0
  79. package/dist/shortcuts/archive/operations/upload.js +1 -1
  80. package/dist/shortcuts/asset-idle/errors.js +44 -0
  81. package/dist/shortcuts/asset-idle/host.js +161 -0
  82. package/dist/shortcuts/asset-idle/index.js +114 -0
  83. package/dist/shortcuts/asset-idle/manifest.js +159 -0
  84. package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
  85. package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
  86. package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
  87. package/dist/shortcuts/asset-idle/operations/types.js +9 -0
  88. package/dist/shortcuts/asset-idle/operations.js +15 -0
  89. package/dist/shortcuts/cowork/operations/item-write.js +10 -2
  90. package/dist/shortcuts/index.js +5 -0
  91. package/dist/shortcuts/mail/operations/mail-send.js +8 -2
  92. package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -1
  93. package/dist/shortcuts/meeting/operations/eb/create-flow.js +381 -104
  94. package/dist/shortcuts/meeting/operations/eb/crm-search.js +87 -5
  95. package/dist/shortcuts/meeting/operations/eb/flow-memory.js +49 -0
  96. package/dist/shortcuts/meeting/operations/eb/group-chat.js +583 -0
  97. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  98. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +19 -3
  99. package/dist/shortcuts/meeting/operations/eb/room-summary.js +2 -0
  100. package/dist/shortcuts/meeting/operations/eb/usage-cache.js +99 -0
  101. package/dist/shortcuts/meeting/operations/from-group-chat.js +35 -12
  102. package/dist/shortcuts/workflow/host.js +3 -0
  103. package/dist/shortcuts/workflow/index.js +1 -1
  104. package/dist/shortcuts/workflow/operations/form.js +65 -171
  105. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +129 -27
  106. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
  107. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +60 -117
  108. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
  109. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
  110. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +48 -8
  111. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  112. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
  113. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +17 -9
  114. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
  115. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +5 -2
  116. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
  117. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +122 -20
  118. package/docs/_catalog.md +3 -0
  119. package/docs/asset-idle.md +45 -0
  120. package/docs/e10-auth.md +16 -1
  121. package/docs/todo-smart-convert/subgroup-rules.md +1 -1
  122. package/package.json +1 -1
  123. package/scripts/mcp-meeting.mjs +234 -0
  124. package/scripts/package-skill.mjs +2 -0
  125. package/skill-template/business-info.json +14 -1
  126. package/skill-template/domains/asset-idle.md +13 -0
  127. package/skill-template/domains/wenshuding.md +3 -0
  128. package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
  129. package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
  130. package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
  131. package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
  132. package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
  133. package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
  134. package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
  135. package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
  136. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
  137. package/skills/weaver-e10-caizhilian/references/files.md +1 -1
  138. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
  139. package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
  140. package/skills/weaver-e10-cowork/SKILL.md +2 -0
  141. package/skills/weaver-e10-cowork/references/cowork-write.md +1 -1
  142. package/skills/weaver-e10-jucailin/SKILL.md +2 -0
  143. package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
  144. package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
  145. package/skills/weaver-e10-mail/SKILL.md +3 -0
  146. package/skills/weaver-e10-mail/references/mail-send.md +2 -0
  147. package/skills/weaver-e10-meeting/SKILL.md +88 -25
  148. package/skills/weaver-e10-meeting/references/meeting-minute-file.md +25 -11
  149. package/skills/weaver-e10-meeting.zip +0 -0
  150. package/skills/weaver-e10-shared/SKILL.md +5 -5
  151. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
  152. package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
  153. package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
  154. package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
  155. package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
  156. package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
  157. package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
  158. package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
  159. package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
  160. package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
  161. package/skills/weaver-e10-workflow/SKILL.md +9 -2
  162. package/skills/weaver-e10-workflow/aigroup/default.json +166 -19
  163. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +72 -34
  164. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
  165. package/skills/weaver-e10-workflow/tools/todo-summary.cjs +350 -50
  166. package/dist/chunks/{chunk-2LV74IWP.js → chunk-E7OK5WD5.js} +5 -5
@@ -7,6 +7,8 @@ import { meetingValidationError } from '../../errors.js';
7
7
  import { fetchEbAppSettings, findCommonFreeSlots } from './conflict.js';
8
8
  import { EB_BASE, ebBusinessSuccess, writeBody } from './factory.js';
9
9
  import { loadRooms, recommendRooms } from './room-recommend.js';
10
+ import { dayOf, filterUsages, invalidateDayUsages, loadDayUsages } from './usage-cache.js';
11
+ import { pickRememberedFlow, readFlowPick, rememberFlowPick } from './flow-memory.js';
10
12
  /**
11
13
  * 通过**流程**新建 EB 会议(`meeting.eb.meeting.create.flow`)。
12
14
  *
@@ -643,50 +645,55 @@ async function resolveAutoCrmDetailRows(env, raw) {
643
645
  const entries = crmMemberEntriesFromArgs(raw);
644
646
  const missing = [];
645
647
  /**
646
- * ⛔ 2026-10-31 修:以前只在「入参一个联系人都没带」时才回查,结果调用方(小E)只传了
647
- * 用户挑中的那一位时,会议明细里就真只有一位 —— 而客户在 CRM 里其实有好几位。
648
- * 用户的要求是「有几位就全带几位」,所以现在**一律回查一次,按 CRM 实时数据补全**(取并集,
649
- * 已带的额外字段如性别/备注保留)。只有 CRM 也查不到时才留空。
648
+ * ⛔ 2026-11-02 定案(用户确认):
649
+ * - 入参**已经带了 `contacts`**(调用方查过 `crm.search`,用户也选定了客户)⇒ **直接用,不再回查**。
650
+ * 回查一次只会用客户名再模糊搜一遍,反而可能拿到**别的客户**的联系人。
651
+ * - 只有没带 `contacts` 时才回查,并且**必须按 `customerId` 精确匹配** ——
652
+ * 动作流是 like 模糊查,同一个词能命中一票近名客户("中国民航"能回 10 个),
653
+ * 拿 `items[0]` 就等于把别人家的联系人写进这场会议。
650
654
  */
651
655
  if (entries.length > 0) {
652
656
  try {
653
657
  const { getMeetingOperation } = await import('../registry.js');
654
658
  const search = getMeetingOperation('meeting.eb.crm.search');
655
659
  for (const entry of entries) {
660
+ if (entry.contacts.length > 0)
661
+ continue;
656
662
  const keyword = (entry.name || entry.id).trim();
657
663
  if (keyword === '') {
658
664
  missing.push(entry.id);
659
665
  continue;
660
666
  }
661
- const searchOnce = async (kw) => {
662
- const found = await search.handler({ keyword: kw, limit: 5 }, env).catch(() => null);
667
+ /**
668
+ * ⛔ 2026-11-02 实测:**同一个客户(sjID 相同),keyword 越"精确",动作流回的 lxr 越少** ——
669
+ * 「中国民航」→ 4 条原始(利明×2、胡老师、谷俊);「…股份有限公司」→ 2 条;带 `--` 全名 → 1 条。
670
+ * 是动作流把 keyword 掺进了 lxr 的关联过滤,ESB 那边要修。
671
+ * CLI 侧先兜住:由长到短几个候选各查一次,**取这批里联系人最多的那次**;
672
+ * 但每次都**必须按 customerId 精确命中**(找不到就丢,绝不用 items[0] 兜底)——
673
+ * 模糊查会命中一票近名客户,兜底就等于把别人家的联系人写进这场会议。
674
+ */
675
+ const shortBase = keyword.split('--')[0].trim();
676
+ const stripped = shortBase
677
+ .replace(/[((][^))]*[))]/gu, '')
678
+ .replace(/(股份有限公司|有限责任公司|集团有限公司|有限公司|集团|公司)$/u, '')
679
+ .trim();
680
+ const candidates = [...new Set([keyword, shortBase, stripped, shortBase.slice(0, 4)].filter((one) => one !== ''))];
681
+ let live = [];
682
+ for (const candidate of candidates) {
683
+ const found = await search.handler({ keyword: candidate, limit: 50 }, env).catch(() => null);
663
684
  const items = (found?.data?.items ?? []);
664
- const hit = items.find((item) => String(item?.customerId ?? '') === entry.id) ?? items[0];
665
- return (hit?.contacts ?? []);
666
- };
667
- let live = await searchOnce(keyword);
668
- // 客户全名常带「--子公司、专项」后缀,动作流按全名可能一个都匹配不到;
669
- // 这时退回「--」前的简称再搜一次(仍要按 customerId 认,别拿首个匹配凑数)。
670
- if (live.length === 0 && keyword.includes('--')) {
671
- const short = keyword.split('--')[0].trim();
672
- if (short !== '')
673
- live = await searchOnce(short);
685
+ const hit = entry.id !== ''
686
+ ? items.find((item) => String(item?.customerId ?? '') === entry.id)
687
+ : undefined;
688
+ const contacts = (hit?.contacts ?? []);
689
+ if (contacts.length > live.length)
690
+ live = contacts;
674
691
  }
675
692
  if (live.length === 0) {
676
- if (entry.contacts.length === 0)
677
- missing.push(entry.name || entry.id);
693
+ missing.push(entry.name || entry.id);
678
694
  continue;
679
695
  }
680
- const keyOf = (one) => `${String(one.name ?? '').trim()}|${String(one.mobile ?? '').trim()}`;
681
- const merged = new Map();
682
- for (const one of live)
683
- merged.set(keyOf(one), one);
684
- for (const one of entry.contacts) {
685
- const key = keyOf(one);
686
- const base = merged.get(key);
687
- merged.set(key, base ? { ...base, ...one } : one);
688
- }
689
- entry.contacts = [...merged.values()];
696
+ entry.contacts = live;
690
697
  }
691
698
  }
692
699
  catch {
@@ -965,7 +972,8 @@ export function normalizeWorkflowIds(raw) {
965
972
  *
966
973
  * ⛔ 固化不等于"不校验权限":仍然逐个探测"当前账号能不能发起",全部发起不了才回退应用设置。
967
974
  */
968
- const FIXED_FLOW_IDS = ['1325677244111462606', '1326190634427154474'];
975
+ // 2026-11-03 用户指定:优先「会议申请流程」,北方大区那个放后面(权限探测照做,顺序只影响默认优先级)。
976
+ const FIXED_FLOW_IDS = ['1326190634427154474', '1325677244111462606'];
969
977
  const FIXED_FLOW_PROFILES = [
970
978
  {
971
979
  host: 'www.e-cology.com.cn',
@@ -1208,7 +1216,15 @@ const ebMeetingCreateFlowPrepare = {
1208
1216
  },
1209
1217
  dateFrom: {
1210
1218
  type: 'string',
1211
- description: '没给具体时间时按哪一天推荐时段(yyyy-MM-dd;不传默认明天)',
1219
+ description: '没给具体时间时按哪一天推荐时段(yyyy-MM-dd;不传默认明天)。'
1220
+ + '⚠️ 用户说了「上午/下午/晚上」就必须同时传 time_slot —— 只传 dateFrom 会弹时段选择器让用户再选一次',
1221
+ },
1222
+ time_slot: {
1223
+ type: 'string',
1224
+ enum: ['上午', '早上', '下午', '晚上'],
1225
+ description: '用户只说了「上午/下午/晚上」时传这个(配合 dateFrom):'
1226
+ + '上午/早上=09:00–12:00、下午=13:00–18:00、晚上=18:00–21:00,CLI 直接定下来、不再返回时段候选。'
1227
+ + '⛔ 不要只传 dateFrom 就期待"让用户选时段"——用户已经说了时段就别再问',
1212
1228
  },
1213
1229
  durationMinutes: {
1214
1230
  type: 'number',
@@ -1227,8 +1243,10 @@ const ebMeetingCreateFlowPrepare = {
1227
1243
  flowChoice: {
1228
1244
  type: 'string',
1229
1245
  enum: ['auto', 'ask'],
1230
- description: '多个建会流程**都有发起权限**时怎么办:**默认(不传 / ask)= 返回选项让用户挑**;'
1246
+ description: '多个建会流程**都有发起权限**时怎么办:默认(不传 / ask)= 返回选项让用户挑;'
1231
1247
  + 'auto = 自动取优先级最高的那个(省一轮,但选错流程会漏字段,慎用)。'
1248
+ + '有**流程记忆**(该账号上次成功用过的流程)且本次仍可发起时,默认直接沿用上次选择并留痕,'
1249
+ + '不再弹选项;要强制回到"每次都问"才传 ask。'
1232
1250
  + '只有 1 个有权限时不受此参数影响,CLI 直接发起',
1233
1251
  },
1234
1252
  }, { required: ['mt_name'] }),
@@ -1245,12 +1263,94 @@ const ebMeetingCreateFlowPrepare = {
1245
1263
  // enrich=false:只做"建会必需"的事(载入表单 + 填字段 + 提交),跳过会议室推荐/冲突检测/空闲时段。
1246
1264
  // 适用场景:人、时间、房间都已经确定,只想尽快把会建出来(可省掉 3 组额外请求与一次 ESB 调用)。
1247
1265
  const enrich = args.enrich !== false;
1248
- const beginText = String(args.begin_datetime ?? '').trim();
1266
+ let beginText = String(args.begin_datetime ?? '').trim();
1249
1267
  let endText = String(args.end_datetime ?? '').trim();
1268
+ /**
1269
+ * 「上午/下午/晚上」→ 固定区间(2026-11-02 用户确认口径):
1270
+ * 上午 09:00–12:00、下午 13:00–18:00、晚上 18:00–21:00。
1271
+ * 用户说了时段就等于拍了板,**不再返回候选让他挑**;
1272
+ * 给了完整区间("09:00 到 11:00")就按给的,不做映射;只给开始时间的仍按 +1 小时补结束。
1273
+ */
1274
+ const TIME_SLOT_RANGES = {
1275
+ 上午: ['09:00', '12:00'],
1276
+ 早上: ['09:00', '12:00'],
1277
+ 下午: ['13:00', '18:00'],
1278
+ 晚上: ['18:00', '21:00'],
1279
+ };
1280
+ let slotConfirmed = false;
1281
+ // ① 结构化入参 `time_slot`(推荐调用方用这个,比让模型拼中文串可靠):
1282
+ // 用户说"明天上午"就传 { dateFrom: '2026-11-03', time_slot: '上午' }。
1283
+ const slotArg = String(args.time_slot ?? '').trim();
1284
+ if (slotArg !== '' && !/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(beginText)) {
1285
+ const range = TIME_SLOT_RANGES[slotArg];
1286
+ const day = String(args.dateFrom ?? '').trim().slice(0, 10)
1287
+ || beginText.slice(0, 10)
1288
+ || tomorrowIso();
1289
+ if (range && /^\d{4}-\d{2}-\d{2}$/.test(day)) {
1290
+ beginText = `${day} ${range[0]}`;
1291
+ endText = `${day} ${range[1]}`;
1292
+ slotConfirmed = true;
1293
+ }
1294
+ }
1295
+ // ①-b 老式写法兼容:有些调用方还会传 workStartHour/workEndHour(原本是"推荐时段范围")。
1296
+ // 2026-11-03 实测:模型用 {dateFrom, workStartHour:8, workEndHour:12} 表达"上午",
1297
+ // 结果 CLI 仍返回时段候选、用户被二次追问。这里按窗口**归一到固定时段**,不再问时间。
1298
+ if (!slotConfirmed && !/\d{1,2}:\d{2}/.test(beginText)) {
1299
+ const workStart = Number(args.workStartHour);
1300
+ const workEnd = Number(args.workEndHour);
1301
+ if (Number.isFinite(workStart) && Number.isFinite(workEnd) && workEnd > workStart) {
1302
+ const slotName = workEnd <= 12 && workStart < 12
1303
+ ? '上午'
1304
+ : workStart >= 13 && workEnd <= 18
1305
+ ? '下午'
1306
+ : workStart >= 18
1307
+ ? '晚上'
1308
+ : '';
1309
+ const range = slotName ? TIME_SLOT_RANGES[slotName] : undefined;
1310
+ const day = String(args.dateFrom ?? '').trim().slice(0, 10)
1311
+ || beginText.slice(0, 10)
1312
+ || tomorrowIso();
1313
+ if (range && /^\d{4}-\d{2}-\d{2}$/.test(day)) {
1314
+ beginText = `${day} ${range[0]}`;
1315
+ endText = `${day} ${range[1]}`;
1316
+ slotConfirmed = true;
1317
+ }
1318
+ }
1319
+ }
1320
+ // ② begin_datetime 里直接写中文("2026-11-03 上午")也认
1321
+ const slotHit = beginText.match(/(\d{4}-\d{2}-\d{2})?\s*(上午|早上|下午|晚上)/u);
1322
+ if (slotHit) {
1323
+ const range = TIME_SLOT_RANGES[slotHit[2]];
1324
+ const day = slotHit[1]
1325
+ || beginText.slice(0, 10)
1326
+ || String(args.dateFrom ?? '').trim().slice(0, 10)
1327
+ || tomorrowIso();
1328
+ const rest = beginText.replace(slotHit[0], '');
1329
+ if (range && /^\d{4}-\d{2}-\d{2}$/.test(day) && !/\d{1,2}:\d{2}/.test(rest)) {
1330
+ beginText = `${day} ${range[0]}`;
1331
+ endText = `${day} ${range[1]}`;
1332
+ slotConfirmed = true;
1333
+ }
1334
+ }
1250
1335
  const rawMembers = String(args.hrm_members ?? '')
1251
1336
  .split(/[,;,;\s]+/)
1252
1337
  .map((item) => item.trim())
1253
1338
  .filter((item) => item !== '');
1339
+ /**
1340
+ * 没传参会人 → **默认就是当前登录人**(2026-11-02 用户要求)。
1341
+ * 以前这里空着会让调用方弹窗"请选择参会人",但「我自己开的会」是最常见的情况,
1342
+ * 多问一轮没必要;真要加人,用户会在后续对话里说。
1343
+ */
1344
+ let membersDefaulted = false;
1345
+ if (rawMembers.length === 0) {
1346
+ const self = await resolveSelfPerson(env);
1347
+ if (self) {
1348
+ rawMembers.push(self.id);
1349
+ membersDefaulted = true;
1350
+ // 写回入参:后面 shapeValues / mapFields 都读 args.hrm_members,不写回就进不了表单
1351
+ args.hrm_members = rawMembers.join(',');
1352
+ }
1353
+ }
1254
1354
  // ⛔ 参会人冲突检测(conflict.member)与共同空闲时段(schedule.free)**要的是员工 id**,
1255
1355
  // 传姓名会被接口拒 —— 两路 Promise 都是 `.catch(() => null)`,于是"检测没跑"和"检测通过"
1256
1356
  // 在返回里长得一模一样,用户以为查过了(2026-10-30 真机:agent 传"易鹏",两路全部静默跳过)。
@@ -1267,7 +1367,7 @@ const ebMeetingCreateFlowPrepare = {
1267
1367
  // CLI 当成"已定"直接进确认摘要,用户看到「时间:09:00–10:00」质问"谁决定的"。
1268
1368
  // 所以只有 `timeConfirmed: true`(用户原话明确给了时间,或用户在选项里点选后把 value 原样回传)
1269
1369
  // 才认这个时间;否则一律返回时段选项让用户点 —— 宁可多问一轮,也不替用户拍板。
1270
- const timeConfirmed = args.timeConfirmed === true;
1370
+ const timeConfirmed = args.timeConfirmed === true || slotConfirmed;
1271
1371
  // ⛔ 2026-10-30 真机:agent 在用户只说"明天上午"时就自己塞了 workflowId,
1272
1372
  // 于是它脑内"流程已定",回复里先讲流程再问时间(用户投诉顺序反了)。
1273
1373
  // 时间/地点闸门阶段流程根本没定 —— 这里只做提示,不阻断(用户可能确实点名了流程,
@@ -1392,15 +1492,33 @@ const ebMeetingCreateFlowPrepare = {
1392
1492
  const callerInMembers = callerToken !== '' && (rawMembers.includes(callerToken) || memberIds.includes(callerToken));
1393
1493
  const roomMemberCount = declaredTotal
1394
1494
  ?? (memberIds.length > 0 ? memberIds.length + (callerInMembers ? 0 : 1) : undefined);
1495
+ // 有没有客户:决定要不要优先推接待类会议室(crm_members 可能是数组、"id,id" 或名称)
1496
+ const hasCustomer = Array.isArray(args.crm_members)
1497
+ ? args.crm_members.length > 0
1498
+ : String(args.crm_members ?? '').trim() !== '';
1499
+ // 全天占用缓存(2026-11-03 用户方案):不带 address 条件一次拉当天全部会议室占用(~2s,
1500
+ // 5 分钟 TTL),推荐 busy 判定与指定房间的占用检测**共用同一份**;同一天内改时间/换房间
1501
+ // 不再打占用接口。提交成功后当天缓存立即作废(见提交成功处)。
1502
+ const dayUsagesTask = (enrich && beginText && endText)
1503
+ ? loadDayUsages(env, day)
1504
+ : Promise.resolve(null);
1395
1505
  const roomTask = (enrich && beginText && endText)
1396
- ? recommendRooms(env, {
1397
- beginDatetime: beginText,
1398
- endDatetime: endText,
1399
- ...(roomMemberCount ? { memberCount: roomMemberCount } : {}),
1400
- // 建会是"要下单"的场景:宁可多一次请求也必须拿最新台账(24h 缓存会让刚导入的会议室看不到)。
1401
- refreshRooms: true,
1402
- topN: 3,
1403
- }).catch(noteFailure('会议室推荐'))
1506
+ ? (async () => {
1507
+ const usageData = await dayUsagesTask;
1508
+ return recommendRooms(env, {
1509
+ beginDatetime: beginText,
1510
+ endDatetime: endText,
1511
+ ...(roomMemberCount ? { memberCount: roomMemberCount } : {}),
1512
+ // 带客户时优先接待类会议室(2026-11-02 用户口径:加了客户就推接待类)
1513
+ ...(hasCustomer ? { hasCustomer: true } : {}),
1514
+ // 2026-11-03 用户批准松绑:不再每次强制重拉台账(省 ~1s + 2 请求)。
1515
+ // 兜底在 recommendRooms 内部:24h 缓存凑不出候选时自动强制重拉一次再算;
1516
+ // 代价是刚导入的会议室在缓存期可能按名称查不到(按 id 指定不受影响)。
1517
+ topN: 3,
1518
+ // 占用走全天缓存(usageData 为 null 时 recommendRooms 内部自己查,行为不变)
1519
+ ...(usageData ? { dayUsages: usageData.rows } : {}),
1520
+ });
1521
+ })().catch(noteFailure('会议室推荐'))
1404
1522
  : Promise.resolve(null);
1405
1523
  // ⛔ 指定了会议室时,**单独查它在该时段是否已被占用**(query_roomusage_list)。
1406
1524
  // 以前 `!addressInput` 直接跳过整段 → "指定了一间被占用的房"照样往下走,用户到页面上才发现。
@@ -1420,6 +1538,21 @@ const ebMeetingCreateFlowPrepare = {
1420
1538
  // 台账读失败 / 纯 id 台账里没有 → 没法判占用,但不拦(避免网络抖动被当成"房间不存在")
1421
1539
  if (!room.id || room.lookupFailed || room.status !== '0')
1422
1540
  return { room };
1541
+ // 优先走全天占用缓存本地过滤(2026-11-03):与推荐共用一次拉取,0 额外请求。
1542
+ // 缓存拉失败(dayUsagesTask reject 已被 noteFailure 吞成 null)→ 回退老的逐房查询,行为不变。
1543
+ const usageData = await dayUsagesTask.catch(() => null);
1544
+ if (usageData) {
1545
+ const conflicts = filterUsages(usageData.rows, { roomId: room.id, begin: beginText, end: endText })
1546
+ .map((usage) => ({ mtId: usage.mtId, begin: usage.start, end: usage.end }));
1547
+ return {
1548
+ room,
1549
+ hasConflict: conflicts.length > 0,
1550
+ conflicts,
1551
+ verdict: conflicts.length > 0 ? 'warn' : 'ok',
1552
+ requests: usageData.requests,
1553
+ usageFromCache: usageData.fromCache,
1554
+ };
1555
+ }
1423
1556
  const { getMeetingOperation } = await import('../registry.js');
1424
1557
  const result = await getMeetingOperation('meeting.eb.conflict.room').handler({ beginDatetime: beginText, endDatetime: endText, address: room.id }, env);
1425
1558
  return { room, ...(result?.data ?? {}) };
@@ -1764,66 +1897,83 @@ const ebMeetingCreateFlowPrepare = {
1764
1897
  + '**不要**改用直连建会绕过流程管控。', { probes });
1765
1898
  }
1766
1899
  if (launchable.length > 1 && !autoPick) {
1767
- // 仍有多个可选 → 交给用户挑,不替用户决定(用户明确要求"以选择的方式")
1768
- return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
1769
- status: 'NEEDS_SELECTION',
1770
- // ⚠️ 以前漏了这个字段(agent 拿到 undefined 无法判断是哪类选择,2026-10-30 真机发现)。
1771
- selectionType: 'flow',
1772
- // 明确告诉调用方"这是一道选择题,请用选项控件呈现",不要降级成文本列表
1773
- // (2026-10-30 用户反馈:agent 把会议室候选写成文本 + "需要你指定一间",没给选项卡)。
1774
- ui: { component: 'choice', multiple: false, recommendedKey: 'W1' },
1775
- // 四要素已经确认到这一步了:摘要一起给出去,用户只需点一下流程,就能直接提交。
1776
- confirmed: {
1777
- mt_name: String(args.mt_name ?? ''),
1778
- begin_datetime: beginText,
1779
- end_datetime: endText,
1780
- address: confirmedAddressId || null,
1781
- addressName: confirmedAddressLabel || null,
1782
- hrm_members: memberIds,
1783
- },
1784
- message: `会议要素已确认(${String(args.mt_name ?? '')} · ${confirmedWhen}`
1785
- + `${confirmedAddressLabel ? ` · ${confirmedAddressLabel}` : ' · 无会议室'});`
1786
- + `当前账号对 ${sourceLabel}的 ${candidates.length} 个建会流程**都有发起权限**,`
1787
- + '请让用户最后选一下「用哪条流程发起」—— **不要默认取第一个**(各流程表单字段可能完全不同,'
1788
- + '选错会漏字段;2026-10-29 真实事故:取第一个导致字段全填不进另一条流程,'
1789
- + '用户以为"系统只支持一条流程")。⚠️ 只有其中 1 个有权限时 CLI 会直接发起,不会走到这一步',
1790
- options: launchable.map((probe, index) => ({
1791
- key: `W${index + 1}`,
1792
- label: probe.name ? `${probe.name}(${probe.workflowId})` : probe.workflowId,
1793
- // ⛔ 把已确认的四要素一起带回去:用户点选流程后**原样回传 value** 就能直接出确认摘要,
1794
- // 不用再走一遍时间/房间闸门(否则"先定四要素再选流程"会多出一轮无效往返)。
1795
- value: {
1796
- workflowId: probe.workflowId,
1900
+ // 仍有多个可选 → 优先沿用**流程记忆**(2026-11-03 用户方案:这个人上次成功用过的流程),
1901
+ // 记忆命中且本次仍可发起就直接用,不再打断用户(权限过滤照做,记忆只是默认值)。
1902
+ // 传 flowChoice:'ask' 或记忆没命中 → 回到"让用户挑"。
1903
+ const remembered = args.flowChoice === 'ask' ? null : await readFlowPick(env).catch(() => null);
1904
+ const rememberedHit = pickRememberedFlow(launchable, remembered);
1905
+ if (rememberedHit) {
1906
+ workflowId = rememberedHit.workflowId;
1907
+ const others = launchable
1908
+ .filter((probe) => probe.workflowId !== rememberedHit.workflowId)
1909
+ .map((probe) => (probe.name ? `${probe.name}(${probe.workflowId})` : probe.workflowId));
1910
+ flowPickNote = `${sourceLabel}:当前账号可发起 ${launchable.length} 个建会流程,`
1911
+ + `已沿用你上次选的「${rememberedHit.name ?? rememberedHit.workflowId}」(${rememberedHit.workflowId})发起`
1912
+ + ` —— 要换的话说一声${others.length > 0 ? `(其他可用:${others.join('、')})` : ''}。`;
1913
+ }
1914
+ else {
1915
+ // 没有记忆可沿用 → 交给用户挑,不替用户决定(用户明确要求"以选择的方式")
1916
+ return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
1917
+ status: 'NEEDS_SELECTION',
1918
+ // ⚠️ 以前漏了这个字段(agent 拿到 undefined 无法判断是哪类选择,2026-10-30 真机发现)。
1919
+ selectionType: 'flow',
1920
+ // 明确告诉调用方"这是一道选择题,请用选项控件呈现",不要降级成文本列表
1921
+ // (2026-10-30 用户反馈:agent 把会议室候选写成文本 + "需要你指定一间",没给选项卡)。
1922
+ ui: { component: 'choice', multiple: false, recommendedKey: 'W1' },
1923
+ // 四要素已经确认到这一步了:摘要一起给出去,用户只需点一下流程,就能直接提交。
1924
+ confirmed: {
1925
+ mt_name: String(args.mt_name ?? ''),
1797
1926
  begin_datetime: beginText,
1798
1927
  end_datetime: endText,
1799
- timeConfirmed: true,
1800
- ...(confirmedAddressId
1801
- ? { address: confirmedAddressId, roomConfirmed: true }
1802
- : {}),
1928
+ address: confirmedAddressId || null,
1929
+ addressName: confirmedAddressLabel || null,
1930
+ hrm_members: memberIds,
1803
1931
  },
1804
- reason: '当前账号有发起权限',
1805
- // 把该流程的字段名如实带出来:不同流程字段差异很大(有的有设备需求/涉密项目,有的没有),
1806
- // 用户要靠这个才能选对(2026-10-29 用户反馈:选错方式后字段全填不进去)。
1807
- fields: (loadedForms.get(probe.workflowId)?.fields ?? []).map((f) => f.name).filter(Boolean),
1808
- })),
1809
- probes,
1810
- }, {
1811
- warnings: [
1812
- `${sourceLabel}:共 ${candidates.length} 个建会流程,其中 ${launchable.length} 个当前账号可发起;`
1813
- + '**必须把这几个流程做成选项让用户挑一个,禁止自己替用户选**;选定后带单个 workflowId 重新 prepare。',
1814
- ...probes.filter((probe) => !probe.launchable)
1815
- .map((probe) => `流程 ${probe.workflowId} 当前账号无法发起(${probe.reason ?? '无权限'}),已排除`),
1816
- ],
1817
- });
1932
+ message: `会议要素已确认(${String(args.mt_name ?? '')} · ${confirmedWhen}`
1933
+ + `${confirmedAddressLabel ? ` · ${confirmedAddressLabel}` : ' · 无会议室'});`
1934
+ + `当前账号对 ${sourceLabel}的 ${candidates.length} 个建会流程**都有发起权限**,`
1935
+ + '请让用户最后选一下「用哪条流程发起」—— **不要默认取第一个**(各流程表单字段可能完全不同,'
1936
+ + '选错会漏字段;2026-10-29 真实事故:取第一个导致字段全填不进另一条流程,'
1937
+ + '用户以为"系统只支持一条流程")。⚠️ 只有其中 1 个有权限时 CLI 会直接发起,不会走到这一步',
1938
+ options: launchable.map((probe, index) => ({
1939
+ key: `W${index + 1}`,
1940
+ label: probe.name ? `${probe.name}(${probe.workflowId})` : probe.workflowId,
1941
+ // ⛔ 把已确认的四要素一起带回去:用户点选流程后**原样回传 value** 就能直接出确认摘要,
1942
+ // 不用再走一遍时间/房间闸门(否则"先定四要素再选流程"会多出一轮无效往返)。
1943
+ value: {
1944
+ workflowId: probe.workflowId,
1945
+ begin_datetime: beginText,
1946
+ end_datetime: endText,
1947
+ timeConfirmed: true,
1948
+ ...(confirmedAddressId
1949
+ ? { address: confirmedAddressId, roomConfirmed: true }
1950
+ : {}),
1951
+ },
1952
+ reason: '当前账号有发起权限',
1953
+ // 把该流程的字段名如实带出来:不同流程字段差异很大(有的有设备需求/涉密项目,有的没有),
1954
+ // 用户要靠这个才能选对(2026-10-29 用户反馈:选错方式后字段全填不进去)。
1955
+ fields: (loadedForms.get(probe.workflowId)?.fields ?? []).map((f) => f.name).filter(Boolean),
1956
+ })),
1957
+ probes,
1958
+ }, {
1959
+ warnings: [
1960
+ `${sourceLabel}:共 ${candidates.length} 个建会流程,其中 ${launchable.length} 个当前账号可发起;`
1961
+ + '**必须把这几个流程做成选项让用户挑一个,禁止自己替用户选**;选定后带单个 workflowId 重新 prepare。',
1962
+ ...probes.filter((probe) => !probe.launchable)
1963
+ .map((probe) => `流程 ${probe.workflowId} 当前账号无法发起(${probe.reason ?? '无权限'}),已排除`),
1964
+ ],
1965
+ });
1966
+ }
1818
1967
  }
1819
- workflowId = launchable[0].workflowId;
1820
- if (launchable.length === 1 && candidates.length > 1) {
1968
+ if (workflowId === '')
1969
+ workflowId = launchable[0].workflowId;
1970
+ if (launchable.length === 1 && candidates.length > 1 && flowPickNote === null) {
1821
1971
  // 只有一条有权限 → 直接发起,但要说明"为什么没让你选"(其余流程是无权限,不是被忽略)。
1822
1972
  const only = launchable[0];
1823
1973
  flowPickNote = `${sourceLabel}:共 ${candidates.length} 个建会流程,当前账号只有`
1824
1974
  + `「${only.name ?? only.workflowId}」(${only.workflowId})有发起权限,已直接用它发起`;
1825
1975
  }
1826
- if (launchable.length > 1) {
1976
+ if (launchable.length > 1 && flowPickNote === null) {
1827
1977
  // 固化自用:多个可发起时按候选顺序自动取第一个 —— 但必须留痕说明"还有别的可用流程",
1828
1978
  // 不能默默替用户决定(2026-10-30:用户要的是"直接发起",不是"什么都别告诉我")。
1829
1979
  const picked = launchable[0];
@@ -1947,7 +2097,9 @@ const ebMeetingCreateFlowPrepare = {
1947
2097
  if (crmMembersGiven && !onlyDetailMode) {
1948
2098
  const detailTarget = resolveCrmDetailTarget(form.fields);
1949
2099
  if (detailTarget) {
1950
- const auto = await resolveAutoCrmDetailRows(env, args.crm_members);
2100
+ // 入参用**已解析成 [{id,name}] 的版本**:回查要按 customerId 精确命中,
2101
+ // 只给名称的调用方式(`crm_members:"某客户"`)在这里已经没有 id 可用,会一条都拿不到。
2102
+ const auto = await resolveAutoCrmDetailRows(env, shaped.values.crm_members ?? args.crm_members);
1951
2103
  if (auto.rows.length > 0) {
1952
2104
  crmDetailRows = auto.rows;
1953
2105
  // 入参里显式给的额外列(性别/备注等)按姓名补回去,不全丢
@@ -2066,6 +2218,11 @@ const ebMeetingCreateFlowPrepare = {
2066
2218
  const crmPatchIds = (Array.isArray(shaped.values.crm_members) ? shaped.values.crm_members : [])
2067
2219
  .map((row) => String(row?.id ?? '').trim())
2068
2220
  .filter((id) => id !== '');
2221
+ // 建群要用的要素随凭证带到 apply(2026-11-02 用户要求「发起流程时同步建群」):
2222
+ // 动作流的 hrmmembers **必须是员工 id 逗号串**,姓名它不认,所以这里只留 id。
2223
+ const groupMemberIds = (Array.isArray(shaped.values.hrm_members) ? shaped.values.hrm_members : [])
2224
+ .map((one) => String(one?.id ?? one ?? '').trim())
2225
+ .filter((id) => id !== '');
2069
2226
  const session = await env.host.getSession();
2070
2227
  const continuation = await env.continuation.issue({
2071
2228
  operation: 'meeting.eb.meeting.create.flow.apply',
@@ -2074,8 +2231,9 @@ const ebMeetingCreateFlowPrepare = {
2074
2231
  userId: session.userId,
2075
2232
  payload: {
2076
2233
  workflowId,
2077
- requestName: String(args.mt_name),
2078
- mtName: String(args.mt_name),
2234
+ requestName: String(args.mt_name ?? args.mt_subject ?? ''),
2235
+ mtName: String(args.mt_name ?? args.mt_subject ?? ''),
2236
+ workflowName: String(form.workflowName ?? ''),
2079
2237
  data: { main, ...(detailPayload ? { details: detailPayload } : {}) },
2080
2238
  surfaceContinuation: form.continuation,
2081
2239
  // 供 apply 在「会议还没生成」时按申请单信息预生成邀请函(预置函)
@@ -2090,6 +2248,13 @@ const ebMeetingCreateFlowPrepare = {
2090
2248
  ...(crmPatchIds.length > 0
2091
2249
  ? { crmPatch: { ids: crmPatchIds, count: crmMemberCount === undefined ? crmPatchIds.length : crmMemberCount } }
2092
2250
  : {}),
2251
+ // 建群要素:apply 拿到会议 id 后先建群、再把群 id 回写 imchat_groupid
2252
+ groupChat: {
2253
+ memberIds: groupMemberIds,
2254
+ beginDatetime: String(shaped.values.begin_datetime ?? ''),
2255
+ endDatetime: String(shaped.values.end_datetime ?? ''),
2256
+ address: String(shaped.values.address ?? ''),
2257
+ },
2093
2258
  },
2094
2259
  fingerprint: JSON.stringify({ workflowId, main, ...(detailPayload ? { details: detailPayload } : {}) }),
2095
2260
  }, 10 * 60 * 1000);
@@ -2102,7 +2267,7 @@ const ebMeetingCreateFlowPrepare = {
2102
2267
  ...(flowPickNote
2103
2268
  ? { workflowAutoPicked: { workflowId, source: resolved.source, note: flowPickNote } }
2104
2269
  : {}),
2105
- requestName: String(args.mt_name),
2270
+ requestName: String(args.mt_name ?? args.mt_subject ?? ''),
2106
2271
  fieldMapping: mapped,
2107
2272
  formData: { main },
2108
2273
  // 人话版「即将提交的内容」—— 提交前必须先贴给用户核对(见 SKILL 建会章节)。
@@ -2323,6 +2488,44 @@ const ebMeetingCreateFlowApply = {
2323
2488
  // 不能报错把整轮卡死(用户要的表单就在 finalMarkdown 里)。
2324
2489
  // ⛔ 邀请函必须照给:它是预置函,按申请单已知信息生成,不依赖会议是否已生成。
2325
2490
  const pre = await renderPreInvitation(payload, env);
2491
+ // 建群:这一档服务端没有单据、也没有会议 id,只能先把群建出来(回写等会议生成后再补)。
2492
+ // 用户口径是「发起流程时就同步建群」,表单都递到手上了,群该先建好。
2493
+ let groupChat;
2494
+ const groupPayload = payload?.groupChat;
2495
+ if (groupPayload) {
2496
+ try {
2497
+ const { ensureMeetingGroupChat } = await import('./group-chat.js');
2498
+ groupChat = {
2499
+ ...(await ensureMeetingGroupChat(env, {
2500
+ meetingName: String(payload.mtName ?? ''),
2501
+ beginDatetime: String(groupPayload.beginDatetime ?? ''),
2502
+ endDatetime: String(groupPayload.endDatetime ?? ''),
2503
+ memberIds: (Array.isArray(groupPayload.memberIds) ? groupPayload.memberIds : []),
2504
+ ...(groupPayload.address ? { address: String(groupPayload.address) } : {}),
2505
+ })),
2506
+ };
2507
+ }
2508
+ catch (error) {
2509
+ groupChat = { ok: false, error: String(error?.message ?? error).slice(0, 200) };
2510
+ }
2511
+ }
2512
+ // 群建好了但还没有会议 id:落一条待绑记录(群 id + 工作流 + 时间),
2513
+ // 用户点提交后用 lookupPending 按「流程单 → 会议」补绑,**不依赖会议名称**。
2514
+ if (groupChat && groupChat.ok) {
2515
+ try {
2516
+ const { savePendingForm } = await import('./group-chat.js');
2517
+ await savePendingForm(env, {
2518
+ workflowId: String(payload.workflowId ?? ''),
2519
+ workflowName: String(payload.workflowName ?? ''),
2520
+ mtName: String(payload.mtName ?? ''),
2521
+ groupId: String(groupChat.groupId ?? ''),
2522
+ groupName: String(groupChat.groupName ?? ''),
2523
+ });
2524
+ }
2525
+ catch {
2526
+ // 落盘失败不影响输出表单;补绑时可改传 requestId
2527
+ }
2528
+ }
2326
2529
  return meetingSuccess('meeting.eb.meeting.create.flow.apply', {
2327
2530
  status: 'BROWSER_FILL',
2328
2531
  workflowId: payload.workflowId,
@@ -2332,13 +2535,24 @@ const ebMeetingCreateFlowApply = {
2332
2535
  contextId: submittedData.contextId ?? null,
2333
2536
  // 宿主要靠这段 <browsertool> 才能把表单拉到浏览器里填,必须原样透出
2334
2537
  finalMarkdown: submittedData.finalMarkdown ?? null,
2538
+ // 与 finalMarkdown 相邻的硬提示:模型「以为 CLI 已经打开了表单」是这次翻车的直接原因
2539
+ finalMarkdownHint: '把 finalMarkdown 原样贴在回复最后(从 <browsertool> 到 </browsertool>,不包代码块);'
2540
+ + '不贴这段=用户看不到表单,也不能说"表单已打开"。',
2335
2541
  message: String(submittedData.message ?? ''),
2336
2542
  invitation: pre.invitation,
2543
+ ...(groupChat ? { groupChat } : {}),
2337
2544
  timings: { submitMs: Date.now() - submitStartedAt, invitationMs: pre.ms },
2338
2545
  }, {
2339
2546
  warnings: [
2340
2547
  '当前是小e 环境:本次**没有向服务端写入**,建会流程改由浏览器页面完成填单(见 finalMarkdown)。'
2341
2548
  + '因此还没有 requestId / 会议 id,也**不能说"流程已提交"**。',
2549
+ ...(groupChat && groupChat.ok
2550
+ ? [`群聊已同步建好(群 id ${String(groupChat.groupId)},群名 ${String(groupChat.groupName)}):`
2551
+ + '流程还没提交、没有会议 id,所以 imchat_groupid 暂时没回写;'
2552
+ + '会议生成后再跑一次 meeting.eb.meeting.group.ensure(传 meetingId + groupId)绑定。']
2553
+ : groupChat
2554
+ ? [`群聊没建成功(${String(groupChat.error ?? '未知原因')}):会议照常可以在页面里提交,群需要手工建或稍后重试。`]
2555
+ : []),
2342
2556
  `邀请函已按申请单信息**预生成**(预置函,${String(pre.invitation.filePath ?? '见 filePath')}):`
2343
2557
  + '请下载后人工核对、自行发送;流程审批通过、会议生成后可用 mtId 再跑一次 meeting.invitation.render 拿完整字段版本。',
2344
2558
  '⛔ 不要因为"没有 requestId / 没有会议 id"重复 apply —— 填单要在那个浏览器页面里点提交。',
@@ -2354,6 +2568,14 @@ const ebMeetingCreateFlowApply = {
2354
2568
  : '服务端未返回 requestId,请查看 result 排查'), { submit: submittedData, missing });
2355
2569
  }
2356
2570
  const warnings = [];
2571
+ // ── 提交成功后的收尾(都是 void 异步,不阻塞返回)──────────────────────
2572
+ // ① 当天占用缓存作废:这场会已经占了房,不删的话 5 分钟内同房间的占用检测会漏判
2573
+ // (2026-11-03 用户方案:缓存只服务"提交前"的检测,提交即失效)。
2574
+ const submitDay = dayOf(String(payload?.groupChat?.beginDatetime ?? ''));
2575
+ if (submitDay !== '')
2576
+ void invalidateDayUsages(env, submitDay).catch(() => undefined);
2577
+ // ② 流程记忆:记住这次实际用的流程 —— 下次"多流程可发起"时默认沿用(2026-11-03 用户方案)。
2578
+ void rememberFlowPick(env, String(payload?.workflowId ?? ''), typeof payload?.workflowName === 'string' ? payload.workflowName : undefined).catch(() => undefined);
2357
2579
  // 会议由流程/ESB 创建:提交后不一定立刻出现,轮询一下再决定要不要出邀请函。
2358
2580
  // 默认只等 20s:流程多走审批,干等会把"建个会"拖成好几分钟(用户 2026-10-28 反馈);
2359
2581
  // 没等到就给 pending,让调用方稍后补邀请函。
@@ -2361,13 +2583,16 @@ const ebMeetingCreateFlowApply = {
2361
2583
  let meetingId = '';
2362
2584
  const timings = { submitMs: Date.now() - submitStartedAt };
2363
2585
  if (waitSeconds > 0) {
2364
- // 轮询策略:**先立即查一次**(多数情况下会议是同步生成的),没有就 5 秒后再查一次;
2365
- // 仍未生成说明还在审批,直接返回 pending —— 不再按 waitSeconds 死等(用户体感"建会好几分钟"多半是这里)。
2586
+ // 轮询策略(2026-11-03 用户确认「还是要轮询」):**先立即查一次**,之后按 0.5/1/2/4 秒加密连查——
2587
+ // 审批多数几秒内通过,加密能早 3~4 秒拿到会议号(原来固定 0/5s 两查,命中最晚要等 5 秒);
2588
+ // 封顶 ~7.5s 仍未生成就返回 pending,不死等(审批中的会调用方稍后补邀请函)。
2366
2589
  const { getMeetingOperation } = await import('../registry.js');
2367
2590
  const list = getMeetingOperation('meeting.eb.meeting.list');
2368
2591
  const deadline = Date.now() + Math.min(waitSeconds, 30) * 1000;
2369
2592
  const pollStart = Date.now();
2370
2593
  let waited = 0;
2594
+ let stepIndex = 0;
2595
+ const pollSteps = [500, 1000, 2000, 4000];
2371
2596
  while (Date.now() <= deadline) {
2372
2597
  try {
2373
2598
  const page = await list.handler({ mt_name: payload.mtName, pageSize: 5 }, env);
@@ -2384,10 +2609,12 @@ const ebMeetingCreateFlowApply = {
2384
2609
  catch {
2385
2610
  // 查询失败不中断,继续按节奏重试
2386
2611
  }
2387
- if (waited >= 5000)
2388
- break; // 第二次仍未命中:不再等,交给调用方稍后补邀请函
2389
- await new Promise((resolve) => setTimeout(resolve, 5000));
2390
- waited += 5000;
2612
+ if (waited >= 7500)
2613
+ break; // 0 + 0.5 + 1 + 2 + 4 秒五查仍未命中:还在审批,交给调用方稍后补邀请函
2614
+ const step = pollSteps[Math.min(stepIndex, pollSteps.length - 1)];
2615
+ stepIndex += 1;
2616
+ await new Promise((resolve) => setTimeout(resolve, step));
2617
+ waited += step;
2391
2618
  }
2392
2619
  timings.pollMs = Date.now() - pollStart;
2393
2620
  }
@@ -2415,6 +2642,42 @@ const ebMeetingCreateFlowApply = {
2415
2642
  + '不依赖 ESB 字段映射时才需要它。');
2416
2643
  }
2417
2644
  }
2645
+ // 「发起流程时同步建群」(2026-11-02 用户要求):建群动作流一次做完「建群 + 群内发会议信息」,
2646
+ // 但它的 responseData 是空对象、不回群 id ⇒ 建完按「群名 = 会议名称」反查,再回写 imchat_groupid。
2647
+ // ⛔ 建群失败**不影响建会结果**:这里只记 warning,不抛错(会议已经建好了,群可以后补)。
2648
+ let groupChat;
2649
+ const groupPayload = payload?.groupChat;
2650
+ if (groupPayload) {
2651
+ const groupStart = Date.now();
2652
+ try {
2653
+ const { ensureMeetingGroupChat } = await import('./group-chat.js');
2654
+ const group = await ensureMeetingGroupChat(env, {
2655
+ meetingName: String(payload.mtName ?? ''),
2656
+ beginDatetime: String(groupPayload.beginDatetime ?? ''),
2657
+ endDatetime: String(groupPayload.endDatetime ?? ''),
2658
+ memberIds: (Array.isArray(groupPayload.memberIds) ? groupPayload.memberIds : []),
2659
+ ...(groupPayload.address ? { address: String(groupPayload.address) } : {}),
2660
+ ...(meetingId ? { meetingId } : {}),
2661
+ });
2662
+ groupChat = { ...group };
2663
+ if (group.ok && group.bound) {
2664
+ warnings.push(`已按会议同步建群「${group.groupName ?? payload.mtName}」(群 id ${group.groupId}),`
2665
+ + `并回写到会议记录 imchat_groupid${group.readBack ? `(回读 ${group.readBack})` : ''}。`);
2666
+ }
2667
+ else if (group.ok) {
2668
+ warnings.push(`群已建好(群 id ${group.groupId}),但会议还没生成,暂时没法回写 imchat_groupid;`
2669
+ + '会议生成后传 meetingId+groupId 跑一次 meeting.eb.meeting.group.ensure 绑定即可。');
2670
+ }
2671
+ else {
2672
+ warnings.push(`群聊没建成功/没绑上(${group.error ?? '未知原因'}):会议本身已建好,群可在 IM 里手工建,`
2673
+ + '再把群 id 传到 meeting.eb.meeting.group.ensure 的 groupId 绑定。');
2674
+ }
2675
+ }
2676
+ catch (error) {
2677
+ warnings.push(`建群异常(不影响建会):${String(error?.message ?? error).slice(0, 200)}`);
2678
+ }
2679
+ timings.groupMs = Date.now() - groupStart;
2680
+ }
2418
2681
  let invitation = { pending: true };
2419
2682
  if (meetingId) {
2420
2683
  const invitationStart = Date.now();
@@ -2458,6 +2721,8 @@ const ebMeetingCreateFlowApply = {
2458
2721
  meetingId: meetingId || null,
2459
2722
  // 参会客户有没有真的写进会议记录(表单侧是 Ebuilder,需 apply 补写;回读确认才算 ok)
2460
2723
  ...(crmLinked ? { crmLinked } : {}),
2724
+ // 同步建的群(群 id / 群名 / 有没有回写 imchat_groupid)
2725
+ ...(groupChat ? { groupChat } : {}),
2461
2726
  // 提交后回显「实际提交了什么」:字段名 → 值(人员/客户显示姓名,不显示 id)。
2462
2727
  // 用户要的就是"能看见内容",替代走不通的浏览器填单核对。
2463
2728
  submittedPreview: renderFormPreview(payload?.fieldLabels ?? [], await decoratePreviewPersonNames(env, (payload?.data?.main ?? {}), payload?.fieldLabels ?? [], new Map(Object.entries((payload?.personLabels ?? {}))))),
@@ -2540,7 +2805,15 @@ const ebMeetingCreateFlowQuick = {
2540
2805
  },
2541
2806
  dateFrom: {
2542
2807
  type: 'string',
2543
- description: '没给具体时间时按哪一天推荐时段(yyyy-MM-dd;不传默认明天)',
2808
+ description: '没给具体时间时按哪一天推荐时段(yyyy-MM-dd;不传默认明天)。'
2809
+ + '⚠️ 用户说了「上午/下午/晚上」就必须同时传 time_slot —— 只传 dateFrom 会弹时段选择器让用户再选一次',
2810
+ },
2811
+ time_slot: {
2812
+ type: 'string',
2813
+ enum: ['上午', '早上', '下午', '晚上'],
2814
+ description: '用户只说了「上午/下午/晚上」时传这个(配合 dateFrom):'
2815
+ + '上午/早上=09:00–12:00、下午=13:00–18:00、晚上=18:00–21:00,CLI 直接定下来、不再返回时段候选。'
2816
+ + '⛔ 不要只传 dateFrom 就期待"让用户选时段"——用户已经说了时段就别再问',
2544
2817
  },
2545
2818
  durationMinutes: {
2546
2819
  type: 'number',
@@ -2558,7 +2831,8 @@ const ebMeetingCreateFlowQuick = {
2558
2831
  type: 'string',
2559
2832
  enum: ['auto', 'ask'],
2560
2833
  description: '多个建会流程**都有发起权限**时:默认(不传 / ask)= 返回选项让用户挑;'
2561
- + 'auto = 自动取优先级最高的。只有 1 个有权限时直接发起',
2834
+ + 'auto = 自动取优先级最高的。有流程记忆(上次成功用过的流程)且仍可发起时默认沿用上次选择,'
2835
+ + '传 ask 强制每次都问。只有 1 个有权限时直接发起',
2562
2836
  },
2563
2837
  crm_skip: {
2564
2838
  type: 'boolean',
@@ -2622,12 +2896,15 @@ const ebMeetingCreateFlowQuick = {
2622
2896
  requestId: applied?.data?.requestId,
2623
2897
  meetingId: applied?.data?.meetingId ?? null,
2624
2898
  invitation: applied?.data?.invitation ?? null,
2899
+ // 同步建的群(群 id / 群名 / 有没有回写 imchat_groupid)
2900
+ ...(applied?.data?.groupChat ? { groupChat: (applied?.data).groupChat } : {}),
2625
2901
  // 已提交内容回显(字段名 → 值),提交后可直接贴给用户看"到底填进去了什么"
2626
2902
  submittedPreview: applied?.data?.submittedPreview ?? null,
2627
2903
  // 小e 环境(BROWSER_FILL)不写服务端,表单靠这段 <browsertool> 在浏览器里填 —— 必须一起透出,
2628
2904
  // 否则宿主拿不到表单,用户看到的就是"提交了但没反应"。
2629
2905
  ...(applied?.data?.contextId ? { contextId: (applied?.data).contextId } : {}),
2630
2906
  ...(applied?.data?.finalMarkdown ? { finalMarkdown: (applied?.data).finalMarkdown } : {}),
2907
+ ...(applied?.data?.finalMarkdownHint ? { finalMarkdownHint: (applied?.data).finalMarkdownHint } : {}),
2631
2908
  decisions: prepared?.data?.preview?.decisions ?? null,
2632
2909
  timings: {
2633
2910
  ...(prepared?.data?.preview?.decisions?.timings ?? {}),