weaver-work-cli 0.1.33 → 0.1.38

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 (126) hide show
  1. package/dist/chunks/{archive-FVFY6GZN.js → archive-PDV724YK.js} +5 -4
  2. package/dist/chunks/{archive-oa-NW3OD4UV.js → archive-oa-4XE4MNXQ.js} +4 -3
  3. package/dist/chunks/{asset-7QJKMIDV.js → asset-PDL2QE2N.js} +8 -7
  4. package/dist/chunks/{attachment-content-OSAVA6K3.js → attachment-content-MDDGOE5E.js} +3 -3
  5. package/dist/chunks/{blog-I7SDPNW4.js → blog-UYR3EUXW.js} +5 -4
  6. package/dist/chunks/{caizhilian-O37ZIQNY.js → caizhilian-LG2X5NMF.js} +5 -4
  7. package/dist/chunks/{calendar-7KXE4ED3.js → calendar-FG4BAZHE.js} +4 -3
  8. package/dist/chunks/{chunk-Q6LWB5HX.js → chunk-6FTTSA45.js} +4 -3
  9. package/dist/chunks/{chunk-MQ2I5AA3.js → chunk-6LTFIFLM.js} +3 -2
  10. package/dist/chunks/{chunk-WRRJ4GG2.js → chunk-CORFRD7Q.js} +4 -3
  11. package/dist/chunks/{chunk-WY74UBJ6.js → chunk-DMV2TOMH.js} +2194 -240
  12. package/dist/chunks/{chunk-MEEFGWTM.js → chunk-ELYDWBZJ.js} +135 -10
  13. package/dist/chunks/{chunk-EEWPQE3O.js → chunk-FUWVHMVV.js} +7 -6
  14. package/dist/chunks/{chunk-HEL5XTYI.js → chunk-GGJFMNJM.js} +26 -4
  15. package/dist/chunks/{chunk-KAS45KNR.js → chunk-IZUDA353.js} +1 -1
  16. package/dist/chunks/{chunk-2FLWXFVS.js → chunk-J7USS3ED.js} +4 -4
  17. package/dist/chunks/{chunk-IHUYP6LO.js → chunk-JSMFPO43.js} +1 -1
  18. package/dist/chunks/{chunk-RBGOMK4Y.js → chunk-KKRL3REV.js} +5 -4
  19. package/dist/chunks/{chunk-OCU4RLHC.js → chunk-NI6GIPIM.js} +195 -36
  20. package/dist/chunks/{chunk-PRHJGPMA.js → chunk-NR6RKXPA.js} +2 -2
  21. package/dist/chunks/{chunk-D223PTXU.js → chunk-OJQUJZQF.js} +33 -7
  22. package/dist/chunks/{chunk-ILMBNVSG.js → chunk-UJJD4QWF.js} +4 -3
  23. package/dist/chunks/{chunk-6ATGWRZM.js → chunk-UZZBA6PV.js} +177 -14
  24. package/dist/chunks/{chunk-GFPXQIY2.js → chunk-VRYHGG6O.js} +2 -2
  25. package/dist/chunks/{chunk-NHA62QD7.js → chunk-XJY7G4DD.js} +3 -3
  26. package/dist/chunks/{chunk-VBCFFFGT.js → chunk-Z32KWY6G.js} +4 -1
  27. package/dist/chunks/{conflict-SFNEVJY5.js → conflict-BQZEQ4SP.js} +2 -2
  28. package/dist/chunks/contract-XIRAFTEV.js +9111 -0
  29. package/dist/chunks/{cowork-CKWYHXYB.js → cowork-BLG62BL2.js} +46 -26
  30. package/dist/chunks/{ebuilder-form-KOLJRVZD.js → ebuilder-form-NUKG5QW4.js} +3 -3
  31. package/dist/chunks/{ehr-WFOC3B4D.js → ehr-2STW5ILH.js} +174 -10
  32. package/dist/chunks/{esb-LJZTN663.js → esb-UUDQUNKY.js} +4 -4
  33. package/dist/chunks/{fna-F7QRIIZC.js → fna-6RCY4NU5.js} +15 -9
  34. package/dist/chunks/{host-VQ6JJXGQ.js → host-7RXEDMAH.js} +3 -3
  35. package/dist/chunks/{host-KXPTEWFS.js → host-EJZFROU3.js} +3 -3
  36. package/dist/chunks/{host-BCGUVEWX.js → host-FWDOSRH5.js} +3 -3
  37. package/dist/chunks/{host-DGE2TZD4.js → host-RHM3NPXV.js} +3 -3
  38. package/dist/chunks/{host-BQBRHVR3.js → host-UFF33TWJ.js} +3 -3
  39. package/dist/chunks/{hrm-NI6XAKW5.js → hrm-PKJ6ELSH.js} +3 -3
  40. package/dist/chunks/{im-7TRTF3KT.js → im-YJAXS7CO.js} +3 -3
  41. package/dist/chunks/{invoice-AOXWJKYT.js → invoice-HWVSXGX6.js} +3 -3
  42. package/dist/chunks/{jiuchuanhui-7NQWEZAW.js → jiuchuanhui-L7VKILZB.js} +4 -3
  43. package/dist/chunks/{lazy-dispatch-RNS4EE4S.js → lazy-dispatch-NMYEIHRH.js} +4 -4
  44. package/dist/chunks/{mail-VDHJAB4O.js → mail-B42RRU7G.js} +46 -24
  45. package/dist/chunks/{meeting-GA7XCNNP.js → meeting-CFYVBJSS.js} +77 -12
  46. package/dist/chunks/{okr-TQIRERPQ.js → okr-GCLF6V7W.js} +4 -3
  47. package/dist/chunks/{plan-4RY4LN2P.js → plan-ZVL7GBRI.js} +4 -3
  48. package/dist/chunks/{project-D6XT2OC5.js → project-4YWAAE2K.js} +4 -3
  49. package/dist/chunks/{registry-QKMI43Q6.js → registry-EP4VTHZS.js} +8 -8
  50. package/dist/chunks/{registry-JHRIDQ3A.js → registry-FRJ6H7BT.js} +4 -4
  51. package/dist/chunks/{registry-VOGKFFTR.js → registry-SG2PGVCF.js} +2 -2
  52. package/dist/chunks/{room-recommend-D4EUOOWY.js → room-recommend-R3JVUUAC.js} +4 -4
  53. package/dist/chunks/{schedule-XPFBHPCV.js → schedule-NIJBTYDO.js} +4 -4
  54. package/dist/chunks/{workflow-HLBZRWIY.js → workflow-BB24DFBY.js} +11 -4
  55. package/dist/index.js +31 -26
  56. package/dist/internal/e10/auth-recovery.js +114 -1
  57. package/dist/internal/e10/client.js +16 -2
  58. package/dist/internal/e10/fetch-timing.js +31 -0
  59. package/dist/internal/e10/request-runtime.js +108 -7
  60. package/dist/shortcuts/archive/host.js +3 -2
  61. package/dist/shortcuts/archive-oa/host.js +2 -1
  62. package/dist/shortcuts/asset/host.js +6 -5
  63. package/dist/shortcuts/blog/host.js +3 -2
  64. package/dist/shortcuts/caizhilian/host.js +3 -2
  65. package/dist/shortcuts/calendar/host.js +2 -1
  66. package/dist/shortcuts/contract/host.js +2 -1
  67. package/dist/shortcuts/cowork/host.js +10 -1
  68. package/dist/shortcuts/cowork/operations/types.js +17 -2
  69. package/dist/shortcuts/ebuilder-form/host.js +3 -1
  70. package/dist/shortcuts/ehr/host.js +2 -1
  71. package/dist/shortcuts/ehr/operations/recruit-write.js +35 -0
  72. package/dist/shortcuts/ehr/operations/recruit.js +147 -0
  73. package/dist/shortcuts/esb/host.js +2 -1
  74. package/dist/shortcuts/fna/host.js +2 -1
  75. package/dist/shortcuts/fna/operations/expense-chart.js +19 -7
  76. package/dist/shortcuts/hrm/host.js +2 -1
  77. package/dist/shortcuts/im/host.js +3 -2
  78. package/dist/shortcuts/index.js +5 -0
  79. package/dist/shortcuts/invoice/host.js +5 -4
  80. package/dist/shortcuts/jiuchuanhui/host.js +2 -1
  81. package/dist/shortcuts/mail/host.js +10 -1
  82. package/dist/shortcuts/mail/operations/types.js +17 -2
  83. package/dist/shortcuts/meeting/host.js +3 -2
  84. package/dist/shortcuts/meeting/index.js +79 -1
  85. package/dist/shortcuts/meeting/invitation/text.js +71 -0
  86. package/dist/shortcuts/meeting/manifest.js +10 -0
  87. package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -0
  88. package/dist/shortcuts/meeting/operations/eb/conflict.js +6 -1
  89. package/dist/shortcuts/meeting/operations/eb/create-flow.js +2105 -169
  90. package/dist/shortcuts/meeting/operations/eb/crm-search.js +478 -0
  91. package/dist/shortcuts/meeting/operations/eb/factory.js +161 -8
  92. package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
  93. package/dist/shortcuts/meeting/operations/eb/room-recommend.js +36 -2
  94. package/dist/shortcuts/meeting/operations/eb-fallback.js +4 -0
  95. package/dist/shortcuts/meeting/operations/env.js +6 -0
  96. package/dist/shortcuts/meeting/operations/invitation.js +200 -28
  97. package/dist/shortcuts/okr/host.js +2 -1
  98. package/dist/shortcuts/plan/host.js +2 -1
  99. package/dist/shortcuts/project/host.js +2 -1
  100. package/dist/shortcuts/workflow/endpoints.js +8 -3
  101. package/dist/shortcuts/workflow/operations/form.js +341 -36
  102. package/dist/shortcuts/workflow/operations.js +23 -1
  103. package/docs/ehr.md +1 -1
  104. package/docs/fna.md +2 -1
  105. package/package.json +1 -1
  106. package/skill-template/business-info.json +18 -0
  107. package/skill-template/domains/cowork.md +14 -0
  108. package/skills/weaver-e10-cowork/SKILL.md +101 -0
  109. package/skills/weaver-e10-cowork/references/cowork-read.md +90 -0
  110. package/skills/weaver-e10-cowork/references/cowork-write.md +131 -0
  111. package/skills/weaver-e10-cowork/references/safety-boundaries.md +45 -0
  112. package/skills/weaver-e10-jucailin/SKILL.md +21 -3
  113. package/skills/weaver-e10-jucailin/references/recruit-read.md +25 -0
  114. package/skills/weaver-e10-jucailin/references/recruit-write.md +13 -0
  115. package/skills/weaver-e10-jucailin/references/salary.md +15 -11
  116. package/skills/weaver-e10-mail/SKILL.md +12 -9
  117. package/skills/weaver-e10-meeting/SKILL.md +499 -300
  118. package/skills/weaver-e10-meeting/references/meeting-eb.md +334 -0
  119. package/skills/weaver-e10-meeting/references/meeting-invitation.md +64 -6
  120. package/skills/weaver-e10-meeting.zip +0 -0
  121. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  122. package/skills/weaver-e10-qiyecheng/SKILL.md +5 -2
  123. package/skills/weaver-e10-qiyecheng/references/catalog.md +2 -2
  124. package/skills/weaver-e10-qiyecheng/references/expense-chart.md +15 -1
  125. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +22 -21
  126. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
@@ -7,6 +7,57 @@ import { isWriteKind } from './catalog.js';
7
7
  export const EB_BASE = '/api/ebuilder/form/formdata/v2';
8
8
  const EB_READ_OP = { list: 'getFormDataList', detail: 'getFormDataByPk', get: 'getFormDataList' };
9
9
  const EB_WRITE_OP = { create: 'saveFormData', update: 'updateFormData', delete: 'deleteFormData' };
10
+ /** 详情里的人员类字段:值可能是员工 id,补一份姓名回显,别让调用方把 id 当成"没填/填错了"。 */
11
+ const PERSONNEL_FIELDS = [
12
+ 'caller', 'contacter', 'mt_creator', 'creator', 'updater',
13
+ 'hrm_members', 'attend_user', 'inviters', 'reporting_user', 'topic_presenter',
14
+ ];
15
+ /**
16
+ * 人员字段 id → 姓名。
17
+ *
18
+ * ⛔ 2026-10-31 用户反馈「联系人那一栏显示成一串数字」:detail 接口回的是**原始值**
19
+ * (页面建的单存 id、工作流建的单存姓名文本),调用方看到 id 就以为字段没填好。
20
+ * 这里只补一份 `personnelText`(字段名 → 姓名),**不动原字段**,避免破坏既有消费方。
21
+ */
22
+ async function buildPersonnelText(env, data) {
23
+ const ids = [];
24
+ for (const key of PERSONNEL_FIELDS) {
25
+ const raw = String(data?.[key] ?? '').trim();
26
+ // 已经是姓名文本(含中文/括号)就不用查;纯数字与逗号才算 id 列表
27
+ if (raw === '' || !/^[\d,\s]+$/u.test(raw))
28
+ continue;
29
+ ids.push(...raw.split(',').map((one) => one.trim()).filter((one) => one !== ''));
30
+ }
31
+ if (ids.length === 0)
32
+ return {};
33
+ const names = new Map();
34
+ try {
35
+ const { hrmEmployeeOperations } = await import('../../../hrm/operations/employee.js');
36
+ const batch = hrmEmployeeOperations.find((operation) => operation.name === 'hrm.employee.batch');
37
+ if (batch) {
38
+ const result = await batch.handler({ ids: [...new Set(ids)] }, env);
39
+ for (const row of (result?.data?.rows ?? result?.data?.userList ?? [])) {
40
+ const id = String(row.id ?? '').trim();
41
+ const name = String(row.name ?? '').trim();
42
+ if (id && name)
43
+ names.set(id, name);
44
+ }
45
+ }
46
+ }
47
+ catch {
48
+ return {};
49
+ }
50
+ const out = {};
51
+ for (const key of PERSONNEL_FIELDS) {
52
+ const raw = String(data?.[key] ?? '').trim();
53
+ if (raw === '' || !/^[\d,\s]+$/u.test(raw))
54
+ continue;
55
+ const mapped = raw.split(',').map((one) => names.get(one.trim()) ?? one.trim()).filter((one) => one !== '');
56
+ if (mapped.length > 0)
57
+ out[key] = mapped.join(',');
58
+ }
59
+ return out;
60
+ }
10
61
  const CONTINUATION_TTL_SECONDS = 600;
11
62
  /**
12
63
  * EB 响应成功判定。
@@ -71,10 +122,12 @@ function isNetworkError(error) {
71
122
  * 该缺陷曾导致 CLI 的 EB 读操作全线失败,并连带使 `meeting.env.detect` 的 EB 冒烟失败、
72
123
  * 环境被误判为 standard 版。
73
124
  */
74
- export function queryBody(userId, page) {
125
+ export function queryBody(userId, page, mainTable) {
75
126
  return {
76
127
  datajson: {
77
- mainTable: {},
128
+ // 传入字段即过滤条件(e-builder openApi 文档:getFormDataList 的传入字段挂 mainTable 节点)。
129
+ // 空对象 = 不过滤。
130
+ mainTable: mainTable ?? {},
78
131
  // ⚠️ 分页必须放在 body 的 pageInfo 里(值为字符串):只放 URL query 时 EB 会忽略,
79
132
  // 恒定返回首页 10 条(2026-09-21 真机暴露:pageSize=60 仍只回 10 条)。
80
133
  ...(page ? { pageInfo: { pageNo: String(page.pageNo), pageSize: String(page.pageSize) } } : {}),
@@ -82,6 +135,27 @@ export function queryBody(userId, page) {
82
135
  },
83
136
  };
84
137
  }
138
+ /**
139
+ * 模糊回退最多翻几页。EB 的 pageSize 不生效(恒定回 10 条),所以翻页数直接决定能覆盖多少数据。
140
+ * 环境变量 `WEAVER_MEETING_EB_FALLBACK_MAX_PAGES` 可覆盖,默认 30(≈300 条)。
141
+ */
142
+ function readFallbackMaxPages() {
143
+ const raw = Number(process.env.WEAVER_MEETING_EB_FALLBACK_MAX_PAGES ?? '');
144
+ if (Number.isFinite(raw) && raw >= 1)
145
+ return Math.min(Math.floor(raw), 100);
146
+ return 30;
147
+ }
148
+ /** 从入参里挑出「服务端过滤字段」(mainTableParams),没传的不进 body。 */
149
+ function pickMainTableFilters(args, names) {
150
+ const out = {};
151
+ for (const name of names ?? []) {
152
+ const value = args[name];
153
+ if (value === undefined || value === null || value === '')
154
+ continue;
155
+ out[name] = typeof value === 'string' ? value : String(value);
156
+ }
157
+ return out;
158
+ }
85
159
  /** 详情接口 body(getFormDataByPk):顶层 mainTable 传 id。 */
86
160
  function detailBody(userId, id) {
87
161
  return { datajson: { mainTable: { id }, header: { operator: userId } } };
@@ -138,11 +212,24 @@ function buildReadInputSchema(endpoint) {
138
212
  for (const param of endpoint.queryParams ?? []) {
139
213
  properties[param] = { type: 'string', description: `EB 筛选参数(URL 占位符名 ${param})` };
140
214
  }
215
+ for (const param of endpoint.mainTableParams ?? []) {
216
+ properties[param] = {
217
+ type: 'string',
218
+ description: `按 ${param} 过滤(走 body datajson.mainTable,服务端**精确匹配**;`
219
+ + `传部分词时服务端会返回 0 条,CLI 会自动翻页扫全量并按「包含」重新筛,结果一样是模糊的)`,
220
+ };
221
+ }
141
222
  properties.pageNo = { type: 'integer', minimum: 1, description: '页码,默认 1' };
142
223
  properties.pageSize = { type: 'integer', minimum: 1, maximum: 200, description: '每页条数,默认 20' };
143
224
  if (endpoint.countPk) {
144
225
  properties.withCount = { type: 'boolean', description: '是否先查总数(默认 true,条件与列表一致)' };
145
226
  }
227
+ if ((endpoint.mainTableParams ?? []).length > 0) {
228
+ properties.noFallback = {
229
+ type: 'boolean',
230
+ description: '跳过「模糊兜底」:服务端精确匹配不到时,不再翻页扫全量(默认 false=会扫,保证部分词也能查到)',
231
+ };
232
+ }
146
233
  }
147
234
  return {
148
235
  type: 'object',
@@ -194,7 +281,15 @@ function buildReadOperation(endpoint) {
194
281
  detailNodes[key] = value;
195
282
  }
196
283
  }
197
- return meetingSuccess(endpoint.operation, { id, data, ...(Object.keys(detailNodes).length > 0 ? { details: detailNodes } : {}) }, endpoint.notes ? { meta: { notes: endpoint.notes } } : {});
284
+ const personnelText = endpoint.operation.startsWith('meeting.')
285
+ ? await buildPersonnelText(env, data)
286
+ : {};
287
+ return meetingSuccess(endpoint.operation, {
288
+ id,
289
+ data,
290
+ ...(Object.keys(personnelText).length > 0 ? { personnelText } : {}),
291
+ ...(Object.keys(detailNodes).length > 0 ? { details: detailNodes } : {}),
292
+ }, endpoint.notes ? { meta: { notes: endpoint.notes } } : {});
198
293
  }
199
294
  const query = {};
200
295
  for (const param of endpoint.queryParams ?? []) {
@@ -216,6 +311,9 @@ function buildReadOperation(endpoint) {
216
311
  }
217
312
  const pageNo = typeof args.pageNo === 'number' ? args.pageNo : 1;
218
313
  const pageSize = typeof args.pageSize === 'number' ? args.pageSize : 20;
314
+ // 服务端过滤条件:count 与 list 必须用**同一份**,否则 total 与当页行数对不上
315
+ // (总数是不带过滤的、列表是过滤后的,用户会看到"共 155 间"却只回 1 条)。
316
+ const mainTableFilters = pickMainTableFilters(args, endpoint.mainTableParams);
219
317
  let total;
220
318
  if (endpoint.countPk && args.withCount !== false) {
221
319
  const countResponse = await env.host.request({
@@ -223,7 +321,7 @@ function buildReadOperation(endpoint) {
223
321
  path: `${EB_BASE}/getFormDataCount/${endpoint.countPk}`,
224
322
  operation: endpoint.operation,
225
323
  query,
226
- body: queryBody(session.userId),
324
+ body: queryBody(session.userId, undefined, mainTableFilters),
227
325
  acceptBusiness,
228
326
  });
229
327
  total = extractEbCount(countResponse);
@@ -234,13 +332,61 @@ function buildReadOperation(endpoint) {
234
332
  operation: endpoint.operation,
235
333
  query: { ...query, pageNo, pageSize },
236
334
  // 分页同时进 body.pageInfo:EB 只认 body 里的 pageInfo,仅放 URL query 会被忽略。
237
- body: queryBody(session.userId, { pageNo, pageSize }),
335
+ body: queryBody(session.userId, { pageNo, pageSize }, mainTableFilters),
238
336
  acceptBusiness,
239
337
  });
240
- const { rows, total: listTotal } = extractEbRows(response);
338
+ const { rows: rawRows, total: listTotal } = extractEbRows(response);
339
+ // 服务端过滤不可靠时的兜底:2026-10-30 实测 e-cology 生产环境,
340
+ // `getFormDataCount/query_meeting_room_count` **完全忽略** mainTable / URL query 的过滤条件
341
+ // (无过滤 155、room_name=测试 155、room_name=zzz不存在 155,全是 155),
342
+ // 而 list 又可能只回当页 —— 只靠服务端过滤会得到"total=155 但一条都不匹配"的假结果。
343
+ // 所以只要调用方传了筛选字段,就在本地按同一条件再筛一遍,保证结果正确。
344
+ const filterKeys = Object.keys(mainTableFilters);
345
+ const matchesFilters = (row) => {
346
+ const main = (row?.mainTable ?? row ?? {});
347
+ return filterKeys.every((key) => {
348
+ const cell = main[key];
349
+ if (cell === undefined || cell === null)
350
+ return false;
351
+ return String(cell).includes(String(mainTableFilters[key]));
352
+ });
353
+ };
354
+ let rows = filterKeys.length === 0 ? rawRows : rawRows.filter(matchesFilters);
355
+ let fallback;
356
+ // ⚠️ 服务端是**精确匹配**,模糊词拿不到数据(2026-10-30 实测 e-cology:
357
+ // room_name=测试会议室1 → 1 条 ✅;room_name=测试 / 重庆 → 0 条 ❌,
358
+ // 但全量里明明有「重庆会议室一/二」)。
359
+ // 传了筛选却一条没命中时,回退:不带过滤翻页拉全量,在客户端按 contains 再筛一遍。
360
+ // 只在首轮 0 条时触发(精确命中时零额外开销);`noFallback:true` 可关。
361
+ if (filterKeys.length > 0 && rows.length === 0 && args.noFallback !== true) {
362
+ const maxPages = readFallbackMaxPages();
363
+ const collected = [];
364
+ let pages = 0;
365
+ for (let p = 1; p <= maxPages; p += 1) {
366
+ const pageResponse = await env.host.request({
367
+ method: 'POST',
368
+ path,
369
+ operation: endpoint.operation,
370
+ query: { ...query, pageNo: p, pageSize },
371
+ // 不带 mainTableFilters:服务端只会做精确匹配,带上就又变成 0 条了
372
+ body: queryBody(session.userId, { pageNo: p, pageSize }),
373
+ acceptBusiness,
374
+ });
375
+ const { rows: pageRows } = extractEbRows(pageResponse);
376
+ pages = p;
377
+ if (pageRows.length === 0)
378
+ break; // 翻到空页 = 到底了
379
+ collected.push(...pageRows);
380
+ if (p >= maxPages)
381
+ break;
382
+ }
383
+ fallback = { pages, scanned: collected.length };
384
+ rows = collected.filter(matchesFilters);
385
+ }
241
386
  // count 接口与实际行数不一致时(本租户 `query_mt_topic_count` 恒返回 0,而 list 有数据),
242
387
  // 以"实际取到的行数"为下限,避免出现 total=0 却有数据的自相矛盾结果。
243
- const resolvedTotal = total ?? listTotal ?? rows.length;
388
+ // ⚠️ 有筛选时 count 不可信(见上),一律以本地筛完的行数为准。
389
+ const resolvedTotal = filterKeys.length > 0 ? rows.length : (total ?? listTotal ?? rawRows.length);
244
390
  // 结果后处理(如决议任务进度 / 签到回执出席汇总):只**增**一个字段,原始 rows 原样返回。
245
391
  // 统一注入 `page`(本页 + 总数),避免各实现把"当页行数"误当成总数(2026-09-22 踩到:
246
392
  // 签到全量 261 条、pageSize=200 时,summary 里写的 total 只有 200)。
@@ -249,6 +395,8 @@ function buildReadOperation(endpoint) {
249
395
  pageSize,
250
396
  pageCount: rows.length,
251
397
  total: Math.max(resolvedTotal, rows.length),
398
+ // 模糊回退时告知调用方:结果是客户端筛出来的,扫了多少条
399
+ ...(fallback ? { fallback } : {}),
252
400
  };
253
401
  const summarized = endpoint.summarizeRows
254
402
  ? endpoint.summarizeRows(rows, {
@@ -278,6 +426,11 @@ function buildReadOperation(endpoint) {
278
426
  pageWarnings.push(`本页返回 ${rows.length} 条,共 ${effectiveTotal} 条:还有 ${missing} 条未取到。`
279
427
  + `需要全量请翻页(pageNo=${pageNo + 1} 起)或加大 pageSize,或收窄筛选条件`);
280
428
  }
429
+ if (fallback) {
430
+ pageWarnings.push(`服务端只支持「精确匹配」,按 ${filterKeys.join('/')} 过滤后 0 条;`
431
+ + `已翻页扫描全量 ${fallback.scanned} 条(${fallback.pages} 页)在客户端按「包含」重新筛选,命中 ${rows.length} 条。`
432
+ + `传 noFallback:true 可跳过这次扫描`);
433
+ }
281
434
  return meetingSuccess(endpoint.operation, {
282
435
  total: effectiveTotal,
283
436
  pageNo,
@@ -285,7 +438,7 @@ function buildReadOperation(endpoint) {
285
438
  count: rows.length,
286
439
  ...(progress ? { progress } : {}),
287
440
  rows,
288
- filters: query,
441
+ filters: { ...query, ...mainTableFilters },
289
442
  }, {
290
443
  ...(endpoint.notes ? { meta: { notes: endpoint.notes } } : {}),
291
444
  ...(pageWarnings.length > 0 ? { warnings: pageWarnings } : {}),
@@ -6,6 +6,7 @@ import { ebMeetingAssociation } from './association.js';
6
6
  import { ebRoomRecommendOperation, recommendRooms, rememberFrequentRoom } from './room-recommend.js';
7
7
  import { meetingValidationError } from '../../errors.js';
8
8
  import { meetingEbScheduleOperations } from './schedule.js';
9
+ import { meetingEbCrmOperations } from './crm-search.js';
9
10
  import { meetingEbCreateFlowOperations } from './create-flow.js';
10
11
  /**
11
12
  * 会议 EB 版(eBuilder)operations。
@@ -167,6 +168,7 @@ export const meetingEbOperations = [
167
168
  ...meetingEbConflictOperations,
168
169
  ...meetingEbDecisionOperations,
169
170
  ...meetingEbScheduleOperations,
171
+ ...meetingEbCrmOperations,
170
172
  ...meetingEbCreateFlowOperations,
171
173
  ebRoomRecommendOperation,
172
174
  ];
@@ -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 };