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
@@ -111,49 +111,42 @@ export function createMeetingHost(ctx, dependencies = {}) {
111
111
  return sessionContext;
112
112
  }
113
113
  async function download(request) {
114
- const session = await ensureSession();
115
- const url = new URL(request.path, session.getBaseUrl());
116
- for (const [key, value] of Object.entries(request.query ?? {})) {
117
- if (value !== undefined && value !== null && String(value) !== '')
118
- url.searchParams.set(key, String(value));
119
- }
120
- const controller = new AbortController();
121
- const timer = setTimeout(() => controller.abort(), request.timeoutMs ?? 60_000);
114
+ assertPath(request.path);
115
+ let response;
122
116
  try {
123
- const response = await (dependencies.fetch || globalThis.fetch)(url.toString(), {
124
- headers: {
125
- accept: '*/*',
126
- cookie: session.cookieHeader(),
127
- eteamsid: session.eteamsId,
128
- 'user-agent': session.uaHeader(),
129
- origin: session.getBaseUrl(),
130
- },
131
- redirect: 'manual',
132
- signal: controller.signal,
117
+ response = await (await runtime()).requestBinary({
118
+ module: 'meeting',
119
+ operation: 'meeting.attachment.download',
120
+ method: 'GET',
121
+ path: request.path,
122
+ query: omitEmptyQueryValues(request.query),
123
+ headers: { accept: '*/*' },
124
+ timeoutMs: request.timeoutMs ?? 60_000,
133
125
  });
134
- if (response.status === 401 || response.status === 302) {
135
- throw meetingAuthError('session_expired', '登录态已失效:下载需有效登录态,请重新登录后重试', { httpStatus: response.status });
136
- }
137
- if (response.status !== 200) {
138
- throw new MeetingCliError('api', 'download_failed', `下载失败 HTTP ${response.status}`, {
139
- code: response.status,
140
- exitCode: 1,
141
- });
142
- }
143
- return {
144
- buffer: Buffer.from(await response.arrayBuffer()),
145
- contentType: response.headers.get('content-type') ?? '',
146
- contentDisposition: response.headers.get('content-disposition') ?? '',
147
- };
148
126
  }
149
- finally {
150
- clearTimeout(timer);
127
+ catch (error) {
128
+ mapRuntimeNetworkError(error);
129
+ }
130
+ if (response.status === 401 || response.status === 302) {
131
+ throw meetingAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
132
+ }
133
+ if (response.status !== 200) {
134
+ throw new MeetingCliError('api', 'download_failed', `下载失败 HTTP ${response.status}`, {
135
+ code: response.status,
136
+ exitCode: 1,
137
+ });
151
138
  }
139
+ return {
140
+ buffer: response.content,
141
+ contentType: response.contentType,
142
+ contentDisposition: response.headers.get('content-disposition') ?? '',
143
+ };
152
144
  }
153
145
  async function runtime() {
154
146
  return createE10RequestRuntime(ctx, await ensureSession(), {
155
147
  interceptors: dependencies.interceptors,
156
148
  fetch: dependencies.fetch,
149
+ recoverAuthentication: dependencies.recoverAuthentication,
157
150
  });
158
151
  }
159
152
  async function request(request) {
@@ -144,7 +144,10 @@ async function assertCreatePathAllowed(args, env) {
144
144
  throw meetingConflictError('conflict_rejected', '该租户为周期会议配置了工作流(repeatWorkFlowBaseIds 非空),周期会议必须走流程建会;CLI v1 未集成工作流发起,请通过工作流流程创建', { repeatWorkFlowBaseIds: repeatWorkflowIds });
145
145
  }
146
146
  if (!isRepeat && Number(base.onlyFlowCreate) === 1) {
147
- throw meetingConflictError('conflict_rejected', '该租户 onlyFlowCreate=1:会议只能通过流程创建;CLI v1 未集成工作流发起,请通过工作流流程创建', { workFlowBaseIds: base.workFlowBaseIds ?? '' });
147
+ throw meetingConflictError('conflict_rejected', '该租户 onlyFlowCreate=1:会议只能通过流程创建,直连建会已被拒绝(不会写入任何数据)。'
148
+ + '不用停手 —— 改走 **meeting.eb.meeting.create.flow**(prepare→apply)即可:'
149
+ + '它会载入建会流程表单、提交审批,并在会议生成后自动出邀请函。'
150
+ + '如需直连建会,请管理员先在会议应用设置中关闭该开关。', { workFlowBaseIds: base.workFlowBaseIds ?? '' });
148
151
  }
149
152
  }
150
153
  /** 强制冲突检测:接口失败视为冲突(拒绝建会),cansub=false 拒绝提交。 */
@@ -206,6 +206,11 @@ export const EB_ENDPOINTS = [
206
206
  queryParams: ['manager', 'departmentId', 'mr_type', 'room_attr'],
207
207
  summarizeRows: summarizeRoomRows,
208
208
  summary: 'EB 版会议室列表(query_meeting_room_list + count):自带 statusText 与状态字典',
209
+ emptyHint: 'EB 会议室表(`uf_mt_room_base`)查不到任何记录。可能是:① 该环境确实还没建过会议室;'
210
+ + '② 会议应用的「会议室列表」接口/视图未配置好(`query_meeting_room_list` 读不到数据,但表里有数据)。'
211
+ + '**这不代表"环境是标准版"——绝对不要改用 `meeting.room.list`**:那是标准版会议应用的会议室,'
212
+ + '与 EB 完全是两套数据,拿给用户就是凭空捏造房间。请如实告诉用户"EB 会议室查不到",'
213
+ + '并请管理员检查会议应用的会议室配置。',
209
214
  notes: [
210
215
  '**status:`0` = 正常(可用)、`1` = 封存(不可用)** —— 不是"启用/停用",`0` 才是正常。',
211
216
  '明细在 `data.progress.items[]`:`statusText` 已把编码翻成中文,`summary.unusable` 直接列出不可用的房间名;需要原始编码时看 `status`。',
@@ -91,13 +91,18 @@ export async function fetchEbAppSettings(env, operation) {
91
91
  const valid = rows.filter((row) => String(row.data_status ?? '1') === '1');
92
92
  const ordered = [...valid].sort((left, right) => (String(right.update_time ?? '').localeCompare(String(left.update_time ?? ''))));
93
93
  const base = ordered[0] ?? {};
94
+ // ⚠️ 管控开关必须「取严不取松」(2026-10-29 真机事故):
95
+ // 生产环境库里可能有多份设置记录,其中最新一份的开关是空的、旧的一份才是 1。
96
+ // 此前只按最新一条取值 ⇒ 闸门被绕过,only_flow_create=1 的租户也能直连建出会议。
97
+ // 建会路径是**管控**而非偏好:只要任意一份有效记录开了,就必须走流程。
98
+ const strictest = (key) => (valid.some((row) => String(row[key] ?? '') === '1') ? '1' : String(base[key] ?? ''));
94
99
  return {
95
100
  roomChk: String(base.room_conflict_chk ?? ''),
96
101
  roomAction: String(base.room_conflict ?? ''),
97
102
  memberChk: String(base.member_conflict_chk ?? ''),
98
103
  memberAction: String(base.member_conflict ?? ''),
99
- onlyFlowCreate: String(base.only_flow_create ?? ''),
100
- openMtApprove: String(base.open_mt_approve ?? ''),
104
+ onlyFlowCreate: strictest('only_flow_create'),
105
+ openMtApprove: strictest('open_mt_approve'),
101
106
  mtNewWf: String(base.mt_new_wf ?? ''),
102
107
  mtRepeatNewWf: String(base.mt_repeat_new_wf ?? ''),
103
108
  settingsRows: valid.length,
@@ -146,7 +151,14 @@ export function assertEbCreatePathAllowed(settings) {
146
151
  }
147
152
  if (blocked.length === 0)
148
153
  return;
149
- throw meetingConflictError('conflict_rejected', `应用设置不允许直接建会:${blocked.join(';')}。该应用的会议必须经流程创建`
154
+ // 多条设置时,warnings 会随抛错一起丢掉 ⇒ 必须把「存在多份设置、按最严判定」写进错误本身,
155
+ // 否则排查的人只会看到"不许建会",不知道库里有两份冲突的设置要清理。
156
+ const multiRow = (settings.settingsRows ?? 0) > 1
157
+ ? `(注意:会议应用设置存在 ${settings.settingsRows} 条有效记录,管控开关按**最严**取值 —— 任意一份开了就走流程;`
158
+ + `冲突策略等其余字段仍按最新一条 id=${settings.settingsId ?? '未知'}、更新于 ${settings.settingsUpdatedAt || '未知'} 判断。`
159
+ + '建议让管理员在系统页面清理重复的设置记录。)'
160
+ : '';
161
+ throw meetingConflictError('conflict_rejected', `应用设置不允许直接建会:${blocked.join(';')}。${multiRow}该应用的会议必须经流程创建`
150
162
  + `(新建会议工作流 mt_new_wf=${settings.mtNewWf || '未配置'},周期会议工作流 mt_repeat_new_wf=${settings.mtRepeatNewWf || '未配置'})——`
151
163
  + 'CLI 不直接建会、也不代替流程建会(否则会产生流程之外、request_id 为空的会议,且与流程/ESB 自动建会重复)。'
152
164
  + '这属于应用管控,CLI 不会绕过 —— 但也不用停手:改走 **meeting.eb.meeting.create.flow**'
@@ -156,6 +168,9 @@ export function assertEbCreatePathAllowed(settings) {
156
168
  openMtApprove: settings.openMtApprove,
157
169
  mtNewWf: settings.mtNewWf,
158
170
  mtRepeatNewWf: settings.mtRepeatNewWf,
171
+ settingsRows: settings.settingsRows ?? 0,
172
+ settingsId: settings.settingsId ?? '',
173
+ settingsUpdatedAt: settings.settingsUpdatedAt ?? '',
159
174
  });
160
175
  }
161
176
  /** 会议室冲突:查占用视图(query_roomusage_list),时间重叠即冲突。 */
@@ -354,7 +369,42 @@ export const ebMeetingWritePreflight = async ({ endpoint, payload, args, env, ex
354
369
  const begin = typeof main.begin_datetime === 'string' ? main.begin_datetime : '';
355
370
  const end = typeof main.end_datetime === 'string' ? main.end_datetime : '';
356
371
  const address = typeof main.address === 'string' && main.address ? main.address : undefined;
357
- const hrmMembers = typeof main.hrm_members === 'string' && main.hrm_members ? main.hrm_members : undefined;
372
+ // ⛔ 参会人必须是**员工 id**,不能是姓名(2026-10-29 真机事故):
373
+ // 传姓名时会议主表能写进去(hrm_members 是文本渲染字段),但
374
+ // ① 回执/签到按 id 建 → 建不起来,参会人收不到邀请;
375
+ // ② 人员冲突检测按 id 比对 → 一个都查不到,等于冲突检测被静默跳过。
376
+ // 所以在这里把姓名解析成 id 再往下走,解析不出来的明确列出来,不静默丢。
377
+ const memberWarnings = [];
378
+ const rawMembers = typeof main.hrm_members === 'string' ? main.hrm_members.trim() : '';
379
+ if (rawMembers) {
380
+ const items = rawMembers.split(/[,;,;]+/u).map((item) => item.trim()).filter((item) => item !== '');
381
+ const nameLike = items.filter((item) => !/^\d+$/u.test(item));
382
+ if (nameLike.length > 0) {
383
+ const { resolveMemberIdsByName } = await import('./schedule.js');
384
+ const idsByName = await resolveMemberIdsByName(env, nameLike);
385
+ const resolved = items.map((item) => idsByName.get(item) ?? item);
386
+ payload.mainTable.hrm_members = [...new Set(resolved)].join(',');
387
+ const unresolved = nameLike.filter((name) => !idsByName.has(name));
388
+ if (unresolved.length > 0) {
389
+ memberWarnings.push(`参会人中有 ${unresolved.length} 位未能解析为员工 id:${unresolved.join('、')}`
390
+ + ' —— 已按原名写入。**这几位的回执与签到建不起来,且不会参与人员冲突检测**,请核对姓名或改用员工 id。');
391
+ }
392
+ const resolvedNames = nameLike.filter((name) => idsByName.has(name));
393
+ if (resolvedNames.length > 0) {
394
+ memberWarnings.push(`参会人已由姓名解析为员工 id:${resolvedNames.join('、')}`);
395
+ }
396
+ }
397
+ }
398
+ // 冲突检测只认数字 id(`toIdCsv` 会硬校验):解析不出 id 的那几位仍然留在主表里(不静默丢人),
399
+ // 但不参与人员冲突检测 —— 否则一个姓名就会让整场建会直接 id_invalid 失败。
400
+ const memberItems = String(main.hrm_members ?? '').split(/[,;,;]+/u).map((item) => item.trim()).filter((item) => item !== '');
401
+ const numericMembers = memberItems.filter((item) => /^\d+$/u.test(item));
402
+ if (memberItems.length > numericMembers.length) {
403
+ const skipped = memberItems.filter((item) => !/^\d+$/u.test(item));
404
+ memberWarnings.push(`参会人 ${skipped.join('、')} 不是员工 id,**不参与本次人员冲突检测**`
405
+ + '(这几位的回执与签到也建不起来)——请用员工 id 或可唯一匹配的姓名重新提交。');
406
+ }
407
+ const hrmMembers = numericMembers.length > 0 ? numericMembers.join(',') : undefined;
358
408
  // 建会必读应用设置(闸门);变更只在确有冲突检测目标时才读(避免多一次请求)。
359
409
  const settings = isCreate || address || hrmMembers ? await fetchEbAppSettings(env, operation) : undefined;
360
410
  const preflightWarnings = [];
@@ -367,10 +417,11 @@ export const ebMeetingWritePreflight = async ({ endpoint, payload, args, env, ex
367
417
  + '如需以另一条为准,请先在系统页面清理重复的设置记录。');
368
418
  }
369
419
  }
420
+ const head = [...preflightWarnings, ...memberWarnings];
370
421
  if (!begin || !end)
371
- return preflightWarnings;
422
+ return head;
372
423
  if (!address && !hrmMembers)
373
- return preflightWarnings;
424
+ return head;
374
425
  const outcome = await runEbConflictCheck({
375
426
  beginDatetime: begin,
376
427
  endDatetime: end,
@@ -424,8 +475,28 @@ export const ebMeetingWritePreflight = async ({ endpoint, payload, args, env, ex
424
475
  extraWarnings.push(`共同空闲时段计算失败(${String(error?.message ?? error).slice(0, 80)}),请手动换时间`);
425
476
  }
426
477
  }
478
+ // ⛔ 冲突必须**拦下来让用户拍板**,不能只在 warnings 里提一句就放行
479
+ // (2026-10-29 真机事故:CLI 返回 ok + continuation,智能体直接提交了,
480
+ // 用户事后才发现和已有会议撞了 2 小时 —— 冲突提示等于没有)。
481
+ if (args.conflictAck !== true) {
482
+ const detail = outcome.member.conflicts
483
+ .map((item) => `${item.conflictedMembers.join('、')} 在 ${item.begin}~${item.end} 已有「${item.name}」`)
484
+ .join(';');
485
+ const slots = extras.slotOptions ?? [];
486
+ const slotText = slots.length > 0
487
+ ? `当天所有人都有空的时段:${slots.map((item) => `[${item.key}] ${item.label}`).join(';')}`
488
+ : (extras.dayBlocked ? String(extras.dayBlocked.summary ?? '当天已排满') : '当天没有全员空闲时段');
489
+ throw meetingConflictError('conflict_rejected', `参会人时间冲突,**未写入任何数据**:${detail}。${slotText}。`
490
+ + '请用**选项方式**让用户确认:改到上面的候选时段,或明确接受冲突'
491
+ + '(确认后带 conflictAck=true 重新提交,届时才会真正写入)。', {
492
+ conflicts: outcome.member.conflicts,
493
+ slotOptions: extras.slotOptions ?? [],
494
+ suggestedSlots: extras.suggestedSlots ?? [],
495
+ dayBlocked: extras.dayBlocked ?? null,
496
+ });
497
+ }
427
498
  }
428
- return [...preflightWarnings, ...outcome.warnings, ...extraWarnings];
499
+ return [...preflightWarnings, ...memberWarnings, ...outcome.warnings, ...extraWarnings];
429
500
  };
430
501
  /** 只查不建:会议室冲突检测 operation。 */
431
502
  const ebConflictRoom = {
@@ -168,7 +168,7 @@ async function shapeBrowserValues(env, values) {
168
168
  return { values: shaped, notes };
169
169
  }
170
170
  /** 按别名把会议字段映射到表单字段 id。 */
171
- function mapFields(fields, values) {
171
+ export function mapFields(fields, values) {
172
172
  const byName = new Map(fields.map((field) => [field.name.trim(), field]));
173
173
  const main = {};
174
174
  const mapped = {};
@@ -176,7 +176,9 @@ function mapFields(fields, values) {
176
176
  for (const [key, raw] of Object.entries(values)) {
177
177
  if (raw === undefined || raw === null || raw === '')
178
178
  continue;
179
- const aliases = FIELD_ALIASES[key] ?? [];
179
+ // 2026-10-28:租户自建字段不在别名表里 —— 此时按调用方给的**字段名原文**精确匹配,
180
+ // 这样 extraFields 能填流程表单上的任何字段(参会客户/联系方式之类),而不是只能填内置的十来个。
181
+ const aliases = FIELD_ALIASES[key] ?? [key];
180
182
  const hit = aliases.map((name) => byName.get(name)).find((field) => field !== undefined);
181
183
  if (!hit) {
182
184
  unmapped.push(key);
@@ -187,11 +189,43 @@ function mapFields(fields, values) {
187
189
  }
188
190
  return { main, mapped, unmapped };
189
191
  }
190
- /** 解析要用的建会流程 id:入参优先,其次取应用设置里的 mt_new_wf(逗号分隔取第一个)。 */
191
- async function resolveWorkflowId(env, args) {
192
- const explicit = String(args.workflowId ?? '').trim();
193
- if (explicit)
194
- return { workflowId: explicit, candidates: [explicit] };
192
+ /**
193
+ * 把 `workflowId` 入参规整成 id 列表。
194
+ *
195
+ * ⛔ 2026-10-29 回归修复:此前直接 `String(args.workflowId)` —— 传数组会被拼成 `"a,b"`,
196
+ * 当成**单个** id 塞给下游 `workflow.createForm`,而 workflow 模块对多值只取第一个能载入的、
197
+ * 并回 `selectionSource: workflowId`,表面像"传了两个",实际是 CLI 替用户指定了第一个。
198
+ * 现在单值 / 数组 / 逗号分隔 / 「名称(id)」写法统一解析成列表,多个就交给选择流程。
199
+ */
200
+ export function normalizeWorkflowIds(raw) {
201
+ if (raw === undefined || raw === null)
202
+ return [];
203
+ const list = Array.isArray(raw) ? raw : [raw];
204
+ const ids = [];
205
+ for (const item of list) {
206
+ for (const piece of String(item ?? '').split(/[,;,;\s]+/)) {
207
+ const term = piece.trim();
208
+ if (!term)
209
+ continue;
210
+ const matched = term.match(/[((](\d{6,})[))]$/);
211
+ const id = matched ? matched[1] : term;
212
+ if (!ids.includes(id))
213
+ ids.push(id);
214
+ }
215
+ }
216
+ return ids;
217
+ }
218
+ /** 解析要用的建会流程 id:入参优先(可多个),其次取应用设置里的 mt_new_wf。 */
219
+ export async function resolveWorkflowId(env, args) {
220
+ const explicitList = normalizeWorkflowIds(args.workflowId);
221
+ if (explicitList.length > 0) {
222
+ // 显式传多个同样**不替用户拍板**:与「应用设置配了多个」走同一套探测 + 选择逻辑。
223
+ return {
224
+ workflowId: explicitList.length === 1 ? explicitList[0] : '',
225
+ candidates: explicitList,
226
+ source: 'argument',
227
+ };
228
+ }
195
229
  const settings = await fetchEbAppSettings(env, 'meeting.eb.meeting.create.flow.prepare');
196
230
  const candidates = String(settings.mtNewWf ?? '')
197
231
  .split(/[,;,;\s]+/)
@@ -201,7 +235,7 @@ async function resolveWorkflowId(env, args) {
201
235
  throw meetingValidationError('flow_not_configured', '应用设置里没有配置「新建会议工作流」(mt_new_wf 为空),无法走流程建会;请管理员在会议应用设置里配置,或关闭「只允许通过流程新建会议」');
202
236
  }
203
237
  // 多个流程时先不拍板:由调用方探测"当前人员对每个流程有没有发起权限",再决定是否让用户选。
204
- return { workflowId: candidates.length === 1 ? candidates[0] : '', candidates };
238
+ return { workflowId: candidates.length === 1 ? candidates[0] : '', candidates, source: 'settings' };
205
239
  }
206
240
  const ebMeetingCreateFlowPrepare = {
207
241
  name: 'meeting.eb.meeting.create.flow.prepare',
@@ -224,19 +258,33 @@ const ebMeetingCreateFlowPrepare = {
224
258
  caller: { type: 'string', description: '召集人' },
225
259
  contacter: { type: 'string', description: '联系人' },
226
260
  other_remark: { type: 'string', description: '备注' },
227
- workflowId: { type: 'string', description: '指定建会流程 id(默认取应用设置 mt_new_wf 的第一个)' },
261
+ workflowId: {
262
+ oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
263
+ description: '指定建会流程 id:单个 id、id 数组或逗号分隔都可以,也接受「流程名(123456)」写法。'
264
+ + '**传多个时 CLI 同样不会替你选**——会逐个探测可发起性,多个可用则返回 NEEDS_SELECTION 让用户挑;'
265
+ + '不传则用应用设置 mt_new_wf 配置的流程(配了多个时同样让用户选)',
266
+ },
228
267
  autoRoom: { type: 'boolean', description: '默认 false:未指定会议室时返回候选让用户选;传 true 才自动采用推荐' },
229
268
  allowNoRoom: { type: 'boolean', description: '确实不需要会议室(线上会等)时传 true,避免被“请选择会议室”拦住' },
269
+ extraFields: {
270
+ type: 'object',
271
+ description: '按**流程表单里的字段名**填额外字段,形如 {"参会客户": "xx公司", "联系方式": "138xxxx"}'
272
+ + ':用于租户在流程表单上自建、CLI 内置别名里没有的字段。键必须与该流程表单的字段名完全一致'
273
+ + '(prepare 的 warnings 会列出表单现有全部字段名);匹配不到的会进 unmapped 提示,不会瞎填',
274
+ },
230
275
  }, { required: ['mt_name', 'begin_datetime', 'end_datetime'] }),
231
276
  handler: async (args, env) => {
232
277
  const resolved = await resolveWorkflowId(env, args);
233
278
  const candidates = resolved.candidates;
234
279
  let workflowId = resolved.workflowId;
280
+ // 候选来源决定文案:是应用设置配了多个,还是调用方自己传了多个。
281
+ const sourceLabel = resolved.source === 'argument' ? '调用方传入' : '应用设置配置';
235
282
  const probes = [];
236
283
  const loadedForms = new Map();
237
284
  if (!workflowId && candidates.length > 1) {
238
- // 应用设置里配了多个建会流程:**逐个探测当前人员能否发起**(createForm 能载入 = 有发起权限),
239
- // 不能默认拿第一个就提交 —— 用户可能对第一个没有发起权限(2026-10-28 用户要求)。
285
+ // 有多个建会流程候选(应用设置配了多个,或入参传了多个 id):**逐个探测当前人员能否发起**
286
+ // (createForm 能载入 = 有发起权限),不能默认拿第一个就提交 —— 用户可能对第一个没有发起权限,
287
+ // 也可能各流程表单字段完全不同(2026-10-28 / 2026-10-29 用户两次要求)。
240
288
  // 并行探测:每个流程一次 createForm(内含多次 OA 请求),串行会成倍放大等待
241
289
  const probed = await Promise.all(candidates.map(async (id) => {
242
290
  try {
@@ -270,25 +318,30 @@ const ebMeetingCreateFlowPrepare = {
270
318
  }
271
319
  const launchable = probes.filter((probe) => probe.launchable);
272
320
  if (launchable.length === 0) {
273
- throw meetingValidationError('flow_not_launchable', `应用设置配置了 ${candidates.length} 个建会流程,但当前账号一个都发起不了(无发起权限或流程已停用):`
321
+ throw meetingValidationError('flow_not_launchable', `${sourceLabel}了 ${candidates.length} 个建会流程,但当前账号一个都发起不了(无发起权限或流程已停用):`
274
322
  + probes.map((probe) => `${probe.workflowId}(${probe.reason ?? '不可发起'})`).join(';'), { probes });
275
323
  }
276
324
  if (launchable.length > 1) {
277
325
  // 仍有多个可选 → 交给用户挑,不替用户决定(用户明确要求"以选择的方式")
278
326
  return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
279
327
  status: 'NEEDS_SELECTION',
280
- message: `当前账号可发起 ${launchable.length} 个建会流程,请让用户选择要触发哪一个`,
328
+ message: `${sourceLabel}了 ${candidates.length} 个建会流程,当前账号可发起其中 ${launchable.length} 个,`
329
+ + '请让用户选择要触发哪一个 —— **不要默认取第一个**(各流程表单字段可能完全不同,选错会漏字段;'
330
+ + '2026-10-29 真实事故:取第一个导致字段全填不进另一条流程,用户以为"系统只支持一条流程")',
281
331
  options: launchable.map((probe, index) => ({
282
332
  key: `W${index + 1}`,
283
333
  label: probe.name ? `${probe.name}(${probe.workflowId})` : probe.workflowId,
284
334
  value: { workflowId: probe.workflowId },
285
335
  reason: '当前账号有发起权限',
336
+ // 把该流程的字段名如实带出来:不同流程字段差异很大(有的有设备需求/涉密项目,有的没有),
337
+ // 用户要靠这个才能选对(2026-10-29 用户反馈:选错方式后字段全填不进去)。
338
+ fields: (loadedForms.get(probe.workflowId)?.fields ?? []).map((f) => f.name).filter(Boolean),
286
339
  })),
287
340
  probes,
288
341
  }, {
289
342
  warnings: [
290
- `检测到 ${candidates.length} 个建会流程,其中 ${launchable.length} 个当前账号可发起;`
291
- + '请把这几个流程做成选项让用户挑一个,选定后带 workflowId 重新 prepare。',
343
+ `${sourceLabel}了 ${candidates.length} 个建会流程,其中 ${launchable.length} 个当前账号可发起;`
344
+ + '**必须把这几个流程做成选项让用户挑一个,禁止自己替用户选**;选定后带单个 workflowId 重新 prepare。',
292
345
  ...probes.filter((probe) => !probe.launchable)
293
346
  .map((probe) => `流程 ${probe.workflowId} 当前账号无法发起(${probe.reason ?? '无权限'}),已排除`),
294
347
  ],
@@ -314,13 +367,17 @@ const ebMeetingCreateFlowPrepare = {
314
367
  .map((item) => item.trim())
315
368
  .filter((item) => item !== '');
316
369
  let addressInput = args.address;
370
+ // 记录"用户有没有自己指定会议室":后面决定文案是"按你的指定"还是"自动选择"。
371
+ const hadAddressInput = String(args.address ?? '').trim() !== '';
317
372
  const day = beginText.slice(0, 10);
318
373
  const durationMinutes = beginText && endText
319
374
  ? Math.max(15, Math.round((Date.parse(endText.replace(' ', 'T')) - Date.parse(beginText.replace(' ', 'T'))) / 60000) || 60)
320
375
  : 60;
321
376
  const timings = {};
322
377
  const startedAt = Date.now();
323
- const roomTask = (enrich && !addressInput && beginText && endText)
378
+ // 推荐照常跑:**即使调用方指定了会议室**也要跑 —— 一来说明"你指定的那间是否被占用",
379
+ // 二来冲突时能立刻给出替代候选(2026-10-29 用户明确要求:指定了也要判冲突、冲突了要推荐)。
380
+ const roomTask = (enrich && beginText && endText)
324
381
  ? recommendRooms(env, {
325
382
  beginDatetime: beginText,
326
383
  endDatetime: endText,
@@ -328,6 +385,16 @@ const ebMeetingCreateFlowPrepare = {
328
385
  topN: 3,
329
386
  }).catch(() => null)
330
387
  : Promise.resolve(null);
388
+ // ⛔ 指定了会议室时,**单独查它在该时段是否已被占用**(query_roomusage_list)。
389
+ // 以前 `!addressInput` 直接跳过整段 → "指定了一间被占用的房"照样往下走,用户到页面上才发现。
390
+ const addressId = String(addressInput ?? '').trim();
391
+ const addressIsRoomId = /^\d+$/.test(addressId);
392
+ const roomConflictTask = (enrich && beginText && endText && addressIsRoomId)
393
+ ? (async () => {
394
+ const { getMeetingOperation } = await import('../registry.js');
395
+ return getMeetingOperation('meeting.eb.conflict.room').handler({ beginDatetime: beginText, endDatetime: endText, address: addressId }, env);
396
+ })().catch(() => null)
397
+ : Promise.resolve(null);
331
398
  const conflictTask = (enrich && beginText && endText && memberIds.length > 0)
332
399
  ? (async () => {
333
400
  const { getMeetingOperation } = await import('../registry.js');
@@ -345,16 +412,49 @@ const ebMeetingCreateFlowPrepare = {
345
412
  includeSchedule: true,
346
413
  }).catch(() => null)
347
414
  : Promise.resolve(null);
348
- const [roomRec, memberConflict, free] = await Promise.all([roomTask, conflictTask, availabilityTask]);
415
+ const [roomRec, memberConflict, free, roomConflict] = await Promise.all([
416
+ roomTask, conflictTask, availabilityTask, roomConflictTask,
417
+ ]);
349
418
  timings.parallelMs = Date.now() - startedAt;
419
+ // 会议室候选(指定/未指定都用这一份):冲突时直接拿它当替代方案。
420
+ const roomOptions = (roomRec?.candidates ?? []).map((candidate, index) => ({
421
+ key: `R${index + 1}`,
422
+ label: `${candidate.name}${candidate.capacity.max > 0 ? `(可容 ${candidate.capacity.max} 人)` : ''}`
423
+ + `${index === 0 ? ' · 推荐' : ''}`,
424
+ value: { id: candidate.id, name: candidate.name },
425
+ reason: candidate.reasons.join(';'),
426
+ }));
427
+ // ① 指定的会议室该时段已被占用 → 不替用户改,但必须拦下来并给替代候选。
428
+ const roomConflictData = roomConflict?.data;
429
+ if (addressInput && roomConflictData?.hasConflict === true) {
430
+ const busy = Array.isArray(roomConflictData.conflicts) ? roomConflictData.conflicts : [];
431
+ decisions.roomConflict = {
432
+ address: addressId,
433
+ hasConflict: true,
434
+ conflicts: busy,
435
+ verdict: roomConflictData.verdict ?? 'conflict',
436
+ };
437
+ if (roomOptions.length > 0)
438
+ decisions.roomOptions = roomOptions;
439
+ return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
440
+ status: 'NEEDS_SELECTION',
441
+ selectionType: 'room_conflict',
442
+ message: `你指定的会议室在 ${beginText}~${endText} 已被占用(${busy.length} 条占用记录),`
443
+ + '本次**没有提交**。请改用下面空闲的会议室,或调整会议时间',
444
+ options: roomOptions,
445
+ recommended: roomOptions[0]?.value ?? null,
446
+ conflicts: busy,
447
+ hint: '选定后把 value.id 作为 address 重新调用;要改时间就同时传新的 begin_datetime/end_datetime',
448
+ }, {
449
+ warnings: [
450
+ `会议室 ${addressId} 在该时段已有 ${busy.length} 条占用,已拦下未提交。`,
451
+ roomOptions.length > 0
452
+ ? `已按人数/常用/空闲给出 ${roomOptions.length} 个替代会议室(R1 为首选)。`
453
+ : '该时段没有可替代的空闲会议室,建议调整会议时间或改线上会议(allowNoRoom: true)。',
454
+ ],
455
+ });
456
+ }
350
457
  if (roomRec) {
351
- const roomOptions = roomRec.candidates.map((candidate, index) => ({
352
- key: `R${index + 1}`,
353
- label: `${candidate.name}${candidate.capacity.max > 0 ? `(可容 ${candidate.capacity.max} 人)` : ''}`
354
- + `${index === 0 ? ' · 推荐' : ''}`,
355
- value: { id: candidate.id, name: candidate.name },
356
- reason: candidate.reasons.join(';'),
357
- }));
358
458
  decisions.roomOptions = roomOptions;
359
459
  decisions.autoRoom = {
360
460
  picked: roomRec.best
@@ -388,6 +488,34 @@ const ebMeetingCreateFlowPrepare = {
388
488
  addressInput = roomRec.best.id;
389
489
  }
390
490
  }
491
+ // ② 没指定会议室、也没允许无房间,且一个候选都给不出来 → **必须拦住**。
492
+ // 否则表单里「会议地点」留空、流程照样提交,用户拿到的是"没有会议室"的会(2026-10-29 用户要求:
493
+ // 没填会议室就得先按规则推荐空闲的,推荐不出来也要说清楚,不能静默留空)。
494
+ if (enrich && !addressInput && args.allowNoRoom !== true) {
495
+ decisions.roomUnavailable = {
496
+ reason: roomRec
497
+ ? `该时段没有满足条件的空闲会议室(共 ${roomRec.meta?.roomsTotal ?? 0} 间,可用 ${roomRec.meta?.roomsUsable ?? 0} 间,被占用 ${roomRec.meta?.roomsBusy ?? 0} 间)`
498
+ : '会议室推荐没有返回结果(会议室列表接口读不到,或推荐过程出错)',
499
+ meta: roomRec?.meta ?? null,
500
+ };
501
+ return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
502
+ status: 'NEEDS_SELECTION',
503
+ selectionType: 'room_unavailable',
504
+ message: '未指定会议室,且按人数/时段没能推荐出可用的空闲会议室。'
505
+ + '请让用户从下面三条里选一条,**不要**直接提交一个没有地点的会议',
506
+ options: [
507
+ { key: 'C1', label: '我指定具体会议室(表单里点选,或告诉我会议室名称/id)', value: { action: 'specify' } },
508
+ { key: 'C2', label: '改用线上会议(腾讯会议/视频)', value: { action: 'no_room' } },
509
+ { key: 'C3', label: '先按"地点待定"提交', value: { action: 'allow_no_room' } },
510
+ ],
511
+ roomUnavailable: decisions.roomUnavailable,
512
+ }, {
513
+ warnings: [
514
+ `会议室推荐无结果:${String(decisions.roomUnavailable.reason)}。本次**没有提交**。`,
515
+ '确实不需要会议室(线上会)就带 allowNoRoom: true 重新调用;要指定房间就带 address。',
516
+ ],
517
+ });
518
+ }
391
519
  const conflicts = (memberConflict?.data?.conflicts ?? []);
392
520
  if (conflicts.length > 0)
393
521
  decisions.memberConflicts = conflicts;
@@ -421,13 +549,20 @@ const ebMeetingCreateFlowPrepare = {
421
549
  contacter: args.contacter,
422
550
  other_remark: args.other_remark,
423
551
  mt_type: args.mt_type,
552
+ ...(args.extraFields && typeof args.extraFields === 'object' && !Array.isArray(args.extraFields)
553
+ ? args.extraFields
554
+ : {}),
424
555
  });
425
556
  const { main, mapped, unmapped } = mapFields(form.fields, shaped.values);
426
557
  const warnings = [];
427
558
  for (const note of shaped.notes)
428
559
  warnings.push(note);
429
560
  const picked = decisions.autoRoom?.picked;
430
- if (picked) {
561
+ // ⚠️ 只在"用户没指定会议室、由 CLI 自动选"时才说这句。
562
+ // 以前无条件输出"未指定会议室 → 已自动选择",用户明明传了 address 也被这么讲(2026-10-29 真机踩到);
563
+ // 而 picked 是**推荐排序第一**,未必等于用户指定的那间,说错会直接误导。
564
+ // 指定会议室时不再另加提示 —— `shaped.notes` 里已有"会议地点按浏览按钮格式提交:<名称>(<id>)"。
565
+ if (picked && !hadAddressInput) {
431
566
  warnings.push(`未指定会议室 → 已自动选择「${picked.name}」(${(picked.reasons ?? []).join(';')});`
432
567
  + `另有候选 ${(decisions.roomOptions ?? []).slice(1).map((item) => item.label).join(' / ') || '无'}`);
433
568
  }
@@ -653,12 +788,19 @@ const ebMeetingCreateFlowQuick = {
653
788
  caller: { type: 'string', description: '召集人' },
654
789
  contacter: { type: 'string', description: '联系人' },
655
790
  other_remark: { type: 'string', description: '备注' },
656
- workflowId: { type: 'string', description: '指定建会流程 id(多流程时用来跳过选择)' },
791
+ workflowId: {
792
+ oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
793
+ description: '指定建会流程 id(单个 / 数组 / 逗号分隔)。传多个时不会替你选:会返回 NEEDS_SELECTION 让用户挑',
794
+ },
657
795
  autoRoom: { type: 'boolean', description: '默认 false:不指定会议室时返回候选让用户选;传 true 才自动采用推荐会议室' },
658
796
  allowNoRoom: { type: 'boolean', description: '确实不需要会议室(线上会等)时传 true' },
659
797
  confirm: { type: 'boolean', description: '必须显式 true 才会真正提交' },
660
798
  waitSeconds: { type: 'number', description: '提交后等待会议生成的秒数(默认 20;算法是立即查一次 + 5s 再查一次,仍未生成就返回 pending)' },
661
799
  enrich: { type: 'boolean', description: '是否顺带做会议室推荐/冲突检测/空闲时段(默认 true;人、时间、房间都已确定时传 false 可显著加快)' },
800
+ extraFields: {
801
+ type: 'object',
802
+ description: '按流程表单字段名填额外字段(租户自建字段用这个口子带进去),形如 {"参会客户": "xx公司"}',
803
+ },
662
804
  }, { required: ['mt_name', 'begin_datetime', 'end_datetime', 'confirm'] }),
663
805
  handler: async (args, env) => {
664
806
  if (args.confirm !== true) {