weaver-work-cli 0.1.32 → 0.1.36

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 (211) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/{archive-UMGX72K6.js → archive-XBVH7JU7.js} +2 -2
  3. package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-MZM3EKFQ.js} +2 -2
  4. package/dist/chunks/{asset-GLEGKFG5.js → asset-4XO7UOUW.js} +2 -2
  5. package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-VXSWANAC.js} +3 -2
  6. package/dist/chunks/{blog-E44OTVDA.js → blog-PGZKOPSC.js} +2 -2
  7. package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-BH4ZVGUN.js} +2 -2
  8. package/dist/chunks/{calendar-JKPIRKFD.js → calendar-MXEMTIO4.js} +2 -2
  9. package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
  10. package/dist/chunks/chunk-3SO6QL6U.js +73 -0
  11. package/dist/chunks/chunk-5DQEEGCC.js +229 -0
  12. package/dist/chunks/{chunk-G3QJULO2.js → chunk-6LLJFKJE.js} +1 -1
  13. package/dist/chunks/{chunk-C6R3B2ML.js → chunk-BPE37FPB.js} +35 -7
  14. package/dist/chunks/chunk-BYATLLEF.js +68 -0
  15. package/dist/chunks/chunk-BYBSCN4X.js +932 -0
  16. package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-F746NKNF.js} +1 -1
  17. package/dist/chunks/{chunk-GCG2NMWR.js → chunk-HXJQI5NM.js} +4 -2
  18. package/dist/chunks/{chunk-YRVDW4WN.js → chunk-I6QGIBDM.js} +2 -2
  19. package/dist/chunks/{chunk-H7J4DC5R.js → chunk-JCSN3JGM.js} +2 -2
  20. package/dist/chunks/{chunk-2GA5ZIQL.js → chunk-JNG42JKG.js} +30 -1
  21. package/dist/chunks/{chunk-DW6AUUKS.js → chunk-JUXVYKII.js} +9 -4
  22. package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-JYKABYCV.js} +2 -2
  23. package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-NCOVUAZK.js} +2 -2
  24. package/dist/chunks/{chunk-E4Z23GHM.js → chunk-QHFANTX7.js} +1114 -904
  25. package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-QYGO5EE2.js} +22 -1148
  26. package/dist/chunks/{chunk-IURUFGZK.js → chunk-T72N74ZT.js} +50 -33
  27. package/dist/chunks/{chunk-H6XSTSLM.js → chunk-TC6MZAO3.js} +137 -204
  28. package/dist/chunks/{chunk-WKI2FBIQ.js → chunk-VPUTVEMC.js} +737 -341
  29. package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-VPXOPQJI.js} +2 -2
  30. package/dist/chunks/chunk-WXFPWCXG.js +774 -0
  31. package/dist/chunks/{chunk-4YC43QQ4.js → chunk-XMGSYD6S.js} +4 -4
  32. package/dist/chunks/conflict-JAB2LJEZ.js +21 -0
  33. package/dist/chunks/continuation-RN6PZN2M.js +11 -0
  34. package/dist/chunks/contract-XI2EEIZU.js +9110 -0
  35. package/dist/chunks/cowork-SCPGLBA4.js +2527 -0
  36. package/dist/chunks/ebuilder-form-EYLHNOTA.js +121 -0
  37. package/dist/chunks/{ehr-HMFY6VIL.js → ehr-EAP77IMT.js} +1344 -27
  38. package/dist/chunks/{esb-ZYSVB7SG.js → esb-2TGGXEE7.js} +4 -4
  39. package/dist/chunks/{fna-B7RKPAZD.js → fna-2X7ZE7BU.js} +13 -8
  40. package/dist/chunks/{host-OCH2BCCV.js → host-C6EL2RKL.js} +3 -3
  41. package/dist/chunks/{host-45WED2KK.js → host-I4NRK4MX.js} +3 -3
  42. package/dist/chunks/{host-I4Z6BFYX.js → host-Q4LZGCRU.js} +3 -3
  43. package/dist/chunks/host-UEI5FGDM.js +23 -0
  44. package/dist/chunks/{host-KNZ52LJZ.js → host-XCLDDXKQ.js} +3 -3
  45. package/dist/chunks/{hrm-2KAZGUHI.js → hrm-YXERJM76.js} +3 -3
  46. package/dist/chunks/{im-ACJJWH4U.js → im-A4VZN2TQ.js} +88 -12
  47. package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-2Z6YPMIS.js} +3 -3
  48. package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-AR5GMKZB.js} +2 -2
  49. package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-QXW7P4X3.js} +4 -4
  50. package/dist/chunks/{mail-S2HDDXO2.js → mail-7GZ6CPBJ.js} +45 -24
  51. package/dist/chunks/{meeting-QVN3I37L.js → meeting-3NMGA3MB.js} +278 -43
  52. package/dist/chunks/{okr-QNG4VC2Q.js → okr-BZ6XUOAG.js} +2 -2
  53. package/dist/chunks/operations-5QL3AECO.js +11 -0
  54. package/dist/chunks/{plan-A25EMW6U.js → plan-DR5CMUYP.js} +2 -2
  55. package/dist/chunks/{project-G6XV66DZ.js → project-6J63MK33.js} +2 -2
  56. package/dist/chunks/{registry-DC6KR3NH.js → registry-KDD7DHNK.js} +4 -4
  57. package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
  58. package/dist/chunks/{registry-7C5UMIPL.js → registry-TKX2E7C7.js} +2 -2
  59. package/dist/chunks/{registry-TI3CIUXF.js → registry-VRTNC6UR.js} +8 -6
  60. package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-XCWRMB5P.js} +4 -3
  61. package/dist/chunks/{schedule-KFIITGSK.js → schedule-IVBICP32.js} +4 -3
  62. package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-D74NFK3C.js} +179 -6
  63. package/dist/index.js +35 -25
  64. package/dist/internal/e10/auth-recovery.js +14 -4
  65. package/dist/internal/e10/fetch-timing.js +31 -0
  66. package/dist/internal/e10/request-runtime.js +5 -0
  67. package/dist/shortcuts/contract/host.js +34 -59
  68. package/dist/shortcuts/cowork/continuation.js +70 -0
  69. package/dist/shortcuts/cowork/errors.js +59 -0
  70. package/dist/shortcuts/cowork/host.js +180 -0
  71. package/dist/shortcuts/cowork/index.js +166 -0
  72. package/dist/shortcuts/cowork/manifest.js +65 -0
  73. package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
  74. package/dist/shortcuts/cowork/operations/item-write.js +380 -0
  75. package/dist/shortcuts/cowork/operations/member-write.js +283 -0
  76. package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
  77. package/dist/shortcuts/cowork/operations/read.js +423 -0
  78. package/dist/shortcuts/cowork/operations/registry.js +39 -0
  79. package/dist/shortcuts/cowork/operations/review-write.js +157 -0
  80. package/dist/shortcuts/cowork/operations/shared.js +215 -0
  81. package/dist/shortcuts/cowork/operations/types.js +39 -0
  82. package/dist/shortcuts/cowork/operations.js +19 -0
  83. package/dist/shortcuts/ehr/host.js +4 -1
  84. package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
  85. package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
  86. package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
  87. package/dist/shortcuts/ehr/operations/recruit-write.js +43 -16
  88. package/dist/shortcuts/ehr/operations/recruit.js +182 -10
  89. package/dist/shortcuts/ehr/operations/registry.js +6 -0
  90. package/dist/shortcuts/fna/operations/expense-chart.js +19 -7
  91. package/dist/shortcuts/im/diagnostics.js +97 -0
  92. package/dist/shortcuts/im/host.js +58 -31
  93. package/dist/shortcuts/im/index.js +7 -2
  94. package/dist/shortcuts/im/operations/at-mention.js +127 -0
  95. package/dist/shortcuts/im/operations/file-user.js +10 -2
  96. package/dist/shortcuts/im/operations/group-read.js +26 -5
  97. package/dist/shortcuts/im/operations/group-write.js +35 -28
  98. package/dist/shortcuts/im/operations/msg-sync.js +7 -2
  99. package/dist/shortcuts/im/operations/msg-write.js +76 -10
  100. package/dist/shortcuts/im/operations/quote-mention.js +119 -0
  101. package/dist/shortcuts/im/operations/session.js +1 -1
  102. package/dist/shortcuts/im/operations/shared.js +10 -0
  103. package/dist/shortcuts/im/operations/types.js +13 -0
  104. package/dist/shortcuts/index.js +10 -0
  105. package/dist/shortcuts/mail/host.js +12 -1
  106. package/dist/shortcuts/mail/operations/types.js +17 -2
  107. package/dist/shortcuts/meeting/host.js +27 -34
  108. package/dist/shortcuts/meeting/index.js +79 -1
  109. package/dist/shortcuts/meeting/manifest.js +10 -0
  110. package/dist/shortcuts/meeting/operations/create.js +4 -1
  111. package/dist/shortcuts/meeting/operations/eb/catalog.js +10 -0
  112. package/dist/shortcuts/meeting/operations/eb/conflict.js +84 -8
  113. package/dist/shortcuts/meeting/operations/eb/create-flow.js +1195 -140
  114. package/dist/shortcuts/meeting/operations/eb/factory.js +170 -8
  115. package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
  116. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +50 -5
  117. package/dist/shortcuts/meeting/operations/eb-fallback.js +163 -0
  118. package/dist/shortcuts/meeting/operations/env.js +99 -46
  119. package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
  120. package/dist/shortcuts/meeting/operations/invitation.js +79 -20
  121. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  122. package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
  123. package/dist/shortcuts/meeting/operations.js +16 -0
  124. package/dist/shortcuts/workflow/endpoints.js +8 -3
  125. package/dist/shortcuts/workflow/index.js +110 -1
  126. package/dist/shortcuts/workflow/operations/form.js +204 -36
  127. package/dist/shortcuts/workflow/operations/read.js +104 -23
  128. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
  129. package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
  130. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
  131. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
  132. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
  133. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
  134. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
  135. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
  136. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
  137. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
  138. package/dist/shortcuts/workflow/operations.js +23 -1
  139. package/docs/e10-auth.md +1 -1
  140. package/docs/ehr.md +1 -1
  141. package/docs/fna.md +2 -1
  142. package/docs/im.md +8 -1
  143. package/docs/todo-smart-convert/README.md +1 -1
  144. package/docs/todo-smart-convert/data-contract.md +2 -2
  145. package/docs/todo-smart-convert/subgroup-rules.md +6 -0
  146. package/package.json +1 -1
  147. package/skill-template/business-info.json +18 -0
  148. package/skill-template/domains/cowork.md +14 -0
  149. package/skill-template/domains/yimiaoban.md +3 -3
  150. package/skill-template/skill-template.md +1 -1
  151. package/skills/weaver-e10-blog/SKILL.md +5 -5
  152. package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
  153. package/skills/weaver-e10-calendar/SKILL.md +5 -5
  154. package/skills/weaver-e10-cowork/SKILL.md +101 -0
  155. package/skills/weaver-e10-cowork/references/cowork-read.md +90 -0
  156. package/skills/weaver-e10-cowork/references/cowork-write.md +131 -0
  157. package/skills/weaver-e10-cowork/references/safety-boundaries.md +45 -0
  158. package/skills/weaver-e10-esb/SKILL.md +5 -5
  159. package/skills/weaver-e10-hrm/SKILL.md +5 -5
  160. package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
  161. package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
  162. package/skills/weaver-e10-jucailin/SKILL.md +38 -14
  163. package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
  164. package/skills/weaver-e10-jucailin/references/recruit-read.md +25 -0
  165. package/skills/weaver-e10-jucailin/references/recruit-write.md +13 -0
  166. package/skills/weaver-e10-jucailin/references/salary.md +15 -11
  167. package/skills/weaver-e10-mail/SKILL.md +17 -14
  168. package/skills/weaver-e10-meeting/SKILL.md +442 -46
  169. package/skills/weaver-e10-meeting/references/meeting-eb.md +135 -3
  170. package/skills/weaver-e10-meeting/references/meeting-invitation.md +28 -2
  171. package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
  172. package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
  173. package/skills/weaver-e10-meeting.zip +0 -0
  174. package/skills/weaver-e10-okr/SKILL.md +5 -5
  175. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  176. package/skills/weaver-e10-plan/SKILL.md +5 -5
  177. package/skills/weaver-e10-qiyecheng/SKILL.md +5 -2
  178. package/skills/weaver-e10-qiyecheng/references/catalog.md +2 -2
  179. package/skills/weaver-e10-qiyecheng/references/expense-chart.md +15 -1
  180. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +22 -21
  181. package/skills/weaver-e10-shared/SKILL.md +12 -12
  182. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
  183. package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
  184. package/skills/weaver-e10-workflow/SKILL.md +29 -5
  185. package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
  186. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
  187. package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
  188. package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
  189. package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
  190. package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
  191. package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
  192. package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
  193. package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
  194. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
  195. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
  196. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
  197. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
  198. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
  199. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
  200. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
  201. package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
  202. package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
  203. package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
  204. package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
  205. package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
  206. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
  207. package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
  208. package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
  209. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
  210. package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
  211. package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
@@ -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,132 @@ 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
+ // ⓪ 固化锁定(2026-10-30):`WEAVER_MEETING_LOCK_MODE=eb` 时恒返回 EB,不读缓存、不冒烟;
140
+ // 只有 `refresh=true` 才允许实探(用户主动要求重新判定时)。
141
+ if (options.refresh !== true
142
+ && String(process.env.WEAVER_MEETING_LOCK_MODE ?? '').trim().toLowerCase() === 'eb') {
143
+ return { mode: 'eb', smokePath: EB_SMOKE_PATH, fromCache: false, assumed: true, locked: true };
144
+ }
145
+ // ① refresh=true 才实探(EB 优先,失败回退标准版,结论写进缓存)。
146
+ if (options.refresh === true) {
147
+ return { ...(await probeAndPersist(host, ctx)), fromCache: false };
148
+ }
149
+ // ② 命中缓存直接用(按 baseUrl 固化)。
150
+ const cache = await readCache(ctx);
151
+ const cached = cache.environments[session.baseUrl];
152
+ if (cached && (cached.mode === 'eb' || cached.mode === 'standard')) {
153
+ return {
154
+ mode: cached.mode,
155
+ smokePath: cached.smokePath,
156
+ fromCache: true,
157
+ detectedAt: cached.detectedAt,
158
+ };
159
+ }
160
+ // ③ 无结论 → 默认按 EB 走。业务调用前不再做双路冒烟(省两请求,也避免抖动误判)。
161
+ return { mode: DEFAULT_MEETING_MODE, smokePath: EB_SMOKE_PATH, fromCache: false, assumed: true };
162
+ }
163
+ /**
164
+ * EB 版走不通时的降级入口:实探标准版,通过后把 `standard` 固化进 env 缓存
165
+ * (后续同一 baseUrl 直接走标准版,不再重复试 EB)。
166
+ *
167
+ * @param options.persist false = 只降级本次、**不写缓存**。用于偶发的服务端错误
168
+ * (5xx / 网络抖动),避免一次抖动就把整个环境永久标成标准版。
169
+ *
170
+ * 标准版也不通则抛 `env_unavailable`。
171
+ */
172
+ export async function degradeMeetingEnvToStandard(host, ctx, ebError, options = {}) {
173
+ const probed = await probeAndPersist(host, ctx, ebError, options);
174
+ return {
175
+ mode: 'standard',
176
+ smokePath: probed.smokePath,
177
+ ebError: probed.ebSmokeError ?? describeEnvError(ebError),
178
+ ...(probed.warning ? { warning: probed.warning } : {}),
179
+ };
180
+ }
128
181
  const meetingEnvDetect = {
129
182
  name: 'meeting.env.detect',
130
183
  resource: 'env',
131
184
  method: 'detect',
132
185
  action: 'meeting.env.detect',
133
- summary: '探测当前环境会议模式:默认按 baseUrl 读缓存;未命中或 refresh=true 时 EB 版冒烟优先、失败回退标准版,成功后固化缓存',
186
+ summary: '探测当前环境会议模式:**无探测结论时直接按 EB 版返回**(`assumed=true`,不冒烟、不写缓存);EB 走不通时由 EB 调用链自动实探标准版并把 `standard` 固化进缓存;`refresh=true` 强制重新冒烟',
134
187
  capability: 'env',
135
188
  risk: 'read',
136
189
  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];
@@ -13,6 +13,7 @@
13
13
  * - 数据源必须是 `meeting.eb.meeting.list` 单查(47 字段);
14
14
  * `meeting.eb.meeting.detail` 在该环境只回 `{id}`,不可用。
15
15
  */
16
+ import { existsSync } from 'node:fs';
16
17
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
17
18
  import { dirname, join, resolve } from 'node:path';
18
19
  import { fileURLToPath } from 'node:url';
@@ -25,8 +26,23 @@ function templatePath() {
25
26
  const override = process.env.WEAVER_MEETING_INVITATION_TEMPLATE;
26
27
  if (override)
27
28
  return resolve(override);
28
- // 本文件编译后位于 <pkg>/dist/shortcuts/meeting/operations/,向上四级即包根
29
+ // ⛔ 不能写死「向上四级」:同一个源文件在两种产物里的位置不一样 ——
30
+ // · tsc 直出:<pkg>/dist/shortcuts/meeting/operations/ → 向上四级是包根
31
+ // · bundle 打包:<pkg>/dist/chunks/chunk-xxxx.js → 向上两级才是包根
32
+ // 2026-10-30 真机踩过:小e 跑的是 bundle 版,写死四级会算出 <pkg>/../templates/… 而报「模板缺失」。
33
+ // 所以直接逐级向上找,找到第一个存在的就是它。
29
34
  const here = dirname(fileURLToPath(import.meta.url));
35
+ let dir = here;
36
+ for (let depth = 0; depth < 6; depth += 1) {
37
+ const candidate = join(dir, 'templates', 'meeting', 'invitation.docx');
38
+ if (existsSync(candidate))
39
+ return candidate;
40
+ const parent = dirname(dir);
41
+ if (parent === dir)
42
+ break;
43
+ dir = parent;
44
+ }
45
+ // 都没找到:仍按传统层级拼一个路径返回,让报错信息能指向具体位置
30
46
  return resolve(here, '../../../../templates/meeting/invitation.docx');
31
47
  }
32
48
  const WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
@@ -344,7 +360,17 @@ const meetingInvitationRender = {
344
360
  type: 'object',
345
361
  additionalProperties: false,
346
362
  properties: {
347
- mtId: { ...SCHEMA_ID, description: '会议 id(EB 版 mt_detail 主键)' },
363
+ mtId: { ...SCHEMA_ID, description: '会议 id(EB 版 mt_detail 主键)。**与下面的字段二选一**:有 mtId 走真实会议数据;没有则用传入字段直接生成(流程建会审批中也能出邀请函)' },
364
+ mt_name: { type: 'string', description: '会议名称(无 mtId 时必填)' },
365
+ begin_datetime: { type: 'string', description: '开始时间 `yyyy-MM-dd HH:mm`(无 mtId 时必填)' },
366
+ end_datetime: { type: 'string', description: '结束时间 `yyyy-MM-dd HH:mm`(无 mtId 时建议填,缺了结束时间会是【待补充】)' },
367
+ address: { type: 'string', description: '会议地点/会议室**名称**(无 mtId 时用;这里要传名称,不是 id)' },
368
+ hrm_members: { type: 'string', description: '参会人姓名(逗号/顿号分隔,无 mtId 时用)' },
369
+ mt_desc: { type: 'string', description: '会议内容/议题概要' },
370
+ org_members: { type: 'string', description: '落款单位(不传则尝试用发起人部门 / 留【待补充】)' },
371
+ caller: { type: 'string', description: '召集人' },
372
+ contacter: { type: 'string', description: '联系人' },
373
+ total_member: { type: 'string', description: '参会人数' },
348
374
  extra: {
349
375
  type: 'object',
350
376
  additionalProperties: { type: 'string' },
@@ -355,13 +381,10 @@ const meetingInvitationRender = {
355
381
  description: '输出文件完整路径(可选)。缺省写到 ~/.weaver-work-cli/meeting/invitations/邀请函-{会议名}-{日期}.docx',
356
382
  },
357
383
  },
358
- required: ['mtId'],
384
+ required: [],
359
385
  },
360
386
  handler: async (args, env) => {
361
387
  const mtId = String(args.mtId ?? '').trim();
362
- if (!mtId) {
363
- throw meetingValidationError('mt_id_required', '必须提供会议 id(mtId)');
364
- }
365
388
  const extra = {};
366
389
  if (args.extra && typeof args.extra === 'object') {
367
390
  for (const [key, value] of Object.entries(args.extra)) {
@@ -369,20 +392,51 @@ const meetingInvitationRender = {
369
392
  extra[key] = String(value);
370
393
  }
371
394
  }
372
- // 复用已实现的 EB 列表操作(内部先 count 再 list,筛选走 URL query)
373
- const { getMeetingOperation } = await import('./registry.js');
374
- const listOperation = getMeetingOperation('meeting.eb.meeting.list');
375
395
  let row;
376
- try {
377
- const listed = await listOperation.handler({ mt_id: mtId, pageNo: 1, pageSize: 1 }, env);
378
- const rows = (listed.data?.rows ?? []);
379
- row = rows[0]?.mainTable ?? rows[0];
380
- }
381
- catch (error) {
382
- throw meetingValidationError('invitation_source_unavailable', `读取会议数据失败(邀请函当前依赖 EB 版会议列表):${error instanceof Error ? error.message : String(error)}`, { mtId });
396
+ /** meeting=按真实会议数据;payload=按调用方传入的申请单字段(会议可能还没生成)。 */
397
+ let dataSource = 'meeting';
398
+ if (!mtId) {
399
+ // ⛔ 邀请函本质是「预置函」:项目人员下载后自己改、自己发(2026-10-30 用户明确要求)。
400
+ // 所以不该卡在"会议必须已生成"——流程还在审批时,用申请单已知信息先生成一份,
401
+ // 取不到的字段渲染成【待补充】,让人工补。这也保证流程建会提交后立刻能拿到邀请函。
402
+ const mtName = String(args.mt_name ?? '').trim();
403
+ const beginText = String(args.begin_datetime ?? '').trim();
404
+ if (!mtName || !beginText) {
405
+ throw meetingValidationError('invitation_source_required', '要么传 mtId(按真实会议生成),要么传 mt_name + begin_datetime(按申请单信息预填生成)');
406
+ }
407
+ // 无 mtId 时落款单位取当前登录用户的部门(比让用户手填更省事),取不到再留【待补充】
408
+ const session = await env.host.getSession().catch(() => null);
409
+ const sessionUserId = String(session?.userId ?? '');
410
+ row = {
411
+ mt_name: mtName,
412
+ begin_datetime: beginText,
413
+ end_datetime: String(args.end_datetime ?? '').trim(),
414
+ address: String(args.address ?? '').trim(),
415
+ hrm_members: String(args.hrm_members ?? '').trim(),
416
+ mt_desc: String(args.mt_desc ?? '').trim(),
417
+ org_members: String(args.org_members ?? '').trim(),
418
+ caller: String(args.caller ?? '').trim(),
419
+ contacter: String(args.contacter ?? '').trim(),
420
+ total_member: String(args.total_member ?? '').trim(),
421
+ creator: sessionUserId,
422
+ };
423
+ dataSource = 'payload';
383
424
  }
384
- if (!row) {
385
- throw meetingValidationError('not_found', `未找到会议 id=${mtId},或当前账号无权限查看`, { mtId });
425
+ else {
426
+ // 复用已实现的 EB 列表操作(内部先 count 再 list,筛选走 URL query)
427
+ const { getMeetingOperation } = await import('./registry.js');
428
+ const listOperation = getMeetingOperation('meeting.eb.meeting.list');
429
+ try {
430
+ const listed = await listOperation.handler({ mt_id: mtId, pageNo: 1, pageSize: 1 }, env);
431
+ const rows = (listed.data?.rows ?? []);
432
+ row = rows[0]?.mainTable ?? rows[0];
433
+ }
434
+ catch (error) {
435
+ throw meetingValidationError('invitation_source_unavailable', `读取会议数据失败(邀请函当前依赖 EB 版会议列表):${error instanceof Error ? error.message : String(error)}`, { mtId });
436
+ }
437
+ if (!row) {
438
+ throw meetingValidationError('not_found', `未找到会议 id=${mtId},或当前账号无权限查看`, { mtId });
439
+ }
386
440
  }
387
441
  // 落款单位回落:会议 org_members 为空时,用「发起人(creator 人员 id)的所属组织」
388
442
  const creatorId = String(row.creator ?? '').trim();
@@ -390,7 +444,7 @@ const meetingInvitationRender = {
390
444
  const issuerOrg = needsIssuerOrg ? await resolveIssuerOrganization(env, creatorId) : null;
391
445
  const { resolver, orgSource } = buildResolver(row, extra, issuerOrg);
392
446
  // 议程:议题表 → extra.agenda → 保留模板里的示例行(用户口径:没有就这样)
393
- let agendaRows = await loadAgendaFromTopics(env, mtId);
447
+ let agendaRows = dataSource === 'meeting' && mtId ? await loadAgendaFromTopics(env, mtId) : [];
394
448
  let agendaSource = agendaRows.length > 0 ? 'meeting.topic' : 'template';
395
449
  if (agendaRows.length === 0) {
396
450
  const fromExtra = parseAgendaFromExtra(extra.agenda);
@@ -418,6 +472,9 @@ const meetingInvitationRender = {
418
472
  await mkdir(dirname(target), { recursive: true });
419
473
  await writeFile(target, rendered.buffer);
420
474
  const warnings = [];
475
+ if (dataSource === 'payload') {
476
+ warnings.push('本邀请函按**申请单已知信息**预生成(会议可能还在审批中、尚未生成),属于**预置函**:请下载后人工核对/修改再自行发送。会议正式生成后可用 mtId 重新生成一版拿到完整字段。');
477
+ }
421
478
  if (rendered.pending.length > 0) {
422
479
  warnings.push(`以下字段取不到值,已渲染成黄色高亮【待补充】,请人工补充:${rendered.pending.join('、')}`);
423
480
  }
@@ -438,8 +495,10 @@ const meetingInvitationRender = {
438
495
  filePath: target,
439
496
  fileName,
440
497
  bytes: rendered.buffer.length,
441
- meetingId: mtId,
498
+ meetingId: mtId || null,
442
499
  meetingName: String(row.mt_name ?? ''),
500
+ // meeting=按真实会议数据;payload=按申请单字段预生成(会议可能还没生成,属预置函)
501
+ dataSource,
443
502
  filledFields: rendered.filled,
444
503
  pendingFields: rendered.pending,
445
504
  // 落款单位的实际来源,便于调用方判断是否需要人工改写
@@ -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) {
@@ -0,0 +1,71 @@
1
+ import { MeetingCliError } from '../errors.js';
2
+ import { readCache } from './env.js';
3
+ /**
4
+ * 标准版会议 operation → 对应的 EB operation。
5
+ *
6
+ * ⛔ 存在的理由(2026-10-29 真机事故):e-cology 这类租户**同时装了标准会议应用和 EB 会议应用**,
7
+ * 两套数据完全不通(标准版 `meeting.room.list` 返回 74 间「317/342/442会议室」,
8
+ * EB 的 `uf_mt_room_base` 只有 1 间「测试会议室1」)。agent 在 EB 环境误调标准版接口时,
9
+ * 接口**不会报错、照常返回数据**,于是把根本不存在的会议室/会议摆到用户面前,用户还以为数据错了。
10
+ *
11
+ * 所以这里做一道闸:**环境已确认是 EB 时,标准版会议接口直接拒绝**,并给出对应的 EB operation。
12
+ * 只拦"会议域且语义对得上"的 operation —— 其余的(登录、附件、二维码等)不涉及双套数据,放行。
13
+ */
14
+ const STANDARD_TO_EB = {
15
+ 'meeting.list': 'meeting.eb.meeting.list',
16
+ 'meeting.detail.field': 'meeting.eb.meeting.detail',
17
+ 'meeting.room.list': 'meeting.eb.room.list',
18
+ 'meeting.room.usage': 'meeting.eb.roomUsage.list',
19
+ 'meeting.type.list': 'meeting.eb.type.list',
20
+ 'meeting.receipt.list': 'meeting.eb.receipt.list',
21
+ 'meeting.sign.list': 'meeting.eb.sign.list',
22
+ 'meeting.conflict.room': 'meeting.eb.conflict.room',
23
+ 'meeting.conflict.member': 'meeting.eb.conflict.member',
24
+ 'meeting.roomAttribute.list': 'meeting.eb.room.list',
25
+ };
26
+ /** 逃生开关:确认要看标准版数据时设 `WEAVER_MEETING_ALLOW_STANDARD_IN_EB=1`。 */
27
+ function escapeEnabled() {
28
+ const flag = String(process.env.WEAVER_MEETING_ALLOW_STANDARD_IN_EB ?? '').trim().toLowerCase();
29
+ return flag === '1' || flag === 'true';
30
+ }
31
+ /**
32
+ * 标准版会议 operation 的环境守卫。
33
+ *
34
+ * 判据只用**已确认的**环境结论(env-mode 缓存):
35
+ * - 缓存说 `eb` → 拒绝(本次事故场景);
36
+ * - 缓存说 `standard` → 放行(这正是降级后的正常状态);
37
+ * - **没有缓存 → 放行**。未探测不等于 EB:标准版租户首次使用不该被拦,
38
+ * 拦住反而会让"第一次就能用"的体验断掉。要纠偏就跑 `meeting.env.detect --refresh`。
39
+ */
40
+ export async function assertStandardOperationAllowed(operation, context) {
41
+ const peer = STANDARD_TO_EB[operation];
42
+ if (!peer)
43
+ return;
44
+ if (escapeEnabled())
45
+ return;
46
+ let mode;
47
+ try {
48
+ const session = await context.host.getSession();
49
+ const cache = await readCache(context.ctx);
50
+ mode = cache.environments[session.baseUrl]?.mode;
51
+ }
52
+ catch {
53
+ // 缓存读不到(权限/损坏)不应阻断业务:按"未确认"处理,放行。
54
+ return;
55
+ }
56
+ if (mode !== 'eb')
57
+ return;
58
+ throw new MeetingCliError('api', 'env_mismatch', `当前环境已确认为 **EB 版**,\`${operation}\` 是**标准版**接口,返回的是另一套数据(两套会议数据互不相通,`
59
+ + '标准版会议室/会议在这里是不存在的东西)。请改用 EB operation **`' + peer + '`**。'
60
+ + '如果确实要读标准版数据(极少见),设环境变量 `WEAVER_MEETING_ALLOW_STANDARD_IN_EB=1` 后再跑。', {
61
+ code: 'env_mismatch',
62
+ details: {
63
+ requestedOperation: operation,
64
+ standardOperation: operation,
65
+ ebOperation: peer,
66
+ envMode: 'eb',
67
+ },
68
+ retryable: false,
69
+ exitCode: 1,
70
+ });
71
+ }
@@ -3,6 +3,8 @@ import { dirname, join } from 'node:path';
3
3
  import { getMeetingOperation } from './operations/registry.js';
4
4
  import { assertNoForbiddenKeys, asObject } from './operations/shared.js';
5
5
  import { meetingStateDir } from './operations/env.js';
6
+ import { tryDegradeEbFailure } from './operations/eb-fallback.js';
7
+ import { assertStandardOperationAllowed } from './operations/standard-guard.js';
6
8
  /**
7
9
  * 调用流水(best-effort):每次 operation 追加一行 JSON 到 `<state>/meeting/logs/calls.jsonl`。
8
10
  *
@@ -49,12 +51,26 @@ export async function executeMeetingOperation(operation, rawInput, context) {
49
51
  };
50
52
  const startedAt = Date.now();
51
53
  try {
54
+ // 环境守卫(2026-10-29):环境已确认是 EB 时,标准版会议接口直接拒绝 ——
55
+ // 两套数据不通,标准版照常返回 74 间"不存在的会议室",agent 会原样转述给用户。
56
+ await assertStandardOperationAllowed(operation, { ...context, host });
52
57
  const envelope = await definition.handler(input, { ...context, host });
53
58
  recordCall(context, operation, startedAt, true);
54
59
  return envelope;
55
60
  }
56
61
  catch (error) {
57
62
  recordCall(context, operation, startedAt, false, String(error?.type ?? 'unknown'));
63
+ // EB 优先 + 惰性降级:EB operation 在这个环境走不通时,实探标准版并把 standard 固化进
64
+ // env 缓存,然后抛 `env_fallback` 指引调用方改用标准版 operation。标准版也不通则保持原错误。
65
+ try {
66
+ const degraded = await tryDegradeEbFailure(operation, error, context.host, context.ctx);
67
+ if (degraded)
68
+ throw degraded;
69
+ }
70
+ catch (rethrow) {
71
+ if (rethrow !== error)
72
+ throw rethrow;
73
+ }
58
74
  throw error;
59
75
  }
60
76
  }
@@ -161,11 +161,16 @@ export const AJAX_QUERY = { sf_request_type: 'ajax' };
161
161
  * `/api/workflow/core/` 而原样返回,导致请求路径非法。
162
162
  *
163
163
  * - WEA_FORM(工作流表单)→ 保持 `FORM_ENDPOINTS` 默认地址
164
- * - WEA_EBUILDER(建模表单)→ `/api/workflow/core/` 段替换为 `/api/ebuilder/flow/`
164
+ * - WEA_EBUILDER(建模表单)→ `/api/workflow/core/` 段替换为 `ebPrefix`
165
+ *
166
+ * ⚠️ `ebPrefix` 默认 `/api/ebuilder/flow/`,但**建模表单内部前缀不统一**,必须按接口给
167
+ * (均为真机请求实证):
168
+ * - `getFormLayout` / `getFormDataAndLayout` / `saveFormData` → `/api/ebuilder/flow/`
169
+ * - `executeFdLinkage` → `/api/ebuilder/form/`
165
170
  */
166
- export function formApiUrl(path, relateType) {
171
+ export function formApiUrl(path, relateType, ebPrefix = '/api/ebuilder/flow/') {
167
172
  if (String(relateType || '').trim() === 'WEA_EBUILDER') {
168
- return path.replace('/api/workflow/core/', '/api/ebuilder/flow/');
173
+ return path.replace('/api/workflow/core/', ebPrefix);
169
174
  }
170
175
  return path;
171
176
  }