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.
- package/dist/chunks/{archive-FVFY6GZN.js → archive-PDV724YK.js} +5 -4
- package/dist/chunks/{archive-oa-NW3OD4UV.js → archive-oa-4XE4MNXQ.js} +4 -3
- package/dist/chunks/{asset-7QJKMIDV.js → asset-PDL2QE2N.js} +8 -7
- package/dist/chunks/{attachment-content-OSAVA6K3.js → attachment-content-MDDGOE5E.js} +3 -3
- package/dist/chunks/{blog-I7SDPNW4.js → blog-UYR3EUXW.js} +5 -4
- package/dist/chunks/{caizhilian-O37ZIQNY.js → caizhilian-LG2X5NMF.js} +5 -4
- package/dist/chunks/{calendar-7KXE4ED3.js → calendar-FG4BAZHE.js} +4 -3
- package/dist/chunks/{chunk-Q6LWB5HX.js → chunk-6FTTSA45.js} +4 -3
- package/dist/chunks/{chunk-MQ2I5AA3.js → chunk-6LTFIFLM.js} +3 -2
- package/dist/chunks/{chunk-WRRJ4GG2.js → chunk-CORFRD7Q.js} +4 -3
- package/dist/chunks/{chunk-WY74UBJ6.js → chunk-DMV2TOMH.js} +2194 -240
- package/dist/chunks/{chunk-MEEFGWTM.js → chunk-ELYDWBZJ.js} +135 -10
- package/dist/chunks/{chunk-EEWPQE3O.js → chunk-FUWVHMVV.js} +7 -6
- package/dist/chunks/{chunk-HEL5XTYI.js → chunk-GGJFMNJM.js} +26 -4
- package/dist/chunks/{chunk-KAS45KNR.js → chunk-IZUDA353.js} +1 -1
- package/dist/chunks/{chunk-2FLWXFVS.js → chunk-J7USS3ED.js} +4 -4
- package/dist/chunks/{chunk-IHUYP6LO.js → chunk-JSMFPO43.js} +1 -1
- package/dist/chunks/{chunk-RBGOMK4Y.js → chunk-KKRL3REV.js} +5 -4
- package/dist/chunks/{chunk-OCU4RLHC.js → chunk-NI6GIPIM.js} +195 -36
- package/dist/chunks/{chunk-PRHJGPMA.js → chunk-NR6RKXPA.js} +2 -2
- package/dist/chunks/{chunk-D223PTXU.js → chunk-OJQUJZQF.js} +33 -7
- package/dist/chunks/{chunk-ILMBNVSG.js → chunk-UJJD4QWF.js} +4 -3
- package/dist/chunks/{chunk-6ATGWRZM.js → chunk-UZZBA6PV.js} +177 -14
- package/dist/chunks/{chunk-GFPXQIY2.js → chunk-VRYHGG6O.js} +2 -2
- package/dist/chunks/{chunk-NHA62QD7.js → chunk-XJY7G4DD.js} +3 -3
- package/dist/chunks/{chunk-VBCFFFGT.js → chunk-Z32KWY6G.js} +4 -1
- package/dist/chunks/{conflict-SFNEVJY5.js → conflict-BQZEQ4SP.js} +2 -2
- package/dist/chunks/contract-XIRAFTEV.js +9111 -0
- package/dist/chunks/{cowork-CKWYHXYB.js → cowork-BLG62BL2.js} +46 -26
- package/dist/chunks/{ebuilder-form-KOLJRVZD.js → ebuilder-form-NUKG5QW4.js} +3 -3
- package/dist/chunks/{ehr-WFOC3B4D.js → ehr-2STW5ILH.js} +174 -10
- package/dist/chunks/{esb-LJZTN663.js → esb-UUDQUNKY.js} +4 -4
- package/dist/chunks/{fna-F7QRIIZC.js → fna-6RCY4NU5.js} +15 -9
- package/dist/chunks/{host-VQ6JJXGQ.js → host-7RXEDMAH.js} +3 -3
- package/dist/chunks/{host-KXPTEWFS.js → host-EJZFROU3.js} +3 -3
- package/dist/chunks/{host-BCGUVEWX.js → host-FWDOSRH5.js} +3 -3
- package/dist/chunks/{host-DGE2TZD4.js → host-RHM3NPXV.js} +3 -3
- package/dist/chunks/{host-BQBRHVR3.js → host-UFF33TWJ.js} +3 -3
- package/dist/chunks/{hrm-NI6XAKW5.js → hrm-PKJ6ELSH.js} +3 -3
- package/dist/chunks/{im-7TRTF3KT.js → im-YJAXS7CO.js} +3 -3
- package/dist/chunks/{invoice-AOXWJKYT.js → invoice-HWVSXGX6.js} +3 -3
- package/dist/chunks/{jiuchuanhui-7NQWEZAW.js → jiuchuanhui-L7VKILZB.js} +4 -3
- package/dist/chunks/{lazy-dispatch-RNS4EE4S.js → lazy-dispatch-NMYEIHRH.js} +4 -4
- package/dist/chunks/{mail-VDHJAB4O.js → mail-B42RRU7G.js} +46 -24
- package/dist/chunks/{meeting-GA7XCNNP.js → meeting-CFYVBJSS.js} +77 -12
- package/dist/chunks/{okr-TQIRERPQ.js → okr-GCLF6V7W.js} +4 -3
- package/dist/chunks/{plan-4RY4LN2P.js → plan-ZVL7GBRI.js} +4 -3
- package/dist/chunks/{project-D6XT2OC5.js → project-4YWAAE2K.js} +4 -3
- package/dist/chunks/{registry-QKMI43Q6.js → registry-EP4VTHZS.js} +8 -8
- package/dist/chunks/{registry-JHRIDQ3A.js → registry-FRJ6H7BT.js} +4 -4
- package/dist/chunks/{registry-VOGKFFTR.js → registry-SG2PGVCF.js} +2 -2
- package/dist/chunks/{room-recommend-D4EUOOWY.js → room-recommend-R3JVUUAC.js} +4 -4
- package/dist/chunks/{schedule-XPFBHPCV.js → schedule-NIJBTYDO.js} +4 -4
- package/dist/chunks/{workflow-HLBZRWIY.js → workflow-BB24DFBY.js} +11 -4
- package/dist/index.js +31 -26
- package/dist/internal/e10/auth-recovery.js +114 -1
- package/dist/internal/e10/client.js +16 -2
- package/dist/internal/e10/fetch-timing.js +31 -0
- package/dist/internal/e10/request-runtime.js +108 -7
- package/dist/shortcuts/archive/host.js +3 -2
- package/dist/shortcuts/archive-oa/host.js +2 -1
- package/dist/shortcuts/asset/host.js +6 -5
- package/dist/shortcuts/blog/host.js +3 -2
- package/dist/shortcuts/caizhilian/host.js +3 -2
- package/dist/shortcuts/calendar/host.js +2 -1
- package/dist/shortcuts/contract/host.js +2 -1
- package/dist/shortcuts/cowork/host.js +10 -1
- package/dist/shortcuts/cowork/operations/types.js +17 -2
- package/dist/shortcuts/ebuilder-form/host.js +3 -1
- package/dist/shortcuts/ehr/host.js +2 -1
- package/dist/shortcuts/ehr/operations/recruit-write.js +35 -0
- package/dist/shortcuts/ehr/operations/recruit.js +147 -0
- package/dist/shortcuts/esb/host.js +2 -1
- package/dist/shortcuts/fna/host.js +2 -1
- package/dist/shortcuts/fna/operations/expense-chart.js +19 -7
- package/dist/shortcuts/hrm/host.js +2 -1
- package/dist/shortcuts/im/host.js +3 -2
- package/dist/shortcuts/index.js +5 -0
- package/dist/shortcuts/invoice/host.js +5 -4
- package/dist/shortcuts/jiuchuanhui/host.js +2 -1
- package/dist/shortcuts/mail/host.js +10 -1
- package/dist/shortcuts/mail/operations/types.js +17 -2
- package/dist/shortcuts/meeting/host.js +3 -2
- package/dist/shortcuts/meeting/index.js +79 -1
- package/dist/shortcuts/meeting/invitation/text.js +71 -0
- package/dist/shortcuts/meeting/manifest.js +10 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +5 -0
- package/dist/shortcuts/meeting/operations/eb/conflict.js +6 -1
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +2105 -169
- package/dist/shortcuts/meeting/operations/eb/crm-search.js +478 -0
- package/dist/shortcuts/meeting/operations/eb/factory.js +161 -8
- package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +36 -2
- package/dist/shortcuts/meeting/operations/eb-fallback.js +4 -0
- package/dist/shortcuts/meeting/operations/env.js +6 -0
- package/dist/shortcuts/meeting/operations/invitation.js +200 -28
- package/dist/shortcuts/okr/host.js +2 -1
- package/dist/shortcuts/plan/host.js +2 -1
- package/dist/shortcuts/project/host.js +2 -1
- package/dist/shortcuts/workflow/endpoints.js +8 -3
- package/dist/shortcuts/workflow/operations/form.js +341 -36
- package/dist/shortcuts/workflow/operations.js +23 -1
- package/docs/ehr.md +1 -1
- package/docs/fna.md +2 -1
- package/package.json +1 -1
- package/skill-template/business-info.json +18 -0
- package/skill-template/domains/cowork.md +14 -0
- package/skills/weaver-e10-cowork/SKILL.md +101 -0
- package/skills/weaver-e10-cowork/references/cowork-read.md +90 -0
- package/skills/weaver-e10-cowork/references/cowork-write.md +131 -0
- package/skills/weaver-e10-cowork/references/safety-boundaries.md +45 -0
- package/skills/weaver-e10-jucailin/SKILL.md +21 -3
- package/skills/weaver-e10-jucailin/references/recruit-read.md +25 -0
- package/skills/weaver-e10-jucailin/references/recruit-write.md +13 -0
- package/skills/weaver-e10-jucailin/references/salary.md +15 -11
- package/skills/weaver-e10-mail/SKILL.md +12 -9
- package/skills/weaver-e10-meeting/SKILL.md +499 -300
- package/skills/weaver-e10-meeting/references/meeting-eb.md +334 -0
- package/skills/weaver-e10-meeting/references/meeting-invitation.md +64 -6
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-qiyecheng/SKILL.md +5 -2
- package/skills/weaver-e10-qiyecheng/references/catalog.md +2 -2
- package/skills/weaver-e10-qiyecheng/references/expense-chart.md +15 -1
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +22 -21
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|