weaver-work-cli 0.1.32 → 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 (185) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/{archive-UMGX72K6.js → archive-FVFY6GZN.js} +2 -2
  3. package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-NW3OD4UV.js} +2 -2
  4. package/dist/chunks/{asset-GLEGKFG5.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-E44OTVDA.js → blog-I7SDPNW4.js} +2 -2
  7. package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-O37ZIQNY.js} +2 -2
  8. package/dist/chunks/{calendar-JKPIRKFD.js → calendar-7KXE4ED3.js} +2 -2
  9. package/dist/chunks/{chunk-4YC43QQ4.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-DW6AUUKS.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-2GA5ZIQL.js → chunk-D223PTXU.js} +1 -0
  16. package/dist/chunks/{chunk-H7J4DC5R.js → chunk-EEWPQE3O.js} +2 -2
  17. package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-GFPXQIY2.js} +2 -2
  18. package/dist/chunks/{chunk-C6R3B2ML.js → chunk-HEL5XTYI.js} +12 -6
  19. package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-IHUYP6LO.js} +1 -1
  20. package/dist/chunks/{chunk-YRVDW4WN.js → chunk-ILMBNVSG.js} +2 -2
  21. package/dist/chunks/{chunk-G3QJULO2.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-WKI2FBIQ.js → chunk-OCU4RLHC.js} +641 -309
  26. package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-PRHJGPMA.js} +2 -2
  27. package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-Q6LWB5HX.js} +2 -2
  28. package/dist/chunks/{chunk-IURUFGZK.js → chunk-RBGOMK4Y.js} +50 -33
  29. package/dist/chunks/chunk-VBCFFFGT.js +226 -0
  30. package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-WRRJ4GG2.js} +22 -1148
  31. package/dist/chunks/{chunk-E4Z23GHM.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-HMFY6VIL.js → ehr-WFOC3B4D.js} +1174 -20
  37. package/dist/chunks/{esb-ZYSVB7SG.js → esb-LJZTN663.js} +4 -4
  38. package/dist/chunks/{fna-B7RKPAZD.js → fna-F7QRIIZC.js} +2 -2
  39. package/dist/chunks/{host-45WED2KK.js → host-BCGUVEWX.js} +3 -3
  40. package/dist/chunks/host-BQBRHVR3.js +23 -0
  41. package/dist/chunks/{host-I4Z6BFYX.js → host-DGE2TZD4.js} +3 -3
  42. package/dist/chunks/{host-KNZ52LJZ.js → host-KXPTEWFS.js} +3 -3
  43. package/dist/chunks/{host-OCH2BCCV.js → host-VQ6JJXGQ.js} +3 -3
  44. package/dist/chunks/{hrm-2KAZGUHI.js → hrm-NI6XAKW5.js} +3 -3
  45. package/dist/chunks/{im-ACJJWH4U.js → im-7TRTF3KT.js} +88 -12
  46. package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-AOXWJKYT.js} +3 -3
  47. package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-7NQWEZAW.js} +2 -2
  48. package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-RNS4EE4S.js} +4 -4
  49. package/dist/chunks/{mail-S2HDDXO2.js → mail-VDHJAB4O.js} +3 -3
  50. package/dist/chunks/{meeting-QVN3I37L.js → meeting-GA7XCNNP.js} +214 -43
  51. package/dist/chunks/{okr-QNG4VC2Q.js → okr-TQIRERPQ.js} +2 -2
  52. package/dist/chunks/operations-5QL3AECO.js +11 -0
  53. package/dist/chunks/{plan-A25EMW6U.js → plan-4RY4LN2P.js} +2 -2
  54. package/dist/chunks/{project-G6XV66DZ.js → project-D6XT2OC5.js} +2 -2
  55. package/dist/chunks/{registry-DC6KR3NH.js → registry-JHRIDQ3A.js} +4 -4
  56. package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
  57. package/dist/chunks/{registry-TI3CIUXF.js → registry-QKMI43Q6.js} +8 -6
  58. package/dist/chunks/{registry-7C5UMIPL.js → registry-VOGKFFTR.js} +2 -2
  59. package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-D4EUOOWY.js} +4 -3
  60. package/dist/chunks/{schedule-KFIITGSK.js → schedule-XPFBHPCV.js} +4 -3
  61. package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-HLBZRWIY.js} +172 -6
  62. package/dist/index.js +30 -25
  63. package/dist/internal/e10/auth-recovery.js +14 -4
  64. package/dist/shortcuts/contract/host.js +34 -59
  65. package/dist/shortcuts/cowork/continuation.js +70 -0
  66. package/dist/shortcuts/cowork/errors.js +59 -0
  67. package/dist/shortcuts/cowork/host.js +172 -0
  68. package/dist/shortcuts/cowork/index.js +166 -0
  69. package/dist/shortcuts/cowork/manifest.js +65 -0
  70. package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
  71. package/dist/shortcuts/cowork/operations/item-write.js +380 -0
  72. package/dist/shortcuts/cowork/operations/member-write.js +283 -0
  73. package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
  74. package/dist/shortcuts/cowork/operations/read.js +423 -0
  75. package/dist/shortcuts/cowork/operations/registry.js +39 -0
  76. package/dist/shortcuts/cowork/operations/review-write.js +157 -0
  77. package/dist/shortcuts/cowork/operations/shared.js +215 -0
  78. package/dist/shortcuts/cowork/operations/types.js +24 -0
  79. package/dist/shortcuts/cowork/operations.js +19 -0
  80. package/dist/shortcuts/ehr/host.js +4 -1
  81. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
  82. package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
  83. package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
  84. package/dist/shortcuts/ehr/operations/recruit-write.js +8 -16
  85. package/dist/shortcuts/ehr/operations/recruit.js +35 -10
  86. package/dist/shortcuts/ehr/operations/registry.js +6 -0
  87. package/dist/shortcuts/im/diagnostics.js +97 -0
  88. package/dist/shortcuts/im/host.js +58 -31
  89. package/dist/shortcuts/im/index.js +7 -2
  90. package/dist/shortcuts/im/operations/at-mention.js +127 -0
  91. package/dist/shortcuts/im/operations/file-user.js +10 -2
  92. package/dist/shortcuts/im/operations/group-read.js +26 -5
  93. package/dist/shortcuts/im/operations/group-write.js +35 -28
  94. package/dist/shortcuts/im/operations/msg-sync.js +7 -2
  95. package/dist/shortcuts/im/operations/msg-write.js +76 -10
  96. package/dist/shortcuts/im/operations/quote-mention.js +119 -0
  97. package/dist/shortcuts/im/operations/session.js +1 -1
  98. package/dist/shortcuts/im/operations/shared.js +10 -0
  99. package/dist/shortcuts/im/operations/types.js +13 -0
  100. package/dist/shortcuts/index.js +5 -0
  101. package/dist/shortcuts/mail/host.js +4 -1
  102. package/dist/shortcuts/meeting/host.js +27 -34
  103. package/dist/shortcuts/meeting/operations/create.js +4 -1
  104. package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -0
  105. package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -7
  106. package/dist/shortcuts/meeting/operations/eb/create-flow.js +168 -26
  107. package/dist/shortcuts/meeting/operations/eb/factory.js +69 -1
  108. package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
  109. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +14 -3
  110. package/dist/shortcuts/meeting/operations/eb-fallback.js +159 -0
  111. package/dist/shortcuts/meeting/operations/env.js +93 -46
  112. package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
  113. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  114. package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
  115. package/dist/shortcuts/meeting/operations.js +16 -0
  116. package/dist/shortcuts/workflow/index.js +110 -1
  117. package/dist/shortcuts/workflow/operations/form.js +24 -3
  118. package/dist/shortcuts/workflow/operations/read.js +104 -23
  119. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
  120. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
  121. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
  122. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
  123. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  124. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
  125. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
  126. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
  127. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
  129. package/docs/e10-auth.md +1 -1
  130. package/docs/im.md +8 -1
  131. package/docs/todo-smart-convert/README.md +1 -1
  132. package/docs/todo-smart-convert/data-contract.md +2 -2
  133. package/docs/todo-smart-convert/subgroup-rules.md +6 -0
  134. package/package.json +1 -1
  135. package/skill-template/domains/yimiaoban.md +3 -3
  136. package/skill-template/skill-template.md +1 -1
  137. package/skills/weaver-e10-blog/SKILL.md +5 -5
  138. package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
  139. package/skills/weaver-e10-calendar/SKILL.md +5 -5
  140. package/skills/weaver-e10-esb/SKILL.md +5 -5
  141. package/skills/weaver-e10-hrm/SKILL.md +5 -5
  142. package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
  143. package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
  144. package/skills/weaver-e10-jucailin/SKILL.md +18 -12
  145. package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
  146. package/skills/weaver-e10-mail/SKILL.md +5 -5
  147. package/skills/weaver-e10-meeting/SKILL.md +171 -25
  148. package/skills/weaver-e10-meeting/references/meeting-eb.md +4 -3
  149. package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
  150. package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
  151. package/skills/weaver-e10-meeting.zip +0 -0
  152. package/skills/weaver-e10-okr/SKILL.md +5 -5
  153. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  154. package/skills/weaver-e10-plan/SKILL.md +5 -5
  155. package/skills/weaver-e10-shared/SKILL.md +12 -12
  156. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
  157. package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
  158. package/skills/weaver-e10-workflow/SKILL.md +29 -5
  159. package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
  160. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
  161. package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
  162. package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
  163. package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
  164. package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
  165. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
  166. package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
  167. package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
  168. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
  169. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
  170. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
  171. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
  172. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
  173. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
  174. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
  175. package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
  176. package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
  177. package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
  178. package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
  179. package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
  180. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
  181. package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
  182. package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
  183. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
  184. package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
  185. package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
@@ -270,6 +270,9 @@ function buildReadOperation(endpoint) {
270
270
  // 所以这里只提示翻页/加大 pageSize,不要写成"服务端限制"。
271
271
  const effectiveTotal = Math.max(resolvedTotal, rows.length);
272
272
  const pageWarnings = [];
273
+ if (effectiveTotal === 0 && endpoint.emptyHint) {
274
+ pageWarnings.push(endpoint.emptyHint);
275
+ }
273
276
  if (effectiveTotal > rows.length) {
274
277
  const missing = effectiveTotal - rows.length;
275
278
  pageWarnings.push(`本页返回 ${rows.length} 条,共 ${effectiveTotal} 条:还有 ${missing} 条未取到。`
@@ -426,6 +429,26 @@ export function normalizeEbWrite(endpoint, args, userId) {
426
429
  if (hours !== undefined)
427
430
  mainTable.mt_hours = hours;
428
431
  }
432
+ // 自定义字段透传(2026-10-28):租户在表单上新增的字段不在内置白名单内,
433
+ // 调用方用 extraFields 按**真实字段名**(config.dataKey,或字段 id)传入,原样并入主表。
434
+ // ⚠️ 这里不做字段名校验 —— 校验了就等于又造一份白名单,而租户加了什么字段只有他的环境知道。
435
+ // 但必须如实回报"哪些进去了、哪些被内置字段挤掉了",避免调用方以为写上了。
436
+ let extraApplied = [];
437
+ let extraSkipped = [];
438
+ const extra = args.extraFields;
439
+ if (extra && typeof extra === 'object' && !Array.isArray(extra)) {
440
+ for (const [rawKey, value] of Object.entries(extra)) {
441
+ const key = String(rawKey).trim();
442
+ if (!key)
443
+ continue;
444
+ if (mainTable[key] !== undefined) {
445
+ extraSkipped.push(key);
446
+ continue;
447
+ }
448
+ mainTable[key] = value;
449
+ extraApplied.push(key);
450
+ }
451
+ }
429
452
  const required = endpoint.kind === 'delete'
430
453
  ? (endpoint.batchDeleteFields ?? ['id'])
431
454
  : endpoint.requiredFields ?? [];
@@ -472,7 +495,14 @@ export function normalizeEbWrite(endpoint, args, userId) {
472
495
  operateType[node] = 'cover';
473
496
  }
474
497
  }
475
- return { mainTable, details, operateType };
498
+ return {
499
+ mainTable,
500
+ details,
501
+ operateType,
502
+ ...(extraApplied.length > 0 || extraSkipped.length > 0
503
+ ? { extraFields: { applied: extraApplied, skipped: extraSkipped } }
504
+ : {}),
505
+ };
476
506
  }
477
507
  function buildWriteInputSchema(endpoint, includeConfirm) {
478
508
  const properties = {};
@@ -480,6 +510,15 @@ function buildWriteInputSchema(endpoint, includeConfirm) {
480
510
  properties.confirm = { const: true };
481
511
  properties.continuation = { type: 'string', minLength: 1 };
482
512
  }
513
+ // 建会/改会:参会人时间冲突时默认阻断,由用户确认后带 conflictAck=true 再提交。
514
+ if (endpoint.interfacePk === 'create_meeting' || endpoint.interfacePk === 'change_meeting') {
515
+ properties.conflictAck = {
516
+ type: 'boolean',
517
+ description: '参会人时间冲突时仍按原时间建会/改期(默认 false)。'
518
+ + '冲突时 CLI 会先拒绝并返回冲突明细与当天可选时段,请**用选项方式让用户确认**;'
519
+ + '用户确认继续后才带 conflictAck=true 重新提交。',
520
+ };
521
+ }
483
522
  if (endpoint.kind === 'delete') {
484
523
  if (endpoint.batchDeleteFields && endpoint.batchDeleteFields.length > 0) {
485
524
  // 批量删除:按业务条件删(如 delete_roomusage 的 mt_id),不是主键
@@ -524,6 +563,15 @@ function buildWriteInputSchema(endpoint, includeConfirm) {
524
563
  },
525
564
  };
526
565
  }
566
+ // 租户自定义字段透传:表单上后加的字段(如参会客户、联系方式)不在内置清单里,
567
+ // 只有这个口子能带进去。字段名必须是表单真实 dataKey 或字段 id —— 先用
568
+ // `meeting.form.fields` 读出来再填,不要凭中文名猜。
569
+ properties.extraFields = {
570
+ type: 'object',
571
+ description: '自定义字段透传,形如 {"字段名": 值}:用于租户在表单上新增、不在本接口内置字段清单里的字段'
572
+ + '(例如参会客户 crm_members、自建的联系方式字段)。字段名必须是表单真实 dataKey 或字段 id'
573
+ + '(用 meeting.form.fields 查);与内置字段同名时以内置字段为准,并在 warnings 里说明',
574
+ };
527
575
  if (endpoint.detailCover) {
528
576
  properties.detail1Mode = {
529
577
  type: 'string',
@@ -597,6 +645,18 @@ function buildWriteOperations(endpoint, preflight, afterWrite) {
597
645
  // EB 版建会缺少强制冲突检测会重复占用会议室/参会人撞会,故 prepare 阶段由钩子补齐。
598
646
  const extras = {};
599
647
  const warnings = preflight ? await preflight({ endpoint, args, payload, env, extras }) : [];
648
+ // 自定义字段如实回报:服务端未配置该字段时会**静默丢弃**(不报错),
649
+ // 不提示的话调用方会以为写上了。
650
+ const extraInfo = payload.extraFields;
651
+ if (extraInfo && extraInfo.applied.length > 0) {
652
+ warnings.push(`已透传 ${extraInfo.applied.length} 个自定义字段:${extraInfo.applied.join('、')}`
653
+ + '——这些不在内置字段清单里,按你给的原样提交;若服务端表单没配这个字段会被静默丢弃,'
654
+ + '提交后请回读确认(可用 meeting.form.fields 核对真实字段名)');
655
+ }
656
+ if (extraInfo && extraInfo.skipped.length > 0) {
657
+ warnings.push(`这些自定义字段与内置字段重名,已忽略你的取值、沿用内置字段:${extraInfo.skipped.join('、')}`
658
+ + '(要传自定义口径请改用不冲突的字段名)');
659
+ }
600
660
  // 支持明细覆盖的接口(如 update_mt_decision)默认是「更新追加」:真机实测同 sort_number 也会**新增一行**。
601
661
  // 调用方最容易在这里理解错,故 prepare 就明确提示,而不是等回读发现多了一行。
602
662
  if (endpoint.detailCover && Object.keys(payload.details).length > 0 && payload.operateType.detail1 !== 'cover') {
@@ -648,6 +708,14 @@ function buildWriteOperations(endpoint, preflight, afterWrite) {
648
708
  // 带上两侧快照:`target_changed` 否则无从判断到底哪个字段变了(排查成本极高)。
649
709
  throw meetingValidationError('target_changed', '提交内容与 prepare 时的快照不一致,请重新执行 prepare', { expected: lease.payload, actual: payload });
650
710
  }
711
+ // ⛔ 建会路径闸门在 apply 侧**重新校验**(2026-10-29 真机事故):
712
+ // 闸门此前只在 prepare 的 preflight 里,apply 只认 continuation 签名 ⇒ 只要 prepare 那次判定
713
+ // 侥幸通过(或多份设置里取到了开关为空的那份),10 分钟有效期内就能直连建出会议。
714
+ // 管控开关可能在 prepare 之后被管理员改过,所以这里不信任快照,重新读一次。
715
+ if (endpoint.interfacePk === 'create_meeting') {
716
+ const { fetchEbAppSettings, assertEbCreatePathAllowed } = await import('./conflict.js');
717
+ assertEbCreatePathAllowed(await fetchEbAppSettings(env, applyName));
718
+ }
651
719
  const pkQuery = writePkQuery(endpoint, payload);
652
720
  let response;
653
721
  try {
@@ -46,8 +46,13 @@ import { meetingEbCreateFlowOperations } from './create-flow.js';
46
46
  const ebAutoRoomPreflight = async (context) => {
47
47
  const { extras } = context;
48
48
  const warnings = [];
49
- const enabled = String(process.env.WEAVER_MEETING_AUTO_ROOM ?? '1') !== '0';
50
49
  const isCreate = context.endpoint.kind === 'create' && context.endpoint.interfacePk === 'create_meeting';
50
+ // 会议室推荐拿不到候选时,必须把原因显式暴露给调用方(2026-10-29 真机事故):
51
+ // 此前只 push 一条 warning,prepare 照常返回 ok,智能体当没发生 → 会议建成「会议室未指定」,
52
+ // 用户看到的就是"根本没给我推荐会议室"。
53
+ const markRoomUnavailable = (reason, meta) => {
54
+ extras.roomUnavailable = { reason, ...(meta ? { meta } : {}) };
55
+ };
51
56
  // 会议室策略(用户口径):**不替用户决定** —— 没填会议室就列候选让用户选。
52
57
  // - WEAVER_MEETING_AUTO_ROOM=1 显式开启"自动选推荐间"(自动化场景)
53
58
  // - WEAVER_MEETING_AUTO_ROOM=0 不推荐也不拦(线上会/无会议室场景)
@@ -98,16 +103,24 @@ const ebAutoRoomPreflight = async (context) => {
98
103
  }
99
104
  else {
100
105
  warnings.push(...result.warnings);
106
+ markRoomUnavailable(`没有可用的会议室候选(共 ${result.meta?.roomsTotal ?? 0} 间,可用 ${result.meta?.roomsUsable ?? 0} 间,`
107
+ + `该时段被占用 ${result.meta?.roomsBusy ?? 0} 间)`, result.meta);
108
+ warnings.push('⚠️ 本次建会**没有会议室**:推荐不出候选(见 roomUnavailable.reason)。'
109
+ + '必须用选项方式让用户确认地点 —— 改指定具体会议室(传 address)、改为线上会议,'
110
+ + '或明确接受「地点待定」。不要默默建成一场没有地点的会。');
101
111
  }
102
112
  }
103
113
  catch (error) {
104
114
  if (error?.subtype === 'room_selection_required')
105
115
  throw error;
106
- warnings.push(`会议室推荐失败(${String(error?.message ?? error).slice(0, 80)}):本次不填会议室继续,请与用户确认地点`);
116
+ const reason = String(error?.message ?? error).slice(0, 200);
117
+ markRoomUnavailable(`会议室推荐失败:${reason}`);
118
+ warnings.push(`⚠️ 会议室推荐失败(${reason}):本次**不填会议室继续**,请与用户确认地点,不要默认「地点待定」。`);
107
119
  }
108
120
  }
109
121
  else {
110
- warnings.push('会议开始/结束时间不完整,跳过会议室推荐');
122
+ markRoomUnavailable('会议开始/结束时间不完整,无法按时段推荐');
123
+ warnings.push('会议开始/结束时间不完整,跳过会议室推荐(本次不会填会议室,请与用户确认地点)');
111
124
  }
112
125
  }
113
126
  const conflictWarnings = await ebMeetingWritePreflight(context);
@@ -266,9 +266,20 @@ export async function recommendRooms(env, input) {
266
266
  return left.name.localeCompare(right.name, 'zh-Hans-CN');
267
267
  });
268
268
  if (candidates.length === 0) {
269
- warnings.push(`没有满足条件的空闲会议室(共 ${roomData.rooms.length} 间,可用 ${usable.length} 间,`
270
- + `该时段被占用 ${busyCount} 间${memberCount ? `,人数 ${memberCount}` : ''})`
271
- + '——请放宽人数/地点/设备要求,或改为线上会议。');
269
+ if (roomData.rooms.length === 0) {
270
+ // 2026-10-29 真机事故:0 间时只说了"放宽条件",agent 转头去调标准版 meeting.room.list,
271
+ // 把标准版那套 74 间会议室外加给了用户。空结果必须把"不是环境问题、更不许换标准版"说清楚。
272
+ warnings.push('EB 会议室表(`uf_mt_room_base`)查不到任何记录(roomsTotal=0)。可能是:① 该环境确实还没建过会议室;'
273
+ + '② 会议应用的「会议室列表」接口/视图未配置(`query_meeting_room_list` 读不到数据,但表里可能有)。'
274
+ + '**绝对不要改用 `meeting.room.list`** —— 那是标准版会议应用的会议室,与 EB 是两套完全不通的数据,'
275
+ + '拿给用户等于凭空捏造房间。请如实告知用户"EB 会议室查不到",并请管理员检查会议室配置,'
276
+ + '或让用户直接提供会议室名称/改用线上会议。');
277
+ }
278
+ else {
279
+ warnings.push(`没有满足条件的空闲会议室(共 ${roomData.rooms.length} 间,可用 ${usable.length} 间,`
280
+ + `该时段被占用 ${busyCount} 间${memberCount ? `,人数 ${memberCount}` : ''})`
281
+ + '——请放宽人数/地点/设备要求,或改为线上会议。');
282
+ }
272
283
  }
273
284
  return {
274
285
  best: candidates[0] ?? null,
@@ -0,0 +1,159 @@
1
+ import { MeetingCliError } from '../errors.js';
2
+ import { degradeMeetingEnvToStandard } from './env.js';
3
+ // ---------------------------------------------------------------------------
4
+ // EB 优先 + 失败降级到标准版
5
+ //
6
+ // 会议有两种实现(EB 版 eBuilder 表单接口 / 标准版 Java REST),此前靠调用方先跑
7
+ // `meeting.env.detect` 决定走哪一套 —— 每次调用前都要两路冒烟,且一次偶发抖动就会把
8
+ // 整个环境误判成另一套。现在改为**惰性降级**:默认按 EB 执行,EB 真走不通时才实探
9
+ // 标准版,成功后把 `standard` 写进 env 缓存(`meeting/env-mode.json`),
10
+ // 后续同一 baseUrl 直接走标准版。
11
+ // ---------------------------------------------------------------------------
12
+ /**
13
+ * EB operation → 标准版对等 operation。
14
+ * - 字符串:可直接改用的标准版 operation;
15
+ * - `null`:标准版**明确没有**对应能力(不要引导用户去试)。
16
+ */
17
+ const EB_TO_STANDARD = {
18
+ 'meeting.eb.meeting.list': 'meeting.list',
19
+ 'meeting.eb.meeting.detail': 'meeting.detail.field',
20
+ 'meeting.eb.meeting.create.prepare': 'meeting.create.prepare',
21
+ 'meeting.eb.meeting.create.apply': 'meeting.create.apply',
22
+ 'meeting.eb.meeting.change.prepare': 'meeting.change.prepare',
23
+ 'meeting.eb.meeting.change.apply': 'meeting.change.apply',
24
+ 'meeting.eb.meeting.delete.prepare': 'meeting.delete.prepare',
25
+ 'meeting.eb.meeting.delete.apply': 'meeting.delete.apply',
26
+ 'meeting.eb.room.list': 'meeting.room.list',
27
+ 'meeting.eb.room.detail': null,
28
+ 'meeting.eb.room.create.prepare': null,
29
+ 'meeting.eb.room.create.apply': null,
30
+ 'meeting.eb.room.update.prepare': null,
31
+ 'meeting.eb.room.update.apply': null,
32
+ 'meeting.eb.room.delete.prepare': null,
33
+ 'meeting.eb.room.delete.apply': null,
34
+ 'meeting.eb.roomUsage.list': 'meeting.room.usage',
35
+ 'meeting.eb.sign.list': 'meeting.sign.list',
36
+ 'meeting.eb.sign.mark.prepare': 'meeting.sign.mark.prepare',
37
+ 'meeting.eb.sign.mark.apply': 'meeting.sign.mark.apply',
38
+ 'meeting.eb.receipt.list': 'meeting.receipt.list',
39
+ 'meeting.eb.receipt.update.prepare': 'meeting.receipt.update.prepare',
40
+ 'meeting.eb.receipt.update.apply': 'meeting.receipt.update.apply',
41
+ 'meeting.eb.type.list': 'meeting.type.list',
42
+ 'meeting.eb.conflict.room': 'meeting.conflict.room',
43
+ 'meeting.eb.conflict.member': 'meeting.conflict.member',
44
+ 'meeting.eb.conflict.availability': null,
45
+ 'meeting.eb.baseSet.get': 'meeting.calendar.base',
46
+ 'meeting.eb.minute.create.prepare': null,
47
+ 'meeting.eb.minute.create.apply': null,
48
+ 'meeting.eb.topic.list': null,
49
+ 'meeting.eb.decision.create.prepare': null,
50
+ 'meeting.eb.decision.create.apply': null,
51
+ 'meeting.eb.decisionTask.list': null,
52
+ 'meeting.eb.schedule.free': null,
53
+ 'meeting.eb.room.recommend': null,
54
+ 'meeting.eb.meeting.create.flow.prepare': null,
55
+ 'meeting.eb.meeting.create.flow.apply': null,
56
+ 'meeting.eb.meeting.create.flow.quick': null,
57
+ };
58
+ /**
59
+ * **不该**触发降级的错误:这些是登录、权限、参数、业务策略或确认链问题,
60
+ * 换成标准版接口也一定重蹈覆辙,降级只会掩盖真正的原因。
61
+ */
62
+ const NO_FALLBACK_TYPES = new Set(['validation', 'confirmation', 'continuation']);
63
+ const NO_FALLBACK_SUBTYPES = new Set([
64
+ 'session_expired',
65
+ 'permission_denied',
66
+ 'not_found',
67
+ 'missing_required_field',
68
+ 'datetime_invalid',
69
+ 'id_invalid',
70
+ 'conflict_rejected',
71
+ 'flow_not_launchable',
72
+ 'target_changed',
73
+ 'env_unavailable',
74
+ 'env_fallback',
75
+ ]);
76
+ function describe(error) {
77
+ if (error instanceof MeetingCliError) {
78
+ return `${error.type}/${error.subtype}: ${error.message}`;
79
+ }
80
+ if (error instanceof Error) {
81
+ return `${error.type ?? 'api'}/${error.subtype ?? 'error'}: ${error.message}`;
82
+ }
83
+ return String(error);
84
+ }
85
+ /**
86
+ * 是不是「EB 接口这座城根本没有」的结构性信号(404/405/501 或服务端明说接口不存在)。
87
+ * 只有这类错误才**持久**降级并写缓存;5xx / 网络抖动这类偶发故障只降级本次,不动缓存,
88
+ * 否则一次抖动就会把整个环境永久标成标准版(2026-10-29 定)。
89
+ */
90
+ function isEbStructurallyAbsent(error) {
91
+ const status = error?.details?.status ?? error?.code;
92
+ if (status === 404 || status === 405 || status === 501)
93
+ return true;
94
+ if (error?.subtype === 'http_error' && typeof status === 'number' && status >= 400 && status < 500)
95
+ return true;
96
+ const message = String(error?.message ?? '');
97
+ return /接口不存在|不存在该接口|NoHandler|No mapping|Not Found|路由不存在|does not exist/i.test(message);
98
+ }
99
+ function peerHint(operation) {
100
+ if (!(operation in EB_TO_STANDARD))
101
+ return {};
102
+ const peer = EB_TO_STANDARD[operation];
103
+ return peer == null ? { unavailable: true } : { peer };
104
+ }
105
+ /**
106
+ * EB operation 失败后的降级处理。
107
+ *
108
+ * @returns 降级成功时返回一个携带切换指引的 `env_fallback` 错误;**标准版也不通或本就不该降级时
109
+ * 返回 `null`**(调用方应保持抛出原始错误,原始错误的诊断价值更高)。
110
+ */
111
+ export async function tryDegradeEbFailure(operation, error, host, ctx) {
112
+ if (!operation.startsWith('meeting.eb.'))
113
+ return null;
114
+ // 手动开关:确认某环境就是 EB 版时,关掉自动降级避免反复实探。
115
+ if (process.env.WEAVER_MEETING_ENV_AUTOFALLBACK === '0')
116
+ return null;
117
+ const type = String(error?.type ?? '');
118
+ const subtype = String(error?.subtype ?? '');
119
+ if (NO_FALLBACK_TYPES.has(type) || NO_FALLBACK_SUBTYPES.has(subtype))
120
+ return null;
121
+ let ebErrorDetail = describe(error);
122
+ const persist = isEbStructurallyAbsent(error);
123
+ try {
124
+ const degraded = await degradeMeetingEnvToStandard(host, ctx, error, { persist });
125
+ ebErrorDetail = degraded.ebError || ebErrorDetail;
126
+ }
127
+ catch {
128
+ // 标准版也不通 → 保留原始错误(比 env_unavailable 更有信息量)。
129
+ return null;
130
+ }
131
+ const hint = peerHint(operation);
132
+ const action = hint.peer
133
+ ? `请改用标准版 operation **\`${hint.peer}\`** 重试(参数口径不同,先跑 \`meeting schema\` 看该 operation 的 inputSchema)。`
134
+ : hint.unavailable
135
+ ? '标准版**没有**该能力的对等实现,请如实告知用户本环境无法完成这项操作,不要用其它接口拼凑。'
136
+ : '标准版清单见 `meeting schema`,挑语义最接近的 operation(先确认 inputSchema 再调用)。';
137
+ const modeNote = persist
138
+ ? '已实探标准版接口可用,**环境模式已更新为 `standard` 并写入缓存**,后续同一环境直接走标准版。'
139
+ : '标准版接口本次探测可用,但这是 EB 侧的偶发故障(HTTP 5xx / 网络类),**没有改动模式缓存**,下次仍会先按 EB 版尝试。';
140
+ const message = [
141
+ `EB 版会议接口在当前环境不可用(${ebErrorDetail})。`,
142
+ modeNote,
143
+ action,
144
+ '若确认本环境其实是 EB 版(例如刚安装完会议 EB 应用),跑 `meeting.env.detect`(`refresh:true`)重新判定即可。',
145
+ ].join(' ');
146
+ return new MeetingCliError('api', 'env_fallback', message, {
147
+ code: 'env_fallback',
148
+ details: {
149
+ ebOperation: operation,
150
+ ebError: ebErrorDetail,
151
+ standardOperation: hint.peer ?? null,
152
+ standardUnavailable: hint.unavailable === true,
153
+ // persist=true 时降级已落盘,调用方不必重复 detect。
154
+ envModeUpdated: persist ? 'standard' : null,
155
+ },
156
+ retryable: false,
157
+ exitCode: 1,
158
+ });
159
+ }
@@ -29,7 +29,7 @@ function cacheFile(ctx) {
29
29
  function emptyCache() {
30
30
  return { schemaVersion: 1, environments: {} };
31
31
  }
32
- async function readCache(ctx) {
32
+ export async function readCache(ctx) {
33
33
  const file = cacheFile(ctx);
34
34
  if (!existsSync(file))
35
35
  return emptyCache();
@@ -58,79 +58,126 @@ async function writeCache(ctx, baseUrl, mode, smokePath) {
58
58
  }
59
59
  }
60
60
  /**
61
- * 解析当前环境的会议模式(EB / 标准版),供 `meeting.env.detect` 与 `meeting.link`
62
- * 共用同一套缓存与冒烟逻辑(避免两处判定不一致导致链接模式选错)。
61
+ * 没有探测结论时的默认模式:**一律先按 EB 版走**。
62
+ *
63
+ * ⛔ 此前每次业务调用前都会先跑 EB + 标准版两路冒烟才算得出模式,既多两次请求,
64
+ * 又把「BB 月接口偶发抖动」直接放大成「整环境被判成标准版」。现改为惰性判定:
65
+ * 先假设 EB,等 EB 真走不通了再由 `degradeMeetingEnvToStandard` 实探降级。
63
66
  */
64
- export async function resolveMeetingEnvMode(host, ctx, options = {}) {
67
+ const DEFAULT_MEETING_MODE = 'eb';
68
+ function describeEnvError(error) {
69
+ if (error instanceof Error) {
70
+ return `${error.type ?? 'api'}/${error.subtype ?? 'error'}: ${error.message}`;
71
+ }
72
+ return String(error);
73
+ }
74
+ /**
75
+ * 双路冒烟并把结论写进缓存:EB 优先,失败回退标准版。
76
+ * **只在 `refresh=true` 或 EB 实际失败需要降级时调用** —— 业务调用前不再冒烟。
77
+ *
78
+ * @param knownEbError 已经发生的 EB 错误:直接跳过 EB 冒烟,避免为同一次失败多打一个请求。
79
+ */
80
+ async function probeAndPersist(host, ctx, knownEbError, options = {}) {
81
+ const persist = options.persist !== false;
65
82
  const session = await host.getSession();
66
- // ① 默认先读缓存(按 baseUrl 固化);refresh=true 才强制重新冒烟。
67
- if (options.refresh !== true) {
68
- const cache = await readCache(ctx);
69
- const cached = cache.environments[session.baseUrl];
70
- if (cached && (cached.mode === 'eb' || cached.mode === 'standard')) {
71
- return {
72
- mode: cached.mode,
73
- smokePath: cached.smokePath,
74
- fromCache: true,
75
- detectedAt: cached.detectedAt,
76
- };
83
+ // ① EB 版冒烟。接口 404/业务失败均视为「EB 未配置」。
84
+ let ebError = knownEbError;
85
+ if (ebError === undefined) {
86
+ try {
87
+ await host.request({
88
+ method: 'POST',
89
+ path: EB_SMOKE_PATH,
90
+ operation: 'meeting.env.detect',
91
+ // EB list 的 datajson 内必须出现 mainTable 节点(空对象即可):缺失时服务端报
92
+ // “mainTable解析异常,请参照文档修正数据格式”,冒烟会**假失败**并把 EB 环境误判为
93
+ // 标准版(2026-09-20 真机复现)。
94
+ body: { datajson: { mainTable: {}, header: { operator: session.userId } } },
95
+ // EB 响应不含 code/status(形如 {datajson:{datas:[...]}}),默认的 isBusinessSuccess
96
+ // 会判为 false → 冒烟假失败 → EB 环境被误判为标准版。
97
+ acceptBusiness: ebBusinessSuccess,
98
+ });
99
+ const warning = await writeCache(ctx, session.baseUrl, 'eb', EB_SMOKE_PATH);
100
+ return { mode: 'eb', smokePath: EB_SMOKE_PATH, warning };
101
+ }
102
+ catch (error) {
103
+ ebError = error;
77
104
  }
78
105
  }
79
- // ② 第 1 步:EB 版冒烟(EB 优先)。接口 404/业务失败均视为「EB 未配置」。
80
- let ebError;
81
- try {
82
- await host.request({
83
- method: 'POST',
84
- path: EB_SMOKE_PATH,
85
- operation: 'meeting.env.detect',
86
- // EB list 的 datajson 内必须出现 mainTable 节点(空对象即可):缺失时服务端报
87
- // “mainTable解析异常,请参照文档修正数据格式”,冒烟会**假失败**并把 EB 环境误判为
88
- // 标准版(2026-09-20 真机复现)。
89
- body: { datajson: { mainTable: {}, header: { operator: session.userId } } },
90
- // EB 响应不含 code/status(形如 {datajson:{datas:[...]}}),默认的 isBusinessSuccess
91
- // 会判为 false → 冒烟假失败 → EB 环境被误判为标准版。
92
- acceptBusiness: ebBusinessSuccess,
93
- });
94
- const warning = await writeCache(ctx, session.baseUrl, 'eb', EB_SMOKE_PATH);
95
- return { mode: 'eb', smokePath: EB_SMOKE_PATH, fromCache: false, warning };
96
- }
97
- catch (error) {
98
- ebError = error;
99
- }
100
- // ③ 第 2 步:标准版冒烟(仅当 EB 冒烟未通过)。
106
+ // ② 标准版冒烟(仅当 EB 冒烟未通过 / 已知 EB 失败)。
101
107
  try {
102
108
  await host.request({
103
109
  method: 'GET',
104
110
  path: STD_CALENDAR_PATH,
105
111
  operation: 'meeting.env.detect',
106
112
  });
107
- const ebDetail = ebError instanceof Error
108
- ? `${ebError.type ?? 'api'}/${ebError.subtype ?? 'error'}: ${ebError.message}`
109
- : String(ebError);
110
- const warning = await writeCache(ctx, session.baseUrl, 'standard', STD_CALENDAR_PATH);
113
+ const warning = persist ? await writeCache(ctx, session.baseUrl, 'standard', STD_CALENDAR_PATH) : undefined;
111
114
  return {
112
115
  mode: 'standard',
113
116
  smokePath: STD_CALENDAR_PATH,
114
- fromCache: false,
115
117
  ebSmokeFailed: true,
116
- ebSmokeError: ebDetail,
118
+ ebSmokeError: describeEnvError(ebError),
117
119
  warning,
118
120
  };
119
121
  }
120
122
  catch (stdError) {
121
123
  // 两步都不通 → 不写缓存,提示接口未配置完整。
122
124
  throw meetingValidationError('env_unavailable', '当前环境 EB 版与标准版会议接口冒烟均未通过,请确认会议应用已安装且接口已配置完整', {
123
- eb: ebError instanceof Error ? ebError.message : String(ebError),
124
- standard: stdError instanceof Error ? stdError.message : String(stdError),
125
+ eb: describeEnvError(ebError),
126
+ standard: describeEnvError(stdError),
125
127
  });
126
128
  }
127
129
  }
130
+ /**
131
+ * 解析当前环境的会议模式(EB / 标准版),供 `meeting.env.detect` 与 `meeting.link`
132
+ * 共用同一套缓存与冒烟逻辑(避免两处判定不一致导致链接模式选错)。
133
+ *
134
+ * **默认 EB**:没有探测结论时直接返回 `mode='eb'` + `assumed=true`,不冒烟、不写缓存;
135
+ * EB 真走不通由调用方触发 `degradeMeetingEnvToStandard` 实探降级。
136
+ */
137
+ export async function resolveMeetingEnvMode(host, ctx, options = {}) {
138
+ const session = await host.getSession();
139
+ // ① refresh=true 才实探(EB 优先,失败回退标准版,结论写进缓存)。
140
+ if (options.refresh === true) {
141
+ return { ...(await probeAndPersist(host, ctx)), fromCache: false };
142
+ }
143
+ // ② 命中缓存直接用(按 baseUrl 固化)。
144
+ const cache = await readCache(ctx);
145
+ const cached = cache.environments[session.baseUrl];
146
+ if (cached && (cached.mode === 'eb' || cached.mode === 'standard')) {
147
+ return {
148
+ mode: cached.mode,
149
+ smokePath: cached.smokePath,
150
+ fromCache: true,
151
+ detectedAt: cached.detectedAt,
152
+ };
153
+ }
154
+ // ③ 无结论 → 默认按 EB 走。业务调用前不再做双路冒烟(省两请求,也避免抖动误判)。
155
+ return { mode: DEFAULT_MEETING_MODE, smokePath: EB_SMOKE_PATH, fromCache: false, assumed: true };
156
+ }
157
+ /**
158
+ * EB 版走不通时的降级入口:实探标准版,通过后把 `standard` 固化进 env 缓存
159
+ * (后续同一 baseUrl 直接走标准版,不再重复试 EB)。
160
+ *
161
+ * @param options.persist false = 只降级本次、**不写缓存**。用于偶发的服务端错误
162
+ * (5xx / 网络抖动),避免一次抖动就把整个环境永久标成标准版。
163
+ *
164
+ * 标准版也不通则抛 `env_unavailable`。
165
+ */
166
+ export async function degradeMeetingEnvToStandard(host, ctx, ebError, options = {}) {
167
+ const probed = await probeAndPersist(host, ctx, ebError, options);
168
+ return {
169
+ mode: 'standard',
170
+ smokePath: probed.smokePath,
171
+ ebError: probed.ebSmokeError ?? describeEnvError(ebError),
172
+ ...(probed.warning ? { warning: probed.warning } : {}),
173
+ };
174
+ }
128
175
  const meetingEnvDetect = {
129
176
  name: 'meeting.env.detect',
130
177
  resource: 'env',
131
178
  method: 'detect',
132
179
  action: 'meeting.env.detect',
133
- summary: '探测当前环境会议模式:默认按 baseUrl 读缓存;未命中或 refresh=true 时 EB 版冒烟优先、失败回退标准版,成功后固化缓存',
180
+ summary: '探测当前环境会议模式:**无探测结论时直接按 EB 版返回**(`assumed=true`,不冒烟、不写缓存);EB 走不通时由 EB 调用链自动实探标准版并把 `standard` 固化进缓存;`refresh=true` 强制重新冒烟',
134
181
  capability: 'env',
135
182
  risk: 'read',
136
183
  inputSchema: {
@@ -0,0 +1,110 @@
1
+ /**
2
+ * 会议表单**真实字段清单**(只读)。
3
+ *
4
+ * 为什么需要:写链路(`create_meeting` / `change_meeting` / 建会流程表单)是按字段名取值与过滤的,
5
+ * 而租户常常在表单上**自己加字段**(参会客户、联系方式、项目编号……)。这些字段:
6
+ * - 不在 CLI 内置白名单里 ⇒ 传不进去(以前只能通过改代码放开);
7
+ * - 也不在接口文档的字段表里 ⇒ 智能体只能瞎猜名字,猜错就被服务端静默丢弃。
8
+ *
9
+ * 本 operation 直接读 eBuilder 表单字段定义,把「中文名 / 字段 id / dataKey(写接口真正认的名字)/
10
+ * 组件类型 / 是否多选」列出来,让调用方**先查再写**:
11
+ * 查 → `meeting.form.fields { type: 'meeting', keyword: '客户' }`
12
+ * 写 → 建会/改会传 `extraFields: { "<dataKey>": 值 }`,流程建会传 `extraFields: { "<表单字段名>": 值 }`
13
+ *
14
+ * ⚠️ 只读,不写任何东西。
15
+ */
16
+ import { meetingSuccess } from './types.js';
17
+ import { MEETING_EB_TAGS, resolveMeetingEbObjs } from './eb/link.js';
18
+ function textOf(value) {
19
+ return value === undefined || value === null ? '' : String(value);
20
+ }
21
+ const TYPES = Object.keys(MEETING_EB_TAGS);
22
+ const meetingFormFields = {
23
+ name: 'meeting.form.fields',
24
+ resource: 'eb',
25
+ method: 'meeting.form.fields',
26
+ action: 'meeting.form.fields',
27
+ summary: '读取会议表单的真实字段清单(中文名 / dataKey / 字段 id / 组件类型 / 是否多选),供 extraFields 按真实字段名写入租户自定义字段',
28
+ capability: 'eb.meta',
29
+ risk: 'read',
30
+ inputSchema: {
31
+ type: 'object',
32
+ additionalProperties: false,
33
+ properties: {
34
+ type: {
35
+ type: 'string',
36
+ enum: TYPES,
37
+ description: '要查哪张表:meeting=会议、topic=议题、minutes=纪要、room=会议室、sign=签到、receipt=回执…(默认 meeting)',
38
+ },
39
+ keyword: { type: 'string', description: '只返回中文名/dataKey 包含该关键词的字段(如「客户」「联系」)' },
40
+ refresh: { type: 'boolean', description: '默认读缓存;传 true 强制重新解析表 id 与字段定义' },
41
+ },
42
+ },
43
+ handler: async (args, env) => {
44
+ const type = (TYPES.includes(args.type) ? args.type : 'meeting');
45
+ const tag = MEETING_EB_TAGS[type];
46
+ const resolved = await resolveMeetingEbObjs(env.host, env.ctx, {
47
+ tags: [tag],
48
+ ...(args.refresh === true ? { refresh: true } : {}),
49
+ });
50
+ const obj = resolved.objs[tag];
51
+ if (!obj?.objId) {
52
+ return meetingSuccess('meeting.form.fields', {
53
+ type,
54
+ tag,
55
+ objId: '',
56
+ fields: [],
57
+ hint: '没解析到该表的 objId(可能当前环境没有这张表,或账号无权限)',
58
+ }, { warnings: resolved.warnings });
59
+ }
60
+ const { createEbuilderFormHost } = await import('../../ebuilder-form/host.js');
61
+ const { createEbuilderFormContinuationSigner } = await import('../../ebuilder-form/continuation.js');
62
+ const { executeEbuilderFormOperation } = await import('../../ebuilder-form/operations.js');
63
+ const formEnv = {
64
+ ctx: env.ctx,
65
+ host: createEbuilderFormHost(env.ctx),
66
+ continuation: createEbuilderFormContinuationSigner(env.ctx),
67
+ };
68
+ const raw = await executeEbuilderFormOperation('ebuilder-form.fields.get', { objId: obj.objId }, formEnv);
69
+ const nodes = Array.isArray(raw?.data?.result?.data) ? raw.data.result.data : [];
70
+ const rows = [];
71
+ const detailTables = [];
72
+ for (const node of nodes) {
73
+ const fields = Array.isArray(node?.fields) ? node.fields : [];
74
+ // 明细表节点:没有 fields,只有名字(如「明细子表1」),单独列出来告诉调用方
75
+ if (fields.length === 0 && textOf(node?.name)) {
76
+ detailTables.push(textOf(node.name));
77
+ continue;
78
+ }
79
+ for (const field of fields) {
80
+ const config = field?.config ?? {};
81
+ const dataKey = textOf(config.dataKey || config.columnName || field.physicalField);
82
+ rows.push({
83
+ dataKey,
84
+ fieldId: textOf(field.name || field.id),
85
+ label: textOf(field.text || config.title),
86
+ compType: textOf(field.compType || config.componentKey),
87
+ type: textOf(field.type || config.type),
88
+ multiSelect: String(field.multiSelect ?? 'false') === 'true',
89
+ });
90
+ }
91
+ }
92
+ const keyword = textOf(args.keyword).trim();
93
+ const matched = keyword
94
+ ? rows.filter((row) => row.label.includes(keyword) || row.dataKey.includes(keyword) || row.fieldId.includes(keyword))
95
+ : rows;
96
+ return meetingSuccess('meeting.form.fields', {
97
+ type,
98
+ tag,
99
+ objId: obj.objId,
100
+ tableName: obj.tableName,
101
+ total: matched.length,
102
+ fields: matched,
103
+ detailTables,
104
+ hint: keyword && matched.length === 0
105
+ ? `没有匹配「${keyword}」的字段 —— 这张表里确实没有该字段,不要硬填(硬填会被服务端静默丢弃)`
106
+ : '写会议字段用 dataKey(extraFields);写建会流程表单用 label(中文名)',
107
+ }, { warnings: resolved.warnings });
108
+ },
109
+ };
110
+ export const meetingFormFieldOperations = [meetingFormFields];
@@ -19,6 +19,7 @@ import { meetingPipelineOperations } from './pipeline.js';
19
19
  import { meetingInvitationOperations } from './invitation.js';
20
20
  import { meetingQrOperations } from './qrcode.js';
21
21
  import { meetingSummaryOperations } from './summary.js';
22
+ import { meetingFormFieldOperations } from './form-fields.js';
22
23
  import { meetingEbOperations } from './eb/index.js';
23
24
  function buildRegistry() {
24
25
  const registry = new Map();
@@ -43,6 +44,7 @@ function buildRegistry() {
43
44
  ...meetingInvitationOperations,
44
45
  ...meetingQrOperations,
45
46
  ...meetingSummaryOperations,
47
+ ...meetingFormFieldOperations,
46
48
  ...meetingEbOperations,
47
49
  ];
48
50
  for (const definition of definitions) {