weaver-work-cli 0.1.33 → 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 (100) hide show
  1. package/dist/chunks/{archive-FVFY6GZN.js → archive-XBVH7JU7.js} +2 -2
  2. package/dist/chunks/{archive-oa-NW3OD4UV.js → archive-oa-MZM3EKFQ.js} +2 -2
  3. package/dist/chunks/{asset-7QJKMIDV.js → asset-4XO7UOUW.js} +2 -2
  4. package/dist/chunks/{attachment-content-OSAVA6K3.js → attachment-content-VXSWANAC.js} +3 -3
  5. package/dist/chunks/{blog-I7SDPNW4.js → blog-PGZKOPSC.js} +2 -2
  6. package/dist/chunks/{caizhilian-O37ZIQNY.js → caizhilian-BH4ZVGUN.js} +2 -2
  7. package/dist/chunks/{calendar-7KXE4ED3.js → calendar-MXEMTIO4.js} +2 -2
  8. package/dist/chunks/{chunk-VBCFFFGT.js → chunk-5DQEEGCC.js} +4 -1
  9. package/dist/chunks/{chunk-KAS45KNR.js → chunk-6LLJFKJE.js} +1 -1
  10. package/dist/chunks/{chunk-HEL5XTYI.js → chunk-BPE37FPB.js} +26 -4
  11. package/dist/chunks/{chunk-IHUYP6LO.js → chunk-F746NKNF.js} +1 -1
  12. package/dist/chunks/{chunk-MQ2I5AA3.js → chunk-HXJQI5NM.js} +2 -2
  13. package/dist/chunks/{chunk-ILMBNVSG.js → chunk-I6QGIBDM.js} +2 -2
  14. package/dist/chunks/{chunk-EEWPQE3O.js → chunk-JCSN3JGM.js} +2 -2
  15. package/dist/chunks/{chunk-D223PTXU.js → chunk-JNG42JKG.js} +29 -1
  16. package/dist/chunks/{chunk-6ATGWRZM.js → chunk-JUXVYKII.js} +4 -1
  17. package/dist/chunks/{chunk-GFPXQIY2.js → chunk-JYKABYCV.js} +2 -2
  18. package/dist/chunks/{chunk-Q6LWB5HX.js → chunk-NCOVUAZK.js} +2 -2
  19. package/dist/chunks/{chunk-WY74UBJ6.js → chunk-QHFANTX7.js} +900 -204
  20. package/dist/chunks/{chunk-WRRJ4GG2.js → chunk-QYGO5EE2.js} +2 -2
  21. package/dist/chunks/{chunk-RBGOMK4Y.js → chunk-T72N74ZT.js} +2 -2
  22. package/dist/chunks/{chunk-MEEFGWTM.js → chunk-TC6MZAO3.js} +84 -9
  23. package/dist/chunks/{chunk-OCU4RLHC.js → chunk-VPUTVEMC.js} +98 -34
  24. package/dist/chunks/{chunk-PRHJGPMA.js → chunk-VPXOPQJI.js} +2 -2
  25. package/dist/chunks/{chunk-NHA62QD7.js → chunk-WXFPWCXG.js} +3 -3
  26. package/dist/chunks/{chunk-2FLWXFVS.js → chunk-XMGSYD6S.js} +4 -4
  27. package/dist/chunks/{conflict-SFNEVJY5.js → conflict-JAB2LJEZ.js} +2 -2
  28. package/dist/chunks/contract-XI2EEIZU.js +9110 -0
  29. package/dist/chunks/{cowork-CKWYHXYB.js → cowork-SCPGLBA4.js} +44 -25
  30. package/dist/chunks/{ebuilder-form-KOLJRVZD.js → ebuilder-form-EYLHNOTA.js} +3 -3
  31. package/dist/chunks/{ehr-WFOC3B4D.js → ehr-EAP77IMT.js} +172 -9
  32. package/dist/chunks/{esb-LJZTN663.js → esb-2TGGXEE7.js} +4 -4
  33. package/dist/chunks/{fna-F7QRIIZC.js → fna-2X7ZE7BU.js} +13 -8
  34. package/dist/chunks/{host-VQ6JJXGQ.js → host-C6EL2RKL.js} +3 -3
  35. package/dist/chunks/{host-BCGUVEWX.js → host-I4NRK4MX.js} +3 -3
  36. package/dist/chunks/{host-DGE2TZD4.js → host-Q4LZGCRU.js} +3 -3
  37. package/dist/chunks/{host-BQBRHVR3.js → host-UEI5FGDM.js} +3 -3
  38. package/dist/chunks/{host-KXPTEWFS.js → host-XCLDDXKQ.js} +3 -3
  39. package/dist/chunks/{hrm-NI6XAKW5.js → hrm-YXERJM76.js} +3 -3
  40. package/dist/chunks/{im-7TRTF3KT.js → im-A4VZN2TQ.js} +3 -3
  41. package/dist/chunks/{invoice-AOXWJKYT.js → invoice-2Z6YPMIS.js} +3 -3
  42. package/dist/chunks/{jiuchuanhui-7NQWEZAW.js → jiuchuanhui-AR5GMKZB.js} +2 -2
  43. package/dist/chunks/{lazy-dispatch-RNS4EE4S.js → lazy-dispatch-QXW7P4X3.js} +4 -4
  44. package/dist/chunks/{mail-VDHJAB4O.js → mail-7GZ6CPBJ.js} +44 -23
  45. package/dist/chunks/{meeting-GA7XCNNP.js → meeting-3NMGA3MB.js} +74 -10
  46. package/dist/chunks/{okr-TQIRERPQ.js → okr-BZ6XUOAG.js} +2 -2
  47. package/dist/chunks/{plan-4RY4LN2P.js → plan-DR5CMUYP.js} +2 -2
  48. package/dist/chunks/{project-D6XT2OC5.js → project-6J63MK33.js} +2 -2
  49. package/dist/chunks/{registry-JHRIDQ3A.js → registry-KDD7DHNK.js} +4 -4
  50. package/dist/chunks/{registry-VOGKFFTR.js → registry-TKX2E7C7.js} +2 -2
  51. package/dist/chunks/{registry-QKMI43Q6.js → registry-VRTNC6UR.js} +8 -8
  52. package/dist/chunks/{room-recommend-D4EUOOWY.js → room-recommend-XCWRMB5P.js} +4 -4
  53. package/dist/chunks/{schedule-XPFBHPCV.js → schedule-IVBICP32.js} +4 -4
  54. package/dist/chunks/{workflow-HLBZRWIY.js → workflow-D74NFK3C.js} +11 -4
  55. package/dist/index.js +31 -26
  56. package/dist/internal/e10/fetch-timing.js +31 -0
  57. package/dist/internal/e10/request-runtime.js +5 -0
  58. package/dist/shortcuts/cowork/host.js +8 -0
  59. package/dist/shortcuts/cowork/operations/types.js +17 -2
  60. package/dist/shortcuts/ehr/operations/recruit-write.js +35 -0
  61. package/dist/shortcuts/ehr/operations/recruit.js +147 -0
  62. package/dist/shortcuts/fna/operations/expense-chart.js +19 -7
  63. package/dist/shortcuts/index.js +5 -0
  64. package/dist/shortcuts/mail/host.js +8 -0
  65. package/dist/shortcuts/mail/operations/types.js +17 -2
  66. package/dist/shortcuts/meeting/index.js +79 -1
  67. package/dist/shortcuts/meeting/manifest.js +10 -0
  68. package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -0
  69. package/dist/shortcuts/meeting/operations/eb/conflict.js +6 -1
  70. package/dist/shortcuts/meeting/operations/eb/create-flow.js +1047 -134
  71. package/dist/shortcuts/meeting/operations/eb/factory.js +101 -7
  72. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +36 -2
  73. package/dist/shortcuts/meeting/operations/eb-fallback.js +4 -0
  74. package/dist/shortcuts/meeting/operations/env.js +6 -0
  75. package/dist/shortcuts/meeting/operations/invitation.js +79 -20
  76. package/dist/shortcuts/workflow/endpoints.js +8 -3
  77. package/dist/shortcuts/workflow/operations/form.js +181 -34
  78. package/dist/shortcuts/workflow/operations.js +23 -1
  79. package/docs/ehr.md +1 -1
  80. package/docs/fna.md +2 -1
  81. package/package.json +1 -1
  82. package/skill-template/business-info.json +18 -0
  83. package/skill-template/domains/cowork.md +14 -0
  84. package/skills/weaver-e10-cowork/SKILL.md +101 -0
  85. package/skills/weaver-e10-cowork/references/cowork-read.md +90 -0
  86. package/skills/weaver-e10-cowork/references/cowork-write.md +131 -0
  87. package/skills/weaver-e10-cowork/references/safety-boundaries.md +45 -0
  88. package/skills/weaver-e10-jucailin/SKILL.md +21 -3
  89. package/skills/weaver-e10-jucailin/references/recruit-read.md +25 -0
  90. package/skills/weaver-e10-jucailin/references/recruit-write.md +13 -0
  91. package/skills/weaver-e10-jucailin/references/salary.md +15 -11
  92. package/skills/weaver-e10-mail/SKILL.md +12 -9
  93. package/skills/weaver-e10-meeting/SKILL.md +284 -34
  94. package/skills/weaver-e10-meeting/references/meeting-eb.md +131 -0
  95. package/skills/weaver-e10-meeting/references/meeting-invitation.md +28 -2
  96. package/skills/weaver-e10-meeting.zip +0 -0
  97. package/skills/weaver-e10-qiyecheng/SKILL.md +5 -2
  98. package/skills/weaver-e10-qiyecheng/references/catalog.md +2 -2
  99. package/skills/weaver-e10-qiyecheng/references/expense-chart.md +15 -1
  100. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +22 -21
@@ -71,10 +71,12 @@ function isNetworkError(error) {
71
71
  * 该缺陷曾导致 CLI 的 EB 读操作全线失败,并连带使 `meeting.env.detect` 的 EB 冒烟失败、
72
72
  * 环境被误判为 standard 版。
73
73
  */
74
- export function queryBody(userId, page) {
74
+ export function queryBody(userId, page, mainTable) {
75
75
  return {
76
76
  datajson: {
77
- mainTable: {},
77
+ // 传入字段即过滤条件(e-builder openApi 文档:getFormDataList 的传入字段挂 mainTable 节点)。
78
+ // 空对象 = 不过滤。
79
+ mainTable: mainTable ?? {},
78
80
  // ⚠️ 分页必须放在 body 的 pageInfo 里(值为字符串):只放 URL query 时 EB 会忽略,
79
81
  // 恒定返回首页 10 条(2026-09-21 真机暴露:pageSize=60 仍只回 10 条)。
80
82
  ...(page ? { pageInfo: { pageNo: String(page.pageNo), pageSize: String(page.pageSize) } } : {}),
@@ -82,6 +84,27 @@ export function queryBody(userId, page) {
82
84
  },
83
85
  };
84
86
  }
87
+ /**
88
+ * 模糊回退最多翻几页。EB 的 pageSize 不生效(恒定回 10 条),所以翻页数直接决定能覆盖多少数据。
89
+ * 环境变量 `WEAVER_MEETING_EB_FALLBACK_MAX_PAGES` 可覆盖,默认 30(≈300 条)。
90
+ */
91
+ function readFallbackMaxPages() {
92
+ const raw = Number(process.env.WEAVER_MEETING_EB_FALLBACK_MAX_PAGES ?? '');
93
+ if (Number.isFinite(raw) && raw >= 1)
94
+ return Math.min(Math.floor(raw), 100);
95
+ return 30;
96
+ }
97
+ /** 从入参里挑出「服务端过滤字段」(mainTableParams),没传的不进 body。 */
98
+ function pickMainTableFilters(args, names) {
99
+ const out = {};
100
+ for (const name of names ?? []) {
101
+ const value = args[name];
102
+ if (value === undefined || value === null || value === '')
103
+ continue;
104
+ out[name] = typeof value === 'string' ? value : String(value);
105
+ }
106
+ return out;
107
+ }
85
108
  /** 详情接口 body(getFormDataByPk):顶层 mainTable 传 id。 */
86
109
  function detailBody(userId, id) {
87
110
  return { datajson: { mainTable: { id }, header: { operator: userId } } };
@@ -138,11 +161,24 @@ function buildReadInputSchema(endpoint) {
138
161
  for (const param of endpoint.queryParams ?? []) {
139
162
  properties[param] = { type: 'string', description: `EB 筛选参数(URL 占位符名 ${param})` };
140
163
  }
164
+ for (const param of endpoint.mainTableParams ?? []) {
165
+ properties[param] = {
166
+ type: 'string',
167
+ description: `按 ${param} 过滤(走 body datajson.mainTable,服务端**精确匹配**;`
168
+ + `传部分词时服务端会返回 0 条,CLI 会自动翻页扫全量并按「包含」重新筛,结果一样是模糊的)`,
169
+ };
170
+ }
141
171
  properties.pageNo = { type: 'integer', minimum: 1, description: '页码,默认 1' };
142
172
  properties.pageSize = { type: 'integer', minimum: 1, maximum: 200, description: '每页条数,默认 20' };
143
173
  if (endpoint.countPk) {
144
174
  properties.withCount = { type: 'boolean', description: '是否先查总数(默认 true,条件与列表一致)' };
145
175
  }
176
+ if ((endpoint.mainTableParams ?? []).length > 0) {
177
+ properties.noFallback = {
178
+ type: 'boolean',
179
+ description: '跳过「模糊兜底」:服务端精确匹配不到时,不再翻页扫全量(默认 false=会扫,保证部分词也能查到)',
180
+ };
181
+ }
146
182
  }
147
183
  return {
148
184
  type: 'object',
@@ -216,6 +252,9 @@ function buildReadOperation(endpoint) {
216
252
  }
217
253
  const pageNo = typeof args.pageNo === 'number' ? args.pageNo : 1;
218
254
  const pageSize = typeof args.pageSize === 'number' ? args.pageSize : 20;
255
+ // 服务端过滤条件:count 与 list 必须用**同一份**,否则 total 与当页行数对不上
256
+ // (总数是不带过滤的、列表是过滤后的,用户会看到"共 155 间"却只回 1 条)。
257
+ const mainTableFilters = pickMainTableFilters(args, endpoint.mainTableParams);
219
258
  let total;
220
259
  if (endpoint.countPk && args.withCount !== false) {
221
260
  const countResponse = await env.host.request({
@@ -223,7 +262,7 @@ function buildReadOperation(endpoint) {
223
262
  path: `${EB_BASE}/getFormDataCount/${endpoint.countPk}`,
224
263
  operation: endpoint.operation,
225
264
  query,
226
- body: queryBody(session.userId),
265
+ body: queryBody(session.userId, undefined, mainTableFilters),
227
266
  acceptBusiness,
228
267
  });
229
268
  total = extractEbCount(countResponse);
@@ -234,13 +273,61 @@ function buildReadOperation(endpoint) {
234
273
  operation: endpoint.operation,
235
274
  query: { ...query, pageNo, pageSize },
236
275
  // 分页同时进 body.pageInfo:EB 只认 body 里的 pageInfo,仅放 URL query 会被忽略。
237
- body: queryBody(session.userId, { pageNo, pageSize }),
276
+ body: queryBody(session.userId, { pageNo, pageSize }, mainTableFilters),
238
277
  acceptBusiness,
239
278
  });
240
- const { rows, total: listTotal } = extractEbRows(response);
279
+ const { rows: rawRows, total: listTotal } = extractEbRows(response);
280
+ // 服务端过滤不可靠时的兜底:2026-10-30 实测 e-cology 生产环境,
281
+ // `getFormDataCount/query_meeting_room_count` **完全忽略** mainTable / URL query 的过滤条件
282
+ // (无过滤 155、room_name=测试 155、room_name=zzz不存在 155,全是 155),
283
+ // 而 list 又可能只回当页 —— 只靠服务端过滤会得到"total=155 但一条都不匹配"的假结果。
284
+ // 所以只要调用方传了筛选字段,就在本地按同一条件再筛一遍,保证结果正确。
285
+ const filterKeys = Object.keys(mainTableFilters);
286
+ const matchesFilters = (row) => {
287
+ const main = (row?.mainTable ?? row ?? {});
288
+ return filterKeys.every((key) => {
289
+ const cell = main[key];
290
+ if (cell === undefined || cell === null)
291
+ return false;
292
+ return String(cell).includes(String(mainTableFilters[key]));
293
+ });
294
+ };
295
+ let rows = filterKeys.length === 0 ? rawRows : rawRows.filter(matchesFilters);
296
+ let fallback;
297
+ // ⚠️ 服务端是**精确匹配**,模糊词拿不到数据(2026-10-30 实测 e-cology:
298
+ // room_name=测试会议室1 → 1 条 ✅;room_name=测试 / 重庆 → 0 条 ❌,
299
+ // 但全量里明明有「重庆会议室一/二」)。
300
+ // 传了筛选却一条没命中时,回退:不带过滤翻页拉全量,在客户端按 contains 再筛一遍。
301
+ // 只在首轮 0 条时触发(精确命中时零额外开销);`noFallback:true` 可关。
302
+ if (filterKeys.length > 0 && rows.length === 0 && args.noFallback !== true) {
303
+ const maxPages = readFallbackMaxPages();
304
+ const collected = [];
305
+ let pages = 0;
306
+ for (let p = 1; p <= maxPages; p += 1) {
307
+ const pageResponse = await env.host.request({
308
+ method: 'POST',
309
+ path,
310
+ operation: endpoint.operation,
311
+ query: { ...query, pageNo: p, pageSize },
312
+ // 不带 mainTableFilters:服务端只会做精确匹配,带上就又变成 0 条了
313
+ body: queryBody(session.userId, { pageNo: p, pageSize }),
314
+ acceptBusiness,
315
+ });
316
+ const { rows: pageRows } = extractEbRows(pageResponse);
317
+ pages = p;
318
+ if (pageRows.length === 0)
319
+ break; // 翻到空页 = 到底了
320
+ collected.push(...pageRows);
321
+ if (p >= maxPages)
322
+ break;
323
+ }
324
+ fallback = { pages, scanned: collected.length };
325
+ rows = collected.filter(matchesFilters);
326
+ }
241
327
  // count 接口与实际行数不一致时(本租户 `query_mt_topic_count` 恒返回 0,而 list 有数据),
242
328
  // 以"实际取到的行数"为下限,避免出现 total=0 却有数据的自相矛盾结果。
243
- const resolvedTotal = total ?? listTotal ?? rows.length;
329
+ // ⚠️ 有筛选时 count 不可信(见上),一律以本地筛完的行数为准。
330
+ const resolvedTotal = filterKeys.length > 0 ? rows.length : (total ?? listTotal ?? rawRows.length);
244
331
  // 结果后处理(如决议任务进度 / 签到回执出席汇总):只**增**一个字段,原始 rows 原样返回。
245
332
  // 统一注入 `page`(本页 + 总数),避免各实现把"当页行数"误当成总数(2026-09-22 踩到:
246
333
  // 签到全量 261 条、pageSize=200 时,summary 里写的 total 只有 200)。
@@ -249,6 +336,8 @@ function buildReadOperation(endpoint) {
249
336
  pageSize,
250
337
  pageCount: rows.length,
251
338
  total: Math.max(resolvedTotal, rows.length),
339
+ // 模糊回退时告知调用方:结果是客户端筛出来的,扫了多少条
340
+ ...(fallback ? { fallback } : {}),
252
341
  };
253
342
  const summarized = endpoint.summarizeRows
254
343
  ? endpoint.summarizeRows(rows, {
@@ -278,6 +367,11 @@ function buildReadOperation(endpoint) {
278
367
  pageWarnings.push(`本页返回 ${rows.length} 条,共 ${effectiveTotal} 条:还有 ${missing} 条未取到。`
279
368
  + `需要全量请翻页(pageNo=${pageNo + 1} 起)或加大 pageSize,或收窄筛选条件`);
280
369
  }
370
+ if (fallback) {
371
+ pageWarnings.push(`服务端只支持「精确匹配」,按 ${filterKeys.join('/')} 过滤后 0 条;`
372
+ + `已翻页扫描全量 ${fallback.scanned} 条(${fallback.pages} 页)在客户端按「包含」重新筛选,命中 ${rows.length} 条。`
373
+ + `传 noFallback:true 可跳过这次扫描`);
374
+ }
281
375
  return meetingSuccess(endpoint.operation, {
282
376
  total: effectiveTotal,
283
377
  pageNo,
@@ -285,7 +379,7 @@ function buildReadOperation(endpoint) {
285
379
  count: rows.length,
286
380
  ...(progress ? { progress } : {}),
287
381
  rows,
288
- filters: query,
382
+ filters: { ...query, ...mainTableFilters },
289
383
  }, {
290
384
  ...(endpoint.notes ? { meta: { notes: endpoint.notes } } : {}),
291
385
  ...(pageWarnings.length > 0 ? { warnings: pageWarnings } : {}),
@@ -1,7 +1,14 @@
1
1
  import { meetingSuccess, schemaOf, SCHEMA_DATETIME_MINUTE } from '../types.js';
2
2
  import { EB_BASE, ebBusinessSuccess, extractEbRows, queryBody } from './factory.js';
3
3
  import { readCache, writeCache, cacheScopeSuffix } from './cache.js';
4
+ // 会议室台账缓存 24h(用户 2026-10-30 明确要求:不要为了新鲜度把 TTL 缩到 15 分钟,太频繁)。
5
+ // 「刚导入/新建的会议室看不到」靠两层兜底解决,而不是靠缩短 TTL:
6
+ // ① **建会链路强制拉最新**(create.flow.prepare / quick / create.suggest 传 refreshRooms: true)——
7
+ // 这些是"要下单"的场景,宁可多一次请求也必须准;
8
+ // ② **候选为空且来自缓存 → 自动强制重拉重算**(见下方 candidates.length === 0 分支)。
9
+ // 纯查询(room.recommend)走 24h 缓存保持快;缓存超过 STALE_NOTICE_MS 会在 warnings 提示可用 refresh 刷新。
4
10
  const ROOMS_TTL_MS = 24 * 60 * 60 * 1000;
11
+ const STALE_NOTICE_MS = 4 * 60 * 60 * 1000;
5
12
  const FREQUENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
6
13
  const FREQUENT_WINDOW_DAYS = 180;
7
14
  const PAGE_SIZE = 100;
@@ -176,6 +183,11 @@ export async function recommendRooms(env, input) {
176
183
  const topN = Math.max(1, Math.min(10, input.topN ?? 3));
177
184
  const roomData = await loadRooms(env, { refresh: input.refreshRooms === true });
178
185
  warnings.push(...roomData.warnings);
186
+ // 缓存 4 小时以上 → 提示数据可能过时(不是报错,只是让调用方知道"刚加的会议室可能还看不到")。
187
+ if (roomData.fromCache && (roomData.ageSeconds ?? 0) * 1000 > STALE_NOTICE_MS) {
188
+ warnings.push(`会议室台账来自 ${Math.round((roomData.ageSeconds ?? 0) / 3600)} 小时前的缓存(共 ${roomData.rooms.length} 间)。`
189
+ + '如果最近新建/导入/封存过会议室,请带 `refresh: true` 重新查询,否则看到的是旧清单。');
190
+ }
179
191
  const frequent = await loadFrequentRooms(env, { refresh: input.refreshFrequent === true });
180
192
  warnings.push(...frequent.warnings);
181
193
  const busy = await fetchBusyRoomIds(env, input.beginDatetime, input.endDatetime);
@@ -195,12 +207,15 @@ export async function recommendRooms(env, input) {
195
207
  if (!input.includeBusy)
196
208
  continue;
197
209
  }
210
+ let belowMinCapacity = false;
198
211
  if (memberCount !== null) {
199
- // allow_maxnum = 0 视为「不限」,超出上限或低于下限都算不满足(下限只在配置了才判)
212
+ // allow_maxnum = 0 视为「不限」。上限不够 → 真的坐不下,必须排除。
200
213
  if (room.maxNum > 0 && room.maxNum < memberCount)
201
214
  continue;
215
+ // ⚠️ 下限(建议最少人数)不满足**不再直接排除**:小会议(1~2 人)会被所有"最少 2 人"的房间全判死,
216
+ // 出现「可用 N 间却 0 候选」的死角(2026-10-30 真机)。改为降级候选并标注,让用户自己判断。
202
217
  if (room.minNum > 0 && room.minNum > memberCount)
203
- continue;
218
+ belowMinCapacity = true;
204
219
  }
205
220
  if (requirement.roomAttr && room.roomAttr !== requirement.roomAttr)
206
221
  continue;
@@ -228,6 +243,10 @@ export async function recommendRooms(env, input) {
228
243
  reasons.push(`容量 ${room.maxNum} 人`);
229
244
  score += 5;
230
245
  }
246
+ if (belowMinCapacity) {
247
+ reasons.push(`该房间建议至少 ${room.minNum} 人使用(本次 ${memberCount} 人,未达建议下限,如可用请自行确认)`);
248
+ score -= 15;
249
+ }
231
250
  if (requirement.placeKeyword) {
232
251
  reasons.push(`地点匹配「${requirement.placeKeyword}」`);
233
252
  score += 10;
@@ -265,6 +284,21 @@ export async function recommendRooms(env, input) {
265
284
  return leftCap - rightCap;
266
285
  return left.name.localeCompare(right.name, 'zh-Hans-CN');
267
286
  });
287
+ // ⛔ 候选为空且数据来自缓存 → 缓存可能已过时(刚导入会议室、房间刚解封、容量刚改),
288
+ // 静默用旧缓存回答会让用户得出"系统里没有会议室/只有一间"的错误结论(2026-10-30 真机事故)。
289
+ // 这里自动强制重拉一次再算;只有重拉后仍为空才如实报告。不递归:refreshRooms=true 时直接跳过。
290
+ if (candidates.length === 0 && roomData.fromCache && input.refreshRooms !== true) {
291
+ const retried = await recommendRooms(env, { ...input, refreshRooms: true, refreshFrequent: true });
292
+ return {
293
+ ...retried,
294
+ meta: { ...retried.meta, cacheRetried: true },
295
+ warnings: [
296
+ `缓存里的会议室凑不出候选(缓存 ${Math.round((roomData.ageSeconds ?? 0) / 60)} 分钟前,`
297
+ + `${roomData.rooms.length} 间)→ 已强制重新拉取台账后重算`,
298
+ ...retried.warnings,
299
+ ],
300
+ };
301
+ }
268
302
  if (candidates.length === 0) {
269
303
  if (roomData.rooms.length === 0) {
270
304
  // 2026-10-29 真机事故:0 间时只说了"放宽条件",agent 转头去调标准版 meeting.room.list,
@@ -114,6 +114,10 @@ export async function tryDegradeEbFailure(operation, error, host, ctx) {
114
114
  // 手动开关:确认某环境就是 EB 版时,关掉自动降级避免反复实探。
115
115
  if (process.env.WEAVER_MEETING_ENV_AUTOFALLBACK === '0')
116
116
  return null;
117
+ // 固化自用(2026-10-30):`WEAVER_MEETING_LOCK_MODE=eb` 明确锁死 EB —— 生产环境一次接口抖动
118
+ // 就把模式改成 standard,后面整条链路走标准版(会议室台账、字段口径都不一样),比直接报错更难排查。
119
+ if (String(process.env.WEAVER_MEETING_LOCK_MODE ?? '').trim().toLowerCase() === 'eb')
120
+ return null;
117
121
  const type = String(error?.type ?? '');
118
122
  const subtype = String(error?.subtype ?? '');
119
123
  if (NO_FALLBACK_TYPES.has(type) || NO_FALLBACK_SUBTYPES.has(subtype))
@@ -136,6 +136,12 @@ async function probeAndPersist(host, ctx, knownEbError, options = {}) {
136
136
  */
137
137
  export async function resolveMeetingEnvMode(host, ctx, options = {}) {
138
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
+ }
139
145
  // ① refresh=true 才实探(EB 优先,失败回退标准版,结论写进缓存)。
140
146
  if (options.refresh === true) {
141
147
  return { ...(await probeAndPersist(host, ctx)), fromCache: false };
@@ -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
  // 落款单位的实际来源,便于调用方判断是否需要人工改写
@@ -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
  }