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.
- package/README.md +2 -2
- package/dist/chunks/{archive-UMGX72K6.js → archive-XBVH7JU7.js} +2 -2
- package/dist/chunks/{archive-oa-N2IYBLF3.js → archive-oa-MZM3EKFQ.js} +2 -2
- package/dist/chunks/{asset-GLEGKFG5.js → asset-4XO7UOUW.js} +2 -2
- package/dist/chunks/{attachment-content-AQNX5N3S.js → attachment-content-VXSWANAC.js} +3 -2
- package/dist/chunks/{blog-E44OTVDA.js → blog-PGZKOPSC.js} +2 -2
- package/dist/chunks/{caizhilian-DQUFZXZL.js → caizhilian-BH4ZVGUN.js} +2 -2
- package/dist/chunks/{calendar-JKPIRKFD.js → calendar-MXEMTIO4.js} +2 -2
- package/dist/chunks/{chunk-QWY6PGL6.js → chunk-2LV74IWP.js} +361 -56
- package/dist/chunks/chunk-3SO6QL6U.js +73 -0
- package/dist/chunks/chunk-5DQEEGCC.js +229 -0
- package/dist/chunks/{chunk-G3QJULO2.js → chunk-6LLJFKJE.js} +1 -1
- package/dist/chunks/{chunk-C6R3B2ML.js → chunk-BPE37FPB.js} +35 -7
- package/dist/chunks/chunk-BYATLLEF.js +68 -0
- package/dist/chunks/chunk-BYBSCN4X.js +932 -0
- package/dist/chunks/{chunk-CVZ2JOSW.js → chunk-F746NKNF.js} +1 -1
- package/dist/chunks/{chunk-GCG2NMWR.js → chunk-HXJQI5NM.js} +4 -2
- package/dist/chunks/{chunk-YRVDW4WN.js → chunk-I6QGIBDM.js} +2 -2
- package/dist/chunks/{chunk-H7J4DC5R.js → chunk-JCSN3JGM.js} +2 -2
- package/dist/chunks/{chunk-2GA5ZIQL.js → chunk-JNG42JKG.js} +30 -1
- package/dist/chunks/{chunk-DW6AUUKS.js → chunk-JUXVYKII.js} +9 -4
- package/dist/chunks/{chunk-6RKD4DRZ.js → chunk-JYKABYCV.js} +2 -2
- package/dist/chunks/{chunk-6EFWDL7Y.js → chunk-NCOVUAZK.js} +2 -2
- package/dist/chunks/{chunk-E4Z23GHM.js → chunk-QHFANTX7.js} +1114 -904
- package/dist/chunks/{ebuilder-form-V5FG2EHC.js → chunk-QYGO5EE2.js} +22 -1148
- package/dist/chunks/{chunk-IURUFGZK.js → chunk-T72N74ZT.js} +50 -33
- package/dist/chunks/{chunk-H6XSTSLM.js → chunk-TC6MZAO3.js} +137 -204
- package/dist/chunks/{chunk-WKI2FBIQ.js → chunk-VPUTVEMC.js} +737 -341
- package/dist/chunks/{chunk-WSKBSQ2U.js → chunk-VPXOPQJI.js} +2 -2
- package/dist/chunks/chunk-WXFPWCXG.js +774 -0
- package/dist/chunks/{chunk-4YC43QQ4.js → chunk-XMGSYD6S.js} +4 -4
- package/dist/chunks/conflict-JAB2LJEZ.js +21 -0
- package/dist/chunks/continuation-RN6PZN2M.js +11 -0
- package/dist/chunks/contract-XI2EEIZU.js +9110 -0
- package/dist/chunks/cowork-SCPGLBA4.js +2527 -0
- package/dist/chunks/ebuilder-form-EYLHNOTA.js +121 -0
- package/dist/chunks/{ehr-HMFY6VIL.js → ehr-EAP77IMT.js} +1344 -27
- package/dist/chunks/{esb-ZYSVB7SG.js → esb-2TGGXEE7.js} +4 -4
- package/dist/chunks/{fna-B7RKPAZD.js → fna-2X7ZE7BU.js} +13 -8
- package/dist/chunks/{host-OCH2BCCV.js → host-C6EL2RKL.js} +3 -3
- package/dist/chunks/{host-45WED2KK.js → host-I4NRK4MX.js} +3 -3
- package/dist/chunks/{host-I4Z6BFYX.js → host-Q4LZGCRU.js} +3 -3
- package/dist/chunks/host-UEI5FGDM.js +23 -0
- package/dist/chunks/{host-KNZ52LJZ.js → host-XCLDDXKQ.js} +3 -3
- package/dist/chunks/{hrm-2KAZGUHI.js → hrm-YXERJM76.js} +3 -3
- package/dist/chunks/{im-ACJJWH4U.js → im-A4VZN2TQ.js} +88 -12
- package/dist/chunks/{invoice-ZK3WCX2U.js → invoice-2Z6YPMIS.js} +3 -3
- package/dist/chunks/{jiuchuanhui-5PQD3IL2.js → jiuchuanhui-AR5GMKZB.js} +2 -2
- package/dist/chunks/{lazy-dispatch-JQXBW3GM.js → lazy-dispatch-QXW7P4X3.js} +4 -4
- package/dist/chunks/{mail-S2HDDXO2.js → mail-7GZ6CPBJ.js} +45 -24
- package/dist/chunks/{meeting-QVN3I37L.js → meeting-3NMGA3MB.js} +278 -43
- package/dist/chunks/{okr-QNG4VC2Q.js → okr-BZ6XUOAG.js} +2 -2
- package/dist/chunks/operations-5QL3AECO.js +11 -0
- package/dist/chunks/{plan-A25EMW6U.js → plan-DR5CMUYP.js} +2 -2
- package/dist/chunks/{project-G6XV66DZ.js → project-6J63MK33.js} +2 -2
- package/dist/chunks/{registry-DC6KR3NH.js → registry-KDD7DHNK.js} +4 -4
- package/dist/chunks/{registry-2VLUAESA.js → registry-OZSMDVPP.js} +2 -2
- package/dist/chunks/{registry-7C5UMIPL.js → registry-TKX2E7C7.js} +2 -2
- package/dist/chunks/{registry-TI3CIUXF.js → registry-VRTNC6UR.js} +8 -6
- package/dist/chunks/{room-recommend-WRTHO6Y2.js → room-recommend-XCWRMB5P.js} +4 -3
- package/dist/chunks/{schedule-KFIITGSK.js → schedule-IVBICP32.js} +4 -3
- package/dist/chunks/{workflow-DJ7SYWAF.js → workflow-D74NFK3C.js} +179 -6
- package/dist/index.js +35 -25
- package/dist/internal/e10/auth-recovery.js +14 -4
- package/dist/internal/e10/fetch-timing.js +31 -0
- package/dist/internal/e10/request-runtime.js +5 -0
- package/dist/shortcuts/contract/host.js +34 -59
- package/dist/shortcuts/cowork/continuation.js +70 -0
- package/dist/shortcuts/cowork/errors.js +59 -0
- package/dist/shortcuts/cowork/host.js +180 -0
- package/dist/shortcuts/cowork/index.js +166 -0
- package/dist/shortcuts/cowork/manifest.js +65 -0
- package/dist/shortcuts/cowork/operations/discuss-write.js +277 -0
- package/dist/shortcuts/cowork/operations/item-write.js +380 -0
- package/dist/shortcuts/cowork/operations/member-write.js +283 -0
- package/dist/shortcuts/cowork/operations/permission-write.js +286 -0
- package/dist/shortcuts/cowork/operations/read.js +423 -0
- package/dist/shortcuts/cowork/operations/registry.js +39 -0
- package/dist/shortcuts/cowork/operations/review-write.js +157 -0
- package/dist/shortcuts/cowork/operations/shared.js +215 -0
- package/dist/shortcuts/cowork/operations/types.js +39 -0
- package/dist/shortcuts/cowork/operations.js +19 -0
- package/dist/shortcuts/ehr/host.js +4 -1
- package/dist/shortcuts/ehr/operations/recruit-candidate-import.js +757 -0
- package/dist/shortcuts/ehr/operations/recruit-position.js +367 -0
- package/dist/shortcuts/ehr/operations/recruit-stats.js +133 -0
- package/dist/shortcuts/ehr/operations/recruit-write.js +43 -16
- package/dist/shortcuts/ehr/operations/recruit.js +182 -10
- package/dist/shortcuts/ehr/operations/registry.js +6 -0
- package/dist/shortcuts/fna/operations/expense-chart.js +19 -7
- package/dist/shortcuts/im/diagnostics.js +97 -0
- package/dist/shortcuts/im/host.js +58 -31
- package/dist/shortcuts/im/index.js +7 -2
- package/dist/shortcuts/im/operations/at-mention.js +127 -0
- package/dist/shortcuts/im/operations/file-user.js +10 -2
- package/dist/shortcuts/im/operations/group-read.js +26 -5
- package/dist/shortcuts/im/operations/group-write.js +35 -28
- package/dist/shortcuts/im/operations/msg-sync.js +7 -2
- package/dist/shortcuts/im/operations/msg-write.js +76 -10
- package/dist/shortcuts/im/operations/quote-mention.js +119 -0
- package/dist/shortcuts/im/operations/session.js +1 -1
- package/dist/shortcuts/im/operations/shared.js +10 -0
- package/dist/shortcuts/im/operations/types.js +13 -0
- package/dist/shortcuts/index.js +10 -0
- package/dist/shortcuts/mail/host.js +12 -1
- package/dist/shortcuts/mail/operations/types.js +17 -2
- package/dist/shortcuts/meeting/host.js +27 -34
- package/dist/shortcuts/meeting/index.js +79 -1
- package/dist/shortcuts/meeting/manifest.js +10 -0
- package/dist/shortcuts/meeting/operations/create.js +4 -1
- package/dist/shortcuts/meeting/operations/eb/catalog.js +10 -0
- package/dist/shortcuts/meeting/operations/eb/conflict.js +84 -8
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +1195 -140
- package/dist/shortcuts/meeting/operations/eb/factory.js +170 -8
- package/dist/shortcuts/meeting/operations/eb/index.js +16 -3
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +50 -5
- package/dist/shortcuts/meeting/operations/eb-fallback.js +163 -0
- package/dist/shortcuts/meeting/operations/env.js +99 -46
- package/dist/shortcuts/meeting/operations/form-fields.js +110 -0
- package/dist/shortcuts/meeting/operations/invitation.js +79 -20
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/standard-guard.js +71 -0
- package/dist/shortcuts/meeting/operations.js +16 -0
- package/dist/shortcuts/workflow/endpoints.js +8 -3
- package/dist/shortcuts/workflow/index.js +110 -1
- package/dist/shortcuts/workflow/operations/form.js +204 -36
- package/dist/shortcuts/workflow/operations/read.js +104 -23
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +448 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +106 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +47 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +30 -23
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +1 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +2 -1
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +2 -4
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +138 -181
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +12 -7
- package/dist/shortcuts/workflow/operations.js +23 -1
- package/docs/e10-auth.md +1 -1
- package/docs/ehr.md +1 -1
- package/docs/fna.md +2 -1
- package/docs/im.md +8 -1
- package/docs/todo-smart-convert/README.md +1 -1
- package/docs/todo-smart-convert/data-contract.md +2 -2
- package/docs/todo-smart-convert/subgroup-rules.md +6 -0
- package/package.json +1 -1
- package/skill-template/business-info.json +18 -0
- package/skill-template/domains/cowork.md +14 -0
- package/skill-template/domains/yimiaoban.md +3 -3
- package/skill-template/skill-template.md +1 -1
- package/skills/weaver-e10-blog/SKILL.md +5 -5
- package/skills/weaver-e10-caizhilian/SKILL.md +5 -5
- package/skills/weaver-e10-calendar/SKILL.md +5 -5
- 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-esb/SKILL.md +5 -5
- package/skills/weaver-e10-hrm/SKILL.md +5 -5
- package/skills/weaver-e10-jinchengda/SKILL.md +2 -2
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -5
- package/skills/weaver-e10-jucailin/SKILL.md +38 -14
- package/skills/weaver-e10-jucailin/references/recruit-create.md +104 -0
- 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 +17 -14
- package/skills/weaver-e10-meeting/SKILL.md +442 -46
- package/skills/weaver-e10-meeting/references/meeting-eb.md +135 -3
- package/skills/weaver-e10-meeting/references/meeting-invitation.md +28 -2
- package/skills/weaver-e10-meeting/references/meeting-queries.md +9 -6
- package/skills/weaver-e10-meeting/references/meeting-verification.md +8 -6
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-okr/SKILL.md +5 -5
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-plan/SKILL.md +5 -5
- 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-shared/SKILL.md +12 -12
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +4 -4
- package/skills/weaver-e10-wenshuding/SKILL.md +5 -5
- package/skills/weaver-e10-workflow/SKILL.md +29 -5
- package/skills/weaver-e10-workflow/aigroup/default.json +88 -0
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +66 -14
- package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +1 -1
- package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -1
- package/skills/weaver-e10-workflow/references/workflow-query.md +12 -10
- package/skills/weaver-e10-workflow/references/workflow-view.md +2 -2
- package/skills/weaver-e10-workflow/tools/todo-serve.cjs +239 -0
- package/skills/weaver-e10-workflow/{scripts → tools}/todo-summary.cjs +218 -28
- package/skills/weaver-e10-yepiaotong/SKILL.md +10 -8
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +10 -10
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +40 -37
- package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -4
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +27 -38
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +4 -4
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +37 -29
- package/skills/weaver-e10-yimiaoban/SKILL.md +156 -33
- package/skills/weaver-e10-yimiaoban/references/ding-write.md +1 -1
- package/skills/weaver-e10-yimiaoban/references/error-codes.md +5 -1
- package/skills/weaver-e10-yimiaoban/references/group-read.md +5 -1
- package/skills/weaver-e10-yimiaoban/references/group-write.md +6 -4
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -0
- package/skills/weaver-e10-yimiaoban/references/msg-write.md +34 -2
- package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +5 -2
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +79 -58
- package/skills/weaver-e10-yimiaoban/references/user-remind.md +2 -2
- package/skills/weaver-e10-ziguanjia/SKILL.md +5 -5
|
@@ -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
|
-
|
|
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, {
|
|
@@ -270,11 +359,19 @@ function buildReadOperation(endpoint) {
|
|
|
270
359
|
// 所以这里只提示翻页/加大 pageSize,不要写成"服务端限制"。
|
|
271
360
|
const effectiveTotal = Math.max(resolvedTotal, rows.length);
|
|
272
361
|
const pageWarnings = [];
|
|
362
|
+
if (effectiveTotal === 0 && endpoint.emptyHint) {
|
|
363
|
+
pageWarnings.push(endpoint.emptyHint);
|
|
364
|
+
}
|
|
273
365
|
if (effectiveTotal > rows.length) {
|
|
274
366
|
const missing = effectiveTotal - rows.length;
|
|
275
367
|
pageWarnings.push(`本页返回 ${rows.length} 条,共 ${effectiveTotal} 条:还有 ${missing} 条未取到。`
|
|
276
368
|
+ `需要全量请翻页(pageNo=${pageNo + 1} 起)或加大 pageSize,或收窄筛选条件`);
|
|
277
369
|
}
|
|
370
|
+
if (fallback) {
|
|
371
|
+
pageWarnings.push(`服务端只支持「精确匹配」,按 ${filterKeys.join('/')} 过滤后 0 条;`
|
|
372
|
+
+ `已翻页扫描全量 ${fallback.scanned} 条(${fallback.pages} 页)在客户端按「包含」重新筛选,命中 ${rows.length} 条。`
|
|
373
|
+
+ `传 noFallback:true 可跳过这次扫描`);
|
|
374
|
+
}
|
|
278
375
|
return meetingSuccess(endpoint.operation, {
|
|
279
376
|
total: effectiveTotal,
|
|
280
377
|
pageNo,
|
|
@@ -282,7 +379,7 @@ function buildReadOperation(endpoint) {
|
|
|
282
379
|
count: rows.length,
|
|
283
380
|
...(progress ? { progress } : {}),
|
|
284
381
|
rows,
|
|
285
|
-
filters: query,
|
|
382
|
+
filters: { ...query, ...mainTableFilters },
|
|
286
383
|
}, {
|
|
287
384
|
...(endpoint.notes ? { meta: { notes: endpoint.notes } } : {}),
|
|
288
385
|
...(pageWarnings.length > 0 ? { warnings: pageWarnings } : {}),
|
|
@@ -426,6 +523,26 @@ export function normalizeEbWrite(endpoint, args, userId) {
|
|
|
426
523
|
if (hours !== undefined)
|
|
427
524
|
mainTable.mt_hours = hours;
|
|
428
525
|
}
|
|
526
|
+
// 自定义字段透传(2026-10-28):租户在表单上新增的字段不在内置白名单内,
|
|
527
|
+
// 调用方用 extraFields 按**真实字段名**(config.dataKey,或字段 id)传入,原样并入主表。
|
|
528
|
+
// ⚠️ 这里不做字段名校验 —— 校验了就等于又造一份白名单,而租户加了什么字段只有他的环境知道。
|
|
529
|
+
// 但必须如实回报"哪些进去了、哪些被内置字段挤掉了",避免调用方以为写上了。
|
|
530
|
+
let extraApplied = [];
|
|
531
|
+
let extraSkipped = [];
|
|
532
|
+
const extra = args.extraFields;
|
|
533
|
+
if (extra && typeof extra === 'object' && !Array.isArray(extra)) {
|
|
534
|
+
for (const [rawKey, value] of Object.entries(extra)) {
|
|
535
|
+
const key = String(rawKey).trim();
|
|
536
|
+
if (!key)
|
|
537
|
+
continue;
|
|
538
|
+
if (mainTable[key] !== undefined) {
|
|
539
|
+
extraSkipped.push(key);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
mainTable[key] = value;
|
|
543
|
+
extraApplied.push(key);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
429
546
|
const required = endpoint.kind === 'delete'
|
|
430
547
|
? (endpoint.batchDeleteFields ?? ['id'])
|
|
431
548
|
: endpoint.requiredFields ?? [];
|
|
@@ -472,7 +589,14 @@ export function normalizeEbWrite(endpoint, args, userId) {
|
|
|
472
589
|
operateType[node] = 'cover';
|
|
473
590
|
}
|
|
474
591
|
}
|
|
475
|
-
return {
|
|
592
|
+
return {
|
|
593
|
+
mainTable,
|
|
594
|
+
details,
|
|
595
|
+
operateType,
|
|
596
|
+
...(extraApplied.length > 0 || extraSkipped.length > 0
|
|
597
|
+
? { extraFields: { applied: extraApplied, skipped: extraSkipped } }
|
|
598
|
+
: {}),
|
|
599
|
+
};
|
|
476
600
|
}
|
|
477
601
|
function buildWriteInputSchema(endpoint, includeConfirm) {
|
|
478
602
|
const properties = {};
|
|
@@ -480,6 +604,15 @@ function buildWriteInputSchema(endpoint, includeConfirm) {
|
|
|
480
604
|
properties.confirm = { const: true };
|
|
481
605
|
properties.continuation = { type: 'string', minLength: 1 };
|
|
482
606
|
}
|
|
607
|
+
// 建会/改会:参会人时间冲突时默认阻断,由用户确认后带 conflictAck=true 再提交。
|
|
608
|
+
if (endpoint.interfacePk === 'create_meeting' || endpoint.interfacePk === 'change_meeting') {
|
|
609
|
+
properties.conflictAck = {
|
|
610
|
+
type: 'boolean',
|
|
611
|
+
description: '参会人时间冲突时仍按原时间建会/改期(默认 false)。'
|
|
612
|
+
+ '冲突时 CLI 会先拒绝并返回冲突明细与当天可选时段,请**用选项方式让用户确认**;'
|
|
613
|
+
+ '用户确认继续后才带 conflictAck=true 重新提交。',
|
|
614
|
+
};
|
|
615
|
+
}
|
|
483
616
|
if (endpoint.kind === 'delete') {
|
|
484
617
|
if (endpoint.batchDeleteFields && endpoint.batchDeleteFields.length > 0) {
|
|
485
618
|
// 批量删除:按业务条件删(如 delete_roomusage 的 mt_id),不是主键
|
|
@@ -524,6 +657,15 @@ function buildWriteInputSchema(endpoint, includeConfirm) {
|
|
|
524
657
|
},
|
|
525
658
|
};
|
|
526
659
|
}
|
|
660
|
+
// 租户自定义字段透传:表单上后加的字段(如参会客户、联系方式)不在内置清单里,
|
|
661
|
+
// 只有这个口子能带进去。字段名必须是表单真实 dataKey 或字段 id —— 先用
|
|
662
|
+
// `meeting.form.fields` 读出来再填,不要凭中文名猜。
|
|
663
|
+
properties.extraFields = {
|
|
664
|
+
type: 'object',
|
|
665
|
+
description: '自定义字段透传,形如 {"字段名": 值}:用于租户在表单上新增、不在本接口内置字段清单里的字段'
|
|
666
|
+
+ '(例如参会客户 crm_members、自建的联系方式字段)。字段名必须是表单真实 dataKey 或字段 id'
|
|
667
|
+
+ '(用 meeting.form.fields 查);与内置字段同名时以内置字段为准,并在 warnings 里说明',
|
|
668
|
+
};
|
|
527
669
|
if (endpoint.detailCover) {
|
|
528
670
|
properties.detail1Mode = {
|
|
529
671
|
type: 'string',
|
|
@@ -597,6 +739,18 @@ function buildWriteOperations(endpoint, preflight, afterWrite) {
|
|
|
597
739
|
// EB 版建会缺少强制冲突检测会重复占用会议室/参会人撞会,故 prepare 阶段由钩子补齐。
|
|
598
740
|
const extras = {};
|
|
599
741
|
const warnings = preflight ? await preflight({ endpoint, args, payload, env, extras }) : [];
|
|
742
|
+
// 自定义字段如实回报:服务端未配置该字段时会**静默丢弃**(不报错),
|
|
743
|
+
// 不提示的话调用方会以为写上了。
|
|
744
|
+
const extraInfo = payload.extraFields;
|
|
745
|
+
if (extraInfo && extraInfo.applied.length > 0) {
|
|
746
|
+
warnings.push(`已透传 ${extraInfo.applied.length} 个自定义字段:${extraInfo.applied.join('、')}`
|
|
747
|
+
+ '——这些不在内置字段清单里,按你给的原样提交;若服务端表单没配这个字段会被静默丢弃,'
|
|
748
|
+
+ '提交后请回读确认(可用 meeting.form.fields 核对真实字段名)');
|
|
749
|
+
}
|
|
750
|
+
if (extraInfo && extraInfo.skipped.length > 0) {
|
|
751
|
+
warnings.push(`这些自定义字段与内置字段重名,已忽略你的取值、沿用内置字段:${extraInfo.skipped.join('、')}`
|
|
752
|
+
+ '(要传自定义口径请改用不冲突的字段名)');
|
|
753
|
+
}
|
|
600
754
|
// 支持明细覆盖的接口(如 update_mt_decision)默认是「更新追加」:真机实测同 sort_number 也会**新增一行**。
|
|
601
755
|
// 调用方最容易在这里理解错,故 prepare 就明确提示,而不是等回读发现多了一行。
|
|
602
756
|
if (endpoint.detailCover && Object.keys(payload.details).length > 0 && payload.operateType.detail1 !== 'cover') {
|
|
@@ -648,6 +802,14 @@ function buildWriteOperations(endpoint, preflight, afterWrite) {
|
|
|
648
802
|
// 带上两侧快照:`target_changed` 否则无从判断到底哪个字段变了(排查成本极高)。
|
|
649
803
|
throw meetingValidationError('target_changed', '提交内容与 prepare 时的快照不一致,请重新执行 prepare', { expected: lease.payload, actual: payload });
|
|
650
804
|
}
|
|
805
|
+
// ⛔ 建会路径闸门在 apply 侧**重新校验**(2026-10-29 真机事故):
|
|
806
|
+
// 闸门此前只在 prepare 的 preflight 里,apply 只认 continuation 签名 ⇒ 只要 prepare 那次判定
|
|
807
|
+
// 侥幸通过(或多份设置里取到了开关为空的那份),10 分钟有效期内就能直连建出会议。
|
|
808
|
+
// 管控开关可能在 prepare 之后被管理员改过,所以这里不信任快照,重新读一次。
|
|
809
|
+
if (endpoint.interfacePk === 'create_meeting') {
|
|
810
|
+
const { fetchEbAppSettings, assertEbCreatePathAllowed } = await import('./conflict.js');
|
|
811
|
+
assertEbCreatePathAllowed(await fetchEbAppSettings(env, applyName));
|
|
812
|
+
}
|
|
651
813
|
const pkQuery = writePkQuery(endpoint, payload);
|
|
652
814
|
let response;
|
|
653
815
|
try {
|
|
@@ -46,8 +46,13 @@ import { meetingEbCreateFlowOperations } from './create-flow.js';
|
|
|
46
46
|
const ebAutoRoomPreflight = async (context) => {
|
|
47
47
|
const { extras } = context;
|
|
48
48
|
const warnings = [];
|
|
49
|
-
const enabled = String(process.env.WEAVER_MEETING_AUTO_ROOM ?? '1') !== '0';
|
|
50
49
|
const isCreate = context.endpoint.kind === 'create' && context.endpoint.interfacePk === 'create_meeting';
|
|
50
|
+
// 会议室推荐拿不到候选时,必须把原因显式暴露给调用方(2026-10-29 真机事故):
|
|
51
|
+
// 此前只 push 一条 warning,prepare 照常返回 ok,智能体当没发生 → 会议建成「会议室未指定」,
|
|
52
|
+
// 用户看到的就是"根本没给我推荐会议室"。
|
|
53
|
+
const markRoomUnavailable = (reason, meta) => {
|
|
54
|
+
extras.roomUnavailable = { reason, ...(meta ? { meta } : {}) };
|
|
55
|
+
};
|
|
51
56
|
// 会议室策略(用户口径):**不替用户决定** —— 没填会议室就列候选让用户选。
|
|
52
57
|
// - WEAVER_MEETING_AUTO_ROOM=1 显式开启"自动选推荐间"(自动化场景)
|
|
53
58
|
// - WEAVER_MEETING_AUTO_ROOM=0 不推荐也不拦(线上会/无会议室场景)
|
|
@@ -98,16 +103,24 @@ const ebAutoRoomPreflight = async (context) => {
|
|
|
98
103
|
}
|
|
99
104
|
else {
|
|
100
105
|
warnings.push(...result.warnings);
|
|
106
|
+
markRoomUnavailable(`没有可用的会议室候选(共 ${result.meta?.roomsTotal ?? 0} 间,可用 ${result.meta?.roomsUsable ?? 0} 间,`
|
|
107
|
+
+ `该时段被占用 ${result.meta?.roomsBusy ?? 0} 间)`, result.meta);
|
|
108
|
+
warnings.push('⚠️ 本次建会**没有会议室**:推荐不出候选(见 roomUnavailable.reason)。'
|
|
109
|
+
+ '必须用选项方式让用户确认地点 —— 改指定具体会议室(传 address)、改为线上会议,'
|
|
110
|
+
+ '或明确接受「地点待定」。不要默默建成一场没有地点的会。');
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
catch (error) {
|
|
104
114
|
if (error?.subtype === 'room_selection_required')
|
|
105
115
|
throw error;
|
|
106
|
-
|
|
116
|
+
const reason = String(error?.message ?? error).slice(0, 200);
|
|
117
|
+
markRoomUnavailable(`会议室推荐失败:${reason}`);
|
|
118
|
+
warnings.push(`⚠️ 会议室推荐失败(${reason}):本次**不填会议室继续**,请与用户确认地点,不要默认「地点待定」。`);
|
|
107
119
|
}
|
|
108
120
|
}
|
|
109
121
|
else {
|
|
110
|
-
|
|
122
|
+
markRoomUnavailable('会议开始/结束时间不完整,无法按时段推荐');
|
|
123
|
+
warnings.push('会议开始/结束时间不完整,跳过会议室推荐(本次不会填会议室,请与用户确认地点)');
|
|
111
124
|
}
|
|
112
125
|
}
|
|
113
126
|
const conflictWarnings = await ebMeetingWritePreflight(context);
|
|
@@ -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,10 +284,36 @@ 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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
303
|
+
if (roomData.rooms.length === 0) {
|
|
304
|
+
// 2026-10-29 真机事故:0 间时只说了"放宽条件",agent 转头去调标准版 meeting.room.list,
|
|
305
|
+
// 把标准版那套 74 间会议室外加给了用户。空结果必须把"不是环境问题、更不许换标准版"说清楚。
|
|
306
|
+
warnings.push('EB 会议室表(`uf_mt_room_base`)查不到任何记录(roomsTotal=0)。可能是:① 该环境确实还没建过会议室;'
|
|
307
|
+
+ '② 会议应用的「会议室列表」接口/视图未配置(`query_meeting_room_list` 读不到数据,但表里可能有)。'
|
|
308
|
+
+ '**绝对不要改用 `meeting.room.list`** —— 那是标准版会议应用的会议室,与 EB 是两套完全不通的数据,'
|
|
309
|
+
+ '拿给用户等于凭空捏造房间。请如实告知用户"EB 会议室查不到",并请管理员检查会议室配置,'
|
|
310
|
+
+ '或让用户直接提供会议室名称/改用线上会议。');
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
warnings.push(`没有满足条件的空闲会议室(共 ${roomData.rooms.length} 间,可用 ${usable.length} 间,`
|
|
314
|
+
+ `该时段被占用 ${busyCount} 间${memberCount ? `,人数 ${memberCount}` : ''})`
|
|
315
|
+
+ '——请放宽人数/地点/设备要求,或改为线上会议。');
|
|
316
|
+
}
|
|
272
317
|
}
|
|
273
318
|
return {
|
|
274
319
|
best: candidates[0] ?? null,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { MeetingCliError } from '../errors.js';
|
|
2
|
+
import { degradeMeetingEnvToStandard } from './env.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// EB 优先 + 失败降级到标准版
|
|
5
|
+
//
|
|
6
|
+
// 会议有两种实现(EB 版 eBuilder 表单接口 / 标准版 Java REST),此前靠调用方先跑
|
|
7
|
+
// `meeting.env.detect` 决定走哪一套 —— 每次调用前都要两路冒烟,且一次偶发抖动就会把
|
|
8
|
+
// 整个环境误判成另一套。现在改为**惰性降级**:默认按 EB 执行,EB 真走不通时才实探
|
|
9
|
+
// 标准版,成功后把 `standard` 写进 env 缓存(`meeting/env-mode.json`),
|
|
10
|
+
// 后续同一 baseUrl 直接走标准版。
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* EB operation → 标准版对等 operation。
|
|
14
|
+
* - 字符串:可直接改用的标准版 operation;
|
|
15
|
+
* - `null`:标准版**明确没有**对应能力(不要引导用户去试)。
|
|
16
|
+
*/
|
|
17
|
+
const EB_TO_STANDARD = {
|
|
18
|
+
'meeting.eb.meeting.list': 'meeting.list',
|
|
19
|
+
'meeting.eb.meeting.detail': 'meeting.detail.field',
|
|
20
|
+
'meeting.eb.meeting.create.prepare': 'meeting.create.prepare',
|
|
21
|
+
'meeting.eb.meeting.create.apply': 'meeting.create.apply',
|
|
22
|
+
'meeting.eb.meeting.change.prepare': 'meeting.change.prepare',
|
|
23
|
+
'meeting.eb.meeting.change.apply': 'meeting.change.apply',
|
|
24
|
+
'meeting.eb.meeting.delete.prepare': 'meeting.delete.prepare',
|
|
25
|
+
'meeting.eb.meeting.delete.apply': 'meeting.delete.apply',
|
|
26
|
+
'meeting.eb.room.list': 'meeting.room.list',
|
|
27
|
+
'meeting.eb.room.detail': null,
|
|
28
|
+
'meeting.eb.room.create.prepare': null,
|
|
29
|
+
'meeting.eb.room.create.apply': null,
|
|
30
|
+
'meeting.eb.room.update.prepare': null,
|
|
31
|
+
'meeting.eb.room.update.apply': null,
|
|
32
|
+
'meeting.eb.room.delete.prepare': null,
|
|
33
|
+
'meeting.eb.room.delete.apply': null,
|
|
34
|
+
'meeting.eb.roomUsage.list': 'meeting.room.usage',
|
|
35
|
+
'meeting.eb.sign.list': 'meeting.sign.list',
|
|
36
|
+
'meeting.eb.sign.mark.prepare': 'meeting.sign.mark.prepare',
|
|
37
|
+
'meeting.eb.sign.mark.apply': 'meeting.sign.mark.apply',
|
|
38
|
+
'meeting.eb.receipt.list': 'meeting.receipt.list',
|
|
39
|
+
'meeting.eb.receipt.update.prepare': 'meeting.receipt.update.prepare',
|
|
40
|
+
'meeting.eb.receipt.update.apply': 'meeting.receipt.update.apply',
|
|
41
|
+
'meeting.eb.type.list': 'meeting.type.list',
|
|
42
|
+
'meeting.eb.conflict.room': 'meeting.conflict.room',
|
|
43
|
+
'meeting.eb.conflict.member': 'meeting.conflict.member',
|
|
44
|
+
'meeting.eb.conflict.availability': null,
|
|
45
|
+
'meeting.eb.baseSet.get': 'meeting.calendar.base',
|
|
46
|
+
'meeting.eb.minute.create.prepare': null,
|
|
47
|
+
'meeting.eb.minute.create.apply': null,
|
|
48
|
+
'meeting.eb.topic.list': null,
|
|
49
|
+
'meeting.eb.decision.create.prepare': null,
|
|
50
|
+
'meeting.eb.decision.create.apply': null,
|
|
51
|
+
'meeting.eb.decisionTask.list': null,
|
|
52
|
+
'meeting.eb.schedule.free': null,
|
|
53
|
+
'meeting.eb.room.recommend': null,
|
|
54
|
+
'meeting.eb.meeting.create.flow.prepare': null,
|
|
55
|
+
'meeting.eb.meeting.create.flow.apply': null,
|
|
56
|
+
'meeting.eb.meeting.create.flow.quick': null,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* **不该**触发降级的错误:这些是登录、权限、参数、业务策略或确认链问题,
|
|
60
|
+
* 换成标准版接口也一定重蹈覆辙,降级只会掩盖真正的原因。
|
|
61
|
+
*/
|
|
62
|
+
const NO_FALLBACK_TYPES = new Set(['validation', 'confirmation', 'continuation']);
|
|
63
|
+
const NO_FALLBACK_SUBTYPES = new Set([
|
|
64
|
+
'session_expired',
|
|
65
|
+
'permission_denied',
|
|
66
|
+
'not_found',
|
|
67
|
+
'missing_required_field',
|
|
68
|
+
'datetime_invalid',
|
|
69
|
+
'id_invalid',
|
|
70
|
+
'conflict_rejected',
|
|
71
|
+
'flow_not_launchable',
|
|
72
|
+
'target_changed',
|
|
73
|
+
'env_unavailable',
|
|
74
|
+
'env_fallback',
|
|
75
|
+
]);
|
|
76
|
+
function describe(error) {
|
|
77
|
+
if (error instanceof MeetingCliError) {
|
|
78
|
+
return `${error.type}/${error.subtype}: ${error.message}`;
|
|
79
|
+
}
|
|
80
|
+
if (error instanceof Error) {
|
|
81
|
+
return `${error.type ?? 'api'}/${error.subtype ?? 'error'}: ${error.message}`;
|
|
82
|
+
}
|
|
83
|
+
return String(error);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 是不是「EB 接口这座城根本没有」的结构性信号(404/405/501 或服务端明说接口不存在)。
|
|
87
|
+
* 只有这类错误才**持久**降级并写缓存;5xx / 网络抖动这类偶发故障只降级本次,不动缓存,
|
|
88
|
+
* 否则一次抖动就会把整个环境永久标成标准版(2026-10-29 定)。
|
|
89
|
+
*/
|
|
90
|
+
function isEbStructurallyAbsent(error) {
|
|
91
|
+
const status = error?.details?.status ?? error?.code;
|
|
92
|
+
if (status === 404 || status === 405 || status === 501)
|
|
93
|
+
return true;
|
|
94
|
+
if (error?.subtype === 'http_error' && typeof status === 'number' && status >= 400 && status < 500)
|
|
95
|
+
return true;
|
|
96
|
+
const message = String(error?.message ?? '');
|
|
97
|
+
return /接口不存在|不存在该接口|NoHandler|No mapping|Not Found|路由不存在|does not exist/i.test(message);
|
|
98
|
+
}
|
|
99
|
+
function peerHint(operation) {
|
|
100
|
+
if (!(operation in EB_TO_STANDARD))
|
|
101
|
+
return {};
|
|
102
|
+
const peer = EB_TO_STANDARD[operation];
|
|
103
|
+
return peer == null ? { unavailable: true } : { peer };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* EB operation 失败后的降级处理。
|
|
107
|
+
*
|
|
108
|
+
* @returns 降级成功时返回一个携带切换指引的 `env_fallback` 错误;**标准版也不通或本就不该降级时
|
|
109
|
+
* 返回 `null`**(调用方应保持抛出原始错误,原始错误的诊断价值更高)。
|
|
110
|
+
*/
|
|
111
|
+
export async function tryDegradeEbFailure(operation, error, host, ctx) {
|
|
112
|
+
if (!operation.startsWith('meeting.eb.'))
|
|
113
|
+
return null;
|
|
114
|
+
// 手动开关:确认某环境就是 EB 版时,关掉自动降级避免反复实探。
|
|
115
|
+
if (process.env.WEAVER_MEETING_ENV_AUTOFALLBACK === '0')
|
|
116
|
+
return null;
|
|
117
|
+
// 固化自用(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;
|
|
121
|
+
const type = String(error?.type ?? '');
|
|
122
|
+
const subtype = String(error?.subtype ?? '');
|
|
123
|
+
if (NO_FALLBACK_TYPES.has(type) || NO_FALLBACK_SUBTYPES.has(subtype))
|
|
124
|
+
return null;
|
|
125
|
+
let ebErrorDetail = describe(error);
|
|
126
|
+
const persist = isEbStructurallyAbsent(error);
|
|
127
|
+
try {
|
|
128
|
+
const degraded = await degradeMeetingEnvToStandard(host, ctx, error, { persist });
|
|
129
|
+
ebErrorDetail = degraded.ebError || ebErrorDetail;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// 标准版也不通 → 保留原始错误(比 env_unavailable 更有信息量)。
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const hint = peerHint(operation);
|
|
136
|
+
const action = hint.peer
|
|
137
|
+
? `请改用标准版 operation **\`${hint.peer}\`** 重试(参数口径不同,先跑 \`meeting schema\` 看该 operation 的 inputSchema)。`
|
|
138
|
+
: hint.unavailable
|
|
139
|
+
? '标准版**没有**该能力的对等实现,请如实告知用户本环境无法完成这项操作,不要用其它接口拼凑。'
|
|
140
|
+
: '标准版清单见 `meeting schema`,挑语义最接近的 operation(先确认 inputSchema 再调用)。';
|
|
141
|
+
const modeNote = persist
|
|
142
|
+
? '已实探标准版接口可用,**环境模式已更新为 `standard` 并写入缓存**,后续同一环境直接走标准版。'
|
|
143
|
+
: '标准版接口本次探测可用,但这是 EB 侧的偶发故障(HTTP 5xx / 网络类),**没有改动模式缓存**,下次仍会先按 EB 版尝试。';
|
|
144
|
+
const message = [
|
|
145
|
+
`EB 版会议接口在当前环境不可用(${ebErrorDetail})。`,
|
|
146
|
+
modeNote,
|
|
147
|
+
action,
|
|
148
|
+
'若确认本环境其实是 EB 版(例如刚安装完会议 EB 应用),跑 `meeting.env.detect`(`refresh:true`)重新判定即可。',
|
|
149
|
+
].join(' ');
|
|
150
|
+
return new MeetingCliError('api', 'env_fallback', message, {
|
|
151
|
+
code: 'env_fallback',
|
|
152
|
+
details: {
|
|
153
|
+
ebOperation: operation,
|
|
154
|
+
ebError: ebErrorDetail,
|
|
155
|
+
standardOperation: hint.peer ?? null,
|
|
156
|
+
standardUnavailable: hint.unavailable === true,
|
|
157
|
+
// persist=true 时降级已落盘,调用方不必重复 detect。
|
|
158
|
+
envModeUpdated: persist ? 'standard' : null,
|
|
159
|
+
},
|
|
160
|
+
retryable: false,
|
|
161
|
+
exitCode: 1,
|
|
162
|
+
});
|
|
163
|
+
}
|